From f678ef68d8be308ee7ee9f2be6021fd5059e17ea Mon Sep 17 00:00:00 2001 From: public govdpub Date: Mon, 14 Nov 2022 15:15:22 +0800 Subject: [PATCH 1/3] Add README.md --- README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 -- Gitee From 626040c4a4062315a0fb7adfdaa2d0093f9787a7 Mon Sep 17 00:00:00 2001 From: OpenTiny Date: Thu, 6 Apr 2023 10:18:34 +0800 Subject: [PATCH 2/3] init project Match-id-ba586089c82018b96d46581772a0491a4b03d842 --- .codecheck/check.yml | 9 + .compodocrc.json | 17 + .cz.json | 3 + .editorconfig | 13 + .eslintrc.js | 57 + .githooks/commit-msg.sample | 12 + .githooks/pre-commit | 6 + .github/ISSUE_TEMPLATE/bug-report.yml | 61 + .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature-request.yml | 23 + .github/PULL_REQUEST_TEMPLATE.md | 41 + .gitignore | 52 + .lintstagedrc.json | 4 + .prettierignore | 12 + .prettierrc.js | 11 + .vscode/extensions.json | 40 + .vscode/settings.json | 44 + CHANGELOG.md | 11 + CONTRIBUTING.md | 98 + LICENSE | 21 + README-zh_CN.md | 103 + README.md | 104 + angular.json | 198 + build.md | 107 + build/AppWcModule.ts.ejs | 44 + build/add-default-theme.js | 106 + build/autoCreateDemoProjectJson.js | 21 + build/autoCreateLibProjectJson.js | 21 + build/autoCreateWebsite.js | 172 + build/bindTest.js | 53 + build/build-api.js | 933 +++ build/buildThemes.js | 13 + build/buildwc.js | 17 + build/clear-default-theme.js | 12 + build/component.cn.ejs | 27 + build/component.ejs | 122 + build/component.en.ejs | 29 + build/demoProject.json.ejs | 99 + build/libProject.json.ejs | 62 + build/package.js | 63 + build/peerDependencies.js | 51 + build/preview-demo.js | 130 + build/publish.js | 65 + build/reset-preview.js | 45 + build/unitTestingResources/config.js | 65 + .../karma.conf.js.template | 44 + .../project_test.json.template | 17 + build/unitTestingResources/test.ts.template | 26 + .../tsconfig.spec.json.template | 20 + c.bat | 1 + commit.template | 10 + commitlint.config.js | 3 + decorate-angular-cli.js | 79 + .../eslint-config-cbc-1-7-8-angular.js | 24 + eslint-rules/eslint-config-cbc-1-7-8-base.js | 912 +++ .../eslint-config-cbc-1-7-8-typescript.js | 381 ++ logo.png | Bin 0 -> 44683 bytes nx.json | 30 + package.json | 82 + src/accordion/demo/karma.conf.js | 44 + src/accordion/demo/project.json | 90 + src/accordion/demo/src/app/AppComponent.ts | 7 + src/accordion/demo/src/app/AppModule.ts | 26 + src/accordion/demo/src/app/IndexComponent.ts | 49 + .../app/accordion/AccordionBasicComponent.ts | 6 + .../app/accordion/AccordionClassComponent.ts | 11 + .../AccordionClickToggleComponent.ts | 14 + .../AccordionCloseOthersComponent.ts | 6 + .../accordion/AccordionDisabledComponent.ts | 8 + .../app/accordion/AccordionOpenComponent.ts | 8 + .../src/app/accordion/AccordionTestModule.ts | 71 + .../src/app/accordion/accordion-basic.html | 12 + .../src/app/accordion/accordion-class.html | 12 + .../app/accordion/accordion-click-toggle.html | 14 + .../app/accordion/accordion-close-others.html | 12 + .../src/app/accordion/accordion-disabled.html | 12 + .../src/app/accordion/accordion-open.html | 12 + .../demo/src/app/accordion/accordion.spec.ts | 85 + .../app/accordion/webdoc/accordion-demos.js | 92 + .../src/app/accordion/webdoc/accordion.cn.md | 26 + .../src/app/accordion/webdoc/accordion.en.md | 28 + src/accordion/demo/src/app/app.html | 3 + src/accordion/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/accordion/demo/src/index.html | 16 + src/accordion/demo/src/main.ts | 13 + src/accordion/demo/test.ts | 24 + src/accordion/demo/tsconfig.app.json | 15 + src/accordion/demo/tsconfig.spec.json | 20 + src/accordion/lib/index.ts | 1 + src/accordion/lib/ng-package.json | 7 + src/accordion/lib/package.json | 13 + src/accordion/lib/project.json | 62 + src/accordion/lib/src/TiAccordionComponent.ts | 84 + .../lib/src/TiAccordionHeadComponent.ts | 22 + .../lib/src/TiAccordionItemComponent.ts | 160 + src/accordion/lib/src/TiAccordionModule.ts | 30 + src/accordion/lib/src/accordion-item.html | 26 + src/accordion/lib/src/accordion.html | 1 + src/accordion/lib/src/accordion.less | 93 + src/actionmenu/demo/karma.conf.js | 44 + src/actionmenu/demo/project.json | 90 + src/actionmenu/demo/src/app/AppComponent.ts | 7 + src/actionmenu/demo/src/app/AppModule.ts | 24 + src/actionmenu/demo/src/app/IndexComponent.ts | 49 + .../actionmenu/ActionmenuBasicComponent.ts | 40 + .../actionmenu/ActionmenuData2Component.ts | 106 + .../app/actionmenu/ActionmenuDataComponent.ts | 94 + .../actionmenu/ActionmenuDisabledComponent.ts | 43 + .../actionmenu/ActionmenuDividerComponent.ts | 99 + .../actionmenu/ActionmenuEventComponent.ts | 49 + .../actionmenu/ActionmenuFocusComponent.ts | 61 + .../app/actionmenu/ActionmenuIdComponent.ts | 75 + .../ActionmenuItemsChangeComponent.ts | 84 + .../actionmenu/ActionmenuItemsComponent.ts | 82 + .../actionmenu/ActionmenuLabelkeyComponent.ts | 40 + .../app/actionmenu/ActionmenuManyComponent.ts | 105 + .../actionmenu/ActionmenuMenutextComponent.ts | 40 + .../ActionmenuPanelstyleComponent.ts | 48 + .../actionmenu/ActionmenuShownumComponent.ts | 44 + .../actionmenu/ActionmenuSpaceComponent.ts | 40 + .../actionmenu/ActionmenuTableComponent.ts | 111 + .../actionmenu/ActionmenuTempleteComponent.ts | 36 + .../ActionmenuTempleteTestComponent.ts | 65 + .../app/actionmenu/ActionmenuTestModule.ts | 148 + .../app/actionmenu/ActionmenuTipsComponent.ts | 45 + .../actionmenu/ActionmenuTipsTestComponent.ts | 98 + .../src/app/actionmenu/actionmenu-basic.html | 1 + .../src/app/actionmenu/actionmenu-data.html | 11 + .../src/app/actionmenu/actionmenu-data2.html | 12 + .../app/actionmenu/actionmenu-disabled.html | 1 + .../app/actionmenu/actionmenu-divider.html | 50 + .../src/app/actionmenu/actionmenu-event.html | 9 + .../src/app/actionmenu/actionmenu-focus.html | 25 + .../src/app/actionmenu/actionmenu-id.html | 13 + .../actionmenu/actionmenu-items-change.html | 14 + .../src/app/actionmenu/actionmenu-items.html | 52 + .../app/actionmenu/actionmenu-labelkey.html | 1 + .../src/app/actionmenu/actionmenu-many.html | 25 + .../app/actionmenu/actionmenu-menutext.html | 2 + .../app/actionmenu/actionmenu-panelstyle.html | 1 + .../app/actionmenu/actionmenu-shownum.html | 3 + .../src/app/actionmenu/actionmenu-space.html | 1 + .../src/app/actionmenu/actionmenu-table.html | 23 + .../actionmenu/actionmenu-templete-test.html | 13 + .../app/actionmenu/actionmenu-templete.html | 10 + .../app/actionmenu/actionmenu-tips-test.html | 59 + .../src/app/actionmenu/actionmenu-tips.html | 1 + .../app/actionmenu/webdoc/actionmenu-demos.js | 179 + .../app/actionmenu/webdoc/actionmenu.cn.md | 23 + .../app/actionmenu/webdoc/actionmenu.en.md | 29 + src/actionmenu/demo/src/app/app.html | 3 + src/actionmenu/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/actionmenu/demo/src/index.html | 16 + src/actionmenu/demo/src/main.ts | 13 + src/actionmenu/demo/test.ts | 24 + src/actionmenu/demo/tsconfig.app.json | 15 + src/actionmenu/demo/tsconfig.spec.json | 20 + src/actionmenu/lib/index.ts | 1 + src/actionmenu/lib/ng-package.json | 7 + src/actionmenu/lib/package.json | 14 + src/actionmenu/lib/project.json | 62 + .../lib/src/TiActionmenuComponent.ts | 520 ++ src/actionmenu/lib/src/TiActionmenuModule.ts | 31 + src/actionmenu/lib/src/actionmenu.html | 50 + src/actionmenu/lib/src/actionmenu.less | 49 + .../lib/src/i18n/TiActionmenuWords.ts | 6 + src/actionmenu/lib/src/i18n/en_US.ts | 8 + src/actionmenu/lib/src/i18n/es_US.ts | 8 + src/actionmenu/lib/src/i18n/fr_FR.ts | 8 + src/actionmenu/lib/src/i18n/index.ts | 7 + src/actionmenu/lib/src/i18n/pt_BR.ts | 8 + src/actionmenu/lib/src/i18n/zh_CN.ts | 8 + src/alert/demo/karma.conf.js | 44 + src/alert/demo/project.json | 90 + src/alert/demo/src/app/AppComponent.ts | 7 + src/alert/demo/src/app/AppModule.ts | 26 + src/alert/demo/src/app/IndexComponent.ts | 49 + .../src/app/alert/AlertBoxshadowComponent.ts | 8 + .../src/app/alert/AlertDarkthemeComponent.ts | 6 + .../src/app/alert/AlertDismissComponent.ts | 6 + .../demo/src/app/alert/AlertEventComponent.ts | 17 + .../demo/src/app/alert/AlertIconComponent.ts | 6 + .../src/app/alert/AlertMessagesComponent.ts | 43 + .../demo/src/app/alert/AlertOpenComponent.ts | 17 + .../src/app/alert/AlertOpenTestComponent.ts | 20 + .../demo/src/app/alert/AlertSizeComponent.ts | 6 + .../demo/src/app/alert/AlertTestModule.ts | 81 + .../app/alert/AlertTriggerScrollComponent.ts | 6 + .../demo/src/app/alert/AlertTypeComponent.ts | 6 + .../demo/src/app/alert/alert-boxshadow.html | 11 + .../demo/src/app/alert/alert-darktheme.html | 7 + .../demo/src/app/alert/alert-dismiss.html | 1 + src/alert/demo/src/app/alert/alert-event.html | 4 + src/alert/demo/src/app/alert/alert-icon.html | 3 + .../demo/src/app/alert/alert-messages.html | 13 + .../demo/src/app/alert/alert-open-test.html | 12 + src/alert/demo/src/app/alert/alert-open.html | 4 + src/alert/demo/src/app/alert/alert-size.html | 3 + .../src/app/alert/alert-trigger-scroll.html | 3 + src/alert/demo/src/app/alert/alert-type.html | 9 + .../demo/src/app/alert/webdoc/alert-demos.js | 117 + .../demo/src/app/alert/webdoc/alert.cn.md | 27 + .../demo/src/app/alert/webdoc/alert.en.md | 29 + src/alert/demo/src/app/app.html | 3 + src/alert/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/alert/demo/src/index.html | 16 + src/alert/demo/src/main.ts | 13 + src/alert/demo/test.ts | 24 + src/alert/demo/tsconfig.app.json | 15 + src/alert/demo/tsconfig.spec.json | 20 + src/alert/lib/index.ts | 1 + src/alert/lib/ng-package.json | 7 + src/alert/lib/package.json | 14 + src/alert/lib/project.json | 62 + src/alert/lib/src/TiAlertComponent.ts | 348 ++ src/alert/lib/src/TiAlertMessageComponent.ts | 20 + src/alert/lib/src/TiAlertModule.ts | 26 + src/alert/lib/src/alert.html | 107 + src/alert/lib/src/alert.less | 228 + src/anchor/demo/karma.conf.js | 44 + src/anchor/demo/project.json | 90 + src/anchor/demo/src/app/AppComponent.ts | 7 + src/anchor/demo/src/app/AppModule.ts | 24 + src/anchor/demo/src/app/IndexComponent.ts | 49 + .../src/app/anchor/AnchorBasicComponent.ts | 38 + .../src/app/anchor/AnchorEventsComponent.ts | 43 + .../demo/src/app/anchor/AnchorIdComponent.ts | 44 + .../src/app/anchor/AnchorItemsComponent.ts | 33 + .../app/anchor/AnchorOffsettopComponent.ts | 39 + .../app/anchor/AnchorScrolltargetComponent.ts | 38 + .../src/app/anchor/AnchorSpeedComponent.ts | 39 + .../src/app/anchor/AnchorTemplateComponent.ts | 38 + .../src/app/anchor/AnchorTestComponent.ts | 58 + .../demo/src/app/anchor/AnchorTestModule.ts | 66 + .../demo/src/app/anchor/anchor-basic.html | 9 + .../demo/src/app/anchor/anchor-events.html | 11 + src/anchor/demo/src/app/anchor/anchor-id.html | 17 + .../demo/src/app/anchor/anchor-items.html | 22 + .../demo/src/app/anchor/anchor-offsettop.html | 9 + .../src/app/anchor/anchor-scrolltarget.html | 9 + .../demo/src/app/anchor/anchor-speed.html | 9 + .../demo/src/app/anchor/anchor-template.html | 11 + .../demo/src/app/anchor/anchor-test.html | 47 + .../demo/src/app/anchor/anchortest.less | 18 + .../src/app/anchor/webdoc/anchor-demos.js | 96 + .../demo/src/app/anchor/webdoc/anchor.cn.md | 23 + .../demo/src/app/anchor/webdoc/anchor.en.md | 29 + src/anchor/demo/src/app/app.html | 3 + src/anchor/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/anchor/demo/src/index.html | 16 + src/anchor/demo/src/main.ts | 13 + src/anchor/demo/test.ts | 24 + src/anchor/demo/tsconfig.app.json | 15 + src/anchor/demo/tsconfig.spec.json | 20 + src/anchor/lib/index.ts | 1 + src/anchor/lib/ng-package.json | 7 + src/anchor/lib/package.json | 10 + src/anchor/lib/project.json | 62 + src/anchor/lib/src/TiAnchorComponent.ts | 267 + src/anchor/lib/src/TiAnchorModule.ts | 22 + src/anchor/lib/src/anchor.html | 18 + src/anchor/lib/src/anchor.less | 83 + src/autocomplete/demo/karma.conf.js | 44 + src/autocomplete/demo/project.json | 90 + src/autocomplete/demo/src/app/AppComponent.ts | 7 + src/autocomplete/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/autocomplete/demo/src/app/app.html | 3 + .../AutocompleteAppendtobodyComponent.ts | 22 + .../AutocompleteBasicComponent.ts | 53 + .../AutocompleteClearableComponent.ts | 23 + .../AutocompleteDisabledComponent.ts | 23 + .../AutocompleteEventsComponent.ts | 44 + .../AutocompleteGroupComponent.ts | 37 + .../AutocompleteLabelkeyComponent.ts | 27 + .../AutocompleteMaxlengthComponent.ts | 23 + .../AutocompletePanelSizeComponent.ts | 22 + .../AutocompleteTemplateComponent.ts | 22 + .../autocomplete/AutocompleteTestComponent.ts | 232 + .../autocomplete/AutocompleteTestModule.ts | 111 + .../autocomplete/AutocompleteTipComponent.ts | 24 + .../AutocompleteValidComponent.ts | 22 + .../autocomplete-appendtobody.html | 8 + .../app/autocomplete/autocomplete-basic.html | 26 + .../autocomplete/autocomplete-clearable.html | 1 + .../autocomplete/autocomplete-disabled.html | 8 + .../app/autocomplete/autocomplete-events.html | 16 + .../app/autocomplete/autocomplete-group.html | 1 + .../autocomplete/autocomplete-labelkey.html | 8 + .../autocomplete/autocomplete-maxlength.html | 8 + .../autocomplete/autocomplete-panel-size.html | 9 + .../autocomplete/autocomplete-template.html | 5 + .../app/autocomplete/autocomplete-test.html | 30 + .../app/autocomplete/autocomplete-tip.html | 9 + .../app/autocomplete/autocomplete-valid.html | 9 + .../autocomplete/webdoc/autocomplete-demos.js | 164 + .../autocomplete/webdoc/autocomplete.cn.md | 36 + .../autocomplete/webdoc/autocomplete.en.md | 29 + src/autocomplete/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/autocomplete/demo/src/index.html | 16 + src/autocomplete/demo/src/main.ts | 13 + src/autocomplete/demo/test.ts | 24 + src/autocomplete/demo/tsconfig.app.json | 15 + src/autocomplete/demo/tsconfig.spec.json | 20 + src/autocomplete/lib/index.ts | 1 + src/autocomplete/lib/ng-package.json | 7 + src/autocomplete/lib/package.json | 14 + src/autocomplete/lib/project.json | 62 + .../lib/src/TiAutocompleteComponent.ts | 374 ++ .../lib/src/TiAutocompleteModule.ts | 26 + src/autocomplete/lib/src/autocomplete.html | 37 + src/autocomplete/lib/src/autocomplete.less | 22 + src/avatar/demo/karma.conf.js | 44 + src/avatar/demo/project.json | 98 + src/avatar/demo/src/app/AppComponent.ts | 7 + src/avatar/demo/src/app/AppModule.ts | 24 + src/avatar/demo/src/app/IndexComponent.ts | 49 + src/avatar/demo/src/app/app.html | 3 + .../src/app/avatar/AvatarImageComponent.ts | 9 + .../avatar/AvatarImageErrorTestComponent.ts | 9 + .../src/app/avatar/AvatarShapeComponent.ts | 6 + .../src/app/avatar/AvatarSizeComponent.ts | 6 + .../src/app/avatar/AvatarStyleComponent.ts | 9 + .../demo/src/app/avatar/AvatarTestModule.ts | 60 + .../src/app/avatar/AvatarTextComponent.ts | 22 + .../app/avatar/avatar-image-error-test.html | 12 + .../demo/src/app/avatar/avatar-image.html | 12 + .../demo/src/app/avatar/avatar-shape.html | 5 + .../demo/src/app/avatar/avatar-size.html | 14 + .../demo/src/app/avatar/avatar-style.html | 16 + .../demo/src/app/avatar/avatar-text.html | 6 + .../src/app/avatar/webdoc/avatar-demos.js | 76 + .../demo/src/app/avatar/webdoc/avatar.cn.md | 23 + .../demo/src/app/avatar/webdoc/avatar.en.md | 28 + src/avatar/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/avatar/demo/src/index.html | 16 + src/avatar/demo/src/main.ts | 13 + src/avatar/demo/test.ts | 24 + src/avatar/demo/tsconfig.app.json | 15 + src/avatar/demo/tsconfig.spec.json | 20 + src/avatar/lib/index.ts | 1 + src/avatar/lib/ng-package.json | 7 + src/avatar/lib/package.json | 11 + src/avatar/lib/project.json | 62 + src/avatar/lib/src/TiAvatarComponent.ts | 116 + src/avatar/lib/src/TiAvatarModule.ts | 23 + src/avatar/lib/src/avatar.html | 14 + src/avatar/lib/src/avatar.less | 62 + src/base/lib/index.ts | 1 + src/base/lib/ng-package.json | 7 + src/base/lib/package.json | 10 + src/base/lib/project.json | 62 + src/base/lib/src/TiAutofocusComponent.ts | 34 + src/base/lib/src/TiBaseComponent.ts | 171 + src/base/lib/src/TiBaseModule.ts | 32 + src/base/lib/src/TiFormComponent.ts | 529 ++ src/base/lib/src/TiRadioBaseComponent.ts | 127 + src/base/lib/src/TiWholeComponent.ts | 93 + src/browserslist | 8 + src/button/demo/karma.conf.js | 44 + src/button/demo/project.json | 90 + src/button/demo/src/app/AppComponent.ts | 7 + src/button/demo/src/app/AppModule.ts | 24 + src/button/demo/src/app/IndexComponent.ts | 49 + src/button/demo/src/app/app.html | 3 + .../src/app/button/ButtonColorComponent.ts | 6 + .../src/app/button/ButtonDisabledComponent.ts | 6 + .../src/app/button/ButtonEventComponent.ts | 11 + .../src/app/button/ButtonFocusComponent.ts | 6 + .../app/button/ButtonHasborderComponent.ts | 6 + .../button/ButtonHasborderTestComponent.ts | 8 + .../src/app/button/ButtonIconComponent.ts | 6 + .../src/app/button/ButtonLabelComponent.ts | 6 + .../src/app/button/ButtonLoadingComponent.ts | 6 + .../src/app/button/ButtonOnlyiconComponent.ts | 6 + .../src/app/button/ButtonSizeComponent.ts | 6 + .../demo/src/app/button/ButtonTestModule.ts | 90 + .../demo/src/app/button/ButtonTipComponent.ts | 8 + .../demo/src/app/button/button-color.html | 3 + .../demo/src/app/button/button-disabled.html | 1 + .../demo/src/app/button/button-event.html | 1 + .../demo/src/app/button/button-focus.html | 1 + .../src/app/button/button-hasborder-test.html | 31 + .../demo/src/app/button/button-hasborder.html | 1 + .../demo/src/app/button/button-icon.html | 4 + .../demo/src/app/button/button-label.html | 2 + .../demo/src/app/button/button-loading.html | 3 + .../demo/src/app/button/button-onlyicon.html | 3 + .../demo/src/app/button/button-size.html | 4 + .../demo/src/app/button/button-tip.html | 1 + .../src/app/button/webdoc/button-demos.js | 134 + .../demo/src/app/button/webdoc/button.cn.md | 25 + .../demo/src/app/button/webdoc/button.en.md | 7 + src/button/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/button/demo/src/index.html | 16 + src/button/demo/src/main.ts | 13 + src/button/demo/test.ts | 24 + src/button/demo/tsconfig.app.json | 15 + src/button/demo/tsconfig.spec.json | 20 + src/button/lib/index.ts | 1 + src/button/lib/ng-package.json | 7 + src/button/lib/package.json | 12 + src/button/lib/project.json | 62 + src/button/lib/src/TiButtonComponent.ts | 127 + src/button/lib/src/TiButtonModule.ts | 29 + src/button/lib/src/button-mixin.less | 76 + src/button/lib/src/button.html | 5 + src/button/lib/src/button.less | 138 + src/button/lib/src/i18n/TiButtonWords.ts | 6 + src/button/lib/src/i18n/en_US.ts | 8 + src/button/lib/src/i18n/es_US.ts | 8 + src/button/lib/src/i18n/fr_FR.ts | 8 + src/button/lib/src/i18n/index.ts | 7 + src/button/lib/src/i18n/pt_BR.ts | 8 + src/button/lib/src/i18n/zh_CN.ts | 8 + src/buttongroup/demo/karma.conf.js | 44 + src/buttongroup/demo/project.json | 90 + src/buttongroup/demo/src/app/AppComponent.ts | 7 + src/buttongroup/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/buttongroup/demo/src/app/app.html | 3 + .../app/buttongroup/ButtonGroupTestModule.ts | 171 + .../ButtongroupActiveclassComponent.ts | 31 + .../ButtongroupBeforeclickComponent.ts | 51 + .../ButtongroupDeselectableComponent.ts | 23 + .../ButtongroupDisabledComponent.ts | 29 + .../buttongroup/ButtongroupEnumComponent.ts | 35 + .../buttongroup/ButtongroupEventComponent.ts | 48 + .../buttongroup/ButtongroupFocusComponent.ts | 57 + .../app/buttongroup/ButtongroupIdComponent.ts | 42 + .../buttongroup/ButtongroupIdTestComponent.ts | 43 + .../buttongroup/ButtongroupItemsComponent.ts | 23 + .../ButtongroupItemsTestComponent.ts | 45 + .../buttongroup/ButtongroupManyComponent.ts | 80 + .../ButtongroupMinwidthComponent.ts | 38 + .../ButtongroupMultiTypeComponent.ts | 25 + .../ButtongroupMultilineComponent.ts | 47 + .../ButtongroupMultipleComponent.ts | 23 + .../ButtongroupRadioTypeComponent.ts | 27 + .../ButtongroupReactiveFormsComponent.ts | 32 + .../buttongroup/ButtongroupSupComponent.ts | 40 + .../ButtongroupSupTestComponent.ts | 38 + .../ButtongroupTemplateComponent.ts | 29 + .../buttongroup/ButtongroupTipComponent.ts | 51 + .../ButtongroupValuekeyComponent.ts | 29 + .../ButtongroupValuekeyTestComponent.ts | 80 + .../buttongroup/buttongroup-activeclass.html | 13 + .../buttongroup/buttongroup-beforeclick.html | 15 + .../buttongroup/buttongroup-deselectable.html | 1 + .../app/buttongroup/buttongroup-disabled.html | 4 + .../src/app/buttongroup/buttongroup-enum.html | 12 + .../app/buttongroup/buttongroup-event.html | 22 + .../app/buttongroup/buttongroup-focus.html | 23 + .../app/buttongroup/buttongroup-id-test.html | 12 + .../src/app/buttongroup/buttongroup-id.html | 11 + .../buttongroup/buttongroup-items-test.html | 18 + .../app/buttongroup/buttongroup-items.html | 4 + .../src/app/buttongroup/buttongroup-many.html | 16 + .../app/buttongroup/buttongroup-minwidth.html | 8 + .../buttongroup/buttongroup-multi-type.html | 2 + .../buttongroup/buttongroup-multiline.html | 1 + .../app/buttongroup/buttongroup-multiple.html | 4 + .../buttongroup/buttongroup-radio-type.html | 4 + .../buttongroup-reactive-forms.html | 7 + .../app/buttongroup/buttongroup-sup-test.html | 24 + .../src/app/buttongroup/buttongroup-sup.html | 28 + .../app/buttongroup/buttongroup-template.html | 14 + .../src/app/buttongroup/buttongroup-tip.html | 4 + .../buttongroup-valuekey-test.html | 23 + .../app/buttongroup/buttongroup-valuekey.html | 9 + .../demo/src/app/buttongroup/buttongroup.less | 8 + .../buttongroup/webdoc/buttongroup-demos.js | 180 + .../app/buttongroup/webdoc/buttongroup.cn.md | 33 + .../app/buttongroup/webdoc/buttongroup.en.md | 29 + src/buttongroup/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/buttongroup/demo/src/index.html | 16 + src/buttongroup/demo/src/main.ts | 13 + src/buttongroup/demo/test.ts | 24 + src/buttongroup/demo/tsconfig.app.json | 15 + src/buttongroup/demo/tsconfig.spec.json | 20 + src/buttongroup/lib/index.ts | 1 + src/buttongroup/lib/ng-package.json | 7 + src/buttongroup/lib/package.json | 14 + src/buttongroup/lib/project.json | 62 + .../lib/src/TiButtongroupComponent.ts | 310 + .../lib/src/TiButtongroupModule.ts | 27 + src/buttongroup/lib/src/buttongroup.html | 56 + src/buttongroup/lib/src/buttongroup.less | 193 + src/buttonselect/demo/project.json | 78 + src/buttonselect/demo/src/app/AppComponent.ts | 7 + src/buttonselect/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/buttonselect/demo/src/app/app.html | 3 + .../ButtonselectBasicComponent.ts | 15 + .../ButtonselectLabelkeyComponent.ts | 10 + .../buttonselect/ButtonselectTestModule.ts | 26 + .../app/buttonselect/buttonselect-basic.html | 1 + .../buttonselect/buttonselect-labelkey.html | 1 + .../buttonselect/webdoc/buttonselect-demos.js | 38 + .../buttonselect/webdoc/buttonselect.cn.md | 17 + .../buttonselect/webdoc/buttonselect.en.md | 13 + src/buttonselect/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/buttonselect/demo/src/index.html | 16 + src/buttonselect/demo/src/main.ts | 13 + src/buttonselect/demo/tsconfig.app.json | 10 + src/buttonselect/lib/index.ts | 2 + src/buttonselect/lib/ng-package.json | 7 + src/buttonselect/lib/package.json | 13 + src/buttonselect/lib/project.json | 62 + .../lib/src/TiButtonselectComponent.ts | 132 + .../lib/src/TiButtonselectModule.ts | 30 + src/buttonselect/lib/src/buttonselect.html | 28 + src/buttonselect/lib/src/buttonselect.less | 77 + src/card/demo/karma.conf.js | 44 + src/card/demo/project.json | 90 + src/card/demo/src/app/AppComponent.ts | 7 + src/card/demo/src/app/AppModule.ts | 24 + src/card/demo/src/app/IndexComponent.ts | 49 + src/card/demo/src/app/app.html | 3 + .../demo/src/app/card/CardAddComponent.ts | 13 + .../demo/src/app/card/CardBasicComponent.ts | 6 + .../demo/src/app/card/CardGrid2Component.ts | 27 + .../demo/src/app/card/CardGridComponent.ts | 27 + .../demo/src/app/card/CardHeaderComponent.ts | 6 + src/card/demo/src/app/card/CardTestModule.ts | 27 + src/card/demo/src/app/card/card-add.html | 6 + src/card/demo/src/app/card/card-basic.html | 11 + src/card/demo/src/app/card/card-grid.html | 5 + src/card/demo/src/app/card/card-grid2.html | 5 + src/card/demo/src/app/card/card-header.html | 22 + .../demo/src/app/card/webdoc/card-demos.js | 74 + src/card/demo/src/app/card/webdoc/card.cn.md | 25 + src/card/demo/src/app/card/webdoc/card.en.md | 25 + src/card/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/card/demo/src/index.html | 16 + src/card/demo/src/main.ts | 13 + src/card/demo/test.ts | 24 + src/card/demo/tsconfig.app.json | 15 + src/card/demo/tsconfig.spec.json | 20 + src/card/lib/index.ts | 1 + src/card/lib/ng-package.json | 7 + src/card/lib/package.json | 11 + src/card/lib/project.json | 62 + src/card/lib/src/TiCardAddComponent.ts | 57 + src/card/lib/src/TiCardComponent.ts | 42 + src/card/lib/src/TiCardHeaderComponent.ts | 20 + src/card/lib/src/TiCardModule.ts | 28 + src/card/lib/src/card-add.html | 1 + src/card/lib/src/card-add.less | 60 + src/card/lib/src/card-header.less | 27 + src/card/lib/src/card.less | 41 + src/cascader/demo/karma.conf.js | 44 + src/cascader/demo/project.json | 90 + src/cascader/demo/src/app/AppComponent.ts | 7 + src/cascader/demo/src/app/AppModule.ts | 24 + src/cascader/demo/src/app/IndexComponent.ts | 49 + src/cascader/demo/src/app/app.html | 3 + .../app/cascader/CascaderBasicComponent.ts | 37 + .../cascader/CascaderClearableComponent.ts | 37 + .../demo/src/app/cascader/CascaderData.ts | 1050 ++++ .../app/cascader/CascaderDisabledComponent.ts | 41 + .../app/cascader/CascaderEventsComponent.ts | 21 + .../app/cascader/CascaderIdkeyComponent.ts | 40 + .../app/cascader/CascaderItemTestComponent.ts | 49 + .../app/cascader/CascaderLabelkeyComponent.ts | 40 + .../CascaderOnlyselectleafComponent.ts | 12 + .../app/cascader/CascaderPanelComponent.ts | 10 + .../app/cascader/CascaderSearchComponent.ts | 13 + .../cascader/CascaderShowalllevelComponent.ts | 12 + .../src/app/cascader/CascaderTestModule.ts | 90 + .../app/cascader/CascaderTriggerComponent.ts | 12 + .../app/cascader/CascaderValidComponent.ts | 16 + .../app/cascader/CascaderValuekeyComponent.ts | 40 + .../demo/src/app/cascader/cascader-basic.html | 4 + .../src/app/cascader/cascader-clearable.html | 4 + .../src/app/cascader/cascader-disabled.html | 4 + .../src/app/cascader/cascader-events.html | 14 + .../demo/src/app/cascader/cascader-idkey.html | 4 + .../src/app/cascader/cascader-item-test.html | 16 + .../src/app/cascader/cascader-labelkey.html | 1 + .../app/cascader/cascader-onlyselectleaf.html | 1 + .../demo/src/app/cascader/cascader-panel.html | 1 + .../src/app/cascader/cascader-search.html | 4 + .../app/cascader/cascader-showalllevel.html | 1 + .../src/app/cascader/cascader-trigger.html | 1 + .../demo/src/app/cascader/cascader-valid.html | 11 + .../src/app/cascader/cascader-valuekey.html | 4 + .../src/app/cascader/webdoc/cascader-demos.js | 138 + .../src/app/cascader/webdoc/cascader.cn.md | 23 + .../src/app/cascader/webdoc/cascader.en.md | 29 + src/cascader/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/cascader/demo/src/index.html | 16 + src/cascader/demo/src/main.ts | 13 + src/cascader/demo/test.ts | 24 + src/cascader/demo/tsconfig.app.json | 15 + src/cascader/demo/tsconfig.spec.json | 20 + src/cascader/lib/index.ts | 1 + src/cascader/lib/ng-package.json | 7 + src/cascader/lib/package.json | 17 + src/cascader/lib/project.json | 62 + src/cascader/lib/src/TiCascaderComponent.ts | 611 ++ src/cascader/lib/src/TiCascaderModule.ts | 29 + src/cascader/lib/src/cascader.html | 67 + src/cascader/lib/src/cascader.less | 25 + src/checkbox/demo/karma.conf.js | 44 + src/checkbox/demo/project.json | 90 + src/checkbox/demo/src/app/AppComponent.ts | 7 + src/checkbox/demo/src/app/AppModule.ts | 24 + src/checkbox/demo/src/app/IndexComponent.ts | 49 + src/checkbox/demo/src/app/app.html | 3 + .../app/checkbox/CheckboxBasicComponent.ts | 8 + .../app/checkbox/CheckboxDisabledComponent.ts | 29 + .../app/checkbox/CheckboxEventComponent.ts | 36 + .../app/checkbox/CheckboxFocusedComponent.ts | 48 + .../app/checkbox/CheckboxGroupComponent.ts | 35 + .../CheckboxGroupDirectionComponent.ts | 26 + .../CheckboxGroupLabelkeyComponent.ts | 26 + .../checkbox/CheckboxGroupLevelComponent.ts | 35 + .../CheckboxGroupLinewrapComponent.ts | 21 + .../CheckboxGroupValidationComponent.ts | 72 + .../CheckboxGroupValuekeyComponent.ts | 34 + .../CheckboxIndeterminateComponent.ts | 15 + .../app/checkbox/CheckboxLabelComponent.ts | 14 + .../src/app/checkbox/CheckboxTestModule.ts | 145 + .../app/checkbox/CheckgroupChangeComponent.ts | 58 + .../app/checkbox/CheckgroupCrossComponent.ts | 23 + .../checkbox/CheckgroupDefaultComponent.ts | 35 + .../CheckgroupDisabledTestComponent.ts | 61 + .../checkbox/CheckgroupRefreshComponent.ts | 39 + .../app/checkbox/CheckgroupScenesComponent.ts | 79 + .../app/checkbox/CheckgroupTreeComponent.ts | 89 + .../demo/src/app/checkbox/checkbox-basic.html | 1 + .../src/app/checkbox/checkbox-disabled.html | 4 + .../demo/src/app/checkbox/checkbox-event.html | 12 + .../src/app/checkbox/checkbox-focused.html | 31 + .../checkbox/checkbox-group-direction.html | 4 + .../app/checkbox/checkbox-group-labelkey.html | 4 + .../app/checkbox/checkbox-group-level.html | 19 + .../app/checkbox/checkbox-group-linewrap.html | 1 + .../checkbox/checkbox-group-validation.html | 25 + .../app/checkbox/checkbox-group-valuekey.html | 13 + .../demo/src/app/checkbox/checkbox-group.html | 12 + .../app/checkbox/checkbox-indeterminate.html | 9 + .../demo/src/app/checkbox/checkbox-label.html | 25 + .../src/app/checkbox/checkgroup-change.html | 44 + .../src/app/checkbox/checkgroup-cross.html | 16 + .../src/app/checkbox/checkgroup-default.html | 64 + .../checkbox/checkgroup-disabled-test.html | 88 + .../src/app/checkbox/checkgroup-refresh.html | 30 + .../src/app/checkbox/checkgroup-scenes.html | 68 + .../src/app/checkbox/checkgroup-tree.html | 162 + .../src/app/checkbox/webdoc/checkbox-demos.js | 158 + .../src/app/checkbox/webdoc/checkbox.cn.md | 29 + .../src/app/checkbox/webdoc/checkbox.en.md | 29 + src/checkbox/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/checkbox/demo/src/index.html | 16 + src/checkbox/demo/src/main.ts | 13 + src/checkbox/demo/test.ts | 24 + src/checkbox/demo/tsconfig.app.json | 15 + src/checkbox/demo/tsconfig.spec.json | 20 + src/checkbox/lib/index.ts | 1 + src/checkbox/lib/ng-package.json | 7 + src/checkbox/lib/package.json | 13 + src/checkbox/lib/project.json | 62 + src/checkbox/lib/src/TiCheckboxComponent.ts | 97 + .../lib/src/TiCheckboxGroupComponent.ts | 96 + src/checkbox/lib/src/TiCheckboxModule.ts | 29 + src/checkbox/lib/src/TiCheckgroupComponent.ts | 422 ++ src/checkbox/lib/src/TiCheckitemComponent.ts | 228 + src/checkbox/lib/src/checkbox.html | 14 + src/checkbox/lib/src/checkbox.less | 226 + src/checkbox/lib/src/checkboxgroup.html | 23 + src/checkbox/lib/src/checkboxgroup.less | 33 + src/collapse/demo/karma.conf.js | 44 + src/collapse/demo/project.json | 90 + src/collapse/demo/src/app/AppComponent.ts | 7 + src/collapse/demo/src/app/AppModule.ts | 26 + src/collapse/demo/src/app/IndexComponent.ts | 49 + src/collapse/demo/src/app/app.html | 3 + .../app/collapse/CollapseBasicComponent.ts | 12 + .../app/collapse/CollapseEventComponent.ts | 18 + .../src/app/collapse/CollapseTestModule.ts | 27 + .../demo/src/app/collapse/collapse-basic.html | 8 + .../demo/src/app/collapse/collapse-event.html | 11 + .../demo/src/app/collapse/collapse.less | 6 + .../src/app/collapse/webdoc/collapse-demos.js | 30 + .../src/app/collapse/webdoc/collapse.cn.md | 34 + .../src/app/collapse/webdoc/collapse.en.md | 29 + src/collapse/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/collapse/demo/src/index.html | 16 + src/collapse/demo/src/main.ts | 13 + src/collapse/demo/test.ts | 24 + src/collapse/demo/tsconfig.app.json | 15 + src/collapse/demo/tsconfig.spec.json | 20 + src/collapse/lib/index.ts | 1 + src/collapse/lib/ng-package.json | 7 + src/collapse/lib/package.json | 11 + src/collapse/lib/project.json | 62 + src/collapse/lib/src/TiCollapseDirective.ts | 151 + src/collapse/lib/src/TiCollapseModule.ts | 22 + src/collapsebox/demo/karma.conf.js | 41 + src/collapsebox/demo/project.json | 90 + src/collapsebox/demo/src/app/AppComponent.ts | 7 + src/collapsebox/demo/src/app/AppModule.ts | 26 + .../demo/src/app/IndexComponent.ts | 49 + src/collapsebox/demo/src/app/app.html | 3 + .../collapsebox/CollapseboxBasicComponent.ts | 7 + .../CollapseboxCloseableComponent.ts | 7 + .../collapsebox/CollapseboxEventComponent.ts | 17 + .../app/collapsebox/CollapseboxTestModule.ts | 32 + .../app/collapsebox/collapsebox-basic.html | 5 + .../collapsebox/collapsebox-closeable.html | 6 + .../app/collapsebox/collapsebox-event.html | 8 + .../collapsebox/webdoc/collapsebox-demos.js | 40 + .../app/collapsebox/webdoc/collapsebox.cn.md | 24 + .../app/collapsebox/webdoc/collapsebox.en.md | 29 + src/collapsebox/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/collapsebox/demo/src/index.html | 16 + src/collapsebox/demo/src/main.ts | 13 + src/collapsebox/demo/test.ts | 24 + src/collapsebox/demo/tsconfig.app.json | 10 + src/collapsebox/demo/tsconfig.spec.json | 15 + src/collapsebox/lib/index.ts | 1 + src/collapsebox/lib/ng-package.json | 7 + src/collapsebox/lib/package.json | 12 + src/collapsebox/lib/project.json | 62 + .../lib/src/TiCollapseboxComponent.ts | 51 + .../lib/src/TiCollapseboxModule.ts | 27 + src/collapsebox/lib/src/collapsebox.html | 13 + src/collapsebox/lib/src/collapsebox.less | 23 + src/collapsebutton/demo/karma.conf.js | 41 + src/collapsebutton/demo/project.json | 90 + .../demo/src/app/AppComponent.ts | 7 + src/collapsebutton/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/collapsebutton/demo/src/app/app.html | 3 + .../CollapsebuttonBasicComponent.ts | 8 + .../CollapsebuttonCustomtextComponent.ts | 9 + .../CollapsebuttonEventComponent.ts | 14 + .../CollapsebuttonSearchcountComponent.ts | 38 + .../CollapsebuttonTestModule.ts | 56 + .../collapsebutton/collapsebutton-basic.html | 2 + .../collapsebutton-customtext.html | 7 + .../collapsebutton/collapsebutton-event.html | 2 + .../collapsebutton-searchcount.html | 19 + .../webdoc/collapsebutton-demos.js | 52 + .../webdoc/collapsebutton.cn.md | 22 + .../webdoc/collapsebutton.en.md | 18 + src/collapsebutton/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/collapsebutton/demo/src/index.html | 16 + src/collapsebutton/demo/src/main.ts | 13 + src/collapsebutton/demo/test.ts | 24 + src/collapsebutton/demo/tsconfig.app.json | 10 + src/collapsebutton/demo/tsconfig.spec.json | 15 + src/collapsebutton/lib/index.ts | 1 + src/collapsebutton/lib/ng-package.json | 7 + src/collapsebutton/lib/package.json | 13 + src/collapsebutton/lib/project.json | 62 + .../lib/src/TiCollapsebuttonComponent.ts | 56 + .../lib/src/TiCollapsebuttonModule.ts | 36 + .../lib/src/TiCollapsepanelComponent.ts | 23 + .../lib/src/collapsebutton.html | 9 + .../lib/src/collapsebutton.less | 81 + src/collapsebutton/lib/src/collapsepanel.less | 14 + .../lib/src/i18n/TiCollapsebuttonWords.ts | 5 + src/collapsebutton/lib/src/i18n/en_US.ts | 7 + src/collapsebutton/lib/src/i18n/es_US.ts | 7 + src/collapsebutton/lib/src/i18n/fr_FR.ts | 7 + src/collapsebutton/lib/src/i18n/index.ts | 7 + src/collapsebutton/lib/src/i18n/pt_BR.ts | 7 + src/collapsebutton/lib/src/i18n/zh_CN.ts | 7 + src/collapsetext/demo/project.json | 78 + src/collapsetext/demo/src/app/AppComponent.ts | 7 + src/collapsetext/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/collapsetext/demo/src/app/app.html | 3 + .../CollapsetextBasicComponent.ts | 8 + .../CollapsetextCollapsedComponent.ts | 13 + .../CollapsetextHighlightComponent.ts | 9 + .../CollapsetextSceneComponent.ts | 18 + .../collapsetext/CollapsetextTestModule.ts | 31 + .../collapsetext/CollapsetextTypeComponent.ts | 11 + .../app/collapsetext/collapsetext-basic.html | 1 + .../collapsetext/collapsetext-collapsed.html | 3 + .../collapsetext/collapsetext-highlight.html | 1 + .../app/collapsetext/collapsetext-scene.html | 36 + .../app/collapsetext/collapsetext-type.html | 4 + .../collapsetext/webdoc/collapsetext-demos.js | 65 + .../collapsetext/webdoc/collapsetext.cn.md | 15 + .../collapsetext/webdoc/collapsetext.en.md | 13 + src/collapsetext/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/collapsetext/demo/src/index.html | 16 + src/collapsetext/demo/src/main.ts | 13 + src/collapsetext/demo/tsconfig.app.json | 10 + src/collapsetext/lib/index.ts | 2 + src/collapsetext/lib/ng-package.json | 7 + src/collapsetext/lib/package.json | 8 + src/collapsetext/lib/project.json | 62 + .../lib/src/TiCollapsetextComponent.ts | 54 + .../lib/src/TiCollapsetextModule.ts | 24 + src/collapsetext/lib/src/collapsetext.html | 10 + src/collapsetext/lib/src/collapsetext.less | 122 + src/copy/demo/karma.conf.js | 41 + src/copy/demo/project.json | 90 + src/copy/demo/src/app/AppComponent.ts | 7 + src/copy/demo/src/app/AppModule.ts | 24 + src/copy/demo/src/app/IndexComponent.ts | 49 + src/copy/demo/src/app/app.html | 3 + .../demo/src/app/copy/CopyBasicComponent.ts | 8 + .../demo/src/app/copy/CopyDarkComponent.ts | 6 + .../demo/src/app/copy/CopyEventComponent.ts | 11 + .../demo/src/app/copy/CopyTableComponent.ts | 54 + src/copy/demo/src/app/copy/CopyTestModule.ts | 33 + .../demo/src/app/copy/CopyTipComponent.ts | 8 + src/copy/demo/src/app/copy/copy-basic.html | 5 + src/copy/demo/src/app/copy/copy-dark.html | 3 + src/copy/demo/src/app/copy/copy-event.html | 5 + src/copy/demo/src/app/copy/copy-table.html | 22 + src/copy/demo/src/app/copy/copy-tip.html | 7 + .../demo/src/app/copy/webdoc/copy-demos.js | 63 + src/copy/demo/src/app/copy/webdoc/copy.cn.md | 25 + src/copy/demo/src/app/copy/webdoc/copy.en.md | 21 + src/copy/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/copy/demo/src/index.html | 16 + src/copy/demo/src/main.ts | 13 + src/copy/demo/test.ts | 24 + src/copy/demo/tsconfig.app.json | 10 + src/copy/demo/tsconfig.spec.json | 15 + src/copy/lib/index.ts | 1 + src/copy/lib/ng-package.json | 7 + src/copy/lib/package.json | 15 + src/copy/lib/project.json | 62 + src/copy/lib/src/TiCopyComponent.ts | 148 + src/copy/lib/src/TiCopyModule.ts | 36 + src/copy/lib/src/TiToastComponent.ts | 24 + src/copy/lib/src/copy.html | 2 + src/copy/lib/src/copy.less | 22 + src/copy/lib/src/i18n/TiCopyWords.ts | 5 + src/copy/lib/src/i18n/en_US.ts | 7 + src/copy/lib/src/i18n/es_US.ts | 7 + src/copy/lib/src/i18n/fr_FR.ts | 7 + src/copy/lib/src/i18n/index.ts | 7 + src/copy/lib/src/i18n/pt_BR.ts | 7 + src/copy/lib/src/i18n/zh_CN.ts | 7 + src/copy/lib/src/toast.less | 27 + src/crumb/demo/karma.conf.js | 44 + src/crumb/demo/project.json | 90 + src/crumb/demo/src/app/AppComponent.ts | 7 + src/crumb/demo/src/app/AppModule.ts | 24 + src/crumb/demo/src/app/IndexComponent.ts | 49 + src/crumb/demo/src/app/app.html | 3 + .../demo/src/app/crumb/CrumbBasicComponent.ts | 25 + .../src/app/crumb/CrumbEventsComponent.ts | 30 + .../demo/src/app/crumb/CrumbHrefComponent.ts | 32 + .../src/app/crumb/CrumbRouterComponent.ts | 25 + .../src/app/crumb/CrumbRouterTestComponent.ts | 41 + .../demo/src/app/crumb/CrumbTestModule.ts | 59 + .../demo/src/app/crumb/Router1Component.ts | 6 + .../demo/src/app/crumb/Router2Component.ts | 6 + .../demo/src/app/crumb/Router3Component.ts | 6 + src/crumb/demo/src/app/crumb/crumb-basic.html | 1 + .../demo/src/app/crumb/crumb-events.html | 3 + src/crumb/demo/src/app/crumb/crumb-href.html | 1 + .../demo/src/app/crumb/crumb-router-test.html | 19 + .../demo/src/app/crumb/crumb-router.html | 1 + .../demo/src/app/crumb/webdoc/crumb-demos.js | 63 + .../demo/src/app/crumb/webdoc/crumb.cn.md | 24 + .../demo/src/app/crumb/webdoc/crumb.en.md | 29 + src/crumb/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/crumb/demo/src/index.html | 16 + src/crumb/demo/src/main.ts | 13 + src/crumb/demo/test.ts | 24 + src/crumb/demo/tsconfig.app.json | 15 + src/crumb/demo/tsconfig.spec.json | 20 + src/crumb/lib/index.ts | 1 + src/crumb/lib/ng-package.json | 7 + src/crumb/lib/package.json | 11 + src/crumb/lib/project.json | 62 + src/crumb/lib/src/TiCrumbComponent.ts | 89 + src/crumb/lib/src/TiCrumbModule.ts | 24 + src/crumb/lib/src/crumb.html | 27 + src/crumb/lib/src/crumb.less | 34 + src/date/demo/karma.conf.js | 44 + src/date/demo/project.json | 90 + src/date/demo/src/app/AppComponent.ts | 7 + src/date/demo/src/app/AppModule.ts | 24 + src/date/demo/src/app/IndexComponent.ts | 49 + src/date/demo/src/app/app.html | 3 + .../src/app/date/DateCleariconComponent.ts | 8 + .../src/app/date/DateCustomizeComponent.ts | 38 + .../src/app/date/DateDisabledComponent.ts | 9 + .../src/app/date/DateDisableddaysComponent.ts | 16 + .../demo/src/app/date/DateEventComponent.ts | 13 + .../demo/src/app/date/DateFormComponent.ts | 22 + .../demo/src/app/date/DateFormatComponent.ts | 16 + .../src/app/date/DateFormatTestComponent.ts | 23 + .../demo/src/app/date/DateMaxComponent.ts | 19 + .../demo/src/app/date/DateMaxminComponent.ts | 12 + .../src/app/date/DateMaxminTestComponent.ts | 54 + .../demo/src/app/date/DateMinComponent.ts | 23 + .../src/app/date/DateNowdatetimeComponent.ts | 9 + .../src/app/date/DatePanelalignComponent.ts | 9 + src/date/demo/src/app/date/DateTestModule.ts | 111 + .../src/app/date/DateValidationComponent.ts | 12 + .../demo/src/app/date/DateValueComponent.ts | 9 + .../src/app/date/DateValueTestComponent.ts | 23 + .../demo/src/app/date/date-clearicon.html | 1 + .../demo/src/app/date/date-customize.html | 8 + src/date/demo/src/app/date/date-disabled.html | 1 + .../demo/src/app/date/date-disableddays.html | 1 + src/date/demo/src/app/date/date-event.html | 3 + src/date/demo/src/app/date/date-form.html | 13 + .../demo/src/app/date/date-format-test.html | 29 + src/date/demo/src/app/date/date-format.html | 7 + src/date/demo/src/app/date/date-max.html | 17 + .../demo/src/app/date/date-maxmin-test.html | 51 + src/date/demo/src/app/date/date-maxmin.html | 5 + src/date/demo/src/app/date/date-min.html | 20 + .../demo/src/app/date/date-nowdatetime.html | 1 + .../demo/src/app/date/date-panelalign.html | 1 + .../demo/src/app/date/date-validation.html | 1 + .../demo/src/app/date/date-value-test.html | 22 + src/date/demo/src/app/date/date-value.html | 3 + .../demo/src/app/date/webdoc/date-demos.js | 149 + src/date/demo/src/app/date/webdoc/date.cn.md | 29 + src/date/demo/src/app/date/webdoc/date.en.md | 29 + src/date/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/date/demo/src/index.html | 16 + src/date/demo/src/main.ts | 13 + src/date/demo/test.ts | 24 + src/date/demo/tsconfig.app.json | 15 + src/date/demo/tsconfig.spec.json | 20 + src/date/lib/index.ts | 2 + src/date/lib/ng-package.json | 7 + src/date/lib/package.json | 19 + src/date/lib/project.json | 62 + src/date/lib/src/TiDateComponent.ts | 180 + src/date/lib/src/TiDateModule.ts | 38 + src/date/lib/src/date-common.less | 37 + src/date/lib/src/date.html | 70 + src/date/lib/src/date.less | 39 + src/datebase/lib/index.ts | 1 + src/datebase/lib/ng-package.json | 7 + src/datebase/lib/package.json | 17 + src/datebase/lib/project.json | 62 + src/datebase/lib/src/TiDateBaseComponent.ts | 1157 ++++ src/datebase/lib/src/TiDateBaseModule.ts | 24 + src/datedominator/demo/project.json | 78 + .../demo/src/app/AppComponent.ts | 7 + src/datedominator/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/datedominator/demo/src/app/app.html | 3 + .../datedominator/DateDominatorComponent.ts | 48 + .../datedominator/DateDominatorTestModule.ts | 27 + .../src/app/datedominator/datedominator.html | 34 + .../src/app/datedominator/datedominator.less | 28 + src/datedominator/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/datedominator/demo/src/index.html | 16 + src/datedominator/demo/src/main.ts | 13 + src/datedominator/demo/tsconfig.app.json | 15 + src/datedominator/lib/index.ts | 1 + src/datedominator/lib/ng-package.json | 7 + src/datedominator/lib/package.json | 13 + src/datedominator/lib/project.json | 62 + .../lib/src/TiDateDominatorComponent.ts | 71 + .../lib/src/TiDateDominatorModule.ts | 32 + src/datedominator/lib/src/datedominator.html | 17 + src/datedominator/lib/src/datedominator.less | 102 + .../lib/src/i18n/TiDatedominatorWords.ts | 21 + src/datedominator/lib/src/i18n/en_US.ts | 23 + src/datedominator/lib/src/i18n/es_US.ts | 23 + src/datedominator/lib/src/i18n/fr_FR.ts | 23 + src/datedominator/lib/src/i18n/index.ts | 7 + src/datedominator/lib/src/i18n/pt_BR.ts | 23 + src/datedominator/lib/src/i18n/zh_CN.ts | 23 + src/dateedit/demo/project.json | 78 + src/dateedit/demo/src/app/AppComponent.ts | 7 + src/dateedit/demo/src/app/AppModule.ts | 24 + src/dateedit/demo/src/app/IndexComponent.ts | 49 + src/dateedit/demo/src/app/app.html | 3 + .../src/app/dateedit/DateEditComponent.ts | 20 + .../app/dateedit/DateEditFormatComponent.ts | 22 + .../src/app/dateedit/DateEditMaxComponent.ts | 21 + .../src/app/dateedit/DateEditMinComponent.ts | 21 + .../src/app/dateedit/DateEditTestModule.ts | 27 + .../app/dateedit/DateEditValueComponent.ts | 25 + .../src/app/dateedit/dateedit-format.html | 14 + .../demo/src/app/dateedit/dateedit-max.html | 12 + .../demo/src/app/dateedit/dateedit-min.html | 12 + .../demo/src/app/dateedit/dateedit-value.html | 18 + .../demo/src/app/dateedit/dateedit.html | 6 + .../demo/src/app/dateedit/dateedit.less | 17 + src/dateedit/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/dateedit/demo/src/index.html | 16 + src/dateedit/demo/src/main.ts | 13 + src/dateedit/demo/tsconfig.app.json | 15 + src/dateedit/lib/index.ts | 1 + src/dateedit/lib/ng-package.json | 7 + src/dateedit/lib/package.json | 14 + src/dateedit/lib/project.json | 62 + src/dateedit/lib/src/TiDateEditComponent.ts | 335 + src/dateedit/lib/src/TiDateEditModule.ts | 33 + src/dateedit/lib/src/dateedit.html | 14 + src/dateedit/lib/src/i18n/TiDateeditWords.ts | 5 + src/dateedit/lib/src/i18n/en_US.ts | 7 + src/dateedit/lib/src/i18n/es_US.ts | 7 + src/dateedit/lib/src/i18n/fr_FR.ts | 7 + src/dateedit/lib/src/i18n/index.ts | 7 + src/dateedit/lib/src/i18n/pt_BR.ts | 7 + src/dateedit/lib/src/i18n/zh_CN.ts | 7 + src/datepanel/lib/index.ts | 1 + src/datepanel/lib/ng-package.json | 7 + src/datepanel/lib/package.json | 17 + src/datepanel/lib/project.json | 62 + src/datepanel/lib/src/TiDatePanelComponent.ts | 1691 ++++++ src/datepanel/lib/src/TiDatePanelModule.ts | 35 + src/datepanel/lib/src/datepanel.html | 380 ++ src/datepanel/lib/src/datepanel.less | 430 ++ .../lib/src/i18n/TiDatepanelWords.ts | 11 + src/datepanel/lib/src/i18n/en_US.ts | 13 + src/datepanel/lib/src/i18n/es_US.ts | 13 + src/datepanel/lib/src/i18n/fr_FR.ts | 13 + src/datepanel/lib/src/i18n/index.ts | 7 + src/datepanel/lib/src/i18n/pt_BR.ts | 13 + src/datepanel/lib/src/i18n/zh_CN.ts | 13 + src/daterange/demo/karma.conf.js | 44 + src/daterange/demo/project.json | 90 + src/daterange/demo/src/app/AppComponent.ts | 7 + src/daterange/demo/src/app/AppModule.ts | 24 + src/daterange/demo/src/app/IndexComponent.ts | 49 + src/daterange/demo/src/app/app.html | 3 + .../src/app/daterange/DateRangeTestModule.ts | 131 + .../daterange/DaterangeCustomizeComponent.ts | 44 + .../daterange/DaterangeDisabledComponent.ts | 13 + .../DaterangeDisableddaysComponent.ts | 19 + .../app/daterange/DaterangeEventComponent.ts | 23 + .../daterange/DaterangeFixedvalueComponent.ts | 18 + .../DaterangeFixedvalueTestComponent.ts | 18 + .../app/daterange/DaterangeFormatComponent.ts | 17 + .../daterange/DaterangeFormatTestComponent.ts | 53 + .../DaterangeIsallowbeginequalendComponent.ts | 10 + .../app/daterange/DaterangeMaxComponent.ts | 36 + .../app/daterange/DaterangeMaxminComponent.ts | 13 + .../daterange/DaterangeMaxminTestComponent.ts | 47 + .../app/daterange/DaterangeMinComponent.ts | 35 + .../DaterangeNowdatetimeComponent.ts | 10 + .../daterange/DaterangePanelalignComponent.ts | 13 + .../daterange/DaterangeValidationComponent.ts | 15 + .../app/daterange/DaterangeValueComponent.ts | 13 + .../daterange/DaterangeValueTestComponent.ts | 48 + .../app/daterange/daterange-customize.html | 8 + .../src/app/daterange/daterange-disabled.html | 1 + .../app/daterange/daterange-disableddays.html | 1 + .../src/app/daterange/daterange-event.html | 10 + .../daterange/daterange-fixedvalue-test.html | 11 + .../app/daterange/daterange-fixedvalue.html | 9 + .../app/daterange/daterange-format-test.html | 36 + .../src/app/daterange/daterange-format.html | 7 + .../daterange-isallowbeginequalend.html | 1 + .../demo/src/app/daterange/daterange-max.html | 24 + .../app/daterange/daterange-maxmin-test.html | 42 + .../src/app/daterange/daterange-maxmin.html | 5 + .../demo/src/app/daterange/daterange-min.html | 24 + .../app/daterange/daterange-nowdatetime.html | 1 + .../app/daterange/daterange-panelalign.html | 1 + .../app/daterange/daterange-validation.html | 1 + .../app/daterange/daterange-value-test.html | 26 + .../src/app/daterange/daterange-value.html | 2 + .../app/daterange/webdoc/daterange-demos.js | 162 + .../src/app/daterange/webdoc/daterange.cn.md | 29 + .../src/app/daterange/webdoc/daterange.en.md | 29 + src/daterange/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/daterange/demo/src/index.html | 16 + src/daterange/demo/src/main.ts | 13 + src/daterange/demo/test.ts | 24 + src/daterange/demo/tsconfig.app.json | 15 + src/daterange/demo/tsconfig.spec.json | 20 + src/daterange/lib/index.ts | 2 + src/daterange/lib/ng-package.json | 7 + src/daterange/lib/package.json | 19 + src/daterange/lib/project.json | 62 + src/daterange/lib/src/TiDateRangeComponent.ts | 319 + src/daterange/lib/src/TiDateRangeModule.ts | 38 + src/daterange/lib/src/daterange.html | 112 + src/daterange/lib/src/daterange.less | 53 + src/datetime/demo/karma.conf.js | 44 + src/datetime/demo/project.json | 90 + src/datetime/demo/src/app/AppComponent.ts | 7 + src/datetime/demo/src/app/AppModule.ts | 24 + src/datetime/demo/src/app/IndexComponent.ts | 49 + src/datetime/demo/src/app/app.html | 3 + .../datetime/DatetimeCleariconComponent.ts | 8 + .../datetime/DatetimeCustomizeComponent.ts | 40 + .../app/datetime/DatetimeDisabledComponent.ts | 9 + .../app/datetime/DatetimeEventComponent.ts | 17 + .../app/datetime/DatetimeFormatComponent.ts | 25 + .../datetime/DatetimeFormatTestComponent.ts | 33 + .../src/app/datetime/DatetimeMaxComponent.ts | 22 + .../app/datetime/DatetimeMaxminComponent.ts | 12 + .../datetime/DatetimeMaxminTestComponent.ts | 60 + .../src/app/datetime/DatetimeMinComponent.ts | 22 + .../datetime/DatetimeNowdatetimeComponent.ts | 9 + .../datetime/DatetimePanelalignComponent.ts | 8 + .../src/app/datetime/DatetimeTestModule.ts | 123 + .../datetime/DatetimeTimezoneableComponent.ts | 49 + .../datetime/DatetimeValidationComponent.ts | 12 + .../app/datetime/DatetimeValueComponent.ts | 9 + .../datetime/DatetimeValueTestComponent.ts | 33 + .../src/app/datetime/datetime-clearicon.html | 1 + .../src/app/datetime/datetime-customize.html | 9 + .../src/app/datetime/datetime-disabled.html | 1 + .../demo/src/app/datetime/datetime-event.html | 9 + .../app/datetime/datetime-format-test.html | 23 + .../src/app/datetime/datetime-format.html | 5 + .../demo/src/app/datetime/datetime-max.html | 18 + .../app/datetime/datetime-maxmin-test.html | 35 + .../src/app/datetime/datetime-maxmin.html | 5 + .../demo/src/app/datetime/datetime-min.html | 20 + .../app/datetime/datetime-nowdatetime.html | 1 + .../src/app/datetime/datetime-panelalign.html | 1 + .../app/datetime/datetime-timezoneable.html | 33 + .../src/app/datetime/datetime-validation.html | 1 + .../src/app/datetime/datetime-value-test.html | 31 + .../demo/src/app/datetime/datetime-value.html | 3 + .../src/app/datetime/webdoc/datetime-demos.js | 157 + .../src/app/datetime/webdoc/datetime.cn.md | 30 + .../src/app/datetime/webdoc/datetime.en.md | 29 + src/datetime/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/datetime/demo/src/index.html | 16 + src/datetime/demo/src/main.ts | 13 + src/datetime/demo/test.ts | 24 + src/datetime/demo/tsconfig.app.json | 15 + src/datetime/demo/tsconfig.spec.json | 20 + src/datetime/lib/index.ts | 2 + src/datetime/lib/ng-package.json | 7 + src/datetime/lib/package.json | 22 + src/datetime/lib/project.json | 62 + src/datetime/lib/src/TiDatetimeComponent.ts | 469 ++ src/datetime/lib/src/TiDatetimeModule.ts | 46 + src/datetime/lib/src/datetime.html | 133 + src/datetime/lib/src/datetime.less | 104 + src/datetimerange/demo/karma.conf.js | 44 + src/datetimerange/demo/project.json | 90 + .../demo/src/app/AppComponent.ts | 7 + src/datetimerange/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/datetimerange/demo/src/app/app.html | 3 + .../datetimerange/DatetimeRangeTestModule.ts | 140 + .../DatetimerangeCleariconComponent.ts | 12 + .../DatetimerangeCustomizeComponent.ts | 47 + .../DatetimerangeDisabledComponent.ts | 13 + .../DatetimerangeEventComponent.ts | 26 + .../DatetimerangeFormatComponent.ts | 25 + .../DatetimerangeFormatTestComponent.ts | 55 + ...etimerangeIsallowbeginequalendComponent.ts | 9 + .../DatetimerangeManyTestComponent.ts | 14 + .../DatetimerangeMaxComponent.ts | 51 + .../DatetimerangeMaxminComponent.ts | 13 + .../DatetimerangeMaxminTestComponent.ts | 60 + .../DatetimerangeMinComponent.ts | 36 + .../DatetimerangeNowdatetimeComponent.ts | 10 + .../DatetimerangePanelalignComponent.ts | 25 + .../DatetimerangeTimezoneableComponent.ts | 45 + .../DatetimerangeValidationComponent.ts | 15 + .../DatetimerangeValueComponent.ts | 13 + .../DatetimerangeValueTestComponent.ts | 37 + .../datetimerange-clearicon.html | 1 + .../datetimerange-customize.html | 9 + .../datetimerange/datetimerange-disabled.html | 1 + .../datetimerange/datetimerange-event.html | 10 + .../datetimerange-format-test.html | 24 + .../datetimerange/datetimerange-format.html | 5 + .../datetimerange-isallowbeginequalend.html | 6 + .../datetimerange-many-test.html | 6 + .../app/datetimerange/datetimerange-max.html | 39 + .../datetimerange-maxmin-test.html | 43 + .../datetimerange/datetimerange-maxmin.html | 5 + .../app/datetimerange/datetimerange-min.html | 26 + .../datetimerange-nowdatetime.html | 1 + .../datetimerange-panelalign.html | 1 + .../datetimerange-timezoneable.html | 20 + .../datetimerange-validation.html | 1 + .../datetimerange-value-test.html | 19 + .../datetimerange/datetimerange-value.html | 3 + .../webdoc/datetimerange-demos.js | 162 + .../datetimerange/webdoc/datetimerange.cn.md | 30 + .../datetimerange/webdoc/datetimerange.en.md | 29 + src/datetimerange/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/datetimerange/demo/src/index.html | 16 + src/datetimerange/demo/src/main.ts | 13 + src/datetimerange/demo/test.ts | 24 + src/datetimerange/demo/tsconfig.app.json | 15 + src/datetimerange/demo/tsconfig.spec.json | 20 + src/datetimerange/lib/index.ts | 2 + src/datetimerange/lib/ng-package.json | 7 + src/datetimerange/lib/package.json | 22 + src/datetimerange/lib/project.json | 62 + .../lib/src/TiDatetimeRangeComponent.ts | 824 +++ .../lib/src/TiDatetimeRangeModule.ts | 44 + src/datetimerange/lib/src/datetimerange.html | 167 + src/datetimerange/lib/src/datetimerange.less | 94 + src/dominator/demo/project.json | 78 + src/dominator/demo/src/app/AppComponent.ts | 7 + src/dominator/demo/src/app/AppModule.ts | 24 + src/dominator/demo/src/app/IndexComponent.ts | 49 + src/dominator/demo/src/app/app.html | 3 + .../dominator/DominatorDefaultComponent.ts | 45 + .../src/app/dominator/DominatorTestModule.ts | 18 + .../src/app/dominator/dominator-default.html | 57 + src/dominator/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/dominator/demo/src/index.html | 16 + src/dominator/demo/src/main.ts | 13 + src/dominator/demo/tsconfig.app.json | 15 + src/dominator/lib/index.ts | 1 + src/dominator/lib/ng-package.json | 7 + src/dominator/lib/package.json | 16 + src/dominator/lib/project.json | 62 + src/dominator/lib/src/TiDominatorComponent.ts | 277 + src/dominator/lib/src/TiDominatorModule.ts | 35 + src/dominator/lib/src/dominator.html | 105 + src/dominator/lib/src/dominator.less | 188 + .../lib/src/i18n/TiDominatorWords.ts | 5 + src/dominator/lib/src/i18n/en_US.ts | 7 + src/dominator/lib/src/i18n/es_US.ts | 7 + src/dominator/lib/src/i18n/fr_FR.ts | 7 + src/dominator/lib/src/i18n/index.ts | 7 + src/dominator/lib/src/i18n/pt_BR.ts | 7 + src/dominator/lib/src/i18n/zh_CN.ts | 7 + src/drag/demo/project.json | 78 + src/drag/demo/src/app/AppComponent.ts | 7 + src/drag/demo/src/app/AppModule.ts | 24 + src/drag/demo/src/app/IndexComponent.ts | 49 + src/drag/demo/src/app/app.html | 3 + .../demo/src/app/drag/DragBasicComponent.ts | 31 + .../demo/src/app/drag/DragServiceComponent.ts | 37 + .../demo/src/app/drag/DragTestComponent.ts | 20 + src/drag/demo/src/app/drag/DragTestModule.ts | 29 + src/drag/demo/src/app/drag/drag-basic.html | 9 + src/drag/demo/src/app/drag/drag-service.html | 7 + src/drag/demo/src/app/drag/drag-test.html | 17 + src/drag/demo/src/app/drag/dragTest.less | 14 + src/drag/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/drag/demo/src/index.html | 16 + src/drag/demo/src/main.ts | 13 + src/drag/demo/tsconfig.app.json | 15 + src/drag/lib/index.ts | 3 + src/drag/lib/ng-package.json | 7 + src/drag/lib/package.json | 11 + src/drag/lib/project.json | 62 + src/drag/lib/src/TiDragService.ts | 199 + src/drag/lib/src/TiDragServiceModule.ts | 22 + src/drag/lib/src/TiDraggableDirective.ts | 58 + src/drag/lib/src/TiDraggableModule.ts | 25 + src/drop/demo/project.json | 78 + src/drop/demo/src/app/AppComponent.ts | 7 + src/drop/demo/src/app/AppModule.ts | 24 + src/drop/demo/src/app/IndexComponent.ts | 49 + src/drop/demo/src/app/app.html | 3 + .../src/app/drop/DropAppendtobodyComponent.ts | 30 + .../demo/src/app/drop/DropDefaultComponent.ts | 6 + src/drop/demo/src/app/drop/DropTestModule.ts | 21 + .../demo/src/app/drop/drop-appendtobody.html | 27 + src/drop/demo/src/app/drop/drop-default.html | 251 + src/drop/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/drop/demo/src/index.html | 16 + src/drop/demo/src/main.ts | 13 + src/drop/demo/tsconfig.app.json | 15 + src/drop/lib/index.ts | 1 + src/drop/lib/ng-package.json | 7 + src/drop/lib/package.json | 11 + src/drop/lib/project.json | 62 + src/drop/lib/src/TiDropComponent.ts | 362 ++ src/drop/lib/src/TiDropModule.ts | 24 + src/drop/lib/src/drop.html | 3 + src/drop/lib/src/drop.less | 15 + src/droplist/demo/project.json | 78 + src/droplist/demo/src/app/AppComponent.ts | 7 + src/droplist/demo/src/app/AppModule.ts | 24 + src/droplist/demo/src/app/IndexComponent.ts | 49 + src/droplist/demo/src/app/app.html | 3 + .../app/droplist/DroplistDefaultComponent.ts | 47 + .../src/app/droplist/DroplistTestModule.ts | 17 + .../src/app/droplist/droplist-default.html | 31 + src/droplist/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/droplist/demo/src/index.html | 16 + src/droplist/demo/src/main.ts | 13 + src/droplist/demo/tsconfig.app.json | 15 + src/droplist/lib/index.ts | 1 + src/droplist/lib/ng-package.json | 7 + src/droplist/lib/package.json | 15 + src/droplist/lib/project.json | 62 + src/droplist/lib/src/TiDroplistComponent.ts | 458 ++ src/droplist/lib/src/TiDroplistModule.ts | 28 + src/droplist/lib/src/droplist.html | 34 + src/dropsearch/lib/index.ts | 1 + src/dropsearch/lib/ng-package.json | 7 + src/dropsearch/lib/package.json | 17 + src/dropsearch/lib/project.json | 62 + .../lib/src/TiDropsearchComponent.ts | 282 + src/dropsearch/lib/src/TiDropsearchModule.ts | 29 + src/dropsearch/lib/src/dropsearch.html | 52 + src/dropsearch/lib/src/dropsearch.less | 5 + src/environments/environment.prod.ts | 7 + src/environments/environment.ts | 18 + src/environments/environment.wc.ts | 4 + src/foldtext/demo/karma.conf.js | 41 + src/foldtext/demo/project.json | 90 + src/foldtext/demo/src/app/AppComponent.ts | 7 + src/foldtext/demo/src/app/AppModule.ts | 26 + src/foldtext/demo/src/app/IndexComponent.ts | 49 + src/foldtext/demo/src/app/app.html | 3 + .../app/foldtext/FoldtextBasicComponent.ts | 11 + .../app/foldtext/FoldtextTableComponent.ts | 75 + .../src/app/foldtext/FoldtextTestModule.ts | 19 + .../demo/src/app/foldtext/foldtext-basic.html | 3 + .../demo/src/app/foldtext/foldtext-table.html | 25 + .../src/app/foldtext/webdoc/foldtext-demos.js | 28 + .../src/app/foldtext/webdoc/foldtext.cn.md | 24 + .../src/app/foldtext/webdoc/foldtext.en.md | 29 + src/foldtext/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/foldtext/demo/src/index.html | 16 + src/foldtext/demo/src/main.ts | 13 + src/foldtext/demo/test.ts | 24 + src/foldtext/demo/tsconfig.app.json | 10 + src/foldtext/demo/tsconfig.spec.json | 15 + src/foldtext/lib/index.ts | 2 + src/foldtext/lib/ng-package.json | 7 + src/foldtext/lib/package.json | 12 + src/foldtext/lib/project.json | 62 + src/foldtext/lib/src/TiFoldtextComponent.ts | 100 + src/foldtext/lib/src/TiFoldtextModule.ts | 30 + src/foldtext/lib/src/foldtext.html | 16 + src/foldtext/lib/src/foldtext.less | 14 + src/foldtext/lib/src/i18n/TiFoldtextWords.ts | 6 + src/foldtext/lib/src/i18n/en_US.ts | 8 + src/foldtext/lib/src/i18n/es_US.ts | 8 + src/foldtext/lib/src/i18n/fr_FR.ts | 8 + src/foldtext/lib/src/i18n/index.ts | 7 + src/foldtext/lib/src/i18n/pt_BR.ts | 8 + src/foldtext/lib/src/i18n/zh_CN.ts | 8 + src/formfield/demo/karma.conf.js | 44 + src/formfield/demo/project.json | 90 + src/formfield/demo/src/app/AppComponent.ts | 7 + src/formfield/demo/src/app/AppModule.ts | 24 + src/formfield/demo/src/app/IndexComponent.ts | 49 + src/formfield/demo/src/app/app.html | 3 + .../FormfieldColspanRowspanComponent.ts | 11 + .../FormfieldColspanRowspanTestComponent.ts | 44 + .../formfield/FormfieldColswidthComponent.ts | 22 + .../app/formfield/FormfieldFooComponent.ts | 67 + .../app/formfield/FormfieldIndexComponent.ts | 13 + .../app/formfield/FormfieldLabelComponent.ts | 17 + .../formfield/FormfieldLabelwidthComponent.ts | 59 + .../FormfieldMultiColumnComponent.ts | 13 + .../FormfieldNestFormfiledComponent.ts | 40 + .../formfield/FormfieldNgforTestComponent.ts | 37 + .../app/formfield/FormfieldRawgapComponent.ts | 38 + .../formfield/FormfieldRequiredComponent.ts | 17 + .../FormfieldRequiredspaceComponent.ts | 14 + .../FormfieldRequiredspaceTestComponent.ts | 14 + .../app/formfield/FormfieldShowComponent.ts | 18 + .../FormfieldSingleColumnComponent.ts | 13 + .../app/formfield/FormfieldTestComponent.ts | 144 + .../src/app/formfield/FormfieldTestModule.ts | 156 + .../formfield/FormfieldTextFormComponent.ts | 44 + .../FormfieldVerticalAlignComponent.ts | 8 + .../formfield/FormfieldVerticalComponent.ts | 12 + .../formfield-colspan-rowspan-test.html | 34 + .../formfield/formfield-colspan-rowspan.html | 41 + .../app/formfield/formfield-colswidth.html | 22 + .../demo/src/app/formfield/formfield-foo.html | 12 + .../src/app/formfield/formfield-index.html | 21 + .../src/app/formfield/formfield-label.html | 25 + .../app/formfield/formfield-labelwidth.html | 27 + .../app/formfield/formfield-multi-column.html | 21 + .../formfield/formfield-nest-formfiled.html | 177 + .../app/formfield/formfield-ngfor-test.html | 6 + .../src/app/formfield/formfield-required.html | 22 + .../formfield-requiredspace-test.html | 177 + .../formfield/formfield-requiredspace.html | 42 + .../src/app/formfield/formfield-rowgap.html | 61 + .../src/app/formfield/formfield-show.html | 23 + .../formfield/formfield-single-column.html | 21 + .../src/app/formfield/formfield-test.html | 65 + .../app/formfield/formfield-text-form.html | 20 + .../formfield/formfield-vertical-align.html | 38 + .../src/app/formfield/formfield-vertical.html | 21 + .../app/formfield/webdoc/formfield-demos.js | 178 + .../src/app/formfield/webdoc/formfield.cn.md | 27 + .../src/app/formfield/webdoc/formfield.en.md | 29 + src/formfield/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/formfield/demo/src/index.html | 16 + src/formfield/demo/src/main.ts | 13 + src/formfield/demo/test.ts | 24 + src/formfield/demo/tsconfig.app.json | 15 + src/formfield/demo/tsconfig.spec.json | 20 + src/formfield/lib/index.ts | 1 + src/formfield/lib/ng-package.json | 7 + src/formfield/lib/package.json | 13 + src/formfield/lib/project.json | 62 + .../lib/src/TiButtonItemComponent.ts | 88 + src/formfield/lib/src/TiFormfieldComponent.ts | 373 ++ src/formfield/lib/src/TiFormfieldModule.ts | 30 + src/formfield/lib/src/TiItemComponent.ts | 203 + src/formfield/lib/src/TiItemLabelComponent.ts | 30 + src/formfield/lib/src/formfield-btn-item.html | 1 + .../lib/src/formfield-item-label.html | 1 + src/formfield/lib/src/formfield-item.html | 1 + src/formfield/lib/src/formfield.html | 112 + src/formfield/lib/src/formfield.less | 114 + src/grid/lib/index.ts | 1 + src/grid/lib/ng-package.json | 7 + src/grid/lib/package.json | 8 + src/grid/lib/project.json | 62 + src/grid/lib/src/TiGridComponent.ts | 30 + src/grid/lib/src/TiGridModule.ts | 25 + src/grid/lib/src/grid.less | 108 + src/guides/demo/project.json | 90 + src/guides/demo/src/app/AppComponent.ts | 7 + src/guides/demo/src/app/AppModule.ts | 24 + src/guides/demo/src/app/IndexComponent.ts | 49 + src/guides/demo/src/app/app.html | 3 + .../src/app/guides/GuidesBasicComponent.ts | 17 + .../app/guides/GuidesGuidestepsComponent.ts | 70 + .../demo/src/app/guides/GuidesTabComponent.ts | 89 + .../demo/src/app/guides/GuidesTestModule.ts | 39 + .../src/app/guides/GuidesTypeComponent.ts | 32 + .../demo/src/app/guides/guides-basic.html | 14 + .../src/app/guides/guides-guidesteps.html | 22 + .../demo/src/app/guides/guides-tab.html | 38 + .../demo/src/app/guides/guides-type.html | 16 + .../src/app/guides/webdoc/guides-demos.js | 29 + .../demo/src/app/guides/webdoc/guides.cn.md | 15 + .../demo/src/app/guides/webdoc/guides.en.md | 13 + src/guides/demo/src/favicon.ico | Bin 0 -> 300852 bytes src/guides/demo/src/index.html | 16 + src/guides/demo/src/main.ts | 13 + src/guides/demo/tsconfig.app.json | 10 + src/guides/lib/index.ts | 2 + src/guides/lib/ng-package.json | 7 + src/guides/lib/package.json | 11 + src/guides/lib/project.json | 62 + src/guides/lib/src/TiGuideBodyComponent.ts | 60 + src/guides/lib/src/TiGuideComponent.ts | 113 + src/guides/lib/src/TiGuideContentComponent.ts | 23 + src/guides/lib/src/TiGuideHeaderComponent.ts | 23 + src/guides/lib/src/TiGuidesComponent.ts | 58 + src/guides/lib/src/TiGuidesModule.ts | 33 + src/guides/lib/src/guide-body.html | 3 + src/guides/lib/src/guide-body.less | 11 + src/guides/lib/src/guide-content.less | 7 + src/guides/lib/src/guide-header.less | 14 + src/guides/lib/src/guide.html | 18 + src/guides/lib/src/guide.less | 165 + src/guides/lib/src/guides.less | 16 + src/guidesteps/demo/project.json | 78 + src/guidesteps/demo/src/app/AppComponent.ts | 7 + src/guidesteps/demo/src/app/AppModule.ts | 24 + src/guidesteps/demo/src/app/IndexComponent.ts | 49 + src/guidesteps/demo/src/app/app.html | 3 + .../guidesteps/GuidestepsBasicComponent.ts | 21 + .../GuidestepsIscompleteComponent.ts | 22 + .../guidesteps/GuidestepsLargeComponent.ts | 21 + .../app/guidesteps/GuidestepsTestModule.ts | 21 + .../src/app/guidesteps/guidesteps-basic.html | 6 + .../app/guidesteps/guidesteps-iscomplete.html | 8 + .../src/app/guidesteps/guidesteps-large.html | 6 + .../app/guidesteps/webdoc/guidesteps-demos.js | 39 + .../app/guidesteps/webdoc/guidesteps.cn.md | 15 + .../app/guidesteps/webdoc/guidesteps.en.md | 13 + src/guidesteps/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/guidesteps/demo/src/index.html | 16 + src/guidesteps/demo/src/main.ts | 13 + src/guidesteps/demo/tsconfig.app.json | 10 + src/guidesteps/lib/index.ts | 2 + src/guidesteps/lib/ng-package.json | 7 + src/guidesteps/lib/package.json | 9 + src/guidesteps/lib/project.json | 62 + .../lib/src/TiGuidestepComponent.ts | 30 + .../lib/src/TiGuidestepContentComponent.ts | 22 + .../lib/src/TiGuidestepHeaderComponent.ts | 31 + .../lib/src/TiGuidestepsComponent.ts | 21 + src/guidesteps/lib/src/TiGuidestepsModule.ts | 30 + src/guidesteps/lib/src/guidestep-content.less | 7 + src/guidesteps/lib/src/guidestep-header.less | 33 + src/guidesteps/lib/src/guidestep.less | 39 + src/guidesteps/lib/src/guidesteps.less | 14 + src/halfmodal/demo/karma.conf.js | 44 + src/halfmodal/demo/project.json | 90 + src/halfmodal/demo/src/app/AppComponent.ts | 7 + src/halfmodal/demo/src/app/AppModule.ts | 24 + src/halfmodal/demo/src/app/IndexComponent.ts | 49 + src/halfmodal/demo/src/app/app.html | 3 + .../app/halfmodal/HalfmodalAsyncComponent.ts | 28 + .../halfmodal/HalfmodalBackdropComponent.ts | 12 + .../app/halfmodal/HalfmodalBasicComponent.ts | 23 + .../halfmodal/HalfmodalBeforehideComponent.ts | 22 + .../halfmodal/HalfmodalContentComponent.ts | 128 + .../app/halfmodal/HalfmodalModalComponent.ts | 19 + .../HalfmodalModalselectComponent.ts | 39 + .../app/halfmodal/HalfmodalMultiComponent.ts | 32 + .../halfmodal/HalfmodalServiceComponent.ts | 103 + .../HalfmodalServiceTestComponent.ts | 34 + .../src/app/halfmodal/HalfmodalTestModule.ts | 110 + .../src/app/halfmodal/halfmodal-async.html | 10 + .../src/app/halfmodal/halfmodal-backdrop.html | 5 + .../src/app/halfmodal/halfmodal-basic.html | 20 + .../app/halfmodal/halfmodal-beforehide.html | 7 + .../src/app/halfmodal/halfmodal-content.html | 61 + .../src/app/halfmodal/halfmodal-modal.html | 7 + .../app/halfmodal/halfmodal-modalselect.html | 16 + .../src/app/halfmodal/halfmodal-multi.html | 58 + .../app/halfmodal/halfmodal-service-test.html | 24 + .../src/app/halfmodal/halfmodal-service.html | 26 + .../demo/src/app/halfmodal/halfmodalTest.less | 12 + .../src/app/halfmodal/webdoc/formfield.en.md | 29 + .../app/halfmodal/webdoc/halfmodal-demos.js | 75 + .../src/app/halfmodal/webdoc/halfmodal.cn.md | 27 + src/halfmodal/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/halfmodal/demo/src/index.html | 16 + src/halfmodal/demo/src/main.ts | 13 + src/halfmodal/demo/test.ts | 24 + src/halfmodal/demo/tsconfig.app.json | 15 + src/halfmodal/demo/tsconfig.spec.json | 20 + src/halfmodal/lib/index.ts | 2 + src/halfmodal/lib/ng-package.json | 7 + src/halfmodal/lib/package.json | 15 + src/halfmodal/lib/project.json | 62 + .../lib/src/TiHalfmodalBodyComponent.ts | 28 + src/halfmodal/lib/src/TiHalfmodalComponent.ts | 300 + .../lib/src/TiHalfmodalContainerComponent.ts | 77 + .../lib/src/TiHalfmodalFooterComponent.ts | 28 + .../lib/src/TiHalfmodalHeaderComponent.ts | 29 + src/halfmodal/lib/src/TiHalfmodalModule.ts | 46 + src/halfmodal/lib/src/TiHalfmodalService.ts | 177 + src/halfmodal/lib/src/halfmodal-body.less | 6 + src/halfmodal/lib/src/halfmodal-footer.less | 11 + src/halfmodal/lib/src/halfmodal-header.less | 11 + src/halfmodal/lib/src/halfmodal.html | 15 + src/halfmodal/lib/src/halfmodal.less | 56 + src/icon/demo/karma.conf.js | 44 + src/icon/demo/project.json | 90 + src/icon/demo/src/app/AppComponent.ts | 7 + src/icon/demo/src/app/AppModule.ts | 25 + src/icon/demo/src/app/IndexComponent.ts | 49 + src/icon/demo/src/app/app.html | 3 + .../demo/src/app/icon/IconBasicComponent.ts | 7 + .../demo/src/app/icon/IconShowComponent.ts | 63 + src/icon/demo/src/app/icon/IconTestModule.ts | 31 + .../demo/src/app/icon/SvgSetpathComponent.ts | 12 + src/icon/demo/src/app/icon/icon-basic.html | 6 + src/icon/demo/src/app/icon/icon-show.html | 6 + src/icon/demo/src/app/icon/icon.less | 32 + src/icon/demo/src/app/icon/svg-setpath.html | 6 + src/icon/demo/src/app/icon/ui3-icons.PNG | Bin 0 -> 21280 bytes .../demo/src/app/icon/webdoc/icon-demos.js | 42 + src/icon/demo/src/app/icon/webdoc/icon.cn.md | 23 + src/icon/demo/src/app/icon/webdoc/icon.en.md | 29 + src/icon/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/icon/demo/src/index.html | 16 + src/icon/demo/src/main.ts | 13 + src/icon/demo/test.ts | 24 + src/icon/demo/tsconfig.app.json | 15 + src/icon/demo/tsconfig.spec.json | 20 + src/icon/lib/index.ts | 1 + src/icon/lib/ng-package.json | 7 + src/icon/lib/package.json | 12 + src/icon/lib/project.json | 62 + src/icon/lib/src/Ti-Icon.svg | 83 + src/icon/lib/src/Ti-Icon.woff | Bin 0 -> 9248 bytes src/icon/lib/src/TiIconClassComponent.ts | 30 + src/icon/lib/src/TiIconComponent.ts | 61 + src/icon/lib/src/TiIconModule.ts | 26 + src/icon/lib/src/TiSvgComponent.ts | 100 + src/icon/lib/src/icon.less | 193 + src/icon/lib/src/svg.less | 11 + src/iconaction/demo/project.json | 90 + src/iconaction/demo/src/app/AppComponent.ts | 7 + src/iconaction/demo/src/app/AppModule.ts | 26 + src/iconaction/demo/src/app/IndexComponent.ts | 49 + src/iconaction/demo/src/app/app.html | 3 + .../iconaction/IconactionBasicComponent.ts | 6 + .../app/iconaction/IconactionDarkComponent.ts | 14 + .../iconaction/IconactionDisabledComponent.ts | 14 + .../app/iconaction/IconactionHrefComponent.ts | 6 + .../app/iconaction/IconactionTestModule.ts | 37 + .../src/app/iconaction/iconaction-basic.html | 1 + .../src/app/iconaction/iconaction-dark.html | 3 + .../app/iconaction/iconaction-disabled.html | 4 + .../src/app/iconaction/iconaction-href.html | 1 + .../app/iconaction/webdoc/iconaction-demos.js | 53 + .../app/iconaction/webdoc/iconaction.cn.md | 17 + .../app/iconaction/webdoc/iconaction.en.md | 13 + src/iconaction/demo/src/favicon.ico | Bin 0 -> 300852 bytes src/iconaction/demo/src/index.html | 16 + src/iconaction/demo/src/main.ts | 13 + src/iconaction/demo/tsconfig.app.json | 10 + src/iconaction/lib/index.ts | 2 + src/iconaction/lib/ng-package.json | 7 + src/iconaction/lib/package.json | 10 + src/iconaction/lib/project.json | 62 + .../lib/src/TiIconactionComponent.ts | 66 + src/iconaction/lib/src/TiIconactionModule.ts | 24 + src/iconaction/lib/src/iconaction.html | 13 + src/iconaction/lib/src/iconaction.less | 74 + src/imagepreview/demo/project.json | 86 + src/imagepreview/demo/src/app/AppComponent.ts | 7 + src/imagepreview/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/imagepreview/demo/src/app/app.html | 3 + .../ImagepreviewBasicComponent.ts | 39 + .../imagepreview/ImagepreviewTestModule.ts | 21 + .../app/imagepreview/imagepreview-basic.html | 6 + .../src/app/imagepreview/imagepreview.less | 9 + .../imagepreview/webdoc/imagepreview-demos.js | 47 + .../imagepreview/webdoc/imagepreview.cn.md | 27 + .../imagepreview/webdoc/imagepreview.en.md | 29 + src/imagepreview/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/imagepreview/demo/src/index.html | 16 + src/imagepreview/demo/src/main.ts | 13 + src/imagepreview/demo/tsconfig.app.json | 15 + src/imagepreview/lib/index.ts | 1 + src/imagepreview/lib/ng-package.json | 7 + src/imagepreview/lib/package.json | 14 + src/imagepreview/lib/project.json | 62 + .../lib/src/TiImagepreviewComponent.ts | 117 + .../lib/src/TiImagepreviewModule.ts | 27 + src/imagepreview/lib/src/imagepreview.html | 30 + src/imagepreview/lib/src/imagepreview.less | 58 + src/include/lib/index.ts | 1 + src/include/lib/ng-package.json | 7 + src/include/lib/package.json | 10 + src/include/lib/project.json | 62 + src/include/lib/src/TiIncludeDirective.ts | 43 + src/include/lib/src/TiIncludeModule.ts | 24 + src/inputnumber/demo/karma.conf.js | 44 + src/inputnumber/demo/project.json | 90 + src/inputnumber/demo/src/app/AppComponent.ts | 7 + src/inputnumber/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/inputnumber/demo/src/app/app.html | 3 + .../inputnumber/InputnumberBasicComponent.ts | 8 + .../inputnumber/InputnumberEventComponent.ts | 21 + .../inputnumber/InputnumberFocusComponent.ts | 8 + .../inputnumber/InputnumberFormatComponent.ts | 9 + .../inputnumber/InputnumberLoadComponent.ts | 24 + .../InputnumberLocaleableComponent.ts | 9 + .../InputnumberMaxlengthComponent.ts | 8 + .../app/inputnumber/InputnumberTestModule.ts | 70 + .../app/inputnumber/inputnumber-basic.html | 4 + .../app/inputnumber/inputnumber-event.html | 11 + .../app/inputnumber/inputnumber-focus.html | 9 + .../app/inputnumber/inputnumber-format.html | 1 + .../src/app/inputnumber/inputnumber-load.html | 20 + .../inputnumber/inputnumber-localeable.html | 1 + .../inputnumber/inputnumber-maxlength.html | 1 + .../inputnumber/webdoc/inputnumber-demos.js | 66 + .../app/inputnumber/webdoc/inputnumber.cn.md | 27 + .../app/inputnumber/webdoc/inputnumber.en.md | 29 + src/inputnumber/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/inputnumber/demo/src/index.html | 16 + src/inputnumber/demo/src/main.ts | 13 + src/inputnumber/demo/test.ts | 24 + src/inputnumber/demo/tsconfig.app.json | 15 + src/inputnumber/demo/tsconfig.spec.json | 20 + src/inputnumber/lib/index.ts | 1 + src/inputnumber/lib/ng-package.json | 7 + src/inputnumber/lib/package.json | 12 + src/inputnumber/lib/project.json | 62 + .../lib/src/TiInputNumberDirective.ts | 273 + .../lib/src/TiInputNumberModule.ts | 22 + src/intro/demo/karma.conf.js | 44 + src/intro/demo/project.json | 98 + src/intro/demo/src/app/AppComponent.ts | 7 + src/intro/demo/src/app/AppModule.ts | 26 + src/intro/demo/src/app/IndexComponent.ts | 49 + src/intro/demo/src/app/app.html | 3 + .../demo/src/app/intro/IntroBasicComponent.ts | 66 + .../demo/src/app/intro/IntroEventComponent.ts | 66 + .../demo/src/app/intro/IntroModalComponent.ts | 54 + .../src/app/intro/IntroSkipableComponent.ts | 57 + .../src/app/intro/IntroTemplateComponent.ts | 58 + .../demo/src/app/intro/IntroTestModule.ts | 67 + .../demo/src/app/intro/IntroTipComponent.ts | 74 + .../src/app/intro/IntroTiscrollComponent.ts | 37 + src/intro/demo/src/app/intro/intro-basic.html | 10 + src/intro/demo/src/app/intro/intro-event.html | 10 + src/intro/demo/src/app/intro/intro-modal.html | 22 + .../demo/src/app/intro/intro-skipable.html | 24 + .../demo/src/app/intro/intro-template.html | 29 + src/intro/demo/src/app/intro/intro-tip.html | 45 + .../demo/src/app/intro/intro-tiscroll.html | 27 + .../demo/src/app/intro/webdoc/intro-demos.js | 47 + .../demo/src/app/intro/webdoc/intro.cn.md | 30 + .../demo/src/app/intro/webdoc/intro.en.md | 29 + src/intro/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/intro/demo/src/index.html | 16 + src/intro/demo/src/main.ts | 13 + src/intro/demo/test.ts | 24 + src/intro/demo/tsconfig.app.json | 15 + src/intro/demo/tsconfig.spec.json | 20 + src/intro/lib/index.ts | 2 + src/intro/lib/ng-package.json | 7 + src/intro/lib/package.json | 17 + src/intro/lib/project.json | 62 + src/intro/lib/src/TiIntroInterface.ts | 107 + src/intro/lib/src/TiIntroModule.ts | 38 + src/intro/lib/src/TiIntroService.ts | 456 ++ src/intro/lib/src/TiIntromodalComponent.ts | 55 + src/intro/lib/src/TiIntrotipComponent.ts | 27 + src/intro/lib/src/i18n/TiIntroWords.ts | 8 + src/intro/lib/src/i18n/en_US.ts | 10 + src/intro/lib/src/i18n/es_US.ts | 10 + src/intro/lib/src/i18n/fr_FR.ts | 10 + src/intro/lib/src/i18n/index.ts | 7 + src/intro/lib/src/i18n/pt_BR.ts | 10 + src/intro/lib/src/i18n/zh_CN.ts | 10 + src/intro/lib/src/intro.less | 87 + src/intro/lib/src/intromodal.html | 32 + src/intro/lib/src/introtip.html | 49 + src/ip/demo/karma.conf.js | 44 + src/ip/demo/project.json | 90 + src/ip/demo/src/app/AppComponent.ts | 7 + src/ip/demo/src/app/AppModule.ts | 24 + src/ip/demo/src/app/IndexComponent.ts | 49 + src/ip/demo/src/app/app.html | 3 + src/ip/demo/src/app/ip/IpBasicComponent.ts | 10 + src/ip/demo/src/app/ip/IpDisabledComponent.ts | 9 + .../demo/src/app/ip/IpFormcontrolComponent.ts | 15 + src/ip/demo/src/app/ip/IpTestModule.ts | 45 + src/ip/demo/src/app/ip/IpValidComponent.ts | 9 + src/ip/demo/src/app/ip/ip-basic.html | 16 + src/ip/demo/src/app/ip/ip-disabled.html | 1 + src/ip/demo/src/app/ip/ip-formcontrol.html | 6 + src/ip/demo/src/app/ip/ip-valid.html | 9 + src/ip/demo/src/app/ip/webdoc/ip-demos.js | 53 + src/ip/demo/src/app/ip/webdoc/ip.cn.md | 23 + src/ip/demo/src/app/ip/webdoc/ip.en.md | 29 + src/ip/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/ip/demo/src/index.html | 16 + src/ip/demo/src/main.ts | 13 + src/ip/demo/test.ts | 24 + src/ip/demo/tsconfig.app.json | 15 + src/ip/demo/tsconfig.spec.json | 20 + src/ip/lib/index.ts | 1 + src/ip/lib/ng-package.json | 7 + src/ip/lib/package.json | 13 + src/ip/lib/project.json | 62 + src/ip/lib/src/TiIpComponent.ts | 464 ++ src/ip/lib/src/TiIpModule.ts | 23 + src/ip/lib/src/ip.html | 20 + src/ip/lib/src/ip.less | 64 + src/ipsection/demo/karma.conf.js | 44 + src/ipsection/demo/project.json | 90 + src/ipsection/demo/src/app/AppComponent.ts | 7 + src/ipsection/demo/src/app/AppModule.ts | 24 + src/ipsection/demo/src/app/IndexComponent.ts | 49 + src/ipsection/demo/src/app/app.html | 3 + .../app/ipsection/IpsectionBasicComponent.ts | 23 + .../ipsection/IpsectionDisabledComponent.ts | 22 + .../app/ipsection/IpsectionEventsComponent.ts | 23 + .../app/ipsection/IpsectionFocusComponent.ts | 22 + .../app/ipsection/IpsectionTestComponent.ts | 43 + .../src/app/ipsection/IpsectionTestModule.ts | 68 + .../app/ipsection/IpsectionValidComponent.ts | 31 + .../IpsectionValidFormgroupComponent.ts | 37 + .../src/app/ipsection/ipsection-basic.html | 11 + .../src/app/ipsection/ipsection-disabled.html | 5 + .../src/app/ipsection/ipsection-events.html | 13 + .../src/app/ipsection/ipsection-focus.html | 12 + .../src/app/ipsection/ipsection-test.html | 12 + .../ipsection/ipsection-valid-formgroup.html | 6 + .../src/app/ipsection/ipsection-valid.html | 4 + .../app/ipsection/webdoc/ipsection-demos.js | 66 + .../src/app/ipsection/webdoc/ipsection.cn.md | 28 + .../src/app/ipsection/webdoc/ipsection.en.md | 29 + src/ipsection/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/ipsection/demo/src/index.html | 16 + src/ipsection/demo/src/main.ts | 13 + src/ipsection/demo/test.ts | 24 + src/ipsection/demo/tsconfig.app.json | 15 + src/ipsection/demo/tsconfig.spec.json | 20 + src/ipsection/lib/index.ts | 1 + src/ipsection/lib/ng-package.json | 7 + src/ipsection/lib/package.json | 14 + src/ipsection/lib/project.json | 62 + src/ipsection/lib/src/TiIpsectionComponent.ts | 267 + src/ipsection/lib/src/TiIpsectionModule.ts | 26 + src/ipsection/lib/src/ipsection.html | 35 + src/ipsection/lib/src/ipsection.less | 52 + src/labeleditor/demo/project.json | 90 + src/labeleditor/demo/src/app/AppComponent.ts | 7 + src/labeleditor/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/labeleditor/demo/src/app/app.html | 3 + .../LabeleditorAutotipComponent.ts | 18 + .../labeleditor/LabeleditorBasicComponent.ts | 17 + .../LabeleditorDisabledComponent.ts | 19 + .../labeleditor/LabeleditorEventsComponent.ts | 26 + .../LabeleditorIcontipcontextComponent.ts | 49 + .../LabeleditorMaxlengthComponent.ts | 19 + .../LabeleditorMaxlineComponent.ts | 17 + .../LabeleditorMultilineSizeComponent.ts | 17 + .../labeleditor/LabeleditorResizeComponent.ts | 17 + .../app/labeleditor/LabeleditorTestModule.ts | 70 + .../LabeleditorValidationAsyncComponent.ts | 56 + .../LabeleditorValidationComponent.ts | 25 + .../app/labeleditor/labeleditor-autotip.html | 2 + .../app/labeleditor/labeleditor-basic.html | 5 + .../app/labeleditor/labeleditor-disabled.html | 2 + .../app/labeleditor/labeleditor-events.html | 10 + .../labeleditor-icontipcontext.html | 25 + .../labeleditor/labeleditor-maxlength.html | 11 + .../app/labeleditor/labeleditor-maxline.html | 1 + .../labeleditor-multiline-size.html | 9 + .../app/labeleditor/labeleditor-resize.html | 24 + .../labeleditor-validation-async.html | 4 + .../labeleditor/labeleditor-validation.html | 17 + .../labeleditor/webdoc/labeleditor-demos.js | 150 + .../app/labeleditor/webdoc/labeleditor.cn.md | 15 + .../app/labeleditor/webdoc/labeleditor.en.md | 13 + src/labeleditor/demo/src/favicon.ico | Bin 0 -> 300852 bytes src/labeleditor/demo/src/index.html | 16 + src/labeleditor/demo/src/main.ts | 13 + src/labeleditor/demo/tsconfig.app.json | 10 + src/labeleditor/lib/index.ts | 2 + src/labeleditor/lib/ng-package.json | 7 + src/labeleditor/lib/package.json | 18 + src/labeleditor/lib/project.json | 62 + .../lib/src/TiLabeleditorComponent.ts | 259 + .../lib/src/TiLabeleditorModule.ts | 42 + src/labeleditor/lib/src/labeleditor.html | 103 + src/labeleditor/lib/src/labeleditor.less | 65 + src/layout/demo/karma.conf.js | 44 + src/layout/demo/project.json | 90 + src/layout/demo/src/app/AppComponent.ts | 7 + src/layout/demo/src/app/AppModule.ts | 24 + src/layout/demo/src/app/IndexComponent.ts | 49 + src/layout/demo/src/app/app.html | 3 + .../src/app/layout/LayoutBasicComponent.ts | 37 + .../app/layout/LayoutBasicSimpleComponent.ts | 7 + .../LayoutBasicSimpleResponsiveComponent.ts | 7 + .../app/layout/LayoutDetailColumnComponent.ts | 61 + .../src/app/layout/LayoutDetailComponent.ts | 69 + .../src/app/layout/LayoutListComponent.ts | 157 + .../layout/LayoutListLargedataComponent.ts | 153 + .../app/layout/LayoutMultiColumnComponent.ts | 7 + .../src/app/layout/LayoutOverviewComponent.ts | 49 + .../layout/LayoutOverviewVerticalComponent.ts | 49 + .../src/app/layout/LayoutPurchaseComponent.ts | 52 + ...LayoutPurchaseResponsiveChangeComponent.ts | 56 + .../LayoutPurchaseResponsiveComponent.ts | 51 + .../layout/LayoutPurchaseSimpleComponent.ts | 14 + ...LayoutPurchaseSimpleResponsiveComponent.ts | 14 + .../src/app/layout/LayoutSingleComponent.ts | 7 + .../demo/src/app/layout/LayoutTestModule.ts | 160 + .../layout-basic-simple-responsive.html | 14 + .../src/app/layout/layout-basic-simple.html | 14 + .../demo/src/app/layout/layout-basic.html | 31 + .../demo/src/app/layout/layout-basic.less | 30 + .../demo/src/app/layout/layout-column.less | 18 + .../src/app/layout/layout-detail-column.html | 67 + .../demo/src/app/layout/layout-detail.html | 56 + .../src/app/layout/layout-list-largedata.html | 57 + .../demo/src/app/layout/layout-list.html | 56 + .../src/app/layout/layout-multi-column.html | 21 + .../app/layout/layout-overview-vertical.html | 36 + .../demo/src/app/layout/layout-overview.html | 38 + .../layout-purchase-responsive-change.html | 41 + .../layout/layout-purchase-responsive.html | 40 + .../layout-purchase-simple-responsive.html | 26 + .../app/layout/layout-purchase-simple.html | 21 + .../demo/src/app/layout/layout-purchase.html | 34 + .../demo/src/app/layout/layout-simple.less | 14 + .../demo/src/app/layout/layout-single.html | 14 + src/layout/demo/src/app/layout/layout.less | 21 + .../src/app/layout/webdoc/layout-demos.js | 90 + .../demo/src/app/layout/webdoc/layout.cn.md | 23 + .../demo/src/app/layout/webdoc/layout.en.md | 29 + src/layout/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/layout/demo/src/index.html | 16 + src/layout/demo/src/main.ts | 13 + src/layout/demo/test.ts | 24 + src/layout/demo/tsconfig.app.json | 15 + src/layout/demo/tsconfig.spec.json | 20 + src/layout/lib/index.ts | 1 + src/layout/lib/ng-package.json | 7 + src/layout/lib/package.json | 9 + src/layout/lib/project.json | 62 + src/layout/lib/src/TiLayoutColumnComponent.ts | 26 + .../lib/src/TiLayoutContentBodyComponent.ts | 26 + .../lib/src/TiLayoutContentComponent.ts | 37 + .../lib/src/TiLayoutContentHeaderComponent.ts | 26 + src/layout/lib/src/TiLayoutHeaderComponent.ts | 37 + src/layout/lib/src/TiLayoutModule.ts | 46 + .../lib/src/TiLayoutSectionComponent.ts | 29 + src/layout/lib/src/layout-column.less | 11 + src/layout/lib/src/layout-content-body.less | 39 + src/layout/lib/src/layout-content-header.less | 5 + src/layout/lib/src/layout-content.less | 68 + src/layout/lib/src/layout-header.less | 31 + src/layout/lib/src/layout-section.less | 30 + src/layout/lib/src/layout.less | 4 + src/leftmenu/demo/karma.conf.js | 44 + src/leftmenu/demo/project.json | 90 + src/leftmenu/demo/src/app/AppComponent.ts | 7 + src/leftmenu/demo/src/app/AppModule.ts | 24 + src/leftmenu/demo/src/app/IndexComponent.ts | 49 + src/leftmenu/demo/src/app/app.html | 3 + .../leftmenu/LeftmenuActiveChangeComponent.ts | 41 + .../app/leftmenu/LeftmenuBasicComponent.ts | 36 + .../LeftmenuCandeactivateComponent.ts | 32 + .../leftmenu/LeftmenuCollapsedComponent.ts | 37 + .../app/leftmenu/LeftmenuDisabledComponent.ts | 38 + .../app/leftmenu/LeftmenuDividingComponent.ts | 38 + .../src/app/leftmenu/LeftmenuFootComponent.ts | 37 + .../app/leftmenu/LeftmenuGroupComponent.ts | 49 + .../src/app/leftmenu/LeftmenuHrefComponent.ts | 36 + .../src/app/leftmenu/LeftmenuIdComponent.ts | 65 + .../app/leftmenu/LeftmenuNoRouterComponent.ts | 34 + .../app/leftmenu/LeftmenuParamsComponent.ts | 43 + .../leftmenu/LeftmenuReloadStateComponent.ts | 35 + .../leftmenu/LeftmenuRouterlistComponent.ts | 38 + .../app/leftmenu/LeftmenuScrollComponent.ts | 63 + .../app/leftmenu/LeftmenuSecurityComponent.ts | 57 + .../src/app/leftmenu/LeftmenuTestModule.ts | 277 + .../leftmenu/LeftmenuToggleableComponent.ts | 32 + .../demo/src/app/leftmenu/OpenerComponent.ts | 32 + .../src/app/leftmenu/Router11Component.ts | 7 + .../src/app/leftmenu/Router12Component.ts | 25 + .../demo/src/app/leftmenu/Router2Component.ts | 24 + .../src/app/leftmenu/Router31Component.ts | 26 + .../src/app/leftmenu/Router32Component.ts | 28 + .../demo/src/app/leftmenu/RouterAComponent.ts | 12 + .../demo/src/app/leftmenu/RouterBComponent.ts | 16 + .../demo/src/app/leftmenu/RouterCComponent.ts | 12 + .../demo/src/app/leftmenu/RouterDComponent.ts | 12 + .../src/app/leftmenu/RouterparamsComponent.ts | 27 + .../app/leftmenu/leftmenu-active-change.html | 21 + .../demo/src/app/leftmenu/leftmenu-basic.html | 13 + .../app/leftmenu/leftmenu-candeactivate.html | 13 + .../src/app/leftmenu/leftmenu-collapsed.html | 19 + .../src/app/leftmenu/leftmenu-disabled.html | 13 + .../src/app/leftmenu/leftmenu-dividing.html | 13 + .../demo/src/app/leftmenu/leftmenu-foot.html | 19 + .../demo/src/app/leftmenu/leftmenu-group.html | 29 + .../demo/src/app/leftmenu/leftmenu-href.html | 13 + .../demo/src/app/leftmenu/leftmenu-id.html | 29 + .../src/app/leftmenu/leftmenu-no-router.html | 23 + .../src/app/leftmenu/leftmenu-params.html | 13 + .../app/leftmenu/leftmenu-reload-state.html | 13 + .../src/app/leftmenu/leftmenu-routerlist.html | 13 + .../src/app/leftmenu/leftmenu-scroll.html | 26 + .../src/app/leftmenu/leftmenu-security.html | 25 + .../src/app/leftmenu/leftmenu-toggleable.html | 13 + .../demo/src/app/leftmenu/leftmenuTest.less | 9 + src/leftmenu/demo/src/app/leftmenu/unsave.ts | 8 + .../src/app/leftmenu/webdoc/leftmenu-demos.js | 163 + .../src/app/leftmenu/webdoc/leftmenu.cn.md | 36 + .../src/app/leftmenu/webdoc/leftmenu.en.md | 29 + ...eftmenuActiveChangeWebsiteViewComponent.ts | 39 + .../LeftmenuBasicWebsiteViewComponent.ts | 34 + .../LeftmenuCollapsedWebsiteViewComponent.ts | 35 + .../LeftmenuDisabledWebsiteViewComponent.ts | 36 + .../LeftmenuDividingWebsiteViewComponent.ts | 35 + .../LeftmenuFootWebsiteViewComponent.ts | 34 + .../LeftmenuGroupWebsiteViewComponent.ts | 46 + .../LeftmenuHrefWebsiteViewComponent.ts | 36 + .../LeftmenuNoRouterWebsiteViewComponent.ts | 34 + .../LeftmenuParamsWebsiteViewComponent.ts | 34 + ...LeftmenuReloadStateWebsiteViewComponent.ts | 33 + .../LeftmenuRouterlistWebsiteViewComponent.ts | 42 + .../LeftmenuToggleableWebsiteViewComponent.ts | 30 + .../leftmenu-active-change-website-view.html | 27 + .../leftmenu-basic-website-view.html | 25 + .../leftmenu-collapsed-website-view.html | 26 + .../leftmenu-disabled-website-view.html | 25 + .../leftmenu-dividing-website-view.html | 26 + .../leftmenu-foot-website-view.html | 29 + .../leftmenu-group-website-view.html | 40 + .../leftmenu-href-website-view.html | 23 + .../leftmenu-no-router-website-view.html | 28 + .../leftmenu-params-website-view.html | 27 + .../leftmenu-reload-state-website-view.html | 19 + .../leftmenu-routerlist-website-view.html | 27 + .../leftmenu-toggleable-website-view.html | 19 + src/leftmenu/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/leftmenu/demo/src/index.html | 16 + src/leftmenu/demo/src/main.ts | 13 + src/leftmenu/demo/test.ts | 24 + src/leftmenu/demo/tsconfig.app.json | 15 + src/leftmenu/demo/tsconfig.spec.json | 20 + src/leftmenu/lib/index.ts | 1 + src/leftmenu/lib/ng-package.json | 7 + src/leftmenu/lib/package.json | 16 + src/leftmenu/lib/project.json | 62 + src/leftmenu/lib/src/TiLeftmenuComponent.ts | 513 ++ .../lib/src/TiLeftmenuFootComponent.ts | 40 + .../lib/src/TiLeftmenuGroupComponent.ts | 66 + .../lib/src/TiLeftmenuGroupItemComponent.ts | 23 + .../lib/src/TiLeftmenuHeadComponent.ts | 24 + .../lib/src/TiLeftmenuItemComponent.ts | 32 + .../lib/src/TiLeftmenuLevel1Component.ts | 261 + .../lib/src/TiLeftmenuLevel2Component.ts | 158 + src/leftmenu/lib/src/TiLeftmenuModule.ts | 58 + src/leftmenu/lib/src/leftmenu-foot.html | 1 + src/leftmenu/lib/src/leftmenu-group-item.html | 1 + src/leftmenu/lib/src/leftmenu-group.html | 4 + src/leftmenu/lib/src/leftmenu-head.html | 1 + src/leftmenu/lib/src/leftmenu-item.html | 1 + src/leftmenu/lib/src/leftmenu-level1.html | 28 + src/leftmenu/lib/src/leftmenu-level2.html | 3 + src/leftmenu/lib/src/leftmenu.html | 17 + src/leftmenu/lib/src/leftmenu.less | 340 ++ src/linkbutton/demo/karma.conf.js | 41 + src/linkbutton/demo/project.json | 90 + src/linkbutton/demo/src/app/AppComponent.ts | 7 + src/linkbutton/demo/src/app/AppModule.ts | 26 + src/linkbutton/demo/src/app/IndexComponent.ts | 49 + src/linkbutton/demo/src/app/app.html | 3 + .../linkbutton/LinkbuttonBasicComponent.ts | 8 + .../app/linkbutton/LinkbuttonTestModule.ts | 13 + .../src/app/linkbutton/linkbutton-basic.html | 1 + .../app/linkbutton/webdoc/linkbutton-demos.js | 25 + .../app/linkbutton/webdoc/linkbutton.cn.md | 24 + .../app/linkbutton/webdoc/linkbutton.en.md | 29 + src/linkbutton/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/linkbutton/demo/src/index.html | 16 + src/linkbutton/demo/src/main.ts | 13 + src/linkbutton/demo/test.ts | 24 + src/linkbutton/demo/tsconfig.app.json | 10 + src/linkbutton/demo/tsconfig.spec.json | 15 + src/linkbutton/lib/index.ts | 1 + src/linkbutton/lib/ng-package.json | 7 + src/linkbutton/lib/package.json | 11 + src/linkbutton/lib/project.json | 62 + .../lib/src/TiLinkbuttonComponent.ts | 30 + src/linkbutton/lib/src/TiLinkbuttonModule.ts | 24 + src/linkbutton/lib/src/linkbutton.html | 4 + src/linkbutton/lib/src/linkbutton.less | 33 + src/list/demo/project.json | 78 + src/list/demo/src/app/AppComponent.ts | 7 + src/list/demo/src/app/AppModule.ts | 24 + src/list/demo/src/app/IndexComponent.ts | 49 + src/list/demo/src/app/app.html | 3 + .../demo/src/app/list/ListDefaultComponent.ts | 32 + .../demo/src/app/list/ListGroupComponent.ts | 92 + .../demo/src/app/list/ListMultiComponent.ts | 32 + .../src/app/list/ListSelectallComponent.ts | 181 + src/list/demo/src/app/list/ListTestModule.ts | 27 + .../demo/src/app/list/ListTipComponent.ts | 21 + src/list/demo/src/app/list/list-default.html | 46 + src/list/demo/src/app/list/list-group.html | 35 + src/list/demo/src/app/list/list-multi.html | 64 + .../demo/src/app/list/list-selectall.html | 40 + src/list/demo/src/app/list/list-tip.html | 41 + src/list/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/list/demo/src/index.html | 16 + src/list/demo/src/main.ts | 13 + src/list/demo/tsconfig.app.json | 15 + src/list/lib/index.ts | 1 + src/list/lib/ng-package.json | 7 + src/list/lib/package.json | 16 + src/list/lib/project.json | 62 + src/list/lib/src/TiListComponent.ts | 764 +++ src/list/lib/src/TiListModule.ts | 34 + src/list/lib/src/i18n/TiListWords.ts | 6 + src/list/lib/src/i18n/en_US.ts | 8 + src/list/lib/src/i18n/es_US.ts | 8 + src/list/lib/src/i18n/fr_FR.ts | 8 + src/list/lib/src/i18n/index.ts | 7 + src/list/lib/src/i18n/pt_BR.ts | 8 + src/list/lib/src/i18n/zh_CN.ts | 8 + src/list/lib/src/list-multi.less | 53 + src/list/lib/src/list.html | 108 + src/list/lib/src/list.less | 149 + src/loading/demo/karma.conf.js | 44 + src/loading/demo/project.json | 90 + src/loading/demo/src/app/AppComponent.ts | 7 + src/loading/demo/src/app/AppModule.ts | 24 + src/loading/demo/src/app/IndexComponent.ts | 49 + src/loading/demo/src/app/app.html | 3 + .../src/app/loading/LoadingAreaComponent.ts | 31 + .../src/app/loading/LoadingBasicComponent.ts | 6 + .../src/app/loading/LoadingSizeComponent.ts | 6 + .../demo/src/app/loading/LoadingTestModule.ts | 36 + .../src/app/loading/LoadingTypeComponent.ts | 6 + .../demo/src/app/loading/loading-area.html | 9 + .../demo/src/app/loading/loading-basic.html | 1 + .../demo/src/app/loading/loading-size.html | 6 + .../demo/src/app/loading/loading-type.html | 1 + .../src/app/loading/webdoc/loading-demos.js | 54 + .../demo/src/app/loading/webdoc/loading.cn.md | 22 + .../demo/src/app/loading/webdoc/loading.en.md | 29 + src/loading/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/loading/demo/src/index.html | 16 + src/loading/demo/src/main.ts | 13 + src/loading/demo/test.ts | 24 + src/loading/demo/tsconfig.app.json | 15 + src/loading/demo/tsconfig.spec.json | 20 + src/loading/lib/index.ts | 1 + src/loading/lib/ng-package.json | 7 + src/loading/lib/package.json | 12 + src/loading/lib/project.json | 62 + src/loading/lib/src/TiLoadingComponent.ts | 68 + src/loading/lib/src/TiLoadingModule.ts | 32 + src/loading/lib/src/TiLoadingfailComponent.ts | 46 + src/loading/lib/src/i18n/TiLoadingWords.ts | 6 + src/loading/lib/src/i18n/en_US.ts | 8 + src/loading/lib/src/i18n/es_US.ts | 8 + src/loading/lib/src/i18n/fr_FR.ts | 8 + src/loading/lib/src/i18n/index.ts | 7 + src/loading/lib/src/i18n/pt_BR.ts | 8 + src/loading/lib/src/i18n/zh_CN.ts | 8 + src/loading/lib/src/loading.html | 20 + src/loading/lib/src/loading.less | 116 + src/loading/lib/src/loadingfail.html | 12 + src/loading/lib/src/loadingfail.less | 36 + src/locale/demo/karma.conf.js | 44 + src/locale/demo/project.json | 90 + src/locale/demo/src/app/AppComponent.ts | 7 + src/locale/demo/src/app/AppModule.ts | 24 + src/locale/demo/src/app/IndexComponent.ts | 49 + src/locale/demo/src/app/app.html | 3 + .../src/app/locale/LocaleBasicComponent.ts | 61 + .../src/app/locale/LocaleFormatComponent.ts | 12 + .../src/app/locale/LocaleReloadComponent.ts | 70 + .../demo/src/app/locale/LocaleTestModule.ts | 35 + .../demo/src/app/locale/locale-basic.html | 7 + .../demo/src/app/locale/locale-format.html | 5 + .../demo/src/app/locale/locale-reload.html | 6 + .../src/app/locale/webdoc/locale-demos.js | 40 + .../demo/src/app/locale/webdoc/locale.cn.md | 28 + .../demo/src/app/locale/webdoc/locale.en.md | 29 + src/locale/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/locale/demo/src/index.html | 16 + src/locale/demo/src/main.ts | 13 + src/locale/demo/test.ts | 24 + src/locale/demo/tsconfig.app.json | 15 + src/locale/demo/tsconfig.spec.json | 20 + src/locale/lib/index.ts | 1 + src/locale/lib/ng-package.json | 7 + src/locale/lib/package.json | 10 + src/locale/lib/project.json | 62 + src/locale/lib/src/TiLocale.ts | 141 + src/locale/lib/src/TiLocaleFormat.ts | 162 + src/locale/lib/src/TiLocaleModule.ts | 24 + src/locale/lib/src/TiTranslatePipe.ts | 25 + src/menu/demo/karma.conf.js | 44 + src/menu/demo/project.json | 90 + src/menu/demo/src/app/AppComponent.ts | 7 + src/menu/demo/src/app/AppModule.ts | 24 + src/menu/demo/src/app/IndexComponent.ts | 49 + src/menu/demo/src/app/app.html | 3 + .../demo/src/app/menu/MenuBasicComponent.ts | 40 + .../src/app/menu/MenuBeforeopenComponent.ts | 46 + .../demo/src/app/menu/MenuBorderComponent.ts | 55 + .../src/app/menu/MenuButtoncolorComponent.ts | 39 + .../demo/src/app/menu/MenuDefaultComponent.ts | 78 + .../src/app/menu/MenuDisabledComponent.ts | 42 + .../demo/src/app/menu/MenuEventComponent.ts | 45 + .../demo/src/app/menu/MenuGroupComponent.ts | 42 + src/menu/demo/src/app/menu/MenuIdComponent.ts | 76 + .../src/app/menu/MenuLabelkeyComponent.ts | 40 + .../src/app/menu/MenuPanelalignComponent.ts | 40 + .../src/app/menu/MenuPanelstyleComponent.ts | 40 + .../src/app/menu/MenuTempleteComponent.ts | 26 + .../src/app/menu/MenuTempleteTestComponent.ts | 74 + src/menu/demo/src/app/menu/MenuTestModule.ts | 100 + .../demo/src/app/menu/MenuTipsComponent.ts | 45 + src/menu/demo/src/app/menu/menu-basic.html | 2 + .../demo/src/app/menu/menu-beforeopen.html | 1 + src/menu/demo/src/app/menu/menu-border.html | 10 + .../demo/src/app/menu/menu-buttoncolor.html | 1 + src/menu/demo/src/app/menu/menu-default.html | 38 + src/menu/demo/src/app/menu/menu-disabled.html | 3 + src/menu/demo/src/app/menu/menu-event.html | 3 + src/menu/demo/src/app/menu/menu-group.html | 1 + src/menu/demo/src/app/menu/menu-id.html | 13 + src/menu/demo/src/app/menu/menu-labelkey.html | 1 + .../demo/src/app/menu/menu-panelalign.html | 1 + .../demo/src/app/menu/menu-panelstyle.html | 1 + .../demo/src/app/menu/menu-templete-test.html | 14 + src/menu/demo/src/app/menu/menu-templete.html | 11 + src/menu/demo/src/app/menu/menu-tips.html | 1 + .../demo/src/app/menu/webdoc/menu-demos.js | 166 + src/menu/demo/src/app/menu/webdoc/menu.cn.md | 24 + src/menu/demo/src/app/menu/webdoc/menu.en.md | 29 + src/menu/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/menu/demo/src/index.html | 16 + src/menu/demo/src/main.ts | 13 + src/menu/demo/test.ts | 24 + src/menu/demo/tsconfig.app.json | 15 + src/menu/demo/tsconfig.spec.json | 20 + src/menu/lib/index.ts | 1 + src/menu/lib/ng-package.json | 7 + src/menu/lib/package.json | 14 + src/menu/lib/project.json | 62 + src/menu/lib/src/TiMenuComponent.ts | 395 ++ src/menu/lib/src/TiMenuItem.ts | 49 + src/menu/lib/src/TiMenuListComponent.ts | 53 + src/menu/lib/src/TiMenuModule.ts | 27 + src/menu/lib/src/menu.html | 56 + src/menu/lib/src/menu.less | 115 + src/menu/lib/src/menulist.html | 39 + src/menu/lib/src/menulist.less | 103 + src/message/demo/karma.conf.js | 44 + src/message/demo/project.json | 90 + src/message/demo/src/app/AppComponent.ts | 7 + src/message/demo/src/app/AppModule.ts | 26 + src/message/demo/src/app/IndexComponent.ts | 49 + src/message/demo/src/app/app.html | 3 + .../src/app/message/MessageBasicComponent.ts | 20 + .../src/app/message/MessageBtnComponent.ts | 51 + .../app/message/MessageBtnTestComponent.ts | 66 + .../app/message/MessageContentComponent.ts | 39 + .../src/app/message/MessageIdComponent.ts | 21 + .../app/message/MessageSecurityComponent.ts | 24 + .../demo/src/app/message/MessageTestModule.ts | 79 + .../src/app/message/MessageTitleComponent.ts | 15 + .../src/app/message/MessageTypeComponent.ts | 32 + .../demo/src/app/message/message-basic.html | 6 + .../src/app/message/message-btn-test.html | 6 + .../demo/src/app/message/message-btn.html | 1 + .../demo/src/app/message/message-content.html | 6 + .../demo/src/app/message/message-id.html | 4 + .../src/app/message/message-security.html | 10 + .../demo/src/app/message/message-title.html | 1 + .../demo/src/app/message/message-type.html | 4 + .../src/app/message/webdoc/message-demos.js | 68 + .../demo/src/app/message/webdoc/message.cn.md | 34 + .../demo/src/app/message/webdoc/message.en.md | 29 + src/message/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/message/demo/src/index.html | 16 + src/message/demo/src/main.ts | 13 + src/message/demo/test.ts | 24 + src/message/demo/tsconfig.app.json | 15 + src/message/demo/tsconfig.spec.json | 20 + src/message/lib/index.ts | 2 + src/message/lib/ng-package.json | 7 + src/message/lib/package.json | 15 + src/message/lib/project.json | 62 + .../lib/src/TiContentWrapperComponent.ts | 21 + src/message/lib/src/TiMessageComponent.html | 39 + src/message/lib/src/TiMessageComponent.ts | 138 + src/message/lib/src/TiMessageInterface.ts | 89 + src/message/lib/src/TiMessageModule.ts | 37 + src/message/lib/src/TiMessageService.ts | 78 + src/message/lib/src/TiTranscludeDirective.ts | 40 + src/message/lib/src/i18n/TiMessageWords.ts | 10 + src/message/lib/src/i18n/en_US.ts | 12 + src/message/lib/src/i18n/es_US.ts | 12 + src/message/lib/src/i18n/fr_FR.ts | 12 + src/message/lib/src/i18n/index.ts | 7 + src/message/lib/src/i18n/pt_BR.ts | 12 + src/message/lib/src/i18n/zh_CN.ts | 12 + src/message/lib/src/message.less | 53 + src/modal/demo/karma.conf.js | 44 + src/modal/demo/project.json | 90 + src/modal/demo/src/app/AppComponent.ts | 7 + src/modal/demo/src/app/AppModule.ts | 26 + src/modal/demo/src/app/IndexComponent.ts | 49 + src/modal/demo/src/app/app.html | 3 + .../src/app/modal/ModalAnimationComponent.ts | 15 + .../src/app/modal/ModalBackdropComponent.ts | 15 + .../demo/src/app/modal/ModalClassComponent.ts | 17 + .../src/app/modal/ModalCloseIconComponent.ts | 15 + .../src/app/modal/ModalConfigTestComponent.ts | 57 + .../app/modal/ModalContentCompComponent.ts | 58 + .../app/modal/ModalContentTempComponent.ts | 15 + .../src/app/modal/ModalDraggableComponent.ts | 15 + .../demo/src/app/modal/ModalEscComponent.ts | 15 + .../demo/src/app/modal/ModalEventComponent.ts | 25 + .../app/modal/ModalHeaderAlignComponent.ts | 15 + .../app/modal/ModalHeaderStyleComponent.ts | 16 + .../demo/src/app/modal/ModalTestModule.ts | 133 + .../app/modal/ModalTwoBackdropComponent.ts | 25 + .../src/app/modal/ModalTwoTestComponent.ts | 78 + src/modal/demo/src/app/modal/TestComponent.ts | 72 + .../demo/src/app/modal/modal-animation.html | 11 + .../demo/src/app/modal/modal-backdrop.html | 11 + src/modal/demo/src/app/modal/modal-class.html | 11 + .../demo/src/app/modal/modal-close-icon.html | 11 + .../demo/src/app/modal/modal-config-test.html | 24 + .../src/app/modal/modal-content-comp.html | 2 + .../src/app/modal/modal-content-temp.html | 11 + .../demo/src/app/modal/modal-draggable.html | 11 + src/modal/demo/src/app/modal/modal-esc.html | 11 + src/modal/demo/src/app/modal/modal-event.html | 11 + .../src/app/modal/modal-header-align.html | 11 + .../src/app/modal/modal-header-style.html | 22 + .../src/app/modal/modal-two-backdrop.html | 6 + .../demo/src/app/modal/modal-two-test.html | 7 + .../demo/src/app/modal/webdoc/modal-demos.js | 138 + .../demo/src/app/modal/webdoc/modal.cn.md | 34 + .../demo/src/app/modal/webdoc/modal.en.md | 29 + src/modal/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/modal/demo/src/index.html | 16 + src/modal/demo/src/main.ts | 13 + src/modal/demo/test.ts | 24 + src/modal/demo/tsconfig.app.json | 15 + src/modal/demo/tsconfig.spec.json | 20 + src/modal/lib/index.ts | 2 + src/modal/lib/ng-package.json | 7 + src/modal/lib/package.json | 18 + src/modal/lib/project.json | 62 + src/modal/lib/src/TiBackdropComponent.ts | 85 + .../lib/src/TiBackdropNoAnimationComponent.ts | 28 + src/modal/lib/src/TiModalBodyComponent.ts | 24 + src/modal/lib/src/TiModalComponent.html | 16 + src/modal/lib/src/TiModalComponent.ts | 351 ++ .../lib/src/TiModalComponentNoAnimation.html | 15 + src/modal/lib/src/TiModalFooterComponent.ts | 24 + src/modal/lib/src/TiModalHeaderComponent.ts | 24 + src/modal/lib/src/TiModalInterface.ts | 134 + src/modal/lib/src/TiModalModule.ts | 56 + .../lib/src/TiModalNoAnimationComponent.ts | 29 + src/modal/lib/src/TiModalService.ts | 244 + src/modal/lib/src/backdrop.less | 17 + src/modal/lib/src/modal.less | 114 + src/nav/demo/karma.conf.js | 44 + src/nav/demo/project.json | 98 + src/nav/demo/src/app/AppComponent.ts | 7 + src/nav/demo/src/app/AppModule.ts | 24 + src/nav/demo/src/app/IndexComponent.ts | 49 + src/nav/demo/src/app/app.html | 3 + .../demo/src/app/nav/NavActiveComponent.ts | 84 + src/nav/demo/src/app/nav/NavAlignComponent.ts | 14 + src/nav/demo/src/app/nav/NavBasicComponent.ts | 15 + .../demo/src/app/nav/NavDisabledComponent.ts | 85 + src/nav/demo/src/app/nav/NavEventComponent.ts | 49 + src/nav/demo/src/app/nav/NavLeftComponent.ts | 16 + src/nav/demo/src/app/nav/NavRightComponent.ts | 14 + .../src/app/nav/NavSelectableComponent.ts | 91 + .../demo/src/app/nav/NavSubmenuComponent.ts | 45 + .../demo/src/app/nav/NavTemplateComponent.ts | 49 + src/nav/demo/src/app/nav/NavTestModule.ts | 100 + src/nav/demo/src/app/nav/NavThemeComponent.ts | 14 + src/nav/demo/src/app/nav/NavWidthComponent.ts | 14 + src/nav/demo/src/app/nav/nav-active.html | 19 + src/nav/demo/src/app/nav/nav-align.html | 23 + src/nav/demo/src/app/nav/nav-basic.html | 9 + src/nav/demo/src/app/nav/nav-disabled.html | 19 + src/nav/demo/src/app/nav/nav-event.html | 16 + src/nav/demo/src/app/nav/nav-left.html | 15 + src/nav/demo/src/app/nav/nav-right.html | 16 + src/nav/demo/src/app/nav/nav-selectable.html | 19 + src/nav/demo/src/app/nav/nav-submenu.html | 33 + src/nav/demo/src/app/nav/nav-template.html | 17 + src/nav/demo/src/app/nav/nav-theme.html | 9 + src/nav/demo/src/app/nav/nav-width.html | 4 + src/nav/demo/src/app/nav/webdoc/nav-demos.js | 149 + src/nav/demo/src/app/nav/webdoc/nav.cn.md | 25 + src/nav/demo/src/app/nav/webdoc/nav.en.md | 7 + src/nav/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/nav/demo/src/index.html | 16 + src/nav/demo/src/main.ts | 13 + src/nav/demo/test.ts | 24 + src/nav/demo/tsconfig.app.json | 15 + src/nav/demo/tsconfig.spec.json | 20 + src/nav/lib/index.ts | 1 + src/nav/lib/ng-package.json | 7 + src/nav/lib/package.json | 9 + src/nav/lib/project.json | 62 + src/nav/lib/src/TiNavComponent.ts | 35 + src/nav/lib/src/TiNavLeftComponent.ts | 28 + src/nav/lib/src/TiNavMenuComponent.ts | 257 + src/nav/lib/src/TiNavModule.ts | 30 + src/nav/lib/src/TiNavRightComponent.ts | 25 + src/nav/lib/src/common.less | 18 + src/nav/lib/src/interface.ts | 46 + src/nav/lib/src/nav.html | 1 + src/nav/lib/src/nav.less | 16 + src/nav/lib/src/navleft.html | 1 + src/nav/lib/src/navleft.less | 15 + src/nav/lib/src/navmenu.html | 72 + src/nav/lib/src/navmenu.less | 186 + src/nav/lib/src/navright.html | 1 + src/nav/lib/src/navright.less | 11 + src/ng/demo/karma.conf.js | 44 + src/ng/demo/project.json | 120 + src/ng/demo/src/app/AppComponent.ts | 32 + src/ng/demo/src/app/AppModule.ts | 32 + src/ng/demo/src/app/AppWcModule.ts | 5394 +++++++++++++++++ src/ng/demo/src/app/DemoModules.ts | 204 + src/ng/demo/src/app/IndexComponent.ts | 58 + src/ng/demo/src/app/app.html | 3 + src/ng/demo/src/assets/browser/chrome.PNG | Bin 0 -> 1994 bytes src/ng/demo/src/assets/browser/edge.PNG | Bin 0 -> 2067 bytes src/ng/demo/src/assets/browser/firefox.PNG | Bin 0 -> 2185 bytes src/ng/demo/src/assets/browser/safari.PNG | Bin 0 -> 2402 bytes src/ng/demo/src/assets/food/cake.png | Bin 0 -> 4044 bytes src/ng/demo/src/assets/food/coffee.png | Bin 0 -> 4799 bytes src/ng/demo/src/assets/food/cola.png | Bin 0 -> 4115 bytes src/ng/demo/src/assets/food/fried_chicken.png | Bin 0 -> 6156 bytes src/ng/demo/src/assets/food/fries.png | Bin 0 -> 6098 bytes src/ng/demo/src/assets/food/hamburger.png | Bin 0 -> 7032 bytes src/ng/demo/src/assets/food/milk.png | Bin 0 -> 4271 bytes src/ng/demo/src/assets/food/pizza.png | Bin 0 -> 6149 bytes src/ng/demo/src/assets/image/1.jpg | Bin 0 -> 107192 bytes src/ng/demo/src/assets/image/2.jpg | Bin 0 -> 46016 bytes src/ng/demo/src/assets/image/3.jpg | Bin 0 -> 100875 bytes src/ng/demo/src/assets/nav_logo/logo.png | Bin 0 -> 6847 bytes src/ng/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/ng/demo/src/index.html | 16 + src/ng/demo/src/main.ts | 49 + src/ng/demo/src/webdoc/LICENSE | 103 + src/ng/demo/src/webdoc/faq-en.md | 0 src/ng/demo/src/webdoc/faq.md | 79 + src/ng/demo/src/webdoc/getstart-en.md | 0 src/ng/demo/src/webdoc/getstart.md | 89 + src/ng/demo/src/webdoc/images/basecolor1.png | Bin 0 -> 28643 bytes src/ng/demo/src/webdoc/images/demo.png | Bin 0 -> 2042 bytes src/ng/demo/src/webdoc/images/theme.png | Bin 0 -> 18370 bytes src/ng/demo/src/webdoc/introduce-en.md | 0 src/ng/demo/src/webdoc/introduce.md | 34 + src/ng/demo/src/webdoc/joinus-en.md | 0 src/ng/demo/src/webdoc/joinus.md | 156 + src/ng/demo/src/webdoc/language-en.md | 0 src/ng/demo/src/webdoc/language.md | 209 + src/ng/demo/src/webdoc/menus.js | 206 + src/ng/demo/src/webdoc/themedoc-en.md | 0 src/ng/demo/src/webdoc/themedoc.md | 227 + src/ng/demo/src/webdoc/validators.md | 42 + src/ng/demo/test.ts | 24 + src/ng/demo/tsconfig.app.json | 10 + src/ng/demo/tsconfig.spec.json | 20 + src/ng/demolog/DemoLogComponent.ts | 22 + src/ng/demolog/DemoLogModule.ts | 10 + src/ng/demolog/log.html | 3 + src/ng/demolog/log.less | 12 + src/ng/lib/index.ts | 102 + src/ng/lib/ng-package.json | 7 + src/ng/lib/package.json | 110 + src/ng/lib/project.json | 49 + src/notification/demo/karma.conf.js | 44 + src/notification/demo/project.json | 90 + src/notification/demo/src/app/AppComponent.ts | 7 + src/notification/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/notification/demo/src/app/app.html | 3 + .../NotificationAnimationComponent.ts | 18 + .../NotificationBasicComponent.ts | 12 + .../NotificationCloseComponent.ts | 29 + .../NotificationConfigComponent.ts | 26 + .../NotificationDurationComponent.ts | 20 + .../NotificationEventsComponent.ts | 19 + .../NotificationHoverPauseComponent.ts | 18 + .../notification/NotificationNameComponent.ts | 23 + .../NotificationPositionComponent.ts | 49 + .../NotificationTemplateComponent.ts | 14 + .../notification/NotificationTestModule.ts | 99 + .../notification/NotificationTypeComponent.ts | 25 + .../notification/notification-animation.html | 2 + .../app/notification/notification-basic.html | 1 + .../app/notification/notification-close.html | 5 + .../app/notification/notification-config.html | 3 + .../notification/notification-duration.html | 3 + .../app/notification/notification-events.html | 1 + .../notification-hover-pause.html | 2 + .../app/notification/notification-name.html | 2 + .../notification/notification-position.html | 8 + .../notification/notification-template.html | 12 + .../app/notification/notification-type.html | 5 + .../notification/webdoc/notification-demos.js | 146 + .../notification/webdoc/notification.cn.md | 23 + .../notification/webdoc/notification.en.md | 29 + src/notification/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/notification/demo/src/index.html | 16 + src/notification/demo/src/main.ts | 13 + src/notification/demo/test.ts | 24 + src/notification/demo/tsconfig.app.json | 15 + src/notification/demo/tsconfig.spec.json | 20 + src/notification/lib/index.ts | 5 + src/notification/lib/ng-package.json | 7 + src/notification/lib/package.json | 15 + src/notification/lib/project.json | 62 + .../lib/src/TiNotificationComponent.html | 19 + .../lib/src/TiNotificationComponent.ts | 145 + .../src/TiNotificationContainerComponent.html | 48 + .../src/TiNotificationContainerComponent.ts | 180 + .../lib/src/TiNotificationInterface.ts | 138 + .../lib/src/TiNotificationMapper.ts | 37 + .../lib/src/TiNotificationModule.ts | 26 + .../lib/src/TiNotificationMotion.ts | 25 + .../lib/src/TiNotificationService.ts | 138 + src/notification/lib/src/notification.less | 34 + src/notification/lib/src/position.less | 33 + src/outline/lib/index.ts | 1 + src/outline/lib/ng-package.json | 7 + src/outline/lib/package.json | 9 + src/outline/lib/project.json | 62 + src/outline/lib/src/TiOutlineDirective.ts | 56 + src/outline/lib/src/TiOutlineModule.ts | 22 + src/overflow/demo/karma.conf.js | 44 + src/overflow/demo/project.json | 90 + src/overflow/demo/src/app/AppComponent.ts | 7 + src/overflow/demo/src/app/AppModule.ts | 24 + src/overflow/demo/src/app/IndexComponent.ts | 49 + src/overflow/demo/src/app/app.html | 3 + .../app/overflow/OverflowDestoryComponent.ts | 23 + .../overflow/OverflowDirectiveComponent.ts | 20 + .../app/overflow/OverflowMaxlineComponent.ts | 8 + .../app/overflow/OverflowMaxwidthComponent.ts | 8 + .../app/overflow/OverflowPositionComponent.ts | 8 + .../app/overflow/OverflowServiceComponent.ts | 42 + .../src/app/overflow/OverflowTestComponent.ts | 12 + .../src/app/overflow/OverflowTestModule.ts | 56 + .../overflow/OverflowTipcontentComponent.ts | 9 + .../src/app/overflow/overflow-destory.html | 10 + .../src/app/overflow/overflow-directive.html | 16 + .../src/app/overflow/overflow-maxline.html | 2 + .../src/app/overflow/overflow-maxwidth.html | 4 + .../src/app/overflow/overflow-position.html | 9 + .../src/app/overflow/overflow-service.html | 9 + .../demo/src/app/overflow/overflow-test.html | 60 + .../src/app/overflow/overflow-tipcontent.html | 7 + .../src/app/overflow/webdoc/overflow-demos.js | 71 + .../src/app/overflow/webdoc/overflow.cn.md | 27 + .../src/app/overflow/webdoc/overflow.en.md | 29 + src/overflow/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/overflow/demo/src/index.html | 16 + src/overflow/demo/src/main.ts | 13 + src/overflow/demo/test.ts | 24 + src/overflow/demo/tsconfig.app.json | 15 + src/overflow/demo/tsconfig.spec.json | 20 + src/overflow/lib/index.ts | 3 + src/overflow/lib/ng-package.json | 7 + src/overflow/lib/package.json | 12 + src/overflow/lib/project.json | 62 + src/overflow/lib/src/TiOverflowDirective.ts | 61 + .../lib/src/TiOverflowMaxlineDirective.ts | 290 + src/overflow/lib/src/TiOverflowModule.ts | 24 + src/overflow/lib/src/TiOverflowService.ts | 181 + .../lib/src/TiOverflowServiceModule.ts | 19 + src/pagination/demo/karma.conf.js | 44 + src/pagination/demo/project.json | 90 + src/pagination/demo/src/app/AppComponent.ts | 7 + src/pagination/demo/src/app/AppModule.ts | 24 + src/pagination/demo/src/app/IndexComponent.ts | 49 + src/pagination/demo/src/app/app.html | 3 + .../pagination/PaginationAutohideComponent.ts | 17 + .../pagination/PaginationDisabledComponent.ts | 10 + .../pagination/PaginationEventComponent.ts | 32 + .../pagination/PaginationFixedComponent.ts | 13 + .../pagination/PaginationLoadingComponent.ts | 17 + .../PaginationPageselectwidthComponent.ts | 10 + .../pagination/PaginationPagesizeComponent.ts | 30 + .../PaginationShowgotolinkComponent.ts | 10 + .../PaginationShowlastpageComponent.ts | 10 + .../PaginationShowtotalnumberComponent.ts | 9 + .../app/pagination/PaginationTestModule.ts | 92 + .../app/pagination/PaginationTypeComponent.ts | 17 + .../app/pagination/pagination-autohide.html | 4 + .../app/pagination/pagination-disabled.html | 1 + .../src/app/pagination/pagination-event.html | 10 + .../src/app/pagination/pagination-fixed.html | 9 + .../app/pagination/pagination-loading.html | 18 + .../pagination-pageselectwidth.html | 8 + .../app/pagination/pagination-pagesize.html | 28 + .../pagination/pagination-showgotolink.html | 6 + .../pagination/pagination-showlastpage.html | 6 + .../pagination-showtotalnumber.html | 6 + .../src/app/pagination/pagination-type.html | 16 + .../app/pagination/webdoc/pagination-demos.js | 161 + .../app/pagination/webdoc/pagination.cn.md | 24 + .../app/pagination/webdoc/pagination.en.md | 29 + src/pagination/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/pagination/demo/src/index.html | 16 + src/pagination/demo/src/main.ts | 13 + src/pagination/demo/test.ts | 24 + src/pagination/demo/tsconfig.app.json | 15 + src/pagination/demo/tsconfig.spec.json | 20 + src/pagination/lib/index.ts | 1 + src/pagination/lib/ng-package.json | 7 + src/pagination/lib/package.json | 18 + src/pagination/lib/project.json | 62 + .../lib/src/TiPaginationComponent.ts | 853 +++ src/pagination/lib/src/TiPaginationModule.ts | 36 + .../lib/src/i18n/TiPaginationWords.ts | 8 + src/pagination/lib/src/i18n/en_US.ts | 10 + src/pagination/lib/src/i18n/es_US.ts | 10 + src/pagination/lib/src/i18n/fr_FR.ts | 10 + src/pagination/lib/src/i18n/index.ts | 7 + src/pagination/lib/src/i18n/pt_BR.ts | 10 + src/pagination/lib/src/i18n/zh_CN.ts | 10 + src/pagination/lib/src/pagination.html | 134 + src/pagination/lib/src/pagination.less | 231 + src/path/demo/project.json | 78 + src/path/demo/src/app/AppComponent.ts | 7 + src/path/demo/src/app/AppModule.ts | 25 + src/path/demo/src/app/IndexComponent.ts | 49 + src/path/demo/src/app/app.html | 3 + .../demo/src/app/path/PathListComponent.ts | 32 + .../demo/src/app/path/PathSelectComponent.ts | 277 + src/path/demo/src/app/path/PathTestModule.ts | 56 + .../app/path/PathfieldEditableComponent.ts | 44 + .../src/app/path/PathfieldEventComponent.ts | 29 + .../src/app/path/PathfieldIspanelComponent.ts | 26 + .../src/app/path/PathfieldItemsComponent.ts | 25 + .../app/path/PathfieldPanelwidthComponent.ts | 26 + src/path/demo/src/app/path/path-list.html | 9 + src/path/demo/src/app/path/path-select.html | 111 + .../demo/src/app/path/pathfield-editable.html | 9 + .../demo/src/app/path/pathfield-event.html | 4 + .../demo/src/app/path/pathfield-ispanel.html | 3 + .../demo/src/app/path/pathfield-items.html | 3 + .../src/app/path/pathfield-panelwidth.html | 3 + .../demo/src/app/path/webdoc/path-demos.js | 85 + src/path/demo/src/app/path/webdoc/path.cn.md | 15 + src/path/demo/src/app/path/webdoc/path.en.md | 13 + src/path/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/path/demo/src/index.html | 16 + src/path/demo/src/main.ts | 13 + src/path/demo/tsconfig.app.json | 10 + src/path/lib/index.ts | 3 + src/path/lib/ng-package.json | 7 + src/path/lib/package.json | 16 + src/path/lib/project.json | 62 + src/path/lib/src/TiPathFieldComponent.ts | 393 ++ src/path/lib/src/TiPathListComponent.ts | 62 + src/path/lib/src/TiPathModule.ts | 31 + src/path/lib/src/path-field.html | 29 + src/path/lib/src/path-field.less | 78 + src/path/lib/src/path-list.html | 8 + src/path/lib/src/path-list.less | 42 + src/phonenumber/demo/project.json | 78 + src/phonenumber/demo/src/app/AppComponent.ts | 7 + src/phonenumber/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/phonenumber/demo/src/app/app.html | 3 + .../phonenumber/PhonenumberBasicComponent.ts | 30 + .../PhonenumberCountryComponent.ts | 33 + .../PhonenumberDisabledComponent.ts | 32 + .../phonenumber/PhonenumberEventComponent.ts | 39 + .../app/phonenumber/PhonenumberTestModule.ts | 32 + .../phonenumber/PhonenumberValidComponent.ts | 32 + .../app/phonenumber/phonenumber-basic.html | 1 + .../app/phonenumber/phonenumber-country.html | 1 + .../app/phonenumber/phonenumber-disabled.html | 1 + .../app/phonenumber/phonenumber-event.html | 8 + .../app/phonenumber/phonenumber-valid.html | 4 + .../phonenumber/webdoc/phonenumber-demos.js | 75 + .../app/phonenumber/webdoc/phonenumber.cn.md | 15 + .../app/phonenumber/webdoc/phonenumber.en.md | 13 + src/phonenumber/demo/src/favicon.ico | Bin 0 -> 300852 bytes src/phonenumber/demo/src/index.html | 16 + src/phonenumber/demo/src/main.ts | 13 + src/phonenumber/demo/tsconfig.app.json | 10 + src/phonenumber/lib/index.ts | 2 + src/phonenumber/lib/ng-package.json | 7 + src/phonenumber/lib/package.json | 15 + src/phonenumber/lib/project.json | 62 + .../lib/src/TiPhoneValidatorDirective.ts | 80 + .../lib/src/TiPhonenumberComponent.ts | 107 + .../lib/src/TiPhonenumberModule.ts | 34 + .../lib/src/i18n/TiPhonenumberWords.ts | 8 + src/phonenumber/lib/src/i18n/en_US.ts | 10 + src/phonenumber/lib/src/i18n/es_US.ts | 10 + src/phonenumber/lib/src/i18n/fr_FR.ts | 10 + src/phonenumber/lib/src/i18n/index.ts | 7 + src/phonenumber/lib/src/i18n/pt_BR.ts | 10 + src/phonenumber/lib/src/i18n/zh_CN.ts | 10 + src/phonenumber/lib/src/phonenumber.html | 34 + src/phonenumber/lib/src/phonenumber.less | 102 + src/polyfills.ts | 60 + src/popconfirm/demo/project.json | 78 + src/popconfirm/demo/src/app/AppComponent.ts | 7 + src/popconfirm/demo/src/app/AppModule.ts | 24 + src/popconfirm/demo/src/app/IndexComponent.ts | 49 + src/popconfirm/demo/src/app/app.html | 3 + .../popconfirm/PopconfirmBasicComponent.ts | 11 + .../popconfirm/PopconfirmDefineComponent.ts | 16 + .../popconfirm/PopconfirmEventComponent.ts | 20 + .../popconfirm/PopconfirmTableComponent.ts | 123 + .../PopconfirmTableDefineComponent.ts | 87 + .../app/popconfirm/PopconfirmTestModule.ts | 56 + .../src/app/popconfirm/popconfirm-basic.html | 1 + .../src/app/popconfirm/popconfirm-define.html | 1 + .../src/app/popconfirm/popconfirm-event.html | 3 + .../popconfirm/popconfirm-table-define.html | 21 + .../src/app/popconfirm/popconfirm-table.html | 27 + .../app/popconfirm/webdoc/popconfirm-demos.js | 68 + .../app/popconfirm/webdoc/popconfirm.cn.md | 23 + .../app/popconfirm/webdoc/popconfirm.en.md | 29 + src/popconfirm/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/popconfirm/demo/src/index.html | 16 + src/popconfirm/demo/src/main.ts | 13 + src/popconfirm/demo/tsconfig.app.json | 15 + src/popconfirm/lib/index.ts | 1 + src/popconfirm/lib/ng-package.json | 7 + src/popconfirm/lib/package.json | 15 + src/popconfirm/lib/project.json | 62 + .../lib/src/TiPopconfirmComponent.ts | 42 + .../lib/src/TiPopconfirmDirective.ts | 230 + src/popconfirm/lib/src/TiPopconfirmModule.ts | 34 + .../lib/src/i18n/TiPopconfirmWords.ts | 6 + src/popconfirm/lib/src/i18n/en_US.ts | 8 + src/popconfirm/lib/src/i18n/es_US.ts | 8 + src/popconfirm/lib/src/i18n/fr_FR.ts | 8 + src/popconfirm/lib/src/i18n/index.ts | 7 + src/popconfirm/lib/src/i18n/pt_BR.ts | 8 + src/popconfirm/lib/src/i18n/zh_CN.ts | 8 + src/popconfirm/lib/src/popconfirm.html | 20 + src/popconfirm/lib/src/popconfirm.less | 49 + src/popup/lib/index.ts | 2 + src/popup/lib/ng-package.json | 7 + src/popup/lib/package.json | 11 + src/popup/lib/project.json | 62 + src/popup/lib/src/TiPopupModule.ts | 19 + src/popup/lib/src/TiPopupService.ts | 296 + src/productpreview/demo/project.json | 86 + .../demo/src/app/AppComponent.ts | 7 + src/productpreview/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/productpreview/demo/src/app/app.html | 3 + .../ProductpreviewBasicComponent.ts | 29 + .../ProductpreviewTestModule.ts | 14 + .../productpreview/productpreview-basic.html | 3 + .../webdoc/productpreview-demos.js | 17 + .../webdoc/productpreview.cn.md | 32 + .../webdoc/productpreview.en.md | 13 + src/productpreview/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/productpreview/demo/src/index.html | 16 + src/productpreview/demo/src/main.ts | 13 + src/productpreview/demo/tsconfig.app.json | 10 + src/productpreview/lib/index.ts | 2 + src/productpreview/lib/ng-package.json | 7 + src/productpreview/lib/package.json | 14 + src/productpreview/lib/project.json | 62 + .../lib/src/TiProductpreviewComponent.ts | 198 + .../lib/src/TiProductpreviewModule.ts | 29 + .../lib/src/productpreview.html | 48 + .../lib/src/productpreview.less | 157 + src/progressbar/demo/karma.conf.js | 44 + src/progressbar/demo/project.json | 90 + src/progressbar/demo/src/app/AppComponent.ts | 7 + src/progressbar/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/progressbar/demo/src/app/app.html | 3 + .../ProgressbarAnimationComponent.ts | 27 + .../progressbar/ProgressbarBasicComponent.ts | 11 + .../progressbar/ProgressbarClassComponent.ts | 32 + .../app/progressbar/ProgressbarTestModule.ts | 32 + .../progressbar/progressbar-animation.html | 9 + .../app/progressbar/progressbar-basic.html | 18 + .../app/progressbar/progressbar-class.html | 16 + .../app/progressbar/progressbar-class.less | 52 + .../progressbar/webdoc/progressbar-demos.js | 48 + .../app/progressbar/webdoc/progressbar.cn.md | 24 + .../app/progressbar/webdoc/progressbar.en.md | 29 + src/progressbar/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/progressbar/demo/src/index.html | 16 + src/progressbar/demo/src/main.ts | 13 + src/progressbar/demo/test.ts | 24 + src/progressbar/demo/tsconfig.app.json | 15 + src/progressbar/demo/tsconfig.spec.json | 20 + src/progressbar/lib/index.ts | 1 + src/progressbar/lib/ng-package.json | 7 + src/progressbar/lib/package.json | 10 + src/progressbar/lib/project.json | 62 + .../lib/src/TiProgressbarComponent.ts | 100 + .../lib/src/TiProgressbarModule.ts | 22 + src/progressbar/lib/src/progressbar.html | 9 + src/progressbar/lib/src/progressbar.less | 54 + src/progresspie/demo/project.json | 78 + src/progresspie/demo/src/app/AppComponent.ts | 7 + src/progresspie/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/progresspie/demo/src/app/app.html | 3 + .../progresspie/ProgresspieTestComponent.ts | 26 + .../app/progresspie/ProgresspieTestModule.ts | 22 + src/progresspie/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/progresspie/demo/src/index.html | 16 + src/progresspie/demo/src/main.ts | 13 + src/progresspie/demo/tsconfig.app.json | 15 + src/progresspie/lib/index.ts | 1 + src/progresspie/lib/ng-package.json | 7 + src/progresspie/lib/package.json | 8 + src/progresspie/lib/project.json | 62 + .../lib/src/TiProgresspieComponent.ts | 123 + .../lib/src/TiProgresspieModule.ts | 22 + src/radio/demo/karma.conf.js | 44 + src/radio/demo/project.json | 90 + src/radio/demo/src/app/AppComponent.ts | 7 + src/radio/demo/src/app/AppModule.ts | 24 + src/radio/demo/src/app/IndexComponent.ts | 51 + src/radio/demo/src/app/app.html | 3 + .../demo/src/app/radio/RadioBasicComponent.ts | 8 + .../demo/src/app/radio/RadioDarkComponent.ts | 8 + .../src/app/radio/RadioDisabledComponent.ts | 49 + .../demo/src/app/radio/RadioEventComponent.ts | 61 + .../demo/src/app/radio/RadioFocusComponent.ts | 34 + .../demo/src/app/radio/RadioGroupComponent.ts | 26 + .../app/radio/RadioGroupDirectionComponent.ts | 26 + .../app/radio/RadioGroupLabelkeyComponent.ts | 26 + .../app/radio/RadioGroupLinewrapComponent.ts | 21 + .../radio/RadioGroupValidationComponent.ts | 72 + .../app/radio/RadioGroupValuekeyComponent.ts | 26 + .../demo/src/app/radio/RadioLabelComponent.ts | 15 + .../demo/src/app/radio/RadioTestModule.ts | 101 + src/radio/demo/src/app/radio/radio-basic.html | 10 + src/radio/demo/src/app/radio/radio-dark.html | 12 + .../demo/src/app/radio/radio-disabled.html | 43 + src/radio/demo/src/app/radio/radio-event.html | 23 + src/radio/demo/src/app/radio/radio-focus.html | 39 + .../src/app/radio/radio-group-direction.html | 4 + .../src/app/radio/radio-group-labelkey.html | 4 + .../src/app/radio/radio-group-linewrap.html | 1 + .../src/app/radio/radio-group-validation.html | 25 + .../src/app/radio/radio-group-valuekey.html | 4 + src/radio/demo/src/app/radio/radio-group.html | 4 + src/radio/demo/src/app/radio/radio-label.html | 40 + .../demo/src/app/radio/webdoc/radio-demos.js | 134 + .../demo/src/app/radio/webdoc/radio.cn.md | 29 + .../demo/src/app/radio/webdoc/radio.en.md | 29 + src/radio/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/radio/demo/src/index.html | 16 + src/radio/demo/src/main.ts | 13 + src/radio/demo/test.ts | 24 + src/radio/demo/tsconfig.app.json | 15 + src/radio/demo/tsconfig.spec.json | 20 + src/radio/lib/index.ts | 1 + src/radio/lib/ng-package.json | 7 + src/radio/lib/package.json | 13 + src/radio/lib/project.json | 62 + src/radio/lib/src/TiRadioComponent.ts | 36 + src/radio/lib/src/TiRadioGroupComponent.ts | 82 + src/radio/lib/src/TiRadioModule.ts | 26 + src/radio/lib/src/radio-group.html | 30 + src/radio/lib/src/radio.html | 14 + src/radio/lib/src/radio.less | 175 + src/radio/lib/src/radiogroup.less | 23 + src/rate/demo/karma.conf.js | 44 + src/rate/demo/project.json | 90 + src/rate/demo/src/app/AppComponent.ts | 7 + src/rate/demo/src/app/AppModule.ts | 24 + src/rate/demo/src/app/IndexComponent.ts | 49 + src/rate/demo/src/app/app.html | 3 + .../demo/src/app/rate/RateBasicComponent.ts | 8 + .../src/app/rate/RateDisabledComponent.ts | 9 + .../demo/src/app/rate/RateEventComponent.ts | 13 + src/rate/demo/src/app/rate/RateIdComponent.ts | 24 + .../demo/src/app/rate/RateLoadComponent.ts | 45 + src/rate/demo/src/app/rate/RateTestModule.ts | 37 + src/rate/demo/src/app/rate/rate-basic.html | 1 + src/rate/demo/src/app/rate/rate-disabled.html | 1 + src/rate/demo/src/app/rate/rate-event.html | 3 + src/rate/demo/src/app/rate/rate-id.html | 10 + src/rate/demo/src/app/rate/rate-load.html | 24 + .../demo/src/app/rate/webdoc/rate-demos.js | 39 + src/rate/demo/src/app/rate/webdoc/rate.cn.md | 23 + src/rate/demo/src/app/rate/webdoc/rate.en.md | 29 + src/rate/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/rate/demo/src/index.html | 16 + src/rate/demo/src/main.ts | 13 + src/rate/demo/test.ts | 24 + src/rate/demo/tsconfig.app.json | 15 + src/rate/demo/tsconfig.spec.json | 20 + src/rate/lib/index.ts | 1 + src/rate/lib/ng-package.json | 7 + src/rate/lib/package.json | 11 + src/rate/lib/project.json | 62 + src/rate/lib/src/TiRateComponent.ts | 108 + src/rate/lib/src/TiRateModule.ts | 24 + src/rate/lib/src/rate.html | 11 + src/rate/lib/src/rate.less | 30 + src/renderer/lib/index.ts | 2 + src/renderer/lib/ng-package.json | 7 + src/renderer/lib/package.json | 8 + src/renderer/lib/project.json | 62 + src/renderer/lib/src/TiRenderer.ts | 114 + src/renderer/lib/src/TiRendererModule.ts | 19 + src/rights/demo/project.json | 78 + src/rights/demo/src/app/AppComponent.ts | 7 + src/rights/demo/src/app/AppModule.ts | 24 + src/rights/demo/src/app/IndexComponent.ts | 49 + src/rights/demo/src/app/app.html | 3 + .../src/app/rights/RightsBasicComponent.ts | 31 + .../demo/src/app/rights/RightsTestModule.ts | 24 + .../src/app/rights/RightsTypeComponent.ts | 32 + .../demo/src/app/rights/rights-basic.html | 1 + .../demo/src/app/rights/rights-type.html | 1 + .../src/app/rights/webdoc/rights-demos.js | 29 + .../demo/src/app/rights/webdoc/rights.cn.md | 19 + .../demo/src/app/rights/webdoc/rights.en.md | 13 + src/rights/demo/src/favicon.ico | Bin 0 -> 300852 bytes src/rights/demo/src/index.html | 16 + src/rights/demo/src/main.ts | 13 + src/rights/demo/tsconfig.app.json | 10 + src/rights/lib/index.ts | 2 + src/rights/lib/ng-package.json | 7 + src/rights/lib/package.json | 12 + src/rights/lib/project.json | 62 + src/rights/lib/src/TiRightsComponent.ts | 64 + src/rights/lib/src/TiRightsModule.ts | 25 + src/rights/lib/src/fonts/rightsFont.svg | 28 + src/rights/lib/src/fonts/rightsFont.woff | Bin 0 -> 1832 bytes src/rights/lib/src/icon.less | 32 + src/rights/lib/src/rights.html | 15 + src/rights/lib/src/rights.less | 38 + src/score/demo/karma.conf.js | 44 + src/score/demo/project.json | 90 + src/score/demo/src/app/AppComponent.ts | 7 + src/score/demo/src/app/AppModule.ts | 24 + src/score/demo/src/app/IndexComponent.ts | 49 + src/score/demo/src/app/app.html | 3 + .../demo/src/app/score/ScoreBasicComponent.ts | 8 + .../src/app/score/ScoreEventsComponent.ts | 12 + .../src/app/score/ScoreLimittextComponent.ts | 9 + .../src/app/score/ScorePaddingComponent.ts | 8 + .../demo/src/app/score/ScoreTestModule.ts | 26 + src/score/demo/src/app/score/score-basic.html | 1 + .../demo/src/app/score/score-events.html | 1 + .../demo/src/app/score/score-limittext.html | 1 + .../demo/src/app/score/score-padding.html | 1 + .../demo/src/app/score/webdoc/score-demos.js | 32 + .../demo/src/app/score/webdoc/score.cn.md | 23 + .../demo/src/app/score/webdoc/score.en.md | 29 + src/score/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/score/demo/src/index.html | 16 + src/score/demo/src/main.ts | 13 + src/score/demo/test.ts | 24 + src/score/demo/tsconfig.app.json | 15 + src/score/demo/tsconfig.spec.json | 20 + src/score/lib/index.ts | 1 + src/score/lib/ng-package.json | 7 + src/score/lib/package.json | 12 + src/score/lib/project.json | 62 + src/score/lib/src/TiScoreComponent.ts | 61 + src/score/lib/src/TiScoreModule.ts | 30 + src/score/lib/src/i18n/TiScoreWords.ts | 6 + src/score/lib/src/i18n/en_US.ts | 8 + src/score/lib/src/i18n/es_US.ts | 8 + src/score/lib/src/i18n/fr_FR.ts | 8 + src/score/lib/src/i18n/index.ts | 7 + src/score/lib/src/i18n/pt_BR.ts | 8 + src/score/lib/src/i18n/zh_CN.ts | 8 + src/score/lib/src/score.html | 18 + src/score/lib/src/score.less | 48 + src/scroll/lib/index.ts | 1 + src/scroll/lib/ng-package.json | 7 + src/scroll/lib/package.json | 9 + src/scroll/lib/project.json | 62 + src/scroll/lib/src/TiScrollDirective.ts | 30 + src/scroll/lib/src/TiScrollModule.ts | 22 + src/searchbox/demo/karma.conf.js | 44 + src/searchbox/demo/project.json | 98 + src/searchbox/demo/src/app/AppComponent.ts | 7 + src/searchbox/demo/src/app/AppModule.ts | 24 + src/searchbox/demo/src/app/IndexComponent.ts | 49 + src/searchbox/demo/src/app/app.html | 3 + .../SearchboxAppendtobodyComponent.ts | 9 + .../app/searchbox/SearchboxBasicComponent.ts | 8 + .../searchbox/SearchboxDisabledComponent.ts | 8 + .../app/searchbox/SearchboxEventComponent.ts | 41 + .../searchbox/SearchboxMaxlengthComponent.ts | 8 + .../searchbox/SearchboxNotsearchComponent.ts | 13 + .../searchbox/SearchboxOptionsComponent.ts | 23 + .../searchbox/SearchboxPanelsizeComponent.ts | 28 + .../searchbox/SearchboxReactiveComponent.ts | 10 + .../searchbox/SearchboxSuggestComponent.ts | 31 + .../searchbox/SearchboxTemplateComponent.ts | 35 + .../app/searchbox/SearchboxTestComponent.ts | 58 + .../src/app/searchbox/SearchboxTestModule.ts | 115 + .../searchbox/SearchboxTrimmedComponent.ts | 15 + .../app/searchbox/SearchboxValidComponent.ts | 8 + .../SearchboxVirtualscrollComponent.ts | 41 + .../app/searchbox/searchbox-appendtobody.html | 7 + .../src/app/searchbox/searchbox-basic.html | 1 + .../src/app/searchbox/searchbox-disabled.html | 1 + .../src/app/searchbox/searchbox-event.html | 14 + .../app/searchbox/searchbox-maxlength.html | 1 + .../app/searchbox/searchbox-notsearch.html | 7 + .../src/app/searchbox/searchbox-options.html | 5 + .../app/searchbox/searchbox-panelsize.html | 8 + .../src/app/searchbox/searchbox-reactive.html | 1 + .../src/app/searchbox/searchbox-suggest.html | 5 + .../src/app/searchbox/searchbox-template.html | 8 + .../src/app/searchbox/searchbox-test.html | 37 + .../src/app/searchbox/searchbox-trimmed.html | 13 + .../src/app/searchbox/searchbox-valid.html | 1 + .../searchbox/searchbox-virtualscroll.html | 1 + .../app/searchbox/webdoc/searchbox-demos.js | 169 + .../src/app/searchbox/webdoc/searchbox.cn.md | 23 + .../src/app/searchbox/webdoc/searchbox.en.md | 29 + src/searchbox/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/searchbox/demo/src/index.html | 16 + src/searchbox/demo/src/main.ts | 13 + src/searchbox/demo/test.ts | 24 + src/searchbox/demo/tsconfig.app.json | 15 + src/searchbox/demo/tsconfig.spec.json | 20 + src/searchbox/lib/index.ts | 1 + src/searchbox/lib/ng-package.json | 7 + src/searchbox/lib/package.json | 18 + src/searchbox/lib/project.json | 62 + src/searchbox/lib/src/TiSearchboxComponent.ts | 138 + src/searchbox/lib/src/TiSearchboxModule.ts | 36 + .../lib/src/TiSearchboxNotsearchComponent.ts | 43 + .../lib/src/i18n/TiSearchboxWords.ts | 5 + src/searchbox/lib/src/i18n/en_US.ts | 7 + src/searchbox/lib/src/i18n/es_US.ts | 7 + src/searchbox/lib/src/i18n/fr_FR.ts | 7 + src/searchbox/lib/src/i18n/index.ts | 7 + src/searchbox/lib/src/i18n/pt_BR.ts | 7 + src/searchbox/lib/src/i18n/zh_CN.ts | 7 + .../lib/src/searchbox-notsearch.less | 26 + src/searchbox/lib/src/searchbox.html | 47 + src/searchbox/lib/src/searchbox.less | 101 + src/select/demo/karma.conf.js | 44 + src/select/demo/project.json | 98 + src/select/demo/src/app/AppComponent.ts | 7 + src/select/demo/src/app/AppModule.ts | 24 + src/select/demo/src/app/IndexComponent.ts | 49 + src/select/demo/src/app/app.html | 3 + src/select/demo/src/app/select/NoEmptyPipe.ts | 9 + .../app/select/SelectAppendtobodyComponent.ts | 8 + .../src/app/select/SelectBasicComponent.ts | 20 + .../app/select/SelectBeforesearchComponent.ts | 60 + .../select/SelectBeforesearchTestComponent.ts | 123 + .../select/SelectChangeSelectallComponent.ts | 126 + .../app/select/SelectClearableComponent.ts | 25 + .../src/app/select/SelectDisabledComponent.ts | 25 + .../select/SelectDisabledfocusComponent.ts | 25 + .../src/app/select/SelectEnumComponent.ts | 65 + .../src/app/select/SelectEventComponent.ts | 26 + .../src/app/select/SelectFocusComponent.ts | 53 + .../src/app/select/SelectGroupComponent.ts | 63 + .../demo/src/app/select/SelectIdComponent.ts | 161 + .../src/app/select/SelectIdkeyComponent.ts | 22 + .../src/app/select/SelectInputComponent.ts | 51 + .../src/app/select/SelectLabelkeyComponent.ts | 51 + .../src/app/select/SelectLazyComponent.ts | 33 + .../src/app/select/SelectLeakComponent.ts | 87 + .../src/app/select/SelectLoadComponent.ts | 108 + .../src/app/select/SelectManyComponent.ts | 95 + .../src/app/select/SelectMaxlineComponent.ts | 41 + .../src/app/select/SelectMuchComponent.ts | 63 + .../src/app/select/SelectMultiComponent.ts | 20 + .../src/app/select/SelectNoborderComponent.ts | 53 + .../src/app/select/SelectNodataComponent.ts | 9 + .../src/app/select/SelectNoemptyComponent.ts | 51 + .../src/app/select/SelectNullComponent.ts | 6 + .../SelectPaginBeforesearchComponent.ts | 188 + .../app/select/SelectPaginationComponent.ts | 65 + .../src/app/select/SelectPanelComponent.ts | 21 + .../SelectReservesearchwordComponent.ts | 41 + .../app/select/SelectScrollLoadComponent.ts | 116 + .../src/app/select/SelectSearchComponent.ts | 41 + .../app/select/SelectSearchkeysComponent.ts | 63 + .../app/select/SelectSelectallComponent.ts | 20 + .../SelectShowselectednumberComponent.ts | 21 + .../src/app/select/SelectSmallComponent.ts | 20 + .../demo/src/app/select/SelectTagComponent.ts | 84 + .../src/app/select/SelectTemplateComponent.ts | 50 + .../demo/src/app/select/SelectTestModule.ts | 293 + .../demo/src/app/select/SelectTipComponent.ts | 29 + .../src/app/select/SelectTiscrollComponent.ts | 61 + .../src/app/select/SelectTworowComponent.ts | 28 + .../src/app/select/SelectValidComponent.ts | 40 + .../app/select/SelectValidGroupComponent.ts | 75 + .../src/app/select/SelectValuekeyComponent.ts | 51 + .../app/select/SelectValuekeyTestComponent.ts | 111 + .../select/SelectVirtualscrollComponent.ts | 51 + .../SelectVirtualscrollMultiComponent.ts | 201 + .../src/app/select/select-appendtobody.html | 1 + .../demo/src/app/select/select-basic.html | 5 + .../app/select/select-beforesearch-test.html | 41 + .../src/app/select/select-beforesearch.html | 9 + .../app/select/select-change-selectall.html | 34 + .../demo/src/app/select/select-clearable.html | 24 + .../demo/src/app/select/select-disabled.html | 5 + .../src/app/select/select-disabledfocus.html | 10 + .../demo/src/app/select/select-enum.html | 46 + .../demo/src/app/select/select-event.html | 3 + .../demo/src/app/select/select-focus.html | 38 + .../demo/src/app/select/select-group.html | 1 + src/select/demo/src/app/select/select-id.html | 53 + .../demo/src/app/select/select-idkey.html | 1 + .../demo/src/app/select/select-input.html | 29 + .../demo/src/app/select/select-labelkey.html | 1 + .../demo/src/app/select/select-lazy.html | 2 + .../demo/src/app/select/select-leak.html | 10 + .../demo/src/app/select/select-load.html | 35 + .../demo/src/app/select/select-many.html | 29 + .../demo/src/app/select/select-maxline.html | 1 + .../demo/src/app/select/select-much.html | 9 + .../demo/src/app/select/select-multi.html | 5 + .../demo/src/app/select/select-noborder.html | 53 + .../demo/src/app/select/select-nodata.html | 1 + .../demo/src/app/select/select-noempty.html | 21 + .../demo/src/app/select/select-null.html | 42 + .../app/select/select-pagin-beforesearch.html | 110 + .../src/app/select/select-pagination.html | 11 + .../demo/src/app/select/select-panel.html | 2 + .../app/select/select-reservesearchword.html | 9 + .../src/app/select/select-scroll-load.html | 12 + .../demo/src/app/select/select-search.html | 1 + .../src/app/select/select-searchkeys.html | 9 + .../demo/src/app/select/select-selectall.html | 2 + .../app/select/select-showselectednumber.html | 11 + .../demo/src/app/select/select-small.html | 1 + .../demo/src/app/select/select-tag.html | 70 + .../demo/src/app/select/select-tag.less | 70 + .../demo/src/app/select/select-template.html | 25 + .../demo/src/app/select/select-tip.html | 12 + .../demo/src/app/select/select-tiscroll.html | 56 + .../demo/src/app/select/select-tworow.html | 13 + .../demo/src/app/select/select-valid.html | 10 + .../src/app/select/select-validgroup.html | 30 + .../src/app/select/select-valuekey-test.html | 73 + .../demo/src/app/select/select-valuekey.html | 5 + .../select/select-virtualscroll-multi.html | 141 + .../src/app/select/select-virtualscroll.html | 1 + .../src/app/select/webdoc/select-demos.js | 324 + .../demo/src/app/select/webdoc/select.cn.md | 28 + .../demo/src/app/select/webdoc/select.en.md | 29 + ...3\350\257\225\347\224\250\344\276\213.txt" | 83 + src/select/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/select/demo/src/index.html | 16 + src/select/demo/src/main.ts | 13 + src/select/demo/test.ts | 24 + src/select/demo/tsconfig.app.json | 15 + src/select/demo/tsconfig.spec.json | 20 + src/select/lib/index.ts | 1 + src/select/lib/ng-package.json | 7 + src/select/lib/package.json | 17 + src/select/lib/project.json | 62 + src/select/lib/src/TiSelectComponent.ts | 429 ++ src/select/lib/src/TiSelectModule.ts | 27 + src/select/lib/src/select-small.less | 9 + src/select/lib/src/select.html | 64 + src/select/lib/src/select.less | 37 + src/selectgroup/demo/project.json | 86 + src/selectgroup/demo/src/app/AppComponent.ts | 7 + src/selectgroup/demo/src/app/AppModule.ts | 26 + .../demo/src/app/IndexComponent.ts | 49 + src/selectgroup/demo/src/app/app.html | 3 + .../selectgroup/SelectgroupBasicComponent.ts | 22 + .../SelectgroupMultipleComponent.ts | 22 + .../selectgroup/SelectgroupSelectComponent.ts | 21 + .../SelectgroupTemplateComponent.ts | 21 + .../app/selectgroup/SelectgroupTestModule.ts | 40 + .../SelectgroupValuekeyComponent.ts | 21 + .../app/selectgroup/selectgroup-basic.html | 3 + .../app/selectgroup/selectgroup-multiple.html | 9 + .../app/selectgroup/selectgroup-select.html | 10 + .../app/selectgroup/selectgroup-template.html | 28 + .../app/selectgroup/selectgroup-valuekey.html | 6 + .../selectgroup/webdoc/selectgroup-demos.js | 73 + .../app/selectgroup/webdoc/selectgroup.cn.md | 15 + .../app/selectgroup/webdoc/selectgroup.en.md | 13 + src/selectgroup/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/selectgroup/demo/src/index.html | 16 + src/selectgroup/demo/src/main.ts | 13 + src/selectgroup/demo/tsconfig.app.json | 10 + src/selectgroup/lib/index.ts | 2 + src/selectgroup/lib/ng-package.json | 7 + src/selectgroup/lib/package.json | 11 + src/selectgroup/lib/project.json | 62 + .../lib/src/TiSelectgroupComponent.ts | 32 + .../lib/src/TiSelectgroupModule.ts | 27 + .../lib/src/TiSelectitemComponent.ts | 129 + src/selectgroup/lib/src/selectgroup.html | 1 + src/selectgroup/lib/src/selectgroup.less | 180 + src/selectgroup/lib/src/selectitem.html | 23 + src/skeleton/demo/karma.conf.js | 44 + src/skeleton/demo/project.json | 90 + src/skeleton/demo/src/app/AppComponent.ts | 7 + src/skeleton/demo/src/app/AppModule.ts | 24 + src/skeleton/demo/src/app/IndexComponent.ts | 49 + src/skeleton/demo/src/app/app.html | 3 + .../src/app/skeleton/SkeletonPageComponent.ts | 8 + .../src/app/skeleton/SkeletonTestModule.ts | 31 + .../app/skeleton/SkeletonTitleComponent.ts | 7 + .../src/app/skeleton/SkeletonTypeComponent.ts | 7 + .../demo/src/app/skeleton/skeleton-page.html | 24 + .../demo/src/app/skeleton/skeleton-page.less | 43 + .../demo/src/app/skeleton/skeleton-title.html | 6 + .../demo/src/app/skeleton/skeleton-type.html | 6 + .../demo/src/app/skeleton/skeleton.less | 8 + .../src/app/skeleton/webdoc/skeleton-demos.js | 43 + .../src/app/skeleton/webdoc/skeleton.cn.md | 26 + .../src/app/skeleton/webdoc/skeleton.en.md | 29 + src/skeleton/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/skeleton/demo/src/index.html | 16 + src/skeleton/demo/src/main.ts | 13 + src/skeleton/demo/test.ts | 24 + src/skeleton/demo/tsconfig.app.json | 15 + src/skeleton/demo/tsconfig.spec.json | 20 + src/skeleton/lib/index.ts | 1 + src/skeleton/lib/ng-package.json | 7 + src/skeleton/lib/package.json | 9 + src/skeleton/lib/project.json | 62 + src/skeleton/lib/src/TiSkeletonComponent.ts | 34 + src/skeleton/lib/src/TiSkeletonModule.ts | 24 + src/skeleton/lib/src/skeleton.html | 10 + src/skeleton/lib/src/skeleton.less | 35 + src/slider/demo/karma.conf.js | 44 + src/slider/demo/project.json | 90 + src/slider/demo/src/app/AppComponent.ts | 7 + src/slider/demo/src/app/AppModule.ts | 24 + src/slider/demo/src/app/IndexComponent.ts | 49 + src/slider/demo/src/app/app.html | 3 + .../src/app/slider/SliderEventComponent.ts | 16 + .../app/slider/SliderFormcontrolComponent.ts | 25 + .../src/app/slider/SliderHiddenComponent.ts | 22 + .../src/app/slider/SliderLimitsComponent.ts | 16 + .../src/app/slider/SliderRatiosComponent.ts | 14 + .../src/app/slider/SliderScalesComponent.ts | 29 + .../src/app/slider/SliderTemplateComponent.ts | 54 + .../demo/src/app/slider/SliderTestModule.ts | 72 + .../demo/src/app/slider/SliderTipComponent.ts | 27 + .../demo/src/app/slider/slider-event.html | 5 + .../src/app/slider/slider-formcontrol.html | 4 + .../demo/src/app/slider/slider-hidden.html | 20 + .../demo/src/app/slider/slider-limits.html | 2 + .../demo/src/app/slider/slider-ratios.html | 2 + .../demo/src/app/slider/slider-scales.html | 6 + .../demo/src/app/slider/slider-template.html | 17 + .../demo/src/app/slider/slider-tip.html | 11 + .../src/app/slider/webdoc/slider-demos.js | 100 + .../demo/src/app/slider/webdoc/slider.cn.md | 24 + .../demo/src/app/slider/webdoc/slider.en.md | 29 + src/slider/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/slider/demo/src/index.html | 16 + src/slider/demo/src/main.ts | 13 + src/slider/demo/test.ts | 24 + src/slider/demo/tsconfig.app.json | 15 + src/slider/demo/tsconfig.spec.json | 20 + src/slider/lib/index.ts | 1 + src/slider/lib/ng-package.json | 7 + src/slider/lib/package.json | 15 + src/slider/lib/project.json | 62 + src/slider/lib/src/TiSliderComponent.ts | 1027 ++++ src/slider/lib/src/TiSliderModule.ts | 25 + src/slider/lib/src/slider.html | 43 + src/slider/lib/src/slider.less | 179 + src/spinner/demo/karma.conf.js | 44 + src/spinner/demo/project.json | 90 + src/spinner/demo/src/app/AppComponent.ts | 7 + src/spinner/demo/src/app/AppModule.ts | 24 + src/spinner/demo/src/app/IndexComponent.ts | 49 + src/spinner/demo/src/app/app.html | 3 + .../src/app/spinner/SpinnerBasicComponent.ts | 10 + .../app/spinner/SpinnerBasicTestComponent.ts | 119 + .../spinner/SpinnerCorrectableComponent.ts | 18 + .../app/spinner/SpinnerDisabledComponent.ts | 10 + .../src/app/spinner/SpinnerEventComponent.ts | 15 + .../src/app/spinner/SpinnerFormatComponent.ts | 10 + .../src/app/spinner/SpinnerIdComponent.ts | 9 + .../src/app/spinner/SpinnerLoadComponent.ts | 39 + .../app/spinner/SpinnerLocaleableComponent.ts | 10 + .../src/app/spinner/SpinnerMaxMinComponent.ts | 38 + .../app/spinner/SpinnerMaxlengthComponent.ts | 11 + .../src/app/spinner/SpinnerStepComponent.ts | 12 + .../src/app/spinner/SpinnerStepfnComponent.ts | 26 + .../demo/src/app/spinner/SpinnerTestModule.ts | 118 + .../src/app/spinner/SpinnerTipComponent.ts | 10 + .../app/spinner/SpinnerTipTestComponent.ts | 28 + .../app/spinner/SpinnerValidationComponent.ts | 8 + .../spinner/SpinnerValidationTestComponent.ts | 32 + .../src/app/spinner/spinner-basic-test.html | 129 + .../demo/src/app/spinner/spinner-basic.html | 1 + .../src/app/spinner/spinner-correctable.html | 17 + .../src/app/spinner/spinner-disabled.html | 1 + .../demo/src/app/spinner/spinner-event.html | 9 + .../demo/src/app/spinner/spinner-format.html | 1 + .../demo/src/app/spinner/spinner-load.html | 21 + .../src/app/spinner/spinner-localeable.html | 8 + .../demo/src/app/spinner/spinner-max-min.html | 18 + .../src/app/spinner/spinner-maxlength.html | 8 + .../demo/src/app/spinner/spinner-step.html | 1 + .../demo/src/app/spinner/spinner-stepfn.html | 2 + .../src/app/spinner/spinner-tip-test.html | 32 + .../demo/src/app/spinner/spinner-tip.html | 9 + .../app/spinner/spinner-validation-test.html | 18 + .../src/app/spinner/spinner-validation.html | 9 + .../demo/src/app/spinner/spinnerTest.less | 22 + .../src/app/spinner/webdoc/spinner-demos.js | 137 + .../demo/src/app/spinner/webdoc/spinner.cn.md | 36 + .../demo/src/app/spinner/webdoc/spinner.en.md | 29 + src/spinner/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/spinner/demo/src/index.html | 16 + src/spinner/demo/src/main.ts | 13 + src/spinner/demo/test.ts | 24 + src/spinner/demo/tsconfig.app.json | 15 + src/spinner/demo/tsconfig.spec.json | 20 + src/spinner/lib/index.ts | 1 + src/spinner/lib/ng-package.json | 7 + src/spinner/lib/package.json | 17 + src/spinner/lib/project.json | 62 + src/spinner/lib/src/TiSpinnerComponent.ts | 484 ++ src/spinner/lib/src/TiSpinnerModule.ts | 32 + src/spinner/lib/src/i18n/TiSpinnerWords.ts | 7 + src/spinner/lib/src/i18n/en_US.ts | 9 + src/spinner/lib/src/i18n/es_US.ts | 9 + src/spinner/lib/src/i18n/fr_FR.ts | 9 + src/spinner/lib/src/i18n/index.ts | 7 + src/spinner/lib/src/i18n/pt_BR.ts | 9 + src/spinner/lib/src/i18n/zh_CN.ts | 9 + src/spinner/lib/src/spinner.html | 32 + src/spinner/lib/src/spinner.less | 109 + src/steps/demo/karma.conf.js | 44 + src/steps/demo/project.json | 90 + src/steps/demo/src/app/AppComponent.ts | 7 + src/steps/demo/src/app/AppModule.ts | 24 + src/steps/demo/src/app/IndexComponent.ts | 49 + src/steps/demo/src/app/app.html | 3 + .../src/app/steps/StepsActiveComponent.ts | 33 + .../src/app/steps/StepsAdaptiveComponent.ts | 23 + .../app/steps/StepsAdaptiveTestComponent.ts | 69 + .../demo/src/app/steps/StepsBaseComponent.ts | 23 + .../src/app/steps/StepsBeforeComponent.ts | 40 + .../src/app/steps/StepsClickableComponent.ts | 26 + .../src/app/steps/StepsEventsComponent.ts | 27 + .../demo/src/app/steps/StepsLabelComponent.ts | 27 + .../src/app/steps/StepsMaxwidthComponent.ts | 23 + .../src/app/steps/StepsTemplateComponent.ts | 36 + .../demo/src/app/steps/StepsTestModule.ts | 78 + .../demo/src/app/steps/steps-active.html | 9 + .../src/app/steps/steps-adaptive-test.html | 29 + .../demo/src/app/steps/steps-adaptive.html | 3 + src/steps/demo/src/app/steps/steps-base.html | 1 + .../demo/src/app/steps/steps-before.html | 2 + .../demo/src/app/steps/steps-clickable.html | 4 + .../demo/src/app/steps/steps-events.html | 8 + src/steps/demo/src/app/steps/steps-label.html | 1 + .../demo/src/app/steps/steps-maxwidth.html | 1 + .../demo/src/app/steps/steps-template.html | 11 + .../demo/src/app/steps/webdoc/steps-demos.js | 107 + .../demo/src/app/steps/webdoc/steps.cn.md | 23 + .../demo/src/app/steps/webdoc/steps.en.md | 29 + src/steps/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/steps/demo/src/index.html | 16 + src/steps/demo/src/main.ts | 13 + src/steps/demo/test.ts | 24 + src/steps/demo/tsconfig.app.json | 15 + src/steps/demo/tsconfig.spec.json | 20 + src/steps/lib/index.ts | 1 + src/steps/lib/ng-package.json | 7 + src/steps/lib/package.json | 14 + src/steps/lib/project.json | 62 + src/steps/lib/src/TiStepsComponent.ts | 283 + src/steps/lib/src/TiStepsModule.ts | 25 + src/steps/lib/src/steps.html | 51 + src/steps/lib/src/steps.less | 227 + src/styles.less | 382 ++ src/subtitle/demo/karma.conf.js | 44 + src/subtitle/demo/project.json | 90 + src/subtitle/demo/src/app/AppComponent.ts | 7 + src/subtitle/demo/src/app/AppModule.ts | 24 + src/subtitle/demo/src/app/IndexComponent.ts | 49 + src/subtitle/demo/src/app/app.html | 3 + .../app/subtitle/SubtitleBasicComponent.ts | 34 + .../subtitle/SubtitleBeforeSearchComponent.ts | 85 + .../src/app/subtitle/SubtitleDarkComponent.ts | 27 + .../app/subtitle/SubtitleEventComponent.ts | 35 + .../app/subtitle/SubtitleIdkeyComponent.ts | 27 + .../app/subtitle/SubtitleItemsComponent.ts | 29 + .../app/subtitle/SubtitleMaxwidthComponent.ts | 29 + .../subtitle/SubtitlePanelwidthComponent.ts | 23 + .../app/subtitle/SubtitleRouteComponent.ts | 19 + .../subtitle/SubtitleScrollLoadComponent.ts | 74 + .../subtitle/SubtitleSearchableComponent.ts | 23 + .../app/subtitle/SubtitleTargetComponent.ts | 29 + .../src/app/subtitle/SubtitleTestModule.ts | 85 + .../subtitle/SubtitleTipPositionComponent.ts | 29 + .../demo/src/app/subtitle/subtitle-basic.html | 40 + .../app/subtitle/subtitle-before-search.html | 8 + .../demo/src/app/subtitle/subtitle-dark.html | 10 + .../demo/src/app/subtitle/subtitle-event.html | 3 + .../demo/src/app/subtitle/subtitle-idkey.html | 1 + .../demo/src/app/subtitle/subtitle-items.html | 3 + .../src/app/subtitle/subtitle-maxwidth.html | 3 + .../src/app/subtitle/subtitle-panelwidth.html | 1 + .../demo/src/app/subtitle/subtitle-route.html | 14 + .../app/subtitle/subtitle-scroll-load.html | 1 + .../src/app/subtitle/subtitle-searchable.html | 1 + .../src/app/subtitle/subtitle-target.html | 3 + .../app/subtitle/subtitle-tip-position.html | 3 + .../src/app/subtitle/webdoc/subtitle-demos.js | 147 + .../src/app/subtitle/webdoc/subtitle.cn.md | 23 + .../src/app/subtitle/webdoc/subtitle.en.md | 29 + src/subtitle/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/subtitle/demo/src/index.html | 16 + src/subtitle/demo/src/main.ts | 13 + src/subtitle/demo/test.ts | 24 + src/subtitle/demo/tsconfig.app.json | 15 + src/subtitle/demo/tsconfig.spec.json | 20 + src/subtitle/lib/index.ts | 1 + src/subtitle/lib/ng-package.json | 7 + src/subtitle/lib/package.json | 18 + src/subtitle/lib/project.json | 62 + src/subtitle/lib/src/TiSubtitleComponent.ts | 233 + src/subtitle/lib/src/TiSubtitleModule.ts | 29 + src/subtitle/lib/src/subtitle.html | 73 + src/subtitle/lib/src/subtitle.less | 106 + src/swiper/demo/karma.conf.js | 44 + src/swiper/demo/project.json | 90 + src/swiper/demo/src/app/AppComponent.ts | 7 + src/swiper/demo/src/app/AppModule.ts | 24 + src/swiper/demo/src/app/IndexComponent.ts | 49 + src/swiper/demo/src/app/app.html | 3 + .../app/swiper/SwiperActiveindexComponent.ts | 10 + .../src/app/swiper/SwiperAutoplayComponent.ts | 9 + .../src/app/swiper/SwiperBasicComponent.ts | 9 + .../src/app/swiper/SwiperEventsComponent.ts | 14 + .../SwiperIndicatorpositionComponent.ts | 9 + .../src/app/swiper/SwiperLoopComponent.ts | 9 + .../app/swiper/SwiperShowcardnumComponent.ts | 18 + .../swiper/SwiperShowcardnumTestComponent.ts | 32 + .../demo/src/app/swiper/SwiperTestModule.ts | 76 + .../src/app/swiper/swiper-activeindex.html | 10 + .../demo/src/app/swiper/swiper-autoplay.html | 7 + .../demo/src/app/swiper/swiper-basic.html | 7 + .../demo/src/app/swiper/swiper-events.html | 8 + .../app/swiper/swiper-indicatorposition.html | 27 + .../demo/src/app/swiper/swiper-loop.html | 7 + .../app/swiper/swiper-showcardnum-test.html | 36 + .../src/app/swiper/swiper-showcardnum.html | 7 + src/swiper/demo/src/app/swiper/swiper.less | 13 + .../src/app/swiper/webdoc/swiper-demos.js | 96 + .../demo/src/app/swiper/webdoc/swiper.cn.md | 31 + .../demo/src/app/swiper/webdoc/swiper.en.md | 29 + src/swiper/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/swiper/demo/src/index.html | 16 + src/swiper/demo/src/main.ts | 13 + src/swiper/demo/test.ts | 24 + src/swiper/demo/tsconfig.app.json | 15 + src/swiper/demo/tsconfig.spec.json | 20 + src/swiper/lib/index.ts | 1 + src/swiper/lib/ng-package.json | 7 + src/swiper/lib/package.json | 12 + src/swiper/lib/project.json | 62 + src/swiper/lib/src/TiSwiperComponent.ts | 397 ++ src/swiper/lib/src/TiSwiperModule.ts | 27 + src/swiper/lib/src/TiSwipercardComponent.ts | 26 + src/swiper/lib/src/swiper.html | 51 + src/swiper/lib/src/swiper.less | 155 + src/swiper/lib/src/swipercard.less | 4 + src/switch/demo/karma.conf.js | 44 + src/switch/demo/project.json | 90 + src/switch/demo/src/app/AppComponent.ts | 7 + src/switch/demo/src/app/AppModule.ts | 24 + src/switch/demo/src/app/IndexComponent.ts | 49 + src/switch/demo/src/app/app.html | 3 + .../src/app/switch/SwitchBasicComponent.ts | 8 + .../src/app/switch/SwitchBeforeComponent.ts | 17 + .../src/app/switch/SwitchDisabledComponent.ts | 8 + .../src/app/switch/SwitchEventComponent.ts | 21 + .../app/switch/SwitchExplanationComponent.ts | 10 + .../src/app/switch/SwitchFocusComponent.ts | 14 + .../demo/src/app/switch/SwitchIdComponent.ts | 8 + .../src/app/switch/SwitchLoadComponent.ts | 29 + .../src/app/switch/SwitchTemplateComponent.ts | 8 + .../demo/src/app/switch/SwitchTestModule.ts | 69 + .../demo/src/app/switch/switch-basic.html | 1 + .../demo/src/app/switch/switch-before.html | 2 + .../demo/src/app/switch/switch-disabled.html | 1 + .../demo/src/app/switch/switch-event.html | 19 + .../src/app/switch/switch-explanation.html | 1 + .../demo/src/app/switch/switch-focus.html | 29 + src/switch/demo/src/app/switch/switch-id.html | 8 + .../demo/src/app/switch/switch-load.html | 15 + .../demo/src/app/switch/switch-template.html | 4 + .../src/app/switch/webdoc/switch-demos.js | 74 + .../demo/src/app/switch/webdoc/switch.cn.md | 23 + .../demo/src/app/switch/webdoc/switch.en.md | 29 + src/switch/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/switch/demo/src/index.html | 16 + src/switch/demo/src/main.ts | 13 + src/switch/demo/test.ts | 24 + src/switch/demo/tsconfig.app.json | 15 + src/switch/demo/tsconfig.spec.json | 20 + src/switch/lib/index.ts | 1 + src/switch/lib/ng-package.json | 7 + src/switch/lib/package.json | 10 + src/switch/lib/project.json | 62 + src/switch/lib/src/TiSwitchComponent.ts | 113 + src/switch/lib/src/TiSwitchModule.ts | 22 + src/switch/lib/src/switch.html | 30 + src/switch/lib/src/switch.less | 125 + src/tab/demo/karma.conf.js | 44 + src/tab/demo/project.json | 90 + src/tab/demo/src/app/AppComponent.ts | 7 + src/tab/demo/src/app/AppModule.ts | 24 + src/tab/demo/src/app/IndexComponent.ts | 49 + src/tab/demo/src/app/app.html | 3 + src/tab/demo/src/app/tab/TabBasicComponent.ts | 20 + .../app/tab/TabBeforeactivechangeComponent.ts | 34 + .../src/app/tab/TabContentCompComponent.ts | 100 + .../src/app/tab/TabCustomHeadComponent.ts | 27 + src/tab/demo/src/app/tab/TabDarkComponent.ts | 18 + .../src/app/tab/TabDefaultTestComponent.ts | 89 + .../demo/src/app/tab/TabLazyLoadComponent.ts | 19 + .../demo/src/app/tab/TabLevel2Component.ts | 18 + .../src/app/tab/TabLevel2TestComponent.ts | 50 + .../demo/src/app/tab/TabOverflowComponent.ts | 46 + src/tab/demo/src/app/tab/TabRouteComponent.ts | 66 + .../demo/src/app/tab/TabScrollComponent.ts | 31 + src/tab/demo/src/app/tab/TabSmallComponent.ts | 18 + src/tab/demo/src/app/tab/TabTestModule.ts | 102 + src/tab/demo/src/app/tab/tab-basic.html | 11 + .../src/app/tab/tab-beforeactivechange.html | 23 + .../demo/src/app/tab/tab-content-comp.html | 34 + src/tab/demo/src/app/tab/tab-custom-head.html | 17 + src/tab/demo/src/app/tab/tab-dark.html | 11 + .../demo/src/app/tab/tab-default-test.html | 51 + src/tab/demo/src/app/tab/tab-lazy-load.html | 18 + src/tab/demo/src/app/tab/tab-level2-test.html | 24 + src/tab/demo/src/app/tab/tab-level2.html | 11 + src/tab/demo/src/app/tab/tab-overflow.html | 7 + src/tab/demo/src/app/tab/tab-route.html | 8 + src/tab/demo/src/app/tab/tab-scroll.html | 40 + src/tab/demo/src/app/tab/tab-small.html | 11 + src/tab/demo/src/app/tab/webdoc/tab-demos.js | 126 + src/tab/demo/src/app/tab/webdoc/tab.cn.md | 23 + src/tab/demo/src/app/tab/webdoc/tab.en.md | 29 + src/tab/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/tab/demo/src/index.html | 16 + src/tab/demo/src/main.ts | 13 + src/tab/demo/test.ts | 24 + src/tab/demo/tsconfig.app.json | 15 + src/tab/demo/tsconfig.spec.json | 20 + src/tab/lib/index.ts | 1 + src/tab/lib/ng-package.json | 7 + src/tab/lib/package.json | 16 + src/tab/lib/project.json | 62 + src/tab/lib/src/TiTabComponent.ts | 149 + src/tab/lib/src/TiTabHeaderComponent.ts | 66 + src/tab/lib/src/TiTabModule.ts | 31 + src/tab/lib/src/TiTabsComponent.ts | 507 ++ src/tab/lib/src/tab-head.html | 1 + src/tab/lib/src/tab.html | 4 + src/tab/lib/src/tab.less | 19 + src/tab/lib/src/tabs.html | 44 + src/tab/lib/src/tabs.less | 265 + src/table/demo/karma.conf.js | 44 + src/table/demo/project.json | 90 + src/table/demo/src/app/AppComponent.ts | 7 + src/table/demo/src/app/AppModule.ts | 24 + src/table/demo/src/app/IndexComponent.ts | 49 + src/table/demo/src/app/app.html | 3 + .../src/app/table/TableActionmenuComponent.ts | 81 + .../demo/src/app/table/TableBasicComponent.ts | 52 + .../src/app/table/TableBasicTestComponent.ts | 100 + .../src/app/table/TableCellTipComponent.ts | 56 + .../TableCelliconsColsresizableComponent.ts | 77 + .../src/app/table/TableCheckboxComponent.ts | 65 + .../table/TableCheckboxPaginationComponent.ts | 146 + ...ableCheckboxPaginationHeadmenuComponent.ts | 133 + .../src/app/table/TableColAlignComponent.ts | 52 + ...TableColalignSortResizableTestComponent.ts | 72 + .../app/table/TableColsResizableComponent.ts | 59 + .../src/app/table/TableColsToggleComponent.ts | 65 + .../table/TableColsToggleDetailsComponent.ts | 81 + .../app/table/TableColsToggleTestComponent.ts | 103 + .../table/TableColsresizableBasicComponent.ts | 72 + .../TableColsresizableColstoggleComponent.ts | 79 + ...lsresizableColstoggleFixedheadComponent.ts | 79 + .../TableColsresizableLoadfailComponent.ts | 56 + .../table/TableColsresizableSortComponent.ts | 77 + ...bleColsresizableSortHeadfilterComponent.ts | 120 + .../app/table/TableColumnFixedComponent.ts | 80 + .../TableColumnfixedCheckboxComponent.ts | 110 + .../TableColumnfixedColstoggleComponent.ts | 125 + .../table/TableColumnfixedEditrowComponent.ts | 188 + ...xedheadColsresizablePaginationComponent.ts | 99 + .../TableColumnfixedHeadfixedComponent.ts | 112 + .../TableColumnfixedLeftmenuComponent.ts | 119 + .../table/TableColumnfixedNodataComponent.ts | 71 + .../TableColumnfixedPaginationComponent.ts | 109 + .../TableColumnfixedResizableComponent.ts | 85 + .../app/table/TableComprehensiveComponent.ts | 121 + .../TableDetailsCloseotherdetailsComponent.ts | 77 + .../src/app/table/TableDetailsComponent.ts | 55 + .../table/TableDetailsNesttableComponent.ts | 129 + .../table/TableDetailsPaginationComponent.ts | 82 + .../app/table/TableDynamicDetailsComponent.ts | 66 + .../src/app/table/TableEditallComponent.ts | 80 + .../app/table/TableEditallTestComponent.ts | 168 + .../src/app/table/TableEditrowComponent.ts | 123 + .../app/table/TableEditrowTestComponent.ts | 184 + .../src/app/table/TableFilterComponent.ts | 138 + .../app/table/TableFilterStrictComponent.ts | 140 + .../TableFixedHeadColsResizableComponent.ts | 74 + .../src/app/table/TableFixedHeadComponent.ts | 76 + .../TableFixedHeadInAccordionComponent.ts | 76 + .../table/TableFixedHeadNodataComponent.ts | 58 + ...ableFixedHeadPaginationDetailsComponent.ts | 83 + ...ColsresizablePaginationDetailsComponent.ts | 83 + .../app/table/TableFixheadScrollComponent.ts | 81 + .../demo/src/app/table/TableGroupComponent.ts | 151 + .../demo/src/app/table/TableGuideComponent.ts | 55 + .../src/app/table/TableHeadFilterComponent.ts | 183 + .../table/TableHeadFilterDatetimeComponent.ts | 124 + .../TableHeadFilterDatetimeTestComponent.ts | 213 + .../table/TableHeadFilterMultiComponent.ts | 152 + .../TableHeadFilterMultiValuekeyComponent.ts | 222 + .../app/table/TableHeadFilterTestComponent.ts | 184 + .../table/TableHeadFilterValuekeyComponent.ts | 226 + .../TableHeadFilterVirtualscrollComponent.ts | 178 + .../src/app/table/TableLoadFailComponent.ts | 56 + .../src/app/table/TableNodataComponent.ts | 28 + .../app/table/TableNodataSimpleComponent.ts | 50 + .../src/app/table/TableNodataTestComponent.ts | 50 + .../app/table/TableOverflowLinkComponent.ts | 90 + .../app/table/TablePagiWithFilterComponent.ts | 95 + .../src/app/table/TablePaginationComponent.ts | 70 + .../demo/src/app/table/TableRadioComponent.ts | 65 + .../src/app/table/TableRadioTestComponent.ts | 108 + .../src/app/table/TableRowDrag2Component.ts | 77 + .../src/app/table/TableRowspanComponent.ts | 58 + .../src/app/table/TableSearchComponent.ts | 103 + .../src/app/table/TableServerPagiComponent.ts | 111 + .../TableServerPagiSearchSortComponent.ts | 152 + .../demo/src/app/table/TableSmallComponent.ts | 52 + .../src/app/table/TableSoldoutComponent.ts | 91 + .../src/app/table/TableSortBasicComponent.ts | 93 + .../app/table/TableSortComparefnComponent.ts | 85 + .../TableSortComparefnLocaleComponent.ts | 82 + .../demo/src/app/table/TableSortComponent.ts | 72 + .../app/table/TableSortDetailsComponent.ts | 89 + .../src/app/table/TableSortResetComponent.ts | 71 + .../src/app/table/TableSortTestComponent.ts | 93 + .../src/app/table/TableStorageComponent.ts | 69 + .../app/table/TableStorageConfigComponent.ts | 101 + .../app/table/TableStorageFilterComponent.ts | 123 + .../app/table/TableStorageServeComponent.ts | 115 + .../demo/src/app/table/TableTestModule.ts | 642 ++ .../demo/src/app/table/TableTreeComponent.ts | 310 + .../table/TableTreeMulitiselectComponent.ts | 356 ++ .../table/TableTreeUnknowdeepthComponent.ts | 151 + .../table/TableVirtualscrollBasicComponent.ts | 88 + .../app/table/TableVirtualscrollComponent.ts | 64 + ...ableVirtualscrollComprehensiveComponent.ts | 139 + .../table/TableVirtualscrollSizesComponent.ts | 88 + .../table/TableVirtualscrollTreeComponent.ts | 308 + .../demo/src/app/table/table-actionmenu.html | 20 + .../demo/src/app/table/table-basic-test.html | 43 + src/table/demo/src/app/table/table-basic.html | 17 + .../demo/src/app/table/table-cell-tip.html | 20 + .../table/table-cellicons-colsresizable.html | 62 + .../table-checkbox-pagination-headmenu.html | 69 + .../app/table/table-checkbox-pagination.html | 87 + .../demo/src/app/table/table-checkbox.html | 27 + .../demo/src/app/table/table-col-align.html | 20 + .../table-colalign-sort-resizable-test.html | 46 + .../src/app/table/table-cols-resizable.html | 34 + .../app/table/table-cols-toggle-details.html | 52 + .../src/app/table/table-cols-toggle-test.html | 55 + .../demo/src/app/table/table-cols-toggle.html | 21 + .../app/table/table-colsresizable-basic.html | 36 + ...le-colsresizable-colstoggle-fixedhead.html | 52 + .../table/table-colsresizable-colstoggle.html | 36 + .../table/table-colsresizable-loadfail.html | 34 + .../table-colsresizable-sort-headfilter.html | 67 + .../app/table/table-colsresizable-sort.html | 43 + .../src/app/table/table-column-fixed.html | 22 + .../app/table/table-columnfixed-checkbox.html | 41 + .../table/table-columnfixed-colstoggle.html | 49 + .../app/table/table-columnfixed-editrow.html | 95 + ...ed-fixedhead-colsresizable-pagination.html | 44 + .../table/table-columnfixed-headfixed.html | 31 + .../app/table/table-columnfixed-leftmenu.html | 54 + .../app/table/table-columnfixed-nodata.html | 35 + .../table/table-columnfixed-pagination.html | 33 + .../table/table-columnfixed-resizable.html | 37 + .../src/app/table/table-comprehensive.html | 82 + .../table-details-closeotherdetails.html | 51 + .../app/table/table-details-nesttable.html | 33 + .../app/table/table-details-pagination.html | 52 + .../demo/src/app/table/table-details.html | 34 + .../src/app/table/table-dynamic-details.html | 33 + .../src/app/table/table-editall-test.html | 60 + .../demo/src/app/table/table-editall.html | 34 + .../src/app/table/table-editrow-test.html | 87 + .../demo/src/app/table/table-editrow.html | 39 + .../src/app/table/table-filter-strict.html | 93 + .../demo/src/app/table/table-filter.html | 85 + .../table-fixed-head-cols-resizable.html | 41 + .../table/table-fixed-head-in-accordion.html | 49 + .../app/table/table-fixed-head-nodata.html | 187 + .../table-fixed-head-pagination-details.html | 72 + .../demo/src/app/table/table-fixed-head.html | 28 + ...head-colsresizable-pagination-details.html | 72 + .../src/app/table/table-fixhead-scroll.html | 51 + src/table/demo/src/app/table/table-group.html | 42 + src/table/demo/src/app/table/table-guide.html | 65 + .../table-head-filter-datetime-test.html | 65 + .../app/table/table-head-filter-datetime.html | 37 + .../table-head-filter-multi-valuekey.html | 59 + .../app/table/table-head-filter-multi.html | 57 + .../src/app/table/table-head-filter-test.html | 84 + .../app/table/table-head-filter-valuekey.html | 52 + .../table-head-filter-virtualscroll.html | 55 + .../demo/src/app/table/table-head-filter.html | 55 + .../demo/src/app/table/table-load-fail.html | 65 + .../src/app/table/table-nodata-simple.html | 28 + .../demo/src/app/table/table-nodata-test.html | 57 + .../demo/src/app/table/table-nodata.html | 22 + .../src/app/table/table-overflow-link.html | 49 + .../src/app/table/table-pagi-with-filter.html | 36 + .../demo/src/app/table/table-pagination.html | 19 + .../demo/src/app/table/table-radio-test.html | 46 + src/table/demo/src/app/table/table-radio.html | 24 + .../demo/src/app/table/table-row-drag2.html | 25 + .../demo/src/app/table/table-rowspan.html | 17 + .../demo/src/app/table/table-search.html | 70 + .../table/table-server-pagi-search-sort.html | 46 + .../demo/src/app/table/table-server-pagi.html | 32 + src/table/demo/src/app/table/table-small.html | 17 + .../demo/src/app/table/table-soldout.html | 46 + .../demo/src/app/table/table-sort-basic.html | 51 + .../table/table-sort-comparefn-locale.html | 50 + .../src/app/table/table-sort-comparefn.html | 47 + .../src/app/table/table-sort-details.html | 67 + .../demo/src/app/table/table-sort-reset.html | 32 + .../demo/src/app/table/table-sort-test.html | 37 + src/table/demo/src/app/table/table-sort.html | 34 + .../src/app/table/table-storage-config.html | 49 + .../src/app/table/table-storage-filter.html | 49 + .../src/app/table/table-storage-serve.html | 34 + .../demo/src/app/table/table-storage.html | 29 + .../app/table/table-tree-mulitiselect.html | 76 + .../app/table/table-tree-unknowdeepth.html | 21 + src/table/demo/src/app/table/table-tree.html | 44 + .../app/table/table-virtualscroll-basic.html | 65 + .../table-virtualscroll-comprehensive.html | 106 + .../app/table/table-virtualscroll-sizes.html | 204 + .../app/table/table-virtualscroll-tree.html | 67 + .../src/app/table/table-virtualscroll.html | 29 + src/table/demo/src/app/table/tableTest.less | 19 + .../demo/src/app/table/webdoc/table-demos.js | 504 ++ .../demo/src/app/table/webdoc/table.cn.md | 44 + .../demo/src/app/table/webdoc/table.en.md | 29 + src/table/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/table/demo/src/index.html | 16 + src/table/demo/src/main.ts | 13 + src/table/demo/test.ts | 24 + src/table/demo/tsconfig.app.json | 15 + src/table/demo/tsconfig.spec.json | 20 + src/table/lib/index.ts | 1 + src/table/lib/ng-package.json | 7 + src/table/lib/package.json | 28 + src/table/lib/project.json | 62 + src/table/lib/src/TiCellIconsComponent.ts | 29 + src/table/lib/src/TiCellTextComponent.ts | 87 + src/table/lib/src/TiColClickDirective.ts | 43 + src/table/lib/src/TiColsResizableDirective.ts | 604 ++ src/table/lib/src/TiColsToggleComponent.ts | 185 + .../lib/src/TiColsToggleDropComponent.ts | 157 + src/table/lib/src/TiColspanDirective.ts | 55 + src/table/lib/src/TiColumnFixedDirective.ts | 266 + src/table/lib/src/TiColumnsPipe.ts | 24 + src/table/lib/src/TiDetailsIconComponent.ts | 118 + src/table/lib/src/TiDetailsTrDirective.ts | 75 + src/table/lib/src/TiHeadFilterComponent.ts | 154 + .../lib/src/TiHeadFilterDropComponent.ts | 270 + src/table/lib/src/TiHeadMenuComponent.ts | 98 + src/table/lib/src/TiHeadSortComponent.ts | 110 + src/table/lib/src/TiTableComponent.ts | 1088 ++++ src/table/lib/src/TiTableFixedHeadService.ts | 290 + .../lib/src/TiTableFixedHeadServiceModule.ts | 19 + .../TiTableFixedSizeVirtualScrollDirective.ts | 44 + src/table/lib/src/TiTableModule.ts | 133 + .../TiTableVirtualScrollViewportComponent.ts | 159 + src/table/lib/src/cell-icons.html | 1 + src/table/lib/src/cell-text.html | 3 + src/table/lib/src/cols-toggle-drop.html | 31 + src/table/lib/src/cols-toggle.html | 22 + src/table/lib/src/details-icon.html | 7 + src/table/lib/src/head-filter-drop.html | 138 + src/table/lib/src/head-filter-drop.less | 40 + src/table/lib/src/head-filter.html | 27 + src/table/lib/src/head-filter.less | 20 + src/table/lib/src/head-menu.html | 15 + src/table/lib/src/head-menu.less | 26 + src/table/lib/src/head-sort.html | 3 + src/table/lib/src/i18n/TiTableWords.ts | 8 + src/table/lib/src/i18n/en_US.ts | 10 + src/table/lib/src/i18n/es_US.ts | 10 + src/table/lib/src/i18n/fr_FR.ts | 10 + src/table/lib/src/i18n/index.ts | 7 + src/table/lib/src/i18n/pt_BR.ts | 10 + src/table/lib/src/i18n/zh_CN.ts | 10 + .../src/table-nodata-small-nest-resize.less | 198 + .../lib/src/table-toggle-sort-details.less | 124 + src/table/lib/src/table-tree-fix.less | 145 + .../src/table-virtual-scroll-viewport.html | 3 + .../src/table-virtual-scroll-viewport.less | 34 + src/table/lib/src/table.html | 1 + src/table/lib/src/table.less | 187 + src/tag/demo/karma.conf.js | 44 + src/tag/demo/project.json | 90 + src/tag/demo/src/app/AppComponent.ts | 7 + src/tag/demo/src/app/AppModule.ts | 24 + src/tag/demo/src/app/IndexComponent.ts | 49 + src/tag/demo/src/app/app.html | 3 + src/tag/demo/src/app/tag/TagBasicComponent.ts | 12 + .../demo/src/app/tag/TagDefaultComponent.ts | 23 + .../demo/src/app/tag/TagDisabledComponent.ts | 6 + src/tag/demo/src/app/tag/TagEditComponent.ts | 62 + src/tag/demo/src/app/tag/TagTestModule.ts | 48 + src/tag/demo/src/app/tag/tag-basic.html | 1 + src/tag/demo/src/app/tag/tag-default.html | 25 + src/tag/demo/src/app/tag/tag-disabled.html | 1 + src/tag/demo/src/app/tag/tag-edit.html | 9 + src/tag/demo/src/app/tag/webdoc/tag-demos.js | 40 + src/tag/demo/src/app/tag/webdoc/tag.cn.md | 23 + src/tag/demo/src/app/tag/webdoc/tag.en.md | 29 + src/tag/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/tag/demo/src/index.html | 16 + src/tag/demo/src/main.ts | 13 + src/tag/demo/test.ts | 24 + src/tag/demo/tsconfig.app.json | 15 + src/tag/demo/tsconfig.spec.json | 20 + src/tag/lib/index.ts | 1 + src/tag/lib/ng-package.json | 7 + src/tag/lib/package.json | 12 + src/tag/lib/project.json | 62 + src/tag/lib/src/TiTagComponent.ts | 72 + src/tag/lib/src/TiTagModule.ts | 23 + src/tag/lib/src/tag-arrow.less | 44 + src/tag/lib/src/tag-rect.less | 66 + src/tag/lib/src/tag.html | 2 + src/tagsinput/demo/karma.conf.js | 44 + src/tagsinput/demo/project.json | 90 + src/tagsinput/demo/src/app/AppComponent.ts | 7 + src/tagsinput/demo/src/app/AppModule.ts | 24 + src/tagsinput/demo/src/app/IndexComponent.ts | 49 + src/tagsinput/demo/src/app/app.html | 3 + .../src/app/tagsinput/TagsInputTestModule.ts | 105 + .../app/tagsinput/TagsinputBasicComponent.ts | 18 + .../tagsinput/TagsinputDisabledComponent.ts | 18 + .../app/tagsinput/TagsinputEventsComponent.ts | 23 + .../tagsinput/TagsinputLabelkeyComponent.ts | 16 + .../tagsinput/TagsinputMaxlengthComponent.ts | 18 + .../app/tagsinput/TagsinputNullComponent.ts | 10 + .../tagsinput/TagsinputPanelwidthComponent.ts | 21 + .../tagsinput/TagsinputReactiveComponent.ts | 25 + .../tagsinput/TagsinputSeparatorsComponent.ts | 18 + .../tagsinput/TagsinputSuggestionComponent.ts | 18 + .../tagsinput/TagsinputTemplateComponent.ts | 18 + .../app/tagsinput/TagsinputValidComponent.ts | 18 + .../tagsinput/TagsinputValuekeyComponent.ts | 16 + .../src/app/tagsinput/tagsinput-basic.html | 10 + .../src/app/tagsinput/tagsinput-disabled.html | 7 + .../src/app/tagsinput/tagsinput-events.html | 8 + .../src/app/tagsinput/tagsinput-labelkey.html | 1 + .../app/tagsinput/tagsinput-maxlength.html | 11 + .../src/app/tagsinput/tagsinput-null.html | 17 + .../app/tagsinput/tagsinput-panelwidth.html | 1 + .../src/app/tagsinput/tagsinput-reactive.html | 3 + .../app/tagsinput/tagsinput-separators.html | 10 + .../app/tagsinput/tagsinput-suggestion.html | 1 + .../src/app/tagsinput/tagsinput-template.html | 5 + .../src/app/tagsinput/tagsinput-valid.html | 1 + .../src/app/tagsinput/tagsinput-valuekey.html | 4 + .../app/tagsinput/webdoc/tagsinput-demos.js | 135 + .../src/app/tagsinput/webdoc/tagsinput.cn.md | 40 + .../src/app/tagsinput/webdoc/tagsinput.en.md | 29 + src/tagsinput/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/tagsinput/demo/src/index.html | 16 + src/tagsinput/demo/src/main.ts | 13 + src/tagsinput/demo/test.ts | 24 + src/tagsinput/demo/tsconfig.app.json | 15 + src/tagsinput/demo/tsconfig.spec.json | 20 + src/tagsinput/lib/index.ts | 1 + src/tagsinput/lib/ng-package.json | 7 + src/tagsinput/lib/package.json | 15 + src/tagsinput/lib/project.json | 62 + src/tagsinput/lib/src/TiTagsInputComponent.ts | 479 ++ src/tagsinput/lib/src/TiTagsInputModule.ts | 28 + src/tagsinput/lib/src/tagsinput.html | 63 + src/tagsinput/lib/src/tagsinput.less | 37 + src/text/demo/karma.conf.js | 44 + src/text/demo/project.json | 90 + src/text/demo/src/app/AppComponent.ts | 7 + src/text/demo/src/app/AppModule.ts | 24 + src/text/demo/src/app/IndexComponent.ts | 49 + src/text/demo/src/app/app.html | 3 + .../demo/src/app/text/TextBasicComponent.ts | 8 + .../demo/src/app/text/TextClearComponent.ts | 8 + .../src/app/text/TextDisabledComponent.ts | 9 + .../demo/src/app/text/TextEventsComponent.ts | 25 + .../demo/src/app/text/TextFocusComponent.ts | 8 + .../src/app/text/TextMaskinputComponent.ts | 9 + .../src/app/text/TextNoborderTestComponent.ts | 9 + .../src/app/text/TextPasswordComponent.ts | 8 + .../app/text/TextPasswordVisibleComponent.ts | 21 + .../src/app/text/TextReactiveComponent.ts | 16 + .../src/app/text/TextReadonlyComponent.ts | 8 + src/text/demo/src/app/text/TextTestModule.ts | 90 + src/text/demo/src/app/text/text-basic.html | 4 + src/text/demo/src/app/text/text-clear.html | 1 + src/text/demo/src/app/text/text-disabled.html | 1 + src/text/demo/src/app/text/text-events.html | 13 + src/text/demo/src/app/text/text-focus.html | 1 + .../demo/src/app/text/text-maskinput.html | 4 + .../demo/src/app/text/text-noborder-test.html | 2 + .../src/app/text/text-password-visible.html | 12 + src/text/demo/src/app/text/text-password.html | 5 + src/text/demo/src/app/text/text-reactive.html | 6 + src/text/demo/src/app/text/text-readonly.html | 1 + src/text/demo/src/app/text/text.spec.ts | 54 + .../demo/src/app/text/webdoc/text-demos.js | 131 + src/text/demo/src/app/text/webdoc/text.cn.md | 29 + src/text/demo/src/app/text/webdoc/text.en.md | 29 + src/text/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/text/demo/src/index.html | 16 + src/text/demo/src/main.ts | 13 + src/text/demo/test.ts | 24 + src/text/demo/tsconfig.app.json | 15 + src/text/demo/tsconfig.spec.json | 20 + src/text/lib/index.ts | 1 + src/text/lib/ng-package.json | 7 + src/text/lib/package.json | 13 + src/text/lib/project.json | 62 + src/text/lib/src/TiMaskDirective.ts | 283 + src/text/lib/src/TiTextComponent.ts | 389 ++ src/text/lib/src/TiTextModule.ts | 25 + src/text/lib/src/clear.svg | 1 + src/text/lib/src/invisible.svg | 1 + src/text/lib/src/text.less | 101 + src/text/lib/src/visible.svg | 1 + src/textarea/demo/karma.conf.js | 44 + src/textarea/demo/project.json | 90 + src/textarea/demo/src/app/AppComponent.ts | 7 + src/textarea/demo/src/app/AppModule.ts | 24 + src/textarea/demo/src/app/IndexComponent.ts | 49 + src/textarea/demo/src/app/app.html | 3 + .../textarea/TextareaAutofocusComponent.ts | 9 + .../app/textarea/TextareaDisabledComponent.ts | 9 + .../textarea/TextareaMaxlengthComponent.ts | 10 + .../src/app/textarea/TextareaNoneComponent.ts | 12 + .../app/textarea/TextareaResizeComponent.ts | 11 + .../app/textarea/TextareaScrollComponent.ts | 20 + .../src/app/textarea/TextareaTestModule.ts | 63 + .../app/textarea/TextareaValidComponent.ts | 15 + .../app/textarea/TextareaWidthComponent.ts | 9 + .../src/app/textarea/textarea-autofocus.html | 11 + .../src/app/textarea/textarea-disabled.html | 7 + .../src/app/textarea/textarea-maxlength.html | 8 + .../demo/src/app/textarea/textarea-none.html | 18 + .../src/app/textarea/textarea-resize.html | 5 + .../src/app/textarea/textarea-scroll.html | 19 + .../demo/src/app/textarea/textarea-valid.html | 8 + .../demo/src/app/textarea/textarea-width.html | 7 + .../src/app/textarea/webdoc/textarea-demos.js | 54 + .../src/app/textarea/webdoc/textarea.cn.md | 27 + .../src/app/textarea/webdoc/textarea.en.md | 29 + src/textarea/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/textarea/demo/src/index.html | 16 + src/textarea/demo/src/main.ts | 13 + src/textarea/demo/test.ts | 24 + src/textarea/demo/tsconfig.app.json | 15 + src/textarea/demo/tsconfig.spec.json | 20 + src/textarea/lib/index.ts | 1 + src/textarea/lib/ng-package.json | 7 + src/textarea/lib/package.json | 14 + src/textarea/lib/project.json | 62 + src/textarea/lib/src/TiFormatNumPipe.ts | 26 + src/textarea/lib/src/TiTextareaComponent.ts | 456 ++ src/textarea/lib/src/TiTextareaModule.ts | 24 + src/textarea/lib/src/textarea.html | 5 + src/textarea/lib/src/textarea.less | 144 + src/themes/README.md | 30 + src/themes/basic/base-all.less | 6 + src/themes/basic/basic-var.css | 346 ++ src/themes/basic/build.less | 4 + src/themes/basic/common.less | 21 + .../basic/compnent-container-border.less | 33 + src/themes/basic/img/table-loadfail-bg.png | Bin 0 -> 3437 bytes src/themes/basic/img/table-nodata-bg.png | Bin 0 -> 3430 bytes src/themes/basic/img/upload-image-delete.png | Bin 0 -> 400 bytes src/themes/basic/img/upload-image-error.png | Bin 0 -> 1097 bytes src/themes/basic/img/upload-image-preview.png | Bin 0 -> 230 bytes src/themes/basic/link-no-decoration.less | 10 + src/themes/basic/mixins.less | 242 + src/themes/basic/normalize.less | 201 + src/themes/package.json | 5 + src/themes/project.json | 38 + src/themes/theme-blue/basic-var.less | 16 + src/themes/theme-blue/build.less | 5 + src/themes/theme-default/build.less | 3 + src/themes/theme-green/basic-var.less | 16 + src/themes/theme-green/build.less | 5 + src/themes/theme-purple/basic-var.less | 16 + src/themes/theme-purple/build.less | 5 + src/themes/theme-red/basic-var.less | 16 + src/themes/theme-red/build.less | 5 + src/time/demo/karma.conf.js | 44 + src/time/demo/project.json | 90 + src/time/demo/src/app/AppComponent.ts | 7 + src/time/demo/src/app/AppModule.ts | 24 + src/time/demo/src/app/IndexComponent.ts | 49 + src/time/demo/src/app/app.html | 3 + .../src/app/time/TimeCleariconComponent.ts | 7 + .../src/app/time/TimeDisabledComponent.ts | 7 + .../demo/src/app/time/TimeEventComponent.ts | 12 + .../demo/src/app/time/TimeFormatComponent.ts | 14 + .../demo/src/app/time/TimeMaxComponent.ts | 40 + .../demo/src/app/time/TimeMaxminComponent.ts | 11 + .../demo/src/app/time/TimeMinComponent.ts | 43 + .../app/time/TimeOptionDisabledComponent.ts | 23 + .../src/app/time/TimePanelalignComponent.ts | 7 + .../src/app/time/TimeReactiveComponent.ts | 16 + src/time/demo/src/app/time/TimeTestModule.ts | 94 + .../src/app/time/TimeValidationComponent.ts | 8 + .../demo/src/app/time/time-clearicon.html | 1 + src/time/demo/src/app/time/time-disabled.html | 1 + src/time/demo/src/app/time/time-event.html | 3 + src/time/demo/src/app/time/time-format.html | 10 + src/time/demo/src/app/time/time-max.html | 16 + src/time/demo/src/app/time/time-maxmin.html | 4 + src/time/demo/src/app/time/time-min.html | 17 + .../src/app/time/time-option-disabled.html | 21 + .../demo/src/app/time/time-panelalign.html | 1 + src/time/demo/src/app/time/time-reactive.html | 6 + .../demo/src/app/time/time-validation.html | 1 + .../demo/src/app/time/webdoc/time-demos.js | 100 + src/time/demo/src/app/time/webdoc/time.cn.md | 23 + src/time/demo/src/app/time/webdoc/time.en.md | 29 + src/time/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/time/demo/src/index.html | 16 + src/time/demo/src/main.ts | 13 + src/time/demo/test.ts | 24 + src/time/demo/tsconfig.app.json | 15 + src/time/demo/tsconfig.spec.json | 20 + src/time/lib/index.ts | 1 + src/time/lib/ng-package.json | 7 + src/time/lib/package.json | 18 + src/time/lib/project.json | 62 + src/time/lib/src/TiTimeComponent.ts | 943 +++ src/time/lib/src/TiTimeModule.ts | 34 + src/time/lib/src/i18n/TiTimeWords.ts | 7 + src/time/lib/src/i18n/en_US.ts | 9 + src/time/lib/src/i18n/es_US.ts | 9 + src/time/lib/src/i18n/fr_FR.ts | 9 + src/time/lib/src/i18n/index.ts | 7 + src/time/lib/src/i18n/pt_BR.ts | 9 + src/time/lib/src/i18n/zh_CN.ts | 9 + src/time/lib/src/time.html | 93 + src/time/lib/src/time.less | 89 + src/timeline/demo/karma.conf.js | 44 + src/timeline/demo/project.json | 90 + src/timeline/demo/src/app/AppComponent.ts | 7 + src/timeline/demo/src/app/AppModule.ts | 24 + src/timeline/demo/src/app/IndexComponent.ts | 49 + src/timeline/demo/src/app/app.html | 3 + .../app/timeline/TimelineBasicComponent.ts | 25 + .../src/app/timeline/TimelineDarkComponent.ts | 56 + .../app/timeline/TimelineHelptipComponent.ts | 58 + .../app/timeline/TimelineMultiComponent.ts | 56 + .../app/timeline/TimelineTempleteComponent.ts | 34 + .../src/app/timeline/TimelineTestComponent.ts | 113 + .../src/app/timeline/TimelineTestModule.ts | 63 + .../src/app/timeline/TimelineTypeComponent.ts | 34 + .../demo/src/app/timeline/timeline-basic.html | 1 + .../demo/src/app/timeline/timeline-dark.html | 3 + .../src/app/timeline/timeline-helptip.html | 1 + .../demo/src/app/timeline/timeline-multi.html | 1 + .../src/app/timeline/timeline-templete.html | 6 + .../demo/src/app/timeline/timeline-test.html | 23 + .../demo/src/app/timeline/timeline-type.html | 1 + .../src/app/timeline/webdoc/timeline-demos.js | 89 + .../src/app/timeline/webdoc/timeline.cn.md | 23 + .../src/app/timeline/webdoc/timeline.en.md | 29 + src/timeline/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/timeline/demo/src/index.html | 16 + src/timeline/demo/src/main.ts | 13 + src/timeline/demo/test.ts | 24 + src/timeline/demo/tsconfig.app.json | 15 + src/timeline/demo/tsconfig.spec.json | 20 + src/timeline/lib/index.ts | 1 + src/timeline/lib/ng-package.json | 7 + src/timeline/lib/package.json | 13 + src/timeline/lib/project.json | 62 + src/timeline/lib/src/TiTimelineComponent.ts | 165 + src/timeline/lib/src/TiTimelineModule.ts | 24 + src/timeline/lib/src/timeline.html | 60 + src/timeline/lib/src/timeline.less | 220 + src/tip/demo/karma.conf.js | 44 + src/tip/demo/project.json | 90 + src/tip/demo/src/app/AppComponent.ts | 7 + src/tip/demo/src/app/AppModule.ts | 24 + src/tip/demo/src/app/IndexComponent.ts | 49 + src/tip/demo/src/app/app.html | 3 + src/tip/demo/src/app/tip/TipBasicComponent.ts | 8 + .../src/app/tip/TipContentCompComponent.ts | 51 + .../app/tip/TipContentTemplateComponent.ts | 9 + src/tip/demo/src/app/tip/TipEmptyComponent.ts | 59 + .../demo/src/app/tip/TipHasArrowComponent.ts | 6 + .../app/tip/TipLongTextPositionComponent.ts | 63 + .../demo/src/app/tip/TipMaxWidthComponent.ts | 8 + .../demo/src/app/tip/TipPositionComponent.ts | 19 + .../src/app/tip/TipPositionTestComponent.ts | 8 + .../demo/src/app/tip/TipServiceComponent.ts | 42 + .../src/app/tip/TipServiceDestroyComponent.ts | 32 + src/tip/demo/src/app/tip/TipTestModule.ts | 111 + .../demo/src/app/tip/TipTriggerComponent.ts | 15 + .../app/tip/TipValidPositionTestComponent.ts | 22 + .../demo/src/app/tip/TipZindexComponent.ts | 15 + src/tip/demo/src/app/tip/tip-basic.html | 1 + .../demo/src/app/tip/tip-content-comp.html | 2 + .../src/app/tip/tip-content-template.html | 8 + src/tip/demo/src/app/tip/tip-empty.html | 21 + src/tip/demo/src/app/tip/tip-has-arrow.html | 1 + .../src/app/tip/tip-long-text-position.html | 17 + src/tip/demo/src/app/tip/tip-max-width.html | 1 + .../demo/src/app/tip/tip-position-test.html | 18 + src/tip/demo/src/app/tip/tip-position.html | 23 + .../demo/src/app/tip/tip-service-destroy.html | 9 + src/tip/demo/src/app/tip/tip-service.html | 2 + src/tip/demo/src/app/tip/tip-trigger.html | 1 + .../src/app/tip/tip-valid-position-test.html | 17 + src/tip/demo/src/app/tip/tip-zindex.html | 6 + src/tip/demo/src/app/tip/tipTest.less | 38 + src/tip/demo/src/app/tip/webdoc/tip-demos.js | 123 + src/tip/demo/src/app/tip/webdoc/tip.cn.md | 23 + src/tip/demo/src/app/tip/webdoc/tip.en.md | 29 + src/tip/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/tip/demo/src/index.html | 16 + src/tip/demo/src/main.ts | 13 + src/tip/demo/test.ts | 24 + src/tip/demo/tsconfig.app.json | 15 + src/tip/demo/tsconfig.spec.json | 20 + src/tip/lib/index.ts | 4 + src/tip/lib/ng-package.json | 7 + src/tip/lib/package.json | 13 + src/tip/lib/project.json | 62 + src/tip/lib/src/TiTipContainerComponent.ts | 29 + src/tip/lib/src/TiTipDirective.ts | 141 + src/tip/lib/src/TiTipInterface.ts | 121 + src/tip/lib/src/TiTipModule.ts | 23 + src/tip/lib/src/TiTipService.ts | 343 ++ src/tip/lib/src/TiTipServiceModule.ts | 26 + src/tip/lib/src/tip.less | 343 ++ src/transfer/demo/karma.conf.js | 44 + src/transfer/demo/project.json | 90 + src/transfer/demo/src/app/AppComponent.ts | 7 + src/transfer/demo/src/app/AppModule.ts | 24 + src/transfer/demo/src/app/IndexComponent.ts | 49 + src/transfer/demo/src/app/app.html | 3 + .../app/transfer/TransferBasicComponent.ts | 9 + .../app/transfer/TransferDisabledComponent.ts | 44 + .../app/transfer/TransferEventComponent.ts | 22 + .../src/app/transfer/TransferIdComponent.ts | 38 + .../app/transfer/TransferIdkeyComponent.ts | 55 + .../app/transfer/TransferLabelkeyComponent.ts | 10 + .../src/app/transfer/TransferLazyComponent.ts | 21 + .../src/app/transfer/TransferLoadComponent.ts | 85 + .../transfer/TransferNodatatextComponent.ts | 10 + .../transfer/TransferPaginationComponent.ts | 34 + .../transfer/TransferPlaceholderComponent.ts | 16 + .../transfer/TransferSearchableComponent.ts | 10 + .../transfer/TransferSearchkeysComponent.ts | 19 + .../src/app/transfer/TransferSizeComponent.ts | 9 + .../app/transfer/TransferTableComponent.ts | 56 + .../src/app/transfer/TransferTestModule.ts | 122 + .../app/transfer/TransferTitlesComponent.ts | 9 + src/transfer/demo/src/app/transfer/data.js | 81 + .../demo/src/app/transfer/transfer-basic.html | 4 + .../src/app/transfer/transfer-disabled.html | 1 + .../demo/src/app/transfer/transfer-event.html | 9 + .../demo/src/app/transfer/transfer-id.html | 15 + .../demo/src/app/transfer/transfer-idkey.html | 1 + .../src/app/transfer/transfer-labelkey.html | 1 + .../demo/src/app/transfer/transfer-lazy.html | 1 + .../demo/src/app/transfer/transfer-load.html | 14 + .../src/app/transfer/transfer-nodatatext.html | 1 + .../src/app/transfer/transfer-pagination.html | 11 + .../app/transfer/transfer-placeholder.html | 7 + .../src/app/transfer/transfer-searchable.html | 1 + .../src/app/transfer/transfer-searchkeys.html | 8 + .../demo/src/app/transfer/transfer-size.html | 1 + .../demo/src/app/transfer/transfer-table.html | 37 + .../src/app/transfer/transfer-titles.html | 1 + .../demo/src/app/transfer/transfer.less | 4 + .../src/app/transfer/webdoc/transfer-demos.js | 248 + .../src/app/transfer/webdoc/transfer.cn.md | 23 + .../src/app/transfer/webdoc/transfer.en.md | 29 + src/transfer/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/transfer/demo/src/index.html | 16 + src/transfer/demo/src/main.ts | 13 + src/transfer/demo/test.ts | 24 + src/transfer/demo/tsconfig.app.json | 15 + src/transfer/demo/tsconfig.spec.json | 20 + src/transfer/lib/index.ts | 1 + src/transfer/lib/ng-package.json | 7 + src/transfer/lib/package.json | 22 + src/transfer/lib/project.json | 62 + src/transfer/lib/src/TiTransferColumn.ts | 19 + src/transfer/lib/src/TiTransferComponent.ts | 325 + src/transfer/lib/src/TiTransferModule.ts | 54 + src/transfer/lib/src/i18n/TiTransferWords.ts | 7 + src/transfer/lib/src/i18n/en_US.ts | 9 + src/transfer/lib/src/i18n/es_US.ts | 9 + src/transfer/lib/src/i18n/fr_FR.ts | 9 + src/transfer/lib/src/i18n/index.ts | 7 + src/transfer/lib/src/i18n/pt_BR.ts | 9 + src/transfer/lib/src/i18n/zh_CN.ts | 9 + src/transfer/lib/src/transfer.html | 73 + src/transfer/lib/src/transfer.less | 75 + src/transfer/lib/src/transferUtil.ts | 27 + .../transferlist/TiTransferListComponent.ts | 404 ++ .../lib/src/transferlist/transfer-list.html | 153 + .../lib/src/transferlist/transfer-list.less | 156 + src/tree/demo/karma.conf.js | 44 + src/tree/demo/project.json | 90 + src/tree/demo/src/app/AppComponent.ts | 7 + src/tree/demo/src/app/AppModule.ts | 24 + src/tree/demo/src/app/IndexComponent.ts | 49 + src/tree/demo/src/app/app.html | 3 + .../src/app/tree/TreeBeforeExpandComponent.ts | 70 + .../src/app/tree/TreeBeforeMoreComponent.ts | 100 + .../tree/TreeChangedbycheckboxComponent.ts | 46 + .../app/tree/TreeCheckRelationComponent.ts | 55 + .../src/app/tree/TreeDisabledComponent.ts | 48 + .../app/tree/TreeDragBeforedropComponent.ts | 95 + .../demo/src/app/tree/TreeDragComponent.ts | 84 + .../demo/src/app/tree/TreeEventComponent.ts | 74 + .../demo/src/app/tree/TreeIconComponent.ts | 106 + .../demo/src/app/tree/TreeLoadComponent.ts | 132 + .../demo/src/app/tree/TreeManyComponent.ts | 169 + .../src/app/tree/TreeMultiselectComponent.ts | 57 + .../demo/src/app/tree/TreeOperateComponent.ts | 110 + .../app/tree/TreeParentcheckableComponent.ts | 48 + .../src/app/tree/TreeRadioselectComponent.ts | 84 + .../demo/src/app/tree/TreeSearchComponent.ts | 238 + .../src/app/tree/TreeShortcutkeyComponent.ts | 88 + .../demo/src/app/tree/TreeSmallComponent.ts | 58 + .../src/app/tree/TreeTemplateComponent.ts | 72 + src/tree/demo/src/app/tree/TreeTestModule.ts | 160 + .../demo/src/app/tree/TreeUtilComponent.ts | 278 + .../app/tree/TreeVirtualscrollComponent.ts | 48 + .../tree/TreeVirtualscrollDragComponent.ts | 43 + .../tree/TreeVirtualscrollSmallComponent.ts | 42 + .../demo/src/app/tree/tree-before-expand.html | 1 + .../demo/src/app/tree/tree-before-more.html | 7 + .../src/app/tree/tree-changedbycheckbox.html | 1 + .../src/app/tree/tree-check-relation.html | 1 + src/tree/demo/src/app/tree/tree-disabled.html | 1 + .../src/app/tree/tree-drag-beforedrop.html | 7 + src/tree/demo/src/app/tree/tree-drag.html | 2 + src/tree/demo/src/app/tree/tree-event.html | 10 + src/tree/demo/src/app/tree/tree-icon.html | 6 + src/tree/demo/src/app/tree/tree-load.html | 35 + src/tree/demo/src/app/tree/tree-many.html | 60 + .../demo/src/app/tree/tree-multiselect.html | 1 + src/tree/demo/src/app/tree/tree-operate.html | 11 + .../src/app/tree/tree-parentcheckable.html | 1 + .../demo/src/app/tree/tree-radioselect.html | 5 + src/tree/demo/src/app/tree/tree-search.html | 30 + .../demo/src/app/tree/tree-shortcutkey.html | 1 + src/tree/demo/src/app/tree/tree-small.html | 1 + src/tree/demo/src/app/tree/tree-template.html | 14 + src/tree/demo/src/app/tree/tree-util.html | 18 + .../src/app/tree/tree-virtualscroll-drag.html | 41 + .../app/tree/tree-virtualscroll-small.html | 35 + .../demo/src/app/tree/tree-virtualscroll.html | 4 + src/tree/demo/src/app/tree/treeTest.less | 4 + .../demo/src/app/tree/webdoc/tree-demos.js | 249 + src/tree/demo/src/app/tree/webdoc/tree.cn.md | 31 + src/tree/demo/src/app/tree/webdoc/tree.en.md | 29 + src/tree/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/tree/demo/src/index.html | 16 + src/tree/demo/src/main.ts | 13 + src/tree/demo/test.ts | 24 + src/tree/demo/tsconfig.app.json | 15 + src/tree/demo/tsconfig.spec.json | 20 + src/tree/lib/index.ts | 1 + src/tree/lib/ng-package.json | 7 + src/tree/lib/package.json | 20 + src/tree/lib/project.json | 62 + src/tree/lib/src/TiAutoSelectDirective.ts | 28 + src/tree/lib/src/TiHighlightPipe.ts | 36 + src/tree/lib/src/TiTreeComponent.ts | 1111 ++++ src/tree/lib/src/TiTreeModule.ts | 51 + src/tree/lib/src/TiTreeUtil.ts | 620 ++ src/tree/lib/src/i18n/TiTreeWords.ts | 9 + src/tree/lib/src/i18n/en_US.ts | 11 + src/tree/lib/src/i18n/es_US.ts | 11 + src/tree/lib/src/i18n/fr_FR.ts | 11 + src/tree/lib/src/i18n/index.ts | 7 + src/tree/lib/src/i18n/pt_BR.ts | 11 + src/tree/lib/src/i18n/zh_CN.ts | 11 + src/tree/lib/src/tree.html | 293 + src/tree/lib/src/tree.less | 360 ++ src/treeselect/demo/karma.conf.js | 44 + src/treeselect/demo/project.json | 90 + src/treeselect/demo/src/app/AppComponent.ts | 7 + src/treeselect/demo/src/app/AppModule.ts | 24 + src/treeselect/demo/src/app/IndexComponent.ts | 49 + src/treeselect/demo/src/app/app.html | 3 + .../treeselect/TreeselectBasicComponent.ts | 84 + .../TreeselectBeforeExpandComponent.ts | 98 + .../TreeselectBeforeMoreComponent.ts | 143 + .../TreeselectClearableComponent.ts | 90 + .../treeselect/TreeselectDisabledComponent.ts | 36 + .../TreeselectDropmaxheightComponent.ts | 84 + .../treeselect/TreeselectEventComponent.ts | 89 + .../treeselect/TreeselectFocusComponent.ts | 82 + .../treeselect/TreeselectLabelkeyComponent.ts | 84 + .../treeselect/TreeselectLazyloadComponent.ts | 97 + .../app/treeselect/TreeselectLoadComponent.ts | 130 + .../treeselect/TreeselectMaxlineComponent.ts | 84 + .../treeselect/TreeselectModalComponent.ts | 112 + .../treeselect/TreeselectMultiComponent.ts | 83 + .../treeselect/TreeselectNodataComponent.ts | 11 + .../TreeselectOptionsChangeComponent.ts | 69 + .../TreeselectPanelwidthComponent.ts | 84 + .../treeselect/TreeselectSearchComponent.ts | 82 + .../TreeselectSelectallComponent.ts | 84 + .../app/treeselect/TreeselectTestModule.ts | 153 + .../TreeselectValidationComponent.ts | 83 + .../src/app/treeselect/treeselect-basic.html | 5 + .../treeselect/treeselect-before-expand.html | 8 + .../treeselect/treeselect-before-more.html | 10 + .../app/treeselect/treeselect-clearable.html | 11 + .../app/treeselect/treeselect-disabled.html | 6 + .../treeselect/treeselect-dropmaxheight.html | 8 + .../src/app/treeselect/treeselect-event.html | 4 + .../src/app/treeselect/treeselect-focus.html | 33 + .../app/treeselect/treeselect-labelkey.html | 1 + .../app/treeselect/treeselect-lazyload.html | 8 + .../src/app/treeselect/treeselect-load.html | 19 + .../app/treeselect/treeselect-maxline.html | 2 + .../src/app/treeselect/treeselect-modal.html | 1 + .../src/app/treeselect/treeselect-multi.html | 5 + .../src/app/treeselect/treeselect-nodata.html | 2 + .../treeselect/treeselect-options-change.html | 5 + .../app/treeselect/treeselect-panelwidth.html | 2 + .../src/app/treeselect/treeselect-search.html | 10 + .../app/treeselect/treeselect-selectall.html | 9 + .../app/treeselect/treeselect-validation.html | 10 + .../app/treeselect/webdoc/treeselect-demos.js | 202 + .../app/treeselect/webdoc/treeselect.cn.md | 25 + .../app/treeselect/webdoc/treeselect.en.md | 29 + src/treeselect/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/treeselect/demo/src/index.html | 16 + src/treeselect/demo/src/main.ts | 13 + src/treeselect/demo/test.ts | 24 + src/treeselect/demo/tsconfig.app.json | 15 + src/treeselect/demo/tsconfig.spec.json | 20 + src/treeselect/lib/index.ts | 1 + src/treeselect/lib/ng-package.json | 7 + src/treeselect/lib/package.json | 20 + src/treeselect/lib/project.json | 62 + .../lib/src/TiTreeselectComponent.ts | 625 ++ src/treeselect/lib/src/TiTreeselectModule.ts | 40 + src/treeselect/lib/src/treeselect.html | 73 + src/treeselect/lib/src/treeselect.less | 49 + src/tsconfig.lib.json | 21 + src/tsconfig.lib.prod.json | 9 + src/upload/demo/karma.conf.js | 44 + src/upload/demo/project.json | 98 + src/upload/demo/src/app/AppComponent.ts | 7 + src/upload/demo/src/app/AppModule.ts | 28 + src/upload/demo/src/app/IndexComponent.ts | 52 + src/upload/demo/src/app/app.html | 3 + .../app/upload/UploadAutoUploadComponent.ts | 6 + .../src/app/upload/UploadBasicComponent.ts | 6 + .../app/upload/UploadBatchSendComponent.ts | 6 + .../app/upload/UploadBeforeremoveComponent.ts | 15 + .../src/app/upload/UploadButtonComponent.ts | 6 + .../app/upload/UploadButtonTestComponent.ts | 77 + .../src/app/upload/UploadCaseTestComponent.ts | 15 + .../src/app/upload/UploadChangesComponent.ts | 29 + .../app/upload/UploadChunksizeComponent.ts | 49 + .../src/app/upload/UploadCustomComponent.ts | 22 + .../src/app/upload/UploadEventComponent.ts | 48 + .../src/app/upload/UploadFilterComponent.ts | 29 + .../src/app/upload/UploadFormDataComponent.ts | 19 + .../upload/UploadInitfilesTestComponent.ts | 36 + .../upload/UploadInputFieldTestComponent.ts | 83 + .../src/app/upload/UploadPropsComponent.ts | 28 + .../src/app/upload/UploadServiceComponent.ts | 72 + .../app/upload/UploadServiceTestComponent.ts | 89 + .../src/app/upload/UploadSingleComponent.ts | 17 + .../demo/src/app/upload/UploadTestModule.ts | 132 + .../src/app/upload/upload-auto-upload.html | 2 + .../demo/src/app/upload/upload-basic.html | 1 + .../src/app/upload/upload-batch-send.html | 1 + .../src/app/upload/upload-beforeremove.html | 5 + .../src/app/upload/upload-button-test.html | 78 + .../demo/src/app/upload/upload-button.html | 3 + .../demo/src/app/upload/upload-case-test.html | 5 + .../demo/src/app/upload/upload-changes.html | 46 + .../demo/src/app/upload/upload-chunksize.html | 17 + .../demo/src/app/upload/upload-custom.html | 11 + .../demo/src/app/upload/upload-event.html | 16 + .../demo/src/app/upload/upload-filter.html | 8 + .../demo/src/app/upload/upload-form-data.html | 9 + .../src/app/upload/upload-initfiles-test.html | 9 + .../app/upload/upload-input-field-test.html | 93 + .../demo/src/app/upload/upload-props.html | 18 + .../src/app/upload/upload-service-test.html | 39 + .../demo/src/app/upload/upload-service.html | 37 + .../demo/src/app/upload/upload-single.html | 4 + .../src/app/upload/webdoc/upload-demos.js | 157 + .../demo/src/app/upload/webdoc/upload.cn.md | 22 + .../demo/src/app/upload/webdoc/upload.en.md | 29 + .../UploadimageAutoUploadComponent.ts | 6 + .../uploadimage/UploadimageBasicComponent.ts | 6 + .../UploadimageChangesComponent.ts | 26 + .../UploadimageDeletableComponent.ts | 8 + .../UploadimageDisabledComponent.ts | 8 + .../uploadimage/UploadimageDragComponent.ts | 8 + .../uploadimage/UploadimageEventComponent.ts | 47 + .../uploadimage/UploadimageFilterComponent.ts | 29 + .../UploadimageInitfilesComponent.ts | 25 + .../UploadimageMaxcountComponent.ts | 11 + .../UploadimageTemplateComponent.ts | 8 + .../app/uploadimage/UploadimageTestModule.ts | 91 + .../uploadimage/uploadimage-auto-upload.html | 3 + .../app/uploadimage/uploadimage-basic.html | 1 + .../app/uploadimage/uploadimage-changes.html | 15 + .../uploadimage/uploadimage-deletable.html | 1 + .../app/uploadimage/uploadimage-disabled.html | 1 + .../src/app/uploadimage/uploadimage-drag.html | 16 + .../app/uploadimage/uploadimage-event.html | 17 + .../app/uploadimage/uploadimage-filter.html | 8 + .../uploadimage/uploadimage-initfiles.html | 1 + .../app/uploadimage/uploadimage-maxcount.html | 2 + .../app/uploadimage/uploadimage-template.html | 8 + .../src/app/uploadimage/uploadimagetest.less | 5 + .../uploadimage/webdoc/uploadimage-demos.js | 143 + .../app/uploadimage/webdoc/uploadimage.cn.md | 22 + .../app/uploadimage/webdoc/uploadimage.en.md | 29 + src/upload/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/upload/demo/src/index.html | 16 + src/upload/demo/src/main.ts | 13 + src/upload/demo/test.ts | 24 + src/upload/demo/tsconfig.app.json | 15 + src/upload/demo/tsconfig.spec.json | 20 + src/upload/lib/index.ts | 4 + src/upload/lib/ng-package.json | 7 + src/upload/lib/package.json | 23 + src/upload/lib/project.json | 62 + src/upload/lib/src/TiDisabledDirective.ts | 29 + src/upload/lib/src/TiFileInterface.ts | 564 ++ src/upload/lib/src/TiFileSelectDirective.ts | 97 + src/upload/lib/src/TiUploadComponent.ts | 297 + src/upload/lib/src/TiUploadModule.ts | 60 + src/upload/lib/src/TiUploadService.ts | 395 ++ src/upload/lib/src/TiUploadServiceModule.ts | 21 + src/upload/lib/src/TiUploadUtil.ts | 749 +++ src/upload/lib/src/TiUploadbaseComponent.ts | 401 ++ src/upload/lib/src/TiUploadimageComponent.ts | 383 ++ src/upload/lib/src/i18n/TiUploadWords.ts | 21 + src/upload/lib/src/i18n/en_US.ts | 23 + src/upload/lib/src/i18n/es_US.ts | 23 + src/upload/lib/src/i18n/fr_FR.ts | 23 + src/upload/lib/src/i18n/index.ts | 7 + src/upload/lib/src/i18n/pt_BR.ts | 23 + src/upload/lib/src/i18n/zh_CN.ts | 23 + src/upload/lib/src/upload.html | 293 + src/upload/lib/src/upload.less | 356 ++ src/upload/lib/src/uploadimage.html | 96 + src/upload/lib/src/uploadimage.less | 166 + src/utils/demo/project.json | 78 + src/utils/demo/src/app/AppComponent.ts | 7 + src/utils/demo/src/app/AppModule.ts | 30 + src/utils/demo/src/app/IndexComponent.ts | 54 + src/utils/demo/src/app/app.html | 3 + .../demo/src/app/browser/BrowserTestModule.ts | 19 + .../src/app/browser/BrowserUsageComponent.ts | 17 + .../demo/src/app/browser/browser-usage.html | 10 + .../src/app/browser/webdoc/browser-demos.js | 27 + .../demo/src/app/browser/webdoc/browser.cn.md | 19 + .../demo/src/app/browser/webdoc/browser.en.md | 28 + .../demo/src/app/keymap/KeymapTestModule.ts | 21 + .../src/app/keymap/KeymapUsageComponent.ts | 24 + .../demo/src/app/keymap/keymap-usage.html | 8 + .../src/app/keymap/webdoc/keymap-demos.js | 17 + .../demo/src/app/keymap/webdoc/keymap.cn.md | 16 + .../demo/src/app/keymap/webdoc/keymap.en.md | 28 + src/utils/demo/src/app/log/LogTestModule.ts | 19 + .../demo/src/app/log/LogUsageComponent.ts | 45 + src/utils/demo/src/app/log/log-usage.html | 1 + .../demo/src/app/log/webdoc/log-demos.js | 23 + src/utils/demo/src/app/log/webdoc/log.cn.md | 16 + src/utils/demo/src/app/log/webdoc/log.en.md | 26 + .../demo/src/app/theme/ThemeBasicComponent.ts | 26 + .../demo/src/app/theme/ThemeTestModule.ts | 22 + src/utils/demo/src/app/theme/theme-basic.html | 5 + .../demo/src/app/theme/webdoc/theme-demos.js | 17 + .../demo/src/app/theme/webdoc/theme.cn.md | 16 + .../demo/src/app/theme/webdoc/theme.en.md | 29 + src/utils/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/utils/demo/src/index.html | 16 + src/utils/demo/src/main.ts | 13 + src/utils/demo/tsconfig.app.json | 15 + src/utils/lib/index.ts | 9 + src/utils/lib/ng-package.json | 7 + src/utils/lib/package.json | 11 + src/utils/lib/project.json | 62 + src/utils/lib/src/ObservableMap.ts | 73 + src/utils/lib/src/ObservableSet.ts | 63 + src/utils/lib/src/Position.ts | 720 +++ src/utils/lib/src/TiBrowser.ts | 162 + src/utils/lib/src/TiDateUtil.ts | 470 ++ src/utils/lib/src/TiKeymap.ts | 381 ++ src/utils/lib/src/TiLog.ts | 81 + src/utils/lib/src/TiTheme.ts | 62 + src/utils/lib/src/Util.ts | 169 + src/validation/demo/karma.conf.js | 44 + src/validation/demo/project.json | 90 + src/validation/demo/src/app/AppComponent.ts | 7 + src/validation/demo/src/app/AppModule.ts | 24 + src/validation/demo/src/app/IndexComponent.ts | 49 + src/validation/demo/src/app/app.html | 3 + .../ValidationAsyncCheckComponent.ts | 53 + .../ValidationAsyncCheckTestComponent.ts | 58 + .../ValidationBasicControlComponent.ts | 10 + .../ValidationBasicDirectiveComponent.ts | 9 + .../ValidationBlurCheckComponent.ts | 12 + .../validation/ValidationErrorMsgComponent.ts | 29 + .../ValidationFormGroupComponent.ts | 150 + .../ValidationFormGroupConfigComponent.ts | 60 + .../ValidationFormGroupTestComponent.ts | 69 + .../ValidationParamChangeComponent.ts | 19 + .../validation/ValidationPwdCheckComponent.ts | 166 + .../ValidationRulesCustomComponent.ts | 24 + ...ValidationRulesCustomDirectiveComponent.ts | 40 + .../ValidationRulesTestComponent.ts | 41 + .../ValidationTemplateFormNestedComponent.ts | 63 + .../app/validation/ValidationTestModule.ts | 153 + .../app/validation/ValidationTipComponent.ts | 14 + .../validation-async-check-test.html | 24 + .../validation/validation-async-check.html | 5 + .../validation/validation-basic-control.html | 1 + .../validation-basic-directive.html | 4 + .../app/validation/validation-blur-check.html | 1 + .../app/validation/validation-error-msg.html | 6 + .../validation-form-group-config.html | 10 + .../validation-form-group-test.html | 39 + .../app/validation/validation-form-group.html | 36 + .../validation/validation-param-change.html | 11 + .../app/validation/validation-pwd-check.html | 30 + .../validation-rules-custom-directive.html | 1 + .../validation/validation-rules-custom.html | 1 + .../app/validation/validation-rules-test.html | 75 + .../validation-template-form-nested.html | 33 + .../src/app/validation/validation-tip.html | 1 + .../app/validation/validation-tiscroll.html | 24 + .../validation/validationTiscrollComponent.ts | 7 + .../app/validation/webdoc/validation-demos.js | 165 + .../app/validation/webdoc/validation.cn.md | 52 + .../app/validation/webdoc/validation.en.md | 29 + src/validation/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/validation/demo/src/index.html | 16 + src/validation/demo/src/main.ts | 13 + src/validation/demo/test.ts | 24 + src/validation/demo/tsconfig.app.json | 15 + src/validation/demo/tsconfig.spec.json | 20 + src/validation/lib/index.ts | 1 + src/validation/lib/ng-package.json | 7 + src/validation/lib/package.json | 18 + src/validation/lib/project.json | 62 + src/validation/lib/src/TiErrorMsgComponent.ts | 38 + .../lib/src/TiPendingStateComponent.ts | 41 + src/validation/lib/src/TiPwdMsgComponent.html | 24 + src/validation/lib/src/TiPwdMsgComponent.ts | 125 + src/validation/lib/src/TiValidationConfig.ts | 22 + .../lib/src/TiValidationDirective.ts | 140 + .../lib/src/TiValidationInterface.ts | 125 + src/validation/lib/src/TiValidationModule.ts | 176 + .../lib/src/checkHandle/AsyncCheck.ts | 82 + .../lib/src/checkHandle/BlurCheck.ts | 58 + .../lib/src/checkHandle/ChangeCheck.ts | 78 + .../lib/src/checkHandle/CheckHandle.ts | 27 + .../lib/src/checkHandle/CheckStyleModule.ts | 21 + .../lib/src/checkHandle/CheckStyleService.ts | 60 + .../lib/src/checkHandle/CommonService.ts | 155 + .../src/checkHandle/CommonServiceModule.ts | 18 + .../lib/src/checkHandle/PwdCheck.ts | 112 + .../lib/src/checkHandle/RadiobaseCheck.ts | 36 + .../lib/src/checkHandle/TiPwdConfig.ts | 180 + src/validation/lib/src/errorMsg.less | 40 + .../lib/src/i18n/TiValidationWords.ts | 39 + src/validation/lib/src/i18n/en_US.ts | 43 + src/validation/lib/src/i18n/es_US.ts | 43 + src/validation/lib/src/i18n/fr_FR.ts | 43 + src/validation/lib/src/i18n/index.ts | 7 + src/validation/lib/src/i18n/pt_BR.ts | 43 + src/validation/lib/src/i18n/zh_CN.ts | 41 + src/validation/lib/src/pending-state.html | 1 + src/validation/lib/src/pending-state.less | 37 + src/validation/lib/src/pwdMsg.less | 161 + .../lib/src/validators/TiValidators.ts | 792 +++ .../validators/directives/BaseValidator.ts | 80 + .../directives/BigDigitsValidatorDirective.ts | 30 + .../BigIntegerValidatorDirective.ts | 30 + .../directives/BigNumberValidatorDirective.ts | 30 + .../directives/ContainsValidatorDirective.ts | 30 + .../directives/DateValidatorDirective.ts | 29 + .../directives/DigitsValidatorDirective.ts | 29 + .../directives/EmailValidatorDirective.ts | 30 + .../directives/EqualValidatorDirective.ts | 30 + .../directives/IntegerValidatorDirective.ts | 30 + .../directives/Ipv4ValidatorDirective.ts | 29 + .../directives/Ipv6ValidatorDirective.ts | 29 + .../directives/MaxLengthValidatorDirective.ts | 29 + .../MaxValueByStringValidatorDirective.ts | 29 + .../directives/MaxValueValidatorDirective.ts | 29 + .../directives/MinLengthValidatorDirective.ts | 29 + .../MinValueByStringValidatorDirective.ts | 29 + .../directives/MinValueValidatorDirective.ts | 29 + .../NotContainsValidatorDirective.ts | 29 + .../directives/NotEqualValidatorDirective.ts | 29 + .../directives/NotScriptValidatorDirective.ts | 29 + .../directives/NumberValidatorDirective.ts | 30 + .../directives/PasswordValidatorDirective.ts | 29 + .../directives/PortValidatorDirective.ts | 29 + .../directives/RangeSizeValidatorDirective.ts | 29 + .../RangeValueByStringValidatorDirective.ts | 29 + .../RangeValueValidatorDirective.ts | 29 + .../directives/RegExpValidatorDirective.ts | 29 + .../directives/RequiredValidatorDirective.ts | 29 + .../directives/UrlValidatorDirective.ts | 29 + src/zoom/demo/project.json | 86 + src/zoom/demo/src/app/AppComponent.ts | 7 + src/zoom/demo/src/app/AppModule.ts | 24 + src/zoom/demo/src/app/IndexComponent.ts | 49 + src/zoom/demo/src/app/app.html | 3 + .../demo/src/app/zoom/ZoomBasicComponent.ts | 7 + .../src/app/zoom/ZoomChangeSrcComponent.ts | 20 + .../demo/src/app/zoom/ZoomRatioComponent.ts | 7 + .../demo/src/app/zoom/ZoomTestComponent.ts | 7 + src/zoom/demo/src/app/zoom/ZoomTestModule.ts | 30 + src/zoom/demo/src/app/zoom/zoom-basic.html | 11 + .../demo/src/app/zoom/zoom-change-src.html | 5 + src/zoom/demo/src/app/zoom/zoom-ratio.html | 9 + src/zoom/demo/src/app/zoom/zoom-test.html | 15 + src/zoom/demo/src/app/zoom/zoom.less | 11 + src/zoom/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/zoom/demo/src/index.html | 16 + src/zoom/demo/src/main.ts | 13 + src/zoom/demo/tsconfig.app.json | 15 + src/zoom/lib/index.ts | 1 + src/zoom/lib/ng-package.json | 7 + src/zoom/lib/package.json | 10 + src/zoom/lib/project.json | 62 + src/zoom/lib/src/TiZoomComponent.ts | 257 + src/zoom/lib/src/TiZoomModule.ts | 23 + src/zoom/lib/src/zoom.less | 19 + .../files/demo/project.json__tmpl__ | 85 + .../demo/src/app/AppComponent.ts__tmpl__ | 7 + .../files/demo/src/app/AppModule.ts__tmpl__ | 24 + .../demo/src/app/IndexComponent.ts__tmpl__ | 54 + .../__name__/__demoComponentName__.ts__tmpl__ | 7 + .../__name__/__demoModuleName__.ts__tmpl__ | 23 + .../app/__name__/__name__-basic.html__tmpl__ | 1 + .../__name__/webdoc/__name__-demos.js__tmpl__ | 17 + .../__name__/webdoc/__name__.cn.md__tmpl__ | 15 + .../__name__/webdoc/__name__.en.md__tmpl__ | 13 + .../files/demo/src/app/app.html__tmpl__ | 3 + .../files/demo/src/favicon.ico__tmpl__ | Bin 0 -> 187502 bytes .../files/demo/src/index.html__tmpl__ | 16 + .../files/demo/src/main.ts__tmpl__ | 14 + .../files/demo/tsconfig.app.json__tmpl__ | 15 + .../files/lib/index.ts__tmpl__ | 2 + .../files/lib/ng-package.json__tmpl__ | 7 + .../files/lib/package.json__tmpl__ | 8 + .../files/lib/project.json__tmpl__ | 63 + .../lib/src/__componentName__.ts__tmpl__ | 14 + .../files/lib/src/__moduleName__.ts__tmpl__ | 20 + .../files/lib/src/__name__.html__tmpl__ | 0 .../files/lib/src/__name__.less__tmpl__ | 0 .../lib/src/i18n/__i18nWords__.ts__tmpl__ | 5 + .../files/lib/src/i18n/en_US.ts__tmpl__ | 7 + .../files/lib/src/i18n/es_US.ts__tmpl__ | 7 + .../files/lib/src/i18n/fr_FR.ts__tmpl__ | 7 + .../files/lib/src/i18n/index.ts__tmpl__ | 8 + .../files/lib/src/i18n/pt_BR.ts__tmpl__ | 7 + tools/generators/ti-lib-generator/index.ts | 76 + tools/generators/ti-lib-generator/schema.d.ts | 5 + tools/generators/ti-lib-generator/schema.json | 39 + tools/tsconfig.tools.json | 24 + tsconfig.base.json | 126 + tsconfig.json | 8 + website-tinydoc/build/copy.js | 34 + website-tinydoc/config.js | 3 + website-tinydoc/package.json | 19 + 4688 files changed, 197730 insertions(+) create mode 100644 .codecheck/check.yml create mode 100644 .compodocrc.json create mode 100644 .cz.json create mode 100644 .editorconfig create mode 100644 .eslintrc.js create mode 100644 .githooks/commit-msg.sample create mode 100644 .githooks/pre-commit create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitignore create mode 100644 .lintstagedrc.json create mode 100644 .prettierignore create mode 100644 .prettierrc.js create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README-zh_CN.md create mode 100644 angular.json create mode 100644 build.md create mode 100644 build/AppWcModule.ts.ejs create mode 100644 build/add-default-theme.js create mode 100644 build/autoCreateDemoProjectJson.js create mode 100644 build/autoCreateLibProjectJson.js create mode 100644 build/autoCreateWebsite.js create mode 100644 build/bindTest.js create mode 100644 build/build-api.js create mode 100644 build/buildThemes.js create mode 100644 build/buildwc.js create mode 100644 build/clear-default-theme.js create mode 100644 build/component.cn.ejs create mode 100644 build/component.ejs create mode 100644 build/component.en.ejs create mode 100644 build/demoProject.json.ejs create mode 100644 build/libProject.json.ejs create mode 100644 build/package.js create mode 100644 build/peerDependencies.js create mode 100644 build/preview-demo.js create mode 100644 build/publish.js create mode 100644 build/reset-preview.js create mode 100644 build/unitTestingResources/config.js create mode 100644 build/unitTestingResources/karma.conf.js.template create mode 100644 build/unitTestingResources/project_test.json.template create mode 100644 build/unitTestingResources/test.ts.template create mode 100644 build/unitTestingResources/tsconfig.spec.json.template create mode 100644 c.bat create mode 100644 commit.template create mode 100644 commitlint.config.js create mode 100644 decorate-angular-cli.js create mode 100644 eslint-rules/eslint-config-cbc-1-7-8-angular.js create mode 100644 eslint-rules/eslint-config-cbc-1-7-8-base.js create mode 100644 eslint-rules/eslint-config-cbc-1-7-8-typescript.js create mode 100644 logo.png create mode 100644 nx.json create mode 100644 package.json create mode 100644 src/accordion/demo/karma.conf.js create mode 100644 src/accordion/demo/project.json create mode 100644 src/accordion/demo/src/app/AppComponent.ts create mode 100644 src/accordion/demo/src/app/AppModule.ts create mode 100644 src/accordion/demo/src/app/IndexComponent.ts create mode 100644 src/accordion/demo/src/app/accordion/AccordionBasicComponent.ts create mode 100644 src/accordion/demo/src/app/accordion/AccordionClassComponent.ts create mode 100644 src/accordion/demo/src/app/accordion/AccordionClickToggleComponent.ts create mode 100644 src/accordion/demo/src/app/accordion/AccordionCloseOthersComponent.ts create mode 100644 src/accordion/demo/src/app/accordion/AccordionDisabledComponent.ts create mode 100644 src/accordion/demo/src/app/accordion/AccordionOpenComponent.ts create mode 100644 src/accordion/demo/src/app/accordion/AccordionTestModule.ts create mode 100644 src/accordion/demo/src/app/accordion/accordion-basic.html create mode 100644 src/accordion/demo/src/app/accordion/accordion-class.html create mode 100644 src/accordion/demo/src/app/accordion/accordion-click-toggle.html create mode 100644 src/accordion/demo/src/app/accordion/accordion-close-others.html create mode 100644 src/accordion/demo/src/app/accordion/accordion-disabled.html create mode 100644 src/accordion/demo/src/app/accordion/accordion-open.html create mode 100644 src/accordion/demo/src/app/accordion/accordion.spec.ts create mode 100644 src/accordion/demo/src/app/accordion/webdoc/accordion-demos.js create mode 100644 src/accordion/demo/src/app/accordion/webdoc/accordion.cn.md create mode 100644 src/accordion/demo/src/app/accordion/webdoc/accordion.en.md create mode 100644 src/accordion/demo/src/app/app.html create mode 100644 src/accordion/demo/src/favicon.ico create mode 100644 src/accordion/demo/src/index.html create mode 100644 src/accordion/demo/src/main.ts create mode 100644 src/accordion/demo/test.ts create mode 100644 src/accordion/demo/tsconfig.app.json create mode 100644 src/accordion/demo/tsconfig.spec.json create mode 100644 src/accordion/lib/index.ts create mode 100644 src/accordion/lib/ng-package.json create mode 100644 src/accordion/lib/package.json create mode 100644 src/accordion/lib/project.json create mode 100644 src/accordion/lib/src/TiAccordionComponent.ts create mode 100644 src/accordion/lib/src/TiAccordionHeadComponent.ts create mode 100644 src/accordion/lib/src/TiAccordionItemComponent.ts create mode 100644 src/accordion/lib/src/TiAccordionModule.ts create mode 100644 src/accordion/lib/src/accordion-item.html create mode 100644 src/accordion/lib/src/accordion.html create mode 100644 src/accordion/lib/src/accordion.less create mode 100644 src/actionmenu/demo/karma.conf.js create mode 100644 src/actionmenu/demo/project.json create mode 100644 src/actionmenu/demo/src/app/AppComponent.ts create mode 100644 src/actionmenu/demo/src/app/AppModule.ts create mode 100644 src/actionmenu/demo/src/app/IndexComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuBasicComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuData2Component.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuDataComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuDisabledComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuDividerComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuEventComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuFocusComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuIdComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuItemsChangeComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuItemsComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuLabelkeyComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuManyComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuMenutextComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuPanelstyleComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuShownumComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuSpaceComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuTableComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuTempleteComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuTempleteTestComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuTestModule.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuTipsComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/ActionmenuTipsTestComponent.ts create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-basic.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-data.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-data2.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-disabled.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-divider.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-event.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-focus.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-id.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-items-change.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-items.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-labelkey.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-many.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-menutext.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-panelstyle.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-shownum.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-space.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-table.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-templete-test.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-templete.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-tips-test.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/actionmenu-tips.html create mode 100644 src/actionmenu/demo/src/app/actionmenu/webdoc/actionmenu-demos.js create mode 100644 src/actionmenu/demo/src/app/actionmenu/webdoc/actionmenu.cn.md create mode 100644 src/actionmenu/demo/src/app/actionmenu/webdoc/actionmenu.en.md create mode 100644 src/actionmenu/demo/src/app/app.html create mode 100644 src/actionmenu/demo/src/favicon.ico create mode 100644 src/actionmenu/demo/src/index.html create mode 100644 src/actionmenu/demo/src/main.ts create mode 100644 src/actionmenu/demo/test.ts create mode 100644 src/actionmenu/demo/tsconfig.app.json create mode 100644 src/actionmenu/demo/tsconfig.spec.json create mode 100644 src/actionmenu/lib/index.ts create mode 100644 src/actionmenu/lib/ng-package.json create mode 100644 src/actionmenu/lib/package.json create mode 100644 src/actionmenu/lib/project.json create mode 100644 src/actionmenu/lib/src/TiActionmenuComponent.ts create mode 100644 src/actionmenu/lib/src/TiActionmenuModule.ts create mode 100644 src/actionmenu/lib/src/actionmenu.html create mode 100644 src/actionmenu/lib/src/actionmenu.less create mode 100644 src/actionmenu/lib/src/i18n/TiActionmenuWords.ts create mode 100644 src/actionmenu/lib/src/i18n/en_US.ts create mode 100644 src/actionmenu/lib/src/i18n/es_US.ts create mode 100644 src/actionmenu/lib/src/i18n/fr_FR.ts create mode 100644 src/actionmenu/lib/src/i18n/index.ts create mode 100644 src/actionmenu/lib/src/i18n/pt_BR.ts create mode 100644 src/actionmenu/lib/src/i18n/zh_CN.ts create mode 100644 src/alert/demo/karma.conf.js create mode 100644 src/alert/demo/project.json create mode 100644 src/alert/demo/src/app/AppComponent.ts create mode 100644 src/alert/demo/src/app/AppModule.ts create mode 100644 src/alert/demo/src/app/IndexComponent.ts create mode 100644 src/alert/demo/src/app/alert/AlertBoxshadowComponent.ts create mode 100644 src/alert/demo/src/app/alert/AlertDarkthemeComponent.ts create mode 100644 src/alert/demo/src/app/alert/AlertDismissComponent.ts create mode 100644 src/alert/demo/src/app/alert/AlertEventComponent.ts create mode 100644 src/alert/demo/src/app/alert/AlertIconComponent.ts create mode 100644 src/alert/demo/src/app/alert/AlertMessagesComponent.ts create mode 100644 src/alert/demo/src/app/alert/AlertOpenComponent.ts create mode 100644 src/alert/demo/src/app/alert/AlertOpenTestComponent.ts create mode 100644 src/alert/demo/src/app/alert/AlertSizeComponent.ts create mode 100644 src/alert/demo/src/app/alert/AlertTestModule.ts create mode 100644 src/alert/demo/src/app/alert/AlertTriggerScrollComponent.ts create mode 100644 src/alert/demo/src/app/alert/AlertTypeComponent.ts create mode 100644 src/alert/demo/src/app/alert/alert-boxshadow.html create mode 100644 src/alert/demo/src/app/alert/alert-darktheme.html create mode 100644 src/alert/demo/src/app/alert/alert-dismiss.html create mode 100644 src/alert/demo/src/app/alert/alert-event.html create mode 100644 src/alert/demo/src/app/alert/alert-icon.html create mode 100644 src/alert/demo/src/app/alert/alert-messages.html create mode 100644 src/alert/demo/src/app/alert/alert-open-test.html create mode 100644 src/alert/demo/src/app/alert/alert-open.html create mode 100644 src/alert/demo/src/app/alert/alert-size.html create mode 100644 src/alert/demo/src/app/alert/alert-trigger-scroll.html create mode 100644 src/alert/demo/src/app/alert/alert-type.html create mode 100644 src/alert/demo/src/app/alert/webdoc/alert-demos.js create mode 100644 src/alert/demo/src/app/alert/webdoc/alert.cn.md create mode 100644 src/alert/demo/src/app/alert/webdoc/alert.en.md create mode 100644 src/alert/demo/src/app/app.html create mode 100644 src/alert/demo/src/favicon.ico create mode 100644 src/alert/demo/src/index.html create mode 100644 src/alert/demo/src/main.ts create mode 100644 src/alert/demo/test.ts create mode 100644 src/alert/demo/tsconfig.app.json create mode 100644 src/alert/demo/tsconfig.spec.json create mode 100644 src/alert/lib/index.ts create mode 100644 src/alert/lib/ng-package.json create mode 100644 src/alert/lib/package.json create mode 100644 src/alert/lib/project.json create mode 100644 src/alert/lib/src/TiAlertComponent.ts create mode 100644 src/alert/lib/src/TiAlertMessageComponent.ts create mode 100644 src/alert/lib/src/TiAlertModule.ts create mode 100644 src/alert/lib/src/alert.html create mode 100644 src/alert/lib/src/alert.less create mode 100644 src/anchor/demo/karma.conf.js create mode 100644 src/anchor/demo/project.json create mode 100644 src/anchor/demo/src/app/AppComponent.ts create mode 100644 src/anchor/demo/src/app/AppModule.ts create mode 100644 src/anchor/demo/src/app/IndexComponent.ts create mode 100644 src/anchor/demo/src/app/anchor/AnchorBasicComponent.ts create mode 100644 src/anchor/demo/src/app/anchor/AnchorEventsComponent.ts create mode 100644 src/anchor/demo/src/app/anchor/AnchorIdComponent.ts create mode 100644 src/anchor/demo/src/app/anchor/AnchorItemsComponent.ts create mode 100644 src/anchor/demo/src/app/anchor/AnchorOffsettopComponent.ts create mode 100644 src/anchor/demo/src/app/anchor/AnchorScrolltargetComponent.ts create mode 100644 src/anchor/demo/src/app/anchor/AnchorSpeedComponent.ts create mode 100644 src/anchor/demo/src/app/anchor/AnchorTemplateComponent.ts create mode 100644 src/anchor/demo/src/app/anchor/AnchorTestComponent.ts create mode 100644 src/anchor/demo/src/app/anchor/AnchorTestModule.ts create mode 100644 src/anchor/demo/src/app/anchor/anchor-basic.html create mode 100644 src/anchor/demo/src/app/anchor/anchor-events.html create mode 100644 src/anchor/demo/src/app/anchor/anchor-id.html create mode 100644 src/anchor/demo/src/app/anchor/anchor-items.html create mode 100644 src/anchor/demo/src/app/anchor/anchor-offsettop.html create mode 100644 src/anchor/demo/src/app/anchor/anchor-scrolltarget.html create mode 100644 src/anchor/demo/src/app/anchor/anchor-speed.html create mode 100644 src/anchor/demo/src/app/anchor/anchor-template.html create mode 100644 src/anchor/demo/src/app/anchor/anchor-test.html create mode 100644 src/anchor/demo/src/app/anchor/anchortest.less create mode 100644 src/anchor/demo/src/app/anchor/webdoc/anchor-demos.js create mode 100644 src/anchor/demo/src/app/anchor/webdoc/anchor.cn.md create mode 100644 src/anchor/demo/src/app/anchor/webdoc/anchor.en.md create mode 100644 src/anchor/demo/src/app/app.html create mode 100644 src/anchor/demo/src/favicon.ico create mode 100644 src/anchor/demo/src/index.html create mode 100644 src/anchor/demo/src/main.ts create mode 100644 src/anchor/demo/test.ts create mode 100644 src/anchor/demo/tsconfig.app.json create mode 100644 src/anchor/demo/tsconfig.spec.json create mode 100644 src/anchor/lib/index.ts create mode 100644 src/anchor/lib/ng-package.json create mode 100644 src/anchor/lib/package.json create mode 100644 src/anchor/lib/project.json create mode 100644 src/anchor/lib/src/TiAnchorComponent.ts create mode 100644 src/anchor/lib/src/TiAnchorModule.ts create mode 100644 src/anchor/lib/src/anchor.html create mode 100644 src/anchor/lib/src/anchor.less create mode 100644 src/autocomplete/demo/karma.conf.js create mode 100644 src/autocomplete/demo/project.json create mode 100644 src/autocomplete/demo/src/app/AppComponent.ts create mode 100644 src/autocomplete/demo/src/app/AppModule.ts create mode 100644 src/autocomplete/demo/src/app/IndexComponent.ts create mode 100644 src/autocomplete/demo/src/app/app.html create mode 100644 src/autocomplete/demo/src/app/autocomplete/AutocompleteAppendtobodyComponent.ts create mode 100644 src/autocomplete/demo/src/app/autocomplete/AutocompleteBasicComponent.ts create mode 100644 src/autocomplete/demo/src/app/autocomplete/AutocompleteClearableComponent.ts create mode 100644 src/autocomplete/demo/src/app/autocomplete/AutocompleteDisabledComponent.ts create mode 100644 src/autocomplete/demo/src/app/autocomplete/AutocompleteEventsComponent.ts create mode 100644 src/autocomplete/demo/src/app/autocomplete/AutocompleteGroupComponent.ts create mode 100644 src/autocomplete/demo/src/app/autocomplete/AutocompleteLabelkeyComponent.ts create mode 100644 src/autocomplete/demo/src/app/autocomplete/AutocompleteMaxlengthComponent.ts create mode 100644 src/autocomplete/demo/src/app/autocomplete/AutocompletePanelSizeComponent.ts create mode 100644 src/autocomplete/demo/src/app/autocomplete/AutocompleteTemplateComponent.ts create mode 100644 src/autocomplete/demo/src/app/autocomplete/AutocompleteTestComponent.ts create mode 100644 src/autocomplete/demo/src/app/autocomplete/AutocompleteTestModule.ts create mode 100644 src/autocomplete/demo/src/app/autocomplete/AutocompleteTipComponent.ts create mode 100644 src/autocomplete/demo/src/app/autocomplete/AutocompleteValidComponent.ts create mode 100644 src/autocomplete/demo/src/app/autocomplete/autocomplete-appendtobody.html create mode 100644 src/autocomplete/demo/src/app/autocomplete/autocomplete-basic.html create mode 100644 src/autocomplete/demo/src/app/autocomplete/autocomplete-clearable.html create mode 100644 src/autocomplete/demo/src/app/autocomplete/autocomplete-disabled.html create mode 100644 src/autocomplete/demo/src/app/autocomplete/autocomplete-events.html create mode 100644 src/autocomplete/demo/src/app/autocomplete/autocomplete-group.html create mode 100644 src/autocomplete/demo/src/app/autocomplete/autocomplete-labelkey.html create mode 100644 src/autocomplete/demo/src/app/autocomplete/autocomplete-maxlength.html create mode 100644 src/autocomplete/demo/src/app/autocomplete/autocomplete-panel-size.html create mode 100644 src/autocomplete/demo/src/app/autocomplete/autocomplete-template.html create mode 100644 src/autocomplete/demo/src/app/autocomplete/autocomplete-test.html create mode 100644 src/autocomplete/demo/src/app/autocomplete/autocomplete-tip.html create mode 100644 src/autocomplete/demo/src/app/autocomplete/autocomplete-valid.html create mode 100644 src/autocomplete/demo/src/app/autocomplete/webdoc/autocomplete-demos.js create mode 100644 src/autocomplete/demo/src/app/autocomplete/webdoc/autocomplete.cn.md create mode 100644 src/autocomplete/demo/src/app/autocomplete/webdoc/autocomplete.en.md create mode 100644 src/autocomplete/demo/src/favicon.ico create mode 100644 src/autocomplete/demo/src/index.html create mode 100644 src/autocomplete/demo/src/main.ts create mode 100644 src/autocomplete/demo/test.ts create mode 100644 src/autocomplete/demo/tsconfig.app.json create mode 100644 src/autocomplete/demo/tsconfig.spec.json create mode 100644 src/autocomplete/lib/index.ts create mode 100644 src/autocomplete/lib/ng-package.json create mode 100644 src/autocomplete/lib/package.json create mode 100644 src/autocomplete/lib/project.json create mode 100644 src/autocomplete/lib/src/TiAutocompleteComponent.ts create mode 100644 src/autocomplete/lib/src/TiAutocompleteModule.ts create mode 100644 src/autocomplete/lib/src/autocomplete.html create mode 100644 src/autocomplete/lib/src/autocomplete.less create mode 100644 src/avatar/demo/karma.conf.js create mode 100644 src/avatar/demo/project.json create mode 100644 src/avatar/demo/src/app/AppComponent.ts create mode 100644 src/avatar/demo/src/app/AppModule.ts create mode 100644 src/avatar/demo/src/app/IndexComponent.ts create mode 100644 src/avatar/demo/src/app/app.html create mode 100644 src/avatar/demo/src/app/avatar/AvatarImageComponent.ts create mode 100644 src/avatar/demo/src/app/avatar/AvatarImageErrorTestComponent.ts create mode 100644 src/avatar/demo/src/app/avatar/AvatarShapeComponent.ts create mode 100644 src/avatar/demo/src/app/avatar/AvatarSizeComponent.ts create mode 100644 src/avatar/demo/src/app/avatar/AvatarStyleComponent.ts create mode 100644 src/avatar/demo/src/app/avatar/AvatarTestModule.ts create mode 100644 src/avatar/demo/src/app/avatar/AvatarTextComponent.ts create mode 100644 src/avatar/demo/src/app/avatar/avatar-image-error-test.html create mode 100644 src/avatar/demo/src/app/avatar/avatar-image.html create mode 100644 src/avatar/demo/src/app/avatar/avatar-shape.html create mode 100644 src/avatar/demo/src/app/avatar/avatar-size.html create mode 100644 src/avatar/demo/src/app/avatar/avatar-style.html create mode 100644 src/avatar/demo/src/app/avatar/avatar-text.html create mode 100644 src/avatar/demo/src/app/avatar/webdoc/avatar-demos.js create mode 100644 src/avatar/demo/src/app/avatar/webdoc/avatar.cn.md create mode 100644 src/avatar/demo/src/app/avatar/webdoc/avatar.en.md create mode 100644 src/avatar/demo/src/favicon.ico create mode 100644 src/avatar/demo/src/index.html create mode 100644 src/avatar/demo/src/main.ts create mode 100644 src/avatar/demo/test.ts create mode 100644 src/avatar/demo/tsconfig.app.json create mode 100644 src/avatar/demo/tsconfig.spec.json create mode 100644 src/avatar/lib/index.ts create mode 100644 src/avatar/lib/ng-package.json create mode 100644 src/avatar/lib/package.json create mode 100644 src/avatar/lib/project.json create mode 100644 src/avatar/lib/src/TiAvatarComponent.ts create mode 100644 src/avatar/lib/src/TiAvatarModule.ts create mode 100644 src/avatar/lib/src/avatar.html create mode 100644 src/avatar/lib/src/avatar.less create mode 100644 src/base/lib/index.ts create mode 100644 src/base/lib/ng-package.json create mode 100644 src/base/lib/package.json create mode 100644 src/base/lib/project.json create mode 100644 src/base/lib/src/TiAutofocusComponent.ts create mode 100644 src/base/lib/src/TiBaseComponent.ts create mode 100644 src/base/lib/src/TiBaseModule.ts create mode 100644 src/base/lib/src/TiFormComponent.ts create mode 100644 src/base/lib/src/TiRadioBaseComponent.ts create mode 100644 src/base/lib/src/TiWholeComponent.ts create mode 100644 src/browserslist create mode 100644 src/button/demo/karma.conf.js create mode 100644 src/button/demo/project.json create mode 100644 src/button/demo/src/app/AppComponent.ts create mode 100644 src/button/demo/src/app/AppModule.ts create mode 100644 src/button/demo/src/app/IndexComponent.ts create mode 100644 src/button/demo/src/app/app.html create mode 100644 src/button/demo/src/app/button/ButtonColorComponent.ts create mode 100644 src/button/demo/src/app/button/ButtonDisabledComponent.ts create mode 100644 src/button/demo/src/app/button/ButtonEventComponent.ts create mode 100644 src/button/demo/src/app/button/ButtonFocusComponent.ts create mode 100644 src/button/demo/src/app/button/ButtonHasborderComponent.ts create mode 100644 src/button/demo/src/app/button/ButtonHasborderTestComponent.ts create mode 100644 src/button/demo/src/app/button/ButtonIconComponent.ts create mode 100644 src/button/demo/src/app/button/ButtonLabelComponent.ts create mode 100644 src/button/demo/src/app/button/ButtonLoadingComponent.ts create mode 100644 src/button/demo/src/app/button/ButtonOnlyiconComponent.ts create mode 100644 src/button/demo/src/app/button/ButtonSizeComponent.ts create mode 100644 src/button/demo/src/app/button/ButtonTestModule.ts create mode 100644 src/button/demo/src/app/button/ButtonTipComponent.ts create mode 100644 src/button/demo/src/app/button/button-color.html create mode 100644 src/button/demo/src/app/button/button-disabled.html create mode 100644 src/button/demo/src/app/button/button-event.html create mode 100644 src/button/demo/src/app/button/button-focus.html create mode 100644 src/button/demo/src/app/button/button-hasborder-test.html create mode 100644 src/button/demo/src/app/button/button-hasborder.html create mode 100644 src/button/demo/src/app/button/button-icon.html create mode 100644 src/button/demo/src/app/button/button-label.html create mode 100644 src/button/demo/src/app/button/button-loading.html create mode 100644 src/button/demo/src/app/button/button-onlyicon.html create mode 100644 src/button/demo/src/app/button/button-size.html create mode 100644 src/button/demo/src/app/button/button-tip.html create mode 100644 src/button/demo/src/app/button/webdoc/button-demos.js create mode 100644 src/button/demo/src/app/button/webdoc/button.cn.md create mode 100644 src/button/demo/src/app/button/webdoc/button.en.md create mode 100644 src/button/demo/src/favicon.ico create mode 100644 src/button/demo/src/index.html create mode 100644 src/button/demo/src/main.ts create mode 100644 src/button/demo/test.ts create mode 100644 src/button/demo/tsconfig.app.json create mode 100644 src/button/demo/tsconfig.spec.json create mode 100644 src/button/lib/index.ts create mode 100644 src/button/lib/ng-package.json create mode 100644 src/button/lib/package.json create mode 100644 src/button/lib/project.json create mode 100644 src/button/lib/src/TiButtonComponent.ts create mode 100644 src/button/lib/src/TiButtonModule.ts create mode 100644 src/button/lib/src/button-mixin.less create mode 100644 src/button/lib/src/button.html create mode 100644 src/button/lib/src/button.less create mode 100644 src/button/lib/src/i18n/TiButtonWords.ts create mode 100644 src/button/lib/src/i18n/en_US.ts create mode 100644 src/button/lib/src/i18n/es_US.ts create mode 100644 src/button/lib/src/i18n/fr_FR.ts create mode 100644 src/button/lib/src/i18n/index.ts create mode 100644 src/button/lib/src/i18n/pt_BR.ts create mode 100644 src/button/lib/src/i18n/zh_CN.ts create mode 100644 src/buttongroup/demo/karma.conf.js create mode 100644 src/buttongroup/demo/project.json create mode 100644 src/buttongroup/demo/src/app/AppComponent.ts create mode 100644 src/buttongroup/demo/src/app/AppModule.ts create mode 100644 src/buttongroup/demo/src/app/IndexComponent.ts create mode 100644 src/buttongroup/demo/src/app/app.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtonGroupTestModule.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupActiveclassComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupBeforeclickComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupDeselectableComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupDisabledComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupEnumComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupEventComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupFocusComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupIdComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupIdTestComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupItemsComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupItemsTestComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupManyComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupMinwidthComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupMultiTypeComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupMultilineComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupMultipleComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupRadioTypeComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupReactiveFormsComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupSupComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupSupTestComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupTemplateComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupTipComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupValuekeyComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/ButtongroupValuekeyTestComponent.ts create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-activeclass.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-beforeclick.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-deselectable.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-disabled.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-enum.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-event.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-focus.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-id-test.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-id.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-items-test.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-items.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-many.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-minwidth.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-multi-type.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-multiline.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-multiple.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-radio-type.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-reactive-forms.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-sup-test.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-sup.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-template.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-tip.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-valuekey-test.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup-valuekey.html create mode 100644 src/buttongroup/demo/src/app/buttongroup/buttongroup.less create mode 100644 src/buttongroup/demo/src/app/buttongroup/webdoc/buttongroup-demos.js create mode 100644 src/buttongroup/demo/src/app/buttongroup/webdoc/buttongroup.cn.md create mode 100644 src/buttongroup/demo/src/app/buttongroup/webdoc/buttongroup.en.md create mode 100644 src/buttongroup/demo/src/favicon.ico create mode 100644 src/buttongroup/demo/src/index.html create mode 100644 src/buttongroup/demo/src/main.ts create mode 100644 src/buttongroup/demo/test.ts create mode 100644 src/buttongroup/demo/tsconfig.app.json create mode 100644 src/buttongroup/demo/tsconfig.spec.json create mode 100644 src/buttongroup/lib/index.ts create mode 100644 src/buttongroup/lib/ng-package.json create mode 100644 src/buttongroup/lib/package.json create mode 100644 src/buttongroup/lib/project.json create mode 100644 src/buttongroup/lib/src/TiButtongroupComponent.ts create mode 100644 src/buttongroup/lib/src/TiButtongroupModule.ts create mode 100644 src/buttongroup/lib/src/buttongroup.html create mode 100644 src/buttongroup/lib/src/buttongroup.less create mode 100644 src/buttonselect/demo/project.json create mode 100644 src/buttonselect/demo/src/app/AppComponent.ts create mode 100644 src/buttonselect/demo/src/app/AppModule.ts create mode 100644 src/buttonselect/demo/src/app/IndexComponent.ts create mode 100644 src/buttonselect/demo/src/app/app.html create mode 100644 src/buttonselect/demo/src/app/buttonselect/ButtonselectBasicComponent.ts create mode 100644 src/buttonselect/demo/src/app/buttonselect/ButtonselectLabelkeyComponent.ts create mode 100644 src/buttonselect/demo/src/app/buttonselect/ButtonselectTestModule.ts create mode 100644 src/buttonselect/demo/src/app/buttonselect/buttonselect-basic.html create mode 100644 src/buttonselect/demo/src/app/buttonselect/buttonselect-labelkey.html create mode 100644 src/buttonselect/demo/src/app/buttonselect/webdoc/buttonselect-demos.js create mode 100644 src/buttonselect/demo/src/app/buttonselect/webdoc/buttonselect.cn.md create mode 100644 src/buttonselect/demo/src/app/buttonselect/webdoc/buttonselect.en.md create mode 100644 src/buttonselect/demo/src/favicon.ico create mode 100644 src/buttonselect/demo/src/index.html create mode 100644 src/buttonselect/demo/src/main.ts create mode 100644 src/buttonselect/demo/tsconfig.app.json create mode 100644 src/buttonselect/lib/index.ts create mode 100644 src/buttonselect/lib/ng-package.json create mode 100644 src/buttonselect/lib/package.json create mode 100644 src/buttonselect/lib/project.json create mode 100644 src/buttonselect/lib/src/TiButtonselectComponent.ts create mode 100644 src/buttonselect/lib/src/TiButtonselectModule.ts create mode 100644 src/buttonselect/lib/src/buttonselect.html create mode 100644 src/buttonselect/lib/src/buttonselect.less create mode 100644 src/card/demo/karma.conf.js create mode 100644 src/card/demo/project.json create mode 100644 src/card/demo/src/app/AppComponent.ts create mode 100644 src/card/demo/src/app/AppModule.ts create mode 100644 src/card/demo/src/app/IndexComponent.ts create mode 100644 src/card/demo/src/app/app.html create mode 100644 src/card/demo/src/app/card/CardAddComponent.ts create mode 100644 src/card/demo/src/app/card/CardBasicComponent.ts create mode 100644 src/card/demo/src/app/card/CardGrid2Component.ts create mode 100644 src/card/demo/src/app/card/CardGridComponent.ts create mode 100644 src/card/demo/src/app/card/CardHeaderComponent.ts create mode 100644 src/card/demo/src/app/card/CardTestModule.ts create mode 100644 src/card/demo/src/app/card/card-add.html create mode 100644 src/card/demo/src/app/card/card-basic.html create mode 100644 src/card/demo/src/app/card/card-grid.html create mode 100644 src/card/demo/src/app/card/card-grid2.html create mode 100644 src/card/demo/src/app/card/card-header.html create mode 100644 src/card/demo/src/app/card/webdoc/card-demos.js create mode 100644 src/card/demo/src/app/card/webdoc/card.cn.md create mode 100644 src/card/demo/src/app/card/webdoc/card.en.md create mode 100644 src/card/demo/src/favicon.ico create mode 100644 src/card/demo/src/index.html create mode 100644 src/card/demo/src/main.ts create mode 100644 src/card/demo/test.ts create mode 100644 src/card/demo/tsconfig.app.json create mode 100644 src/card/demo/tsconfig.spec.json create mode 100644 src/card/lib/index.ts create mode 100644 src/card/lib/ng-package.json create mode 100644 src/card/lib/package.json create mode 100644 src/card/lib/project.json create mode 100644 src/card/lib/src/TiCardAddComponent.ts create mode 100644 src/card/lib/src/TiCardComponent.ts create mode 100644 src/card/lib/src/TiCardHeaderComponent.ts create mode 100644 src/card/lib/src/TiCardModule.ts create mode 100644 src/card/lib/src/card-add.html create mode 100644 src/card/lib/src/card-add.less create mode 100644 src/card/lib/src/card-header.less create mode 100644 src/card/lib/src/card.less create mode 100644 src/cascader/demo/karma.conf.js create mode 100644 src/cascader/demo/project.json create mode 100644 src/cascader/demo/src/app/AppComponent.ts create mode 100644 src/cascader/demo/src/app/AppModule.ts create mode 100644 src/cascader/demo/src/app/IndexComponent.ts create mode 100644 src/cascader/demo/src/app/app.html create mode 100644 src/cascader/demo/src/app/cascader/CascaderBasicComponent.ts create mode 100644 src/cascader/demo/src/app/cascader/CascaderClearableComponent.ts create mode 100644 src/cascader/demo/src/app/cascader/CascaderData.ts create mode 100644 src/cascader/demo/src/app/cascader/CascaderDisabledComponent.ts create mode 100644 src/cascader/demo/src/app/cascader/CascaderEventsComponent.ts create mode 100644 src/cascader/demo/src/app/cascader/CascaderIdkeyComponent.ts create mode 100644 src/cascader/demo/src/app/cascader/CascaderItemTestComponent.ts create mode 100644 src/cascader/demo/src/app/cascader/CascaderLabelkeyComponent.ts create mode 100644 src/cascader/demo/src/app/cascader/CascaderOnlyselectleafComponent.ts create mode 100644 src/cascader/demo/src/app/cascader/CascaderPanelComponent.ts create mode 100644 src/cascader/demo/src/app/cascader/CascaderSearchComponent.ts create mode 100644 src/cascader/demo/src/app/cascader/CascaderShowalllevelComponent.ts create mode 100644 src/cascader/demo/src/app/cascader/CascaderTestModule.ts create mode 100644 src/cascader/demo/src/app/cascader/CascaderTriggerComponent.ts create mode 100644 src/cascader/demo/src/app/cascader/CascaderValidComponent.ts create mode 100644 src/cascader/demo/src/app/cascader/CascaderValuekeyComponent.ts create mode 100644 src/cascader/demo/src/app/cascader/cascader-basic.html create mode 100644 src/cascader/demo/src/app/cascader/cascader-clearable.html create mode 100644 src/cascader/demo/src/app/cascader/cascader-disabled.html create mode 100644 src/cascader/demo/src/app/cascader/cascader-events.html create mode 100644 src/cascader/demo/src/app/cascader/cascader-idkey.html create mode 100644 src/cascader/demo/src/app/cascader/cascader-item-test.html create mode 100644 src/cascader/demo/src/app/cascader/cascader-labelkey.html create mode 100644 src/cascader/demo/src/app/cascader/cascader-onlyselectleaf.html create mode 100644 src/cascader/demo/src/app/cascader/cascader-panel.html create mode 100644 src/cascader/demo/src/app/cascader/cascader-search.html create mode 100644 src/cascader/demo/src/app/cascader/cascader-showalllevel.html create mode 100644 src/cascader/demo/src/app/cascader/cascader-trigger.html create mode 100644 src/cascader/demo/src/app/cascader/cascader-valid.html create mode 100644 src/cascader/demo/src/app/cascader/cascader-valuekey.html create mode 100644 src/cascader/demo/src/app/cascader/webdoc/cascader-demos.js create mode 100644 src/cascader/demo/src/app/cascader/webdoc/cascader.cn.md create mode 100644 src/cascader/demo/src/app/cascader/webdoc/cascader.en.md create mode 100644 src/cascader/demo/src/favicon.ico create mode 100644 src/cascader/demo/src/index.html create mode 100644 src/cascader/demo/src/main.ts create mode 100644 src/cascader/demo/test.ts create mode 100644 src/cascader/demo/tsconfig.app.json create mode 100644 src/cascader/demo/tsconfig.spec.json create mode 100644 src/cascader/lib/index.ts create mode 100644 src/cascader/lib/ng-package.json create mode 100644 src/cascader/lib/package.json create mode 100644 src/cascader/lib/project.json create mode 100644 src/cascader/lib/src/TiCascaderComponent.ts create mode 100644 src/cascader/lib/src/TiCascaderModule.ts create mode 100644 src/cascader/lib/src/cascader.html create mode 100644 src/cascader/lib/src/cascader.less create mode 100644 src/checkbox/demo/karma.conf.js create mode 100644 src/checkbox/demo/project.json create mode 100644 src/checkbox/demo/src/app/AppComponent.ts create mode 100644 src/checkbox/demo/src/app/AppModule.ts create mode 100644 src/checkbox/demo/src/app/IndexComponent.ts create mode 100644 src/checkbox/demo/src/app/app.html create mode 100644 src/checkbox/demo/src/app/checkbox/CheckboxBasicComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckboxDisabledComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckboxEventComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckboxFocusedComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckboxGroupComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckboxGroupDirectionComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckboxGroupLabelkeyComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckboxGroupLevelComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckboxGroupLinewrapComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckboxGroupValidationComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckboxGroupValuekeyComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckboxIndeterminateComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckboxLabelComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckboxTestModule.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckgroupChangeComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckgroupCrossComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckgroupDefaultComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckgroupDisabledTestComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckgroupRefreshComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckgroupScenesComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/CheckgroupTreeComponent.ts create mode 100644 src/checkbox/demo/src/app/checkbox/checkbox-basic.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkbox-disabled.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkbox-event.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkbox-focused.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkbox-group-direction.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkbox-group-labelkey.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkbox-group-level.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkbox-group-linewrap.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkbox-group-validation.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkbox-group-valuekey.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkbox-group.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkbox-indeterminate.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkbox-label.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkgroup-change.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkgroup-cross.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkgroup-default.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkgroup-disabled-test.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkgroup-refresh.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkgroup-scenes.html create mode 100644 src/checkbox/demo/src/app/checkbox/checkgroup-tree.html create mode 100644 src/checkbox/demo/src/app/checkbox/webdoc/checkbox-demos.js create mode 100644 src/checkbox/demo/src/app/checkbox/webdoc/checkbox.cn.md create mode 100644 src/checkbox/demo/src/app/checkbox/webdoc/checkbox.en.md create mode 100644 src/checkbox/demo/src/favicon.ico create mode 100644 src/checkbox/demo/src/index.html create mode 100644 src/checkbox/demo/src/main.ts create mode 100644 src/checkbox/demo/test.ts create mode 100644 src/checkbox/demo/tsconfig.app.json create mode 100644 src/checkbox/demo/tsconfig.spec.json create mode 100644 src/checkbox/lib/index.ts create mode 100644 src/checkbox/lib/ng-package.json create mode 100644 src/checkbox/lib/package.json create mode 100644 src/checkbox/lib/project.json create mode 100644 src/checkbox/lib/src/TiCheckboxComponent.ts create mode 100644 src/checkbox/lib/src/TiCheckboxGroupComponent.ts create mode 100644 src/checkbox/lib/src/TiCheckboxModule.ts create mode 100644 src/checkbox/lib/src/TiCheckgroupComponent.ts create mode 100644 src/checkbox/lib/src/TiCheckitemComponent.ts create mode 100644 src/checkbox/lib/src/checkbox.html create mode 100644 src/checkbox/lib/src/checkbox.less create mode 100644 src/checkbox/lib/src/checkboxgroup.html create mode 100644 src/checkbox/lib/src/checkboxgroup.less create mode 100644 src/collapse/demo/karma.conf.js create mode 100644 src/collapse/demo/project.json create mode 100644 src/collapse/demo/src/app/AppComponent.ts create mode 100644 src/collapse/demo/src/app/AppModule.ts create mode 100644 src/collapse/demo/src/app/IndexComponent.ts create mode 100644 src/collapse/demo/src/app/app.html create mode 100644 src/collapse/demo/src/app/collapse/CollapseBasicComponent.ts create mode 100644 src/collapse/demo/src/app/collapse/CollapseEventComponent.ts create mode 100644 src/collapse/demo/src/app/collapse/CollapseTestModule.ts create mode 100644 src/collapse/demo/src/app/collapse/collapse-basic.html create mode 100644 src/collapse/demo/src/app/collapse/collapse-event.html create mode 100644 src/collapse/demo/src/app/collapse/collapse.less create mode 100644 src/collapse/demo/src/app/collapse/webdoc/collapse-demos.js create mode 100644 src/collapse/demo/src/app/collapse/webdoc/collapse.cn.md create mode 100644 src/collapse/demo/src/app/collapse/webdoc/collapse.en.md create mode 100644 src/collapse/demo/src/favicon.ico create mode 100644 src/collapse/demo/src/index.html create mode 100644 src/collapse/demo/src/main.ts create mode 100644 src/collapse/demo/test.ts create mode 100644 src/collapse/demo/tsconfig.app.json create mode 100644 src/collapse/demo/tsconfig.spec.json create mode 100644 src/collapse/lib/index.ts create mode 100644 src/collapse/lib/ng-package.json create mode 100644 src/collapse/lib/package.json create mode 100644 src/collapse/lib/project.json create mode 100644 src/collapse/lib/src/TiCollapseDirective.ts create mode 100644 src/collapse/lib/src/TiCollapseModule.ts create mode 100644 src/collapsebox/demo/karma.conf.js create mode 100644 src/collapsebox/demo/project.json create mode 100644 src/collapsebox/demo/src/app/AppComponent.ts create mode 100644 src/collapsebox/demo/src/app/AppModule.ts create mode 100644 src/collapsebox/demo/src/app/IndexComponent.ts create mode 100644 src/collapsebox/demo/src/app/app.html create mode 100644 src/collapsebox/demo/src/app/collapsebox/CollapseboxBasicComponent.ts create mode 100644 src/collapsebox/demo/src/app/collapsebox/CollapseboxCloseableComponent.ts create mode 100644 src/collapsebox/demo/src/app/collapsebox/CollapseboxEventComponent.ts create mode 100644 src/collapsebox/demo/src/app/collapsebox/CollapseboxTestModule.ts create mode 100644 src/collapsebox/demo/src/app/collapsebox/collapsebox-basic.html create mode 100644 src/collapsebox/demo/src/app/collapsebox/collapsebox-closeable.html create mode 100644 src/collapsebox/demo/src/app/collapsebox/collapsebox-event.html create mode 100644 src/collapsebox/demo/src/app/collapsebox/webdoc/collapsebox-demos.js create mode 100644 src/collapsebox/demo/src/app/collapsebox/webdoc/collapsebox.cn.md create mode 100644 src/collapsebox/demo/src/app/collapsebox/webdoc/collapsebox.en.md create mode 100644 src/collapsebox/demo/src/favicon.ico create mode 100644 src/collapsebox/demo/src/index.html create mode 100644 src/collapsebox/demo/src/main.ts create mode 100644 src/collapsebox/demo/test.ts create mode 100644 src/collapsebox/demo/tsconfig.app.json create mode 100644 src/collapsebox/demo/tsconfig.spec.json create mode 100644 src/collapsebox/lib/index.ts create mode 100644 src/collapsebox/lib/ng-package.json create mode 100644 src/collapsebox/lib/package.json create mode 100644 src/collapsebox/lib/project.json create mode 100644 src/collapsebox/lib/src/TiCollapseboxComponent.ts create mode 100644 src/collapsebox/lib/src/TiCollapseboxModule.ts create mode 100644 src/collapsebox/lib/src/collapsebox.html create mode 100644 src/collapsebox/lib/src/collapsebox.less create mode 100644 src/collapsebutton/demo/karma.conf.js create mode 100644 src/collapsebutton/demo/project.json create mode 100644 src/collapsebutton/demo/src/app/AppComponent.ts create mode 100644 src/collapsebutton/demo/src/app/AppModule.ts create mode 100644 src/collapsebutton/demo/src/app/IndexComponent.ts create mode 100644 src/collapsebutton/demo/src/app/app.html create mode 100644 src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonBasicComponent.ts create mode 100644 src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonCustomtextComponent.ts create mode 100644 src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonEventComponent.ts create mode 100644 src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonSearchcountComponent.ts create mode 100644 src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonTestModule.ts create mode 100644 src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-basic.html create mode 100644 src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-customtext.html create mode 100644 src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-event.html create mode 100644 src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-searchcount.html create mode 100644 src/collapsebutton/demo/src/app/collapsebutton/webdoc/collapsebutton-demos.js create mode 100644 src/collapsebutton/demo/src/app/collapsebutton/webdoc/collapsebutton.cn.md create mode 100644 src/collapsebutton/demo/src/app/collapsebutton/webdoc/collapsebutton.en.md create mode 100644 src/collapsebutton/demo/src/favicon.ico create mode 100644 src/collapsebutton/demo/src/index.html create mode 100644 src/collapsebutton/demo/src/main.ts create mode 100644 src/collapsebutton/demo/test.ts create mode 100644 src/collapsebutton/demo/tsconfig.app.json create mode 100644 src/collapsebutton/demo/tsconfig.spec.json create mode 100644 src/collapsebutton/lib/index.ts create mode 100644 src/collapsebutton/lib/ng-package.json create mode 100644 src/collapsebutton/lib/package.json create mode 100644 src/collapsebutton/lib/project.json create mode 100644 src/collapsebutton/lib/src/TiCollapsebuttonComponent.ts create mode 100644 src/collapsebutton/lib/src/TiCollapsebuttonModule.ts create mode 100644 src/collapsebutton/lib/src/TiCollapsepanelComponent.ts create mode 100644 src/collapsebutton/lib/src/collapsebutton.html create mode 100644 src/collapsebutton/lib/src/collapsebutton.less create mode 100644 src/collapsebutton/lib/src/collapsepanel.less create mode 100644 src/collapsebutton/lib/src/i18n/TiCollapsebuttonWords.ts create mode 100644 src/collapsebutton/lib/src/i18n/en_US.ts create mode 100644 src/collapsebutton/lib/src/i18n/es_US.ts create mode 100644 src/collapsebutton/lib/src/i18n/fr_FR.ts create mode 100644 src/collapsebutton/lib/src/i18n/index.ts create mode 100644 src/collapsebutton/lib/src/i18n/pt_BR.ts create mode 100644 src/collapsebutton/lib/src/i18n/zh_CN.ts create mode 100644 src/collapsetext/demo/project.json create mode 100644 src/collapsetext/demo/src/app/AppComponent.ts create mode 100644 src/collapsetext/demo/src/app/AppModule.ts create mode 100644 src/collapsetext/demo/src/app/IndexComponent.ts create mode 100644 src/collapsetext/demo/src/app/app.html create mode 100644 src/collapsetext/demo/src/app/collapsetext/CollapsetextBasicComponent.ts create mode 100644 src/collapsetext/demo/src/app/collapsetext/CollapsetextCollapsedComponent.ts create mode 100644 src/collapsetext/demo/src/app/collapsetext/CollapsetextHighlightComponent.ts create mode 100644 src/collapsetext/demo/src/app/collapsetext/CollapsetextSceneComponent.ts create mode 100644 src/collapsetext/demo/src/app/collapsetext/CollapsetextTestModule.ts create mode 100644 src/collapsetext/demo/src/app/collapsetext/CollapsetextTypeComponent.ts create mode 100644 src/collapsetext/demo/src/app/collapsetext/collapsetext-basic.html create mode 100644 src/collapsetext/demo/src/app/collapsetext/collapsetext-collapsed.html create mode 100644 src/collapsetext/demo/src/app/collapsetext/collapsetext-highlight.html create mode 100644 src/collapsetext/demo/src/app/collapsetext/collapsetext-scene.html create mode 100644 src/collapsetext/demo/src/app/collapsetext/collapsetext-type.html create mode 100644 src/collapsetext/demo/src/app/collapsetext/webdoc/collapsetext-demos.js create mode 100644 src/collapsetext/demo/src/app/collapsetext/webdoc/collapsetext.cn.md create mode 100644 src/collapsetext/demo/src/app/collapsetext/webdoc/collapsetext.en.md create mode 100644 src/collapsetext/demo/src/favicon.ico create mode 100644 src/collapsetext/demo/src/index.html create mode 100644 src/collapsetext/demo/src/main.ts create mode 100644 src/collapsetext/demo/tsconfig.app.json create mode 100644 src/collapsetext/lib/index.ts create mode 100644 src/collapsetext/lib/ng-package.json create mode 100644 src/collapsetext/lib/package.json create mode 100644 src/collapsetext/lib/project.json create mode 100644 src/collapsetext/lib/src/TiCollapsetextComponent.ts create mode 100644 src/collapsetext/lib/src/TiCollapsetextModule.ts create mode 100644 src/collapsetext/lib/src/collapsetext.html create mode 100644 src/collapsetext/lib/src/collapsetext.less create mode 100644 src/copy/demo/karma.conf.js create mode 100644 src/copy/demo/project.json create mode 100644 src/copy/demo/src/app/AppComponent.ts create mode 100644 src/copy/demo/src/app/AppModule.ts create mode 100644 src/copy/demo/src/app/IndexComponent.ts create mode 100644 src/copy/demo/src/app/app.html create mode 100644 src/copy/demo/src/app/copy/CopyBasicComponent.ts create mode 100644 src/copy/demo/src/app/copy/CopyDarkComponent.ts create mode 100644 src/copy/demo/src/app/copy/CopyEventComponent.ts create mode 100644 src/copy/demo/src/app/copy/CopyTableComponent.ts create mode 100644 src/copy/demo/src/app/copy/CopyTestModule.ts create mode 100644 src/copy/demo/src/app/copy/CopyTipComponent.ts create mode 100644 src/copy/demo/src/app/copy/copy-basic.html create mode 100644 src/copy/demo/src/app/copy/copy-dark.html create mode 100644 src/copy/demo/src/app/copy/copy-event.html create mode 100644 src/copy/demo/src/app/copy/copy-table.html create mode 100644 src/copy/demo/src/app/copy/copy-tip.html create mode 100644 src/copy/demo/src/app/copy/webdoc/copy-demos.js create mode 100644 src/copy/demo/src/app/copy/webdoc/copy.cn.md create mode 100644 src/copy/demo/src/app/copy/webdoc/copy.en.md create mode 100644 src/copy/demo/src/favicon.ico create mode 100644 src/copy/demo/src/index.html create mode 100644 src/copy/demo/src/main.ts create mode 100644 src/copy/demo/test.ts create mode 100644 src/copy/demo/tsconfig.app.json create mode 100644 src/copy/demo/tsconfig.spec.json create mode 100644 src/copy/lib/index.ts create mode 100644 src/copy/lib/ng-package.json create mode 100644 src/copy/lib/package.json create mode 100644 src/copy/lib/project.json create mode 100644 src/copy/lib/src/TiCopyComponent.ts create mode 100644 src/copy/lib/src/TiCopyModule.ts create mode 100644 src/copy/lib/src/TiToastComponent.ts create mode 100644 src/copy/lib/src/copy.html create mode 100644 src/copy/lib/src/copy.less create mode 100644 src/copy/lib/src/i18n/TiCopyWords.ts create mode 100644 src/copy/lib/src/i18n/en_US.ts create mode 100644 src/copy/lib/src/i18n/es_US.ts create mode 100644 src/copy/lib/src/i18n/fr_FR.ts create mode 100644 src/copy/lib/src/i18n/index.ts create mode 100644 src/copy/lib/src/i18n/pt_BR.ts create mode 100644 src/copy/lib/src/i18n/zh_CN.ts create mode 100644 src/copy/lib/src/toast.less create mode 100644 src/crumb/demo/karma.conf.js create mode 100644 src/crumb/demo/project.json create mode 100644 src/crumb/demo/src/app/AppComponent.ts create mode 100644 src/crumb/demo/src/app/AppModule.ts create mode 100644 src/crumb/demo/src/app/IndexComponent.ts create mode 100644 src/crumb/demo/src/app/app.html create mode 100644 src/crumb/demo/src/app/crumb/CrumbBasicComponent.ts create mode 100644 src/crumb/demo/src/app/crumb/CrumbEventsComponent.ts create mode 100644 src/crumb/demo/src/app/crumb/CrumbHrefComponent.ts create mode 100644 src/crumb/demo/src/app/crumb/CrumbRouterComponent.ts create mode 100644 src/crumb/demo/src/app/crumb/CrumbRouterTestComponent.ts create mode 100644 src/crumb/demo/src/app/crumb/CrumbTestModule.ts create mode 100644 src/crumb/demo/src/app/crumb/Router1Component.ts create mode 100644 src/crumb/demo/src/app/crumb/Router2Component.ts create mode 100644 src/crumb/demo/src/app/crumb/Router3Component.ts create mode 100644 src/crumb/demo/src/app/crumb/crumb-basic.html create mode 100644 src/crumb/demo/src/app/crumb/crumb-events.html create mode 100644 src/crumb/demo/src/app/crumb/crumb-href.html create mode 100644 src/crumb/demo/src/app/crumb/crumb-router-test.html create mode 100644 src/crumb/demo/src/app/crumb/crumb-router.html create mode 100644 src/crumb/demo/src/app/crumb/webdoc/crumb-demos.js create mode 100644 src/crumb/demo/src/app/crumb/webdoc/crumb.cn.md create mode 100644 src/crumb/demo/src/app/crumb/webdoc/crumb.en.md create mode 100644 src/crumb/demo/src/favicon.ico create mode 100644 src/crumb/demo/src/index.html create mode 100644 src/crumb/demo/src/main.ts create mode 100644 src/crumb/demo/test.ts create mode 100644 src/crumb/demo/tsconfig.app.json create mode 100644 src/crumb/demo/tsconfig.spec.json create mode 100644 src/crumb/lib/index.ts create mode 100644 src/crumb/lib/ng-package.json create mode 100644 src/crumb/lib/package.json create mode 100644 src/crumb/lib/project.json create mode 100644 src/crumb/lib/src/TiCrumbComponent.ts create mode 100644 src/crumb/lib/src/TiCrumbModule.ts create mode 100644 src/crumb/lib/src/crumb.html create mode 100644 src/crumb/lib/src/crumb.less create mode 100644 src/date/demo/karma.conf.js create mode 100644 src/date/demo/project.json create mode 100644 src/date/demo/src/app/AppComponent.ts create mode 100644 src/date/demo/src/app/AppModule.ts create mode 100644 src/date/demo/src/app/IndexComponent.ts create mode 100644 src/date/demo/src/app/app.html create mode 100644 src/date/demo/src/app/date/DateCleariconComponent.ts create mode 100644 src/date/demo/src/app/date/DateCustomizeComponent.ts create mode 100644 src/date/demo/src/app/date/DateDisabledComponent.ts create mode 100644 src/date/demo/src/app/date/DateDisableddaysComponent.ts create mode 100644 src/date/demo/src/app/date/DateEventComponent.ts create mode 100644 src/date/demo/src/app/date/DateFormComponent.ts create mode 100644 src/date/demo/src/app/date/DateFormatComponent.ts create mode 100644 src/date/demo/src/app/date/DateFormatTestComponent.ts create mode 100644 src/date/demo/src/app/date/DateMaxComponent.ts create mode 100644 src/date/demo/src/app/date/DateMaxminComponent.ts create mode 100644 src/date/demo/src/app/date/DateMaxminTestComponent.ts create mode 100644 src/date/demo/src/app/date/DateMinComponent.ts create mode 100644 src/date/demo/src/app/date/DateNowdatetimeComponent.ts create mode 100644 src/date/demo/src/app/date/DatePanelalignComponent.ts create mode 100644 src/date/demo/src/app/date/DateTestModule.ts create mode 100644 src/date/demo/src/app/date/DateValidationComponent.ts create mode 100644 src/date/demo/src/app/date/DateValueComponent.ts create mode 100644 src/date/demo/src/app/date/DateValueTestComponent.ts create mode 100644 src/date/demo/src/app/date/date-clearicon.html create mode 100644 src/date/demo/src/app/date/date-customize.html create mode 100644 src/date/demo/src/app/date/date-disabled.html create mode 100644 src/date/demo/src/app/date/date-disableddays.html create mode 100644 src/date/demo/src/app/date/date-event.html create mode 100644 src/date/demo/src/app/date/date-form.html create mode 100644 src/date/demo/src/app/date/date-format-test.html create mode 100644 src/date/demo/src/app/date/date-format.html create mode 100644 src/date/demo/src/app/date/date-max.html create mode 100644 src/date/demo/src/app/date/date-maxmin-test.html create mode 100644 src/date/demo/src/app/date/date-maxmin.html create mode 100644 src/date/demo/src/app/date/date-min.html create mode 100644 src/date/demo/src/app/date/date-nowdatetime.html create mode 100644 src/date/demo/src/app/date/date-panelalign.html create mode 100644 src/date/demo/src/app/date/date-validation.html create mode 100644 src/date/demo/src/app/date/date-value-test.html create mode 100644 src/date/demo/src/app/date/date-value.html create mode 100644 src/date/demo/src/app/date/webdoc/date-demos.js create mode 100644 src/date/demo/src/app/date/webdoc/date.cn.md create mode 100644 src/date/demo/src/app/date/webdoc/date.en.md create mode 100644 src/date/demo/src/favicon.ico create mode 100644 src/date/demo/src/index.html create mode 100644 src/date/demo/src/main.ts create mode 100644 src/date/demo/test.ts create mode 100644 src/date/demo/tsconfig.app.json create mode 100644 src/date/demo/tsconfig.spec.json create mode 100644 src/date/lib/index.ts create mode 100644 src/date/lib/ng-package.json create mode 100644 src/date/lib/package.json create mode 100644 src/date/lib/project.json create mode 100644 src/date/lib/src/TiDateComponent.ts create mode 100644 src/date/lib/src/TiDateModule.ts create mode 100644 src/date/lib/src/date-common.less create mode 100644 src/date/lib/src/date.html create mode 100644 src/date/lib/src/date.less create mode 100644 src/datebase/lib/index.ts create mode 100644 src/datebase/lib/ng-package.json create mode 100644 src/datebase/lib/package.json create mode 100644 src/datebase/lib/project.json create mode 100644 src/datebase/lib/src/TiDateBaseComponent.ts create mode 100644 src/datebase/lib/src/TiDateBaseModule.ts create mode 100644 src/datedominator/demo/project.json create mode 100644 src/datedominator/demo/src/app/AppComponent.ts create mode 100644 src/datedominator/demo/src/app/AppModule.ts create mode 100644 src/datedominator/demo/src/app/IndexComponent.ts create mode 100644 src/datedominator/demo/src/app/app.html create mode 100644 src/datedominator/demo/src/app/datedominator/DateDominatorComponent.ts create mode 100644 src/datedominator/demo/src/app/datedominator/DateDominatorTestModule.ts create mode 100644 src/datedominator/demo/src/app/datedominator/datedominator.html create mode 100644 src/datedominator/demo/src/app/datedominator/datedominator.less create mode 100644 src/datedominator/demo/src/favicon.ico create mode 100644 src/datedominator/demo/src/index.html create mode 100644 src/datedominator/demo/src/main.ts create mode 100644 src/datedominator/demo/tsconfig.app.json create mode 100644 src/datedominator/lib/index.ts create mode 100644 src/datedominator/lib/ng-package.json create mode 100644 src/datedominator/lib/package.json create mode 100644 src/datedominator/lib/project.json create mode 100644 src/datedominator/lib/src/TiDateDominatorComponent.ts create mode 100644 src/datedominator/lib/src/TiDateDominatorModule.ts create mode 100644 src/datedominator/lib/src/datedominator.html create mode 100644 src/datedominator/lib/src/datedominator.less create mode 100644 src/datedominator/lib/src/i18n/TiDatedominatorWords.ts create mode 100644 src/datedominator/lib/src/i18n/en_US.ts create mode 100644 src/datedominator/lib/src/i18n/es_US.ts create mode 100644 src/datedominator/lib/src/i18n/fr_FR.ts create mode 100644 src/datedominator/lib/src/i18n/index.ts create mode 100644 src/datedominator/lib/src/i18n/pt_BR.ts create mode 100644 src/datedominator/lib/src/i18n/zh_CN.ts create mode 100644 src/dateedit/demo/project.json create mode 100644 src/dateedit/demo/src/app/AppComponent.ts create mode 100644 src/dateedit/demo/src/app/AppModule.ts create mode 100644 src/dateedit/demo/src/app/IndexComponent.ts create mode 100644 src/dateedit/demo/src/app/app.html create mode 100644 src/dateedit/demo/src/app/dateedit/DateEditComponent.ts create mode 100644 src/dateedit/demo/src/app/dateedit/DateEditFormatComponent.ts create mode 100644 src/dateedit/demo/src/app/dateedit/DateEditMaxComponent.ts create mode 100644 src/dateedit/demo/src/app/dateedit/DateEditMinComponent.ts create mode 100644 src/dateedit/demo/src/app/dateedit/DateEditTestModule.ts create mode 100644 src/dateedit/demo/src/app/dateedit/DateEditValueComponent.ts create mode 100644 src/dateedit/demo/src/app/dateedit/dateedit-format.html create mode 100644 src/dateedit/demo/src/app/dateedit/dateedit-max.html create mode 100644 src/dateedit/demo/src/app/dateedit/dateedit-min.html create mode 100644 src/dateedit/demo/src/app/dateedit/dateedit-value.html create mode 100644 src/dateedit/demo/src/app/dateedit/dateedit.html create mode 100644 src/dateedit/demo/src/app/dateedit/dateedit.less create mode 100644 src/dateedit/demo/src/favicon.ico create mode 100644 src/dateedit/demo/src/index.html create mode 100644 src/dateedit/demo/src/main.ts create mode 100644 src/dateedit/demo/tsconfig.app.json create mode 100644 src/dateedit/lib/index.ts create mode 100644 src/dateedit/lib/ng-package.json create mode 100644 src/dateedit/lib/package.json create mode 100644 src/dateedit/lib/project.json create mode 100644 src/dateedit/lib/src/TiDateEditComponent.ts create mode 100644 src/dateedit/lib/src/TiDateEditModule.ts create mode 100644 src/dateedit/lib/src/dateedit.html create mode 100644 src/dateedit/lib/src/i18n/TiDateeditWords.ts create mode 100644 src/dateedit/lib/src/i18n/en_US.ts create mode 100644 src/dateedit/lib/src/i18n/es_US.ts create mode 100644 src/dateedit/lib/src/i18n/fr_FR.ts create mode 100644 src/dateedit/lib/src/i18n/index.ts create mode 100644 src/dateedit/lib/src/i18n/pt_BR.ts create mode 100644 src/dateedit/lib/src/i18n/zh_CN.ts create mode 100644 src/datepanel/lib/index.ts create mode 100644 src/datepanel/lib/ng-package.json create mode 100644 src/datepanel/lib/package.json create mode 100644 src/datepanel/lib/project.json create mode 100644 src/datepanel/lib/src/TiDatePanelComponent.ts create mode 100644 src/datepanel/lib/src/TiDatePanelModule.ts create mode 100644 src/datepanel/lib/src/datepanel.html create mode 100644 src/datepanel/lib/src/datepanel.less create mode 100644 src/datepanel/lib/src/i18n/TiDatepanelWords.ts create mode 100644 src/datepanel/lib/src/i18n/en_US.ts create mode 100644 src/datepanel/lib/src/i18n/es_US.ts create mode 100644 src/datepanel/lib/src/i18n/fr_FR.ts create mode 100644 src/datepanel/lib/src/i18n/index.ts create mode 100644 src/datepanel/lib/src/i18n/pt_BR.ts create mode 100644 src/datepanel/lib/src/i18n/zh_CN.ts create mode 100644 src/daterange/demo/karma.conf.js create mode 100644 src/daterange/demo/project.json create mode 100644 src/daterange/demo/src/app/AppComponent.ts create mode 100644 src/daterange/demo/src/app/AppModule.ts create mode 100644 src/daterange/demo/src/app/IndexComponent.ts create mode 100644 src/daterange/demo/src/app/app.html create mode 100644 src/daterange/demo/src/app/daterange/DateRangeTestModule.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeCustomizeComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeDisabledComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeDisableddaysComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeEventComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeFixedvalueComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeFixedvalueTestComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeFormatComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeFormatTestComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeIsallowbeginequalendComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeMaxComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeMaxminComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeMaxminTestComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeMinComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeNowdatetimeComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangePanelalignComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeValidationComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeValueComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/DaterangeValueTestComponent.ts create mode 100644 src/daterange/demo/src/app/daterange/daterange-customize.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-disabled.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-disableddays.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-event.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-fixedvalue-test.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-fixedvalue.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-format-test.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-format.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-isallowbeginequalend.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-max.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-maxmin-test.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-maxmin.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-min.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-nowdatetime.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-panelalign.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-validation.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-value-test.html create mode 100644 src/daterange/demo/src/app/daterange/daterange-value.html create mode 100644 src/daterange/demo/src/app/daterange/webdoc/daterange-demos.js create mode 100644 src/daterange/demo/src/app/daterange/webdoc/daterange.cn.md create mode 100644 src/daterange/demo/src/app/daterange/webdoc/daterange.en.md create mode 100644 src/daterange/demo/src/favicon.ico create mode 100644 src/daterange/demo/src/index.html create mode 100644 src/daterange/demo/src/main.ts create mode 100644 src/daterange/demo/test.ts create mode 100644 src/daterange/demo/tsconfig.app.json create mode 100644 src/daterange/demo/tsconfig.spec.json create mode 100644 src/daterange/lib/index.ts create mode 100644 src/daterange/lib/ng-package.json create mode 100644 src/daterange/lib/package.json create mode 100644 src/daterange/lib/project.json create mode 100644 src/daterange/lib/src/TiDateRangeComponent.ts create mode 100644 src/daterange/lib/src/TiDateRangeModule.ts create mode 100644 src/daterange/lib/src/daterange.html create mode 100644 src/daterange/lib/src/daterange.less create mode 100644 src/datetime/demo/karma.conf.js create mode 100644 src/datetime/demo/project.json create mode 100644 src/datetime/demo/src/app/AppComponent.ts create mode 100644 src/datetime/demo/src/app/AppModule.ts create mode 100644 src/datetime/demo/src/app/IndexComponent.ts create mode 100644 src/datetime/demo/src/app/app.html create mode 100644 src/datetime/demo/src/app/datetime/DatetimeCleariconComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimeCustomizeComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimeDisabledComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimeEventComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimeFormatComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimeFormatTestComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimeMaxComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimeMaxminComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimeMaxminTestComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimeMinComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimeNowdatetimeComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimePanelalignComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimeTestModule.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimeTimezoneableComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimeValidationComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimeValueComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/DatetimeValueTestComponent.ts create mode 100644 src/datetime/demo/src/app/datetime/datetime-clearicon.html create mode 100644 src/datetime/demo/src/app/datetime/datetime-customize.html create mode 100644 src/datetime/demo/src/app/datetime/datetime-disabled.html create mode 100644 src/datetime/demo/src/app/datetime/datetime-event.html create mode 100644 src/datetime/demo/src/app/datetime/datetime-format-test.html create mode 100644 src/datetime/demo/src/app/datetime/datetime-format.html create mode 100644 src/datetime/demo/src/app/datetime/datetime-max.html create mode 100644 src/datetime/demo/src/app/datetime/datetime-maxmin-test.html create mode 100644 src/datetime/demo/src/app/datetime/datetime-maxmin.html create mode 100644 src/datetime/demo/src/app/datetime/datetime-min.html create mode 100644 src/datetime/demo/src/app/datetime/datetime-nowdatetime.html create mode 100644 src/datetime/demo/src/app/datetime/datetime-panelalign.html create mode 100644 src/datetime/demo/src/app/datetime/datetime-timezoneable.html create mode 100644 src/datetime/demo/src/app/datetime/datetime-validation.html create mode 100644 src/datetime/demo/src/app/datetime/datetime-value-test.html create mode 100644 src/datetime/demo/src/app/datetime/datetime-value.html create mode 100644 src/datetime/demo/src/app/datetime/webdoc/datetime-demos.js create mode 100644 src/datetime/demo/src/app/datetime/webdoc/datetime.cn.md create mode 100644 src/datetime/demo/src/app/datetime/webdoc/datetime.en.md create mode 100644 src/datetime/demo/src/favicon.ico create mode 100644 src/datetime/demo/src/index.html create mode 100644 src/datetime/demo/src/main.ts create mode 100644 src/datetime/demo/test.ts create mode 100644 src/datetime/demo/tsconfig.app.json create mode 100644 src/datetime/demo/tsconfig.spec.json create mode 100644 src/datetime/lib/index.ts create mode 100644 src/datetime/lib/ng-package.json create mode 100644 src/datetime/lib/package.json create mode 100644 src/datetime/lib/project.json create mode 100644 src/datetime/lib/src/TiDatetimeComponent.ts create mode 100644 src/datetime/lib/src/TiDatetimeModule.ts create mode 100644 src/datetime/lib/src/datetime.html create mode 100644 src/datetime/lib/src/datetime.less create mode 100644 src/datetimerange/demo/karma.conf.js create mode 100644 src/datetimerange/demo/project.json create mode 100644 src/datetimerange/demo/src/app/AppComponent.ts create mode 100644 src/datetimerange/demo/src/app/AppModule.ts create mode 100644 src/datetimerange/demo/src/app/IndexComponent.ts create mode 100644 src/datetimerange/demo/src/app/app.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimeRangeTestModule.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeCleariconComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeCustomizeComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeDisabledComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeEventComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeFormatComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeFormatTestComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeIsallowbeginequalendComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeManyTestComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeMaxComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeMaxminComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeMaxminTestComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeMinComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeNowdatetimeComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangePanelalignComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeTimezoneableComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeValidationComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeValueComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/DatetimerangeValueTestComponent.ts create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-clearicon.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-customize.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-disabled.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-event.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-format-test.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-format.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-isallowbeginequalend.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-many-test.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-max.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-maxmin-test.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-maxmin.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-min.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-nowdatetime.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-panelalign.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-timezoneable.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-validation.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-value-test.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/datetimerange-value.html create mode 100644 src/datetimerange/demo/src/app/datetimerange/webdoc/datetimerange-demos.js create mode 100644 src/datetimerange/demo/src/app/datetimerange/webdoc/datetimerange.cn.md create mode 100644 src/datetimerange/demo/src/app/datetimerange/webdoc/datetimerange.en.md create mode 100644 src/datetimerange/demo/src/favicon.ico create mode 100644 src/datetimerange/demo/src/index.html create mode 100644 src/datetimerange/demo/src/main.ts create mode 100644 src/datetimerange/demo/test.ts create mode 100644 src/datetimerange/demo/tsconfig.app.json create mode 100644 src/datetimerange/demo/tsconfig.spec.json create mode 100644 src/datetimerange/lib/index.ts create mode 100644 src/datetimerange/lib/ng-package.json create mode 100644 src/datetimerange/lib/package.json create mode 100644 src/datetimerange/lib/project.json create mode 100644 src/datetimerange/lib/src/TiDatetimeRangeComponent.ts create mode 100644 src/datetimerange/lib/src/TiDatetimeRangeModule.ts create mode 100644 src/datetimerange/lib/src/datetimerange.html create mode 100644 src/datetimerange/lib/src/datetimerange.less create mode 100644 src/dominator/demo/project.json create mode 100644 src/dominator/demo/src/app/AppComponent.ts create mode 100644 src/dominator/demo/src/app/AppModule.ts create mode 100644 src/dominator/demo/src/app/IndexComponent.ts create mode 100644 src/dominator/demo/src/app/app.html create mode 100644 src/dominator/demo/src/app/dominator/DominatorDefaultComponent.ts create mode 100644 src/dominator/demo/src/app/dominator/DominatorTestModule.ts create mode 100644 src/dominator/demo/src/app/dominator/dominator-default.html create mode 100644 src/dominator/demo/src/favicon.ico create mode 100644 src/dominator/demo/src/index.html create mode 100644 src/dominator/demo/src/main.ts create mode 100644 src/dominator/demo/tsconfig.app.json create mode 100644 src/dominator/lib/index.ts create mode 100644 src/dominator/lib/ng-package.json create mode 100644 src/dominator/lib/package.json create mode 100644 src/dominator/lib/project.json create mode 100644 src/dominator/lib/src/TiDominatorComponent.ts create mode 100644 src/dominator/lib/src/TiDominatorModule.ts create mode 100644 src/dominator/lib/src/dominator.html create mode 100644 src/dominator/lib/src/dominator.less create mode 100644 src/dominator/lib/src/i18n/TiDominatorWords.ts create mode 100644 src/dominator/lib/src/i18n/en_US.ts create mode 100644 src/dominator/lib/src/i18n/es_US.ts create mode 100644 src/dominator/lib/src/i18n/fr_FR.ts create mode 100644 src/dominator/lib/src/i18n/index.ts create mode 100644 src/dominator/lib/src/i18n/pt_BR.ts create mode 100644 src/dominator/lib/src/i18n/zh_CN.ts create mode 100644 src/drag/demo/project.json create mode 100644 src/drag/demo/src/app/AppComponent.ts create mode 100644 src/drag/demo/src/app/AppModule.ts create mode 100644 src/drag/demo/src/app/IndexComponent.ts create mode 100644 src/drag/demo/src/app/app.html create mode 100644 src/drag/demo/src/app/drag/DragBasicComponent.ts create mode 100644 src/drag/demo/src/app/drag/DragServiceComponent.ts create mode 100644 src/drag/demo/src/app/drag/DragTestComponent.ts create mode 100644 src/drag/demo/src/app/drag/DragTestModule.ts create mode 100644 src/drag/demo/src/app/drag/drag-basic.html create mode 100644 src/drag/demo/src/app/drag/drag-service.html create mode 100644 src/drag/demo/src/app/drag/drag-test.html create mode 100644 src/drag/demo/src/app/drag/dragTest.less create mode 100644 src/drag/demo/src/favicon.ico create mode 100644 src/drag/demo/src/index.html create mode 100644 src/drag/demo/src/main.ts create mode 100644 src/drag/demo/tsconfig.app.json create mode 100644 src/drag/lib/index.ts create mode 100644 src/drag/lib/ng-package.json create mode 100644 src/drag/lib/package.json create mode 100644 src/drag/lib/project.json create mode 100644 src/drag/lib/src/TiDragService.ts create mode 100644 src/drag/lib/src/TiDragServiceModule.ts create mode 100644 src/drag/lib/src/TiDraggableDirective.ts create mode 100644 src/drag/lib/src/TiDraggableModule.ts create mode 100644 src/drop/demo/project.json create mode 100644 src/drop/demo/src/app/AppComponent.ts create mode 100644 src/drop/demo/src/app/AppModule.ts create mode 100644 src/drop/demo/src/app/IndexComponent.ts create mode 100644 src/drop/demo/src/app/app.html create mode 100644 src/drop/demo/src/app/drop/DropAppendtobodyComponent.ts create mode 100644 src/drop/demo/src/app/drop/DropDefaultComponent.ts create mode 100644 src/drop/demo/src/app/drop/DropTestModule.ts create mode 100644 src/drop/demo/src/app/drop/drop-appendtobody.html create mode 100644 src/drop/demo/src/app/drop/drop-default.html create mode 100644 src/drop/demo/src/favicon.ico create mode 100644 src/drop/demo/src/index.html create mode 100644 src/drop/demo/src/main.ts create mode 100644 src/drop/demo/tsconfig.app.json create mode 100644 src/drop/lib/index.ts create mode 100644 src/drop/lib/ng-package.json create mode 100644 src/drop/lib/package.json create mode 100644 src/drop/lib/project.json create mode 100644 src/drop/lib/src/TiDropComponent.ts create mode 100644 src/drop/lib/src/TiDropModule.ts create mode 100644 src/drop/lib/src/drop.html create mode 100644 src/drop/lib/src/drop.less create mode 100644 src/droplist/demo/project.json create mode 100644 src/droplist/demo/src/app/AppComponent.ts create mode 100644 src/droplist/demo/src/app/AppModule.ts create mode 100644 src/droplist/demo/src/app/IndexComponent.ts create mode 100644 src/droplist/demo/src/app/app.html create mode 100644 src/droplist/demo/src/app/droplist/DroplistDefaultComponent.ts create mode 100644 src/droplist/demo/src/app/droplist/DroplistTestModule.ts create mode 100644 src/droplist/demo/src/app/droplist/droplist-default.html create mode 100644 src/droplist/demo/src/favicon.ico create mode 100644 src/droplist/demo/src/index.html create mode 100644 src/droplist/demo/src/main.ts create mode 100644 src/droplist/demo/tsconfig.app.json create mode 100644 src/droplist/lib/index.ts create mode 100644 src/droplist/lib/ng-package.json create mode 100644 src/droplist/lib/package.json create mode 100644 src/droplist/lib/project.json create mode 100644 src/droplist/lib/src/TiDroplistComponent.ts create mode 100644 src/droplist/lib/src/TiDroplistModule.ts create mode 100644 src/droplist/lib/src/droplist.html create mode 100644 src/dropsearch/lib/index.ts create mode 100644 src/dropsearch/lib/ng-package.json create mode 100644 src/dropsearch/lib/package.json create mode 100644 src/dropsearch/lib/project.json create mode 100644 src/dropsearch/lib/src/TiDropsearchComponent.ts create mode 100644 src/dropsearch/lib/src/TiDropsearchModule.ts create mode 100644 src/dropsearch/lib/src/dropsearch.html create mode 100644 src/dropsearch/lib/src/dropsearch.less create mode 100644 src/environments/environment.prod.ts create mode 100644 src/environments/environment.ts create mode 100644 src/environments/environment.wc.ts create mode 100644 src/foldtext/demo/karma.conf.js create mode 100644 src/foldtext/demo/project.json create mode 100644 src/foldtext/demo/src/app/AppComponent.ts create mode 100644 src/foldtext/demo/src/app/AppModule.ts create mode 100644 src/foldtext/demo/src/app/IndexComponent.ts create mode 100644 src/foldtext/demo/src/app/app.html create mode 100644 src/foldtext/demo/src/app/foldtext/FoldtextBasicComponent.ts create mode 100644 src/foldtext/demo/src/app/foldtext/FoldtextTableComponent.ts create mode 100644 src/foldtext/demo/src/app/foldtext/FoldtextTestModule.ts create mode 100644 src/foldtext/demo/src/app/foldtext/foldtext-basic.html create mode 100644 src/foldtext/demo/src/app/foldtext/foldtext-table.html create mode 100644 src/foldtext/demo/src/app/foldtext/webdoc/foldtext-demos.js create mode 100644 src/foldtext/demo/src/app/foldtext/webdoc/foldtext.cn.md create mode 100644 src/foldtext/demo/src/app/foldtext/webdoc/foldtext.en.md create mode 100644 src/foldtext/demo/src/favicon.ico create mode 100644 src/foldtext/demo/src/index.html create mode 100644 src/foldtext/demo/src/main.ts create mode 100644 src/foldtext/demo/test.ts create mode 100644 src/foldtext/demo/tsconfig.app.json create mode 100644 src/foldtext/demo/tsconfig.spec.json create mode 100644 src/foldtext/lib/index.ts create mode 100644 src/foldtext/lib/ng-package.json create mode 100644 src/foldtext/lib/package.json create mode 100644 src/foldtext/lib/project.json create mode 100644 src/foldtext/lib/src/TiFoldtextComponent.ts create mode 100644 src/foldtext/lib/src/TiFoldtextModule.ts create mode 100644 src/foldtext/lib/src/foldtext.html create mode 100644 src/foldtext/lib/src/foldtext.less create mode 100644 src/foldtext/lib/src/i18n/TiFoldtextWords.ts create mode 100644 src/foldtext/lib/src/i18n/en_US.ts create mode 100644 src/foldtext/lib/src/i18n/es_US.ts create mode 100644 src/foldtext/lib/src/i18n/fr_FR.ts create mode 100644 src/foldtext/lib/src/i18n/index.ts create mode 100644 src/foldtext/lib/src/i18n/pt_BR.ts create mode 100644 src/foldtext/lib/src/i18n/zh_CN.ts create mode 100644 src/formfield/demo/karma.conf.js create mode 100644 src/formfield/demo/project.json create mode 100644 src/formfield/demo/src/app/AppComponent.ts create mode 100644 src/formfield/demo/src/app/AppModule.ts create mode 100644 src/formfield/demo/src/app/IndexComponent.ts create mode 100644 src/formfield/demo/src/app/app.html create mode 100644 src/formfield/demo/src/app/formfield/FormfieldColspanRowspanComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldColspanRowspanTestComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldColswidthComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldFooComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldIndexComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldLabelComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldLabelwidthComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldMultiColumnComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldNestFormfiledComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldNgforTestComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldRawgapComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldRequiredComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldRequiredspaceComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldRequiredspaceTestComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldShowComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldSingleColumnComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldTestComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldTestModule.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldTextFormComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldVerticalAlignComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/FormfieldVerticalComponent.ts create mode 100644 src/formfield/demo/src/app/formfield/formfield-colspan-rowspan-test.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-colspan-rowspan.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-colswidth.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-foo.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-index.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-label.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-labelwidth.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-multi-column.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-nest-formfiled.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-ngfor-test.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-required.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-requiredspace-test.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-requiredspace.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-rowgap.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-show.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-single-column.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-test.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-text-form.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-vertical-align.html create mode 100644 src/formfield/demo/src/app/formfield/formfield-vertical.html create mode 100644 src/formfield/demo/src/app/formfield/webdoc/formfield-demos.js create mode 100644 src/formfield/demo/src/app/formfield/webdoc/formfield.cn.md create mode 100644 src/formfield/demo/src/app/formfield/webdoc/formfield.en.md create mode 100644 src/formfield/demo/src/favicon.ico create mode 100644 src/formfield/demo/src/index.html create mode 100644 src/formfield/demo/src/main.ts create mode 100644 src/formfield/demo/test.ts create mode 100644 src/formfield/demo/tsconfig.app.json create mode 100644 src/formfield/demo/tsconfig.spec.json create mode 100644 src/formfield/lib/index.ts create mode 100644 src/formfield/lib/ng-package.json create mode 100644 src/formfield/lib/package.json create mode 100644 src/formfield/lib/project.json create mode 100644 src/formfield/lib/src/TiButtonItemComponent.ts create mode 100644 src/formfield/lib/src/TiFormfieldComponent.ts create mode 100644 src/formfield/lib/src/TiFormfieldModule.ts create mode 100644 src/formfield/lib/src/TiItemComponent.ts create mode 100644 src/formfield/lib/src/TiItemLabelComponent.ts create mode 100644 src/formfield/lib/src/formfield-btn-item.html create mode 100644 src/formfield/lib/src/formfield-item-label.html create mode 100644 src/formfield/lib/src/formfield-item.html create mode 100644 src/formfield/lib/src/formfield.html create mode 100644 src/formfield/lib/src/formfield.less create mode 100644 src/grid/lib/index.ts create mode 100644 src/grid/lib/ng-package.json create mode 100644 src/grid/lib/package.json create mode 100644 src/grid/lib/project.json create mode 100644 src/grid/lib/src/TiGridComponent.ts create mode 100644 src/grid/lib/src/TiGridModule.ts create mode 100644 src/grid/lib/src/grid.less create mode 100644 src/guides/demo/project.json create mode 100644 src/guides/demo/src/app/AppComponent.ts create mode 100644 src/guides/demo/src/app/AppModule.ts create mode 100644 src/guides/demo/src/app/IndexComponent.ts create mode 100644 src/guides/demo/src/app/app.html create mode 100644 src/guides/demo/src/app/guides/GuidesBasicComponent.ts create mode 100644 src/guides/demo/src/app/guides/GuidesGuidestepsComponent.ts create mode 100644 src/guides/demo/src/app/guides/GuidesTabComponent.ts create mode 100644 src/guides/demo/src/app/guides/GuidesTestModule.ts create mode 100644 src/guides/demo/src/app/guides/GuidesTypeComponent.ts create mode 100644 src/guides/demo/src/app/guides/guides-basic.html create mode 100644 src/guides/demo/src/app/guides/guides-guidesteps.html create mode 100644 src/guides/demo/src/app/guides/guides-tab.html create mode 100644 src/guides/demo/src/app/guides/guides-type.html create mode 100644 src/guides/demo/src/app/guides/webdoc/guides-demos.js create mode 100644 src/guides/demo/src/app/guides/webdoc/guides.cn.md create mode 100644 src/guides/demo/src/app/guides/webdoc/guides.en.md create mode 100644 src/guides/demo/src/favicon.ico create mode 100644 src/guides/demo/src/index.html create mode 100644 src/guides/demo/src/main.ts create mode 100644 src/guides/demo/tsconfig.app.json create mode 100644 src/guides/lib/index.ts create mode 100644 src/guides/lib/ng-package.json create mode 100644 src/guides/lib/package.json create mode 100644 src/guides/lib/project.json create mode 100644 src/guides/lib/src/TiGuideBodyComponent.ts create mode 100644 src/guides/lib/src/TiGuideComponent.ts create mode 100644 src/guides/lib/src/TiGuideContentComponent.ts create mode 100644 src/guides/lib/src/TiGuideHeaderComponent.ts create mode 100644 src/guides/lib/src/TiGuidesComponent.ts create mode 100644 src/guides/lib/src/TiGuidesModule.ts create mode 100644 src/guides/lib/src/guide-body.html create mode 100644 src/guides/lib/src/guide-body.less create mode 100644 src/guides/lib/src/guide-content.less create mode 100644 src/guides/lib/src/guide-header.less create mode 100644 src/guides/lib/src/guide.html create mode 100644 src/guides/lib/src/guide.less create mode 100644 src/guides/lib/src/guides.less create mode 100644 src/guidesteps/demo/project.json create mode 100644 src/guidesteps/demo/src/app/AppComponent.ts create mode 100644 src/guidesteps/demo/src/app/AppModule.ts create mode 100644 src/guidesteps/demo/src/app/IndexComponent.ts create mode 100644 src/guidesteps/demo/src/app/app.html create mode 100644 src/guidesteps/demo/src/app/guidesteps/GuidestepsBasicComponent.ts create mode 100644 src/guidesteps/demo/src/app/guidesteps/GuidestepsIscompleteComponent.ts create mode 100644 src/guidesteps/demo/src/app/guidesteps/GuidestepsLargeComponent.ts create mode 100644 src/guidesteps/demo/src/app/guidesteps/GuidestepsTestModule.ts create mode 100644 src/guidesteps/demo/src/app/guidesteps/guidesteps-basic.html create mode 100644 src/guidesteps/demo/src/app/guidesteps/guidesteps-iscomplete.html create mode 100644 src/guidesteps/demo/src/app/guidesteps/guidesteps-large.html create mode 100644 src/guidesteps/demo/src/app/guidesteps/webdoc/guidesteps-demos.js create mode 100644 src/guidesteps/demo/src/app/guidesteps/webdoc/guidesteps.cn.md create mode 100644 src/guidesteps/demo/src/app/guidesteps/webdoc/guidesteps.en.md create mode 100644 src/guidesteps/demo/src/favicon.ico create mode 100644 src/guidesteps/demo/src/index.html create mode 100644 src/guidesteps/demo/src/main.ts create mode 100644 src/guidesteps/demo/tsconfig.app.json create mode 100644 src/guidesteps/lib/index.ts create mode 100644 src/guidesteps/lib/ng-package.json create mode 100644 src/guidesteps/lib/package.json create mode 100644 src/guidesteps/lib/project.json create mode 100644 src/guidesteps/lib/src/TiGuidestepComponent.ts create mode 100644 src/guidesteps/lib/src/TiGuidestepContentComponent.ts create mode 100644 src/guidesteps/lib/src/TiGuidestepHeaderComponent.ts create mode 100644 src/guidesteps/lib/src/TiGuidestepsComponent.ts create mode 100644 src/guidesteps/lib/src/TiGuidestepsModule.ts create mode 100644 src/guidesteps/lib/src/guidestep-content.less create mode 100644 src/guidesteps/lib/src/guidestep-header.less create mode 100644 src/guidesteps/lib/src/guidestep.less create mode 100644 src/guidesteps/lib/src/guidesteps.less create mode 100644 src/halfmodal/demo/karma.conf.js create mode 100644 src/halfmodal/demo/project.json create mode 100644 src/halfmodal/demo/src/app/AppComponent.ts create mode 100644 src/halfmodal/demo/src/app/AppModule.ts create mode 100644 src/halfmodal/demo/src/app/IndexComponent.ts create mode 100644 src/halfmodal/demo/src/app/app.html create mode 100644 src/halfmodal/demo/src/app/halfmodal/HalfmodalAsyncComponent.ts create mode 100644 src/halfmodal/demo/src/app/halfmodal/HalfmodalBackdropComponent.ts create mode 100644 src/halfmodal/demo/src/app/halfmodal/HalfmodalBasicComponent.ts create mode 100644 src/halfmodal/demo/src/app/halfmodal/HalfmodalBeforehideComponent.ts create mode 100644 src/halfmodal/demo/src/app/halfmodal/HalfmodalContentComponent.ts create mode 100644 src/halfmodal/demo/src/app/halfmodal/HalfmodalModalComponent.ts create mode 100644 src/halfmodal/demo/src/app/halfmodal/HalfmodalModalselectComponent.ts create mode 100644 src/halfmodal/demo/src/app/halfmodal/HalfmodalMultiComponent.ts create mode 100644 src/halfmodal/demo/src/app/halfmodal/HalfmodalServiceComponent.ts create mode 100644 src/halfmodal/demo/src/app/halfmodal/HalfmodalServiceTestComponent.ts create mode 100644 src/halfmodal/demo/src/app/halfmodal/HalfmodalTestModule.ts create mode 100644 src/halfmodal/demo/src/app/halfmodal/halfmodal-async.html create mode 100644 src/halfmodal/demo/src/app/halfmodal/halfmodal-backdrop.html create mode 100644 src/halfmodal/demo/src/app/halfmodal/halfmodal-basic.html create mode 100644 src/halfmodal/demo/src/app/halfmodal/halfmodal-beforehide.html create mode 100644 src/halfmodal/demo/src/app/halfmodal/halfmodal-content.html create mode 100644 src/halfmodal/demo/src/app/halfmodal/halfmodal-modal.html create mode 100644 src/halfmodal/demo/src/app/halfmodal/halfmodal-modalselect.html create mode 100644 src/halfmodal/demo/src/app/halfmodal/halfmodal-multi.html create mode 100644 src/halfmodal/demo/src/app/halfmodal/halfmodal-service-test.html create mode 100644 src/halfmodal/demo/src/app/halfmodal/halfmodal-service.html create mode 100644 src/halfmodal/demo/src/app/halfmodal/halfmodalTest.less create mode 100644 src/halfmodal/demo/src/app/halfmodal/webdoc/formfield.en.md create mode 100644 src/halfmodal/demo/src/app/halfmodal/webdoc/halfmodal-demos.js create mode 100644 src/halfmodal/demo/src/app/halfmodal/webdoc/halfmodal.cn.md create mode 100644 src/halfmodal/demo/src/favicon.ico create mode 100644 src/halfmodal/demo/src/index.html create mode 100644 src/halfmodal/demo/src/main.ts create mode 100644 src/halfmodal/demo/test.ts create mode 100644 src/halfmodal/demo/tsconfig.app.json create mode 100644 src/halfmodal/demo/tsconfig.spec.json create mode 100644 src/halfmodal/lib/index.ts create mode 100644 src/halfmodal/lib/ng-package.json create mode 100644 src/halfmodal/lib/package.json create mode 100644 src/halfmodal/lib/project.json create mode 100644 src/halfmodal/lib/src/TiHalfmodalBodyComponent.ts create mode 100644 src/halfmodal/lib/src/TiHalfmodalComponent.ts create mode 100644 src/halfmodal/lib/src/TiHalfmodalContainerComponent.ts create mode 100644 src/halfmodal/lib/src/TiHalfmodalFooterComponent.ts create mode 100644 src/halfmodal/lib/src/TiHalfmodalHeaderComponent.ts create mode 100644 src/halfmodal/lib/src/TiHalfmodalModule.ts create mode 100644 src/halfmodal/lib/src/TiHalfmodalService.ts create mode 100644 src/halfmodal/lib/src/halfmodal-body.less create mode 100644 src/halfmodal/lib/src/halfmodal-footer.less create mode 100644 src/halfmodal/lib/src/halfmodal-header.less create mode 100644 src/halfmodal/lib/src/halfmodal.html create mode 100644 src/halfmodal/lib/src/halfmodal.less create mode 100644 src/icon/demo/karma.conf.js create mode 100644 src/icon/demo/project.json create mode 100644 src/icon/demo/src/app/AppComponent.ts create mode 100644 src/icon/demo/src/app/AppModule.ts create mode 100644 src/icon/demo/src/app/IndexComponent.ts create mode 100644 src/icon/demo/src/app/app.html create mode 100644 src/icon/demo/src/app/icon/IconBasicComponent.ts create mode 100644 src/icon/demo/src/app/icon/IconShowComponent.ts create mode 100644 src/icon/demo/src/app/icon/IconTestModule.ts create mode 100644 src/icon/demo/src/app/icon/SvgSetpathComponent.ts create mode 100644 src/icon/demo/src/app/icon/icon-basic.html create mode 100644 src/icon/demo/src/app/icon/icon-show.html create mode 100644 src/icon/demo/src/app/icon/icon.less create mode 100644 src/icon/demo/src/app/icon/svg-setpath.html create mode 100644 src/icon/demo/src/app/icon/ui3-icons.PNG create mode 100644 src/icon/demo/src/app/icon/webdoc/icon-demos.js create mode 100644 src/icon/demo/src/app/icon/webdoc/icon.cn.md create mode 100644 src/icon/demo/src/app/icon/webdoc/icon.en.md create mode 100644 src/icon/demo/src/favicon.ico create mode 100644 src/icon/demo/src/index.html create mode 100644 src/icon/demo/src/main.ts create mode 100644 src/icon/demo/test.ts create mode 100644 src/icon/demo/tsconfig.app.json create mode 100644 src/icon/demo/tsconfig.spec.json create mode 100644 src/icon/lib/index.ts create mode 100644 src/icon/lib/ng-package.json create mode 100644 src/icon/lib/package.json create mode 100644 src/icon/lib/project.json create mode 100644 src/icon/lib/src/Ti-Icon.svg create mode 100644 src/icon/lib/src/Ti-Icon.woff create mode 100644 src/icon/lib/src/TiIconClassComponent.ts create mode 100644 src/icon/lib/src/TiIconComponent.ts create mode 100644 src/icon/lib/src/TiIconModule.ts create mode 100644 src/icon/lib/src/TiSvgComponent.ts create mode 100644 src/icon/lib/src/icon.less create mode 100644 src/icon/lib/src/svg.less create mode 100644 src/iconaction/demo/project.json create mode 100644 src/iconaction/demo/src/app/AppComponent.ts create mode 100644 src/iconaction/demo/src/app/AppModule.ts create mode 100644 src/iconaction/demo/src/app/IndexComponent.ts create mode 100644 src/iconaction/demo/src/app/app.html create mode 100644 src/iconaction/demo/src/app/iconaction/IconactionBasicComponent.ts create mode 100644 src/iconaction/demo/src/app/iconaction/IconactionDarkComponent.ts create mode 100644 src/iconaction/demo/src/app/iconaction/IconactionDisabledComponent.ts create mode 100644 src/iconaction/demo/src/app/iconaction/IconactionHrefComponent.ts create mode 100644 src/iconaction/demo/src/app/iconaction/IconactionTestModule.ts create mode 100644 src/iconaction/demo/src/app/iconaction/iconaction-basic.html create mode 100644 src/iconaction/demo/src/app/iconaction/iconaction-dark.html create mode 100644 src/iconaction/demo/src/app/iconaction/iconaction-disabled.html create mode 100644 src/iconaction/demo/src/app/iconaction/iconaction-href.html create mode 100644 src/iconaction/demo/src/app/iconaction/webdoc/iconaction-demos.js create mode 100644 src/iconaction/demo/src/app/iconaction/webdoc/iconaction.cn.md create mode 100644 src/iconaction/demo/src/app/iconaction/webdoc/iconaction.en.md create mode 100644 src/iconaction/demo/src/favicon.ico create mode 100644 src/iconaction/demo/src/index.html create mode 100644 src/iconaction/demo/src/main.ts create mode 100644 src/iconaction/demo/tsconfig.app.json create mode 100644 src/iconaction/lib/index.ts create mode 100644 src/iconaction/lib/ng-package.json create mode 100644 src/iconaction/lib/package.json create mode 100644 src/iconaction/lib/project.json create mode 100644 src/iconaction/lib/src/TiIconactionComponent.ts create mode 100644 src/iconaction/lib/src/TiIconactionModule.ts create mode 100644 src/iconaction/lib/src/iconaction.html create mode 100644 src/iconaction/lib/src/iconaction.less create mode 100644 src/imagepreview/demo/project.json create mode 100644 src/imagepreview/demo/src/app/AppComponent.ts create mode 100644 src/imagepreview/demo/src/app/AppModule.ts create mode 100644 src/imagepreview/demo/src/app/IndexComponent.ts create mode 100644 src/imagepreview/demo/src/app/app.html create mode 100644 src/imagepreview/demo/src/app/imagepreview/ImagepreviewBasicComponent.ts create mode 100644 src/imagepreview/demo/src/app/imagepreview/ImagepreviewTestModule.ts create mode 100644 src/imagepreview/demo/src/app/imagepreview/imagepreview-basic.html create mode 100644 src/imagepreview/demo/src/app/imagepreview/imagepreview.less create mode 100644 src/imagepreview/demo/src/app/imagepreview/webdoc/imagepreview-demos.js create mode 100644 src/imagepreview/demo/src/app/imagepreview/webdoc/imagepreview.cn.md create mode 100644 src/imagepreview/demo/src/app/imagepreview/webdoc/imagepreview.en.md create mode 100644 src/imagepreview/demo/src/favicon.ico create mode 100644 src/imagepreview/demo/src/index.html create mode 100644 src/imagepreview/demo/src/main.ts create mode 100644 src/imagepreview/demo/tsconfig.app.json create mode 100644 src/imagepreview/lib/index.ts create mode 100644 src/imagepreview/lib/ng-package.json create mode 100644 src/imagepreview/lib/package.json create mode 100644 src/imagepreview/lib/project.json create mode 100644 src/imagepreview/lib/src/TiImagepreviewComponent.ts create mode 100644 src/imagepreview/lib/src/TiImagepreviewModule.ts create mode 100644 src/imagepreview/lib/src/imagepreview.html create mode 100644 src/imagepreview/lib/src/imagepreview.less create mode 100644 src/include/lib/index.ts create mode 100644 src/include/lib/ng-package.json create mode 100644 src/include/lib/package.json create mode 100644 src/include/lib/project.json create mode 100644 src/include/lib/src/TiIncludeDirective.ts create mode 100644 src/include/lib/src/TiIncludeModule.ts create mode 100644 src/inputnumber/demo/karma.conf.js create mode 100644 src/inputnumber/demo/project.json create mode 100644 src/inputnumber/demo/src/app/AppComponent.ts create mode 100644 src/inputnumber/demo/src/app/AppModule.ts create mode 100644 src/inputnumber/demo/src/app/IndexComponent.ts create mode 100644 src/inputnumber/demo/src/app/app.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberBasicComponent.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberEventComponent.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberFocusComponent.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberFormatComponent.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberLoadComponent.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberLocaleableComponent.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberMaxlengthComponent.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberTestModule.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/inputnumber-basic.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/inputnumber-event.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/inputnumber-focus.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/inputnumber-format.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/inputnumber-load.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/inputnumber-localeable.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/inputnumber-maxlength.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber-demos.js create mode 100644 src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.cn.md create mode 100644 src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.en.md create mode 100644 src/inputnumber/demo/src/favicon.ico create mode 100644 src/inputnumber/demo/src/index.html create mode 100644 src/inputnumber/demo/src/main.ts create mode 100644 src/inputnumber/demo/test.ts create mode 100644 src/inputnumber/demo/tsconfig.app.json create mode 100644 src/inputnumber/demo/tsconfig.spec.json create mode 100644 src/inputnumber/lib/index.ts create mode 100644 src/inputnumber/lib/ng-package.json create mode 100644 src/inputnumber/lib/package.json create mode 100644 src/inputnumber/lib/project.json create mode 100644 src/inputnumber/lib/src/TiInputNumberDirective.ts create mode 100644 src/inputnumber/lib/src/TiInputNumberModule.ts create mode 100644 src/intro/demo/karma.conf.js create mode 100644 src/intro/demo/project.json create mode 100644 src/intro/demo/src/app/AppComponent.ts create mode 100644 src/intro/demo/src/app/AppModule.ts create mode 100644 src/intro/demo/src/app/IndexComponent.ts create mode 100644 src/intro/demo/src/app/app.html create mode 100644 src/intro/demo/src/app/intro/IntroBasicComponent.ts create mode 100644 src/intro/demo/src/app/intro/IntroEventComponent.ts create mode 100644 src/intro/demo/src/app/intro/IntroModalComponent.ts create mode 100644 src/intro/demo/src/app/intro/IntroSkipableComponent.ts create mode 100644 src/intro/demo/src/app/intro/IntroTemplateComponent.ts create mode 100644 src/intro/demo/src/app/intro/IntroTestModule.ts create mode 100644 src/intro/demo/src/app/intro/IntroTipComponent.ts create mode 100644 src/intro/demo/src/app/intro/IntroTiscrollComponent.ts create mode 100644 src/intro/demo/src/app/intro/intro-basic.html create mode 100644 src/intro/demo/src/app/intro/intro-event.html create mode 100644 src/intro/demo/src/app/intro/intro-modal.html create mode 100644 src/intro/demo/src/app/intro/intro-skipable.html create mode 100644 src/intro/demo/src/app/intro/intro-template.html create mode 100644 src/intro/demo/src/app/intro/intro-tip.html create mode 100644 src/intro/demo/src/app/intro/intro-tiscroll.html create mode 100644 src/intro/demo/src/app/intro/webdoc/intro-demos.js create mode 100644 src/intro/demo/src/app/intro/webdoc/intro.cn.md create mode 100644 src/intro/demo/src/app/intro/webdoc/intro.en.md create mode 100644 src/intro/demo/src/favicon.ico create mode 100644 src/intro/demo/src/index.html create mode 100644 src/intro/demo/src/main.ts create mode 100644 src/intro/demo/test.ts create mode 100644 src/intro/demo/tsconfig.app.json create mode 100644 src/intro/demo/tsconfig.spec.json create mode 100644 src/intro/lib/index.ts create mode 100644 src/intro/lib/ng-package.json create mode 100644 src/intro/lib/package.json create mode 100644 src/intro/lib/project.json create mode 100644 src/intro/lib/src/TiIntroInterface.ts create mode 100644 src/intro/lib/src/TiIntroModule.ts create mode 100644 src/intro/lib/src/TiIntroService.ts create mode 100644 src/intro/lib/src/TiIntromodalComponent.ts create mode 100644 src/intro/lib/src/TiIntrotipComponent.ts create mode 100644 src/intro/lib/src/i18n/TiIntroWords.ts create mode 100644 src/intro/lib/src/i18n/en_US.ts create mode 100644 src/intro/lib/src/i18n/es_US.ts create mode 100644 src/intro/lib/src/i18n/fr_FR.ts create mode 100644 src/intro/lib/src/i18n/index.ts create mode 100644 src/intro/lib/src/i18n/pt_BR.ts create mode 100644 src/intro/lib/src/i18n/zh_CN.ts create mode 100644 src/intro/lib/src/intro.less create mode 100644 src/intro/lib/src/intromodal.html create mode 100644 src/intro/lib/src/introtip.html create mode 100644 src/ip/demo/karma.conf.js create mode 100644 src/ip/demo/project.json create mode 100644 src/ip/demo/src/app/AppComponent.ts create mode 100644 src/ip/demo/src/app/AppModule.ts create mode 100644 src/ip/demo/src/app/IndexComponent.ts create mode 100644 src/ip/demo/src/app/app.html create mode 100644 src/ip/demo/src/app/ip/IpBasicComponent.ts create mode 100644 src/ip/demo/src/app/ip/IpDisabledComponent.ts create mode 100644 src/ip/demo/src/app/ip/IpFormcontrolComponent.ts create mode 100644 src/ip/demo/src/app/ip/IpTestModule.ts create mode 100644 src/ip/demo/src/app/ip/IpValidComponent.ts create mode 100644 src/ip/demo/src/app/ip/ip-basic.html create mode 100644 src/ip/demo/src/app/ip/ip-disabled.html create mode 100644 src/ip/demo/src/app/ip/ip-formcontrol.html create mode 100644 src/ip/demo/src/app/ip/ip-valid.html create mode 100644 src/ip/demo/src/app/ip/webdoc/ip-demos.js create mode 100644 src/ip/demo/src/app/ip/webdoc/ip.cn.md create mode 100644 src/ip/demo/src/app/ip/webdoc/ip.en.md create mode 100644 src/ip/demo/src/favicon.ico create mode 100644 src/ip/demo/src/index.html create mode 100644 src/ip/demo/src/main.ts create mode 100644 src/ip/demo/test.ts create mode 100644 src/ip/demo/tsconfig.app.json create mode 100644 src/ip/demo/tsconfig.spec.json create mode 100644 src/ip/lib/index.ts create mode 100644 src/ip/lib/ng-package.json create mode 100644 src/ip/lib/package.json create mode 100644 src/ip/lib/project.json create mode 100644 src/ip/lib/src/TiIpComponent.ts create mode 100644 src/ip/lib/src/TiIpModule.ts create mode 100644 src/ip/lib/src/ip.html create mode 100644 src/ip/lib/src/ip.less create mode 100644 src/ipsection/demo/karma.conf.js create mode 100644 src/ipsection/demo/project.json create mode 100644 src/ipsection/demo/src/app/AppComponent.ts create mode 100644 src/ipsection/demo/src/app/AppModule.ts create mode 100644 src/ipsection/demo/src/app/IndexComponent.ts create mode 100644 src/ipsection/demo/src/app/app.html create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionBasicComponent.ts create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionDisabledComponent.ts create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionEventsComponent.ts create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionFocusComponent.ts create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionTestComponent.ts create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionTestModule.ts create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionValidComponent.ts create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionValidFormgroupComponent.ts create mode 100644 src/ipsection/demo/src/app/ipsection/ipsection-basic.html create mode 100644 src/ipsection/demo/src/app/ipsection/ipsection-disabled.html create mode 100644 src/ipsection/demo/src/app/ipsection/ipsection-events.html create mode 100644 src/ipsection/demo/src/app/ipsection/ipsection-focus.html create mode 100644 src/ipsection/demo/src/app/ipsection/ipsection-test.html create mode 100644 src/ipsection/demo/src/app/ipsection/ipsection-valid-formgroup.html create mode 100644 src/ipsection/demo/src/app/ipsection/ipsection-valid.html create mode 100644 src/ipsection/demo/src/app/ipsection/webdoc/ipsection-demos.js create mode 100644 src/ipsection/demo/src/app/ipsection/webdoc/ipsection.cn.md create mode 100644 src/ipsection/demo/src/app/ipsection/webdoc/ipsection.en.md create mode 100644 src/ipsection/demo/src/favicon.ico create mode 100644 src/ipsection/demo/src/index.html create mode 100644 src/ipsection/demo/src/main.ts create mode 100644 src/ipsection/demo/test.ts create mode 100644 src/ipsection/demo/tsconfig.app.json create mode 100644 src/ipsection/demo/tsconfig.spec.json create mode 100644 src/ipsection/lib/index.ts create mode 100644 src/ipsection/lib/ng-package.json create mode 100644 src/ipsection/lib/package.json create mode 100644 src/ipsection/lib/project.json create mode 100644 src/ipsection/lib/src/TiIpsectionComponent.ts create mode 100644 src/ipsection/lib/src/TiIpsectionModule.ts create mode 100644 src/ipsection/lib/src/ipsection.html create mode 100644 src/ipsection/lib/src/ipsection.less create mode 100644 src/labeleditor/demo/project.json create mode 100644 src/labeleditor/demo/src/app/AppComponent.ts create mode 100644 src/labeleditor/demo/src/app/AppModule.ts create mode 100644 src/labeleditor/demo/src/app/IndexComponent.ts create mode 100644 src/labeleditor/demo/src/app/app.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorAutotipComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorBasicComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorDisabledComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorEventsComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorIcontipcontextComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlengthComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlineComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorMultilineSizeComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorResizeComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorTestModule.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationAsyncComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-autotip.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-basic.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-disabled.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-events.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-icontipcontext.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-maxlength.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-maxline.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-multiline-size.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-resize.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-validation-async.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-validation.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor-demos.js create mode 100644 src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.cn.md create mode 100644 src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.en.md create mode 100644 src/labeleditor/demo/src/favicon.ico create mode 100644 src/labeleditor/demo/src/index.html create mode 100644 src/labeleditor/demo/src/main.ts create mode 100644 src/labeleditor/demo/tsconfig.app.json create mode 100644 src/labeleditor/lib/index.ts create mode 100644 src/labeleditor/lib/ng-package.json create mode 100644 src/labeleditor/lib/package.json create mode 100644 src/labeleditor/lib/project.json create mode 100644 src/labeleditor/lib/src/TiLabeleditorComponent.ts create mode 100644 src/labeleditor/lib/src/TiLabeleditorModule.ts create mode 100644 src/labeleditor/lib/src/labeleditor.html create mode 100644 src/labeleditor/lib/src/labeleditor.less create mode 100644 src/layout/demo/karma.conf.js create mode 100644 src/layout/demo/project.json create mode 100644 src/layout/demo/src/app/AppComponent.ts create mode 100644 src/layout/demo/src/app/AppModule.ts create mode 100644 src/layout/demo/src/app/IndexComponent.ts create mode 100644 src/layout/demo/src/app/app.html create mode 100644 src/layout/demo/src/app/layout/LayoutBasicComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutBasicSimpleComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutBasicSimpleResponsiveComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutDetailColumnComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutDetailComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutListComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutListLargedataComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutMultiColumnComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutOverviewComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutOverviewVerticalComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutPurchaseComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutPurchaseResponsiveChangeComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutPurchaseResponsiveComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutPurchaseSimpleComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutPurchaseSimpleResponsiveComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutSingleComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutTestModule.ts create mode 100644 src/layout/demo/src/app/layout/layout-basic-simple-responsive.html create mode 100644 src/layout/demo/src/app/layout/layout-basic-simple.html create mode 100644 src/layout/demo/src/app/layout/layout-basic.html create mode 100644 src/layout/demo/src/app/layout/layout-basic.less create mode 100644 src/layout/demo/src/app/layout/layout-column.less create mode 100644 src/layout/demo/src/app/layout/layout-detail-column.html create mode 100644 src/layout/demo/src/app/layout/layout-detail.html create mode 100644 src/layout/demo/src/app/layout/layout-list-largedata.html create mode 100644 src/layout/demo/src/app/layout/layout-list.html create mode 100644 src/layout/demo/src/app/layout/layout-multi-column.html create mode 100644 src/layout/demo/src/app/layout/layout-overview-vertical.html create mode 100644 src/layout/demo/src/app/layout/layout-overview.html create mode 100644 src/layout/demo/src/app/layout/layout-purchase-responsive-change.html create mode 100644 src/layout/demo/src/app/layout/layout-purchase-responsive.html create mode 100644 src/layout/demo/src/app/layout/layout-purchase-simple-responsive.html create mode 100644 src/layout/demo/src/app/layout/layout-purchase-simple.html create mode 100644 src/layout/demo/src/app/layout/layout-purchase.html create mode 100644 src/layout/demo/src/app/layout/layout-simple.less create mode 100644 src/layout/demo/src/app/layout/layout-single.html create mode 100644 src/layout/demo/src/app/layout/layout.less create mode 100644 src/layout/demo/src/app/layout/webdoc/layout-demos.js create mode 100644 src/layout/demo/src/app/layout/webdoc/layout.cn.md create mode 100644 src/layout/demo/src/app/layout/webdoc/layout.en.md create mode 100644 src/layout/demo/src/favicon.ico create mode 100644 src/layout/demo/src/index.html create mode 100644 src/layout/demo/src/main.ts create mode 100644 src/layout/demo/test.ts create mode 100644 src/layout/demo/tsconfig.app.json create mode 100644 src/layout/demo/tsconfig.spec.json create mode 100644 src/layout/lib/index.ts create mode 100644 src/layout/lib/ng-package.json create mode 100644 src/layout/lib/package.json create mode 100644 src/layout/lib/project.json create mode 100644 src/layout/lib/src/TiLayoutColumnComponent.ts create mode 100644 src/layout/lib/src/TiLayoutContentBodyComponent.ts create mode 100644 src/layout/lib/src/TiLayoutContentComponent.ts create mode 100644 src/layout/lib/src/TiLayoutContentHeaderComponent.ts create mode 100644 src/layout/lib/src/TiLayoutHeaderComponent.ts create mode 100644 src/layout/lib/src/TiLayoutModule.ts create mode 100644 src/layout/lib/src/TiLayoutSectionComponent.ts create mode 100644 src/layout/lib/src/layout-column.less create mode 100644 src/layout/lib/src/layout-content-body.less create mode 100644 src/layout/lib/src/layout-content-header.less create mode 100644 src/layout/lib/src/layout-content.less create mode 100644 src/layout/lib/src/layout-header.less create mode 100644 src/layout/lib/src/layout-section.less create mode 100644 src/layout/lib/src/layout.less create mode 100644 src/leftmenu/demo/karma.conf.js create mode 100644 src/leftmenu/demo/project.json create mode 100644 src/leftmenu/demo/src/app/AppComponent.ts create mode 100644 src/leftmenu/demo/src/app/AppModule.ts create mode 100644 src/leftmenu/demo/src/app/IndexComponent.ts create mode 100644 src/leftmenu/demo/src/app/app.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuActiveChangeComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuBasicComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuCandeactivateComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuCollapsedComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuDisabledComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuDividingComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuFootComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuGroupComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuHrefComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuIdComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuNoRouterComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuParamsComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuReloadStateComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuRouterlistComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuScrollComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuSecurityComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuTestModule.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuToggleableComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/OpenerComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/Router11Component.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/Router12Component.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/Router2Component.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/Router31Component.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/Router32Component.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/RouterAComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/RouterBComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/RouterCComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/RouterDComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/RouterparamsComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-active-change.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-basic.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-candeactivate.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-collapsed.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-disabled.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-dividing.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-foot.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-group.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-href.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-id.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-no-router.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-params.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-reload-state.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-routerlist.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-scroll.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-security.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-toggleable.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenuTest.less create mode 100644 src/leftmenu/demo/src/app/leftmenu/unsave.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu-demos.js create mode 100644 src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.cn.md create mode 100644 src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.en.md create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuActiveChangeWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuBasicWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuCollapsedWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDisabledWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDividingWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuFootWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuGroupWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuHrefWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuNoRouterWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuParamsWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuReloadStateWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuRouterlistWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuToggleableWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-active-change-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-basic-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-collapsed-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-disabled-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-dividing-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-foot-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-group-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-href-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-no-router-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-params-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-reload-state-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-routerlist-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-toggleable-website-view.html create mode 100644 src/leftmenu/demo/src/favicon.ico create mode 100644 src/leftmenu/demo/src/index.html create mode 100644 src/leftmenu/demo/src/main.ts create mode 100644 src/leftmenu/demo/test.ts create mode 100644 src/leftmenu/demo/tsconfig.app.json create mode 100644 src/leftmenu/demo/tsconfig.spec.json create mode 100644 src/leftmenu/lib/index.ts create mode 100644 src/leftmenu/lib/ng-package.json create mode 100644 src/leftmenu/lib/package.json create mode 100644 src/leftmenu/lib/project.json create mode 100644 src/leftmenu/lib/src/TiLeftmenuComponent.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuFootComponent.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuGroupComponent.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuGroupItemComponent.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuHeadComponent.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuItemComponent.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuLevel1Component.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuLevel2Component.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuModule.ts create mode 100644 src/leftmenu/lib/src/leftmenu-foot.html create mode 100644 src/leftmenu/lib/src/leftmenu-group-item.html create mode 100644 src/leftmenu/lib/src/leftmenu-group.html create mode 100644 src/leftmenu/lib/src/leftmenu-head.html create mode 100644 src/leftmenu/lib/src/leftmenu-item.html create mode 100644 src/leftmenu/lib/src/leftmenu-level1.html create mode 100644 src/leftmenu/lib/src/leftmenu-level2.html create mode 100644 src/leftmenu/lib/src/leftmenu.html create mode 100644 src/leftmenu/lib/src/leftmenu.less create mode 100644 src/linkbutton/demo/karma.conf.js create mode 100644 src/linkbutton/demo/project.json create mode 100644 src/linkbutton/demo/src/app/AppComponent.ts create mode 100644 src/linkbutton/demo/src/app/AppModule.ts create mode 100644 src/linkbutton/demo/src/app/IndexComponent.ts create mode 100644 src/linkbutton/demo/src/app/app.html create mode 100644 src/linkbutton/demo/src/app/linkbutton/LinkbuttonBasicComponent.ts create mode 100644 src/linkbutton/demo/src/app/linkbutton/LinkbuttonTestModule.ts create mode 100644 src/linkbutton/demo/src/app/linkbutton/linkbutton-basic.html create mode 100644 src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton-demos.js create mode 100644 src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.cn.md create mode 100644 src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.en.md create mode 100644 src/linkbutton/demo/src/favicon.ico create mode 100644 src/linkbutton/demo/src/index.html create mode 100644 src/linkbutton/demo/src/main.ts create mode 100644 src/linkbutton/demo/test.ts create mode 100644 src/linkbutton/demo/tsconfig.app.json create mode 100644 src/linkbutton/demo/tsconfig.spec.json create mode 100644 src/linkbutton/lib/index.ts create mode 100644 src/linkbutton/lib/ng-package.json create mode 100644 src/linkbutton/lib/package.json create mode 100644 src/linkbutton/lib/project.json create mode 100644 src/linkbutton/lib/src/TiLinkbuttonComponent.ts create mode 100644 src/linkbutton/lib/src/TiLinkbuttonModule.ts create mode 100644 src/linkbutton/lib/src/linkbutton.html create mode 100644 src/linkbutton/lib/src/linkbutton.less create mode 100644 src/list/demo/project.json create mode 100644 src/list/demo/src/app/AppComponent.ts create mode 100644 src/list/demo/src/app/AppModule.ts create mode 100644 src/list/demo/src/app/IndexComponent.ts create mode 100644 src/list/demo/src/app/app.html create mode 100644 src/list/demo/src/app/list/ListDefaultComponent.ts create mode 100644 src/list/demo/src/app/list/ListGroupComponent.ts create mode 100644 src/list/demo/src/app/list/ListMultiComponent.ts create mode 100644 src/list/demo/src/app/list/ListSelectallComponent.ts create mode 100644 src/list/demo/src/app/list/ListTestModule.ts create mode 100644 src/list/demo/src/app/list/ListTipComponent.ts create mode 100644 src/list/demo/src/app/list/list-default.html create mode 100644 src/list/demo/src/app/list/list-group.html create mode 100644 src/list/demo/src/app/list/list-multi.html create mode 100644 src/list/demo/src/app/list/list-selectall.html create mode 100644 src/list/demo/src/app/list/list-tip.html create mode 100644 src/list/demo/src/favicon.ico create mode 100644 src/list/demo/src/index.html create mode 100644 src/list/demo/src/main.ts create mode 100644 src/list/demo/tsconfig.app.json create mode 100644 src/list/lib/index.ts create mode 100644 src/list/lib/ng-package.json create mode 100644 src/list/lib/package.json create mode 100644 src/list/lib/project.json create mode 100644 src/list/lib/src/TiListComponent.ts create mode 100644 src/list/lib/src/TiListModule.ts create mode 100644 src/list/lib/src/i18n/TiListWords.ts create mode 100644 src/list/lib/src/i18n/en_US.ts create mode 100644 src/list/lib/src/i18n/es_US.ts create mode 100644 src/list/lib/src/i18n/fr_FR.ts create mode 100644 src/list/lib/src/i18n/index.ts create mode 100644 src/list/lib/src/i18n/pt_BR.ts create mode 100644 src/list/lib/src/i18n/zh_CN.ts create mode 100644 src/list/lib/src/list-multi.less create mode 100644 src/list/lib/src/list.html create mode 100644 src/list/lib/src/list.less create mode 100644 src/loading/demo/karma.conf.js create mode 100644 src/loading/demo/project.json create mode 100644 src/loading/demo/src/app/AppComponent.ts create mode 100644 src/loading/demo/src/app/AppModule.ts create mode 100644 src/loading/demo/src/app/IndexComponent.ts create mode 100644 src/loading/demo/src/app/app.html create mode 100644 src/loading/demo/src/app/loading/LoadingAreaComponent.ts create mode 100644 src/loading/demo/src/app/loading/LoadingBasicComponent.ts create mode 100644 src/loading/demo/src/app/loading/LoadingSizeComponent.ts create mode 100644 src/loading/demo/src/app/loading/LoadingTestModule.ts create mode 100644 src/loading/demo/src/app/loading/LoadingTypeComponent.ts create mode 100644 src/loading/demo/src/app/loading/loading-area.html create mode 100644 src/loading/demo/src/app/loading/loading-basic.html create mode 100644 src/loading/demo/src/app/loading/loading-size.html create mode 100644 src/loading/demo/src/app/loading/loading-type.html create mode 100644 src/loading/demo/src/app/loading/webdoc/loading-demos.js create mode 100644 src/loading/demo/src/app/loading/webdoc/loading.cn.md create mode 100644 src/loading/demo/src/app/loading/webdoc/loading.en.md create mode 100644 src/loading/demo/src/favicon.ico create mode 100644 src/loading/demo/src/index.html create mode 100644 src/loading/demo/src/main.ts create mode 100644 src/loading/demo/test.ts create mode 100644 src/loading/demo/tsconfig.app.json create mode 100644 src/loading/demo/tsconfig.spec.json create mode 100644 src/loading/lib/index.ts create mode 100644 src/loading/lib/ng-package.json create mode 100644 src/loading/lib/package.json create mode 100644 src/loading/lib/project.json create mode 100644 src/loading/lib/src/TiLoadingComponent.ts create mode 100644 src/loading/lib/src/TiLoadingModule.ts create mode 100644 src/loading/lib/src/TiLoadingfailComponent.ts create mode 100644 src/loading/lib/src/i18n/TiLoadingWords.ts create mode 100644 src/loading/lib/src/i18n/en_US.ts create mode 100644 src/loading/lib/src/i18n/es_US.ts create mode 100644 src/loading/lib/src/i18n/fr_FR.ts create mode 100644 src/loading/lib/src/i18n/index.ts create mode 100644 src/loading/lib/src/i18n/pt_BR.ts create mode 100644 src/loading/lib/src/i18n/zh_CN.ts create mode 100644 src/loading/lib/src/loading.html create mode 100644 src/loading/lib/src/loading.less create mode 100644 src/loading/lib/src/loadingfail.html create mode 100644 src/loading/lib/src/loadingfail.less create mode 100644 src/locale/demo/karma.conf.js create mode 100644 src/locale/demo/project.json create mode 100644 src/locale/demo/src/app/AppComponent.ts create mode 100644 src/locale/demo/src/app/AppModule.ts create mode 100644 src/locale/demo/src/app/IndexComponent.ts create mode 100644 src/locale/demo/src/app/app.html create mode 100644 src/locale/demo/src/app/locale/LocaleBasicComponent.ts create mode 100644 src/locale/demo/src/app/locale/LocaleFormatComponent.ts create mode 100644 src/locale/demo/src/app/locale/LocaleReloadComponent.ts create mode 100644 src/locale/demo/src/app/locale/LocaleTestModule.ts create mode 100644 src/locale/demo/src/app/locale/locale-basic.html create mode 100644 src/locale/demo/src/app/locale/locale-format.html create mode 100644 src/locale/demo/src/app/locale/locale-reload.html create mode 100644 src/locale/demo/src/app/locale/webdoc/locale-demos.js create mode 100644 src/locale/demo/src/app/locale/webdoc/locale.cn.md create mode 100644 src/locale/demo/src/app/locale/webdoc/locale.en.md create mode 100644 src/locale/demo/src/favicon.ico create mode 100644 src/locale/demo/src/index.html create mode 100644 src/locale/demo/src/main.ts create mode 100644 src/locale/demo/test.ts create mode 100644 src/locale/demo/tsconfig.app.json create mode 100644 src/locale/demo/tsconfig.spec.json create mode 100644 src/locale/lib/index.ts create mode 100644 src/locale/lib/ng-package.json create mode 100644 src/locale/lib/package.json create mode 100644 src/locale/lib/project.json create mode 100644 src/locale/lib/src/TiLocale.ts create mode 100644 src/locale/lib/src/TiLocaleFormat.ts create mode 100644 src/locale/lib/src/TiLocaleModule.ts create mode 100644 src/locale/lib/src/TiTranslatePipe.ts create mode 100644 src/menu/demo/karma.conf.js create mode 100644 src/menu/demo/project.json create mode 100644 src/menu/demo/src/app/AppComponent.ts create mode 100644 src/menu/demo/src/app/AppModule.ts create mode 100644 src/menu/demo/src/app/IndexComponent.ts create mode 100644 src/menu/demo/src/app/app.html create mode 100644 src/menu/demo/src/app/menu/MenuBasicComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuBeforeopenComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuBorderComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuButtoncolorComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuDefaultComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuDisabledComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuEventComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuGroupComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuIdComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuLabelkeyComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuPanelalignComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuPanelstyleComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuTempleteComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuTempleteTestComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuTestModule.ts create mode 100644 src/menu/demo/src/app/menu/MenuTipsComponent.ts create mode 100644 src/menu/demo/src/app/menu/menu-basic.html create mode 100644 src/menu/demo/src/app/menu/menu-beforeopen.html create mode 100644 src/menu/demo/src/app/menu/menu-border.html create mode 100644 src/menu/demo/src/app/menu/menu-buttoncolor.html create mode 100644 src/menu/demo/src/app/menu/menu-default.html create mode 100644 src/menu/demo/src/app/menu/menu-disabled.html create mode 100644 src/menu/demo/src/app/menu/menu-event.html create mode 100644 src/menu/demo/src/app/menu/menu-group.html create mode 100644 src/menu/demo/src/app/menu/menu-id.html create mode 100644 src/menu/demo/src/app/menu/menu-labelkey.html create mode 100644 src/menu/demo/src/app/menu/menu-panelalign.html create mode 100644 src/menu/demo/src/app/menu/menu-panelstyle.html create mode 100644 src/menu/demo/src/app/menu/menu-templete-test.html create mode 100644 src/menu/demo/src/app/menu/menu-templete.html create mode 100644 src/menu/demo/src/app/menu/menu-tips.html create mode 100644 src/menu/demo/src/app/menu/webdoc/menu-demos.js create mode 100644 src/menu/demo/src/app/menu/webdoc/menu.cn.md create mode 100644 src/menu/demo/src/app/menu/webdoc/menu.en.md create mode 100644 src/menu/demo/src/favicon.ico create mode 100644 src/menu/demo/src/index.html create mode 100644 src/menu/demo/src/main.ts create mode 100644 src/menu/demo/test.ts create mode 100644 src/menu/demo/tsconfig.app.json create mode 100644 src/menu/demo/tsconfig.spec.json create mode 100644 src/menu/lib/index.ts create mode 100644 src/menu/lib/ng-package.json create mode 100644 src/menu/lib/package.json create mode 100644 src/menu/lib/project.json create mode 100644 src/menu/lib/src/TiMenuComponent.ts create mode 100644 src/menu/lib/src/TiMenuItem.ts create mode 100644 src/menu/lib/src/TiMenuListComponent.ts create mode 100644 src/menu/lib/src/TiMenuModule.ts create mode 100644 src/menu/lib/src/menu.html create mode 100644 src/menu/lib/src/menu.less create mode 100644 src/menu/lib/src/menulist.html create mode 100644 src/menu/lib/src/menulist.less create mode 100644 src/message/demo/karma.conf.js create mode 100644 src/message/demo/project.json create mode 100644 src/message/demo/src/app/AppComponent.ts create mode 100644 src/message/demo/src/app/AppModule.ts create mode 100644 src/message/demo/src/app/IndexComponent.ts create mode 100644 src/message/demo/src/app/app.html create mode 100644 src/message/demo/src/app/message/MessageBasicComponent.ts create mode 100644 src/message/demo/src/app/message/MessageBtnComponent.ts create mode 100644 src/message/demo/src/app/message/MessageBtnTestComponent.ts create mode 100644 src/message/demo/src/app/message/MessageContentComponent.ts create mode 100644 src/message/demo/src/app/message/MessageIdComponent.ts create mode 100644 src/message/demo/src/app/message/MessageSecurityComponent.ts create mode 100644 src/message/demo/src/app/message/MessageTestModule.ts create mode 100644 src/message/demo/src/app/message/MessageTitleComponent.ts create mode 100644 src/message/demo/src/app/message/MessageTypeComponent.ts create mode 100644 src/message/demo/src/app/message/message-basic.html create mode 100644 src/message/demo/src/app/message/message-btn-test.html create mode 100644 src/message/demo/src/app/message/message-btn.html create mode 100644 src/message/demo/src/app/message/message-content.html create mode 100644 src/message/demo/src/app/message/message-id.html create mode 100644 src/message/demo/src/app/message/message-security.html create mode 100644 src/message/demo/src/app/message/message-title.html create mode 100644 src/message/demo/src/app/message/message-type.html create mode 100644 src/message/demo/src/app/message/webdoc/message-demos.js create mode 100644 src/message/demo/src/app/message/webdoc/message.cn.md create mode 100644 src/message/demo/src/app/message/webdoc/message.en.md create mode 100644 src/message/demo/src/favicon.ico create mode 100644 src/message/demo/src/index.html create mode 100644 src/message/demo/src/main.ts create mode 100644 src/message/demo/test.ts create mode 100644 src/message/demo/tsconfig.app.json create mode 100644 src/message/demo/tsconfig.spec.json create mode 100644 src/message/lib/index.ts create mode 100644 src/message/lib/ng-package.json create mode 100644 src/message/lib/package.json create mode 100644 src/message/lib/project.json create mode 100644 src/message/lib/src/TiContentWrapperComponent.ts create mode 100644 src/message/lib/src/TiMessageComponent.html create mode 100644 src/message/lib/src/TiMessageComponent.ts create mode 100644 src/message/lib/src/TiMessageInterface.ts create mode 100644 src/message/lib/src/TiMessageModule.ts create mode 100644 src/message/lib/src/TiMessageService.ts create mode 100644 src/message/lib/src/TiTranscludeDirective.ts create mode 100644 src/message/lib/src/i18n/TiMessageWords.ts create mode 100644 src/message/lib/src/i18n/en_US.ts create mode 100644 src/message/lib/src/i18n/es_US.ts create mode 100644 src/message/lib/src/i18n/fr_FR.ts create mode 100644 src/message/lib/src/i18n/index.ts create mode 100644 src/message/lib/src/i18n/pt_BR.ts create mode 100644 src/message/lib/src/i18n/zh_CN.ts create mode 100644 src/message/lib/src/message.less create mode 100644 src/modal/demo/karma.conf.js create mode 100644 src/modal/demo/project.json create mode 100644 src/modal/demo/src/app/AppComponent.ts create mode 100644 src/modal/demo/src/app/AppModule.ts create mode 100644 src/modal/demo/src/app/IndexComponent.ts create mode 100644 src/modal/demo/src/app/app.html create mode 100644 src/modal/demo/src/app/modal/ModalAnimationComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalBackdropComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalClassComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalCloseIconComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalConfigTestComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalContentCompComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalContentTempComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalDraggableComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalEscComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalEventComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalHeaderAlignComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalHeaderStyleComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalTestModule.ts create mode 100644 src/modal/demo/src/app/modal/ModalTwoBackdropComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalTwoTestComponent.ts create mode 100644 src/modal/demo/src/app/modal/TestComponent.ts create mode 100644 src/modal/demo/src/app/modal/modal-animation.html create mode 100644 src/modal/demo/src/app/modal/modal-backdrop.html create mode 100644 src/modal/demo/src/app/modal/modal-class.html create mode 100644 src/modal/demo/src/app/modal/modal-close-icon.html create mode 100644 src/modal/demo/src/app/modal/modal-config-test.html create mode 100644 src/modal/demo/src/app/modal/modal-content-comp.html create mode 100644 src/modal/demo/src/app/modal/modal-content-temp.html create mode 100644 src/modal/demo/src/app/modal/modal-draggable.html create mode 100644 src/modal/demo/src/app/modal/modal-esc.html create mode 100644 src/modal/demo/src/app/modal/modal-event.html create mode 100644 src/modal/demo/src/app/modal/modal-header-align.html create mode 100644 src/modal/demo/src/app/modal/modal-header-style.html create mode 100644 src/modal/demo/src/app/modal/modal-two-backdrop.html create mode 100644 src/modal/demo/src/app/modal/modal-two-test.html create mode 100644 src/modal/demo/src/app/modal/webdoc/modal-demos.js create mode 100644 src/modal/demo/src/app/modal/webdoc/modal.cn.md create mode 100644 src/modal/demo/src/app/modal/webdoc/modal.en.md create mode 100644 src/modal/demo/src/favicon.ico create mode 100644 src/modal/demo/src/index.html create mode 100644 src/modal/demo/src/main.ts create mode 100644 src/modal/demo/test.ts create mode 100644 src/modal/demo/tsconfig.app.json create mode 100644 src/modal/demo/tsconfig.spec.json create mode 100644 src/modal/lib/index.ts create mode 100644 src/modal/lib/ng-package.json create mode 100644 src/modal/lib/package.json create mode 100644 src/modal/lib/project.json create mode 100644 src/modal/lib/src/TiBackdropComponent.ts create mode 100644 src/modal/lib/src/TiBackdropNoAnimationComponent.ts create mode 100644 src/modal/lib/src/TiModalBodyComponent.ts create mode 100644 src/modal/lib/src/TiModalComponent.html create mode 100644 src/modal/lib/src/TiModalComponent.ts create mode 100644 src/modal/lib/src/TiModalComponentNoAnimation.html create mode 100644 src/modal/lib/src/TiModalFooterComponent.ts create mode 100644 src/modal/lib/src/TiModalHeaderComponent.ts create mode 100644 src/modal/lib/src/TiModalInterface.ts create mode 100644 src/modal/lib/src/TiModalModule.ts create mode 100644 src/modal/lib/src/TiModalNoAnimationComponent.ts create mode 100644 src/modal/lib/src/TiModalService.ts create mode 100644 src/modal/lib/src/backdrop.less create mode 100644 src/modal/lib/src/modal.less create mode 100644 src/nav/demo/karma.conf.js create mode 100644 src/nav/demo/project.json create mode 100644 src/nav/demo/src/app/AppComponent.ts create mode 100644 src/nav/demo/src/app/AppModule.ts create mode 100644 src/nav/demo/src/app/IndexComponent.ts create mode 100644 src/nav/demo/src/app/app.html create mode 100644 src/nav/demo/src/app/nav/NavActiveComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavAlignComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavBasicComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavDisabledComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavEventComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavLeftComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavRightComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavSelectableComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavSubmenuComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavTemplateComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavTestModule.ts create mode 100644 src/nav/demo/src/app/nav/NavThemeComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavWidthComponent.ts create mode 100644 src/nav/demo/src/app/nav/nav-active.html create mode 100644 src/nav/demo/src/app/nav/nav-align.html create mode 100644 src/nav/demo/src/app/nav/nav-basic.html create mode 100644 src/nav/demo/src/app/nav/nav-disabled.html create mode 100644 src/nav/demo/src/app/nav/nav-event.html create mode 100644 src/nav/demo/src/app/nav/nav-left.html create mode 100644 src/nav/demo/src/app/nav/nav-right.html create mode 100644 src/nav/demo/src/app/nav/nav-selectable.html create mode 100644 src/nav/demo/src/app/nav/nav-submenu.html create mode 100644 src/nav/demo/src/app/nav/nav-template.html create mode 100644 src/nav/demo/src/app/nav/nav-theme.html create mode 100644 src/nav/demo/src/app/nav/nav-width.html create mode 100644 src/nav/demo/src/app/nav/webdoc/nav-demos.js create mode 100644 src/nav/demo/src/app/nav/webdoc/nav.cn.md create mode 100644 src/nav/demo/src/app/nav/webdoc/nav.en.md create mode 100644 src/nav/demo/src/favicon.ico create mode 100644 src/nav/demo/src/index.html create mode 100644 src/nav/demo/src/main.ts create mode 100644 src/nav/demo/test.ts create mode 100644 src/nav/demo/tsconfig.app.json create mode 100644 src/nav/demo/tsconfig.spec.json create mode 100644 src/nav/lib/index.ts create mode 100644 src/nav/lib/ng-package.json create mode 100644 src/nav/lib/package.json create mode 100644 src/nav/lib/project.json create mode 100644 src/nav/lib/src/TiNavComponent.ts create mode 100644 src/nav/lib/src/TiNavLeftComponent.ts create mode 100644 src/nav/lib/src/TiNavMenuComponent.ts create mode 100644 src/nav/lib/src/TiNavModule.ts create mode 100644 src/nav/lib/src/TiNavRightComponent.ts create mode 100644 src/nav/lib/src/common.less create mode 100644 src/nav/lib/src/interface.ts create mode 100644 src/nav/lib/src/nav.html create mode 100644 src/nav/lib/src/nav.less create mode 100644 src/nav/lib/src/navleft.html create mode 100644 src/nav/lib/src/navleft.less create mode 100644 src/nav/lib/src/navmenu.html create mode 100644 src/nav/lib/src/navmenu.less create mode 100644 src/nav/lib/src/navright.html create mode 100644 src/nav/lib/src/navright.less create mode 100644 src/ng/demo/karma.conf.js create mode 100644 src/ng/demo/project.json create mode 100644 src/ng/demo/src/app/AppComponent.ts create mode 100644 src/ng/demo/src/app/AppModule.ts create mode 100644 src/ng/demo/src/app/AppWcModule.ts create mode 100644 src/ng/demo/src/app/DemoModules.ts create mode 100644 src/ng/demo/src/app/IndexComponent.ts create mode 100644 src/ng/demo/src/app/app.html create mode 100644 src/ng/demo/src/assets/browser/chrome.PNG create mode 100644 src/ng/demo/src/assets/browser/edge.PNG create mode 100644 src/ng/demo/src/assets/browser/firefox.PNG create mode 100644 src/ng/demo/src/assets/browser/safari.PNG create mode 100644 src/ng/demo/src/assets/food/cake.png create mode 100644 src/ng/demo/src/assets/food/coffee.png create mode 100644 src/ng/demo/src/assets/food/cola.png create mode 100644 src/ng/demo/src/assets/food/fried_chicken.png create mode 100644 src/ng/demo/src/assets/food/fries.png create mode 100644 src/ng/demo/src/assets/food/hamburger.png create mode 100644 src/ng/demo/src/assets/food/milk.png create mode 100644 src/ng/demo/src/assets/food/pizza.png create mode 100644 src/ng/demo/src/assets/image/1.jpg create mode 100644 src/ng/demo/src/assets/image/2.jpg create mode 100644 src/ng/demo/src/assets/image/3.jpg create mode 100644 src/ng/demo/src/assets/nav_logo/logo.png create mode 100644 src/ng/demo/src/favicon.ico create mode 100644 src/ng/demo/src/index.html create mode 100644 src/ng/demo/src/main.ts create mode 100644 src/ng/demo/src/webdoc/LICENSE create mode 100644 src/ng/demo/src/webdoc/faq-en.md create mode 100644 src/ng/demo/src/webdoc/faq.md create mode 100644 src/ng/demo/src/webdoc/getstart-en.md create mode 100644 src/ng/demo/src/webdoc/getstart.md create mode 100644 src/ng/demo/src/webdoc/images/basecolor1.png create mode 100644 src/ng/demo/src/webdoc/images/demo.png create mode 100644 src/ng/demo/src/webdoc/images/theme.png create mode 100644 src/ng/demo/src/webdoc/introduce-en.md create mode 100644 src/ng/demo/src/webdoc/introduce.md create mode 100644 src/ng/demo/src/webdoc/joinus-en.md create mode 100644 src/ng/demo/src/webdoc/joinus.md create mode 100644 src/ng/demo/src/webdoc/language-en.md create mode 100644 src/ng/demo/src/webdoc/language.md create mode 100644 src/ng/demo/src/webdoc/menus.js create mode 100644 src/ng/demo/src/webdoc/themedoc-en.md create mode 100644 src/ng/demo/src/webdoc/themedoc.md create mode 100644 src/ng/demo/src/webdoc/validators.md create mode 100644 src/ng/demo/test.ts create mode 100644 src/ng/demo/tsconfig.app.json create mode 100644 src/ng/demo/tsconfig.spec.json create mode 100644 src/ng/demolog/DemoLogComponent.ts create mode 100644 src/ng/demolog/DemoLogModule.ts create mode 100644 src/ng/demolog/log.html create mode 100644 src/ng/demolog/log.less create mode 100644 src/ng/lib/index.ts create mode 100644 src/ng/lib/ng-package.json create mode 100644 src/ng/lib/package.json create mode 100644 src/ng/lib/project.json create mode 100644 src/notification/demo/karma.conf.js create mode 100644 src/notification/demo/project.json create mode 100644 src/notification/demo/src/app/AppComponent.ts create mode 100644 src/notification/demo/src/app/AppModule.ts create mode 100644 src/notification/demo/src/app/IndexComponent.ts create mode 100644 src/notification/demo/src/app/app.html create mode 100644 src/notification/demo/src/app/notification/NotificationAnimationComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationBasicComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationCloseComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationConfigComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationDurationComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationEventsComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationHoverPauseComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationNameComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationPositionComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationTemplateComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationTestModule.ts create mode 100644 src/notification/demo/src/app/notification/NotificationTypeComponent.ts create mode 100644 src/notification/demo/src/app/notification/notification-animation.html create mode 100644 src/notification/demo/src/app/notification/notification-basic.html create mode 100644 src/notification/demo/src/app/notification/notification-close.html create mode 100644 src/notification/demo/src/app/notification/notification-config.html create mode 100644 src/notification/demo/src/app/notification/notification-duration.html create mode 100644 src/notification/demo/src/app/notification/notification-events.html create mode 100644 src/notification/demo/src/app/notification/notification-hover-pause.html create mode 100644 src/notification/demo/src/app/notification/notification-name.html create mode 100644 src/notification/demo/src/app/notification/notification-position.html create mode 100644 src/notification/demo/src/app/notification/notification-template.html create mode 100644 src/notification/demo/src/app/notification/notification-type.html create mode 100644 src/notification/demo/src/app/notification/webdoc/notification-demos.js create mode 100644 src/notification/demo/src/app/notification/webdoc/notification.cn.md create mode 100644 src/notification/demo/src/app/notification/webdoc/notification.en.md create mode 100644 src/notification/demo/src/favicon.ico create mode 100644 src/notification/demo/src/index.html create mode 100644 src/notification/demo/src/main.ts create mode 100644 src/notification/demo/test.ts create mode 100644 src/notification/demo/tsconfig.app.json create mode 100644 src/notification/demo/tsconfig.spec.json create mode 100644 src/notification/lib/index.ts create mode 100644 src/notification/lib/ng-package.json create mode 100644 src/notification/lib/package.json create mode 100644 src/notification/lib/project.json create mode 100644 src/notification/lib/src/TiNotificationComponent.html create mode 100644 src/notification/lib/src/TiNotificationComponent.ts create mode 100644 src/notification/lib/src/TiNotificationContainerComponent.html create mode 100644 src/notification/lib/src/TiNotificationContainerComponent.ts create mode 100644 src/notification/lib/src/TiNotificationInterface.ts create mode 100644 src/notification/lib/src/TiNotificationMapper.ts create mode 100644 src/notification/lib/src/TiNotificationModule.ts create mode 100644 src/notification/lib/src/TiNotificationMotion.ts create mode 100644 src/notification/lib/src/TiNotificationService.ts create mode 100644 src/notification/lib/src/notification.less create mode 100644 src/notification/lib/src/position.less create mode 100644 src/outline/lib/index.ts create mode 100644 src/outline/lib/ng-package.json create mode 100644 src/outline/lib/package.json create mode 100644 src/outline/lib/project.json create mode 100644 src/outline/lib/src/TiOutlineDirective.ts create mode 100644 src/outline/lib/src/TiOutlineModule.ts create mode 100644 src/overflow/demo/karma.conf.js create mode 100644 src/overflow/demo/project.json create mode 100644 src/overflow/demo/src/app/AppComponent.ts create mode 100644 src/overflow/demo/src/app/AppModule.ts create mode 100644 src/overflow/demo/src/app/IndexComponent.ts create mode 100644 src/overflow/demo/src/app/app.html create mode 100644 src/overflow/demo/src/app/overflow/OverflowDestoryComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowDirectiveComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowMaxlineComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowMaxwidthComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowPositionComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowServiceComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowTestComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowTestModule.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowTipcontentComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/overflow-destory.html create mode 100644 src/overflow/demo/src/app/overflow/overflow-directive.html create mode 100644 src/overflow/demo/src/app/overflow/overflow-maxline.html create mode 100644 src/overflow/demo/src/app/overflow/overflow-maxwidth.html create mode 100644 src/overflow/demo/src/app/overflow/overflow-position.html create mode 100644 src/overflow/demo/src/app/overflow/overflow-service.html create mode 100644 src/overflow/demo/src/app/overflow/overflow-test.html create mode 100644 src/overflow/demo/src/app/overflow/overflow-tipcontent.html create mode 100644 src/overflow/demo/src/app/overflow/webdoc/overflow-demos.js create mode 100644 src/overflow/demo/src/app/overflow/webdoc/overflow.cn.md create mode 100644 src/overflow/demo/src/app/overflow/webdoc/overflow.en.md create mode 100644 src/overflow/demo/src/favicon.ico create mode 100644 src/overflow/demo/src/index.html create mode 100644 src/overflow/demo/src/main.ts create mode 100644 src/overflow/demo/test.ts create mode 100644 src/overflow/demo/tsconfig.app.json create mode 100644 src/overflow/demo/tsconfig.spec.json create mode 100644 src/overflow/lib/index.ts create mode 100644 src/overflow/lib/ng-package.json create mode 100644 src/overflow/lib/package.json create mode 100644 src/overflow/lib/project.json create mode 100644 src/overflow/lib/src/TiOverflowDirective.ts create mode 100644 src/overflow/lib/src/TiOverflowMaxlineDirective.ts create mode 100644 src/overflow/lib/src/TiOverflowModule.ts create mode 100644 src/overflow/lib/src/TiOverflowService.ts create mode 100644 src/overflow/lib/src/TiOverflowServiceModule.ts create mode 100644 src/pagination/demo/karma.conf.js create mode 100644 src/pagination/demo/project.json create mode 100644 src/pagination/demo/src/app/AppComponent.ts create mode 100644 src/pagination/demo/src/app/AppModule.ts create mode 100644 src/pagination/demo/src/app/IndexComponent.ts create mode 100644 src/pagination/demo/src/app/app.html create mode 100644 src/pagination/demo/src/app/pagination/PaginationAutohideComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationDisabledComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationEventComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationFixedComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationLoadingComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationPageselectwidthComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationPagesizeComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationShowgotolinkComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationShowlastpageComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationShowtotalnumberComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationTestModule.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationTypeComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/pagination-autohide.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-disabled.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-event.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-fixed.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-loading.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-pageselectwidth.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-pagesize.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-showgotolink.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-showlastpage.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-showtotalnumber.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-type.html create mode 100644 src/pagination/demo/src/app/pagination/webdoc/pagination-demos.js create mode 100644 src/pagination/demo/src/app/pagination/webdoc/pagination.cn.md create mode 100644 src/pagination/demo/src/app/pagination/webdoc/pagination.en.md create mode 100644 src/pagination/demo/src/favicon.ico create mode 100644 src/pagination/demo/src/index.html create mode 100644 src/pagination/demo/src/main.ts create mode 100644 src/pagination/demo/test.ts create mode 100644 src/pagination/demo/tsconfig.app.json create mode 100644 src/pagination/demo/tsconfig.spec.json create mode 100644 src/pagination/lib/index.ts create mode 100644 src/pagination/lib/ng-package.json create mode 100644 src/pagination/lib/package.json create mode 100644 src/pagination/lib/project.json create mode 100644 src/pagination/lib/src/TiPaginationComponent.ts create mode 100644 src/pagination/lib/src/TiPaginationModule.ts create mode 100644 src/pagination/lib/src/i18n/TiPaginationWords.ts create mode 100644 src/pagination/lib/src/i18n/en_US.ts create mode 100644 src/pagination/lib/src/i18n/es_US.ts create mode 100644 src/pagination/lib/src/i18n/fr_FR.ts create mode 100644 src/pagination/lib/src/i18n/index.ts create mode 100644 src/pagination/lib/src/i18n/pt_BR.ts create mode 100644 src/pagination/lib/src/i18n/zh_CN.ts create mode 100644 src/pagination/lib/src/pagination.html create mode 100644 src/pagination/lib/src/pagination.less create mode 100644 src/path/demo/project.json create mode 100644 src/path/demo/src/app/AppComponent.ts create mode 100644 src/path/demo/src/app/AppModule.ts create mode 100644 src/path/demo/src/app/IndexComponent.ts create mode 100644 src/path/demo/src/app/app.html create mode 100644 src/path/demo/src/app/path/PathListComponent.ts create mode 100644 src/path/demo/src/app/path/PathSelectComponent.ts create mode 100644 src/path/demo/src/app/path/PathTestModule.ts create mode 100644 src/path/demo/src/app/path/PathfieldEditableComponent.ts create mode 100644 src/path/demo/src/app/path/PathfieldEventComponent.ts create mode 100644 src/path/demo/src/app/path/PathfieldIspanelComponent.ts create mode 100644 src/path/demo/src/app/path/PathfieldItemsComponent.ts create mode 100644 src/path/demo/src/app/path/PathfieldPanelwidthComponent.ts create mode 100644 src/path/demo/src/app/path/path-list.html create mode 100644 src/path/demo/src/app/path/path-select.html create mode 100644 src/path/demo/src/app/path/pathfield-editable.html create mode 100644 src/path/demo/src/app/path/pathfield-event.html create mode 100644 src/path/demo/src/app/path/pathfield-ispanel.html create mode 100644 src/path/demo/src/app/path/pathfield-items.html create mode 100644 src/path/demo/src/app/path/pathfield-panelwidth.html create mode 100644 src/path/demo/src/app/path/webdoc/path-demos.js create mode 100644 src/path/demo/src/app/path/webdoc/path.cn.md create mode 100644 src/path/demo/src/app/path/webdoc/path.en.md create mode 100644 src/path/demo/src/favicon.ico create mode 100644 src/path/demo/src/index.html create mode 100644 src/path/demo/src/main.ts create mode 100644 src/path/demo/tsconfig.app.json create mode 100644 src/path/lib/index.ts create mode 100644 src/path/lib/ng-package.json create mode 100644 src/path/lib/package.json create mode 100644 src/path/lib/project.json create mode 100644 src/path/lib/src/TiPathFieldComponent.ts create mode 100644 src/path/lib/src/TiPathListComponent.ts create mode 100644 src/path/lib/src/TiPathModule.ts create mode 100644 src/path/lib/src/path-field.html create mode 100644 src/path/lib/src/path-field.less create mode 100644 src/path/lib/src/path-list.html create mode 100644 src/path/lib/src/path-list.less create mode 100644 src/phonenumber/demo/project.json create mode 100644 src/phonenumber/demo/src/app/AppComponent.ts create mode 100644 src/phonenumber/demo/src/app/AppModule.ts create mode 100644 src/phonenumber/demo/src/app/IndexComponent.ts create mode 100644 src/phonenumber/demo/src/app/app.html create mode 100644 src/phonenumber/demo/src/app/phonenumber/PhonenumberBasicComponent.ts create mode 100644 src/phonenumber/demo/src/app/phonenumber/PhonenumberCountryComponent.ts create mode 100644 src/phonenumber/demo/src/app/phonenumber/PhonenumberDisabledComponent.ts create mode 100644 src/phonenumber/demo/src/app/phonenumber/PhonenumberEventComponent.ts create mode 100644 src/phonenumber/demo/src/app/phonenumber/PhonenumberTestModule.ts create mode 100644 src/phonenumber/demo/src/app/phonenumber/PhonenumberValidComponent.ts create mode 100644 src/phonenumber/demo/src/app/phonenumber/phonenumber-basic.html create mode 100644 src/phonenumber/demo/src/app/phonenumber/phonenumber-country.html create mode 100644 src/phonenumber/demo/src/app/phonenumber/phonenumber-disabled.html create mode 100644 src/phonenumber/demo/src/app/phonenumber/phonenumber-event.html create mode 100644 src/phonenumber/demo/src/app/phonenumber/phonenumber-valid.html create mode 100644 src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber-demos.js create mode 100644 src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.cn.md create mode 100644 src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.en.md create mode 100644 src/phonenumber/demo/src/favicon.ico create mode 100644 src/phonenumber/demo/src/index.html create mode 100644 src/phonenumber/demo/src/main.ts create mode 100644 src/phonenumber/demo/tsconfig.app.json create mode 100644 src/phonenumber/lib/index.ts create mode 100644 src/phonenumber/lib/ng-package.json create mode 100644 src/phonenumber/lib/package.json create mode 100644 src/phonenumber/lib/project.json create mode 100644 src/phonenumber/lib/src/TiPhoneValidatorDirective.ts create mode 100644 src/phonenumber/lib/src/TiPhonenumberComponent.ts create mode 100644 src/phonenumber/lib/src/TiPhonenumberModule.ts create mode 100644 src/phonenumber/lib/src/i18n/TiPhonenumberWords.ts create mode 100644 src/phonenumber/lib/src/i18n/en_US.ts create mode 100644 src/phonenumber/lib/src/i18n/es_US.ts create mode 100644 src/phonenumber/lib/src/i18n/fr_FR.ts create mode 100644 src/phonenumber/lib/src/i18n/index.ts create mode 100644 src/phonenumber/lib/src/i18n/pt_BR.ts create mode 100644 src/phonenumber/lib/src/i18n/zh_CN.ts create mode 100644 src/phonenumber/lib/src/phonenumber.html create mode 100644 src/phonenumber/lib/src/phonenumber.less create mode 100644 src/polyfills.ts create mode 100644 src/popconfirm/demo/project.json create mode 100644 src/popconfirm/demo/src/app/AppComponent.ts create mode 100644 src/popconfirm/demo/src/app/AppModule.ts create mode 100644 src/popconfirm/demo/src/app/IndexComponent.ts create mode 100644 src/popconfirm/demo/src/app/app.html create mode 100644 src/popconfirm/demo/src/app/popconfirm/PopconfirmBasicComponent.ts create mode 100644 src/popconfirm/demo/src/app/popconfirm/PopconfirmDefineComponent.ts create mode 100644 src/popconfirm/demo/src/app/popconfirm/PopconfirmEventComponent.ts create mode 100644 src/popconfirm/demo/src/app/popconfirm/PopconfirmTableComponent.ts create mode 100644 src/popconfirm/demo/src/app/popconfirm/PopconfirmTableDefineComponent.ts create mode 100644 src/popconfirm/demo/src/app/popconfirm/PopconfirmTestModule.ts create mode 100644 src/popconfirm/demo/src/app/popconfirm/popconfirm-basic.html create mode 100644 src/popconfirm/demo/src/app/popconfirm/popconfirm-define.html create mode 100644 src/popconfirm/demo/src/app/popconfirm/popconfirm-event.html create mode 100644 src/popconfirm/demo/src/app/popconfirm/popconfirm-table-define.html create mode 100644 src/popconfirm/demo/src/app/popconfirm/popconfirm-table.html create mode 100644 src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm-demos.js create mode 100644 src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.cn.md create mode 100644 src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.en.md create mode 100644 src/popconfirm/demo/src/favicon.ico create mode 100644 src/popconfirm/demo/src/index.html create mode 100644 src/popconfirm/demo/src/main.ts create mode 100644 src/popconfirm/demo/tsconfig.app.json create mode 100644 src/popconfirm/lib/index.ts create mode 100644 src/popconfirm/lib/ng-package.json create mode 100644 src/popconfirm/lib/package.json create mode 100644 src/popconfirm/lib/project.json create mode 100644 src/popconfirm/lib/src/TiPopconfirmComponent.ts create mode 100644 src/popconfirm/lib/src/TiPopconfirmDirective.ts create mode 100644 src/popconfirm/lib/src/TiPopconfirmModule.ts create mode 100644 src/popconfirm/lib/src/i18n/TiPopconfirmWords.ts create mode 100644 src/popconfirm/lib/src/i18n/en_US.ts create mode 100644 src/popconfirm/lib/src/i18n/es_US.ts create mode 100644 src/popconfirm/lib/src/i18n/fr_FR.ts create mode 100644 src/popconfirm/lib/src/i18n/index.ts create mode 100644 src/popconfirm/lib/src/i18n/pt_BR.ts create mode 100644 src/popconfirm/lib/src/i18n/zh_CN.ts create mode 100644 src/popconfirm/lib/src/popconfirm.html create mode 100644 src/popconfirm/lib/src/popconfirm.less create mode 100644 src/popup/lib/index.ts create mode 100644 src/popup/lib/ng-package.json create mode 100644 src/popup/lib/package.json create mode 100644 src/popup/lib/project.json create mode 100644 src/popup/lib/src/TiPopupModule.ts create mode 100644 src/popup/lib/src/TiPopupService.ts create mode 100644 src/productpreview/demo/project.json create mode 100644 src/productpreview/demo/src/app/AppComponent.ts create mode 100644 src/productpreview/demo/src/app/AppModule.ts create mode 100644 src/productpreview/demo/src/app/IndexComponent.ts create mode 100644 src/productpreview/demo/src/app/app.html create mode 100644 src/productpreview/demo/src/app/productpreview/ProductpreviewBasicComponent.ts create mode 100644 src/productpreview/demo/src/app/productpreview/ProductpreviewTestModule.ts create mode 100644 src/productpreview/demo/src/app/productpreview/productpreview-basic.html create mode 100644 src/productpreview/demo/src/app/productpreview/webdoc/productpreview-demos.js create mode 100644 src/productpreview/demo/src/app/productpreview/webdoc/productpreview.cn.md create mode 100644 src/productpreview/demo/src/app/productpreview/webdoc/productpreview.en.md create mode 100644 src/productpreview/demo/src/favicon.ico create mode 100644 src/productpreview/demo/src/index.html create mode 100644 src/productpreview/demo/src/main.ts create mode 100644 src/productpreview/demo/tsconfig.app.json create mode 100644 src/productpreview/lib/index.ts create mode 100644 src/productpreview/lib/ng-package.json create mode 100644 src/productpreview/lib/package.json create mode 100644 src/productpreview/lib/project.json create mode 100644 src/productpreview/lib/src/TiProductpreviewComponent.ts create mode 100644 src/productpreview/lib/src/TiProductpreviewModule.ts create mode 100644 src/productpreview/lib/src/productpreview.html create mode 100644 src/productpreview/lib/src/productpreview.less create mode 100644 src/progressbar/demo/karma.conf.js create mode 100644 src/progressbar/demo/project.json create mode 100644 src/progressbar/demo/src/app/AppComponent.ts create mode 100644 src/progressbar/demo/src/app/AppModule.ts create mode 100644 src/progressbar/demo/src/app/IndexComponent.ts create mode 100644 src/progressbar/demo/src/app/app.html create mode 100644 src/progressbar/demo/src/app/progressbar/ProgressbarAnimationComponent.ts create mode 100644 src/progressbar/demo/src/app/progressbar/ProgressbarBasicComponent.ts create mode 100644 src/progressbar/demo/src/app/progressbar/ProgressbarClassComponent.ts create mode 100644 src/progressbar/demo/src/app/progressbar/ProgressbarTestModule.ts create mode 100644 src/progressbar/demo/src/app/progressbar/progressbar-animation.html create mode 100644 src/progressbar/demo/src/app/progressbar/progressbar-basic.html create mode 100644 src/progressbar/demo/src/app/progressbar/progressbar-class.html create mode 100644 src/progressbar/demo/src/app/progressbar/progressbar-class.less create mode 100644 src/progressbar/demo/src/app/progressbar/webdoc/progressbar-demos.js create mode 100644 src/progressbar/demo/src/app/progressbar/webdoc/progressbar.cn.md create mode 100644 src/progressbar/demo/src/app/progressbar/webdoc/progressbar.en.md create mode 100644 src/progressbar/demo/src/favicon.ico create mode 100644 src/progressbar/demo/src/index.html create mode 100644 src/progressbar/demo/src/main.ts create mode 100644 src/progressbar/demo/test.ts create mode 100644 src/progressbar/demo/tsconfig.app.json create mode 100644 src/progressbar/demo/tsconfig.spec.json create mode 100644 src/progressbar/lib/index.ts create mode 100644 src/progressbar/lib/ng-package.json create mode 100644 src/progressbar/lib/package.json create mode 100644 src/progressbar/lib/project.json create mode 100644 src/progressbar/lib/src/TiProgressbarComponent.ts create mode 100644 src/progressbar/lib/src/TiProgressbarModule.ts create mode 100644 src/progressbar/lib/src/progressbar.html create mode 100644 src/progressbar/lib/src/progressbar.less create mode 100644 src/progresspie/demo/project.json create mode 100644 src/progresspie/demo/src/app/AppComponent.ts create mode 100644 src/progresspie/demo/src/app/AppModule.ts create mode 100644 src/progresspie/demo/src/app/IndexComponent.ts create mode 100644 src/progresspie/demo/src/app/app.html create mode 100644 src/progresspie/demo/src/app/progresspie/ProgresspieTestComponent.ts create mode 100644 src/progresspie/demo/src/app/progresspie/ProgresspieTestModule.ts create mode 100644 src/progresspie/demo/src/favicon.ico create mode 100644 src/progresspie/demo/src/index.html create mode 100644 src/progresspie/demo/src/main.ts create mode 100644 src/progresspie/demo/tsconfig.app.json create mode 100644 src/progresspie/lib/index.ts create mode 100644 src/progresspie/lib/ng-package.json create mode 100644 src/progresspie/lib/package.json create mode 100644 src/progresspie/lib/project.json create mode 100644 src/progresspie/lib/src/TiProgresspieComponent.ts create mode 100644 src/progresspie/lib/src/TiProgresspieModule.ts create mode 100644 src/radio/demo/karma.conf.js create mode 100644 src/radio/demo/project.json create mode 100644 src/radio/demo/src/app/AppComponent.ts create mode 100644 src/radio/demo/src/app/AppModule.ts create mode 100644 src/radio/demo/src/app/IndexComponent.ts create mode 100644 src/radio/demo/src/app/app.html create mode 100644 src/radio/demo/src/app/radio/RadioBasicComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioDarkComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioDisabledComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioEventComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioFocusComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioGroupComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioGroupDirectionComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioGroupLabelkeyComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioGroupLinewrapComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioGroupValidationComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioGroupValuekeyComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioLabelComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioTestModule.ts create mode 100644 src/radio/demo/src/app/radio/radio-basic.html create mode 100644 src/radio/demo/src/app/radio/radio-dark.html create mode 100644 src/radio/demo/src/app/radio/radio-disabled.html create mode 100644 src/radio/demo/src/app/radio/radio-event.html create mode 100644 src/radio/demo/src/app/radio/radio-focus.html create mode 100644 src/radio/demo/src/app/radio/radio-group-direction.html create mode 100644 src/radio/demo/src/app/radio/radio-group-labelkey.html create mode 100644 src/radio/demo/src/app/radio/radio-group-linewrap.html create mode 100644 src/radio/demo/src/app/radio/radio-group-validation.html create mode 100644 src/radio/demo/src/app/radio/radio-group-valuekey.html create mode 100644 src/radio/demo/src/app/radio/radio-group.html create mode 100644 src/radio/demo/src/app/radio/radio-label.html create mode 100644 src/radio/demo/src/app/radio/webdoc/radio-demos.js create mode 100644 src/radio/demo/src/app/radio/webdoc/radio.cn.md create mode 100644 src/radio/demo/src/app/radio/webdoc/radio.en.md create mode 100644 src/radio/demo/src/favicon.ico create mode 100644 src/radio/demo/src/index.html create mode 100644 src/radio/demo/src/main.ts create mode 100644 src/radio/demo/test.ts create mode 100644 src/radio/demo/tsconfig.app.json create mode 100644 src/radio/demo/tsconfig.spec.json create mode 100644 src/radio/lib/index.ts create mode 100644 src/radio/lib/ng-package.json create mode 100644 src/radio/lib/package.json create mode 100644 src/radio/lib/project.json create mode 100644 src/radio/lib/src/TiRadioComponent.ts create mode 100644 src/radio/lib/src/TiRadioGroupComponent.ts create mode 100644 src/radio/lib/src/TiRadioModule.ts create mode 100644 src/radio/lib/src/radio-group.html create mode 100644 src/radio/lib/src/radio.html create mode 100644 src/radio/lib/src/radio.less create mode 100644 src/radio/lib/src/radiogroup.less create mode 100644 src/rate/demo/karma.conf.js create mode 100644 src/rate/demo/project.json create mode 100644 src/rate/demo/src/app/AppComponent.ts create mode 100644 src/rate/demo/src/app/AppModule.ts create mode 100644 src/rate/demo/src/app/IndexComponent.ts create mode 100644 src/rate/demo/src/app/app.html create mode 100644 src/rate/demo/src/app/rate/RateBasicComponent.ts create mode 100644 src/rate/demo/src/app/rate/RateDisabledComponent.ts create mode 100644 src/rate/demo/src/app/rate/RateEventComponent.ts create mode 100644 src/rate/demo/src/app/rate/RateIdComponent.ts create mode 100644 src/rate/demo/src/app/rate/RateLoadComponent.ts create mode 100644 src/rate/demo/src/app/rate/RateTestModule.ts create mode 100644 src/rate/demo/src/app/rate/rate-basic.html create mode 100644 src/rate/demo/src/app/rate/rate-disabled.html create mode 100644 src/rate/demo/src/app/rate/rate-event.html create mode 100644 src/rate/demo/src/app/rate/rate-id.html create mode 100644 src/rate/demo/src/app/rate/rate-load.html create mode 100644 src/rate/demo/src/app/rate/webdoc/rate-demos.js create mode 100644 src/rate/demo/src/app/rate/webdoc/rate.cn.md create mode 100644 src/rate/demo/src/app/rate/webdoc/rate.en.md create mode 100644 src/rate/demo/src/favicon.ico create mode 100644 src/rate/demo/src/index.html create mode 100644 src/rate/demo/src/main.ts create mode 100644 src/rate/demo/test.ts create mode 100644 src/rate/demo/tsconfig.app.json create mode 100644 src/rate/demo/tsconfig.spec.json create mode 100644 src/rate/lib/index.ts create mode 100644 src/rate/lib/ng-package.json create mode 100644 src/rate/lib/package.json create mode 100644 src/rate/lib/project.json create mode 100644 src/rate/lib/src/TiRateComponent.ts create mode 100644 src/rate/lib/src/TiRateModule.ts create mode 100644 src/rate/lib/src/rate.html create mode 100644 src/rate/lib/src/rate.less create mode 100644 src/renderer/lib/index.ts create mode 100644 src/renderer/lib/ng-package.json create mode 100644 src/renderer/lib/package.json create mode 100644 src/renderer/lib/project.json create mode 100644 src/renderer/lib/src/TiRenderer.ts create mode 100644 src/renderer/lib/src/TiRendererModule.ts create mode 100644 src/rights/demo/project.json create mode 100644 src/rights/demo/src/app/AppComponent.ts create mode 100644 src/rights/demo/src/app/AppModule.ts create mode 100644 src/rights/demo/src/app/IndexComponent.ts create mode 100644 src/rights/demo/src/app/app.html create mode 100644 src/rights/demo/src/app/rights/RightsBasicComponent.ts create mode 100644 src/rights/demo/src/app/rights/RightsTestModule.ts create mode 100644 src/rights/demo/src/app/rights/RightsTypeComponent.ts create mode 100644 src/rights/demo/src/app/rights/rights-basic.html create mode 100644 src/rights/demo/src/app/rights/rights-type.html create mode 100644 src/rights/demo/src/app/rights/webdoc/rights-demos.js create mode 100644 src/rights/demo/src/app/rights/webdoc/rights.cn.md create mode 100644 src/rights/demo/src/app/rights/webdoc/rights.en.md create mode 100644 src/rights/demo/src/favicon.ico create mode 100644 src/rights/demo/src/index.html create mode 100644 src/rights/demo/src/main.ts create mode 100644 src/rights/demo/tsconfig.app.json create mode 100644 src/rights/lib/index.ts create mode 100644 src/rights/lib/ng-package.json create mode 100644 src/rights/lib/package.json create mode 100644 src/rights/lib/project.json create mode 100644 src/rights/lib/src/TiRightsComponent.ts create mode 100644 src/rights/lib/src/TiRightsModule.ts create mode 100644 src/rights/lib/src/fonts/rightsFont.svg create mode 100644 src/rights/lib/src/fonts/rightsFont.woff create mode 100644 src/rights/lib/src/icon.less create mode 100644 src/rights/lib/src/rights.html create mode 100644 src/rights/lib/src/rights.less create mode 100644 src/score/demo/karma.conf.js create mode 100644 src/score/demo/project.json create mode 100644 src/score/demo/src/app/AppComponent.ts create mode 100644 src/score/demo/src/app/AppModule.ts create mode 100644 src/score/demo/src/app/IndexComponent.ts create mode 100644 src/score/demo/src/app/app.html create mode 100644 src/score/demo/src/app/score/ScoreBasicComponent.ts create mode 100644 src/score/demo/src/app/score/ScoreEventsComponent.ts create mode 100644 src/score/demo/src/app/score/ScoreLimittextComponent.ts create mode 100644 src/score/demo/src/app/score/ScorePaddingComponent.ts create mode 100644 src/score/demo/src/app/score/ScoreTestModule.ts create mode 100644 src/score/demo/src/app/score/score-basic.html create mode 100644 src/score/demo/src/app/score/score-events.html create mode 100644 src/score/demo/src/app/score/score-limittext.html create mode 100644 src/score/demo/src/app/score/score-padding.html create mode 100644 src/score/demo/src/app/score/webdoc/score-demos.js create mode 100644 src/score/demo/src/app/score/webdoc/score.cn.md create mode 100644 src/score/demo/src/app/score/webdoc/score.en.md create mode 100644 src/score/demo/src/favicon.ico create mode 100644 src/score/demo/src/index.html create mode 100644 src/score/demo/src/main.ts create mode 100644 src/score/demo/test.ts create mode 100644 src/score/demo/tsconfig.app.json create mode 100644 src/score/demo/tsconfig.spec.json create mode 100644 src/score/lib/index.ts create mode 100644 src/score/lib/ng-package.json create mode 100644 src/score/lib/package.json create mode 100644 src/score/lib/project.json create mode 100644 src/score/lib/src/TiScoreComponent.ts create mode 100644 src/score/lib/src/TiScoreModule.ts create mode 100644 src/score/lib/src/i18n/TiScoreWords.ts create mode 100644 src/score/lib/src/i18n/en_US.ts create mode 100644 src/score/lib/src/i18n/es_US.ts create mode 100644 src/score/lib/src/i18n/fr_FR.ts create mode 100644 src/score/lib/src/i18n/index.ts create mode 100644 src/score/lib/src/i18n/pt_BR.ts create mode 100644 src/score/lib/src/i18n/zh_CN.ts create mode 100644 src/score/lib/src/score.html create mode 100644 src/score/lib/src/score.less create mode 100644 src/scroll/lib/index.ts create mode 100644 src/scroll/lib/ng-package.json create mode 100644 src/scroll/lib/package.json create mode 100644 src/scroll/lib/project.json create mode 100644 src/scroll/lib/src/TiScrollDirective.ts create mode 100644 src/scroll/lib/src/TiScrollModule.ts create mode 100644 src/searchbox/demo/karma.conf.js create mode 100644 src/searchbox/demo/project.json create mode 100644 src/searchbox/demo/src/app/AppComponent.ts create mode 100644 src/searchbox/demo/src/app/AppModule.ts create mode 100644 src/searchbox/demo/src/app/IndexComponent.ts create mode 100644 src/searchbox/demo/src/app/app.html create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxAppendtobodyComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxBasicComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxDisabledComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxEventComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxMaxlengthComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxNotsearchComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxOptionsComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxPanelsizeComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxReactiveComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxSuggestComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxTemplateComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxTestComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxTestModule.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxTrimmedComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxValidComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxVirtualscrollComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-appendtobody.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-basic.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-disabled.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-event.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-maxlength.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-notsearch.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-options.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-panelsize.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-reactive.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-suggest.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-template.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-test.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-trimmed.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-valid.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-virtualscroll.html create mode 100644 src/searchbox/demo/src/app/searchbox/webdoc/searchbox-demos.js create mode 100644 src/searchbox/demo/src/app/searchbox/webdoc/searchbox.cn.md create mode 100644 src/searchbox/demo/src/app/searchbox/webdoc/searchbox.en.md create mode 100644 src/searchbox/demo/src/favicon.ico create mode 100644 src/searchbox/demo/src/index.html create mode 100644 src/searchbox/demo/src/main.ts create mode 100644 src/searchbox/demo/test.ts create mode 100644 src/searchbox/demo/tsconfig.app.json create mode 100644 src/searchbox/demo/tsconfig.spec.json create mode 100644 src/searchbox/lib/index.ts create mode 100644 src/searchbox/lib/ng-package.json create mode 100644 src/searchbox/lib/package.json create mode 100644 src/searchbox/lib/project.json create mode 100644 src/searchbox/lib/src/TiSearchboxComponent.ts create mode 100644 src/searchbox/lib/src/TiSearchboxModule.ts create mode 100644 src/searchbox/lib/src/TiSearchboxNotsearchComponent.ts create mode 100644 src/searchbox/lib/src/i18n/TiSearchboxWords.ts create mode 100644 src/searchbox/lib/src/i18n/en_US.ts create mode 100644 src/searchbox/lib/src/i18n/es_US.ts create mode 100644 src/searchbox/lib/src/i18n/fr_FR.ts create mode 100644 src/searchbox/lib/src/i18n/index.ts create mode 100644 src/searchbox/lib/src/i18n/pt_BR.ts create mode 100644 src/searchbox/lib/src/i18n/zh_CN.ts create mode 100644 src/searchbox/lib/src/searchbox-notsearch.less create mode 100644 src/searchbox/lib/src/searchbox.html create mode 100644 src/searchbox/lib/src/searchbox.less create mode 100644 src/select/demo/karma.conf.js create mode 100644 src/select/demo/project.json create mode 100644 src/select/demo/src/app/AppComponent.ts create mode 100644 src/select/demo/src/app/AppModule.ts create mode 100644 src/select/demo/src/app/IndexComponent.ts create mode 100644 src/select/demo/src/app/app.html create mode 100644 src/select/demo/src/app/select/NoEmptyPipe.ts create mode 100644 src/select/demo/src/app/select/SelectAppendtobodyComponent.ts create mode 100644 src/select/demo/src/app/select/SelectBasicComponent.ts create mode 100644 src/select/demo/src/app/select/SelectBeforesearchComponent.ts create mode 100644 src/select/demo/src/app/select/SelectBeforesearchTestComponent.ts create mode 100644 src/select/demo/src/app/select/SelectChangeSelectallComponent.ts create mode 100644 src/select/demo/src/app/select/SelectClearableComponent.ts create mode 100644 src/select/demo/src/app/select/SelectDisabledComponent.ts create mode 100644 src/select/demo/src/app/select/SelectDisabledfocusComponent.ts create mode 100644 src/select/demo/src/app/select/SelectEnumComponent.ts create mode 100644 src/select/demo/src/app/select/SelectEventComponent.ts create mode 100644 src/select/demo/src/app/select/SelectFocusComponent.ts create mode 100644 src/select/demo/src/app/select/SelectGroupComponent.ts create mode 100644 src/select/demo/src/app/select/SelectIdComponent.ts create mode 100644 src/select/demo/src/app/select/SelectIdkeyComponent.ts create mode 100644 src/select/demo/src/app/select/SelectInputComponent.ts create mode 100644 src/select/demo/src/app/select/SelectLabelkeyComponent.ts create mode 100644 src/select/demo/src/app/select/SelectLazyComponent.ts create mode 100644 src/select/demo/src/app/select/SelectLeakComponent.ts create mode 100644 src/select/demo/src/app/select/SelectLoadComponent.ts create mode 100644 src/select/demo/src/app/select/SelectManyComponent.ts create mode 100644 src/select/demo/src/app/select/SelectMaxlineComponent.ts create mode 100644 src/select/demo/src/app/select/SelectMuchComponent.ts create mode 100644 src/select/demo/src/app/select/SelectMultiComponent.ts create mode 100644 src/select/demo/src/app/select/SelectNoborderComponent.ts create mode 100644 src/select/demo/src/app/select/SelectNodataComponent.ts create mode 100644 src/select/demo/src/app/select/SelectNoemptyComponent.ts create mode 100644 src/select/demo/src/app/select/SelectNullComponent.ts create mode 100644 src/select/demo/src/app/select/SelectPaginBeforesearchComponent.ts create mode 100644 src/select/demo/src/app/select/SelectPaginationComponent.ts create mode 100644 src/select/demo/src/app/select/SelectPanelComponent.ts create mode 100644 src/select/demo/src/app/select/SelectReservesearchwordComponent.ts create mode 100644 src/select/demo/src/app/select/SelectScrollLoadComponent.ts create mode 100644 src/select/demo/src/app/select/SelectSearchComponent.ts create mode 100644 src/select/demo/src/app/select/SelectSearchkeysComponent.ts create mode 100644 src/select/demo/src/app/select/SelectSelectallComponent.ts create mode 100644 src/select/demo/src/app/select/SelectShowselectednumberComponent.ts create mode 100644 src/select/demo/src/app/select/SelectSmallComponent.ts create mode 100644 src/select/demo/src/app/select/SelectTagComponent.ts create mode 100644 src/select/demo/src/app/select/SelectTemplateComponent.ts create mode 100644 src/select/demo/src/app/select/SelectTestModule.ts create mode 100644 src/select/demo/src/app/select/SelectTipComponent.ts create mode 100644 src/select/demo/src/app/select/SelectTiscrollComponent.ts create mode 100644 src/select/demo/src/app/select/SelectTworowComponent.ts create mode 100644 src/select/demo/src/app/select/SelectValidComponent.ts create mode 100644 src/select/demo/src/app/select/SelectValidGroupComponent.ts create mode 100644 src/select/demo/src/app/select/SelectValuekeyComponent.ts create mode 100644 src/select/demo/src/app/select/SelectValuekeyTestComponent.ts create mode 100644 src/select/demo/src/app/select/SelectVirtualscrollComponent.ts create mode 100644 src/select/demo/src/app/select/SelectVirtualscrollMultiComponent.ts create mode 100644 src/select/demo/src/app/select/select-appendtobody.html create mode 100644 src/select/demo/src/app/select/select-basic.html create mode 100644 src/select/demo/src/app/select/select-beforesearch-test.html create mode 100644 src/select/demo/src/app/select/select-beforesearch.html create mode 100644 src/select/demo/src/app/select/select-change-selectall.html create mode 100644 src/select/demo/src/app/select/select-clearable.html create mode 100644 src/select/demo/src/app/select/select-disabled.html create mode 100644 src/select/demo/src/app/select/select-disabledfocus.html create mode 100644 src/select/demo/src/app/select/select-enum.html create mode 100644 src/select/demo/src/app/select/select-event.html create mode 100644 src/select/demo/src/app/select/select-focus.html create mode 100644 src/select/demo/src/app/select/select-group.html create mode 100644 src/select/demo/src/app/select/select-id.html create mode 100644 src/select/demo/src/app/select/select-idkey.html create mode 100644 src/select/demo/src/app/select/select-input.html create mode 100644 src/select/demo/src/app/select/select-labelkey.html create mode 100644 src/select/demo/src/app/select/select-lazy.html create mode 100644 src/select/demo/src/app/select/select-leak.html create mode 100644 src/select/demo/src/app/select/select-load.html create mode 100644 src/select/demo/src/app/select/select-many.html create mode 100644 src/select/demo/src/app/select/select-maxline.html create mode 100644 src/select/demo/src/app/select/select-much.html create mode 100644 src/select/demo/src/app/select/select-multi.html create mode 100644 src/select/demo/src/app/select/select-noborder.html create mode 100644 src/select/demo/src/app/select/select-nodata.html create mode 100644 src/select/demo/src/app/select/select-noempty.html create mode 100644 src/select/demo/src/app/select/select-null.html create mode 100644 src/select/demo/src/app/select/select-pagin-beforesearch.html create mode 100644 src/select/demo/src/app/select/select-pagination.html create mode 100644 src/select/demo/src/app/select/select-panel.html create mode 100644 src/select/demo/src/app/select/select-reservesearchword.html create mode 100644 src/select/demo/src/app/select/select-scroll-load.html create mode 100644 src/select/demo/src/app/select/select-search.html create mode 100644 src/select/demo/src/app/select/select-searchkeys.html create mode 100644 src/select/demo/src/app/select/select-selectall.html create mode 100644 src/select/demo/src/app/select/select-showselectednumber.html create mode 100644 src/select/demo/src/app/select/select-small.html create mode 100644 src/select/demo/src/app/select/select-tag.html create mode 100644 src/select/demo/src/app/select/select-tag.less create mode 100644 src/select/demo/src/app/select/select-template.html create mode 100644 src/select/demo/src/app/select/select-tip.html create mode 100644 src/select/demo/src/app/select/select-tiscroll.html create mode 100644 src/select/demo/src/app/select/select-tworow.html create mode 100644 src/select/demo/src/app/select/select-valid.html create mode 100644 src/select/demo/src/app/select/select-validgroup.html create mode 100644 src/select/demo/src/app/select/select-valuekey-test.html create mode 100644 src/select/demo/src/app/select/select-valuekey.html create mode 100644 src/select/demo/src/app/select/select-virtualscroll-multi.html create mode 100644 src/select/demo/src/app/select/select-virtualscroll.html create mode 100644 src/select/demo/src/app/select/webdoc/select-demos.js create mode 100644 src/select/demo/src/app/select/webdoc/select.cn.md create mode 100644 src/select/demo/src/app/select/webdoc/select.en.md create mode 100644 "src/select/demo/src/app/select/\346\265\213\350\257\225\347\224\250\344\276\213.txt" create mode 100644 src/select/demo/src/favicon.ico create mode 100644 src/select/demo/src/index.html create mode 100644 src/select/demo/src/main.ts create mode 100644 src/select/demo/test.ts create mode 100644 src/select/demo/tsconfig.app.json create mode 100644 src/select/demo/tsconfig.spec.json create mode 100644 src/select/lib/index.ts create mode 100644 src/select/lib/ng-package.json create mode 100644 src/select/lib/package.json create mode 100644 src/select/lib/project.json create mode 100644 src/select/lib/src/TiSelectComponent.ts create mode 100644 src/select/lib/src/TiSelectModule.ts create mode 100644 src/select/lib/src/select-small.less create mode 100644 src/select/lib/src/select.html create mode 100644 src/select/lib/src/select.less create mode 100644 src/selectgroup/demo/project.json create mode 100644 src/selectgroup/demo/src/app/AppComponent.ts create mode 100644 src/selectgroup/demo/src/app/AppModule.ts create mode 100644 src/selectgroup/demo/src/app/IndexComponent.ts create mode 100644 src/selectgroup/demo/src/app/app.html create mode 100644 src/selectgroup/demo/src/app/selectgroup/SelectgroupBasicComponent.ts create mode 100644 src/selectgroup/demo/src/app/selectgroup/SelectgroupMultipleComponent.ts create mode 100644 src/selectgroup/demo/src/app/selectgroup/SelectgroupSelectComponent.ts create mode 100644 src/selectgroup/demo/src/app/selectgroup/SelectgroupTemplateComponent.ts create mode 100644 src/selectgroup/demo/src/app/selectgroup/SelectgroupTestModule.ts create mode 100644 src/selectgroup/demo/src/app/selectgroup/SelectgroupValuekeyComponent.ts create mode 100644 src/selectgroup/demo/src/app/selectgroup/selectgroup-basic.html create mode 100644 src/selectgroup/demo/src/app/selectgroup/selectgroup-multiple.html create mode 100644 src/selectgroup/demo/src/app/selectgroup/selectgroup-select.html create mode 100644 src/selectgroup/demo/src/app/selectgroup/selectgroup-template.html create mode 100644 src/selectgroup/demo/src/app/selectgroup/selectgroup-valuekey.html create mode 100644 src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup-demos.js create mode 100644 src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.cn.md create mode 100644 src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.en.md create mode 100644 src/selectgroup/demo/src/favicon.ico create mode 100644 src/selectgroup/demo/src/index.html create mode 100644 src/selectgroup/demo/src/main.ts create mode 100644 src/selectgroup/demo/tsconfig.app.json create mode 100644 src/selectgroup/lib/index.ts create mode 100644 src/selectgroup/lib/ng-package.json create mode 100644 src/selectgroup/lib/package.json create mode 100644 src/selectgroup/lib/project.json create mode 100644 src/selectgroup/lib/src/TiSelectgroupComponent.ts create mode 100644 src/selectgroup/lib/src/TiSelectgroupModule.ts create mode 100644 src/selectgroup/lib/src/TiSelectitemComponent.ts create mode 100644 src/selectgroup/lib/src/selectgroup.html create mode 100644 src/selectgroup/lib/src/selectgroup.less create mode 100644 src/selectgroup/lib/src/selectitem.html create mode 100644 src/skeleton/demo/karma.conf.js create mode 100644 src/skeleton/demo/project.json create mode 100644 src/skeleton/demo/src/app/AppComponent.ts create mode 100644 src/skeleton/demo/src/app/AppModule.ts create mode 100644 src/skeleton/demo/src/app/IndexComponent.ts create mode 100644 src/skeleton/demo/src/app/app.html create mode 100644 src/skeleton/demo/src/app/skeleton/SkeletonPageComponent.ts create mode 100644 src/skeleton/demo/src/app/skeleton/SkeletonTestModule.ts create mode 100644 src/skeleton/demo/src/app/skeleton/SkeletonTitleComponent.ts create mode 100644 src/skeleton/demo/src/app/skeleton/SkeletonTypeComponent.ts create mode 100644 src/skeleton/demo/src/app/skeleton/skeleton-page.html create mode 100644 src/skeleton/demo/src/app/skeleton/skeleton-page.less create mode 100644 src/skeleton/demo/src/app/skeleton/skeleton-title.html create mode 100644 src/skeleton/demo/src/app/skeleton/skeleton-type.html create mode 100644 src/skeleton/demo/src/app/skeleton/skeleton.less create mode 100644 src/skeleton/demo/src/app/skeleton/webdoc/skeleton-demos.js create mode 100644 src/skeleton/demo/src/app/skeleton/webdoc/skeleton.cn.md create mode 100644 src/skeleton/demo/src/app/skeleton/webdoc/skeleton.en.md create mode 100644 src/skeleton/demo/src/favicon.ico create mode 100644 src/skeleton/demo/src/index.html create mode 100644 src/skeleton/demo/src/main.ts create mode 100644 src/skeleton/demo/test.ts create mode 100644 src/skeleton/demo/tsconfig.app.json create mode 100644 src/skeleton/demo/tsconfig.spec.json create mode 100644 src/skeleton/lib/index.ts create mode 100644 src/skeleton/lib/ng-package.json create mode 100644 src/skeleton/lib/package.json create mode 100644 src/skeleton/lib/project.json create mode 100644 src/skeleton/lib/src/TiSkeletonComponent.ts create mode 100644 src/skeleton/lib/src/TiSkeletonModule.ts create mode 100644 src/skeleton/lib/src/skeleton.html create mode 100644 src/skeleton/lib/src/skeleton.less create mode 100644 src/slider/demo/karma.conf.js create mode 100644 src/slider/demo/project.json create mode 100644 src/slider/demo/src/app/AppComponent.ts create mode 100644 src/slider/demo/src/app/AppModule.ts create mode 100644 src/slider/demo/src/app/IndexComponent.ts create mode 100644 src/slider/demo/src/app/app.html create mode 100644 src/slider/demo/src/app/slider/SliderEventComponent.ts create mode 100644 src/slider/demo/src/app/slider/SliderFormcontrolComponent.ts create mode 100644 src/slider/demo/src/app/slider/SliderHiddenComponent.ts create mode 100644 src/slider/demo/src/app/slider/SliderLimitsComponent.ts create mode 100644 src/slider/demo/src/app/slider/SliderRatiosComponent.ts create mode 100644 src/slider/demo/src/app/slider/SliderScalesComponent.ts create mode 100644 src/slider/demo/src/app/slider/SliderTemplateComponent.ts create mode 100644 src/slider/demo/src/app/slider/SliderTestModule.ts create mode 100644 src/slider/demo/src/app/slider/SliderTipComponent.ts create mode 100644 src/slider/demo/src/app/slider/slider-event.html create mode 100644 src/slider/demo/src/app/slider/slider-formcontrol.html create mode 100644 src/slider/demo/src/app/slider/slider-hidden.html create mode 100644 src/slider/demo/src/app/slider/slider-limits.html create mode 100644 src/slider/demo/src/app/slider/slider-ratios.html create mode 100644 src/slider/demo/src/app/slider/slider-scales.html create mode 100644 src/slider/demo/src/app/slider/slider-template.html create mode 100644 src/slider/demo/src/app/slider/slider-tip.html create mode 100644 src/slider/demo/src/app/slider/webdoc/slider-demos.js create mode 100644 src/slider/demo/src/app/slider/webdoc/slider.cn.md create mode 100644 src/slider/demo/src/app/slider/webdoc/slider.en.md create mode 100644 src/slider/demo/src/favicon.ico create mode 100644 src/slider/demo/src/index.html create mode 100644 src/slider/demo/src/main.ts create mode 100644 src/slider/demo/test.ts create mode 100644 src/slider/demo/tsconfig.app.json create mode 100644 src/slider/demo/tsconfig.spec.json create mode 100644 src/slider/lib/index.ts create mode 100644 src/slider/lib/ng-package.json create mode 100644 src/slider/lib/package.json create mode 100644 src/slider/lib/project.json create mode 100644 src/slider/lib/src/TiSliderComponent.ts create mode 100644 src/slider/lib/src/TiSliderModule.ts create mode 100644 src/slider/lib/src/slider.html create mode 100644 src/slider/lib/src/slider.less create mode 100644 src/spinner/demo/karma.conf.js create mode 100644 src/spinner/demo/project.json create mode 100644 src/spinner/demo/src/app/AppComponent.ts create mode 100644 src/spinner/demo/src/app/AppModule.ts create mode 100644 src/spinner/demo/src/app/IndexComponent.ts create mode 100644 src/spinner/demo/src/app/app.html create mode 100644 src/spinner/demo/src/app/spinner/SpinnerBasicComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerBasicTestComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerCorrectableComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerDisabledComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerEventComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerFormatComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerIdComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerLoadComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerLocaleableComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerMaxMinComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerMaxlengthComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerStepComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerStepfnComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerTestModule.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerTipComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerTipTestComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerValidationComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerValidationTestComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/spinner-basic-test.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-basic.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-correctable.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-disabled.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-event.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-format.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-load.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-localeable.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-max-min.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-maxlength.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-step.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-stepfn.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-tip-test.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-tip.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-validation-test.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-validation.html create mode 100644 src/spinner/demo/src/app/spinner/spinnerTest.less create mode 100644 src/spinner/demo/src/app/spinner/webdoc/spinner-demos.js create mode 100644 src/spinner/demo/src/app/spinner/webdoc/spinner.cn.md create mode 100644 src/spinner/demo/src/app/spinner/webdoc/spinner.en.md create mode 100644 src/spinner/demo/src/favicon.ico create mode 100644 src/spinner/demo/src/index.html create mode 100644 src/spinner/demo/src/main.ts create mode 100644 src/spinner/demo/test.ts create mode 100644 src/spinner/demo/tsconfig.app.json create mode 100644 src/spinner/demo/tsconfig.spec.json create mode 100644 src/spinner/lib/index.ts create mode 100644 src/spinner/lib/ng-package.json create mode 100644 src/spinner/lib/package.json create mode 100644 src/spinner/lib/project.json create mode 100644 src/spinner/lib/src/TiSpinnerComponent.ts create mode 100644 src/spinner/lib/src/TiSpinnerModule.ts create mode 100644 src/spinner/lib/src/i18n/TiSpinnerWords.ts create mode 100644 src/spinner/lib/src/i18n/en_US.ts create mode 100644 src/spinner/lib/src/i18n/es_US.ts create mode 100644 src/spinner/lib/src/i18n/fr_FR.ts create mode 100644 src/spinner/lib/src/i18n/index.ts create mode 100644 src/spinner/lib/src/i18n/pt_BR.ts create mode 100644 src/spinner/lib/src/i18n/zh_CN.ts create mode 100644 src/spinner/lib/src/spinner.html create mode 100644 src/spinner/lib/src/spinner.less create mode 100644 src/steps/demo/karma.conf.js create mode 100644 src/steps/demo/project.json create mode 100644 src/steps/demo/src/app/AppComponent.ts create mode 100644 src/steps/demo/src/app/AppModule.ts create mode 100644 src/steps/demo/src/app/IndexComponent.ts create mode 100644 src/steps/demo/src/app/app.html create mode 100644 src/steps/demo/src/app/steps/StepsActiveComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsAdaptiveComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsAdaptiveTestComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsBaseComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsBeforeComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsClickableComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsEventsComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsLabelComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsMaxwidthComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsTemplateComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsTestModule.ts create mode 100644 src/steps/demo/src/app/steps/steps-active.html create mode 100644 src/steps/demo/src/app/steps/steps-adaptive-test.html create mode 100644 src/steps/demo/src/app/steps/steps-adaptive.html create mode 100644 src/steps/demo/src/app/steps/steps-base.html create mode 100644 src/steps/demo/src/app/steps/steps-before.html create mode 100644 src/steps/demo/src/app/steps/steps-clickable.html create mode 100644 src/steps/demo/src/app/steps/steps-events.html create mode 100644 src/steps/demo/src/app/steps/steps-label.html create mode 100644 src/steps/demo/src/app/steps/steps-maxwidth.html create mode 100644 src/steps/demo/src/app/steps/steps-template.html create mode 100644 src/steps/demo/src/app/steps/webdoc/steps-demos.js create mode 100644 src/steps/demo/src/app/steps/webdoc/steps.cn.md create mode 100644 src/steps/demo/src/app/steps/webdoc/steps.en.md create mode 100644 src/steps/demo/src/favicon.ico create mode 100644 src/steps/demo/src/index.html create mode 100644 src/steps/demo/src/main.ts create mode 100644 src/steps/demo/test.ts create mode 100644 src/steps/demo/tsconfig.app.json create mode 100644 src/steps/demo/tsconfig.spec.json create mode 100644 src/steps/lib/index.ts create mode 100644 src/steps/lib/ng-package.json create mode 100644 src/steps/lib/package.json create mode 100644 src/steps/lib/project.json create mode 100644 src/steps/lib/src/TiStepsComponent.ts create mode 100644 src/steps/lib/src/TiStepsModule.ts create mode 100644 src/steps/lib/src/steps.html create mode 100644 src/steps/lib/src/steps.less create mode 100644 src/styles.less create mode 100644 src/subtitle/demo/karma.conf.js create mode 100644 src/subtitle/demo/project.json create mode 100644 src/subtitle/demo/src/app/AppComponent.ts create mode 100644 src/subtitle/demo/src/app/AppModule.ts create mode 100644 src/subtitle/demo/src/app/IndexComponent.ts create mode 100644 src/subtitle/demo/src/app/app.html create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleBasicComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleBeforeSearchComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleDarkComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleEventComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleIdkeyComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleItemsComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleMaxwidthComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitlePanelwidthComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleRouteComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleScrollLoadComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleSearchableComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleTargetComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleTestModule.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleTipPositionComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-basic.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-before-search.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-dark.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-event.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-idkey.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-items.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-maxwidth.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-panelwidth.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-route.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-scroll-load.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-searchable.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-target.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-tip-position.html create mode 100644 src/subtitle/demo/src/app/subtitle/webdoc/subtitle-demos.js create mode 100644 src/subtitle/demo/src/app/subtitle/webdoc/subtitle.cn.md create mode 100644 src/subtitle/demo/src/app/subtitle/webdoc/subtitle.en.md create mode 100644 src/subtitle/demo/src/favicon.ico create mode 100644 src/subtitle/demo/src/index.html create mode 100644 src/subtitle/demo/src/main.ts create mode 100644 src/subtitle/demo/test.ts create mode 100644 src/subtitle/demo/tsconfig.app.json create mode 100644 src/subtitle/demo/tsconfig.spec.json create mode 100644 src/subtitle/lib/index.ts create mode 100644 src/subtitle/lib/ng-package.json create mode 100644 src/subtitle/lib/package.json create mode 100644 src/subtitle/lib/project.json create mode 100644 src/subtitle/lib/src/TiSubtitleComponent.ts create mode 100644 src/subtitle/lib/src/TiSubtitleModule.ts create mode 100644 src/subtitle/lib/src/subtitle.html create mode 100644 src/subtitle/lib/src/subtitle.less create mode 100644 src/swiper/demo/karma.conf.js create mode 100644 src/swiper/demo/project.json create mode 100644 src/swiper/demo/src/app/AppComponent.ts create mode 100644 src/swiper/demo/src/app/AppModule.ts create mode 100644 src/swiper/demo/src/app/IndexComponent.ts create mode 100644 src/swiper/demo/src/app/app.html create mode 100644 src/swiper/demo/src/app/swiper/SwiperActiveindexComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperAutoplayComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperBasicComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperEventsComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperIndicatorpositionComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperLoopComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperShowcardnumComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperShowcardnumTestComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperTestModule.ts create mode 100644 src/swiper/demo/src/app/swiper/swiper-activeindex.html create mode 100644 src/swiper/demo/src/app/swiper/swiper-autoplay.html create mode 100644 src/swiper/demo/src/app/swiper/swiper-basic.html create mode 100644 src/swiper/demo/src/app/swiper/swiper-events.html create mode 100644 src/swiper/demo/src/app/swiper/swiper-indicatorposition.html create mode 100644 src/swiper/demo/src/app/swiper/swiper-loop.html create mode 100644 src/swiper/demo/src/app/swiper/swiper-showcardnum-test.html create mode 100644 src/swiper/demo/src/app/swiper/swiper-showcardnum.html create mode 100644 src/swiper/demo/src/app/swiper/swiper.less create mode 100644 src/swiper/demo/src/app/swiper/webdoc/swiper-demos.js create mode 100644 src/swiper/demo/src/app/swiper/webdoc/swiper.cn.md create mode 100644 src/swiper/demo/src/app/swiper/webdoc/swiper.en.md create mode 100644 src/swiper/demo/src/favicon.ico create mode 100644 src/swiper/demo/src/index.html create mode 100644 src/swiper/demo/src/main.ts create mode 100644 src/swiper/demo/test.ts create mode 100644 src/swiper/demo/tsconfig.app.json create mode 100644 src/swiper/demo/tsconfig.spec.json create mode 100644 src/swiper/lib/index.ts create mode 100644 src/swiper/lib/ng-package.json create mode 100644 src/swiper/lib/package.json create mode 100644 src/swiper/lib/project.json create mode 100644 src/swiper/lib/src/TiSwiperComponent.ts create mode 100644 src/swiper/lib/src/TiSwiperModule.ts create mode 100644 src/swiper/lib/src/TiSwipercardComponent.ts create mode 100644 src/swiper/lib/src/swiper.html create mode 100644 src/swiper/lib/src/swiper.less create mode 100644 src/swiper/lib/src/swipercard.less create mode 100644 src/switch/demo/karma.conf.js create mode 100644 src/switch/demo/project.json create mode 100644 src/switch/demo/src/app/AppComponent.ts create mode 100644 src/switch/demo/src/app/AppModule.ts create mode 100644 src/switch/demo/src/app/IndexComponent.ts create mode 100644 src/switch/demo/src/app/app.html create mode 100644 src/switch/demo/src/app/switch/SwitchBasicComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchBeforeComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchDisabledComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchEventComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchExplanationComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchFocusComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchIdComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchLoadComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchTemplateComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchTestModule.ts create mode 100644 src/switch/demo/src/app/switch/switch-basic.html create mode 100644 src/switch/demo/src/app/switch/switch-before.html create mode 100644 src/switch/demo/src/app/switch/switch-disabled.html create mode 100644 src/switch/demo/src/app/switch/switch-event.html create mode 100644 src/switch/demo/src/app/switch/switch-explanation.html create mode 100644 src/switch/demo/src/app/switch/switch-focus.html create mode 100644 src/switch/demo/src/app/switch/switch-id.html create mode 100644 src/switch/demo/src/app/switch/switch-load.html create mode 100644 src/switch/demo/src/app/switch/switch-template.html create mode 100644 src/switch/demo/src/app/switch/webdoc/switch-demos.js create mode 100644 src/switch/demo/src/app/switch/webdoc/switch.cn.md create mode 100644 src/switch/demo/src/app/switch/webdoc/switch.en.md create mode 100644 src/switch/demo/src/favicon.ico create mode 100644 src/switch/demo/src/index.html create mode 100644 src/switch/demo/src/main.ts create mode 100644 src/switch/demo/test.ts create mode 100644 src/switch/demo/tsconfig.app.json create mode 100644 src/switch/demo/tsconfig.spec.json create mode 100644 src/switch/lib/index.ts create mode 100644 src/switch/lib/ng-package.json create mode 100644 src/switch/lib/package.json create mode 100644 src/switch/lib/project.json create mode 100644 src/switch/lib/src/TiSwitchComponent.ts create mode 100644 src/switch/lib/src/TiSwitchModule.ts create mode 100644 src/switch/lib/src/switch.html create mode 100644 src/switch/lib/src/switch.less create mode 100644 src/tab/demo/karma.conf.js create mode 100644 src/tab/demo/project.json create mode 100644 src/tab/demo/src/app/AppComponent.ts create mode 100644 src/tab/demo/src/app/AppModule.ts create mode 100644 src/tab/demo/src/app/IndexComponent.ts create mode 100644 src/tab/demo/src/app/app.html create mode 100644 src/tab/demo/src/app/tab/TabBasicComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabBeforeactivechangeComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabContentCompComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabCustomHeadComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabDarkComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabDefaultTestComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabLazyLoadComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabLevel2Component.ts create mode 100644 src/tab/demo/src/app/tab/TabLevel2TestComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabOverflowComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabRouteComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabScrollComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabSmallComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabTestModule.ts create mode 100644 src/tab/demo/src/app/tab/tab-basic.html create mode 100644 src/tab/demo/src/app/tab/tab-beforeactivechange.html create mode 100644 src/tab/demo/src/app/tab/tab-content-comp.html create mode 100644 src/tab/demo/src/app/tab/tab-custom-head.html create mode 100644 src/tab/demo/src/app/tab/tab-dark.html create mode 100644 src/tab/demo/src/app/tab/tab-default-test.html create mode 100644 src/tab/demo/src/app/tab/tab-lazy-load.html create mode 100644 src/tab/demo/src/app/tab/tab-level2-test.html create mode 100644 src/tab/demo/src/app/tab/tab-level2.html create mode 100644 src/tab/demo/src/app/tab/tab-overflow.html create mode 100644 src/tab/demo/src/app/tab/tab-route.html create mode 100644 src/tab/demo/src/app/tab/tab-scroll.html create mode 100644 src/tab/demo/src/app/tab/tab-small.html create mode 100644 src/tab/demo/src/app/tab/webdoc/tab-demos.js create mode 100644 src/tab/demo/src/app/tab/webdoc/tab.cn.md create mode 100644 src/tab/demo/src/app/tab/webdoc/tab.en.md create mode 100644 src/tab/demo/src/favicon.ico create mode 100644 src/tab/demo/src/index.html create mode 100644 src/tab/demo/src/main.ts create mode 100644 src/tab/demo/test.ts create mode 100644 src/tab/demo/tsconfig.app.json create mode 100644 src/tab/demo/tsconfig.spec.json create mode 100644 src/tab/lib/index.ts create mode 100644 src/tab/lib/ng-package.json create mode 100644 src/tab/lib/package.json create mode 100644 src/tab/lib/project.json create mode 100644 src/tab/lib/src/TiTabComponent.ts create mode 100644 src/tab/lib/src/TiTabHeaderComponent.ts create mode 100644 src/tab/lib/src/TiTabModule.ts create mode 100644 src/tab/lib/src/TiTabsComponent.ts create mode 100644 src/tab/lib/src/tab-head.html create mode 100644 src/tab/lib/src/tab.html create mode 100644 src/tab/lib/src/tab.less create mode 100644 src/tab/lib/src/tabs.html create mode 100644 src/tab/lib/src/tabs.less create mode 100644 src/table/demo/karma.conf.js create mode 100644 src/table/demo/project.json create mode 100644 src/table/demo/src/app/AppComponent.ts create mode 100644 src/table/demo/src/app/AppModule.ts create mode 100644 src/table/demo/src/app/IndexComponent.ts create mode 100644 src/table/demo/src/app/app.html create mode 100644 src/table/demo/src/app/table/TableActionmenuComponent.ts create mode 100644 src/table/demo/src/app/table/TableBasicComponent.ts create mode 100644 src/table/demo/src/app/table/TableBasicTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableCellTipComponent.ts create mode 100644 src/table/demo/src/app/table/TableCelliconsColsresizableComponent.ts create mode 100644 src/table/demo/src/app/table/TableCheckboxComponent.ts create mode 100644 src/table/demo/src/app/table/TableCheckboxPaginationComponent.ts create mode 100644 src/table/demo/src/app/table/TableCheckboxPaginationHeadmenuComponent.ts create mode 100644 src/table/demo/src/app/table/TableColAlignComponent.ts create mode 100644 src/table/demo/src/app/table/TableColalignSortResizableTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsResizableComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsToggleComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsToggleDetailsComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsToggleTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsresizableBasicComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsresizableColstoggleComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsresizableColstoggleFixedheadComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsresizableLoadfailComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsresizableSortComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsresizableSortHeadfilterComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnFixedComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedCheckboxComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedColstoggleComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedEditrowComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedFixedheadColsresizablePaginationComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedHeadfixedComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedLeftmenuComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedNodataComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedPaginationComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedResizableComponent.ts create mode 100644 src/table/demo/src/app/table/TableComprehensiveComponent.ts create mode 100644 src/table/demo/src/app/table/TableDetailsCloseotherdetailsComponent.ts create mode 100644 src/table/demo/src/app/table/TableDetailsComponent.ts create mode 100644 src/table/demo/src/app/table/TableDetailsNesttableComponent.ts create mode 100644 src/table/demo/src/app/table/TableDetailsPaginationComponent.ts create mode 100644 src/table/demo/src/app/table/TableDynamicDetailsComponent.ts create mode 100644 src/table/demo/src/app/table/TableEditallComponent.ts create mode 100644 src/table/demo/src/app/table/TableEditallTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableEditrowComponent.ts create mode 100644 src/table/demo/src/app/table/TableEditrowTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableFilterComponent.ts create mode 100644 src/table/demo/src/app/table/TableFilterStrictComponent.ts create mode 100644 src/table/demo/src/app/table/TableFixedHeadColsResizableComponent.ts create mode 100644 src/table/demo/src/app/table/TableFixedHeadComponent.ts create mode 100644 src/table/demo/src/app/table/TableFixedHeadInAccordionComponent.ts create mode 100644 src/table/demo/src/app/table/TableFixedHeadNodataComponent.ts create mode 100644 src/table/demo/src/app/table/TableFixedHeadPaginationDetailsComponent.ts create mode 100644 src/table/demo/src/app/table/TableFixedheadColsresizablePaginationDetailsComponent.ts create mode 100644 src/table/demo/src/app/table/TableFixheadScrollComponent.ts create mode 100644 src/table/demo/src/app/table/TableGroupComponent.ts create mode 100644 src/table/demo/src/app/table/TableGuideComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterDatetimeComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterDatetimeTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterMultiComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterMultiValuekeyComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterValuekeyComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterVirtualscrollComponent.ts create mode 100644 src/table/demo/src/app/table/TableLoadFailComponent.ts create mode 100644 src/table/demo/src/app/table/TableNodataComponent.ts create mode 100644 src/table/demo/src/app/table/TableNodataSimpleComponent.ts create mode 100644 src/table/demo/src/app/table/TableNodataTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableOverflowLinkComponent.ts create mode 100644 src/table/demo/src/app/table/TablePagiWithFilterComponent.ts create mode 100644 src/table/demo/src/app/table/TablePaginationComponent.ts create mode 100644 src/table/demo/src/app/table/TableRadioComponent.ts create mode 100644 src/table/demo/src/app/table/TableRadioTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableRowDrag2Component.ts create mode 100644 src/table/demo/src/app/table/TableRowspanComponent.ts create mode 100644 src/table/demo/src/app/table/TableSearchComponent.ts create mode 100644 src/table/demo/src/app/table/TableServerPagiComponent.ts create mode 100644 src/table/demo/src/app/table/TableServerPagiSearchSortComponent.ts create mode 100644 src/table/demo/src/app/table/TableSmallComponent.ts create mode 100644 src/table/demo/src/app/table/TableSoldoutComponent.ts create mode 100644 src/table/demo/src/app/table/TableSortBasicComponent.ts create mode 100644 src/table/demo/src/app/table/TableSortComparefnComponent.ts create mode 100644 src/table/demo/src/app/table/TableSortComparefnLocaleComponent.ts create mode 100644 src/table/demo/src/app/table/TableSortComponent.ts create mode 100644 src/table/demo/src/app/table/TableSortDetailsComponent.ts create mode 100644 src/table/demo/src/app/table/TableSortResetComponent.ts create mode 100644 src/table/demo/src/app/table/TableSortTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableStorageComponent.ts create mode 100644 src/table/demo/src/app/table/TableStorageConfigComponent.ts create mode 100644 src/table/demo/src/app/table/TableStorageFilterComponent.ts create mode 100644 src/table/demo/src/app/table/TableStorageServeComponent.ts create mode 100644 src/table/demo/src/app/table/TableTestModule.ts create mode 100644 src/table/demo/src/app/table/TableTreeComponent.ts create mode 100644 src/table/demo/src/app/table/TableTreeMulitiselectComponent.ts create mode 100644 src/table/demo/src/app/table/TableTreeUnknowdeepthComponent.ts create mode 100644 src/table/demo/src/app/table/TableVirtualscrollBasicComponent.ts create mode 100644 src/table/demo/src/app/table/TableVirtualscrollComponent.ts create mode 100644 src/table/demo/src/app/table/TableVirtualscrollComprehensiveComponent.ts create mode 100644 src/table/demo/src/app/table/TableVirtualscrollSizesComponent.ts create mode 100644 src/table/demo/src/app/table/TableVirtualscrollTreeComponent.ts create mode 100644 src/table/demo/src/app/table/table-actionmenu.html create mode 100644 src/table/demo/src/app/table/table-basic-test.html create mode 100644 src/table/demo/src/app/table/table-basic.html create mode 100644 src/table/demo/src/app/table/table-cell-tip.html create mode 100644 src/table/demo/src/app/table/table-cellicons-colsresizable.html create mode 100644 src/table/demo/src/app/table/table-checkbox-pagination-headmenu.html create mode 100644 src/table/demo/src/app/table/table-checkbox-pagination.html create mode 100644 src/table/demo/src/app/table/table-checkbox.html create mode 100644 src/table/demo/src/app/table/table-col-align.html create mode 100644 src/table/demo/src/app/table/table-colalign-sort-resizable-test.html create mode 100644 src/table/demo/src/app/table/table-cols-resizable.html create mode 100644 src/table/demo/src/app/table/table-cols-toggle-details.html create mode 100644 src/table/demo/src/app/table/table-cols-toggle-test.html create mode 100644 src/table/demo/src/app/table/table-cols-toggle.html create mode 100644 src/table/demo/src/app/table/table-colsresizable-basic.html create mode 100644 src/table/demo/src/app/table/table-colsresizable-colstoggle-fixedhead.html create mode 100644 src/table/demo/src/app/table/table-colsresizable-colstoggle.html create mode 100644 src/table/demo/src/app/table/table-colsresizable-loadfail.html create mode 100644 src/table/demo/src/app/table/table-colsresizable-sort-headfilter.html create mode 100644 src/table/demo/src/app/table/table-colsresizable-sort.html create mode 100644 src/table/demo/src/app/table/table-column-fixed.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-checkbox.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-colstoggle.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-editrow.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-fixedhead-colsresizable-pagination.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-headfixed.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-leftmenu.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-nodata.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-pagination.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-resizable.html create mode 100644 src/table/demo/src/app/table/table-comprehensive.html create mode 100644 src/table/demo/src/app/table/table-details-closeotherdetails.html create mode 100644 src/table/demo/src/app/table/table-details-nesttable.html create mode 100644 src/table/demo/src/app/table/table-details-pagination.html create mode 100644 src/table/demo/src/app/table/table-details.html create mode 100644 src/table/demo/src/app/table/table-dynamic-details.html create mode 100644 src/table/demo/src/app/table/table-editall-test.html create mode 100644 src/table/demo/src/app/table/table-editall.html create mode 100644 src/table/demo/src/app/table/table-editrow-test.html create mode 100644 src/table/demo/src/app/table/table-editrow.html create mode 100644 src/table/demo/src/app/table/table-filter-strict.html create mode 100644 src/table/demo/src/app/table/table-filter.html create mode 100644 src/table/demo/src/app/table/table-fixed-head-cols-resizable.html create mode 100644 src/table/demo/src/app/table/table-fixed-head-in-accordion.html create mode 100644 src/table/demo/src/app/table/table-fixed-head-nodata.html create mode 100644 src/table/demo/src/app/table/table-fixed-head-pagination-details.html create mode 100644 src/table/demo/src/app/table/table-fixed-head.html create mode 100644 src/table/demo/src/app/table/table-fixedhead-colsresizable-pagination-details.html create mode 100644 src/table/demo/src/app/table/table-fixhead-scroll.html create mode 100644 src/table/demo/src/app/table/table-group.html create mode 100644 src/table/demo/src/app/table/table-guide.html create mode 100644 src/table/demo/src/app/table/table-head-filter-datetime-test.html create mode 100644 src/table/demo/src/app/table/table-head-filter-datetime.html create mode 100644 src/table/demo/src/app/table/table-head-filter-multi-valuekey.html create mode 100644 src/table/demo/src/app/table/table-head-filter-multi.html create mode 100644 src/table/demo/src/app/table/table-head-filter-test.html create mode 100644 src/table/demo/src/app/table/table-head-filter-valuekey.html create mode 100644 src/table/demo/src/app/table/table-head-filter-virtualscroll.html create mode 100644 src/table/demo/src/app/table/table-head-filter.html create mode 100644 src/table/demo/src/app/table/table-load-fail.html create mode 100644 src/table/demo/src/app/table/table-nodata-simple.html create mode 100644 src/table/demo/src/app/table/table-nodata-test.html create mode 100644 src/table/demo/src/app/table/table-nodata.html create mode 100644 src/table/demo/src/app/table/table-overflow-link.html create mode 100644 src/table/demo/src/app/table/table-pagi-with-filter.html create mode 100644 src/table/demo/src/app/table/table-pagination.html create mode 100644 src/table/demo/src/app/table/table-radio-test.html create mode 100644 src/table/demo/src/app/table/table-radio.html create mode 100644 src/table/demo/src/app/table/table-row-drag2.html create mode 100644 src/table/demo/src/app/table/table-rowspan.html create mode 100644 src/table/demo/src/app/table/table-search.html create mode 100644 src/table/demo/src/app/table/table-server-pagi-search-sort.html create mode 100644 src/table/demo/src/app/table/table-server-pagi.html create mode 100644 src/table/demo/src/app/table/table-small.html create mode 100644 src/table/demo/src/app/table/table-soldout.html create mode 100644 src/table/demo/src/app/table/table-sort-basic.html create mode 100644 src/table/demo/src/app/table/table-sort-comparefn-locale.html create mode 100644 src/table/demo/src/app/table/table-sort-comparefn.html create mode 100644 src/table/demo/src/app/table/table-sort-details.html create mode 100644 src/table/demo/src/app/table/table-sort-reset.html create mode 100644 src/table/demo/src/app/table/table-sort-test.html create mode 100644 src/table/demo/src/app/table/table-sort.html create mode 100644 src/table/demo/src/app/table/table-storage-config.html create mode 100644 src/table/demo/src/app/table/table-storage-filter.html create mode 100644 src/table/demo/src/app/table/table-storage-serve.html create mode 100644 src/table/demo/src/app/table/table-storage.html create mode 100644 src/table/demo/src/app/table/table-tree-mulitiselect.html create mode 100644 src/table/demo/src/app/table/table-tree-unknowdeepth.html create mode 100644 src/table/demo/src/app/table/table-tree.html create mode 100644 src/table/demo/src/app/table/table-virtualscroll-basic.html create mode 100644 src/table/demo/src/app/table/table-virtualscroll-comprehensive.html create mode 100644 src/table/demo/src/app/table/table-virtualscroll-sizes.html create mode 100644 src/table/demo/src/app/table/table-virtualscroll-tree.html create mode 100644 src/table/demo/src/app/table/table-virtualscroll.html create mode 100644 src/table/demo/src/app/table/tableTest.less create mode 100644 src/table/demo/src/app/table/webdoc/table-demos.js create mode 100644 src/table/demo/src/app/table/webdoc/table.cn.md create mode 100644 src/table/demo/src/app/table/webdoc/table.en.md create mode 100644 src/table/demo/src/favicon.ico create mode 100644 src/table/demo/src/index.html create mode 100644 src/table/demo/src/main.ts create mode 100644 src/table/demo/test.ts create mode 100644 src/table/demo/tsconfig.app.json create mode 100644 src/table/demo/tsconfig.spec.json create mode 100644 src/table/lib/index.ts create mode 100644 src/table/lib/ng-package.json create mode 100644 src/table/lib/package.json create mode 100644 src/table/lib/project.json create mode 100644 src/table/lib/src/TiCellIconsComponent.ts create mode 100644 src/table/lib/src/TiCellTextComponent.ts create mode 100644 src/table/lib/src/TiColClickDirective.ts create mode 100644 src/table/lib/src/TiColsResizableDirective.ts create mode 100644 src/table/lib/src/TiColsToggleComponent.ts create mode 100644 src/table/lib/src/TiColsToggleDropComponent.ts create mode 100644 src/table/lib/src/TiColspanDirective.ts create mode 100644 src/table/lib/src/TiColumnFixedDirective.ts create mode 100644 src/table/lib/src/TiColumnsPipe.ts create mode 100644 src/table/lib/src/TiDetailsIconComponent.ts create mode 100644 src/table/lib/src/TiDetailsTrDirective.ts create mode 100644 src/table/lib/src/TiHeadFilterComponent.ts create mode 100644 src/table/lib/src/TiHeadFilterDropComponent.ts create mode 100644 src/table/lib/src/TiHeadMenuComponent.ts create mode 100644 src/table/lib/src/TiHeadSortComponent.ts create mode 100644 src/table/lib/src/TiTableComponent.ts create mode 100644 src/table/lib/src/TiTableFixedHeadService.ts create mode 100644 src/table/lib/src/TiTableFixedHeadServiceModule.ts create mode 100644 src/table/lib/src/TiTableFixedSizeVirtualScrollDirective.ts create mode 100644 src/table/lib/src/TiTableModule.ts create mode 100644 src/table/lib/src/TiTableVirtualScrollViewportComponent.ts create mode 100644 src/table/lib/src/cell-icons.html create mode 100644 src/table/lib/src/cell-text.html create mode 100644 src/table/lib/src/cols-toggle-drop.html create mode 100644 src/table/lib/src/cols-toggle.html create mode 100644 src/table/lib/src/details-icon.html create mode 100644 src/table/lib/src/head-filter-drop.html create mode 100644 src/table/lib/src/head-filter-drop.less create mode 100644 src/table/lib/src/head-filter.html create mode 100644 src/table/lib/src/head-filter.less create mode 100644 src/table/lib/src/head-menu.html create mode 100644 src/table/lib/src/head-menu.less create mode 100644 src/table/lib/src/head-sort.html create mode 100644 src/table/lib/src/i18n/TiTableWords.ts create mode 100644 src/table/lib/src/i18n/en_US.ts create mode 100644 src/table/lib/src/i18n/es_US.ts create mode 100644 src/table/lib/src/i18n/fr_FR.ts create mode 100644 src/table/lib/src/i18n/index.ts create mode 100644 src/table/lib/src/i18n/pt_BR.ts create mode 100644 src/table/lib/src/i18n/zh_CN.ts create mode 100644 src/table/lib/src/table-nodata-small-nest-resize.less create mode 100644 src/table/lib/src/table-toggle-sort-details.less create mode 100644 src/table/lib/src/table-tree-fix.less create mode 100644 src/table/lib/src/table-virtual-scroll-viewport.html create mode 100644 src/table/lib/src/table-virtual-scroll-viewport.less create mode 100644 src/table/lib/src/table.html create mode 100644 src/table/lib/src/table.less create mode 100644 src/tag/demo/karma.conf.js create mode 100644 src/tag/demo/project.json create mode 100644 src/tag/demo/src/app/AppComponent.ts create mode 100644 src/tag/demo/src/app/AppModule.ts create mode 100644 src/tag/demo/src/app/IndexComponent.ts create mode 100644 src/tag/demo/src/app/app.html create mode 100644 src/tag/demo/src/app/tag/TagBasicComponent.ts create mode 100644 src/tag/demo/src/app/tag/TagDefaultComponent.ts create mode 100644 src/tag/demo/src/app/tag/TagDisabledComponent.ts create mode 100644 src/tag/demo/src/app/tag/TagEditComponent.ts create mode 100644 src/tag/demo/src/app/tag/TagTestModule.ts create mode 100644 src/tag/demo/src/app/tag/tag-basic.html create mode 100644 src/tag/demo/src/app/tag/tag-default.html create mode 100644 src/tag/demo/src/app/tag/tag-disabled.html create mode 100644 src/tag/demo/src/app/tag/tag-edit.html create mode 100644 src/tag/demo/src/app/tag/webdoc/tag-demos.js create mode 100644 src/tag/demo/src/app/tag/webdoc/tag.cn.md create mode 100644 src/tag/demo/src/app/tag/webdoc/tag.en.md create mode 100644 src/tag/demo/src/favicon.ico create mode 100644 src/tag/demo/src/index.html create mode 100644 src/tag/demo/src/main.ts create mode 100644 src/tag/demo/test.ts create mode 100644 src/tag/demo/tsconfig.app.json create mode 100644 src/tag/demo/tsconfig.spec.json create mode 100644 src/tag/lib/index.ts create mode 100644 src/tag/lib/ng-package.json create mode 100644 src/tag/lib/package.json create mode 100644 src/tag/lib/project.json create mode 100644 src/tag/lib/src/TiTagComponent.ts create mode 100644 src/tag/lib/src/TiTagModule.ts create mode 100644 src/tag/lib/src/tag-arrow.less create mode 100644 src/tag/lib/src/tag-rect.less create mode 100644 src/tag/lib/src/tag.html create mode 100644 src/tagsinput/demo/karma.conf.js create mode 100644 src/tagsinput/demo/project.json create mode 100644 src/tagsinput/demo/src/app/AppComponent.ts create mode 100644 src/tagsinput/demo/src/app/AppModule.ts create mode 100644 src/tagsinput/demo/src/app/IndexComponent.ts create mode 100644 src/tagsinput/demo/src/app/app.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsInputTestModule.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputBasicComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputDisabledComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputEventsComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputLabelkeyComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputMaxlengthComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputNullComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputPanelwidthComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputReactiveComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputSeparatorsComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputSuggestionComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputTemplateComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputValidComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputValuekeyComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-basic.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-disabled.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-events.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-labelkey.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-maxlength.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-null.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-panelwidth.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-reactive.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-separators.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-suggestion.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-template.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-valid.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-valuekey.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput-demos.js create mode 100644 src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.cn.md create mode 100644 src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.en.md create mode 100644 src/tagsinput/demo/src/favicon.ico create mode 100644 src/tagsinput/demo/src/index.html create mode 100644 src/tagsinput/demo/src/main.ts create mode 100644 src/tagsinput/demo/test.ts create mode 100644 src/tagsinput/demo/tsconfig.app.json create mode 100644 src/tagsinput/demo/tsconfig.spec.json create mode 100644 src/tagsinput/lib/index.ts create mode 100644 src/tagsinput/lib/ng-package.json create mode 100644 src/tagsinput/lib/package.json create mode 100644 src/tagsinput/lib/project.json create mode 100644 src/tagsinput/lib/src/TiTagsInputComponent.ts create mode 100644 src/tagsinput/lib/src/TiTagsInputModule.ts create mode 100644 src/tagsinput/lib/src/tagsinput.html create mode 100644 src/tagsinput/lib/src/tagsinput.less create mode 100644 src/text/demo/karma.conf.js create mode 100644 src/text/demo/project.json create mode 100644 src/text/demo/src/app/AppComponent.ts create mode 100644 src/text/demo/src/app/AppModule.ts create mode 100644 src/text/demo/src/app/IndexComponent.ts create mode 100644 src/text/demo/src/app/app.html create mode 100644 src/text/demo/src/app/text/TextBasicComponent.ts create mode 100644 src/text/demo/src/app/text/TextClearComponent.ts create mode 100644 src/text/demo/src/app/text/TextDisabledComponent.ts create mode 100644 src/text/demo/src/app/text/TextEventsComponent.ts create mode 100644 src/text/demo/src/app/text/TextFocusComponent.ts create mode 100644 src/text/demo/src/app/text/TextMaskinputComponent.ts create mode 100644 src/text/demo/src/app/text/TextNoborderTestComponent.ts create mode 100644 src/text/demo/src/app/text/TextPasswordComponent.ts create mode 100644 src/text/demo/src/app/text/TextPasswordVisibleComponent.ts create mode 100644 src/text/demo/src/app/text/TextReactiveComponent.ts create mode 100644 src/text/demo/src/app/text/TextReadonlyComponent.ts create mode 100644 src/text/demo/src/app/text/TextTestModule.ts create mode 100644 src/text/demo/src/app/text/text-basic.html create mode 100644 src/text/demo/src/app/text/text-clear.html create mode 100644 src/text/demo/src/app/text/text-disabled.html create mode 100644 src/text/demo/src/app/text/text-events.html create mode 100644 src/text/demo/src/app/text/text-focus.html create mode 100644 src/text/demo/src/app/text/text-maskinput.html create mode 100644 src/text/demo/src/app/text/text-noborder-test.html create mode 100644 src/text/demo/src/app/text/text-password-visible.html create mode 100644 src/text/demo/src/app/text/text-password.html create mode 100644 src/text/demo/src/app/text/text-reactive.html create mode 100644 src/text/demo/src/app/text/text-readonly.html create mode 100644 src/text/demo/src/app/text/text.spec.ts create mode 100644 src/text/demo/src/app/text/webdoc/text-demos.js create mode 100644 src/text/demo/src/app/text/webdoc/text.cn.md create mode 100644 src/text/demo/src/app/text/webdoc/text.en.md create mode 100644 src/text/demo/src/favicon.ico create mode 100644 src/text/demo/src/index.html create mode 100644 src/text/demo/src/main.ts create mode 100644 src/text/demo/test.ts create mode 100644 src/text/demo/tsconfig.app.json create mode 100644 src/text/demo/tsconfig.spec.json create mode 100644 src/text/lib/index.ts create mode 100644 src/text/lib/ng-package.json create mode 100644 src/text/lib/package.json create mode 100644 src/text/lib/project.json create mode 100644 src/text/lib/src/TiMaskDirective.ts create mode 100644 src/text/lib/src/TiTextComponent.ts create mode 100644 src/text/lib/src/TiTextModule.ts create mode 100644 src/text/lib/src/clear.svg create mode 100644 src/text/lib/src/invisible.svg create mode 100644 src/text/lib/src/text.less create mode 100644 src/text/lib/src/visible.svg create mode 100644 src/textarea/demo/karma.conf.js create mode 100644 src/textarea/demo/project.json create mode 100644 src/textarea/demo/src/app/AppComponent.ts create mode 100644 src/textarea/demo/src/app/AppModule.ts create mode 100644 src/textarea/demo/src/app/IndexComponent.ts create mode 100644 src/textarea/demo/src/app/app.html create mode 100644 src/textarea/demo/src/app/textarea/TextareaAutofocusComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaDisabledComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaMaxlengthComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaNoneComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaResizeComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaScrollComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaTestModule.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaValidComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaWidthComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/textarea-autofocus.html create mode 100644 src/textarea/demo/src/app/textarea/textarea-disabled.html create mode 100644 src/textarea/demo/src/app/textarea/textarea-maxlength.html create mode 100644 src/textarea/demo/src/app/textarea/textarea-none.html create mode 100644 src/textarea/demo/src/app/textarea/textarea-resize.html create mode 100644 src/textarea/demo/src/app/textarea/textarea-scroll.html create mode 100644 src/textarea/demo/src/app/textarea/textarea-valid.html create mode 100644 src/textarea/demo/src/app/textarea/textarea-width.html create mode 100644 src/textarea/demo/src/app/textarea/webdoc/textarea-demos.js create mode 100644 src/textarea/demo/src/app/textarea/webdoc/textarea.cn.md create mode 100644 src/textarea/demo/src/app/textarea/webdoc/textarea.en.md create mode 100644 src/textarea/demo/src/favicon.ico create mode 100644 src/textarea/demo/src/index.html create mode 100644 src/textarea/demo/src/main.ts create mode 100644 src/textarea/demo/test.ts create mode 100644 src/textarea/demo/tsconfig.app.json create mode 100644 src/textarea/demo/tsconfig.spec.json create mode 100644 src/textarea/lib/index.ts create mode 100644 src/textarea/lib/ng-package.json create mode 100644 src/textarea/lib/package.json create mode 100644 src/textarea/lib/project.json create mode 100644 src/textarea/lib/src/TiFormatNumPipe.ts create mode 100644 src/textarea/lib/src/TiTextareaComponent.ts create mode 100644 src/textarea/lib/src/TiTextareaModule.ts create mode 100644 src/textarea/lib/src/textarea.html create mode 100644 src/textarea/lib/src/textarea.less create mode 100644 src/themes/README.md create mode 100644 src/themes/basic/base-all.less create mode 100644 src/themes/basic/basic-var.css create mode 100644 src/themes/basic/build.less create mode 100644 src/themes/basic/common.less create mode 100644 src/themes/basic/compnent-container-border.less create mode 100644 src/themes/basic/img/table-loadfail-bg.png create mode 100644 src/themes/basic/img/table-nodata-bg.png create mode 100644 src/themes/basic/img/upload-image-delete.png create mode 100644 src/themes/basic/img/upload-image-error.png create mode 100644 src/themes/basic/img/upload-image-preview.png create mode 100644 src/themes/basic/link-no-decoration.less create mode 100644 src/themes/basic/mixins.less create mode 100644 src/themes/basic/normalize.less create mode 100644 src/themes/package.json create mode 100644 src/themes/project.json create mode 100644 src/themes/theme-blue/basic-var.less create mode 100644 src/themes/theme-blue/build.less create mode 100644 src/themes/theme-default/build.less create mode 100644 src/themes/theme-green/basic-var.less create mode 100644 src/themes/theme-green/build.less create mode 100644 src/themes/theme-purple/basic-var.less create mode 100644 src/themes/theme-purple/build.less create mode 100644 src/themes/theme-red/basic-var.less create mode 100644 src/themes/theme-red/build.less create mode 100644 src/time/demo/karma.conf.js create mode 100644 src/time/demo/project.json create mode 100644 src/time/demo/src/app/AppComponent.ts create mode 100644 src/time/demo/src/app/AppModule.ts create mode 100644 src/time/demo/src/app/IndexComponent.ts create mode 100644 src/time/demo/src/app/app.html create mode 100644 src/time/demo/src/app/time/TimeCleariconComponent.ts create mode 100644 src/time/demo/src/app/time/TimeDisabledComponent.ts create mode 100644 src/time/demo/src/app/time/TimeEventComponent.ts create mode 100644 src/time/demo/src/app/time/TimeFormatComponent.ts create mode 100644 src/time/demo/src/app/time/TimeMaxComponent.ts create mode 100644 src/time/demo/src/app/time/TimeMaxminComponent.ts create mode 100644 src/time/demo/src/app/time/TimeMinComponent.ts create mode 100644 src/time/demo/src/app/time/TimeOptionDisabledComponent.ts create mode 100644 src/time/demo/src/app/time/TimePanelalignComponent.ts create mode 100644 src/time/demo/src/app/time/TimeReactiveComponent.ts create mode 100644 src/time/demo/src/app/time/TimeTestModule.ts create mode 100644 src/time/demo/src/app/time/TimeValidationComponent.ts create mode 100644 src/time/demo/src/app/time/time-clearicon.html create mode 100644 src/time/demo/src/app/time/time-disabled.html create mode 100644 src/time/demo/src/app/time/time-event.html create mode 100644 src/time/demo/src/app/time/time-format.html create mode 100644 src/time/demo/src/app/time/time-max.html create mode 100644 src/time/demo/src/app/time/time-maxmin.html create mode 100644 src/time/demo/src/app/time/time-min.html create mode 100644 src/time/demo/src/app/time/time-option-disabled.html create mode 100644 src/time/demo/src/app/time/time-panelalign.html create mode 100644 src/time/demo/src/app/time/time-reactive.html create mode 100644 src/time/demo/src/app/time/time-validation.html create mode 100644 src/time/demo/src/app/time/webdoc/time-demos.js create mode 100644 src/time/demo/src/app/time/webdoc/time.cn.md create mode 100644 src/time/demo/src/app/time/webdoc/time.en.md create mode 100644 src/time/demo/src/favicon.ico create mode 100644 src/time/demo/src/index.html create mode 100644 src/time/demo/src/main.ts create mode 100644 src/time/demo/test.ts create mode 100644 src/time/demo/tsconfig.app.json create mode 100644 src/time/demo/tsconfig.spec.json create mode 100644 src/time/lib/index.ts create mode 100644 src/time/lib/ng-package.json create mode 100644 src/time/lib/package.json create mode 100644 src/time/lib/project.json create mode 100644 src/time/lib/src/TiTimeComponent.ts create mode 100644 src/time/lib/src/TiTimeModule.ts create mode 100644 src/time/lib/src/i18n/TiTimeWords.ts create mode 100644 src/time/lib/src/i18n/en_US.ts create mode 100644 src/time/lib/src/i18n/es_US.ts create mode 100644 src/time/lib/src/i18n/fr_FR.ts create mode 100644 src/time/lib/src/i18n/index.ts create mode 100644 src/time/lib/src/i18n/pt_BR.ts create mode 100644 src/time/lib/src/i18n/zh_CN.ts create mode 100644 src/time/lib/src/time.html create mode 100644 src/time/lib/src/time.less create mode 100644 src/timeline/demo/karma.conf.js create mode 100644 src/timeline/demo/project.json create mode 100644 src/timeline/demo/src/app/AppComponent.ts create mode 100644 src/timeline/demo/src/app/AppModule.ts create mode 100644 src/timeline/demo/src/app/IndexComponent.ts create mode 100644 src/timeline/demo/src/app/app.html create mode 100644 src/timeline/demo/src/app/timeline/TimelineBasicComponent.ts create mode 100644 src/timeline/demo/src/app/timeline/TimelineDarkComponent.ts create mode 100644 src/timeline/demo/src/app/timeline/TimelineHelptipComponent.ts create mode 100644 src/timeline/demo/src/app/timeline/TimelineMultiComponent.ts create mode 100644 src/timeline/demo/src/app/timeline/TimelineTempleteComponent.ts create mode 100644 src/timeline/demo/src/app/timeline/TimelineTestComponent.ts create mode 100644 src/timeline/demo/src/app/timeline/TimelineTestModule.ts create mode 100644 src/timeline/demo/src/app/timeline/TimelineTypeComponent.ts create mode 100644 src/timeline/demo/src/app/timeline/timeline-basic.html create mode 100644 src/timeline/demo/src/app/timeline/timeline-dark.html create mode 100644 src/timeline/demo/src/app/timeline/timeline-helptip.html create mode 100644 src/timeline/demo/src/app/timeline/timeline-multi.html create mode 100644 src/timeline/demo/src/app/timeline/timeline-templete.html create mode 100644 src/timeline/demo/src/app/timeline/timeline-test.html create mode 100644 src/timeline/demo/src/app/timeline/timeline-type.html create mode 100644 src/timeline/demo/src/app/timeline/webdoc/timeline-demos.js create mode 100644 src/timeline/demo/src/app/timeline/webdoc/timeline.cn.md create mode 100644 src/timeline/demo/src/app/timeline/webdoc/timeline.en.md create mode 100644 src/timeline/demo/src/favicon.ico create mode 100644 src/timeline/demo/src/index.html create mode 100644 src/timeline/demo/src/main.ts create mode 100644 src/timeline/demo/test.ts create mode 100644 src/timeline/demo/tsconfig.app.json create mode 100644 src/timeline/demo/tsconfig.spec.json create mode 100644 src/timeline/lib/index.ts create mode 100644 src/timeline/lib/ng-package.json create mode 100644 src/timeline/lib/package.json create mode 100644 src/timeline/lib/project.json create mode 100644 src/timeline/lib/src/TiTimelineComponent.ts create mode 100644 src/timeline/lib/src/TiTimelineModule.ts create mode 100644 src/timeline/lib/src/timeline.html create mode 100644 src/timeline/lib/src/timeline.less create mode 100644 src/tip/demo/karma.conf.js create mode 100644 src/tip/demo/project.json create mode 100644 src/tip/demo/src/app/AppComponent.ts create mode 100644 src/tip/demo/src/app/AppModule.ts create mode 100644 src/tip/demo/src/app/IndexComponent.ts create mode 100644 src/tip/demo/src/app/app.html create mode 100644 src/tip/demo/src/app/tip/TipBasicComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipContentCompComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipContentTemplateComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipEmptyComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipHasArrowComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipLongTextPositionComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipMaxWidthComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipPositionComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipPositionTestComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipServiceComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipServiceDestroyComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipTestModule.ts create mode 100644 src/tip/demo/src/app/tip/TipTriggerComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipValidPositionTestComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipZindexComponent.ts create mode 100644 src/tip/demo/src/app/tip/tip-basic.html create mode 100644 src/tip/demo/src/app/tip/tip-content-comp.html create mode 100644 src/tip/demo/src/app/tip/tip-content-template.html create mode 100644 src/tip/demo/src/app/tip/tip-empty.html create mode 100644 src/tip/demo/src/app/tip/tip-has-arrow.html create mode 100644 src/tip/demo/src/app/tip/tip-long-text-position.html create mode 100644 src/tip/demo/src/app/tip/tip-max-width.html create mode 100644 src/tip/demo/src/app/tip/tip-position-test.html create mode 100644 src/tip/demo/src/app/tip/tip-position.html create mode 100644 src/tip/demo/src/app/tip/tip-service-destroy.html create mode 100644 src/tip/demo/src/app/tip/tip-service.html create mode 100644 src/tip/demo/src/app/tip/tip-trigger.html create mode 100644 src/tip/demo/src/app/tip/tip-valid-position-test.html create mode 100644 src/tip/demo/src/app/tip/tip-zindex.html create mode 100644 src/tip/demo/src/app/tip/tipTest.less create mode 100644 src/tip/demo/src/app/tip/webdoc/tip-demos.js create mode 100644 src/tip/demo/src/app/tip/webdoc/tip.cn.md create mode 100644 src/tip/demo/src/app/tip/webdoc/tip.en.md create mode 100644 src/tip/demo/src/favicon.ico create mode 100644 src/tip/demo/src/index.html create mode 100644 src/tip/demo/src/main.ts create mode 100644 src/tip/demo/test.ts create mode 100644 src/tip/demo/tsconfig.app.json create mode 100644 src/tip/demo/tsconfig.spec.json create mode 100644 src/tip/lib/index.ts create mode 100644 src/tip/lib/ng-package.json create mode 100644 src/tip/lib/package.json create mode 100644 src/tip/lib/project.json create mode 100644 src/tip/lib/src/TiTipContainerComponent.ts create mode 100644 src/tip/lib/src/TiTipDirective.ts create mode 100644 src/tip/lib/src/TiTipInterface.ts create mode 100644 src/tip/lib/src/TiTipModule.ts create mode 100644 src/tip/lib/src/TiTipService.ts create mode 100644 src/tip/lib/src/TiTipServiceModule.ts create mode 100644 src/tip/lib/src/tip.less create mode 100644 src/transfer/demo/karma.conf.js create mode 100644 src/transfer/demo/project.json create mode 100644 src/transfer/demo/src/app/AppComponent.ts create mode 100644 src/transfer/demo/src/app/AppModule.ts create mode 100644 src/transfer/demo/src/app/IndexComponent.ts create mode 100644 src/transfer/demo/src/app/app.html create mode 100644 src/transfer/demo/src/app/transfer/TransferBasicComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferDisabledComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferEventComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferIdComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferIdkeyComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferLabelkeyComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferLazyComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferLoadComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferNodatatextComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferPaginationComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferPlaceholderComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferSearchableComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferSearchkeysComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferSizeComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferTableComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferTestModule.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferTitlesComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/data.js create mode 100644 src/transfer/demo/src/app/transfer/transfer-basic.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-disabled.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-event.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-id.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-idkey.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-labelkey.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-lazy.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-load.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-nodatatext.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-pagination.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-placeholder.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-searchable.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-searchkeys.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-size.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-table.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-titles.html create mode 100644 src/transfer/demo/src/app/transfer/transfer.less create mode 100644 src/transfer/demo/src/app/transfer/webdoc/transfer-demos.js create mode 100644 src/transfer/demo/src/app/transfer/webdoc/transfer.cn.md create mode 100644 src/transfer/demo/src/app/transfer/webdoc/transfer.en.md create mode 100644 src/transfer/demo/src/favicon.ico create mode 100644 src/transfer/demo/src/index.html create mode 100644 src/transfer/demo/src/main.ts create mode 100644 src/transfer/demo/test.ts create mode 100644 src/transfer/demo/tsconfig.app.json create mode 100644 src/transfer/demo/tsconfig.spec.json create mode 100644 src/transfer/lib/index.ts create mode 100644 src/transfer/lib/ng-package.json create mode 100644 src/transfer/lib/package.json create mode 100644 src/transfer/lib/project.json create mode 100644 src/transfer/lib/src/TiTransferColumn.ts create mode 100644 src/transfer/lib/src/TiTransferComponent.ts create mode 100644 src/transfer/lib/src/TiTransferModule.ts create mode 100644 src/transfer/lib/src/i18n/TiTransferWords.ts create mode 100644 src/transfer/lib/src/i18n/en_US.ts create mode 100644 src/transfer/lib/src/i18n/es_US.ts create mode 100644 src/transfer/lib/src/i18n/fr_FR.ts create mode 100644 src/transfer/lib/src/i18n/index.ts create mode 100644 src/transfer/lib/src/i18n/pt_BR.ts create mode 100644 src/transfer/lib/src/i18n/zh_CN.ts create mode 100644 src/transfer/lib/src/transfer.html create mode 100644 src/transfer/lib/src/transfer.less create mode 100644 src/transfer/lib/src/transferUtil.ts create mode 100644 src/transfer/lib/src/transferlist/TiTransferListComponent.ts create mode 100644 src/transfer/lib/src/transferlist/transfer-list.html create mode 100644 src/transfer/lib/src/transferlist/transfer-list.less create mode 100644 src/tree/demo/karma.conf.js create mode 100644 src/tree/demo/project.json create mode 100644 src/tree/demo/src/app/AppComponent.ts create mode 100644 src/tree/demo/src/app/AppModule.ts create mode 100644 src/tree/demo/src/app/IndexComponent.ts create mode 100644 src/tree/demo/src/app/app.html create mode 100644 src/tree/demo/src/app/tree/TreeBeforeExpandComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeBeforeMoreComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeChangedbycheckboxComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeCheckRelationComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeDisabledComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeDragBeforedropComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeDragComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeEventComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeIconComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeLoadComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeManyComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeMultiselectComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeOperateComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeParentcheckableComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeRadioselectComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeSearchComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeShortcutkeyComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeSmallComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeTemplateComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeTestModule.ts create mode 100644 src/tree/demo/src/app/tree/TreeUtilComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeVirtualscrollComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeVirtualscrollDragComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeVirtualscrollSmallComponent.ts create mode 100644 src/tree/demo/src/app/tree/tree-before-expand.html create mode 100644 src/tree/demo/src/app/tree/tree-before-more.html create mode 100644 src/tree/demo/src/app/tree/tree-changedbycheckbox.html create mode 100644 src/tree/demo/src/app/tree/tree-check-relation.html create mode 100644 src/tree/demo/src/app/tree/tree-disabled.html create mode 100644 src/tree/demo/src/app/tree/tree-drag-beforedrop.html create mode 100644 src/tree/demo/src/app/tree/tree-drag.html create mode 100644 src/tree/demo/src/app/tree/tree-event.html create mode 100644 src/tree/demo/src/app/tree/tree-icon.html create mode 100644 src/tree/demo/src/app/tree/tree-load.html create mode 100644 src/tree/demo/src/app/tree/tree-many.html create mode 100644 src/tree/demo/src/app/tree/tree-multiselect.html create mode 100644 src/tree/demo/src/app/tree/tree-operate.html create mode 100644 src/tree/demo/src/app/tree/tree-parentcheckable.html create mode 100644 src/tree/demo/src/app/tree/tree-radioselect.html create mode 100644 src/tree/demo/src/app/tree/tree-search.html create mode 100644 src/tree/demo/src/app/tree/tree-shortcutkey.html create mode 100644 src/tree/demo/src/app/tree/tree-small.html create mode 100644 src/tree/demo/src/app/tree/tree-template.html create mode 100644 src/tree/demo/src/app/tree/tree-util.html create mode 100644 src/tree/demo/src/app/tree/tree-virtualscroll-drag.html create mode 100644 src/tree/demo/src/app/tree/tree-virtualscroll-small.html create mode 100644 src/tree/demo/src/app/tree/tree-virtualscroll.html create mode 100644 src/tree/demo/src/app/tree/treeTest.less create mode 100644 src/tree/demo/src/app/tree/webdoc/tree-demos.js create mode 100644 src/tree/demo/src/app/tree/webdoc/tree.cn.md create mode 100644 src/tree/demo/src/app/tree/webdoc/tree.en.md create mode 100644 src/tree/demo/src/favicon.ico create mode 100644 src/tree/demo/src/index.html create mode 100644 src/tree/demo/src/main.ts create mode 100644 src/tree/demo/test.ts create mode 100644 src/tree/demo/tsconfig.app.json create mode 100644 src/tree/demo/tsconfig.spec.json create mode 100644 src/tree/lib/index.ts create mode 100644 src/tree/lib/ng-package.json create mode 100644 src/tree/lib/package.json create mode 100644 src/tree/lib/project.json create mode 100644 src/tree/lib/src/TiAutoSelectDirective.ts create mode 100644 src/tree/lib/src/TiHighlightPipe.ts create mode 100644 src/tree/lib/src/TiTreeComponent.ts create mode 100644 src/tree/lib/src/TiTreeModule.ts create mode 100644 src/tree/lib/src/TiTreeUtil.ts create mode 100644 src/tree/lib/src/i18n/TiTreeWords.ts create mode 100644 src/tree/lib/src/i18n/en_US.ts create mode 100644 src/tree/lib/src/i18n/es_US.ts create mode 100644 src/tree/lib/src/i18n/fr_FR.ts create mode 100644 src/tree/lib/src/i18n/index.ts create mode 100644 src/tree/lib/src/i18n/pt_BR.ts create mode 100644 src/tree/lib/src/i18n/zh_CN.ts create mode 100644 src/tree/lib/src/tree.html create mode 100644 src/tree/lib/src/tree.less create mode 100644 src/treeselect/demo/karma.conf.js create mode 100644 src/treeselect/demo/project.json create mode 100644 src/treeselect/demo/src/app/AppComponent.ts create mode 100644 src/treeselect/demo/src/app/AppModule.ts create mode 100644 src/treeselect/demo/src/app/IndexComponent.ts create mode 100644 src/treeselect/demo/src/app/app.html create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectBasicComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectBeforeExpandComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectBeforeMoreComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectClearableComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectDisabledComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectDropmaxheightComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectEventComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectFocusComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectLabelkeyComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectLazyloadComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectLoadComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectMaxlineComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectModalComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectMultiComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectNodataComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectOptionsChangeComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectPanelwidthComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectSearchComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectSelectallComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectTestModule.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectValidationComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-basic.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-before-expand.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-before-more.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-clearable.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-disabled.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-dropmaxheight.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-event.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-focus.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-labelkey.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-lazyload.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-load.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-maxline.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-modal.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-multi.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-nodata.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-options-change.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-panelwidth.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-search.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-selectall.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-validation.html create mode 100644 src/treeselect/demo/src/app/treeselect/webdoc/treeselect-demos.js create mode 100644 src/treeselect/demo/src/app/treeselect/webdoc/treeselect.cn.md create mode 100644 src/treeselect/demo/src/app/treeselect/webdoc/treeselect.en.md create mode 100644 src/treeselect/demo/src/favicon.ico create mode 100644 src/treeselect/demo/src/index.html create mode 100644 src/treeselect/demo/src/main.ts create mode 100644 src/treeselect/demo/test.ts create mode 100644 src/treeselect/demo/tsconfig.app.json create mode 100644 src/treeselect/demo/tsconfig.spec.json create mode 100644 src/treeselect/lib/index.ts create mode 100644 src/treeselect/lib/ng-package.json create mode 100644 src/treeselect/lib/package.json create mode 100644 src/treeselect/lib/project.json create mode 100644 src/treeselect/lib/src/TiTreeselectComponent.ts create mode 100644 src/treeselect/lib/src/TiTreeselectModule.ts create mode 100644 src/treeselect/lib/src/treeselect.html create mode 100644 src/treeselect/lib/src/treeselect.less create mode 100644 src/tsconfig.lib.json create mode 100644 src/tsconfig.lib.prod.json create mode 100644 src/upload/demo/karma.conf.js create mode 100644 src/upload/demo/project.json create mode 100644 src/upload/demo/src/app/AppComponent.ts create mode 100644 src/upload/demo/src/app/AppModule.ts create mode 100644 src/upload/demo/src/app/IndexComponent.ts create mode 100644 src/upload/demo/src/app/app.html create mode 100644 src/upload/demo/src/app/upload/UploadAutoUploadComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadBasicComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadBatchSendComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadBeforeremoveComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadButtonComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadButtonTestComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadCaseTestComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadChangesComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadChunksizeComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadCustomComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadEventComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadFilterComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadFormDataComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadInitfilesTestComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadInputFieldTestComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadPropsComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadServiceComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadServiceTestComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadSingleComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadTestModule.ts create mode 100644 src/upload/demo/src/app/upload/upload-auto-upload.html create mode 100644 src/upload/demo/src/app/upload/upload-basic.html create mode 100644 src/upload/demo/src/app/upload/upload-batch-send.html create mode 100644 src/upload/demo/src/app/upload/upload-beforeremove.html create mode 100644 src/upload/demo/src/app/upload/upload-button-test.html create mode 100644 src/upload/demo/src/app/upload/upload-button.html create mode 100644 src/upload/demo/src/app/upload/upload-case-test.html create mode 100644 src/upload/demo/src/app/upload/upload-changes.html create mode 100644 src/upload/demo/src/app/upload/upload-chunksize.html create mode 100644 src/upload/demo/src/app/upload/upload-custom.html create mode 100644 src/upload/demo/src/app/upload/upload-event.html create mode 100644 src/upload/demo/src/app/upload/upload-filter.html create mode 100644 src/upload/demo/src/app/upload/upload-form-data.html create mode 100644 src/upload/demo/src/app/upload/upload-initfiles-test.html create mode 100644 src/upload/demo/src/app/upload/upload-input-field-test.html create mode 100644 src/upload/demo/src/app/upload/upload-props.html create mode 100644 src/upload/demo/src/app/upload/upload-service-test.html create mode 100644 src/upload/demo/src/app/upload/upload-service.html create mode 100644 src/upload/demo/src/app/upload/upload-single.html create mode 100644 src/upload/demo/src/app/upload/webdoc/upload-demos.js create mode 100644 src/upload/demo/src/app/upload/webdoc/upload.cn.md create mode 100644 src/upload/demo/src/app/upload/webdoc/upload.en.md create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageAutoUploadComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageBasicComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageChangesComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageDeletableComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageDisabledComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageDragComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageEventComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageFilterComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageInitfilesComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageMaxcountComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageTemplateComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageTestModule.ts create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-auto-upload.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-basic.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-changes.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-deletable.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-disabled.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-drag.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-event.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-filter.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-initfiles.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-maxcount.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-template.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimagetest.less create mode 100644 src/upload/demo/src/app/uploadimage/webdoc/uploadimage-demos.js create mode 100644 src/upload/demo/src/app/uploadimage/webdoc/uploadimage.cn.md create mode 100644 src/upload/demo/src/app/uploadimage/webdoc/uploadimage.en.md create mode 100644 src/upload/demo/src/favicon.ico create mode 100644 src/upload/demo/src/index.html create mode 100644 src/upload/demo/src/main.ts create mode 100644 src/upload/demo/test.ts create mode 100644 src/upload/demo/tsconfig.app.json create mode 100644 src/upload/demo/tsconfig.spec.json create mode 100644 src/upload/lib/index.ts create mode 100644 src/upload/lib/ng-package.json create mode 100644 src/upload/lib/package.json create mode 100644 src/upload/lib/project.json create mode 100644 src/upload/lib/src/TiDisabledDirective.ts create mode 100644 src/upload/lib/src/TiFileInterface.ts create mode 100644 src/upload/lib/src/TiFileSelectDirective.ts create mode 100644 src/upload/lib/src/TiUploadComponent.ts create mode 100644 src/upload/lib/src/TiUploadModule.ts create mode 100644 src/upload/lib/src/TiUploadService.ts create mode 100644 src/upload/lib/src/TiUploadServiceModule.ts create mode 100644 src/upload/lib/src/TiUploadUtil.ts create mode 100644 src/upload/lib/src/TiUploadbaseComponent.ts create mode 100644 src/upload/lib/src/TiUploadimageComponent.ts create mode 100644 src/upload/lib/src/i18n/TiUploadWords.ts create mode 100644 src/upload/lib/src/i18n/en_US.ts create mode 100644 src/upload/lib/src/i18n/es_US.ts create mode 100644 src/upload/lib/src/i18n/fr_FR.ts create mode 100644 src/upload/lib/src/i18n/index.ts create mode 100644 src/upload/lib/src/i18n/pt_BR.ts create mode 100644 src/upload/lib/src/i18n/zh_CN.ts create mode 100644 src/upload/lib/src/upload.html create mode 100644 src/upload/lib/src/upload.less create mode 100644 src/upload/lib/src/uploadimage.html create mode 100644 src/upload/lib/src/uploadimage.less create mode 100644 src/utils/demo/project.json create mode 100644 src/utils/demo/src/app/AppComponent.ts create mode 100644 src/utils/demo/src/app/AppModule.ts create mode 100644 src/utils/demo/src/app/IndexComponent.ts create mode 100644 src/utils/demo/src/app/app.html create mode 100644 src/utils/demo/src/app/browser/BrowserTestModule.ts create mode 100644 src/utils/demo/src/app/browser/BrowserUsageComponent.ts create mode 100644 src/utils/demo/src/app/browser/browser-usage.html create mode 100644 src/utils/demo/src/app/browser/webdoc/browser-demos.js create mode 100644 src/utils/demo/src/app/browser/webdoc/browser.cn.md create mode 100644 src/utils/demo/src/app/browser/webdoc/browser.en.md create mode 100644 src/utils/demo/src/app/keymap/KeymapTestModule.ts create mode 100644 src/utils/demo/src/app/keymap/KeymapUsageComponent.ts create mode 100644 src/utils/demo/src/app/keymap/keymap-usage.html create mode 100644 src/utils/demo/src/app/keymap/webdoc/keymap-demos.js create mode 100644 src/utils/demo/src/app/keymap/webdoc/keymap.cn.md create mode 100644 src/utils/demo/src/app/keymap/webdoc/keymap.en.md create mode 100644 src/utils/demo/src/app/log/LogTestModule.ts create mode 100644 src/utils/demo/src/app/log/LogUsageComponent.ts create mode 100644 src/utils/demo/src/app/log/log-usage.html create mode 100644 src/utils/demo/src/app/log/webdoc/log-demos.js create mode 100644 src/utils/demo/src/app/log/webdoc/log.cn.md create mode 100644 src/utils/demo/src/app/log/webdoc/log.en.md create mode 100644 src/utils/demo/src/app/theme/ThemeBasicComponent.ts create mode 100644 src/utils/demo/src/app/theme/ThemeTestModule.ts create mode 100644 src/utils/demo/src/app/theme/theme-basic.html create mode 100644 src/utils/demo/src/app/theme/webdoc/theme-demos.js create mode 100644 src/utils/demo/src/app/theme/webdoc/theme.cn.md create mode 100644 src/utils/demo/src/app/theme/webdoc/theme.en.md create mode 100644 src/utils/demo/src/favicon.ico create mode 100644 src/utils/demo/src/index.html create mode 100644 src/utils/demo/src/main.ts create mode 100644 src/utils/demo/tsconfig.app.json create mode 100644 src/utils/lib/index.ts create mode 100644 src/utils/lib/ng-package.json create mode 100644 src/utils/lib/package.json create mode 100644 src/utils/lib/project.json create mode 100644 src/utils/lib/src/ObservableMap.ts create mode 100644 src/utils/lib/src/ObservableSet.ts create mode 100644 src/utils/lib/src/Position.ts create mode 100644 src/utils/lib/src/TiBrowser.ts create mode 100644 src/utils/lib/src/TiDateUtil.ts create mode 100644 src/utils/lib/src/TiKeymap.ts create mode 100644 src/utils/lib/src/TiLog.ts create mode 100644 src/utils/lib/src/TiTheme.ts create mode 100644 src/utils/lib/src/Util.ts create mode 100644 src/validation/demo/karma.conf.js create mode 100644 src/validation/demo/project.json create mode 100644 src/validation/demo/src/app/AppComponent.ts create mode 100644 src/validation/demo/src/app/AppModule.ts create mode 100644 src/validation/demo/src/app/IndexComponent.ts create mode 100644 src/validation/demo/src/app/app.html create mode 100644 src/validation/demo/src/app/validation/ValidationAsyncCheckComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationAsyncCheckTestComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationBasicControlComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationBasicDirectiveComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationBlurCheckComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationErrorMsgComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationFormGroupComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationFormGroupConfigComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationFormGroupTestComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationParamChangeComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationPwdCheckComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationRulesCustomComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationRulesCustomDirectiveComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationRulesTestComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationTemplateFormNestedComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationTestModule.ts create mode 100644 src/validation/demo/src/app/validation/ValidationTipComponent.ts create mode 100644 src/validation/demo/src/app/validation/validation-async-check-test.html create mode 100644 src/validation/demo/src/app/validation/validation-async-check.html create mode 100644 src/validation/demo/src/app/validation/validation-basic-control.html create mode 100644 src/validation/demo/src/app/validation/validation-basic-directive.html create mode 100644 src/validation/demo/src/app/validation/validation-blur-check.html create mode 100644 src/validation/demo/src/app/validation/validation-error-msg.html create mode 100644 src/validation/demo/src/app/validation/validation-form-group-config.html create mode 100644 src/validation/demo/src/app/validation/validation-form-group-test.html create mode 100644 src/validation/demo/src/app/validation/validation-form-group.html create mode 100644 src/validation/demo/src/app/validation/validation-param-change.html create mode 100644 src/validation/demo/src/app/validation/validation-pwd-check.html create mode 100644 src/validation/demo/src/app/validation/validation-rules-custom-directive.html create mode 100644 src/validation/demo/src/app/validation/validation-rules-custom.html create mode 100644 src/validation/demo/src/app/validation/validation-rules-test.html create mode 100644 src/validation/demo/src/app/validation/validation-template-form-nested.html create mode 100644 src/validation/demo/src/app/validation/validation-tip.html create mode 100644 src/validation/demo/src/app/validation/validation-tiscroll.html create mode 100644 src/validation/demo/src/app/validation/validationTiscrollComponent.ts create mode 100644 src/validation/demo/src/app/validation/webdoc/validation-demos.js create mode 100644 src/validation/demo/src/app/validation/webdoc/validation.cn.md create mode 100644 src/validation/demo/src/app/validation/webdoc/validation.en.md create mode 100644 src/validation/demo/src/favicon.ico create mode 100644 src/validation/demo/src/index.html create mode 100644 src/validation/demo/src/main.ts create mode 100644 src/validation/demo/test.ts create mode 100644 src/validation/demo/tsconfig.app.json create mode 100644 src/validation/demo/tsconfig.spec.json create mode 100644 src/validation/lib/index.ts create mode 100644 src/validation/lib/ng-package.json create mode 100644 src/validation/lib/package.json create mode 100644 src/validation/lib/project.json create mode 100644 src/validation/lib/src/TiErrorMsgComponent.ts create mode 100644 src/validation/lib/src/TiPendingStateComponent.ts create mode 100644 src/validation/lib/src/TiPwdMsgComponent.html create mode 100644 src/validation/lib/src/TiPwdMsgComponent.ts create mode 100644 src/validation/lib/src/TiValidationConfig.ts create mode 100644 src/validation/lib/src/TiValidationDirective.ts create mode 100644 src/validation/lib/src/TiValidationInterface.ts create mode 100644 src/validation/lib/src/TiValidationModule.ts create mode 100644 src/validation/lib/src/checkHandle/AsyncCheck.ts create mode 100644 src/validation/lib/src/checkHandle/BlurCheck.ts create mode 100644 src/validation/lib/src/checkHandle/ChangeCheck.ts create mode 100644 src/validation/lib/src/checkHandle/CheckHandle.ts create mode 100644 src/validation/lib/src/checkHandle/CheckStyleModule.ts create mode 100644 src/validation/lib/src/checkHandle/CheckStyleService.ts create mode 100644 src/validation/lib/src/checkHandle/CommonService.ts create mode 100644 src/validation/lib/src/checkHandle/CommonServiceModule.ts create mode 100644 src/validation/lib/src/checkHandle/PwdCheck.ts create mode 100644 src/validation/lib/src/checkHandle/RadiobaseCheck.ts create mode 100644 src/validation/lib/src/checkHandle/TiPwdConfig.ts create mode 100644 src/validation/lib/src/errorMsg.less create mode 100644 src/validation/lib/src/i18n/TiValidationWords.ts create mode 100644 src/validation/lib/src/i18n/en_US.ts create mode 100644 src/validation/lib/src/i18n/es_US.ts create mode 100644 src/validation/lib/src/i18n/fr_FR.ts create mode 100644 src/validation/lib/src/i18n/index.ts create mode 100644 src/validation/lib/src/i18n/pt_BR.ts create mode 100644 src/validation/lib/src/i18n/zh_CN.ts create mode 100644 src/validation/lib/src/pending-state.html create mode 100644 src/validation/lib/src/pending-state.less create mode 100644 src/validation/lib/src/pwdMsg.less create mode 100644 src/validation/lib/src/validators/TiValidators.ts create mode 100644 src/validation/lib/src/validators/directives/BaseValidator.ts create mode 100644 src/validation/lib/src/validators/directives/BigDigitsValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/BigIntegerValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/BigNumberValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/ContainsValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/DateValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/DigitsValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/EmailValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/EqualValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/IntegerValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/Ipv4ValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/Ipv6ValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/MaxLengthValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/MaxValueByStringValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/MaxValueValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/MinLengthValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/MinValueByStringValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/MinValueValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/NotContainsValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/NotEqualValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/NotScriptValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/NumberValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/PasswordValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/PortValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/RangeSizeValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/RangeValueByStringValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/RangeValueValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/RegExpValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/RequiredValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/UrlValidatorDirective.ts create mode 100644 src/zoom/demo/project.json create mode 100644 src/zoom/demo/src/app/AppComponent.ts create mode 100644 src/zoom/demo/src/app/AppModule.ts create mode 100644 src/zoom/demo/src/app/IndexComponent.ts create mode 100644 src/zoom/demo/src/app/app.html create mode 100644 src/zoom/demo/src/app/zoom/ZoomBasicComponent.ts create mode 100644 src/zoom/demo/src/app/zoom/ZoomChangeSrcComponent.ts create mode 100644 src/zoom/demo/src/app/zoom/ZoomRatioComponent.ts create mode 100644 src/zoom/demo/src/app/zoom/ZoomTestComponent.ts create mode 100644 src/zoom/demo/src/app/zoom/ZoomTestModule.ts create mode 100644 src/zoom/demo/src/app/zoom/zoom-basic.html create mode 100644 src/zoom/demo/src/app/zoom/zoom-change-src.html create mode 100644 src/zoom/demo/src/app/zoom/zoom-ratio.html create mode 100644 src/zoom/demo/src/app/zoom/zoom-test.html create mode 100644 src/zoom/demo/src/app/zoom/zoom.less create mode 100644 src/zoom/demo/src/favicon.ico create mode 100644 src/zoom/demo/src/index.html create mode 100644 src/zoom/demo/src/main.ts create mode 100644 src/zoom/demo/tsconfig.app.json create mode 100644 src/zoom/lib/index.ts create mode 100644 src/zoom/lib/ng-package.json create mode 100644 src/zoom/lib/package.json create mode 100644 src/zoom/lib/project.json create mode 100644 src/zoom/lib/src/TiZoomComponent.ts create mode 100644 src/zoom/lib/src/TiZoomModule.ts create mode 100644 src/zoom/lib/src/zoom.less create mode 100644 tools/generators/ti-lib-generator/files/demo/project.json__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/AppComponent.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/AppModule.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/IndexComponent.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoComponentName__.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoModuleName__.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/__name__/__name__-basic.html__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__-demos.js__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.cn.md__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.en.md__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/app.html__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/favicon.ico__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/index.html__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/main.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/tsconfig.app.json__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/index.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/ng-package.json__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/package.json__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/project.json__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/__componentName__.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/__moduleName__.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/__name__.html__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/__name__.less__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/i18n/__i18nWords__.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/i18n/en_US.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/i18n/es_US.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/i18n/fr_FR.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/i18n/index.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/i18n/pt_BR.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/index.ts create mode 100644 tools/generators/ti-lib-generator/schema.d.ts create mode 100644 tools/generators/ti-lib-generator/schema.json create mode 100644 tools/tsconfig.tools.json create mode 100644 tsconfig.base.json create mode 100644 tsconfig.json create mode 100644 website-tinydoc/build/copy.js create mode 100644 website-tinydoc/config.js create mode 100644 website-tinydoc/package.json diff --git a/.codecheck/check.yml b/.codecheck/check.yml new file mode 100644 index 0000000..fe3687d --- /dev/null +++ b/.codecheck/check.yml @@ -0,0 +1,9 @@ +version: 2.0 + +steps: + pre_codecheck: + - checkout + +tool_params: + secsolar: #安全扫描工具,必选工具(codedex工具的替代者) + source_dir: ./ #指定扫描源码目录,多目录时用 分号 分隔。./表示代码库根目录,填写相对根目录的相对路径 \ No newline at end of file diff --git a/.compodocrc.json b/.compodocrc.json new file mode 100644 index 0000000..2565aff --- /dev/null +++ b/.compodocrc.json @@ -0,0 +1,17 @@ +{ + "tsconfig": "./src/tsconfig.lib.json", + "output": "./dist/tinydoc", + "name": "TinyNG Website API Doc", + "language": "zh-CN", + "hideGenerator": true, + "disableSourceCode": true, + "disableDomTree": true, + "disableTemplateTab": true, + "disableStyleTab": true, + "disableGraph": true, + "disablePrivate": true, + "disableProtected": true, + "disableInternal": true, + "disableLifeCycleHooks": true, + "disableRoutesGraph": true +} \ No newline at end of file diff --git a/.cz.json b/.cz.json new file mode 100644 index 0000000..d1bcc20 --- /dev/null +++ b/.cz.json @@ -0,0 +1,3 @@ +{ + "path": "cz-conventional-changelog" +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6e87a00 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..1135dd5 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,57 @@ +module.exports = { + extends: [ + './eslint-rules/eslint-config-cbc-1-7-8-angular.js', + 'plugin:prettier/recommended', + ], + ignorePatterns: ['build', 'website-tinydoc', 'tools', '*.js'], + globals: { + // 这里填入你的项目需要的全局变量 + // 这里值为 false 表示这个全局变量不允许被重新赋值,比如: + // + // jQuery: false, + // $: false + }, + rules: { + + '@typescript-eslint/no-unused-expressions': ['error'], + // 添加类型声明提示 + '@typescript-eslint/typedef': [ + 'warn', + { + arrayDestructuring: true, + arrowParameter: true, + memberVariableDeclaration: true, + objectDestructuring: true, + parameter: true, + propertyDeclaration: true, + variableDeclaration: true, + variableDeclarationIgnoreFunction: true, + } + ], + // 函数返回值类型声明提示 + '@typescript-eslint/explicit-function-return-type': 'warn', + // 类型声明周围的间距,提高代码可读性 + '@typescript-eslint/type-annotation-spacing': 'warn', + "no-unused-vars": "error", + // 变量必须先定义后使用 + 'no-use-before-define': 'off', + // 允许在参数,变量和属性上进行显式类型声明 + '@typescript-eslint/no-inferrable-types': 'off', + 'no-inferrable-types': 'off', + // angular场景下,constructor为空的情况很多,也是官方推荐的用法 + '@typescript-eslint/no-useless-constructor': 'off', + 'no-useless-constructor': 'off', + // 函数的参数禁止超过 5 个,不适合angular场景 + 'max-params': 'off', + 'no-array-constructor': 'off', + 'prefer-regex-literals': 'off', + // 消除参数顺序告警(eslint) + '@typescript-eslint/member-ordering': 'off', + 'member-ordering': 'off', + // 禁止给类的构造函数的参数添加修饰符 + '@typescript-eslint/no-parameter-properties': 'off', + 'no-parameter-properties': 'off', + // 禁止重复引用 + 'no-duplicate-imports': 'error', + } +}; diff --git a/.githooks/commit-msg.sample b/.githooks/commit-msg.sample new file mode 100644 index 0000000..e388cce --- /dev/null +++ b/.githooks/commit-msg.sample @@ -0,0 +1,12 @@ +#!/bin/sh +# +# 去掉文件名后面的 .sample 从而启用该 hook +# + +echo -e "\nGit hooks (commit-msg): \n" + +# 获取提交信息,保存在 COMMIT_MSG 变量中 +COMMIT_MSG=`awk '{printf("%s",$0)}' $1` + +# 使用标准输入 stdin 传递给 commitlint 用于检查提交信息 +echo "${COMMIT_MSG}" | npx --no-install commitlint diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..8853404 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,6 @@ +#!/bin/sh + +echo -e "\nGit hooks (pre-commit): \n" + +# 启动 lint-staged 进行代码格式和语法检查和自动修复 +npx --no-install lint-staged diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..04f85ef --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,61 @@ +name: 'Bug report' +description: Create a report to help us improve TinyUI For Angular +title: '[Bug]: ' +labels: ['bug'] +body: + - type: markdown + attributes: + value: | + Please fill out the following carefully in order to better fix the problem. + - type: input + id: tiny-ng-version + attributes: + label: Version + description: | + ### **Check if the issue is reproducible with the latest stable version.** + You can use the command `npm ls @opentiny/ng` to view it + placeholder: latest + validations: + required: true + - type: input + id: ng-version + attributes: + label: Angular Version + placeholder: latest + validations: + required: true + - type: textarea + id: minimal-repo + attributes: + label: Link to minimal reproduction + description: | + **Provide a streamlined CodePen / CodeSandbox or GitHub repository link as much as possible. Please don't fill in a link randomly, it will only close your issue directly.** + placeholder: Please Input + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Step to reproduce + description: | + **After the replay is turned on, what actions do we need to perform to make the bug appear? Simple and clear steps can help us locate the problem more quickly. Please clearly describe the steps of reproducing the issue. Issues without clear reproducing steps will not be repaired. If the issue marked with 'need reproduction' does not provide relevant steps within 7 days, it will be closed directly.** + placeholder: Please Input + validations: + required: true + - type: textarea + id: expected + attributes: + label: What is expected + placeholder: Please Input + - type: textarea + id: actually + attributes: + label: What is actually happening + placeholder: Please Input + - type: textarea + id: additional-comments + attributes: + label: Any additional comments (optional) + description: | + **Some background / context of how you ran into this bug.** + placeholder: Please Input diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a682839 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Questions or need help + url: https://github.com/opentiny/ng/discussions + about: Add this WeChat(opentiny), we will invite you to the WeChat discussion group later. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..acbdf5b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,23 @@ +name: Feature Request +description: Propose new features to TinyUI For Angular to improve it. +title: '[Feature]: ' +labels: ['feature'] +body: + - type: textarea + id: feature-solve + attributes: + label: What problem does this feature solve + description: | + Explain your use case, context, and rationale behind this feature request. More importantly, what is the end user experience you are trying to build that led to the need for this feature? + placeholder: Please Input + validations: + required: true + - type: textarea + id: feature-api + attributes: + label: What does the proposed API look like + description: | + Describe how you propose to solve the problem and provide code samples of how the API would work once implemented. Note that you can use Markdown to format your code blocks. + placeholder: Please Input + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..782af3f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,41 @@ +# PR + +## PR Checklist + +Please check if your PR fulfills the following requirements: + +- [ ] The commit message follows our [Commit Message Guidelines](https://github.com/opentiny/ng/blob/main/CONTRIBUTING.md) +- [ ] Tests for the changes have been added (for bug fixes / features) +- [ ] Docs have been added / updated (for bug fixes / features) + +## PR Type + +What kind of change does this PR introduce? + + + +- [ ] Bugfix +- [ ] Feature +- [ ] Code style update (formatting, local variables) +- [ ] Refactoring (no functional changes, no api changes) +- [ ] Build related changes +- [ ] CI related changes +- [ ] Documentation content changes +- [ ] Other... Please describe: + +## What is the current behavior? + + + +Issue Number: N/A + +## What is the new behavior? + +## Does this PR introduce a breaking change? + +- [ ] Yes +- [ ] No + + + +## Other information diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..278ac17 --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. +# compiled output +/dist +/tmp +/out-tsc +/src_bak + +# dependencies +/node_modules +**/node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace +.angular + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db + + + +/tiny-design + +website-tinydoc/webdoc +website-tinydoc/app +website-tinydoc/scripts +website-tinydoc/.npmrc +website-tinydoc/package-lock.json +/build/basicVarList.json \ No newline at end of file diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 0000000..8b0dd1f --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,4 @@ +{ + "*.{js,ts}": ["prettier --write", "eslint --fix"], + "*.{html,css,less,json}": "prettier --write" +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..bf2c511 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +# package.json is formatted by package managers, so we ignore it here +package.json +website-tinydoc +build +eslint-rules +.* +*.log +tiny-design +dist +# 格式化文件会给如下文件的not选择器前加上空格 导致选择器失效,先在这里忽略掉 +src/themes/basic/common.less +src/phonenumber/lib/src/phonenumber.less \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..bb082e3 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,11 @@ +module.exports = { + tabWidth: 2, // tab等2个空格 + printWidth: 140, + useTabs: false, // 用空格缩进行 + semi: true, // 行尾使用分号 + singleQuote: true, // 字符串使用单引号 + quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号 + trailingComma: 'none', // 在对象或数组最后一个元素后面是否加逗号(在ES5中加尾逗号) + bracketSpacing: true, // 在对象,数组括号与文字之间加空格 "{ foo: bar }" + endOfLine: 'auto' // 结尾是 lf-\n cr-\r lfcr-\n\r auto-保持现有的行尾设置 +}; diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..ca8a4d4 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,40 @@ +{ + "recommendations": [ + /*使用最新版VSCODE,打开项目后右下角有提示安装所有插件,左侧插件栏目有列出工作空间插件*/ + /*必选插件:*/ + /*Angular模板变量检查*/ + "angular.ng-template", + /*eslint检查*/ + "dbaeumer.vscode-eslint", + /*prettier检查*/ + "esbenp.prettier-vscode", + /*style静态检查,初期暂不要求*/ + "stuartzhang.stylelint-stzhang", + /*彩色括号配对*/ + "coenraads.bracket-pair-colorizer-2", + /*注释*/ + "oouo-diogo-perdigao.docthis", + /*变量顺序排序*/ + "tyriar.sort-lines", + /*Angular,typescript, html等语法填充*/ + "mikael.angular-beastcode", + /*修改标签名时闭合标签自动跟着改变*/ + "formulahendry.auto-rename-tag", + /*圈复杂度*/ + "kisstkondoros.vscode-codemetrics", + /*可选插件:*/ + /*代码中出现git修改记录*/ + "eamodio.gitlens", + /*markdown编辑*/ + "shd101wyy.markdown-preview-enhanced", + /*代码行数统计*/ + "uctakeoff.vscode-counter", + /*导入包体积*/ + "wix.vscode-import-cost", + /*中文包*/ + "ms-ceintl.vscode-language-pack-zh-hans", + "nrwl.angular-console" + ], + "unwantedRecommendations": [ + ] + } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1815a33 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,44 @@ +{ + "files.autoSave": "onWindowChange", + "files.trimTrailingWhitespace": true, + "[markdown]": { + "files.trimTrailingWhitespace": false + }, + /*typescript和tslint2019年初已经决定放弃tslint,已转向eslint来做ts静态检查。*/ + "eslint.alwaysShowStatus": true, + "eslint.autoFixOnSave": false, + "eslint.validate": [ + "javascript", + "javascriptreact", + { + "language": "vue", + "autoFix": true + }, + { + "language": "typescript", + "autoFix": true + }, + { + "language": "typescriptreact", + "autoFix": true + } + ], + "docthis.includeMemberOfOnClassMembers": false, + "docthis.includeMemberOfOnInterfaceMembers": false, + "docthis.includeTypes": false, + "stylelint.enable": false, + "stylelint.autoFix": false, + /*这是vscode的css检查项。如果启用stylelint,那么应该停用vscode的CSS检查*/ + "css.validate": true, + "less.validate": true, + "scss.validate": false, + "CodeCheck.RealTimeCodeMarsCheck.Enable": false, + "CodeCheck.AllRelatedRemoteBranch": "", + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "editor.formatOnSave": false + } +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2886190 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +--- +title: 更新日志 | TinyNG +--- + +## 更新日志 + +### 14.0.0 + +*2022-11-15* + +1. Initial version diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..79cc4ba --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,98 @@ +--- +title: 贡献指南 | TinyNG +--- + +## 贡献指南 + +### Issue 规范 + +- Issue 仅用于提交 Bug 或 Feature 以及用户体验相关的内容,其它内容可能会被直接关闭。 + +- 在提交 Issue 之前,请搜索相关内容是否已被提出。 + +- 在提 Bug 时请说明清楚使用的 @opentiny/ng 的版本号及相关环境。 + +### Pull Request 规范 + +- 请先 fork 一份到自己的项目下,新建一个分支用于变更。 + + ```bash + git checkout -b my-branch master + ``` + +- commit 信息请遵循 [commit rules](commit.template)。 + +- 提交 PR 前请先进行 rebase,确保 commit 记录的整洁。 + ```bash + git rebase master -i + git push -f + ``` + +- 如果是修复 `bug` 或者 `issues`,请在 PR 中描述清楚。 + + +### 开发 + +```bash + +# fork && git clone +... +# my-branch +npm install +npm start + +``` + +## 单元测试 + +### 整体测试 +```bash +$ npm test ng-demo +``` +或 +```bash +$ npx ng test ng-demo +``` + +### 指定组件测试 +```bash +$ npm test ${component name for test}-demo +``` +或 +```bash +$ npx ng test ${component name for test}-demo +``` + +例如: +```bash +$ npm test text-demo +``` + +### 添加测试用例 +- 由于库中每个组件都有单独版本,所以对于新加入的组件,需要开发者为其准备单元测试环境。(也可使用自动化脚本为新组件生成环境所需文件,例如: `npm build:test radio`) +- 对于已有组件,直接在组件 `demo/` 目录下新建 `${your component name}.spec.json` 文件后编写测试脚本。 +- 若组件 demo 目录中已经存在 `${your component name}.spec.json` 文件,则直接在此文件中修改用例。 + +TinyNG 使用 [Jasmine 测试框架](https://jasmine.github.io/) 对组件库内容进行单元测试。 +`npm test` 命令在**监视模式**下构建应用,并启动 [karma 测试运行器](https://karma-runner.github.io/)。 +运行结束后,控制台会输出测试结果,内容格式如下: + +``` +✔ Browser application bundle generation complete. +28 11 2022 08:40:17.804:WARN [karma]: No captured browser, open http://localhost:9876/ +28 11 2022 08:40:17.824:INFO [karma-server]: Karma v6.3.20 server started at http://localhost:9876/ +28 11 2022 08:40:17.825:INFO [launcher]: Launching browsers Chrome with concurrency unlimited +28 11 2022 08:40:17.833:INFO [launcher]: Starting browser Chrome +28 11 2022 08:40:19.278:INFO [Chrome 107.0.0.0 (Windows 10)]: Connected on socket swhnqYi_RwdYbu8uAAAB with id 55443332 +Chrome 107.0.0.0 (Windows 10): Executed 5 of 5 SUCCESS (0.562 secs / 0.815 secs) +TOTAL: 5 SUCCESS +``` + +它还会打开 Chrome 浏览器并在 [Jasmine HTML 报告器](https://github.com/dfederm/karma-jasmine-html-reporter)中显示测试输出。可以点击某一行用例,来单独重跑这个用例,或者点击一行描述信息来重跑所选的测试套件中的那些测试。 + +单元测试用例及相关配置在 `/src/test/` 目录下。 + +更多详细内容可以通过查阅 [Angular 官网关于测试的介绍](https://angular.cn/guide/testing)获得。 + +### 代码规范 +遵循 [ESLint](eslint-rules/) \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7f66ede --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2022 - present TinyUI Authors. +Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README-zh_CN.md b/README-zh_CN.md new file mode 100644 index 0000000..1adb63e --- /dev/null +++ b/README-zh_CN.md @@ -0,0 +1,103 @@ +

+ + logo 图片 + +

+ +

+TinyNG +

+ +
+ +基于 Angular + TypeScript 的企业级 Web UI 组件库。 + +
+ +[English](README.md) | 简体中文 + +## 特性 + +- 70+ 开箱即用的高质量 Angular 组件。 +- 支持 OnPush 模式。 +- 使用 TypeScript 开发。 +- 利用 CSS variables 提供强大的主题配置能力。 +- 支持 5 种 国际化语言。 + +## 支持环境 + +* Angular `^14.0.0` +* 现代浏览器 + +| Edge | Firefox | Chrome | Safari | +| --------- | --------- | --------- | --------- | +| last 3 versions | last 3 versions | last 3 versions | last 2 versions | + +## 安装 + +```bash +$ npm install @opentiny/ng +``` + +## 使用 + +将想要使用的组件模块引入到你的模块中。 + +```ts +import { TiButtonModule } from '@opentiny/ng'; + +@NgModule({ + imports: [ TiButtonModule ] +}) +export class AppModule { +} +``` + +然后在 `angular.json` 文件中引入全局样式文件。 + +```diff +{ + "styles": [ ++ "node_modules/@opentiny/ng/themes/styles.css", ++ "node_modules/@opentiny/ng/themes/theme-default.css", + ] +} +``` + +可参考官网 [快速上手](https://www.opentiny.design/tiny-ng/docs/getstart) 了解更多。 + +## 开发 + +```bash +$ git clone 代码仓 clone 地址 +$ cd ng +$ npm install +$ npm start +``` + +浏览器会自动打开。 + +## 更新日志 + +详细的变更内容请在 [change logs](CHANGELOG.md) 查看。 + +## 常见问题 + +我们已经收集了一常见问题[常见问题](https://www.opentiny.design/tiny-ng/docs/faq)。在提 issue 前,请先在常见问题中查找你的问题是否已有答案。 + +## 如何贡献 + +我们欢迎所有的贡献。请随时贡献代码或提出您的想法。 + +在提交你的任何想法之前,请先阅读我们的[贡献指南](CONTRIBUTING.md) 。 + +## 授权协议 + +[MIT](LICENSE) + + + + + + + diff --git a/README.md b/README.md index e69de29..b4920b8 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,104 @@ +

+ + logo 图片 + +

+ +

+TinyNG +

+ +
+ +An enterprise-class Angular UI component library. + +
+ +English | [简体中文](README-zh_CN.md) + +## Features + +- 70+ high-quality Angular components out of the box. +- Support OnPush mode. +- Use TypeScript. +- Powerful theme customization with CSS variables. +- Internationalization support for 5 languages. + +## Environment Support + +* Angular `^14.0.0` +* Modern browsers + +| Edge | Firefox | Chrome | Safari | +| --------- | --------- | --------- | --------- | +| last 3 versions | last 3 versions | last 3 versions | last 2 versions | + +## Installation + + +```bash +$ npm install @opentiny/ng +``` + +## Usage + +Import the component modules you want to use into your module files. + +```ts +import { TiButtonModule } from '@opentiny/ng'; + +@NgModule({ + imports: [ TiButtonModule ] +}) +export class AppModule { +} +``` + +And import style file link in `angular.json`. + +```diff +{ + "styles": [ ++ "node_modules/@opentiny/ng/themes/styles.css", ++ "node_modules/@opentiny/ng/themes/theme-default.css", + ] +} +``` + +See [Getting Started](https://www.opentiny.design/tiny-ng/docs/getstart) for more details. + +## Development + +```bash +$ git clone 代码仓 clone 地址 +$ cd ng +$ npm install +$ npm start +``` + +Browser would open automatically. + +## Changelog + +Detailed changes for each release are documented in the [change logs](CHANGELOG.md). + +## FAQ + +We have collected some [frequently asked questions](https://www.opentiny.design/tiny-ng/docs/faq). Before reporting an issue, please search if the FAQ has the answer to your problem. + +## Contributing + +We welcome all contributions. Please feel free to contribute code or discuss your idea. + +Please read our [CONTRIBUTING.md](CONTRIBUTING.md) before submitting any ideas. + +## LICENSE + +[MIT](LICENSE) + + + + + + + diff --git a/angular.json b/angular.json new file mode 100644 index 0000000..91f1dd0 --- /dev/null +++ b/angular.json @@ -0,0 +1,198 @@ +{ + "version": 2, + "projects": { + "accordion": "src/accordion/lib", + "accordion-demo": "src/accordion/demo", + "actionmenu": "src/actionmenu/lib", + "actionmenu-demo": "src/actionmenu/demo", + "alert": "src/alert/lib", + "alert-demo": "src/alert/demo", + "anchor": "src/anchor/lib", + "anchor-demo": "src/anchor/demo", + "autocomplete": "src/autocomplete/lib", + "autocomplete-demo": "src/autocomplete/demo", + "avatar": "src/avatar/lib", + "avatar-demo": "src/avatar/demo", + "base": "src/base/lib", + "button": "src/button/lib", + "button-demo": "src/button/demo", + "buttongroup": "src/buttongroup/lib", + "buttongroup-demo": "src/buttongroup/demo", + "buttonselect": "src/buttonselect/lib", + "buttonselect-demo": "src/buttonselect/demo", + "card": "src/card/lib", + "card-demo": "src/card/demo", + "cascader": "src/cascader/lib", + "cascader-demo": "src/cascader/demo", + "checkbox": "src/checkbox/lib", + "checkbox-demo": "src/checkbox/demo", + "collapse": "src/collapse/lib", + "collapse-demo": "src/collapse/demo", + "collapsebox": "src/collapsebox/lib", + "collapsebox-demo": "src/collapsebox/demo", + "collapsebutton": "src/collapsebutton/lib", + "collapsebutton-demo": "src/collapsebutton/demo", + "collapsetext": "src/collapsetext/lib", + "collapsetext-demo": "src/collapsetext/demo", + "copy": "src/copy/lib", + "copy-demo": "src/copy/demo", + "crumb": "src/crumb/lib", + "crumb-demo": "src/crumb/demo", + "date": "src/date/lib", + "date-demo": "src/date/demo", + "datebase": "src/datebase/lib", + "datedominator": "src/datedominator/lib", + "datedominator-demo": "src/datedominator/demo", + "dateedit": "src/dateedit/lib", + "dateedit-demo": "src/dateedit/demo", + "datepanel": "src/datepanel/lib", + "daterange": "src/daterange/lib", + "daterange-demo": "src/daterange/demo", + "datetime": "src/datetime/lib", + "datetime-demo": "src/datetime/demo", + "datetimerange": "src/datetimerange/lib", + "datetimerange-demo": "src/datetimerange/demo", + "dominator": "src/dominator/lib", + "dominator-demo": "src/dominator/demo", + "drag": "src/drag/lib", + "drag-demo": "src/drag/demo", + "drop": "src/drop/lib", + "drop-demo": "src/drop/demo", + "droplist": "src/droplist/lib", + "droplist-demo": "src/droplist/demo", + "dropsearch": "src/dropsearch/lib", + "foldtext": "src/foldtext/lib", + "foldtext-demo": "src/foldtext/demo", + "formfield": "src/formfield/lib", + "formfield-demo": "src/formfield/demo", + "grid": "src/grid/lib", + "guides": "src/guides/lib", + "guides-demo": "src/guides/demo", + "guidesteps": "src/guidesteps/lib", + "guidesteps-demo": "src/guidesteps/demo", + "halfmodal": "src/halfmodal/lib", + "halfmodal-demo": "src/halfmodal/demo", + "icon": "src/icon/lib", + "icon-demo": "src/icon/demo", + "iconaction": "src/iconaction/lib", + "iconaction-demo": "src/iconaction/demo", + "imagepreview": "src/imagepreview/lib", + "imagepreview-demo": "src/imagepreview/demo", + "include": "src/include/lib", + "inputnumber": "src/inputnumber/lib", + "inputnumber-demo": "src/inputnumber/demo", + "intro": "src/intro/lib", + "intro-demo": "src/intro/demo", + "ip": "src/ip/lib", + "ip-demo": "src/ip/demo", + "ipsection": "src/ipsection/lib", + "ipsection-demo": "src/ipsection/demo", + "labeleditor": "src/labeleditor/lib", + "labeleditor-demo": "src/labeleditor/demo", + "layout": "src/layout/lib", + "layout-demo": "src/layout/demo", + "leftmenu": "src/leftmenu/lib", + "leftmenu-demo": "src/leftmenu/demo", + "linkbutton": "src/linkbutton/lib", + "linkbutton-demo": "src/linkbutton/demo", + "list": "src/list/lib", + "list-demo": "src/list/demo", + "loading": "src/loading/lib", + "loading-demo": "src/loading/demo", + "locale": "src/locale/lib", + "locale-demo": "src/locale/demo", + "menu": "src/menu/lib", + "menu-demo": "src/menu/demo", + "message": "src/message/lib", + "message-demo": "src/message/demo", + "modal": "src/modal/lib", + "modal-demo": "src/modal/demo", + "nav": "src/nav/lib", + "nav-demo": "src/nav/demo", + "ng": "src/ng/lib", + "ng-demo": "src/ng/demo", + "notification": "src/notification/lib", + "notification-demo": "src/notification/demo", + "outline": "src/outline/lib", + "overflow": "src/overflow/lib", + "overflow-demo": "src/overflow/demo", + "pagination": "src/pagination/lib", + "pagination-demo": "src/pagination/demo", + "path": "src/path/lib", + "path-demo": "src/path/demo", + "phonenumber": "src/phonenumber/lib", + "phonenumber-demo": "src/phonenumber/demo", + "popconfirm": "src/popconfirm/lib", + "popconfirm-demo": "src/popconfirm/demo", + "popup": "src/popup/lib", + "productpreview": "src/productpreview/lib", + "productpreview-demo": "src/productpreview/demo", + "progressbar": "src/progressbar/lib", + "progressbar-demo": "src/progressbar/demo", + "progresspie": "src/progresspie/lib", + "progresspie-demo": "src/progresspie/demo", + "radio": "src/radio/lib", + "radio-demo": "src/radio/demo", + "rate": "src/rate/lib", + "rate-demo": "src/rate/demo", + "renderer": "src/renderer/lib", + "rights": "src/rights/lib", + "rights-demo": "src/rights/demo", + "score": "src/score/lib", + "score-demo": "src/score/demo", + "scroll": "src/scroll/lib", + "searchbox": "src/searchbox/lib", + "searchbox-demo": "src/searchbox/demo", + "select": "src/select/lib", + "select-demo": "src/select/demo", + "selectgroup": "src/selectgroup/lib", + "selectgroup-demo": "src/selectgroup/demo", + "skeleton": "src/skeleton/lib", + "skeleton-demo": "src/skeleton/demo", + "slider": "src/slider/lib", + "slider-demo": "src/slider/demo", + "spinner": "src/spinner/lib", + "spinner-demo": "src/spinner/demo", + "steps": "src/steps/lib", + "steps-demo": "src/steps/demo", + "subtitle": "src/subtitle/lib", + "subtitle-demo": "src/subtitle/demo", + "swiper": "src/swiper/lib", + "swiper-demo": "src/swiper/demo", + "switch": "src/switch/lib", + "switch-demo": "src/switch/demo", + "tab": "src/tab/lib", + "tab-demo": "src/tab/demo", + "table": "src/table/lib", + "table-demo": "src/table/demo", + "tag": "src/tag/lib", + "tag-demo": "src/tag/demo", + "tagsinput": "src/tagsinput/lib", + "tagsinput-demo": "src/tagsinput/demo", + "text": "src/text/lib", + "text-demo": "src/text/demo", + "textarea": "src/textarea/lib", + "textarea-demo": "src/textarea/demo", + "themes": "src/themes", + "time": "src/time/lib", + "time-demo": "src/time/demo", + "timeline": "src/timeline/lib", + "timeline-demo": "src/timeline/demo", + "tip": "src/tip/lib", + "tip-demo": "src/tip/demo", + "transfer": "src/transfer/lib", + "transfer-demo": "src/transfer/demo", + "tree": "src/tree/lib", + "tree-demo": "src/tree/demo", + "treeselect": "src/treeselect/lib", + "treeselect-demo": "src/treeselect/demo", + "upload": "src/upload/lib", + "upload-demo": "src/upload/demo", + "utils": "src/utils/lib", + "utils-demo": "src/utils/demo", + "validation": "src/validation/lib", + "validation-demo": "src/validation/demo", + "zoom": "src/zoom/lib", + "zoom-demo": "src/zoom/demo" + } +} diff --git a/build.md b/build.md new file mode 100644 index 0000000..1605545 --- /dev/null +++ b/build.md @@ -0,0 +1,107 @@ +TinyNG 基于 [Angular CLI](https://angular.cn/cli) 和 [Nx CLI](https://nx.dev/reference/commands) 封装了以下(windows系统下)适用本项目的命令: + +建议使用 cmd 命令窗口运行。 + +### 新增一个 library + 执行命令行: + ```bash + ng workspace-generator ti-lib-generator (运行该命令时,会询问你要创建的 library 的名称和版本号,按要求输入即可) + ``` + 或可直接执行命令行: + ```bash + ng workspace-generator ti-lib-generator lib库名 lib版本号 (例如:ng workspace-generator ti-lib-generator alert 1.2.0) + ``` + +### 调试环境启动运行命令 +1. 启动单个组件 demo 项目 + + 执行命令行: + ```bash + ng serve xxx-demo ('xxx' 是组件名,例如:ng serve alert-demo) + ``` + + 或执行命令行: + ```bash + ng run xxx-demo:serve (例如:ng run alert-demo:serve) + ``` + + 可在 angular.json 文件中的 projects 配置中查看各组件 demo 项目名称。 + +2. 启动全部组件 demo 项目 + + 执行命令行: + ```bash + ng serve ng-demo + ``` + + 或执行命令行: + ``` + ng run ng-demo:serve + ``` + +### 生产环境常用打包命令 +1. 打包单个组件 + + 执行命令行: + ```bash + ng build xxx (例如:ng build alert) + ``` + + 或执行命令行: + ```bash + ng run xxx:build (例如:ng run alert:build) + ``` + + 可在 angular.json 文件中的 projects 配置中查看各组件项目名称。 + +2. 打包多个组件 + + 执行命令行: + ```bash + ng run-many --target=build --projects=xxx1,xxx2 (例如:ng run-many --target=build --projects=alert,select) + ``` + + 可在 angular.json 文件中的 projects 配置中查看各组件项目名称。 + +3. 打包全部组件 + + 执行命令行: + ```bash + ng build ng + ``` + + 或执行命令行: + ```bash + ng run ng:build + ``` + +### 生产环境启动预览命令 + +1. 生产环境启动预览(模拟使用组件的开发者实际应用场景)单个组件 demo 项目 + + 执行命令行: + ```bash + ng preview xxx-demo (例如:ng preview alert-demo) + ``` + + 或执行命令行: + ```bash + ng run xxx-demo:preview (例如:ng run alert-demo:preview) + ``` + + 功能:1. 构建打包相关 @opentiny/ng-xxx 发布包(.tgz);2. 在 xxx-demo 项目中 npm install 安装 .tgz 包;3. xxx-demo 项目生产环境构建;4. 启动浏览器在8020 端口运行 + +2. 生产环境启动预览(模拟使用组件的开发者实际应用场景)全部组件 demo 项目 + + 执行命令行: + ```bash + ng preview ng-demo + ``` + + 或执行命令行: + ``` + ng run ng-demo:preview + ``` + + 功能:1. 构建打包相关 @opentiny/ng-xxx 发布包(.tgz);2. 在 ng-demo 项目中 npm install 安装 .tgz 包;3. ng-demo 项目生产环境构建;4. 启动浏览器在8020 端口运行 + diff --git a/build/AppWcModule.ts.ejs b/build/AppWcModule.ts.ejs new file mode 100644 index 0000000..ca90533 --- /dev/null +++ b/build/AppWcModule.ts.ejs @@ -0,0 +1,44 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { Component, Injector, NgModule } from '@angular/core'; +import { createCustomElement } from '@angular/elements'; +import { DemoModules } from './DemoModules'; +<% imports.forEach(importItem => { %> +<%-importItem-%>; +<% }) %> +@Component({ + selector: `app-root`, + template: `` + }) +export class AppComponent { } + +const WCS: any = [<% WCS.forEach(item => { %> + { selector: <%-item.selector%>, component: <%-item.component%> }, +<% }) %>] + +@NgModule({ + imports: [ + DemoModules.allModules, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule // 因为modal,message,collapse,accordion组件使用了angular动画,所以需要引入此模块 + ], + declarations: [ + AppComponent + ], + bootstrap: [AppComponent] + }) + export class AppModule { + constructor(private injector: Injector) { + // 国际化功能开启,测试其它语种的显示,可打开这段注释 + // 设置当前语言,默认为中文 + // TiLocale.setLocale(TiLocale.EN_US); // 设置国际化语种 + for (const item of WCS) { + if (!customElements.get(item.selector)) { + const el: any = createCustomElement(item.component, { injector: this.injector }); + customElements.define(item.selector, el); + } + } + } + + ngDoBootstrap(): void { } +} diff --git a/build/add-default-theme.js b/build/add-default-theme.js new file mode 100644 index 0000000..422df90 --- /dev/null +++ b/build/add-default-theme.js @@ -0,0 +1,106 @@ +const fs = require('fs-extra'); +const path = require('path'); + +const baseDir = process.cwd(); +const basicVarListPath = path.resolve(baseDir, './build/basicVarList.json'); +const baseVarPath = path.resolve(baseDir, './src/themes/basic/basic-var.css'); +const writeFileArr = ['.less']; // 需要插入的文件后缀 +const args = process.argv; +const filePath = args[2] === 'themes' ? `${args[2]}/basic` : `${args[2]}/lib/src`; +const fullPath = path.resolve(baseDir, `./src/${filePath}`); + +// 备份lib内容 +try { + if (fs.pathExistsSync(fullPath)) { + fs.copySync(fullPath, path.resolve(baseDir, `./src_bak/${filePath}`)); + console.log('copy success'); + } +} catch (error) { + console.log(error); +} + +// 将basic-var.css转换成basicVar对象 +const getBasicVar = () => { + const baseList = []; // 直接使用的变量集合 + const varList = []; // 使用 var 的队列 + const calcBaseValue = 4; // 使用 calc 的基础值 + const reg = /(\-\-ti\-\b(common|base)\b(.+?))(?=;)/g; + const baseVar = fs.readFileSync(baseVarPath, "utf8").match(reg); + for (let i = 0; i < baseVar.length; i++) { + const item = baseVar[i]; + const [key, value] = item.split(':'); + if (item.match(/\b(calc)\b/)) { + // 这里计算一下通过 calc 的特殊值 + const value = parseInt(item.match(/[0-9]+/)[0]) * calcBaseValue + 'px'; + baseList.push({ key: key.trim(), value: value.trim() }); + } else if (item.match(/\b(var)\b/)) { + varList.push({ key: key.trim(), value: value.trim() }); + } else { + baseList.push({ key: key.trim(), value: value.trim() }) + } + } + while (varList?.length) { + const item = varList.shift(); + const replacedObj = baseList.find(i => item.value.indexOf(i.key) !== -1); + if (replacedObj) { + baseList.unshift({ key: item.key, value: replacedObj.value }); // 这里添加到数组第二轮循环开始优先查找这些变量 + } + } + return baseList; +} + +// 将basicvar数据保存在./build/basicVarList.json +const writeBasicVarData = () => { + try { + fs.writeFileSync(basicVarListPath, JSON.stringify(getBasicVar())) + } catch (err) { + console.log(err); + } +} + +// 读取basicVar数据 +const getBasicVarList = (path) => { + try { + return fs.readFileSync(path, 'utf8'); + } catch (err) { + console.log(err); + } +} + +if (!fs.pathExistsSync(basicVarListPath)) { + writeBasicVarData(); +} +const basicVarList = JSON.parse(getBasicVarList(basicVarListPath)); + +const readFiles = (filePath) => { + if (fs.pathExistsSync(filePath)) { + const readDir = fs.readdirSync(filePath); + for (let i = 0; i < readDir.length; i++) { + const curDir = `${filePath}/${readDir[i]}` + const stat = fs.statSync(curDir); + if (stat.isDirectory()) { + readFiles(curDir); + } + if (stat.isFile()) { + const isWrite = writeFileArr.some(file => curDir.endsWith(file)); + if (isWrite) { + addDefaultVar(curDir); + } + } + } + } +} + +// 读取less并在var函数中添加默认值 +const addDefaultVar = (path) => { + const reg = /(?<=var\()(\-\-ti\-\b(common|base)\b(.+?))(?=\))/g; + const addDefaultVal = fs.readFileSync(path, "utf-8").replace(reg, (str) => { + const val = str.split(',')[0]; + const defVal = basicVarList.find(item => item.key === val); + return defVal && `${defVal.key}, ${defVal.value}`; + }); + console.log(`当前文件 ${path} 需要插入; 正在插入中...`); + fs.writeFileSync(path, addDefaultVal, 'utf-8'); +} + +readFiles(fullPath); diff --git a/build/autoCreateDemoProjectJson.js b/build/autoCreateDemoProjectJson.js new file mode 100644 index 0000000..b98468f --- /dev/null +++ b/build/autoCreateDemoProjectJson.js @@ -0,0 +1,21 @@ +const path = require('path'); +const fs = require('fs-extra'); +const ejs = require('ejs'); + +const rootPath = path.join(__dirname, '../src'); +const ejsPath = path.join(__dirname, './demoProject.json.ejs'); +const compDirs = fs.readdirSync(rootPath); +const blackFiles = ['ng']; +const { ROOT_EXCLUDES } = require('./unitTestingResources/config'); + +compDirs.forEach(dir => { + if (blackFiles.includes(dir)) { + return; + } + const dirPath = path.join(rootPath, `${dir}/demo`); + if (fs.pathExistsSync(dirPath)) { + ejs.renderFile(ejsPath, { libDir: dir, shouldTest: !ROOT_EXCLUDES.includes(dir) }, {}, function (err, str) { + fs.writeFileSync(path.join(dirPath, 'project.json'), str); + }); + } +}); \ No newline at end of file diff --git a/build/autoCreateLibProjectJson.js b/build/autoCreateLibProjectJson.js new file mode 100644 index 0000000..50f89e9 --- /dev/null +++ b/build/autoCreateLibProjectJson.js @@ -0,0 +1,21 @@ +const path = require('path'); +const fs = require('fs-extra'); +const ejs = require('ejs'); + +const rootPath = path.join(__dirname, '../src'); +const ejsPath = path.join(__dirname, './libProject.json.ejs'); +const compDirs = fs.readdirSync(rootPath); +const blackFiles = ['ng', 'themes']; + +compDirs.forEach(dir => { + if (blackFiles.includes(dir)) { + return; + } + + const dirPath = path.join(rootPath, `${dir}/lib`); + if (fs.pathExistsSync(dirPath)) { + ejs.renderFile(ejsPath, { libDir: dir }, {}, function (err, str) { + fs.writeFileSync(path.join(dirPath, 'project.json'), str); + }); + } +}); \ No newline at end of file diff --git a/build/autoCreateWebsite.js b/build/autoCreateWebsite.js new file mode 100644 index 0000000..a6642ef --- /dev/null +++ b/build/autoCreateWebsite.js @@ -0,0 +1,172 @@ +/** + * 该脚本用于自动化生成AppWcModules.ts文件、组件的md文件 js文件 + * 在根目录下,执行`npm run create:websitefiles`命令即可生效 + * 如果有需要忽略的文件,使用ignore参数处理,例如:npm run createWebsiteFiles --ignore button,alert +*/ +const fs = require('fs-extra'); +const path = require('path'); +const ejs = require('ejs'); +var argv = require('minimist')(process.argv.slice(2)); +const ignoreComs = argv._; +const blackFiles = ['app.html', 'app.server.module.ts', 'AppComponent.ts', 'AppModule.ts', +'AppWcModule.ts', 'DemoModules.ts', 'IndexComponent.ts', 'NoneComponent.ts', +'app\\zoom', 'app\\vars', 'app\\datedominator', 'app\\dateedit','app\\tab-old', +'app\\droplist', 'app\\drop', 'app\\dominator', 'app\\drag', +'app\\list', 'app\\many', 'app\\tokens', 'table-row-drag1.html', 'app\\allcomp', 'src\\ng' +]; // 文件黑名单 //TODO: + +const appDir = path.resolve(__dirname, '../src'); // __dirname是当前模块的目录名 + +const filesList = []; +const selectors = []; +const importComs = []; + + +function isContain(fullPath, blackFiles) { + let ans = false; + for (let i = 0; i < blackFiles.length; i++) { + if (fullPath.includes(blackFiles[i])) { + ans = true; + break; + } + } + + return ans; +} + +function getFileList(dir, filesList = [], isFirst = false) { + const files = fs.readdirSync(dir); // 返回一个指定目录下的数组对象。 + files.forEach((item, index) => { + var fullPath = path.resolve(dir, isFirst ? `${item}/demo/src/app` : item); // 将路径或路径片段的序列解析为绝对路径。给定的路径序列从右到左处理 + if (!fs.pathExistsSync(fullPath)) { + return; + } + const stat = fs.statSync(fullPath); // 获取路径的 + if (stat.isDirectory()) { + getFileList(fullPath, filesList); //递归读取文件 + } else { + if (stat.isFile() && !isContain(fullPath, blackFiles) && fullPath.endsWith('.html')) { + const pathArr = fullPath.split('\\'); + if (pathArr.includes('leftmenu') && !pathArr.includes('website-views')) { + return; + } + const appIndex = pathArr.indexOf('app'); + const compName = pathArr[appIndex + 1]; // 组件名称是app目录的下一层 + const firstSrcIndex = pathArr.indexOf('src'); + filesList.push({ + component: compName, + path: `../../../../${pathArr.slice(firstSrcIndex + 1).join('/')}` + }); + } + } + }); + + return filesList; +} + +function transform2CamelCase(arr) { + let res = []; + for (let i = 0; i < arr.length; i++) { + let temp = arr[i]; + res.push(temp[0].toUpperCase() + temp.slice(1)); + } + + return res.join(''); +} + +function getSelectorAndComp(filesList) { + filesList.forEach((item, index) => { + let lastItem = item.path.split('/').slice(-1).join(''); + let temp = lastItem.replace('.html', ''); + let com = `${transform2CamelCase(temp.split('-'))}${'Component'}` + + let tempSelector = { + selector: `\'${'website-tiny-'}${temp.replace('-website-view', '')}\'`, + component: com, + }; + + let pathArr= item.path.split('/'); + let dir = pathArr.slice(0, pathArr.length - 1).join('/'); + let tempData = `import { ${com} } from \'./${dir}/${com}\'`; + + importComs.push(tempData); + selectors.push(tempSelector); + }) +} + +const ejsConfig = { + imports: { + ejsFile: path.resolve(__dirname, './AppWcModule.ts.ejs'), // 模板文件 + file: path.resolve(__dirname, './autoAppWcModule.ts') // 生成的文件 + } +} + +const generatorAppCode = () => { + Object.keys(ejsConfig).forEach((key) => { + ejs.renderFile(ejsConfig[key].ejsFile, {imports: importComs, WCS: selectors}, {}, function (err, str) { + fs.writeFileSync(ejsConfig[key].file, str); + }); + }); +} +const copy = async () => { + await fs.copy( + path.resolve(__dirname, 'autoAppWcModule.ts'), + path.resolve(__dirname, '../src/ng/demo/src/app/AppWcModule.ts') + ) + + fs.removeSync(path.resolve(__dirname, 'autoAppWcModule.ts')) +} + +const componentejsConfig = { + cn: { + file: path.resolve(__dirname, './component.cn.ejs'), // 模板文件 + }, + en: { + file: path.resolve(__dirname, './component.en.ejs'), // 模板文件 + }, + js: { + file: path.resolve(__dirname, './component.ejs'), // 模板文件 + } +} + +const deleteWebdoc = () => { + const folderPath = path.resolve(__dirname, '../src/app/'); + filesList.forEach((item, index) => { + if(ignoreComs[0]?.includes(item.component)) { + return; + } + fs.remove(`${folderPath}/${item.component}/webdoc/`, ()=> {}); + }); +} +const generatorComponentCode = () => { + const folderPath = path.resolve(__dirname, '../src/app/'); // TODO: + filesList.forEach((item, index) => { + if(ignoreComs[0]?.includes(item.component)) { + return; + } + fs.ensureDir(`${folderPath}/${item.component}/webdoc`, function (err) { + const filePath = path.resolve(folderPath, `${item.component}/webdoc`); + const pathMap = new Map([ + ['cn', path.resolve(filePath, `${item.component}.cn.md`)], + ['en', path.resolve(filePath, `${item.component}.en.md`)], + ['js', path.resolve(filePath, `${item.component}.js`)]]); + Object.keys(componentejsConfig).forEach((key) => { + ejs.renderFile(componentejsConfig[key].file, {}, {}, function (err, str) { + let filePath = pathMap.get(key); + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, str); + } + }); + }); + }); + + }); +} + +getFileList(appDir, filesList, true); +getSelectorAndComp(filesList); +generatorAppCode(); +copy(); +// deleteWebdoc(); // 删除webdoc中目录 +// generatorComponentCode(); + diff --git a/build/bindTest.js b/build/bindTest.js new file mode 100644 index 0000000..dda5cf8 --- /dev/null +++ b/build/bindTest.js @@ -0,0 +1,53 @@ +// 处理用户输入参数,获取目标组件名 +const ARGS = process.argv; +if (ARGS.length !== 3) { + throw new TypeError('请输入要初始化的组件名称!'); + return; +} +const TARGET_COMPONENT = process.argv[2]; + +// 遍历 src/ 目录,获取所有组件名称 +const { SEP, ROOT_RESOLVE_PATH : ROOT, ROOT_EXCLUDES, KARMA_TEMPLATE, TEST_TS_TEMPLATE, TSCONFIG_SPEC_TEMPLATE, PROJECT_TEST_INFO_TEMPLATE } = require('./unitTestingResources/config'); +const fs = require('fs'); +const ALL_COMPONENTS = fs.readdirSync(ROOT); + +// 处理非法的组件名 +if (ROOT_EXCLUDES.includes(TARGET_COMPONENT)) { + throw new RangeError(`${TARGET_COMPONENT} 是保留组件,不提供单测能力!`); + return; +} +if (!ALL_COMPONENTS.includes(TARGET_COMPONENT)) { + throw new ReferenceError(`未能找到 ${TARGET_COMPONENT}!`); + return; +} + +// 处理已经初始化过的组件名 +const DEMO_ROOT = `${ROOT}${SEP}${TARGET_COMPONENT}${SEP}demo`; +const DEMO_FILES = fs.readdirSync(DEMO_ROOT); +if (DEMO_FILES.includes('karma.conf.js')) { + console.log(`${TARGET_COMPONENT} 的单元测试环境已经初始化,请直接新建或修改脚本。`); + return; +} + +// 写入 test.ts +fs.copyFileSync(TEST_TS_TEMPLATE, `${DEMO_ROOT}${SEP}test.ts`) +// 写入 karma.conf.js +fs.copyFileSync(KARMA_TEMPLATE, `${DEMO_ROOT}${SEP}karma.conf.js`) +// 写入 tsconfig.spec.json +fs.copyFileSync(TSCONFIG_SPEC_TEMPLATE, `${DEMO_ROOT}${SEP}tsconfig.spec.json`) + +// 修改 project.json +const CURRENT_PROJECT = `${DEMO_ROOT}${SEP}project.json` +fs.open(CURRENT_PROJECT, 'wx', (err, fd) => { + if (err) { + if (err.code === 'EEXIST') { + } else { + throw err; + } + } else { + let project_test_content = fs.readFileSync(PROJECT_TEST_INFO_TEMPLATE, 'utf8') + let project_content = fs.readFileSync(CURRENT_PROJECT, 'utf8') + project_content = project_content.substring(0, project_content.length - 4).concat(project_test_content.replaceAll('##component##', TARGET_COMPONENT)) + fs.writeFileSync(CURRENT_PROJECT, project_content) + } +}); \ No newline at end of file diff --git a/build/build-api.js b/build/build-api.js new file mode 100644 index 0000000..d0ea075 --- /dev/null +++ b/build/build-api.js @@ -0,0 +1,933 @@ +/** + * 基于 CompoDoc 导出的组件数据,提取组件 api + * 使用方式:在 Tiny3 根目录下,执行 `npm run build:api` + * 如组件源码无变化,可使用 `npm run build:api -- --quick` 跳过 CompoDoc 导出 + */ +const { execSync } = require('child_process'); +const path = require('path'); +const chalk = require('chalk'); +const fs = require('fs-extra'); +const log = require('fancy-log'); +const JSON5 = require('json5'); +const minimist = require('minimist'); + +chalk.enabled = true; +chalk.level = 1; + +const processArgvs = minimist(process.argv.slice(2)); +const baseDir = process.cwd(); +const compoDocJsonFile = path.resolve(baseDir, './dist/tinydoc/documentation.json'); +const docDir = path.resolve(baseDir, './src'); +const outputDocDir = path.resolve(baseDir, './dist/tinydoc/tinyapis/'); +// 全局性的文档 +const globalDocDir = ['interfaces', 'types']; +// 过滤的目录 +const excludeDocDir = [ + 'allcomp', + 'base', + 'code', + 'datedominator', + 'dateedit', + 'dominator', + 'drag', + 'droplist', + 'drop', + 'ng', + 'imagepreview', + 'list', + 'many', + 'progresspie', + 'tab-old', + 'tokens', + // 'utils', + 'vars', + 'webdoc', + 'zoom', + // 接口和类型额外处理 + ...globalDocDir, +]; + +const TipsConfig = { + exportJsonSuccess: 'CompoDoc 导出文档 JSON 成功', + exportJsonFail: 'CompoDoc 导出文档 JSON 失败', + copyDocFail: '拷贝源目录失败', + getDemosDataFail: '获取组件 demos 数据失败', + getDisplayApis: '解析 demos 数据获取需展示 apis 项', + getApisFail: '提取下列 apis 内容失败', + unExistedCodeFiles: 'CodeFiles 不存在', + generateSuccess: '文档生成成功', + generateFail: '文档生成失败', +}; + +const Logger = { + types: { + success: 'success', + warn: 'warn', + error: 'error', + }, + + colors: { + success: 'green', + warn: 'yellow', + error: 'red', + }, + + log: (message) => { + if (typeof message === 'object') { + const { type = Logger.types.success, title = '', info = '' } = message; + const color = Logger.colors[type]; + + log(`${chalk[color](`[${title}]:`)} ${info}`); + } else { + log(message); + } + }, +}; + +const Utils = { + firstUpperCase: (str) => { + return str.replace(/^\S/, (s) => s.toUpperCase()); + }, + + hyphenToCamelCase: (str, isPascalCase) => { + const ret = str.replace(/-(\w)/g, (match, $1) => { + return $1.toUpperCase(); + }); + + return isPascalCase ? Utils.firstUpperCase(ret) : ret; + }, + + convertListToMap: (list, key, lowerCaseKey) => { + const map = new Map(); + + list.forEach((item) => { + map.set(lowerCaseKey ? item[key].toLowerCase() : item[key], item); + }); + + return map; + }, + + escape: (str) => { + if (!str) { + return str; + } + + const replacements = { + '<': '<', + '>': '>', + }; + + return str.replace(/[<>]/g, (match) => replacements[match]); + }, +}; + +const DocItemType = { + interface: 'interface', + pipe: 'pipe', + class: 'class', + injectable: 'injectable', + component: 'component', + directive: 'directive', + service: 'service', + typealias: 'typealias', +}; + +/** + * 处理 api 数据 + */ +const ApiFormatter = { + format: function (data) { + // typealias 中字段为 subtype + const { name, type, subtype } = data; + const itemType = type || subtype; + let ret = {}; + + switch (itemType) { + case DocItemType.interface: + case DocItemType.pipe: + case DocItemType.class: + case DocItemType.injectable: + ret = this.formatCommonElement(data); + break; + case DocItemType.component: + case DocItemType.directive: + ret = this.formatCompOrDirective(data); + break; + case DocItemType.typealias: + ret = this.formatTypealias(data); + break; + } + + return { + name, + type: itemType === DocItemType.injectable ? DocItemType.service : itemType, + ...ret, + }; + }, + + /** + * 提取 typealias 的 api 数据 + */ + formatTypealias: function (data) { + const { rawtype } = data; + + return { + value: rawtype, + desc: this.formatDescription(data), + }; + }, + + /** + * 提取 interface | pipe | class | injectable 的 api 数据 + */ + formatCommonElement: function (data) { + const { properties = [], methods = [] } = data; + + return { + properties: this.formatApis(properties), + methods: this.formatMethods(methods), + }; + }, + + /** + * 提取 Components | Directive api 数据 + */ + formatCompOrDirective: function (data) { + const { inputsClass = [], outputsClass = [], propertiesClass = [], methodsClass = [] } = data; + + return { + properties: this.formatApis(inputsClass), + events: this.formatApis(outputsClass), + slots: this.getSlots(propertiesClass), + methods: this.formatMethods(methodsClass), + }; + }, + + formatApis: function (data, itemFormatter) { + if (!data || !data.length) { + return; + } + + return data.map((item) => (typeof itemFormatter === 'function' ? itemFormatter(item) : this.formatApiItem(item))); + }, + + formatApiItem: function (data) { + const { name, defaultValue, type, jsdoctags } = data; + const jsdocTags = this.formatJsdocTags(jsdoctags) || []; + const customTypeTag = jsdocTags.find((item) => item.tagName === 'customType'); + // 优先级:jsdoc @customType > 代码中 type + const outputType = customTypeTag ? customTypeTag.comment : Utils.escape(type); + // 优先级:代码中默认值 > jsdoc 中默认值 + let outputDefault = Utils.escape(defaultValue); + + if (!outputDefault) { + const defaultTag = jsdocTags.find((item) => item.tagName === 'default'); + + if (defaultTag) { + outputDefault = defaultTag.comment; + } + } + + return { + name, + type: outputType, + defaultValue: outputDefault, + desc: this.formatDescription(data), + }; + }, + + formatMethods: function (data) { + return this.formatApis(data, this.formatMethodItem.bind(this)); + }, + + formatMethodItem: function (data) { + const { args, returnType } = data; + const argsStr = `${args + .map((arg) => { + const { name, type, optional } = arg; + return `${name}${optional ? '?' : ''}${type ? `: ${type}` : ''}`; + }) + .join(', ')}`; + const funcType = `(${argsStr}) => ${returnType ? returnType : 'void'}`; + + return { + ...this.formatApiItem(data), + type: Utils.escape(funcType), + }; + }, + + formatJsdocTags: function (data = []) { + const ret = []; + + data.forEach((item) => { + const { tagName = {}, comment = '' } = item; + let text = tagName.escapedText; + + if (text === 'defaultvalue') { + text = 'default'; + } + + if (text) { + ret.push({ + tagName: text, + comment: comment.replace(/\n/g, ''), + }); + } + }); + + return ret; + }, + + /** + * 描述数据,暂时用 name 作为英文描述 + */ + formatDescription: function (data) { + const { name, description = '' } = data; + + return { + 'zh-CN': description.replace(/\n/g, '').replace(/\[([^\[\]]*)\]\{@link[^\{\}]*\}/g, (match, $1) => $1), + 'en-US': name, + }; + }, + + /** + * 是否插槽属性 + */ + isSlotApi: function (prop) { + const { decorators } = prop; + const slotDecorators = ['ContentChild', 'ContentChildren']; + + return decorators && decorators.find((item) => slotDecorators.includes(item.name)); + }, + + /** + * props 中提取 slots + */ + getSlots: function (props) { + return this.formatApis(props.filter((prop) => this.isSlotApi(prop))); + }, +}; + +/** + * 解析 compocDoc 导出的 json,获取组件 api 数据 + */ +function getDocData() { + const isQuickMode = processArgvs.quick; + const isFileExisted = fs.existsSync(compoDocJsonFile); + + if (!isQuickMode) { + fs.removeSync(compoDocJsonFile); + } + + if (!isQuickMode || !isFileExisted) { + Logger.log(`${chalk.green.bold('======> CompoDoc 导出文档 JSON 中,稍等一小会...')}`); + execSync('npx compodoc --exportFormat json', { maxBuffer: 1024 * 1024 * 10 }); // 超出 maxBuffer 会停止执行 + + if (fs.existsSync(compoDocJsonFile)) { + Logger.log({ + title: TipsConfig.exportJsonSuccess, + info: `path -> ${compoDocJsonFile}`, + }); + } else { + Logger.log({ + type: Logger.types.error, + title: TipsConfig.exportJsonFail, + info: '请执行 `npx compodoc --exportFormat json` 检查', + }); + + return; + } + } + + const docJson = fs.readJSONSync(compoDocJsonFile, { encoding: 'utf8' }); + const { components = [], directives = [], interfaces = [], injectables = [], classes = [], pipes = [] } = docJson; + const key = 'name'; + + return { + ...docJson, + componentMap: Utils.convertListToMap(components, key), + directiveMap: Utils.convertListToMap(directives, key), + serviceMap: Utils.convertListToMap(injectables, key), + interfaceMap: Utils.convertListToMap(interfaces, key), + pipeMap: Utils.convertListToMap(pipes, key), + classMap: Utils.convertListToMap(classes, key), + }; +} + +/** + * 获取组件文档文件夹列表 + */ +function getDocDirList() { + const result = []; + const dirPath = path.resolve(baseDir, './src'); + const dirs = fs.readdirSync(dirPath); + dirs.forEach((dir) => { + const filePath = path.resolve(dirPath, dir); + const stat = fs.statSync(filePath); + + if (!excludeDocDir.includes(dir) && stat.isDirectory()) { + const childDirsPath = path.resolve(filePath, 'demo/src/app'); + if (fs.pathExistsSync(childDirsPath)) { + const childDirs = fs.readdirSync(childDirsPath); + + childDirs.forEach((childDir) => { + const childFilePath = path.resolve(childDirsPath, childDir); + const childStat = fs.statSync(childFilePath); + + if (childStat.isDirectory()) { + result.push([dir, childDir]); + } + }); + } + } + }); + + return result; + +} + +/** + * 根据 demos 中配置的 apis 字段,获取需要展示的文档元素(Components, Directives, etc.) + */ +function getDisplayDocItems(demosData) { + if (!demosData) { + return; + } + + const { displayApis = [], demos = [] } = demosData; + const ret = [...displayApis]; + + demos.forEach((demo) => { + const { apis } = demo; + if (!apis || !apis.length) { + return; + } + + apis.forEach((item) => { + // 取第一段 + const name = item.split('.')[0]; + ret.push(name); + }); + }); + + return [...new Set(ret)]; +} + +/** + * apis 增加 demoId 字段 + */ +function appendDemoIdToApis(demos, apis) { + if (!demos || !apis) { + return; + } + + const apisMap = new Map(); + const demosMap = new Map(); + + // 收集 demo 下 apis 字段 + demos.forEach((demo) => { + const { apis: demoApis, demoId } = demo; + if (!demoApis || !demoApis.length) { + return; + } + + demoApis.forEach((item) => { + if (!demosMap.get(item)) { + demosMap.set(item, demoId); + } + }); + }); + + if (!demosMap.size) { + return; + } + + // 转成 map 如:{ 'TiButtonComponent.properties.disabled': {} } + apis.forEach((api) => { + const { name } = api; + const keys = ['properties', 'events', 'slots', 'methods']; + + keys.forEach((key) => { + const item = api[key]; + if (!item) { + return; + } + + item.forEach((subItem) => { + apisMap.set(`${name}.${key}.${subItem.name}`, subItem); + }); + }); + }); + + // 打上 demoId + demosMap.forEach((val, key) => { + const target = apisMap.get(key); + + if (target) { + Object.assign(target, { + demoId: val, + }); + } + }); +} + +/** + * demos 增加 tag 和 codeFiles 数据, 如果已手动填写,以手动填写为准。 + * 自动生成规则: + * demoId = 'button-color' => tag: `website-tiny-button-color`, codeFiles: ['button-color.html', 'ButtonColorComponent.ts'] + */ +function appendMetaToDemos(demos) { + demos.forEach((demo, idx) => { + const { demoId, tag, codeFiles } = demo; + + if (!demoId) { + return; + } + + const addedMeta = { + tag: tag || `website-tiny-${demoId}`.toLowerCase(), + codeFiles: codeFiles || [`${demoId}.html`, `${Utils.hyphenToCamelCase(demoId, true)}Component.ts`], + }; + + demos[idx] = { + ...demo, + ...addedMeta, + }; + }); +} + +/** + * 类型打上锚点链接 + */ +function appendLinkToType(apis = [], linkMap = {}) { + const urlPrefix = '/tiny-ng/components'; + const ownApiList = apis.map((item) => item.name); + const itemList = []; + + apis.forEach((api) => { + const keys = ['properties', 'events', 'slots', 'methods']; + + keys.forEach((key) => { + const list = api[key]; + + if (list) { + itemList.push(...list); + } + }); + }); + + itemList.forEach((item) => { + const keys = ['type', 'defaultValue']; + + keys.forEach((key) => { + const str = item[key]; + + if (!str) { + return; + } + + item[key] = str.replace(/Ti[A-Za-z]+/g, (name) => { + let href = ''; + + if (ownApiList.includes(name)) { + href = `#${name}`; + } else if (linkMap[name]) { + href = `${urlPrefix}/${linkMap[name]}#${name}`; + } + + return href ? `${name}` : name; + }); + }); + }); +} + +/** + * 检查 demos 是否符合规范 + */ +function validDemos(demos, dir) { + demos.forEach((demo) => { + const { codeFiles, demoId } = demo; + + if (!codeFiles || !codeFiles.length) { + return; + } + + const dirPath = path.resolve(docDir, `./${dir[0]}/demo/src/app/${dir[1]}`); + const unExistedFiles = codeFiles.filter((file) => !fs.existsSync(`${dirPath}/${file}`)); + + if (unExistedFiles.length) { + Logger.log({ + type: Logger.types.warn, + title: TipsConfig.unExistedCodeFiles, + info: `demoId: ${demoId} 下 codeFiles: ${unExistedFiles.join(' ')} 不存在, 请检查 demoId 和示例文件是否正确!`, + }); + } + }); +} + +/** + * 获取组件 demos 数据,文件为 `${组件目录名}-demos.js` + */ +function getDemosData(dir) { + const demosFile = path.resolve(docDir, `./${dir[0]}/demo/src/app/${dir[1]}/webdoc/${dir[1]}-demos.js`); + let data; + + if (!fs.existsSync(demosFile)) { + return { + message: `Demos 文档不存在, 请补充 ${dir[1]}-demos.js!`, + }; + } + + const demosFileStr = fs.readFileSync(demosFile, { encoding: 'utf8' }); + const match = demosFileStr + // 兼容 demo.js 中字符串模板 + .replace(/[\n\r\t]\s*/g, '') + .replace(/(["'`])(?:(?=(\\?))\2.)*?\1/g, (str) => { + return str.replace(/^`(.*)`$/, ($0, $1) => `"${$1.trim()}"`); + }) + .match(/(?![\s\S]*export\s*default[\s\S]*){[\s\S]*}/); + + try { + data = match && JSON5.parse(match[0]); + } catch (err) { + return { + message: `JSON.parse 失败, ${err}`, + }; + } + + if (typeof data === 'object') { + return { data }; + } else { + return { + message: `${dir}-demo.js 无有效数据!`, + }; + } +} + +/** + * 过滤组件中不需要显示的 apis + */ +function filterApis(apiData, ignoreApis = []) { + if (!ignoreApis.length) { + return apiData; + } + + const ret = { ...apiData }; + const { name } = ret; + const keys = ['properties', 'events', 'slots', 'methods']; + + keys.forEach((key) => { + const list = ret[key]; + + if (!list || !list.length) { + return; + } + + ret[key] = list.filter((item) => { + const { name: attrName } = item; + const attrKey = `${name}.${key}.${attrName}`; + + return !ignoreApis.includes(attrKey); + }); + }); + + return ret; +} + +/** + * 获取组件 apis 数据 + * types 统一放到单独页面,displayItems 中的 types 不会呈现在组件文档中 + */ +function getApisData(docData, displayItems, ignoreApis = []) { + const ret = []; + // interface | class 无命名规则 + const reg = /^(?:\w)+(component|directive|service|pipe)$/i; + + displayItems.forEach((name) => { + const match = name.match(reg); + const type = match && match[1].toLowerCase(); + let apiData; + + if (type) { + const key = `${type}Map`; + apiData = docData[key] && docData[key].get(name); + } else { + const { classMap, interfaceMap } = docData; + apiData = classMap.get(name) || interfaceMap.get(name); + } + + if (apiData) { + const formattedData = filterApis(ApiFormatter.format(apiData), ignoreApis); + + ret.push(formattedData); + } + }); + + return ret; +} + +/** + * 获取 interfaces & types + */ +function getInterfacesAndTypes(docData) { + const { interfaces = [], miscellaneous = {} } = docData; + const { typealiases = [] } = miscellaneous; + const formatList = (list) => list.map((item) => ApiFormatter.format(item)); + + return { + interfaces: formatList(interfaces), + types: formatList(typealiases), + }; +} + +/** + * 拷贝文档源目录到 dist, 只复制文件夹部分 + */ +async function copyDocDirToDist(docDirList) { + await fs.remove(outputDocDir); + + return Promise.all( + docDirList.map(async (dir) => { + const filePath = path.resolve(docDir, `./${dir[0]}/demo/src/app/${dir[1]}`); + const stat = await fs.stat(filePath); + + if (stat.isDirectory()) { + return fs.copy(filePath, `${outputDocDir}/${dir[1]}`); + } + }) + ); +} + +/** + * 全局性文档( interfaces & types )数据 + */ +function getGlobalDocData(docDirList = [], docData) { + const globalData = getInterfacesAndTypes(docData); + + return docDirList.map((dir) => { + const apisData = globalData[dir]; + + return { + dir, + apisData, + }; + }); +} + +function handleDatas(docDirList, docData, types) { + return docDirList.map((dir) => { + // 收集处理过程中的 log 信息,生成文件时再输出 + const logs = []; + const { data: demosData, message } = getDemosData(dir); + if (!demosData) { + logs.push({ + type: Logger.types.error, + title: TipsConfig.getDemosDataFail, + info: message, + }); + return { dir, logs }; + } + + const displayItems = getDisplayDocItems(demosData) || []; + const hasDisplayItems = displayItems.length; + + if (hasDisplayItems) { + logs.push({ + title: TipsConfig.getDisplayApis, + info: `${displayItems.join(' ')}`, + }); + } else { + logs.push({ + type: Logger.types.warn, + title: TipsConfig.getDisplayApis, + info: `需展示 apis 项为空,请检查 ${dir[1]}-demos.js 下 apis 字段是否填写正确!`, + }); + } + + // ignoreApis 用于过滤组件中的某个属性、方法等 + const { ignoreApis } = demosData; + const apisData = hasDisplayItems ? getApisData(docData, displayItems, ignoreApis) : []; + + if (apisData.length < displayItems.length) { + const list = apisData.map((item) => item.name); + // types 统一放到单独页面,不会呈现在组件文档,不提示不存在 + const unExistedItems = displayItems.filter((item) => !list.includes(item) && !types.includes(item)); + + if (unExistedItems.length) { + logs.push({ + type: Logger.types.warn, + title: TipsConfig.getApisFail, + info: `${unExistedItems.join(' ')}, 请检查是否存在或被 ignore !`, + }); + } + } + return { dir, logs, demosData, apisData }; + }); +} + + +/** + * 普通组件文档数据 + */ +function getCompsDocData(docDirList = [], docData, globalDocData = []) { + // 收集 interface & type name 列表 + const globalApiList = globalDocData.reduce((acc, cur) => { + const { dir, apisData = [] } = cur; + const list = apisData.map((item) => item.name); + acc[dir] = list; + return acc; + }, {}); + const { types = [] } = globalApiList; + + return handleDatas(docDirList, docData, types); +} + +/** + * 获取组件、指令等等和目录的对应关系 + */ +function getLinkMap(data) { + if (!data || !data.length) { + return; + } + + const linkMap = {}; + + data.forEach((item) => { + const { dir, apisData } = item; + + if (!apisData || !apisData.length) { + return; + } + + apisData.forEach((item) => { + const { name } = item; + + // 以第一个收集到的为准 + if (!linkMap[name]) { + linkMap[name] = dir; + } + }); + }); + + return linkMap; +} + +/** + * 写入组件文档 js + */ +function writeDocFile(json, dir) { + const outputWebdocPath = path.resolve(outputDocDir, `./${dir[1]}/webdoc/`); + const apiFilePath = `${outputWebdocPath}/${dir[1]}.js`; + const apisJsonStr = JSON5.stringify(json, { + space: 2, + quote: '"', + }); + console.log(outputWebdocPath); + // 删除 output 下的不需要的 demos js + fs.remove(`${outputWebdocPath}/${dir[1]}-demos.js`); + fs.ensureDirSync(outputWebdocPath); + // 改为同步,log 信息不会乱 + try { + fs.writeFileSync(apiFilePath, `export default ${apisJsonStr}`); + + Logger.log({ + title: TipsConfig.generateSuccess, + info: `path -> ${apiFilePath}`, + }); + } catch (err) { + Logger.log({ + type: Logger.types.error, + title: TipsConfig.generateFail, + info: err, + }); + } +} + +function dealAllDocData(allDocData) { + // 收集组件、指令等等和目录的对应关系,处理锚点用 + const linkMap = getLinkMap(allDocData); + const defaultData = { + column: 2, + }; + allDocData.forEach((item) => { + const { logs = [], demosData, apisData } = item; + const dir = Array.isArray(item.dir) ? item.dir : [item.dir, item.dir]; + const isCompDocDir = !globalDocDir.includes(dir[1]); + + const apisKey = dir[1] === 'types' ? 'types' : 'apis'; + + Logger.log(`${chalk.green.bold(`======> ${dir[1]} 文档生成中...`)}`); + + // 输出处理数据时收集的 logs + logs.forEach((item) => Logger.log(item)); + + // 一般组件文档 + if (isCompDocDir) { + if (!demosData) { + Logger.log({ + type: Logger.types.error, + title: TipsConfig.generateFail, + info: `${dir[1]} demos 数据获取失败!`, + }); + + return; + } + + const { demos = [] } = demosData; + + // apis 数据增加 demoId + appendDemoIdToApis(demos, apisData); + // demos 数据增加 tag & codeFiles 字段 + appendMetaToDemos(demos); + // 检查 demos 下的 codefiles 是否存在 + validDemos(demos, dir); + } + + // 类型打锚点 + appendLinkToType(apisData, linkMap); + + const json = { + ...(isCompDocDir ? { ...defaultData, ...demosData } : {}), + [apisKey]: apisData, + }; + + writeDocFile(json, dir); + }); +} + +/** + * 生成 api 文档 + */ +async function generateApiDoc() { + const docData = getDocData(); + + if (!docData) { + return; + } + + Logger.log(`${chalk.green.bold('======> 组件文档生成中...')}`); + + // 一般组件文档 + const docDirList = getDocDirList(); + + try { + await copyDocDirToDist(docDirList); + } catch (err) { + Logger.log({ + type: Logger.types.error, + title: TipsConfig.copyDocFail, + info: err, + }); + + return; + } + + // interface & type 文档 + const globalDocData = getGlobalDocData(globalDocDir, docData); + const compsDocData = getCompsDocData(docDirList, docData, globalDocData); + const allDocData = [...globalDocData, ...compsDocData]; + + dealAllDocData(allDocData) +} + +generateApiDoc(); diff --git a/build/buildThemes.js b/build/buildThemes.js new file mode 100644 index 0000000..81f1a1a --- /dev/null +++ b/build/buildThemes.js @@ -0,0 +1,13 @@ +const { execSync } = require('child_process'); +const path = require('path'); +const fs = require('fs-extra'); + +execSync('npx lessc src/themes/basic/build.less dist/libs/themes/styles.css'); + +execSync('npx lessc src/themes/theme-default/build.less dist/libs/themes/theme-default.css'); +execSync('npx lessc src/themes/theme-blue/build.less dist/libs/themes/theme-blue.css'); +execSync('npx lessc src/themes/theme-green/build.less dist/libs/themes/theme-green.css'); +execSync('npx lessc src/themes/theme-purple/build.less dist/libs/themes/theme-purple.css'); +execSync('npx lessc src/themes/theme-red/build.less dist/libs/themes/theme-red.css'); + +fs.copy(path.resolve(__dirname, '../src/themes/package.json') , path.resolve(__dirname, '../dist/libs/themes/package.json')); \ No newline at end of file diff --git a/build/buildwc.js b/build/buildwc.js new file mode 100644 index 0000000..e17e6d2 --- /dev/null +++ b/build/buildwc.js @@ -0,0 +1,17 @@ +const fs = require('fs-extra'); +const path = require('path'); +const { execSync } = require('child_process'); +// 1.删除 dist 目录 +execSync('npm run clean'); + +// 2.编译生产环境 tiny3demo +execSync('ng run ng-demo:build:wc --skip-nx-cache'); +// 3.编译基础样式 +execSync('npx lessc src/themes/basic/build.less dist/apps/ng/assets/themes/styles.css'); +// 4.编译主题样式 +execSync('npx lessc src/themes/theme-default/build.less dist/apps/ng/assets/themes/theme-default.css'); +execSync('npx lessc src/themes/theme-blue/build.less dist/apps/ng/assets/themes/theme-blue.css'); +execSync('npx lessc src/themes/theme-green/build.less dist/apps/ng/assets/themes/theme-green.css'); +execSync('npx lessc src/themes/theme-purple/build.less dist/apps/ng/assets/themes/theme-purple.css'); +execSync('npx lessc src/themes/theme-red/build.less dist/apps/ng/assets/themes/theme-red.css'); + diff --git a/build/clear-default-theme.js b/build/clear-default-theme.js new file mode 100644 index 0000000..2f9f114 --- /dev/null +++ b/build/clear-default-theme.js @@ -0,0 +1,12 @@ +const fs = require('fs-extra'); + +const args = process.argv; +const filePath = args[2] === 'themes' ? `${args[2]}/basic` : `${args[2]}/lib/src`; +try { + if(fs.pathExistsSync(`./src_bak/${filePath}`)) { + fs.copySync(`./src_bak/${filePath}`, `./src/${filePath}`); + fs.removeSync(`./src_bak/${args[2]}`); + } +} catch (error) { + console.log(error); +} \ No newline at end of file diff --git a/build/component.cn.ejs b/build/component.cn.ejs new file mode 100644 index 0000000..1a03001 --- /dev/null +++ b/build/component.cn.ejs @@ -0,0 +1,27 @@ +--- +title: Select 选择器 +--- +# Select 选择器 + +
+ +select是提供选项菜单的控件。   + ++ 支持单选、多选、分组、搜索、懒加载等场景。 + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +select是提供选项菜单的控件。   + ++ 支持单选、多选、分组、搜索、懒加载等场景。 + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
diff --git a/build/component.ejs b/build/component.ejs new file mode 100644 index 0000000..a3c6d73 --- /dev/null +++ b/build/component.ejs @@ -0,0 +1,122 @@ +export default { + column: "2", + + demos: [ + { + demoId: "appendToBody", + name: { + "zh-CN": "附着在body上", + "en-US": "appendToBody" + }, + desc: { + "zh-CN": "

通过属性appendToBody配置下拉面板是否附着在body上。在有局部滚动条的场景下,如果配置成false,下拉面板和选择框不会分离", + "en-US": "

button color

" + }, + tag: "website-tiny-select-appendtobody", + codeFiles: [ + "select-appendtobody.html", + "SelectAppendtobodyComponent.ts" + ] + }, + { + demoId: "beforeOpen", + name: { + "zh-CN": "懒加载", + "en-US": "beforeOpen" + }, + desc: { + "zh-CN": "下拉面板展开前触发的回调,一般用于懒加载场景。", + "en-US": "

button color

" + }, + tag: "website-tiny-select-lazy", + codeFiles:[ + "select-lazy.html", + "SelectLazyComponent.ts" + ] + + }, + { + demoId: "template", + name: { + "zh-CN": "自定义模板", + "en-US": "item" + }, + desc: { + "zh-CN": "通过placeholder配置选择框提示文本的模板。通过selected配置选择框中选中项的模板。通过item配置下拉列表中选项的模板。通过footer配置下拉列表底部的模板。", + "en-US": "

item

" + }, + tag: "website-tiny-select-template", + codeFiles:[ + "select-template.html", + "SelectTemplateComponent.ts" + ]} + ], + api: { + properties: [ + { + demoId: "appendToBody", + name: "appendToBody", + type: "boolean", + desc: { + "zh-CN": "下拉面板是否附着在body上", + "en-US": "appendToBody" + }, + defaultValue: "true" + } + ], + events: [ + { + demoId: "beforeOpen", + name: "beforeOpen", + type: "EventEmitter", + desc: { + "zh-CN": "用户想要打开面板的通知事件,一般用于数据懒加载", + "en-US": "beforeOpen" + }, + defaultValue: "" + } + ], + solts: [ + { + demoId: "template", + name: "footer", + type: "TemplateRef", + desc: { + "zh-CN": "下拉列表中底部的插槽", + "en-US": "footer" + }, + defaultValue: "" + }, + { + demoId: "template", + name: "item", + type: "TemplateRef", + desc: { + "zh-CN": "下拉列表中选项的插槽", + "en-US": "item", + }, + defaultValue: "" + }, + { + demoId: "template", + name: "placeholder", + type: "TemplateRef", + desc: { + "zh-CN": "选择框提示文本的插槽", + "en-US": "placeholder", + }, + defaultValue: "" + }, + { + demoId: "template", + name: "selected", + type: "TemplateRef", + desc: { + "zh-CN": "选择框选中项的插槽", + "en-US": "selected", + }, + defaultValue: "" + } + ] + } +} diff --git a/build/component.en.ejs b/build/component.en.ejs new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/build/component.en.ejs @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/build/demoProject.json.ejs b/build/demoProject.json.ejs new file mode 100644 index 0000000..ae92e3e --- /dev/null +++ b/build/demoProject.json.ejs @@ -0,0 +1,99 @@ +{ + "projectType": "application", + "root": "src/<%-libDir-%>/demo", + "sourceRoot": "src/<%-libDir-%>/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/<%-libDir-%>", + "index": "src/<%-libDir-%>/demo/src/index.html", + "main": "src/<%-libDir-%>/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/<%-libDir-%>/demo/tsconfig.app.json", + "assets": [ + "src/<%-libDir-%>/demo/src/favicon.ico", + "src/<%-libDir-%>/demo/src/assets" + ], + "styles": [ + "src/themes/basic/build.less", + "src/themes/theme-default/build.less", + "src/styles.less" + ], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "<%-libDir-%>-demo:build:production" + }, + "development": { + "browserTarget": "<%-libDir-%>-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js <%-libDir-%>" + } + ] + } + }<% if (shouldTest) { %>, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/<%=libDir%>/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/<%=libDir%>/demo/tsconfig.spec.json", + "karmaConfig": "src/<%=libDir%>/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": [ + "src/styles.less" + ], + "scripts": [] + } + }<% } %> + } +} \ No newline at end of file diff --git a/build/libProject.json.ejs b/build/libProject.json.ejs new file mode 100644 index 0000000..0436485 --- /dev/null +++ b/build/libProject.json.ejs @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/<%-libDir-%>/lib", + "sourceRoot": "src/<%-libDir-%>/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/<%-libDir-%>"], + "options": { + "project": "src/<%-libDir-%>/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/<%-libDir-%>"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js <%-libDir-%>" + }, + { + "command": "ng default-build <%-libDir-%>" + }, + { + "command": "node build/clear-default-theme.js <%-libDir-%>" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/<%-libDir-%> && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build <%-libDir-%> && ng pack <%-libDir-%> && node build/publish.js <%-libDir-%> --tag={args.tag}" + } + ] + } + } + } +} diff --git a/build/package.js b/build/package.js new file mode 100644 index 0000000..271e9c4 --- /dev/null +++ b/build/package.js @@ -0,0 +1,63 @@ +const path = require('path'); +const fs = require('fs-extra'); +const log = require('fancy-log'); +const wcSrc = './dist/apps/ng'; + +function read(fileName) { + return new Promise((res, rej) => { + fs.readFile(path.resolve(fileName), (err, str) => { + if (err) + rej(err); + res(str); + }); + }) +} + +function write(fileName, outFile) { + return new Promise((res, rej) => { + fs.writeFile(path.resolve(fileName), outFile, (err) => { + if (err) + return rej(err); + return res(outFile); + }); + }); +} +function concatFile(files) { + return new Promise((res, rej) => { + return Promise.all(files.map(read)) + .then(src => { + return res(src.join('\n')) + }) + .catch(rej); + }); +} + +function concat(files, outFile) { + return new Promise((res, rej) => { + const concated = concatFile(files); + if (outFile) { + concated.then((out) => write(outFile, out) + .then(res) + .catch(rej)).catch(rej); + } + else { + concated.then(res).catch(rej); + } + }); +} + +const package = async () => { + const files = [ + `${wcSrc}/runtime.js`, + `${wcSrc}/polyfills.js`, + `${wcSrc}/main.js`, + ]; + + try { + await concat(files, `${wcSrc}/web-components.js`); + } catch (e) { + log(e); + } +}; + +package(); diff --git a/build/peerDependencies.js b/build/peerDependencies.js new file mode 100644 index 0000000..e9f6cf7 --- /dev/null +++ b/build/peerDependencies.js @@ -0,0 +1,51 @@ +const fs = require('fs'); +const path = require('path'); +const reg = /(?<=import.*from\s*')[@a-zA-z/-]+/g; +const dirList = fs.readdirSync('../src'); +const ignoreComs = ['ng', 'themes', 'environments', 'browserslist']; +console.log(dirList); +dirList.forEach(dir => { + if (ignoreComs.indexOf(dir) === -1 && fs.statSync(path.join(__dirname, '../src', dir)).isDirectory()) { + const libDir = path.join(__dirname, '../src', dir, 'lib/src'); + const dependenciesArr = []; + readFile(libDir, dependenciesArr); + updatePackage(libDir, dependenciesArr); + } +}) +function readFile(dir, dependenciesArr) { + const dirList = fs.readdirSync(dir); + dirList.forEach((file) => { + const fileDir = path.join(dir, file); + if (fs.statSync(fileDir).isDirectory()) { + readFile(fileDir,dependenciesArr); + } else if (file.endsWith('ts')) { + getDependencies(fileDir, dependenciesArr); + } + }); +} +function getDependencies(file, dependenciesArr) { + const data = fs.readFileSync(file, 'utf-8').match(reg); + if (data) { + dependenciesArr.splice(-1, 0, ...data); + } +} +function updatePackage(libDir, dependenciesArr) { + const dir = path.join(libDir, '../package.json'); + const dependenciesSet = Array.from(new Set(dependenciesArr)); + const data = JSON.parse(fs.readFileSync(dir, 'utf-8')); + data.peerDependencies = createObj(dependenciesSet, '>=13.0.0'); + fs.writeFileSync(dir, JSON.stringify(data, null, ' ')); +} +function createObj(arr, version) { + const obj = {}; + arr.forEach((i) => { + if(i.startsWith('@angular')){ + obj[i] = '>=13.0.0'; + + }else{ + obj[i] = '~1.0.0'; + + } + }); + return obj; +} diff --git a/build/preview-demo.js b/build/preview-demo.js new file mode 100644 index 0000000..1697539 --- /dev/null +++ b/build/preview-demo.js @@ -0,0 +1,130 @@ +/** + * 生产环境启动预览(模拟使用组件的开发者实际应用场景)组件 demo。 + * 使用方式:在根目录下,执行 `ng preview xxx-demo`。 + */ + const path = require('path'); + const fs = require('fs-extra'); + const { execSync } = require('child_process'); + + // 处理输入参数,获取目标组件名 +const args = process.argv; +if (args.length !== 3) { + throw new TypeError('请输入要预览的组件名称!'); + return; +} +const name = args[2]; +const ngDemoDir = 'ng'; +const isNgDemoPreview = name === ngDemoDir; + +// 删除 dist、node_modules/@opentiny 等 +ready(); + +// 打包构建 lib +buildLib(); +// 修改 tsconfig.base.json 中的 compilerOptions.paths,以确保在后续构建 demo 时使用 node_modules 中的 lib 库; +// mpm install 安装 demo 所依赖的 lib 的 tgz 包。 +changePathAliasAndInstallLib(); + +// 配置有在线切换主题样式功能的 demo(utils 和 ng)的 project.json assets,使其 demo 在build 时可以将 +// node_modules/@opentiny/ng-themes/ 的主题样式放在其 demo 的 assets 目录下。 +configDemoThemes(); + +// 打包构建 demo +buildDemo(); + +// 消除、还原在执行 changePathAliasAndInstallLib 后对相关文件的产生的影响 +reset(); + +// 启动 demo +serverDemo(); + + +function ready(){ + execSync('npm run prepreview'); +} + +function buildLib() { + execSync(`ng build ${name}`); + + if (isNeedThemesBuild() && !isNgDemoPreview) { + execSync('ng build themes'); + } +} + +function changePathAliasAndInstallLib() { + // 读取 tsconfig.base.json 文件内容 + const baseTsConfigPath = path.resolve(__dirname, '../tsconfig.base.json'); + const baseTsConfig = fs.readFileSync(baseTsConfigPath); + const baseTsConfigData = JSON.parse(baseTsConfig); + // 备份 tsconfig.base.json 文件中 compilerOptions.paths 配置项的内容,之后会在 resetBaseTsconfig.js 中还原 + baseTsConfigData.paths_bak = { ...baseTsConfigData.compilerOptions.paths }; + + const distLibsPath = path.resolve(__dirname, '../dist/libs'); + const dirs = fs.readdirSync(distLibsPath); + let installs = ''; + + if (isNgDemoPreview) { + delete baseTsConfigData.compilerOptions.paths; + execSync(`ng pack ${ngDemoDir}`); + } + + dirs.forEach((dir) => { + // 去除掉 compilerOptions.paths 中当前组件 demo 中使用的当前组件 lib 库的路径,以便在后续构建当前组件demo 时使用 node_modules 中的当前组件 lib 库 + if (!isNgDemoPreview) { + delete baseTsConfigData.compilerOptions.paths[`@opentiny/ng-${dir}`]; + // 打包压缩 lib 库 + execSync(`ng pack ${dir}`); + } + + // 拼凑 npm install 时所需的 lib 库的 tgz 包路径 + const dirPath = path.resolve(distLibsPath, `${dir}`); + const fileName = fs.readdirSync(dirPath).filter((file) => { + return file.endsWith('.tgz'); + })[0]; + + if (fileName) { + installs += `dist/libs/${dir}/${fileName} `; + } + }); + // 安装当前组件 lib 库 + execSync(`npm install ${installs} --legacy-peer-deps`); + + // 将经上面处理过的 tsconfig.base.json 文件内容再写入 + fs.writeFileSync(baseTsConfigPath, JSON.stringify(baseTsConfigData, "", "\t")); +} + +function configDemoThemes() { + if (!isNeedThemesBuild()) { + return; + } + + const projectJsonPath = path.resolve(__dirname, `../src/${name}/demo/project.json`); + const projectConfig = fs.readFileSync(projectJsonPath); + const projectConfigData = JSON.parse(projectConfig); + + projectConfigData.targets.build.options.assets.push({ + "glob": "**/*", + "input": "node_modules/@opentiny/ng-themes/", + "output": "/assets/themes/" + }); + fs.writeFileSync(projectJsonPath, JSON.stringify(projectConfigData, "", "\t")); +} + +function buildDemo() { + execSync(`ng build ${name}-demo`); +} + +function reset() { + const param = isNeedThemesBuild() ? ` ${name}` : ''; + execSync(`npm run resetpreview${param}`); +} + +function serverDemo() { + execSync(`npx live-server dist/apps/${name} --port=8020`); +} + +function isNeedThemesBuild() { + return name === 'utils' || name === 'ng'; // utils-demo 中的 theme 示例利用 TiTheme 在线切换主题样式文件 +} + + diff --git a/build/publish.js b/build/publish.js new file mode 100644 index 0000000..8c1e003 --- /dev/null +++ b/build/publish.js @@ -0,0 +1,65 @@ +/** + * 发布 ng 全量组件或单个组件。 + * 在 lib 的 project.json 文件中的 publish 中使用。 + * 调用使用方式:在根目录下,执行 `ng publish xxx`。 + */ +const fs = require('fs-extra'); +const path = require('path'); +const { execSync } = require('child_process'); + +const args = process.argv; +if (args.length !== 4) { + console.log('请输入组件名'); + return; +} + +// 组件名 +const compDir = args[2]; +// tag信息 +const tag = args[3].slice(6); + +if (compDir === 'ng') { + const dirs = fs.readdirSync(path.resolve(__dirname, '../dist/libs')); + dirs.forEach(dir => { + publishFile(dir); + }); +} else { + publishFile(compDir); +} + + +function publishFile(dir) { + const baseDir = path.resolve(__dirname, `../dist/libs/${dir}`); + if (!fs.pathExistsSync(baseDir)) { + return; + } + + const files = fs.readdirSync(baseDir); + const tgzFile = files.filter((file) => { + return file.endsWith('.tgz'); + })[0]; + + if (!tgzFile) { + return; + } + + // 没有 tag 信息时 + if (tag === 'undefined') { + try { + console.log(`npm publish dist/libs/${dir}/${tgzFile} --access public`); + // execSync(`npm publish dist/libs/${dir}/${tgzFile} --access public`); // 实际发布时 publish 命令 + } catch (error) { + console.log(error); + } + } else { + try { + console.log(`npm publish dist/libs/${dir}/${tgzFile} --access public --tag ${tag}`); + // execSync(`npm publish dist/libs/${dir}/${tgzFile} --access public --tag ${tag}`); // 实际发布时 publish 命令 + } catch (error) { + console.log(error); + } + } +} + + + diff --git a/build/reset-preview.js b/build/reset-preview.js new file mode 100644 index 0000000..1bb243d --- /dev/null +++ b/build/reset-preview.js @@ -0,0 +1,45 @@ +/** + * ng preview xxx-demo 命令所需。 + * 消除、还原在 preview-demo.js 执行后的影响。 + */ +const path = require('path'); +const fs = require('fs-extra'); + +// 还原对 tsconfig.base.json 文件中 compilerOptions.paths 的修改 +resetTsBaseConfig(); + +// 还原对有在线切换主题样式功能的 demo(utils 和 ng)的 project.json assets 的修改 +resetDemoProjectJson(); + +// TODO: 是否需要还原 ng preview xxx-demo 执行过程中 npm install tgz 文件产生的影响(比如,根目录下 package.json 文件) + +function resetTsBaseConfig() { + const baseTsConfigPath = path.resolve(__dirname, '../tsconfig.base.json'); + const baseTsConfig = fs.readFileSync(baseTsConfigPath); + const baseTsConfigData = JSON.parse(baseTsConfig); + + // 还原在 preview-demo.js 中对 tsconfig.base.json 文件中 compilerOptions.paths 配置项的内容修改 + if (baseTsConfigData.paths_bak) { + baseTsConfigData.compilerOptions.paths = baseTsConfigData.paths_bak; + delete baseTsConfigData.paths_bak; + fs.writeFileSync(baseTsConfigPath, JSON.stringify(baseTsConfigData, "", "\t")); + } +} + +function resetDemoProjectJson() { + const args = process.argv; + // 没有指定组件名称时不需处理 + if (args.length !== 3) { + return; + } + const name = args[2]; + const projectJsonPath = path.resolve(__dirname, `../src/${name}/demo/project.json`); + const projectConfig = fs.readFileSync(projectJsonPath); + const projectConfigData = JSON.parse(projectConfig); + + if (projectConfigData.targets.build.options.assets.length > 3) { + projectConfigData.targets.build.options.assets.pop(); + fs.writeFileSync(projectJsonPath, JSON.stringify(projectConfigData, "", "\t")); + } +} + diff --git a/build/unitTestingResources/config.js b/build/unitTestingResources/config.js new file mode 100644 index 0000000..764c84c --- /dev/null +++ b/build/unitTestingResources/config.js @@ -0,0 +1,65 @@ +const path = require('path'); +// 系统资源管理器分隔符,Windows 上是 \,POSIX 上是 / +const SEP = path.sep; +// 组件根目录相对路径 +const ROOT_RELATIVE_PATH = `..${SEP}..${SEP}src`; +// 组件根目录绝对路径 +const ROOT_RESOLVE_PATH = path.resolve(__dirname, ROOT_RELATIVE_PATH); +// 单测依赖模板相对路径 +const RESOURCE_RELATIVE_PATH = `..${SEP}..${SEP}build${SEP}unitTestingResources`; +// 单测依赖模板绝对路径 +const RESOURCE_RESOLVE_PATH = path.resolve(__dirname, RESOURCE_RELATIVE_PATH); +// 构建配置信息模板路径 +const PROJECT_TEST_INFO_TEMPLATE = `${RESOURCE_RESOLVE_PATH}${SEP}project_test.json.template`; +// test.ts 模板路径 +const TEST_TS_TEMPLATE = `${RESOURCE_RESOLVE_PATH}${SEP}test.ts.template`; +// karma.conf.js 模板路径 +const KARMA_TEMPLATE = `${RESOURCE_RESOLVE_PATH}${SEP}karma.conf.js.template`; +// tsconfig.spec.json 模板路径 +const TSCONFIG_SPEC_TEMPLATE = `${RESOURCE_RESOLVE_PATH}${SEP}tsconfig.spec.json.template`; +// 组件目录下的保留字 +const ROOT_EXCLUDES = [ + 'base', + 'browserslist', + 'datebase', + 'datedominator', + 'dateedit', + 'datepanel', + 'dominator', + 'drag', + 'drop', + 'droplist', + 'dropsearch', + 'environments', + 'grid', + 'imagepreview', + 'include', + 'list', + 'ng', + 'outline', + 'popconfirm', + 'popup', + 'progresspie', + 'renderer', + 'scroll', + 'themes', + 'utils', + 'zoom', + 'browserslist', + 'polyfills.ts', + 'styles.less', + 'tsconfig.lib.json', + 'tsconfig.lib.prod.json', +]; + +const CONFIG = { + SEP, + ROOT_RESOLVE_PATH, + ROOT_EXCLUDES, + PROJECT_TEST_INFO_TEMPLATE, + TEST_TS_TEMPLATE, + KARMA_TEMPLATE, + TSCONFIG_SPEC_TEMPLATE +} + +module.exports = CONFIG \ No newline at end of file diff --git a/build/unitTestingResources/karma.conf.js.template b/build/unitTestingResources/karma.conf.js.template new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/build/unitTestingResources/karma.conf.js.template @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/build/unitTestingResources/project_test.json.template b/build/unitTestingResources/project_test.json.template new file mode 100644 index 0000000..1d9d78a --- /dev/null +++ b/build/unitTestingResources/project_test.json.template @@ -0,0 +1,17 @@ +, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/##component##/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/##component##/demo/tsconfig.spec.json", + "karmaConfig": "src/##component##/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": [ + "src/styles.less" + ], + "scripts": [] + } + } + } +} \ No newline at end of file diff --git a/build/unitTestingResources/test.ts.template b/build/unitTestingResources/test.ts.template new file mode 100644 index 0000000..68a2a2a --- /dev/null +++ b/build/unitTestingResources/test.ts.template @@ -0,0 +1,26 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), +); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/build/unitTestingResources/tsconfig.spec.json.template b/build/unitTestingResources/tsconfig.spec.json.template new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/build/unitTestingResources/tsconfig.spec.json.template @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/c.bat b/c.bat new file mode 100644 index 0000000..911dc38 --- /dev/null +++ b/c.bat @@ -0,0 +1 @@ +cmd \ No newline at end of file diff --git a/commit.template b/commit.template new file mode 100644 index 0000000..f355a37 --- /dev/null +++ b/commit.template @@ -0,0 +1,10 @@ +【问题单号 Defect】issue单号【修改说明 Modification】[feat/fix/..]组件名:说明。 + +修改说明类型: +feat:功能(feature) +fix:修补bug +docs:文档(documentation) +style: 格式(不影响代码运行的变动) +refactor:重构(即不是新增功能,也不是修改bug的代码变动) +test:测试用例 +chore:构建过程或辅助工具的变动 \ No newline at end of file diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..c34aa79 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@commitlint/config-conventional'] +}; diff --git a/decorate-angular-cli.js b/decorate-angular-cli.js new file mode 100644 index 0000000..733c249 --- /dev/null +++ b/decorate-angular-cli.js @@ -0,0 +1,79 @@ +/** + * This file decorates the Angular CLI with the Nx CLI to enable features such as computation caching + * and faster execution of tasks. + * + * It does this by: + * + * - Patching the Angular CLI to warn you in case you accidentally use the undecorated ng command. + * - Symlinking the ng to nx command, so all commands run through the Nx CLI + * - Updating the package.json postinstall script to give you control over this script + * + * The Nx CLI decorates the Angular CLI, so the Nx CLI is fully compatible with it. + * Every command you run should work the same when using the Nx CLI, except faster. + * + * Because of symlinking you can still type `ng build/test/lint` in the terminal. The ng command, in this case, + * will point to nx, which will perform optimizations before invoking ng. So the Angular CLI is always invoked. + * The Nx CLI simply does some optimizations before invoking the Angular CLI. + * + * To opt out of this patch: + * - Replace occurrences of nx with ng in your package.json + * - Remove the script from your postinstall script in your package.json + * - Delete and reinstall your node_modules + */ + +const fs = require('fs'); +const os = require('os'); +const cp = require('child_process'); +const isWindows = os.platform() === 'win32'; +let output; +try { + output = require('@nrwl/workspace').output; +} catch (e) { + console.warn( + 'Angular CLI could not be decorated to enable computation caching. Please ensure @nrwl/workspace is installed.' + ); + process.exit(0); +} + +/** + * Symlink of ng to nx, so you can keep using `ng build/test/lint` and still + * invoke the Nx CLI and get the benefits of computation caching. + */ +function symlinkNgCLItoNxCLI() { + try { + const ngPath = './node_modules/.bin/ng'; + const nxPath = './node_modules/.bin/nx'; + if (isWindows) { + /** + * This is the most reliable way to create symlink-like behavior on Windows. + * Such that it works in all shells and works with npx. + */ + ['', '.cmd', '.ps1'].forEach((ext) => { + if (fs.existsSync(nxPath + ext)) + fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext)); + }); + } else { + // If unix-based, symlink + cp.execSync(`ln -sf ./nx ${ngPath}`); + } + } catch (e) { + output.error({ + title: + 'Unable to create a symlink from the Angular CLI to the Nx CLI:' + + e.message, + }); + throw e; + } +} + +try { + symlinkNgCLItoNxCLI(); + require('nx/src/adapter/decorate-cli').decorateCli(); + output.log({ + title: 'Angular CLI has been decorated to enable computation caching.', + }); +} catch (e) { + output.error({ + title: 'Decoration of the Angular CLI did not complete successfully', + }); +} diff --git a/eslint-rules/eslint-config-cbc-1-7-8-angular.js b/eslint-rules/eslint-config-cbc-1-7-8-angular.js new file mode 100644 index 0000000..62f4286 --- /dev/null +++ b/eslint-rules/eslint-config-cbc-1-7-8-angular.js @@ -0,0 +1,24 @@ +module.exports = { + env: { + jasmine: true, + }, + extends: ['./eslint-config-cbc-1-7-8-typescript.js'], + plugins: ['@angular-eslint/eslint-plugin'], + rules: { + /** + * 禁止出现没必要的 constructor + * @reason angular场景下,constructor为空的情况很多,也是官方推荐的用法 + */ + '@typescript-eslint/no-useless-constructor': 'off', + /** + * 禁止出现没必要的 constructor + * @reason angular场景下,constructor为空的情况很多,也是官方推荐的用法 + */ + 'no-useless-constructor': 'off', + + /** + * 函数的参数禁止超过 5 个,不适合angular场景 + */ + 'max-params': 'off', + }, +}; diff --git a/eslint-rules/eslint-config-cbc-1-7-8-base.js b/eslint-rules/eslint-config-cbc-1-7-8-base.js new file mode 100644 index 0000000..200d8b5 --- /dev/null +++ b/eslint-rules/eslint-config-cbc-1-7-8-base.js @@ -0,0 +1,912 @@ +/** + * + * 依赖版本: + * eslint ^7.7.0 + * babel-eslint ^10.1.0 + * eslint-plugin-react ^7.20.6 + * vue-eslint-parser ^7.1.0 + * eslint-plugin-vue ^6.2.2 + * @typescript-eslint/parser ^3.10.1 + * @typescript-eslint/eslint-plugin ^3.10.1 + * + * @reason 为什么要开启(关闭)此规则 + */ +module.exports = { + parser: 'babel-eslint', + parserOptions: { + ecmaVersion: 2015, + // ECMAScript modules 模式 + sourceType: 'module', + ecmaFeatures: { + // 不允许 return 语句出现在 global 环境下 + globalReturn: false, + // 开启全局 script 模式 + impliedStrict: true, + jsx: true, + }, + // 即使没有 babelrc 配置文件,也使用 babel-eslint 来解析 + requireConfigFile: false, + // 仅允许 import export 语句出现在模块的顶层 + allowImportExportEverywhere: false, + }, + env: { + browser: true, + node: true, + commonjs: true, + es6: true, + mocha: true, + amd: true, + jquery: true, + }, + // 一些全局变量 + globals: { + jQuery: false, + $: true, + module: true, + Swiper: true, + }, + // 以当前目录为根目录,不再向上查找 .eslintrc.js + root: true, + rules: { + /** + * setter 必须有对应的 getter,getter 可以没有对应的 setter + */ + 'accessor-pairs': [ + 'error', + { + setWithoutGet: true, + getWithoutSet: false, + }, + ], + /** + * 数组的方法除了 forEach 之外,回调函数必须有返回值 + */ + 'array-callback-return': 'error', + /** + * 将 var 定义的变量视为块作用域,禁止在块外使用 + * @reason 已经禁止使用 var 了 + */ + 'block-scoped-var': 'off', + /** + * callback 之后必须立即 return + */ + 'callback-return': 'off', + /** + * 变量名必须是 camelcase 风格的 + * @reason 很多 api 或文件名都不是 camelcase 风格的 + */ + camelcase: 'off', + /** + * 注释的首字母必须大写 + */ + 'capitalized-comments': 'off', + /** + * 在类的非静态方法中,必须存在对 this 的引用 + */ + 'class-methods-use-this': 'off', + /** + * 禁止函数的循环复杂度超过 20 + * @reason https://en.wikipedia.org/wiki/Cyclomatic_complexity + */ + complexity: [ + 'error', + { + max: 20, + }, + ], + /** + * 禁止函数在不同分支返回不同类型的值 + * @reason 缺少 TypeScript 的支持,类型判断是不准确的 + */ + 'consistent-return': 'off', + /** + * 限制 this 的别名 + */ + 'consistent-this': 'off', + /** + * constructor 中必须有 super + */ + 'constructor-super': 'error', + /** + * switch 语句必须有 default + */ + 'default-case': 'off', + /** + * 有默认值的参数必须放在函数参数的末尾 + */ + 'default-param-last': 'off', + + /** + * 禁止方向错误的 for 循环 + */ + 'for-direction': 'error', + /** + * 函数赋值给变量的时候,函数名必须与变量名一致 + */ + 'func-name-matching': [ + 'error', + 'always', + { + includeCommonJSModuleExports: false, + }, + ], + /** + * 函数必须有名字 + */ + 'func-names': 'off', + /** + * 必须只使用函数声明或只使用函数表达式 + */ + 'func-style': 'off', + /** + * getter 必须有返回值,并且禁止返回空 + */ + 'getter-return': 'error', + /** + * require 必须在全局作用域下 + */ + 'global-require': 'off', + /** + * setter 和 getter 必须写在一起 + */ + 'grouped-accessor-pairs': 'error', + /** + * for in 内部必须有 hasOwnProperty + */ + 'guard-for-in': 'error', + /** + * callback 中的 err 必须被处理 + * @reason 它是通过字符串匹配来判断函数参数 err 的,不准确 + */ + 'handle-callback-err': 'off', + /** + * 禁止使用指定的标识符 + */ + 'id-blacklist': 'off', + /** + * 限制变量名长度 + */ + 'id-length': 'off', + /** + * 限制变量名必须匹配指定的正则表达式 + */ + 'id-match': 'off', + /** + * 变量必须在定义的时候赋值 + */ + 'init-declarations': 'off', + + /** + * 限制一个文件中类的数量 + */ + 'max-classes-per-file': 'off', + /** + * 代码块嵌套的深度禁止超过 5 层 + */ + 'max-depth': ['error', 5], + + /** + * 限制函数块中的代码行数 + */ + 'max-lines-per-function': 'off', + /** + * 回调函数嵌套禁止超过 3 层,多了请用 async await 替代 + */ + 'max-nested-callbacks': ['error', 3], + /** + * 函数的参数禁止超过 5 个 + */ + 'max-params': ['error', 5], + /** + * 限制函数块中的语句数量 + */ + 'max-statements': 'off', + /** + * 限制一行中的语句数量 + */ + 'max-statements-per-line': 'off', + + /** + * new 后面的类名必须首字母大写 + */ + 'new-cap': [ + 'error', + { + newIsCap: true, + capIsNew: false, + properties: true, + }, + ], + /** + * 禁止使用 alert + */ + 'no-alert': 'off', + /** + * 禁止使用 Array 构造函数时传入的参数超过一个 + * @reason 参数为一个时表示创建一个指定长度的数组,比较常用 + * 参数为多个时表示创建一个指定内容的数组,此时可以用数组字面量实现,不必使用构造函数 + */ + 'no-array-constructor': 'error', + /** + * 禁止将 async 函数做为 new Promise 的回调函数 + * @reason 出现这种情况时,一般不需要使用 new Promise 实现异步了 + */ + 'no-async-promise-executor': 'error', + /** + * 禁止将 await 写在循环里,因为这样就无法同时发送多个异步请求了 + * @reason 要求太严格了,有时需要在循环中写 await + */ + 'no-await-in-loop': 'off', + /** + * 禁止使用位运算 + */ + 'no-bitwise': 'off', + /** + * 禁止直接使用 Buffer + * @reason Buffer 构造函数是已废弃的语法 + */ + 'no-buffer-constructor': 'error', + /** + * 禁止使用 caller 或 callee + * @reason 它们是已废弃的语法 + */ + 'no-caller': 'error', + /** + * switch 的 case 内有变量定义的时候,必须使用大括号将 case 内变成一个代码块 + */ + 'no-case-declarations': 'error', + /** + * 禁止对已定义的 class 重新赋值 + */ + 'no-class-assign': 'error', + /** + * 禁止与负零进行比较 + */ + 'no-compare-neg-zero': 'error', + /** + * 禁止在测试表达式中使用赋值语句,除非这个赋值语句被括号包起来了 + */ + 'no-cond-assign': ['error', 'except-parens'], + /** + * 禁止使用 console + */ + 'no-console': 'off', + /** + * 禁止对使用 const 定义的常量重新赋值 + */ + 'no-const-assign': 'error', + /** + * 禁止将常量作为分支条件判断中的测试表达式,但允许作为循环条件判断中的测试表达式 + */ + 'no-constant-condition': [ + 'error', + { + checkLoops: false, + }, + ], + /** + * 禁止在构造函数中返回值 + */ + 'no-constructor-return': 'error', + /** + * 禁止使用 continue + */ + 'no-continue': 'off', + /** + * 禁止在正则表达式中出现 Ctrl 键的 ASCII 表示,即禁止使用 /\x1f/ + * @reason 几乎不会遇到这种场景 + */ + 'no-control-regex': 'off', + /** + * 禁止使用 debugger + */ + 'no-debugger': 'error', + /** + * 禁止对一个变量使用 delete + * @reason 编译阶段就会报错了 + */ + 'no-delete-var': 'off', + /** + * 禁止在正则表达式中出现形似除法操作符的开头,如 let a = /=foo/ + * @reason 有代码高亮的话,在阅读这种代码时,也完全不会产生歧义或理解上的困难 + */ + 'no-div-regex': 'off', + /** + * 禁止在函数参数中出现重复名称的参数 + * @reason 编译阶段就会报错了 + */ + 'no-dupe-args': 'off', + /** + * 禁止重复定义类的成员 + */ + 'no-dupe-class-members': 'error', + /** + * 禁止 if else 的条件判断中出现重复的条件 + */ + 'no-dupe-else-if': 'error', + /** + * 禁止在对象字面量中出现重复的键名 + */ + 'no-dupe-keys': 'error', + /** + * 禁止在 switch 语句中出现重复测试表达式的 case + */ + 'no-duplicate-case': 'error', + + /** + * 禁止在 else 内使用 return,必须改为提前结束 + * @reason else 中使用 return 可以使代码结构更清晰 + */ + 'no-else-return': 'off', + /** + * 禁止出现空代码块,允许 catch 为空代码块 + */ + 'no-empty': [ + 'error', + { + allowEmptyCatch: true, + }, + ], + /** + * 禁止在正则表达式中使用空的字符集 [] + */ + 'no-empty-character-class': 'error', + /** + * 不允许有空函数 + * @reason 有时需要将一个空函数设置为某个项的默认值 + */ + 'no-empty-function': 'off', + /** + * 禁止解构赋值中出现空 {} 或 [] + */ + 'no-empty-pattern': 'error', + /** + * 禁止使用 foo == null,必须使用 foo === null + */ + 'no-eq-null': 'error', + /** + * 禁止使用 eval + */ + 'no-eval': 'error', + /** + * 禁止将 catch 的第一个参数 error 重新赋值 + */ + 'no-ex-assign': 'error', + /** + * 禁止修改原生对象 + * @reason 修改原生对象可能会与将来版本的 js 冲突 + */ + 'no-extend-native': 'error', + /** + * 禁止出现没必要的 bind + */ + 'no-extra-bind': 'error', + /** + * 禁止不必要的布尔类型转换 + */ + 'no-extra-boolean-cast': 'error', + /** + * 禁止出现没必要的 label + * @reason 已经禁止使用 label 了 + */ + 'no-extra-label': 'off', + /** + * switch 的 case 内必须有 break, return 或 throw,空的 case 除外 + */ + 'no-fallthrough': 'error', + /** + * 禁止将一个函数声明重新赋值 + */ + 'no-func-assign': 'error', + /** + * 禁止对全局变量赋值 + */ + 'no-global-assign': 'error', + /** + * 禁止使用 ~+ 等难以理解的类型转换,仅允许使用 !! + */ + 'no-implicit-coercion': [ + 'error', + { + allow: ['!!'], + }, + ], + /** + * 禁止在全局作用域下定义变量或申明函数 + * @reason 模块化之后,不会出现这种在全局作用域下定义变量的情况 + */ + 'no-implicit-globals': 'off', + /** + * 禁止在 setTimeout 或 setInterval 中传入字符串 + */ + 'no-implied-eval': 'error', + /** + * 禁止对导入的模块进行赋值 + */ + 'no-import-assign': 'error', + /** + * 禁止在代码后添加单行注释 + */ + 'no-inline-comments': 'off', + /** + * 禁止在 if 代码块内出现函数声明 + */ + 'no-inner-declarations': ['error', 'both'], + /** + * 禁止在 RegExp 构造函数中出现非法的正则表达式 + */ + 'no-invalid-regexp': 'error', + /** + * 禁止在类之外的地方使用 this + * @reason 只允许在 class 中使用 this + */ + 'no-invalid-this': 'off', + /** + * 禁止使用特殊空白符(比如全角空格),除非是出现在字符串、正则表达式或模版字符串中 + */ + 'no-irregular-whitespace': [ + 'error', + { + skipStrings: true, + skipComments: false, + skipRegExps: true, + skipTemplates: true, + }, + ], + /** + * 禁止使用 __iterator__ + * @reason __iterator__ 是一个已废弃的属性 + * 使用 [Symbol.iterator] 替代它 + */ + 'no-iterator': 'error', + /** + * 禁止 label 名称与已定义的变量重复 + * @reason 已经禁止使用 label 了 + */ + 'no-label-var': 'off', + /** + * 禁止使用 label + */ + 'no-labels': 'error', + /** + * 禁止使用没必要的 {} 作为代码块 + */ + 'no-lone-blocks': 'error', + /** + * 禁止 else 中只有一个单独的 if + * @reason 单独的 if 可以把逻辑表达的更清楚 + */ + 'no-lonely-if': 'off', + /** + * 禁止在循环内的函数内部出现循环体条件语句中定义的变量 + * @reason 使用 let 就已经解决了这个问题了 + */ + 'no-loop-func': 'off', + /** + * 禁止使用 magic numbers + */ + 'no-magic-numbers': 'off', + /** + * 禁止正则表达式中使用肉眼无法区分的特殊字符 + * @reason 某些特殊字符很难看出差异,最好不要在正则中使用 + */ + 'no-misleading-character-class': 'error', + /** + * 相同类型的 require 必须放在一起 + */ + 'no-mixed-requires': 'off', + /** + * 禁止连续赋值,比如 foo = bar = 1 + */ + 'no-multi-assign': 'off', + /** + * 禁止使用 \ 来换行字符串 + */ + 'no-multi-str': 'error', + /** + * 禁止 if 里有否定的表达式 + * @reason 否定的表达式可以把逻辑表达的更清楚 + */ + 'no-negated-condition': 'off', + /** + * 禁止使用嵌套的三元表达式,比如 a ? b : c ? d : e + */ + 'no-nested-ternary': 'off', + /** + * 禁止直接 new 一个类而不赋值 + * @reason new 应该作为创建一个类的实例的方法,所以不能不赋值 + */ + 'no-new': 'warn', + /** + * 禁止使用 new Function + * @reason 这和 eval 是等价的 + */ + 'no-new-func': 'error', + /** + * 禁止直接 new Object + */ + 'no-new-object': 'error', + /** + * 禁止直接 new require('foo') + */ + 'no-new-require': 'error', + /** + * 禁止使用 new 来生成 Symbol + */ + 'no-new-symbol': 'error', + /** + * 禁止使用 new 来生成 String, Number 或 Boolean + */ + 'no-new-wrappers': 'error', + /** + * 禁止将 Math, JSON 或 Reflect 直接作为函数调用 + */ + 'no-obj-calls': 'error', + /** + * 禁止使用 0 开头的数字表示八进制数 + * @reason 编译阶段就会报错了 + */ + 'no-octal': 'off', + /** + * 禁止使用八进制的转义符 + * @reason 编译阶段就会报错了 + */ + 'no-octal-escape': 'off', + /** + * 禁止对函数的参数重新赋值 + */ + 'no-param-reassign': 'off', + /** + * 禁止对 __dirname 或 __filename 使用字符串连接 + * @reason 不同平台下的路径符号不一致,建议使用 path 处理平台差异性 + */ + 'no-path-concat': 'error', + /** + * 禁止使用 ++ 或 -- + */ + 'no-plusplus': 'off', + /** + * 禁止使用 process.env.NODE_ENV + */ + 'no-process-env': 'off', + /** + * 禁止使用 process.exit(0) + */ + 'no-process-exit': 'off', + /** + * 禁止使用 __proto__ + * @reason __proto__ 是已废弃的语法 + */ + 'no-proto': 'error', + /** + * 禁止使用 hasOwnProperty, isPrototypeOf 或 propertyIsEnumerable + * @reason hasOwnProperty 比较常用 + */ + 'no-prototype-builtins': 'off', + /** + * 禁止重复定义变量 + * @reason 禁用 var 之后,编译阶段就会报错了 + */ + 'no-redeclare': 'off', + /** + * 禁止在正则表达式中出现连续的空格 + */ + 'no-regex-spaces': 'error', + /** + * 禁止使用指定的全局变量 + */ + 'no-restricted-globals': 'off', + /** + * 禁止导入指定的模块 + */ + 'no-restricted-imports': 'off', + /** + * 禁止使用指定的模块 + */ + 'no-restricted-modules': 'off', + /** + * 禁止使用指定的对象属性 + */ + 'no-restricted-properties': 'off', + /** + * 禁止使用指定的语法 + */ + 'no-restricted-syntax': 'off', + /** + * 禁止在 return 语句里赋值 + */ + 'no-return-assign': ['error', 'always'], + + /** + * 禁止出现 location.href = 'javascript:void(0)'; + * @reason 有些场景下还是需要用到这个 + */ + 'no-script-url': 'off', + /** + * 禁止将自己赋值给自己 + */ + 'no-self-assign': 'error', + /** + * 禁止将自己与自己比较 + */ + 'no-self-compare': 'error', + /** + * 禁止使用逗号操作符 + */ + 'no-sequences': 'error', + /** + * 禁止 setter 有返回值 + */ + 'no-setter-return': 'error', + + /** + * 禁止使用保留字作为变量名 + */ + 'no-shadow-restricted-names': 'error', + /** + * 禁止在数组中出现连续的逗号 + */ + 'no-sparse-arrays': 'error', + /** + * 禁止使用 node 中的同步的方法,比如 fs.readFileSync + */ + 'no-sync': 'off', + /** + * 禁止在普通字符串中出现模版字符串里的变量形式 + */ + 'no-template-curly-in-string': 'error', + /** + * 禁止使用三元表达式 + */ + 'no-ternary': 'off', + /** + * 禁止在 super 被调用之前使用 this 或 super + */ + 'no-this-before-super': 'error', + /** + * 禁止 throw 字面量,必须 throw 一个 Error 对象 + */ + 'no-throw-literal': 'error', + /** + * 禁止使用未定义的变量 + */ + 'no-undef': 'error', + /** + * 禁止将 undefined 赋值给变量 + */ + 'no-undef-init': 'error', + /** + * 禁止使用 undefined + */ + 'no-undefined': 'off', + /** + * 禁止变量名出现下划线 + */ + 'no-underscore-dangle': 'off', + /** + * 循环内必须对循环条件中的变量有修改 + */ + 'no-unmodified-loop-condition': 'error', + /** + * 必须使用 !a 替代 a ? false : true + * @reason 后者表达的更清晰 + */ + 'no-unneeded-ternary': 'off', + /** + * 禁止在 return, throw, break 或 continue 之后还有代码 + */ + 'no-unreachable': 'error', + + /** + * 禁止在 in 或 instanceof 操作符的左侧变量前使用感叹号 + */ + 'no-unsafe-negation': 'error', + /** + * 禁止无用的表达式 + */ + 'no-unused-expressions': [ + 'error', + { + allowShortCircuit: true, + allowTernary: true, + allowTaggedTemplates: true, + }, + ], + /** + * 禁止出现没用到的 label + * @reason 已经禁止使用 label 了 + */ + 'no-unused-labels': 'off', + /** + * 已定义的变量必须使用 + */ + 'no-unused-vars': [ + 'error', + { + vars: 'all', + args: 'none', + ignoreRestSiblings: false, + caughtErrors: 'none', + }, + ], + /** + * 变量必须先定义后使用 + */ + 'no-use-before-define': [ + 'error', + { + variables: false, + functions: false, + classes: false, + }, + ], + /** + * 禁止出现没必要的 call 或 apply + */ + 'no-useless-call': 'error', + /** + * 禁止在 catch 中仅仅只是把错误 throw 出去 + * @reason 这样的 catch 是没有意义的,等价于直接执行 try 里的代码 + */ + 'no-useless-catch': 'error', + /** + * 禁止出现没必要的计算键名 + */ + 'no-useless-computed-key': 'error', + /** + * 禁止出现没必要的字符串连接 + */ + 'no-useless-concat': 'error', + /** + * 禁止出现没必要的 constructor + */ + 'no-useless-constructor': 'error', + /** + * 禁止出现没必要的转义 + * @reason 转义可以使代码更易懂 + */ + 'no-useless-escape': 'off', + /** + * 禁止解构赋值时出现同样名字的的重命名,比如 let { foo: foo } = bar; + */ + 'no-useless-rename': 'error', + /** + * 禁止没必要的 return + */ + 'no-useless-return': 'off', + + /** + * 禁止使用 void + */ + 'no-void': 'warn', + /** + * 禁止使用 with + * @reason 编译阶段就会报错了 + */ + 'no-with': 'off', + /** + * 必须使用 a = {b} 而不是 a = {b: b} + * @reason 有时后者可以使代码结构更清晰 + */ + 'object-shorthand': 'off', + /** + * 禁止变量申明时用逗号一次申明多个 + */ + 'one-var': ['error', 'never'], + /** + * 必须使用 x = x + y 而不是 x += y + */ + 'operator-assignment': 'off', + /** + * 限制语句之间的空行规则,比如变量定义完之后必须要空行 + */ + 'padding-line-between-statements': 'off', + /** + * 申明后不再被修改的变量必须使用 const 来申明 + */ + 'prefer-const': 'off', + /** + * 必须使用解构赋值 + */ + 'prefer-destructuring': 'off', + /** + * 使用 ES2016 的语法 ** 替代 Math.pow + */ + 'prefer-exponentiation-operator': 'off', + /** + * 使用 ES2018 中的正则表达式命名组 + * @reason 正则表达式已经较难理解了,没必要强制加上命名组 + */ + 'prefer-named-capture-group': 'off', + /** + * 必须使用 0b11111011 而不是 parseInt() + */ + 'prefer-numeric-literals': 'off', + /** + * 必须使用 ... 而不是 Object.assign,除非 Object.assign 的第一个参数是一个变量 + */ + 'prefer-object-spread': 'error', + /** + * Promise 的 reject 中必须传入 Error 对象,而不是字面量 + */ + + 'prefer-promise-reject-errors': 'error', + /** + * 优先使用正则表达式字面量,而不是 RegExp 构造函数 + */ + 'prefer-regex-literals': 'error', + + /** + * 必须使用 ... 而不是 apply,比如 foo(...args) + */ + 'prefer-spread': 'off', + /** + * 必须使用模版字符串而不是字符串连接 + */ + 'prefer-template': 'off', + /** + * parseInt 必须传入第二个参数 + */ + radix: 'off', + /** + * 禁止将 await 或 yield 的结果做为运算符的后面项 + * @reason 这样会导致不符合预期的结果 + * https://github.com/eslint/eslint/issues/11899 + * 在上面 issue 修复之前,关闭此规则 + */ + 'require-atomic-updates': 'off', + /** + * async 函数中必须存在 await 语句 + */ + 'require-await': 'off', + /** + * 正则表达式中必须要加上 u 标志 + */ + 'require-unicode-regexp': 'off', + /** + * generator 函数内必须有 yield + */ + 'require-yield': 'error', + /** + * 导入必须按规则排序 + */ + 'sort-imports': 'off', + /** + * 对象字面量的键名必须排好序 + */ + 'sort-keys': 'off', + /** + * 变量申明必须排好序 + */ + 'sort-vars': 'off', + + /** + * 禁止使用 'strict'; + */ + strict: ['error', 'never'], + /** + * 创建 Symbol 时必须传入参数 + */ + 'symbol-description': 'error', + + /** + * typeof 表达式比较的对象必须是 'undefined', 'object', 'boolean', 'number', 'string', 'function', 'symbol', 或 'bigint' + */ + 'valid-typeof': 'error', + /** + * var 必须在作用域的最前面 + */ + 'vars-on-top': 'off', + + /** + * 必须使用 if (foo === 5) 而不是 if (5 === foo) + */ + yoda: [ + 'error', + 'never', + { + onlyEquality: true, + }, + ], + }, +}; diff --git a/eslint-rules/eslint-config-cbc-1-7-8-typescript.js b/eslint-rules/eslint-config-cbc-1-7-8-typescript.js new file mode 100644 index 0000000..370df35 --- /dev/null +++ b/eslint-rules/eslint-config-cbc-1-7-8-typescript.js @@ -0,0 +1,381 @@ +/** + * + * 依赖版本: + * eslint ^7.16.0 + * babel-eslint ^10.1.0 + * eslint-plugin-react ^7.21.5 + * vue-eslint-parser ^7.3.0 + * eslint-plugin-vue ^7.3.0 + * @typescript-eslint/parser ^4.11.0 + * @typescript-eslint/eslint-plugin ^4.11.0 + * + * @reason 为什么要开启(关闭)此规则 + */ +module.exports = { + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: ['./eslint-config-cbc-1-7-8-base.js'], + rules: { + //关闭参考 https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/src/configs/eslint-recommended.ts + 'constructor-super': 'off', // ts(2335) & ts(2377) + 'getter-return': 'off', // ts(2378) + 'no-const-assign': 'off', // ts(2588) + 'no-dupe-args': 'off', // ts(2300) + 'no-dupe-class-members': 'off', // ts(2393) & ts(2300) + 'no-dupe-keys': 'off', // ts(1117) + 'no-func-assign': 'off', // ts(2539) + 'no-import-assign': 'off', // ts(2539) & ts(2540) + 'no-new-symbol': 'off', // ts(2588) + 'no-obj-calls': 'off', // ts(2349) + 'no-redeclare': 'off', // ts(2451) + 'no-setter-return': 'off', // ts(2408) + 'no-this-before-super': 'off', // ts(2376) + 'no-undef': 'off', // ts(2304) + 'no-unreachable': 'off', // ts(7027) + 'no-unsafe-negation': 'off', // ts(2365) & ts(2360) & ts(2358) + 'valid-typeof': 'off', // ts(2367) + 'no-empty-function': 'off', // https://github.com/typescript-eslint/typescript-eslint/issues/491 + 'no-invalid-this': 'off', + 'no-magic-numbers': 'off', + 'no-unused-vars': 'off', + 'react/sort-comp': 'off', + /** + * 重载的函数必须写在一起 + * @reason 增加可读性 + */ + '@typescript-eslint/adjacent-overload-signatures': 'error', + /** + * 限制数组类型必须使用 Array 或 T[] + * @reason 允许灵活运用两者 + */ + '@typescript-eslint/array-type': 'off', + /** + * 禁止对没有 then 方法的对象使用 await + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/await-thenable': 'off', + /** + * 禁止使用 // @ts-ignore // @ts-nocheck // @ts-check + * @reason 这种注释本身就是对特殊代码的说明 + */ + '@typescript-eslint/ban-ts-comment': 'off', + /** + * 禁止使用指定的类型 + */ + '@typescript-eslint/ban-types': 'off', + /** + * 类型断言必须使用 as Type,禁止使用 ,禁止对对象字面量进行类型断言(断言成 any 是允许的) + * @reason 容易被理解为 jsx + */ + '@typescript-eslint/consistent-type-assertions': 'off', + /** + * 优先使用 interface 而不是 type + * @reason interface 可以 implement, extend 和 merge + */ + '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], + /** + * 有默认值或可选的参数必须放到最后 + */ + '@typescript-eslint/default-param-last': 'off', + /** + * 函数返回值必须与声明的类型一致 + * @reason 编译阶段检查就足够了 + */ + '@typescript-eslint/explicit-function-return-type': 'off', + /** + * 必须设置类的成员的可访问性 + * @reason 将不需要公开的成员设为私有的,可以增强代码的可理解性,对文档输出也很友好 + */ + '@typescript-eslint/explicit-member-accessibility': 'off', + /** + * 导出的函数或类中的 public 方法必须定义输入输出参数的类型 + */ + '@typescript-eslint/explicit-module-boundary-types': 'off', + + /** + * 限制各种变量或类型的命名规则 + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/naming-convention': 'off', + /** + * 禁止使用 Array 构造函数 + */ + '@typescript-eslint/no-array-constructor': 'off', + /** + * 禁止重复定义类的成员 + * @reason 编译阶段就会报错了 + */ + '@typescript-eslint/no-dupe-class-members': 'off', + /** + * 禁止 delete 时传入的 key 是动态的 + */ + '@typescript-eslint/no-dynamic-delete': 'off', + /** + * 不允许有空函数 + * @reason 有时需要将一个空函数设置为某个项的默认值 + */ + '@typescript-eslint/no-empty-function': 'off', + /** + * 禁止定义空的接口 + */ + '@typescript-eslint/no-empty-interface': 'error', + /** + * 禁止使用 any + */ + '@typescript-eslint/no-explicit-any': 'off', + /** + * 禁止多余的 non-null 断言 + */ + '@typescript-eslint/no-extra-non-null-assertion': 'off', + /** + * 禁止定义没必要的类,比如只有静态方法的类 + */ + '@typescript-eslint/no-extraneous-class': 'off', + /** + * 禁止调用 Promise 时没有处理异常情况 + */ + '@typescript-eslint/no-floating-promises': 'off', + /** + * 禁止对 array 使用 for in 循环 + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/no-for-in-array': 'off', + /** + * 禁止使用 eval + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/no-implied-eval': 'off', + /** + * 禁止给一个初始化时直接赋值为 number, string 的变量显式的声明类型 + * @reason 可以简化代码 + */ + '@typescript-eslint/no-inferrable-types': 'warn', + /** + * 禁止使用 magic numbers + */ + '@typescript-eslint/no-magic-numbers': 'off', + /** + * 禁止在接口中定义 constructor,或在类中定义 new + */ + '@typescript-eslint/no-misused-new': 'off', + /** + * 避免错误的使用 Promise + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/no-misused-promises': 'off', + /** + * 禁止使用 namespace 来定义命名空间 + * @reason 使用 es6 引入模块,才是更标准的方式。 + * 但是允许使用 declare namespace ... {} 来定义外部命名空间 + */ + '@typescript-eslint/no-namespace': [ + 'error', + { + allowDeclarations: true, + allowDefinitionFiles: true, + }, + ], + /** + * 禁止在 optional chaining 之后使用 non-null 断言(感叹号) + * @reason optional chaining 后面的属性一定是非空的 + */ + '@typescript-eslint/no-non-null-asserted-optional-chain': 'off', + /** + * 禁止使用 non-null 断言(感叹号) + * @reason 使用 non-null 断言时就已经清楚了风险 + */ + '@typescript-eslint/no-non-null-assertion': 'off', + /** + * 禁止给类的构造函数的参数添加修饰符 + * @reason angular例子也加了,不强制执行 + */ + '@typescript-eslint/no-parameter-properties': 'warn', + /** + * 禁止使用 require + * @reason 统一使用 import 来引入模块,特殊情况使用单行注释允许 require 引入 + */ + '@typescript-eslint/no-require-imports': 'warn', + /** + * 禁止将 this 赋值给其他变量,除非是解构赋值 + */ + '@typescript-eslint/no-this-alias': [ + 'error', + { + allowDestructuring: true, + }, + ], + /** + * 禁止 throw 字面量,必须 throw 一个 Error 对象 + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/no-throw-literal': 'off', + /** + * 禁止使用类型别名 + */ + '@typescript-eslint/no-type-alias': 'off', + /** + * 测试表达式中的布尔类型禁止与 true 或 false 直接比较 + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off', + /** + * 条件表达式禁止是永远为真(或永远为假)的 + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/no-unnecessary-condition': 'off', + /** + * 在命名空间中,可以直接使用内部变量,不需要添加命名空间前缀 + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/no-unnecessary-qualifier': 'off', + /** + * 禁止范型的类型有默认值时,将范型设置为该默认值 + */ + '@typescript-eslint/no-unnecessary-type-arguments': 'off', + /** + * 禁止无用的类型断言 + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/no-unnecessary-type-assertion': 'off', + /** + * 禁止无用的表达式 + */ + '@typescript-eslint/no-unused-expressions': 'off', + /** + * 已定义的变量必须使用 + * @reason 编译阶段检查就足够了 + */ + '@typescript-eslint/no-unused-vars': 'off', + /** + * 禁止已定义的变量未使用 + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/no-unused-vars-experimental': 'off', + /** + * 禁止在定义变量之前就使用它 + * @reason 编译阶段检查就足够了 + */ + '@typescript-eslint/no-use-before-define': 'off', + /** + * 禁止出现没必要的 constructor + */ + '@typescript-eslint/no-useless-constructor': 'error', + /** + * 禁止使用 require 来引入模块 + * @reason no-require-imports 规则已经约束了 require + */ + '@typescript-eslint/no-var-requires': 'warn', + /** + * 使用 as const 替代 as 'bar' + * @reason as const 是新语法,不是很常见 + */ + '@typescript-eslint/prefer-as-const': 'off', + /** + * 使用 for 循环遍历数组时,如果索引仅用于获取成员,则必须使用 for of 循环替代 for 循环 + * @reason for of 循环更加易读 + */ + '@typescript-eslint/prefer-for-of': 'off', + /** + * 使用函数类型别名替代包含函数调用声明的接口 + */ + '@typescript-eslint/prefer-function-type': 'error', + /** + * 使用 includes 而不是 indexOf + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/prefer-includes': 'off', + /** + * 禁止使用 module 来定义命名空间 + * @reason module 已成为 js 的关键字 + */ + '@typescript-eslint/prefer-namespace-keyword': 'error', + /** + * 使用 ?? 替代 || + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/prefer-nullish-coalescing': 'off', + /** + * 使用 optional chaining 替代 && + */ + '@typescript-eslint/prefer-optional-chain': 'off', + /** + * 私有变量如果没有在构造函数外被赋值,则必须设为 readonly + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/prefer-readonly': 'off', + /** + * 使用 RegExp#exec 而不是 String#match + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/prefer-regexp-exec': 'off', + /** + * 使用 String#startsWith 而不是其他方式 + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/prefer-string-starts-ends-with': 'off', + /** + * async 函数的返回值必须是 Promise + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/promise-function-async': 'off', + /** + * 使用 sort 时必须传入比较函数 + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/require-array-sort-compare': 'off', + /** + * async 函数中必须存在 await 语句 + */ + '@typescript-eslint/require-await': 'off', + /** + * 使用加号时,两者必须同为数字或同为字符串 + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/restrict-plus-operands': 'off', + /** + * 模版字符串中的变量类型必须是字符串 + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/restrict-template-expressions': 'off', + /** + * async 函数必须返回 await + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/return-await': 'off', + /** + * 条件判断必须传入布尔值 + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/strict-boolean-expressions': 'off', + /** + * 使用联合类型作为 switch 的对象时,必须包含每一个类型的 case + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/switch-exhaustiveness-check': 'off', + /** + * 禁止使用三斜杠导入文件 + * @reason 三斜杠是已废弃的语法,但在类型声明文件中还是可以使用的 + */ + '@typescript-eslint/triple-slash-reference': [ + 'error', + { + path: 'never', + types: 'always', + lib: 'always', + }, + ], + /** + * interface 和 type 定义时必须声明成员的类型 + */ + '@typescript-eslint/typedef': 'off', + /** + * 方法调用时需要绑定到正确的 this 上 + * @reason 统一关闭 requires type information 的规则 + */ + '@typescript-eslint/unbound-method': 'off', + /** + * 函数重载时,若能通过联合类型将两个函数的类型声明合为一个,则使用联合类型而不是两个函数声明 + */ + '@typescript-eslint/unified-signatures': 'warn', + }, +}; diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f059e3daee54107f3b1d2089c98691d703cb5f1a GIT binary patch literal 44683 zcmeFZRa6{L^e#GrySsquWMZD84sPv)0rEaG^e90f6Cl0JO(jPzMd_006*wKmZtZ20pgs zf&TZmKzJVb|9pPDkyg+N^>7G4O;JwI3%J+n`_0POhq3PKPS%wFZJX0lzgJ%pwMrk6 zZg^`YLlaB$IrHygZmg(6( zDgCSBXs3z2nc?0-WFza0;2y7v!SZ64xOF!NwEz0(oidW-Ce7?m6~TS&g43wX^uqI8 z^3;FBIe}bEY5s)&ZWw$p0C&*`Y0dqh0qi}rD|Yk;^%zRv4Vus7TdXjPj-Uxo?chtS zn`IHc{=LILUUemS*OD^o0gL_&-}zV1Ni5tF%xfgOc6}~ikf-}TZMHpR1HXnWYNo!} zcx45#xkH*>+Ir#w0=OYLW(UH4x!b-6E)-!dK!N>Z+|O`NW1SL}`){|PzpJ3`uM`xN z7cGgO^E>=JFE2t(oX|fVP^Y)0j7jI=k|#Ql}&`p!E9NEq(R7zTM)Bl|Lic@A^3e821-J zt+POD#0i52W`i*QO5&|H@XDJzoT&{h{gk22X&KMvt^^;FdwJvEE{ofJmk(T#db$$; zyb4$^4H%!4jh6j!nRj}7o_2D25bb;~baA@z2N!qj$AQ~xGY0p56BGY7^e?Y54u&Ae znFFqatlNMG&Koqh3qRtuw^&}OhqVDqi^~d&JY+mL(VovYWq0u;fm}<)y4Rf0rl{Jr z@s-2b#otTr{*=6np0rCXw##SxTsf4$N&Xt87ekzw6?%SeE@pxc$ zAw}m6yvPA#+NVTY-gEs3r-^ob;6M-}rx(H55;x}l29y#Qo)Bnw`990`O4q98!}7i$ z1+`4@wvO3@&ShV>v#uCJ0LB;C^Si7z_UMK`Zr|eFPJm8}9P~8GWe|bt^&nvE~W>U-I;d%KpYtCEZH#N3n88{ON- zY5F3Y^54bv?Yj2&ba8_D8oP^9`1bX#=)6T}(0bT^gd zP97fGpF7r*MIs3Ih3Fh?g6+I{ylmf})tNY+C zb1uG~E1KIUo+3Mp>N2Vwy5qzpyRN;d3)OuvJgvRUe*ZXBsDO$6e{UDq6sDj|ZizB) z@7Z-h&D^}dBT=i#lajJof0Tc39~e8^4O^W2`a2FW4o-A>Hal|MJbH45FY^cW__zJ{ zL%GGW>$0|fy1q8Oz7)uSea-dKQh$Gj#fR@YCj-dEhoR`98~TC|zxT8~+J6uZSAf3vwiapo+>_-L@~nOc z{n>Ff5fHY?>o6spT5CZ)X3OKEsI)yQ2>>bBgm25KkQFLLR?6nhQZ0{07ee2=l<@kzbW-eg0LZGb;TjVDuFO~U5{wEvy z37g|_oHe=g@$bmIyV@10XlN=D^AE~>f=|Yy9*sYY|J|Hv9=};|io4(DoI1e?6bX_I zII_9u-_;KppGCJh{tC-V$#>hhgc3q^540}dVs)eU{_J>=9Jx<=YN$6RIhNfp{$r!r z14lp#dxb9Bq~v_u+2}X8<5WWynDY~Mz=zSs+rtfedKI4s1)?LGkI4}qHZuaBlZ}f; zxCTI}Cl0M!f8XacoQs;&j5KY!|48)xgj1~x$Y{zQLZzP3(u>lIu@&uoS%>TL*7A0{ zmPc1Fzb|jCz0$oUG!FRSwBtl`nA|RDYMycVfPb`V(NXKScwpWx-wqkK0k?p>$nF^r z+rs8#&kP*ng*$%g{ZLN~e1P`s^98h|>vKMg;q)(s)j$nBafmK)Icx^CPrce7N^btM zd@|)#`j}sk0qczQ55E`PWn!I%ax0y9WV@Ht*wd?k8-CUH&X@ZPaimu>o5AK3z@2Z- z7fAne&%dSHGCh(@7Jv9$P+q-LiKyZ|TJ7mhW4;$$EoHp<-Vab()dgfeo3Fq7y;Q$B z_io%QcS&N!6PM&l-09W|h)_WA0C>n+(0eS7MHn6e2tQ(=bis48N6;_>UA}Hb9v;73 zxkF3O{d)9ay)&R`-JIRh2!;XhpU&}cJS1ymlsQhyy&E)s?kx(}$H0Zj;C8r>!Jw_a z6(5m%K4~fKzVGvJ23o(n=7~J7ZV&sKa`?M8r3L@5`GF}%+ilcJ0Py}>kTV6e$@D-! z;0v_e>WX#ilT-^WPsWdU+iOc)FUdr1f%@3phOYa~8gq@vDS?}`OFaEGQwO;p8 z!plsvJnzTPR8DEHNY`_(1}SaMZxt_Z4>+$;Y7vo#x*vUwzwK&=w2F5LA)afjK=Sdj z_JDD;X(sl%pxtFY`>wG4eC2lOBS`dr>&oWGVW~{sQn5L9o2%7Eq=|n#i@uIkY5v-N zByQ^WFzOpYQU0F}rE|a_sD#}aBe&BSP1}zD?q2@rw-LTO&VESq4i(zn6xU+#J=;Y7 zHTAY$<=SZ5OL6{>v^Stpmw`?gJXRdUj1~47J!>Cq4D=Yhi*6f{J5>Q&_J^H%<^Q92 zTSq%O$>HrsD`^bAZr2wI4{C?h4Ebm6&oak9ervvt{d(Ff+asJ@wYT3~0IEXi;<2Y- zMxxljW$MliowDsUaAt@v<5-*iKI5bJ=q;^^e&xfS_T3S#>3q~zAtTgu znrz|S`66ey<69msR<5VkcS3tz@9%sT+b1e)#>Y1naPOO-4A9zrbRL9vR!AG{xRwfe z*B@}F0KJmu=L(XV4QFDsj`_#%_R{I&{WdzEYSY0J|9{XTQ%~eUcBXhU$AflCxxs@a zJmO0hL4aPcyLh>yuOIJmtip3%%Hs{DgWix#6UA<37Oi9-6o+nCA;$_$ zefIxeLkm8L-Qu$%ZH`JBYZ_eLSKIFs2wP4NU%7D~jrtCD5Z?d0KR&^&dLC{(!GUE@ z8Nv{17A8O8$&aWi&KGd!N3iNhZx~Bm_X+>aMIJt^65i)uevf;*>%fcM{BEH6{PM=w zp7O@?Yc+3U8}bd>NreE?sp@0)3C{XcKbQgGdc4+O@qiT-*}ECXM!x2hK!pK7uIJ*; zN13Gx3J$b}Pa#3D)4NS66VB>>QONwWj8^B}l(bwIt=ad1rB~dx_yPhG--rpAqnEs5 zG4&yBF3-l|qc69^k1y`zTI8Td1P`{mxyWB>&|!U!`&jSxhT|mJhfb(Zz^|0ep^j@C zfUKMd((f|V=3|xCOCpN#abHu;o?0*&#-h-U|87}sD~xdX`AXR}=bg)4rjmsoOP{=G z0423V1L$On^y|Z4{Us&qG2^1ccIw0HLlvr}_P+ZmS>zLbsJYngaV_^Lt;+qc*xak* zOQh^3y~TAE-g8@whZX3esrTvmxNLh{tPQqwC7yL_71_k92l2+jX~Wy12jSsy>F^iT z@bQ3|_teggR9rpo4|#$IMvUx9zNfa!2%Le+B|O4$Q>3Ps<^4>XF>*#~f!-v)=;9&Yrz zuI^JW#;-a@GhOzkKG%aHi>GpKZNa~s=J`mZKbOC}_qIWxO#XYf8F@dvnSF_Qx#Zar zerw~LeE)gsoMv1{=8`L?@Qz33-o-MYuHkt30Gu0etmMCZ-07bZI@IBBZB`#}ug&}K zUVCRs#iblUL!<7yn$WFH2nv9=6Y2XZ$@bPX0j= zDy9$N^f*H+WzEN}OQ@N<^IsC5D#cm~K7LjHUzN-A{o~uMj=SI6n>KM1rp3W5PRODL z+xsYY)GpftZq~(}Lr>faxs$b*anQ3q{8uAGwEp)aU-hX6`(=;L)drg*b{(Auv??8$ zDgT2C#;b%hC_}DWfAIA~-%p-^f9x-H4)%~Q_?K|5PAO#XciYd`KFqv5jI;jtZ}?B` z0^RDLy=T79O(*EYc^Ge}p(pu=t$x%J-^iC|bQZdr%9`(P^j*+hUk1sy@E!A2&2Pr1Yxz zHx3ff!P6nS5&fvNvdLqe?%BXQGg^skHT6ed{UP_BdiwjyyKdZZN{N-J$^bFw?y`yr z1vmIGE3gUVy*W=uRk~@jf#mq;fB%bI24C2?&5u@}qrE5h-7@)5%Jnh-U&fhPKlQtb z=-hS>pIKAW(Po?G0PLf2ZqP%{ubZYQ$P)POEfL`CA9QtWSbG9TMi3LB;;qAMM&DsT zm>E2lq$90fOs1`7C?U{AkQgvxuKGlvLrfd=qa1IK35*I(8KkBJ&aR5Wn4di`suGa! zp!)LwiPb0T~NlpRo_4$gN%Ni(zrE3n+a@9yPgcvF3~-aN~*o6&>larfc#b3^D*EtCw+1c;#c zMWc@{A?9LS=1ZZn^~p>UM;Us53r+tS;Vi6Xz^BSy(p)29*29hVj`--S82L9im3=&zu+^oZ-u^BKRmppJOK%YVqh>~U%GhMlhCA|j_xE3s~*3b{ch=!h2pL@9SjPA-qpB^OrSTjYxVjXlT!@RX5|NJ2mG z0OR;aC*>csZ*ZcbpfLt`^jLV|Hil?ic)eup^YgA0O-lwZeo{#W$|N4vx^MDbokS4f z4p$<%^2?OI2xaL3iAs(a1yH!$>s29%!2g$9vs5vz6z zGbIHWuu-1BsA7o*YXOn}$t zrvH|kDWx_~(6&u=n3+x(GXse%wbiJBoF&*_$gB%FY(bv@-_9hpeJAu^!lmOwyOXgP z6A5I_7a#-X(>P&GI_FfRW1KmVkL0v@jDwa#E*Ya#gL*MyL|sDrv3LVP_|BSZu(5Z? zmfW9?<^Rqm+IG|EVR*m;Q9dIzVAq&#WhQNmwe3f-FKo|Jpk0R*f&D{=wVIvp!=75V zFJ51$qK7IjijoLm4SktnbD1Nc2@Wb0i+WG?PeS>_7kEv7G|k=&nN6m_7|9@G6`=^V zYVKBe=xj6BOP|uLe;?Wzk}~=Bi|bK=X7vat@%IMw_RZe*jy`HI8>f!d0Rh!PvA;7O z6QuWrwhaw1N~W*1*X~cqDew$Pwr1Is*7+UE3uT9fWqo&QYzo^{z^1=h>)RrqKIqaa zPO7b>w^0W>e4!*hCBn0(u1&7k+ETVp$?PP0g0H=-@vhtKlW!()&tiYa-{6tX`cHGF zG51NzFs1Bvh<^*suD9@*}!gjQn7%Lc+=^fm?^&mB3T zMrlg+#?;fVA6*`S(3Y5e9XCuUWacNlMtp=7Hcvc{gA==zp*w3*Xiq!!0>t?=VnE)> z0Lv$c=QdC|I(fNLfJptdnAIyk*W#)iVXg_neg%6W@L-#Ow&9lv)0<_9d2t#IOS;lb zkK)FKAWm><0ylq$(1fmmYuTUJ0!ieujDP0MUF{EQphxW@SLW|%NvQ@<5ZJ@XW31P2 zcpj>^+o+>u%VoBHOst~Dz>Pg5Ii6Gfl|C$%Nf@B(tO219{t)vtUI4vXa+wYdC}yX( z-v0Zid-{{?-Q-Z^XijR+A2GQx0-tw^RCE4P1ut*|2Aa|w@dw{W%S^M_0j&~jA4CEM z|C9(-7viD3yc1}nH~lhbI952Xi1bVkpn6Iz^7BqbK>VrXfo3hk``^Di3%G+EuywlY zqZO~BS{H2XAUo9r^j`*%s$#(B8exQ-mZUCg?vuj|3-MbQ#36`;RQ`++7R3yci1^qO zWt#iM!Le5NSIo8)WsdL7ZXb!{_xt43a661UJTyHJ9Rxm+*4rT+=wb_UQ>~#i-Qy9E zH^NGaNQ01fcl;@ON_C32U~}E}Ns0VKqx6|0UYjI~)skEd;PqZ6_!yv9B(5XOMFQtx5;0J?x=$^T`Pb7SLC482$v;G6@r2)} zk&wNdGZ9kTex_s_%)i5`{Z7D6MwGyq%z}2xVv)`UU>LhaVZa+G zVYS2Lk9d(~DMHDhoXBPX7wHXi*T#?;zn*NBczCYcBb#(tEUJbjV8~8ZHyP?FA}9DkQ`#k3^8a$z%r49 zEg6P*K$_KcOuDOsR0~NUZ(|zpGi_$F=lAdEa49yDVx9wmf>hexYnJ#rvA;db;?j5$ zZ#33abiWZpC%-H$;ho)mUYE8**}bq@GRYA5XAjAO3#F2Ae1>S~u`%qFQAdC-denA) z3M%vPcyBDmiET9@CgP)Gh^Q?)Yws*ZWX?s!yh^1%=!Wv#q=@wszb~5zQd%%nvn1t7 zdpQ?QW+8l08ZoJLc9zy=^w2StF2;e`8}p+t3j_^XCTS-2`fetDj zLxBd&I?`%k5I=s5WfM-&l#TzP7^uV}-%70(osxPO?v$-KbHw5@#Y0_ltVwLk`%LWj zX0+7|j`Aec=hqm>xYT<2el&;fcIC_y4NT*P_?{5xjxGbd^_ej0S^te zcxM64eYye4Z7WE&bOl^y!UR!gEfxA$L;5J>oQ~czU6+Z+Se6zbEho&(2!wA@sxdpx zTPy-TKTjWQoThz7OF9>Zvpz+L8wI5O{-9mx+0O(I>liS_O}Z!>#y7TfCK0*84!Gb< z|B$G4Y#pNa)2i5yf^}?A*k9sDnje>F0M*w@q3U2fI&0vJoW*c29C*i<`ZB&@v`HSq zdW23yiwJMjuHt=?P^LsjnD-)S6bAU)8)SXYA2{y{{{!8rW>N&0%L6&Y|Bj%BRPasx0n zm~aPp65cZ|z4fI!O%xr=O2b zO@$Dg8YvIi7h$pyOsJ^?cRT!!)u$lo6Q*+R6y%MLzb?`2ui0P*;LQIKdVW#Y{Vud< zJ&Y^eIj4*DF_usa-Vove|K}<=1PLGvfe)s^v{VNofi;oZ+J#T)iXj*)cqa9#d2M!e zIZ6t64&TjGE034;%)Z*%GWXKQc(DdElx68t7RBOX&lrxZDIAM~-=ulp5*aUY2%m6g z@Wd#Cp%ZSZ7rXD_l;&a!kI~9o$n(U9MJJ?!#qe!#6@(tCLPl4e(;zei8ZM zV%UzS<9l@nIzp5zF;9*Bc_Se>OFoxNBtzN;yzu;bjcC-xC+q9rSLV5aYBPg+UgNo; z4n860I$U>4q$yGni9UN0__yY8xs6Ly(P*&s;B(uLc zpEpuqjvie3Jdo{6hhcp=Jw{Qu+Ubl%5a7?WBrGDBfU2tu-s7c_1Z_FCWjep^o*-`9 zaz`}SC5*ZhnNGDOAd@AKZMhZHem^b~f$fG%(Hmo*2u>*ej@9}h2##x`ymuTsV*H3| z4Z+iBU#R-)&gs?+8=R#eEu3M29q!c0_(2Vt;)Q4CpN#5C_h`#>+5LwgFW|r+IzxTI z30eOHh8naqoKf*`vV4=W2uZWhtkB3hvkV1I8eV+fa%yBp`OE)+a0DdOmAYTr@LT{) z3!sFkdy$nDut!>ooeOHy)JXl7j|eKHOb`i~d0Jf}7)A3$~(1}n_#w%(Q z1I<mF6V`&VY5E=miB}2FzjU>|iy08HQ6TwYfM2a9mHB(^jp=`U2i3*O zMB{)g;GJAofb@&74l=h+R!GXJpbQ%YE*s`IvMrooOh0)d7UrWue z+J|xM__z{Rlyh4IGy`M&>=t7nRLJ#cVoo!uYUpX01>35-S^h*m&udJ73feJFv4NqP zzLwABojKO9<^WT9*#T~%yVrBa#0~tnAF5XTN`qmr0cazb#8#3$z7>|}A0w$sqlz!A zW|@Q^cUnC)O**8?V_S66%XvQHmh1BV7>0EPM}ZcDkUcJVJW<&bXZ3xNlZc zCG&sMudPg4sJKArumrp08oE|xXNHdx!Fq4sn?*maQGW0BOWpRhf_Z(;vzh1<$rz4g zUlT?2F!d*Iq_=jw38_jpuNabp9F<7B@~BO0j$kMSmw5{@ zXHv~*^(mDKdMqQJ?FTo}aI?-8xA9g_NNiMdEY%5$pc73A`oqgmb8Gx)HOTU3T%BWU z;o>ho-8SqIGJ3>xRF~0YXjad{MBuaVtYdNqPHb%@yz1DR^ztHXF`*888V4b+n;{m2 z;P#r%JW|B2*4+$jsN(?lY3qVV#^nB{D;#H;xQnK!>1es;6M0j+;Bpl=DClJ}Hw*{v z9eM@7yZb|D1#G-(UUyY$*l;o@M#hvmx~#4$9@!Q-_)UrhQlJHWZT5J?V7yt0PwV=m z4h7{aL=%>AaA>&@i~7SB^l1>n}-GUebnvEM#!{h-(pL3SHF3>MOecYfGSb5kpB zT9?my%i(K)id_OBXE=ID{|SMsF<&1wk#nLM(FpM^4=X;8n&K*T^N~m&i+r~GVaId= z8Hm|gvX_+|?5d_n5skq!&Y3)^a`Q7#;eBN@V8*stq`A9vE@PGVczC8@Y zLbs%smyTJW4GDj0J;RjuDoBO2Kk$|z+B!z88slB~XAiUz;N<&=C_#!*z-T_5EkdxD zO0F*{w0q|QF7l-~>&{t04uz{1bptRZw<^gWy`^!q{=N%;hr1FQ^LR58p2X_@y!kI} zrK7WIn|UTIgUwd#gD^-dkGg^on@EC$!OJ9x1s*5is*FXS8qWFH5rjB;l@UVDH#E zh@`|)VPxNk*)>w>Vx6r8Mk&kHiOAG*Hc`*l7l_4Gl$U0)k|WJ1MDE$0$N;4db*0m` zF+qH+jTTi4*Oq%jvFqzG}w!mbG2cxl!6{TfvG4B3rmEksi=I6j# zZUme9gQm7hjOgn7Tm<8*?~XEfHh|deAUV(PlrF5ssd7ost)cdGPQl5vgjv1+#q#E~ z?Y*gN%;KsFNXa8Vqsep&snHdR&RS`(;`?a`lt`#yuVs}g#oBSZ7s=4*RyMLolh4=; zr@K~8s4RA-L66jaP+ykB#0rv8Bs%=YDt#4mtSS#o3@*Z+AkKuFtfq>XPydMq?64{B5j{ZwK*+_6sO}0uZ3ocG!);EDUY{>ss899qB zxe~HY`p{fT-SKfgQ`#GjZl%4X?@L}UAi|P!?8{_pX|^#_Tcu3G!P&VQc1CBDtFtdN z-dCHpAeZ@~o0!T>Jp+;ozHw%kn_5j*O7j4p7R#Xz2}qdV3mn1<&s|4xWl zok}q72@WP#CsDD>=xZ{pgEQ=m)t`YIvVRkml-f8lJ9U=E=rqtWMP^F;(~Gqx)t(*p zX)pIK5lbEFT#U&s$=#B;zYW|Q&Y4}6_~(NWgbu*Nb_GoVTG8GFGn(myYhApnv+J{V z2p)**lKI<*u*=jGTEpMySuF|e409agUGO2m+cKLFH6?q|yY+FVh@U9CR2K3^PT0nIKS;%^S@2eY)GQE*di$0&rQiWU7@PouGHX z>$NV!2zfKBj^lrt>kz*`CFFH6DCn;={p%`Y+dPDiipm$|KM9QW#Q zazcis+K~1{na11;I;xdld8gqoQF>+U5ihK4W_VSG096pYGT;VUJHWuhJ5`HkM39_R zu{ICRR!!~|q%KgbH!YMAMb*Th8|CVNVINdO`^n9LhybI!lPXl5ct(|>7!z4Or^oqC z|7PGylyK?q=1y&~h@PD5XJ*Ibc0AacLHlGP+n1{U#p0b?V&nhP7d4u2tb(2~=Ri^n zi6NkCw1TZ#gK8Vn*5&eO3_G+xXd1&t9OnZ+&-JW-4b({1X?wp&Ts5%JSiKsD7n94= zNxi`<;Ean5j2Xqa3rnJ>p7#R_Vf0xe`98))wcU?bx~7gnevn{1G3t4}5MT}PHFQex zk9a^S7n!~EVX{@D+)cDebgWG?ydI0~2I@HaK;Iy4A4*I>g)MMRV5fVuPr4pIBHym? zBM13d<(*0aCQ=SdbFP{^*8G`Wc_smUWeD#Ph2b5h4n+6^{8{nD}pec>u6&W*ri#VOr#^8I_v$86wx%K9g3wspKN$U)}$#X$(wfK(~FR$xS~z(})^clH5qPn0ehw~M(MwZ&tKhk|u7=X=Pse}g%aDraGVOxfz#7Q1!@)J(D=YSA^* z=9zvoiNN8Q-^Khg`)}qB@idlfDZNBdjDYXxp+O}7y!LgsFjAT*NWcg@x6C29Mxf5< z7Sr>}fy1F2UclRe)+WMxI!M2_uFB-#5yFfgT(y-W)k8!Fg4~7e#qi&rY0K8MxZBAy z`TxZlT*vsRK?vQkz-y%%jQfB|XTaJTA2p8np_~$!ZG~WhYScV13}`JH(`sS)^!(b3 z&B(#mQKin^w$UyYD-nBOH(M1S&3Fy^sXZ3i#K+a0f~l7LN&Viig?QvUZ(w6s{gXqQ zjB&tsxRv(TQA(?sX-PKHHHDrSQooU4#o7#8nTA<|Br6VOW@U3s*JQ3WlaP`ndEG2Z z=8XF&;B8v78@~|>je9@N9x~~#wqphrZ*2jtYQ|R*43bXw8eNCo&^Yr;MI9j~@Xk{K zslZyDtS`37%h6{1knkd0^>@8^XVz#wsl`CUW$WL{55cohulcyPvaHi5yOirCnUhZ8 zj@ejp#jXSIb+)+&HLEaE0_m*~0=QV(EMNCUz#xf&b&gZI6P|a^wpnAI)l0hv1(jdFij1x%Bvw>!LfCv`6x^L`zuPgtIZ+*^ z{On5Wj*ThXZzy7wfH6TP+*g-<9Q`u$KM8~4)R>XHlnNLdccSk^fOLT*mY^KddF83? zD?pg;Lu<5}0S+YPDX4B@TAxZ}SoqVe=eTzpnQ9+QEeSn#HO3vVWdckYu%X55_`F$d zE&Bv}_N7K1C&rg}8gE05wVda_%#se256Bt4C)sOf+iU0Q(C0_&6oIAPV1LjiH|4&AnNPM2iiZR{&&sOp>r9 zAkDQFe9Pv<_oi6xH=F7-(9w*0VX=Z4FF2wPsaP5Xacj3)9>(Mr8Z@ATFSQf zVMiz0&4ZqMS?}lTGEF4bphp_^mISJ$vRu`@kcUj*grpWY`D^_@>W)gM=DPy^H54Q% zEpevtwu$Bj60MP(fsR7dLYc!tPt48`cCO>a}J=S?@qm-vTKHnq`lU2DsT-4 zj;(}8ZQewMWer3BM`g#V?&pj*<_Z>^!=eP1J5$hC)6D`dkV?12%HQe{HR=3~bV(v1 z44}snb!K2U57X2W?`BE9-BQ)Smwy`t4rrbOVUfPriCv15`J@HFN@ev|Zj{#A-54Ij za8%_sXN|EntUe<#G5EAbW`9stPUh1$CZC`N-zqZ>9z11?hR$eS-#Ck`3ya0GBm%(~ zvwCE-8V$)rE{yWjN%PsK)-wJW9%ILQDYq9GjaM%jtx-jR+x1sU62?Tc5q<^~3Gr2Q zq|)>lo4inaM-kMjdh$=4~>ZdTO|M1bMYGU`@E6k)}$($f!Yl?t%O1F9kvxL z)jV(jyiM00FTis3s`2fo&;Sbcf%F{EW_3u6Z6$x0ZDoU8X;g0zxwh)G)l@Q~gsdOY zzz$ z_&2jSr$#``*STFhhX=v>=+Em}+IRiuvJ|#lozz zfTk5xzQHU5_!`;|%W5`$m@1!0#pnh|p<@QcBnX<2;~h8aOd}0u&=uIq!sS3scFF5E zjz0xefL2)qFreBKpD-KjFm@ybk!d4Mxa9|welQ+=#NP_*x|pRDpg$?SEf?a6zCn~` zLBPe@(;e3liagZ>5vEUCN>;J}>Hd!gm;=R4$VmR?%sy8ou&3dYlXvisC2K)Nn|z#dj_O`;q`vdnVESM<9oPj4{6vR!zs4q z=TTuDxr&5qDmU{HS-rjbjAzS17SHI5H7{M+NOM&DDx`S49C_rO6MTz@*D^$jFDZ(!~MX zY%#2Os>ZZLtOMBZ>IA=Mhqo%OCs=gHd@+)>WvgY+VuMsOO(~zEzBAWaoD~Rk0;%8_bP;m{}SC$cFxSM~c960Zl#MeY1=uK)`ERGhPgtD z$*lbSFMUhP35ztIPXw=b>bV59T5Sux%{Sh9vdRFcaGv-~KnN6$UW6iA7fV^5{-n}G zV)3y&Y+rD&RPu5<)c4|vUW;_lLm0EJELZ+A3xu}_{>ccOe-^C$%yyKo_&ZDf<Z)H3`z9^D%6w+fpdbPiS!C?RV{QZWokf`#pa?FV*F)HL%*a@XKg@GEIdriv7^ zm`x7t%ywT7$K#j`>oiI<7v?1`6kkn2Qa~}}yd|iw6GHJE^oZ!76j(Qkc$}p~7)xs| z6-G{qeb|n08xr4Lfl1WPs<+k%CS0tzpU);euKDcS`Vd=h;ec#dF5)AcRV*w{Kp2pF zAuyhu?!^_fhSk(r?31oj>fHkD!cbBxQ9g|qq?NXI@EmvS=%A-yp}H=8+v^0CWJCPC zGSia@7GoG@+!E@kT9wO=vU^`J>O;lqDL9WNXNcif$z zd*guh!NfyR-9K)SG&VFaG~_Smpx3Lkq$5U9{gd(%(zpWQ`1(h-k@5(>V7@)B@%3;7 zM>QRcuN1ojyS~v^m2#2Ax1s@}*6}qa%xYT8|NC0#&?t`W`cu#dWium6q>@$iPDAa8rV*4xk&ypTQpI?s3_@^z; zCy8PMmIzM>vM2ag&IC}5W1cikqU3sKdV(FYE-#61%Le(N)fAukMklCH5}qh6id{F^ z7s5t*mFcZE$zas9B4L8#zW)LPuWOC{*Y}I}qF=+IHN3m&9BPu2Mn( zU5e<|_eKf%HMU0fJ?mZO{=-dIc)Mwje&zN%P}_eQ>tW-$<{n5LboOItAG?^Q5hEHSfvoNqxq|Vw{4Z`42{! z_oI-OAD8ApluC2f*b!I=r+5Es!Ma7QhaV!}RS#i#nvf7el_?)YGro3R1>`JO)D^Lt z2+&Yvi`5kQxW*_=t!OPMFvj*d-8qYeD!P|}|7$Z`H3Lm#FyvRvl)LLhS(esoY9?}? z+F)&83p|+~ATMq>?w~65OUa*gdKE&|ng4ZN^TV*ZwHfs|#GQf{L4d#|B2RY$*nc4D zXKtKw3(8w9ItSEhTzJt z!d4)~Qm@h=x`gd(Hpc@7;9yLh-J>!M2%Qfqu+RKVmOUGkmyi6)IU{Zh_Z6Bqr~DCIEkYRe~7to-{hK<#En{ z*)PCZ;vJ4O!{5*rH3u>|hsSYDv$AiW*}ZdBbtz084)U%iapmP3_*f=nnispmh-Ma8 zgAnCoU9Lo}fc!5&9_eFlOrlF8wPM1>OcF$I2@F4V3-X2)Y=cLec`KEfN|yqo_Gt3L z7lsmSywlb~ zL_@@8eKXIW=5xMWB)uu2v!S-U@P1L)*i)16a#tQ5AFX(j!dCxSm+Y*b0{0&(y8a92 zK?S$n8D30Ya8IjX2et-w*)dDJrM&$Gl@||QN+95mh`M*%G{>5tc?Kkl&ecxWS|XqS zmqr@h5?MYI63u@ki7nI`HWU!a=u;5-HY$n^L*n9wccj9Uk1O4Rp(R0giZ1$}szR!@ za}@bKT=wKKgC+$kw1)o3+)Jf=Zx-2(Deo}S1@W8bVatqsAz;-WI0H-$Tf`qL0g@92 zyGRw5B8ee$-$B?!3>sw~6O^iSv^Cg&1xvSUe4}D$y$nOd{!v2+XXN~;im@qWZ#M$EX*H=-j_Usbl5C09}&_a@4k($kqLzgT!Itzi%&HB3;pXBt%R!8s3nQ zWO<*ad`9~6@ zD&)ia_XeXwOQ;6b?76VAj_WK)8K{ht3Cd^mC(1@P6!UTIokcR~*YMFd3+p!%1AY3l z)n^HZ%X0!*^FUW50+9`@o7n%bJfFxZs}C9XHSx(9#!QRrh353HR>b1h_W?BDKIlH- z5I4?SmbF~5Co;$D&ib}yN0(VWjklK7WE6j4|l3%Ks@+ z1E{(}=+}OlSYgm?;T(ffu;YMbXq^BR&=FS^Bk}Q2$6}!W-32dlSo3=4k`EjpwWl<(_CYEZ zYa0e;eBSS}b#m~7#rpI7k&4AA&w*gp?|}|w$xIl$dn1O1GTUX z3p!T!GATM>T8*hhCmjBE*(1yIp@Cy?{EEL<|1OpixNnWn!Bu5Qn;ZRT$SB0*Oba&c zLg)bS{#rg8tIt=t0UY!gOxex@963@08i?M?lp=79feqLP-OraO4z~MKyG!Zf`moo} zz$rXpMk1Ozl6V}+0$2C3`J~Cxat(g8q{(b@=zJIl7F)&)5%d_r(@Hd}|^U zc$s`_-1yYt>H&m`5-Jjk!!Bp|S0Uf9H`}#T<5&%Qup^()8O@=X)01GX> zQam9w=Fw4xhTNxY+B5Wqi;<$iS9-2&*ajds5H}_5$~$X5b(T$W}92#_!62{~n0Rd6wTmQrH*B8h|a} z1)VZkL^$ABR~Zx+&%l`ueWj9DELljbXE5hDbOh2I<0T;HW^B*;8LuQ61EeS^|L7GI zdk`}=S;0nYM4xjGLCm|oj`%mClo`x5s- z7LYgw_hlblnm7|Txu5~aN6QlqV#C8H0N&7uE&qe&!Vy3!nL*!t&n@TUu!)7f95f_P zeL0QEFt&fWw|__ws1wK?=ipQFP^V9Ow{D=mCuGn+*AyPvDb65hY>_5MR@!9@V_oTU zQ>`+v!wei`0oh@G%M~)PSe^*$gqxX>4fG=?S^%p-9<)GG;k_W4R|Wy3APP{Ur-fFS z!kh|8jHzxuo%2P%=w+O9wjmau(VOnEC3vgIb`9cT#oDyhOhtS$8+KDTD*IVQ`l#MdxwGEA##t2K}2HQe6};8zu5uV4H%9A*Kz`1@t=BTSs*LQeDV zfne+_fQE}&r}1()W(J_90$xH&45GIrqzK$33w31*H22<)A=YxqcN`RUQ5n=Q(!`W; zELkv8HhF{ph$%vFxRAzxIt+8eDkowjJ?5dGdYVE`%WMm8V?NWnc#xILHEm z`mSSp2l*`*k%7cYPMQ1!U7y*9ka*rT3Dy*DV~Pn#Wx;@IR8=wIP<8>P>{%q(=QxW- z(quX048WERCvqm=G#t>+-n7lQtL%S;x9CYVM< zlM(SKK@v5jMKe}!3{R7og6P>~06p2EnDLLiQ?goRU?&+k$O3|2!s78(ZC4q%V(b&< zMdk^l9A-~UGzgoXp|xElJxK-njto)Q2BfB|0L*1iAkHfhVlIW;$Q!>1$dZSl>=}v2 z)RBSE&W_PJ4i^%Ssl&NvmNhN(=nF%}q!-DGH~1uKJ?4u_g&}-}Ln9PrFL6!6Ms<~e z9c17jf6jdec`X;C3@nyizrfGg0)C$1lTWbLoCXq2sS_Ydsv2ln z1j3}qqKQZU^m6Y;o&X>HSS)F=f{}8T5tA5WQWUQjq%hmsQkxE#7_BLYL87evOG{#! zQVO!L5}LRP9A%A_sP2hF&^aIA?W>=Us;G{;7L%NWeVfPCAQW%8}1O)oZCML6p<-x@L#bj2cA=hd6NvfAwF`dRR*@7frGq- zwf(e~hj9i@j{u1QbOde!q#9@>3-|&PIiEI)C!M+iWeJ(;q69({1Vx4^L?T{+uJ(^` zLO^1{qx9g(V`X}z8pKL>WPoyM00Y{veGlkgG!R7*GN(Ry)UmAk@tB4Z$x}r%$D}P(SsdIr zvaOVm|D2#7@?nFNwtuf*!Gh(Or9cNmQ#`}8MwNkWXW%dki1A|Exh?zS42;2zV*sB3 z6@B8bK{}L32ZKCfq|`a!82@6}0G9LwTaH7^LvGW6?Aoer@an_j)^CA^LTu+NX?8rZqDAxRA(#GJ(#76w~a^72}?7U;xSN}bC z@Se28EFgQbb1qAm{xx19pV5#28KB3$;Vx-;=#Y+SiZT~GtVA1&5#Y&0Yrq^cb-9rc zjRSOZ%svVP#eCcj0g#==Bl(;Uv?wex=wd@TwKSvXGDZ^(`=lv==tuD=d(%#SD%#f&Wmm zIwb=vCqqsf%diVVxM=f{O!BOher%ioO>0*76^IK5$Y^HdI zF~L_MWB=erMPU4jC>*x9XaEe&v6;qW`?akMY%c=`SwOaz&a!;34Dfp(LOB3po+S%x z6Zxn_giI#s0>l7u$r&h9gkTwD>skd{(#=Ug%{j?!JTRdFW3-pXCFnw6665B}GE`;! zgS_0vJn30TtVhp!r4RXMG@#2p*Gd^Tj^!h0(q;@`Lp*pGfX4Y|{3KM{+YH3pejnmL z$O4kb+&=7}OA}*&u5lAUNFYt*<06HO6p9tIpNJT^ElPda3UPK+Nm{6jn%5KDg!&pz(E#}9p$#%L>w`k$5v#ZHkE-LX5cUj$Z)E+!&D!#tE&Q1fiKelToir_oK4VNc!`~p zsK^0nlow!PEJ628ohCw*>{R180ew*OXyYxQctS#5$fyG|{iC;W4HT0DB$)**i;Kq$ z%KlBKEalvzYm2d2(MgMi4KcD%xL{C=-q{+TelbtFn6=s43}AQe&8|Gm09i|Dy7{zW7>=9XW?xDeWL`z!<<)|hktZ~og4QNe=fmZdMsWKSx`lwnK#)EQ!fcevHu z1I6P|s2nk=cdRmq&DdAc&a>z8&(r@t``)MA)fLD-zti?4_B+Q!L&IMJw9$p2>XAT| zAgeWHFSSM!ZW==c7ip$vFRpYVticRfX_Di%0*)E2nX)wr|xMy3x}A% zJOVjs0#%F&s2sDYCz2*!n>Yf}W*`ljXLabulykm9T;QBC!AcRikTCso=EfM+)WHzv z?AY_agjp1F>^|q_4_73zMQ!MfUo>i2F|o0)Ar=N8$Fao}0c3nEBsnIhT4i7t892yW zSi4AU`4}>=IPLrO2;c~mK654*2XGP}qh&E6T>4C%3<7RIXd93&-dbPc zK@n?Xvmvb*6(`B?O@P*1v4*VT2k##7(=Q8&+fYOl{=i6qCZ`x7#IK%6uNk24~M@Iwt z5$^df;hKGpiN0+tury){dWpJd1ZM+uWD)dWk!y6usM(>|Ko0%DVwj{=g7`Z%`sS-O zOc=jg7?2gl1*%Mi_huIBn#Aw91(MfnhIJaFO|934IT_~RD-$rp)eO|2GO*PQ9A*Kj zzX!6_H`O~EPedIq0g1T;K0@xsSZHkEl@{kL7LfNmy^Mfc1}K-HOJPhnPdzG&kDf$! zRLGF>q0E031_}{di1=q+3rj1!SAnB^658 zFw$z5A_F`e_M{zV0ojwCb6LW?jb|VRxd6HVJOgonC>CpA99B(^ffjv^3A8CfWW_{B z8;K2R;(u8Dr26a$GK$fdG3YLqH!e3{1>IlEoqu zMmqlqHnTXO3J6?7h`cNugTAPy2gat1#X*FKB0!(8s{#|MG3n;z6^e}|P1>-RQ4Z_3+<9*2Oj5P?lmR?e+nd}SWC3|k zd$p9_a|Vv$dmsjJA|77>q5w&k63#b|8%y$f9&}9Di$-u2&3ytVfz>U0NK-*m8PHtv zDS|$6U+QBzRLL8W>_1Ja-z+R3g#rz8qK$pX0k0}7q@L@K0383=8~wX@NF!tint9eo zMr5Co)hYwK$-qGt5FC>}s&|v)gYuKd?D5}$Kdj(|7; zIiX)ZX&Nu>c)4!|WRj!^vGpoa6bn7+Vj^{$lWuOrmz@|%kOd~@DL@7BKoE`KPd}Js zpE{8b^2nBI6pA)C+4?cAt}?KT3>;hm`FH-x|MGO#uO8b)a?6J^aC8Ew3()gfGJ%t5 zC=?A4%{h9>8h8cbGQ*MoaY{fHFYMA@o`K6M=mum-*%fFKrz`rmpsdS#+RCux18mGm z(u;P->eQw!v`|AIBa?@@$jOt(NQ;Yu;+GENbo>sd)^_v-`W>~+1sma=rYPZax3qIL^QIbhUbBHe0>Fug}cy?A`h1n+8__ zI=l)fd58$a^H>o9Gzu^@qp!D|lj4~G8ZrXWCJ+S!atX4wY%P6HBrM3k3_{rq&SX!6 z2!(OTJT~p?9uN?22hTpd%}_Yw*i??A*rhRYXawu`_ekV*rD;~y6hddvLU zT5T%>+t0wIDIhPt`0CM&qaEQ+5v-!NAFRf4y<$)RCGmzCOF*5)$3y0=F)udB+JQjkJum1*f0wqTxd(46&n$!)< zG(}qi*%)I&Qvz;w@}*cb3)09I?UIisA5m;f8DbiT94nL#H;hXKuTermMpAm{X)9bt zcoc*M(XmmVv{>Ibfaa$Lm4WSNU@r^E$3On@#Y6LPd~WskMUZ6@N_+U|IvDN zyvA>4EM~L$s$VZqIo25BcQgQCC?p-OI@YUoccuH%FE7`R{`TqonLp5XM^A1)&E+A_ zmN$B)CBQG~f?VLEi;V_KUIMWIT{@%?NQVY&9Ra?_q|ry(cW;p9uK~Lyf6#A0Edyao z;7xmYK~)z8VWHXQ9Cou()HP`^9Pwp*0S$IS)+GajYJ#-};?df9rof?VkS5_3Vj{fVdaA*o$`+ zf*jw+J-X8O%R2$mf>H2C$B4kUEsST|LQ(D~foHN*FpQ_%g#pME3;9M%MfCI&g<|kS z-QZE1fS*oA@+4W(_GL!?p&bh($8=lZHh;y1ytFHN*h43~?9~`bO4>kUFi#HO`5Rlz z6f*v5m4Wlfz=Z?NkA3V{AANH%djXLDJ`{p)TK4_>Z{A#9JI3i%-!FLoiKSt7G+(Ti z>s5#HF`qBGRnHm%iEfEPvRDulSjgu~T$12-nOIFa)D^J$j#nvj77`lkU;XrH|Jr9) zv*-TBes<*p=dlwG!KcB_aGHRZ;Cxpx5oiL`r9^`&8zx{um_D_Cjef0EqL-^WG_rfK2 zc6@u?-^Ttj@B&DQEzuHI$rH#1Z;&xGOPt^;Rcr2*yeI(2*$e8vb!` zU{tL#u)7TGtbly{NcM@6EErit zvIjxn=wbqxBtR0xg_DHMaT|Ela@iSVWD`_b3}5mcBF#Tc-sPWe(y!#h7s=$IWoV2| zqU_1wCdQH>4K4LN^O7>|P3Fh|4S|`l@=fmy(ZBPT_NuQ8>>>kODIhoiXaCzT{l??R z%SWHOb9(!k)qM8M^3G?TI_i%XV9l4y)A<6I9Q}g#pcb?47}pS|%kCIg2P<4MEb!MO z)^l7YtQNf0j3a2xM;-VO)fzlJ^1ytKqQHAnGd%Rb0}Z;I@pcs?SWZ9(1s5W?3enXH zJg~gr8iqQ!3V|&?KIvRSbX?-*7QOYy>o0z4xqj_4{o?t5p`RT;w~HNdnSI>EGmsfR zg+*W{R2pCjhHRdD>}`S}`;@Yr!PD?Na6Ff(+}IaH#ugH6RUl*;@g(vqwuygQBVP^L zA?|^xS*Wr*_p27s^d>;kCHr`t;;1P?Ge zpgjN`MFIf7gb1E-@KPVLxZ=POhEGz>R*NNMa7zkv(D2y8S0OW8IN)mHpd)sPz&hyyZmRFHm zTS|l+QG1gCdqFOO~Y31t;I8(G(dfMOqWp9dL&N=wv6xbFh;sZaICCnukK?AGkD$Bs_B$8UBwpFHj6 z$8+3KPZ}Lfpz9O{)MuPv;4I2y&E&XW%p42f3_nzB@)2YT=Du+`X&%XafDk}%f$+` zMZda(n!tzPxZyNMao`87z(PX-!Ev#~l?iWM@mYo%Zbz{);C_{lo7DQx1W_TSuNtVXDU$DkVv5J6sZdA=JnT6GCl!MTWG2W&7EEHY*}a!-F$843*T3jEd66r9o*UoqA@i3Zik!+^x3Nd*)1T8lh{h?GM#^qOhf zqZ;+YWLqzJ(KculY|<%F{XSP3;162%f)(e0(4#iycZ|O-5k$FbV0(a1Q2ok!Gc0$n4=895ULA`28`+1 z#u6n11q0a6n^S=M6<{9}B%jw4_+%9>BT$X-eGxt};WKj796S?2++IJlJHNZxHzu z$sEir2P@=-jLq7vrLi3r)C|ae7-&oLQXme0^de*KQbsD_&+3fBg4EI23_7;I=sc-y zWnlXmIC}Nf{)sy$Z(e!z)te``fu?unw-=Akp2E5K?K%DqP`^0B1;?G+-Qwt&$Hxrk z_Q2F0kd1fi>JHHRgg2k?{fusQf~$!;_zngbxNSA-?%ctp#7Vb4x`Xd!phg^@`upJg zWYiI^9&lG{KEv%U9z(cg1pB<@gim4tmY2Ba!B6C(m;lI8aC$sQK|^h@Z$4UKnZb=G z9zXym9+ueZF}aEcAF1>EBD{)Vd0`dd_dnnU-xmP|r3ae2lSTvnL>DOhq!wgR@31!B z8RHidd355?C7`Un@@xI-^_SPj&wY0{zxqwv-%XdmgTU465O88|affl+<^;?H!=Ot7 zWXqVJ<8f~}0xL~m8{o@n^AR2iz5?#>m2~!sE(Cmsbb?W@ui) zi8vl<6JlpDhqL+HxEevtIL7B=A$ZC&e4Nqqdl~%H6n~rw_r3Z#E;?@Gxd_l61?Lzt z{7N~B2V{>>SXe}GrGTpmlnDr-MDPhXpJ3?L7bAcwKCFdrlAu1|hZ1#g;sC{6CR7Gg zB~=F$qS^Wg&sF%bE@<(R1l4JV(xNLQANjnV(qXD+A-I5n1wF4v^}tmTDhyO{$%7s~ zS9ji+-+tw1*NZ2=xjVl8&m$rA-H~^AzrS@;=-Rjq=Y)3z=3q4>bXgGw5AHX8xiwz3~{B@$s1ylE$WX-t6;jk@w5@wpnnzFpXt9kj9H zuxW;)kVT_y%A$q3#-ZNE983#~n6;@4>?i|As2ZzlfWEiieyh7Wf26y0^X+bZMF;Qf z@slgv8@T#F;pmR8;X}5k_^Cn^kyE^_f%^dAImh(}dnIf5?)$Gb~-LeA$WC^7(eUKFuPpenLD;UWe1z#xq8hu|8D z#mqlz!mA}3gCU5~lnz+1F+<}A$Mk(f6hGTxf#W3&mWEq+*sNdst?u@lzq2}d?jy6s zCMs0mr25!g zs8m-O*hvOZK)w!Sd`edc^mz3u50^*!6Mn&OcC%mIx(Vtd09=6bF+SMD6F2>;q*G)e@IJc!&>caDhuEx&@C)ZkMQ z+C@o25#vLC)F~Xi_Jz_1IjoCjA-Z1N{^GCnGkg*8nL$!Wei0)De)1BUuYIfDM@}97-oiFcBB$8ZjE!$6`d28_ z51J?RR^%`H~SX{*w!W%cc`3WvJP=rtgfVzB3 z^DG}2iz^Zq5uClxu0FEB6$uIkE@SZFtnL)gBJf;ZpQAm+Qh-$t>0mAhG zeutem$?%YVrSFU&<9x)es}p^x5I*=(U3}~pU0i{n&McRxB&a&PI^iV-k9$7I=fibA zi(wJsQe}qw;7&}nN$wHE2o8X*{{4tMY zsbD|}=`)DNkQTkzuiV>~w~D|#>N8%-K%b`?ZuTil>0gZSDY1~HZyRXmKBS~6#;UR8 z$iV;6SZkq-t#Cn9*Ia|zT<{FcK8pZ9f%nlTvFkqZ>=P&d_uu%-?|bWl2Yt`*_$eZu zbg$Pa6gMg2=V(O_&Ijm zB)Wrw0vaw|@C*YWje`#Wujd# zO`3%85TNwsGwGa2H2i7-Qq{)(!xlw3lz*$qOo*o`*$-NE7c2v_?n`)od=gQ80?_x# z$FI&l@n8Oj-~8$Yi}F2(qXP2fFMk1l_oNdpp0V4Z1=GU%0K0Fs$-^7P_-^MQt zUenK_zoBOcC=LK=Jj0kR@hBVx0Jn5-n+kStYX^6ba1M_n&d)_~6reC%!AG@lJ%HnZ zmjk#&Va?$m8^Q;y@X#F3Mfl08S$_rSe|vtXn_oG`gLYo0padZ{+|>fG`?&~Q@QWuX zLUC>(9H9D` zFm6#T@Rb%mNP#T!!VPAqCV29W&jG*_lpv1_CloZ^@WL;%z%@T-z5u63Y!p@M^7*hD}Lct!y0OR4RgztY}jRy6B%e0%FXLn{K9w`RuoEO2CCj$Pep4PaH`o>5afn744$#vE zMPu2mZ&xMa<8EujO0{ac&j9ulZ`A%Wj{Z*qD?V|2_4t$j&5ykQ<=scJJC7Wg2*$v#fR@tQyRq~5#z8-T^`=DfS|G(O1x)meXX&Cf*u)p(B4b&o#wcz^olZQOD? z>2CLTbR+B$pgKN0g@^4kJs06;ZCCgvI;sFLd(ofZGUqlQNT8scV1kPnJYT>y2mTfm zKfa5yfSSQS`h?H_;+R?Da!216!7m{$aTS!$MNoiHukd9OUP&-)Jcm$yz%!ZlJ6d4y z`y#N4D+k;WL($+*!1Hz$0?d+Yq`ob#RY888JMTYw1m44oO# zPsOS+;Dh$UUj(F31IGA#v|UUbGF$KzbZ{={Vl0p_B%fFS42Nr12X8L-U-;kk`0<~r z$B+FqKQd|l&>t`}`Cc5icAy%MR>zmgE3CM?UGjrX_BrdS7VS7`VWOq3S+v1D8LMAP zDadE!_4SYQy#?+h3x78j1*WjqtEq7;ms?gb$Yb0yhNsMuRjS!>bWX8nj@m znM@lS2ag8efUvP_hB%-RdJ*3c-uRJFw2SByh(Vok5gesv0k7-AXiSjM48k*q1SBl* zP+_Q0lf~$P5*G6o`+Eo}RzgiSK%Ip$jFn%S=tUwxMAN+?O|+}vnZnB+Uh@1jTo9o3 z-7{hcy#Y{60mBORnPL|aL=;va3(KCVMEQ39#@AMh>#sChPkp>@wjVj)kxlj%3>Gr> zywY=j_~YBSj9;VW*X){`!7yd#@d{pQ^le!on65uoGW)W@$h(#c;Ml+SOSyIb{k0Xn z&p-d0?vFliQv&GQvW#Qx$Gq;V%Z)b6pp94Cq z+56ywAv)BD;^4dGX>Y&A_ll9by##l7?#HXAzW`Y9*8FH60*JrR)$@FOr2IaEA_xH- z*Q=eKv+cpHTUC4NqP51RI`;*2*GtR%d=Y`Z|w7S zuocD#7(eT73DI7-DzbgzP@$I!nkF$T~=^a zOK;Tik|X*j-#Y!VPTK(a>s!YO^1Du>j5Vnrd1YkYwQa8g5^TP!vQCnSF-AGt;IKc> z-tcd;@ONn-nvF_Uj3%O_`NzN4xRoF8k^+1y-{iZv#8>hhuf3N}H`}|v`RoU7zRTtw z>S{(C@ugkx@TgyW@x|)m#f$B^b5(Wq>e={>@9g+KeXH5u>v=kO3>TYHS)?NAucCDv284)< z)nXTER3!m2ODy3es>u*%M&gjL)EB9Q`r-ppLnrZIKhcPd95K+C!V3chkXaZEQ(%52 z{5u)gMLftthb2aE-PuI|O+42TmQan+mn*AyC@l|MV4N2ooS?jgC|+1zbFo@p|JHo}=9}0?er7N_^Ai+)=%0NVtAv8?LBL?fV< zl6z4mD9#5*6feo+&%U1bE#T2NK*GFrUtJ3&&wHiC{@j0aev9VCx4F?kl(sS3=^OPj zj)03u2?hOCw6k$-f!p%BzG9$J+orqzOX1_by8BVKA4bg4k@chh&f))?d-Y*n@mB)`)5Q%5y;2_q^#3KTTv~Ruj7R+r>ojG&H z679jk9{MUc=ONw><1GMyfQj{_edFi`-Xmv;aRnTV2xS4@RCwE0O~iRfdDkD4#=b(R z5yR;sW1x}hLZQgOqm9rMct>axSbz9PddFpw1U(EaQ1ML%@J)$t@XyUkm z?&s|9D~kDHUSZVnoA2_u-*vw(=$N{)$F-01Hb&BQzn%e22ql&%jmA~Uh`dPJc2_D0 z;bi^mkG_Ae*WMj z5jga+P%_b?fWHW7m-+%CB8{>J@AIs^LM7D#B8(m}VT5Ib(C`>NzyoRlE|v{9bkZ<$ zB?{MC>8z;y;Le9|c>qADqAeIbQC0*mfCvM4udy_G4G-%CoSilDC&H^SQuaw^8%cTIH+$#Bn zkw>2)s(Qo!1!!og6znP-gHhL!W#b;@m`6YU=)N0F-*z-?dz3Fz?n*I`lJZs=m;hli zZaC0BZGA7|y43QWsBhqSd{OYx{@@Yk>Hac=yaa`x5>8 zAhyU4P}b-Bb+x-&RiF4o)xIK%R8O0QN~@}F-n@=+oL6|h6+)mi2Q?qqMJC7^)NE&~ zX*HZ+Gv@xb#tGtR>3ld`0vgywmQzgPTQFa~UW1xw=;qlC&pqNe!i=XZtuoZ2Z}T*1 zfJP+)>>xN8y2O@vD&!MZC1emK5b|3#8L|HwLwgAguZoIxF@VnuDa=q*Cb>e3c{Z^p ziF_PIm39%u2!TO+eyE%@egj^?7?gx~W$_vm3BVK>XiGVSMwFMZpZI~);grB5z?HJL zOB^kZ07MjWcNJ}LlXJ(5%LS$T>*$99GF!kt8Vbf= z22dK_Abs@5y~3QiD*wxMKL{J=MwE?_2`1*0&yu1)|CIqaqPTWoYe4Z{KTzxPKgvgT zT$n+YHMo(m|$c_hFD-1@u_H7n>)jh!9pqa`5UzZ>>{#d<5LX{#z<(9Dd-XLf0ZK^ zzRt5rl>^f74ZPrx5=(s-=$y2R#8O{?p(RLS9~xeD7yKN1%yAqog6y(}S4|f4q#X?< zIv8T(&{}~)%ZDCV=IAYVn3T@64I0E$*2p?)$n zK@nxW5&|W{vp67@fzfueT1*!QuY92yoqB1w^(032(L=NEh-@#IcHw&R!8rOi=-`{? zkYCb;{hHo9?zoIwHnq<0+wIQLC+XXGGo}0fZ9pd7mvaCmUCySD57Onowc>vnEa@nB zoY%2=zivt%U-hYS?wfJ09iTWCv%7QjAA{}sh_RM>e3k^aN~Oqn1U~93%=Z${*>gOG zm$tX7&;6%=_M_Jxfr;ODQ~VA9=|{*9R}k^H3>Wlik*ih}iu$H{tZJ`aTNS&=O*C0B z#1HIjwTE!R$>C00j|snuU4+-_c6Hb`BS2*yA>qf^MP@AHMeBq~Lc55ob)$#QF2a5y zYk!g3>>`*}cw+~+c}QZwe`%!7^) zf`EgJ#qL<(2lSxpB0vh&5@J$KieXE+dEnOPxvX9#?#VqOf(_MAxs8R-C zd(nqq7p2N|Up~GtbG!V)39F-kG%SjP4j`B&i7&^M|=gr5&Kvt)0vz{JODq*BUfEo>ll zZD@Fx`VRFM5gtH_1qJ#obCC~XHI{r{o~_j!kwbVub%tPo4~R>Bsfol>>>{8j{R1T0 zYM85jB;-*>nS+u72z_EQ4>g-%y{w{c%PPt&U>K!@2Q_NkK{B!hL88502r0HH z+AOE9e|iv5Hh=lOVi#h-#1@56v$%7=&5cxR`@Ut|EVtfu^?T(LAH>Mn*1SmcbhCJ_|`gEdxVwmwwr zg9M;~n~?5d;8ww-7ke|qIgp>_YBUy1+W8Fh!44sthH=-!Ip7o_P^gb@VZ8iDJH;+S zv4R`a8VFM0k zLlkBCaM(qBoWh3%$UuC;U;-!b^%(SYF0RrdJ~5eq3-x=}fKBLVw*S&|&FHZw2a_jQ zt<{|9zlaCre2Ue4*KZv&!C-VTLkQ$KIIq7o)6R;7=qkM}!ljA0Z;~UjJX@!`?(@8V z=TX`%xqZ6tEBdEJ8F7EF@4$H^F2%fW;c|)wZFij+vx)*G<-M*=awBbB zv*>$$ryEC%)ik%gZ=W$nE=&L3cn}U=$*Stl_?|mY?$xEyg&+OX-}*y{c~_ zkFZrbp6l@U@aI68H6oA4!O-w4oe&8P&&D84b;R$eY)u%eoLot}WPcJo0UP#bCy%OQ z0xzPrLbZehuw>s6cstu1x=rpXHZE}m7C6GbCAi5qV~h|BJO#CjR3r3H8ZneaFkXZK zo|X)E!YM!m|8iuSSb|l=kIpW_P>h`d^r+n{{kvtZ*BEG!VHkgRD2Ws>k;MQGYRQes##AAJJj@mQI&20PWuPxt~*~~9#nQsiBR4~(= zV8n4z#nyeRDek{ z!knS52;`U>eK%H3iA4b($956)XvWdfS46)I=0=!d9T!nE*)~ECyc{e;&R}Q5*vEvG zQz4b`saraqVHXiul|+ICuql@Ls;L~Kl7~_gvKcF~0z#N8GXx?ir3eKV0%Ag{A%nA# zgGJ+*qcA}ul+2YympoElPSo!dk}H*>%3r{g&7C4 z436lOyF5UoWZ6i^POTo9e5~6lq$v4*8_`8qx4YM0dDFu-$`{e|>&Uc?Xg1CTU@UG0 z^KG4a>>Z;nO>5g$ny4<>R{5_D^ zZWQar9~aS_vm+5{`zpN!cy#Hm({iuerg*6n^Y7avukXLF^M0Ox=U-p^pY-ov9uF}E z-Z6kk3qgdFFW!QOY5yARBGm=#B3E%3IZ>Z$_xEpBCyDH??u| zp=#VR*Em|}uJA47ahkAU6;32N=OJ?NP_P6SK!C;a4vr-n5VAP@**Jx~)0ZuCSF5q% z)MLwB-~ET$ScvvGXUjtsQHF-+rVLv{eE5s3x6JkJpHg8L>1_ByoxL$dL;`wrEH1O6 zalO~QHN*`bi+BeNoQe)Az^7Q;86@Ha*n{$mF!0u)W;i&arm`g?ri4VL+{1D)Z9w=Z zaud6V>NGyt*%zTo!NdwU=7*u5jM=xpIa}PiGMYU3(R$>*BXXAe?|AG&)xLR)!N<>r zS3-Ca*D=pvq#c8d^twK!Nz0V|;q_39OdWNjutxBN+A@POg_ag<>(*3V)HARQ7oc_#<7hRU=cvpSC{%8R zIHUl9i321-SdfcrHi4}J4T6noGejG(pe*&(E)xch&?|HjONTW8g3^>0bYKggd!-(x zPG9mx5&(&-y^x%kg+v3tfKLjzmdgnMh0KOyHCxOMUiso+bn@lF_W6(1_2{hG_uCPS z)Gs_ZE~8`c!5o>MUq_!GX9rLIo%aiK56;=uFXc+%9#Ep;!u-8WJ08-?WMTNn*WUB6 zuQHBuWogSsEyrTFakif_-eal6`+a_?Zy7s{Gg-&?`*NNwTaNX8E7`AoltdXf=@{R} zx!kv`?JKk9krcSM6!@V4#G&$lgfib`KlJGnPw2ZIkAeOoEpfDV@CDe1Q|_=~m3EQo z;laQSsv>~m(a~Ua82*8wyBQ3}%MwScCXN>T6h1p(FOX`0odkao?IJ|y9W*1>Lg6|- zn*a(^w#Lx;>bRk_0w34FPE26xi!$(c_IL-wsME#K0$d1_605QL(D0$Uq8%P~ zCIoa)F=Qy7{1tW8#YBH8KMi|g6j+X1$ISiKr{I|9R?E%5Rnlr=X=*!{r1Ph z@e@Bj7(MpW6n5(G+aVVAoqrG~^f$PWNtzpskm<%b$nQ_391=Xrd1>)ABmDvE(`qm2 zSif*-`|5s>-Kf@svmcI);l@>Dnxde?_9*ARCN;Wb8E?5L%MiP?k6&Qb?Owq^z9ui>jwmWq8fXG;Jn@p>n(jnKmho4=P$Cwl3ju^0vYJI!Y+bo z0YI5dhHhL%OcFxF69UDEzDrb7BN(y%A_iA<%Up1z9f2S)VeHrd!HOkU!Rg`Mp;O|7 z!5e-{>?`7E0RqO+0tV1_!LZ}uWl*V*+z-ROp#LW+{2~v_IrTuv`k1|?bVuz&P$VF6jsDQ|YUn`vlr5lLKIw37;^}ML zdOPT;)`LIO@OX{eORjpmBzeo=<{+!h*`ph1!$BcqS4ykQRB(rH=T?i*a(c~(VAb7k z&BP9ElfV6Dqu&jt9$;mY2$$_m)#e4_Y@ZTJv3NlZGx@+4lzka0`0FS;k6RV@@izc8 zavS$xOy&S=?yDI*G}Vi8^saxK?Odw&87AO)0-A_iA>*d6@0Xs37-xvvmI&tZx7NHG zNo&4(QbXh`m)nh85lx>9zOd+TI8Zf2rP4EHlsG=A*`sUWnCg3tMLB=&xG*e<#UOzm zwNx+}FW~7^e)dj>_zlhpuzoH=o%?V4^4oN)(k1j21Crz6!o|s=@CBsc`ZW>yli&%~ zLRN9rycL+-<&*TA8O0_t3CdUb)I`rTKw&^XzN)G7ssR;BYo#WOi7$={qI9@no5c8p zX-)B&f;MFsmVJuuy_u}mXFk0dc51cl>2l8*bNu5{PFQg7SB=(Et7MSQj}{n_mI2Wq zQR7&X3>yF>oyGqyd)7A7==U|J#IGX*`NNay6B-t#1yWSS5{TzMz4hLHd0%MuPvu9{ z+|dipssxyG)|34!+0r^MBL3Avvi6~a+jlvK41YgV45dtrMT>q9B=mOi=2^OE=Swa^ zv=<=)aaiy3{8@i{KeXIhc*;-=Z8z@~-90m;rOQ#UTaH%fdeEgjwIR@!d8yU++?BbT zBkr5+FeR1BuE3;~h0$Z_=C}5BKfG|=rhPvnr`QLTJ=6BuCz=Vh*+>!ZoKWB>4 zAXB6QQaJth7av;PIx~%avCT!PEdTqqA<3PI#KK&3GKCEhhtyvjNXw9?OvB=(t7EM| z)tNfMG9f|RAHZgwKc4>kYlcgqFkmfI?VhHrizV z(f0ma%7d&x6YDyYs_8jZ6CYr6{QBfJ%e<`aTffzBc9v^(Vb9qSxbk4=dVp{&-?d`(d#S8@o%tG4{f zh@ZE6@&FaDnUwF8Lxjzkor?eVY~G)cgp#9lZal)6fVg(();4-U*SKEiAjQ`@Zh6=f z+BijE-uWb_q;yO|D0v>}BW^EHfHQ~sBKt3C9=SyVi|GNlYu)+t#pl3??ZFQb&HU-O zw`KPrIcZw?i*w8UlVnj^pXSa$wahWqTc)vnIy)8n@n@On{i^}AqtWzk>>~^-_aHhs6 zA0YDeXtTD-lf4BvQ;*gA2oxDX)W=p+vmL@US@%ul+#`bDJUnjN+-`FKn|J3Pzi5t+ zU;NE2zZiLLWffUF^Xt=Gu(6+=t9Y5~&@#T2?w$|qi=jX(GeyeDIyYvQ6EABEJ~Q)y zs{><&&m_^YLUvaL+8h5?ARxRBO)_0%Fxdg8@h?*kZnv-lGcq?;s$txqgzDv|Qo65N z2VC+u=)w)L{X-WlfRR$!fu3uq@ZyM*?6fpjEzWg33MjQDra9w2XB&|QT&D#-0^p*4 zwRbjvHU3u()T1+G6tcg#x z_1B;5SMT{E-%xMG96V9xDQ!-Ny5ZOt_0^m$qmb{8j}JnGpG949krOChzVpyfA!Byi z8_&<*HO3s)%5J(2-VvR1KvmSTSXO05lM7aV)16m$z(V7|!ZLp02-(rfcv+$?w$2f^|&r`GH;>-`FPcJ{;o+xR(g=&w)Y- z-h=X6)(EMT)F32n^Y;+iMN443_ig3Sz(^r^0s|`*#VcK~KUN&jm3e)FZf+>cpLX=U z5`=n6;YL_YhWELY6+jFSP%KFp4&ftt0gt@Xaa9ER(|TuOG-szeGh$C+TWVoEgL30N zW!0_QbN)O&!1Z;0qp@Rp^h4XQPs^dS$6T;l#O{r)ltuEx?=fWERy+sGD8B^PugcLt zv(S6bqQ~x{R{oTj397=j-9zdMclts~j zPz`HF;RDz!l8wO~Gw0JRRYr_@FP%8;GuPkPwg82$3lscDF1F-n6IgEP=v(+Y*oW)yTRYDkg>7#^+%iK@+v$69tE%#0ZVZv}~lqM#gCEjkr&Td|$_Z_6pOR6Bgo&9lp>Uz7_SU zG%Mgb2s!VevHp~MfgE_s4Sn9! zc8(cdTDw~g*b;eDm_)2tay(al63=wv)oO^d-=zH5x%gOZZayE9l zK@_(W8oc-jE*T11Y1N>a;TjD^P5B;7-0!}Yk^h8A3dBC-2~#J}h)Dh{wEFG-9absQ z(PNunl%C{I++Gj9`93~DjTC+$lDI%w`L? zzqx3&SwrEc`7F480A;e~uUl=7#vFVk)Nm8&GYRgaO#CAW3#DVwGEq2V3Mr;VIJKGV zkT6yo5~OTAYsG8dA|I3Sdd-0eAI}f#I?q;~|AnRBEvQtMU?}7zRa5*im2LgaD za-Eakxg_EZ`H_^!kxtS>neMJaQzAF!nX87K7OiiQcWis=b4_Rdu<-Ba1JSSU#u%B8 zn54wmhmv9EpCjq6tQpfmLgr%C4}oO?8p>&ZOMV#9tMP_ zR#XJYwZ8dB)VSzW?d@7VUHW6BJd>#3@lKa{1e`PrEWrdRO9&@?A>+Q{H{7a@agY$O zYA|f7D;#*Q%8zn+a|$bkY2oy@%CEAKPpiDb;OiI>1P??{FsgRifOrpRk7WeN=LLW` zTts>xMl>o6Bho4G=1i$6a8v}Y0m#vL)ooCQMNzU;34u2zF&{@_P<%v$1Q71R&$hUw zt=4lYH`9S;+RAPOFBxC}Dr6mOU|+1ST$1_h(~YUn&hs_roxsPR3v!R2A}2C)IgF4) z(sB-gh2Eu89eHH2Q*mr=D_<7d4nsj0w6nV*T$e2&tg#je$V2~QK>@mfcIDX?KphbnxWcn&r!*dQ4h=>=f>PN?{MP&WJbWxi&OS_kJa(O`Eap;Dj zSgy`0lUh?WYtcxdx>R&RC&@XNWA0XFD3u}1GZ&~4?lC-!Z2Y1-!(W4MU;I0D2<>?6&K2A8WTAic%`fJwan|bn#$OpAwq(tmY+gpEzx-qxPvPl> z%jHqPeQ5y9YR{;e5MPLL*7A}xyoOy#|<~&he+& z32uU)n?jgkUDxbMG*?y#)vgyjF9H)U$fTwyw4c2GoB)a=cw2m@Y$KWsE`OUAh@^tJ zp7JzffV(m{6@3TJjB3lOMt7cD^nbdy$0^hUHA5JV4%`k!bW0~J08oX=pSQE%Dy+o% zx3H`ae8FvMs5euV@bw|DnxQ!lx{rqs$=bok-$S&}q!Nm_w?67m?NPj%A__|e^ zR=j<4>1d9$5jh5StGcP7bLNnpK_gh~Zo_It%TU@yR*tB3fVg2;9H>#7IEWuOK+?+0 zY$Ve0MR3A^rg4};W)Dek~u>!2m=7*%?*B?|v>mJs_tl6m^LU9iUHWOfLLB#b&KqJOMvv&9yAU zfGkgO%-7@sX#N%tU@8BJR4ID#70R#lQjo;re#+5(bGz$zNwQL+FBJtO&lpQ-&QBdQ zF(;A&%qzeB7Z&qXH5KS&pc)gyZyWW9G z7E1y}@S$Cp>ois+2R||KxtjtP82VG?U`SHHcLjA5@`+xe=X{7r%T~pQEg!3x_tn zwF0q82+7QoaSJl*wTqweLyv0&tnlfH##t5NieFywI+J2XHfqo4d)VRhM}OND4Y^nP z{JeXoTX@#J4j@Sr!)ET#5=-vX`d10*1j4ul;8VNRJX+_*#r>+She0Ju8zstWC-cnP z{<38bHTMYCnbBD4h)pwbtS?}&%EI~o+&-tN{ScG}NCfG+ytfH8sW)7QtR>m2*=x#L zj%xxmmW?m}URwOn-TS;x8~!`M9<{q^px!|^YL3I<8~gICS^Kf?QZX&^bycd^a+#lk{pp~0mOd4^QKHwSsw566Z^ZT)!&{XN8xc8<$A3)aFMXX80! z3cl#>O4C%zCF)1fd@VzEwXH-9u{4)J_c%69iIMU->Zb5FIjvTuE*KOXROf1hF6{G= zI!&_I9XaqXhhF*ZQNb^a|9aUo3e3D=gt8+d_d~6+fCMpg5n37~8Vrkamgn>JExB+KPK;`j4vPAEFPFdN%uIejh}XEf@QZz>YW8s|kw*(exDxe^~79P%b$kQ8+l; zvd&>arTGCaFqWCaWa zI9>obBCtY~lCFuu37(daS$JP9&uvnWi?G~%pWAJ>w$5p*G@B?5R7U~1;iU$MZ+^DC zt!7SCHw4p5)xz}pdV?K*_%S%s0kd^zaPs;yTqbdv)3Ix!gBg0wH zysbE?#6NjNukhxZx%_1t6ZhiXXECunA5;$>}@hAx(d9 zBHSE;1n?Two8^G= z(gMTgLp!JEuFQ|k2?EsFE3rxa1g1Pt)YE9nZy`VRR#Bq}e5{@v)1{p#98&%wYFy#2S+50WAbYWuO1n_JJ=_W=vf_v00lum>t5jCZka_w zZHMZgFI7EN)YthZn4jX?KQg=h_~2w;1^Irb{@-uGh+z2=0QRApRxjP7sK4BN|knkID9fTroTg2 zr%^?QLYLr0o>`4{tL?jKVb6;eFdjP+vdPwi1|pUB5CuH)S21yiB#M!JKuS`vw{^IL zkeAW;Iem)w3S3?e%Ob$|#_z9zK|nnRk`wHU1XhgCJwx(oAM-4JN)pPGx3T@BFR6sP zxI7gk?ITc$*-T+AKv%5cD!n+@8tG|x!1ua7Vqi); z_p(J8@|tVm6khq}aB0=?#Mps^#E-f!_`X=GxX<2a5y_wy`Dw{y$fWjttxJVV+RdoD zT$G|CXjd6o_9ULO!%b?n51&p5XB{iK6T0uUB4D&LtSQH)lQyptv|*ImE0Jj2-D2hg zW}Dv{5bnezaHdVFihaj_M$L1D`>JVrj;9Gywagr*K-OjHnoci}qSYO=TGHM93Jlt5 zxTas<9*ianvIb$?7>^L9v>34EDc5$aP`X39aAn9kfvhC$b2E9~KIk}ax;wm>=zUK# z)4eth}Q|W_5`jf(c><(w@^$OM) zW&AFBRwGn3b082glI1|zD_C&Jl;%yX2k822+v(k_+A@QAi(6e?2SbPaxqL1TC%#b( zGx}RfZsaiUT5B;@((RSuf5dkx_oD3zQe@DV6gKCMmadJSK2DUyU1QtNNL2 zwNTPDymtpYvUyB~A1O8*;@{IwUNz=6|6L(G2W);ep_r} z!Tm5;9AsksNf#sr;W4h6(kO9@ougY>zrK4ojFuJ>jyWay07f2z6dxfHmcN<)k<2P~ zht)r|4_0ep+4Y5K6LNHZ;$z_=xw#v ztUPe^HlR_+j$-Y8@g*%iq^@TS%_LB7I+zt#R*qT|b(rT6gpw=e9b}gZ$6A6gq@`Nb zpU$i=8#Z1MMw8vb{;C#eJ+*)I-f|f6tLi72oHlva3({h3G7IA-G_t^_-TS0tkvQ&K zF`mj3XBpow66Me#Y;PO(D_g+xSHGjhd(a!dTsOX%J#Iru3>?U(q5zGg${+aG1P!iN zet8tCETW)GEdVZ?eE741WSAE<-Lwn!PY-HOK|L%%baKdm4WT>UTUxcGu%AXEdu`pU z=9mfQ4FlkZnsw8P8ts(!D6(P5A&78+tCCFs3dKo3E!l&zV!G+@>CMziKsq2NvO9` zl)^;Qs}9AV!Zop-c#g&7C$(req4h_3_RWa<3In`+F5^v09)Q^)$&ICFi}AbBQ?Fi{ zkJvtB9Qe~#11iRXe?c53R&_r$d#S3Fy4}n^5%Bb|9WZ!$tkEW`8vQuib}H;THF>nKeNiU%{^*9)Nf_~TBr(RTNWn?wO^Jv%flyiYB1&iacPW-J0=>tt z**Sruu66r0#zqGuaZ|6<1=GSSj`ir)LE6(6s%6yJ%Bp^m>XoUDYWc}2!BW*;nerA)r<|}{*k1a~UiW0e3r0M-;zic6k zx_SN?UKF&Qi6KC|R80_N_91gWvJG8&mPf zXpFx{U$-)O=WqD```KRe2hW+wU}`6=d3iFGiHky!=`JA_q>13D*16%z+ekIZy% zU~zHIEbAN)zJnZ7Zb?q(AcKu%JJSLP6sl^%y`T9TH{}JGC6fNUl^BrTMel5#$lasKhV$@)9Zx4kHZk}TQLv|i@PVZ1qq1!Ql?Q!9fR zuV}o8=jc*U$5J3Z2C!C!S6~^KskiG?0theU%D9IdQG(|8 zcsU8VKmBL-kMs1_Q!&Xp%ZcA}ghiF>IPD5e#wv&$4OWxLwgZk;w3zH1y$5?vntG()^-8=KtU{QY~DKMB$F$xx(*x zK}-jt)c`^=|F({h_Tu^rC&l0TWOP$-QMcv00IZlM)J$7kqp-14DP&bFTsXC+K&+2=~8UWD83&<;#N+rvP{|CJkd$9lj literal 0 HcmV?d00001 diff --git a/nx.json b/nx.json new file mode 100644 index 0000000..b8082c7 --- /dev/null +++ b/nx.json @@ -0,0 +1,30 @@ +{ + "npmScope": "@opentiny", + "affected": { + "defaultBase": "main" + }, + "implicitDependencies": { + "package.json": { + "dependencies": "*", + "devDependencies": "*" + }, + ".eslintrc.json": "*" + }, + "tasksRunnerOptions": { + "default": { + "runner": "nx/tasks-runners/default", + "options": { + "cacheableOperations": ["lint", "test", "e2e"] + } + } + }, + "targetDependencies": { + "build": [ + { + "target": "build", + "projects": "dependencies" + } + ] + }, + "defaultProject": "ng-demo" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..183949e --- /dev/null +++ b/package.json @@ -0,0 +1,82 @@ +{ + "name": "ng", + "version": "0.0.0", + "scripts": { + "prepare": "git config core.hooksPath .githooks", + "ng": "nx", + "start": "npx ng serve ng-demo -o", + "lint": "nx lint", + "pretty": "prettier --write \"./src/**/*.{html,ts,less}\"", + "e2e": "nx e2e", + "test": "ng test", + "preinstall": "git config commit.template commit.template", + "build:docs": "node ./build/buildwc.js && node ./build/package.js && node ./build/build-api.js", + "clean": "rimraf dist", + "create:websitefiles": "node ./build/autoCreateWebsite.js", + "create:demoprojectjson": "node ./build/autoCreateDemoProjectJson.js", + "create:libprojectjson": "node ./build/autoCreateLibProjectJson.js", + "postinstall": "node ./decorate-angular-cli.js && ngcc --properties es2015 browser module main", + "prepreview": "npm run clean && rimraf node_modules/@opentiny && npm run resetpreview", + "resetpreview": "node ./build/reset-preview.js", + "build:test": "node ./build/bindTest", + "commit": "cz" + }, + "private": true, + "dependencies": { + "@angular/animations": "~13.3.0", + "@angular/cdk": "~13.3.0", + "@angular/common": "~13.3.0", + "@angular/compiler": "~13.3.0", + "@angular/core": "~13.3.0", + "@angular/elements": "~13.3.0", + "@angular/forms": "~13.3.0", + "@angular/platform-browser": "~13.3.0", + "@angular/platform-browser-dynamic": "~13.3.0", + "@angular/router": "~13.3.0", + "rxjs": "^6.5.3 || ^7.4.0", + "tslib": "^2.0.0", + "web-animations-js": "^2.3.2", + "libphonenumber-js": "1.10.7", + "zone.js": "^0.11.8" + }, + "devDependencies": { + "@angular-devkit/build-angular": "~13.3.0", + "@angular-eslint/eslint-plugin": "~13.1.0", + "@angular/cli": "~13.3.0", + "@angular/compiler-cli": "~13.3.0", + "@angular/language-service": "~13.3.0", + "@commitlint/cli": "^17.3.0", + "@commitlint/config-conventional": "^17.3.0", + "@compodoc/compodoc": "^1.1.19", + "@nrwl/angular": "13.10.6", + "@nrwl/workspace": "13.10.6", + "@types/jasmine": "~4.0.0", + "@types/node": "16.11.7", + "@typescript-eslint/eslint-plugin": "~5.18.0", + "@typescript-eslint/parser": "~5.18.0", + "chalk": "4.1.2", + "commitizen": "^4.2.5", + "cz-conventional-changelog": "^3.3.0", + "ejs": "^3.1.6", + "eslint": "^7.32.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.2.1", + "fancy-log": "^2.0.0", + "fs-extra": "^10.1.0", + "ionicons": "^6.0.4", + "jasmine-core": "~4.0.0", + "json5": "^2.2.1", + "karma": "~6.3.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.1.0", + "karma-jasmine": "~4.0.0", + "karma-jasmine-html-reporter": "~1.7.0", + "lint-staged": "^13.0.3", + "ng-packagr": "~13.3.0", + "nx": "13.10.6", + "prettier": "^2.8.0", + "rimraf": "^3.0.2", + "shx": "^0.3.3", + "typescript": "~4.6.2" + } +} diff --git a/src/accordion/demo/karma.conf.js b/src/accordion/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/accordion/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/accordion/demo/project.json b/src/accordion/demo/project.json new file mode 100644 index 0000000..69f959d --- /dev/null +++ b/src/accordion/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/accordion/demo", + "sourceRoot": "src/accordion/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/accordion", + "index": "src/accordion/demo/src/index.html", + "main": "src/accordion/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/accordion/demo/tsconfig.app.json", + "assets": ["src/accordion/demo/src/favicon.ico", "src/accordion/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "accordion-demo:build:production" + }, + "development": { + "browserTarget": "accordion-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js accordion" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/accordion/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/accordion/demo/tsconfig.spec.json", + "karmaConfig": "src/accordion/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/accordion/demo/src/app/AppComponent.ts b/src/accordion/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/accordion/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/accordion/demo/src/app/AppModule.ts b/src/accordion/demo/src/app/AppModule.ts new file mode 100644 index 0000000..f662549 --- /dev/null +++ b/src/accordion/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { AccordionTestModule } from './accordion/AccordionTestModule'; + +@NgModule({ + imports: [ + AccordionTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/accordion/demo/src/app/IndexComponent.ts b/src/accordion/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..5ac543f --- /dev/null +++ b/src/accordion/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { AccordionTestModule } from './accordion/AccordionTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = AccordionTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/accordion/demo/src/app/accordion/AccordionBasicComponent.ts b/src/accordion/demo/src/app/accordion/AccordionBasicComponent.ts new file mode 100644 index 0000000..a0ad065 --- /dev/null +++ b/src/accordion/demo/src/app/accordion/AccordionBasicComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './accordion-basic.html' +}) +export class AccordionBasicComponent {} diff --git a/src/accordion/demo/src/app/accordion/AccordionClassComponent.ts b/src/accordion/demo/src/app/accordion/AccordionClassComponent.ts new file mode 100644 index 0000000..41bda4e --- /dev/null +++ b/src/accordion/demo/src/app/accordion/AccordionClassComponent.ts @@ -0,0 +1,11 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './accordion-class.html', + styles: ['.headClass {background-color: #DDDDDD !important}', '.bodyClass {color: blue !important}'], + encapsulation: ViewEncapsulation.None +}) +export class AccordionClassComponent { + headClass: string = 'headClass'; + bodyClass: string = 'bodyClass'; +} diff --git a/src/accordion/demo/src/app/accordion/AccordionClickToggleComponent.ts b/src/accordion/demo/src/app/accordion/AccordionClickToggleComponent.ts new file mode 100644 index 0000000..309cdfe --- /dev/null +++ b/src/accordion/demo/src/app/accordion/AccordionClickToggleComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './accordion-click-toggle.html' +}) +export class AccordionClickToggleComponent { + isOpen: boolean = false; + headClick($event: any): void { + console.log($event); + } + changeState(): void { + this.isOpen = !this.isOpen; + } +} diff --git a/src/accordion/demo/src/app/accordion/AccordionCloseOthersComponent.ts b/src/accordion/demo/src/app/accordion/AccordionCloseOthersComponent.ts new file mode 100644 index 0000000..f62e011 --- /dev/null +++ b/src/accordion/demo/src/app/accordion/AccordionCloseOthersComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './accordion-close-others.html' +}) +export class AccordionCloseOthersComponent {} diff --git a/src/accordion/demo/src/app/accordion/AccordionDisabledComponent.ts b/src/accordion/demo/src/app/accordion/AccordionDisabledComponent.ts new file mode 100644 index 0000000..5e74b5d --- /dev/null +++ b/src/accordion/demo/src/app/accordion/AccordionDisabledComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './accordion-disabled.html' +}) +export class AccordionDisabledComponent { + enabled: boolean = false; +} diff --git a/src/accordion/demo/src/app/accordion/AccordionOpenComponent.ts b/src/accordion/demo/src/app/accordion/AccordionOpenComponent.ts new file mode 100644 index 0000000..ec2277f --- /dev/null +++ b/src/accordion/demo/src/app/accordion/AccordionOpenComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './accordion-open.html' +}) +export class AccordionOpenComponent { + isOpen: boolean = true; +} diff --git a/src/accordion/demo/src/app/accordion/AccordionTestModule.ts b/src/accordion/demo/src/app/accordion/AccordionTestModule.ts new file mode 100644 index 0000000..e5a1cbd --- /dev/null +++ b/src/accordion/demo/src/app/accordion/AccordionTestModule.ts @@ -0,0 +1,71 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiAccordionModule, TiButtonModule, TiTextareaModule } from '@opentiny/ng'; + +import { AccordionBasicComponent } from './AccordionBasicComponent'; +import { AccordionOpenComponent } from './AccordionOpenComponent'; +import { AccordionClassComponent } from './AccordionClassComponent'; +import { AccordionClickToggleComponent } from './AccordionClickToggleComponent'; +import { AccordionCloseOthersComponent } from './AccordionCloseOthersComponent'; +import { AccordionDisabledComponent } from './AccordionDisabledComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiAccordionModule, + TiTextareaModule, + TiButtonModule, + RouterModule.forChild(AccordionTestModule.ROUTES) + ], + declarations: [ + AccordionBasicComponent, + AccordionOpenComponent, + AccordionClassComponent, + AccordionClickToggleComponent, + AccordionCloseOthersComponent, + AccordionDisabledComponent + ] +}) +export class AccordionTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiAccordionComponent.html', label: 'Accordion' }, + { + href: 'components/TiAccordionHeadComponent.html', + label: 'AccordionHead' + }, + { + href: 'components/TiAccordionItemComponent.html', + label: 'AccordionItem' + } + ]; + static readonly ROUTES: Routes = [ + { + path: 'accordion/accordion-basic', + component: AccordionBasicComponent + }, + { + path: 'accordion/accordion-open', + component: AccordionOpenComponent + }, + { + path: 'accordion/accordion-class', + component: AccordionClassComponent + }, + { + path: 'accordion/accordion-click-toggle', + component: AccordionClickToggleComponent + }, + { + path: 'accordion/accordion-close-others', + component: AccordionCloseOthersComponent + }, + { + path: 'accordion/accordion-disabled', + component: AccordionDisabledComponent + } + ]; +} diff --git a/src/accordion/demo/src/app/accordion/accordion-basic.html b/src/accordion/demo/src/app/accordion/accordion-basic.html new file mode 100644 index 0000000..1615731 --- /dev/null +++ b/src/accordion/demo/src/app/accordion/accordion-basic.html @@ -0,0 +1,12 @@ + + + 凉州词二首其一 +

黄河远上白云间,一片孤城万仞山。

+

羌笛何须怨杨柳,春风不度玉门关。

+
+ + 凉州词二首其二 +

单于北望拂云堆,杀马登坛祭几回。

+

汉家天子今神武,不肯和亲归去来。

+
+
diff --git a/src/accordion/demo/src/app/accordion/accordion-class.html b/src/accordion/demo/src/app/accordion/accordion-class.html new file mode 100644 index 0000000..418e85b --- /dev/null +++ b/src/accordion/demo/src/app/accordion/accordion-class.html @@ -0,0 +1,12 @@ + + + 凉州词二首其一 +

黄河远上白云间,一片孤城万仞山。

+

羌笛何须怨杨柳,春风不度玉门关。

+
+ + 凉州词二首其二 +

单于北望拂云堆,杀马登坛祭几回。

+

汉家天子今神武,不肯和亲归去来。

+
+
diff --git a/src/accordion/demo/src/app/accordion/accordion-click-toggle.html b/src/accordion/demo/src/app/accordion/accordion-click-toggle.html new file mode 100644 index 0000000..6ab9b83 --- /dev/null +++ b/src/accordion/demo/src/app/accordion/accordion-click-toggle.html @@ -0,0 +1,14 @@ + + + 凉州词二首其一(clickToggle 为 false,点击面板标题时面板不会展开或收起) +

黄河远上白云间,一片孤城万仞山。

+

羌笛何须怨杨柳,春风不度玉门关。

+
+ + 凉州词二首其二 +

单于北望拂云堆,杀马登坛祭几回。

+

汉家天子今神武,不肯和亲归去来。

+
+
+
+ diff --git a/src/accordion/demo/src/app/accordion/accordion-close-others.html b/src/accordion/demo/src/app/accordion/accordion-close-others.html new file mode 100644 index 0000000..52027df --- /dev/null +++ b/src/accordion/demo/src/app/accordion/accordion-close-others.html @@ -0,0 +1,12 @@ + + + 凉州词二首其一 +

黄河远上白云间,一片孤城万仞山。

+

羌笛何须怨杨柳,春风不度玉门关。

+
+ + 凉州词二首其二 +

单于北望拂云堆,杀马登坛祭几回。

+

汉家天子今神武,不肯和亲归去来。

+
+
diff --git a/src/accordion/demo/src/app/accordion/accordion-disabled.html b/src/accordion/demo/src/app/accordion/accordion-disabled.html new file mode 100644 index 0000000..39e7b9b --- /dev/null +++ b/src/accordion/demo/src/app/accordion/accordion-disabled.html @@ -0,0 +1,12 @@ + + + 凉州词二首其一 +

黄河远上白云间,一片孤城万仞山。

+

羌笛何须怨杨柳,春风不度玉门关。

+
+ + 凉州词二首其二 +

单于北望拂云堆,杀马登坛祭几回。

+

汉家天子今神武,不肯和亲归去来。

+
+
diff --git a/src/accordion/demo/src/app/accordion/accordion-open.html b/src/accordion/demo/src/app/accordion/accordion-open.html new file mode 100644 index 0000000..d49474e --- /dev/null +++ b/src/accordion/demo/src/app/accordion/accordion-open.html @@ -0,0 +1,12 @@ + + + 凉州词二首其一 +

黄河远上白云间,一片孤城万仞山。

+

羌笛何须怨杨柳,春风不度玉门关。

+
+ + 凉州词二首其二 +

单于北望拂云堆,杀马登坛祭几回。

+

汉家天子今神武,不肯和亲归去来。

+
+
diff --git a/src/accordion/demo/src/app/accordion/accordion.spec.ts b/src/accordion/demo/src/app/accordion/accordion.spec.ts new file mode 100644 index 0000000..ac22355 --- /dev/null +++ b/src/accordion/demo/src/app/accordion/accordion.spec.ts @@ -0,0 +1,85 @@ +import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { TiAccordionComponent, TiAccordionItemComponent, TiAccordionHeadComponent } from '../../../../lib/src/TiAccordionModule'; +import { AccordionBasicComponent } from './AccordionBasicComponent'; +import { AccordionOpenComponent } from './AccordionOpenComponent'; + +describe('accordion', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NoopAnimationsModule], + declarations: [ + TiAccordionComponent, + TiAccordionItemComponent, + TiAccordionHeadComponent, + AccordionBasicComponent, + AccordionOpenComponent + ] + }).compileComponents(); + }); + + describe('accordion basic', () => { + let fixture: ComponentFixture; + let accordion: DebugElement; + let items: DebugElement[]; + + beforeEach(() => { + fixture = TestBed.createComponent(AccordionBasicComponent); + fixture.detectChanges(); + accordion = fixture.debugElement.query(By.directive(TiAccordionComponent)); + items = fixture.debugElement.queryAll(By.directive(TiAccordionItemComponent)); + }); + + it('should component defined', () => { + fixture.detectChanges(); + expect(accordion).toBeDefined(); + expect(items).toBeDefined(); + }); + + it('should className correct', () => { + fixture.detectChanges(); + expect(accordion.nativeElement!.classList).toContain('ti3-accordion-group'); + expect(items[0].nativeElement!.classList).toContain('ti3-accordion-panel'); + }); + }); + + describe('accordion open', () => { + let fixture: ComponentFixture; + let testComponent: AccordionOpenComponent; + let accordion: DebugElement; + let items: DebugElement[]; + + beforeEach(() => { + fixture = TestBed.createComponent(AccordionOpenComponent); + fixture.detectChanges(); + testComponent = fixture.componentInstance; + accordion = fixture.debugElement.query(By.directive(TiAccordionComponent)); + items = fixture.debugElement.queryAll(By.directive(TiAccordionItemComponent)); + }); + + it('should open api correct', fakeAsync(() => { + fixture.detectChanges(); + expect(testComponent.isOpen).toBeTrue(); + testComponent.isOpen = false; + fixture.detectChanges(); + tick(); + expect(testComponent.isOpen).toBeFalse(); + })); + + it('should style correct when open changed', fakeAsync(() => { + fixture.detectChanges(); + let firstItem: DebugElement = items[0].children[1]; + let secondItem: DebugElement = items[1].children[1]; + expect(firstItem.nativeElement.clientHeight).toBeGreaterThan(0); + expect(secondItem.nativeElement.clientHeight).toBe(0); + testComponent.isOpen = false; + fixture.detectChanges(); + tick(); + expect(firstItem.nativeElement.clientHeight).toBe(0); + expect(secondItem.nativeElement.clientHeight).toBe(0); + })); + }); +}); diff --git a/src/accordion/demo/src/app/accordion/webdoc/accordion-demos.js b/src/accordion/demo/src/app/accordion/webdoc/accordion-demos.js new file mode 100644 index 0000000..721591f --- /dev/null +++ b/src/accordion/demo/src/app/accordion/webdoc/accordion-demos.js @@ -0,0 +1,92 @@ +export default { + column: '2', + demos: [ + { + demoId: 'accordion-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

Accordion 组件的最简用法。注意:(1)同时只有一个面板是展开状态(2)使用分割多个面板。ti-accordion-head定义面板标题。

', + 'en-US': + '

only one panel can be expanded at the same time.

Use ti-accordion-item to separate group of content. ti-accordion-head is used for title of panels.

', + }, + }, + { + demoId: 'accordion-class', + name: { + 'zh-CN': '样式设置', + 'en-US': 'Class', + }, + desc: { + 'zh-CN': + '

通过属性headClassbodyClass分别配置面板标题及内容区域的样式。

', + 'en-US': + '

Set the styles of the head and content through headClass bodyClass props respectively.

', + }, + apis: [ + 'TiAccordionComponent.properties.bodyClass', + 'TiAccordionComponent.properties.headClass', + ], + }, + { + demoId: 'accordion-open', + name: { + 'zh-CN': '默认打开', + 'en-US': 'Active', + }, + desc: { + 'zh-CN': '

通过属性open配置面板展开状态。

', + 'en-US': + '

Use the open prop to control the active panels.

', + }, + apis: ['TiAccordionItemComponent.properties.open'], + }, + { + demoId: 'accordion-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'Disabled', + }, + desc: { + 'zh-CN': '

通过属性disabled配置面板禁用状态。

', + 'en-US': + '

Use the disabled prop to disable the panel.

', + }, + apis: ['TiAccordionItemComponent.properties.disabled'], + }, + { + demoId: 'accordion-close-others', + name: { + 'zh-CN': '关闭其他', + 'en-US': 'Close Others', + }, + desc: { + 'zh-CN': + '

通过属性closeOthers配置用户打开一个面板时其他面板的展开状态。

', + 'en-US': + '

The closeOthers prop is used to control whether other panel is expanded when user opens a panel.

', + }, + apis: ['TiAccordionComponent.properties.closeOthers'], + }, + { + demoId: 'accordion-click-toggle', + name: { + 'zh-CN': '点击不展开面板', + 'en-US': 'clickabled', + }, + desc: { + 'zh-CN': + '

通过属性clickToggle配置用户点击面板标题是否展开内容区域。注意:此属性设置为 false 后,您仍然可以通过属性open实现面板展开状态的变更。

', + 'en-US': + '

The clickToggle prop is used to control whether the panel is expanded when user clicks.

You can also control the panel by changing the value of the open prop

', + }, + apis: [ + 'TiAccordionItemComponent.properties.clickToggle', + 'TiAccordionItemComponent.events.headClick', + ], + }, + ], +}; diff --git a/src/accordion/demo/src/app/accordion/webdoc/accordion.cn.md b/src/accordion/demo/src/app/accordion/webdoc/accordion.cn.md new file mode 100644 index 0000000..e748c8a --- /dev/null +++ b/src/accordion/demo/src/app/accordion/webdoc/accordion.cn.md @@ -0,0 +1,26 @@ +--- +title: Accordion 手风琴 +--- +# Accordion 手风琴 + +
+ +通过手风琴收纳内容区域。 + +- 将一组面板放在手风琴中,点击面板标题可以展开或收起该面板的内容区域。 + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
+ +
+ +通过手风琴收纳内容区域。 + +- 将一组面板放在手风琴中,点击面板标题可以展开或收起该面板的内容区域。 + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
diff --git a/src/accordion/demo/src/app/accordion/webdoc/accordion.en.md b/src/accordion/demo/src/app/accordion/webdoc/accordion.en.md new file mode 100644 index 0000000..9fcb90f --- /dev/null +++ b/src/accordion/demo/src/app/accordion/webdoc/accordion.en.md @@ -0,0 +1,28 @@ +--- +title: Accordion +--- +# Accordion + +
+ +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
+ +
+ +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/accordion/demo/src/app/app.html b/src/accordion/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/accordion/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/accordion/demo/src/favicon.ico b/src/accordion/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/accordion/demo/src/index.html b/src/accordion/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/accordion/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/accordion/demo/src/main.ts b/src/accordion/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/accordion/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/accordion/demo/test.ts b/src/accordion/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/accordion/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/accordion/demo/tsconfig.app.json b/src/accordion/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/accordion/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/accordion/demo/tsconfig.spec.json b/src/accordion/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/accordion/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/accordion/lib/index.ts b/src/accordion/lib/index.ts new file mode 100644 index 0000000..285c039 --- /dev/null +++ b/src/accordion/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiAccordionModule'; diff --git a/src/accordion/lib/ng-package.json b/src/accordion/lib/ng-package.json new file mode 100644 index 0000000..6d70021 --- /dev/null +++ b/src/accordion/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/accordion", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/accordion/lib/package.json b/src/accordion/lib/package.json new file mode 100644 index 0000000..373d599 --- /dev/null +++ b/src/accordion/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-accordion", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/animations": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/accordion/lib/project.json b/src/accordion/lib/project.json new file mode 100644 index 0000000..c6d21a0 --- /dev/null +++ b/src/accordion/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/accordion/lib", + "sourceRoot": "src/accordion/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/accordion"], + "options": { + "project": "src/accordion/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/accordion"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js accordion" + }, + { + "command": "ng default-build accordion" + }, + { + "command": "node build/clear-default-theme.js accordion" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/accordion && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build accordion && ng pack accordion && node build/publish.js accordion --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/accordion/lib/src/TiAccordionComponent.ts b/src/accordion/lib/src/TiAccordionComponent.ts new file mode 100644 index 0000000..029abcd --- /dev/null +++ b/src/accordion/lib/src/TiAccordionComponent.ts @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; +import { TiAccordionItemComponent } from './TiAccordionItemComponent'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +/** + * 手风琴组件, + * 用于实现多个折叠面板的呈现方式,通过ti-accordion、ti-accordion-item、ti-accordion-head组件嵌套方式实现 + * + *

使用此组件时需要开发者在项目模块(建议在根模块) + * 中引入BrowserAnimationsModule。这是因为此组件中使用了Angular动画,需要引入BrowserAnimationsModule, + * 但是 BrowserAnimationsModule 不能在懒加载模块被重复引入,所以需要开发者来引入BrowserAnimationsModule,保证其引入一次。

+ * + */ +@Component({ + selector: 'ti-accordion', + templateUrl: './accordion.html', + styleUrls: ['./accordion.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-accordion-group]': 'true' + } +}) +export class TiAccordionComponent extends TiBaseComponent { + /** + * 展开某一项时,是否关闭其它项 + */ + @Input() closeOthers: boolean = true; + /** + * 头部样式类 + */ + @Input() headClass: string = ''; + /** + * 内容体样式类 + */ + @Input() bodyClass: string = ''; + + protected items: Array = []; + protected versionInfo: string = super.getVersion(packageInfo); + /** + * @ignore + * 关闭其余item + */ + closeOtherItems(openItem: TiAccordionItemComponent): void { + if (!this.closeOthers) { + return; + } + this.items.forEach((item: TiAccordionItemComponent) => { + if (item.open && item !== openItem) { + item.open = false; + } + }); + } + + /** + * @ignore + * 添加某一accordion条目到items数组中 + */ + addItem(item: TiAccordionItemComponent): void { + this.items.push(item); + } + + /** + * @ignore + * 从items数组中删除某一accordion条目 + */ + removeItem(item: TiAccordionItemComponent): void { + const index: number = this.items.indexOf(item); + if (index !== -1) { + this.items.splice(index, 1); + } + } +} diff --git a/src/accordion/lib/src/TiAccordionHeadComponent.ts b/src/accordion/lib/src/TiAccordionHeadComponent.ts new file mode 100644 index 0000000..3e34620 --- /dev/null +++ b/src/accordion/lib/src/TiAccordionHeadComponent.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; + +/** + * 定义单个手风琴折叠面板头部内容,该组件包含在ti-accordion-item中使用 + */ +@Component({ + selector: 'ti-accordion-head', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiAccordionHeadComponent {} diff --git a/src/accordion/lib/src/TiAccordionItemComponent.ts b/src/accordion/lib/src/TiAccordionItemComponent.ts new file mode 100644 index 0000000..c41ca83 --- /dev/null +++ b/src/accordion/lib/src/TiAccordionItemComponent.ts @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ElementRef, + EventEmitter, + Inject, + Input, + Output, + Renderer2, + ViewEncapsulation, + ChangeDetectorRef, + ChangeDetectionStrategy +} from '@angular/core'; +import { TiAccordionComponent } from './TiAccordionComponent'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { animate, state, style, transition, trigger } from '@angular/animations'; +import packageInfo from '../package.json'; + +export interface TiAccordionHeadClickEvent extends Event { + /** + * 当前面板打开状态 + */ + open?: boolean; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} +/** + * @ignore + */ +export const animateStyle: string = '0.15s cubic-bezier(0.645, 0.045, 0.355, 1)'; + +/** + * 定义手风琴组件单个折叠面板,该组件包含在ti-accordion标签中 + */ +@Component({ + selector: 'ti-accordion-item', + templateUrl: './accordion-item.html', + styleUrls: ['./accordion.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger('accordionAnimate', [ + state('expanded', style({ height: '*' })), + state('collapsed', style({ height: 0, overflow: 'hidden', borderTopWidth: 0 })), + transition('expanded => collapsed', animate(animateStyle)), + transition('collapsed => expanded', animate(animateStyle)) + ]) + ], + host: { + '[class.ti3-accordion-panel]': 'true', + '[class.ti3-accordion-panel-disabled]': 'disabled' + } +}) +export class TiAccordionItemComponent extends TiBaseComponent { + /** + * 是否禁用 + */ + @Input() disabled: boolean = false; + /** + * 点击面板标题时是否展开、收起面板内容区域。注意:若设置为false,仍可通过属性open实现变更面板状态 + */ + @Input() clickToggle: boolean = true; + /** + * 点击面板标题时触发的回调 + */ + @Output() readonly headClick: EventEmitter = new EventEmitter(); + + /** + * @ignore + * 头部class,需要显示声明,否则ngClass会报错 + */ + public headClass: string = ''; + /** + * @ignore + * body class,需要显示声明,否则ngClass会报错 + */ + public bodyClass: string = ''; + protected versionInfo: string = super.getVersion(packageInfo); + /** + * @ignore + * 依赖注入tiAccordion(后续使用其方法) + */ + constructor( + hostRef: ElementRef, + renderer: Renderer2, + @Inject(TiAccordionComponent) private accordion: TiAccordionComponent, + private changeRef: ChangeDetectorRef + ) { + super(hostRef, renderer); + } + + // 面板展开状态获取/设置:此处处理涉及到内部和外部修改,因此此处通过set/get进行处理 + private isOpen: boolean = false; + /** + * 单个折叠面板是否处于展开状态,用于初始化设置,默认值为false + * + */ + @Input() + /** + * @ignore + */ + get open(): boolean { + return this.isOpen; + } + /** + * @ignore + */ + set open(value: boolean) { + if (value !== this.open) { + if (value) { + // open属性更新且open值为true时,处理其它item的关闭状态 + this.accordion.closeOtherItems(this); + } + this.isOpen = value; + // open变化时触发变更检测 + this.changeRef.markForCheck(); + } + } + + // 初始化处理:添加当前item到列表中,设置该item的id值 + ngOnInit(): void { + super.ngOnInit(); + this.accordion.addItem(this); + this.headClass = this.accordion.headClass; + this.bodyClass = this.accordion.bodyClass; + } + + // item销毁处理:移除item列表中的该项数据 + ngOnDestroy(): void { + super.ngOnDestroy(); + this.accordion.removeItem(this); + } + + /** + * @ignore + * item头部点击事件逻辑处理 + */ + toggleOpen($event: TiAccordionHeadClickEvent): void { + if (this.disabled) { + return; + } + if (this.clickToggle) { + this.open = !this.open; + } + + $event.open = this.open; // 将当前面板展开状态通知出去 + this.headClick.emit($event); + } +} diff --git a/src/accordion/lib/src/TiAccordionModule.ts b/src/accordion/lib/src/TiAccordionModule.ts new file mode 100644 index 0000000..04c3fd3 --- /dev/null +++ b/src/accordion/lib/src/TiAccordionModule.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiIconModule } from '@opentiny/ng-icon'; + +import { TiAccordionComponent } from './TiAccordionComponent'; +import { TiAccordionItemComponent } from './TiAccordionItemComponent'; +import { TiAccordionHeadComponent } from './TiAccordionHeadComponent'; + +@NgModule({ + imports: [CommonModule, TiIconModule, TiOutlineModule], + exports: [TiAccordionComponent, TiAccordionItemComponent, TiAccordionHeadComponent], + declarations: [TiAccordionComponent, TiAccordionItemComponent, TiAccordionHeadComponent] +}) +export class TiAccordionModule {} + +export { TiAccordionComponent } from './TiAccordionComponent'; +export { TiAccordionItemComponent, TiAccordionHeadClickEvent } from './TiAccordionItemComponent'; +export { TiAccordionHeadComponent } from './TiAccordionHeadComponent'; diff --git a/src/accordion/lib/src/accordion-item.html b/src/accordion/lib/src/accordion-item.html new file mode 100644 index 0000000..de2f27c --- /dev/null +++ b/src/accordion/lib/src/accordion-item.html @@ -0,0 +1,26 @@ +
+
+
+ + + + + +
+
+
+
+ +
+
diff --git a/src/accordion/lib/src/accordion.html b/src/accordion/lib/src/accordion.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/accordion/lib/src/accordion.html @@ -0,0 +1 @@ + diff --git a/src/accordion/lib/src/accordion.less b/src/accordion/lib/src/accordion.less new file mode 100644 index 0000000..d28a1f8 --- /dev/null +++ b/src/accordion/lib/src/accordion.less @@ -0,0 +1,93 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-accordion-panel-height: calc(var(--ti-common-size-9x) - var(--ti-common-border-weight-normal) * 2); + --ti-accordion-panel-icon-size: var(--ti-common-size-4x); +} + +:host.ti3-accordion-group { + display: block; +} + +:host.ti3-accordion-panel { + display: block; + background-color: var(--ti-common-color-bg-white-emphasize); + margin-bottom: 0; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + border-radius: var(--ti-common-border-radius-normal); + + &.ti3-accordion-panel-disabled { + border-color: var(--ti-common-color-line-disabled); + } +} + +.ti3-accordion-panel-heading { + height: var(--ti-accordion-panel-height); + padding: 0 var(--ti-common-space-4x); + color: var(--ti-common-color-text-primary); + background-color: var(--ti-common-color-bg-white-emphasize); + border-radius: var(--ti-common-border-radius-normal); + line-height: var(--ti-accordion-panel-height); + cursor: pointer; + .user-select(none); + + &.disabled { + background-color: var(--ti-common-color-bg-disabled); + cursor: not-allowed; + outline: none; + } +} + +.ti3-accordion-panel-title-icon { + display: inline-block; + font-size: var(--ti-accordion-panel-icon-size); + vertical-align: middle; + color: var(--ti-common-color-icon-normal); + line-height: var(--ti-accordion-panel-icon-size); + .transition (transform; 0.15s); +} + +.ti3-accordion-panel-title-icon-disabled { + color: var(--ti-common-color-icon-disabled); +} + +.ti3-icon-angle-transform-down { + .rotate(90deg); +} + +.ti3-icon-angle-transform-up { + .rotate(-90deg); +} + +.ti3-accordion-panel-title { + display: flex; + align-items: center; + margin-top: 0; + margin-bottom: 0; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-accordion-panel-font-weight); + color: var(--ti-common-color-text-primary); +} + +.ti3-accordion-panel-title-content { + margin-left: var(--ti-common-space-4x); +} + +.ti3-accordion-panel-title-content-disabled { + color: var(--ti-common-color-text-disabled); +} + +:host.ti3-accordion-group ::ng-deep .ti3-accordion-panel + .ti3-accordion-panel { + margin-top: var(--ti-common-space-2x); +} + +.ti3-accordion-panel-collapse { + border-top: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); +} + +.ti3-accordion-panel-body { + padding: var(--ti-common-space-3x) var(--ti-common-space-4x); + background-color: var(--ti-common-color-bg-white-normal); + border-radius: 0 0 var(--ti-common-border-radius-normal) var(--ti-common-border-radius-normal); + box-shadow: none; +} diff --git a/src/actionmenu/demo/karma.conf.js b/src/actionmenu/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/actionmenu/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/actionmenu/demo/project.json b/src/actionmenu/demo/project.json new file mode 100644 index 0000000..6c270fd --- /dev/null +++ b/src/actionmenu/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/actionmenu/demo", + "sourceRoot": "src/actionmenu/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/actionmenu", + "index": "src/actionmenu/demo/src/index.html", + "main": "src/actionmenu/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/actionmenu/demo/tsconfig.app.json", + "assets": ["src/actionmenu/demo/src/favicon.ico", "src/actionmenu/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "actionmenu-demo:build:production" + }, + "development": { + "browserTarget": "actionmenu-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js actionmenu" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/actionmenu/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/actionmenu/demo/tsconfig.spec.json", + "karmaConfig": "src/actionmenu/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/actionmenu/demo/src/app/AppComponent.ts b/src/actionmenu/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/actionmenu/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/actionmenu/demo/src/app/AppModule.ts b/src/actionmenu/demo/src/app/AppModule.ts new file mode 100644 index 0000000..1f72143 --- /dev/null +++ b/src/actionmenu/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ActionmenuTestModule } from './actionmenu/ActionmenuTestModule'; + +@NgModule({ + imports: [ + ActionmenuTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/actionmenu/demo/src/app/IndexComponent.ts b/src/actionmenu/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..ff29bb2 --- /dev/null +++ b/src/actionmenu/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ActionmenuTestModule } from './actionmenu/ActionmenuTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = ActionmenuTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuBasicComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuBasicComponent.ts new file mode 100644 index 0000000..2973342 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuBasicComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-basic.html' +}) +export class ActionmenuBasicComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuData2Component.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuData2Component.ts new file mode 100644 index 0000000..e9d342a --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuData2Component.ts @@ -0,0 +1,106 @@ +import { Component, ChangeDetectorRef } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +export interface MachineState { + state: string; +} +@Component({ + templateUrl: './actionmenu-data2.html' +}) +export class ActionmenuData2Component { + constructor(private changeDetectorRef: ChangeDetectorRef) { + // OnPush模式下,setTime更新数据,需要使用changeDetectorRef + } + + rows: Array = [ + { + state: '已启动' + }, + { + state: '已停止' + }, + { + state: '已启动' + }, + { + state: '已停止' + } + ]; + dataToItemsFn: (data: MachineState) => Array = (data: { state: string }) => { + let items: Array = [ + { + label: '启动', + disabled: false, + click(item: TiActionmenuItem, row: MachineState): void { + console.log('启动'); + row.state = '已启动'; + } + }, + { + label: '停止', + disabled: false, + click(item: TiActionmenuItem, row: MachineState): void { + console.log('停止'); + console.log(row); + row.state = '已停止'; + } + }, + { + label: '重启', + disabled: false, + click: (item: TiActionmenuItem, row: MachineState) => { + console.log('重启'); + row.state = '正在重启'; + setTimeout(() => { + row.state = '已启动'; + // OnPush模式下,setTimeout更新数据,需要使用changeDetectorRef + // 默认模式不需要 + // 必须放在箭头函数里,才能找到this对象 + this.changeDetectorRef.markForCheck(); + }, 2000); + } + }, + { + label: '热更新', + disabled: false, + click: (item: TiActionmenuItem, row: MachineState) => { + console.log('热更新'); + row.state = '正在热更新'; + setTimeout(() => { + row.state = '已启动'; + // OnPush模式下,setTimeout更新数据,需要使用changeDetectorRef + // 默认模式不需要 + // 必须放在箭头函数里,才能找到this对象 + this.changeDetectorRef.markForCheck(); + }, 2000); + } + } + ]; + switch (data.state) { + case '已启动': + items = items.slice(1, 4); + break; + case '已停止': + items = [items[0]]; + break; + case '正在重启': + items = []; + break; + case '正在热更新': + items = [items[1]]; + break; + default: + break; + } + + return items; + }; + addRow(): void { + this.rows.push({ + state: '已启动' + }); + } + switchFirstRow(): void { + this.rows[0].state = this.rows[0].state === '已启动' ? '已停止' : '已启动'; + } +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuDataComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuDataComponent.ts new file mode 100644 index 0000000..b51b52e --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuDataComponent.ts @@ -0,0 +1,94 @@ +import { Component, ChangeDetectorRef } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +export interface MachineState { + state: string; +} +@Component({ + templateUrl: './actionmenu-data.html' +}) +export class ActionmenuDataComponent { + constructor(private changeDetectorRef: ChangeDetectorRef) { + // OnPush模式下,setTime更新数据,需要使用changeDetectorRef + } + + rows: Array = [ + { + state: '已启动' + }, + { + state: '已停止' + }, + { + state: '已启动' + }, + { + state: '已停止' + } + ]; + dataToItemsFn: (data: MachineState) => Array = (data: { state: string }) => { + const items: Array = [ + { + label: '启动', + disabled: false, + click(item: TiActionmenuItem, row: MachineState): void { + console.log('启动'); + row.state = '已启动'; + } + }, + { + label: '停止', + disabled: false, + click(item: TiActionmenuItem, row: MachineState): void { + console.log('停止'); + console.log(row); + row.state = '已停止'; + } + }, + { + label: '重启', + disabled: false, + click: (item: TiActionmenuItem, row: MachineState) => { + console.log('重启'); + row.state = '正在重启'; + setTimeout(() => { + row.state = '已启动'; + // OnPush模式下,setTimeout更新数据,需要使用changeDetectorRef + // 默认模式不需要 + // 必须放在箭头函数里,才能找到this对象 + this.changeDetectorRef.markForCheck(); + }, 2000); + } + } + ]; + switch (data.state) { + case '已启动': + items[0].disabled = true; + items[1].disabled = false; + items[2].disabled = false; + break; + case '已停止': + items[0].disabled = false; + items[1].disabled = true; + items[2].disabled = true; + break; + case '正在重启': + items[0].disabled = true; + items[1].disabled = true; + items[2].disabled = true; + break; + default: + break; + } + + return items; + }; + addRow(): void { + this.rows.push({ + state: '已启动' + }); + } + switchFirstRow(): void { + this.rows[0].state = this.rows[0].state === '已启动' ? '已停止' : '已启动'; + } +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuDisabledComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuDisabledComponent.ts new file mode 100644 index 0000000..a01c659 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuDisabledComponent.ts @@ -0,0 +1,43 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-disabled.html' +}) +export class ActionmenuDisabledComponent { + disabled: boolean = true; + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机', + disabled: true + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + disabled: true, + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuDividerComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuDividerComponent.ts new file mode 100644 index 0000000..91dfc5e --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuDividerComponent.ts @@ -0,0 +1,99 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-divider.html' +}) +export class ActionmenuDividerComponent { + showDivider: boolean = true; + labelKey: string = 'title'; + panelMaxWidth: string = '230px'; + space: string = '20px'; + maxShowNum: number = 4; + moreText: string = '更多操作'; + menuText: string = '操作项'; + items1: Array = [ + { + title: '启用', + disabled: false, + association: 'switch' + }, + { + title: '禁用', + association: 'switch', + disabled: true + }, + { + title: '制作镜像' + } + ]; + items2: Array = [ + { + title: '启用', + disabled: false, + association: 'switch' + } + ]; + items3: Array = [ + { + title: '启用', + disabled: false, + association: 'switch' + }, + { + title: '禁用', + association: 'switch', + disabled: true + }, + { + title: '制作镜像' + } + ]; + items4: Array = [ + { + title: '启用', + disabled: false, + association: 'switch' + }, + { + title: '禁用', + association: 'switch', + disabled: true + }, + { + title: '制作镜像' + }, + { + title: '这是一个很长的选项只有云服务器处于关机状态才能执行该操作' + }, + { + title: '变更规格2', + children: [ + { + title: '制作镜像', + label: '制作规格', + children: [ + { + title: '制作镜像', + label: '规格1' + }, + { + title: '变更规格', + label: '规格2', + disabled: true + } + ] + }, + { + title: '变更规格', + label: '规格', + disabled: true + } + ] + }, + { + title: '制作镜像2', + disabled: true + } + ]; +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuEventComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuEventComponent.ts new file mode 100644 index 0000000..a6bc527 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuEventComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-event.html' +}) +export class ActionmenuEventComponent { + myLogs: Array = []; + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; + + onSelect(item: TiActionmenuItem): void { + this.myLogs = [...this.myLogs, `onSelect() event = ${JSON.stringify(item)}`]; + } + + moreClick(isOpen: boolean): void { + this.myLogs = [...this.myLogs, `moreClick() event = ${isOpen}`]; + } +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuFocusComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuFocusComponent.ts new file mode 100644 index 0000000..2b67546 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuFocusComponent.ts @@ -0,0 +1,61 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './actionmenu-focus.html' +}) +export class ActionmenuFocusComponent { + items: Array = [ + { + label: '启用', + disabled: true, + association: 'switch' + }, + { + label: '禁用', + association: 'switch', + disabled: true + }, + { + label: '制作镜像', + tip: '只有云服务器处于关机状态,才能执行该操作' + }, + { + label: '这是一个很长的选项,只有云服务器处于关机状态,才能执行该操作', + tip: 'this is a tip' + }, + { + label: '变更规格2', + tip: 'welcome tip', + children: [ + { + label: '制作镜像', + tip: '只有云服务器处于关机状态,才能执行该操作', + children: [ + { + label: '制作镜像', + tip: '只有云服务器处于关机状态,才能执行该操作' + }, + { + label: '变更规格', + disabled: true + } + ] + }, + { + label: '变更规格', + disabled: true + } + ] + }, + { + label: '制作镜像2', + tip: '只有云服务器处于关机状态,才能执行该操作', + disabled: true + } + ]; + + onSelect(item: any): void { + console.log('onSelect()'); + console.log(item); + } +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuIdComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuIdComponent.ts new file mode 100644 index 0000000..aa9abeb --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuIdComponent.ts @@ -0,0 +1,75 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-id.html' +}) +export class ActionmenuIdComponent { + items: Array = [ + { + title: '启用', + id: 'id1', + label: '远程登录', + association: 'switch' + }, + { + title: '禁用', + id: 'id2', + label: '变更规格', + association: 'switch', + disabled: true + }, + { + title: '制作镜像', + id: 'id3', + label: '制作镜像' + }, + { + title: '这是一个很长的选项只有云服务器处于关机状态才能执行该操作', + id: 'id4', + label: '云服务器' + }, + { + title: '变更规格2', + id: 'id5', + label: '规格', + children: [ + { + title: '制作镜像', + id: 'id6', + label: '制作规格', + children: [ + { + title: '制作镜像', + id: 'id7', + label: '规格1' + }, + { + title: '变更规格', + id: 'id8', + label: '规格2', + disabled: true + } + ] + }, + { + title: '变更规格', + id: 'id9', + label: '规格', + disabled: true + } + ] + }, + { + title: '制作镜像2', + id: 'id10', + label: '镜像', + disabled: true + } + ]; + + onSelect(item: TiActionmenuItem): void { + console.log('onSelect()'); + console.log(item); + } +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuItemsChangeComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuItemsChangeComponent.ts new file mode 100644 index 0000000..ef13405 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuItemsChangeComponent.ts @@ -0,0 +1,84 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-items-change.html' +}) +export class ActionmenuItemsChangeComponent { + constructor(protected changeDetectorRef: ChangeDetectorRef) {} + items: Array = [ + { + title: '启用', + label: '远程登录', + association: 'switch' + }, + { + title: '禁用', + label: '变更规格', + association: 'switch', + disabled: true + }, + { + title: '制作镜像', + label: '制作镜像' + }, + { + title: '这是一个很长的选项只有云服务器处于关机状态才能执行该操作', + label: '云服务器' + }, + { + title: '变更规格2', + label: '规格', + children: [ + { + title: '制作镜像', + label: '制作规格', + children: [ + { + title: '制作镜像', + label: '规格1' + }, + { + title: '变更规格', + label: '规格2', + disabled: true + } + ] + }, + { + title: '变更规格', + label: '规格', + disabled: true + } + ] + }, + { + title: '制作镜像2', + label: '镜像', + disabled: true + } + ]; + + click(): void { + this.items[1].disabled = false; + this.items = this.items.concat(); + } + + click1(): void { + this.items[1].label = '文本改变了文本改变了文本改变了'; + this.items = this.items.concat(); + } + + click2(): void { + this.items.unshift({ + title: '新增', + label: '新增新增' + }); + this.items = this.items.concat(); + } + + click3(): void { + this.items.shift(); + this.items = this.items.concat(); + } +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuItemsComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuItemsComponent.ts new file mode 100644 index 0000000..c374d69 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuItemsComponent.ts @@ -0,0 +1,82 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-items.html' +}) +export class ActionmenuItemsComponent { + labelKey: string = 'title'; + items: Array = [ + { + title: '启用', + disabled: false, + association: 'switch' + }, + { + title: '禁用', + association: 'switch', + disabled: true + }, + { + title: '制作镜像' + }, + { + title: '这是一个很长的选项只有云服务器处于关机状态才能执行该操作' + }, + { + title: '变更规格2', + childrens: [ + { + title: '只有云服务器处于关机状态,才能执行该操作', + childrens: [ + { + title: '制作镜像' + }, + { + title: '变更规格', + disabled: true + } + ] + }, + { + title: '变更规格', + disabled: true + } + ] + }, + { + title: '制作镜像2', + disabled: true + } + ]; + maxWidth: string = '265px'; + panelMaxWidth: string = '230px'; + space: string = '20px'; + maxShowNum: number = 4; + moreText: string = '更多操作'; + menuText: string = '操作项'; + maxWidth3: string = '500px'; + maxWidth4: string = '264px'; + maxWidth5: string = '50px'; + items25: Array = [ + { + title: '远程登录' + }, + { + title: '变更规格' + } + ]; + maxWidth25: string = '58px'; + maxWidth26: string = '116px'; + items6: Array = [ + { + title: '这是一个很长的选项只有云服务器处于关机状态才能执行该操作', + disabled: true + } + ]; + items7: Array = [ + { + title: '远程登录' + } + ]; +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuLabelkeyComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuLabelkeyComponent.ts new file mode 100644 index 0000000..969a460 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuLabelkeyComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-labelkey.html' +}) +export class ActionmenuLabelkeyComponent { + items: Array = [ + { + id: 'telnet', + title: '远程登录' + }, + { + id: 'powerOn', + title: '开机' + }, + { + id: 'powerOff', + title: '关机' + }, + { + id: 'reboot', + title: '重启' + }, + { + id: 'setting', + title: '网络设置', + children: [ + { + id: 'modify', + title: '更改安全组' + }, + { + id: 'toggle', + title: '切换VPC' + } + ] + } + ]; +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuManyComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuManyComponent.ts new file mode 100644 index 0000000..c334895 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuManyComponent.ts @@ -0,0 +1,105 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-many.html' +}) +export class ActionmenuManyComponent { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: Object; + data: Array = []; + columns: Array = [ + { + label: 'first name', + width: '25%' + }, + { + label: 'last name', + width: '20%' + }, + { + label: 'birth date', + width: '20%' + }, + { + label: 'state', + width: '15%' + }, + { + label: 'email', + width: '20%' + } + ]; + + onSelect(item: any): void { + console.log('onSelect()'); + console.log(item); + } + + ngOnInit(): void { + for (let j: number = 0; j < 1000; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、过滤、分页等操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为$scope.displayed显示出来 + // 本示例中,开发者没有设置分页、过滤和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + dataToItemsFn: (data: any) => Array = (data: any) => { + let items: Array = [ + { + label: '启用', + association: 'switch', + disabled: true + }, + { + label: '禁用', + association: 'switch' + }, + { + label: '制作镜像' + }, + { + label: '这是一个很长的选项' + }, + { + label: '制作镜像2', + disabled: true + } + ]; + + return items; + }; + + private createRandomItem(id: number): any { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[Math.floor(Math.random() * 4)]; + const lastName: string = familyName[Math.floor(Math.random() * 4)]; + const age: number = Math.floor(Math.random() * 100); + const state: string = ['已启动', '已停止'][Math.floor(Math.random() * 2)]; + + return { + firstName, + lastName, + age, + state, + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuMenutextComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuMenutextComponent.ts new file mode 100644 index 0000000..0242461 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuMenutextComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-menutext.html' +}) +export class ActionmenuMenutextComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuPanelstyleComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuPanelstyleComponent.ts new file mode 100644 index 0000000..2f3aab0 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuPanelstyleComponent.ts @@ -0,0 +1,48 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-panelstyle.html' +}) +export class ActionmenuPanelstyleComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'renew', + label: '续费' + }, + { + id: 'reset', + label: '重置' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuShownumComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuShownumComponent.ts new file mode 100644 index 0000000..076f4b3 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuShownumComponent.ts @@ -0,0 +1,44 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-shownum.html' +}) +export class ActionmenuShownumComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'renew', + label: '续费' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuSpaceComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuSpaceComponent.ts new file mode 100644 index 0000000..28f09ba --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuSpaceComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-space.html' +}) +export class ActionmenuSpaceComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuTableComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuTableComponent.ts new file mode 100644 index 0000000..b971951 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuTableComponent.ts @@ -0,0 +1,111 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-table.html' +}) +export class ActionmenuTableComponent { + myLogs: Array = []; + displayed: Array = []; + srcData: TiTableSrcData; + data: Array = []; + columns: Array = [ + { + label: 'first name', + width: '25%' + }, + { + label: 'last name', + width: '20%' + }, + { + label: 'birth date', + width: '20%' + }, + { + label: 'state', + width: '15%' + }, + { + label: 'email', + width: '20%' + } + ]; + private static createRandomItem(id: number): any { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[Math.floor(Math.random() * 4)]; + const lastName: string = familyName[Math.floor(Math.random() * 4)]; + const age: number = Math.floor(Math.random() * 100); + const state: string = ['已启动', '已停止'][Math.floor(Math.random() * 2)]; + + return { + firstName, + lastName, + age, + state, + id + }; + } + + ngOnInit(): void { + for (let j: number = 0; j < 5; j++) { + // 生成10条数据 + this.data.push(ActionmenuTableComponent.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + dataToItemsFn: (data: any) => Array = (data: any) => { + let items: Array = [ + { + id: 'start', + label: '启用', + disabled: true + }, + { + label: '禁用', + // 配置确认框相关属性 + popConfig: { + content: '确定要禁用该项吗?', + close: (): void => { + this.myLogs = [...this.myLogs, `onClose() event = confirm}`]; + }, + dismiss: (): void => { + this.myLogs = [...this.myLogs, `onDismiss() event = dismiss}`]; + } + } + }, + { + id: 'modify', + label: '修改' + }, + { + id: 'delete', + label: '删除' + } + ]; + if (data.state === '已停止') { + items[0].disabled = false; + items[1].disabled = true; + items = [items[0], items[1]]; + } + + return items; + }; + + onSelect(item: any): void { + this.myLogs = [...this.myLogs, `onSelect() event = ${JSON.stringify(item)}}`]; + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuTempleteComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuTempleteComponent.ts new file mode 100644 index 0000000..c3eab71 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuTempleteComponent.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-templete.html' +}) +export class ActionmenuTempleteComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录', + tipPosition: 'top' + }, + { + id: 'powerOn', + label: '开机', + tipPosition: 'top' + }, + { + id: 'refresh', + label: '刷新', + iconName: 'refresh', + tipIconName: 'warn', + customTip: '确定要刷新吗?', + tipPosition: 'left' + }, + { + id: 'delete', + label: '删除', + iconName: 'delete-1', + tipIconName: 'warn', + customTip: '确定要删除吗?', + tipPosition: 'left' + } + ]; +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuTempleteTestComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuTempleteTestComponent.ts new file mode 100644 index 0000000..4c53431 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuTempleteTestComponent.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-templete-test.html' +}) +export class ActionmenuTempleteTestComponent { + items: Array = [ + { + title: '启用', + label: '远程登录', + association: 'switch' + }, + { + title: '禁用', + label: '变更规格', + association: 'switch', + disabled: true + }, + { + title: '制作镜像', + label: '制作镜像' + }, + { + title: '这是一个很长的选项只有云服务器处于关机状态才能执行该操作', + label: '云服务器' + }, + { + title: '变更规格2', + label: '规格', + children: [ + { + title: '制作镜像', + label: '制作规格', + children: [ + { + title: '制作镜像', + label: '规格1' + }, + { + title: '变更规格', + label: '规格2', + disabled: true + } + ] + }, + { + title: '变更规格', + label: '规格', + disabled: true + } + ] + }, + { + title: '制作镜像2', + label: '镜像', + disabled: true + } + ]; + + onSelect(item: TiActionmenuItem): void { + console.log('onSelect()'); + console.log(item); + } +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuTestModule.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuTestModule.ts new file mode 100644 index 0000000..6647951 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuTestModule.ts @@ -0,0 +1,148 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiActionmenuModule, TiIconModule, TiTableModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { ActionmenuBasicComponent } from './ActionmenuBasicComponent'; +import { ActionmenuDataComponent } from './ActionmenuDataComponent'; +import { ActionmenuItemsComponent } from './ActionmenuItemsComponent'; +import { ActionmenuTipsComponent } from './ActionmenuTipsComponent'; +import { ActionmenuData2Component } from './ActionmenuData2Component'; +import { ActionmenuFocusComponent } from './ActionmenuFocusComponent'; +import { ActionmenuTableComponent } from './ActionmenuTableComponent'; +import { ActionmenuEventComponent } from './ActionmenuEventComponent'; +import { ActionmenuDividerComponent } from './ActionmenuDividerComponent'; +import { ActionmenuIdComponent } from './ActionmenuIdComponent'; +import { ActionmenuItemsChangeComponent } from './ActionmenuItemsChangeComponent'; +import { ActionmenuManyComponent } from './ActionmenuManyComponent'; +import { ActionmenuLabelkeyComponent } from './ActionmenuLabelkeyComponent'; +import { ActionmenuDisabledComponent } from './ActionmenuDisabledComponent'; +import { ActionmenuShownumComponent } from './ActionmenuShownumComponent'; +import { ActionmenuSpaceComponent } from './ActionmenuSpaceComponent'; +import { ActionmenuTipsTestComponent } from './ActionmenuTipsTestComponent'; +import { ActionmenuMenutextComponent } from './ActionmenuMenutextComponent'; +import { ActionmenuTempleteTestComponent } from './ActionmenuTempleteTestComponent'; +import { ActionmenuTempleteComponent } from './ActionmenuTempleteComponent'; +import { ActionmenuPanelstyleComponent } from './ActionmenuPanelstyleComponent'; + +@NgModule({ + imports: [ + CommonModule, + TiActionmenuModule, + TiTableModule, + TiIconModule, + DemoLogModule, + RouterModule.forChild(ActionmenuTestModule.ROUTES) + ], + declarations: [ + ActionmenuBasicComponent, + ActionmenuDataComponent, + ActionmenuData2Component, + ActionmenuEventComponent, + ActionmenuItemsComponent, + ActionmenuTipsComponent, + ActionmenuFocusComponent, + ActionmenuTableComponent, + ActionmenuIdComponent, + ActionmenuDividerComponent, + ActionmenuItemsChangeComponent, + ActionmenuManyComponent, + ActionmenuLabelkeyComponent, + ActionmenuDisabledComponent, + ActionmenuShownumComponent, + ActionmenuSpaceComponent, + ActionmenuTipsTestComponent, + ActionmenuMenutextComponent, + ActionmenuTempleteTestComponent, + ActionmenuTempleteComponent, + ActionmenuPanelstyleComponent + ] +}) +export class ActionmenuTestModule { + static readonly LINKS: Array = [{ href: 'components/TiActionmenuComponent.html', label: 'Actionmenu' }]; + static readonly ROUTES: Routes = [ + { + path: 'actionmenu/actionmenu-basic', + component: ActionmenuBasicComponent + }, + { + path: 'actionmenu/actionmenu-items', + component: ActionmenuItemsComponent + }, + { + path: 'actionmenu/actionmenu-divider', + component: ActionmenuDividerComponent + }, + { + path: 'actionmenu/actionmenu-tips', + component: ActionmenuTipsComponent + }, + { + path: 'actionmenu/actionmenu-event', + component: ActionmenuEventComponent + }, + { + path: 'actionmenu/actionmenu-data', + component: ActionmenuDataComponent + }, + { + path: 'actionmenu/actionmenu-id', + component: ActionmenuIdComponent + }, + { + path: 'actionmenu/actionmenu-data2', + component: ActionmenuData2Component + }, + { + path: 'actionmenu/actionmenu-items-change', + component: ActionmenuItemsChangeComponent + }, + { + path: 'actionmenu/actionmenu-table', + component: ActionmenuTableComponent + }, + { + path: 'actionmenu/actionmenu-focus', + component: ActionmenuFocusComponent + }, + { path: 'actionmenu/actionmenu-many', component: ActionmenuManyComponent }, + { + path: 'actionmenu/actionmenu-labelkey', + component: ActionmenuLabelkeyComponent + }, + { + path: 'actionmenu/actionmenu-shownum', + component: ActionmenuShownumComponent + }, + { + path: 'actionmenu/actionmenu-space', + component: ActionmenuSpaceComponent + }, + { + path: 'actionmenu/actionmenu-disabled', + component: ActionmenuDisabledComponent + }, + { + path: 'actionmenu/actionmenu-menutext', + component: ActionmenuMenutextComponent + }, + { + path: 'actionmenu/actionmenu-panelstyle', + component: ActionmenuPanelstyleComponent + }, + { + path: 'actionmenu/actionmenu-templete', + component: ActionmenuTempleteComponent + }, + { + path: 'actionmenu/actionmenu-tips-test', + component: ActionmenuTipsTestComponent + }, + { + path: 'actionmenu/actionmenu-templete-test', + component: ActionmenuTempleteTestComponent + } + ]; +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuTipsComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuTipsComponent.ts new file mode 100644 index 0000000..d43b1df --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuTipsComponent.ts @@ -0,0 +1,45 @@ +import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-tips.html' +}) +export class ActionmenuTipsComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机', + disabled: true, + tip: '已冻结,不能执行此操作。请联系客服。', + tipPosition: 'top' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启', + tip: '重新启动服务器。', + tipPosition: 'right' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/actionmenu/demo/src/app/actionmenu/ActionmenuTipsTestComponent.ts b/src/actionmenu/demo/src/app/actionmenu/ActionmenuTipsTestComponent.ts new file mode 100644 index 0000000..b448957 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/ActionmenuTipsTestComponent.ts @@ -0,0 +1,98 @@ +import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { TiActionmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './actionmenu-tips-test.html' +}) +export class ActionmenuTipsTestComponent { + items: Array = [ + { + label: '启用', + association: 'switch' + }, + { + label: '禁用', + association: 'switch', + disabled: true, + tip: '已禁用操作', + tipPosition: 'left' + }, + { + label: '制作镜像', + tip: '只有云服务器处于关机状态,才能执行该操作' + }, + { + label: '这是一个很长的选项,只有云服务器处于关机状态,才能执行该操作', + tip: 'this is a tip', + tipPosition: 'right' + }, + { + label: '变更规格2', + tip: 'welcome tip', + children: [ + { + label: '制作镜像', + tip: '只有云服务器处于关机状态,才能执行该操作', + children: [ + { + label: '制作镜像', + tip: '只有云服务器处于关机状态,才能执行该操作' + }, + { + label: '变更规格', + disabled: true + } + ] + }, + { + label: '变更规格', + disabled: true + } + ] + }, + { + label: '制作镜像2', + tip: '只有云服务器处于关机状态,才能执行该操作', + disabled: true + } + ]; + + items2: Array = [ + { + label: '启用', + association: 'switch' + }, + { + label: '禁用', + association: 'switch', + disabled: true, + tip: '已禁用操作', + tipPosition: 'left' + }, + { + label: '制作镜像', + tip: '只有云服务器处于关机状态,才能执行该操作' + }, + { + label: '这是一个很长的选项,只有云服务器处于关机状态,才能执行该操作', + tip: 'this is a tip', + tipPosition: 'right' + } + ]; + // 获取tip模板 + @ViewChild('tipTemplate') tipTemplateRef: TemplateRef; + + onSelect(item: TiActionmenuItem): void { + console.log('onSelect()'); + console.log(item); + } + + private showTip: boolean = false; + onClickToggleShowTip() { + this.showTip = !this.showTip; + // item.tip=''时,不显示tip + this.items2.forEach((item: TiActionmenuItem) => { + item.tip = this.showTip ? (this.tipTemplateRef as any) : ''; + }); + } +} diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-basic.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-basic.html new file mode 100644 index 0000000..48b5d9f --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-basic.html @@ -0,0 +1 @@ + diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-data.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-data.html new file mode 100644 index 0000000..ab9f142 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-data.html @@ -0,0 +1,11 @@ +

描述

+

Actionmenu菜单按钮组件,监听表格数据变化,点击选择也会改变表格数据。(选项置灰)

+

示例

+
+ {{i+"行当前状态:"+row.state}} + +
+
+ +
+ diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-data2.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-data2.html new file mode 100644 index 0000000..df1f440 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-data2.html @@ -0,0 +1,12 @@ +

描述

+

Actionmenu菜单按钮组件,监听表格数据变化,点击选择也会改变表格数据。(选项数目变化)

+

示例

+
+ {{i+"行当前状态:"+row.state}} + + +
+
+ +
+ diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-disabled.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-disabled.html new file mode 100644 index 0000000..4499e99 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-disabled.html @@ -0,0 +1 @@ + diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-divider.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-divider.html new file mode 100644 index 0000000..8292465 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-divider.html @@ -0,0 +1,50 @@ +

描述

+

Actionmenu菜单按钮组件,各项之间分隔线测试,接口为 showDivding。

+

示例

+
+

1. 不设置 showDivider 接口 ----- 正确显示结果:显示分隔线

+ + +

2. 设置 showDivider 为 true, 只有一项 ----- 正确显示结果:不显示分隔线

+ + + +

+ 3. 设置 showDivider 为 true, 有3项,并设置每项之间的间距为20px,没有“更多”项 ----- + 正确显示结果:分隔线显示,并处于两项中间,实际两项的间距为21px +

+ + + +

+ 4. 设置 showDivider 为 true, 有3项在外,其他项在“更多”项内,并设置每项之间的间距为20px ----- + 正确显示结果:分隔线显示,并处于两项中间,实际两项的间距为21px +

+ + +
diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-event.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-event.html new file mode 100644 index 0000000..6544c1b --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-event.html @@ -0,0 +1,9 @@ + + + diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-focus.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-focus.html new file mode 100644 index 0000000..4999483 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-focus.html @@ -0,0 +1,25 @@ +

描述

+

Actionmenu菜单按钮组件,支持聚焦autofoucs/focus()/tabindex

+

示例

+

1.autofocus:

+
+ +
+

2.focus():

+
+ + + {{item.label}} + + +
+ +   + +   + +
+

3.tabindex:

+
+ +
diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-id.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-id.html new file mode 100644 index 0000000..162608d --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-id.html @@ -0,0 +1,13 @@ +

描述

+

id属性和组件id配合使用,用于自定义每个菜单的id

+

示例

+ +

1.默认:不设置id,设置item.id

+
+ +
+ +

2.设置id,设置item.id

+
+ +
diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-items-change.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-items-change.html new file mode 100644 index 0000000..218a1c0 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-items-change.html @@ -0,0 +1,14 @@ +

描述

+

Actionmenu菜单按钮组件,测试items接口属性变更

+

示例

+ +

1.默认显示label值

+
+ +
+ + + + + + diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-items.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-items.html new file mode 100644 index 0000000..9432098 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-items.html @@ -0,0 +1,52 @@ +

描述

+

Actionmenu菜单按钮组件,选项数目依照最大个数和宽度空间。

+注: 10.1.5版本改动默认最大显示数量3个(包括menu) +

示例

+
+

+ 1.属性默认值的测试:只设置items属性,其余属性均不设置,使用默认值【最大宽度(默认宽度250px),间距(默认间距10px),最大显示个数】 ----- + 正确显示结果:3项显示(包含menu),2项在外,其余在更多中显示 +

+ +

2.最大宽度和最大占位个数的配合

+

2.1 最多显示4个,最大宽度265px(刚好能显示4个),间距20px ----- 正确显示结果:3项在外显示,其余在更多中显示

+ +

2.2.最多显示4个,最大宽度500px(能显示全),间距20px ----- 正确显示结果:3项在外显示,其余在更多中显示

+ +

2.3.最多显示3个,最大宽度264px(265px时能够显示4个),间距20px ----- 正确显示结果:2项在外显示,其余在更多中显示

+ +

2.4.最多显示4个,最大宽度50px(更多项宽度为61px),间距20px ----- 正确显示结果:只显示下拉

+
+ +
+

2.5.item项只有两项,只够显示1项, 配置menuText ----- 正确显示结果:只显示下拉,下拉文本为操作项

+ +

2.6.item项只有两项,够显示两项 ----- 显示两项

+ +

3.item项只有一个

+

3.1 item项为灰化状态

+ +

3.2 item项非灰化状态

+ +
diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-labelkey.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-labelkey.html new file mode 100644 index 0000000..7a20b4a --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-labelkey.html @@ -0,0 +1 @@ + diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-many.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-many.html new file mode 100644 index 0000000..2ce7127 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-many.html @@ -0,0 +1,25 @@ +

描述

+

Actionmenu菜单按钮组件: 表格中使用

+

示例

+ + + + + + + + + + + + + + + + + + +
{{column.label}}
{{row.firstName}}{{row.lastName}}{{row.age}}{{row.state}} + +
+
diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-menutext.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-menutext.html new file mode 100644 index 0000000..ef72d0b --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-menutext.html @@ -0,0 +1,2 @@ +
+ diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-panelstyle.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-panelstyle.html new file mode 100644 index 0000000..4a4e386 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-panelstyle.html @@ -0,0 +1 @@ + diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-shownum.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-shownum.html new file mode 100644 index 0000000..6cbf2f0 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-shownum.html @@ -0,0 +1,3 @@ +
+
+ diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-space.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-space.html new file mode 100644 index 0000000..45dab04 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-space.html @@ -0,0 +1 @@ + diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-table.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-table.html new file mode 100644 index 0000000..349cb63 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-table.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + +
{{column.label}}
{{row.firstName}}{{row.lastName}}{{row.age}}{{row.state}} + +
+
+ diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-templete-test.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-templete-test.html new file mode 100644 index 0000000..450948f --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-templete-test.html @@ -0,0 +1,13 @@ +

描述

+

自定义模板测试

+

示例

+

1.兼容旧版模板测试(10.0.3 之前版本)

+

自定义 item 模板(未添加 #item 标签

+
+ + + + 自定义item模板 - {{item.label}} + + +
diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-templete.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-templete.html new file mode 100644 index 0000000..888b544 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-templete.html @@ -0,0 +1,10 @@ + + + + {{item.label}} + + + + {{item.customTip || item.label}} + + diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-tips-test.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-tips-test.html new file mode 100644 index 0000000..4a5f29b --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-tips-test.html @@ -0,0 +1,59 @@ +

描述

+

Actionmenu菜单按钮组件,提示功能测试

+

示例

+ +

1.一般 Sring 类型 tip

+
+ +
+ +

2.自定义tip模板(模板在组件外)

+

适用tip根据条件,可能显示,也可能不显示的场景

+
+
+

+
+ + + + 自定义tip模板 - item的label: {{item.label}} + +
+

3.自定义 tip 模板(添加 #tip 标签

+
+ + + + 自定义tip模板 - item的label: {{item.label}} + + +
+ +

4.自定义 item 模板(添加 #item 标签) + 自定义 tip 模板

+
+ + + + 自定义item模板 - {{item.label}} + + + + 自定义tip模板 - item的label: {{item.label}} + + +
+ +

5.兼容旧版模板测试(10.0.3 之前版本)

+

自定义 item 模板(未添加 #item 标签) + 自定义 tip 模板

+
+ + + + 自定义item模板 - {{item.label}} + + + + 自定义tip模板 - item的label: {{item.label}} + + +
diff --git a/src/actionmenu/demo/src/app/actionmenu/actionmenu-tips.html b/src/actionmenu/demo/src/app/actionmenu/actionmenu-tips.html new file mode 100644 index 0000000..affac7e --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/actionmenu-tips.html @@ -0,0 +1 @@ + diff --git a/src/actionmenu/demo/src/app/actionmenu/webdoc/actionmenu-demos.js b/src/actionmenu/demo/src/app/actionmenu/webdoc/actionmenu-demos.js new file mode 100644 index 0000000..0a9858a --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/webdoc/actionmenu-demos.js @@ -0,0 +1,179 @@ +export default { + column: '2', + demos: [ + { + demoId: 'actionmenu-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

Actionmenu 组件的基本用法。', + 'en-US': '

basic

', + }, + apis: [ + 'TiActionmenuComponent.properties.items', + 'TiMenuItem.properties.children', + 'TiMenuItem.properties.id', + 'TiMenuItem.properties.label', + ], + }, + { + demoId: 'actionmenu-labelkey', + name: { + 'zh-CN': '显示文本', + 'en-US': 'labelkey', + }, + desc: { + 'zh-CN': '通过属性labelKey配置要显示文本的键值。', + 'en-US': '

labelKey

', + }, + apis: ['TiActionmenuComponent.properties.labelKey'], + }, + { + demoId: 'actionmenu-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled', + }, + desc: { + 'zh-CN': + '通过属性item.disabled配置当前下拉选项是否为禁用状态。', + 'en-US': '

disabled

', + }, + apis: [ + 'TiActionmenuComponent.properties.disabled', + 'TiMenuItem.properties.disabled', + ], + }, + { + demoId: 'actionmenu-tips', + name: { + 'zh-CN': '提示信息', + 'en-US': 'tips', + }, + desc: { + 'zh-CN': + '通过属性item.tip配置当前菜单项提示信息;通过属性item.tipPosition配置当前菜单项提示信息弹出方向;通过属性tipMaxWidth配置各菜单项提示信息最大宽度。', + 'en-US': '

tips

', + }, + apis: [ + 'TiMenuItem.properties.tip', + 'TiMenuItem.properties.tipPosition', + 'TiActionmenuComponent.properties.tipMaxWidth', + ], + }, + { + demoId: 'actionmenu-shownum', + name: { + 'zh-CN': '显示个数', + 'en-US': 'showNum', + }, + desc: { + 'zh-CN': + '通过属性maxShowNum配置显示菜单按钮的最大个数;通过属性maxWidth配置组件最大宽度,使用maxWidth接口在表格中不生效,可通过设置 td 的 white-space: normal 来解决。', + 'en-US': '

showNum

', + }, + apis: [ + 'TiActionmenuComponent.properties.maxShowNum', + 'TiActionmenuComponent.properties.maxWidth', + ], + }, + { + demoId: 'actionmenu-space', + name: { + 'zh-CN': '间距', + 'en-US': 'space', + }, + desc: { + 'zh-CN': '通过属性space配置菜单按钮之间的间距。', + 'en-US': '

space

', + }, + apis: ['TiActionmenuComponent.properties.space'], + }, + { + demoId: 'actionmenu-panelstyle', + name: { + 'zh-CN': '下拉面板样式', + 'en-US': 'panelstyle', + }, + desc: { + 'zh-CN': + '通过属性panelMaxWidth配置下拉面板最大宽度;通过属性panelMaxHeight配置下拉面板最大高度。', + 'en-US': '

panelstyle

', + }, + apis: [ + 'TiActionmenuComponent.properties.panelMaxWidth', + 'TiActionmenuComponent.properties.panelMaxHeight', + ], + }, + { + demoId: 'actionmenu-menutext', + name: { + 'zh-CN': '下拉菜单显示文本', + 'en-US': 'menuText', + }, + desc: { + 'zh-CN': + '通过属性moreText配置下拉菜单显示文本;通过属性menuText配置只有更多项时的下拉菜单显示文本。', + 'en-US': '

menuText

', + }, + apis: [ + 'TiActionmenuComponent.properties.moreText', + 'TiActionmenuComponent.properties.menuText', + ], + }, + { + demoId: 'actionmenu-templete', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'templete', + }, + desc: { + 'zh-CN': + '通过#item配置下拉面板中选项的模板;通过#tip配置当前菜单项提示信息的模板。', + 'en-US': '

templete

', + }, + apis: [ + 'TiActionmenuComponent.slots.itemTemplate', + 'TiActionmenuComponent.slots.tipTemplate', + ], + }, + { + demoId: 'actionmenu-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event', + }, + desc: { + 'zh-CN': + '当选中菜单项时触发select事件,传递出去的参数为当前选中项的数据;当点击下拉菜单时触发moreClick事件,传递出去的参数为面板展开状态。', + 'en-US': '

event

', + }, + apis: [ + 'TiActionmenuComponent.events.select', + 'TiActionmenuComponent.events.moreClick', + ], + }, + { + demoId: 'actionmenu-table', + name: { + 'zh-CN': '表格操作列', + 'en-US': 'table', + }, + desc: { + 'zh-CN': + '通过属性data通常配置表格当前行数据;通过属性dataToItemsFn配置当前行数据转化为items数据;通过属性item.popConfig配置当前行显示在外部的菜单按钮气泡确认框数据。', + 'en-US': '

table

', + }, + apis: [ + 'TiActionmenuComponent.properties.data', + 'TiActionmenuComponent.properties.dataToItemsFn', + 'TiActionmenuItem.properties.popConfig', + 'TiPopconfirmConfig.properties.content', + 'TiPopconfirmConfig.methods.close', + 'TiPopconfirmConfig.methods.dismiss', + ], + }, + ], +}; diff --git a/src/actionmenu/demo/src/app/actionmenu/webdoc/actionmenu.cn.md b/src/actionmenu/demo/src/app/actionmenu/webdoc/actionmenu.cn.md new file mode 100644 index 0000000..997eb68 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/webdoc/actionmenu.cn.md @@ -0,0 +1,23 @@ +--- +title: Actionmenu 菜单按钮 +--- +# Actionmenu 菜单按钮 + +
+ +提供一组操作按钮的组件,当按钮数量太多导致预留空间大小无法显示所有按钮时,自动将超出部分的按钮放置在一个 menu 组件下拉中。   + +```typescript +import { TiActionmenuModule } from '@opentiny/ng'; +``` + +
+ +
+ +提供一组操作按钮的组件,当按钮数量太多导致预留空间大小无法显示所有按钮时,自动将超出部分的按钮放置在一个 menu 组件下拉中。   + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
diff --git a/src/actionmenu/demo/src/app/actionmenu/webdoc/actionmenu.en.md b/src/actionmenu/demo/src/app/actionmenu/webdoc/actionmenu.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/actionmenu/demo/src/app/actionmenu/webdoc/actionmenu.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/actionmenu/demo/src/app/app.html b/src/actionmenu/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/actionmenu/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/actionmenu/demo/src/favicon.ico b/src/actionmenu/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/actionmenu/demo/src/index.html b/src/actionmenu/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/actionmenu/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/actionmenu/demo/src/main.ts b/src/actionmenu/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/actionmenu/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/actionmenu/demo/test.ts b/src/actionmenu/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/actionmenu/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/actionmenu/demo/tsconfig.app.json b/src/actionmenu/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/actionmenu/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/actionmenu/demo/tsconfig.spec.json b/src/actionmenu/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/actionmenu/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/actionmenu/lib/index.ts b/src/actionmenu/lib/index.ts new file mode 100644 index 0000000..3eb2259 --- /dev/null +++ b/src/actionmenu/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiActionmenuModule'; diff --git a/src/actionmenu/lib/ng-package.json b/src/actionmenu/lib/ng-package.json new file mode 100644 index 0000000..86956a3 --- /dev/null +++ b/src/actionmenu/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/actionmenu", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/actionmenu/lib/package.json b/src/actionmenu/lib/package.json new file mode 100644 index 0000000..11cad73 --- /dev/null +++ b/src/actionmenu/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-actionmenu", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-menu": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-popconfirm": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/actionmenu/lib/project.json b/src/actionmenu/lib/project.json new file mode 100644 index 0000000..de4840e --- /dev/null +++ b/src/actionmenu/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/actionmenu/lib", + "sourceRoot": "src/actionmenu/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/actionmenu"], + "options": { + "project": "src/actionmenu/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/actionmenu"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js actionmenu" + }, + { + "command": "ng default-build actionmenu" + }, + { + "command": "node build/clear-default-theme.js actionmenu" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/actionmenu && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build actionmenu && ng pack actionmenu && node build/publish.js actionmenu --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/actionmenu/lib/src/TiActionmenuComponent.ts b/src/actionmenu/lib/src/TiActionmenuComponent.ts new file mode 100644 index 0000000..b172c58 --- /dev/null +++ b/src/actionmenu/lib/src/TiActionmenuComponent.ts @@ -0,0 +1,520 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + Input, + IterableChanges, + IterableDiffer, + IterableDiffers, + KeyValueChanges, + KeyValueDiffer, + KeyValueDiffers, + Output, + Renderer2, + TemplateRef, + ViewChild +} from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiMenuComponent, TiMenuItem } from '@opentiny/ng-menu'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiPopconfirmConfig } from '@opentiny/ng-popconfirm'; +import packageInfo from '../package.json'; + +export interface TiActionmenuItem extends TiMenuItem { + /** + * 配置呈现在更多按钮外部菜单项的气泡确认框 + */ + popConfig?: TiPopconfirmConfig; +} + +// 空间设置规则: +// 可显示项目数(children项目必须出现在“更多”),最大显示数目,空间宽度。 +// 更多”里面至少两个选项 +// 如果只有一个带children项目那么直接用menu。 + +/** + * ActionMenu菜单按钮组件 + * + * ActionMenu组件主要是一组操作按钮。当按钮数量太多导致预留空间大小无法显示所有按钮时,自动将超出部分的按钮放置在一个menu组件下拉中。 + * + * 使用该组件,用户只需关注操作项配置,无需关注操作项显示不足的情况。 + * + * 注意:ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.(本组件在调试环境报此错误,不必理会。生产环境不报错,功能也不受影响。) + * + */ +@Component({ + selector: 'ti-actionmenu', + templateUrl: './actionmenu.html', + styleUrls: ['./actionmenu.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[style.max-width]': 'maxWidth' + } +}) +export class TiActionmenuComponent extends TiFormComponent { + // 本应继承自BaseComp,但是想要A标签焦点功能 + /* Menu参数透传start */ + /** + * 必选,菜单按钮数据集 + */ + @Input() items: Array; + /** + * 下拉面板最大宽度,超过时内容换行显示 + */ + @Input() panelMaxWidth: string = '130px'; + /** + * 下拉面板最大高度,超过时面板出现竖向滚动条 + */ + @Input() panelMaxHeight: string = '9999px'; + /** + * 菜单按钮要展示的键值 + */ + @Input() labelKey: string = 'label'; + /** + * 选中菜单项时触发的回调,参数:当前选中项数据 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + /* Menu参数透传end */ + + /** + * 最大宽度 + */ + @Input() maxWidth: string = '250px'; + /** + * 各菜单项间距 + */ + @Input() space: string = '10px'; + // 分割线为divider,分割字符叫做separator(比如分割ip里的点) + /** + * @ignore + * + * 各item之间是否展示分割线 + * + */ + @Input() showDivider: boolean = true; + /** + * 菜单项最大显示个数, 包含下拉菜单 + */ + @Input() maxShowNum: number = 3; + /** + * 下拉菜单的显示文本,默认值为‘更多’ + */ + @Input() moreText: string; + /** + * 仅显示下拉菜单时的显示文本,默认值为‘操作’ + */ + @Input() menuText: string; + /** + * 绑定数据,一般用于绑定表格本行数据场景 + */ + @Input() data: any; + /** + * data 接口 数据转化为 items 接口数据,一般用于转化表格本行数据场景 + */ + @Input() dataToItemsFn: (data: any) => Array; + /** + * tip 最大宽度 + */ + @Input() tipMaxWidth: string; + /** + * 点击下拉菜单更多触发的回调,参数:下拉菜单是否展开 + */ + @Output() readonly moreClick: EventEmitter = new EventEmitter(); + + /** + * @ignore + * 用户写的自定义模板 + */ + @ContentChild(TemplateRef, { static: true }) firstTemplate: TemplateRef; + /** + * 下拉选项区域的模板 + */ + @ContentChild('item', { static: true }) itemTemplate: TemplateRef; + /** + * tip 提示区域的模板 + */ + @ContentChild('tip', { static: true }) tipTemplate: TemplateRef; + + /** + * @ignore + */ + @ViewChild(TiMenuComponent, { static: true }) menuCom: TiMenuComponent; + /** + * @ignore + * 下拉菜单绑定的选项数据 + */ + public panelItems: Array = []; + /** + * @ignore + * 各item之间间隔的一半距离 + */ + public halfSpace: string = ''; + protected versionInfo: string = super.getVersion(packageInfo); + + private itemsDiffer: IterableDiffer; + private dataDiffer: KeyValueDiffer; + private needInitItems: boolean = false; + + private itemElems: HTMLCollection; + private dividingElems: HTMLCollection; + private menuElem: HTMLElement; + private moreTextElem: HTMLElement; + private menuTextElem: HTMLElement; + + private static lastItemElems: Array; + private static lastOutShowNum: number = 0; + private static lastShowMenu: boolean = true; + private static lastMaxWidth: string = '250px'; + private static lastMaxShowNum: number = 3; + private static lastTime: number = 0; + + constructor( + protected hostRef: ElementRef, + protected renderer: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + private iterableDiffers: IterableDiffers, + private keyValueDiffers: KeyValueDiffers + ) { + super(hostRef, renderer, changeDetectorRef); + const localeWords = TiLocale.getLocaleWords().tiActionmenu; + this.moreText = localeWords.more; + this.menuText = localeWords.operation; + } + + // 纠正 IE下同一行两个元素 offsetTop 值可能存在的一些(目前测试到有1px)的误差 + private static isEqualOffsetTop(elem1: any, elem2: any): boolean { + return Math.abs((elem1 as HTMLElement).offsetTop - (elem2 as HTMLElement).offsetTop) <= 2; + } + + /** + * @ignore + * 选中菜单内选项 + * @param item 菜单项数据 + */ + public onSelect(item: TiActionmenuItem): void { + if (!item.disabled) { + this.select.emit(item); + } + } + /** + * @ignore + * 点击更多按钮 + */ + public onClick(): void { + this.moreClick.emit(this.menuCom.dropComs.first.isShow); + } + + ngOnInit(): void { + super.ngOnInit(); + this.panelItems = this.items; + this.halfSpace = Number.parseInt(this.space, 10) / 2 + 'px'; + } + ngDoCheck(): void { + if (this.data) { + if (!this.dataDiffer) { + // 首次docheck是ngOnInit()之后 + this.dataDiffer = this.keyValueDiffers.find(this.data).create(); + } + const dataChanges: KeyValueChanges | null = this.dataDiffer.diff(this.data); + if (dataChanges) { + // 根据data初始化items. 注意:第一次diff会走入changes + this.items = this.dataToItemsFn(this.data); + } + } + if (this.items) { + if (!this.itemsDiffer) { + this.itemsDiffer = this.iterableDiffers.find(this.items).create((index: number, item: TiActionmenuItem) => { + return item[this.labelKey]; + }); + this.needInitItems = true; + } + const itemsChanges: IterableChanges | null = this.itemsDiffer.diff(this.items); + // 注意:如果有数据,那么第一次diff会就走入changes。如果是空数据,第一次diff结果是null,不会进入changes。 + if (itemsChanges) { + // 如果items改变,则重新初始化。 + this.needInitItems = true; + // 要绘制的数据变了,强制变更检测 + this.changeDetectorRef.markForCheck(); + } + } + } + /** + * 兼容旧版: + * 10.0.3 版本之前只能内嵌一个模板,无命名。 + * 新版可以内嵌两个模板,示例书写要求都命名(#item,#tip)。 + * 但需要兼容旧版无命名测试用例。 + */ + + ngAfterContentInit(): void { + super.ngAfterContentInit(); + // 如果 item 模板为空 && 存在第一个模板,那么把第一个出现的 “非 #tip 标签” 的模板作为 item 模板 + if ( + !this.itemTemplate && + this.firstTemplate && + this.firstTemplate.elementRef.nativeElement !== (this.tipTemplate && this.tipTemplate.elementRef.nativeElement) + ) { + this.itemTemplate = this.firstTemplate; + } + } + ngAfterViewInit() { + // 查找元素赋值 + this.menuElem = this.menuCom.nativeElement; + this.moreTextElem = this.hostRef.nativeElement.getElementsByClassName('tiMoreText')[0]; + this.menuTextElem = this.hostRef.nativeElement.getElementsByClassName('tiMenuText')[0]; + } + ngAfterViewChecked(): void { + if (this.needInitItems) { + this.initItems(); + } + super.ngAfterViewChecked(); // 内部执行了autofocus + } + /** + * 计算那些item应该显示在外部,移除不该显示的item,调整pannel上显示的item项,调整menu显示的文字。 + * TODO:设置display:none,会引起重排,还有优化空间。 + */ + private initItems(): void { + this.needInitItems = false; + + // 需要支持数据动态变更,这两项可能变化。 + this.itemElems = this.hostRef.nativeElement.getElementsByClassName('ti3-action-menu-item'); + this.dividingElems = this.hostRef.nativeElement.getElementsByClassName('ti3-action-menu-divider'); + + // 情况零:如果itemElems相同,且临近时间,那么按照记录恢复布局即可 + if (this.itemElemsEqual() && new Date().getMilliseconds() - TiActionmenuComponent.lastTime < 1000) { + this.recover(); + return; + } + + // 情况一:items为空 + if (!this.items || this.items.length === 0) { + this.setDisplay(this.menuElem, false); + // 将情况一做记录 + // 注意:getElementsByClassName返回的HTMLCollection,类似一个数组引用,内容可能变化。所以转为数组方便记录。 + // https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCollection + TiActionmenuComponent.lastItemElems = Array.from(this.itemElems); + TiActionmenuComponent.lastMaxWidth = this.maxWidth; + TiActionmenuComponent.lastMaxShowNum = this.maxShowNum; + TiActionmenuComponent.lastOutShowNum = 0; + TiActionmenuComponent.lastShowMenu = false; + TiActionmenuComponent.lastTime = new Date().getMilliseconds(); + return; + } else { + this.setDisplay(this.menuElem, true); + } + // 注意: itemElems不是数组,并且数据更新时,取到的length不对。所以,得使用items的length + let firstChildrenIndex: number = this.findfirstChildrenAndSetDisplay(); // 第一个含有子项目的索引 + + // 情况二:不显示menu //条件:没有超过最大显示个数,且最后一个项目也在第一行, 且没有子项 + if ( + this.items.length <= this.maxShowNum && + TiActionmenuComponent.isEqualOffsetTop(this.itemElems[this.items.length - 1], this.itemElems[0]) && + firstChildrenIndex === -1 + ) { + this.setDisplay(this.menuElem, false); + // 设置可聚焦元素 + const focusItems: Array = []; + for (let i: number = 0; i < this.items.length; i++) { + focusItems.push(this.itemElems[i]); + } + this.setFocusableElems(focusItems); + + // 将情况二做记录 + TiActionmenuComponent.lastItemElems = Array.from(this.itemElems); + TiActionmenuComponent.lastMaxWidth = this.maxWidth; + TiActionmenuComponent.lastMaxShowNum = this.maxShowNum; + TiActionmenuComponent.lastOutShowNum = this.items.length; + TiActionmenuComponent.lastShowMenu = false; + TiActionmenuComponent.lastTime = new Date().getMilliseconds(); + return; + } + // 情况三:显示Menu,显示数目条件:有子项目的一定在菜单内/倒数第二个一定在菜单内/最大显示个数(含menu)。3值取最小。 + let outShowNum = this.getAndSetOutShowNum(firstChildrenIndex); + // 将情况三做记录 + TiActionmenuComponent.lastItemElems = Array.from(this.itemElems); + TiActionmenuComponent.lastMaxWidth = this.maxWidth; + TiActionmenuComponent.lastMaxShowNum = this.maxShowNum; + TiActionmenuComponent.lastOutShowNum = outShowNum; + TiActionmenuComponent.lastShowMenu = true; + TiActionmenuComponent.lastTime = new Date().getMilliseconds(); + } + + private setTextElem(outShowNum: number): void { + if (outShowNum === 0) { + // 立即生效,否则根据模板变量生效慢 + this.renderer.setStyle(this.menuElem, 'margin-left', '0px'); + this.setDisplay(this.moreTextElem, false); + this.setDisplay(this.menuTextElem, true); + // 设置可聚焦元素 + this.setFocusableElems(this.menuCom.getFocusableElems()); + } else { + this.setDisplay(this.moreTextElem, true); + this.setDisplay(this.menuTextElem, false); + // 设置可聚焦元素 + const focusItems: Array = []; + for (let i: number = 0; i < outShowNum; i++) { + focusItems.push(this.itemElems[i]); + } + this.setFocusableElems(focusItems.concat(this.menuCom.getFocusableElems())); + } + } + + private recover(): void { + // 恢复是否显示下拉选项。 + this.setDisplay(this.menuElem, TiActionmenuComponent.lastShowMenu); + // 由于使用异步处理导致时序问题,需要新增变量保存该值 + const outShowNum: number = TiActionmenuComponent.lastOutShowNum; + // 恢复露出项 + for (let i: number = 0; i < this.items.length; i++) { + const isShow: boolean = i < outShowNum; + this.setDisplay(this.itemElems[i], isShow); + if (this.dividingElems[i]) { + this.setDisplay(this.dividingElems[i], isShow); + } + } + + // 恢复情况三 + // 设置菜单显示文字,设置可聚焦元素 + if (TiActionmenuComponent.lastShowMenu) { + this.setTextElem(outShowNum); + } else { + // 设置可聚焦元素 + const focusItems: Array = []; + for (let i: number = 0; i < outShowNum; i++) { + focusItems.push(this.itemElems[i]); + } + this.setFocusableElems(focusItems); + } + + /** + * 此句改变了模板变量,需要强制刷新 + * 这里引起ng-serve环境报错ExpressionChangedAfterItHasBeenCheckedError, 所以用Promise延时 + * + * 此处promise不能删除,会导致menu下拉面板定位异常,故先用变量解决时序问题 + */ + Promise.resolve().then(() => { + this.panelItems = this.items.slice(outShowNum, this.items.length); + // 面板的数据变了,需要手动触发变更检测 + this.changeDetectorRef.markForCheck(); + }); + + TiActionmenuComponent.lastTime = new Date().getMilliseconds(); + } + + private findfirstChildrenAndSetDisplay(): number { + let firstChildrenIndex: number = -1; // 第一个含有子项目的索引 + // 这个for循环,做了两件事:找含有子项目,重置item可见。 + for (let i: number = 0; i < this.items.length; i++) { + if (Object.prototype.hasOwnProperty.call(this.items[i], 'children')) { + firstChildrenIndex = i; // 找到第一个含有子项目的索引 + // break; + } + this.setDisplay(this.itemElems[i], true); // 因为动态修改item,所以第二次进入时需要重置item可见。 + if (this.dividingElems[i]) { + // 动态修改item,第二次进入时需要重置分割线可见 + this.setDisplay(this.dividingElems[i], true); + } + } + return firstChildrenIndex; + } + + private getAndSetOutShowNum(myFirstChildrenIndex: number): number { + let firstChildrenIndex: number = myFirstChildrenIndex; + if (firstChildrenIndex === -1) { + firstChildrenIndex = this.items.length; // firstChild给一个超越数组大小的索引,方便下面计算 + } + let outShowNum: number = + this.items.length - 2 < this.maxShowNum - 1 + ? this.items.length - 2 + : this.maxShowNum - 1 < firstChildrenIndex + 1 + ? this.maxShowNum - 1 + : firstChildrenIndex + 1; + + for (let i: number = this.items.length - 1; i >= 0; i--) { + if (i > outShowNum - 1) { + // 本项属于超出的数目,先删除掉 + // TODO: remove和style哪个效率高? + this.setDisplay(this.itemElems[i], false); + if (this.dividingElems[i]) { + this.setDisplay(this.dividingElems[i], false); + } + } else if (!TiActionmenuComponent.isEqualOffsetTop(this.menuElem, this.itemElems[0])) { + // 菜单没有在第一行,删除本项 + this.setDisplay(this.itemElems[i], false); + if (this.dividingElems[i]) { + this.setDisplay(this.dividingElems[i], false); + } + outShowNum--; + } + } + // 设置菜单显示文字,设置可聚焦元素 + this.setTextElem(outShowNum); + + /** + * 此句改变了模板变量,需要强制刷新 + * 这里引起ng-serve环境报错ExpressionChangedAfterItHasBeenCheckedError, 所以用Promise延时 + * 此处promise不能删除,会导致menu下拉面板定位异常 + */ + Promise.resolve().then(() => { + this.panelItems = this.items.slice(outShowNum, this.items.length); + // 面板的数据变了,需要手动触发变更检测 + this.changeDetectorRef.markForCheck(); + }); + + return outShowNum; + } + + /** + * 设置显示/隐藏样式,比ngIf更及时生效。 + * @param elem HTML元素 + * @param isShow 是否显示 + */ + private setDisplay(elem: any, isShow: boolean): void { + this.renderer.setStyle(elem, 'display', isShow ? 'inline-block' : 'none'); + } + + /** + * @ignore + * ngFor遍历的 trackBy函数,防止数据更新导致所有DOM重新渲染。TODO:这里是否该使用trackBy? + * @param index 索引 + * @param item 数据 + * @returns 索引 + */ + public trackByFn(index: any, item: any): any { + return index; + } + /** + * @ignore + * 比较itemElems是否与最后一次相同 + * @returns true if elems equal + */ + public itemElemsEqual(): boolean { + if ( + this.itemElems?.length !== TiActionmenuComponent.lastItemElems?.length || + this.nativeElement.parentNode.tagName !== 'TD' || + this.maxWidth !== TiActionmenuComponent.lastMaxWidth || + this.maxShowNum !== TiActionmenuComponent.lastMaxShowNum + ) { + return false; + } + for (let i = 0; i < this.itemElems.length; i++) { + if ((this.itemElems[i] as HTMLElement).innerHTML !== (TiActionmenuComponent.lastItemElems[i] as HTMLElement).innerHTML) { + return false; + } + } + return true; + } +} diff --git a/src/actionmenu/lib/src/TiActionmenuModule.ts b/src/actionmenu/lib/src/TiActionmenuModule.ts new file mode 100644 index 0000000..7f5789a --- /dev/null +++ b/src/actionmenu/lib/src/TiActionmenuModule.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiActionmenuComponent } from './TiActionmenuComponent'; +import { TiMenuModule } from '@opentiny/ng-menu'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiPopconfirmModule } from '@opentiny/ng-popconfirm'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, TiMenuModule, TiTipModule, TiPopconfirmModule], + exports: [TiActionmenuComponent], + declarations: [TiActionmenuComponent] +}) +export class TiActionmenuModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiActionmenuComponent, TiActionmenuItem } from './TiActionmenuComponent'; diff --git a/src/actionmenu/lib/src/actionmenu.html b/src/actionmenu/lib/src/actionmenu.html new file mode 100644 index 0000000..09d659b --- /dev/null +++ b/src/actionmenu/lib/src/actionmenu.html @@ -0,0 +1,50 @@ + + +
+ + + + {{item[labelKey]}} +
+
+ +
+ +
{{moreText}}
+ + + + + + {{item[labelKey]}} + + + + +
diff --git a/src/actionmenu/lib/src/actionmenu.less b/src/actionmenu/lib/src/actionmenu.less new file mode 100644 index 0000000..caf2059 --- /dev/null +++ b/src/actionmenu/lib/src/actionmenu.less @@ -0,0 +1,49 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-actionmenu-divider-width: 1px; +} + +:host { + display: inline-block; +} + +.ti3-action-menu-item { + text-decoration: none; + color: var(--ti-common-color-text-link); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + cursor: pointer; + // 为了解决服务中的表格使用actionMenu时,偶现的获取$itemDomCache宽度不准确问题 #1131 codeclub + display: inline-block; + .user-select(none); +} + +.ti3-action-menu-item:hover { + color: var(--ti-common-color-text-link-hover); + text-decoration: none; +} + +.ti3-action-menu-item.ti3-action-menu-item-disabled { + cursor: not-allowed; + text-decoration: none; + outline: none !important; + + * { + color: var(--ti-common-color-text-disabled) !important; + } +} + +a.ti3-action-menu-item:focus, +a.ti3-action-menu-item:active { + color: var(--ti-common-color-text-link-hover); +} + +.ti3-action-menu-divider { + position: relative; + top: 2px; + display: inline-block; + width: var(--ti-actionmenu-divider-width); + height: var(--ti-common-size-3x); + background: var(--ti-common-color-line-dividing); +} diff --git a/src/actionmenu/lib/src/i18n/TiActionmenuWords.ts b/src/actionmenu/lib/src/i18n/TiActionmenuWords.ts new file mode 100644 index 0000000..2c81137 --- /dev/null +++ b/src/actionmenu/lib/src/i18n/TiActionmenuWords.ts @@ -0,0 +1,6 @@ +export interface TiActionmenuWords { + tiActionmenu: { + more: string; + operation: string; + }; +} diff --git a/src/actionmenu/lib/src/i18n/en_US.ts b/src/actionmenu/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..7d7c31e --- /dev/null +++ b/src/actionmenu/lib/src/i18n/en_US.ts @@ -0,0 +1,8 @@ +import { TiActionmenuWords } from './TiActionmenuWords'; + +export const en_US: TiActionmenuWords = { + tiActionmenu: { + more: 'More', + operation: 'Operation' + } +}; diff --git a/src/actionmenu/lib/src/i18n/es_US.ts b/src/actionmenu/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..5dc53af --- /dev/null +++ b/src/actionmenu/lib/src/i18n/es_US.ts @@ -0,0 +1,8 @@ +import { TiActionmenuWords } from './TiActionmenuWords'; + +export const es_US: TiActionmenuWords = { + tiActionmenu: { + more: 'Más', + operation: 'Operación' + } +}; diff --git a/src/actionmenu/lib/src/i18n/fr_FR.ts b/src/actionmenu/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..e8f7b5d --- /dev/null +++ b/src/actionmenu/lib/src/i18n/fr_FR.ts @@ -0,0 +1,8 @@ +import { TiActionmenuWords } from './TiActionmenuWords'; + +export const fr_FR: TiActionmenuWords = { + tiActionmenu: { + more: 'Plus', + operation: 'Opérer' + } +}; diff --git a/src/actionmenu/lib/src/i18n/index.ts b/src/actionmenu/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/actionmenu/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/actionmenu/lib/src/i18n/pt_BR.ts b/src/actionmenu/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..e0ebb44 --- /dev/null +++ b/src/actionmenu/lib/src/i18n/pt_BR.ts @@ -0,0 +1,8 @@ +import { TiActionmenuWords } from './TiActionmenuWords'; + +export const pt_BR: TiActionmenuWords = { + tiActionmenu: { + more: 'Mais', + operation: 'Operação' + } +}; diff --git a/src/actionmenu/lib/src/i18n/zh_CN.ts b/src/actionmenu/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..584ea07 --- /dev/null +++ b/src/actionmenu/lib/src/i18n/zh_CN.ts @@ -0,0 +1,8 @@ +import { TiActionmenuWords } from './TiActionmenuWords'; + +export const zh_CN: TiActionmenuWords = { + tiActionmenu: { + more: '更多', + operation: '操作' + } +}; diff --git a/src/alert/demo/karma.conf.js b/src/alert/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/alert/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/alert/demo/project.json b/src/alert/demo/project.json new file mode 100644 index 0000000..760466d --- /dev/null +++ b/src/alert/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/alert/demo", + "sourceRoot": "src/alert/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/alert", + "index": "src/alert/demo/src/index.html", + "main": "src/alert/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/alert/demo/tsconfig.app.json", + "assets": ["src/alert/demo/src/favicon.ico", "src/alert/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "alert-demo:build:production" + }, + "development": { + "browserTarget": "alert-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js alert" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/alert/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/alert/demo/tsconfig.spec.json", + "karmaConfig": "src/alert/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/alert/demo/src/app/AppComponent.ts b/src/alert/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/alert/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/alert/demo/src/app/AppModule.ts b/src/alert/demo/src/app/AppModule.ts new file mode 100644 index 0000000..665e6f0 --- /dev/null +++ b/src/alert/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { AlertTestModule } from './alert/AlertTestModule'; + +@NgModule({ + imports: [ + AlertTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/alert/demo/src/app/IndexComponent.ts b/src/alert/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..03621a6 --- /dev/null +++ b/src/alert/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { AlertTestModule } from './alert/AlertTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = AlertTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/alert/demo/src/app/alert/AlertBoxshadowComponent.ts b/src/alert/demo/src/app/alert/AlertBoxshadowComponent.ts new file mode 100644 index 0000000..eea3ccd --- /dev/null +++ b/src/alert/demo/src/app/alert/AlertBoxshadowComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './alert-boxshadow.html' +}) +export class AlertBoxshadowComponent { + label: string = '提示文本'; +} diff --git a/src/alert/demo/src/app/alert/AlertDarkthemeComponent.ts b/src/alert/demo/src/app/alert/AlertDarkthemeComponent.ts new file mode 100644 index 0000000..3dedf6c --- /dev/null +++ b/src/alert/demo/src/app/alert/AlertDarkthemeComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './alert-darktheme.html' +}) +export class AlertDarkthemeComponent {} diff --git a/src/alert/demo/src/app/alert/AlertDismissComponent.ts b/src/alert/demo/src/app/alert/AlertDismissComponent.ts new file mode 100644 index 0000000..887fc0f --- /dev/null +++ b/src/alert/demo/src/app/alert/AlertDismissComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './alert-dismiss.html' +}) +export class AlertDismissComponent {} diff --git a/src/alert/demo/src/app/alert/AlertEventComponent.ts b/src/alert/demo/src/app/alert/AlertEventComponent.ts new file mode 100644 index 0000000..3883517 --- /dev/null +++ b/src/alert/demo/src/app/alert/AlertEventComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './alert-event.html' +}) +export class AlertEventComponent { + label: string = '提示文本'; + open: boolean = true; + myLogs: Array = []; + setOpen(): void { + this.open = !this.open; + this.onOpenChange(this.open); + } + onOpenChange(openState: boolean): void { + this.myLogs = [...this.myLogs, `openState: ${openState}`]; + } +} diff --git a/src/alert/demo/src/app/alert/AlertIconComponent.ts b/src/alert/demo/src/app/alert/AlertIconComponent.ts new file mode 100644 index 0000000..cd916db --- /dev/null +++ b/src/alert/demo/src/app/alert/AlertIconComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './alert-icon.html' +}) +export class AlertIconComponent {} diff --git a/src/alert/demo/src/app/alert/AlertMessagesComponent.ts b/src/alert/demo/src/app/alert/AlertMessagesComponent.ts new file mode 100644 index 0000000..e075dfb --- /dev/null +++ b/src/alert/demo/src/app/alert/AlertMessagesComponent.ts @@ -0,0 +1,43 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './alert-messages.html' +}) +export class AlertMessagesComponent { + items1: Array = [ + { + title: '咏柳(默认支持轮播,轮播速度2秒)', + content: '碧玉妆成一树高,万条垂下绿丝绦。不知细叶谁裁出,二月春风似剪刀。' + }, + { + title: '静夜思', + content: '床前明月光,疑是地上霜。举头望明月,低头思故乡。' + }, + { + title: '春晓', + content: '春眠不觉晓,处处闻啼鸟。夜来风雨声,花落知多少。' + }, + { + title: '山行', + content: '远上寒山石径斜,白云深处有人家。停车坐爱枫林晚,霜叶红于二月花。' + } + ]; + items2: Array = [ + { + title: '咏柳(不支持轮播)', + content: '碧玉妆成一树高,万条垂下绿丝绦。不知细叶谁裁出,二月春风似剪刀。' + }, + { + title: '静夜思', + content: '床前明月光,疑是地上霜。举头望明月,低头思故乡。' + }, + { + title: '春晓', + content: '春眠不觉晓,处处闻啼鸟。夜来风雨声,花落知多少。' + }, + { + title: '山行', + content: '远上寒山石径斜,白云深处有人家。停车坐爱枫林晚,霜叶红于二月花。' + } + ]; +} diff --git a/src/alert/demo/src/app/alert/AlertOpenComponent.ts b/src/alert/demo/src/app/alert/AlertOpenComponent.ts new file mode 100644 index 0000000..2fab91b --- /dev/null +++ b/src/alert/demo/src/app/alert/AlertOpenComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './alert-open.html' +}) +export class AlertOpenComponent { + label: string = '提示文本'; + open: boolean = true; + myLogs: Array = []; + setOpen(): void { + this.open = !this.open; + this.onOpenChange(this.open); + } + onOpenChange(openState: boolean): void { + this.myLogs = [...this.myLogs, `openState: ${openState}`]; + } +} diff --git a/src/alert/demo/src/app/alert/AlertOpenTestComponent.ts b/src/alert/demo/src/app/alert/AlertOpenTestComponent.ts new file mode 100644 index 0000000..9e376f7 --- /dev/null +++ b/src/alert/demo/src/app/alert/AlertOpenTestComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './alert-open-test.html' +}) +export class AlertOpenTestComponent { + label: string = '提示文本'; + open: boolean = false; + open1: boolean = true; + dismissOnTimeout: number = 5000; + setOpen(): void { + this.open = !this.open; + console.log(this.open); + } + + setOpen1(): void { + this.open1 = !this.open1; + console.log(this.open1); + } +} diff --git a/src/alert/demo/src/app/alert/AlertSizeComponent.ts b/src/alert/demo/src/app/alert/AlertSizeComponent.ts new file mode 100644 index 0000000..44470be --- /dev/null +++ b/src/alert/demo/src/app/alert/AlertSizeComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './alert-size.html' +}) +export class AlertSizeComponent {} diff --git a/src/alert/demo/src/app/alert/AlertTestModule.ts b/src/alert/demo/src/app/alert/AlertTestModule.ts new file mode 100644 index 0000000..a5d5ab0 --- /dev/null +++ b/src/alert/demo/src/app/alert/AlertTestModule.ts @@ -0,0 +1,81 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiAlertModule, TiButtonModule, TiSelectModule } from '@opentiny/ng'; +// import { TiAlertModule } from '@opentiny/ng-alert'; +// import { TiButtonModule } from '@opentiny/ng-button'; +// import { TiSelectModule } from '@opentiny/ng-select'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { AlertTypeComponent } from './AlertTypeComponent'; +import { AlertDismissComponent } from './AlertDismissComponent'; +import { AlertIconComponent } from './AlertIconComponent'; +import { AlertOpenComponent } from './AlertOpenComponent'; +import { AlertOpenTestComponent } from './AlertOpenTestComponent'; +import { AlertTriggerScrollComponent } from './AlertTriggerScrollComponent'; +import { AlertBoxshadowComponent } from './AlertBoxshadowComponent'; +import { AlertEventComponent } from './AlertEventComponent'; +import { AlertSizeComponent } from './AlertSizeComponent'; +import { AlertDarkthemeComponent } from './AlertDarkthemeComponent'; +import { AlertMessagesComponent } from './AlertMessagesComponent'; + +@NgModule({ + imports: [CommonModule, TiAlertModule, TiSelectModule, TiButtonModule, DemoLogModule, RouterModule.forChild(AlertTestModule.ROUTES)], + declarations: [ + AlertTypeComponent, + AlertDismissComponent, + AlertIconComponent, + AlertOpenComponent, + AlertOpenTestComponent, + AlertTriggerScrollComponent, + AlertBoxshadowComponent, + AlertEventComponent, + AlertSizeComponent, + AlertDarkthemeComponent, + AlertMessagesComponent + ] +}) +export class AlertTestModule { + static readonly LINKS: Array = [{ href: 'components/TiAlertComponent.html', label: 'Alert' }]; + static readonly ROUTES: Routes = [ + { + path: 'alert/alert-type', + component: AlertTypeComponent + }, + { + path: 'alert/alert-darktheme', + component: AlertDarkthemeComponent + }, + { + path: 'alert/alert-dismiss', + component: AlertDismissComponent + }, + { + path: 'alert/alert-icon', + component: AlertIconComponent + }, + { + path: 'alert/alert-open', + component: AlertOpenComponent + }, + { + path: 'alert/alert-event', + component: AlertEventComponent + }, + { + path: 'alert/alert-trigger-scroll', + component: AlertTriggerScrollComponent + }, + { + path: 'alert/alert-size', + component: AlertSizeComponent + }, + { + path: 'alert/alert-messages', + component: AlertMessagesComponent + }, + { path: 'alert/alert-boxshadow', component: AlertBoxshadowComponent }, + { path: 'alert/alert-open-test', component: AlertOpenTestComponent } + ]; +} diff --git a/src/alert/demo/src/app/alert/AlertTriggerScrollComponent.ts b/src/alert/demo/src/app/alert/AlertTriggerScrollComponent.ts new file mode 100644 index 0000000..a79518d --- /dev/null +++ b/src/alert/demo/src/app/alert/AlertTriggerScrollComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './alert-trigger-scroll.html' +}) +export class AlertTriggerScrollComponent {} diff --git a/src/alert/demo/src/app/alert/AlertTypeComponent.ts b/src/alert/demo/src/app/alert/AlertTypeComponent.ts new file mode 100644 index 0000000..2a239f1 --- /dev/null +++ b/src/alert/demo/src/app/alert/AlertTypeComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './alert-type.html' +}) +export class AlertTypeComponent {} diff --git a/src/alert/demo/src/app/alert/alert-boxshadow.html b/src/alert/demo/src/app/alert/alert-boxshadow.html new file mode 100644 index 0000000..6f681e8 --- /dev/null +++ b/src/alert/demo/src/app/alert/alert-boxshadow.html @@ -0,0 +1,11 @@ +

描述

+

+ 通过设置boxShadow可以为alert组件添加或者去除阴影效果。
+ 默认值为false,无阴影效果,设置boxShadow为true,则添加阴影效果。
+ 验证方法:可以打开控制台查看样式差异。有阴影效果时,ti-alert标签下的第一个div带有.ti3-alert-box-shadow样式;反之,则没有该样式。 +

+

示例

+

1、无阴影效果

+{{label}}
+

2、有阴影效果

+{{label}} diff --git a/src/alert/demo/src/app/alert/alert-darktheme.html b/src/alert/demo/src/app/alert/alert-darktheme.html new file mode 100644 index 0000000..feeac23 --- /dev/null +++ b/src/alert/demo/src/app/alert/alert-darktheme.html @@ -0,0 +1,7 @@ +success类型 +
+prompt类型 +
+warn类型 +
+error类型 diff --git a/src/alert/demo/src/app/alert/alert-dismiss.html b/src/alert/demo/src/app/alert/alert-dismiss.html new file mode 100644 index 0000000..df27c24 --- /dev/null +++ b/src/alert/demo/src/app/alert/alert-dismiss.html @@ -0,0 +1 @@ +5000ms后自动消失 diff --git a/src/alert/demo/src/app/alert/alert-event.html b/src/alert/demo/src/app/alert/alert-event.html new file mode 100644 index 0000000..24deb0e --- /dev/null +++ b/src/alert/demo/src/app/alert/alert-event.html @@ -0,0 +1,4 @@ +{{label}} +
+ + diff --git a/src/alert/demo/src/app/alert/alert-icon.html b/src/alert/demo/src/app/alert/alert-icon.html new file mode 100644 index 0000000..b367310 --- /dev/null +++ b/src/alert/demo/src/app/alert/alert-icon.html @@ -0,0 +1,3 @@ +不显示类型图标 +
+不显示关闭图标 diff --git a/src/alert/demo/src/app/alert/alert-messages.html b/src/alert/demo/src/app/alert/alert-messages.html new file mode 100644 index 0000000..d197d13 --- /dev/null +++ b/src/alert/demo/src/app/alert/alert-messages.html @@ -0,0 +1,13 @@ + + +

{{item.title}}

+ {{item.content}} +
+
+
+ + +

{{item.title}}

+ {{item.content}} +
+
diff --git a/src/alert/demo/src/app/alert/alert-open-test.html b/src/alert/demo/src/app/alert/alert-open-test.html new file mode 100644 index 0000000..85bbcdc --- /dev/null +++ b/src/alert/demo/src/app/alert/alert-open-test.html @@ -0,0 +1,12 @@ +

描述

+

通过动态修改open属性切换alert显示状态,需要注意的是:open属性要使用双向绑定,以确保自动消失场景下的open状态感知

+

示例

+

超时状态(5000ms后自动消失)下切换open状态,用于测试超时状态下open状态切换可否正常进行:

+{{label}} +

控制显示/隐藏:

+ + +

切换open状态,用于测试open状态的设置是否OK:

+{{label}} +

控制显示/隐藏:

+ diff --git a/src/alert/demo/src/app/alert/alert-open.html b/src/alert/demo/src/app/alert/alert-open.html new file mode 100644 index 0000000..2f7622d --- /dev/null +++ b/src/alert/demo/src/app/alert/alert-open.html @@ -0,0 +1,4 @@ +{{label}} +
+ + diff --git a/src/alert/demo/src/app/alert/alert-size.html b/src/alert/demo/src/app/alert/alert-size.html new file mode 100644 index 0000000..a1a6042 --- /dev/null +++ b/src/alert/demo/src/app/alert/alert-size.html @@ -0,0 +1,3 @@ +normal +
+small diff --git a/src/alert/demo/src/app/alert/alert-trigger-scroll.html b/src/alert/demo/src/app/alert/alert-trigger-scroll.html new file mode 100644 index 0000000..c474c74 --- /dev/null +++ b/src/alert/demo/src/app/alert/alert-trigger-scroll.html @@ -0,0 +1,3 @@ +5000ms后自动消失 +
+ diff --git a/src/alert/demo/src/app/alert/alert-type.html b/src/alert/demo/src/app/alert/alert-type.html new file mode 100644 index 0000000..e473952 --- /dev/null +++ b/src/alert/demo/src/app/alert/alert-type.html @@ -0,0 +1,9 @@ +success(默认)类型 +
+prompt类型 +
+warn类型 +
+error类型 +
+simple类型 diff --git a/src/alert/demo/src/app/alert/webdoc/alert-demos.js b/src/alert/demo/src/app/alert/webdoc/alert-demos.js new file mode 100644 index 0000000..9bb4176 --- /dev/null +++ b/src/alert/demo/src/app/alert/webdoc/alert-demos.js @@ -0,0 +1,117 @@ +export default { + column: '2', + demos: [ + { + demoId: 'alert-type', + name: { + 'zh-CN': 'alert告警类型', + 'en-US': 'alert type', + }, + desc: { + 'zh-CN': + '

通过属性type配置 alert 警告的不同类型的外观,包括success(默认)、promptwarnerrorsimple五种类型。

', + 'en-US': '

alert type description

', + }, + apis: ['TiAlertComponent.properties.type'], + }, + { + demoId: 'alert-darktheme', + name: { + 'zh-CN': '深色主题', + 'en-US': 'darkTheme', + }, + desc: { + 'zh-CN': + '

通过属性darkTheme配置 alert 警告的深色样式。successprompt类型默认的消失时间是 5000ms,warnerror类型默认的消失时间是 10000ms。

', + 'en-US': '

darkTheme

', + }, + apis: ['TiAlertComponent.properties.darkTheme'], + }, + { + demoId: 'alert-dismiss', + name: { + 'zh-CN': '5000ms 后消失', + 'en-US': 'dismissOnTimeout', + }, + desc: { + 'zh-CN': + '

通过属性dismissOnTimeout配置 alert 警告经过 5000ms 时间后自动消失。

', + 'en-US': '

dismissOnTimeout

', + }, + apis: ['TiAlertComponent.properties.dismissOnTimeout'], + }, + { + demoId: 'alert-icon', + name: { + 'zh-CN': '图标隐藏', + 'en-US': 'iconhidden', + }, + desc: { + 'zh-CN': + '

通过属性typeIcon配置类型图标是否显示。通过属性closeIcon配置关闭图标是否显示。

', + 'en-US': '

icon

', + }, + apis: [ + 'TiAlertComponent.properties.typeIcon', + 'TiAlertComponent.properties.closeIcon', + ], + }, + { + demoId: 'alert-open', + name: { + 'zh-CN': '控制 alert 警告的显示与隐藏', + 'en-US': 'open', + }, + desc: { + 'zh-CN': + '

通过属性open配置 alert 警告的显示与隐藏状态。当 alert 的open属性改变的时候触发openChange事件。

', + 'en-US': '

open

', + }, + apis: [ + 'TiAlertComponent.properties.open', + 'TiAlertComponent.events.openChange', + ], + }, + { + demoId: 'alert-size', + name: { + 'zh-CN': '大小', + 'en-US': 'size', + }, + desc: { + 'zh-CN': '

通过属性size配置 alert 警告显示的大小。

', + 'en-US': '

size

', + }, + apis: ['TiAlertComponent.properties.size'], + }, + { + demoId: 'alert-messages', + name: { + 'zh-CN': '轮播', + 'en-US': 'autoplay', + }, + desc: { + 'zh-CN': + '

通过属性autoplay配置 alert 警告是否轮播。通过属性autoplaySpeed配置 alert 警告轮播的速度。仅支持promptsimple两种类型。

', + 'en-US': '

autoplay

', + }, + apis: [ + 'TiAlertComponent.properties.autoplay', + 'TiAlertComponent.properties.autoplaySpeed', + ], + }, + { + demoId: 'alert-trigger-scroll', + name: { + 'zh-CN': 'alert 消失时下拉面板收起', + 'en-US': 'triggerScroll', + }, + desc: { + 'zh-CN': + '

通过属性triggerScroll配置 alert 警告消失时下拉面板收起。

', + 'en-US': '

triggerScroll

', + }, + apis: ['TiAlertComponent.properties.triggerScroll'], + }, + ], +}; diff --git a/src/alert/demo/src/app/alert/webdoc/alert.cn.md b/src/alert/demo/src/app/alert/webdoc/alert.cn.md new file mode 100644 index 0000000..973de62 --- /dev/null +++ b/src/alert/demo/src/app/alert/webdoc/alert.cn.md @@ -0,0 +1,27 @@ +--- +title: Alert 警告 +--- +# Alert 警告 + +
+ +Alert 是警告提示,展现用户需关注的信息的组件。   + ++ 支持改变背景颜色、隐藏icon、改变大小、轮播等场景。 + +```typescript +import { TiAlertModule } from '@opentiny/ng'; +``` + +
+ +
+ +Alert 是警告提示,展现用户需关注的信息的组件。   + ++ 支持改变背景颜色、隐藏icon、改变大小、轮播等场景。 + +```typescript +import { TiAlertModule } from '@opentiny/ng'; +``` +
diff --git a/src/alert/demo/src/app/alert/webdoc/alert.en.md b/src/alert/demo/src/app/alert/webdoc/alert.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/alert/demo/src/app/alert/webdoc/alert.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/alert/demo/src/app/app.html b/src/alert/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/alert/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/alert/demo/src/favicon.ico b/src/alert/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/alert/demo/src/index.html b/src/alert/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/alert/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/alert/demo/src/main.ts b/src/alert/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/alert/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/alert/demo/test.ts b/src/alert/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/alert/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/alert/demo/tsconfig.app.json b/src/alert/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/alert/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/alert/demo/tsconfig.spec.json b/src/alert/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/alert/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/alert/lib/index.ts b/src/alert/lib/index.ts new file mode 100644 index 0000000..a4b69bc --- /dev/null +++ b/src/alert/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiAlertModule'; diff --git a/src/alert/lib/ng-package.json b/src/alert/lib/ng-package.json new file mode 100644 index 0000000..adf546c --- /dev/null +++ b/src/alert/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/alert", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/alert/lib/package.json b/src/alert/lib/package.json new file mode 100644 index 0000000..ec0436c --- /dev/null +++ b/src/alert/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-alert", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@angular/animations": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/alert/lib/project.json b/src/alert/lib/project.json new file mode 100644 index 0000000..951d92a --- /dev/null +++ b/src/alert/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/alert/lib", + "sourceRoot": "src/alert/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/alert"], + "options": { + "project": "src/alert/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/alert"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js alert" + }, + { + "command": "ng default-build alert" + }, + { + "command": "node build/clear-default-theme.js alert" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/alert && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build alert && ng pack alert && node build/publish.js alert --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/alert/lib/src/TiAlertComponent.ts b/src/alert/lib/src/TiAlertComponent.ts new file mode 100644 index 0000000..1069a43 --- /dev/null +++ b/src/alert/lib/src/TiAlertComponent.ts @@ -0,0 +1,348 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + EventEmitter, + Input, + Output, + ElementRef, + Renderer2, + Inject, + ChangeDetectionStrategy, + ChangeDetectorRef, + ViewChild, + ContentChild, + ContentChildren, + QueryList +} from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { Util } from '@opentiny/ng-utils'; +import { TiAlertMessageComponent } from './TiAlertMessageComponent'; +import { animate, style, transition, trigger } from '@angular/animations'; +import packageInfo from '../package.json'; + +/** + * Alert组件用于消息提示,提供了四种类型 + * + *

10.1.13版本之后,使用此组件时需要开发者在项目模块(建议在根模块) + * 中引入BrowserAnimationsModule。这是因为此组件中使用了Angular动画,需要引入BrowserAnimationsModule, + * 但是 BrowserAnimationsModule 不能在懒加载模块被重复引入,所以需要开发者来引入BrowserAnimationsModule,保证其引入一次。

+ * + */ +@Component({ + selector: 'ti-alert', + templateUrl: './alert.html', + styleUrls: ['./alert.less'], + animations: [ + trigger('alertAnimate', [ + transition(':leave', [ + style({ opacity: 1, transform: 'scaleY(1)', transformOrigin: '0% 0%' }), + animate( + '0.3s cubic-bezier(0.42, 0, 1, 1)', + style({ + opacity: 0, + transform: 'scaleY(0)', + transformOrigin: '0% 0%' + }) + ) + ]) + ]) + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiAlertComponent extends TiBaseComponent { + /** + * 是否显示左侧提示类型图标 + */ + @Input() typeIcon: boolean = true; + /** + * 是否显示关闭按钮 + */ + @Input() closeIcon: boolean = true; + /** + * 自动消失时间 + */ + @Input() dismissOnTimeout: number; + /** + * 是否触发下拉类组件消失的事件,适用于 alert 的呈现状态影响页面下拉类组件位置的场景 + */ + @Input() triggerScroll: boolean = false; + /** + * 弹框类型 + */ + @Input() type: 'success' | 'error' | 'warn' | 'prompt' | 'simple' = 'success'; + /** + * 样式大小 + */ + @Input() size: 'normal' | 'small' = 'normal'; + private isOpen: boolean = true; + /** + * 是否为深色背景提示框 + */ + @Input() darkTheme: boolean = false; + /** + * @ignore + * 是否设置阴影,组件规范定义无阴影(页面级及卡片内) + */ + @Input() boxShadow: boolean = false; + /** + * 是否弹出 + * + */ + @Input() + // 由于内部/外部设置open都会执行同样的逻辑,因此使用set/get + /** + * @ignore + */ + get open(): boolean { + return this.isOpen; + } + /** + * @ignore + */ + set open(value: boolean) { + if (value === this.open) { + return; + } + if (value) { + // 值为true时,才会延迟关闭 + this.dismissOnTime(); + } else { + // 值为false时,关闭时清除定时器 + this.clearTimeout(); + } + // open动态修改均涉及tiScroll事件 + this.triggerScrollEvent(); + this.isOpen = value; + } + /** + * + * + * 是否自动轮播 + */ + @Input() autoplay: boolean = true; + /** + * + * 自动轮播的速度 + */ + @Input() autoplaySpeed: number = 3000; + // 动画间隔,单位ms + private transitionSpeed: number = 600; + /** + * 点击关闭按钮或延时消失时触发的回调,一般用于 open 状态变化场景。 + */ + @Output() readonly openChange: EventEmitter = new EventEmitter(); + private timeoutId: any; // 内部变量,存储定时器ID + private timer: any; + /** + * @ignore + */ + @ContentChildren(TiAlertMessageComponent) + alertMessageComs: QueryList; + /** + * @ignore + * 激活项下标 + */ + public activeIndex: number = 0; + protected versionInfo: string = super.getVersion(packageInfo); + /** + * @ignore + */ + @ViewChild('label', { static: false }) labelEle: ElementRef; + /** + * @ignore + */ + @ViewChild('message', { static: false }) messageEle: ElementRef; + /** + * @ignore + */ + @ViewChild('container', { static: false }) containerEle: ElementRef; + private totalPage: number; // 总轮播数 + private totleHeight: number; // 所有轮播提示的总高 + private firstEle: Element; // 第一个ti-alert-message + private messageNav: Element; + private messageHeightArr: Array = []; // 每个message的高 + // 提示类型图标宽度16 + 右侧margin 8 + private static readonly TYPEICON_WIDTH: number = 16 + 8; + // 关闭图标大小12 + 左侧margin16 + private static readonly CLOSEICON_WIDTH: number = 16 + 12; + constructor( + protected hostRef: ElementRef, + protected renderer: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + @Inject(DOCUMENT) private document + ) { + super(hostRef, renderer); + } + + ngOnInit(): void { + super.ngOnInit(); + // Div层图标和关闭按钮必须呈现 + if (this.darkTheme) { + this.closeIcon = true; + this.typeIcon = true; + this.dismissOnTimeout = this.type === 'success' || this.type === 'prompt' ? 5 * 1000 : 10 * 1000; + } + + // 初始open值为true时,设置延时关闭 + if (this.open) { + this.dismissOnTime(); + } + } + ngAfterViewInit(): void { + super.ngAfterViewInit(); + this.totalPage = this.alertMessageComs.length; + if (this.totalPage > 0) { + this.messageNav = this.messageEle?.nativeElement; + let iconWidth: number = 0; + // 有提示图标,simple类型没有图标 + if (this.typeIcon && this.type === 'prompt') { + iconWidth += TiAlertComponent.TYPEICON_WIDTH; + } + + // 有关闭图标 + if (this.closeIcon) { + iconWidth += TiAlertComponent.CLOSEICON_WIDTH; + } + + // 12px为翻页按钮左侧padding + 翻页按钮总宽(2 * ${this.totalPage} - 1) * 8px + const width: string = `calc(100% - (${iconWidth}px + 12px + (2 * ${this.totalPage} - 1) * 8px))`; + this.renderer.setStyle(this.labelEle.nativeElement, 'width', width); + this.renderer.setStyle(this.messageNav, 'transition', `top ${this.transitionSpeed}ms`); + this.alertMessageComs.forEach((element: TiAlertMessageComponent) => { + this.messageHeightArr.push(element.nativeElement.offsetHeight); + }); + this.totleHeight = this.messageHeightArr.reduce((totle: number, value: number) => totle + value); + this.setLabelHeight(this.activeIndex); + if (this.autoplay && !this.timer) { + this.firstEle = this.alertMessageComs.first.nativeElement; + this.startAutoplay(); + this.renderer.listen(this.containerEle.nativeElement, 'mouseenter', () => { + this.stopAutoplay(); + }); + + this.renderer.listen(this.containerEle.nativeElement, 'mouseleave', () => { + this.startAutoplay(); + }); + } + } + } + + ngOnDestroy(): void { + if (this.timeoutId !== undefined) { + clearTimeout(this.timeoutId); + if (this.open) { + this.close(); + } + } + this.stopAutoplay(); + } + + /** + * @ignore + * alert呈现后,处理多长时间后消失 + */ + dismissOnTime(): void { + // 未配置消失时间或当前已处于隐藏状态,则不做处理 + if (isNaN(this.dismissOnTimeout)) { + return; + } + this.timeoutId = setTimeout(() => { + this.close(); + // OnPush模式下,异步需要手动刷新。 + this.changeDetectorRef.markForCheck(); + }, this.dismissOnTimeout); + } + /** + * @ignore + * 关闭alert方法,供内部关闭调用 + */ + close(): void { + this.open = false; + this.openChange.emit(this.open); // 及时通知open状态,确保open状态双向绑定功能正常 + } + + /** + * @ignore + * alert出现或者消失时,可能会触发页面滚动,此时需要触发tiScroll事件通知相关组件 + */ + triggerScrollEvent(): void { + if (!this.triggerScroll) { + return; + } + // 这里为什么需要setTimeout?能否去除? + // setTimeout(() => { + Util.trigger(this.document, 'tiScroll'); + // }, 0); + } + + private clearTimeout(): void { + if (this.timeoutId !== undefined) { + clearTimeout(this.timeoutId); + } + } + /** + * @ignore + * + */ + onPageClick(index: number): void { + if (index === this.activeIndex) { + return; + } + + this.activeIndex = index; + this.setLabelHeight(this.activeIndex); + } + // 根据当前激活项计算当前高度,赋值给文本呈现区域 + private setLabelHeight(index: number): void { + const height: number = this.messageHeightArr[index]; // 当前激活页高度 + this.renderer.setStyle(this.messageNav, 'height', height + 'px'); // 设置高度 + + const heightArr: Array = this.messageHeightArr.concat(); + const top: string = index === 0 ? '0' : -heightArr.splice(0, index).reduce((totle: number, val: number) => totle + val) + 'px'; + this.renderer.setStyle(this.messageNav, 'top', top); + } + + private startAutoplay(): void { + this.timer = setInterval((): void => { + this.activeMessage(this.activeIndex + 1); + this.changeDetectorRef.markForCheck(); + }, this.autoplaySpeed); + } + private activeMessage(index: number): void { + this.renderer.setStyle(this.messageNav, 'transition', `top ${this.transitionSpeed}ms`); + // 最后一页到第一页 + if (index === this.totalPage) { + this.activeIndex = 0; // 设置当前页为第一页 + this.renderer.setStyle(this.firstEle, 'transform', `translateY(${this.totleHeight}px)`); // 调整第一页的位置 + this.renderer.setStyle(this.messageNav, 'top', `${-this.totleHeight}px`); + this.renderer.setStyle(this.messageNav, 'height', `${this.messageHeightArr[0]}px`); + + setTimeout((): void => { + this.renderer.removeStyle(this.firstEle, 'transform'); + this.renderer.removeStyle(this.messageNav, 'transition'); + this.setLabelHeight(this.activeIndex); + }, this.transitionSpeed); + } else { + this.activeIndex = index; + this.setLabelHeight(this.activeIndex); + } + } + + private stopAutoplay(): void { + if (this.timer) { + clearInterval(this.timer); + this.timer = undefined; + } + } +} diff --git a/src/alert/lib/src/TiAlertMessageComponent.ts b/src/alert/lib/src/TiAlertMessageComponent.ts new file mode 100644 index 0000000..aef02b8 --- /dev/null +++ b/src/alert/lib/src/TiAlertMessageComponent.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; + +@Component({ + selector: 'ti-alert-message', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiAlertMessageComponent extends TiBaseComponent {} diff --git a/src/alert/lib/src/TiAlertModule.ts b/src/alert/lib/src/TiAlertModule.ts new file mode 100644 index 0000000..1cc3263 --- /dev/null +++ b/src/alert/lib/src/TiAlertModule.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiAlertComponent } from './TiAlertComponent'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiAlertMessageComponent } from './TiAlertMessageComponent'; + +@NgModule({ + imports: [CommonModule, TiIconModule, TiOutlineModule], + exports: [TiAlertComponent, TiAlertMessageComponent], + declarations: [TiAlertComponent, TiAlertMessageComponent] +}) +export class TiAlertModule {} +export { TiAlertComponent } from './TiAlertComponent'; +export { TiAlertMessageComponent } from './TiAlertMessageComponent'; diff --git a/src/alert/lib/src/alert.html b/src/alert/lib/src/alert.html new file mode 100644 index 0000000..1f0be94 --- /dev/null +++ b/src/alert/lib/src/alert.html @@ -0,0 +1,107 @@ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
diff --git a/src/alert/lib/src/alert.less b/src/alert/lib/src/alert.less new file mode 100644 index 0000000..414284d --- /dev/null +++ b/src/alert/lib/src/alert.less @@ -0,0 +1,228 @@ +@import '../../../themes/basic/mixins.less'; + +:host { + --ti-alert-container-width: 400px; + --ti-alert-padding-horizontal: var(--ti-common-space-4x); + --ti-alert-padding-vertical: var(--ti-common-space-3x); + --ti-alert-small-padding-vertical: var(--ti-common-space-2x); + --ti-alert-type-icon-margin-right: var(--ti-common-space-2x); + --ti-alert-type-icon-size: var(--ti-common-size-4x); + --ti-alert-close-icon-size: var(--ti-common-size-3x); + --ti-alert-close-icon-margin-left: var(--ti-common-space-4x); +} + +:host { + display: block; + width: var(--ti-alert-container-width); +} + +.ti3-icon { + font-size: calc(var(--ti-alert-type-icon-size) * 0.75); +} + +.ti3-alert-container { + padding: var(--ti-alert-padding-vertical) var(--ti-alert-padding-horizontal); + .box-sizing(border-box); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid); + border-radius: var(--ti-common-border-radius-normal); + width: 100%; + font-weight: normal; + display: flex; + align-items: center; + &.ti3-alert-dark-theme { + background: var(--ti-common-color-bg-dark-deep); + border: none; + .ti3-alert-label { + color: var(--ti-common-color-text-gray); + } + .ti3-alert-close-icon { + color: var(--ti-common-color-icon-darkbg-normal); + } + } + + &.ti3-alert-size-small { + padding: var(--ti-alert-small-padding-vertical) var(--ti-alert-padding-horizontal); + } +} + +.ti3-alert-error-container { + background: var(--ti-common-color-error-bg); + border-color: var(--ti-common-color-error-border-secondary); + & .ti3-alert-circle { + background: var(--ti-common-color-error); + } +} + +.ti3-alert-prompt-container { + background: var(--ti-common-color-prompt-bg); + border-color: var(--ti-common-color-prompt-border); + & .ti3-alert-circle { + background: var(--ti-common-color-prompt); + } +} + +// alert-messages布局页头对齐 +.ti-alert-message-container { + align-items: flex-start; +} + +.ti3-alert-warn-container { + background: var(--ti-common-color-warn-bg); + border-color: var(--ti-common-color-warn-border); +} + +.ti3-alert-success-container { + background: var(--ti-common-color-success-bg); + border-color: var(--ti-common-color-success-border); + & .ti3-alert-circle { + background: var(--ti-common-color-success); + } +} + +.ti3-alert-simple-container { + border-color: var(--ti-common-color-line-normal); +} + +.ti3-alert-success-container.ti3-alert-box-shadow { + .box-shadow(var(--ti-common-shadow-success)); +} +.ti3-alert-error-container.ti3-alert-box-shadow { + .box-shadow(var(--ti-common-shadow-error)); +} +.ti3-alert-prompt-container.ti3-alert-box-shadow { + .box-shadow(var(--ti-common-shadow-prompt)); +} +.ti3-alert-warn-container.ti3-alert-box-shadow { + .box-shadow(var(--ti-common-shadow-warn)); +} +.ti3-alert-dark-theme.ti3-alert-box-shadow { + .box-shadow(var(--ti-common-shadow-3-down)); +} + +// alert字体图标有两部分组成:圆圈和字体图标,此处为圆圈样式 +.ti3-bg-circle(@width) { + display: inline-block; + width: @width; + height: @width; + border-radius: 50%; + text-align: center; + line-height: @width; +} + +.ti3-alert-icon { + line-height: var(--ti-alert-type-icon-size); +} + +.ti3-alert-circle { + .ti3-bg-circle(var(--ti-alert-type-icon-size)); + color: var(--ti-common-color-icon-white); + margin-right: var(--ti-alert-type-icon-margin-right); +} + +.ti3-alert-warn-bg { + height: var(--ti-alert-type-icon-size); + width: var(--ti-alert-type-icon-size); + line-height: var(--ti-alert-type-icon-size); + font-size: var(--ti-alert-type-icon-size); + display: inline-block; + color: var(--ti-common-color-warn); + position: relative; + margin-right: var(--ti-alert-type-icon-margin-right); +} + +.ti3-alert-warn-icon { + height: var(--ti-alert-type-icon-size); + position: absolute; + left: calc((var(--ti-alert-type-icon-size) - var(--ti-alert-type-icon-size) * 0.75) / 2); + bottom: -1px; + color: var(--ti-common-color-icon-white); +} + +.ti3-alert-label { + // 修改该行样式时需要同步修改ts中的变量TYPEICON_WIDTH和CLOSEICON_WIDTH + width: calc( + 100% - var(--ti-alert-type-icon-size) - var(--ti-alert-close-icon-size) - var(--ti-alert-type-icon-margin-right) - + var(--ti-alert-close-icon-margin-left) + ); + display: inline-block; + line-height: var(--ti-common-line-height-number); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + word-wrap: break-word; + overflow-wrap: break-word; + color: var(--ti-common-color-text-secondary); + + &.ti3-alert-label-with-closeIcon { + width: calc(100% - var(--ti-alert-close-icon-size) - var(--ti-alert-close-icon-margin-left)); + } + + &.ti3-alert-label-with-typeIcon { + width: calc(100% - var(--ti-alert-type-icon-size) - var(--ti-alert-type-icon-margin-right)); + } + + &.ti3-alert-only-label { + width: 100%; + } +} + +.ti3-alert-close-icon { + display: inline-block; + width: var(--ti-alert-close-icon-size); + font-size: var(--ti-alert-close-icon-size); + vertical-align: top; + margin-left: var(--ti-alert-close-icon-margin-left); + cursor: pointer; + line-height: var(--ti-common-line-height-number); +} + +.ti3-alert-error-container .ti3-alert-close-icon { + color: var(--ti-common-color-error); +} + +.ti3-alert-prompt-container .ti3-alert-close-icon { + color: var(--ti-common-color-prompt); +} + +.ti3-alert-success-container .ti3-alert-close-icon { + color: var(--ti-common-color-success); +} + +.ti3-alert-warn-container .ti3-alert-close-icon { + color: var(--ti-common-color-warn); +} + +.ti3-alert-simple-container .ti3-alert-close-icon { + color: var(--ti-common-color-icon-normal); +} + +.ti3-alert-message { + display: flex; + flex-direction: column; + position: relative; +} + +.ti3-alert-message-page-container { + display: inline-block; + flex-shrink: 0; + margin-left: var(--ti-common-space-3x); +} + +.ti3-alert-message-page { + display: inline-block; + width: var(--ti-common-space-2x); + height: var(--ti-common-space-2x); + margin-left: var(--ti-common-space-2x); + border-radius: 50%; + border: 1px solid var(--ti-common-color-line-active); + box-sizing: border-box; + &:first-child { + margin-left: 0; + } + &:hover { + cursor: pointer; + } +} + +.ti3-alert-message-page-active { + background-color: var(--ti-common-color-line-active); +} diff --git a/src/anchor/demo/karma.conf.js b/src/anchor/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/anchor/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/anchor/demo/project.json b/src/anchor/demo/project.json new file mode 100644 index 0000000..7818862 --- /dev/null +++ b/src/anchor/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/anchor/demo", + "sourceRoot": "src/anchor/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/anchor", + "index": "src/anchor/demo/src/index.html", + "main": "src/anchor/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/anchor/demo/tsconfig.app.json", + "assets": ["src/anchor/demo/src/favicon.ico", "src/anchor/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "anchor-demo:build:production" + }, + "development": { + "browserTarget": "anchor-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js anchor" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/anchor/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/anchor/demo/tsconfig.spec.json", + "karmaConfig": "src/anchor/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/anchor/demo/src/app/AppComponent.ts b/src/anchor/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/anchor/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/anchor/demo/src/app/AppModule.ts b/src/anchor/demo/src/app/AppModule.ts new file mode 100644 index 0000000..bb4c7e8 --- /dev/null +++ b/src/anchor/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { AnchorTestModule } from './anchor/AnchorTestModule'; + +@NgModule({ + imports: [ + AnchorTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/anchor/demo/src/app/IndexComponent.ts b/src/anchor/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..e17c08c --- /dev/null +++ b/src/anchor/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { AnchorTestModule } from './anchor/AnchorTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = AnchorTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/anchor/demo/src/app/anchor/AnchorBasicComponent.ts b/src/anchor/demo/src/app/anchor/AnchorBasicComponent.ts new file mode 100644 index 0000000..c0205ab --- /dev/null +++ b/src/anchor/demo/src/app/anchor/AnchorBasicComponent.ts @@ -0,0 +1,38 @@ +import { Component } from '@angular/core'; +import { TiAnchorItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './anchor-basic.html' +}) +export class AnchorBasicComponent { + items: Array = [ + { + id: 'basic-1', + title: '概览' + }, + { + id: 'basic-2', + title: 'Pod列表' + }, + { + id: 'basic-3', + title: '容器配置' + }, + { + id: 'basic-4', + title: '访问配置' + }, + { + id: 'basic-5', + title: '配置Pod' + }, + { + id: 'basic-6', + title: '事件' + }, + { + id: 'basic-7', + title: '历史版本' + } + ]; +} diff --git a/src/anchor/demo/src/app/anchor/AnchorEventsComponent.ts b/src/anchor/demo/src/app/anchor/AnchorEventsComponent.ts new file mode 100644 index 0000000..c1dbff5 --- /dev/null +++ b/src/anchor/demo/src/app/anchor/AnchorEventsComponent.ts @@ -0,0 +1,43 @@ +import { Component } from '@angular/core'; +import { TiAnchorItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './anchor-events.html' +}) +export class AnchorEventsComponent { + myLogs: Array = []; + items: Array = [ + { + id: 'events-1', + title: '概览' + }, + { + id: 'events-2', + title: 'Pod列表' + }, + { + id: 'events-3', + title: '容器配置' + }, + { + id: 'events-4', + title: '访问配置' + }, + { + id: 'events-5', + title: '配置Pod' + }, + { + id: 'events-6', + title: '事件' + }, + { + id: 'events-7', + title: '历史版本' + } + ]; + + onSelect(item: TiAnchorItem): void { + this.myLogs = [...this.myLogs, `onSelect() item ${JSON.stringify(item)}`]; + } +} diff --git a/src/anchor/demo/src/app/anchor/AnchorIdComponent.ts b/src/anchor/demo/src/app/anchor/AnchorIdComponent.ts new file mode 100644 index 0000000..58cb506 --- /dev/null +++ b/src/anchor/demo/src/app/anchor/AnchorIdComponent.ts @@ -0,0 +1,44 @@ +import { Component } from '@angular/core'; +import { TiAnchorItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './anchor-id.html' +}) +export class AnchorIdComponent { + myLogs: Array = []; + anchorId: string = 'id-4'; + items: Array = [ + { + id: 'id-1', + title: '概览' + }, + { + id: 'id-2', + title: 'Pod列表' + }, + { + id: 'id-3', + title: '容器配置' + }, + { + id: 'id-4', + title: '访问配置' + }, + { + id: 'id-5', + title: '配置Pod' + }, + { + id: 'id-6', + title: '事件' + }, + { + id: 'id-7', + title: '历史版本' + } + ]; + + onAnchorIdChange(id: string): void { + this.myLogs = [...this.myLogs, `onAnchorIdChange() id = ${id}`]; + } +} diff --git a/src/anchor/demo/src/app/anchor/AnchorItemsComponent.ts b/src/anchor/demo/src/app/anchor/AnchorItemsComponent.ts new file mode 100644 index 0000000..2a160cf --- /dev/null +++ b/src/anchor/demo/src/app/anchor/AnchorItemsComponent.ts @@ -0,0 +1,33 @@ +import { Component, ChangeDetectorRef } from '@angular/core'; +import { TiAnchorItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './anchor-items.html', + styleUrls: ['./anchortest.less'] +}) +export class AnchorItemsComponent { + constructor(private changeDetectorRef: ChangeDetectorRef) {} + items: Array = []; + items1: Array = [ + { + id: 'id1', + title: '概览' + }, + { + id: 'id2', + title: 'Pod列表' + }, + { + id: 'id3', + title: '容器配置' + } + ]; + ngOnInit(): void { + // 模拟数据更新 + setTimeout(() => { + this.items = this.items1; + // onpush模式使用setTimeout需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + }, 2000); + } +} diff --git a/src/anchor/demo/src/app/anchor/AnchorOffsettopComponent.ts b/src/anchor/demo/src/app/anchor/AnchorOffsettopComponent.ts new file mode 100644 index 0000000..183b6cb --- /dev/null +++ b/src/anchor/demo/src/app/anchor/AnchorOffsettopComponent.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; +import { TiAnchorItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './anchor-offsettop.html' +}) +export class AnchorOffsettopComponent { + offsetTop: number = 0; + items: Array = [ + { + id: 'offsettop-1', + title: '概览' + }, + { + id: 'offsettop-2', + title: 'Pod列表' + }, + { + id: 'offsettop-3', + title: '容器配置' + }, + { + id: 'offsettop-4', + title: '访问配置' + }, + { + id: 'offsettop-5', + title: '配置Pod' + }, + { + id: 'offsettop-6', + title: '事件' + }, + { + id: 'offsettop-7', + title: '历史版本' + } + ]; +} diff --git a/src/anchor/demo/src/app/anchor/AnchorScrolltargetComponent.ts b/src/anchor/demo/src/app/anchor/AnchorScrolltargetComponent.ts new file mode 100644 index 0000000..313d349 --- /dev/null +++ b/src/anchor/demo/src/app/anchor/AnchorScrolltargetComponent.ts @@ -0,0 +1,38 @@ +import { Component } from '@angular/core'; +import { TiAnchorItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './anchor-scrolltarget.html' +}) +export class AnchorScrolltargetComponent { + items: Array = [ + { + id: 'scrolltarget-1', + title: '概览' + }, + { + id: 'scrolltarget-2', + title: 'Pod列表' + }, + { + id: 'scrolltarget-3', + title: '容器配置' + }, + { + id: 'scrolltarget-4', + title: '访问配置' + }, + { + id: 'scrolltarget-5', + title: '配置Pod' + }, + { + id: 'scrolltarget-6', + title: '事件' + }, + { + id: 'scrolltarget-7', + title: '历史版本' + } + ]; +} diff --git a/src/anchor/demo/src/app/anchor/AnchorSpeedComponent.ts b/src/anchor/demo/src/app/anchor/AnchorSpeedComponent.ts new file mode 100644 index 0000000..8626e10 --- /dev/null +++ b/src/anchor/demo/src/app/anchor/AnchorSpeedComponent.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; +import { TiAnchorItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './anchor-speed.html' +}) +export class AnchorSpeedComponent { + speed: number = 100; + items: Array = [ + { + id: 'speed-1', + title: '概览' + }, + { + id: 'speed-2', + title: 'Pod列表' + }, + { + id: 'speed-3', + title: '容器配置' + }, + { + id: 'speed-4', + title: '访问配置' + }, + { + id: 'speed-5', + title: '配置Pod' + }, + { + id: 'speed-6', + title: '事件' + }, + { + id: 'speed-7', + title: '历史版本' + } + ]; +} diff --git a/src/anchor/demo/src/app/anchor/AnchorTemplateComponent.ts b/src/anchor/demo/src/app/anchor/AnchorTemplateComponent.ts new file mode 100644 index 0000000..5ba5ad7 --- /dev/null +++ b/src/anchor/demo/src/app/anchor/AnchorTemplateComponent.ts @@ -0,0 +1,38 @@ +import { Component } from '@angular/core'; +import { TiAnchorItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './anchor-template.html' +}) +export class AnchorTemplateComponent { + items: Array = [ + { + id: 'template-1', + title: '概览' + }, + { + id: 'template-2', + title: 'Pod列表' + }, + { + id: 'template-3', + title: '容器配置' + }, + { + id: 'template-4', + title: '访问配置' + }, + { + id: 'template-5', + title: '配置Pod' + }, + { + id: 'template-6', + title: '事件' + }, + { + id: 'template-7', + title: '历史版本' + } + ]; +} diff --git a/src/anchor/demo/src/app/anchor/AnchorTestComponent.ts b/src/anchor/demo/src/app/anchor/AnchorTestComponent.ts new file mode 100644 index 0000000..b6db135 --- /dev/null +++ b/src/anchor/demo/src/app/anchor/AnchorTestComponent.ts @@ -0,0 +1,58 @@ +import { Component } from '@angular/core'; +import { TiAnchorItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './anchor-test.html', + styleUrls: ['./anchortest.less'] +}) +export class AnchorTestComponent { + items: Array = [ + { + id: 'id1', + title: '概览' + }, + { + id: 'id2', + title: 'Pod列表' + }, + { + id: 'id3', + title: '容器配置' + }, + { + id: 'id4', + title: '访问配置' + }, + { + id: 'id5', + title: '配置Pod' + }, + { + id: 'id6', + title: '事件' + }, + { + id: 'id7', + title: '历史版本' + }, + { + id: 'id8', + title: 'Container Configuration' + } + ]; + anchorId: string = 'id4'; + offsetTop: number = 100; + itemId: string; + + changeanchorId(): void { + this.anchorId = 'id2'; + } + + changeOffsetTop(): void { + this.offsetTop = 0; + } + + onChange(id: string): void { + this.itemId = id; + } +} diff --git a/src/anchor/demo/src/app/anchor/AnchorTestModule.ts b/src/anchor/demo/src/app/anchor/AnchorTestModule.ts new file mode 100644 index 0000000..a0e65bf --- /dev/null +++ b/src/anchor/demo/src/app/anchor/AnchorTestModule.ts @@ -0,0 +1,66 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiAnchorModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { AnchorBasicComponent } from './AnchorBasicComponent'; +import { AnchorIdComponent } from './AnchorIdComponent'; +import { AnchorOffsettopComponent } from './AnchorOffsettopComponent'; +import { AnchorSpeedComponent } from './AnchorSpeedComponent'; +import { AnchorEventsComponent } from './AnchorEventsComponent'; +import { AnchorTemplateComponent } from './AnchorTemplateComponent'; +import { AnchorItemsComponent } from './AnchorItemsComponent'; +import { AnchorTestComponent } from './AnchorTestComponent'; +import { AnchorScrolltargetComponent } from './AnchorScrolltargetComponent'; + +@NgModule({ + imports: [CommonModule, TiAnchorModule, DemoLogModule, RouterModule.forChild(AnchorTestModule.ROUTES)], + declarations: [ + AnchorBasicComponent, + AnchorIdComponent, + AnchorOffsettopComponent, + AnchorSpeedComponent, + AnchorEventsComponent, + AnchorTemplateComponent, + AnchorItemsComponent, + AnchorTestComponent, + AnchorScrolltargetComponent + ] +}) +export class AnchorTestModule { + static readonly LINKS: Array = [{ label: 'Anchor' }]; + static readonly ROUTES: Routes = [ + { + path: 'anchor/anchor-basic', + component: AnchorBasicComponent + }, + { + path: 'anchor/anchor-id', + component: AnchorIdComponent + }, + { + path: 'anchor/anchor-offsettop', + component: AnchorOffsettopComponent + }, + { + path: 'anchor/anchor-speed', + component: AnchorSpeedComponent + }, + { + path: 'anchor/anchor-template', + component: AnchorTemplateComponent + }, + { + path: 'anchor/anchor-events', + component: AnchorEventsComponent + }, + { path: 'anchor/anchor-items', component: AnchorItemsComponent }, + { + path: 'anchor/anchor-scrolltarget', + component: AnchorScrolltargetComponent + }, + { path: 'anchor/anchor-test', component: AnchorTestComponent } + ]; +} diff --git a/src/anchor/demo/src/app/anchor/anchor-basic.html b/src/anchor/demo/src/app/anchor/anchor-basic.html new file mode 100644 index 0000000..4d55840 --- /dev/null +++ b/src/anchor/demo/src/app/anchor/anchor-basic.html @@ -0,0 +1,9 @@ +
+ +
+
+

{{item.title}}

+ {{item.title}} +
+
+
diff --git a/src/anchor/demo/src/app/anchor/anchor-events.html b/src/anchor/demo/src/app/anchor/anchor-events.html new file mode 100644 index 0000000..c33dbd1 --- /dev/null +++ b/src/anchor/demo/src/app/anchor/anchor-events.html @@ -0,0 +1,11 @@ +
+ +
+
+

{{item.title}}

+ {{item.title}} +
+
+
+ + diff --git a/src/anchor/demo/src/app/anchor/anchor-id.html b/src/anchor/demo/src/app/anchor/anchor-id.html new file mode 100644 index 0000000..8f61450 --- /dev/null +++ b/src/anchor/demo/src/app/anchor/anchor-id.html @@ -0,0 +1,17 @@ +
+ +
+
+

{{item.title}}

+ {{item.title}} +
+
+
+ + diff --git a/src/anchor/demo/src/app/anchor/anchor-items.html b/src/anchor/demo/src/app/anchor/anchor-items.html new file mode 100644 index 0000000..958ff6e --- /dev/null +++ b/src/anchor/demo/src/app/anchor/anchor-items.html @@ -0,0 +1,22 @@ +

1.描述

+

本地模拟数据更新

+
+

2.示例

+
+ +
+ +
+

概览

+
+

概览 Content

+
+

Pod列表

+
+

Pod列表 Content

+
+

容器配置

+
+

容器配置 Content

+
+
diff --git a/src/anchor/demo/src/app/anchor/anchor-offsettop.html b/src/anchor/demo/src/app/anchor/anchor-offsettop.html new file mode 100644 index 0000000..7b07cf5 --- /dev/null +++ b/src/anchor/demo/src/app/anchor/anchor-offsettop.html @@ -0,0 +1,9 @@ +
+ +
+
+

{{item.title}}

+ {{item.title}} +
+
+
diff --git a/src/anchor/demo/src/app/anchor/anchor-scrolltarget.html b/src/anchor/demo/src/app/anchor/anchor-scrolltarget.html new file mode 100644 index 0000000..0dbbc81 --- /dev/null +++ b/src/anchor/demo/src/app/anchor/anchor-scrolltarget.html @@ -0,0 +1,9 @@ +
+ +
+
+

{{item.title}}

+ {{item.title}} +
+
+
diff --git a/src/anchor/demo/src/app/anchor/anchor-speed.html b/src/anchor/demo/src/app/anchor/anchor-speed.html new file mode 100644 index 0000000..610bb68 --- /dev/null +++ b/src/anchor/demo/src/app/anchor/anchor-speed.html @@ -0,0 +1,9 @@ +
+ +
+
+

{{item.title}}

+ {{item.title}} +
+
+
diff --git a/src/anchor/demo/src/app/anchor/anchor-template.html b/src/anchor/demo/src/app/anchor/anchor-template.html new file mode 100644 index 0000000..b6865ff --- /dev/null +++ b/src/anchor/demo/src/app/anchor/anchor-template.html @@ -0,0 +1,11 @@ +
+ + {{i}} {{item.title}} + +
+
+

{{item.title}}

+ {{item.title}} +
+
+
diff --git a/src/anchor/demo/src/app/anchor/anchor-test.html b/src/anchor/demo/src/app/anchor/anchor-test.html new file mode 100644 index 0000000..1346d64 --- /dev/null +++ b/src/anchor/demo/src/app/anchor/anchor-test.html @@ -0,0 +1,47 @@ +

1.描述

+

动态变更测试

+

2.示例

+
+

id:{{itemId}}

+ +

更改anchorId

+
+

+

更改offsetTop

+
+ +
+
+

概览

+
+

概览 Content

+
+

Pod列表

+
+

Pod列表 Content

+
+

容器配置

+
+

容器配置 Content

+
+

访问配置

+
+

访问配置 Content

+
+

配置Pod

+
+

配置Pod Content

+
+

事件

+
+

事件 Content

+
+

历史版本

+
+

历史版本 Content

+
+

Container Configuration

+
+

Container Configuration Content

+
+
diff --git a/src/anchor/demo/src/app/anchor/anchortest.less b/src/anchor/demo/src/app/anchor/anchortest.less new file mode 100644 index 0000000..ab39cbf --- /dev/null +++ b/src/anchor/demo/src/app/anchor/anchortest.less @@ -0,0 +1,18 @@ +.right-content { + margin-left: 260px; +} + +.right-content h1 { + height: 60px; + line-height: 60px; + font-weight: 400; +} + +.right-content > div > p { + margin-left: 10px; +} + +.anchor-box { + position: fixed; + left: 60px; +} diff --git a/src/anchor/demo/src/app/anchor/webdoc/anchor-demos.js b/src/anchor/demo/src/app/anchor/webdoc/anchor-demos.js new file mode 100644 index 0000000..86d2249 --- /dev/null +++ b/src/anchor/demo/src/app/anchor/webdoc/anchor-demos.js @@ -0,0 +1,96 @@ +export default { + column: '1', + demos: [ + { + demoId: 'anchor-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

Anchor 组件的最简用法。

', + 'en-US': '', + }, + apis: [ + 'TiAnchorComponent.properties.items', + 'TiAnchorItem.properties.id', + 'TiAnchorItem.properties.title' + ] + }, + { + demoId: 'anchor-id', + name: { + 'zh-CN': '当前激活锚点 id', + 'en-US': 'id', + }, + desc: { + 'zh-CN': '

通过属性anchorId配置当前激活锚点 id,对应容器中联动的 DOM 元素 id;当锚点改变时触发anchorIdChange事件,传出的参数为当前锚点 id。

', + 'en-US': '', + }, + apis: [ + 'TiAnchorComponent.properties.anchorId', + 'TiAnchorComponent.events.anchorIdChange' + ] + }, + { + demoId: 'anchor-offsettop', + name: { + 'zh-CN': '滚动偏移量', + 'en-US': 'offsettop', + }, + desc: { + 'zh-CN': '

通过属性offsetTop配置点击锚点时,容器滚动至 Dom 元素距容器顶部距离达到该值时对应锚点高亮。

', + 'en-US': '', + }, + apis: ['TiAnchorComponent.properties.offsetTop'] + }, + { + demoId: 'anchor-speed', + name: { + 'zh-CN': '滚动速度', + 'en-US': 'speed', + }, + desc: { + 'zh-CN': '

通过属性speed配置点击锚点时,容器滚动速度。

', + 'en-US': '', + }, + apis: ['TiAnchorComponent.properties.speed'] + }, + { + demoId: 'anchor-template', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'template', + }, + desc: { + 'zh-CN': '

通过#item配置锚点内容区域的模板。

', + 'en-US': '', + }, + apis: ['TiAnchorComponent.slots.itemTemplete'] + }, + { + demoId: 'anchor-events', + name: { + 'zh-CN': '事件', + 'en-US': 'events', + }, + desc: { + 'zh-CN': '

当选中锚点时触发select事件,传递出去的参数为当前锚点 id。

', + 'en-US': '', + }, + apis: ['TiAnchorComponent.events.select'] + }, + { + demoId: 'anchor-scrolltarget', + name: { + 'zh-CN': '滚动容器', + 'en-US': 'scrolltarget', + }, + desc: { + 'zh-CN': '

通过scrollTarget配置滚动容器,当滚动容器是整个文档时无需配置。

', + 'en-US': '', + }, + apis: ['TiAnchorComponent.properties.scrollTarget'] + }, + ], +}; diff --git a/src/anchor/demo/src/app/anchor/webdoc/anchor.cn.md b/src/anchor/demo/src/app/anchor/webdoc/anchor.cn.md new file mode 100644 index 0000000..b094fcc --- /dev/null +++ b/src/anchor/demo/src/app/anchor/webdoc/anchor.cn.md @@ -0,0 +1,23 @@ +--- +title: Anchor 锚点 +--- +# Anchor 锚点 + +
+ +用于跳转到页面指定位置的组件。   + +```typescript +import { TiAnchorModule } from '@opentiny/ng'; +``` + +
+ +
+ +用于跳转到页面指定位置的组件。   + +```typescript +import { TiAnchorModule } from '@opentiny/ng'; +``` +
diff --git a/src/anchor/demo/src/app/anchor/webdoc/anchor.en.md b/src/anchor/demo/src/app/anchor/webdoc/anchor.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/anchor/demo/src/app/anchor/webdoc/anchor.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/anchor/demo/src/app/app.html b/src/anchor/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/anchor/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/anchor/demo/src/favicon.ico b/src/anchor/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/anchor/demo/src/index.html b/src/anchor/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/anchor/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/anchor/demo/src/main.ts b/src/anchor/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/anchor/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/anchor/demo/test.ts b/src/anchor/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/anchor/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/anchor/demo/tsconfig.app.json b/src/anchor/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/anchor/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/anchor/demo/tsconfig.spec.json b/src/anchor/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/anchor/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/anchor/lib/index.ts b/src/anchor/lib/index.ts new file mode 100644 index 0000000..5e72eb2 --- /dev/null +++ b/src/anchor/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiAnchorModule'; diff --git a/src/anchor/lib/ng-package.json b/src/anchor/lib/ng-package.json new file mode 100644 index 0000000..3c5802d --- /dev/null +++ b/src/anchor/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/anchor", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/anchor/lib/package.json b/src/anchor/lib/package.json new file mode 100644 index 0000000..2c4a305 --- /dev/null +++ b/src/anchor/lib/package.json @@ -0,0 +1,10 @@ +{ + "name": "@opentiny/ng-anchor", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/anchor/lib/project.json b/src/anchor/lib/project.json new file mode 100644 index 0000000..637fbda --- /dev/null +++ b/src/anchor/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/anchor/lib", + "sourceRoot": "src/anchor/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/anchor"], + "options": { + "project": "src/anchor/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/anchor"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js anchor" + }, + { + "command": "ng default-build anchor" + }, + { + "command": "node build/clear-default-theme.js anchor" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/anchor && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build anchor && ng pack anchor && node build/publish.js anchor --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/anchor/lib/src/TiAnchorComponent.ts b/src/anchor/lib/src/TiAnchorComponent.ts new file mode 100644 index 0000000..fd750ee --- /dev/null +++ b/src/anchor/lib/src/TiAnchorComponent.ts @@ -0,0 +1,267 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + Input, + NgZone, + Output, + Renderer2, + SimpleChange, + SimpleChanges, + TemplateRef +} from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; + +export interface TiAnchorItem { + /** + * 必选,锚点 id + */ + id: string; + /** + * 锚点文本 + */ + title?: string; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} +/** + * 锚点组件 + * + */ +@Component({ + selector: 'ti-anchor', + templateUrl: './anchor.html', + styleUrls: ['./anchor.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.tp-anchor-container]': 'true' + } +}) +export class TiAnchorComponent extends TiBaseComponent { + /** + * 必选,锚点数据集 + */ + @Input() items: Array; + /** + * 当前锚点 id + */ + @Input() anchorId: string; + /** + * 点击锚点时,锚点对应 Dom 元素距离滚动容器顶部的偏移量像素 + */ + @Input() offsetTop: number = 50; + /** + * 点击锚点时,容器滚动速度即滚动到当前位置所需要的毫秒数 + */ + @Input() speed: number = 300; + /** + * 滚动容器,默认为整个文档 + */ + @Input() scrollTarget: HTMLElement; + /** + * 选中锚点时触发的回调,参数:当前锚点数据 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + /** + * 当前锚点 id 改变时触发的回调,参数:当前锚点 id + */ + @Output() readonly anchorIdChange: EventEmitter = new EventEmitter(); + /** + * 锚点内容区域的模板 + */ + @ContentChild('item', { static: true }) itemTemplete: TemplateRef; + private unlistenScroll: () => void; + /** + * 是否组件内部触发scroll事件 + */ + private isInnerScrolling: boolean; + /** + * @ignore + * 保存组件内部更改后的锚点id + */ + public currentId: string; + private hasGoanchor: boolean = false; + private hasAnimation: boolean = false; // 初始化及数据更新时,无需平滑滚动 + constructor( + private element: ElementRef, + private renderer2: Renderer2, + private zone: NgZone, + private changeDetectorRef: ChangeDetectorRef + ) { + super(element, renderer2); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + const anchorIdObj: SimpleChange = changes['anchorId']; + const itemsObj: SimpleChange = changes['items']; + if (anchorIdObj && anchorIdObj.currentValue && !anchorIdObj.isFirstChange() && this.anchorId !== this.currentId) { + this.hasGoanchor = false; + this.hasAnimation = true; + } + if (itemsObj && itemsObj.currentValue && !itemsObj.isFirstChange()) { + this.hasGoanchor = false; + } + } + + ngOnInit(): void { + super.ngOnInit(); + // 监听滚轮事件 + this.zone.runOutsideAngular(() => { + const scrollTarget: any = this.scrollTarget || window; + + this.unlistenScroll = this.renderer2.listen(scrollTarget, 'scroll', () => { + this.onWindowScroll(); + }); + }); + } + + ngAfterViewChecked(): void { + super.ngAfterViewChecked(); + // 数据更新anchorId有值时,需要滚动容器到指定位置,只触发一次 + if (!this.hasGoanchor) { + this.hasGoanchor = true; + this.goToAnchor(this.anchorId || (this.items && this.items[0] && this.items[0].id)); + } + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + if (this.unlistenScroll) { + this.unlistenScroll(); + } + } + + // 容器滚动时,相应锚点高亮 + private onWindowScroll(): void { + if (this.isInnerScrolling) { + return; + } + const scrollTop: number = + this.scrollTarget?.scrollTop || document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop; + const itemLength: number = this.items && this.items.length; + // 容器滚动时,锚点对应Dom元素距浏览器窗口顶部高度小于等于所配置的顶部偏移量,则应是当前锚点 + for (let i: number = itemLength - 1; i >= 0; i--) { + const anchorEle: HTMLElement = document.getElementById(this.items[i].id); + const scrollTargetTop: number = this.scrollTarget?.offsetTop ? this.scrollTarget.offsetTop : 0; + const targetTop: number = anchorEle?.offsetTop - scrollTargetTop; + // 判断有无当前激活锚点,使用户点击时,只当前锚点高亮;而滚动时,相对应锚点都高亮 + if (anchorEle && !isNaN(targetTop)) { + const clientY: number = targetTop - scrollTop - this.offsetTop; + const id: string = this.anchorId; + if (clientY <= this.offsetTop) { + this.zone.run(() => { + this.anchorId = this.currentId = this.items[i].id; + // 锚点改变时,仅向外通知一次 + if (this.anchorId !== id) { + this.select.emit(this.items[i]); + this.anchorIdChange.emit(this.items[i].id); + } + // onpush模式下 需要手动刷新视图 + this.changeDetectorRef.markForCheck(); + }); + break; + } + } + } + } + + /** + * @ignore + */ + public onClick(item: TiAnchorItem): void { + this.hasAnimation = true; + this.goToAnchor(item.id); + } + + /** + * 容器滚动到目标锚点对应位置 + * @param anchorId 锚点id + */ + private goToAnchor(anchorId: string): void { + this.isInnerScrolling = true; + const targetEle: HTMLElement = document.getElementById(anchorId); + const scrollTop: number = + this.scrollTarget?.scrollTop || document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop; + const scrollTargetTop: number = this.scrollTarget?.offsetTop ? this.scrollTarget.offsetTop : 0; + const scollPosition: number = targetEle?.offsetTop - scrollTargetTop - this.offsetTop; + + /** + * 初始化及数据更新时,容器直接滚动到定位位置; + * 点击锚点切换及用户动态改变当前锚点id时,滚动条平滑运动。 + */ + if (this.hasAnimation) { + this.scrollAnimation(scrollTop, scollPosition, anchorId, this.speed); + } else { + // 修改了当前高亮锚点,使用setTimeout,app default下消除ExpressionChangedAfterItHasBeenCheckedError报错 + setTimeout(() => { + this.scrollToView(scollPosition, anchorId); + }, 0); + } + } + + // 滚动条平滑运动 + private scrollAnimation(currentY: number, targetY: number, anchorId: string, speed?: number): void { + const direction: number = targetY - currentY > 0 ? 1 : -1; + let distance: number = Math.abs(targetY - currentY); // 滚动条需要滚动的距离 + const stepTimes: number = speed / (1000 / 60); // 一般浏览器刷新频率是60HZ,时间间隔为1000ms/60,总帧数 + let stepDist: number = distance / stepTimes; // 每帧滚动条滚动距离 + + if (targetY !== currentY) { + stepDist *= direction; + // 定时器,1000/60ms时间间隔 执行一次 + let positionY: number = currentY; + let timer: any = setInterval(() => { + positionY = positionY + stepDist; + distance -= Math.abs(stepDist); + if (distance <= 0) { + clearInterval(timer); + timer = undefined; + this.scrollToView(targetY, anchorId); + this.hasAnimation = false; // 更新hasAnimation + } else { + this.scrollTarget ? this.scrollTarget.scrollTo(0, positionY) : window.scrollTo(0, positionY); + } + // onpush模式下 需要手动刷新视图 + this.changeDetectorRef.markForCheck(); + }, 1000 / 60); + } + } + + // 容器定位到指定位置,无滚动效果 + private scrollToView(target: number, anchorId: string): void { + this.scrollTarget ? this.scrollTarget.scrollTo(0, target) : window.scrollTo(0, target); // 横向坐标不移动 + this.getCurrentAnchor(anchorId); + this.isInnerScrolling = false; + } + + // 容器滚动到指定位置之后,向外通知当前锚点数据 + private getCurrentAnchor(anchorId: string): void { + this.anchorId = this.currentId = anchorId; + for (const item of this.items) { + if (item.id === anchorId) { + this.select.emit(item); + this.anchorIdChange.emit(item.id); + } + } + + // 绑定在模板上的值变了,需要手动触发变更检测 + this.changeDetectorRef.markForCheck(); + } +} diff --git a/src/anchor/lib/src/TiAnchorModule.ts b/src/anchor/lib/src/TiAnchorModule.ts new file mode 100644 index 0000000..005e01b --- /dev/null +++ b/src/anchor/lib/src/TiAnchorModule.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiAnchorComponent } from './TiAnchorComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [TiAnchorComponent], + declarations: [TiAnchorComponent] +}) +export class TiAnchorModule {} +export { TiAnchorComponent, TiAnchorItem } from './TiAnchorComponent'; diff --git a/src/anchor/lib/src/anchor.html b/src/anchor/lib/src/anchor.html new file mode 100644 index 0000000..fcdcdf2 --- /dev/null +++ b/src/anchor/lib/src/anchor.html @@ -0,0 +1,18 @@ + diff --git a/src/anchor/lib/src/anchor.less b/src/anchor/lib/src/anchor.less new file mode 100644 index 0000000..da279a1 --- /dev/null +++ b/src/anchor/lib/src/anchor.less @@ -0,0 +1,83 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + // --tp-anchor-link-title-padding-left: calc(var(--ti-common-space-5x) + var(--ti-common-space-3x) + var(--tp-anchor-link-badge-width)); // IE下不解析 + --tp-anchor-link-title-padding-left: calc( + var(--tp-anchor-link-badge-left) + var(--ti-common-space-3x) + var(--tp-anchor-link-badge-width) + ); // 锚点小圆左侧间距 20px + 锚点小圆与文本间间距 12px + 锚点小圆宽度 + --tp-anchor-link-title-line-height: calc(var(--ti-common-font-size-base) * var(--ti-common-line-height-number)); + --tp-anchor-link-line-width: var(--ti-common-border-weight-normal); + --tp-anchor-link-badge-width: var(--ti-common-size-2x); + --tp-anchor-link-badge-height: var(--tp-anchor-link-badge-width); + --tp-anchor-link-badge-left: var(--ti-common-space-5x); + --tp-anchor-link-line-color: var(--ti-common-color-line-dividing); + --tp-anchor-link-title-color: var(--ti-common-color-text-primary); + --tp-anchor-link-title-color-hover: var(--ti-common-color-text-highlight); + --tp-anchor-link-title-color-selected: var(--ti-common-color-text-highlight); +} +:host.tp-anchor-container { + display: inline-block; + width: var(--ti-common-size-40x); + padding: 30px 2px; // 左右加2px的padding是为了保证聚焦的情况下边框不会被遮挡。默认聚焦样式是宽度为2的黑色边框。 + overflow: hidden; + .box-sizing(border-box); +} + +.tp-anchor-link { + padding: 0 0 var(--ti-common-space-5x) 0; + position: relative; + list-style: none; + &:after { + content: ''; + position: absolute; + left: calc(var(--tp-anchor-link-badge-left) + (var(--tp-anchor-link-badge-width) - var(--tp-anchor-link-line-width)) / 2); + top: calc((var(--tp-anchor-link-title-line-height) + var(--tp-anchor-link-badge-width)) / 2); + width: var(--tp-anchor-link-line-width); + height: ~'calc(100% - var(--tp-anchor-link-badge-width))'; + background-color: var(--tp-anchor-link-line-color); + } + + &:last-child { + padding-bottom: 0; + + &:after { + display: none; + } + } +} + +.tp-anchor-link-title { + padding: 0 var(--ti-common-space-5x) 0 var(--tp-anchor-link-title-padding-left); + text-decoration: none; + color: var(--tp-anchor-link-title-color); + line-height: var(--tp-anchor-link-title-line-height); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + position: relative; + display: block; + word-wrap: break-word; + + &:before { + content: ''; + position: absolute; + left: var(--tp-anchor-link-badge-left); + top: calc((var(--tp-anchor-link-title-line-height) - var(--tp-anchor-link-badge-height)) / 2); + width: var(--tp-anchor-link-badge-width); + height: var(--tp-anchor-link-badge-height); + background-color: var(--tp-anchor-link-line-color); + border-radius: 50%; + box-sizing: border-box; + } + + &:hover { + color: var(--tp-anchor-link-title-color-hover); + } + + .tp-anchor-link-active & { + color: var(--tp-anchor-link-title-color-selected); + + &:before { + background-color: var(--ti-common-color-bg-emphasize); + } + } +} diff --git a/src/autocomplete/demo/karma.conf.js b/src/autocomplete/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/autocomplete/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/autocomplete/demo/project.json b/src/autocomplete/demo/project.json new file mode 100644 index 0000000..f266302 --- /dev/null +++ b/src/autocomplete/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/autocomplete/demo", + "sourceRoot": "src/autocomplete/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/autocomplete", + "index": "src/autocomplete/demo/src/index.html", + "main": "src/autocomplete/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/autocomplete/demo/tsconfig.app.json", + "assets": ["src/autocomplete/demo/src/favicon.ico", "src/autocomplete/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "autocomplete-demo:build:production" + }, + "development": { + "browserTarget": "autocomplete-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js autocomplete" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/autocomplete/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/autocomplete/demo/tsconfig.spec.json", + "karmaConfig": "src/autocomplete/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/autocomplete/demo/src/app/AppComponent.ts b/src/autocomplete/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/autocomplete/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/autocomplete/demo/src/app/AppModule.ts b/src/autocomplete/demo/src/app/AppModule.ts new file mode 100644 index 0000000..0744000 --- /dev/null +++ b/src/autocomplete/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { AutocompleteTestModule } from './autocomplete/AutocompleteTestModule'; + +@NgModule({ + imports: [ + AutocompleteTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/autocomplete/demo/src/app/IndexComponent.ts b/src/autocomplete/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..2b04e98 --- /dev/null +++ b/src/autocomplete/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { AutocompleteTestModule } from './autocomplete/AutocompleteTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = AutocompleteTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/autocomplete/demo/src/app/app.html b/src/autocomplete/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/autocomplete/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/autocomplete/demo/src/app/autocomplete/AutocompleteAppendtobodyComponent.ts b/src/autocomplete/demo/src/app/autocomplete/AutocompleteAppendtobodyComponent.ts new file mode 100644 index 0000000..e002616 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/AutocompleteAppendtobodyComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './autocomplete-appendtobody.html' +}) +export class AutocompleteAppendtobodyComponent { + value: string = ''; + options: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; +} diff --git a/src/autocomplete/demo/src/app/autocomplete/AutocompleteBasicComponent.ts b/src/autocomplete/demo/src/app/autocomplete/AutocompleteBasicComponent.ts new file mode 100644 index 0000000..d8d0ad8 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/AutocompleteBasicComponent.ts @@ -0,0 +1,53 @@ +import { Component } from '@angular/core'; +import { TiAutocompleteComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './autocomplete-basic.html' +}) +export class AutocompleteBasicComponent { + optionsValue: string = ''; + suggestValue: string = ''; + backendValue: string = ''; + + options: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; + + // 设置建议项 + onSuggest(autocomplete: TiAutocompleteComponent): void { + const suggestions: Array = this.getSuggestionData(autocomplete.model); + autocomplete.setSuggestions(suggestions); // 关键点 + } + + // 设置建议项 + onBackendSuggest(autocomplete: TiAutocompleteComponent): void { + // 模拟后台异步请求 + setTimeout(() => { + const suggestions: Array = this.getSuggestionData(autocomplete.model); + autocomplete.setSuggestions(suggestions); // 关键点 + }, 200); + } + + private getSuggestionData(value: string): Array { + let options: Array; + if (!value || value.indexOf('@') >= 0) { + options = []; + } else { + options = ['@example.com', '@example1.com', '@example2.com'].map((domain) => { + return { label: `${value}@${domain}` }; + }); + } + return options; + } +} diff --git a/src/autocomplete/demo/src/app/autocomplete/AutocompleteClearableComponent.ts b/src/autocomplete/demo/src/app/autocomplete/AutocompleteClearableComponent.ts new file mode 100644 index 0000000..c4bea0c --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/AutocompleteClearableComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiAutocompleteComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './autocomplete-clearable.html' +}) +export class AutocompleteClearableComponent { + value: string = '华西'; + options: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; +} diff --git a/src/autocomplete/demo/src/app/autocomplete/AutocompleteDisabledComponent.ts b/src/autocomplete/demo/src/app/autocomplete/AutocompleteDisabledComponent.ts new file mode 100644 index 0000000..c6d0e62 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/AutocompleteDisabledComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiAutocompleteComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './autocomplete-disabled.html' +}) +export class AutocompleteDisabledComponent { + value: string = ''; + options: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; +} diff --git a/src/autocomplete/demo/src/app/autocomplete/AutocompleteEventsComponent.ts b/src/autocomplete/demo/src/app/autocomplete/AutocompleteEventsComponent.ts new file mode 100644 index 0000000..1f6f003 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/AutocompleteEventsComponent.ts @@ -0,0 +1,44 @@ +import { Component } from '@angular/core'; +import { TiAutocompleteComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './autocomplete-events.html' +}) +export class AutocompleteEventsComponent { + myLogs: Array = []; + value: string = ''; + + onFocus(): void { + this.myLogs = [...this.myLogs, 'on focus']; + } + onBlur(): void { + this.myLogs = [...this.myLogs, 'on blur']; + } + onChange(value: string): void { + this.myLogs = [...this.myLogs, `input value: ${value}`]; + } + onClear(event: MouseEvent): void { + this.myLogs = [...this.myLogs, `on clear: ${this.value}`]; + } + onSelect(option: any): void { + this.myLogs = [...this.myLogs, `select: ${JSON.stringify(option)}`]; + } + + // 设置建议项 + onSuggest(autocomplete: TiAutocompleteComponent): void { + const suggestions: Array = this.getSuggestionData(autocomplete.model); + autocomplete.setSuggestions(suggestions); // 关键点 + } + + private getSuggestionData(value: string): Array { + let options: Array; + if (!value || value.indexOf('@') >= 0) { + options = []; + } else { + options = ['@example.com', '@example1.com', '@example2.com'].map((domain) => { + return { label: `${value}@${domain}` }; + }); + } + return options; + } +} diff --git a/src/autocomplete/demo/src/app/autocomplete/AutocompleteGroupComponent.ts b/src/autocomplete/demo/src/app/autocomplete/AutocompleteGroupComponent.ts new file mode 100644 index 0000000..5e0386f --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/AutocompleteGroupComponent.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './autocomplete-group.html' +}) +export class AutocompleteGroupComponent { + public options: Array = [ + { + label: '北美洲', + children: [ + { label: '美国', englishname: 'America', flag: 'assets/world_falg/america_flag.png', disabled: true }, + { label: '加拿大', englishname: 'Canada', flag: 'assets/world_falg/canada_flag.png', disabled: true } + ] + }, + { + label: '南美洲', + children: [{ label: '巴西', englishname: 'Brazil', flag: 'assets/world_falg/brazil_flag.png', disabled: false }] + }, + { + label: '亚洲', + children: [ + { label: '中国', englishname: 'China', flag: 'assets/world_falg/china_flag.png', disabled: false }, + { label: '日本', englishname: 'Japan', flag: 'assets/world_falg/japan_flag.png' }, + { label: '韩国', englishname: 'South Korea', flag: 'assets/world_falg/south_korea_flag.png' } + ] + }, + { + label: '欧洲', + children: [ + { label: '法国', englishname: 'France', flag: 'assets/world_falg/france_flag.png', disabled: true }, + { label: '德国', englishname: 'Germany', flag: 'assets/world_falg/germany_flag.png', disabled: false }, + { label: '土耳其', englishname: 'Turkey', flag: 'assets/world_falg/turkey_flag.png' }, + { label: '大不列颠和北爱兰联合王国', englishname: 'United Kingdom', flag: 'assets/world_falg/united_kingdom_flag.png' } + ] + } + ]; +} diff --git a/src/autocomplete/demo/src/app/autocomplete/AutocompleteLabelkeyComponent.ts b/src/autocomplete/demo/src/app/autocomplete/AutocompleteLabelkeyComponent.ts new file mode 100644 index 0000000..e3eb9cd --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/AutocompleteLabelkeyComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { TiAutocompleteComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './autocomplete-labelkey.html' +}) +export class AutocompleteLabelkeyComponent { + value: string = ''; + options: Array = [ + { + label: '华北', + english: 'North China' + }, + { + label: '华南', + english: 'South China' + }, + { + label: '西北', + english: 'Northwest China' + }, + { + label: '西南', + english: 'Southwest China' + } + ]; +} diff --git a/src/autocomplete/demo/src/app/autocomplete/AutocompleteMaxlengthComponent.ts b/src/autocomplete/demo/src/app/autocomplete/AutocompleteMaxlengthComponent.ts new file mode 100644 index 0000000..ac91873 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/AutocompleteMaxlengthComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiAutocompleteComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './autocomplete-maxlength.html' +}) +export class AutocompleteMaxlengthComponent { + value: string = ''; + options: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; +} diff --git a/src/autocomplete/demo/src/app/autocomplete/AutocompletePanelSizeComponent.ts b/src/autocomplete/demo/src/app/autocomplete/AutocompletePanelSizeComponent.ts new file mode 100644 index 0000000..ca0a341 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/AutocompletePanelSizeComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './autocomplete-panel-size.html' +}) +export class AutocompletePanelSizeComponent { + value: string = ''; + options: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; +} diff --git a/src/autocomplete/demo/src/app/autocomplete/AutocompleteTemplateComponent.ts b/src/autocomplete/demo/src/app/autocomplete/AutocompleteTemplateComponent.ts new file mode 100644 index 0000000..8b83841 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/AutocompleteTemplateComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './autocomplete-template.html' +}) +export class AutocompleteTemplateComponent { + value: string = ''; + options: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; +} diff --git a/src/autocomplete/demo/src/app/autocomplete/AutocompleteTestComponent.ts b/src/autocomplete/demo/src/app/autocomplete/AutocompleteTestComponent.ts new file mode 100644 index 0000000..3061eb6 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/AutocompleteTestComponent.ts @@ -0,0 +1,232 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiAutocompleteComponent, TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './autocomplete-test.html', + styles: ['body { overflow: auto; }'], + encapsulation: ViewEncapsulation.None +}) +export class AutocompleteTestComponent { + value: string = ''; + value1: string = ''; + mySelected: any = []; + myselected1: any = []; + options: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + }, + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + }, + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; + myOptions: Array = [ + { + label: '美国', + englishname: 'America', + disabled: true, + code: '010' + }, + { + label: '巴西', + englishname: 'Brazil', + disabled: false, + code: '020' + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true, + code: null + }, + { + label: '中国', + englishname: 'China', + disabled: false, + code: '030' + }, + { + label: '法国', + englishname: 'France', + disabled: true, + code: '040' + }, + { + label: '德国', + englishname: 'Germany', + disabled: false, + code: '050' + }, + { + label: '日本', + englishname: 'Japan', + code: null + }, + { + label: '韩国', + englishname: 'South Korea', + code: '060' + }, + { + label: '土耳其', + englishname: 'Turkey', + code: '080' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom', + code: '011' + } + ]; + myData: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + children: [ + { + label: '男装' + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 设置建议项 + onSuggest(autocomplete: TiAutocompleteComponent): void { + // 模拟后台异步请求 + setTimeout(() => { + const suggestions: Array = this.getSuggestionData(autocomplete.model); + autocomplete.setSuggestions(suggestions); // 关键点 + }, 200); + } + private getSuggestionData(value: string): Array { + const options: Array = value + ? [ + { + label: value + }, + { + label: value + value + }, + { + label: value + value + } + ] + : [ + { + label: 'a' + }, + { + label: 'b' + }, + { + label: 'c' + }, + { + label: 'd' + }, + { + label: 'a' + }, + { + label: 'b' + }, + { + label: 'c' + }, + { + label: 'd' + } + ]; + + return options; + } +} diff --git a/src/autocomplete/demo/src/app/autocomplete/AutocompleteTestModule.ts b/src/autocomplete/demo/src/app/autocomplete/AutocompleteTestModule.ts new file mode 100644 index 0000000..60b1736 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/AutocompleteTestModule.ts @@ -0,0 +1,111 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { + TiAutocompleteModule, + TiFormfieldModule, + TiSearchboxModule, + TiSelectModule, + TiTreeselectModule, + TiValidationModule +} from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { AutocompleteBasicComponent } from './AutocompleteBasicComponent'; +import { AutocompletePanelSizeComponent } from './AutocompletePanelSizeComponent'; +import { AutocompleteLabelkeyComponent } from './AutocompleteLabelkeyComponent'; +import { AutocompleteEventsComponent } from './AutocompleteEventsComponent'; +import { AutocompleteClearableComponent } from './AutocompleteClearableComponent'; +import { AutocompleteDisabledComponent } from './AutocompleteDisabledComponent'; +import { AutocompleteMaxlengthComponent } from './AutocompleteMaxlengthComponent'; +import { AutocompleteValidComponent } from './AutocompleteValidComponent'; +import { AutocompleteTipComponent } from './AutocompleteTipComponent'; +import { AutocompleteTestComponent } from './AutocompleteTestComponent'; +import { AutocompleteAppendtobodyComponent } from './AutocompleteAppendtobodyComponent'; +import { AutocompleteTemplateComponent } from './AutocompleteTemplateComponent'; +import { AutocompleteGroupComponent } from './AutocompleteGroupComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiAutocompleteModule, + TiFormfieldModule, + TiSelectModule, + TiTreeselectModule, + TiSearchboxModule, + TiValidationModule, + DemoLogModule, + RouterModule.forChild(AutocompleteTestModule.ROUTES) + ], + declarations: [ + AutocompleteBasicComponent, + AutocompletePanelSizeComponent, + AutocompleteLabelkeyComponent, + AutocompleteEventsComponent, + AutocompleteClearableComponent, + AutocompleteDisabledComponent, + AutocompleteMaxlengthComponent, + AutocompleteTipComponent, + AutocompleteValidComponent, + AutocompleteAppendtobodyComponent, + AutocompleteTestComponent, + AutocompleteTemplateComponent, + AutocompleteGroupComponent + ] +}) +export class AutocompleteTestModule { + static readonly LINKS: Array = [{ href: 'component/TiAutocompleteComponent.html', label: 'Autocomplete' }]; + static readonly ROUTES: Routes = [ + { + path: 'autocomplete/autocomplete-basic', + component: AutocompleteBasicComponent + }, + { + path: 'autocomplete/autocomplete-panel-size', + component: AutocompletePanelSizeComponent + }, + { + path: 'autocomplete/autocomplete-labelkey', + component: AutocompleteLabelkeyComponent + }, + { + path: 'autocomplete/autocomplete-events', + component: AutocompleteEventsComponent + }, + { + path: 'autocomplete/autocomplete-disabled', + component: AutocompleteDisabledComponent + }, + { + path: 'autocomplete/autocomplete-clearable', + component: AutocompleteClearableComponent + }, + { + path: 'autocomplete/autocomplete-maxlength', + component: AutocompleteMaxlengthComponent + }, + { + path: 'autocomplete/autocomplete-tip', + component: AutocompleteTipComponent + }, + { + path: 'autocomplete/autocomplete-appendtobody', + component: AutocompleteAppendtobodyComponent + }, + { + path: 'autocomplete/autocomplete-valid', + component: AutocompleteValidComponent + }, + { + path: 'autocomplete/autocomplete-group', + component: AutocompleteGroupComponent + }, + { + path: 'autocomplete/autocomplete-test', + component: AutocompleteTestComponent + } + ]; +} diff --git a/src/autocomplete/demo/src/app/autocomplete/AutocompleteTipComponent.ts b/src/autocomplete/demo/src/app/autocomplete/AutocompleteTipComponent.ts new file mode 100644 index 0000000..79dd3f3 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/AutocompleteTipComponent.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './autocomplete-tip.html' +}) +export class AutocompleteTipComponent { + value: string = ''; + + options: Array = [ + { + label: '华北(展示tip)', + tip: '在自然地理上一般指秦岭—淮河线以北,长城以南的中国的广大区域。' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; +} diff --git a/src/autocomplete/demo/src/app/autocomplete/AutocompleteValidComponent.ts b/src/autocomplete/demo/src/app/autocomplete/AutocompleteValidComponent.ts new file mode 100644 index 0000000..f42800b --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/AutocompleteValidComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './autocomplete-valid.html' +}) +export class AutocompleteValidComponent { + value: string = ''; + options: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; +} diff --git a/src/autocomplete/demo/src/app/autocomplete/autocomplete-appendtobody.html b/src/autocomplete/demo/src/app/autocomplete/autocomplete-appendtobody.html new file mode 100644 index 0000000..a8551f7 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/autocomplete-appendtobody.html @@ -0,0 +1,8 @@ + diff --git a/src/autocomplete/demo/src/app/autocomplete/autocomplete-basic.html b/src/autocomplete/demo/src/app/autocomplete/autocomplete-basic.html new file mode 100644 index 0000000..2ccb2a3 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/autocomplete-basic.html @@ -0,0 +1,26 @@ +

1.下拉建议项是固定的,直接设置options(可模糊匹配)

+ + +

2.下拉建议项随输入框的内容动态变化

+ + +

3.下拉建议项根据输入框的内容从后台获取

+ diff --git a/src/autocomplete/demo/src/app/autocomplete/autocomplete-clearable.html b/src/autocomplete/demo/src/app/autocomplete/autocomplete-clearable.html new file mode 100644 index 0000000..b7ebc39 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/autocomplete-clearable.html @@ -0,0 +1 @@ + diff --git a/src/autocomplete/demo/src/app/autocomplete/autocomplete-disabled.html b/src/autocomplete/demo/src/app/autocomplete/autocomplete-disabled.html new file mode 100644 index 0000000..8e5567c --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/autocomplete-disabled.html @@ -0,0 +1,8 @@ + diff --git a/src/autocomplete/demo/src/app/autocomplete/autocomplete-events.html b/src/autocomplete/demo/src/app/autocomplete/autocomplete-events.html new file mode 100644 index 0000000..4d3c737 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/autocomplete-events.html @@ -0,0 +1,16 @@ + + + diff --git a/src/autocomplete/demo/src/app/autocomplete/autocomplete-group.html b/src/autocomplete/demo/src/app/autocomplete/autocomplete-group.html new file mode 100644 index 0000000..4aaccba --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/autocomplete-group.html @@ -0,0 +1 @@ + diff --git a/src/autocomplete/demo/src/app/autocomplete/autocomplete-labelkey.html b/src/autocomplete/demo/src/app/autocomplete/autocomplete-labelkey.html new file mode 100644 index 0000000..b9e815d --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/autocomplete-labelkey.html @@ -0,0 +1,8 @@ + diff --git a/src/autocomplete/demo/src/app/autocomplete/autocomplete-maxlength.html b/src/autocomplete/demo/src/app/autocomplete/autocomplete-maxlength.html new file mode 100644 index 0000000..a6f18b7 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/autocomplete-maxlength.html @@ -0,0 +1,8 @@ + diff --git a/src/autocomplete/demo/src/app/autocomplete/autocomplete-panel-size.html b/src/autocomplete/demo/src/app/autocomplete/autocomplete-panel-size.html new file mode 100644 index 0000000..63bcb83 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/autocomplete-panel-size.html @@ -0,0 +1,9 @@ + diff --git a/src/autocomplete/demo/src/app/autocomplete/autocomplete-template.html b/src/autocomplete/demo/src/app/autocomplete/autocomplete-template.html new file mode 100644 index 0000000..d0f4401 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/autocomplete-template.html @@ -0,0 +1,5 @@ + + +
{{ i }} {{ item.label }}
+
+
diff --git a/src/autocomplete/demo/src/app/autocomplete/autocomplete-test.html b/src/autocomplete/demo/src/app/autocomplete/autocomplete-test.html new file mode 100644 index 0000000..41a83e2 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/autocomplete-test.html @@ -0,0 +1,30 @@ +

描述

+

场景测试

+

1.首次聚焦时,下方空间不足在需在上方显示时,不出现先定位在下方后出现在上方的抖动现象

+

+ 2.输入框中输入值过滤或搜索框搜索时,不出现下拉面板与目标元素水平错位现象(下拉面板数据量变化导致body出现纵向滚动条时,目标元素发生水平位移) +

+

示例

+
+

autocomplete,searchbox,select,treeselect组件

+
+ + + + +
diff --git a/src/autocomplete/demo/src/app/autocomplete/autocomplete-tip.html b/src/autocomplete/demo/src/app/autocomplete/autocomplete-tip.html new file mode 100644 index 0000000..6a69230 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/autocomplete-tip.html @@ -0,0 +1,9 @@ + diff --git a/src/autocomplete/demo/src/app/autocomplete/autocomplete-valid.html b/src/autocomplete/demo/src/app/autocomplete/autocomplete-valid.html new file mode 100644 index 0000000..fc58aab --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/autocomplete-valid.html @@ -0,0 +1,9 @@ + diff --git a/src/autocomplete/demo/src/app/autocomplete/webdoc/autocomplete-demos.js b/src/autocomplete/demo/src/app/autocomplete/webdoc/autocomplete-demos.js new file mode 100644 index 0000000..246b5b2 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/webdoc/autocomplete-demos.js @@ -0,0 +1,164 @@ +export default { + column: '2', + demos: [ + { + demoId: 'autocomplete-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': + '

通过options配置下拉建议项固定的场景,在输入过程中对下拉建议项进行模糊过滤。通过suggest配置下拉建议项不固定的场景,在聚焦或值改变时,调用setSuggestions方法设置下拉建议项。

', + 'en-US': '' + }, + apis: [ + 'TiAutocompleteComponent.properties.options', + 'TiAutocompleteComponent.properties.placeholder', + 'TiAutocompleteComponent.events.suggest', + 'TiAutocompleteComponent.methods.setSuggestions' + ] + }, + { + demoId: 'autocomplete-appendtobody', + name: { + 'zh-CN': '附着在body上', + 'en-US': 'appendToBody' + }, + desc: { + 'zh-CN': + '

通过属性appendToBody配置下拉面板是否附着在 body 上。在有局部滚动条的场景下,如果配置成 false,下拉面板和选择框不会分离。', + 'en-US': '' + }, + apis: ['TiAutocompleteComponent.properties.appendToBody'] + }, + { + demoId: 'autocomplete-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled' + }, + desc: { + 'zh-CN': '

通过属性disabled配置是否为禁用状态。

', + 'en-US': '' + }, + apis: ['TiAutocompleteComponent.properties.disabled'] + }, + { + demoId: 'autocomplete-clearable', + name: { + 'zh-CN': '清除功能', + 'en-US': 'clearable' + }, + desc: { + 'zh-CN': '

通过属性clearable配置一键清除功能。

', + 'en-US': '' + }, + apis: ['TiAutocompleteComponent.properties.clearable'] + }, + { + demoId: 'autocomplete-labelkey', + name: { + 'zh-CN': '显示文本', + 'en-US': 'labelkey' + }, + desc: { + 'zh-CN': '

通过属性labelKey配置显示文本的键值。

', + 'en-US': '' + }, + apis: ['TiAutocompleteComponent.properties.labelKey'] + }, + { + demoId: 'autocomplete-maxlength', + name: { + 'zh-CN': '允许的最大字符数', + 'en-US': 'maxlength' + }, + desc: { + 'zh-CN': '

通过属性maxlength配置输入框中允许的最大字符数。

', + 'en-US': '' + }, + apis: ['TiAutocompleteComponent.properties.maxlength'] + }, + { + demoId: 'autocomplete-panel-size', + name: { + 'zh-CN': '下拉面板大小', + 'en-US': 'panelSize' + }, + desc: { + 'zh-CN': + '

通过属性panelWidth配置下拉面板的宽度,包括autojustifiedstring三种类型。通过属性panelMaxHeight配置下拉面板的最大高度。

', + 'en-US': '' + }, + apis: ['TiAutocompleteComponent.properties.panelWidth', 'TiAutocompleteComponent.properties.panelMaxHeight'] + }, + { + demoId: 'autocomplete-tip', + name: { + 'zh-CN': '下拉面板 tip 提示', + 'en-US': 'tip' + }, + desc: { + 'zh-CN': + '

通过属性tipPosition配置下拉 tip 提示方向。通过属性tipMaxWidth配置 tip 框最大宽度。通过options接口中tip属性配置 tip 内容。

', + 'en-US': '' + }, + apis: ['TiAutocompleteComponent.properties.tipPosition', 'TiAutocompleteComponent.properties.tipMaxWidth'] + }, + { + demoId: 'autocomplete-valid', + name: { + 'zh-CN': '校验', + 'en-US': 'autocomplete validation' + }, + desc: { + 'zh-CN': '

通过指令tiValidation实现校验。

', + 'en-US': '' + } + }, + { + demoId: 'autocomplete-events', + name: { + 'zh-CN': '事件', + 'en-US': 'events' + }, + desc: { + 'zh-CN': + '

当元素聚焦的时候触发focus事件。当元素失焦的时候触发blur事件。当元素内容发生变化的时候触发ngModelChange事件。当点击叉号清除时触发clear事件。当选中选项时触发select事件。

', + 'en-US': '' + }, + apis: [ + 'TiAutocompleteComponent.events.focus', + 'TiAutocompleteComponent.events.blur', + 'TiAutocompleteComponent.events.ngModelChange', + 'TiAutocompleteComponent.events.clear', + 'TiAutocompleteComponent.events.select' + ] + }, + { + demoId: 'autocomplete-template', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'template' + }, + desc: { + 'zh-CN': '

通过#item配置下拉面板中选项的模板。

', + 'en-US': '' + }, + apis: ['TiAutocompleteComponent.slots.itemTemplate'] + }, + { + demoId: 'autocomplete-group', + name: { + 'zh-CN': '分组', + 'en-US': 'group' + }, + desc: { + 'zh-CN': '

通过属性options中的children属性设置分组。

', + 'en-US': '' + }, + apis: ['TiAutocompleteComponent.properties.options'] + } + ] +}; diff --git a/src/autocomplete/demo/src/app/autocomplete/webdoc/autocomplete.cn.md b/src/autocomplete/demo/src/app/autocomplete/webdoc/autocomplete.cn.md new file mode 100644 index 0000000..8ea250f --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/webdoc/autocomplete.cn.md @@ -0,0 +1,36 @@ +--- +title: Autocomplete 自动补全 +--- +# Autocomplete 自动补全 + +
+ +Autocomplete 是自动补全输入框组件。   + +```typescript +import { TiAutocompleteModule } from '@opentiny/ng'; +``` + +如需使用校验功能,请导入`TiValidationModule`。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
+ +
+ +Autocomplete 是自动补全输入框组件。   + +```typescript +import { TiAutocompleteModule } from '@cloud/tiny-config'; +``` + +如需使用校验功能,请导入`TiValidationModule`。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
diff --git a/src/autocomplete/demo/src/app/autocomplete/webdoc/autocomplete.en.md b/src/autocomplete/demo/src/app/autocomplete/webdoc/autocomplete.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/autocomplete/demo/src/app/autocomplete/webdoc/autocomplete.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/autocomplete/demo/src/favicon.ico b/src/autocomplete/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/autocomplete/demo/src/index.html b/src/autocomplete/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/autocomplete/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/autocomplete/demo/src/main.ts b/src/autocomplete/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/autocomplete/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/autocomplete/demo/test.ts b/src/autocomplete/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/autocomplete/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/autocomplete/demo/tsconfig.app.json b/src/autocomplete/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/autocomplete/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/autocomplete/demo/tsconfig.spec.json b/src/autocomplete/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/autocomplete/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/autocomplete/lib/index.ts b/src/autocomplete/lib/index.ts new file mode 100644 index 0000000..6b3b251 --- /dev/null +++ b/src/autocomplete/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiAutocompleteModule'; diff --git a/src/autocomplete/lib/ng-package.json b/src/autocomplete/lib/ng-package.json new file mode 100644 index 0000000..424e693 --- /dev/null +++ b/src/autocomplete/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/autocomplete", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/autocomplete/lib/package.json b/src/autocomplete/lib/package.json new file mode 100644 index 0000000..bac605b --- /dev/null +++ b/src/autocomplete/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-autocomplete", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-droplist": "~1.0.0-beta.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/autocomplete/lib/project.json b/src/autocomplete/lib/project.json new file mode 100644 index 0000000..fc677b4 --- /dev/null +++ b/src/autocomplete/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/autocomplete/lib", + "sourceRoot": "src/autocomplete/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/autocomplete"], + "options": { + "project": "src/autocomplete/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/autocomplete"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js autocomplete" + }, + { + "command": "ng default-build autocomplete" + }, + { + "command": "node build/clear-default-theme.js autocomplete" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/autocomplete && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build autocomplete && ng pack autocomplete && node build/publish.js autocomplete --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/autocomplete/lib/src/TiAutocompleteComponent.ts b/src/autocomplete/lib/src/TiAutocompleteComponent.ts new file mode 100644 index 0000000..31c640f --- /dev/null +++ b/src/autocomplete/lib/src/TiAutocompleteComponent.ts @@ -0,0 +1,374 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ContentChild, EventEmitter, Input, Output, TemplateRef, ViewChild } from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiDroplistComponent } from '@opentiny/ng-droplist'; +import { debounceTime, switchMap } from 'rxjs/operators'; +import { TiTextComponent } from '@opentiny/ng-text'; +import { empty, Subject, Subscription } from 'rxjs'; +import { Util } from '@opentiny/ng-utils'; +import { TiPositionType } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; + +/** + * 自动补全输入框组件 + * + */ +@Component({ + selector: 'ti-autocomplete', + templateUrl: './autocomplete.html', + styleUrls: ['./autocomplete.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiAutocompleteComponent)], + host: { + '[class.ti3-autocomplete-container]': 'true', + '(blur)': 'onBlur()', + '(focus)': 'onFocus()' + } +}) +export class TiAutocompleteComponent extends TiFormComponent { + /** + * 输入框的占位文本 + */ + @Input() placeholder: string = ''; + /** + * 是否开启清除功能, + * + * 10.1.0/9.1.0版本之后默认不开启,在此之前版本默认开启 + */ + @Input() clearable: boolean = false; + /** + * 输入框允许的最大字符数 + */ + @Input() maxlength: number; + /** + * 下拉面板宽度。 + * + * 1."justified": 下拉面板的宽度与输入框宽度保持一致; + * + * 2."auto": 下拉面板的宽度根据下拉项的内容自动撑开; + * + * 3.固定的下拉面板宽度: 不小于输入框的宽度,例如:"200px" + */ + @Input() panelWidth: string = 'justified'; + /** + * 下拉面板最大高度 + */ + @Input() panelMaxHeight: string; + /** + * 下拉面板要展示的键值 + */ + @Input() labelKey: string = 'label'; + /** + * 下拉建议项 + */ + @Input() options: Array = []; + /** + * tip 提示方向 + */ + @Input() tipPosition: TiPositionType = 'right'; + /** + * + * 下拉面板是否添加在 body 上 + */ + @Input() appendToBody: boolean = true; + /** + * + * tip 最大宽度 + */ + @Input() tipMaxWidth: string; + /** + * 当聚焦或值改变时触发的回调,提供设置建议项的时机, + * + * 参数:组件实例 + */ + @Output() readonly suggest: EventEmitter = new EventEmitter(); + /** + * 点击清除按钮时触发的回调。 + * + */ + @Output() readonly clear: EventEmitter = new EventEmitter(); + /** + * 选中选项时触发的回调, + * + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + /** + * 自定义下拉建议项模板 + * + */ + @ContentChild('item', { static: true }) itemTemplate: TemplateRef; + /** + * @ignore + * droplist选项值 + */ + public selected: any; + /** + * @ignore + * + */ + public suggestions: Array = []; + /** + * @ignore + * 是否聚焦的标志位 + */ + public isFocused: boolean = false; + /** + * 最后一次下拉建议项 + */ + private lastSuggestions: Array = []; + + protected versionInfo: string = super.getVersion(packageInfo); + + /** + * @ignore + */ + @ViewChild('input', { static: true }) textComp: TiTextComponent; + /** + * @ignore + */ + @ViewChild('droplist', { static: false }) dropListComp: TiDroplistComponent; + /** + * @ignore + */ + public inputChangeObserve: Subject = new Subject(); + private inputChangeSub: Subscription; + // 根据数组中属性查找是否有匹配项 + private static findFirstIndex(arr: any, key: string, value: string): number { + if (!(arr instanceof Array)) { + return -1; + } + + return arr.findIndex((i: any) => i[key] === value); + } + + ngOnInit(): void { + super.ngOnInit(); + this.setFocusableElems([this.textComp.nativeElement]); + this.createInputChangeObserve(); + if (this.clearable) { + this.renderer.setAttribute(this.textComp.nativeElement, 'clearable', ''); + } + } + + ngOnDestroy(): void { + // 修正SSR报错:TypeError: Cannot read property 'unsubscribe' of undefined + this.inputChangeSub && this.inputChangeSub.unsubscribe(); + } + + // 组件交互方法集合--start + /** + * @ignore + * 输入框中内容改变事件 + */ + public onInputChange(value: string): void { + if (this.disabled || !this.isFocused) { + return; + } + + this.inputChangeObserve.next(value); + } + /** + * @ignore + * 两种情况下触发 + * 1.在suggestion面板展开的情况下,通过hover选中一项,然后按下enter + * 2.在suggestion面板展开的情况下,通过鼠标点击选中一项 + */ + public onDroplistChange(value: { label?: string }): void { + if (value) { + this.model = value[this.labelKey] || value.label; + } + } + /** + * @ignore + */ + public onFocus(): void { + if (this.disabled) { + return; + } + this.isFocused = true; + if (this.isInputClear()) { + // 如果是点击清除按钮,值会改变,那么就会在onInputChange中处理 + return; + } + + this.showSuggestions(); + } + /** + * @ignore + */ + public onBlur(): void { + // 问题现象:组件聚焦情况下,手动切换到其他小窗口应用,再在当前页面点击组件以外的地方,下拉面板展开无法收起(此时组件处于失焦状态)#2626; + // 问题根因:以上操作中当点击组件以外的地方,组件会有快速的聚焦再失焦的过程;聚焦时打开面板添加了延时处理,组件再失焦后时机早于聚焦,导致下拉面板无法隐藏; + // 解决方案:失焦时也添加延时处理; + setTimeout(() => { + this.dropListComp && this.dropListComp.hide(); + this.isFocused = false; + }, 0); + } + /** + * @ignore + * + * 避免滚动页面下拉框隐藏之后组件仍聚焦时再次点击,下拉框无法展开 + * + * mousedown在focus事件之前执行 + */ + public onInputMousedown(): void { + if (this.disabled || !this.isFocused || this.isInputClear()) { + // 如果是点击清除按钮,值会改变,那么就会在onInputChange中处理 + return; + } + + this.showSuggestions(); + } + /** + * @ignore + * 点击叉号时触发 + * + */ + onClear(event: MouseEvent): void { + this.clear.emit(event); + } + + /** + * @ignore + * 选中事件,鼠标或enter选中选项后 + * + */ + onSelect(option: any): void { + this.select.emit(option); + } + + /** + * @ignore + * 防止原生input框select(选中文本)事件触发时,冒泡到ti-autocomplete的select(选中选项)事件 + * + */ + inputSelect(event: Event) { + event.stopPropagation(); + } + + // 组件交互方法集合--end + + // 内部公共方法集合--start + /** + * @description: 创建inputValue的observable,确保收集2ms内的数据后再更新下拉 + * 触发该observable时,使用next方法 + */ + private createInputChangeObserve(): void { + this.inputChangeSub = this.inputChangeObserve + .pipe( + debounceTime(200), // 200ms延迟执行,解决请求太频繁问题 + // TODO: 在点击清除按钮或者快捷键删除时数据不准确,可能导致在这些操作时触发不了下面的逻辑 + // distinctUntilChanged(),避免前后两次相同数据重复处理,只有上次数据和200ms后的数据不相等时才触发后续动作。 + switchMap( + // TODO: 这个switchMap有可能没有生效。测试用例增加,switchMap + (value: string) => { + if (this.isFocused) { + if (this.suggest.observers.length === 0) { + this.lastSuggestions = this.suggestions; + this.suggestions = this.filter(value); + if (this.suggestions.length > 0) { + this.show(); + } else { + this.dropListComp && this.dropListComp.hide(); + } + } else { + this.suggest.emit(this); + } + } + // OnPush模式下,异步刷新都需要手动触发。 + this.changeDetectorRef.markForCheck(); + return empty(); + } + ) + ) + .subscribe(); + } + /** + * 设置下拉建议项数据 + * + * @param value 下拉建议项数组 + */ + public setSuggestions(value: Array): void { + this.lastSuggestions = this.suggestions; + this.suggestions = value; + if (this.suggestions.length > 0) { + this.show(); + } else { + this.dropListComp.hide(); + } + + // OnPush模式下,异步获取数据需要手动触发 + this.changeDetectorRef.markForCheck(); + } + + private filter(searchWord: string): Array { + if (this.options && this.options.length >= 0) { + // 搜索结果临时值。结果默认值,是原数据 + let searchResult: Array = this.options; + // 如果搜索词存在 + if (!Util.isEmptyString(searchWord)) { + // 在集合中搜索 + searchResult = searchResult.filter((option: any) => { + return option[this.labelKey].toLowerCase().indexOf(searchWord.toLowerCase()) >= 0; + }); + + if (this.options.length > 0 && TiAutocompleteComponent.findFirstIndex(searchResult, this.labelKey, searchWord) === -1) { + searchResult.unshift({ id: searchWord, [this.labelKey]: searchWord }); + } + } + + return searchResult; + } + + return []; + } + + private show(): void { + if (this.dropListComp.isShow) { + // 搜索时,按需重新定位 + if (this.lastSuggestions.length !== this.suggestions.length) { + setTimeout(() => { + this.dropListComp.rePosition(true); + // OnPush模式下,异步刷新都需要手动触发。否则会出现闪动 + this.changeDetectorRef.markForCheck(); + }, 0); + } + } else { + // 数据更新后,未及时通知到droplist,初始化时按照默认值[]绘制视图,需延时处理 + setTimeout(() => { + this.dropListComp.show(); + this.selected = undefined; // 为了去掉选中样式 + // OnPush模式下,异步刷新都需要手动触发。 + this.changeDetectorRef.markForCheck(); + }, 0); + } + } + + private isInputClear(): boolean { + return this.textComp.isShowClear && this.textComp.isClearActive; + } + + private showSuggestions(): void { + if (this.suggest.observers.length === 0) { + this.lastSuggestions = this.suggestions; + this.suggestions = this.filter(this.model); + if (this.suggestions.length > 0) { + this.show(); + } + } else { + this.suggest.emit(this); + } + } + // 内部公共方法集合--end +} diff --git a/src/autocomplete/lib/src/TiAutocompleteModule.ts b/src/autocomplete/lib/src/TiAutocompleteModule.ts new file mode 100644 index 0000000..3d8e526 --- /dev/null +++ b/src/autocomplete/lib/src/TiAutocompleteModule.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiDroplistModule } from '@opentiny/ng-droplist'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiAutocompleteComponent } from './TiAutocompleteComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiDroplistModule, TiTextModule], + exports: [TiAutocompleteComponent], + declarations: [TiAutocompleteComponent] +}) +export class TiAutocompleteModule {} + +export { TiAutocompleteComponent } from './TiAutocompleteComponent'; diff --git a/src/autocomplete/lib/src/autocomplete.html b/src/autocomplete/lib/src/autocomplete.html new file mode 100644 index 0000000..1b23c08 --- /dev/null +++ b/src/autocomplete/lib/src/autocomplete.html @@ -0,0 +1,37 @@ + + + + + + + {{item[labelKey]}} + + diff --git a/src/autocomplete/lib/src/autocomplete.less b/src/autocomplete/lib/src/autocomplete.less new file mode 100644 index 0000000..6088ec7 --- /dev/null +++ b/src/autocomplete/lib/src/autocomplete.less @@ -0,0 +1,22 @@ +:host.ti3-autocomplete-container { + display: inline-block; + vertical-align: middle; + border-radius: var(--ti-common-border-radius-normal); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + background-color: var(--ti-common-color-bg-white-normal); // У��������ʽ�����ڸ�Ԫ���ϣ������ڴ˴�����background-color���� + &:hover { + border-color: var(--ti-common-color-line-hover); + } + &[tifocused] { + border-color: var(--ti-common-color-line-active); + } + &[disabled] { + border-color: var(--ti-common-color-line-disabled); + background-color: var(--ti-common-color-bg-disabled); + } + & .ti3-autocomplete-input[tiText] { + width: 100%; + border: none !important; + background-color: transparent; // ����͸��������Ϊ�˸���tiText���õİ�ɫ������ʹti-autocomplete���õ�background-color��Ϊ��Ч��� + } +} diff --git a/src/avatar/demo/karma.conf.js b/src/avatar/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/avatar/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/avatar/demo/project.json b/src/avatar/demo/project.json new file mode 100644 index 0000000..3c58302 --- /dev/null +++ b/src/avatar/demo/project.json @@ -0,0 +1,98 @@ +{ + "projectType": "application", + "root": "src/avatar/demo", + "sourceRoot": "src/avatar/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/avatar", + "index": "src/avatar/demo/src/index.html", + "main": "src/avatar/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/avatar/demo/tsconfig.app.json", + "assets": [ + "src/avatar/demo/src/favicon.ico", + "src/avatar/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/nav_logo", + "output": "/assets/nav_logo/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "avatar-demo:build:production" + }, + "development": { + "browserTarget": "avatar-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js avatar" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/avatar/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/avatar/demo/tsconfig.spec.json", + "karmaConfig": "src/avatar/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/avatar/demo/src/app/AppComponent.ts b/src/avatar/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/avatar/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/avatar/demo/src/app/AppModule.ts b/src/avatar/demo/src/app/AppModule.ts new file mode 100644 index 0000000..0d069c2 --- /dev/null +++ b/src/avatar/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { AvatarTestModule } from './avatar/AvatarTestModule'; + +@NgModule({ + imports: [ + AvatarTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/avatar/demo/src/app/IndexComponent.ts b/src/avatar/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..c870c33 --- /dev/null +++ b/src/avatar/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { AvatarTestModule } from './avatar/AvatarTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = AvatarTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/avatar/demo/src/app/app.html b/src/avatar/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/avatar/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/avatar/demo/src/app/avatar/AvatarImageComponent.ts b/src/avatar/demo/src/app/avatar/AvatarImageComponent.ts new file mode 100644 index 0000000..ed9d7a2 --- /dev/null +++ b/src/avatar/demo/src/app/avatar/AvatarImageComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './avatar-image.html' +}) +export class AvatarImageComponent { + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL']; + imgUrl: string = `${this.baseUrl}assets/nav_logo/logo.png`; +} diff --git a/src/avatar/demo/src/app/avatar/AvatarImageErrorTestComponent.ts b/src/avatar/demo/src/app/avatar/AvatarImageErrorTestComponent.ts new file mode 100644 index 0000000..084a87a --- /dev/null +++ b/src/avatar/demo/src/app/avatar/AvatarImageErrorTestComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './avatar-image-error-test.html' +}) +export class AvatarImageErrorTestComponent { + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL']; + imgUrl: string = `${this.baseUrl}assets/nav_logo/logo.png`; +} diff --git a/src/avatar/demo/src/app/avatar/AvatarShapeComponent.ts b/src/avatar/demo/src/app/avatar/AvatarShapeComponent.ts new file mode 100644 index 0000000..cb44a71 --- /dev/null +++ b/src/avatar/demo/src/app/avatar/AvatarShapeComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './avatar-shape.html' +}) +export class AvatarShapeComponent {} diff --git a/src/avatar/demo/src/app/avatar/AvatarSizeComponent.ts b/src/avatar/demo/src/app/avatar/AvatarSizeComponent.ts new file mode 100644 index 0000000..ab6393c --- /dev/null +++ b/src/avatar/demo/src/app/avatar/AvatarSizeComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './avatar-size.html' +}) +export class AvatarSizeComponent {} diff --git a/src/avatar/demo/src/app/avatar/AvatarStyleComponent.ts b/src/avatar/demo/src/app/avatar/AvatarStyleComponent.ts new file mode 100644 index 0000000..51bb1e8 --- /dev/null +++ b/src/avatar/demo/src/app/avatar/AvatarStyleComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './avatar-style.html' +}) +export class AvatarStyleComponent { + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL']; + imgUrl: string = `${this.baseUrl}assets/nav_logo/logo.png`; +} diff --git a/src/avatar/demo/src/app/avatar/AvatarTestModule.ts b/src/avatar/demo/src/app/avatar/AvatarTestModule.ts new file mode 100644 index 0000000..7e7fe3e --- /dev/null +++ b/src/avatar/demo/src/app/avatar/AvatarTestModule.ts @@ -0,0 +1,60 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiAvatarModule } from '@opentiny/ng'; + +import { AvatarSizeComponent } from './AvatarSizeComponent'; +import { AvatarShapeComponent } from './AvatarShapeComponent'; +import { AvatarTextComponent } from './AvatarTextComponent'; +import { AvatarImageComponent } from './AvatarImageComponent'; +import { AvatarStyleComponent } from './AvatarStyleComponent'; +import { AvatarImageErrorTestComponent } from './AvatarImageErrorTestComponent'; + +@NgModule({ + imports: [CommonModule, TiAvatarModule, RouterModule.forChild(AvatarTestModule.ROUTES)], + declarations: [ + AvatarSizeComponent, + AvatarShapeComponent, + AvatarTextComponent, + AvatarStyleComponent, + AvatarImageComponent, + AvatarImageErrorTestComponent + ] +}) +export class AvatarTestModule { + public static OWNER: string = '李麟玄lilinxuan00650216'; + public static readonly LINKS: Array = [{ href: 'components/TiAvatarComponent.html', label: 'Avatar' }]; + public static readonly ROUTES: Routes = [ + { + path: 'avatar/avatar-size', + component: AvatarSizeComponent, + data: { label: '尺寸' } + }, + { + path: 'avatar/avatar-shape', + component: AvatarShapeComponent, + data: { label: '形状' } + }, + { + path: 'avatar/avatar-text', + component: AvatarTextComponent, + data: { label: '文本头像' } + }, + { + path: 'avatar/avatar-image', + component: AvatarImageComponent, + data: { label: '图像头像' } + }, + { + path: 'avatar/avatar-style', + component: AvatarStyleComponent, + data: { label: '样式' } + }, + { + path: 'avatar/avatar-image-error-test', + component: AvatarImageErrorTestComponent, + data: { label: '图像加载失败' } + } + ]; +} diff --git a/src/avatar/demo/src/app/avatar/AvatarTextComponent.ts b/src/avatar/demo/src/app/avatar/AvatarTextComponent.ts new file mode 100644 index 0000000..7979dd5 --- /dev/null +++ b/src/avatar/demo/src/app/avatar/AvatarTextComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './avatar-text.html' +}) +export class AvatarTextComponent { + txt: string = this.trimSpace('Taylor Swift'); + /** + * 取字符串首字母 + * @param text + * @returns + */ + trimSpace(text): string { + let res: string = text[0]; + for (let i = 0; i < text.length; i++) { + if (text[i] === ' ' && i !== text.length - 1) { + res = res.concat(text[i + 1]); + } + } + return res; + } +} diff --git a/src/avatar/demo/src/app/avatar/avatar-image-error-test.html b/src/avatar/demo/src/app/avatar/avatar-image-error-test.html new file mode 100644 index 0000000..9102665 --- /dev/null +++ b/src/avatar/demo/src/app/avatar/avatar-image-error-test.html @@ -0,0 +1,12 @@ +
+ + +

图像类头像加载失败

+

图像+文字类头像加载失败

+
diff --git a/src/avatar/demo/src/app/avatar/avatar-image.html b/src/avatar/demo/src/app/avatar/avatar-image.html new file mode 100644 index 0000000..c33bf9f --- /dev/null +++ b/src/avatar/demo/src/app/avatar/avatar-image.html @@ -0,0 +1,12 @@ +
+ + +

图像类头像

+

图像加文字类头像

+
diff --git a/src/avatar/demo/src/app/avatar/avatar-shape.html b/src/avatar/demo/src/app/avatar/avatar-shape.html new file mode 100644 index 0000000..53b5bfe --- /dev/null +++ b/src/avatar/demo/src/app/avatar/avatar-shape.html @@ -0,0 +1,5 @@ +
+ + + +
diff --git a/src/avatar/demo/src/app/avatar/avatar-size.html b/src/avatar/demo/src/app/avatar/avatar-size.html new file mode 100644 index 0000000..133a7a1 --- /dev/null +++ b/src/avatar/demo/src/app/avatar/avatar-size.html @@ -0,0 +1,14 @@ +
+ + + + + + +
90px
+
xl
+
large
+
middle
+
small
+
xs
+
diff --git a/src/avatar/demo/src/app/avatar/avatar-style.html b/src/avatar/demo/src/app/avatar/avatar-style.html new file mode 100644 index 0000000..e45300e --- /dev/null +++ b/src/avatar/demo/src/app/avatar/avatar-style.html @@ -0,0 +1,16 @@ +
+ + + + +
diff --git a/src/avatar/demo/src/app/avatar/avatar-text.html b/src/avatar/demo/src/app/avatar/avatar-text.html new file mode 100644 index 0000000..6ba3182 --- /dev/null +++ b/src/avatar/demo/src/app/avatar/avatar-text.html @@ -0,0 +1,6 @@ +
+ + + + +
diff --git a/src/avatar/demo/src/app/avatar/webdoc/avatar-demos.js b/src/avatar/demo/src/app/avatar/webdoc/avatar-demos.js new file mode 100644 index 0000000..115bdcb --- /dev/null +++ b/src/avatar/demo/src/app/avatar/webdoc/avatar-demos.js @@ -0,0 +1,76 @@ +export default { + column: '2', + demos: [ + { + demoId: 'avatar-size', + name: { + 'zh-CN': '尺寸', + 'en-US': 'avatar size', + }, + desc: { + 'zh-CN': '

通过属性size配置头像的尺寸,包括 xl、large、middle(默认)、small、xs 五种类型,也可以使用 string 类型自定义。

', + 'en-US': '

avatar size

', + }, + apis: [ + 'TiAvatarComponent.properties.size' + ], + }, + { + demoId: 'avatar-shape', + name: { + 'zh-CN': '形状', + 'en-US': 'avatar shape', + }, + desc: { + 'zh-CN': '

通过属性shape配置头像的形状,包括 circle(默认)、square 两种类型。

', + 'en-US': '

avatar shape

', + }, + apis: [ + 'TiAvatarComponent.properties.shape' + ], + }, + { + demoId: 'avatar-image', + name: { + 'zh-CN': '图像类头像', + 'en-US': 'avatar image', + }, + desc: { + 'zh-CN': '

通过属性imgSrc配置图像类头像,也可以同时配置text属性使文字悬浮于图片上,当图片加载不成功时出发回调。

', + 'en-US': '

avatar type

', + }, + apis: [ + 'TiAvatarComponent.properties.imgSrc', + 'TiAvatarComponent.properties.imgAlt', + 'TiAvatarComponent.properties.text' + ], + }, + { + demoId: 'avatar-text', + name: { + 'zh-CN': '文本类头像', + 'en-US': 'avatar text', + }, + desc: { + 'zh-CN': '

通过属性text配置文本类头像,根据用户输入的字自动调整文本大小。

', + 'en-US': '

avatar type

', + }, + apis: [ + 'TiAvatarComponent.properties.text' + ], + }, + { + demoId: 'avatar-style', + name: { + 'zh-CN': '自定义样式', + 'en-US': 'avatar style', + }, + desc: { + 'zh-CN': '

通过原生属性style配置头像的样式。

', + 'en-US': '

avatar style

', + }, + apis: [ + ], + }, + ], +}; diff --git a/src/avatar/demo/src/app/avatar/webdoc/avatar.cn.md b/src/avatar/demo/src/app/avatar/webdoc/avatar.cn.md new file mode 100644 index 0000000..16fb3b2 --- /dev/null +++ b/src/avatar/demo/src/app/avatar/webdoc/avatar.cn.md @@ -0,0 +1,23 @@ +--- +title: Avatar 头像 +--- +# Avatar 头像 + +
+ +Avatar 是用户头像组件。 + +```typescript +import { TiAvatarModule } from '@opentiny/ng'; +``` + +
+ +
+ +Avatar 是用户头像组件。 + +```typescript +import { TiAvatarModule } from '@opentiny/ng'; +``` +
diff --git a/src/avatar/demo/src/app/avatar/webdoc/avatar.en.md b/src/avatar/demo/src/app/avatar/webdoc/avatar.en.md new file mode 100644 index 0000000..9fcb90f --- /dev/null +++ b/src/avatar/demo/src/app/avatar/webdoc/avatar.en.md @@ -0,0 +1,28 @@ +--- +title: Accordion +--- +# Accordion + +
+ +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
+ +
+ +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/avatar/demo/src/favicon.ico b/src/avatar/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/avatar/demo/src/index.html b/src/avatar/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/avatar/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/avatar/demo/src/main.ts b/src/avatar/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/avatar/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/avatar/demo/test.ts b/src/avatar/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/avatar/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/avatar/demo/tsconfig.app.json b/src/avatar/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/avatar/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/avatar/demo/tsconfig.spec.json b/src/avatar/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/avatar/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/avatar/lib/index.ts b/src/avatar/lib/index.ts new file mode 100644 index 0000000..7150f1f --- /dev/null +++ b/src/avatar/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiAvatarModule'; diff --git a/src/avatar/lib/ng-package.json b/src/avatar/lib/ng-package.json new file mode 100644 index 0000000..4271267 --- /dev/null +++ b/src/avatar/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/avatar", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/avatar/lib/package.json b/src/avatar/lib/package.json new file mode 100644 index 0000000..b1f54bd --- /dev/null +++ b/src/avatar/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-avatar", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/avatar/lib/project.json b/src/avatar/lib/project.json new file mode 100644 index 0000000..e299415 --- /dev/null +++ b/src/avatar/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/avatar/lib", + "sourceRoot": "src/avatar/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/avatar"], + "options": { + "project": "src/avatar/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/avatar"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js avatar" + }, + { + "command": "ng default-build avatar" + }, + { + "command": "node build/clear-default-theme.js avatar" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/avatar && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build avatar && ng pack avatar && node build/publish.js avatar --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/avatar/lib/src/TiAvatarComponent.ts b/src/avatar/lib/src/TiAvatarComponent.ts new file mode 100644 index 0000000..8a51076 --- /dev/null +++ b/src/avatar/lib/src/TiAvatarComponent.ts @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, ViewChild, Renderer2 } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * 头像展示组件 + * + */ +@Component({ + selector: 'ti-avatar', + templateUrl: './avatar.html', + styleUrls: ['./avatar.less'], + host: { + '[class.ti3-avt-shape-square]': 'shape ==="square"', + '[class.ti3-avt-xs]': 'size === "xs"', + '[class.ti3-avt-small]': 'size === "small"', + '[class.ti3-avt-large]': 'size === "large"', + '[class.ti3-avt-xl]': 'size === "xl"', + '[style.width]': 'size', + '[style.height]': 'size' + }, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiAvatarComponent extends TiBaseComponent { + /** + * 头像形状 + * + */ + @Input() shape: 'circle' | 'square' = 'circle'; + /** + * 头像尺寸(Chrome浏览器中限制最小的字体为12px,故默认头像中的icon最小为12px) + * + */ + @Input() size: 'xs' | 'small' | 'middle' | 'large' | 'xl' | string = 'middle'; + /** + * 图片类头像地址 + * + */ + @Input() imgSrc: string; + /** + * 图片类头像加载不成功时显示的文字 + * + */ + @Input() imgAlt: string; + /** + * 文本类头像 + * + */ + @Input() text: string; + /** + * @ignore + * + */ + @ViewChild('textRef', { static: false }) textRef: ElementRef; + /** + * @ignore + * 头像中图标或文字的尺寸 + */ + public fontSize: string; + /** + * @ignore + * 文本缩放的尺寸 + */ + public txtScale: number; + protected versionInfo: string = super.getVersion(packageInfo); + + constructor(protected elementRef: ElementRef, protected renderer2: Renderer2, private cdRef: ChangeDetectorRef) { + super(elementRef, renderer2); + } + /** + * @ignore + * error: This event is not cancelable and does not bubble. + * event.defaultPrevented indicat whether the call to Event.preventDefault() canceled the event. + * @param $event + */ + imgError($event: Event): void { + if (!$event.defaultPrevented) { + this.imgSrc = ''; + } + } + + /** + * @ignore + * 获取scale及其参数这个字符串 + * @param str + * @returns + */ + getScale(str): string { + return `scale(${str})`; + } + /** + * @ignore + * + */ + ngAfterViewInit(): void { + // 根据用户输入的文本的width来确认文本尺寸缩放的比例 + if (this.text) { + const textWidth: number = this.textRef.nativeElement.offsetWidth; + const textHeight: number = this.textRef.nativeElement.offsetHeight; + this.txtScale = Math.min(this.nativeElement.offsetWidth / textWidth, this.nativeElement.offsetHeight / textHeight, 1); + } + //当size的输入为用户自定义字符串时,捕获头像的宽度,计算对应的字体的大小 + this.fontSize = this.nativeElement.offsetWidth / 3 + 'px'; + this.cdRef.detectChanges(); + } +} diff --git a/src/avatar/lib/src/TiAvatarModule.ts b/src/avatar/lib/src/TiAvatarModule.ts new file mode 100644 index 0000000..6477a6f --- /dev/null +++ b/src/avatar/lib/src/TiAvatarModule.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiAvatarComponent } from './TiAvatarComponent'; +import { TiIconModule } from '@opentiny/ng-icon'; + +@NgModule({ + imports: [CommonModule, TiIconModule], + exports: [TiAvatarComponent], + declarations: [TiAvatarComponent] +}) +export class TiAvatarModule {} +export { TiAvatarComponent } from './TiAvatarComponent'; diff --git a/src/avatar/lib/src/avatar.html b/src/avatar/lib/src/avatar.html new file mode 100644 index 0000000..5e93450 --- /dev/null +++ b/src/avatar/lib/src/avatar.html @@ -0,0 +1,14 @@ + + + {{text}} + + + + diff --git a/src/avatar/lib/src/avatar.less b/src/avatar/lib/src/avatar.less new file mode 100644 index 0000000..fbb1eb3 --- /dev/null +++ b/src/avatar/lib/src/avatar.less @@ -0,0 +1,62 @@ +:host { + --ti-avatar-width-default: var(--ti-common-size-14x); + --ti-avatar-width-xs: var(--ti-common-size-9x); + --ti-avatar-width-small: var(--ti-common-size-12x); + --ti-avatar-width-large: var(--ti-common-size-16x); + --ti-avatar-width-xl: var(--ti-common-size-18x); + --ti-avatar-border-radius-square: 15%; +} + +:host { + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--ti-common-border-radius-3); + width: var(--ti-avatar-width-default); + height: var(--ti-avatar-width-default); + font-size: calc(var(--ti-avatar-width-default) / 3); + background-color: var(--ti-common-color-bg-secondary); + color: var(--ti-common-color-icon-white); +} + +:host.ti3-avt-shape-square { + border-radius: var(--ti-avatar-border-radius-square); +} + +:host.ti3-avt-xs { + width: var(--ti-avatar-width-xs); + height: var(--ti-avatar-width-xs); + font-size: calc(var(--ti-avatar-width-xs) / 3); +} + +:host.ti3-avt-small { + width: var(--ti-avatar-width-small); + height: var(--ti-avatar-width-small); + font-size: calc(var(--ti-avatar-width-small) / 3); +} + +:host.ti3-avt-large { + width: var(--ti-avatar-width-large); + height: var(--ti-avatar-width-large); + font-size: calc(var(--ti-avatar-width-large) / 3); +} + +:host.ti3-avt-xl { + width: var(--ti-avatar-width-xl); + height: var(--ti-avatar-width-xl); + font-size: calc(var(--ti-avatar-width-xl) / 3); +} + +.ti3-avt-img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + position: relative; +} + +.ti3-avt-txt { + position: absolute; + white-space: nowrap; + text-shadow: black 0.1em 0.1em 0.2em; +} diff --git a/src/base/lib/index.ts b/src/base/lib/index.ts new file mode 100644 index 0000000..3562952 --- /dev/null +++ b/src/base/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiBaseModule'; diff --git a/src/base/lib/ng-package.json b/src/base/lib/ng-package.json new file mode 100644 index 0000000..d8ff706 --- /dev/null +++ b/src/base/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/base", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/base/lib/package.json b/src/base/lib/package.json new file mode 100644 index 0000000..4e3f94b --- /dev/null +++ b/src/base/lib/package.json @@ -0,0 +1,10 @@ +{ + "name": "@opentiny/ng-base", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/forms": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/base/lib/project.json b/src/base/lib/project.json new file mode 100644 index 0000000..9d5dfd5 --- /dev/null +++ b/src/base/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/base/lib", + "sourceRoot": "src/base/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/base"], + "options": { + "project": "src/base/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/base"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js base" + }, + { + "command": "ng default-build base" + }, + { + "command": "node build/clear-default-theme.js base" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/base && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build base && ng pack base && node build/publish.js base --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/base/lib/src/TiAutofocusComponent.ts b/src/base/lib/src/TiAutofocusComponent.ts new file mode 100644 index 0000000..0701a1d --- /dev/null +++ b/src/base/lib/src/TiAutofocusComponent.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component } from '@angular/core'; +import { TiBaseComponent } from './TiBaseComponent'; + +/** + * @ignore + * 该基类用于组件设置autofocsu时获取焦点,涉及组件button、text、textarea + */ + +@Component({ + selector: 'ti-autofocus', + template: '' +}) +export class TiAutofocusComponent extends TiBaseComponent { + /** + * 此处在ngAfterViewInit生命周期内调用focus方法,因为textarea组件较为特殊 + * textarea组件在ngAfterViewInit会将宿主元素包裹在一层dom中,在此之前调用focus方法,获取不到焦点 + */ + ngAfterViewInit(): void { + if (this.nativeElement.attributes.autofocus) { + this.nativeElement.focus(); + } + } +} diff --git a/src/base/lib/src/TiBaseComponent.ts b/src/base/lib/src/TiBaseComponent.ts new file mode 100644 index 0000000..4b27e1d --- /dev/null +++ b/src/base/lib/src/TiBaseComponent.ts @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + AfterContentChecked, + AfterContentInit, + AfterViewChecked, + AfterViewInit, + Component, + DoCheck, + ElementRef, + Input, + OnChanges, + OnDestroy, + OnInit, + Renderer2, + SimpleChanges +} from '@angular/core'; +import { Util } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; + +// 如果base有@Component,那么所有子类都不必写constructor了,应该跟constructor注入有关 +/** + * 基类,已实现[id],子类也可以使用hostRef, nativeElement, renderer + * + * @ignore + */ +@Component({ + selector: 'ti-base', + template: '' +}) +export class TiBaseComponent + implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy +{ + /** + * HTML属性id,自动化测试要求必须给id赋值 + */ + // TODO:改名字 + @Input() id: string; // TODO:覆盖Angular的[id]变量。因为Angular[id]变量只能在ngAfterViewInit拿到,太迟。 + /** + * @ignore + * 这里用any,原因HTMLElement->Element->Node, attributes定义的格式太死,不能有多余字段,可能是类型定义bug + * interface Node extends EventTarget {readonly attributes: NamedNodeMap; + */ + public nativeElement: any; // 宿主元素 + protected versionInfo: string = `${packageInfo.name}:${packageInfo.version};`; + + constructor(protected hostRef: ElementRef, protected renderer: Renderer2) { + this.nativeElement = this.hostRef.nativeElement; + } + ngOnChanges(changes: SimpleChanges): void { + // id动态变化时,重新设置id + if (changes['id'] && !changes['id'].firstChange) { + this.setId(); + } + } + ngOnInit(): void { + this.setId(); + this.setVersion(); + } + ngDoCheck(): void {} + ngAfterContentInit(): void {} + ngAfterContentChecked(): void {} + ngAfterViewInit(): void {} + ngAfterViewChecked(): void {} + ngOnDestroy(): void {} + // 给宿主设置Id + private setId(): void { + // 空id都自动创建id + if (Util.isEmptyString(this.id)) { + this.id = Util.getUniqueId('ti_auto_id'); + } + this.renderer.setProperty(this.nativeElement, 'id', this.id); + } + /** + * 生成宿主Id:如果组件必须要有Id,那么调用此方法,无ID时会自动生成一个ID。 + */ + protected creatId(): void { + // 如果用户没有写宿主id + if (Util.isEmptyString(this.id)) { + // 生成唯一id + this.id = Util.getUniqueId('ti_auto_id'); + this.setId(); + } + } + + protected setVersion(): void { + this.renderer.setAttribute(this.nativeElement, 'tiversion', this.versionInfo); + } + + protected getVersion(packageInfomation): string { + return `${packageInfomation.name}:${packageInfomation.version};${this.versionInfo}`; + } + + /** + * @ignore + * 供内部子元素使用 + * @example + * {{label}} + * @param suffix 传入的字符串 + * @returns 返回的字符串this.id + '_' + suffix + */ + public appendId(suffix: string): string { + // 应该不存在空id了,因为所有组件都有自动生成id + return `${this.id}_${suffix}`; + } +} +/* +用户写宿主id三种方式: +1)给HTML属性id赋字符串id='mycheckbox', +2)给HTML属性id赋变量[attr.id]='mycheckboxid',用户不太会这么用,暂不考虑支持。 +3)给DOM属性id赋变量[id]='mycheckboxid' +组件开发读写宿主id的三种方法: +1)读HTML属性id constructor(@Attribute('id') public id ) +2)写HTML属性id @HostBinding('attr.id') id: string; +写DOM属性id @HostBinding('id') id: string; +3)读写DOM属性id constructor(private hostRef: ElementRef ) ngAfterViewInit() { ...this.hostRef.nativeElement.id... } +注意: +1)开发Checkbox时,是类似属性指令的组件,宿主元素是input,其他组件暂未尝试。 +2)id='mycheckbox',constructor就能拿到。但Angular[id]变量最早能在ngAfterViewInit()拿到,很迟。 +3)自己定义一个id变量,会覆盖angular的id变量,在ngOnInit()就可以拿到,很早。 +综上,nativeElement.id读写id,可以兼容以上三种用户写宿主id。 +方案一:ngAfterViewInit()取到Angular的id,很晚。但模板中需要使用id,设置太迟,需要强制刷新。 +ngAfterViewInit() { +this.setId(); // 取得[id]变量赋值,必须在ngAfterViewInit()。其他周期拿不到。 +... +} +private setId() { +// 如果用户没有写宿主id +if (Util.isEmptyString(this.hostNode.id)) { +// 生成唯一id +this.hostNode.id = Util.getUniqueId('checkbox'); +} +// 获取宿主id +this.id = this.hostNode.id; +this.cdRef.detectChanges(); // 强制检测,刷新视图中使用的id变量。因为有时视图绘制更早,变量变化后试图未更新。 +// TODO: 强制检测变化会不会有性能负担? +} +方案二:自定义id覆盖覆盖Angular的[id]变量,ngOnInit()取到id,很早。无需强制刷新。 +@Input('id') id: string; // 覆盖Angular的[id]变量。因为Angular[id]变量只能在ngAfterViewInit拿到,太迟。 +ngOnInit() { +... +this.setId(); +} +private setId() { +// 如果用户没有写宿主id +if (Util.isEmptyString(this.id)) { +// 生成唯一id +this.id = Util.getUniqueId('checkbox'); +} +// 设置宿主id +this.renderer.setProperty(this.hostNode, 'id', this.id); +// this.cdRef.detectChanges(); // 无需强制检测 +} +方案三:想办法尽早取到Angular的[id],暂无接口。 +可能Angular确实是在AfterViewInit前一刻才取到[id][disabled]。 +PrimeNG等库,也是用@Input('id')去覆盖[id] [disabled]等。 + +Chorme79版报相同id警告: +{{label}} +原生标签这样写法,哪怕appendID函数返回空字符串,也会有存在id属性。这样页面中存在多个相同空id。 +解决方案:哪怕用户没有赋id,组件都会自动创建id,那么所有组件都有不同id,内部的span等也会有不同id。 +*/ diff --git a/src/base/lib/src/TiBaseModule.ts b/src/base/lib/src/TiBaseModule.ts new file mode 100644 index 0000000..b6fe136 --- /dev/null +++ b/src/base/lib/src/TiBaseModule.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiBaseComponent } from './TiBaseComponent'; +import { TiFormComponent } from './TiFormComponent'; +import { TiRadioBaseComponent } from './TiRadioBaseComponent'; +import { TiWholeComponent } from './TiWholeComponent'; +import { TiAutofocusComponent } from './TiAutofocusComponent'; +// 在使用tiny npm包生产环境编辑时,要求基类也注册。 +/** + * @ignore + */ +@NgModule({ + imports: [], + exports: [], + declarations: [TiBaseComponent, TiFormComponent, TiRadioBaseComponent, TiWholeComponent, TiAutofocusComponent] +}) +export class TiBaseModule {} +export { TiBaseComponent } from './TiBaseComponent'; +export { TiFormComponent } from './TiFormComponent'; +export { TiRadioBaseComponent } from './TiRadioBaseComponent'; +export { TiWholeComponent } from './TiWholeComponent'; +export { TiAutofocusComponent } from './TiAutofocusComponent'; diff --git a/src/base/lib/src/TiFormComponent.ts b/src/base/lib/src/TiFormComponent.ts new file mode 100644 index 0000000..ca0ed20 --- /dev/null +++ b/src/base/lib/src/TiFormComponent.ts @@ -0,0 +1,529 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + Output, + Renderer2, + SimpleChanges, + Type +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { TiBaseComponent } from './TiBaseComponent'; +import { TiBrowser } from '@opentiny/ng-utils'; + +// SSR环境需要 +declare let global: any; + +// TODO: aria-label +/** + * 表单基类,已实现autofocus tabindex disabled focus() blur() focus/blur/change事件 ControlValueAccessor + */ +@Component({ + // lib编译要求@Component + selector: 'ti-form', + template: '' +}) +export class TiFormComponent extends TiBaseComponent implements ControlValueAccessor { + /** + * HTML 属性 tabindex + */ + @Input() tabindex: string = '0'; + /** + * 是否禁用 + */ + @Input() disabled: boolean; // 因为Angualr原生[disabled]->setDisabledState机制在单选多选没有生效。所以,这里接管了。 + /** + * HTML 事件 focus + */ + // TODO:ip组件等是触发原生HTML的focus事件,便于校验? + @Output('focus') readonly focusEvent: EventEmitter = new EventEmitter(); + /** + * HTML 事件 blur + */ + @Output('blur') readonly blurEvent: EventEmitter = new EventEmitter(); + /** + * @ignore + * HTML 事件 change + */ + // blur时model值改变触发change + @Output('change') readonly changeEvent: EventEmitter = new EventEmitter(); + + private unlistenMap: Map void> = new Map void>(); // 执行此函数,可以取消监听focus和blur + + private focusModel: any; // 落焦点时,记录model值 + + protected focusElem: any; // autofocus focus()将要聚焦的元素。默认是宿主元素,子类设置focusableElems后,为第一个非disabled可聚焦元素。 + /** + * 整体focus/blur事件参与的元素,和disabled设置。 + * 组件具有多个input输入框: + * 1)无元素聚焦->任一元素聚焦=整体聚焦事件。 + * 2)任一元素失焦->其他元素无焦点=整体失焦事件。 + */ + private focusableElems: Array = []; + // 实现表单4接口1注册 + private _model: any; // 传入模型值 + private ngAfterViewCheckedFirst: boolean = true; // 是否第一次执行初始化 + + constructor(protected hostRef: ElementRef, protected renderer: Renderer2, protected changeDetectorRef: ChangeDetectorRef) { + super(hostRef, renderer); + } + + protected onModelChange: (model: any) => void; // 模型change回调函数,触发模型值改变 + protected onModelTouched: () => void; // 模型blur回调函数,触发校验 + protected focusedElem: any; // 已聚焦元素, 在子组件中有使用 + /** + * @ignore + * 实现表单注册接口 + * @param type 类型 + * @returns 对象 + */ + public static getValueAccessor(type: Type): any { + return { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => type), + multi: true + }; + } + /** + * 是否组件整体聚焦 + */ + public get focused(): boolean { + return this.nativeElement.attributes.tiFocused !== undefined; + } + /** + * 为Dom元素添加/移除属性,此处用于disabled和focused属性的操作 + * @param element 需要操作的Dom元素 + * @param attr 操作的属性 + * @param value 属性值,为true时属性值即为属性值,为false时,移除属性 + */ + protected setAttr(element: any, attr: string, value: boolean): void { + // 如果可聚焦元素是原生input等,[disabled]属性会生效。 + // this.renderer.setProperty(this.focusElem, 'disabled', isDisabled); // 参照angular官方设置disabled源码,true和fasle都考虑了 + // 如果可聚焦元素是组件、div,[disabled]属性不会生效。但setAttribute会增加一项html属性。setProperty不增加。 + if (value) { + this.renderer.setAttribute(element, attr, attr); + } else { + this.renderer.removeAttribute(element, attr); + } + } + ngOnInit() { + super.ngOnInit(); + // 表单中统一加该属性,用于表单校验中的样式选择器 + this.renderer.setAttribute(this.nativeElement, 'tiForm', ''); + } + /** + * 监听disabled变化 + */ + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + if (changes['disabled']) { + this.setDisabledState(changes['disabled'].currentValue); + } + } + /** + * 注意:子类继承自Form基类,如果有this.setFocusableElems([xxx]), 那么要更晚调用super.ngAfterViewChecked()。 + * 子类ngAfterViewChecked(): void { + * this.setFocusableElems([xxx]); + * // 更晚调用super + * super.ngAfterViewChecked(); + * } + */ + ngAfterViewChecked(): void { + super.ngAfterViewChecked(); + if (this.ngAfterViewCheckedFirst) { + this.ngAfterViewCheckedFirst = false; + // 尽量延迟autoFocus的执行。因为有些组件在ngAfterViewInit才拿得到ngFor ngIf内的元素作为可聚焦元素。 + this.autoFocus(); + } + } + /** + * 当做生命周期用吧,监听模型值变化。包括writeValue和this.model=赋值 这两个时刻。 + * @param model 模型值 + */ + protected ngOnModelChange(model: any): void {} + /** + * 设置tab键/点击时的聚焦元素,会顺便设置组件focus()聚焦时可聚焦元素(第一个非disabled的可聚焦元素)。 + * 当可聚焦元素变动,或者单一元素disabled状态变动,都要重新调用此方法设置。 + * 建议一般在ngOnInit生命周期调用,如果ngFor/ngIf内元素在ngOnInit找不到那么可在ngAfterViewInit生命周期调用。 + * 注意:所有生命周期,都要调用类似super.ngOnInit() + * + * @param elems 可聚焦元素数组Array,第一个非disabled元素为整个组件的可聚焦元素。 + * + * 目前使用分为几种情况: + * 1)一般表单组件,含有可聚焦子元素。 + * 组件主动设置可聚焦元素数组this.setFocusableElems([xxxElement, yyyElement]); + * 2)有的表单组件,永远不聚焦。 + * slider/list/menulist/droplist,统一不调用此接口。 + * a.不调用接口时,@Input() tabindex: string = '0';未生效。但用户在html标签上设置的属性tabindex="0"等,会生效。 + * b.调用了this.setFocusableElems([]);时,强制清除了标签属性tabindex。用户设置的属性tabindex="1",会无效,不出现在标签上。 + * 3)少量组件,内部没有可聚焦元素。但是皮儿需要聚焦,ti-dominator。 + * 需要设置this.setFocusableElems([this.nativeElement]); + */ + protected setFocusableElems(elems: Array): void { + if (!elems) { + // 不支持组件传入 null 和undefined,支持传入[] + return; + } + // TODO: 第二次调用时,如果焦点元素减少,那么需要处理tabindex + // TODO: 支持用户动态第二次设置setFocusableElems。其实,actionmenu会动态设置 + this.focusableElems = elems; + // 将要上焦点的元素 + this.setFocusElem(); + // 给所有子元素添加tabindex。 + this.setElemsTabindex(); + // 设置宿主标签上的focus()/blur()方法 + this.setNativeElementFocusFn(); + // 设置focus/blur事件监听 + this.setFocusListen(); + } + /** + * 调用focus()函数,会触发组件整体聚焦,此时焦点元素。 + */ + private setFocusElem(): void { + this.focusElem = undefined; + // 寻找第一个非disabled元素,作为可聚焦元素 + for (const elem of this.focusableElems) { + // 正常态选取第一个非禁用元素落焦点。禁用态选第一个元素即可。 + if (elem && (elem.attributes.disabled === undefined || this.disabled)) { + this.focusElem = elem; + break; + } + } + } + /** + * 去除皮上的tabindex,给内部可聚焦子元素添加tabindex + * 当然,特殊情况:this.setFocusableElems([this.nativeElement]);时,又会给皮上补上tabindex + * 本来此方法,可以合入setFocusEvents(),减少遍历循环。但为了清晰,依然单独为函数。 + */ + private setElemsTabindex(): void { + // 去除组件标签上的tabindex属性。 + this.renderer.removeAttribute(this.nativeElement, 'tabindex'); + this.focusableElems.forEach((elem: any) => { + // 给每一个元素添加tabindex + this.setTabindex(elem); + }); + } + /** + * @ignore + */ + public getFocusableElems(): Array { + return this.focusableElems; + } + /** + * 宿主元素标签上添加类似原生focus()方法。 + * 用户调用方法: document.getElementById('ti-xxx').focus(); + * 其实,更推荐调用组件实例上的方法tiXxxComp.focus(); + */ + private setNativeElementFocusFn(): void { + // 注意:这里必须用箭头函数,以保证运行时this变量不变。 + if (this.focusElem !== this.nativeElement) { + // 避免无限循环调用,所以要有此条件限制 + // SSR环境直接返回,是因为SSR环境报错:Cannot assign to read only property 'focus' of object + // 采用['nativeElement']['focus']也解决不了 + if (typeof global !== 'undefined') { + return; + } + this.nativeElement.focus = (): void => { + this.focus(); + }; + this.nativeElement.blur = (): void => { + this.blur(); + }; + } + } + /** + * 设置组件中的可聚焦元素并监听事件 + */ + private setFocusListen(): void { + // 原来:先取消所有旧监听,再建立所有新监听。在onpush select分页有问题,select没有监听到focus事件。 + // 原因不明,貌似js执行了多线程一般,A线程setFocusElems取消监听再建立监听,B线程就没有监听到。 + // 改为:如果这个元素,新旧监听都有,那么就不取消监听了,直接保留原监听。 + + // 先遍历去除监听。 + this.unlistenMap.forEach((value: () => void, key: HTMLElement, map: Map void>) => { + if (!this.focusableElems.includes(key)) { + // 解除监听 + value(); + // 从map中移除 + this.unlistenMap.delete(key); + } + }); + + // 再遍历添加监听。 + this.focusableElems.forEach((elem: HTMLElement) => { + // 如果不存在,则添加到map + if (!this.unlistenMap.has(elem)) { + // 监听focus,并获得取消监听句柄 + const unlistenFocusFn: () => void = this.renderer.listen(elem, 'focus', this.focusCallbackFn); + // 监听blur,并获得取消监听句柄 + const unlistenBlurFn: () => void = this.renderer.listen(elem, 'blur', this.blurCallbackFn); + // 取消监听focus和blur的函数 + const unlistenFocusBlurFn: () => void = () => { + unlistenFocusFn(); + unlistenBlurFn(); + }; + // 存入map,按照元素和取消监听函数。 + this.unlistenMap.set(elem, unlistenFocusBlurFn); + } + }); + } + /** + * focus回调函数 + */ + private focusCallbackFn: (event: FocusEvent) => void = (event: FocusEvent) => { + this.focusedElem = event.target; + // 非IE浏览器支持event.relatedTarget,使用event.relatedTarget进行处理 + if (!TiBrowser.isIE()) { + // 当为内部焦点转移,不做处理 + if (this.focusableElems.length > 1 && this.focusableElems.includes(event.relatedTarget)) { + return; + } + } else { + // IE浏览器的处理:浏览器不支持relatedTarget情况下的处理,参见blur中的说明 + if ((event.target as any).tiFocusFrom) { + // return; + // 情况一:失焦后强制聚焦 + if (event.target === (event.target as any).tiFocusFrom) { + // 面板滚动条IE兼容:在focusin中,让失焦元素强制聚焦,会走到这里。 + + return; + } + // 情况二:内部焦点转移 + if (this.focusableElems.length > 1 && this.focusableElems.includes((event.target as any).tiFocusFrom)) { + return; + } + // 情况三:较难理解:外部焦点转移时,(event.target as any).tiFocusFrom也是有值的。 + // 这种情况,是因为组件嵌套使用,此聚焦元素分别从属于多个组件。 + // 对于A组件来说,是焦点内部转移。对于B组件来说,是焦点外部转移。 + } + } + // 设置宿主元素的聚焦属性,用于标识该组件处于聚焦状态,该属性可用于css选择器:在宿主元素上增加属性focused + this.setAttr(this.nativeElement, 'tiFocused', true); + // dateDominator需要根据是否聚焦来判断显示日期图标还是叉号图标,若不做标记,无法识别此变更点。 + this.changeDetectorRef.markForCheck(); + // 触发整体聚焦事件 + this.focusEvent.emit(event); + // 保存focus时model值,blur时如果model值改变,则发送change事件 + this.focusModel = this.model; + }; + /** + * blur 回调函数 + */ + private blurCallbackFn: (event: FocusEvent) => void = (event: FocusEvent) => { + // 非IE浏览器支持event.relatedTarget,使用event.relatedTarget进行处理 + if (!TiBrowser.isIE()) { + // 当为内部焦点转移,不做处理 + if (this.focusableElems.length > 1 && this.focusableElems.includes(event.relatedTarget)) { + return; + } + } else { + // 已经失焦的元素,清除记录焦点从哪里而来。 + (event.target as any).tiFocusFrom = undefined; + + // 如果失焦元素强制聚焦,不触发整体失焦事件。 + if (event.target === document.activeElement) { + // 面板滚动条IE兼容:在focusin中,让失焦元素强制聚焦,会走到这里。 + // 即将聚焦(实际上已经聚焦)的元素,记录一下焦点从哪儿来:从自己来的 + (document.activeElement as any).tiFocusFrom = event.target; + + return; + } + + // 浏览器不支持relatedTarget:IE下不支持 + // 判断是否为内部焦点转移通过元素方法: + // 两个元素的焦点切换时,会先触发a元素的失焦,再触发b元素的聚焦 + // a失焦处理:当前聚焦元素为该元素的可聚焦元素时,则认为是内部焦点转移,并且将聚焦元素的relatedTarget赋值为当前失焦元素,方便blur时的处理; + // b聚焦处理:判断当前聚焦元素的relatedTarget是否为内部可聚焦元素,如果是的话,则判定其为内部焦点转移 + if (this.focusableElems.length > 1 && this.focusableElems.includes(document.activeElement)) { + // 即将聚焦(实际上已经聚焦)的元素,记录一下焦点从哪儿来。 + (document.activeElement as any).tiFocusFrom = event.target; // element setAttribute赋值不支持对象形式,因此使用该方式 + + return; + } + } + this.focusedElem = undefined; + // 取消宿主元素的聚焦属性 + this.setAttr(this.nativeElement, 'tiFocused', false); + // dateDominator需要根据是否聚焦来判断显示日期图标还是叉号图标,若不做标记,无法识别此变更点。 + this.changeDetectorRef.markForCheck(); + // 触发整体失焦事件 + this.blurEvent.emit(event); + // blur时如果model值改变,则发送change事件 + if (this.model !== this.focusModel) { + this.changeEvent.emit(this.model); + } + // 触发组件的touched回调,标志其已被touch过 + if (this.onModelTouched) { + this.onModelTouched(); + } + }; + /** + * 给元素设置焦点 + */ + public focus(): void { + if (this.focusElem) { + this.focusElem.focus(); + } + } + /** + * 给元素移除焦点 + */ + public blur(): void { + if (this.focusedElem) { + this.focusedElem.blur(); // 只对当前聚焦元素进行失焦处理 + } + } + /** + * Tiny组件各种行为,尽量和原生一致。 autofocus也许可以做成一个指令 + */ + private autoFocus(): void { + if (this.nativeElement.attributes.autofocus) { + this.focus(); + } + } + /** + * 实现类表单组件的set和get方法 + */ + get model(): any { + return this._model; + } + /** + * 设置model值,如果有变化,则触发ngModelChange + */ + set model(model: any) { + if (this._model === model) { + return; + } + this._model = model; + if (this.onModelChange) { + this.onModelChange(model); + } + this.ngOnModelChange(model); + } + /* 表单4接口start */ + /** + * @ignore + * 如果用户改变了[ngModel]绑定的变量,那么Angular会通知到这里 + * 子类如果没有特殊需求,都需要首行调用super.writeValue(xxx) + * + * super.writeValue 和 this.model=xxx的区别: + * super.writeValue不会触发ngModelChange + * this.model=xxx会触发ngModelChange + * @param model any类型 + */ + writeValue(model: any): void { + this._model = model; + this.ngOnModelChange(model); + + // Onpush模式,强制刷新 + this.changeDetectorRef.markForCheck(); + // OnPush模式仅监测四种变化: + // @Input引用变化 + // 本组件和子组件的事件 + // markForCheck/detectChanges + // 异步Pipe + + // OnPush模式缺陷:没有监测ngmodel变化,也没有检测setTimeout等异步 + } + /** + * @ignore + * Angular将(ngModelChange)绑定的函数,通知到这里。 + * 当组件内部model值改变,需要调用这个函数向外通知。 + * @param fn 回调函数 + */ + registerOnChange(fn: (model: any) => void): void { + this.onModelChange = fn; + } + /** + * @ignore + * Blur时,或者需要校验时,需要调用此函数。(可能描述不准确,需要再查资料) + * @param fn 回调函数 + */ + registerOnTouched(fn: () => void): void { + this.onModelTouched = fn; + } + /** + * @ignore + * 用户绑定在[disabled]上的变量变化,会通知到这里。 + * ti-xxx组件皮上设置[disabled],会给皮上设置disabled属性,也会给所有可聚焦元素设置disabled属性。 + * 如有特殊需求,子类可以不使用super.setDisabledState(), 而是直接覆盖。 + * Angular原生[disabled]机制:checkbox/radio走不进原生setDisabledState()。原生[disabled][id]等取值也很晚。 + * 注意: Form基类@Input() disabled已经接管了Angular原生[disabled]机制。 + * ngOnChanges监听到disabled值(第一次和后续改变),会通知到这里。 + * @param isDisabled 是否禁用 + */ + setDisabledState?(isDisabled: boolean): void { + // 表单this.form.controls.myselect.disable();会直接通知到这里,但不会通知到ngOnChanges。所以要设置this.disabled = + this.disabled = isDisabled; + // 给宿主添加disabled属性 + this.setAttr(this.nativeElement, 'disabled', isDisabled); + // 给聚焦事件元素添加disabled属性 + this.focusableElems.forEach((element: any) => { + this.setAttr(element, 'disabled', isDisabled); + // 如果可聚焦元素是自定义组件、div等,那么标签上的disabled属性不生效。 + // 需要借助于tabindex控制是否允许焦点。 + // 控制禁用样式,方案一:(推荐)less里[disabled]属性模仿伪类,ti-xxx皮上的[disabled]更可靠 + // 方案二:html的ngClass用Form基类disabled变量。 + this.setTabindex(element); + }); + } + /** + * 设置tabindex + * @param element 原生标签元素 + */ + private setTabindex(element: any): void { + // 整体禁用,或者小元素被禁用。 + if (element.attributes.disabled) { + // 禁用时 + this.renderer.removeAttribute(element, 'tabindex'); + // A和input已经有disabled属性了,不需要加tabindex + // 普通元素,也不需要加tabindex + + // A标签禁用时想要点击和tab都不落焦点,唯一方法是去除href + if (element.tagName === 'A') { + this.renderer.removeAttribute(element, 'href'); + } + } else { + if (element.tagName === 'A') { + // 特例:A标签不设置tabindex='0'。Chrome浏览器不设置tabindex效果:点击时无蓝框,tab时有蓝框。 + if (this.tabindex === '0') { + this.renderer.removeAttribute(element, 'tabindex'); + } else { + this.renderer.setAttribute(element, 'tabindex', this.tabindex); + } + // disabled时去除的href,添加回来 + if (!element.attributes.href) { + // 默认所有的A标签都是tiny使用的标签,href都是javascript:void(0) + this.renderer.setAttribute(element, 'href', 'javascript:void(0)'); + } + } else { + this.renderer.setAttribute(element, 'tabindex', this.tabindex); + } + } + } + /* 表单4接口end */ +} +/** + * tabindex相关知识: + * 所有已设置tabindex的元素,都会点击聚焦。哪怕
test
也会点击聚焦。 + * Tab键,会按照顺序聚焦:1,2,3,4,...0,按照此顺序聚焦。 + * tabindex='-1'不参与Tab键, 但是可以点击聚焦。 + * A标签不设置tabindex效果:Chrome浏览器点击时无蓝框,tab时有蓝框。其他浏览器有差异。 + * A标签禁用时想要点击和tab都不落焦点,唯一方法是去除href + * input标签不设置tabindex效果:Chrome浏览器点击时/tab时都有蓝框。 + * 如果设置tabindex='',或者tabindex='tabindex',这样非法设置,Chrome相当于没有设置。其他浏览器有差异 + */ diff --git a/src/base/lib/src/TiRadioBaseComponent.ts b/src/base/lib/src/TiRadioBaseComponent.ts new file mode 100644 index 0000000..daa2e3c --- /dev/null +++ b/src/base/lib/src/TiRadioBaseComponent.ts @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ElementRef, HostListener, Input, ViewChild } from '@angular/core'; +import { TiBrowser, Util } from '@opentiny/ng-utils'; +import { TiFormComponent } from './TiFormComponent'; +// 编译lib要求注册Component,不能用抽象类 +/** + * 单选/多选共同的基类。单选多选唯一的差异:单选只有在未选状态才接受空格键改变状态。 + */ +@Component({ + // lib编译要求@Component + selector: 'ti-radiobase', + template: '' +}) +export class TiRadioBaseComponent extends TiFormComponent { + /** + * 选项文本 + */ + @Input() label: string; + /** + * @ignore + */ + @ViewChild('labelRef', { static: true }) private labelRef: ElementRef; + /** + * @ignore + */ + @ViewChild('proxyRef', { static: true }) private proxyRef: ElementRef; + + ngOnInit(): void { + super.ngOnInit(); + this.creatId(); + this.setFocusableElems([this.proxyRef.nativeElement]); + this.moveNode(); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + this.renderer.removeChild(this.renderer.parentNode(this.nativeElement), this.renderer.nextSibling(this.nativeElement)); + } + + /** + * 将input子元素label,调整为兄弟元素。 + */ + private moveNode(): void { + const parentNode: any = this.renderer.parentNode(this.nativeElement); + const nextNode: any = this.renderer.nextSibling(this.nativeElement); + this.renderer.insertBefore(parentNode, this.labelRef.nativeElement, nextNode); + } + + // TODO: 原始节点改为0高度,不显示。那么所有宿主元素focus都转移到proxy。 + /** + * @ignore + * 点选元素后,聚集到对应的checkbox-skin + * 经测试,这段逻辑,是为了解决第一次进入页面时 radio/checkbox 火狐下点击 label 无法聚焦。 + * @param event 鼠标点击事件 + */ + @HostListener('click', ['$event']) public onHostClick(event: MouseEvent): void { + if (!this.nativeElement.disabled && TiBrowser.isFirefox()) { + this.focusElem.focus(); + this.renderer.setStyle(this.focusElem, 'outlineStyle', 'none'); + } + } + + // 阻止事件冒泡:当子组件中有input元素时,change事件会冒泡到父组件 + // 问题:多选树模板包含复选框组件,树组件中有自己的change事件,点击复选框会触发两次change事件 + // 一次是树组件自身触发,一次是复选框组件冒泡,故需阻止该事件,例如下边代码会打印日志 + /** + * @ignore + */ + @HostListener('change', ['$event']) public onHostChange(event: Event): void { + event.stopPropagation(); + } + /** + * @ignore + * 阻止checkbox-skin和checkbox-label的事件冒泡,防止上层dom绑定的事件被多次触发 + * 原因:span和label使用for和input关联,input上的click等事件也会触发span/label的事件处理, + * 如果不做处理,上层dom绑定的事件会被触发两次 + * @param event 鼠标点击事件 + */ + public onLabelClick(event: MouseEvent): void { + event.stopPropagation(); + } + /** + * @ignore + * 快捷键的处理(Enter和Space):考虑到交互的友好性及与原生的一致性, + * 在keyup中做相应的事件处理(keydown和keypress会存在一次点击,多次触发的情况); + * 此外,需要阻止浏览器默认事件(空格键会触发页面滚动条滚到底部的行为, + * 默认事件的阻止需要在keyup之前,因此此处在keydown中阻止) + * @param event 键盘按键事件 + */ + public onSpaceKeydown(event: KeyboardEvent): void { + // with type info + if (!this.nativeElement.disabled) { + event.preventDefault(); + } + } + /** + * @ignore + * @param event 键盘按键事件 + */ + public onSpaceKeyup(event: KeyboardEvent): void { + // with type info + if (!this.nativeElement.disabled && this.canChange()) { + this.nativeElement.checked = !this.nativeElement.checked; // 设置元素的选中状态 + // this.cdRef.detectChanges(); 执行并不能触发checked改变检查,所以只有下面的change事件才可以。 + // 触发check原生的change事件"createEvent" in document. 因为原生只有blur时才触发onchange + Util.trigger(this.nativeElement, 'change'); + } + } + /** + * @ignore + * 这是单选多选唯一的差异:单选只有在未选状态才接受空格键改变状态。 + * @returns 默认返回true,多选框会继承这个方法。单选框重写这个方法。 + */ + protected canChange(): boolean { + return true; + } +} // end of class diff --git a/src/base/lib/src/TiWholeComponent.ts b/src/base/lib/src/TiWholeComponent.ts new file mode 100644 index 0000000..f2d9386 --- /dev/null +++ b/src/base/lib/src/TiWholeComponent.ts @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, SimpleChanges } from '@angular/core'; +import { TiFormComponent } from './TiFormComponent'; +/** + * 复原完整model对象基类,基于valueKey时,select/buttongroup实现复原选中值到完整对象 + */ +@Component({ + // lib编译要求@Component + selector: 'ti-whole', + template: '' +}) +export class TiWholeComponent extends TiFormComponent { + /** + * 指定选中项数据的键值 + */ + @Input() valueKey: string; + private _modelWhole: any; + /** + * @ignore + */ + public get modelWhole(): any { + return this._modelWhole; + } + /** + * @ignore + * 用户点选时modelWhole改变,要同时更改model + */ + public set modelWhole(value: any) { + this._modelWhole = value; + if (this.valueKey && value !== undefined && value !== null) { + // 存在valueKey,且value不为空 + this.model = this['multiple'] ? value.map((item: any) => this.valueFn(item)) : this.valueFn(value); + } else { + this.model = value; + } + } + /** + * @ignore + * 找到value值的函数 + */ + @Input() valueFn: (item: any) => any = (item: any) => { + return item[this.valueKey]; + }; + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + // 监听 options/items/suggestions 变化 + if ((changes['options'] || changes['items'] || changes['suggestions']) && this.valueKey) { + // 存在 valueKey 时,modelWhole 需通过匹配 valueKey 来重新赋值(解决动态改变options/items时选中项不高亮问题 issues#1332) + this.writeValue(this.model); + } + } + /** + * @ignore + * 每次用户主动设置ngModel值时,需要顺便更改modelWhole + * @param model 设置的ngModel值 + */ + writeValue(model: any): void { + super.writeValue(model); + if (this.valueKey && model !== undefined && model !== null) { + // 存在valueKey,且value不为空 + // 历史设计缺陷:select总数据集是options,buttongroup总数据集是items,tagsinput总数据集是suggestions + let options: Array = this['options'] || this['items'] || this['suggestions']; + if (!options) { + return; + } + if (options[0]?.children) { + // 如果是分组,把options扁平化处理 + const _options: Array = []; + options.forEach((option: any) => { + _options.push(...option.children); + }); + options = _options; + } + // 直接去写内部值,不触发set函数 + this._modelWhole = this['multiple'] + ? options.filter((item: any) => model.includes(this.valueFn(item))) + : options.find((item: any) => this.valueFn(item) === model); + } else { + this._modelWhole = model; + } + } +} diff --git a/src/browserslist b/src/browserslist new file mode 100644 index 0000000..227c82e --- /dev/null +++ b/src/browserslist @@ -0,0 +1,8 @@ +# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +last 3 Chrome versions +last 3 Firefox versions +last 2 Safari versions +Edge >=12 diff --git a/src/button/demo/karma.conf.js b/src/button/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/button/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/button/demo/project.json b/src/button/demo/project.json new file mode 100644 index 0000000..fc4d9c9 --- /dev/null +++ b/src/button/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/button/demo", + "sourceRoot": "src/button/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/button", + "index": "src/button/demo/src/index.html", + "main": "src/button/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/button/demo/tsconfig.app.json", + "assets": ["src/button/demo/src/favicon.ico", "src/button/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "button-demo:build:production" + }, + "development": { + "browserTarget": "button-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js button" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/button/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/button/demo/tsconfig.spec.json", + "karmaConfig": "src/button/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/button/demo/src/app/AppComponent.ts b/src/button/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/button/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/button/demo/src/app/AppModule.ts b/src/button/demo/src/app/AppModule.ts new file mode 100644 index 0000000..ef86b66 --- /dev/null +++ b/src/button/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ButtonTestModule } from './button/ButtonTestModule'; + +@NgModule({ + imports: [ + ButtonTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/button/demo/src/app/IndexComponent.ts b/src/button/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..092cf4d --- /dev/null +++ b/src/button/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ButtonTestModule } from './button/ButtonTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = ButtonTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/button/demo/src/app/app.html b/src/button/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/button/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/button/demo/src/app/button/ButtonColorComponent.ts b/src/button/demo/src/app/button/ButtonColorComponent.ts new file mode 100644 index 0000000..4e6befe --- /dev/null +++ b/src/button/demo/src/app/button/ButtonColorComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './button-color.html' +}) +export class ButtonColorComponent {} diff --git a/src/button/demo/src/app/button/ButtonDisabledComponent.ts b/src/button/demo/src/app/button/ButtonDisabledComponent.ts new file mode 100644 index 0000000..e46a6ee --- /dev/null +++ b/src/button/demo/src/app/button/ButtonDisabledComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './button-disabled.html' +}) +export class ButtonDisabledComponent {} diff --git a/src/button/demo/src/app/button/ButtonEventComponent.ts b/src/button/demo/src/app/button/ButtonEventComponent.ts new file mode 100644 index 0000000..61e60eb --- /dev/null +++ b/src/button/demo/src/app/button/ButtonEventComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './button-event.html' +}) +export class ButtonEventComponent { + myLogs: Array = []; + onClick(event: MouseEvent): void { + this.myLogs = [...this.myLogs, `onClick() event = ${event.type}`]; + } +} diff --git a/src/button/demo/src/app/button/ButtonFocusComponent.ts b/src/button/demo/src/app/button/ButtonFocusComponent.ts new file mode 100644 index 0000000..6223555 --- /dev/null +++ b/src/button/demo/src/app/button/ButtonFocusComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './button-focus.html' +}) +export class ButtonFocusComponent {} diff --git a/src/button/demo/src/app/button/ButtonHasborderComponent.ts b/src/button/demo/src/app/button/ButtonHasborderComponent.ts new file mode 100644 index 0000000..a6b8626 --- /dev/null +++ b/src/button/demo/src/app/button/ButtonHasborderComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './button-hasborder.html' +}) +export class ButtonHasborderComponent {} diff --git a/src/button/demo/src/app/button/ButtonHasborderTestComponent.ts b/src/button/demo/src/app/button/ButtonHasborderTestComponent.ts new file mode 100644 index 0000000..47c7242 --- /dev/null +++ b/src/button/demo/src/app/button/ButtonHasborderTestComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './button-hasborder-test.html' +}) +export class ButtonHasborderTestComponent { + hasborder: boolean; +} diff --git a/src/button/demo/src/app/button/ButtonIconComponent.ts b/src/button/demo/src/app/button/ButtonIconComponent.ts new file mode 100644 index 0000000..6c16911 --- /dev/null +++ b/src/button/demo/src/app/button/ButtonIconComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './button-icon.html' +}) +export class ButtonIconComponent {} diff --git a/src/button/demo/src/app/button/ButtonLabelComponent.ts b/src/button/demo/src/app/button/ButtonLabelComponent.ts new file mode 100644 index 0000000..1bdbbea --- /dev/null +++ b/src/button/demo/src/app/button/ButtonLabelComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './button-label.html' +}) +export class ButtonLabelComponent {} diff --git a/src/button/demo/src/app/button/ButtonLoadingComponent.ts b/src/button/demo/src/app/button/ButtonLoadingComponent.ts new file mode 100644 index 0000000..c1c14e2 --- /dev/null +++ b/src/button/demo/src/app/button/ButtonLoadingComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './button-loading.html' +}) +export class ButtonLoadingComponent {} diff --git a/src/button/demo/src/app/button/ButtonOnlyiconComponent.ts b/src/button/demo/src/app/button/ButtonOnlyiconComponent.ts new file mode 100644 index 0000000..470fca6 --- /dev/null +++ b/src/button/demo/src/app/button/ButtonOnlyiconComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './button-onlyicon.html' +}) +export class ButtonOnlyiconComponent {} diff --git a/src/button/demo/src/app/button/ButtonSizeComponent.ts b/src/button/demo/src/app/button/ButtonSizeComponent.ts new file mode 100644 index 0000000..d3b6e40 --- /dev/null +++ b/src/button/demo/src/app/button/ButtonSizeComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './button-size.html' +}) +export class ButtonSizeComponent {} diff --git a/src/button/demo/src/app/button/ButtonTestModule.ts b/src/button/demo/src/app/button/ButtonTestModule.ts new file mode 100644 index 0000000..61a5edf --- /dev/null +++ b/src/button/demo/src/app/button/ButtonTestModule.ts @@ -0,0 +1,90 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiIconModule, TiTipModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { ButtonColorComponent } from './ButtonColorComponent'; +import { ButtonIconComponent } from './ButtonIconComponent'; +import { ButtonSizeComponent } from './ButtonSizeComponent'; +import { ButtonEventComponent } from './ButtonEventComponent'; +import { ButtonDisabledComponent } from './ButtonDisabledComponent'; +import { ButtonFocusComponent } from './ButtonFocusComponent'; +import { ButtonOnlyiconComponent } from './ButtonOnlyiconComponent'; +import { ButtonLoadingComponent } from './ButtonLoadingComponent'; +import { ButtonHasborderComponent } from './ButtonHasborderComponent'; +import { ButtonHasborderTestComponent } from './ButtonHasborderTestComponent'; +import { ButtonTipComponent } from './ButtonTipComponent'; +import { ButtonLabelComponent } from './ButtonLabelComponent'; + +@NgModule({ + imports: [CommonModule, TiButtonModule, TiIconModule, TiTipModule, DemoLogModule, RouterModule.forChild(ButtonTestModule.ROUTES)], + declarations: [ + ButtonColorComponent, + ButtonIconComponent, + ButtonSizeComponent, + ButtonDisabledComponent, + ButtonEventComponent, + ButtonFocusComponent, + ButtonOnlyiconComponent, + ButtonLoadingComponent, + ButtonHasborderComponent, + ButtonHasborderTestComponent, + ButtonTipComponent, + ButtonLabelComponent + ] +}) +export class ButtonTestModule { + static readonly LINKS: Array = [{ href: 'components/TiButtonComponent.html', label: 'Button' }]; + static readonly ROUTES: Routes = [ + { + path: 'button/button-color', + component: ButtonColorComponent + }, + { + path: 'button/button-size', + component: ButtonSizeComponent + }, + { + path: 'button/button-icon', + component: ButtonIconComponent + }, + { + path: 'button/button-hasborder', + component: ButtonHasborderComponent + }, + { + path: 'button/button-label', + component: ButtonLabelComponent + }, + { + path: 'button/button-disabled', + component: ButtonDisabledComponent + }, + { + path: 'button/button-event', + component: ButtonEventComponent + }, + { + path: 'button/button-focus', + component: ButtonFocusComponent + }, + { + path: 'button/button-onlyicon', + component: ButtonOnlyiconComponent + }, + { + path: 'button/button-loading', + component: ButtonLoadingComponent + }, + { + path: 'button/button-tip', + component: ButtonTipComponent + }, + { + path: 'button/button-hasborder-test', + component: ButtonHasborderTestComponent + } + ]; +} diff --git a/src/button/demo/src/app/button/ButtonTipComponent.ts b/src/button/demo/src/app/button/ButtonTipComponent.ts new file mode 100644 index 0000000..7fb46a7 --- /dev/null +++ b/src/button/demo/src/app/button/ButtonTipComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './button-tip.html' +}) +export class ButtonTipComponent { + tipContent: string = '这是按钮的提示信息'; +} diff --git a/src/button/demo/src/app/button/button-color.html b/src/button/demo/src/app/button/button-color.html new file mode 100644 index 0000000..bbfcf38 --- /dev/null +++ b/src/button/demo/src/app/button/button-color.html @@ -0,0 +1,3 @@ + + + diff --git a/src/button/demo/src/app/button/button-disabled.html b/src/button/demo/src/app/button/button-disabled.html new file mode 100644 index 0000000..26c5d29 --- /dev/null +++ b/src/button/demo/src/app/button/button-disabled.html @@ -0,0 +1 @@ + diff --git a/src/button/demo/src/app/button/button-event.html b/src/button/demo/src/app/button/button-event.html new file mode 100644 index 0000000..6526b6c --- /dev/null +++ b/src/button/demo/src/app/button/button-event.html @@ -0,0 +1 @@ + diff --git a/src/button/demo/src/app/button/button-focus.html b/src/button/demo/src/app/button/button-focus.html new file mode 100644 index 0000000..4b5f914 --- /dev/null +++ b/src/button/demo/src/app/button/button-focus.html @@ -0,0 +1 @@ + diff --git a/src/button/demo/src/app/button/button-hasborder-test.html b/src/button/demo/src/app/button/button-hasborder-test.html new file mode 100644 index 0000000..b8f75e3 --- /dev/null +++ b/src/button/demo/src/app/button/button-hasborder-test.html @@ -0,0 +1,31 @@ +

描述

+

hasBorder接口:默认值为true,配置为false,兼容undefined显示为有边框按钮(接口值为:true)

+

示例

+

1.不带边框的文本按钮

+ + +

2.不同颜色

+ + +

3.不同大小

+ + + + + +

4. 带图标+正常状态

+ + +

5.加载状态

+ + + diff --git a/src/button/demo/src/app/button/button-hasborder.html b/src/button/demo/src/app/button/button-hasborder.html new file mode 100644 index 0000000..381dd55 --- /dev/null +++ b/src/button/demo/src/app/button/button-hasborder.html @@ -0,0 +1 @@ + diff --git a/src/button/demo/src/app/button/button-icon.html b/src/button/demo/src/app/button/button-icon.html new file mode 100644 index 0000000..d00e593 --- /dev/null +++ b/src/button/demo/src/app/button/button-icon.html @@ -0,0 +1,4 @@ + diff --git a/src/button/demo/src/app/button/button-label.html b/src/button/demo/src/app/button/button-label.html new file mode 100644 index 0000000..8513c97 --- /dev/null +++ b/src/button/demo/src/app/button/button-label.html @@ -0,0 +1,2 @@ + + diff --git a/src/button/demo/src/app/button/button-loading.html b/src/button/demo/src/app/button/button-loading.html new file mode 100644 index 0000000..80ba5c1 --- /dev/null +++ b/src/button/demo/src/app/button/button-loading.html @@ -0,0 +1,3 @@ + + + diff --git a/src/button/demo/src/app/button/button-onlyicon.html b/src/button/demo/src/app/button/button-onlyicon.html new file mode 100644 index 0000000..ed22d03 --- /dev/null +++ b/src/button/demo/src/app/button/button-onlyicon.html @@ -0,0 +1,3 @@ + diff --git a/src/button/demo/src/app/button/button-size.html b/src/button/demo/src/app/button/button-size.html new file mode 100644 index 0000000..676a9b6 --- /dev/null +++ b/src/button/demo/src/app/button/button-size.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/button/demo/src/app/button/button-tip.html b/src/button/demo/src/app/button/button-tip.html new file mode 100644 index 0000000..2889769 --- /dev/null +++ b/src/button/demo/src/app/button/button-tip.html @@ -0,0 +1 @@ + diff --git a/src/button/demo/src/app/button/webdoc/button-demos.js b/src/button/demo/src/app/button/webdoc/button-demos.js new file mode 100644 index 0000000..53928fb --- /dev/null +++ b/src/button/demo/src/app/button/webdoc/button-demos.js @@ -0,0 +1,134 @@ +export default { + column: '2', + demos: [ + { + demoId: 'button-color', + name: { + 'zh-CN': '按钮颜色', + 'en-US': 'button color' + }, + desc: { + 'zh-CN': '

通过属性color配置按钮颜色,包含defaultdangerprimary三种类型。', + 'en-US': '

button color

' + }, + apis: ['TiButtonComponent.properties.color'] + }, + { + demoId: 'button-size', + name: { + 'zh-CN': '按钮大小', + 'en-US': 'button size' + }, + desc: { + 'zh-CN': '

通过属性size配置按钮大小。', + 'en-US': '

button size

' + }, + apis: ['TiButtonComponent.properties.size'] + }, + { + demoId: 'button-hasborder', + name: { + 'zh-CN': '按钮是否带边框', + 'en-US': 'button hasBorder' + }, + desc: { + 'zh-CN': '

通过属性hasBorder配置按钮是否带边框。

', + 'en-US': '

button hasBorder

' + }, + apis: ['TiButtonComponent.properties.hasBorder'] + }, + { + demoId: 'button-label', + name: { + 'zh-CN': '按钮为确认/取消按钮', + 'en-US': 'button label' + }, + desc: { + 'zh-CN': '

通过属性label配置按钮为确认/取消按钮。

', + 'en-US': '

button label

' + }, + apis: ['TiButtonComponent.properties.label'] + }, + { + demoId: 'button-icon', + name: { + 'zh-CN': '图标按钮', + 'en-US': 'button icon' + }, + desc: { + 'zh-CN': '

通过属性icon配置按钮是否包含图标。

', + 'en-US': '

button icon

' + }, + apis: ['TiButtonComponent.properties.icon'] + }, + { + demoId: 'button-loading', + name: { + 'zh-CN': '加载中', + 'en-US': 'button icon' + }, + desc: { + 'zh-CN': '

通过属性loading配置按钮是否加载中状态。

', + 'en-US': '

button loading

' + }, + apis: ['TiButtonComponent.properties.loading'] + }, + { + demoId: 'button-onlyicon', + name: { + 'zh-CN': '纯图标', + 'en-US': 'button onlyicon' + }, + desc: { + 'zh-CN': '

通过属性onlyIcon配置是否为纯图标。

', + 'en-US': '

button onlyicon

' + }, + apis: ['TiButtonComponent.properties.onlyIcon'] + }, + { + demoId: 'button-disabled', + name: { + 'zh-CN': '禁用按钮', + 'en-US': 'button disabled' + }, + desc: { + 'zh-CN': '

通过属性disabled配置是否为禁用状态。

', + 'en-US': '

button disabled

' + }, + apis: ['TiButtonComponent.properties.disabled'] + }, + { + demoId: 'button-focus', + name: { + 'zh-CN': '自动聚焦', + 'en-US': 'button focus' + }, + desc: { + 'zh-CN': '

通过属性autofocus配置页面重新加载时是否自动聚焦。

', + 'en-US': '

A button should automatically get focus when the page loads

' + } + }, + { + demoId: 'button-tip', + name: { + 'zh-CN': '提示气泡', + 'en-US': 'button tip' + }, + desc: { + 'zh-CN': '

可与 Tip 组件结合来显示按钮提示信息,鼠标悬停到元素上即可出现气泡提示。

', + 'en-US': '

button tip

' + } + }, + { + demoId: 'button-event', + name: { + 'zh-CN': '事件', + 'en-US': 'button event' + }, + desc: { + 'zh-CN': '

按钮点击事件。

', + 'en-US': '

button event

' + } + } + ] +}; diff --git a/src/button/demo/src/app/button/webdoc/button.cn.md b/src/button/demo/src/app/button/webdoc/button.cn.md new file mode 100644 index 0000000..02f3c8a --- /dev/null +++ b/src/button/demo/src/app/button/webdoc/button.cn.md @@ -0,0 +1,25 @@ +--- +title: Button 按钮 +--- + +# Button 按钮 + +
+ +按钮组件一般用于触发一些操作。 + +```typescript +import { TiButtonBaseModule } from '@cloud/tiny-config'; +``` + +
+ +
+ +按钮组件一般用于触发一些操作。 + +```typescript +import { TiButtonModule } from '@opentiny/ng'; +``` + +
diff --git a/src/button/demo/src/app/button/webdoc/button.en.md b/src/button/demo/src/app/button/webdoc/button.en.md new file mode 100644 index 0000000..fa7853d --- /dev/null +++ b/src/button/demo/src/app/button/webdoc/button.en.md @@ -0,0 +1,7 @@ +--- +title: Button +--- + +# Button + +The button component is extended based on the native button component. diff --git a/src/button/demo/src/favicon.ico b/src/button/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/button/demo/src/index.html b/src/button/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/button/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/button/demo/src/main.ts b/src/button/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/button/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/button/demo/test.ts b/src/button/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/button/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/button/demo/tsconfig.app.json b/src/button/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/button/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/button/demo/tsconfig.spec.json b/src/button/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/button/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/button/lib/index.ts b/src/button/lib/index.ts new file mode 100644 index 0000000..ea063b0 --- /dev/null +++ b/src/button/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiButtonModule'; diff --git a/src/button/lib/ng-package.json b/src/button/lib/ng-package.json new file mode 100644 index 0000000..573743f --- /dev/null +++ b/src/button/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/button", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/button/lib/package.json b/src/button/lib/package.json new file mode 100644 index 0000000..55a6ac7 --- /dev/null +++ b/src/button/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-button", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-loading": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/button/lib/project.json b/src/button/lib/project.json new file mode 100644 index 0000000..93ea6a8 --- /dev/null +++ b/src/button/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/button/lib", + "sourceRoot": "src/button/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/button"], + "options": { + "project": "src/button/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/button"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js button" + }, + { + "command": "ng default-build button" + }, + { + "command": "node build/clear-default-theme.js button" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/button && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build button && ng pack button && node build/publish.js button --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/button/lib/src/TiButtonComponent.ts b/src/button/lib/src/TiButtonComponent.ts new file mode 100644 index 0000000..2305d3a --- /dev/null +++ b/src/button/lib/src/TiButtonComponent.ts @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + Input, + Renderer2, + ViewEncapsulation, + ElementRef, + NgZone, + AfterViewInit, + Inject, + ChangeDetectionStrategy +} from '@angular/core'; +import { TiAutofocusComponent } from '@opentiny/ng-base'; +import { DOCUMENT } from '@angular/common'; +import packageInfo from '../package.json'; +import { TiLocale } from '@opentiny/ng-locale'; + +/** + * Button按钮组件 + * + * 尽管这是一个组件,但使用方法有点像属性指令。 + * + * 按钮组件基于原生button组件进行扩展,原生button加tiButton属性即为tiny button,button的多种样式则通过为button设置不同的样式类来进行区分,具体用法见示例。 + * + */ +@Component({ + selector: '[tiButton]', + templateUrl: './button.html', + styleUrls: ['./button.less'], + host: { + '[class.ti3-btn-primary]': 'color === "primary" && hasBorder!== false', + '[class.ti3-btn-danger]': 'color === "danger" && hasBorder!== false', + '[class.ti3-btn-noborder]': 'hasBorder === false', + '[class.ti3-btn-icon]': 'icon && hasBorder!== false', + '[class.ti3-btn-onlyIcon]': 'onlyIcon && hasBorder!== false', + '[class.ti3-btn-large]': 'size === "large" && hasBorder!== false', + '[class.ti3-btn-middle]': 'size === "middle" && hasBorder!== false', + '[class.ti3-btn-small]': 'size === "small" && hasBorder!== false', + '[class.ti3-btn-xs]': 'size === "xs" && hasBorder!== false', + '[class.ti3-btn-loading]': 'loading && hasBorder!== false' + }, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None // 让宿主元素也用上button.less样式。否则,默认胶囊封装避免CSS污染。 +}) +export class TiButtonComponent extends TiAutofocusComponent implements AfterViewInit { + /** + * 按钮颜色 + */ + @Input() color: 'default' | 'danger' | 'primary' = 'default'; + /** + * 是否包含图标 + */ + @Input() icon: boolean = false; + /** + * 是否为纯图标按钮 + */ + @Input() onlyIcon: boolean = false; + /** + * 按钮大小 + */ + @Input() size: 'xs' | 'small' | 'middle' | 'large' = 'middle'; + /** + * 是否处于加载状态,一般用于异步提交场景 + */ + @Input() loading: boolean = false; + /** + * 是否为无边框文本按钮 + */ + @Input() hasBorder: boolean = true; + /** + * 是否为确认取消按钮 + */ + @Input() label: 'ok' | 'cancel' = undefined; + /** + * 确认取消按钮词条 + */ + public buttonWords = TiLocale.getLocaleWords().tiButton; + protected versionInfo: string = super.getVersion(packageInfo); + + constructor(public renderer: Renderer2, private hostEle: ElementRef, private zone: NgZone, @Inject(DOCUMENT) private document) { + super(hostEle, renderer); + } + private documentVisibilitychangeListener: () => void; + ngAfterViewInit(): void { + this.zone.runOutsideAngular(() => { + let outlineColor: string; + const transparentColor: string = 'rgba(0, 0, 0, 0)'; // 透明色,跟transparent色值一致 + this.renderer.listen(this.hostEle.nativeElement, 'mousedown', (): void => { + // 当点击的时候,不需要有outline。 + this.renderer.setStyle(this.hostEle.nativeElement, 'outline', 'none'); + // 按钮聚焦时,正常点击获取的outlineColor为按钮border色,但是此处需要区分点击聚焦与tab快捷键聚焦两种场景, + // 所以额外设定点击聚焦场景下outlineColor为透明色。 + // 注意:仅仅设置outlineColor为透明色,会导致按钮点击后border不可见 + this.renderer.setStyle(this.hostEle.nativeElement, 'outlineColor', 'transparent'); + }); + this.renderer.listen(this.hostEle.nativeElement, 'blur', (): void => { + // blur时仅能读取到outlineColor,作为窗口切换前的状态标志。 + outlineColor = getComputedStyle(this.hostEle.nativeElement).outlineColor; + // 恢复outline原生状态 + this.renderer.setStyle(this.hostEle.nativeElement, 'outline', ''); + }); + this.documentVisibilitychangeListener = this.renderer.listen(this.document, 'visibilitychange', (): void => { + if ( + document.visibilityState === 'visible' && + this.document.activeElement === this.hostEle.nativeElement && + outlineColor === transparentColor + ) { + this.renderer.setStyle(this.hostEle.nativeElement, 'outline', 'none'); + } + }); + }); + } + + ngOnDestroy(): void { + this.documentVisibilitychangeListener && this.documentVisibilitychangeListener(); + } +} diff --git a/src/button/lib/src/TiButtonModule.ts b/src/button/lib/src/TiButtonModule.ts new file mode 100644 index 0000000..ee1020c --- /dev/null +++ b/src/button/lib/src/TiButtonModule.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiButtonComponent } from './TiButtonComponent'; +import { TiLoadingModule } from '@opentiny/ng-loading'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, TiLoadingModule], + exports: [TiButtonComponent], + declarations: [TiButtonComponent] +}) +export class TiButtonModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiButtonComponent } from './TiButtonComponent'; diff --git a/src/button/lib/src/button-mixin.less b/src/button/lib/src/button-mixin.less new file mode 100644 index 0000000..3e72a01 --- /dev/null +++ b/src/button/lib/src/button-mixin.less @@ -0,0 +1,76 @@ +@import '../../../themes/basic/mixins.less'; + +:root { + --ti-button-timing-function-default: cubic-bezier(0.25, 0.1, 0.25, 1); +} + +.button-variant(@normal-color; @normal-background; @normal-border; + @hover-color; @hover-background; @hover-border; + @active-color; @active-background; @active-border; + @disable-color; @disable-background; @disable-border) { + .button-state-variant(@normal-color; @normal-background; @normal-border); + &:not([disabled]) { + &:hover, + &:focus { + .button-state-variant(@hover-color; @hover-background; @hover-border); + } + &:active { + .button-state-variant(@active-color; @active-background; @active-border); + } + } + &[disabled] { + .button-state-variant(@disable-color; @disable-background; @disable-border); + } +} + +.button-state-variant(@color; @background; @border) { + color: @color; + background: @background; + border-color: @border; +} + +.button-loading-icon(@border-color) { + display: inline-flex; + align-items: center; + .ti3-loading-default { + border-color: @border-color; + border-right-color: transparent; + } +} + +.button-different-size(@height; @padding; @border-radius; @border-weight) { + height: @height; + line-height: calc(@height - @border-weight * 2); + padding: 0 @padding !important; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + border-radius: @border-radius; +} + +.button-transition-with-diffrent-time(@time) { + .transition(background-color; @time;var(--ti-button-timing-function-default), border-color; @time;var(--ti-button-timing-function-default), + box-shadow; @time;var(--ti-button-timing-function-default), color; @time;var(--ti-button-timing-function-default)); +} +.button-icon-variant(@normal-color; @hover-color; @active-color; @disable-color) { + .ti3-icon { + color: @normal-color; + } + &:not([disabled]) { + &:hover, + &:focus { + .ti3-icon { + color: @hover-color; + } + } + &:active { + .ti3-icon { + color: @active-color; + } + } + } + &[disabled] { + .ti3-icon { + color: @disable-color; + } + } +} diff --git a/src/button/lib/src/button.html b/src/button/lib/src/button.html new file mode 100644 index 0000000..a6ab59a --- /dev/null +++ b/src/button/lib/src/button.html @@ -0,0 +1,5 @@ + +
{{ buttonWords[label] }}
+ + + diff --git a/src/button/lib/src/button.less b/src/button/lib/src/button.less new file mode 100644 index 0000000..be2981e --- /dev/null +++ b/src/button/lib/src/button.less @@ -0,0 +1,138 @@ +@import "button-mixin.less"; + +@button-name: tiButton; + +[@{button-name}] { + --ti-button-onlyIcon-size: var(--ti-common-size-7x); + --ti-button-onlyIcon-padding: 5px; + --ti-button-danger-border-color-hover: var(--ti-common-bg-primary-hover); // 边框悬浮色与背景悬浮色相同变量,增加配置项 + // 因base文件不涉及此种场景且仅有运维及监控场景使用,所以此处直接使用base定义 + --ti-button-primary-bg-color: var(--ti-base-color-brand-6); + --ti-button-primary-border-color: var(--ti-base-color-brand-6); + --ti-button-primary-bg-color-hover: var(--ti-base-color-brand-5); + --ti-button-primary-border-color-hover: var(--ti-base-color-brand-5); + --ti-button-primary-bg-color-active: var(--ti-base-color-brand-7); + --ti-button-primary-border-color-active: var(--ti-button-primary-bg-color-active); +} + +.ti3-btn, [@{button-name}]{ + display: inline-block; + padding: 0 var(--ti-common-space-5x); + height: var(--ti-common-size-7x); + line-height: calc(var(--ti-common-size-7x) - var(--ti-common-border-weight-normal)); + border-radius: var(--ti-common-border-radius-normal); + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + .user-select; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) transparent; + .box-sizing(border-box); + &[disabled] { + cursor: not-allowed; + } + .button-transition-with-diffrent-time(.15s); +} + +[@{button-name}], .ti3-btn { + &.ti3-btn-noborder { + border: none; + color: var(--ti-common-color-text-link); + padding: 0; + line-height: var(--ti-common-line-height-number); + font-size: var(--ti-common-font-size-base); + height: calc(var(--ti-common-font-size-base) * var(--ti-common-line-height-number)); + &:not([disabled]) { + &:hover, &:focus, &:active { + color: var(--ti-common-color-text-link-hover); + } + } + &:disabled { + color: var(--ti-common-color-text-disabled); + } + } +} + +.ti3-btn-icon{ + padding: 0 var(--ti-common-space-5x); +} + +.ti3-btn-large { + .button-different-size(var(--ti-common-size-8x); var(--ti-common-space-6x); var(--ti-common-border-radius-normal); var(--ti-common-border-weight-normal)); +} + +.ti3-btn-middle { + .button-different-size(var(--ti-common-size-7x); var(--ti-common-space-5x); var(--ti-common-border-radius-normal); var(--ti-common-border-weight-normal)); +} + +.ti3-btn-small { + .button-different-size(var(--ti-common-size-6x); var(--ti-common-space-4x); var(--ti-common-border-radius-normal); var(--ti-common-border-weight-normal)); +} + +.ti3-btn-xs { + .button-different-size(var(--ti-common-size-5x); var(--ti-common-space-3x); var(--ti-common-border-radius-normal); var(--ti-common-border-weight-normal)); +} + +.ti3-btn-onlyIcon { + height: var(--ti-button-onlyIcon-size); + width: var(--ti-button-onlyIcon-size); + padding: var(--ti-button-onlyIcon-padding) !important; + font-size: var(--ti-common-size-4x); + line-height: 1; +} + + +.ti3-btn-default, [@{button-name}]{ + .button-variant(var(--ti-common-color-text-primary); var(--ti-common-bg-minor); var(--ti-common-color-line-normal); + var(--ti-common-color-text-highlight); var(--ti-common-bg-minor-hover); var(--ti-common-color-line-active); + var(--ti-common-color-text-highlight); var(--ti-common-bg-minor-active); var(--ti-common-color-line-active); + var(--ti-common-color-text-disabled); var(--ti-common-color-bg-disabled); var(--ti-common-color-line-disabled)); + .button-icon-variant(var(--ti-common-color-icon-normal); var(--ti-common-color-icon-hover); var(--ti-common-color-icon-active); var(--ti-common-color-icon-disabled)); +} + // 主题色按钮(中性无倾向的强调按钮)状态配置 + .ti3-btn-primary { + .button-variant(var(--ti-common-color-text-white); var(--ti-button-primary-bg-color); var(--ti-button-primary-border-color); + var(--ti-common-color-text-white); var(--ti-button-primary-bg-color-hover); var(--ti-button-primary-border-color-hover); + var(--ti-common-color-text-white); var(--ti-button-primary-bg-color-active); var(--ti-button-primary-border-color-active); + var(--ti-common-color-text-disabled); var(--ti-common-color-bg-disabled); var(--ti-common-color-line-disabled)) + } + + .ti3-btn-danger{ + .button-variant(var(--ti-common-color-text-white); var(--ti-common-bg-primary); var(--ti-common-color-transparent); + var(--ti-common-color-text-white); var(--ti-common-bg-primary-hover); var(--ti-button-danger-border-color-hover); + var(--ti-common-color-text-white); var(--ti-common-bg-primary-active); var(--ti-common-bg-primary-active); + var(--ti-common-color-text-disabled); var(--ti-common-color-bg-disabled); var(--ti-common-color-line-disabled)) + } + +.ti3-btn-loading{ + pointer-events: none; // 元素永远不会成为鼠标事件的target + ti-loading { + margin-right: var(--ti-common-space-2x); + display: inline-flex; + align-items: center; + } +} + +.ti3-btn-loading.ti3-btn-default, .ti3-btn-loading[@{button-name}]:not([disabled]) { + .button-state-variant(var(--ti-common-color-text-highlight); var(--ti-common-bg-minor-hover); var(--ti-common-color-line-active)); + .button-loading-icon(var(--ti-common-color-icon-active)); +} + +.ti3-btn-loading.ti3-btn-primary:not([disabled]) { + .button-state-variant(var(--ti-common-color-text-white); var(--ti-button-primary-bg-color-hover); var(--ti-button-primary-border-color-hover)); + .button-loading-icon(var(--ti-common-color-icon-white)); +} + +.ti3-btn-loading.ti3-btn-danger:not([disabled]) { + .button-state-variant(var(--ti-common-color-text-white); var(--ti-common-bg-primary-hover); var(--ti-button-danger-border-color-hover)); + .button-loading-icon(var(--ti-common-color-icon-white)); +} + +/********************************************动效*****************************************************************/ +.ti3-btn-default, [@{button-name}], .ti3-btn-danger{ + //hover,active,focus(200ms) + &:not([disabled]):hover, &:not([disabled]):active, &:not([disabled]):focus { + .button-transition-with-diffrent-time(.2s); + } +} +/********************************************动效*****************************************************************/ \ No newline at end of file diff --git a/src/button/lib/src/i18n/TiButtonWords.ts b/src/button/lib/src/i18n/TiButtonWords.ts new file mode 100644 index 0000000..6d62b03 --- /dev/null +++ b/src/button/lib/src/i18n/TiButtonWords.ts @@ -0,0 +1,6 @@ +export interface TiButtonWords { + tiButton: { + ok: string; + cancel: string; + }; +} diff --git a/src/button/lib/src/i18n/en_US.ts b/src/button/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..9e48a87 --- /dev/null +++ b/src/button/lib/src/i18n/en_US.ts @@ -0,0 +1,8 @@ +import { TiButtonWords } from './TiButtonWords'; + +export const en_US: TiButtonWords = { + tiButton: { + ok: 'OK', + cancel: 'Cancel' + } +}; diff --git a/src/button/lib/src/i18n/es_US.ts b/src/button/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..4901e3e --- /dev/null +++ b/src/button/lib/src/i18n/es_US.ts @@ -0,0 +1,8 @@ +import { TiButtonWords } from './TiButtonWords'; + +export const es_US: TiButtonWords = { + tiButton: { + ok: 'Aceptar', + cancel: 'Cancelar' + } +}; diff --git a/src/button/lib/src/i18n/fr_FR.ts b/src/button/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..e5a5e6e --- /dev/null +++ b/src/button/lib/src/i18n/fr_FR.ts @@ -0,0 +1,8 @@ +import { TiButtonWords } from './TiButtonWords'; + +export const fr_FR: TiButtonWords = { + tiButton: { + ok: 'OK', + cancel: 'Annuler' + } +}; diff --git a/src/button/lib/src/i18n/index.ts b/src/button/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/button/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/button/lib/src/i18n/pt_BR.ts b/src/button/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..170ee6e --- /dev/null +++ b/src/button/lib/src/i18n/pt_BR.ts @@ -0,0 +1,8 @@ +import { TiButtonWords } from './TiButtonWords'; + +export const pt_BR: TiButtonWords = { + tiButton: { + ok: 'OK', + cancel: 'Cancelar' + } +}; diff --git a/src/button/lib/src/i18n/zh_CN.ts b/src/button/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..5ec2071 --- /dev/null +++ b/src/button/lib/src/i18n/zh_CN.ts @@ -0,0 +1,8 @@ +import { TiButtonWords } from './TiButtonWords'; + +export const zh_CN: TiButtonWords = { + tiButton: { + ok: '确定', + cancel: '取消' + } +}; diff --git a/src/buttongroup/demo/karma.conf.js b/src/buttongroup/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/buttongroup/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/buttongroup/demo/project.json b/src/buttongroup/demo/project.json new file mode 100644 index 0000000..979d307 --- /dev/null +++ b/src/buttongroup/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/buttongroup/demo", + "sourceRoot": "src/buttongroup/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/buttongroup", + "index": "src/buttongroup/demo/src/index.html", + "main": "src/buttongroup/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/buttongroup/demo/tsconfig.app.json", + "assets": ["src/buttongroup/demo/src/favicon.ico", "src/buttongroup/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "buttongroup-demo:build:production" + }, + "development": { + "browserTarget": "buttongroup-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js buttongroup" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/buttongroup/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/buttongroup/demo/tsconfig.spec.json", + "karmaConfig": "src/buttongroup/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/buttongroup/demo/src/app/AppComponent.ts b/src/buttongroup/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/buttongroup/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/buttongroup/demo/src/app/AppModule.ts b/src/buttongroup/demo/src/app/AppModule.ts new file mode 100644 index 0000000..c23ee83 --- /dev/null +++ b/src/buttongroup/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ButtonGroupTestModule } from './buttongroup/ButtonGroupTestModule'; + +@NgModule({ + imports: [ + ButtonGroupTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/buttongroup/demo/src/app/IndexComponent.ts b/src/buttongroup/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..f18a22c --- /dev/null +++ b/src/buttongroup/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ButtonGroupTestModule } from './buttongroup/ButtonGroupTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = ButtongroupTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/buttongroup/demo/src/app/app.html b/src/buttongroup/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/buttongroup/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtonGroupTestModule.ts b/src/buttongroup/demo/src/app/buttongroup/ButtonGroupTestModule.ts new file mode 100644 index 0000000..306e860 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtonGroupTestModule.ts @@ -0,0 +1,171 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtongroupModule, TiIconModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { ButtongroupItemsComponent } from './ButtongroupItemsComponent'; +import { ButtongroupMultipleComponent } from './ButtongroupMultipleComponent'; +import { ButtongroupReactiveFormsComponent } from './ButtongroupReactiveFormsComponent'; +import { ButtongroupTemplateComponent } from './ButtongroupTemplateComponent'; +import { ButtongroupSupComponent } from './ButtongroupSupComponent'; +import { ButtongroupRadioTypeComponent } from './ButtongroupRadioTypeComponent'; +import { ButtongroupMultiTypeComponent } from './ButtongroupMultiTypeComponent'; +import { ButtongroupDeselectableComponent } from './ButtongroupDeselectableComponent'; +import { ButtongroupMinwidthComponent } from './ButtongroupMinwidthComponent'; +import { ButtongroupActiveclassComponent } from './ButtongroupActiveclassComponent'; +import { ButtongroupItemsTestComponent } from './ButtongroupItemsTestComponent'; +import { ButtongroupManyComponent } from './ButtongroupManyComponent'; +import { ButtongroupTipComponent } from './ButtongroupTipComponent'; +import { ButtongroupMultilineComponent } from './ButtongroupMultilineComponent'; +import { ButtongroupDisabledComponent } from './ButtongroupDisabledComponent'; +import { ButtongroupValuekeyComponent } from './ButtongroupValuekeyComponent'; +import { ButtongroupValuekeyTestComponent } from './ButtongroupValuekeyTestComponent'; +import { ButtongroupEnumComponent } from './ButtongroupEnumComponent'; +import { ButtongroupEventComponent } from './ButtongroupEventComponent'; +import { ButtongroupFocusComponent } from './ButtongroupFocusComponent'; +import { ButtongroupSupTestComponent } from './ButtongroupSupTestComponent'; +import { ButtongroupIdComponent } from './ButtongroupIdComponent'; +import { ButtongroupIdTestComponent } from './ButtongroupIdTestComponent'; +import { ButtongroupBeforeclickComponent } from './ButtongroupBeforeclickComponent'; + +@NgModule({ + imports: [ + CommonModule, + TiButtongroupModule, + TiIconModule, + FormsModule, + ReactiveFormsModule, + DemoLogModule, + RouterModule.forChild(ButtonGroupTestModule.ROUTES) + ], + declarations: [ + ButtongroupItemsComponent, + ButtongroupMultipleComponent, + ButtongroupValuekeyComponent, + ButtongroupValuekeyTestComponent, + ButtongroupRadioTypeComponent, + ButtongroupMultiTypeComponent, + ButtongroupDeselectableComponent, + ButtongroupMinwidthComponent, + ButtongroupTemplateComponent, + ButtongroupTipComponent, + ButtongroupSupComponent, + ButtongroupReactiveFormsComponent, + ButtongroupActiveclassComponent, + ButtongroupItemsTestComponent, + ButtongroupManyComponent, + ButtongroupMultilineComponent, + ButtongroupDisabledComponent, + ButtongroupEnumComponent, + ButtongroupEventComponent, + ButtongroupFocusComponent, + ButtongroupSupTestComponent, + ButtongroupIdComponent, + ButtongroupIdTestComponent, + ButtongroupBeforeclickComponent + ] +}) +export class ButtonGroupTestModule { + static readonly LINKS: Array = [{ href: 'components/TiButtongroupComponent.html', label: 'Buttongroup' }]; + static readonly ROUTES: Routes = [ + { + path: 'buttongroup/buttongroup-items', + component: ButtongroupItemsComponent + }, + { + path: 'buttongroup/buttongroup-multiple', + component: ButtongroupMultipleComponent + }, + { + path: 'buttongroup/buttongroup-radio-type', + component: ButtongroupRadioTypeComponent + }, + { + path: 'buttongroup/buttongroup-multi-type', + component: ButtongroupMultiTypeComponent + }, + { + path: 'buttongroup/buttongroup-disabled', + component: ButtongroupDisabledComponent + }, + { + path: 'buttongroup/buttongroup-event', + component: ButtongroupEventComponent + }, + { + path: 'buttongroup/buttongroup-beforeclick', + component: ButtongroupBeforeclickComponent + }, + { + path: 'buttongroup/buttongroup-focus', + component: ButtongroupFocusComponent + }, + { + path: 'buttongroup/buttongroup-valuekey', + component: ButtongroupValuekeyComponent + }, + { + path: 'buttongroup/buttongroup-deselectable', + component: ButtongroupDeselectableComponent + }, + { + path: 'buttongroup/buttongroup-minwidth', + component: ButtongroupMinwidthComponent + }, + { + path: 'buttongroup/buttongroup-template', + component: ButtongroupTemplateComponent + }, + { + path: 'buttongroup/buttongroup-tip', + component: ButtongroupTipComponent + }, + { + path: 'buttongroup/buttongroup-reactive-forms', + component: ButtongroupReactiveFormsComponent + }, + { + path: 'buttongroup/buttongroup-sup', + component: ButtongroupSupComponent + }, + { + path: 'buttongroup/buttongroup-id', + component: ButtongroupIdComponent + }, + { + path: 'buttongroup/buttongroup-multiline', + component: ButtongroupMultilineComponent + }, + { + path: 'buttongroup/buttongroup-activeclass', + component: ButtongroupActiveclassComponent + }, + { + path: 'buttongroup/buttongroup-valuekey-test', + component: ButtongroupValuekeyTestComponent + }, + { + path: 'buttongroup/buttongroup-many', + component: ButtongroupManyComponent + }, + { + path: 'buttongroup/buttongroup-enum', + component: ButtongroupEnumComponent + }, + { + path: 'buttongroup/buttongroup-items-test', + component: ButtongroupItemsTestComponent + }, + { + path: 'buttongroup/buttongroup-sup-test', + component: ButtongroupSupTestComponent + }, + { + path: 'buttongroup/buttongroup-id-test', + component: ButtongroupIdTestComponent + } + ]; +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupActiveclassComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupActiveclassComponent.ts new file mode 100644 index 0000000..239a5a3 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupActiveclassComponent.ts @@ -0,0 +1,31 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-activeclass.html', + styleUrls: ['./buttongroup.less'], + encapsulation: ViewEncapsulation.None +}) +export class ButtongroupActiveclassComponent { + items: Array = [ + { + text: '1G', + disabled: true + }, + { + text: '2G' + }, + { + text: '3G' + }, + { + text: '4G' + } + ]; + selected: TiButtonItem = this.items[2]; + activeClass: string = 'bgColor'; + + radioChangeFn(item: TiButtonItem): void { + console.log(item); + } +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupBeforeclickComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupBeforeclickComponent.ts new file mode 100644 index 0000000..f56b3bd --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupBeforeclickComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-beforeclick.html' +}) +export class ButtongroupBeforeclickComponent { + items: Array = [ + { + text: '近1小时', + disabled: true + }, + { + text: '近12小时' + }, + { + text: '近1天' + }, + { + text: '近3天' + } + ]; + items1: Array = this.items; + selected: TiButtonItem = this.items[2]; + selected1: Array = [this.items1[2]]; + myLogs: Array = []; + + beforeClickFn1(event): void { + if (this.items.indexOf(event) > 1) { + this.selected = event; + this.myLogs = [...this.myLogs, `${event.text}---被点击`]; + + return; + } + this.myLogs = [...this.myLogs, `${event.text}---被点击但被阻止选中`]; + } + beforeClickFn2(event): void { + if (this.items1.indexOf(event) > 1) { + if (this.selected1.includes(event)) { + this.selected1.splice(this.selected1.indexOf(event), 1); + } else { + this.selected1.push(event); + this.selected1 = this.selected1.concat(); + } + this.myLogs = [...this.myLogs, `${event.text}---被点击`]; + + return; + } + this.myLogs = [...this.myLogs, `${event.text}---被点击但被阻止选中`]; + } +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupDeselectableComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupDeselectableComponent.ts new file mode 100644 index 0000000..dd53687 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupDeselectableComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-deselectable.html' +}) +export class ButtongroupDeselectableComponent { + items: Array = [ + { + text: '星期一' + }, + { + text: '星期二' + }, + { + text: '星期三' + }, + { + text: '星期四' + } + ]; + selected: TiButtonItem = this.items[1]; +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupDisabledComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupDisabledComponent.ts new file mode 100644 index 0000000..30a52ba --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupDisabledComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-disabled.html' +}) +export class ButtongroupDisabledComponent { + disabled: boolean = true; + items: Array = [ + { + text: '1G', + disabled: true + }, + { + text: '2G' + }, + { + text: '3G' + }, + { + text: '4G' + } + ]; + + items1: Array = this.items; + selected: TiButtonItem = this.items[2]; + selected1: TiButtonItem = this.items1[0]; + selected2: Array = [this.items[2]]; +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupEnumComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupEnumComponent.ts new file mode 100644 index 0000000..877ff77 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupEnumComponent.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +enum TimeLength { + about1hour, + about12hour, + about1day, + about3day +} + +@Component({ + templateUrl: './buttongroup-enum.html' +}) +export class ButtongroupEnumComponent { + items: Array = [ + { + text: '近1小时', + english: TimeLength.about1hour + }, + { + text: '近12小时', + english: TimeLength.about12hour + }, + { + text: '近1天', + english: TimeLength.about1day + }, + { + text: '近3天', + english: TimeLength.about3day + } + ]; + selected: TimeLength = TimeLength.about1hour; + selecteds: Array = [TimeLength.about1hour, TimeLength.about12hour]; +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupEventComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupEventComponent.ts new file mode 100644 index 0000000..1ba89b4 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupEventComponent.ts @@ -0,0 +1,48 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-event.html' +}) +export class ButtongroupEventComponent { + items: Array = [ + { + text: '星期一' + }, + { + text: '星期二' + }, + { + text: '星期三' + }, + { + text: '星期四' + } + ]; + items1: Array = [ + { + text: '1G' + }, + { + text: '2G' + }, + { + text: '3G' + }, + { + text: '4G' + } + ]; + selected: TiButtonItem = this.items[1]; + selected1: Array = [this.items1[3]]; + myLogs: Array = []; + focus(): void { + this.myLogs = [...this.myLogs, 'focus']; + } + blur(): void { + this.myLogs = [...this.myLogs, 'blur']; + } + modelChange($event: TiButtonItem): void { + this.myLogs = [...this.myLogs, JSON.stringify($event)]; + } +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupFocusComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupFocusComponent.ts new file mode 100644 index 0000000..022bf6f --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupFocusComponent.ts @@ -0,0 +1,57 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-focus.html' +}) +export class ButtongroupFocusComponent { + items: Array = [ + { + text: '星期一' + }, + { + text: '星期二' + }, + { + text: '星期三' + }, + { + text: '星期四' + }, + { + text: '星期四1' + }, + { + text: '星期四2' + } + ]; + items1: Array = [ + { + text: '1G' + }, + { + text: '2G' + }, + { + text: '3G' + }, + { + text: '4G' + } + ]; + selected: TiButtonItem = this.items[1]; + selected1: Array = [this.items1[3]]; + myLogs: Array = []; + focus(): void { + this.myLogs = [...this.myLogs, 'focus:单选按钮聚焦,tabindex=2']; + } + blur(): void { + this.myLogs = [...this.myLogs, 'blur:单选按钮失焦']; + } + focus1(): void { + this.myLogs = [...this.myLogs, 'focus:多选按钮聚焦,tabindex=1']; + } + blur1(): void { + this.myLogs = [...this.myLogs, 'blur:多选按钮失焦']; + } +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupIdComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupIdComponent.ts new file mode 100644 index 0000000..6a56503 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupIdComponent.ts @@ -0,0 +1,42 @@ +import { ChangeDetectorRef, Component, ElementRef } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-id.html' +}) +export class ButtongroupIdComponent { + items: Array = [ + { + id: 'changpingqu', + text: '北京市-昌平区', + disabled: true + }, + { + id: 'yantaqu', + text: '西安市-雁塔区' + }, + { + id: 'minhangqu', + text: '上海市-闵行区' + }, + { + id: 'shibeiqu', + text: '青岛市-市北区' + } + ]; + selected: TiButtonItem = this.items[2]; + selected1: Array = [this.items[2], this.items[3]]; + constructor(private hostRef: ElementRef, private cdRef: ChangeDetectorRef) {} + idArray1 = []; + idArray2 = []; + ngAfterViewInit(): void { + // 为了在页面上显示id,没有实际意义 + const tiButtongroup1: Element = this.hostRef.nativeElement.querySelectorAll('ti-button-group')[0]; + const tiButtongroup2: Element = this.hostRef.nativeElement.querySelectorAll('ti-button-group')[1]; + this.items.forEach((item) => { + this.idArray1.push(tiButtongroup1.id + `_${item.id}`); + this.idArray2.push(tiButtongroup2.id + `_${item.id}`); + }); + this.cdRef.detectChanges(); + } +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupIdTestComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupIdTestComponent.ts new file mode 100644 index 0000000..7cd9911 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupIdTestComponent.ts @@ -0,0 +1,43 @@ +import { ChangeDetectorRef, Component, ElementRef } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-id-test.html' +}) +export class ButtongroupIdTestComponent { + items: Array = [ + { + id: 'changpingqu', + text: '北京市-昌平区', + disabled: true + }, + { + id: 'yantaqu', + text: '西安市-雁塔区' + }, + { + id: 'minhangqu', + text: '上海市-闵行区' + }, + { + id: 'shibeiqu', + text: '青岛市-市北区' + } + ]; + selected: TiButtonItem = this.items[2]; + selected1: Array = [this.items[2], this.items[3]]; + constructor(private hostRef: ElementRef, private cdRef: ChangeDetectorRef) {} + idArray1: Array = []; + idArray2: Array = []; + ngAfterViewInit(): void { + const tiButtongroup1: Element = this.hostRef.nativeElement.querySelectorAll('ti-button-group')[0]; + const tiButtongroup2: Element = this.hostRef.nativeElement.querySelectorAll('ti-button-group')[1]; + tiButtongroup1.querySelectorAll('.ti3-btn-item-container').forEach((item: Element) => { + this.idArray1.push(item.id); + }); + tiButtongroup2.querySelectorAll('.ti3-btn-item-container').forEach((item: Element) => { + this.idArray2.push(item.id); + }); + this.cdRef.detectChanges(); + } +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupItemsComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupItemsComponent.ts new file mode 100644 index 0000000..94e6de5 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupItemsComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-items.html' +}) +export class ButtongroupItemsComponent { + items: Array = [ + { + text: '1 hour' + }, + { + text: '12 hours' + }, + { + text: '1 day' + }, + { + text: '3 days' + } + ]; + selected: TiButtonItem = this.items[2]; +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupItemsTestComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupItemsTestComponent.ts new file mode 100644 index 0000000..cfc9eb5 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupItemsTestComponent.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-items-test.html' +}) +export class ButtongroupItemsTestComponent { + items: Array; + selected: TiButtonItem; + selected1: Array; + myLogs: Array = []; + + setItems(): void { + this.items = [ + { + text: '1G' + }, + { + text: '2G' + }, + { + text: '3G' + }, + { + text: '4G', + disabled: true + } + ]; + } + setModel(): void { + this.selected = this.items[0]; + } + clearModel(): void { + this.selected = undefined; + } + setModel1(): void { + this.selected1 = [this.items[0], this.items[2]]; + } + clearModel1(): void { + this.selected1 = undefined; + } + ngmodelChange($event): void { + this.myLogs = [...this.myLogs, JSON.stringify($event)]; + } +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupManyComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupManyComponent.ts new file mode 100644 index 0000000..7f6bd1b --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupManyComponent.ts @@ -0,0 +1,80 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-many.html' +}) +export class ButtongroupManyComponent { + items: Array = [ + { + text: '星期一', + disabled: true + }, + { + text: '星期二' + }, + { + text: '星期三' + }, + { + text: '星期四' + }, + { + text: '星期五' + }, + { + text: '星期六' + }, + { + text: '星期七' + }, + { + text: '星期八' + }, + { + text: '星期九' + }, + { + text: '星期十' + }, + { + text: '星期零' + } + ]; + items1: Array = [ + { + text: '这是超过300px的文本1,超长出点点点,hover上去显示tip', + disabled: true + }, + { + text: '这是超过300px的文本2,超长出点点点,hover上去显示tip' + }, + { + text: '这是超过300px的文本3,超长出点点点,hover上去显示tip' + }, + { + text: '这是超过300px的文本4,超长出点点点,hover上去显示tip' + } + ]; + items2: Array = [ + { + text: '通用计算型' + }, + { + text: '通用计算增强型' + }, + { + text: '内存优化型' + }, + { + text: '高性能计算型' + }, + { + text: '磁盘增强型', + disabled: true + } + ]; + selected: TiButtonItem = this.items[2]; + selected1: TiButtonItem = this.items1[1]; + selected2: TiButtonItem = this.items2[3]; +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupMinwidthComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupMinwidthComponent.ts new file mode 100644 index 0000000..92b969f --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupMinwidthComponent.ts @@ -0,0 +1,38 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-minwidth.html' +}) +export class ButtongroupMinwidthComponent { + items: Array = [ + { + text: '星期一' + }, + { + text: '星期二' + }, + { + text: '星期三' + }, + { + text: '星期四' + } + ]; + items1: Array = [ + { + text: '1G' + }, + { + text: '2G' + }, + { + text: '3G' + }, + { + text: '4G' + } + ]; + selected: TiButtonItem = this.items[1]; + selected1: Array = [this.items1[3]]; +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupMultiTypeComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupMultiTypeComponent.ts new file mode 100644 index 0000000..d7adda3 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupMultiTypeComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-multi-type.html' +}) +export class ButtongroupMultiTypeComponent { + items: Array = [ + { + text: '北京市-昌平区' + }, + { + text: '西安市-雁塔区' + }, + { + text: '上海市-闵行区' + }, + { + text: '青岛市-市北区' + } + ]; + items1: Array = this.items; + selected: Array = [this.items[1], this.items[3]]; + selected1: Array = [this.items1[2], this.items1[3]]; +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupMultilineComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupMultilineComponent.ts new file mode 100644 index 0000000..c2694f7 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupMultilineComponent.ts @@ -0,0 +1,47 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-multiline.html' +}) +export class ButtongroupMultilineComponent { + items: Array = [ + { + text: '一月份' + }, + { + text: '二月份' + }, + { + text: '三月份' + }, + { + text: '四月份' + }, + { + text: '五月份' + }, + { + text: '六月份' + }, + { + text: '七月份' + }, + { + text: '八月份' + }, + { + text: '九月份' + }, + { + text: '十月份' + }, + { + text: '十一月份' + }, + { + text: '十二月份' + } + ]; + selected: TiButtonItem = this.items[2]; +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupMultipleComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupMultipleComponent.ts new file mode 100644 index 0000000..728998e --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupMultipleComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-multiple.html' +}) +export class ButtongroupMultipleComponent { + items: Array = [ + { + text: '北京市-昌平区' + }, + { + text: '西安市-雁塔区' + }, + { + text: '上海市-闵行区' + }, + { + text: '青岛市-市北区' + } + ]; + selected: Array = [this.items[2], this.items[3]]; +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupRadioTypeComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupRadioTypeComponent.ts new file mode 100644 index 0000000..80bf96d --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupRadioTypeComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-radio-type.html' +}) +export class ButtongroupRadioTypeComponent { + items: Array = [ + { + text: '北京市-昌平区' + }, + { + text: '西安市-雁塔区' + }, + { + text: '上海市-闵行区' + }, + { + text: '青岛市-市北区' + } + ]; + items1: Array = this.items; + items2: Array = this.items; + selected: TiButtonItem = this.items[2]; + selected1: TiButtonItem = this.items1[1]; + selected2: TiButtonItem = this.items2[3]; +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupReactiveFormsComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupReactiveFormsComponent.ts new file mode 100644 index 0000000..593ee2d --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupReactiveFormsComponent.ts @@ -0,0 +1,32 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-reactive-forms.html' +}) +export class ButtongroupReactiveFormsComponent implements OnInit { + constructor(private fb: FormBuilder) {} + buttonGroupForm: FormGroup; + items: Array = [ + { + text: '1G', + disabled: true + }, + { + text: '2G' + }, + { + text: '3G' + }, + { + text: '4G' + } + ]; + + ngOnInit(): void { + this.buttonGroupForm = this.fb.group({ + selected: this.items[1] + }); + } +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupSupComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupSupComponent.ts new file mode 100644 index 0000000..88bfbff --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupSupComponent.ts @@ -0,0 +1,40 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-sup.html', + encapsulation: ViewEncapsulation.None +}) +export class ButtongroupSupComponent { + items: Array = [ + { + text: '9个月', + sup: { + class: 'buttongroup-sold-out', + iconName: 'sold-out' + } + }, + { + text: '1年', + sup: { + class: 'buttongroup-discount-icon', + iconName: 'discount-sup' + } + }, + { + text: '2年', + sup: { + text: '88折', + class: 'buttongroup-discount-text' + } + }, + { + text: '3年', + sup: { + text: '7折', + class: 'buttongroup-discount-text' + } + } + ]; + selected: TiButtonItem = this.items[1]; +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupSupTestComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupSupTestComponent.ts new file mode 100644 index 0000000..6e95fd2 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupSupTestComponent.ts @@ -0,0 +1,38 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-sup-test.html', + encapsulation: ViewEncapsulation.None +}) +export class ButtongroupSupTestComponent { + values: Array = [ + { + text: '9个月', + sup: { + class: 'ti3-icon ti3-icon-sold-out buttongroup-sold-out' // 10.1.2版本之后可通过自定义模板设置图标及样式 + } + }, + { + text: '1年', + sup: { + class: 'ti3-icon ti3-icon-discount-sup buttongroup-discount-icon' + } + }, + { + text: '2年', + sup: { + text: '88折', + class: 'buttongroup-discount-text' + } + }, + { + text: '3年', + sup: { + text: '7折', + class: 'buttongroup-discount-text' + } + } + ]; + selected: TiButtonItem = this.values[1]; +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupTemplateComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupTemplateComponent.ts new file mode 100644 index 0000000..38b38d2 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupTemplateComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-template.html' +}) +export class ButtongroupTemplateComponent { + items: Array = [ + { + iconName: 'discount', + title: '优惠价格:200' + }, + { + iconName: 'calendar', + title: '日期' + }, + { + iconName: 'config', + title: '默认配置' + }, + { + iconName: 'search', + title: '搜索' + } + ]; + items1: Array = this.items; + selected: TiButtonItem = this.items[2]; + selected1: Array = [this.items1[2], this.items1[3]]; +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupTipComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupTipComponent.ts new file mode 100644 index 0000000..a672a3b --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupTipComponent.ts @@ -0,0 +1,51 @@ +import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-tip.html' +}) +export class ButtongroupTipComponent { + @ViewChild('tip', { static: true }) tip: TemplateRef; + items: Array = []; + items1: Array = []; + selected: TiButtonItem; + selected1: Array; + + ngOnInit(): void { + this.items = [ + { + text: '配置提示内容', + tipContent: this.tip, + tipPosition: 'top-left' + }, + { + text: '配置提示内容', + tipContent: 'string类型的提示', + tipPosition: 'left' + }, + { + text: '禁用且配置提示内容', + disabled: true, + tipContent: this.tip + } + ]; + this.items1 = [ + { + text: '配置提示内容', + tipContent: 'string类型的提示' + }, + { + text: '配置提示内容', + tipContent: this.tip + }, + { + text: '禁用且配置提示内容', + disabled: true, + tipContent: this.tip + } + ]; + + this.selected = this.items[1]; + this.selected1 = [this.items1[1]]; + } +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupValuekeyComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupValuekeyComponent.ts new file mode 100644 index 0000000..722360e --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupValuekeyComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-valuekey.html' +}) +export class ButtongroupValuekeyComponent { + items: Array = [ + { + text: '近1小时', + english: 'about 1hour' + }, + { + text: '近12小时', + english: 'about 12hour' + }, + { + text: '近1天', + english: 'about 1day' + }, + { + text: '近3天', + english: 'about 3day' + } + ]; + items1: Array = this.items; + selected: string = 'about 1day'; + selected1: Array = ['about 12hour', 'about 1day']; +} diff --git a/src/buttongroup/demo/src/app/buttongroup/ButtongroupValuekeyTestComponent.ts b/src/buttongroup/demo/src/app/buttongroup/ButtongroupValuekeyTestComponent.ts new file mode 100644 index 0000000..d74aa6f --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/ButtongroupValuekeyTestComponent.ts @@ -0,0 +1,80 @@ +import { Component } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './buttongroup-valuekey-test.html' +}) +export class ButtongroupValuekeyTestComponent { + items: Array = [ + { + text: '近1小时', + english: 'about 1hour', + disabled: true + }, + { + text: '近12小时', + english: 'about 12hour' + }, + { + text: '近1天', + english: 'about 1day' + }, + { + text: '近3天', + english: 'about 3day' + } + ]; + items1: Array = [ + { + text: '这是超过300px的文本1,超长出点点点,hover上去显示tip', + english: 'text1', + disabled: true + }, + { + text: '这是超过300px的文本2,超长出点点点,hover上去显示tip', + english: 'text2' + }, + { + text: '这是超过300px的文本3,超长出点点点,hover上去显示tip', + english: 'text3' + }, + { + text: '这是超过300px的文本4,超长出点点点,hover上去显示tip', + english: 'text4' + } + ]; + selected: string = 'about 1day'; + selected1: Array = ['text2', 'text3']; + changeItems(): void { + this.items = [ + { + text: '近1小时', + english: 'about 1hour', + disabled: true + }, + { + text: '近12小时', + english: 'about 12hour' + }, + { + text: '近1天', + english: 'about 1day' + } + ]; + this.items1 = [ + { + text: '北京市-昌平区', + english: 'text1', + disabled: true + }, + { + text: '西安市-雁塔区', + english: 'text2' + }, + { + text: '上海市-闵行区', + english: 'text3' + } + ]; + } +} diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-activeclass.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-activeclass.html new file mode 100644 index 0000000..ca083dc --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-activeclass.html @@ -0,0 +1,13 @@ +

1.描述

+

activeClass接口测试(此接口是否多余)

+

2.示例

+

(2.1)单选按钮组测试

+ + +

选中项:{{selected | json}}

diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-beforeclick.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-beforeclick.html new file mode 100644 index 0000000..dcdef53 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-beforeclick.html @@ -0,0 +1,15 @@ + +

+ + diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-deselectable.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-deselectable.html new file mode 100644 index 0000000..0a8fa29 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-deselectable.html @@ -0,0 +1 @@ + diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-disabled.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-disabled.html new file mode 100644 index 0000000..848c31b --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-disabled.html @@ -0,0 +1,4 @@ +

+ +

+ diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-enum.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-enum.html new file mode 100644 index 0000000..8aac850 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-enum.html @@ -0,0 +1,12 @@ +

描述

+

Buttongroup组件,自定义选中值,设置valueKey时选中值基于valueKey

+

valueKey接口

+

示例

+

1.单选:设置valueKey

+ +

选中项:{{selected | json}}

+
+
+

多选:设置valueKey

+ +

选中项:{{selecteds | json}}

diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-event.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-event.html new file mode 100644 index 0000000..5066e15 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-event.html @@ -0,0 +1,22 @@ +

1.描述

+

focus、blur、modelChange等事件执行

+

2.示例

+

(2.1)单选:

+ + +

选中项:{{selected | json}}

+
+
+

(2.2)多选:

+ + +

选中项:{{selected1 | json}}

+

事件日志:

+ diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-focus.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-focus.html new file mode 100644 index 0000000..1344eb4 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-focus.html @@ -0,0 +1,23 @@ +

1.描述

+

通过方法或tab键获取焦点

+

2.示例

+

(2.1)单选:

+ +

选中项:{{selected | json}}

+
+ +  + +  + +  + +

+
+
+

(2.2)多选:

+ + +

选中项:{{selected1 | json}}

+

事件日志:

+ diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-id-test.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-id-test.html new file mode 100644 index 0000000..893aece --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-id-test.html @@ -0,0 +1,12 @@ +

1.描述

+

id和组件id配合,用于自定义按钮id。

+

和id示例的区别是,id示例显示的id是拼接的,不是真实获取的,更偏重demo意义,不作为test示例;本示例显示的id是真实获取的,具有测试意义。

+

2.示例

+

(2.1)设置组件id = abc 和item.id

+ +

按钮块id依次为:{{idArray1 | json}}

+
+
+

(2.2)不设置组件id,只设置item.id

+ +

按钮块id依次为:{{idArray2 | json}}

diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-id.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-id.html new file mode 100644 index 0000000..fd58fa0 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-id.html @@ -0,0 +1,11 @@ +

1.描述

+

id和组件id配合,用于自定义按钮id。

+

2.示例

+

(2.1)设置组件id = abc 和item.id

+ +

按钮块id依次为:{{idArray1 | json}}

+
+
+

(2.2)不设置组件id,只设置item.id

+ +

按钮块id依次为:{{idArray2 | json}}

diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-items-test.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-items-test.html new file mode 100644 index 0000000..627dd7b --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-items-test.html @@ -0,0 +1,18 @@ +

1.描述

+

测试items初始为空数组场景

+

2.示例

+单选和多选初始化没有选项数据,点击添加数据:
+清空items为空数组:
+ +

2.1 单选

+ +

ngModel:{{selected | json}}

+    + +

2.2 多选

+ +

ngModel:{{selected1 | json}}

+    + +

事件日志:

+ diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-items.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-items.html new file mode 100644 index 0000000..a532e83 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-items.html @@ -0,0 +1,4 @@ + +
+
Current value: {{ selected | json }}
+
diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-many.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-many.html new file mode 100644 index 0000000..b1fa411 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-many.html @@ -0,0 +1,16 @@ +

1.描述

+

单选块:type接口,支持三种类型'large','small','noBorder'

+

2.示例

+

(2.1)大尺寸(默认):文本距左右边框padding为30px,高28px

+ +

选中项:{{selected | json}}

+
+
+

(2.2)小尺寸:文本距左右边框padding为20px,高20px

+ +

选中项:{{selected1 | json}}

+
+
+

(2.3)无边框:文本距左右边框padding为20px,高28px

+ +

选中项:{{selected2 | json}}

diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-minwidth.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-minwidth.html new file mode 100644 index 0000000..8b7bf2d --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-minwidth.html @@ -0,0 +1,8 @@ +

+ diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-multi-type.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-multi-type.html new file mode 100644 index 0000000..45fcf45 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-multi-type.html @@ -0,0 +1,2 @@ +

+ diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-multiline.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-multiline.html new file mode 100644 index 0000000..3eb2cb5 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-multiline.html @@ -0,0 +1 @@ + diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-multiple.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-multiple.html new file mode 100644 index 0000000..5fb5741 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-multiple.html @@ -0,0 +1,4 @@ + +
+
Current value: {{ selected | json }}
+
diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-radio-type.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-radio-type.html new file mode 100644 index 0000000..4eeb199 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-radio-type.html @@ -0,0 +1,4 @@ +

+ +

+ diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-reactive-forms.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-reactive-forms.html new file mode 100644 index 0000000..5c0114d --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-reactive-forms.html @@ -0,0 +1,7 @@ +

1.描述

+

响应式表单

+

2.示例

+

(2.1)单选按钮组测试

+
+ +
diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-sup-test.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-sup-test.html new file mode 100644 index 0000000..04fd267 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-sup-test.html @@ -0,0 +1,24 @@ +

1.描述

+

按钮标志,兼容10.1.2版本之前用法

+

2.示例

+

(2.1)单选按钮组测试

+ +

选中项:{{selected | json}}

+ diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-sup.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-sup.html new file mode 100644 index 0000000..fa966b1 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-sup.html @@ -0,0 +1,28 @@ + + + {{sup.text}} + + + diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-template.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-template.html new file mode 100644 index 0000000..7d17aa1 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-template.html @@ -0,0 +1,14 @@ + + + + {{item.title}} + + +
+
+ + + + 第{{i}}项-{{item.title}} + + diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-tip.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-tip.html new file mode 100644 index 0000000..d829fbd --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-tip.html @@ -0,0 +1,4 @@ +

+ + + Template类型的提示
diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-valuekey-test.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-valuekey-test.html new file mode 100644 index 0000000..0edf288 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-valuekey-test.html @@ -0,0 +1,23 @@ +

描述

+

Buttongroup组件,自定义选中值,设置valueKey时选中值基于valueKey

+

valueKey接口

+

示例

+

1.单选:设置valueKey

+ +

选中项:{{selected | json}}

+
+
+

2.多选:设置valueKey

+ +

选中项:{{selected1 | json}}

+
+
+

+ 3.动态修改 + items 接口数据测试:(buttonGroup组件的选中值高亮不应发生变化) +

+ +
+
+

单选的items:{{items | json}}

+

多选的items:{{items1 | json}}

diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup-valuekey.html b/src/buttongroup/demo/src/app/buttongroup/buttongroup-valuekey.html new file mode 100644 index 0000000..85cf1c9 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup-valuekey.html @@ -0,0 +1,9 @@ + +
+
Current value: {{ selected | json }}
+
+
+ +
+
Current value: {{ selected1 | json }}
+
diff --git a/src/buttongroup/demo/src/app/buttongroup/buttongroup.less b/src/buttongroup/demo/src/app/buttongroup/buttongroup.less new file mode 100644 index 0000000..13cd7c2 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/buttongroup.less @@ -0,0 +1,8 @@ +.bgColor button { + background-color: red !important; + color: white !important; +} + +.color { + color: rgb(8, 107, 38); +} diff --git a/src/buttongroup/demo/src/app/buttongroup/webdoc/buttongroup-demos.js b/src/buttongroup/demo/src/app/buttongroup/webdoc/buttongroup-demos.js new file mode 100644 index 0000000..f04a0c5 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/webdoc/buttongroup-demos.js @@ -0,0 +1,180 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'buttongroup-items', + name: { + 'zh-CN': '基本使用', + 'en-US': '', + }, + desc: { + 'zh-CN': 'Buttongroup 组件的最简用法。默认单选。', + 'en-US': '', + }, + apis: [ + 'TiButtongroupComponent.properties.items', + 'TiButtonItem.properties.text', + ], + }, + { + demoId: 'buttongroup-multiple', + name: { + 'zh-CN': '多选', + 'en-US': '', + }, + desc: { + 'zh-CN': + '通过multiple配置为多选选块组,注意 ngModel 绑定值为数组。', + 'en-US': '', + }, + apis: ['TiButtongroupComponent.properties.multiple'], + }, + { + demoId: 'buttongroup-radio-type', + name: { + 'zh-CN': '不同样式的单选选块组', + 'en-US': '', + }, + desc: { + 'zh-CN': + '通过type属性配置选块样式,默认为large,包括largesmallnoBorder三种类型。', + 'en-US': '', + }, + apis: ['TiButtongroupComponent.properties.type'], + }, + { + demoId: 'buttongroup-multi-type', + name: { + 'zh-CN': '不同大小的多选选块组', + 'en-US': '', + }, + desc: { + 'zh-CN': + '通过type属性配置选块样式,默认为large,包括largesmall两种类型。', + 'en-US': '', + }, + apis: ['TiButtongroupComponent.properties.type'], + }, + { + demoId: 'buttongroup-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': '', + }, + desc: { + 'zh-CN': + '通过属性disabled配置整体是否为禁用状态,仅限于单选模式;通过items.disabled配置单个选块是否为禁用状态。', + 'en-US': '', + }, + apis: [ + 'TiButtongroupComponent.properties.disabled', + 'TiButtonItem.properties.disabled', + ], + }, + { + demoId: 'buttongroup-valuekey', + name: { + 'zh-CN': '选中值是一个基本类型数据', + 'en-US': '', + }, + desc: { + 'zh-CN': + '通过valueKey配置选中值是一个基本类型的数据,而不是默认的引用类型。valueKey传入item对象某个键名,选中值即为该键名的值。', + 'en-US': '', + }, + apis: ['TiButtongroupComponent.properties.valueKey'], + }, + { + demoId: 'buttongroup-sup', + name: { + 'zh-CN': '选块角标', + 'en-US': '', + }, + desc: { + 'zh-CN': + '通过#sup模板配置选块角标。角标样式由业务自定义,不提供公共样式。如果不显示,排查是否设置encapsulation: ViewEncapsulation.None

', + 'en-US': '', + }, + apis: ['TiButtongroupComponent.slots.supTemplate'], + }, + { + demoId: 'buttongroup-deselectable', + name: { + 'zh-CN': '取消单选选中', + 'en-US': '', + }, + desc: { + 'zh-CN': + '通过deselectable属性配置是否可以取消选中,仅限于单选模式。', + 'en-US': '', + }, + apis: ['TiButtongroupComponent.properties.deselectable'], + }, + { + demoId: 'buttongroup-minwidth', + name: { + 'zh-CN': '每个选块的最小宽度', + 'en-US': '', + }, + desc: { + 'zh-CN': '通过minwidth配置每个选块的最小宽度。', + 'en-US': '', + }, + apis: ['TiButtongroupComponent.properties.minWidth'], + }, + { + demoId: 'buttongroup-template', + name: { + 'zh-CN': '自定义内容', + 'en-US': '', + }, + desc: { + 'zh-CN': '通过#item模板配置选块内容。', + 'en-US': '', + }, + apis: ['TiButtongroupComponent.slots.itemTemplate'], + }, + { + demoId: 'buttongroup-tip', + name: { + 'zh-CN': '添加 tip 提示', + 'en-US': '', + }, + desc: { + 'zh-CN': + '通过items.tipContent配置提示信息,10.1.1 版本起,tip 接口的类型扩展为:string | TemplateRef | Component,旧版本为:string。通过items.tipPosition配置提示信息方位。超长文本以 title 显示,不建议使用 tip 提示。', + 'en-US': '', + }, + apis: [ + 'TiButtonItem.properties.tipContent', + 'TiButtonItem.properties.tipPosition', + ], + }, + { + demoId: 'buttongroup-multiline', + name: { + 'zh-CN': '较多的选块折行显示', + 'en-US': '', + }, + desc: { + 'zh-CN': '通过multiline配置多个选块在折行显示时有行间距。', + 'en-US': '', + }, + apis: ['TiButtongroupComponent.properties.multiline'], + }, + { + demoId: 'buttongroup-beforeclick', + name: { + 'zh-CN': 'beforeClick事件', + 'en-US': '', + }, + desc: { + 'zh-CN': + '按下非禁用的选块时触发的回调,参数为当前选块item,是否选中由业务处理,一般用于阻止选中。', + 'en-US': '', + }, + apis: ['TiButtongroupComponent.events.beforeClick'], + }, + ], +}; diff --git a/src/buttongroup/demo/src/app/buttongroup/webdoc/buttongroup.cn.md b/src/buttongroup/demo/src/app/buttongroup/webdoc/buttongroup.cn.md new file mode 100644 index 0000000..bd03ea8 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/webdoc/buttongroup.cn.md @@ -0,0 +1,33 @@ +--- +title: Buttongroup 选块组 +--- +# Buttongroup 选块组 + +
+ +从多个选块中选择一个或几个。 + ++ 支持单选、多选。 ++ 有多种大小样式。 ++ 可以添加角标。 ++ 可以自定义选块内容。 + +```typescript +import { TiButtongroupModule } from '@opentiny/ng'; +``` + +
+ +
+ +从多个选块中选择一个或几个。 + ++ 支持单选、多选。 ++ 有多种大小样式。 ++ 可以添加角标。 ++ 可以自定义选块内容。 + +```typescript +import { TiButtongroupModule } from '@opentiny/ng'; +``` +
diff --git a/src/buttongroup/demo/src/app/buttongroup/webdoc/buttongroup.en.md b/src/buttongroup/demo/src/app/buttongroup/webdoc/buttongroup.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/buttongroup/demo/src/app/buttongroup/webdoc/buttongroup.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/buttongroup/demo/src/favicon.ico b/src/buttongroup/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/buttongroup/demo/src/index.html b/src/buttongroup/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/buttongroup/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/buttongroup/demo/src/main.ts b/src/buttongroup/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/buttongroup/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/buttongroup/demo/test.ts b/src/buttongroup/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/buttongroup/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/buttongroup/demo/tsconfig.app.json b/src/buttongroup/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/buttongroup/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/buttongroup/demo/tsconfig.spec.json b/src/buttongroup/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/buttongroup/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/buttongroup/lib/index.ts b/src/buttongroup/lib/index.ts new file mode 100644 index 0000000..66f3c49 --- /dev/null +++ b/src/buttongroup/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiButtongroupModule'; diff --git a/src/buttongroup/lib/ng-package.json b/src/buttongroup/lib/ng-package.json new file mode 100644 index 0000000..6233b97 --- /dev/null +++ b/src/buttongroup/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/buttongroup", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/buttongroup/lib/package.json b/src/buttongroup/lib/package.json new file mode 100644 index 0000000..723297a --- /dev/null +++ b/src/buttongroup/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-buttongroup", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/buttongroup/lib/project.json b/src/buttongroup/lib/project.json new file mode 100644 index 0000000..68c6e9f --- /dev/null +++ b/src/buttongroup/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/buttongroup/lib", + "sourceRoot": "src/buttongroup/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/buttongroup"], + "options": { + "project": "src/buttongroup/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/buttongroup"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js buttongroup" + }, + { + "command": "ng default-build buttongroup" + }, + { + "command": "node build/clear-default-theme.js buttongroup" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/buttongroup && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build buttongroup && ng pack buttongroup && node build/publish.js buttongroup --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/buttongroup/lib/src/TiButtongroupComponent.ts b/src/buttongroup/lib/src/TiButtongroupComponent.ts new file mode 100644 index 0000000..3370c0f --- /dev/null +++ b/src/buttongroup/lib/src/TiButtongroupComponent.ts @@ -0,0 +1,310 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + Input, + IterableChanges, + IterableDiffer, + IterableDiffers, + OnInit, + Output, + QueryList, + Renderer2, + TemplateRef, + ViewChildren +} from '@angular/core'; +import { Util } from '@opentiny/ng-utils'; +import { TiFormComponent, TiWholeComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +// TODO: 标签命名不规范,TiButtongroup应该对应到ti-buttongroup +/** + * 每个选块的配置 + */ +export interface TiButtonItem { + /** + * 选块显示的文本 + */ + text?: string; + /** + * 选块是否显示 + */ + show?: boolean; + /** + * 选块是否禁用 + */ + disabled?: boolean; + /** + * tip 提示内容 + */ + tipContent?: string | TemplateRef | any; + /** + * tip 提示方向 + */ + tipPosition?: string; + /** + * @ignore + * 配置选块角标: + * + * 对象类型,包含两个属性:1.text: 显示的文本;2.class:标志的样式;eg: {text: string; class: string} + * + * 10.1.2版本之后可以通过 #sup 模板配置选块角标,因此隐藏sup键值对 + */ + sup?: { + text?: string; + class?: string; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; + }; + /** + * 和组件id拼接成选块id + id?: any; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} +/** + * buttonGroup选择按钮组组件 + * + * 该组件支持单选、多选两种形式,显示内容支持用户自定义 + * + * 单选块:用户要从一个数据集中选择单个选项。 + * 分大尺寸,小尺寸,无边框三种类型。 + * 支持可取消选中,默认选中时再次点击不取消当前选中项; + * + * 多选块:允许用户从一个数据集中选择多个选项。 + * 分大尺寸,小尺寸两种类型。 + * + */ +@Component({ + selector: 'ti-button-group', + templateUrl: './buttongroup.html', + styleUrls: ['./buttongroup.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-button-group-bottom-space]': 'multiline' + }, + providers: [TiFormComponent.getValueAccessor(TiButtongroupComponent)] +}) +export class TiButtongroupComponent extends TiWholeComponent { + /** + * 选块组的数据 + */ + @Input() items: Array; + /** + * 激活状态项样式类定义 + */ + @Input() activeClass: string; + /** + * 单个选块的最小宽度 + */ + @Input() minWidth: string; + /** + * 选块的尺寸和间距 + * + * 单选选块可选值包括 'large','small','noBorder';多选选块可选值包括 'large','small'。 + */ + @Input() type: string = 'large'; + /** + * 是否为多选选块组 + */ + @Input() multiple: boolean = false; + // TODO:Tiny4 deselectable改为unselectable + /** + * 单选选块组选中后再次点击是否可以取消选中 + */ + @Input() deselectable: boolean = false; + /** + * 用于取代公共样式类ti3-button-group-bottom-space + * + */ + /** + * 选块是否为多行,多行时有行间距 + */ + @Input() multiline: boolean = false; + /** + * 点击选块,数据选中前的回调事件 + */ + @Output() readonly beforeClick: EventEmitter = new EventEmitter(); + /** + * @ignore + * 获取到用户自定义的模板 + */ + @ContentChild(TemplateRef, { static: true }) firstTemplate: TemplateRef; + /** + * 选块内容区域的模板。 + */ + @ContentChild('item', { static: true }) itemTemplate: TemplateRef; + /** + * 选块角标区域的模板。 + */ + @ContentChild('sup', { static: true }) supTemplate: TemplateRef; + /** + * @ignore + * 获取每个按钮 + */ + @ViewChildren('btn') btns: QueryList; + /** + * @ignore + * 绑在模板上用户自定义激活样式 + */ + public actClass: string; + protected versionInfo: string = super.getVersion(packageInfo); + private itemsDiffer: IterableDiffer; + constructor( + protected elementRef: ElementRef, + protected renderer2: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + private iterableDiffers: IterableDiffers + ) { + super(elementRef, renderer2, changeDetectorRef); + } + + ngOnInit(): void { + super.ngOnInit(); + // 处理激活样式 + this.actClass = this.activeClass || 'ti3-active'; + this.itemsDiffer = this.iterableDiffers.find(this.items || []).create(); + } + + ngDoCheck(): void { + /** + * onPush模式下,可以通过监听items的变化标记变更检测,而且组件内已有itemsDiffer, + * 但是,原本的differ只会监听数组的增删改,要监听数组对象的属性变化,需要this.trackByFn, + * 而且,过多的变化引起多余的setFocusableElems, + * 因此改为直接标记变更检测。 + */ + super.ngDoCheck(); + this.changeDetectorRef.markForCheck(); + } + + ngAfterViewChecked(): void { + super.ngAfterViewChecked(); + const itemsDiffer: IterableChanges = this.itemsDiffer.diff(this.items || []); + if (itemsDiffer) { + const focusElements: Array = []; + this.btns.forEach((item: ElementRef) => { + focusElements.push(item.nativeElement); + }); + this.setFocusableElems(focusElements); + } + } + + /** + * 兼容旧版: + * 10.1.2 版本之前只能内嵌一个模板,无命名。 + * 新版可以内嵌两个模板,示例书写要求都命名(#item,#sup)。 + * 但需要兼容旧版无命名测试用例。 + */ + ngAfterContentInit(): void { + super.ngAfterContentInit(); + // 如果 item 模板为空 && 存在第一个模板,那么把第一个出现的 “非 #sup 标签” 的模板作为 item 模板 + if ( + !this.itemTemplate && + this.firstTemplate && + this.firstTemplate.elementRef.nativeElement !== (this.supTemplate && this.supTemplate.elementRef.nativeElement) + ) { + this.itemTemplate = this.firstTemplate; + } + } + + /** + * @ignore + * sup属性存在class类或text存在且有效 + */ + public isSuperScript(item: TiButtonItem): boolean { + return ( + item.sup && + item.sup.constructor === Object && + ((Util.isString(item.sup.class) && item.sup.class !== '') || (Util.isString(item.sup.text) && item.sup.text !== '')) + ); + } + + /** + * @ignore + * @description 根据selectedId来设置每个按钮的样式 + * @param: item + */ + public setActiveClass(item: string): string { + if (this.hasSelected(item)) { + return this.actClass; + } + + return ''; + } + + /** + * @ignore + * 每个item上的点击事件 + */ + public onClick(item: TiButtonItem): void { + if (item.disabled || this.disabled) { + // 灰化状态下 ,点击操作无效 + return; + } + if (this.beforeClick.observers.length > 0) { + this.beforeClick.emit(item); + + return; + } + + this.toggleItem(item); + } + + /** + * @ignore + * 每个item的点击事件(提供给lowCode平台使用) + */ + public toggleItem(item: TiButtonItem): void { + if (this.multiple) { + if (Util.isUndefined(this.modelWhole)) { + this.modelWhole = [item]; + } else { + const index: number = this.modelWhole.indexOf(item); + if (index !== -1) { + // 如果存在,则将其移除 + this.modelWhole.splice(index, 1); + } else { + // 先前未选中情况下,勾选 + this.modelWhole.push(item); + } + // selectedId是引用类型,内容改变不会触发modelchange,故需改变引用地址 + this.modelWhole = this.modelWhole.concat(); + } + } else { + this.modelWhole = Object.is(item, this.modelWhole) && this.deselectable ? '' : item; + } + } + + /** + * @ignore + * 判断是不是选中项 + */ + private hasSelected(item: string): boolean { + if (Util.isUndefined(this.modelWhole) || Util.isNull(this.modelWhole)) { + return; + } + + if (this.multiple) { + return this.modelWhole.indexOf(item) !== -1; + } else { + return Object.is(item, this.modelWhole); + } + } +} diff --git a/src/buttongroup/lib/src/TiButtongroupModule.ts b/src/buttongroup/lib/src/TiButtongroupModule.ts new file mode 100644 index 0000000..944470c --- /dev/null +++ b/src/buttongroup/lib/src/TiButtongroupModule.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiIconModule } from '@opentiny/ng-icon'; + +import { TiButtongroupComponent } from './TiButtongroupComponent'; + +@NgModule({ + imports: [CommonModule, TiIconModule, TiTipModule, TiOutlineModule], + exports: [TiButtongroupComponent], + declarations: [TiButtongroupComponent] +}) +export class TiButtongroupModule {} +export { TiButtongroupComponent, TiButtonItem } from './TiButtongroupComponent'; diff --git a/src/buttongroup/lib/src/buttongroup.html b/src/buttongroup/lib/src/buttongroup.html new file mode 100644 index 0000000..38b9ae7 --- /dev/null +++ b/src/buttongroup/lib/src/buttongroup.html @@ -0,0 +1,56 @@ + + + +
+ +
+ +
+ + + + {{item.sup.text}} + + + + + + + + +
+
+
diff --git a/src/buttongroup/lib/src/buttongroup.less b/src/buttongroup/lib/src/buttongroup.less new file mode 100644 index 0000000..468232c --- /dev/null +++ b/src/buttongroup/lib/src/buttongroup.less @@ -0,0 +1,193 @@ +@import '../../../themes/basic/base-all.less'; +:host { + --ti-button-group-item-max-width: 300px; + --ti-button-group-item-sup-height: var(--ti-common-size-4x); + --ti-button-group-item-height: var(--ti-common-size-7x); + --ti-button-group-item-small-height: var(--ti-common-size-5x); + --ti-button-group-border-weight: var(--ti-common-border-weight-normal); + --ti-button-group-radio-item-margin-right: 2px; + --ti-button-group-radio-item-padding-horizontal: 30px; +} + +:host[disabled] { + .ti3-btn-item-container.ti3-active .ti3-btn-item-radio { + background-color: var(--ti-common-color-bg-dark-disabled); + color: var(--ti-common-color-text-disabled); + } +} +.ti3-btn-item-container { + display: inline-block; + position: relative; + margin-right: var(--ti-button-group-radio-item-margin-right); + + & .ti3-btn-item-sup { + position: absolute; + min-width: var(--ti-button-group-item-sup-height); + height: var(--ti-button-group-item-sup-height); + top: calc(-1 * var(--ti-button-group-item-sup-height) / 2); + color: var(--ti-common-color-icon-white); + right: 0px; + font-size: var(--ti-common-size-3x); + line-height: var(--ti-button-group-item-sup-height); + text-align: center; + } + + & .ti3-btn-item.ti3-btn-item-checkbox { + border-radius: var(--ti-common-border-radius-normal); + } + + &:last-child { + margin-right: 0; + + & .ti3-btn-item.ti3-btn-item-radio { + border-radius: 0 var(--ti-common-border-radius-normal) var(--ti-common-border-radius-normal) 0; + } + & .ti3-btn-item.ti3-btn-item-noBorder { + border-radius: var(--ti-common-border-radius-normal); + } + } + + &:first-child { + & .ti3-btn-item.ti3-btn-item-radio { + border-radius: var(--ti-common-border-radius-normal) 0 0 var(--ti-common-border-radius-normal); + } + + & .ti3-btn-item.ti3-btn-item-noBorder { + border-radius: var(--ti-common-border-radius-normal); + } + } + + // 单选按钮选中状态有背景色 + &.ti3-active .ti3-btn-item-radio { + background-color: var(--ti-common-color-bg-emphasize); + color: var(--ti-common-color-text-white); + .transition (background; 250ms, color; 250ms); + &[disabled] { + background-color: var(--ti-common-color-bg-dark-disabled); + color: var(--ti-common-color-text-disabled); + } + } + .ti3-btn-item[disabled] { + // 灰化样式优先级要高于hover,所以在选择器上套了一层父容器,增加样式权重 + cursor: not-allowed; + background-color: var(--ti-common-color-bg-disabled); + color: var(--ti-common-color-text-disabled); + border-color: var(--ti-common-color-bg-disabled); + &.ti3-btn-item-noBorder { + background-color: var(--ti-common-color-bg-white-normal); + } + } +} +// 多选块:空间充足 +.ti3-btn-checkbox-enough-space { + margin-right: var(--ti-common-space-2x); + &:last-child { + margin-right: 0px; + } + .ti3-btn-item.ti3-btn-item-checkbox { + padding: var(--ti-common-space-0) var(--ti-common-space-5x); + } +} +// 多选块:空间不足 +.ti3-btn-checkbox-deficient-space { + margin-right: var(--ti-common-space-2x); + &:last-child { + margin-right: 0px; + } + .ti3-btn-item.ti3-btn-item-checkbox { + padding: var(--ti-common-space-0) var(--ti-common-space-2x); + } +} + +.ti3-btn-item { + position: relative; + display: inline-block; + .transition (border-color; 150ms; ease-in, background; 150ms; ease-in); + height: var(--ti-button-group-item-height); + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + color: var(--ti-common-color-text-primary); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + .box-sizing(border-box); + background-color: var(--ti-common-color-bg-light-normal); + line-height: calc(var(--ti-button-group-item-height) - var(--ti-button-group-border-weight) * 2); + border: var(--ti-button-group-border-weight) var(--ti-common-border-style-solid) var(--ti-common-color-bg-light-normal); + max-width: var(--ti-button-group-item-max-width); + + // 单选: + &.ti3-btn-item-radio { + padding: var(--ti-common-space-0) var(--ti-button-group-radio-item-padding-horizontal); + border: none; + line-height: var(--ti-button-group-item-height); + } +} +.ti3-btn-item-text { + display: inline-block; + vertical-align: top; + width: 100%; + .ellipsis(); +} + +.ti3-btn-item-small .ti3-btn-item { + height: var(--ti-button-group-item-small-height); + line-height: var(--ti-button-group-item-small-height); + padding: var(--ti-common-space-0) var(--ti-common-space-5x); +} + +.ti3-btn-item-container:not(.ti3-active) .ti3-btn-item:not([disabled]) { + &:hover, + &:focus { + background-color: var(--ti-common-color-bg-light-emphasize); + color: var(--ti-common-color-text-primary); + border-color: var(--ti-common-color-prompt-border); + .transition (border-color; 200ms, background; 200ms); + } +} + +.ti3-btn-item-container.ti3-active .ti3-btn-item { + border-color: var(--ti-common-color-line-active); +} + +// buttton无边框的样式 +.ti3-btn-item-noBorder.ti3-btn-item { + border-radius: var(--ti-common-border-radius-normal); + padding: var(--ti-common-space-0) var(--ti-common-space-5x); + background-color: transparent; + &:hover { + background-color: var(--ti-common-color-bg-light-emphasize); + } + + &[disabled] { + border: none; + } +} + +// 多选块按钮右上角标: +.ti3-btn-multi-selection-icon { + display: inline-block; + color: var(--ti-common-color-icon-white); + width: 0; + height: 0; + line-height: normal; + border-right: var(--ti-common-size-4x) solid var(--ti-common-color-bg-emphasize); + border-bottom: var(--ti-common-size-4x) solid transparent; + position: absolute; + right: var(--ti-button-group-border-weight); + top: var(--ti-button-group-border-weight); + font-size: var(--ti-common-size-3x); + & .ti3-icon-checkmark-small { + position: relative; + right: -5px; + top: -2px; + } +} + +// buttonGroup组件在换行时,添加样式类来控制间距 +:host.ti3-button-group-bottom-space { + .ti3-btn-item-container { + margin-bottom: var(--ti-common-space-base); + } +} diff --git a/src/buttonselect/demo/project.json b/src/buttonselect/demo/project.json new file mode 100644 index 0000000..c6066f9 --- /dev/null +++ b/src/buttonselect/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/buttonselect/demo", + "sourceRoot": "src/buttonselect/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/buttonselect", + "index": "src/buttonselect/demo/src/index.html", + "main": "src/buttonselect/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/buttonselect/demo/tsconfig.app.json", + "assets": ["src/buttonselect/demo/src/favicon.ico", "src/buttonselect/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "buttonselect-demo:build:production" + }, + "development": { + "browserTarget": "buttonselect-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js buttonselect" + } + ] + } + } + } +} diff --git a/src/buttonselect/demo/src/app/AppComponent.ts b/src/buttonselect/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/buttonselect/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/buttonselect/demo/src/app/AppModule.ts b/src/buttonselect/demo/src/app/AppModule.ts new file mode 100644 index 0000000..21fa10e --- /dev/null +++ b/src/buttonselect/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ButtonselectTestModule } from './buttonselect/ButtonselectTestModule'; + +@NgModule({ + imports: [ + ButtonselectTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/buttonselect/demo/src/app/IndexComponent.ts b/src/buttonselect/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..bebad8d --- /dev/null +++ b/src/buttonselect/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ButtonselectTestModule } from './buttonselect/ButtonselectTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = ButtonselectTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/buttonselect/demo/src/app/app.html b/src/buttonselect/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/buttonselect/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/buttonselect/demo/src/app/buttonselect/ButtonselectBasicComponent.ts b/src/buttonselect/demo/src/app/buttonselect/ButtonselectBasicComponent.ts new file mode 100644 index 0000000..23b9419 --- /dev/null +++ b/src/buttonselect/demo/src/app/buttonselect/ButtonselectBasicComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './buttonselect-basic.html' +}) +export class ButtonselectBasicComponent { + options: Array = [ + { label: '1年原厂换修¥79.00 ' }, + { label: '2年全保修¥139.00' }, + { label: '3年全保修¥189.00 ' }, + { label: '2年全保修+1年免费更换原厂屏幕一次¥159.00' } + ]; + + selected: any = this.options[1]; +} diff --git a/src/buttonselect/demo/src/app/buttonselect/ButtonselectLabelkeyComponent.ts b/src/buttonselect/demo/src/app/buttonselect/ButtonselectLabelkeyComponent.ts new file mode 100644 index 0000000..000b5f3 --- /dev/null +++ b/src/buttonselect/demo/src/app/buttonselect/ButtonselectLabelkeyComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './buttonselect-labelkey.html' +}) +export class ButtonselectLabelkeyComponent { + options: Array = [{ text: '手机壳' }, { text: '移动电源' }, { text: '蓝牙耳机' }]; + + selected: any = this.options[1]; +} diff --git a/src/buttonselect/demo/src/app/buttonselect/ButtonselectTestModule.ts b/src/buttonselect/demo/src/app/buttonselect/ButtonselectTestModule.ts new file mode 100644 index 0000000..e3a572d --- /dev/null +++ b/src/buttonselect/demo/src/app/buttonselect/ButtonselectTestModule.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; +import { TiButtonselectModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { ButtonselectBasicComponent } from './ButtonselectBasicComponent'; +import { ButtonselectLabelkeyComponent } from './ButtonselectLabelkeyComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiButtonselectModule, DemoLogModule, RouterModule.forChild(ButtonselectTestModule.ROUTES)], + declarations: [ButtonselectBasicComponent, ButtonselectLabelkeyComponent] +}) +export class ButtonselectTestModule { + static readonly ROUTES: Routes = [ + { + path: 'buttonselect/buttonselect-basic', + component: ButtonselectBasicComponent + }, + { + path: 'buttonselect/buttonselect-label', + component: ButtonselectLabelkeyComponent + } + ]; +} diff --git a/src/buttonselect/demo/src/app/buttonselect/buttonselect-basic.html b/src/buttonselect/demo/src/app/buttonselect/buttonselect-basic.html new file mode 100644 index 0000000..7831549 --- /dev/null +++ b/src/buttonselect/demo/src/app/buttonselect/buttonselect-basic.html @@ -0,0 +1 @@ + diff --git a/src/buttonselect/demo/src/app/buttonselect/buttonselect-labelkey.html b/src/buttonselect/demo/src/app/buttonselect/buttonselect-labelkey.html new file mode 100644 index 0000000..2b9100a --- /dev/null +++ b/src/buttonselect/demo/src/app/buttonselect/buttonselect-labelkey.html @@ -0,0 +1 @@ + diff --git a/src/buttonselect/demo/src/app/buttonselect/webdoc/buttonselect-demos.js b/src/buttonselect/demo/src/app/buttonselect/webdoc/buttonselect-demos.js new file mode 100644 index 0000000..13ea430 --- /dev/null +++ b/src/buttonselect/demo/src/app/buttonselect/webdoc/buttonselect-demos.js @@ -0,0 +1,38 @@ +export default { + column: '2', + demos: [ + { + demoId: 'buttonselect-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

Buttonselect 组件的最简用法。

', + 'en-US': 'Basic usage' + }, + apis: ['TiButtonselectComponent.properties.options'] + }, + { + demoId: 'buttonselect-labelkey', + name: { + 'zh-CN': '显示字段', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

通过属性labelKey配置组件面板显示字段。

', + 'en-US': 'Basic usage' + }, + apis: ['TiButtonselectComponent.properties.labelKey'] + } + ], + ignoreApis: [ + 'TiButtonselectComponent.properties.tabindex', + 'TiButtonselectComponent.properties.valueKey', + 'TiButtonselectComponent.properties.disabled', + 'TiButtonselectComponent.methods.focus', + 'TiButtonselectComponent.methods.blur', + 'TiButtonselectComponent.events.focus', + 'TiButtonselectComponent.events.blur' + ] +}; diff --git a/src/buttonselect/demo/src/app/buttonselect/webdoc/buttonselect.cn.md b/src/buttonselect/demo/src/app/buttonselect/webdoc/buttonselect.cn.md new file mode 100644 index 0000000..99881b6 --- /dev/null +++ b/src/buttonselect/demo/src/app/buttonselect/webdoc/buttonselect.cn.md @@ -0,0 +1,17 @@ +--- +title: Buttonselect 选块下拉 +--- + +# Buttonselect 选块下拉 + +
+ +Buttonselect 选块下拉组件 + ++ 由选块和下拉面板组成,可以取消或选中选块,下拉面板可以更改选块内容 + +```typescript +import { TiButtonselectModule } from '@opentiny/ng'; +``` + +
diff --git a/src/buttonselect/demo/src/app/buttonselect/webdoc/buttonselect.en.md b/src/buttonselect/demo/src/app/buttonselect/webdoc/buttonselect.en.md new file mode 100644 index 0000000..742e5f2 --- /dev/null +++ b/src/buttonselect/demo/src/app/buttonselect/webdoc/buttonselect.en.md @@ -0,0 +1,13 @@ +--- +title: Buttonselect +--- + +# Buttonselect + +
+ +```typescript +import { TiButtonselectModule } from '@opentiny/ng'; +``` + +
diff --git a/src/buttonselect/demo/src/favicon.ico b/src/buttonselect/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/buttonselect/demo/src/index.html b/src/buttonselect/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/buttonselect/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/buttonselect/demo/src/main.ts b/src/buttonselect/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/buttonselect/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/buttonselect/demo/tsconfig.app.json b/src/buttonselect/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/buttonselect/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/buttonselect/lib/index.ts b/src/buttonselect/lib/index.ts new file mode 100644 index 0000000..5b050f4 --- /dev/null +++ b/src/buttonselect/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiButtonselectModule'; +export * from './src/TiButtonselectComponent'; diff --git a/src/buttonselect/lib/ng-package.json b/src/buttonselect/lib/ng-package.json new file mode 100644 index 0000000..51b93cc --- /dev/null +++ b/src/buttonselect/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/buttonselect", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/buttonselect/lib/package.json b/src/buttonselect/lib/package.json new file mode 100644 index 0000000..df71393 --- /dev/null +++ b/src/buttonselect/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-buttonselect", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-droplist": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/buttonselect/lib/project.json b/src/buttonselect/lib/project.json new file mode 100644 index 0000000..3e6aba8 --- /dev/null +++ b/src/buttonselect/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/buttonselect/lib", + "sourceRoot": "src/buttonselect/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/buttonselect"], + "options": { + "project": "src/buttonselect/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/buttonselect"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js buttonselect" + }, + { + "command": "ng default-build buttonselect" + }, + { + "command": "node build/clear-default-theme.js buttonselect" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/buttonselect && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build buttonselect && ng pack buttonselect && node build/publish.js buttonselect --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/buttonselect/lib/src/TiButtonselectComponent.ts b/src/buttonselect/lib/src/TiButtonselectComponent.ts new file mode 100644 index 0000000..4a13294 --- /dev/null +++ b/src/buttonselect/lib/src/TiButtonselectComponent.ts @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Input, + Renderer2, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { TiFormComponent, TiWholeComponent } from '@opentiny/ng-base'; +import { TiDroplistComponent } from '@opentiny/ng-droplist'; +@Component({ + selector: 'ti-buttonselect', + templateUrl: 'buttonselect.html', + styleUrls: ['buttonselect.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiButtonselectComponent)] +}) +export class TiButtonselectComponent extends TiWholeComponent { + constructor(private render: Renderer2, protected hostRef: ElementRef, protected cdRef: ChangeDetectorRef) { + super(hostRef, render, cdRef); + } + /** + * 下拉面板数据 + */ + @Input() options: Array = []; + /** + * 下拉面板显示options数组对象中的哪个字段 + */ + @Input() labelKey: string = 'label'; + /** + * @ignore + */ + @ViewChild(TiDroplistComponent) droplistCom: TiDroplistComponent; + /** + * @ignore + */ + @ViewChild('button', { static: true }) buttonRef: ElementRef; + /** + * @ignore 待选项 + */ + public candidate: any; + + // chooseItem: any; + + ngOnChanges(changes: SimpleChanges): void { + if (changes['options'] && changes['options'].currentValue.length > 0) { + this.candidate = this.options[0]; + } + } + protected ngOnModelChange(model: any): void { + this.candidate = model ? model : this.candidate; + } + + ngAfterViewInit(): void { + this.setFocusableElems([this.buttonRef.nativeElement]); + this.render.listen(this.droplistCom.dropCom.nativeElement, 'mouseleave', (event: MouseEvent) => { + const dropPosition: string = this.droplistCom.dropCom.position; + if (dropPosition.split('-')[0] === 'bottom') { + if (event.offsetX > 0 && event.offsetX < this.buttonRef.nativeElement.offsetWidth && event.offsetY < 0) { + return; + } + } else { + // 面板在上方展开时,鼠标向上移动不能关闭面板,向下要关闭面板 + if ( + event.offsetX > 0 && + event.offsetX < this.buttonRef.nativeElement.offsetWidth && + event.offsetY >= (event.target as any).offsetHeight + ) { + return; + } + } + this.droplistCom.hide(); + }); + } + /** + * @ignore 鼠标移入button + */ + onMouseenterButton(): void { + this.droplistCom.show(); + } + /** + * @ignore 鼠标移出button + */ + onMouseleaveButton(event: MouseEvent): void { + const dropPosition: string = this.droplistCom.dropCom.position; + if (dropPosition.split('-')[0] === 'bottom') { + // 面板在下方展开时,鼠标向下移动不能关闭面板 + if ( + event.offsetX > 0 && + event.offsetX < (event.target as any).offsetWidth && + event.offsetY > (event.target as any).offsetHeight - 2 + ) { + return; + } + } else { + // 面板在上方展开时,鼠标向上移动不能关闭面板,向下要关闭面板 + if (event.offsetX > 0 && event.offsetX < (event.target as any).offsetWidth && event.offsetY < 1) { + return; + } + } + this.droplistCom.hide(); + } + /** + * @ignore 点击button + */ + onClickButton(): void { + this.droplistCom.hide(); + this.model = this.model ? undefined : this.candidate; + } + + /** + * @ignore 下拉面板选中 + */ + onDroplistChange(event: any): void { + this.model = event; + this.candidate = event; + } +} diff --git a/src/buttonselect/lib/src/TiButtonselectModule.ts b/src/buttonselect/lib/src/TiButtonselectModule.ts new file mode 100644 index 0000000..893e57c --- /dev/null +++ b/src/buttonselect/lib/src/TiButtonselectModule.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { TiDroplistModule } from '@opentiny/ng-droplist'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; + +import { TiButtonselectComponent } from './TiButtonselectComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiDroplistModule, TiIconModule, TiOutlineModule], + exports: [TiButtonselectComponent], + declarations: [TiButtonselectComponent] +}) +export class TiButtonselectModule {} + +export { TiButtonselectComponent } from './TiButtonselectComponent'; diff --git a/src/buttonselect/lib/src/buttonselect.html b/src/buttonselect/lib/src/buttonselect.html new file mode 100644 index 0000000..6eaaf29 --- /dev/null +++ b/src/buttonselect/lib/src/buttonselect.html @@ -0,0 +1,28 @@ + + + + {{item[labelKey]}} + + diff --git a/src/buttonselect/lib/src/buttonselect.less b/src/buttonselect/lib/src/buttonselect.less new file mode 100644 index 0000000..8e74f15 --- /dev/null +++ b/src/buttonselect/lib/src/buttonselect.less @@ -0,0 +1,77 @@ +::ng-deep :root { + --ti-buttonselect-maxwidth: 300px; + --ti-buttonselect-padding: 20px; + --ti-buttonselect-triangle-width: 16px; + --ti-button-group-height: 28px; +} + +:host { + display: inline-block; +} +.ti-btnselect-triangle { + display: inline-block; + width: 16px; + &:after { + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid var(--ti-common-color-icon-normal); + content: ''; + position: absolute; + top: 10.5px; + } +} +.ti-btnselect-triangle-up:after { + transform: rotate(180deg); +} + +.ti-buttonselect-button { + border-radius: var(--ti-common-border-radius-normal); + border: 1px solid transparent; + padding: 0 var(--ti-buttonselect-padding); + position: relative; + display: inline-block; + height: var(--ti-button-group-height); + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + color: #252b3a; + background-color: #e9edfa; + line-height: calc(var(--ti-button-group-height) - 2px); + max-width: var(--ti-buttonselect-maxwidth); + &.ti-buttonselect-dominator-border { + border: 1px solid var(--ti-common-color-bg-emphasize); + } + &:not(.ti-buttonselect-dominator-border):hover, + &:not(.ti-buttonselect-dominator-border):focus { + background-color: var(--ti-common-color-bg-light-emphasize); + } +} +.ti-buttonselect-text { + display: inline-block; + max-width: calc(var(--ti-buttonselect-maxwidth) - var(--ti-buttonselect-padding) * 2 - var(--ti-buttonselect-triangle-width)); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + vertical-align: bottom; +} +.ti-buttonselect-badge { + display: inline-block; + color: #fff; + width: 0; + height: 0; + border-right: 16px solid #5e7ce0; + border-bottom: 16px solid transparent; + position: absolute; + right: 0px; + top: 0px; + font-size: 12px; + & .ti3-icon-checkmark-small { + position: relative; + right: -5px; + top: -8px; + } +} +.ti-buttonselect-list { + margin: 0 10px; +} diff --git a/src/card/demo/karma.conf.js b/src/card/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/card/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/card/demo/project.json b/src/card/demo/project.json new file mode 100644 index 0000000..2a2c933 --- /dev/null +++ b/src/card/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/card/demo", + "sourceRoot": "src/card/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/card", + "index": "src/card/demo/src/index.html", + "main": "src/card/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/card/demo/tsconfig.app.json", + "assets": ["src/card/demo/src/favicon.ico", "src/card/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "card-demo:build:production" + }, + "development": { + "browserTarget": "card-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js card" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/card/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/card/demo/tsconfig.spec.json", + "karmaConfig": "src/card/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/card/demo/src/app/AppComponent.ts b/src/card/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/card/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/card/demo/src/app/AppModule.ts b/src/card/demo/src/app/AppModule.ts new file mode 100644 index 0000000..0116cef --- /dev/null +++ b/src/card/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { CardTestModule } from './card/CardTestModule'; + +@NgModule({ + imports: [ + CardTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/card/demo/src/app/IndexComponent.ts b/src/card/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..7efbea3 --- /dev/null +++ b/src/card/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { CardTestModule } from './card/CardTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = CardTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/card/demo/src/app/app.html b/src/card/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/card/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/card/demo/src/app/card/CardAddComponent.ts b/src/card/demo/src/app/card/CardAddComponent.ts new file mode 100644 index 0000000..985f068 --- /dev/null +++ b/src/card/demo/src/app/card/CardAddComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './card-add.html' +}) +export class CardAddComponent { + // 模拟事件打印日志 + myLogs: Array = []; + // add 事件 + onAddCard(event: Event): void { + this.myLogs = [...this.myLogs, '【event:add】-- callback']; + } +} diff --git a/src/card/demo/src/app/card/CardBasicComponent.ts b/src/card/demo/src/app/card/CardBasicComponent.ts new file mode 100644 index 0000000..d9d0fc4 --- /dev/null +++ b/src/card/demo/src/app/card/CardBasicComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './card-basic.html' +}) +export class CardBasicComponent {} diff --git a/src/card/demo/src/app/card/CardGrid2Component.ts b/src/card/demo/src/app/card/CardGrid2Component.ts new file mode 100644 index 0000000..555deb4 --- /dev/null +++ b/src/card/demo/src/app/card/CardGrid2Component.ts @@ -0,0 +1,27 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './card-grid2.html' +}) +export class CardGrid2Component { + cards: Array = [ + { + title: 'card-0' + }, + { + title: 'card-1' + }, + { + title: 'card-2' + }, + { + title: 'card-3' + }, + { + title: 'card-4' + }, + { + title: 'card-5' + } + ]; +} diff --git a/src/card/demo/src/app/card/CardGridComponent.ts b/src/card/demo/src/app/card/CardGridComponent.ts new file mode 100644 index 0000000..3121e58 --- /dev/null +++ b/src/card/demo/src/app/card/CardGridComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './card-grid.html' +}) +export class CardGridComponent { + cards: Array = [ + { + title: 'card-0' + }, + { + title: 'card-1' + }, + { + title: 'card-2' + }, + { + title: 'card-3' + }, + { + title: 'card-4' + }, + { + title: 'card-5' + } + ]; +} diff --git a/src/card/demo/src/app/card/CardHeaderComponent.ts b/src/card/demo/src/app/card/CardHeaderComponent.ts new file mode 100644 index 0000000..0515c76 --- /dev/null +++ b/src/card/demo/src/app/card/CardHeaderComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './card-header.html' +}) +export class CardHeaderComponent {} diff --git a/src/card/demo/src/app/card/CardTestModule.ts b/src/card/demo/src/app/card/CardTestModule.ts new file mode 100644 index 0000000..4fcd415 --- /dev/null +++ b/src/card/demo/src/app/card/CardTestModule.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiCardModule, TiGridModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { CardAddComponent } from './CardAddComponent'; +import { CardBasicComponent } from './CardBasicComponent'; +import { CardGridComponent } from './CardGridComponent'; +import { CardGrid2Component } from './CardGrid2Component'; +import { CardHeaderComponent } from './CardHeaderComponent'; + +@NgModule({ + declarations: [CardBasicComponent, CardAddComponent, CardGridComponent, CardGrid2Component, CardHeaderComponent], + imports: [CommonModule, TiCardModule, TiGridModule, DemoLogModule, RouterModule.forChild(CardTestModule.ROUTES)] +}) +export class CardTestModule { + static readonly LINKS: Array = [{ href: 'components/TiCardComponent.html', label: 'Card' }]; + public static readonly ROUTES: Routes = [ + { path: 'card/card-basic', component: CardBasicComponent }, + { path: 'card/card-add', component: CardAddComponent }, + { path: 'card/card-header', component: CardHeaderComponent }, + { path: 'card/card-grid', component: CardGridComponent }, + { path: 'card/card-grid2', component: CardGrid2Component } + ]; +} diff --git a/src/card/demo/src/app/card/card-add.html b/src/card/demo/src/app/card/card-add.html new file mode 100644 index 0000000..331fa0c --- /dev/null +++ b/src/card/demo/src/app/card/card-add.html @@ -0,0 +1,6 @@ +
+ + + +
+ diff --git a/src/card/demo/src/app/card/card-basic.html b/src/card/demo/src/app/card/card-basic.html new file mode 100644 index 0000000..c0061cf --- /dev/null +++ b/src/card/demo/src/app/card/card-basic.html @@ -0,0 +1,11 @@ +
+ +

普通状态

+
+ +

激活状态

+
+ +

禁用状态

+
+
diff --git a/src/card/demo/src/app/card/card-grid.html b/src/card/demo/src/app/card/card-grid.html new file mode 100644 index 0000000..cfad878 --- /dev/null +++ b/src/card/demo/src/app/card/card-grid.html @@ -0,0 +1,5 @@ +
+ +

{{card.title}}

+
+
diff --git a/src/card/demo/src/app/card/card-grid2.html b/src/card/demo/src/app/card/card-grid2.html new file mode 100644 index 0000000..3c4150f --- /dev/null +++ b/src/card/demo/src/app/card/card-grid2.html @@ -0,0 +1,5 @@ +
+ +

{{card.title}}

+
+
diff --git a/src/card/demo/src/app/card/card-header.html b/src/card/demo/src/app/card/card-header.html new file mode 100644 index 0000000..7a3e883 --- /dev/null +++ b/src/card/demo/src/app/card/card-header.html @@ -0,0 +1,22 @@ +
+ + +

卡片头部

+
+

ti-card正常状态

+
+ + + +

卡片头部

+
+

ti-card 激活状态

+
+ + + +

卡片头部

+
+

ti-card 禁用状态

+
+
diff --git a/src/card/demo/src/app/card/webdoc/card-demos.js b/src/card/demo/src/app/card/webdoc/card-demos.js new file mode 100644 index 0000000..024f7ef --- /dev/null +++ b/src/card/demo/src/app/card/webdoc/card-demos.js @@ -0,0 +1,74 @@ +export default { + column: '1', + demos: [ + { + demoId: 'card-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'card', + }, + desc: { + 'zh-CN': '

通过属性active配置卡片是否为激活状态,通过属性disabled配置卡片是否为禁用状态。', + 'en-US': '

card

', + }, + apis: [ + 'TicardComponent.properties.active', + 'TicardComponent.properties.disabled' + ] + }, + { + demoId: 'card-add', + name: { + 'zh-CN': '可添加的卡片', + 'en-US': 'card add', + }, + desc: { + 'zh-CN': '

ti-card-add是一种特殊的ti-card,用于卡片添加的入口。', + 'en-US': '

card add

', + }, + apis: [ + 'TiCardAddComponent.properties.active', + 'TiCardAddComponent.properties.disabled', + 'TiCardAddComponent.events.add' + ] + }, + { + demoId: 'card-header', + name: { + 'zh-CN': '卡片头部', + 'en-US': 'card header', + }, + desc: { + 'zh-CN': '

通过组件ti-card-header配置卡片头部。

', + 'en-US': '

card header

', + }, + apis: [ + 'TiCardAddComponent.properties.active', + 'TiCardAddComponent.properties.disabled' + ] + }, + { + demoId: 'card-grid', + name: { + 'zh-CN': '栅格布局固定列', + 'en-US': 'card grid', + }, + desc: { + 'zh-CN': '

使用栅格布局需要导入TiGridModule模块,根据栅格体系,自适应依据规范定义的四种分辨率 xs:1280~1439【1280、1366】, sm:1440~1599【1440】,md:1600~1759【1680】, lg: 1760~1920【1920】,通过不同的样式类设置子元素的宽度,使用 ti-row 定义一行,使用 ti-col-xs-[num] 设置一行所所占的比。

', + 'en-US': '

card icon

', + } + }, + { + demoId: 'card-grid2', + name: { + 'zh-CN': '栅格布局自适应', + 'en-US': 'card grid2', + }, + desc: { + 'zh-CN': + '

使用栅格布局需要导入TiGridModule模块,ti-col-xs-12 样式类设置 1440 以下屏幕分辨率一张卡片所占的宽度,一行显示 2 张卡片,ti-col-sm-8 样式类设置 1440~1599 屏幕分辨率一张卡片所占的宽度,一行显示 3 张卡片,ti-col-md-6 样式类设置 1600~1759 屏幕分辨率一张卡片所占的宽度,一行显示 4 张卡片,ti-col-lg-4 样式类设置 1760~1920 屏幕分辨率一张卡片所占的宽度,一行显示 6 张卡片。

', + 'en-US': '

card grid2

', + } + } + ], +}; diff --git a/src/card/demo/src/app/card/webdoc/card.cn.md b/src/card/demo/src/app/card/webdoc/card.cn.md new file mode 100644 index 0000000..bf0cb14 --- /dev/null +++ b/src/card/demo/src/app/card/webdoc/card.cn.md @@ -0,0 +1,25 @@ +--- +title: Card 卡片 +--- + +# Card 卡片 + +
+ +卡片是承载图片、文字等内容的矩形区块。 + +```typescript +import { TiCardModule } from '@opentiny/ng'; +``` + +
+ +
+ +卡片是承载图片、文字等内容的矩形区块。 + +```typescript +import { TiCardModule } from '@opentiny/ng'; +``` + +
diff --git a/src/card/demo/src/app/card/webdoc/card.en.md b/src/card/demo/src/app/card/webdoc/card.en.md new file mode 100644 index 0000000..bf0cb14 --- /dev/null +++ b/src/card/demo/src/app/card/webdoc/card.en.md @@ -0,0 +1,25 @@ +--- +title: Card 卡片 +--- + +# Card 卡片 + +
+ +卡片是承载图片、文字等内容的矩形区块。 + +```typescript +import { TiCardModule } from '@opentiny/ng'; +``` + +
+ +
+ +卡片是承载图片、文字等内容的矩形区块。 + +```typescript +import { TiCardModule } from '@opentiny/ng'; +``` + +
diff --git a/src/card/demo/src/favicon.ico b/src/card/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/card/demo/src/index.html b/src/card/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/card/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/card/demo/src/main.ts b/src/card/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/card/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/card/demo/test.ts b/src/card/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/card/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/card/demo/tsconfig.app.json b/src/card/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/card/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/card/demo/tsconfig.spec.json b/src/card/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/card/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/card/lib/index.ts b/src/card/lib/index.ts new file mode 100644 index 0000000..41b68d7 --- /dev/null +++ b/src/card/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiCardModule'; diff --git a/src/card/lib/ng-package.json b/src/card/lib/ng-package.json new file mode 100644 index 0000000..166205f --- /dev/null +++ b/src/card/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/card", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/card/lib/package.json b/src/card/lib/package.json new file mode 100644 index 0000000..49b15c3 --- /dev/null +++ b/src/card/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-card", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/card/lib/project.json b/src/card/lib/project.json new file mode 100644 index 0000000..c3583c6 --- /dev/null +++ b/src/card/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/card/lib", + "sourceRoot": "src/card/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/card"], + "options": { + "project": "src/card/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/card"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js card" + }, + { + "command": "ng default-build card" + }, + { + "command": "node build/clear-default-theme.js card" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/card && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build card && ng pack card && node build/publish.js card --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/card/lib/src/TiCardAddComponent.ts b/src/card/lib/src/TiCardAddComponent.ts new file mode 100644 index 0000000..e0376e6 --- /dev/null +++ b/src/card/lib/src/TiCardAddComponent.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +/** + * 特殊卡片组件,添加卡片组件 + * + */ +@Component({ + selector: 'ti-card-add', + templateUrl: './card-add.html', + styleUrls: ['./card-add.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-card-add-disabled]': 'disabled', + '[class.ti3-card-add-active]': 'active', + '[attr.tabindex]': 'disabled? "-1":"0"' + } +}) +export class TiCardAddComponent extends TiBaseComponent { + /** + * 卡片是否禁用 + */ + @Input() disabled: boolean = false; + /** + * 卡片激活选中状态 + */ + @Input() active: boolean = false; + /** + * 点击加号添加卡片时触发的回调 + */ + @Output() readonly add: EventEmitter = new EventEmitter(); + + protected versionInfo: string = super.getVersion(packageInfo); + + /** + * @ignore + * 加号点击回调 + */ + public onClick($event: Event): void { + if (this.disabled) { + return; + } + this.add.emit($event); + } +} diff --git a/src/card/lib/src/TiCardComponent.ts b/src/card/lib/src/TiCardComponent.ts new file mode 100644 index 0000000..75e9b22 --- /dev/null +++ b/src/card/lib/src/TiCardComponent.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +/** + * 卡片组件 + * + */ +@Component({ + selector: 'ti-card', + template: ` `, + styleUrls: ['./card.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-card-disabled]': 'disabled', + '[class.ti3-card-active]': 'active', + '[attr.tabindex]': 'disabled? "-1":"0"' + } +}) +export class TiCardComponent extends TiBaseComponent { + /** + * 卡片是否禁用 + */ + @Input() disabled: boolean = false; + /** + * 卡片是否激活 + */ + @Input() active: boolean = false; + + protected versionInfo: string = super.getVersion(packageInfo); +} diff --git a/src/card/lib/src/TiCardHeaderComponent.ts b/src/card/lib/src/TiCardHeaderComponent.ts new file mode 100644 index 0000000..00c9e76 --- /dev/null +++ b/src/card/lib/src/TiCardHeaderComponent.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'ti-card-header', + template: ` `, + styleUrls: ['./card-header.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiCardHeaderComponent {} diff --git a/src/card/lib/src/TiCardModule.ts b/src/card/lib/src/TiCardModule.ts new file mode 100644 index 0000000..88683d6 --- /dev/null +++ b/src/card/lib/src/TiCardModule.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TiCardComponent } from './TiCardComponent'; +import { TiCardAddComponent } from './TiCardAddComponent'; +import { TiCardHeaderComponent } from './TiCardHeaderComponent'; +import { TiIconModule } from '@opentiny/ng-icon'; + +@NgModule({ + imports: [CommonModule, TiIconModule], + declarations: [TiCardComponent, TiCardAddComponent, TiCardHeaderComponent], + exports: [TiCardComponent, TiCardAddComponent, TiCardHeaderComponent] +}) +export class TiCardModule {} +export { TiCardComponent } from './TiCardComponent'; +export { TiCardAddComponent } from './TiCardAddComponent'; +export { TiCardHeaderComponent } from './TiCardHeaderComponent'; diff --git a/src/card/lib/src/card-add.html b/src/card/lib/src/card-add.html new file mode 100644 index 0000000..3dbf00f --- /dev/null +++ b/src/card/lib/src/card-add.html @@ -0,0 +1 @@ + diff --git a/src/card/lib/src/card-add.less b/src/card/lib/src/card-add.less new file mode 100644 index 0000000..5df2c18 --- /dev/null +++ b/src/card/lib/src/card-add.less @@ -0,0 +1,60 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + display: block; + position: relative; + min-width: var(--ti-common-size-50x); + padding: var(--ti-common-space-5x); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-dashed) var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + background-color: var(--ti-common-color-bg-white-normal); + outline: none; + .box-sizing(border-box); + + &:not(.ti3-card-add-disabled):active, + &:not(.ti3-card-add-disabled):hover, + &:not(.ti3-card-add-disabled):focus, + &.ti3-card-add-active { + border-color: var(--ti-common-color-line-active); + + .ti3-card-add-icon { + color: var(--ti-common-color-icon-active); + } + } +} + +// diabled 状态 +:host.ti3-card-add-disabled { + border-color: var(--ti-common-color-line-disabled); + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + + &:hover, + &:active { + outline: none; + border-color: var(--ti-common-color-line-disabled); + } +} + +// 卡片加号图标 +.ti3-card-add-icon { + position: absolute; + font-size: var(--ti-common-size-4x); + color: var(--ti-common-color-icon-normal); + top: 50%; + left: 50%; + // ng9 calc 计算有问题,所以 margin 写成固定值,正常写法如下 + // calc(0px - (var(--ti-common-font-size-2) / 2)) + margin-top: -8px; + margin-left: -8px; + line-height: 1; + cursor: pointer; +} + +:host.ti3-card-add-disabled > .ti3-card-add-icon { + color: var(--ti-common-color-text-disabled); +} + +:host.ti3-card-add-disabled:hover > .ti3-card-add-icon { + cursor: not-allowed; +} diff --git a/src/card/lib/src/card-header.less b/src/card/lib/src/card-header.less new file mode 100644 index 0000000..95231a3 --- /dev/null +++ b/src/card/lib/src/card-header.less @@ -0,0 +1,27 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + display: block; + width: 100%; + height: var(--ti-common-size-13x); + background-color: transparent; + border-bottom: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + border-top-left-radius: var(--ti-common-border-radius-normal); + border-top-right-radius: var(--ti-common-border-radius-normal); +} + +ti-card:not(.ti3-card-disabled):not(.ti3-card-active) { + &:focus { + :host { + background-color: var(--ti-common-color-bg-light-emphasize); + border-bottom-color: var(--ti-common-color-line-active); + .transition(background-color 200ms); + } + } +} + +ti-card.ti3-card-active > :host { + outline: none; + background-color: var(--ti-common-color-bg-light-emphasize); + border-bottom-color: var(--ti-common-color-line-active); +} diff --git a/src/card/lib/src/card.less b/src/card/lib/src/card.less new file mode 100644 index 0000000..d3a4b38 --- /dev/null +++ b/src/card/lib/src/card.less @@ -0,0 +1,41 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + display: block; + min-width: var(--ti-common-size-50x); + position: relative; + padding: var(--ti-common-space-5x); + border-radius: var(--ti-common-border-radius-normal); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + background: var(--ti-common-color-bg-white-normal); + font-size: var(--ti-common-font-size-base); + color: var(--ti-common-color-text-primary); + .box-sizing(border-box); +} + +// 悬浮、激活 +:host:not(.ti3-card-disabled):hover, +:host:not(.ti3-card-disabled):active { + border-color: var(--ti-common-color-line-active); + .transition(border-color 200ms); +} + +// 聚焦 +:host:not(.ti3-card-disabled):focus { + outline: none; + border-color: var(--ti-common-color-line-active); +} +// 激活 +:host.ti3-card-active { + border-color: var(--ti-common-color-line-active); +} +// 禁选 +:host.ti3-card-disabled { + background-color: var(--ti-common-color-bg-disabled); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-disabled); + color: var(--ti-common-color-text-disabled); + outline: none; + &:hover { + cursor: not-allowed; + } +} diff --git a/src/cascader/demo/karma.conf.js b/src/cascader/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/cascader/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/cascader/demo/project.json b/src/cascader/demo/project.json new file mode 100644 index 0000000..6f047e0 --- /dev/null +++ b/src/cascader/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/cascader/demo", + "sourceRoot": "src/cascader/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/cascader", + "index": "src/cascader/demo/src/index.html", + "main": "src/cascader/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/cascader/demo/tsconfig.app.json", + "assets": ["src/cascader/demo/src/favicon.ico", "src/cascader/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "cascader-demo:build:production" + }, + "development": { + "browserTarget": "cascader-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js cascader" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/cascader/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/cascader/demo/tsconfig.spec.json", + "karmaConfig": "src/cascader/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/cascader/demo/src/app/AppComponent.ts b/src/cascader/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/cascader/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/cascader/demo/src/app/AppModule.ts b/src/cascader/demo/src/app/AppModule.ts new file mode 100644 index 0000000..a116903 --- /dev/null +++ b/src/cascader/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { CascaderTestModule } from './cascader/CascaderTestModule'; + +@NgModule({ + imports: [ + CascaderTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/cascader/demo/src/app/IndexComponent.ts b/src/cascader/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..32ed634 --- /dev/null +++ b/src/cascader/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { CascaderTestModule } from './cascader/CascaderTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = CascaderTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/cascader/demo/src/app/app.html b/src/cascader/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/cascader/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/cascader/demo/src/app/cascader/CascaderBasicComponent.ts b/src/cascader/demo/src/app/cascader/CascaderBasicComponent.ts new file mode 100644 index 0000000..886bf93 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderBasicComponent.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; +import { TiCascaderItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './cascader-basic.html' +}) +export class CascaderBasicComponent { + selected: TiCascaderItem; + + items: Array = [ + { + label: '常用端口', + children: [ + { label: '全部放通' }, + { label: '全放通TCP' }, + { label: '全放通UCP' }, + { label: 'SSH' }, + { label: 'Telnet' }, + { label: 'RDP' }, + { label: 'PING' } + ] + }, + { + label: '基本协议', + children: [ + { label: 'HTTP' }, + { label: 'HTTPS' }, + { label: 'WWW' }, + { label: 'FTP' }, + { label: 'MS SQL' }, + { label: 'My SQL' }, + { label: 'PostgreSQL' }, + { label: 'Oracle' } + ] + } + ]; +} diff --git a/src/cascader/demo/src/app/cascader/CascaderClearableComponent.ts b/src/cascader/demo/src/app/cascader/CascaderClearableComponent.ts new file mode 100644 index 0000000..b3171dd --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderClearableComponent.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; +import { TiCascaderItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './cascader-clearable.html' +}) +export class CascaderClearableComponent { + selected: TiCascaderItem; + + items: Array = [ + { + label: '常用端口', + children: [ + { label: '全部放通' }, + { label: '全放通TCP' }, + { label: '全放通UCP' }, + { label: 'SSH' }, + { label: 'Telnet' }, + { label: 'RDP' }, + { label: 'PING' } + ] + }, + { + label: '基本协议', + children: [ + { label: 'HTTP' }, + { label: 'HTTPS' }, + { label: 'WWW' }, + { label: 'FTP' }, + { label: 'MS SQL' }, + { label: 'My SQL' }, + { label: 'PostgreSQL' }, + { label: 'Oracle' } + ] + } + ]; +} diff --git a/src/cascader/demo/src/app/cascader/CascaderData.ts b/src/cascader/demo/src/app/cascader/CascaderData.ts new file mode 100644 index 0000000..cf5623e --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderData.ts @@ -0,0 +1,1050 @@ +export let foodList: any = [ + { + label: '早饭', + children: [ + { + label: '包子', + children: [ + { label: '南瓜包子' }, + { label: '茄子包子' }, + { label: '粉条包子' }, + { label: '豆沙包子' }, + { label: '豆角包子' }, + { label: '酸菜包子' }, + { label: '肉包子' }, + { label: '鸡肉包子' }, + { label: '牛肉包子' }, + { label: '羊肉包子' }, + { label: '鱼肉包子' } + ] + }, + { label: '油条' }, + { + label: '豆腐脑', + children: [{ label: '甜豆腐脑' }, { label: '咸豆腐脑' }] + }, + { label: '重庆小面' } + ] + }, + { + label: '午饭', + children: [ + { + label: '面食', + children: [ + { label: '油泼面' }, + { label: '干拌面' }, + { label: '刀削面' }, + { label: '拉面' }, + { label: '扯面' }, + { label: '棍棍面' }, + { label: '酸汤面' }, + { label: '旗花面' }, + { label: '凉面' }, + { label: '方便面' }, + { label: '火鸡面' } + ] + }, + { label: '酸菜鱼' }, + { label: '麻辣烫' }, + { label: '手抓饭' } + ] + }, + { + label: '晚饭', + children: [ + { + label: '小菜', + children: [{ label: '芹菜' }, { label: '菠菜' }, { label: '豆芽' }, { label: '腐竹' }] + }, + { label: '稀饭' }, + { label: '面条' }, + { label: '肠粉' } + ] + }, + { + label: '西北风' + } +]; + +export let CityData: any = [ + { + label: '陕西省', + children: [ + { + label: '西安', + children: [ + { label: '高陵县' }, + { label: '蓝田县' }, + { label: '户县' }, + { label: '周至县' }, + { label: '灞桥区' }, + { label: '长安区' }, + { label: '莲湖区' }, + { label: '临潼区' }, + { label: '未央区' }, + { label: '新城区' }, + { label: '阎良区' }, + { label: '雁塔区' }, + { label: '碑林区' }, + { label: '其它区' } + ] + }, + { + label: '铜川', + children: [ + { label: '铜川市' }, + { label: '宜君县' }, + { label: '王益区' }, + { label: '耀州区' }, + { label: '印台区' }, + { label: '其它区' } + ] + }, + { + label: '宝鸡', + children: [ + { label: '宝鸡市' }, + { label: '岐山县' }, + { label: '凤翔县' }, + { label: '太白县' }, + { label: '麟游县' }, + { label: '扶风县' }, + { label: '千阳县' }, + { label: '眉县' }, + { label: '凤县' }, + { label: '陇县' }, + { label: '陈仓区' }, + { label: '渭滨区' }, + { label: '金台区' }, + { label: '其它区' } + ] + }, + { + label: '咸阳', + children: [ + { label: '咸阳市' }, + { label: '兴平市' }, + { label: '礼泉县' }, + { label: '泾阳县' }, + { label: '彬县' }, + { label: '旬邑县' }, + { label: '长武县' }, + { label: '乾县' }, + { label: '武功县' }, + { label: '淳化县' }, + { label: '永寿县' }, + { label: '秦都区' }, + { label: '三原县' }, + { label: '渭城区' }, + { label: '杨凌区' }, + { label: '其它区' } + ] + }, + { + label: '渭南', + children: [ + { label: '渭南市' }, + { label: '韩城市' }, + { label: '华阴市' }, + { label: '潼关县' }, + { label: '白水县' }, + { label: '澄城县' }, + { label: '华县' }, + { label: '合阳县' }, + { label: '富平县' }, + { label: '大荔县' }, + { label: '蒲城县' }, + { label: '临渭区' }, + { label: '其它区' } + ] + }, + { + label: '延安', + children: [ + { label: '延安市' }, + { label: '安塞县' }, + { label: '洛川县' }, + { label: '宜川县' }, + { label: '子长县' }, + { label: '黄陵县' }, + { label: '延川县' }, + { label: '富县' }, + { label: '志丹县' }, + { label: '黄龙县' }, + { label: '吴起县' }, + { label: '宝塔区' }, + { label: '延长县' }, + { label: '甘泉县' }, + { label: '其它区' } + ] + }, + { + label: '榆林', + children: [ + { label: '榆林市' }, + { label: '清涧县' }, + { label: '绥德县' }, + { label: '子洲县' }, + { label: '靖边县' }, + { label: '横山县' }, + { label: '米脂县' }, + { label: '佳县' }, + { label: '吴堡县' }, + { label: '定边县' }, + { label: '府谷县' }, + { label: '神木县' }, + { label: '榆阳区' }, + { label: '其它区' } + ] + }, + { + label: '汉中', + children: [ + { label: '汉中市' }, + { label: '留坝县' }, + { label: '镇巴县' }, + { label: '汉台区' }, + { label: '城固县' }, + { label: '南郑县' }, + { label: '洋县' }, + { label: '宁强县' }, + { label: '佛坪县' }, + { label: '勉县' }, + { label: '西乡县' }, + { label: '略阳县' }, + { label: '其它区' } + ] + }, + { + label: '安康', + children: [ + { label: '安康市' }, + { label: '紫阳县' }, + { label: '岚皋县' }, + { label: '汉阴县' }, + { label: '旬阳县' }, + { label: '平利县' }, + { label: '石泉县' }, + { label: '镇坪县' }, + { label: '宁陕县' }, + { label: '白河县' }, + { label: '汉滨区' }, + { label: '其它区' } + ] + }, + { + label: '商洛', + children: [ + { label: '商洛市' }, + { label: '镇安县' }, + { label: '山阳县' }, + { label: '洛南县' }, + { label: '商南县' }, + { label: '丹凤县' }, + { label: '柞水县' }, + { label: '商州区' }, + { label: '其它区' } + ] + } + ] + }, + { + label: '山西省', + children: [ + { + label: '大同市', + children: [ + { label: '城区' }, + { label: '大同县' }, + { label: '广灵县' }, + { label: '浑源县' }, + { label: '矿区' }, + { label: '灵丘县' }, + { label: '南郊区' }, + { label: '天镇县' }, + { label: '新荣区' }, + { label: '阳高县' }, + { label: '左云县' } + ] + }, + { + label: '晋城市', + children: [ + { label: '城区' }, + { label: '高平市' }, + { label: '陵川县' }, + { label: '沁水县' }, + { label: '阳城县' }, + { label: '泽州县' } + ] + }, + { + label: '晋中市', + children: [ + { label: '和顺县' }, + { label: '介休市' }, + { label: '灵石县' }, + { label: '平遥县' }, + { label: '祁县' }, + { label: '寿阳县' }, + { label: '太谷县' }, + { label: '昔阳县' }, + { label: '榆次区' }, + { label: '榆社县' }, + { label: '左权县' } + ] + }, + { + label: '临汾市', + children: [ + { label: '安泽县' }, + { label: '大宁县' }, + { label: '汾西县' }, + { label: '浮山县' }, + { label: '古县' }, + { label: '洪洞县' }, + { label: '侯马市' }, + { label: '霍州市' }, + { label: '吉县' }, + { label: '蒲县' }, + { label: '曲沃县' }, + { label: '隰县' }, + { label: '乡宁县' }, + { label: '襄汾县' }, + { label: '尧都区' }, + { label: '翼城县' }, + { label: '永和县' } + ] + }, + { + label: '吕梁市', + children: [ + { label: '方山县' }, + { label: '汾阳市' }, + { label: '交城县' }, + { label: '交口县' }, + { label: '岚县' }, + { label: '离石区' }, + { label: '临县' }, + { label: '柳林县' }, + { label: '石楼县' }, + { label: '文水县' }, + { label: '孝义市' }, + { label: '兴县' }, + { label: '中阳县' } + ] + }, + { + label: '朔州市', + children: [ + { label: '怀仁县' }, + { label: '平鲁区' }, + { label: '山阴县' }, + { label: '朔城区' }, + { label: '应县' }, + { label: '右玉县' } + ] + }, + { + label: '太原市', + children: [ + { label: '古交市' }, + { label: '尖草坪区' }, + { label: '晋源区' }, + { label: '娄烦县' }, + { label: '清徐县' }, + { label: '万柏林区' }, + { label: '小店区' }, + { label: '杏花岭区' }, + { label: '阳曲县' }, + { label: '迎泽区' } + ] + }, + { + label: '忻州市', + children: [ + { label: '保德县' }, + { label: '代县' }, + { label: '定襄县' }, + { label: '繁峙县' }, + { label: '河曲县' }, + { label: '静乐县' }, + { label: '岢岚县' }, + { label: '宁武县' }, + { label: '偏关县' }, + { label: '神池县' }, + { label: '五台县' }, + { label: '五寨县' }, + { label: '忻府区' }, + { label: '原平市' } + ] + }, + { + label: '阳泉市', + children: [{ label: '城区' }, { label: '郊区' }, { label: '矿区' }, { label: '平定县' }, { label: '盂县' }] + }, + { + label: '运城市', + children: [ + { label: '河津市' }, + { label: '稷山县' }, + { label: '绛县' }, + { label: '临猗县' }, + { label: '平陆县' }, + { label: '芮城县' }, + { label: '万荣县' }, + { label: '闻喜县' }, + { label: '夏县' }, + { label: '新绛县' }, + { label: '盐湖区' }, + { label: '永济市' }, + { label: '垣曲县' } + ] + }, + { + label: '长治市', + children: [ + { label: '城区' }, + { label: '壶关县' }, + { label: '郊区' }, + { label: '黎城县' }, + { label: '潞城市' }, + { label: '平顺县' }, + { label: '沁县' }, + { label: '沁源县' }, + { label: '屯留县' }, + { label: '武乡县' }, + { label: '襄垣县' }, + { label: '长治县' }, + { label: '长子县' } + ] + } + ] + } +]; + +export const CascaderData: any = [ + { + label: '常用端口', + children: [ + { label: '全部放通' }, + { label: '全放通TCP' }, + { label: '全放通UCP' }, + { label: 'SSH' }, + { label: 'Telnet' }, + { label: 'RDP' }, + { label: 'PING' } + ] + }, + { + label: '基本协议', + children: [ + { label: 'HTTP' }, + { label: 'HTTPS' }, + { label: 'WWW' }, + { label: 'FTP' }, + { label: 'MS SQL' }, + { label: 'My SQL' }, + { label: 'PostgreSQL' }, + { label: 'Oracle' } + ] + } +]; + +export let ProvinceCityData: any = [ + { + code: '11', + label: '北京市' + }, + { + code: '12', + label: '天津市' + }, + { + code: '13', + label: '河北省', + children: [ + { code: '1301', label: '石家庄市' }, + { code: '1302', label: '唐山市' }, + { code: '1303', label: '秦皇岛市' }, + { code: '1304', label: '邯郸市' }, + { code: '1305', label: '邢台市' }, + { code: '1306', label: '保定市' }, + { code: '1307', label: '张家口市' }, + { code: '1308', label: '承德市' }, + { code: '1309', label: '沧州市' }, + { code: '1310', label: '廊坊市' }, + { code: '1311', label: '衡水市' } + ] + }, + { + code: '14', + label: '山西省', + children: [ + { code: '1401', label: '太原市' }, + { code: '1402', label: '大同市' }, + { code: '1403', label: '阳泉市' }, + { code: '1404', label: '长治市' }, + { code: '1405', label: '晋城市' }, + { code: '1406', label: '朔州市' }, + { code: '1407', label: '晋中市' }, + { code: '1408', label: '运城市' }, + { code: '1409', label: '忻州市' }, + { code: '1410', label: '临汾市' }, + { code: '1411', label: '吕梁市' } + ] + }, + { + code: '15', + label: '内蒙古自治区', + children: [ + { code: '1501', label: '呼和浩特市' }, + { code: '1502', label: '包头市' }, + { code: '1503', label: '乌海市' }, + { code: '1504', label: '赤峰市' }, + { code: '1505', label: '通辽市' }, + { code: '1506', label: '鄂尔多斯市' }, + { code: '1507', label: '呼伦贝尔市' }, + { code: '1508', label: '巴彦淖尔市' }, + { code: '1509', label: '乌兰察布市' }, + { code: '1522', label: '兴安盟' }, + { code: '1525', label: '锡林郭勒盟' }, + { code: '1529', label: '阿拉善盟' } + ] + }, + { + code: '21', + label: '辽宁省', + children: [ + { code: '2101', label: '沈阳市' }, + { code: '2102', label: '大连市' }, + { code: '2103', label: '鞍山市' }, + { code: '2104', label: '抚顺市' }, + { code: '2105', label: '本溪市' }, + { code: '2106', label: '丹东市' }, + { code: '2107', label: '锦州市' }, + { code: '2108', label: '营口市' }, + { code: '2109', label: '阜新市' }, + { code: '2110', label: '辽阳市' }, + { code: '2111', label: '盘锦市' }, + { code: '2112', label: '铁岭市' }, + { code: '2113', label: '朝阳市' }, + { code: '2114', label: '葫芦岛市' } + ] + }, + { + code: '22', + label: '吉林省', + children: [ + { code: '2201', label: '长春市' }, + { code: '2202', label: '吉林市' }, + { code: '2203', label: '四平市' }, + { code: '2204', label: '辽源市' }, + { code: '2205', label: '通化市' }, + { code: '2206', label: '白山市' }, + { code: '2207', label: '松原市' }, + { code: '2208', label: '白城市' }, + { code: '2224', label: '延边朝鲜族自治州' } + ] + }, + { + code: '23', + label: '黑龙江省', + children: [ + { code: '2301', label: '哈尔滨市' }, + { code: '2302', label: '齐齐哈尔市' }, + { code: '2303', label: '鸡西市' }, + { code: '2304', label: '鹤岗市' }, + { code: '2305', label: '双鸭山市' }, + { code: '2306', label: '大庆市' }, + { code: '2307', label: '伊春市' }, + { code: '2308', label: '佳木斯市' }, + { code: '2309', label: '七台河市' }, + { code: '2310', label: '牡丹江市' }, + { code: '2311', label: '黑河市' }, + { code: '2312', label: '绥化市' }, + { code: '2327', label: '大兴安岭地区' } + ] + }, + { + code: '31', + label: '上海市', + children: [ + { code: '310101', label: '黄浦区' }, + { code: '310104', label: '徐汇区' }, + { code: '310105', label: '长宁区' }, + { code: '310106', label: '静安区' }, + { code: '310107', label: '普陀区' }, + { code: '310109', label: '虹口区' }, + { code: '310110', label: '杨浦区' }, + { code: '310112', label: '闵行区' }, + { code: '310113', label: '宝山区' }, + { code: '310114', label: '嘉定区' }, + { code: '310115', label: '浦东新区' }, + { code: '310116', label: '金山区' }, + { code: '310117', label: '松江区' }, + { code: '310118', label: '青浦区' }, + { code: '310120', label: '奉贤区' }, + { code: '310151', label: '崇明区' } + ] + }, + { + code: '32', + label: '江苏省', + children: [ + { code: '3201', label: '南京市' }, + { code: '3202', label: '无锡市' }, + { code: '3203', label: '徐州市' }, + { code: '3204', label: '常州市' }, + { code: '3205', label: '苏州市' }, + { code: '3206', label: '南通市' }, + { code: '3207', label: '连云港市' }, + { code: '3208', label: '淮安市' }, + { code: '3209', label: '盐城市' }, + { code: '3210', label: '扬州市' }, + { code: '3211', label: '镇江市' }, + { code: '3212', label: '泰州市' }, + { code: '3213', label: '宿迁市' } + ] + }, + { + code: '33', + label: '浙江省', + children: [ + { code: '3301', label: '杭州市' }, + { code: '3302', label: '宁波市' }, + { code: '3303', label: '温州市' }, + { code: '3304', label: '嘉兴市' }, + { code: '3305', label: '湖州市' }, + { code: '3306', label: '绍兴市' }, + { code: '3307', label: '金华市' }, + { code: '3308', label: '衢州市' }, + { code: '3309', label: '舟山市' }, + { code: '3310', label: '台州市' }, + { code: '3311', label: '丽水市' } + ] + }, + { + code: '34', + label: '安徽省', + children: [ + { code: '3401', label: '合肥市' }, + { code: '3402', label: '芜湖市' }, + { code: '3403', label: '蚌埠市' }, + { code: '3404', label: '淮南市' }, + { code: '3405', label: '马鞍山市' }, + { code: '3406', label: '淮北市' }, + { code: '3407', label: '铜陵市' }, + { code: '3408', label: '安庆市' }, + { code: '3410', label: '黄山市' }, + { code: '3411', label: '滁州市' }, + { code: '3412', label: '阜阳市' }, + { code: '3413', label: '宿州市' }, + { code: '3415', label: '六安市' }, + { code: '3416', label: '亳州市' }, + { code: '3417', label: '池州市' }, + { code: '3418', label: '宣城市' } + ] + }, + { + code: '35', + label: '福建省', + children: [ + { code: '3501', label: '福州市' }, + { code: '3502', label: '厦门市' }, + { code: '3503', label: '莆田市' }, + { code: '3504', label: '三明市' }, + { code: '3505', label: '泉州市' }, + { code: '3506', label: '漳州市' }, + { code: '3507', label: '南平市' }, + { code: '3508', label: '龙岩市' }, + { code: '3509', label: '宁德市' } + ] + }, + { + code: '36', + label: '江西省', + children: [ + { code: '3601', label: '南昌市' }, + { code: '3602', label: '景德镇市' }, + { code: '3603', label: '萍乡市' }, + { code: '3604', label: '九江市' }, + { code: '3605', label: '新余市' }, + { code: '3606', label: '鹰潭市' }, + { code: '3607', label: '赣州市' }, + { code: '3608', label: '吉安市' }, + { code: '3609', label: '宜春市' }, + { code: '3610', label: '抚州市' }, + { code: '3611', label: '上饶市' } + ] + }, + { + code: '37', + label: '山东省', + children: [ + { code: '3701', label: '济南市' }, + { code: '3702', label: '青岛市' }, + { code: '3703', label: '淄博市' }, + { code: '3704', label: '枣庄市' }, + { code: '3705', label: '东营市' }, + { code: '3706', label: '烟台市' }, + { code: '3707', label: '潍坊市' }, + { code: '3708', label: '济宁市' }, + { code: '3709', label: '泰安市' }, + { code: '3710', label: '威海市' }, + { code: '3711', label: '日照市' }, + { code: '3713', label: '临沂市' }, + { code: '3714', label: '德州市' }, + { code: '3715', label: '聊城市' }, + { code: '3716', label: '滨州市' }, + { code: '3717', label: '菏泽市' } + ] + }, + { + code: '41', + label: '河南省', + children: [ + { code: '4101', label: '郑州市' }, + { code: '4102', label: '开封市' }, + { code: '4103', label: '洛阳市' }, + { code: '4104', label: '平顶山市' }, + { code: '4105', label: '安阳市' }, + { code: '4106', label: '鹤壁市' }, + { code: '4107', label: '新乡市' }, + { code: '4108', label: '焦作市' }, + { code: '4109', label: '濮阳市' }, + { code: '4110', label: '许昌市' }, + { code: '4111', label: '漯河市' }, + { code: '4112', label: '三门峡市' }, + { code: '4113', label: '南阳市' }, + { code: '4114', label: '商丘市' }, + { code: '4115', label: '信阳市' }, + { code: '4116', label: '周口市' }, + { code: '4117', label: '驻马店市' }, + { code: '419001', label: '济源市' } + ] + }, + { + code: '42', + label: '湖北省', + children: [ + { code: '4201', label: '武汉市' }, + { code: '4202', label: '黄石市' }, + { code: '4203', label: '十堰市' }, + { code: '4205', label: '宜昌市' }, + { code: '4206', label: '襄阳市' }, + { code: '4207', label: '鄂州市' }, + { code: '4208', label: '荆门市' }, + { code: '4209', label: '孝感市' }, + { code: '4210', label: '荆州市' }, + { code: '4211', label: '黄冈市' }, + { code: '4212', label: '咸宁市' }, + { code: '4213', label: '随州市' }, + { code: '4228', label: '恩施土家族苗族自治州' }, + { code: '429004', label: '仙桃市' }, + { code: '429005', label: '潜江市' }, + { code: '429006', label: '天门市' }, + { code: '429021', label: '神农架林区' } + ] + }, + { + code: '43', + label: '湖南省', + children: [ + { code: '4301', label: '长沙市' }, + { code: '4302', label: '株洲市' }, + { code: '4303', label: '湘潭市' }, + { code: '4304', label: '衡阳市' }, + { code: '4305', label: '邵阳市' }, + { code: '4306', label: '岳阳市' }, + { code: '4307', label: '常德市' }, + { code: '4308', label: '张家界市' }, + { code: '4309', label: '益阳市' }, + { code: '4310', label: '郴州市' }, + { code: '4311', label: '永州市' }, + { code: '4312', label: '怀化市' }, + { code: '4313', label: '娄底市' }, + { code: '4331', label: '湘西土家族苗族自治州' } + ] + }, + { + code: '44', + label: '广东省', + children: [ + { code: '4401', label: '广州市' }, + { code: '4402', label: '韶关市' }, + { code: '4403', label: '深圳市' }, + { code: '4404', label: '珠海市' }, + { code: '4405', label: '汕头市' }, + { code: '4406', label: '佛山市' }, + { code: '4407', label: '江门市' }, + { code: '4408', label: '湛江市' }, + { code: '4409', label: '茂名市' }, + { code: '4412', label: '肇庆市' }, + { code: '4413', label: '惠州市' }, + { code: '4414', label: '梅州市' }, + { code: '4415', label: '汕尾市' }, + { code: '4416', label: '河源市' }, + { code: '4417', label: '阳江市' }, + { code: '4418', label: '清远市' }, + { code: '4419', label: '东莞市' }, + { code: '4420', label: '中山市' }, + { code: '4451', label: '潮州市' }, + { code: '4452', label: '揭阳市' }, + { code: '4453', label: '云浮市' } + ] + }, + { + code: '45', + label: '广西壮族自治区', + children: [ + { code: '4501', label: '南宁市' }, + { code: '4502', label: '柳州市' }, + { code: '4503', label: '桂林市' }, + { code: '4504', label: '梧州市' }, + { code: '4505', label: '北海市' }, + { code: '4506', label: '防城港市' }, + { code: '4507', label: '钦州市' }, + { code: '4508', label: '贵港市' }, + { code: '4509', label: '玉林市' }, + { code: '4510', label: '百色市' }, + { code: '4511', label: '贺州市' }, + { code: '4512', label: '河池市' }, + { code: '4513', label: '来宾市' }, + { code: '4514', label: '崇左市' } + ] + }, + { + code: '46', + label: '海南省', + children: [ + { code: '4601', label: '海口市' }, + { code: '4602', label: '三亚市' }, + { code: '4603', label: '三沙市' }, + { code: '4604', label: '儋州市' }, + { code: '469001', label: '五指山市' }, + { code: '469002', label: '琼海市' }, + { code: '469005', label: '文昌市' }, + { code: '469006', label: '万宁市' }, + { code: '469007', label: '东方市' }, + { code: '469021', label: '定安县' }, + { code: '469022', label: '屯昌县' }, + { code: '469023', label: '澄迈县' }, + { code: '469024', label: '临高县' }, + { code: '469025', label: '白沙黎族自治县' }, + { code: '469026', label: '昌江黎族自治县' }, + { code: '469027', label: '乐东黎族自治县' }, + { code: '469028', label: '陵水黎族自治县' }, + { code: '469029', label: '保亭黎族苗族自治县' }, + { code: '469030', label: '琼中黎族苗族自治县' } + ] + }, + { + code: '50', + label: '重庆市', + children: [ + { code: '500101', label: '万州区' }, + { code: '500102', label: '涪陵区' }, + { code: '500103', label: '渝中区' }, + { code: '500104', label: '大渡口区' }, + { code: '500105', label: '江北区' }, + { code: '500106', label: '沙坪坝区' }, + { code: '500107', label: '九龙坡区' }, + { code: '500108', label: '南岸区' }, + { code: '500109', label: '北碚区' }, + { code: '500110', label: '綦江区' }, + { code: '500111', label: '大足区' }, + { code: '500112', label: '渝北区' }, + { code: '500113', label: '巴南区' }, + { code: '500114', label: '黔江区' }, + { code: '500115', label: '长寿区' }, + { code: '500116', label: '江津区' }, + { code: '500117', label: '合川区' }, + { code: '500118', label: '永川区' }, + { code: '500119', label: '南川区' }, + { code: '500120', label: '璧山区' }, + { code: '500151', label: '铜梁区' }, + { code: '500152', label: '潼南区' }, + { code: '500153', label: '荣昌区' }, + { code: '500154', label: '开州区' }, + { code: '500155', label: '梁平区' }, + { code: '500156', label: '武隆区' }, + { code: '500229', label: '城口县' }, + { code: '500230', label: '丰都县' }, + { code: '500231', label: '垫江县' }, + { code: '500233', label: '忠县' }, + { code: '500235', label: '云阳县' }, + { code: '500236', label: '奉节县' }, + { code: '500237', label: '巫山县' }, + { code: '500238', label: '巫溪县' }, + { code: '500240', label: '石柱土家族自治县' }, + { code: '500241', label: '秀山土家族苗族自治县' }, + { code: '500242', label: '酉阳土家族苗族自治县' }, + { code: '500243', label: '彭水苗族土家族自治县' } + ] + }, + { + code: '51', + label: '四川省', + children: [ + { code: '5101', label: '成都市' }, + { code: '5103', label: '自贡市' }, + { code: '5104', label: '攀枝花市' }, + { code: '5105', label: '泸州市' }, + { code: '5106', label: '德阳市' }, + { code: '5107', label: '绵阳市' }, + { code: '5108', label: '广元市' }, + { code: '5109', label: '遂宁市' }, + { code: '5110', label: '内江市' }, + { code: '5111', label: '乐山市' }, + { code: '5113', label: '南充市' }, + { code: '5114', label: '眉山市' }, + { code: '5115', label: '宜宾市' }, + { code: '5116', label: '广安市' }, + { code: '5117', label: '达州市' }, + { code: '5118', label: '雅安市' }, + { code: '5119', label: '巴中市' }, + { code: '5120', label: '资阳市' }, + { code: '5132', label: '阿坝藏族羌族自治州' }, + { code: '5133', label: '甘孜藏族自治州' }, + { code: '5134', label: '凉山彝族自治州' } + ] + }, + { + code: '52', + label: '贵州省', + children: [ + { code: '5201', label: '贵阳市' }, + { code: '5202', label: '六盘水市' }, + { code: '5203', label: '遵义市' }, + { code: '5204', label: '安顺市' }, + { code: '5205', label: '毕节市' }, + { code: '5206', label: '铜仁市' }, + { code: '5223', label: '黔西南布依族苗族自治州' }, + { code: '5226', label: '黔东南苗族侗族自治州' }, + { code: '5227', label: '黔南布依族苗族自治州' } + ] + }, + { + code: '53', + label: '云南省', + children: [ + { code: '5301', label: '昆明市' }, + { code: '5303', label: '曲靖市' }, + { code: '5304', label: '玉溪市' }, + { code: '5305', label: '保山市' }, + { code: '5306', label: '昭通市' }, + { code: '5307', label: '丽江市' }, + { code: '5308', label: '普洱市' }, + { code: '5309', label: '临沧市' }, + { code: '5323', label: '楚雄彝族自治州' }, + { code: '5325', label: '红河哈尼族彝族自治州' }, + { code: '5326', label: '文山壮族苗族自治州' }, + { code: '5328', label: '西双版纳傣族自治州' }, + { code: '5329', label: '大理白族自治州' }, + { code: '5331', label: '德宏傣族景颇族自治州' }, + { code: '5333', label: '怒江傈僳族自治州' }, + { code: '5334', label: '迪庆藏族自治州' } + ] + }, + { + code: '54', + label: '西藏自治区', + children: [ + { code: '5401', label: '拉萨市' }, + { code: '5402', label: '日喀则市' }, + { code: '5403', label: '昌都市' }, + { code: '5404', label: '林芝市' }, + { code: '5405', label: '山南市' }, + { code: '5406', label: '那曲市' }, + { code: '5425', label: '阿里地区' } + ] + }, + { + code: '61', + label: '陕西省', + children: [ + { code: '6101', label: '西安市' }, + { code: '6102', label: '铜川市' }, + { code: '6103', label: '宝鸡市' }, + { code: '6104', label: '咸阳市' }, + { code: '6105', label: '渭南市' }, + { code: '6106', label: '延安市' }, + { code: '6107', label: '汉中市' }, + { code: '6108', label: '榆林市' }, + { code: '6109', label: '安康市' }, + { code: '6110', label: '商洛市' } + ] + }, + { + code: '62', + label: '甘肃省', + children: [ + { code: '6201', label: '兰州市' }, + { code: '6202', label: '嘉峪关市' }, + { code: '6203', label: '金昌市' }, + { code: '6204', label: '白银市' }, + { code: '6205', label: '天水市' }, + { code: '6206', label: '武威市' }, + { code: '6207', label: '张掖市' }, + { code: '6208', label: '平凉市' }, + { code: '6209', label: '酒泉市' }, + { code: '6210', label: '庆阳市' }, + { code: '6211', label: '定西市' }, + { code: '6212', label: '陇南市' }, + { code: '6229', label: '临夏回族自治州' }, + { code: '6230', label: '甘南藏族自治州' } + ] + }, + { + code: '63', + label: '青海省', + children: [ + { code: '6301', label: '西宁市' }, + { code: '6302', label: '海东市' }, + { code: '6322', label: '海北藏族自治州' }, + { code: '6323', label: '黄南藏族自治州' }, + { code: '6325', label: '海南藏族自治州' }, + { code: '6326', label: '果洛藏族自治州' }, + { code: '6327', label: '玉树藏族自治州' }, + { code: '6328', label: '海西蒙古族藏族自治州' } + ] + }, + { + code: '64', + label: '宁夏回族自治区', + children: [ + { code: '6401', label: '银川市' }, + { code: '6402', label: '石嘴山市' }, + { code: '6403', label: '吴忠市' }, + { code: '6404', label: '固原市' }, + { code: '6405', label: '中卫市' } + ] + }, + { + code: '65', + label: '新疆维吾尔自治区', + children: [ + { code: '6501', label: '乌鲁木齐市' }, + { code: '6502', label: '克拉玛依市' }, + { code: '6504', label: '吐鲁番市' }, + { code: '6505', label: '哈密市' }, + { code: '6523', label: '昌吉回族自治州' }, + { code: '6527', label: '博尔塔拉蒙古自治州' }, + { code: '6528', label: '巴音郭楞蒙古自治州' }, + { code: '6529', label: '阿克苏地区' }, + { code: '6530', label: '克孜勒苏柯尔克孜自治州' }, + { code: '6531', label: '喀什地区' }, + { code: '6532', label: '和田地区' }, + { code: '6540', label: '伊犁哈萨克自治州' }, + { code: '6542', label: '塔城地区' }, + { code: '6543', label: '阿勒泰地区' }, + { code: '659001', label: '石河子市' }, + { code: '659002', label: '阿拉尔市' }, + { code: '659003', label: '图木舒克市' }, + { code: '659004', label: '五家渠市' }, + { code: '659006', label: '铁门关市' } + ] + } +]; diff --git a/src/cascader/demo/src/app/cascader/CascaderDisabledComponent.ts b/src/cascader/demo/src/app/cascader/CascaderDisabledComponent.ts new file mode 100644 index 0000000..362e31b --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderDisabledComponent.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; +import { TiCascaderItem } from '@opentiny/ng'; +@Component({ + templateUrl: './cascader-disabled.html' +}) +export class CascaderDisabledComponent { + items: Array = [ + { + label: '常用端口', + disabled: true, + children: [ + { label: '全部放通' }, + { label: '全放通TCP' }, + { + label: '全放通UCP', + disabled: true + }, + { label: 'SSH' }, + { label: 'Telnet' }, + { label: 'RDP' }, + { label: 'PING' } + ] + }, + { + label: '基本协议', + children: [ + { label: 'HTTP' }, + { label: 'HTTPS' }, + { + label: 'WWW', + disabled: true + }, + { label: 'FTP' }, + { label: 'MS SQL' }, + { label: 'My SQL' }, + { label: 'PostgreSQL' }, + { label: 'Oracle' } + ] + } + ]; +} diff --git a/src/cascader/demo/src/app/cascader/CascaderEventsComponent.ts b/src/cascader/demo/src/app/cascader/CascaderEventsComponent.ts new file mode 100644 index 0000000..83eccd6 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderEventsComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { TiCascaderItem } from '@opentiny/ng'; +import { CascaderData } from './CascaderData'; + +@Component({ + templateUrl: './cascader-events.html' +}) +export class CascaderEventsComponent { + items: Array = CascaderData; + selected: Array = []; + selected1: Array = []; + + myLogs: Array = []; + onClear(): void { + this.myLogs = [...this.myLogs, '已清除']; + } + + ngModelChange(item): void { + this.myLogs = [...this.myLogs, `ngModelChange:${JSON.stringify(item)}`]; + } +} diff --git a/src/cascader/demo/src/app/cascader/CascaderIdkeyComponent.ts b/src/cascader/demo/src/app/cascader/CascaderIdkeyComponent.ts new file mode 100644 index 0000000..ac3106e --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderIdkeyComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiCascaderItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './cascader-idkey.html' +}) +export class CascaderIdkeyComponent { + selected: TiCascaderItem; + + items: Array = [ + { + label: '常用端口', + id: '1', + children: [ + { label: '全部放通', id: '1-1' }, + { label: '全放通TCP', id: '1-2' }, + { label: '全放通UCP', id: '1-3' }, + { label: 'SSH', id: '1-4' }, + { label: 'Telnet', id: '1-5' }, + { label: 'RDP', id: '1-6' }, + { label: 'PING', id: '1-7' }, + { label: 'PING', id: '1-8' } + ] + }, + { + label: '基本协议', + id: '2', + children: [ + { label: 'HTTP', id: '2-1' }, + { label: 'HTTPS', id: '2-2' }, + { label: 'WWW', id: '2-3' }, + { label: 'FTP', id: '2-4' }, + { label: 'MS SQL', id: '2-5' }, + { label: 'My SQL', id: '2-6' }, + { label: 'PostgreSQL', id: '2-7' }, + { label: 'Oracle', id: '2-8' } + ] + } + ]; +} diff --git a/src/cascader/demo/src/app/cascader/CascaderItemTestComponent.ts b/src/cascader/demo/src/app/cascader/CascaderItemTestComponent.ts new file mode 100644 index 0000000..d1626b1 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderItemTestComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TiCascaderItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './cascader-item-test.html' +}) +export class CascaderItemTestComponent { + mySelected: TiCascaderItem; + mySelected1: TiCascaderItem; + mySelected2: TiCascaderItem; + items: Array = []; + + data: Array = [ + { + label: '常用端口', + children: [ + { label: '全部放通' }, + { label: '全放通TCP' }, + { label: '全放通UCP' }, + { label: 'SSH' }, + { label: 'Telnet' }, + { label: 'RDP' }, + { label: 'PING' } + ] + }, + { + label: '基本协议', + children: [ + { label: 'HTTP' }, + { label: 'HTTPS' }, + { label: 'WWW' }, + { label: 'FTP' }, + { label: 'MS SQL' }, + { label: 'My SQL复制' }, + { label: 'My SQL 超长数据 超长数据超长数据' }, + { label: 'PostgreSQL' }, + { label: 'PostgreSQL复制' }, + { label: 'Oracle' } + ] + }, + { + label: '没有children' + }, + { + label: 'children是空数组', + children: [] + } + ]; +} diff --git a/src/cascader/demo/src/app/cascader/CascaderLabelkeyComponent.ts b/src/cascader/demo/src/app/cascader/CascaderLabelkeyComponent.ts new file mode 100644 index 0000000..e02555f --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderLabelkeyComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiCascaderItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './cascader-labelkey.html' +}) +export class CascaderLabelkeyComponent { + selected: TiCascaderItem; + + items: Array = [ + { + label: 'chang yong duan kou', + name: '常用端口', + children: [ + { name: '全部放通', label: 'quan bu fang tong' }, + { name: '全放通TCP', label: 'quan fang tong TCP' }, + { name: '全放通UCP', label: 'quan fang tong UCP' }, + { name: 'SSH', label: 'SSH' }, + { name: 'Telnet', label: 'Telnet' }, + { name: 'RDP', label: 'RDP' }, + { name: 'PING', label: 'PING' }, + { name: 'PING', label: 'PING' } + ] + }, + { + label: 'ji ben xie yi', + name: '基本协议', + children: [ + { name: 'HTTP', label: 'HTTP' }, + { name: 'HTTPS', label: 'HTTPS' }, + { name: 'WWW', label: 'WWW' }, + { name: 'FTP', label: 'FTP' }, + { name: 'MS SQL', label: 'MS SQL' }, + { name: 'My SQL', label: 'MS SQL' }, + { name: 'PostgreSQL', label: 'PostgreSQL' }, + { name: 'Oracle', label: 'Oracle' } + ] + } + ]; +} diff --git a/src/cascader/demo/src/app/cascader/CascaderOnlyselectleafComponent.ts b/src/cascader/demo/src/app/cascader/CascaderOnlyselectleafComponent.ts new file mode 100644 index 0000000..54f1262 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderOnlyselectleafComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { TiCascaderItem } from '@opentiny/ng'; + +import { foodList } from './CascaderData'; + +@Component({ + templateUrl: './cascader-onlyselectleaf.html' +}) +export class CascaderOnlyselectleafComponent { + selected: TiCascaderItem = []; + items: Array = foodList; +} diff --git a/src/cascader/demo/src/app/cascader/CascaderPanelComponent.ts b/src/cascader/demo/src/app/cascader/CascaderPanelComponent.ts new file mode 100644 index 0000000..aa4449c --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderPanelComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { TiCascaderItem } from '@opentiny/ng'; + +import { foodList } from './CascaderData'; +@Component({ + templateUrl: './cascader-panel.html' +}) +export class CascaderPanelComponent { + items: Array = foodList; +} diff --git a/src/cascader/demo/src/app/cascader/CascaderSearchComponent.ts b/src/cascader/demo/src/app/cascader/CascaderSearchComponent.ts new file mode 100644 index 0000000..0643363 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderSearchComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { TiCascaderItem } from '@opentiny/ng'; + +import { foodList } from './CascaderData'; + +@Component({ + templateUrl: './cascader-search.html' +}) +export class CascaderSearchComponent { + items: Array = foodList; + selected: Array = []; + selected1: Array = [foodList[1], foodList[1].children[0]]; +} diff --git a/src/cascader/demo/src/app/cascader/CascaderShowalllevelComponent.ts b/src/cascader/demo/src/app/cascader/CascaderShowalllevelComponent.ts new file mode 100644 index 0000000..103f33c --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderShowalllevelComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { TiCascaderItem } from '@opentiny/ng'; + +import { foodList } from './CascaderData'; + +@Component({ + templateUrl: './cascader-showalllevel.html' +}) +export class CascaderShowalllevelComponent { + items: Array = foodList; + selected: Array = [this.items[2].label, this.items[2].children[0].label, this.items[2].children[0].children[1].label]; +} diff --git a/src/cascader/demo/src/app/cascader/CascaderTestModule.ts b/src/cascader/demo/src/app/cascader/CascaderTestModule.ts new file mode 100644 index 0000000..fb3675f --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderTestModule.ts @@ -0,0 +1,90 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiCascaderModule, TiScrollModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { CascaderBasicComponent } from './CascaderBasicComponent'; +import { CascaderDisabledComponent } from './CascaderDisabledComponent'; +import { CascaderPanelComponent } from './CascaderPanelComponent'; +import { CascaderTriggerComponent } from './CascaderTriggerComponent'; +import { CascaderShowalllevelComponent } from './CascaderShowalllevelComponent'; +import { CascaderOnlyselectleafComponent } from './CascaderOnlyselectleafComponent'; +import { CascaderEventsComponent } from './CascaderEventsComponent'; +import { CascaderIdkeyComponent } from './CascaderIdkeyComponent'; +import { CascaderLabelkeyComponent } from './CascaderLabelkeyComponent'; +import { CascaderValuekeyComponent } from './CascaderValuekeyComponent'; +import { CascaderItemTestComponent } from './CascaderItemTestComponent'; +import { CascaderValidComponent } from './CascaderValidComponent'; +import { CascaderSearchComponent } from './CascaderSearchComponent'; +import { CascaderClearableComponent } from './CascaderClearableComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiScrollModule, + TiCascaderModule, + TiValidationModule, + DemoLogModule, + RouterModule.forChild(CascaderTestModule.ROUTES) + ], + declarations: [ + CascaderBasicComponent, + CascaderShowalllevelComponent, + CascaderDisabledComponent, + CascaderEventsComponent, + CascaderTriggerComponent, + CascaderPanelComponent, + CascaderOnlyselectleafComponent, + CascaderIdkeyComponent, + CascaderLabelkeyComponent, + CascaderValuekeyComponent, + CascaderValidComponent, + CascaderSearchComponent, + CascaderItemTestComponent, + CascaderClearableComponent + ] +}) +export class CascaderTestModule { + public static readonly ROUTES: Routes = [ + { path: 'cascader/cascader-basic', component: CascaderBasicComponent }, + { + path: 'cascader/cascader-disabled', + component: CascaderDisabledComponent + }, + { + path: 'cascader/cascader-labelkey', + component: CascaderLabelkeyComponent + }, + { + path: 'cascader/cascader-valuekey', + component: CascaderValuekeyComponent + }, + { path: 'cascader/cascader-key', component: CascaderIdkeyComponent }, + { path: 'cascader/cascader-panel', component: CascaderPanelComponent }, + { path: 'cascader/cascader-trigger', component: CascaderTriggerComponent }, + { + path: 'cascader/cascader-showalllevel', + component: CascaderShowalllevelComponent + }, + { + path: 'cascader/cascader-onlyselectleaf', + component: CascaderOnlyselectleafComponent + }, + { path: 'cascader/cascader-events', component: CascaderEventsComponent }, + { path: 'cascader/cascader-search', component: CascaderSearchComponent }, + { path: 'cascader/cascader-valid', component: CascaderValidComponent }, + { + path: 'cascader/cascader-clearable', + component: CascaderClearableComponent + }, + { + path: 'cascader/cascader-item-test', + component: CascaderItemTestComponent + } + ]; +} diff --git a/src/cascader/demo/src/app/cascader/CascaderTriggerComponent.ts b/src/cascader/demo/src/app/cascader/CascaderTriggerComponent.ts new file mode 100644 index 0000000..22eb02e --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderTriggerComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { TiCascaderItem } from '@opentiny/ng'; + +import { foodList } from './CascaderData'; + +@Component({ + templateUrl: './cascader-trigger.html' +}) +export class CascaderTriggerComponent { + items: Array = foodList; + selected: TiCascaderItem = []; +} diff --git a/src/cascader/demo/src/app/cascader/CascaderValidComponent.ts b/src/cascader/demo/src/app/cascader/CascaderValidComponent.ts new file mode 100644 index 0000000..e3dc120 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderValidComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { TiValidators } from '@opentiny/ng'; +import { TiCascaderItem } from '@opentiny/ng'; + +import { foodList } from './CascaderData'; + +@Component({ + templateUrl: './cascader-valid.html' +}) +export class CascaderValidComponent { + items: Array = foodList; + mySelected: Array = [this.items[2].label, this.items[2].children[0].label, this.items[2].children[0].children[1].label]; + + cascaderControl: FormControl = new FormControl(this.mySelected, TiValidators.required); +} diff --git a/src/cascader/demo/src/app/cascader/CascaderValuekeyComponent.ts b/src/cascader/demo/src/app/cascader/CascaderValuekeyComponent.ts new file mode 100644 index 0000000..9a3cbab --- /dev/null +++ b/src/cascader/demo/src/app/cascader/CascaderValuekeyComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiCascaderItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './cascader-valuekey.html' +}) +export class CascaderValuekeyComponent { + selected: TiCascaderItem; + + items: Array = [ + { + label: '常用端口', + id: '1', + children: [ + { label: '全部放通', id: '1-1' }, + { label: '全放通TCP', id: '1-2' }, + { label: '全放通UCP', id: '1-3' }, + { label: 'SSH', id: '1-4' }, + { label: 'Telnet', id: '1-5' }, + { label: 'RDP', id: '1-6' }, + { label: 'PING', id: '1-7' }, + { label: 'PING', id: '1-8' } + ] + }, + { + label: '基本协议', + id: '2', + children: [ + { label: 'HTTP', id: '2-1' }, + { label: 'HTTPS', id: '2-2' }, + { label: 'WWW', id: '2-3' }, + { label: 'FTP', id: '2-4' }, + { label: 'MS SQL', id: '2-5' }, + { label: 'My SQL', id: '2-6' }, + { label: 'PostgreSQL', id: '2-7' }, + { label: 'Oracle', id: '2-8' } + ] + } + ]; +} diff --git a/src/cascader/demo/src/app/cascader/cascader-basic.html b/src/cascader/demo/src/app/cascader/cascader-basic.html new file mode 100644 index 0000000..c17a608 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/cascader-basic.html @@ -0,0 +1,4 @@ + +
+
Current value: {{ selected | json }}
+
diff --git a/src/cascader/demo/src/app/cascader/cascader-clearable.html b/src/cascader/demo/src/app/cascader/cascader-clearable.html new file mode 100644 index 0000000..883eaa0 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/cascader-clearable.html @@ -0,0 +1,4 @@ + +
+
Current value: {{ selected | json }}
+
diff --git a/src/cascader/demo/src/app/cascader/cascader-disabled.html b/src/cascader/demo/src/app/cascader/cascader-disabled.html new file mode 100644 index 0000000..f58bdac --- /dev/null +++ b/src/cascader/demo/src/app/cascader/cascader-disabled.html @@ -0,0 +1,4 @@ +

1.组件禁用

+ +

2.数据项禁用

+ diff --git a/src/cascader/demo/src/app/cascader/cascader-events.html b/src/cascader/demo/src/app/cascader/cascader-events.html new file mode 100644 index 0000000..dc13645 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/cascader-events.html @@ -0,0 +1,14 @@ +

1.onlySelectLeaf:true

+ + +

2.onlySelectLeaf:false

+ + + diff --git a/src/cascader/demo/src/app/cascader/cascader-idkey.html b/src/cascader/demo/src/app/cascader/cascader-idkey.html new file mode 100644 index 0000000..f9fa50f --- /dev/null +++ b/src/cascader/demo/src/app/cascader/cascader-idkey.html @@ -0,0 +1,4 @@ + +
+
Current value: {{ selected | json }}
+
diff --git a/src/cascader/demo/src/app/cascader/cascader-item-test.html b/src/cascader/demo/src/app/cascader/cascader-item-test.html new file mode 100644 index 0000000..a217bc4 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/cascader-item-test.html @@ -0,0 +1,16 @@ +

1.内部测试异常数据或特殊场景

+

+

2.示例

+ +
+

外层有tiScroll

+ +
+ 选中项: {{ mySelected | json }} +

+



+ +
+ 选中项: {{mySelected1 | json}} +







+
diff --git a/src/cascader/demo/src/app/cascader/cascader-labelkey.html b/src/cascader/demo/src/app/cascader/cascader-labelkey.html new file mode 100644 index 0000000..24ff131 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/cascader-labelkey.html @@ -0,0 +1 @@ + diff --git a/src/cascader/demo/src/app/cascader/cascader-onlyselectleaf.html b/src/cascader/demo/src/app/cascader/cascader-onlyselectleaf.html new file mode 100644 index 0000000..481ff32 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/cascader-onlyselectleaf.html @@ -0,0 +1 @@ + diff --git a/src/cascader/demo/src/app/cascader/cascader-panel.html b/src/cascader/demo/src/app/cascader/cascader-panel.html new file mode 100644 index 0000000..3f038c1 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/cascader-panel.html @@ -0,0 +1 @@ + diff --git a/src/cascader/demo/src/app/cascader/cascader-search.html b/src/cascader/demo/src/app/cascader/cascader-search.html new file mode 100644 index 0000000..d90c2ec --- /dev/null +++ b/src/cascader/demo/src/app/cascader/cascader-search.html @@ -0,0 +1,4 @@ +

1.onlySelectLeaf:true + searchable:true

+ +

2.onlySelectLeaf:false + searchable:true

+ diff --git a/src/cascader/demo/src/app/cascader/cascader-showalllevel.html b/src/cascader/demo/src/app/cascader/cascader-showalllevel.html new file mode 100644 index 0000000..f312462 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/cascader-showalllevel.html @@ -0,0 +1 @@ + diff --git a/src/cascader/demo/src/app/cascader/cascader-trigger.html b/src/cascader/demo/src/app/cascader/cascader-trigger.html new file mode 100644 index 0000000..878db29 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/cascader-trigger.html @@ -0,0 +1 @@ +
diff --git a/src/cascader/demo/src/app/cascader/cascader-valid.html b/src/cascader/demo/src/app/cascader/cascader-valid.html new file mode 100644 index 0000000..af6947c --- /dev/null +++ b/src/cascader/demo/src/app/cascader/cascader-valid.html @@ -0,0 +1,11 @@ +

1.描述

+

校验场景

+

2.示例

+

当前校验规则为:tiRequired

+1.模板驱动表单 +
+ 选中项: {{mySelected | json}} +
+
+2.响应式表单 +
diff --git a/src/cascader/demo/src/app/cascader/cascader-valuekey.html b/src/cascader/demo/src/app/cascader/cascader-valuekey.html new file mode 100644 index 0000000..93265eb --- /dev/null +++ b/src/cascader/demo/src/app/cascader/cascader-valuekey.html @@ -0,0 +1,4 @@ + +
+
Current value: {{ selected | json }}
+
diff --git a/src/cascader/demo/src/app/cascader/webdoc/cascader-demos.js b/src/cascader/demo/src/app/cascader/webdoc/cascader-demos.js new file mode 100644 index 0000000..b478e36 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/webdoc/cascader-demos.js @@ -0,0 +1,138 @@ +export default { + column: '2', + demos: [ + { + demoId: 'cascader-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'cascader basic' + }, + desc: { + 'zh-CN': 'Cascader 组件的最简用法。', + 'en-US': 'cascader basic' + }, + apis: [ 'TiCascaderComponent.properties.items' ] + }, + { + demoId: 'cascader-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'cascader disabled' + }, + desc: { + 'zh-CN': '

通过属性disabled配置禁用。

', + 'en-US': 'cascader disabled' + }, + apis: [ 'TiCascaderComponent.properties.disabled' ] + }, + { + demoId: 'cascader-panel', + name: { + 'zh-CN': '面板大小', + 'en-US': 'cascader panel' + }, + desc: { + 'zh-CN': '

通过属性panelWidthpanelHeight配置面板大小。

', + 'en-US': 'cascader panel' + }, + apis: [ 'TiCascaderComponent.properties.panelWidth', 'TiCascaderComponent.properties.panelHeight' ] + }, + { + demoId: 'cascader-labelkey', + name: { + 'zh-CN': '显示字段', + 'en-US': 'labelkey' + }, + desc: { + 'zh-CN': '

通过属性labelkey配置显示的键值。

', + 'en-US': 'cascader key' + }, + apis: [ 'TiCascaderComponent.properties.labelKey' ] + }, + { + demoId: 'cascader-valuekey', + name: { + 'zh-CN': ' 选中值是一个基本类型数据', + 'en-US': 'valuekey' + }, + desc: { + 'zh-CN': '

通过属性valueKey指定选中项数据的键值。

', + 'en-US': 'cascader key' + }, + apis: [ 'TiCascaderComponent.properties.valueKey' ] + }, + { + demoId: 'cascader-idkey', + name: { + 'zh-CN': '数据的唯一标识', + 'en-US': 'key' + }, + desc: { + 'zh-CN': '

当显示的文本有重复项时,通过属性idKey指定选项数据的唯一性。

', + 'en-US': 'cascader key' + }, + apis: [ 'TiCascaderComponent.properties.idKey' ] + }, + { + demoId: 'cascader-trigger', + name: { + 'zh-CN': '悬浮触发下一级', + 'en-US': 'cascader trigger' + }, + desc: { + 'zh-CN': '

通过属性trigger配置悬浮触发下一级。

', + 'en-US': 'cascader trigger' + }, + apis: [ 'TiCascaderComponent.properties.trigger' ] + }, + { + demoId: 'cascader-showalllevel', + name: { + 'zh-CN': '显示全部路径', + 'en-US': 'cascader showalllevel' + }, + desc: { + 'zh-CN': '

通过属性showalllevel配置是否显示全部路径。

', + 'en-US': 'cascader showalllevel' + }, + apis: [ 'TiCascaderComponent.properties.showAllLevel' ] + }, + { + demoId: 'cascader-onlyselectleaf', + name: { + 'zh-CN': '点击即选中', + 'en-US': 'cascader onlyselectleaf' + }, + desc: { + 'zh-CN': '

通过属性onlyselectleaf配置是否只能选中叶子节点。

', + 'en-US': 'cascader onlyselectleaf' + }, + apis: [ 'TiCascaderComponent.properties.onlySelectLeaf' ] + }, + { + demoId: 'cascader-search', + name: { + 'zh-CN': '搜索', + 'en-US': 'cascader search' + }, + desc: { + 'zh-CN': '

通过属性search配置可搜索。

', + 'en-US': 'cascader search' + }, + apis: [ 'TiCascaderComponent.properties.searchable' ] + }, + { + demoId: 'cascader-events', + name: { + 'zh-CN': '事件', + 'en-US': 'cascader events' + }, + desc: { + 'zh-CN': '点击“清除”时,触发clear事件。', + 'en-US': 'cascader events' + }, + apis: [ 'TiCascaderComponent.properties.clear' ] + }, + + ], +}; diff --git a/src/cascader/demo/src/app/cascader/webdoc/cascader.cn.md b/src/cascader/demo/src/app/cascader/webdoc/cascader.cn.md new file mode 100644 index 0000000..af62966 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/webdoc/cascader.cn.md @@ -0,0 +1,23 @@ +--- +title: Cascader 级联选择 +--- +# Cascader 级联选择 + +
+ +当一个数据集合有清晰的层级结构时,可通过级联选择器逐级查看并选择。 + +```typescript +import { TiCascaderModule } from '@opentiny/ng'; +``` + +
+ +
+ +当一个数据集合有清晰的层级结构时,可通过级联选择器逐级查看并选择。 + +```typescript +import { TiCascaderModule } from '@opentiny/ng'; +``` +
diff --git a/src/cascader/demo/src/app/cascader/webdoc/cascader.en.md b/src/cascader/demo/src/app/cascader/webdoc/cascader.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/cascader/demo/src/app/cascader/webdoc/cascader.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/cascader/demo/src/favicon.ico b/src/cascader/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/cascader/demo/src/index.html b/src/cascader/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/cascader/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/cascader/demo/src/main.ts b/src/cascader/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/cascader/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/cascader/demo/test.ts b/src/cascader/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/cascader/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/cascader/demo/tsconfig.app.json b/src/cascader/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/cascader/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/cascader/demo/tsconfig.spec.json b/src/cascader/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/cascader/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/cascader/lib/index.ts b/src/cascader/lib/index.ts new file mode 100644 index 0000000..ff41a42 --- /dev/null +++ b/src/cascader/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiCascaderModule'; diff --git a/src/cascader/lib/ng-package.json b/src/cascader/lib/ng-package.json new file mode 100644 index 0000000..a094507 --- /dev/null +++ b/src/cascader/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/cascader", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/cascader/lib/package.json b/src/cascader/lib/package.json new file mode 100644 index 0000000..15c5a60 --- /dev/null +++ b/src/cascader/lib/package.json @@ -0,0 +1,17 @@ +{ + "name": "@opentiny/ng-cascader", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-dominator": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-droplist": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-list": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/cascader/lib/project.json b/src/cascader/lib/project.json new file mode 100644 index 0000000..4d41657 --- /dev/null +++ b/src/cascader/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/cascader/lib", + "sourceRoot": "src/cascader/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/cascader"], + "options": { + "project": "src/cascader/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/cascader"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js cascader" + }, + { + "command": "ng default-build cascader" + }, + { + "command": "node build/clear-default-theme.js cascader" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/cascader && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build cascader && ng pack cascader && node build/publish.js cascader --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/cascader/lib/src/TiCascaderComponent.ts b/src/cascader/lib/src/TiCascaderComponent.ts new file mode 100644 index 0000000..cd7349d --- /dev/null +++ b/src/cascader/lib/src/TiCascaderComponent.ts @@ -0,0 +1,611 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, + QueryList, + SimpleChanges, + ViewChild, + ViewChildren +} from '@angular/core'; +import { TiDominatorComponent } from '@opentiny/ng-dominator'; +import { TiDropComponent } from '@opentiny/ng-drop'; +import { TiDroplistComponent } from '@opentiny/ng-droplist'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiListComponent } from '@opentiny/ng-list'; +import { TiPositionType } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; + +export interface TiCascaderItem { + /** + * 显示文本 + */ + label?: string; + /** + * 是否禁用 + */ + disabled?: boolean; + /** + * 二级面板数据 + * + * 包含:1.label:显示文本 2.disabled:是否禁用 3.tip:提示信息 4.tipPosition:tip 显示方位,默认为 right + */ + children?: Array<{ + label?: string; + disabled?: boolean; + tip?: any; + tipPosition?: TiPositionType; + [propName: string]: any; + }>; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} +/** + * 级联选择器 目前只支持二级或三级级联 + * + */ +@Component({ + selector: 'ti-cascader', + templateUrl: './cascader.html', + styleUrls: ['./cascader.less'], + // eslint-disable-next-line no-use-before-define + providers: [TiFormComponent.getValueAccessor(TiCascaderComponent)], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '(blur)': 'onBlur()', + '[class.tp-cascader-dominator]': 'true' + } +}) +export class TiCascaderComponent extends TiFormComponent { + // TODO: add explicit constructor + + // TODO: add explicit constructor + + /** + * 数据列表 + */ + @Input() items: Array; + /** + * 是否开启点击叉号清除选中项 + */ + @Input() clearable: boolean = false; + /** + * 是否开启搜索 + */ + @Input() searchable: boolean = false; + /** + * 输入框的占位文本 + */ + @Input() placeholder: string; + /** + * 下拉框和输入框文本要展示的键值 + */ + @Input() labelKey: string = 'label'; + /** + * 下拉面板的宽度 + */ + @Input() panelWidth: string = '160px'; + /** + * 下拉面板的高度 + */ + @Input() panelHeight: string = '248px'; + /** + * 是否只能选中子节点 + */ + @Input() onlySelectLeaf: boolean = true; + /** + * 输入框是否呈现选中项的全部节点 + */ + @Input() showAllLevel: boolean = true; + /** + * 指定选中项数据的键值 + */ + @Input() valueKey: string; + /** + * 展开下一级面板的触发方式 + */ + @Input() trigger: 'click' | 'hover' = 'click'; + /** + * 设置数据唯一标识的键值 + */ + @Input() idKey: string; + /** + * @ignore + * 指定子节点数据集 + */ + @Input() childrenKey: string = 'children'; + /** + * @ignore + * 选中事件,选中项没有变化 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + /** + * @ignore + * 选中事件,选中项变化 + */ + @Output() readonly change: EventEmitter = new EventEmitter(); + /** + * 点击叉号清除选中项时触发的回调 + */ + @Output() readonly clear: EventEmitter = new EventEmitter(); + + /** + * @ignore + */ + @ViewChild(TiDominatorComponent, { static: true }) + dominatorCom: TiDominatorComponent; + /** + * @ignore + */ + @ViewChild(TiDropComponent) dropCom: TiDropComponent; + /** + * @ignore 呈现各级数据的list + */ + @ViewChildren('commonList') listComs: QueryList; + /** + * @ignore 宽的面板,是一个droplist,用于无数据和搜索场景 + */ + @ViewChild('wideDroplist') wideDroplist: TiDroplistComponent; + /** + * @ignore 分隔符 以后可能会对外开放 + */ + public separator: string = '/'; + /** + * @ignore 转化为数字的panelWidth + */ + public panelWidthNum: number; + /** + * @ignore 三个TiListComponent的model + */ + public listModel: Array = []; + /** + * @ignore 用于dominator呈现数据 + */ + public dominatorModel: string; + /** + * @ignore + * itemsArr[0]存放根面板内容,itemArr[1]存放次级面板内容,itemArr[2]存放次次级面板内容 + */ + public itemsArr: Array> = []; + /** + * @ignore 宽面板的数据 + */ + public wideDroplistOptions: Array = []; + /** + * @ignore + */ + public listComsArr: Array; + /** + * @ignore 宽面板选中值 + */ + public wideDroplistModel: any; + + protected versionInfo: string = super.getVersion(packageInfo); + + /** + * modelWhole与model的中间变量 + * model可能是['', '', ''],也可能是[{}, {}, {}] + * modelWhole是[{}, {}, {}] + */ + private _modelWhole: Array; + + private valueFn: (item: any) => any = (item: any) => { + return item[this.valueKey]; + }; + + ngOnInit(): void { + super.ngOnInit(); + this.panelWidthNum = parseInt(this.panelWidth, 10); + } + ngOnChanges(change: SimpleChanges): void { + super.ngOnChanges(change); + if (change['items']) { + this.itemsArr[0] = this.items; + } + } + /** + * @ignore + */ + ngOnModelChange(): void { + // model改变的时候 + // 1.更新modelWhole,面板显示会更新 + if (this.model?.length > 0 && this.valueKey) { + let index: number = 0; + let selectedLength: number = this.model.length; // 已选中的 label 的长度 + let tempModelWhole: Array = this.items.filter((item) => this.model[index] === this.valueFn(item)); // 中间变量,找到model里面第一个被选中的label + this._modelWhole = tempModelWhole; // 更新_modelWhole + for (index += 1; index < selectedLength; index += 1) { + // 存在多级面板时,寻找其子节点 + if (tempModelWhole[0]?.[this.childrenKey]) { + tempModelWhole = tempModelWhole[0][this.childrenKey].filter((item) => this.model[index] === this.valueFn(item)); + this._modelWhole.push(tempModelWhole[0]); + } + } + } else { + this._modelWhole = this.model; + } + // 2.更新dominator的显示文本 + this.joinDominatorModel(); + } + /** + * @ignore + */ + public onDominatorClick(): void { + if (this.disabled) { + return; + } + if (this.dropCom.isShow || this.wideDroplist.isShow) { + this.close(); + + return; + } + if (this.items.length === 0) { + this.wideDroplist.show(); + } else { + this.showDropCom(); + } + } + ngAfterViewInit(): void { + this.listComsArr = this.listComs.toArray(); + this.setFocusableElems(this.dominatorCom.getFocusableElems()); + this.renderer.setStyle(this.dropCom.nativeElement, 'width', ''); + } + /** + * @ignore + */ + public close(): void { + this.dropCom.hide(); + this.wideDroplist.hide(); + this.listComsArr.forEach((listCom: TiListComponent): void => { + listCom.unlistenIESrollbarBug(); + }); + } + /** + * @ignore + */ + public onBlur(): void { + this.close(); + } + /** + * @ignore + */ + public onSelect($event: TiCascaderItem, panelIndex: number): void { + // 处理面板 + if ($event.children?.length > 0 && this.trigger === 'click') { + this.showNextList($event, panelIndex); + } else { + // 关闭次级以后所有面板 + this.close(); + if (this.searchable) { + this.dominatorCom.focus(); + } + } + this.handleListModel($event, panelIndex); + /** + * 需要修改cascader选中值的时机是: + * 如果onlySelectLeaf === false,目前的listModel组成的数据集合和modelWhole不相等, + * 如果onlySelectLeaf === true,已经选到最后一级,且内容不一致 + */ + if (!this.onlySelectLeaf || !$event.children || $event.children.length === 0) { + if (this.modelWhole?.length !== this.listModel.length) { + this.handleModel(); + + return; + } + + // 如果listModel和modelWhole的长度一样,则需要分别对比数组中的每一个是否一致 + for (let i: number = 0; i < this.listModel.length; i++) { + if (!this.isSame(this.listModel[i], this.modelWhole[i])) { + this.handleModel(); + + return; + } + } + } + } + /** + * @ignore + */ + public onMouseenter(event: TiCascaderItem, panelIndex: number): void { + if (event.children?.length > 0) { + this.showNextList(event, panelIndex); + } else if (panelIndex < this.listComsArr.length - 1) { + // 没有下一级数据,关闭后面所有面板 + this.listComsArr.forEach((list: TiListComponent, index: number): void => { + if (index > panelIndex) { + this.renderer.setStyle(list.nativeElement, 'display', 'none'); + } + }); + } + } + private showNextList(event: TiCascaderItem, panelIndex: number): void { + // 关闭次次级面板 + for (let i: number = panelIndex + 1; i < this.listComsArr.length - 1; i++) { + this.renderer.setStyle(this.listComsArr[i + 1].nativeElement, 'display', 'none'); + } + // 打开次级面板 + this.itemsArr[panelIndex + 1] = event.children; + this.showList(panelIndex + 1); + } + /** + * 在点选时,处理listModel + */ + private handleListModel(event: TiCascaderItem, panelIndex: number): void { + this.listModel = this.listModel || []; + if (this.trigger === 'hover') { + // trigger === hover,先补齐或修正之前的listModel + this.listComsArr.forEach((list: TiListComponent, index: number): void => { + this.listModel[index] = list.hoverOption; + }); + } + this.listModel.splice(panelIndex); + this.listModel.push(event); + } + private handleModel(): void { + // 处理数据 + this.modelWhole = [...this.listModel]; + this.joinDominatorModel(); + } + /** + * @ignore + */ + public onClear(): void { + this.model = []; + this.clear.emit(); + if (this.dropCom.isShow) { + this.listModel = []; + // 隐藏除第一个外的其他list + this.listComsArr.forEach((list: TiListComponent, index: number): void => { + if (index > 0) { + this.renderer.setStyle(list.nativeElement, 'display', 'none'); + } + }); + } + } + /** + * @ignore + */ + public onMouseleave(panelIndex: number): void { + this.listComsArr[panelIndex].hoverOption = undefined; + } + + /** + * @ignore + * 根据model长度,展开有层级的面板(即dropCom) + */ + private showDropCom(): void { + /** + * 触发tiScroll关闭面板的场景下,drop不显示,但list是显示的, + * 下次重新打开时,一个或者多个list会全部显示,除第一个外,其他的都定位错误, + * 所以,需要打开前隐藏除第一个外的其他list。 + */ + this.listComsArr.forEach((list: TiListComponent, index: number): void => { + if (index > 0) { + this.renderer.setStyle(list.nativeElement, 'display', 'none'); + } + list.listenIESrollbarBug(); + }); + this.dropCom.show(); + // drop.show()会设置drop的最小宽度,cascader不需要,此处覆盖 + this.renderer.setStyle(this.dropCom.nativeElement, 'min-width', ''); + if (this.modelWhole && this.modelWhole.length > 0) { + this.modelWhole.forEach((item: TiCascaderItem, i: number): void => { + this.listModel[i] = item; + this.itemsArr[i + 1] = item.children; + this.showList(i); + // 如果是初始有值,不加setTimeout不能滚到正确的位置 + setTimeout((): void => { + this.listComsArr[i].scrollToSelected(); + }, 0); + }); + } else { + this.listModel[0] = undefined; + this.showList(0); + } + } + + /** + * @ignore + */ + public showList(panelIndex: number): void { + this.renderer.setStyle(this.listComsArr[panelIndex].nativeElement, 'display', 'inline-block'); + if (!this.listComsArr[panelIndex].model) { + this.listComsArr[panelIndex].hoverOption = undefined; + } + } + + /** + * @ignore + */ + public onSearch(event: string): void { + if (!this.items || this.items.length === 0) { + return; + } + // 搜索内容不去除前后空格 + if (event === '') { + // searchWord回删至空,交互表现和点击dominator一致,显示有层级的列表 + this.wideDroplist.hide(); + this.showDropCom(); + + return; + } + const lowercaseSearchword: string = event.toLowerCase(); + const wideDroplistOptionLength: number = this.wideDroplistOptions.length; + this.wideDroplistOptions = []; + + this.items.forEach((items1: TiCascaderItem, index1: number): void => { + /** + * 标识有没有找到wideDroplistModel,找到以后就不需要在后面的循环中继续查找 + */ + let flag: boolean = false; + const lowercaseItems1Label: string = items1[this.labelKey].toLowerCase(); + + // 在该项本身搜索 + if ( + (!this.onlySelectLeaf || !items1.children || items1.children?.length === 0) && + lowercaseItems1Label.includes(lowercaseSearchword) + ) { + this.wideDroplistOptions.push({ + label: `${items1[this.labelKey]}`, + indexs: [index1], + [this.idKey]: `${items1[this.idKey]}`, + valueKeys: [items1[this.valueKey]] + }); + if (!flag && this.modelWhole?.length === 1 && this.modelWhole[0] === items1) { + flag = true; + this.wideDroplistModel = this.wideDroplistOptions[this.wideDroplistOptions.length - 1]; + } + } + // 在该项的子项搜索 + if (items1.children?.length > 0) { + items1.children.forEach((items2: TiCascaderItem, index2: number): void => { + const lowercaseItems2Label: string = items2[this.labelKey].toLowerCase(); + + if (!items2.children || items2.children?.length === 0 || !this.onlySelectLeaf) { + if ( + lowercaseItems1Label.includes(lowercaseSearchword) || + lowercaseItems2Label.includes(lowercaseSearchword) || + `${lowercaseItems1Label}/${lowercaseItems2Label}`.includes(lowercaseSearchword) + ) { + this.wideDroplistOptions.push({ + label: `${items1[this.labelKey]}/${items2[this.labelKey]}`, + indexs: [index1, index2], + [this.idKey]: `${items1[this.idKey]}-${items2[this.idKey]}`, + valueKeys: [items1[this.valueKey], items2[this.valueKey]] + }); + if (!flag && this.modelWhole?.length === 2 && this.modelWhole[0] === items1 && this.modelWhole[1] === items2) { + flag = true; + this.wideDroplistModel = this.wideDroplistOptions[this.wideDroplistOptions.length - 1]; + } + } + } + if (items2.children?.length > 0) { + items2.children.forEach((items3: TiCascaderItem, index3: number): void => { + const lowercaseItems3Label: string = items3[this.labelKey].toLowerCase(); + if ( + lowercaseItems1Label.includes(lowercaseSearchword) || + lowercaseItems2Label.includes(lowercaseSearchword) || + lowercaseItems3Label.includes(lowercaseSearchword) || + `${lowercaseItems1Label}/${lowercaseItems2Label}/${lowercaseItems3Label}`.includes(lowercaseSearchword) + ) { + this.wideDroplistOptions.push({ + label: `${items1[this.labelKey]}/${items2[this.labelKey]}/${items3[this.labelKey]}`, + indexs: [index1, index2, index3], + [this.idKey]: `${items1[this.idKey]}-${items2[this.idKey]}-${items3[this.idKey]}`, + valueKeys: [items1[this.valueKey], items2[this.valueKey], items3[this.valueKey]] + }); + if ( + !flag && + this.modelWhole?.length === 3 && + this.modelWhole[0] === items1 && + this.modelWhole[1] === items2 && + this.modelWhole[2] === items3 + ) { + flag = true; + this.wideDroplistModel = this.wideDroplistOptions[this.wideDroplistOptions.length - 1]; + } + } + }); + } + }); + } + }); + + this.dropCom.hide(); + // 如果数据长度有变化,面板打开前,需要先更新数据 + if (wideDroplistOptionLength !== this.wideDroplistOptions.length) { + this.changeDetectorRef.detectChanges(); + } + if (this.wideDroplist.isShow) { + // 打开状态下,重新定位 + this.wideDroplist.rePosition(); + } else { + this.wideDroplist.show(); + } + } + + /** + * @ignore + * 搜索后,在宽面板点选数据 + */ + public wideDroplistChange(event): void { + // 在change事件中,model肯定是会变化的,赋值为空对象不影响modelchange的触发 + if (event?.indexs?.length > 0 && !this.valueKey) { + this.model = []; + event.indexs.forEach((index: number, i: number): void => { + this.model[i] = i === 0 ? this.items[index] : this.model[i - 1].children[index]; + }); + this.modelWhole = [...this.model]; + } else if (this.valueKey) { + this.model = event.valueKeys; + } + } + /** + * @ignore + */ + public onWideDroplistSelect(): void { + // 选中项没有变化的时候,也需要让输入框失焦,选中项变化时输入框失焦是在dominator中处理的 + this.dominatorCom.focus(); + } + private isSame(option1: TiCascaderItem, option2: TiCascaderItem): boolean { + if (this.idKey) { + return option1[this.idKey] === option2[this.idKey]; + } + + return ( + option1 === option2 || + (option1[this.labelKey] !== undefined && option2[this.labelKey] !== undefined && option1[this.labelKey] === option2[this.labelKey]) + ); + } + /** + * 通过model拼接dominatorModel,显示在dominator上 + */ + private joinDominatorModel(): void { + this.dominatorModel = ''; + if (!this.modelWhole || this.modelWhole?.length === 0) { + return; + } + if (this.showAllLevel) { + this.modelWhole.forEach((item: any, index: number): void => { + if (index === 0) { + this.dominatorModel += `${item[this.labelKey]}`; + } else { + this.dominatorModel += `${this.separator}${item[this.labelKey]}`; + } + }); + } else { + this.dominatorModel = this.modelWhole[this.model.length - 1][this.labelKey]; + } + } + + private get modelWhole(): any { + return this._modelWhole; + } + + /** + * 点选时modelWhole改变,要同时更改model + */ + private set modelWhole(value: any) { + this._modelWhole = value; + if (this.valueKey && value !== undefined && value !== null) { + // 存在valueKey,且value不为空 + this.model = value.map((item) => this.valueFn(item)); + } else { + this.model = value; + } + } +} diff --git a/src/cascader/lib/src/TiCascaderModule.ts b/src/cascader/lib/src/TiCascaderModule.ts new file mode 100644 index 0000000..497cca8 --- /dev/null +++ b/src/cascader/lib/src/TiCascaderModule.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { TiDominatorModule } from '@opentiny/ng-dominator'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiDroplistModule } from '@opentiny/ng-droplist'; +import { TiListModule } from '@opentiny/ng-list'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiCascaderComponent } from './TiCascaderComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiDominatorModule, TiDropModule, TiListModule, TiDroplistModule, TiTipModule], + exports: [TiCascaderComponent], + declarations: [TiCascaderComponent] +}) +export class TiCascaderModule {} +export { TiCascaderComponent, TiCascaderItem } from './TiCascaderComponent'; diff --git a/src/cascader/lib/src/cascader.html b/src/cascader/lib/src/cascader.html new file mode 100644 index 0000000..93c310e --- /dev/null +++ b/src/cascader/lib/src/cascader.html @@ -0,0 +1,67 @@ + + + {{dominatorModel}} + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascader/lib/src/cascader.less b/src/cascader/lib/src/cascader.less new file mode 100644 index 0000000..37d0fed --- /dev/null +++ b/src/cascader/lib/src/cascader.less @@ -0,0 +1,25 @@ +@import '../../../themes/basic/base-all.less'; +@import '../../../themes/basic/compnent-container-border.less'; + +:host.tp-cascader-dominator :extend(.ti3-compnent-container-border all) { + width: var(--ti-common-size-50x); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); +} +.tp-cascader-input { + display: inline-block; + height: 100%; + width: 100%; + outline: none; + border: none; + vertical-align: top; + background-color: transparent; + cursor: inherit; +} +.tp-cascader-list { + height: 100%; + &:not(:first-child) { + display: none; + border-left: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + } +} diff --git a/src/checkbox/demo/karma.conf.js b/src/checkbox/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/checkbox/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/checkbox/demo/project.json b/src/checkbox/demo/project.json new file mode 100644 index 0000000..16cc2e7 --- /dev/null +++ b/src/checkbox/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/checkbox/demo", + "sourceRoot": "src/checkbox/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/checkbox", + "index": "src/checkbox/demo/src/index.html", + "main": "src/checkbox/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/checkbox/demo/tsconfig.app.json", + "assets": ["src/checkbox/demo/src/favicon.ico", "src/checkbox/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "checkbox-demo:build:production" + }, + "development": { + "browserTarget": "checkbox-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js checkbox" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/checkbox/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/checkbox/demo/tsconfig.spec.json", + "karmaConfig": "src/checkbox/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/checkbox/demo/src/app/AppComponent.ts b/src/checkbox/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/checkbox/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/checkbox/demo/src/app/AppModule.ts b/src/checkbox/demo/src/app/AppModule.ts new file mode 100644 index 0000000..b2fd209 --- /dev/null +++ b/src/checkbox/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { CheckboxTestModule } from './checkbox/CheckboxTestModule'; + +@NgModule({ + imports: [ + CheckboxTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/checkbox/demo/src/app/IndexComponent.ts b/src/checkbox/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..7184630 --- /dev/null +++ b/src/checkbox/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { CheckboxTestModule } from './checkbox/CheckboxTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = CheckboxTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/checkbox/demo/src/app/app.html b/src/checkbox/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/checkbox/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/checkbox/demo/src/app/checkbox/CheckboxBasicComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckboxBasicComponent.ts new file mode 100644 index 0000000..679dbdb --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckboxBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './checkbox-basic.html' +}) +export class CheckboxBasicComponent { + checked = true; +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckboxDisabledComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckboxDisabledComponent.ts new file mode 100644 index 0000000..c6d665e --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckboxDisabledComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiCheckboxItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './checkbox-disabled.html' +}) +export class CheckboxDisabledComponent { + checked = true; + items: Array = [ + { + id: '01', + label: '中国', + value: 'China', + disabled: false + }, + { + id: '02', + label: '美国', + value: 'America', + disabled: true + }, + { + id: '03', + label: '英国', + value: 'Britain' + } + ]; + value: Array = [this.items[1]]; +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckboxEventComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckboxEventComponent.ts new file mode 100644 index 0000000..b5b8d80 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckboxEventComponent.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core'; +import { TiCheckboxItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './checkbox-event.html' +}) +export class CheckboxEventComponent { + myValue: boolean = true; + myLogs: Array = []; + items: Array = [ + { + id: '01', + label: '中国', + value: 'China' + }, + { + id: '02', + label: '美国', + value: 'America' + }, + { + id: '03', + label: '英国', + value: 'Britain' + } + ]; + value: Array = []; + + onNgModelChange(model: boolean): void { + this.myLogs = [...this.myLogs, `ngModelChange: ${JSON.stringify(model)}`]; + } + + onNgModelChange1(model: any): void { + this.myLogs = [...this.myLogs, `ngModelChange1:${JSON.stringify(model)}`]; + } +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckboxFocusedComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckboxFocusedComponent.ts new file mode 100644 index 0000000..8d805c2 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckboxFocusedComponent.ts @@ -0,0 +1,48 @@ +import { Component } from '@angular/core'; +import { Route } from '@angular/router'; + +@Component({ + templateUrl: './checkbox-focused.html' +}) +export class CheckboxFocusedComponent { + checkList: Array = [ + { + title: '香港', + checked: false, + disabled: false + }, + { + title: '大陆', + checked: true, + disabled: true + }, + { + title: '台湾', + checked: true, + disabled: false + } + ]; + + lists: Array = [ + { + id: 'xianggang', + title: '香港', + checked: false + }, + { + id: 'dalu', + title: '大陆', + checked: true + }, + { + id: 'taiwan', + title: '台湾', + checked: true + } + ]; + + myDisabled: boolean = true; + changeDisabled(): void { + this.myDisabled = !this.myDisabled; + } +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckboxGroupComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckboxGroupComponent.ts new file mode 100644 index 0000000..90d1e2b --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckboxGroupComponent.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; +import { TiCheckboxItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './checkbox-group.html' +}) +export class CheckboxGroupComponent { + data: Array = [ + { id: 1, text: 'guest' }, + { id: 2, text: 'user' }, + { id: 3, text: 'customer' }, + { id: 4, text: 'admin' } + ]; + + items: Array = [ + { + id: '01', + label: '中国', + value: 'China' + }, + { + id: '02', + label: '美国', + value: 'America' + }, + { + id: '03', + label: '英国', + value: 'Britain' + } + ]; + + checked: Array = []; + value: Array = []; +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckboxGroupDirectionComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckboxGroupDirectionComponent.ts new file mode 100644 index 0000000..cfa0afc --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckboxGroupDirectionComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiCheckboxItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './checkbox-group-direction.html' +}) +export class CheckboxGroupDirectionComponent { + items: Array = [ + { + id: '01', + label: '中国', + value: 'China' + }, + { + id: '02', + label: '美国', + value: 'America' + }, + { + id: '03', + label: '英国', + value: 'Britain' + } + ]; + value: Array = []; +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckboxGroupLabelkeyComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckboxGroupLabelkeyComponent.ts new file mode 100644 index 0000000..e070aa5 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckboxGroupLabelkeyComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiCheckboxItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './checkbox-group-labelkey.html' +}) +export class CheckboxGroupLabelkeyComponent { + items: Array = [ + { + id: '01', + label: '中国', + englishName: 'China' + }, + { + id: '02', + label: '美国', + englishName: 'America' + }, + { + id: '03', + label: '英国', + englishName: 'Britain' + } + ]; + value: Array = []; +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckboxGroupLevelComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckboxGroupLevelComponent.ts new file mode 100644 index 0000000..ee50165 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckboxGroupLevelComponent.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './checkbox-group-level.html' +}) +export class CheckboxGroupLevelComponent { + dataArray1: Array = [ + { id: 1, name: '流川枫', age: 15, sex: 'male' }, + { id: 2, name: '小红帽', age: 5, sex: 'female' }, + { id: 3, name: '花木兰', age: 18, sex: 'female' }, + { id: 4, name: '雅典娜', age: 22, sex: 'female' }, + { id: 5, name: '舒克', age: 3, sex: 'male' }, + { id: 6, name: '机器猫', age: 4, sex: 'male' }, + { id: 7, name: '灰姑娘', age: 18, sex: 'female' }, + { id: 8, name: '天线宝宝', age: 1, sex: 'male' }, + { id: 9, name: '葫芦娃', age: 2, sex: 'male' }, + { id: 10, name: '孙悟空', age: 25, sex: 'male' } + ]; + + older10: Array = this.dataArray1.filter((x: any) => x.age > 10); + males: Array = this.dataArray1.filter((x: any) => x.sex === 'male'); + females: Array = this.dataArray1.filter((x: any) => x.sex === 'female'); + + older10Set: Array = []; + malesSet: Array = []; + femalesSet: Array = []; + + dataArray2: Array = [ + { items: this.older10, checkeds: this.older10Set, label: '大于10岁' }, + { items: this.males, checkeds: this.malesSet, label: '男性' }, + { items: this.females, checkeds: this.femalesSet, label: '女性' } + ]; + + checkedSet1: Array = []; +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckboxGroupLinewrapComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckboxGroupLinewrapComponent.ts new file mode 100644 index 0000000..dc7e386 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckboxGroupLinewrapComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { TiCheckboxItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './checkbox-group-linewrap.html' +}) +export class CheckboxGroupLinewrapComponent { + items: Array = [ + { id: '1', label: '小猪佩琪' }, + { id: '2', label: '小红帽' }, + { id: '3', label: '花木兰' }, + { id: '4', label: '雅典娜' }, + { id: '5', label: '舒克' }, + { id: '6', label: '机器猫' }, + { id: '7', label: '灰姑娘' }, + { id: '8', label: '天线宝宝' }, + { id: '9', label: '葫芦娃' }, + { id: '10', label: '孙悟空' } + ]; + value: Array = []; +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckboxGroupValidationComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckboxGroupValidationComponent.ts new file mode 100644 index 0000000..35e1c4c --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckboxGroupValidationComponent.ts @@ -0,0 +1,72 @@ +import { Component, ElementRef } from '@angular/core'; +import { TiCheckboxItem, TiValidationConfig, TiValidators } from '@opentiny/ng'; +import { FormBuilder, FormControl, FormGroup, ValidationErrors } from '@angular/forms'; + +@Component({ + templateUrl: './checkbox-group-validation.html' +}) +export class CheckboxGroupValidationComponent { + items: Array = [ + { + id: '01', + label: '中国', + value: 'China' + }, + { + id: '02', + label: '美国', + value: 'America' + }, + { + id: '03', + label: '英国', + value: 'Britain' + } + ]; + items1: Array = [ + { + id: '01', + label: '中国', + value: 'China' + }, + { + id: '02', + label: '美国', + value: 'America' + }, + { + id: '03', + label: '英国', + value: 'Britain' + } + ]; + + value: Array = []; + validationConfig: TiValidationConfig = { + errorMessage: { + required: '请至少选择一项' + } + }; + validationConfig1: TiValidationConfig = { + errorMessage: { + required: '请至少选择一项' + } + }; + + myFormGroup: FormGroup; + constructor(fb: FormBuilder, private elementRef: ElementRef) { + this.myFormGroup = fb.group({ + formcheckboxgroup: new FormControl(undefined, [TiValidators.required]) + }); + } + + checkgroup(form: FormGroup): void { + const errors: ValidationErrors | null = TiValidators.check(form); + console.log('errors', errors); + } + + checkgroup1(): void { + const errors: ValidationErrors | null = TiValidators.check(this.myFormGroup); + console.log(errors); + } +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckboxGroupValuekeyComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckboxGroupValuekeyComponent.ts new file mode 100644 index 0000000..2bcb931 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckboxGroupValuekeyComponent.ts @@ -0,0 +1,34 @@ +import { Component } from '@angular/core'; +import { TiCheckboxItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './checkbox-group-valuekey.html' +}) +export class CheckboxGroupValuekeyComponent { + data: Array = [ + { id: 1, text: 'guest' }, + { id: 2, text: 'user' }, + { id: 3, text: 'customer' }, + { id: 4, text: 'admin' } + ]; + checked: Array = [1, 2]; + + items: Array = [ + { + id: '01', + label: '中国', + value: 'China' + }, + { + id: '02', + label: '美国', + value: 'America' + }, + { + id: '03', + label: '英国', + value: 'Britain' + } + ]; + value: Array = []; +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckboxIndeterminateComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckboxIndeterminateComponent.ts new file mode 100644 index 0000000..4902f83 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckboxIndeterminateComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './checkbox-indeterminate.html' +}) +export class CheckboxIndeterminateComponent { + data: Array = [ + { id: 1, text: 'guest' }, + { id: 2, text: 'user' }, + { id: 3, text: 'customer' }, + { id: 4, text: 'admin' } + ]; + + checked: Array = [this.data[0], this.data[2]]; +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckboxLabelComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckboxLabelComponent.ts new file mode 100644 index 0000000..fae682d --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckboxLabelComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { TiCheckboxItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './checkbox-label.html' +}) +export class CheckboxLabelComponent { + items: Array = [ + { id: '1', label: '小猪佩琪' }, + { id: '2', label: '小红帽' }, + { id: '3', label: '花木兰' } + ]; + value: Array = []; +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckboxTestModule.ts b/src/checkbox/demo/src/app/checkbox/CheckboxTestModule.ts new file mode 100644 index 0000000..a35b5de --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckboxTestModule.ts @@ -0,0 +1,145 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiButtonModule, TiCheckboxModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { CheckboxBasicComponent } from './CheckboxBasicComponent'; +import { CheckboxDisabledComponent } from './CheckboxDisabledComponent'; +import { CheckboxLabelComponent } from './CheckboxLabelComponent'; +import { CheckboxIndeterminateComponent } from './CheckboxIndeterminateComponent'; +import { CheckboxEventComponent } from './CheckboxEventComponent'; +import { CheckboxFocusedComponent } from './CheckboxFocusedComponent'; +import { CheckboxGroupComponent } from './CheckboxGroupComponent'; +import { CheckboxGroupDirectionComponent } from './CheckboxGroupDirectionComponent'; +import { CheckboxGroupLabelkeyComponent } from './CheckboxGroupLabelkeyComponent'; +import { CheckboxGroupValuekeyComponent } from './CheckboxGroupValuekeyComponent'; +import { CheckboxGroupLinewrapComponent } from './CheckboxGroupLinewrapComponent'; +import { CheckboxGroupLevelComponent } from './CheckboxGroupLevelComponent'; +import { CheckboxGroupValidationComponent } from './CheckboxGroupValidationComponent'; +import { CheckgroupDefaultComponent } from './CheckgroupDefaultComponent'; +import { CheckgroupDisabledTestComponent } from './CheckgroupDisabledTestComponent'; +import { CheckgroupRefreshComponent } from './CheckgroupRefreshComponent'; +import { CheckgroupChangeComponent } from './CheckgroupChangeComponent'; +import { CheckgroupCrossComponent } from './CheckgroupCrossComponent'; +import { CheckgroupTreeComponent } from './CheckgroupTreeComponent'; +import { CheckgroupScenesComponent } from './CheckgroupScenesComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiCheckboxModule, + TiValidationModule, + TiButtonModule, + DemoLogModule, + RouterModule.forChild(CheckboxTestModule.ROUTES) + ], + declarations: [ + CheckboxBasicComponent, + CheckboxDisabledComponent, + CheckboxLabelComponent, + CheckboxIndeterminateComponent, + CheckboxEventComponent, + CheckboxFocusedComponent, + CheckboxGroupComponent, + CheckboxGroupDirectionComponent, + CheckboxGroupLabelkeyComponent, + CheckboxGroupValuekeyComponent, + CheckboxGroupLinewrapComponent, + CheckboxGroupLevelComponent, + CheckboxGroupValidationComponent, + CheckgroupDefaultComponent, + CheckgroupDisabledTestComponent, + CheckgroupRefreshComponent, + CheckgroupChangeComponent, + CheckgroupCrossComponent, + CheckgroupTreeComponent, + CheckgroupScenesComponent + ] +}) +export class CheckboxTestModule { + static readonly LINKS: Array = [{ href: 'components/TiCheckboxComponent.html', label: 'Checkbox' }]; + static readonly ROUTES: Routes = [ + { + path: 'checkbox/checkbox-basic', + component: CheckboxBasicComponent + }, + { + path: 'checkbox/checkbox-disabled', + component: CheckboxDisabledComponent + }, + { + path: 'checkbox/checkbox-label', + component: CheckboxLabelComponent + }, + { + path: 'checkbox/checkbox-indeterminate', + component: CheckboxIndeterminateComponent + }, + { + path: 'checkbox/checkbox-event', + component: CheckboxEventComponent + }, + { + path: 'checkbox/checkbox-focused', + component: CheckboxFocusedComponent + }, + { + path: 'checkbox/checkbox-group', + component: CheckboxGroupComponent + }, + { + path: 'checkbox/checkbox-group-direction', + component: CheckboxGroupDirectionComponent + }, + { + path: 'checkbox/checkbox-group-labelkey', + component: CheckboxGroupLabelkeyComponent + }, + { + path: 'checkbox/checkbox-group-valuekey', + component: CheckboxGroupValuekeyComponent + }, + { + path: 'checkbox/checkbox-group-linewrap', + component: CheckboxGroupLinewrapComponent + }, + { + path: 'checkbox/checkbox-group-level', + component: CheckboxGroupLevelComponent + }, + { + path: 'checkbox/checkbox-group-validation', + component: CheckboxGroupValidationComponent + }, + { + path: 'checkgroup/checkgroup-default', + component: CheckgroupDefaultComponent + }, + { + path: 'checkgroup/checkgroup-disabled-test', + component: CheckgroupDisabledTestComponent + }, + { + path: 'checkgroup/checkgroup-change', + component: CheckgroupChangeComponent + }, + { + path: 'checkgroup/checkgroup-cross', + component: CheckgroupCrossComponent + }, + { path: 'checkgroup/checkgroup-tree', component: CheckgroupTreeComponent }, + { + path: 'checkgroup/checkgroup-refresh', + component: CheckgroupRefreshComponent + }, + { + path: 'checkgroup/checkgroup-scenes', + component: CheckgroupScenesComponent + } + ]; +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckgroupChangeComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckgroupChangeComponent.ts new file mode 100644 index 0000000..e5043a6 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckgroupChangeComponent.ts @@ -0,0 +1,58 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './checkgroup-change.html' +}) +export class CheckgroupChangeComponent { + private items: Array = new Array(200); + private items1: Array = new Array(100); + private checkeds11: Array = new Array(10); + + dataArray: Array = [ + { id: 1, text: 'guest' }, + { id: 2, text: 'user' }, + { id: 3, text: 'customer' }, + { id: 4, text: 'admin' } + ]; + checkedArray: Array = [this.dataArray[0]]; + + constructor() { + for (let i: number = 0; i < this.items.length; i++) { + this.items[i] = { id: i, text: i }; + } + for (let i: number = 0; i < this.items1.length; i++) { + this.items1[i] = this.items[i]; + } + for (let i: number = 0; i < this.checkeds11.length; i++) { + this.checkeds11[i] = this.items1[i]; + } + } + + newItems(): void { + this.dataArray = [ + { id: 1, text: 'guest' }, + { id: 2, text: 'user' }, + { id: 3, text: 'customer' } + ]; + } + changeItems(): void { + this.dataArray.pop(); + } + newCheckeds(): void { + this.checkedArray = [this.dataArray[1]]; + } + changeCheckeds(): void { + this.checkedArray.push(this.dataArray[2]); + } + newItemsAndCheckeds(): void { + this.dataArray = this.items1; + this.checkedArray = this.checkeds11; + } + changeItemsAndCheckeds(): void { + this.dataArray.pop(); + this.checkedArray.pop(); + } + onMyChange(checkeds: Array): void { + console.log('onMyChange checkeds.length=' + checkeds.length); + } +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckgroupCrossComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckgroupCrossComponent.ts new file mode 100644 index 0000000..08a9559 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckgroupCrossComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './checkgroup-cross.html' +}) +export class CheckgroupCrossComponent { + checkedSet1: Array = []; + + myitems: Array = [ + { id: 1, text: '刘德华', singer: true, actor: true }, + { id: 2, text: '孙燕姿', singer: true, actor: false }, + { id: 3, text: '周杰伦', singer: true, actor: false }, + { id: 4, text: '邓丽君', singer: true, actor: false }, + { id: 5, text: '周星驰', singer: false, actor: true }, + { id: 6, text: '成龙', singer: false, actor: true }, + { id: 7, text: '周润发', singer: false, actor: true } + ]; + singers: Array = this.myitems.filter((x: any) => x.singer === true); + actors: Array = this.myitems.filter((x: any) => x.actor === true); + + singersCheckeds: Array = []; + actorsCheckeds: Array = []; +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckgroupDefaultComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckgroupDefaultComponent.ts new file mode 100644 index 0000000..4b42640 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckgroupDefaultComponent.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './checkgroup-default.html' +}) +export class CheckgroupDefaultComponent { + // 数据列表 + dataArray1: Array = [ + { id: 1, text: 'guest' }, + { id: 2, text: 'user' }, + { id: 3, text: 'customer' }, + { id: 4, text: 'admin' } + ]; + + dataArray2: Array = [ + { id: 1, text: 'guest' }, + { id: 2, text: 'user' }, + { id: 3, text: 'customer' }, + { id: 4, text: 'admin' } + ]; + + dataArray3: Array = [ + { id: 1, text: 'guest' }, + { id: 2, text: 'user' }, + { id: 3, text: 'customer' }, + { id: 4, text: 'admin' } + ]; + + dataArray4: Array = []; + + checkedArray1: Array = [this.dataArray1[0], this.dataArray1[3], this.dataArray1[1], this.dataArray1[2]]; + checkedArray2: Array = []; + checkedArray3: Array = [this.dataArray3[0], this.dataArray3[2]]; + checkedArray4: Array = []; +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckgroupDisabledTestComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckgroupDisabledTestComponent.ts new file mode 100644 index 0000000..86245e6 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckgroupDisabledTestComponent.ts @@ -0,0 +1,61 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './checkgroup-disabled-test.html' +}) +export class CheckgroupDisabledTestComponent { + // 数据列表 + dataArray1: Array = [ + { id: 1, text: 'guest' }, + { id: 2, text: 'user' }, + { id: 3, text: 'customer' }, + { id: 4, text: 'admin' } + ]; + + dataArray2: Array = [ + { id: 1, text: 'guest', disabled: true }, + { id: 2, text: 'user', disabled: true }, + { id: 3, text: 'customer', disabled: true }, + { id: 4, text: 'admin', disabled: true } + ]; + + dataArray3: Array = [ + { myId: 'a', text: 'guest', disabled: true }, + { myId: 'b', text: 'user' }, + { myId: 'c', text: 'customer' }, + { myId: 'd', text: 'admin', disabled: true } + ]; + + dataArray4: Array = [ + { id: 1, text: 'guest' }, + { id: 2, text: 'user' }, + { id: 3, text: 'customer' }, + { id: 4, text: 'admin', disabled: true } + ]; + + dataArray5: Array = [ + { id: 1, text: 'guest' }, + { id: 2, text: 'user' }, + { id: 3, text: 'customer' }, + { id: 4, text: 'admin' } + ]; + + dataArray6: Array = [ + { id: 1, text: 'guest', disabled: true }, + { id: 2, text: 'user', disabled: false }, + { id: 3, text: 'customer', disabled: false }, + { id: 4, text: 'admin', disabled: false } + ]; + + checkedArray1: Array = [this.dataArray1[0], this.dataArray1[2]]; + checkedArray2: Array = [this.dataArray2[0], this.dataArray2[2]]; + checkedArray3: Array = [this.dataArray3[0], this.dataArray3[2]]; + checkedArray4: Array = [this.dataArray4[0], this.dataArray4[2]]; + checkedArray5: Array = [this.dataArray5[0], this.dataArray5[2]]; + checkedArray6: Array = [this.dataArray6[2], this.dataArray6[1], this.dataArray6[3]]; + + item: any = this.dataArray6[0]; + changeDisabled(): void { + this.item.disabled = !this.item.disabled; + } +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckgroupRefreshComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckgroupRefreshComponent.ts new file mode 100644 index 0000000..7ccbeeb --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckgroupRefreshComponent.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './checkgroup-refresh.html' +}) +export class CheckgroupRefreshComponent { + dataArray: Array = [ + { id: 1, text: 'guest', disabled: true }, + { id: 2, text: 'user', disabled: false }, + { id: 3, text: 'customer', disabled: true }, + { id: 4, text: 'admin', disabled: false } + ]; + dataArray2: Array = [ + { id: 1, text: 'guest', disabled: true }, + { id: 2, text: 'user', disabled: false }, + { id: 3, text: 'customer', disabled: true }, + { id: 4, text: 'admin', disabled: false } + ]; + checkedArray: Array = [this.dataArray[0]]; + item1: any = this.dataArray[0]; + item2: any = this.dataArray[1]; + item3: any = this.dataArray[2]; + item4: any = this.dataArray[3]; + + refresh(): void { + this.dataArray = this.dataArray2; + this.checkedArray = [this.dataArray2[0]]; + this.item1 = this.dataArray2[0]; + this.item2 = this.dataArray2[1]; + this.item3 = this.dataArray2[2]; + this.item4 = this.dataArray2[3]; + } + onMyChange(checkeds: Array): void { + console.log('onMyChange checkeds.length=' + checkeds.length); + } + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckgroupScenesComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckgroupScenesComponent.ts new file mode 100644 index 0000000..8e572d5 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckgroupScenesComponent.ts @@ -0,0 +1,79 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './checkgroup-scenes.html' +}) +export class CheckgroupScenesComponent { + dataArray1: Array = [ + { id: 1, text: 'guest' }, + { id: 2, text: 'user', disabled: true }, + { id: 3, text: 'customer', disabled: true }, + { id: 4, text: 'admin', disabled: true } + ]; + + dataArray2: Array = [ + { id: 1, text: 'guest' }, + { id: 2, text: 'user' }, + { id: 3, text: 'customer' }, + { id: 4, text: 'admin', disabled: true }, + { id: 5, text: 'developer', disabled: true } + ]; + + data3: Array = [ + { id: 1, text: 'guest' }, + { id: 2, text: 'user' }, + { id: 3, text: 'customer', disabled: true }, + { id: 4, text: 'admin', disabled: true }, + { id: 5, text: 'developer', disabled: true }, + { id: 6, text: 'guest1', disabled: true }, + { id: 7, text: 'user1', disabled: true }, + { id: 8, text: 'customer1', disabled: true }, + { id: 9, text: 'admin1', disabled: true }, + { id: 10, text: 'developer1', disabled: true } + ]; + dataArray3: Array = [].concat(this.data3); + + checkedArray1: Array = [this.dataArray1[0].id]; + checkedArray2: Array = []; + checkedArray3: Array = []; + + showCheckedData(): void { + this.dataArray1 = this.dataArray1.filter((item: any): boolean => { + return this.checkedArray1.includes(item.id); + }); + } + + next(): void { + this.dataArray2 = [ + { id: 6, text: 'teacher', disabled: true }, + { id: 7, text: 'student', disabled: true }, + { id: 8, text: 'worker', disabled: true }, + { id: 9, text: 'manager', disabled: true }, + { id: 10, text: 'boss', disabled: true } + ]; + } + + back(): void { + this.dataArray2 = [ + { id: 1, text: 'guest' }, + { id: 2, text: 'user' }, + { id: 3, text: 'customer' }, + { id: 4, text: 'admin', disabled: true }, + { id: 5, text: 'developer', disabled: true } + ]; + } + + filter(): void { + this.dataArray3 = this.data3.filter((item: any): boolean => { + return item.id < 9; + }); + } + + reset(): void { + this.dataArray3 = [].concat(this.data3); + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/checkbox/demo/src/app/checkbox/CheckgroupTreeComponent.ts b/src/checkbox/demo/src/app/checkbox/CheckgroupTreeComponent.ts new file mode 100644 index 0000000..e200441 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/CheckgroupTreeComponent.ts @@ -0,0 +1,89 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './checkgroup-tree.html' +}) +export class CheckgroupTreeComponent { + /*方案一start************************************************ */ + // 数据列表 + dataArray: Array = [ + { id: 1, name: '流川枫', age: 15, sex: 'male' }, + { id: 2, name: '小红帽', age: 5, sex: 'female', checked: true }, + { id: 3, name: '花木兰', age: 18, sex: 'female' }, + { id: 4, name: '雅典娜', age: 22, sex: 'female' }, + { id: 5, name: '舒克', age: 3, sex: 'male' }, + { id: 6, name: '机器猫', age: 4, sex: 'male' } + ]; + + males: Array = this.dataArray.filter((x: any) => x.sex === 'male'); + females: Array = this.dataArray.filter((x: any) => x.sex === 'female'); + + // 方案一:专门定义item作为身份标识 + malesItem: any = { id: 11, name: '男性组', age: 0, sex: 'male' }; + femalesItem: any = { id: 12, name: '女性组', age: 0, sex: 'female' }; + rootArray: Array = [this.malesItem, this.femalesItem]; + + rootCheckeds: Array = []; + malesCheckeds: Array = this.males.filter((x: any) => (x as any).checked); // 不用as any的话,ts编译报错,因为上面定义的选项数据,有的项目没有chekced属性。 + femalesCheckeds: Array = this.females.filter((x: any) => (x as any).checked); + /*方案一end************************************************ */ + /*方案二start************************************************ */ + // 数据列表 + dataArray2: Array = [ + { id: 201, name: '流川枫', age: 15, sex: 'male', checked: true }, + { id: 202, name: '小红帽', age: 5, sex: 'female' }, + { id: 203, name: '花木兰', age: 18, sex: 'female' }, + { id: 204, name: '雅典娜', age: 22, sex: 'female' }, + { id: 205, name: '舒克', age: 3, sex: 'male' }, + { id: 206, name: '机器猫', age: 4, sex: 'male' } + ]; + + males2: Array = this.dataArray2.filter((x: any) => x.sex === 'male'); + females2: Array = this.dataArray2.filter((x: any) => x.sex === 'female'); + + // 方案二:临时用已经有的唯一对象(数组)作为身份标识 + rootArray2: Array = [this.males2, this.females2]; + + rootCheckeds2: Array = []; + malesCheckeds2: Array = this.males2.filter((x: any) => (x as any).checked); // 不用as any的话,ts编译报错,因为上面定义的选项数据,有的项目没有chekced属性。 + femalesCheckeds2: Array = this.females2.filter((x: any) => (x as any).checked); + /*方案二end************************************************* */ + /*方案三start************************************************ */ + // 数据列表 + dataArray3: Array = [ + { id: 301, name: '流川枫', age: 15, sex: 'male', checked: true }, + { id: 302, name: '小红帽', age: 5, sex: 'female' }, + { id: 303, name: '花木兰', age: 18, sex: 'female', checked: true }, + { id: 304, name: '雅典娜', age: 22, sex: 'female' }, + { id: 305, name: '舒克', age: 3, sex: 'male' }, + { id: 306, name: '机器猫', age: 4, sex: 'male' } + ]; + + males3: Array = this.dataArray3.filter((x: any) => x.sex === 'male'); + females3: Array = this.dataArray3.filter((x: any) => x.sex === 'female'); + + // 方案三:全选按钮管理所有叶子选项。 + rootArray3: Array = this.dataArray3; + + rootCheckeds3: Array = this.dataArray3.filter((x: any) => (x as any).checked); // 不用as any的话,ts编译报错,因为上面定义的选项数据,有的项目没有chekced属性。 + malesCheckeds3: Array = this.males3.filter((x: any) => (x as any).checked); + femalesCheckeds3: Array = this.females3.filter((x: any) => (x as any).checked); + /*方案三end************************************************* */ + myitems: Array = [ + { id: 1, text: '刘德华', singer: true, actor: true }, + { id: 2, text: '孙燕姿', singer: true, actor: false }, + { id: 3, text: '周杰伦', singer: true, actor: false }, + { id: 4, text: '邓丽君', singer: true, actor: false }, + { id: 5, text: '周星驰', singer: false, actor: true }, + { id: 6, text: '成龙', singer: false, actor: true }, + { id: 7, text: '周润发', singer: false, actor: true } + ]; + singers: Array = this.myitems.filter((x: any) => x.singer === true); + actors: Array = this.myitems.filter((x: any) => x.actor === true); + + rootItems: Array = [this.singers, this.actors]; + + rootItemsCheckeds: Array = []; + singersCheckeds: Array = []; + actorsCheckeds: Array = []; +} diff --git a/src/checkbox/demo/src/app/checkbox/checkbox-basic.html b/src/checkbox/demo/src/app/checkbox/checkbox-basic.html new file mode 100644 index 0000000..a34f981 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkbox-basic.html @@ -0,0 +1 @@ + diff --git a/src/checkbox/demo/src/app/checkbox/checkbox-disabled.html b/src/checkbox/demo/src/app/checkbox/checkbox-disabled.html new file mode 100644 index 0000000..1e52eda --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkbox-disabled.html @@ -0,0 +1,4 @@ +

1.禁用某个复选框

+ +

2.复选组禁用某个选项

+ diff --git a/src/checkbox/demo/src/app/checkbox/checkbox-event.html b/src/checkbox/demo/src/app/checkbox/checkbox-event.html new file mode 100644 index 0000000..ca7be92 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkbox-event.html @@ -0,0 +1,12 @@ + +
+ + + diff --git a/src/checkbox/demo/src/app/checkbox/checkbox-focused.html b/src/checkbox/demo/src/app/checkbox/checkbox-focused.html new file mode 100644 index 0000000..ab7d5be --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkbox-focused.html @@ -0,0 +1,31 @@ +

描述

+

Checkbox复选框组件,支持聚焦autofocus/focus()/tabindex演示

+

示例

+

1.autofocus演示:

+ +
+


+

2.focus()演示:

+ + +document.getElementById('myinput').focus() +
+ + +(click)="myinput2.focus()" + +(click)="myinput2.blur()" +


+

3.tabindex演示:

+ +
+ +
+ +
+ + +

置灰时不参与tabindex

+

+

4.click演示:

+ diff --git a/src/checkbox/demo/src/app/checkbox/checkbox-group-direction.html b/src/checkbox/demo/src/app/checkbox/checkbox-group-direction.html new file mode 100644 index 0000000..8d5cdf4 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkbox-group-direction.html @@ -0,0 +1,4 @@ + +
+
Current Select: {{ value | json }}
+
diff --git a/src/checkbox/demo/src/app/checkbox/checkbox-group-labelkey.html b/src/checkbox/demo/src/app/checkbox/checkbox-group-labelkey.html new file mode 100644 index 0000000..612ffc6 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkbox-group-labelkey.html @@ -0,0 +1,4 @@ + +
+
Current Select: {{ value | json }}
+
diff --git a/src/checkbox/demo/src/app/checkbox/checkbox-group-level.html b/src/checkbox/demo/src/app/checkbox/checkbox-group-level.html new file mode 100644 index 0000000..998cf62 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkbox-group-level.html @@ -0,0 +1,19 @@ + +
+ + + +
+
+ + + + +
diff --git a/src/checkbox/demo/src/app/checkbox/checkbox-group-linewrap.html b/src/checkbox/demo/src/app/checkbox/checkbox-group-linewrap.html new file mode 100644 index 0000000..c66b1fc --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkbox-group-linewrap.html @@ -0,0 +1 @@ + diff --git a/src/checkbox/demo/src/app/checkbox/checkbox-group-validation.html b/src/checkbox/demo/src/app/checkbox/checkbox-group-validation.html new file mode 100644 index 0000000..d71df3b --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkbox-group-validation.html @@ -0,0 +1,25 @@ +

1.模板式表单

+
+ + +
+ +
+

2.响应式表单

+
+ +
+ +
diff --git a/src/checkbox/demo/src/app/checkbox/checkbox-group-valuekey.html b/src/checkbox/demo/src/app/checkbox/checkbox-group-valuekey.html new file mode 100644 index 0000000..708d675 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkbox-group-valuekey.html @@ -0,0 +1,13 @@ + +
+ + + +
+
+
Current value: {{ checked | json }}
+
+ +
+
Current Select: {{ value | json }}
+
diff --git a/src/checkbox/demo/src/app/checkbox/checkbox-group.html b/src/checkbox/demo/src/app/checkbox/checkbox-group.html new file mode 100644 index 0000000..9b5f55c --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkbox-group.html @@ -0,0 +1,12 @@ +

1.使用 ticheckitem 指令实现

+ + + +
+
Current value: {{ checked | json }}
+
+

2.使用 ti-checkbox-group 组件实现

+ +
+
Current Select: {{ value | json }}
+
diff --git a/src/checkbox/demo/src/app/checkbox/checkbox-indeterminate.html b/src/checkbox/demo/src/app/checkbox/checkbox-indeterminate.html new file mode 100644 index 0000000..277080f --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkbox-indeterminate.html @@ -0,0 +1,9 @@ + +
+ + + +
+
+
Current value: {{ checked | json }}
+
diff --git a/src/checkbox/demo/src/app/checkbox/checkbox-label.html b/src/checkbox/demo/src/app/checkbox/checkbox-label.html new file mode 100644 index 0000000..493ea30 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkbox-label.html @@ -0,0 +1,25 @@ + + +
+ + + + + + diff --git a/src/checkbox/demo/src/app/checkbox/checkgroup-change.html b/src/checkbox/demo/src/app/checkbox/checkgroup-change.html new file mode 100644 index 0000000..1f4ee22 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkgroup-change.html @@ -0,0 +1,44 @@ +

描述

+

Checkgroup复选框组组件,动态改变数据

+

示例

+

1.有全选框tiCheckgroup

+ +
+ + + + +
+
+ checked = + {{item | json}} +
+
+checkedArray.size={{checkedArray.length}} +
+ + + + + + +
+

2.没有全选框,只有item,使用beCheckeds

+ + + + +
+
+
+ checked = + {{item | json}} +
diff --git a/src/checkbox/demo/src/app/checkbox/checkgroup-cross.html b/src/checkbox/demo/src/app/checkbox/checkgroup-cross.html new file mode 100644 index 0000000..b85deb0 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkgroup-cross.html @@ -0,0 +1,16 @@ +

2.两组group联动:刘德华既是歌手,也是演员,所以是联动的

+
+

2.1参加晚会的歌手:

+ +
+ + + +
+

2.2参加晚会的演员:

+ +
+ + + +
diff --git a/src/checkbox/demo/src/app/checkbox/checkgroup-default.html b/src/checkbox/demo/src/app/checkbox/checkgroup-default.html new file mode 100644 index 0000000..ff1dd93 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkgroup-default.html @@ -0,0 +1,64 @@ +

描述

+

Checkgroup复选框组组件,动态改变数据

+

导入

+

import {{ '{' }} TiCheckboxModule {{ '}' }} from '@opentiny/ng';

+

示例

+

1.设置为全选:设置checkeds为全部数据

+ +
+ + + + +
+
+checkedArray1.size={{checkedArray1.length}} +
+
+ checked = + {{item | json}} +
+
+

2.都设置为未选中:设置checkeds为空

+ +
+ + + + +
+
+
+ checked = + {{item | json}} +
+
+ +

3.部分设置为选中:checkeds设置1,3项

+ +
+ + + + +
+
+
+ checked = + {{item | json}} +
+
+ +

4.不设置选中项,checkeds 和 items 均为[]

+ +
+ + + + +
+
+
+ checked = + {{item | json}} +
diff --git a/src/checkbox/demo/src/app/checkbox/checkgroup-disabled-test.html b/src/checkbox/demo/src/app/checkbox/checkgroup-disabled-test.html new file mode 100644 index 0000000..20330eb --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkgroup-disabled-test.html @@ -0,0 +1,88 @@ +

描述

+

Checkgroup复选框组组件,禁用项目

+

示例

+

1 全选(下级无disabled)

+ +
+ + + + +
+
+
+ checked = + {{item | json}} +
+
+

2 全选(下级全disabled)

+ +
+ + + + +
+
+
+ checked = + {{item | json}} +
+
+

3 全选(下级部分disabled,且选中)

+ +
+ + + + +
+
+
+ checked = + {{item | json}} +
+
+

4 全选(下级部分disabled,且非选中)

+ +
+ + + + +
+
+
+ checked = + {{item | json}} +
+
+

5 全选disabled

+ +
+ + + + +
+
+
+ checked = + {{item | json}} +
+
+

6 改变disabled

+ +
+ + + + +
+
+
+ checked = + {{item | json}} +
+ +
diff --git a/src/checkbox/demo/src/app/checkbox/checkgroup-refresh.html b/src/checkbox/demo/src/app/checkbox/checkgroup-refresh.html new file mode 100644 index 0000000..693aa4c --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkgroup-refresh.html @@ -0,0 +1,30 @@ +

描述

+

Checkgroup复选框组组件,选项带有disabled,动态改变数据

+

示例

+ +
+ + + + + + + + +
+
+ checked = + {{item | json}} +
+
+checkedArray.size={{checkedArray.length}} +
+ diff --git a/src/checkbox/demo/src/app/checkbox/checkgroup-scenes.html b/src/checkbox/demo/src/app/checkbox/checkgroup-scenes.html new file mode 100644 index 0000000..63a9e48 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkgroup-scenes.html @@ -0,0 +1,68 @@ +

描述

+

Checkgroup复选框组组件在配合表格使用时业务发现反馈的目前存在的一些场景问题(待修复)

+

示例

+ +

+ 1. 初始一项选中,其他项非选中且禁用, 点击 “查看已选” 去除 dataArray1 数据中的非选中禁用项, + 然后正常来说全选和之前的选中项应依旧是选中的状态,但实际结果却不是。 +

+ +
+ + + + +
+
+
+ checked = + {{item}} +
+
+

+ +

+ 2. 初始化后只要去操作选中第一个子项,然后点击 “下一页” 改变items数据, 新数据全部禁用,然后再点击 “返回” 将数据置回, + 置回后正常来说应该选中状态不变,但实际上却是所有都成选中状态了。 +

+ +
+ + + + +
+
+
+ checked = + {{item}} +
+
+ + + +

+ 3. 初始化后去操作选中第一个子项,然后点击 “过滤数据” 改变items数据, 结果第一项和第二项都选中了,正常应该是选中状态不变的只有第一是选中的; +

+ +
+ + + + +
+
+
+ checked = + {{item}} +
+
+ diff --git a/src/checkbox/demo/src/app/checkbox/checkgroup-tree.html b/src/checkbox/demo/src/app/checkbox/checkgroup-tree.html new file mode 100644 index 0000000..7112da0 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/checkgroup-tree.html @@ -0,0 +1,162 @@ +

描述

+

Checkgroup复选框组组件,树形结构

+

示例

+

1.1动漫人物(方案一,全选管两个子选项):

+ +
+ + + + + +
+ + + + + + + + + + +
+
+
+
+ rootCheckeds = + {{item | json}} +
+
+
+ malesCheckeds = + {{item | json}} +
+
+
+ femalesCheckeds = + {{item | json}} +
+
+rootArray.length:{{rootArray.length}} +rootArray.includes(males[0]):{{rootArray.includes(males[0])}} +
+
+
+

1.2动漫人物(方案二, 全选管两个子选项, item临时用一个唯一对象即可):

+ +
+ + + + + +
+ + + + + + + + + + +
+
+
+
+ rootCheckeds2 = + {{item | json}} +
+
+
+ malesCheckeds2 = + {{item | json}} +
+
+
+ femalesCheckeds2 = + {{item | json}} +
+
+rootArray2.length:{{rootArray.length}} +
+
+
+

1.3动漫人物(方案三, 全选管第三排所有子选项,方便检出选中的叶子节点):

+ +
+ + + + + +
+ + + + + + + + + + +
+
+
+
+ rootCheckeds3 = + {{item | json}} +
+
+
+ malesCheckeds3 = + {{item | json}} +
+
+
+ femalesCheckeds3 = + {{item | json}} +
+
+rootArray3.length:{{rootArray3.length}} +
+
+
+

2.两组group联动:刘德华既是歌手,也是演员,所以是联动的

+
+ +
+ + + + + +
+ + + + + + + + +
+
+
+
+ rootItemsCheckeds = + {{item | json}} +
+
+
+ singersCheckeds = + {{item | json}} +
+
+
+ actorsCheckeds = + {{item | json}} +
diff --git a/src/checkbox/demo/src/app/checkbox/webdoc/checkbox-demos.js b/src/checkbox/demo/src/app/checkbox/webdoc/checkbox-demos.js new file mode 100644 index 0000000..e15b6eb --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/webdoc/checkbox-demos.js @@ -0,0 +1,158 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'checkbox-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': 'Checkbox 组件的最简用法。', + 'en-US': '' + }, + apis: ['TiCheckboxComponent.properties.label'] + }, + { + demoId: 'checkbox-group', + name: { + 'zh-CN': '复选组', + 'en-US': 'group' + }, + desc: { + 'zh-CN': + '

通过tiCheckitembeCheckeds获取选中值;通过属性items配置checkbox-group所有选项的数据集合。

', + 'en-US': '' + }, + apis: [ + 'TiCheckgroupComponent.properties.beCheckeds', + 'TiCheckgroupComponent.properties.item', + 'TiCheckgroupComponent.properties.label', + 'TiCheckboxGroupComponent.properties.items', + 'TiCheckboxItem.properties.label', + 'TiCheckboxItem.properties.id' + ] + }, + { + demoId: 'checkbox-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled' + }, + desc: { + 'zh-CN': + '

通过属性disabled配置禁用状态,通过属性item.disabled配置checkbox-group选项禁用状态。

', + 'en-US': '' + }, + apis: ['TiCheckboxComponent.properties.disabled', 'TiCheckgroupComponent.properties.disabled', 'TiCheckboxItem.properties.disabled'] + }, + { + demoId: 'checkbox-label', + name: { + 'zh-CN': '自定义文本', + 'en-US': 'label' + }, + desc: { + 'zh-CN': '通过label标签自定义显示文本;通过#item配置checkbox-group选项区域的模板。', + 'en-US': '' + }, + apis: ['TiCheckboxGroupComponent.slots.itemTemplate'] + }, + { + demoId: 'checkbox-indeterminate', + name: { + 'zh-CN': '全选', + 'en-US': 'indeterminate' + }, + desc: { + 'zh-CN': + '

通过属性indeterminate配置半选状态;通过tiCheckgrouptiCheckitem,实现一组数据的全选功能。

', + 'en-US': '' + }, + apis: ['TiCheckgroupComponent.properties.checkeds', 'TiCheckgroupComponent.properties.items'] + }, + { + demoId: 'checkbox-group-direction', + name: { + 'zh-CN': '竖向排列', + 'en-US': 'direction' + }, + desc: { + 'zh-CN': + '

通过属性direction配置checkbox-group排列方向,包括verticalhorizontal(默认)两种类型。

', + 'en-US': '' + }, + apis: ['TiCheckboxGroupComponent.properties.direction'] + }, + { + demoId: 'checkbox-group-labelkey', + name: { + 'zh-CN': '显示字段', + 'en-US': 'labelkey' + }, + desc: { + 'zh-CN': '

通过属性labelKey配置 Checkboxgroup 组件显示数据的键值。', + 'en-US': 'labelkey' + }, + apis: ['TiCheckboxGroupComponent.properties.labelKey'] + }, + { + demoId: 'checkbox-group-valuekey', + name: { + 'zh-CN': '选中值', + 'en-US': 'valuekey' + }, + desc: { + 'zh-CN': '

通过属性valueKey配置组件选中项数据的键值。', + 'en-US': 'valuekey' + }, + apis: ['TiCheckboxGroupComponent.properties.valueKey', 'TiCheckgroupComponent.properties.valueKey'] + }, + { + demoId: 'checkbox-group-validation', + name: { + 'zh-CN': '表单校验', + 'en-US': 'validation' + }, + desc: { + 'zh-CN': '

通过指令tiValidation实现校验。

', + 'en-US': '' + } + }, + { + demoId: 'checkbox-group-linewrap', + name: { + 'zh-CN': '自动换行', + 'en-US': 'linewrap' + }, + desc: { + 'zh-CN': '超过固定宽度会自动换行。', + 'en-US': '' + } + }, + { + demoId: 'checkbox-group-level', + name: { + 'zh-CN': '关系复杂的数据', + 'en-US': 'level' + }, + desc: { + 'zh-CN': '通过tiCheckgrouptiCheckitem管理较为复杂的数据。', + 'en-US': '' + } + }, + { + demoId: 'checkbox-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event' + }, + desc: { + 'zh-CN': '当元素的值发生变化时触发ngModelChange事件。', + 'en-US': '' + } + } + ], + ignoreApis: ['TiCheckboxGroupComponent.properties.disabled'] +}; diff --git a/src/checkbox/demo/src/app/checkbox/webdoc/checkbox.cn.md b/src/checkbox/demo/src/app/checkbox/webdoc/checkbox.cn.md new file mode 100644 index 0000000..4958907 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/webdoc/checkbox.cn.md @@ -0,0 +1,29 @@ +--- +title: Checkbox 复选框 +--- +# Checkbox 复选框 + +
+ +选择或取消选择一个选项。 + +```typescript +import { TiCheckboxModule } from '@opentiny/ng'; +``` + +如需使用校验功能,请导入。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
+ +
+ +选择或取消选择一个选项。 + +```typescript +import { TiCheckboxModule } from '@opentiny/ng'; +``` +
diff --git a/src/checkbox/demo/src/app/checkbox/webdoc/checkbox.en.md b/src/checkbox/demo/src/app/checkbox/webdoc/checkbox.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/checkbox/demo/src/app/checkbox/webdoc/checkbox.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/checkbox/demo/src/favicon.ico b/src/checkbox/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/checkbox/demo/src/index.html b/src/checkbox/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/checkbox/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/checkbox/demo/src/main.ts b/src/checkbox/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/checkbox/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/checkbox/demo/test.ts b/src/checkbox/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/checkbox/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/checkbox/demo/tsconfig.app.json b/src/checkbox/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/checkbox/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/checkbox/demo/tsconfig.spec.json b/src/checkbox/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/checkbox/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/checkbox/lib/index.ts b/src/checkbox/lib/index.ts new file mode 100644 index 0000000..7c6d8bc --- /dev/null +++ b/src/checkbox/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiCheckboxModule'; diff --git a/src/checkbox/lib/ng-package.json b/src/checkbox/lib/ng-package.json new file mode 100644 index 0000000..8987233 --- /dev/null +++ b/src/checkbox/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/checkbox", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/checkbox/lib/package.json b/src/checkbox/lib/package.json new file mode 100644 index 0000000..2868d81 --- /dev/null +++ b/src/checkbox/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-checkbox", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/checkbox/lib/project.json b/src/checkbox/lib/project.json new file mode 100644 index 0000000..48ecd3c --- /dev/null +++ b/src/checkbox/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/checkbox/lib", + "sourceRoot": "src/checkbox/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/checkbox"], + "options": { + "project": "src/checkbox/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/checkbox"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js checkbox" + }, + { + "command": "ng default-build checkbox" + }, + { + "command": "node build/clear-default-theme.js checkbox" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/checkbox && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build checkbox && ng pack checkbox && node build/publish.js checkbox --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/checkbox/lib/src/TiCheckboxComponent.ts b/src/checkbox/lib/src/TiCheckboxComponent.ts new file mode 100644 index 0000000..c428b75 --- /dev/null +++ b/src/checkbox/lib/src/TiCheckboxComponent.ts @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, HostListener, ViewEncapsulation } from '@angular/core'; +import { TiRadioBaseComponent } from '@opentiny/ng-base'; +import { TiBrowser, TiLog, Util } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; + +/** + * Checkbox组件,尽管这是一个组件,但使用方法有点像属性指令。主要功能为设置某一条件是否被选中。 + * + * Checkbox组件完全基于原生input checkbox实现, + * + * 可以使用Angular针对原生Checkbox提供的所有指令,包括:disabled, model, click等, + * + * 但是要记得在input中添加tiCheckbox属性。 + * + */ +@Component({ + selector: '[tiCheckbox]', + templateUrl: './checkbox.html', + styleUrls: ['./checkbox.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiCheckboxComponent extends TiRadioBaseComponent { + /** + * 问题一:浏览器兼容性问题:用angular提供的indeterminate属性设置半选,在谷歌下全部正常显示。 + * 在火狐和IE下,checked=true 和indeterminate=true同时存在时,半选样式存在问题,不知道是否和tiny组件的css样式写法有关。 + * 为了统一逻辑和预测,checkgroup和tree组件内部本应该这样使用:置indeterminate=true时,同时会置checked = false + * 但是,暂时tree组件没有这样做,由TiCheckboxComponent来纠错。 + * TiCheckboxComponent补充效果:tree组件/业务用户,置indeterminate=true时,组件自身会置checked = false + * 问题二:浏览器兼容性问题:Chrome/Firefox半选点击后变为全选,checked状态改变。IE半选点击后变为不选,checked状态未改变,所以没有通知change事件。 + * 但checkgroup需要change事件去维护子选项 + * TiCheckboxComponent IE补充效果:监听IE的click,当indeterminate从true变为false时,组件会置checked = false,并通知change事件。 + */ + private lastIndeterminate: boolean = undefined; + protected versionInfo: string = super.getVersion(packageInfo); + ngAfterViewChecked(): void { + super.ngAfterViewChecked(); + // 补充效果:tree组件/业务用户,置indeterminate=true时,组件自身会置checked = false + if (this.nativeElement.indeterminate) { + this.nativeElement.checked = false; + } + this.lastIndeterminate = this.nativeElement.indeterminate; + } + /** + * @ignore + * IE补充效果:监听IE的click,当indeterminate从true变为false时,组件会置checked = false,并通知change事件。 + */ + @HostListener('click', ['$event']) public onCheckboxHostClick(event: MouseEvent): void { + if (TiBrowser.isIE() && this.lastIndeterminate && !this.nativeElement.indeterminate) { + this.checked(true); + } + } + + /** + * @ignore + * 设置checked属性,indeterminate,同时可能触发change事件。 + * 以后可以考虑开放这个接口,给用户调用 + * @protected + * @param checked true/false/半选 半选用'indeterminate'或null + */ + protected checked(checked: boolean | 'indeterminate'): void { + const lastChecked: boolean = this.nativeElement.checked; + const lastIndeterminate: boolean = this.nativeElement.indeterminate === true ? true : false; // 将undefined也转为false。 + switch (checked) { + case true: // 全选 + this.nativeElement.checked = true; + this.nativeElement.indeterminate = false; + break; + case false: // 全不选 + this.nativeElement.checked = false; + this.nativeElement.indeterminate = false; + break; + case null: // 半选 + case 'indeterminate': // 半选 + this.nativeElement.checked = false; // TODO: 为了保证IE下点击半选后,变为全选,这里设false。未验证。 + this.nativeElement.indeterminate = true; + break; + default: // 不会走到这里。 + TiLog.error('setChecked(),switch default, can not reach code'); + break; + } + // indeterminate初始时为undefined。 + if (this.nativeElement.checked !== lastChecked || this.nativeElement.indeterminate !== lastIndeterminate) { + Util.trigger(this.nativeElement, 'change'); // 必须主动触发change事件,否则ngModel不更新。也走不到change回调。 + } + } +} diff --git a/src/checkbox/lib/src/TiCheckboxGroupComponent.ts b/src/checkbox/lib/src/TiCheckboxGroupComponent.ts new file mode 100644 index 0000000..3826a88 --- /dev/null +++ b/src/checkbox/lib/src/TiCheckboxGroupComponent.ts @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ContentChild, Input, TemplateRef, ViewEncapsulation } from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +/** + * + * ti-checkbox-group组件的数据集格式 + */ +export interface TiCheckboxItem { + /** + * 显示文本 + */ + label?: string; + /** + * 是否禁用 + */ + disabled?: boolean; + /** + * HTML属性id + */ + id?: string; + [propName: string]: any; +} + +/** + * + * 将多个复选框聚合在一起,成为一个组,在表单校验时需要使用。 + * + */ +@Component({ + selector: 'ti-checkbox-group', + templateUrl: 'checkboxgroup.html', + styleUrls: ['./checkboxgroup.less'], + encapsulation: ViewEncapsulation.None, + host: { + '[class.ti-checkboxgroup-horizon]': 'direction === "horizontal"', + '[class.ti-checkboxgroup-vertical]': 'direction === "vertical"', + '[class.ti-checkboxgroup-defalut-item]': '!itemTemplate' + }, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiCheckboxGroupComponent)] +}) +export class TiCheckboxGroupComponent extends TiFormComponent { + /** + * 选项的全部数据 + */ + @Input() items: Array; + /** + * 数据要显示的键值 + */ + @Input() labelKey: string = 'label'; + /** + * 指定选中项数据的键值 + */ + @Input() valueKey: string; + /** + * 指定排列方向 + */ + @Input() direction: 'vertical' | 'horizontal' = 'horizontal'; + /** + * 自定义数据选项模板 + */ + @ContentChild('item', { static: false }) itemTemplate: TemplateRef; + + /** + * @ignore + * checkgroup的checkeds更新只是emit,数据引用地址没变 + * 通过一个中间变量实现model和checkgroup checkeds接口的更新 + */ + public checkeds: Array = []; + protected versionInfo: string = super.getVersion(packageInfo); + /** + * @ignore + */ + public onCheckedsChange(checkeds: Array): void { + this.model = [...checkeds]; + } + /** + * @ignore + */ + public writeValue(value: Array): void { + super.writeValue(value); + this.checkeds = this.model ? [...this.model] : []; + } +} diff --git a/src/checkbox/lib/src/TiCheckboxModule.ts b/src/checkbox/lib/src/TiCheckboxModule.ts new file mode 100644 index 0000000..58f0df4 --- /dev/null +++ b/src/checkbox/lib/src/TiCheckboxModule.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiCheckboxComponent } from './TiCheckboxComponent'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiCheckboxGroupComponent } from './TiCheckboxGroupComponent'; +import { TiCheckgroupComponent } from './TiCheckgroupComponent'; +import { TiCheckitemComponent } from './TiCheckitemComponent'; +@NgModule({ + imports: [CommonModule, TiIconModule, TiOutlineModule], + exports: [TiCheckboxComponent, TiCheckgroupComponent, TiCheckitemComponent, TiCheckboxGroupComponent], + declarations: [TiCheckboxComponent, TiCheckgroupComponent, TiCheckitemComponent, TiCheckboxGroupComponent] +}) +export class TiCheckboxModule {} +export { TiCheckboxComponent } from './TiCheckboxComponent'; +export { TiCheckboxGroupComponent, TiCheckboxItem } from './TiCheckboxGroupComponent'; +export { TiCheckgroupComponent } from './TiCheckgroupComponent'; +export { TiCheckitemComponent } from './TiCheckitemComponent'; diff --git a/src/checkbox/lib/src/TiCheckgroupComponent.ts b/src/checkbox/lib/src/TiCheckgroupComponent.ts new file mode 100644 index 0000000..9aa944d --- /dev/null +++ b/src/checkbox/lib/src/TiCheckgroupComponent.ts @@ -0,0 +1,422 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, EventEmitter, HostListener, Input, IterableDiffer, Output } from '@angular/core'; + +import { ObservableMap, ObservableSet, TiLog } from '@opentiny/ng-utils'; +import { TiCheckitemComponent } from './TiCheckitemComponent'; +// TODO: 有些用户提出,响应式表单里,checkgroup value没有绑定已选中数据,不方便。 +/** + * Checkgroup全选框组件,主要功能为从一数据集合中选择某几条数据,与多选组件(Select multiple)功能相同,只是数据规模和视觉呈现不同。 + * + * 复选框组通常由一个全选框和多个子复选框构成。 + * + */ +@Component({ + selector: '[tiCheckgroup]', + templateUrl: './checkbox.html', + styleUrls: ['./checkbox.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + tiCheckbox: '' // 给宿主元素添加tiCheckbox属性,实现定制样式 + } +}) +export class TiCheckgroupComponent extends TiCheckitemComponent { + /** + * 复选框的全部数据 + */ + @Input() items: Array; + /** + * 选中项数据 + */ + @Input() checkeds: Array | Set; + /** + * 选中项变化的回调事件 + */ + @Output() readonly checkedsChange: EventEmitter | Array> = new EventEmitter | Array>(); + + private checkedMap: ObservableMap; // 储存本组所有子选项状态:key:item,value:true/false/null, 全选/不选/半选 + private disabledSet: ObservableSet = new ObservableSet(); // 储存本组所有禁用的子选项 + + private itemsDiffer: IterableDiffer; + private checkedsDiffer: IterableDiffer; + + /** + * @ignore + * 监听change事件,会多触发一次DoCheck。 + * @param checked 原生change事件选中状态:全选,全不选 + */ + @HostListener('change', ['$event']) public onHostChange(checked: any): void { + super.onHostChange(checked); + if (this.nativeElement.checked === true) { + // 全选 + this.checkAll(true); + } else if (this.nativeElement.indeterminate === true) { + // 半选情况,不作处理。 + } else { + // 全不选 + this.checkAll(false); + } + } + + ngOnInit(): void { + super.ngOnInit(); + this.initCheckedMap(); + this.initGlobeCheckedMap(); + this.initDisabledSet(); + this.setChecked(); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + // 解除监听 + TiCheckitemComponent.globeCheckedMap.removeObserver(this.globeCheckedMapObserverFn); + TiCheckitemComponent.globeDisabledSet.removeObserver(this.globeDisabledSetObserverFn); + } + + ngDoCheck(): void { + super.ngDoCheck(); + // 【1】[items][checkeds] → checkedMap + const itemsChanges: any = this.itemsDiffer.diff(this.items); + const checkedsChanges: any = this.checkedsDiffer.diff(this.checkeds); + if (checkedsChanges) { + checkedsChanges.forEachAddedItem((record) => { + const item: any = this.getItem(record.item); + if (this.items.includes(item)) { + this.checkedMap.set(item, true, this.checkeds); + } else { + // 正常不会走到这里 + this.checkedMap.delete(item, this.checkeds); + // 表格需求:允许用户设置的checkeds,范围大于items。所以会走到这里 + } + }); + checkedsChanges.forEachRemovedItem((record) => { + const item: any = this.getItem(record.item); + if (this.items.includes(item)) { + if (this.checkedMap.get(item) === true || this.checkedMap.get(item) === undefined) { + this.checkedMap.set(item, false, this.checkeds); + } + } else { + this.checkedMap.delete(item, this.checkeds); + } + // 强调:如果子选项是半选状态null,就不再置false了。 + // 规律:父节点,不会下达让子节点由null到false的指令。父节点,只会下达让节点由null到true的指令。 + // 注意:在树型结构中,外界@Input checkeds不能强制指定某个半选节点,变为不选。(checkeds中去除某个节点,此节点保持半选不变。) + }); + } + // 将每次的变化,合入到全局变量中。因为多出来的项目,可能是刚才使用过的,所以需要重新赋值 + if (itemsChanges) { + itemsChanges.forEachAddedItem((record) => { + // Map中没有此项,会返回undefined + if (this.checkedMap.has(record.item)) { + // 初始化为什么会走到这里?本以为没有代码可以走到这里 + // 【5】checkedMap→globeCheckedMap + TiCheckitemComponent.globeCheckedMap.set(record.item, this.checkedMap.get(record.item), this.checkedMap); + } else { + // 表格需求:checkeds允许范围比items更大。所以,根据checkeds来给checkedMap设置。 + let isChecked: boolean = false; + const value = this.getValue(record.item); + if (this.checkeds instanceof Array) { + isChecked = this.checkeds.includes(value); + } else if (this.checkeds instanceof Set) { + isChecked = this.checkeds.has(value); + } + this.checkedMap.set(record.item, isChecked, this.checkeds); + if (TiCheckitemComponent.globeDisabledSet.has(record.item)) { + this.disabledSet.add(record.item); + } else { + this.disabledSet.delete(record.item); + } + } + }); + itemsChanges.forEachRemovedItem((record) => { + this.checkedMap.delete(record.item, this.checkeds); // from标志从checkeds来,所以词条checkedMap变化,并未同步到checkeds。 + // 注意:会造成checkeds范围大于items + this.disabledSet.delete(record.item); + }); + } + if (checkedsChanges || itemsChanges) { + // 【3】checkedMap → Group checked + this.setChecked(); // 设置全选状态。本地checkedMap变化,元素很多,但只初始化一次。 + } + // 发出事件:checkeds改变 + if (checkedsChanges) { + /** + * checkedsChange 回调中同步修改其他数据会报错,影响正常功能,所以需要延时, + * 为什么不使用setTimout而使用Promise,Promise.then 是微任务,比setTimout(宏任务)先执行,想尽量早emit事件 + * 宏任务和微任务:https://juejin.cn/post/6844903657264136200 + * 扩展:ngDoCheck,ngAfterViewChecked中emit事件,在事件回调中同步修改数据,都存在同样问题。 + */ + Promise.resolve(null).then(() => { + this.checkedsChange.emit(this.checkeds); + }); + } + } + private initCheckedMap(): void { + // 监听items和checkeds + this.itemsDiffer = this.iterableDiffers.find(this.items).create(null); + this.checkedsDiffer = this.iterableDiffers.find(this.checkeds).create(null); + this.checkedMap = new ObservableMap(); + this.items.forEach( + // 【1】[items][checkeds] → checkedMap + (item) => { + let isChecked: boolean = false; + const value: any = this.getValue(item); + if (this.checkeds instanceof Array) { + isChecked = this.checkeds.includes(value); + } else if (this.checkeds instanceof Set) { + isChecked = this.checkeds.has(value); + } + if (isChecked) { + // 选中态,直接添加 + this.checkedMap.set(item, true, this.checkeds); + } else if (this.checkedMap.get(item) === true || this.checkedMap.get(item) === undefined) { + // 初始化 + this.checkedMap.set(item, false, this.checkeds); + // 强调:如果子选项是半选状态null,就不再置false了。 + // 规律:父节点,不会下达让子节点由null到false的指令。父节点,只会下达让节点由null到true的指令。 + // 注意:在树型结构中,外界@Input checkeds不能强制指定某个半选节点,变为不选。(checkeds中去除某个节点,此节点保持半选不变。) + } + } + ); + + this.checkedMap.addObserver((item: any, value: any, isAdd: boolean, from: any) => { + // 有多个来源:chekceds,(items),checkall,globe + // 不是从checkeds来的,就要同步到chekeds + if (from !== this.checkeds) { + // 【2】checkedMap- >[checkeds] + const isChecked: boolean = isAdd && this.checkedMap.get(item); + const newItem: any = this.getValue(item); + if (this.checkeds instanceof Set) { + isChecked ? this.checkeds.add(newItem) : this.checkeds.delete(item); + } else if (this.checkeds instanceof Array) { + // TODO:写公用的Array remove函数 + const index: number = this.checkeds.indexOf(newItem); + if (isChecked && index === -1) { + this.checkeds.push(newItem); + } else if (!isChecked && index !== -1) { + this.checkeds.splice(index, 1); + } + } + } + // 不是从全局来的,就要同步到全球。 + if (from !== TiCheckitemComponent.globeCheckedMap) { + // 【5】checkedMap→globeCheckedMap + TiCheckitemComponent.globeCheckedMap.set(item, value, this.checkedMap); + } + // 从全局来的,需要更新全选状态。(树形结构层级较多,需要这个逻辑) + if (from === TiCheckitemComponent.globeCheckedMap) { + // 【3】checkedMap → Group checked + this.setChecked(); + } + }); + } + private initGlobeCheckedMap(): void { + // checkedMap以group端优先,disabled以item端优先。 + // checkedMap同步加入到globeCheckedMap + this.checkedMap.getMap().forEach((item) => { + // 【5】checkedMap→globeCheckedMap + TiCheckitemComponent.globeCheckedMap.set(item[0], item[1], this.checkedMap); + }); + // 监听globecheckedMap + TiCheckitemComponent.globeCheckedMap.addObserver(this.globeCheckedMapObserverFn); + } + private initDisabledSet(): void { + // 【7】globeDisabledSet→disabledSet,并监听 + this.items.forEach((item) => { + TiCheckitemComponent.globeDisabledSet.has(item) ? this.disabledSet.add(item) : this.disabledSet.delete(item); + }); + // 【7】globeDisabledSet→disabledSet,监听globeDisabledSet + TiCheckitemComponent.globeDisabledSet.addObserver(this.globeDisabledSetObserverFn); + // 【8】disabledSet→Group checked + this.disabledSet.addObserver((item: any, isAdd: boolean, from: any) => { + // 从全局disabledSet而来,说明是用户点击改变disabled,需要重新判断全选状态。 + if (from === TiCheckitemComponent.globeDisabledSet) { + this.setChecked(); + } + // 如果是从其他地方而来,是从docheck diff而来。内部有setsetChecked,不用主动调用。 + }); + } + /** + * 【6】globeCheckedMap→checkedMap + */ + private globeCheckedMapObserverFn = (item: any, value: any, isAdd: boolean, from: any) => { + if (from !== this.checkedMap && this.items.includes(item)) { + // 同步globe的改变到本地 + if (isAdd) { + this.checkedMap.set(item, value, TiCheckitemComponent.globeCheckedMap); + } else { + // !isAdd. 如果globe删除了某项,但这项属于自己的子项,那么把这一项添加回globe。(场景:子项item动态改变,并未销毁,却触发了全局delete) + TiCheckitemComponent.globeCheckedMap.set(item, this.checkedMap.get(item), this.checkedMap); + } + } + }; + /** + * 【7】globeDisabledSet→disabledSet + */ + private globeDisabledSetObserverFn = (item: any, isAdd: boolean) => { + if (isAdd && this.items.includes(item)) { + this.disabledSet.add(item, TiCheckgroupComponent.globeDisabledSet); + } else if (!isAdd) { + this.disabledSet.delete(item, TiCheckgroupComponent.globeDisabledSet); + } + }; + /** + * 设置本group全选框的状态 【3】checkedMap → Group checked + */ + private setChecked(): void { + this.checked(this.getCheckedStatus()); + } + /** + * 规则一:disabled状态的复选框不影响全选状态,所以需要将其从统计信息中过滤 + * 例如有3个复选框,前两个为选中状态,最后一个是disabled,则认为整体为全部选中状态 + * 所以无需关心disabled复选框的选中状态,只需关心非disabled状态即可 + * 规则二:但当子复选框全部是disabled状态时,将会影响全选的状态 + * @returns 全选:true,全不选:false,半选:null + */ + private getCheckedStatus(): boolean { + if (this.items.length === 0) { + // 情景零:没有选项 + return false; + } + // 用户可勾选项目数 + let checkedableNum = this.items.length - this.disabledSet.size; + const isTotalCount = this.disabledSet.size === 0 || checkedableNum === 0; // 全部可用,或者全部禁用。所有选项进入统计。 + if (checkedableNum === 0) { + // 如果全部禁用,那么所有选项都计入统计。 + checkedableNum = this.items.length; + } + let checkedNum = 0; // 选中项数目(非禁用) + let notCheckedNum = 0; // 未选项数目(非禁用) + for (const item of this.items) { + if (isTotalCount || !this.disabledSet.has(item)) { + // 全部进入统计,或者未禁用项进入统计。 + switch (this.checkedMap.get(item)) { + case true: + checkedNum++; + break; + case false: + notCheckedNum++; + break; + case null: + return null; // 只要有一个子选项是半选,那么结果是半选。 + default: + // 不会走到这里 + TiLog.error('getCheckedStatus(),can not reach code, items item is not in checkedMap'); + break; + } + if (checkedNum !== 0 && notCheckedNum !== 0) { + // 有选中,有不选中,所以半选。 + return null; // 半选 + } else if (checkedNum === checkedableNum) { + return true; + } else if (notCheckedNum === checkedableNum) { + return false; + } + } + } + return false; + } + /** + * 设置全选框的状态值 + * 【4】Group checked → checkedMap + * @param checked + */ + private checkAll(checked: boolean): void { + this.items.forEach((item) => { + if (!this.disabledSet.has(item)) { + // 【4】Group checked → checkedMap + this.checkedMap.set(item, checked, this.checkAll); + } + }); + } + /** + * 输出:得到整个item对象 + * @param value item对象或者数据的身份标识 + * + */ + private getItem(value: any): any { + for (const obj of this.items) { + if (obj === value || obj[this.valueKey] === value) { + return obj; + } + } + + return null; + } +} +/* +需求:Group checked,Item checked,[(checkedSet)]三者其一改变,都要通知其他二者。所以采用中介者模式。 + +版本一:Tiny2,中介者是[(checkedSet)],但要求Item也去绑定[(groupCheckedSet)] + + Group checked ← disabledSet + ↕ ↑ +[dataArray] [checkedSet] groupDisabledSet + ↕ ↑ + Item checked Item disabled + +版本二:中介者是全局变量globeCheckedSet,不要求Item绑定其他变量。 + + Group checked ← disabledSet +[dataArray] ↕ ↑ +[(checkedSet)]↔globeCheckedSet globeDisabledSet + ↕ ↑ + Item checked Item disabled + +版本三:链条传递,不要求Item绑定其他变量。 + + Group checked ← disabledSet + ↕ ↑ +[dataArray] [checkedSet] ↑ + ↕ ↑ + globeCheckedSet globeDisabledSet + ↕ ↑ + Item checked Item disabled + +版本四:链条传递,不要求Item绑定其他变量。但也可以绑定所属于组织选中变量。Array/Set兼容 + + Group checked ← disabledSet + ↕ ↑ + [checkeds] ↔ checkedSet ↑ + ↕ ↑ + globeCheckedSet globeDisabledSet + ↕ ↑ + Item checked Item disabled + ↕ + [beCheckeds] ↔ beCheckedSet +版本五:链条传递,不要求Item绑定其他变量。但也可以绑定所属于组织选中变量。Array/Set兼容。 +兼容Tree树形结构,子节点也有可能是半选状态。 + 8 + Group checked ← disabledSet + 1> 3↑↓4 ↑ + [items][checkeds] ↔ checkedMap ↑ + <2 5↓↑6 ↑7 + globeCheckedMap globeDisabledSet + ↓↑ ↑ + Item checked Item disabled + ↓↑ + [beCheckeds] ↔ beCheckedSet +*/ +/* +@Input支持Set|Array +方案一:每次使用时,都判断类型,调用不同方法。 +方案二:@Input set时,给Array绑定上has()、add()、delete()方法,但是get size怎么绑定? +方案三:Set|Array,赋值给新的Set。采用 + */ +/* TODO:已去除此逻辑,推测应该没有此Bug。等IE环境好了后,再测试。 + 解决IE浏览器下的复选框半选状态BUG, + BUG表现:当复选框的状态为半选状态, + 点击复选框的时候IE浏览器会把复选框状态为 checked = false, + 而其他浏览器会直接变成checked = true, + http://www.xuebuyuan.com/1322692.html + 解决方案:绑定点击事件,手动更改checked状态,同时更新子复选框选中状态。 + */ diff --git a/src/checkbox/lib/src/TiCheckitemComponent.ts b/src/checkbox/lib/src/TiCheckitemComponent.ts new file mode 100644 index 0000000..1ecc360 --- /dev/null +++ b/src/checkbox/lib/src/TiCheckitemComponent.ts @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ElementRef, + HostListener, + Input, + IterableDiffer, + IterableDiffers, + Renderer2, + SimpleChanges, + ChangeDetectorRef, + ChangeDetectionStrategy +} from '@angular/core'; +import { TiCheckboxComponent } from './TiCheckboxComponent'; +import { ObservableMap, ObservableSet, Util } from '@opentiny/ng-utils'; + +/** + * Checkitem多选选项组件,尽管这是一个组件,使用时像属性指令。 + * + */ +@Component({ + selector: '[tiCheckitem]', + templateUrl: './checkbox.html', + styleUrls: ['./checkbox.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + tiCheckbox: '' // 给宿主元素添加tiCheckbox属性,实现定制样式 + } +}) +export class TiCheckitemComponent extends TiCheckboxComponent { + /** + * @ignore + * 全局item数据(多个group的item都在这里) + */ + public static globeCheckedMap: ObservableMap = new ObservableMap(); + /** + * @ignore + * 全局灰化禁用数据(多个group的item都在这里) + */ + public static globeDisabledSet: ObservableSet = new ObservableSet(); + /** + * 只使用 TiCheckitem 时的所有数据 + */ + @Input() item: any; + /** + * 只使用 TiCheckitem 时,配置选中数据 + */ + @Input() beCheckeds: Set | Array; + /** + * 指定选中项数据的键值 + */ + @Input() valueKey: string; + private beCheckedSet: ObservableSet; // 用于存储选中的数据项。如果未配置valueKey,则存储的是整个数据项;如果指定valueKey,则存储的值基于选中项。 + private beCheckedsDiffer: IterableDiffer; + constructor( + protected hostRef: ElementRef, + protected renderer: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + protected iterableDiffers: IterableDiffers + ) { + // 是因为group item两个子类都用到differs,所以放在父类这里。 + super(hostRef, renderer, changeDetectorRef); + } + /** + * @ignore + * 监听change事件,会多触发一次DoCheck。 //TODO: 不让这条HostListener('change', ['$event'])在文档显示 + * @param checked + * @returns + */ + @HostListener('change', ['$event']) public onHostChange(checked: any): void { + // item checked同步到globeCheckedSet + if (!this.item) { + return; + } + + this.nativeElement.checked + ? TiCheckitemComponent.globeCheckedMap.set(this.item, true, this) + : this.nativeElement.indeterminate + ? TiCheckitemComponent.globeCheckedMap.set(this.item, null, this) + : TiCheckitemComponent.globeCheckedMap.set(this.item, false, this); + // item checked同步到beCheckedSet + if (this.beCheckedSet) { + const value: any = this.getValue(this.item); + this.nativeElement.checked ? this.beCheckedSet.add(value) : this.beCheckedSet.delete(value); + } + } + + ngOnInit(): void { + super.ngOnInit(); + this.initBeCheckedSet(); + if (!this.item) { + // 用户忘了设定,或者继承的子类无需设定 + return; + } + this.initGlobeCheckedSetItem(); + this.setCheckedItem(); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + // 外部直接给item重新赋值了。 + if (changes['item'] && !changes['item'].isFirstChange()) { + // 删除已使用的静态资源。当然,这里有可能误删除。因为此item还在别处使用。 全选按钮那里,有纠正机制。 + this.deleteGlobeRef(changes['item'].previousValue); + // 添加新的资源 + this.setCheckedItem(); + // 重新设置disabled + this.setDisabledStateGlobe(); + } + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + // 删除已使用的静态资源 + this.deleteGlobeRef(this.item); + // 解除监听 + TiCheckitemComponent.globeCheckedMap.removeObserver(this.globeCheckedMapObserverItemFn); + } + + // 删除已使用的静态资源 + private deleteGlobeRef(item: any): void { + TiCheckitemComponent.globeCheckedMap.delete(this.item); + TiCheckitemComponent.globeDisabledSet.delete(this.item); + } + + ngDoCheck(): void { + super.ngDoCheck(); + if (!this.beCheckedSet) { + return; + } + const beCheckedsChanges = this.beCheckedsDiffer.diff(this.beCheckeds); + if (beCheckedsChanges) { + // beCheckeds同步到beCheckedSet + beCheckedsChanges.forEachAddedItem((record) => { + this.beCheckedSet.add(record.item); + }); + beCheckedsChanges.forEachRemovedItem((record) => { + this.beCheckedSet.delete(record.item); + }); + } + } + private initBeCheckedSet(): void { + if (!this.beCheckeds) { + return; + } + this.beCheckedsDiffer = this.iterableDiffers.find(this.beCheckeds).create(null); + this.beCheckedSet = new ObservableSet(this.beCheckeds); // TODO:写共用的Array/Set转Set + this.beCheckedSet.addObserver((item: any, isAdd: boolean) => { + // beCheckedSet的变化同步到beCheckeds + if (this.beCheckeds instanceof Set) { + isAdd ? this.beCheckeds.add(item) : this.beCheckeds.delete(item); + } else if (this.beCheckeds instanceof Array) { + // TODO:写公用的Array remove函数 + const index: number = this.beCheckeds.indexOf(item); + if (isAdd && index === -1) { + this.beCheckeds.push(item); + } else if (!isAdd && index !== -1) { + this.beCheckeds.splice(index, 1); + } + } + // checkedSet通知到item checked + this.setCheckedItem(); + }); + } + /** + * globeCheckedSet同步到item checked + */ + private initGlobeCheckedSetItem(): void { + // 初始化时,不往globeCheckedSet放值,是因为group优先级更高。 + TiCheckitemComponent.globeCheckedMap.addObserver(this.globeCheckedMapObserverItemFn); + } + + private globeCheckedMapObserverItemFn = (item: any, value: any, isAdd: boolean, from: any) => { + if (from !== this && item === this.item && isAdd) { + // 从全局同步过来的,可能是半选 + const lastChecked = this.nativeElement.checked; + const lastIndeterminate = this.nativeElement.indeterminate === true ? true : false; // 将undefined也转为false。 + this.nativeElement.checked = value === true; + this.nativeElement.indeterminate = value === null; + if (this.nativeElement.checked !== lastChecked || this.nativeElement.indeterminate !== lastIndeterminate) { + Util.trigger(this.nativeElement, 'change'); // 必须主动触发change事件,否则ngModel不更新。 + } + } + }; + private setCheckedItem(): void { + if (!this.item) { + return; + } + const lastChecked: boolean = this.nativeElement.checked; + const value: any = this.getValue(this.item); + // 如果beCheckeds存在,优先级更高,以beCheckeds为准。 + this.nativeElement.checked = this.beCheckedSet ? this.beCheckedSet.has(value) : TiCheckitemComponent.globeCheckedMap.get(this.item); + // 这里应该不用考虑半选吧? + if (this.nativeElement.checked !== lastChecked) { + Util.trigger(this.nativeElement, 'change'); // 必须主动触发change事件,否则ngModel不更新。 + } + } + /** + * @ignore + */ + setDisabledState?(isDisabled: boolean): void { + super.setDisabledState(isDisabled); + this.setDisabledStateGlobe(); + } + + private setDisabledStateGlobe(): void { + this.nativeElement.disabled + ? TiCheckitemComponent.globeDisabledSet.add(this.item) + : TiCheckitemComponent.globeDisabledSet.delete(this.item); + } + /** + * 输入:某个数据项 + * 输出:当有valueKey,返回值基于valueKey;当没有valueKey时,返回输入值 + */ + protected getValue(item: any): any { + return this.valueKey ? item[this.valueKey] : item; + } +} diff --git a/src/checkbox/lib/src/checkbox.html b/src/checkbox/lib/src/checkbox.html new file mode 100644 index 0000000..ec08997 --- /dev/null +++ b/src/checkbox/lib/src/checkbox.html @@ -0,0 +1,14 @@ + + diff --git a/src/checkbox/lib/src/checkbox.less b/src/checkbox/lib/src/checkbox.less new file mode 100644 index 0000000..1e2c611 --- /dev/null +++ b/src/checkbox/lib/src/checkbox.less @@ -0,0 +1,226 @@ +@import '../../../themes/basic/base-all.less'; + +@checkbox-name: tiCheckbox; +@border-box-model: border-box; + +:host[@{checkbox-name}] + label { + --ti-checkbox-container-size: var(--ti-common-size-4x); + --ti-checkbox-partial-center-size: 6px; + --ti-checkbox-border-weight: var(--ti-common-border-weight-normal); + --ti-checkbox-partial-center-position: calc( + (var(--ti-checkbox-container-size) - var(--ti-checkbox-partial-center-size) - var(--ti-checkbox-border-weight) * 2) / 2 + ); + --ti-checkbox-timing-function-default: cubic-bezier(0.25, 0.1, 0.25, 1); + --ti-checkbox-checked-mark-size: 7px; +} + +// :hostinput 的写法只是编译器不认识,能正常运行 +:hostinput[type='checkbox'][@{checkbox-name}] { + // checkbox的input框不需要显示:checkbox的样式使用span/label定制,此处只使用input的点选功能 + // 之前设置为left: -9999px,虽然也能让元素不可见,但是当有横向滚动条时,元素聚焦会导致页面移向最左边 + display: none !important; + // 1.基础样式 + & + .ti3-checkbox { + display: inline-flex; + vertical-align: middle; + margin-bottom: 0; + line-height: 1em; + font-weight: normal; + cursor: pointer; + .user-select(); + .box-sizing(@border-box-model); + + // 复选框样式 + .ti3-checkbox-skin { + position: relative; + display: inline-block; + flex-shrink: 0; + margin-right: var(--ti-common-space-2x); + margin-bottom: 0; + height: var(--ti-checkbox-container-size); + width: var(--ti-checkbox-container-size); + line-height: calc(var(--ti-checkbox-container-size) - var(--ti-checkbox-border-weight) * 2); + background-color: var(--ti-common-color-bg-white-normal); + border: var(--ti-checkbox-border-weight) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + text-align: center; + color: transparent; + vertical-align: top; + // 如果继承父节点的border-collapse属性且为collapse,IE下对勾不显示 + // 场景:复选框用在formfield组件中对勾不显示;多选树在表格中对勾不显示(多选树测试用例) + border-collapse: separate; + .border-radius(var(--ti-common-border-radius-normal)); + .box-sizing(@border-box-model); + .transition (background-color; 0.15s, border-color; 0.15s); + } + + // 文本样式 + .ti3-checkbox-label { + margin-bottom: 0px; + display: inline-block; + color: var(--ti-common-color-text-primary); + line-height: var(--ti-checkbox-container-size); + .user-select(); + font-weight: var(--ti-common-font-weight-4); + font-size: var(--ti-common-font-size-base); + } + } + + // 2.非禁用状态样式:focus hover active等状态 + &:not([disabled]) { + // 2.1选中状态 + &:checked + .ti3-checkbox { + .ti3-checkbox-skin { + border-color: var(--ti-common-color-line-active); + background-color: var(--ti-common-color-bg-emphasize); + color: var(--ti-common-color-icon-white); + &:hover, + &:focus { + border-color: var(--ti-common-color-bg-hover); + background-color: var(--ti-common-color-bg-hover); + } + &:active { + border-color: var(--ti-common-color-bg-hover); + background-color: var(--ti-common-color-bg-hover); + } + } + } + + // 2.2非选中状态 + &:not(:checked) + .ti3-checkbox { + .ti3-checkbox-skin { + &:hover, + &:focus { + border-color: var(--ti-common-color-line-active); + } + &:active { + border-color: var(--ti-common-color-line-active); + background-color: var(--ti-common-color-bg-white-normal); + } + } + } + + // 2.3半选状态 + &:indeterminate + .ti3-checkbox { + .ti3-checkbox-skin { + background-color: var(--ti-common-color-bg-emphasize); + border-color: var(--ti-common-color-line-active); + &:before { + background-color: var(--ti-common-color-icon-white); + } + // 半选且Hover状态 + &:hover { + background-color: var(--ti-common-color-bg-hover); + border-color: var(--ti-common-color-bg-hover); + } + } + } + } + + // 3.禁用状态样式 + &[disabled] { + + .ti3-checkbox { + cursor: not-allowed; + .ti3-checkbox-skin { + outline: none; + } + .ti3-checkbox-label { + color: var(--ti-common-color-text-disabled); + } + } + + // 3.1 选中状态 + &:checked + .ti3-checkbox { + .ti3-checkbox-skin { + color: var(--ti-common-color-icon-disabled); + background-color: var(--ti-common-color-bg-disabled); + border-color: var(--ti-common-color-line-disabled); + } + } + + // 3.2 不选中状态 + &:not(:checked) + .ti3-checkbox { + .ti3-checkbox-skin { + color: transparent; + background-color: var(--ti-common-color-bg-disabled); + border-color: var(--ti-common-color-line-disabled); + } + } + + // 3.3 半选状态 + &:indeterminate + .ti3-checkbox { + .ti3-checkbox-skin { + &:before { + background-color: var(--ti-common-color-icon-disabled); + } + } + } + } + + //对勾(灰化/正常下的全选) + &:checked + .ti3-checkbox { + .ti3-checkbox-skin:before { + content: ' '; + display: table; + position: absolute; + .box-sizing(@border-box-model); + top: 50%; + left: 21%; + width: var(--ti-checkbox-checked-mark-size); + height: calc(var(--ti-checkbox-checked-mark-size) * (9 / 7)); // 按照比例计算 9 / 7; + border-width: 0 2px 2px 0; + border-style: solid; + transform: rotate(45deg) translate(-55%, -55%) scale(1); + } + } + + // 内部小块儿(灰化/正常下的半选) + &:indeterminate + .ti3-checkbox { + .ti3-checkbox-skin:before { + content: ''; + position: absolute; + top: var(--ti-checkbox-partial-center-position); + left: var(--ti-checkbox-partial-center-position); + width: var(--ti-checkbox-partial-center-size); + height: var(--ti-checkbox-partial-center-size); + .border-radius(1px); + } + } +} + +/*******************************************动效************************************************/ +/*选中时tick对号及半选时的内部小方块动效*/ +@keyframes rect-selectedAnimate { + from { + transform: scale(0); + transform-origin: center; + } + to { + color: var(--ti-common-color-text-white); + transform: scale(1); + transform-origin: center; + } +} +@keyframes tick-selectedAnimate { + from { + transform: rotate(45deg) translate(-55%, -55%) scale(0); + transform-origin: center; + } + to { + color: var(--ti-common-color-text-white); + transform: rotate(45deg) translate(-55%, -55%) scale(1); + transform-origin: center; + } +} + +/*动效*/ +input[type='checkbox'][@{checkbox-name}]:not(:disabled):checked + .ti3-checkbox .ti3-icon-checkmark:before { + animation: tick-selectedAnimate 0.2s var(--ti-checkbox-timing-function-default) forwards; +} +input[type='checkbox'][@{checkbox-name}]:not(:disabled):indeterminate + .ti3-checkbox .ti3-icon-checkmark:before { + animation: rect-selectedAnimate 0.2s var(--ti-checkbox-timing-function-default) forwards; +} + +/*选中状态hover动画*/ +input[type='checkbox'][@{checkbox-name}]:not(:disabled):checked + .ti3-checkbox .ti3-checkbox-skin:hover { + .transition(border-color; 0.2s; var(--ti-checkbox-timing-function-default), background-color; 0.2s; var(--ti-checkbox-timing-function-default)); +} diff --git a/src/checkbox/lib/src/checkboxgroup.html b/src/checkbox/lib/src/checkboxgroup.html new file mode 100644 index 0000000..e19521c --- /dev/null +++ b/src/checkbox/lib/src/checkboxgroup.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/src/checkbox/lib/src/checkboxgroup.less b/src/checkbox/lib/src/checkboxgroup.less new file mode 100644 index 0000000..3e082fc --- /dev/null +++ b/src/checkbox/lib/src/checkboxgroup.less @@ -0,0 +1,33 @@ +ti-checkbox-group { + display: inline-flex; + flex-wrap: wrap; + line-height: 1.5em; + // vertical-align的默认值是baseline,ti-checkbox-group在横向排列且不换行时,勾选第一个,会导致行高变高 + vertical-align: bottom; + .ti3-checkbox { + /** + * 加important是为了覆盖checkbox.less 关于.ti3-checkbox的对应样式, + * 但是直接在checkbox.less中修改会导致勾选时行高增高,页面抖动, + * 父元素为flex布局不会抖动,radio点选也不会,抖动和checkbox的对勾(伪元素)有关 + */ + display: inline-flex !important; + align-items: flex-start !important; + flex-shrink: 0; + line-height: 1.5em !important; + } + &.ti-checkboxgroup-vertical { + flex-direction: column; + input[type='checkbox'][tiCheckbox] + .ti3-checkbox, + .checkbox-group-item { + line-height: var(--ti-common-size-7x) !important; + flex-shrink: 0; + } + } +} +.checkbox-group-item { + display: inline-flex; +} + +ti-checkbox-group.ti-checkboxgroup-horizon.ti-checkboxgroup-defalut-item label.ti3-checkbox { + margin-right: var(--ti-common-space-5x); +} diff --git a/src/collapse/demo/karma.conf.js b/src/collapse/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/collapse/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/collapse/demo/project.json b/src/collapse/demo/project.json new file mode 100644 index 0000000..fad5f63 --- /dev/null +++ b/src/collapse/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/collapse/demo", + "sourceRoot": "src/collapse/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/collapse", + "index": "src/collapse/demo/src/index.html", + "main": "src/collapse/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/collapse/demo/tsconfig.app.json", + "assets": ["src/collapse/demo/src/favicon.ico", "src/collapse/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "collapse-demo:build:production" + }, + "development": { + "browserTarget": "collapse-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js collapse" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/collapse/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/collapse/demo/tsconfig.spec.json", + "karmaConfig": "src/collapse/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/collapse/demo/src/app/AppComponent.ts b/src/collapse/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/collapse/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/collapse/demo/src/app/AppModule.ts b/src/collapse/demo/src/app/AppModule.ts new file mode 100644 index 0000000..dc072de --- /dev/null +++ b/src/collapse/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { CollapseTestModule } from './collapse/CollapseTestModule'; + +@NgModule({ + imports: [ + CollapseTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/collapse/demo/src/app/IndexComponent.ts b/src/collapse/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..532869a --- /dev/null +++ b/src/collapse/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { CollapseTestModule } from './collapse/CollapseTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = CollapseTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/collapse/demo/src/app/app.html b/src/collapse/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/collapse/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/collapse/demo/src/app/collapse/CollapseBasicComponent.ts b/src/collapse/demo/src/app/collapse/CollapseBasicComponent.ts new file mode 100644 index 0000000..84ca84b --- /dev/null +++ b/src/collapse/demo/src/app/collapse/CollapseBasicComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './collapse-basic.html', + styleUrls: ['./collapse.less'] +}) +export class CollapseBasicComponent { + collapsed: boolean = false; + change(): void { + this.collapsed = !this.collapsed; + } +} diff --git a/src/collapse/demo/src/app/collapse/CollapseEventComponent.ts b/src/collapse/demo/src/app/collapse/CollapseEventComponent.ts new file mode 100644 index 0000000..30370c4 --- /dev/null +++ b/src/collapse/demo/src/app/collapse/CollapseEventComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './collapse-event.html', + styleUrls: ['./collapse.less'] +}) +export class CollapseEventComponent { + collapsed: boolean = false; + myLogs: Array = []; + + onToggleDone(event: boolean): void { + this.myLogs = [...this.myLogs, `onToggleDone() event=${event}`]; + } + + change(): void { + this.collapsed = !this.collapsed; + } +} diff --git a/src/collapse/demo/src/app/collapse/CollapseTestModule.ts b/src/collapse/demo/src/app/collapse/CollapseTestModule.ts new file mode 100644 index 0000000..e4d63f0 --- /dev/null +++ b/src/collapse/demo/src/app/collapse/CollapseTestModule.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiCollapseModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { CollapseBasicComponent } from './CollapseBasicComponent'; +import { CollapseEventComponent } from './CollapseEventComponent'; + +@NgModule({ + imports: [CommonModule, TiCollapseModule, TiButtonModule, DemoLogModule, RouterModule.forChild(CollapseTestModule.ROUTES)], + declarations: [CollapseBasicComponent, CollapseEventComponent] +}) +export class CollapseTestModule { + static readonly LINKS: Array = [{ href: 'directives/TiCollapseDirective.html', label: 'Collapse' }]; + static readonly ROUTES: Routes = [ + { + path: 'collapse/collapse-basic', + component: CollapseBasicComponent + }, + { + path: 'collapse/collapse-event', + component: CollapseEventComponent + } + ]; +} diff --git a/src/collapse/demo/src/app/collapse/collapse-basic.html b/src/collapse/demo/src/app/collapse/collapse-basic.html new file mode 100644 index 0000000..5f269e6 --- /dev/null +++ b/src/collapse/demo/src/app/collapse/collapse-basic.html @@ -0,0 +1,8 @@ +
+
+

通过折叠面板收纳内容区域。

+

此组件中使用了Angular动画,需要引入 BrowserAnimationsModule。

+

建议在根模块引入 BrowserAnimationsModule。

+
+
+ diff --git a/src/collapse/demo/src/app/collapse/collapse-event.html b/src/collapse/demo/src/app/collapse/collapse-event.html new file mode 100644 index 0000000..e1499b8 --- /dev/null +++ b/src/collapse/demo/src/app/collapse/collapse-event.html @@ -0,0 +1,11 @@ +
+
+

通过折叠面板收纳内容区域。

+

此组件中使用了 Angular 动画,需要引入 BrowserAnimationsModule。

+

建议在根模块引入 BrowserAnimationsModule。

+

当面板状态改变的时候触发 toggleDone 事件。

+

传递出去的参数为 boolean 类型,表示当前面板是否为收起状态。

+
+
+ + diff --git a/src/collapse/demo/src/app/collapse/collapse.less b/src/collapse/demo/src/app/collapse/collapse.less new file mode 100644 index 0000000..5ccdf98 --- /dev/null +++ b/src/collapse/demo/src/app/collapse/collapse.less @@ -0,0 +1,6 @@ +.collapse-panel { + padding: 10px; + border-radius: 2px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; +} diff --git a/src/collapse/demo/src/app/collapse/webdoc/collapse-demos.js b/src/collapse/demo/src/app/collapse/webdoc/collapse-demos.js new file mode 100644 index 0000000..adcb4fb --- /dev/null +++ b/src/collapse/demo/src/app/collapse/webdoc/collapse-demos.js @@ -0,0 +1,30 @@ +export default { + column: '2', + demos: [ + { + demoId: 'collapse-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': '

Collapse 组件的最简用法。

', + 'en-US': '

Basic usage

', + }, + apis: ['TiCollapseDirective.properties.tiCollapse'], + }, + { + demoId: 'collapse-event', + name: { + 'zh-CN': '状态改变事件', + 'en-US': 'Change event', + }, + desc: { + 'zh-CN': + '

当面板状态改变的时候触发toggleDone事件。传递出去的参数为 boolean 类型,表示当前面板是否为收起状态。

', + 'en-US': '

Change even

', + }, + apis: ['TiCollapseDirective.events.toggleDone'], + }, + ], +}; diff --git a/src/collapse/demo/src/app/collapse/webdoc/collapse.cn.md b/src/collapse/demo/src/app/collapse/webdoc/collapse.cn.md new file mode 100644 index 0000000..7c2fd86 --- /dev/null +++ b/src/collapse/demo/src/app/collapse/webdoc/collapse.cn.md @@ -0,0 +1,34 @@ +--- +title: Collapse 折叠面板 +--- +# Collapse 折叠面板 + +
+ +通过折叠面板收纳内容区域。 + +```typescript +import { TiCollapseModule } from '@opentiny/ng'; +``` + +需要在项目中(建议在根模块)中导入`BrowserAnimationsModule`。 + +```typescript +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +``` +
+ +
+ +通过折叠面板收纳内容区域。 + +```typescript +import { TiCollapseModule } from '@opentiny/ng'; +``` + +需要在项目中(建议在根模块)中导入`BrowserAnimationsModule`。 + +```typescript +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +``` +
diff --git a/src/collapse/demo/src/app/collapse/webdoc/collapse.en.md b/src/collapse/demo/src/app/collapse/webdoc/collapse.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/collapse/demo/src/app/collapse/webdoc/collapse.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/collapse/demo/src/favicon.ico b/src/collapse/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/collapse/demo/src/index.html b/src/collapse/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/collapse/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/collapse/demo/src/main.ts b/src/collapse/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/collapse/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/collapse/demo/test.ts b/src/collapse/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/collapse/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/collapse/demo/tsconfig.app.json b/src/collapse/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/collapse/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/collapse/demo/tsconfig.spec.json b/src/collapse/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/collapse/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/collapse/lib/index.ts b/src/collapse/lib/index.ts new file mode 100644 index 0000000..65112f9 --- /dev/null +++ b/src/collapse/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiCollapseModule'; diff --git a/src/collapse/lib/ng-package.json b/src/collapse/lib/ng-package.json new file mode 100644 index 0000000..2b899ff --- /dev/null +++ b/src/collapse/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/collapse", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/collapse/lib/package.json b/src/collapse/lib/package.json new file mode 100644 index 0000000..6487d00 --- /dev/null +++ b/src/collapse/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-collapse", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/animations": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/collapse/lib/project.json b/src/collapse/lib/project.json new file mode 100644 index 0000000..21a7651 --- /dev/null +++ b/src/collapse/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/collapse/lib", + "sourceRoot": "src/collapse/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/collapse"], + "options": { + "project": "src/collapse/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/collapse"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js collapse" + }, + { + "command": "ng default-build collapse" + }, + { + "command": "node build/clear-default-theme.js collapse" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/collapse && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build collapse && ng pack collapse && node build/publish.js collapse --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/collapse/lib/src/TiCollapseDirective.ts b/src/collapse/lib/src/TiCollapseDirective.ts new file mode 100644 index 0000000..85b8c45 --- /dev/null +++ b/src/collapse/lib/src/TiCollapseDirective.ts @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Directive, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + Renderer2, + SimpleChange, + SimpleChanges +} from '@angular/core'; +import { animate, AnimationBuilder, AnimationPlayer, style } from '@angular/animations'; +import { TiBrowser } from '@opentiny/ng-utils'; +/** + * TiCollapseDirective指令主要功能为动态控制某一DOM节点的显示(展开)与隐藏(收起) + * + *

使用此组件时需要开发者在项目模块(建议在根模块) + * 中引入BrowserAnimationsModule。这是因为此组件中使用了Angular动画,需要引入BrowserAnimationsModule, + * 但是 BrowserAnimationsModule 不能在懒加载模块被重复引入,所以需要开发者来引入BrowserAnimationsModule,保证其引入一次。

+ * + */ +@Directive({ + selector: '[tiCollapse]' +}) +export class TiCollapseDirective implements OnInit, OnChanges, OnDestroy { + private static ANIMATE_IN: string = '600ms cubic-bezier(0.755, 0.05, 0.855, 0.06)'; // easeInQuint + private static ANIMATE_OUT: string = '600ms cubic-bezier(0.23, 1, 0.32, 1)'; // easeOutQuint + /** + * 控制折叠面板的收起/展开 + */ + @Input('tiCollapse') isCollapse: boolean = true; // 是否收起 + /** + * 折叠面板在收起/展开结束后触发的回调,参数为面板是否收起 + */ + @Output() readonly toggleDone: EventEmitter = new EventEmitter(); + private element: HTMLElement; + private expandPlayer: AnimationPlayer; + private collapsePlayer: AnimationPlayer; + + constructor(private elementRef: ElementRef, private renderer: Renderer2, private builder: AnimationBuilder) { + this.element = this.elementRef.nativeElement; + } + + ngOnInit(): void { + if (!TiBrowser.isIE() || TiBrowser.version() > 9) { + // 创建展开时动效的实例 + this.expandPlayer = this.builder + .build([ + style({ 'max-height': '0px', overflow: 'hidden' }), + animate(TiCollapseDirective.ANIMATE_IN, style({ 'max-height': '2999px', overflow: 'visible' })) + ]) + .create(this.element); + + // 创建收起时动效的实例 + this.collapsePlayer = this.builder + .build([ + style({ 'max-height': '2999px', overflow: 'visible' }), + animate(TiCollapseDirective.ANIMATE_OUT, style({ 'max-height': '0px', overflow: 'hidden' })) + ]) + .create(this.element); + + // 初始没有动画效果 + if (this.isCollapse) { + this.collapsePlayer.finish(); + } else { + this.expandPlayer.finish(); + } + + // 收起动画结束后的回调 + this.collapsePlayer.onDone(() => { + // 设置元素display:none必须在收起动画结束后,否则没有动画效果 + this.collapse(); + this.toggleDone.emit(this.isCollapse); + }); + + // 展开动画结束后的回调 + this.expandPlayer.onDone(() => { + this.toggleDone.emit(this.isCollapse); + }); + } + + if (this.isCollapse) { + this.collapse(); + } else { + this.expand(); + } + } + + ngOnChanges(changes: SimpleChanges): void { + const isCollapseObj: SimpleChange = changes['isCollapse']; + + if (!isCollapseObj || isCollapseObj.firstChange) { + return; + } + + if (TiBrowser.isIE() && TiBrowser.version() === 9) { + if (isCollapseObj.currentValue) { + this.collapse(); + } else { + this.expand(); + } + this.toggleDone.emit(isCollapseObj.currentValue); + + return; + } + + if (isCollapseObj.currentValue) { + // 执行收起动画前一定要重置展开动画,否则会影响收起动画 + this.expandPlayer.reset(); + // 执行收起动画 + this.collapsePlayer.play(); + } else { + // 执行展开动画前一定要重置收起动画,否则会影响展开动画 + this.collapsePlayer.reset(); + // 设置元素display:block + this.expand(); + // 执行展开动画 + this.expandPlayer.play(); + } + } + + ngOnDestroy(): void { + if (this.expandPlayer) { + this.expandPlayer.destroy(); + } + if (this.collapsePlayer) { + this.collapsePlayer.destroy(); + } + } + + private expand(): void { + this.renderer.setStyle(this.element, 'display', 'block'); + } + + private collapse(): void { + this.renderer.setStyle(this.element, 'display', 'none'); + } +} diff --git a/src/collapse/lib/src/TiCollapseModule.ts b/src/collapse/lib/src/TiCollapseModule.ts new file mode 100644 index 0000000..b8a33bd --- /dev/null +++ b/src/collapse/lib/src/TiCollapseModule.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiCollapseDirective } from './TiCollapseDirective'; + +@NgModule({ + imports: [CommonModule], + exports: [TiCollapseDirective], + declarations: [TiCollapseDirective] +}) +export class TiCollapseModule {} +export { TiCollapseDirective } from './TiCollapseDirective'; diff --git a/src/collapsebox/demo/karma.conf.js b/src/collapsebox/demo/karma.conf.js new file mode 100644 index 0000000..8c5378a --- /dev/null +++ b/src/collapsebox/demo/karma.conf.js @@ -0,0 +1,41 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma'), + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false, // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true, // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [{ type: 'html' }, { type: 'text-summary' }], + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true, + }); +}; diff --git a/src/collapsebox/demo/project.json b/src/collapsebox/demo/project.json new file mode 100644 index 0000000..f2eefe7 --- /dev/null +++ b/src/collapsebox/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/collapsebox/demo", + "sourceRoot": "src/collapsebox/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/collapsebox", + "index": "src/collapsebox/demo/src/index.html", + "main": "src/collapsebox/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/collapsebox/demo/tsconfig.app.json", + "assets": ["src/collapsebox/demo/src/favicon.ico", "src/collapsebox/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "collapsebox-demo:build:production" + }, + "development": { + "browserTarget": "collapsebox-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js collapsebox" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/collapsebox/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/collapsebox/demo/tsconfig.spec.json", + "karmaConfig": "src/collapsebox/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/collapsebox/demo/src/app/AppComponent.ts b/src/collapsebox/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/collapsebox/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/collapsebox/demo/src/app/AppModule.ts b/src/collapsebox/demo/src/app/AppModule.ts new file mode 100644 index 0000000..a550115 --- /dev/null +++ b/src/collapsebox/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { CollapseboxTestModule } from './collapsebox/CollapseboxTestModule'; + +@NgModule({ + imports: [ + CollapseboxTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/collapsebox/demo/src/app/IndexComponent.ts b/src/collapsebox/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..803fbff --- /dev/null +++ b/src/collapsebox/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { CollapseboxTestModule } from './collapsebox/CollapseboxTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = CollapseboxTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/collapsebox/demo/src/app/app.html b/src/collapsebox/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/collapsebox/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/collapsebox/demo/src/app/collapsebox/CollapseboxBasicComponent.ts b/src/collapsebox/demo/src/app/collapsebox/CollapseboxBasicComponent.ts new file mode 100644 index 0000000..f35fc51 --- /dev/null +++ b/src/collapsebox/demo/src/app/collapsebox/CollapseboxBasicComponent.ts @@ -0,0 +1,7 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './collapsebox-basic.html', + encapsulation: ViewEncapsulation.None +}) +export class CollapseboxBasicComponent {} diff --git a/src/collapsebox/demo/src/app/collapsebox/CollapseboxCloseableComponent.ts b/src/collapsebox/demo/src/app/collapsebox/CollapseboxCloseableComponent.ts new file mode 100644 index 0000000..6228ecc --- /dev/null +++ b/src/collapsebox/demo/src/app/collapsebox/CollapseboxCloseableComponent.ts @@ -0,0 +1,7 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './collapsebox-closeable.html', + encapsulation: ViewEncapsulation.None +}) +export class CollapseboxCloseableComponent {} diff --git a/src/collapsebox/demo/src/app/collapsebox/CollapseboxEventComponent.ts b/src/collapsebox/demo/src/app/collapsebox/CollapseboxEventComponent.ts new file mode 100644 index 0000000..ff7cd60 --- /dev/null +++ b/src/collapsebox/demo/src/app/collapsebox/CollapseboxEventComponent.ts @@ -0,0 +1,17 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './collapsebox-event.html', + encapsulation: ViewEncapsulation.None +}) +export class CollapseboxEventComponent { + myLogs: Array = []; + show: boolean = true; + onClose() { + this.show = false; + this.myLogs = [...this.myLogs, `onClose()`]; + } + onClickToggle() { + this.show = true; + } +} diff --git a/src/collapsebox/demo/src/app/collapsebox/CollapseboxTestModule.ts b/src/collapsebox/demo/src/app/collapsebox/CollapseboxTestModule.ts new file mode 100644 index 0000000..043e750 --- /dev/null +++ b/src/collapsebox/demo/src/app/collapsebox/CollapseboxTestModule.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiCollapseboxModule, TiButtonModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { CollapseboxBasicComponent } from './CollapseboxBasicComponent'; +import { CollapseboxCloseableComponent } from './CollapseboxCloseableComponent'; +import { CollapseboxEventComponent } from './CollapseboxEventComponent'; + +@NgModule({ + imports: [CommonModule, TiCollapseboxModule, TiButtonModule, DemoLogModule, RouterModule.forChild(CollapseboxTestModule.ROUTES)], + declarations: [CollapseboxBasicComponent, CollapseboxCloseableComponent, CollapseboxEventComponent] +}) +export class CollapseboxTestModule { + static readonly LINKS: Array = [{ label: 'Collapsebox' }]; + public static readonly ROUTES: Routes = [ + { + path: 'collapsebox/collapsebox-basic', + component: CollapseboxBasicComponent + }, + { + path: 'collapsebox/collapsebox-closeable', + component: CollapseboxCloseableComponent + }, + { + path: 'collapsebox/collapsebox-event', + component: CollapseboxEventComponent + } + ]; +} diff --git a/src/collapsebox/demo/src/app/collapsebox/collapsebox-basic.html b/src/collapsebox/demo/src/app/collapsebox/collapsebox-basic.html new file mode 100644 index 0000000..ac218f4 --- /dev/null +++ b/src/collapsebox/demo/src/app/collapsebox/collapsebox-basic.html @@ -0,0 +1,5 @@ +
+ +
content
+
+
diff --git a/src/collapsebox/demo/src/app/collapsebox/collapsebox-closeable.html b/src/collapsebox/demo/src/app/collapsebox/collapsebox-closeable.html new file mode 100644 index 0000000..f12d897 --- /dev/null +++ b/src/collapsebox/demo/src/app/collapsebox/collapsebox-closeable.html @@ -0,0 +1,6 @@ +
+ +

closeable 设置为false

+

collapsebox组件通过配置closeable属性为false,将关闭按钮隐藏,设置为true或不设置时正常显示。

+
+
diff --git a/src/collapsebox/demo/src/app/collapsebox/collapsebox-event.html b/src/collapsebox/demo/src/app/collapsebox/collapsebox-event.html new file mode 100644 index 0000000..4b0ca4b --- /dev/null +++ b/src/collapsebox/demo/src/app/collapsebox/collapsebox-event.html @@ -0,0 +1,8 @@ +
+ +

collapsebox content

+

collapsebox组件通过事件close,在面板被关闭时,触发相应回调。

+
+
+ + diff --git a/src/collapsebox/demo/src/app/collapsebox/webdoc/collapsebox-demos.js b/src/collapsebox/demo/src/app/collapsebox/webdoc/collapsebox-demos.js new file mode 100644 index 0000000..dd7727a --- /dev/null +++ b/src/collapsebox/demo/src/app/collapsebox/webdoc/collapsebox-demos.js @@ -0,0 +1,40 @@ +export default { + column: '2', + demos: [ + { + demoId: 'collapsebox-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': '

Collapsebox 组件的最简用法。

', + 'en-US': '

Basic usage

', + }, + }, + { + demoId: 'collapsebox-closeable', + name: { + 'zh-CN': '控制关闭按钮显示隐藏', + 'en-US': '', + }, + desc: { + 'zh-CN': '

通过属性closeable配置关闭按钮是否开启。

', + 'en-US': '

', + }, + apis: ['TiCollapseboxComponent.properties.closeable'], + }, + { + demoId: 'collapsebox-event', + name: { + 'zh-CN': '事件', + 'en-US': 'Change event', + }, + desc: { + 'zh-CN': '

当面板关闭的时候触发close事件。

', + 'en-US': '

Change even

', + }, + apis: ['TiCollapseboxComponent.events.close'], + }, + ], +}; diff --git a/src/collapsebox/demo/src/app/collapsebox/webdoc/collapsebox.cn.md b/src/collapsebox/demo/src/app/collapsebox/webdoc/collapsebox.cn.md new file mode 100644 index 0000000..6b88a27 --- /dev/null +++ b/src/collapsebox/demo/src/app/collapsebox/webdoc/collapsebox.cn.md @@ -0,0 +1,24 @@ +--- +title: Collapsebox 折叠框 +--- +# Collapsebox 折叠框 + +
+ +Collapsebox 折叠框组件。   + +```typescript +import { TiCollapseboxModule } from '@opentiny/ng'; +``` + +
+ +
+ +Collapsebox 折叠框组件。   + +```typescript +import { TiCollapseboxModule } from '@opentiny/ng'; +``` + +
diff --git a/src/collapsebox/demo/src/app/collapsebox/webdoc/collapsebox.en.md b/src/collapsebox/demo/src/app/collapsebox/webdoc/collapsebox.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/collapsebox/demo/src/app/collapsebox/webdoc/collapsebox.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/collapsebox/demo/src/favicon.ico b/src/collapsebox/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/collapsebox/demo/src/index.html b/src/collapsebox/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/collapsebox/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/collapsebox/demo/src/main.ts b/src/collapsebox/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/collapsebox/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/collapsebox/demo/test.ts b/src/collapsebox/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/collapsebox/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/collapsebox/demo/tsconfig.app.json b/src/collapsebox/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/collapsebox/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/collapsebox/demo/tsconfig.spec.json b/src/collapsebox/demo/tsconfig.spec.json new file mode 100644 index 0000000..e12f108 --- /dev/null +++ b/src/collapsebox/demo/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": ["jasmine"] + }, + "files": ["./test.ts", "../../polyfills.ts"], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/collapsebox/lib/index.ts b/src/collapsebox/lib/index.ts new file mode 100644 index 0000000..99963e7 --- /dev/null +++ b/src/collapsebox/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiCollapseboxModule'; diff --git a/src/collapsebox/lib/ng-package.json b/src/collapsebox/lib/ng-package.json new file mode 100644 index 0000000..dc857d0 --- /dev/null +++ b/src/collapsebox/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/collapsebox", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/collapsebox/lib/package.json b/src/collapsebox/lib/package.json new file mode 100644 index 0000000..acee3d4 --- /dev/null +++ b/src/collapsebox/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-collapsebox", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-collapse": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/collapsebox/lib/project.json b/src/collapsebox/lib/project.json new file mode 100644 index 0000000..18426dd --- /dev/null +++ b/src/collapsebox/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/collapsebox/lib", + "sourceRoot": "src/collapsebox/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/collapsebox"], + "options": { + "project": "src/collapsebox/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/collapsebox"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js collapsebox" + }, + { + "command": "ng default-build collapsebox" + }, + { + "command": "node build/clear-default-theme.js collapsebox" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/collapsebox && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build collapsebox && ng pack collapsebox && node build/publish.js collapsebox --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/collapsebox/lib/src/TiCollapseboxComponent.ts b/src/collapsebox/lib/src/TiCollapseboxComponent.ts new file mode 100644 index 0000000..a06df8b --- /dev/null +++ b/src/collapsebox/lib/src/TiCollapseboxComponent.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +/** + * 折叠框组件 collapsebox + * + * collapsebox组件是一个带有关闭操作的容器类组件,服务可自定义内容。 + * + */ +@Component({ + selector: 'ti-collapsebox', + templateUrl: './collapsebox.html', + styleUrls: ['./collapsebox.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiCollapseboxComponent extends TiBaseComponent { + /** + * @ignore + * 设置折叠框默认展开 + */ + collapsed: boolean = false; + /** + * 控制关闭按钮的显示/隐藏 + */ + @Input() closeable: boolean = true; + /** + * 折叠框在关闭时触发的回调 + */ + @Output() readonly close: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + public onClickClose(event: Event): void { + if (this.close.observers.length > 0) { + this.close.emit(); + } else { + this.collapsed = true; + } + } +} diff --git a/src/collapsebox/lib/src/TiCollapseboxModule.ts b/src/collapsebox/lib/src/TiCollapseboxModule.ts new file mode 100644 index 0000000..b58a34c --- /dev/null +++ b/src/collapsebox/lib/src/TiCollapseboxModule.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { NgModule } from '@angular/core'; + +import { TiCollapseModule } from '@opentiny/ng-collapse'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; + +import { TiCollapseboxComponent } from './TiCollapseboxComponent'; + +@NgModule({ + imports: [TiCollapseModule, TiIconModule, TiOutlineModule], + exports: [TiCollapseboxComponent], + declarations: [TiCollapseboxComponent] +}) +export class TiCollapseboxModule {} +export { TiCollapseboxComponent } from './TiCollapseboxComponent'; diff --git a/src/collapsebox/lib/src/collapsebox.html b/src/collapsebox/lib/src/collapsebox.html new file mode 100644 index 0000000..41fd00b --- /dev/null +++ b/src/collapsebox/lib/src/collapsebox.html @@ -0,0 +1,13 @@ +
+ + +
diff --git a/src/collapsebox/lib/src/collapsebox.less b/src/collapsebox/lib/src/collapsebox.less new file mode 100644 index 0000000..e424917 --- /dev/null +++ b/src/collapsebox/lib/src/collapsebox.less @@ -0,0 +1,23 @@ +// @import "../../themes/basic/base-.allless"; + +::ng-deep :root { + --ti3-collapsebox-close-position: 20px; +} + +.ti3-collapsebox-container { + display: block; + background: var(--ti-common-color-bg-white-normal); + padding: var(--ti-common-space-6x) var(--ti-common-space-8x); + box-shadow: var(--ti-common-shadow-1-down); + position: relative; +} + +.ti3-collapsebox-close { + cursor: pointer; + position: absolute; + top: var(--ti3-collapsebox-close-position); + right: var(--ti3-collapsebox-close-position); + &:hover { + color: var(--ti-common-color-icon-hover); + } +} diff --git a/src/collapsebutton/demo/karma.conf.js b/src/collapsebutton/demo/karma.conf.js new file mode 100644 index 0000000..22d9d1a --- /dev/null +++ b/src/collapsebutton/demo/karma.conf.js @@ -0,0 +1,41 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [{ type: 'html' }, { type: 'text-summary' }] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/collapsebutton/demo/project.json b/src/collapsebutton/demo/project.json new file mode 100644 index 0000000..8b999fb --- /dev/null +++ b/src/collapsebutton/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/collapsebutton/demo", + "sourceRoot": "src/collapsebutton/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/collapsebutton", + "index": "src/collapsebutton/demo/src/index.html", + "main": "src/collapsebutton/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/collapsebutton/demo/tsconfig.app.json", + "assets": ["src/collapsebutton/demo/src/favicon.ico", "src/collapsebutton/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "collapsebutton-demo:build:production" + }, + "development": { + "browserTarget": "collapsebutton-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js collapsebutton" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/collapsebutton/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/collapsebutton/demo/tsconfig.spec.json", + "karmaConfig": "src/collapsebutton/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/collapsebutton/demo/src/app/AppComponent.ts b/src/collapsebutton/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/collapsebutton/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/collapsebutton/demo/src/app/AppModule.ts b/src/collapsebutton/demo/src/app/AppModule.ts new file mode 100644 index 0000000..7d4913c --- /dev/null +++ b/src/collapsebutton/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { CollapsebuttonTestModule } from './collapsebutton/CollapsebuttonTestModule'; + +@NgModule({ + imports: [ + CollapsebuttonTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/collapsebutton/demo/src/app/IndexComponent.ts b/src/collapsebutton/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..c60f334 --- /dev/null +++ b/src/collapsebutton/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { CollapsebuttonTestModule } from './collapsebutton/CollapsebuttonTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = CollapsebuttonTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/collapsebutton/demo/src/app/app.html b/src/collapsebutton/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/collapsebutton/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonBasicComponent.ts b/src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonBasicComponent.ts new file mode 100644 index 0000000..e624700 --- /dev/null +++ b/src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './collapsebutton-basic.html' +}) +export class CollapsebuttonBasicComponent { + collapsed: boolean = true; +} diff --git a/src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonCustomtextComponent.ts b/src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonCustomtextComponent.ts new file mode 100644 index 0000000..e832cf8 --- /dev/null +++ b/src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonCustomtextComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './collapsebutton-customtext.html' +}) +export class CollapsebuttonCustomtextComponent { + collapsed: boolean = false; + customText: string = '自定义按钮文本'; +} diff --git a/src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonEventComponent.ts b/src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonEventComponent.ts new file mode 100644 index 0000000..dbf0758 --- /dev/null +++ b/src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonEventComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './collapsebutton-event.html' +}) +export class CollapsebuttonEventComponent { + collapsed: boolean = true; + myLogs: Array = []; + + onCollapsedChange(isCollapsed: boolean): void { + this.collapsed = !this.collapsed; + this.myLogs = [...this.myLogs, `collapsed: ${this.collapsed}`]; + } +} diff --git a/src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonSearchcountComponent.ts b/src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonSearchcountComponent.ts new file mode 100644 index 0000000..515116b --- /dev/null +++ b/src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonSearchcountComponent.ts @@ -0,0 +1,38 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './collapsebutton-searchcount.html' +}) +export class CollapsebuttonSearchcountComponent { + collapsed: boolean = true; + text1: string = '服务名称:'; + text2: string = '状态:'; + colsNumber: number = 5; + fieldVerticalAlign: string = 'middle'; + collapseButtonSearch: string = '搜索'; + collapseButtonReset: string = '重置'; + advancedSearch: any = { + serviceName: '', + status: '' + }; + searchCount: number = 0; + + onSearchClick(): void { + this.calcSearchCount(); + } + + onResetButtonClick(): void { + this.advancedSearch.serviceName = ''; + this.advancedSearch.status = ''; + this.calcSearchCount(); + } + + private calcSearchCount(): void { + this.searchCount = 0; + for (let key in this.advancedSearch) { + if (this.advancedSearch[key]) { + this.searchCount++; + } + } + } +} diff --git a/src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonTestModule.ts b/src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonTestModule.ts new file mode 100644 index 0000000..e15d37b --- /dev/null +++ b/src/collapsebutton/demo/src/app/collapsebutton/CollapsebuttonTestModule.ts @@ -0,0 +1,56 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiCollapsebuttonModule, TiButtonModule, TiFormfieldModule, TiIconModule, TiSearchboxModule } from '@opentiny/ng'; +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; + +import { CollapsebuttonBasicComponent } from './CollapsebuttonBasicComponent'; +import { CollapsebuttonCustomtextComponent } from './CollapsebuttonCustomtextComponent'; +import { CollapsebuttonSearchcountComponent } from './CollapsebuttonSearchcountComponent'; +import { CollapsebuttonEventComponent } from './CollapsebuttonEventComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiCollapsebuttonModule, + TiButtonModule, + TiIconModule, + TiSearchboxModule, + TiFormfieldModule, + DemoLogModule, + RouterModule.forChild(CollapsebuttonTestModule.ROUTES) + ], + declarations: [ + CollapsebuttonBasicComponent, + CollapsebuttonCustomtextComponent, + CollapsebuttonSearchcountComponent, + CollapsebuttonEventComponent + ] +}) +export class CollapsebuttonTestModule { + public static readonly ROUTES: Routes = [ + { + path: 'collapsebutton/collapsebutton-basic', + component: CollapsebuttonBasicComponent, + data: { label: '基本使用' } + }, + { + path: 'collapsebutton/collapsebutton-customtext', + component: CollapsebuttonCustomtextComponent, + data: { label: '按钮自定义文本' } + }, + { + path: 'collapsebutton/collapsebutton-searchcount', + component: CollapsebuttonSearchcountComponent, + data: { label: '高级搜索条件计数' } + }, + { + path: 'collapsebutton/collapsebutton-event', + component: CollapsebuttonEventComponent, + data: { label: '事件' } + } + ]; +} diff --git a/src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-basic.html b/src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-basic.html new file mode 100644 index 0000000..dbb602e --- /dev/null +++ b/src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-basic.html @@ -0,0 +1,2 @@ + +collapsed:{{collapsed}} diff --git a/src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-customtext.html b/src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-customtext.html new file mode 100644 index 0000000..cb171b0 --- /dev/null +++ b/src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-customtext.html @@ -0,0 +1,7 @@ + + + + {{customText}} + + +collapsed2:{{collapsed}} diff --git a/src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-event.html b/src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-event.html new file mode 100644 index 0000000..4706ae1 --- /dev/null +++ b/src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-event.html @@ -0,0 +1,2 @@ + + diff --git a/src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-searchcount.html b/src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-searchcount.html new file mode 100644 index 0000000..aa86210 --- /dev/null +++ b/src/collapsebutton/demo/src/app/collapsebutton/collapsebutton-searchcount.html @@ -0,0 +1,19 @@ + + + + +
+ + + + + + + + +
+
+ + +
+
diff --git a/src/collapsebutton/demo/src/app/collapsebutton/webdoc/collapsebutton-demos.js b/src/collapsebutton/demo/src/app/collapsebutton/webdoc/collapsebutton-demos.js new file mode 100644 index 0000000..55c6c34 --- /dev/null +++ b/src/collapsebutton/demo/src/app/collapsebutton/webdoc/collapsebutton-demos.js @@ -0,0 +1,52 @@ +export default { + column: '2', + demos: [ + { + demoId: 'collapsebutton-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'collapsebutton basic' + }, + desc: { + 'zh-CN': '

通过属性collapsed实现双向绑定。

', + 'en-US': '

collapsebutton basic

' + }, + apis: ['TiCollapsebuttonComponent.properties.collapsed'] + }, + { + demoId: 'collapsebutton-customtext', + name: { + 'zh-CN': '按钮自定义文本', + 'en-US': 'collapsebutton customtext' + }, + desc: { + 'zh-CN': '

通过ng-template模板元素自定义按钮文本。

', + 'en-US': '

collapsebutton customtext

' + } + }, + { + demoId: 'collapsebutton-searchcount', + name: { + 'zh-CN': '高级搜索条件计数', + 'en-US': 'collapsebutton searchcount' + }, + desc: { + 'zh-CN': '

通过属性searchCount显示出当前的搜索条件计数。

', + 'en-US': '

collapsebutton searchcount

' + }, + apis: ['TiCollapsebuttonComponent.properties.searchCount'] + }, + { + demoId: 'collapsebutton-event', + name: { + 'zh-CN': '事件', + 'en-US': 'collapsebutton event' + }, + desc: { + 'zh-CN': '

当点击高级搜索按钮后触发collapsedChange事件。

', + 'en-US': '

collapsebutton event

' + }, + apis: ['TiCollapsebuttonComponent.events.collapsedChange'] + } + ] +}; diff --git a/src/collapsebutton/demo/src/app/collapsebutton/webdoc/collapsebutton.cn.md b/src/collapsebutton/demo/src/app/collapsebutton/webdoc/collapsebutton.cn.md new file mode 100644 index 0000000..7678379 --- /dev/null +++ b/src/collapsebutton/demo/src/app/collapsebutton/webdoc/collapsebutton.cn.md @@ -0,0 +1,22 @@ +--- +title: Collapsebutton 高级搜索按钮 +--- +# Collapsebutton 高级搜索按钮 + +
+ +折叠按钮组件,在高级搜索场景中使用 + +```typescript +import { TiCollapsebuttonModule } from '@opentiny/ng'; +``` +
+ +
+ +折叠按钮组件,在高级搜索场景中使用 + +```typescript +import { TiCollapsebuttonModule } from '@opentiny/ng'; +``` +
\ No newline at end of file diff --git a/src/collapsebutton/demo/src/app/collapsebutton/webdoc/collapsebutton.en.md b/src/collapsebutton/demo/src/app/collapsebutton/webdoc/collapsebutton.en.md new file mode 100644 index 0000000..8baa6e2 --- /dev/null +++ b/src/collapsebutton/demo/src/app/collapsebutton/webdoc/collapsebutton.en.md @@ -0,0 +1,18 @@ +--- +title: Collapsebutton +--- +# Collapsebutton + +
+ +```typescript +import { TiCollapsebuttonModule } from '@opentiny/ng'; +``` +
+ +
+ +```typescript +import { TiCollapsebuttonModule } from '@opentiny/ng'; +``` +
\ No newline at end of file diff --git a/src/collapsebutton/demo/src/favicon.ico b/src/collapsebutton/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/collapsebutton/demo/src/index.html b/src/collapsebutton/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/collapsebutton/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/collapsebutton/demo/src/main.ts b/src/collapsebutton/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/collapsebutton/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/collapsebutton/demo/test.ts b/src/collapsebutton/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/collapsebutton/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/collapsebutton/demo/tsconfig.app.json b/src/collapsebutton/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/collapsebutton/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/collapsebutton/demo/tsconfig.spec.json b/src/collapsebutton/demo/tsconfig.spec.json new file mode 100644 index 0000000..e12f108 --- /dev/null +++ b/src/collapsebutton/demo/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": ["jasmine"] + }, + "files": ["./test.ts", "../../polyfills.ts"], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/collapsebutton/lib/index.ts b/src/collapsebutton/lib/index.ts new file mode 100644 index 0000000..a90996b --- /dev/null +++ b/src/collapsebutton/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiCollapsebuttonModule'; diff --git a/src/collapsebutton/lib/ng-package.json b/src/collapsebutton/lib/ng-package.json new file mode 100644 index 0000000..39ff511 --- /dev/null +++ b/src/collapsebutton/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/collapsebutton", + "lib": { + "entryFile": "index.ts" + } +} diff --git a/src/collapsebutton/lib/package.json b/src/collapsebutton/lib/package.json new file mode 100644 index 0000000..3473f5d --- /dev/null +++ b/src/collapsebutton/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-collapsebutton", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/collapsebutton/lib/project.json b/src/collapsebutton/lib/project.json new file mode 100644 index 0000000..3ae1ee5 --- /dev/null +++ b/src/collapsebutton/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/collapsebutton/lib", + "sourceRoot": "src/collapsebutton/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/collapsebutton"], + "options": { + "project": "src/collapsebutton/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/collapsebutton"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js collapsebutton" + }, + { + "command": "ng default-build collapsebutton" + }, + { + "command": "node build/clear-default-theme.js collapsebutton" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/collapsebutton && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build collapsebutton && ng pack collapsebutton && node build/publish.js collapsebutton --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/collapsebutton/lib/src/TiCollapsebuttonComponent.ts b/src/collapsebutton/lib/src/TiCollapsebuttonComponent.ts new file mode 100644 index 0000000..3c23591 --- /dev/null +++ b/src/collapsebutton/lib/src/TiCollapsebuttonComponent.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { ChangeDetectionStrategy, Component, ContentChild, EventEmitter, Input, Output, TemplateRef } from '@angular/core'; + +import { TiBaseComponent } from '@opentiny/ng-base'; +/** + * 折叠按钮组件,在高级搜索场景中使用 + * + */ +@Component({ + selector: 'ti-collapsebutton', + templateUrl: './collapsebutton.html', + styleUrls: ['./collapsebutton.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-collapsebutton-uncollapsed]': '!collapsed' + } +}) +export class TiCollapsebuttonComponent extends TiBaseComponent { + /** + * 是否折叠,true:折叠状态 false:非折叠状态,双向绑定 + */ + @Input() collapsed: boolean = true; + /** + * 高级搜索显示的筛选条件数目 + */ + @Input() searchCount: number = 0; + /** + * 折叠状态改变事件,用于实现折叠状态双向绑定,也可单独使用 + */ + @Output() readonly collapsedChange: EventEmitter = new EventEmitter(); + /** + * @ignore + * 用户自定义按钮文本 + */ + @ContentChild(TemplateRef) textTemplateRef: TemplateRef; + + /** + * @ignore + * 按钮点击事件处理 + */ + public onClickToggle($event: Event): void { + this.collapsed = !this.collapsed; + this.collapsedChange.emit(this.collapsed); + } +} diff --git a/src/collapsebutton/lib/src/TiCollapsebuttonModule.ts b/src/collapsebutton/lib/src/TiCollapsebuttonModule.ts new file mode 100644 index 0000000..bf6e709 --- /dev/null +++ b/src/collapsebutton/lib/src/TiCollapsebuttonModule.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TiButtonModule } from '@opentiny/ng-button'; +import { TiLocaleModule, TiLocale } from '@opentiny/ng-locale'; +import { TiIconModule } from '@opentiny/ng-icon'; + +import { TiCollapsebuttonComponent } from './TiCollapsebuttonComponent'; +import { TiCollapsepanelComponent } from './TiCollapsepanelComponent'; + +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, TiButtonModule, TiLocaleModule, TiIconModule], + exports: [TiCollapsebuttonComponent, TiCollapsepanelComponent], + declarations: [TiCollapsebuttonComponent, TiCollapsepanelComponent] +}) +export class TiCollapsebuttonModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiCollapsebuttonComponent } from './TiCollapsebuttonComponent'; +export { TiCollapsepanelComponent } from './TiCollapsepanelComponent'; diff --git a/src/collapsebutton/lib/src/TiCollapsepanelComponent.ts b/src/collapsebutton/lib/src/TiCollapsepanelComponent.ts new file mode 100644 index 0000000..15ec604 --- /dev/null +++ b/src/collapsebutton/lib/src/TiCollapsepanelComponent.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { ChangeDetectionStrategy, Component } from '@angular/core'; +/** + * 高级搜索中的面板,可选择使用。 + */ +@Component({ + selector: 'ti-collapsepanel', + template: ` `, + styleUrls: ['./collapsepanel.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiCollapsepanelComponent {} diff --git a/src/collapsebutton/lib/src/collapsebutton.html b/src/collapsebutton/lib/src/collapsebutton.html new file mode 100644 index 0000000..a7b0618 --- /dev/null +++ b/src/collapsebutton/lib/src/collapsebutton.html @@ -0,0 +1,9 @@ + + +{{'tiCollapsebutton.text' | tiTranslate }} diff --git a/src/collapsebutton/lib/src/collapsebutton.less b/src/collapsebutton/lib/src/collapsebutton.less new file mode 100644 index 0000000..1c3ed52 --- /dev/null +++ b/src/collapsebutton/lib/src/collapsebutton.less @@ -0,0 +1,81 @@ +@import '../../../themes/basic/base-all.less'; + +::ng-deep :root { + --ti3-collapsebutton-height: 36px; +} + +:host { + display: inline-block; + height: var(--ti3-collapsebutton-height); + .transition(background-color; 0.15s); +} + +:host ::ng-deep [tiButton] { + padding: 0 var(--ti-common-space-10) !important; + // 未展开时,其它局点与普通按钮背景色有区别 + &:not([disabled]):hover, + &:not([disabled]):focus, + &:not([disabled]):active { + background-color: var(--ti-common-bg-minor); + color: var(--ti-common-color-text-highlight); + .ti3-icon-accordion-fold { + color: var(--ti-common-color-icon-hover); + } + } +} + +// 展开时 +:host.ti3-collapsebutton-uncollapsed { + background-color: var(--ti-common-color-bg-white-emphasize); + .transition(background-color; 0.15s); + + ::ng-deep [tiButton] { + background-color: var(--ti-common-color-bg-white-emphasize); + border-color: var(--ti-common-color-bg-white-emphasize); + .transition(background-color; 0.15s); + } + + ::ng-deep [tiButton]:not([disabled]):focus { + border-color: var(--ti-common-color-bg-white-emphasize); + background-color: var(--ti-common-color-bg-white-emphasize); + // 和普通按钮有区别,没有聚焦状态,文字回复默认颜色 + color: var(--ti-common-color-text-primary); + ::ng-deep .ti3-icon-accordion-fold { + color: var(--ti-common-color-icon-normal); + } + } + + ::ng-deep [tiButton]:not([disabled]):hover { + border-color: var(--ti-common-color-bg-white-emphasize); + background-color: var(--ti-common-color-bg-white-emphasize); + color: var(--ti-common-color-text-highlight); + ::ng-deep .ti3-icon-accordion-fold { + color: var(--ti-common-color-icon-hover); + } + } +} + +/*旋转动效的盒子必须是block或者inline-block*/ +.ti3-icon-accordion-fold { + display: inline-block; + margin-left: var(--ti-common-space-10); + color: var(--ti-common-color-icon-normal); + .transition(color; 0.15s); +} + +/********************************动效************************************/ +//旋转-180-0deg动画,打开动画 +.rotate-aniamtion(ti-collapsebutton-rotate-uncollapsed-keyframes, -180deg, 0deg); + +//打开时候的动效 +:host.ti3-collapsebutton-uncollapsed .ti3-icon-accordion-fold { + .animation(ti-collapsebutton-rotate-uncollapsed-keyframes, 0.15s); +} + +//旋转0-180deg,关闭动画 +.rotate-aniamtion(ti-collapsebutton-rotate-collapsed-keyframes, 0deg, 180deg); + +//收起时候的动效 +.ti3-icon-accordion-fold { + .animation(ti-collapsebutton-rotate-collapsed-keyframes, 0.15s); +} diff --git a/src/collapsebutton/lib/src/collapsepanel.less b/src/collapsebutton/lib/src/collapsepanel.less new file mode 100644 index 0000000..9f7d12f --- /dev/null +++ b/src/collapsebutton/lib/src/collapsepanel.less @@ -0,0 +1,14 @@ +@import '../../../themes/basic/base-all.less'; + +// 面板 +:host { + .clearfix(); + display: block; + width: 100%; + padding: var(--ti-common-space-5x); + background-color: var(--ti-common-color-bg-white-emphasize); + border-top: 1px solid var(--ti-common-color-bg-white-emphasize); + .box-sizing(border-box); + border-radius: var(--ti-common-border-radius-normal); + margin-top: -1px; +} diff --git a/src/collapsebutton/lib/src/i18n/TiCollapsebuttonWords.ts b/src/collapsebutton/lib/src/i18n/TiCollapsebuttonWords.ts new file mode 100644 index 0000000..c6c84c1 --- /dev/null +++ b/src/collapsebutton/lib/src/i18n/TiCollapsebuttonWords.ts @@ -0,0 +1,5 @@ +export interface TiCollapsebuttonWords { + tiCollapsebutton: { + text: string; + }; +} diff --git a/src/collapsebutton/lib/src/i18n/en_US.ts b/src/collapsebutton/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..c8f9cdc --- /dev/null +++ b/src/collapsebutton/lib/src/i18n/en_US.ts @@ -0,0 +1,7 @@ +import { TiCollapsebuttonWords } from './TiCollapsebuttonWords'; + +export const en_US: TiCollapsebuttonWords = { + tiCollapsebutton: { + text: 'Advanced Search' + } +}; diff --git a/src/collapsebutton/lib/src/i18n/es_US.ts b/src/collapsebutton/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..489284b --- /dev/null +++ b/src/collapsebutton/lib/src/i18n/es_US.ts @@ -0,0 +1,7 @@ +import { TiCollapsebuttonWords } from './TiCollapsebuttonWords'; + +export const es_US: TiCollapsebuttonWords = { + tiCollapsebutton: { + text: 'Búsqueda avanzada' + } +}; diff --git a/src/collapsebutton/lib/src/i18n/fr_FR.ts b/src/collapsebutton/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..294da6e --- /dev/null +++ b/src/collapsebutton/lib/src/i18n/fr_FR.ts @@ -0,0 +1,7 @@ +import { TiCollapsebuttonWords } from './TiCollapsebuttonWords'; + +export const fr_FR: TiCollapsebuttonWords = { + tiCollapsebutton: { + text: 'Recherche avancée' + } +}; diff --git a/src/collapsebutton/lib/src/i18n/index.ts b/src/collapsebutton/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/collapsebutton/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/collapsebutton/lib/src/i18n/pt_BR.ts b/src/collapsebutton/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..dc565cb --- /dev/null +++ b/src/collapsebutton/lib/src/i18n/pt_BR.ts @@ -0,0 +1,7 @@ +import { TiCollapsebuttonWords } from './TiCollapsebuttonWords'; + +export const pt_BR: TiCollapsebuttonWords = { + tiCollapsebutton: { + text: 'Pesquisa avançada' + } +}; diff --git a/src/collapsebutton/lib/src/i18n/zh_CN.ts b/src/collapsebutton/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..0ac4043 --- /dev/null +++ b/src/collapsebutton/lib/src/i18n/zh_CN.ts @@ -0,0 +1,7 @@ +import { TiCollapsebuttonWords } from './TiCollapsebuttonWords'; + +export const zh_CN: TiCollapsebuttonWords = { + tiCollapsebutton: { + text: '高级搜索' + } +}; diff --git a/src/collapsetext/demo/project.json b/src/collapsetext/demo/project.json new file mode 100644 index 0000000..660bed4 --- /dev/null +++ b/src/collapsetext/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/collapsetext/demo", + "sourceRoot": "src/collapsetext/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/collapsetext", + "index": "src/collapsetext/demo/src/index.html", + "main": "src/collapsetext/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/collapsetext/demo/tsconfig.app.json", + "assets": ["src/collapsetext/demo/src/favicon.ico", "src/collapsetext/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "collapsetext-demo:build:production" + }, + "development": { + "browserTarget": "collapsetext-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js collapsetext" + } + ] + } + } + } +} diff --git a/src/collapsetext/demo/src/app/AppComponent.ts b/src/collapsetext/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/collapsetext/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/collapsetext/demo/src/app/AppModule.ts b/src/collapsetext/demo/src/app/AppModule.ts new file mode 100644 index 0000000..9139aeb --- /dev/null +++ b/src/collapsetext/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { CollapsetextTestModule } from './collapsetext/CollapsetextTestModule'; + +@NgModule({ + imports: [ + CollapsetextTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/collapsetext/demo/src/app/IndexComponent.ts b/src/collapsetext/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..b7fd9e9 --- /dev/null +++ b/src/collapsetext/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { CollapsetextTestModule } from './collapsetext/CollapsetextTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = CollapsetextTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/collapsetext/demo/src/app/app.html b/src/collapsetext/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/collapsetext/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/collapsetext/demo/src/app/collapsetext/CollapsetextBasicComponent.ts b/src/collapsetext/demo/src/app/collapsetext/CollapsetextBasicComponent.ts new file mode 100644 index 0000000..7f4ace9 --- /dev/null +++ b/src/collapsetext/demo/src/app/collapsetext/CollapsetextBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './collapsetext-basic.html' +}) +export class CollapsetextBasicComponent { + label: string = '标题下展'; +} diff --git a/src/collapsetext/demo/src/app/collapsetext/CollapsetextCollapsedComponent.ts b/src/collapsetext/demo/src/app/collapsetext/CollapsetextCollapsedComponent.ts new file mode 100644 index 0000000..31283f6 --- /dev/null +++ b/src/collapsetext/demo/src/app/collapsetext/CollapsetextCollapsedComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './collapsetext-collapsed.html' +}) +export class CollapsetextCollapsedComponent { + label: string = '标题下展'; + collapsed: boolean = false; + myLogs: Array = []; + collapsedChange(collapsed: boolean): void { + this.myLogs = [...this.myLogs, `collapsed = ${collapsed}`]; + } +} diff --git a/src/collapsetext/demo/src/app/collapsetext/CollapsetextHighlightComponent.ts b/src/collapsetext/demo/src/app/collapsetext/CollapsetextHighlightComponent.ts new file mode 100644 index 0000000..9a826d1 --- /dev/null +++ b/src/collapsetext/demo/src/app/collapsetext/CollapsetextHighlightComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './collapsetext-highlight.html' +}) +export class CollapsetextHighlightComponent { + label: string = '标题下展'; + highlight: boolean = true; +} diff --git a/src/collapsetext/demo/src/app/collapsetext/CollapsetextSceneComponent.ts b/src/collapsetext/demo/src/app/collapsetext/CollapsetextSceneComponent.ts new file mode 100644 index 0000000..b6a5f30 --- /dev/null +++ b/src/collapsetext/demo/src/app/collapsetext/CollapsetextSceneComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './collapsetext-scene.html' +}) +export class CollapsetextSceneComponent { + label: string = '查看'; + type: string = 'content'; + collapsed: boolean = true; + highlight: boolean = true; + collapsedChange(collapsed: boolean): void { + if (collapsed) { + this.label = '查看'; + } else { + this.label = '收起'; + } + } +} diff --git a/src/collapsetext/demo/src/app/collapsetext/CollapsetextTestModule.ts b/src/collapsetext/demo/src/app/collapsetext/CollapsetextTestModule.ts new file mode 100644 index 0000000..8393761 --- /dev/null +++ b/src/collapsetext/demo/src/app/collapsetext/CollapsetextTestModule.ts @@ -0,0 +1,31 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Routes, RouterModule } from '@angular/router'; +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; + +import { TiCollapsetextModule } from '@opentiny/ng'; +import { CollapsetextBasicComponent } from './CollapsetextBasicComponent'; +import { CollapsetextTypeComponent } from './CollapsetextTypeComponent'; +import { CollapsetextHighlightComponent } from './CollapsetextHighlightComponent'; +import { CollapsetextCollapsedComponent } from './CollapsetextCollapsedComponent'; +import { CollapsetextSceneComponent } from './CollapsetextSceneComponent'; + +@NgModule({ + imports: [CommonModule, TiCollapsetextModule, DemoLogModule, RouterModule.forChild(CollapsetextTestModule.ROUTES)], + declarations: [ + CollapsetextBasicComponent, + CollapsetextTypeComponent, + CollapsetextHighlightComponent, + CollapsetextCollapsedComponent, + CollapsetextSceneComponent + ] +}) +export class CollapsetextTestModule { + static readonly ROUTES: Routes = [ + { path: 'collapsetext/collapsetext-basic', component: CollapsetextBasicComponent }, + { path: 'collapsetext/collapsetext-type', component: CollapsetextTypeComponent }, + { path: 'collapsetext/collapsetext-highlight', component: CollapsetextHighlightComponent }, + { path: 'collapsetext/collapsetext-collapsed', component: CollapsetextCollapsedComponent }, + { path: 'collapsetext/collapsetext-scene', component: CollapsetextSceneComponent } + ]; +} diff --git a/src/collapsetext/demo/src/app/collapsetext/CollapsetextTypeComponent.ts b/src/collapsetext/demo/src/app/collapsetext/CollapsetextTypeComponent.ts new file mode 100644 index 0000000..1adb93e --- /dev/null +++ b/src/collapsetext/demo/src/app/collapsetext/CollapsetextTypeComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './collapsetext-type.html' +}) +export class CollapsetextTypeComponent { + label: string = '标题下展'; + type: string = 'title'; + label1: string = '内容下展'; + type1: string = 'content'; +} diff --git a/src/collapsetext/demo/src/app/collapsetext/collapsetext-basic.html b/src/collapsetext/demo/src/app/collapsetext/collapsetext-basic.html new file mode 100644 index 0000000..eb2e4ed --- /dev/null +++ b/src/collapsetext/demo/src/app/collapsetext/collapsetext-basic.html @@ -0,0 +1 @@ + diff --git a/src/collapsetext/demo/src/app/collapsetext/collapsetext-collapsed.html b/src/collapsetext/demo/src/app/collapsetext/collapsetext-collapsed.html new file mode 100644 index 0000000..816dd00 --- /dev/null +++ b/src/collapsetext/demo/src/app/collapsetext/collapsetext-collapsed.html @@ -0,0 +1,3 @@ + + + diff --git a/src/collapsetext/demo/src/app/collapsetext/collapsetext-highlight.html b/src/collapsetext/demo/src/app/collapsetext/collapsetext-highlight.html new file mode 100644 index 0000000..778030a --- /dev/null +++ b/src/collapsetext/demo/src/app/collapsetext/collapsetext-highlight.html @@ -0,0 +1 @@ + diff --git a/src/collapsetext/demo/src/app/collapsetext/collapsetext-scene.html b/src/collapsetext/demo/src/app/collapsetext/collapsetext-scene.html new file mode 100644 index 0000000..763535c --- /dev/null +++ b/src/collapsetext/demo/src/app/collapsetext/collapsetext-scene.html @@ -0,0 +1,36 @@ +点击查看阅读 将进酒(李白) 全文 + + +
+
+    君不见黄河之水天上来,奔流到海不复回。
+    君不见高堂明镜悲白发,朝如青丝暮成雪。
+    人生得意须尽欢,莫使金樽空对月。
+    天生我材必有用,千金散尽还复来。
+    烹羊宰牛且为乐,会须一饮三百杯。
+    岑夫子,丹丘生,将进酒,杯莫停。
+    与君歌一曲,请君为我倾耳听。
+    钟鼓馔玉不足贵,但愿长醉不愿醒。
+    古来圣贤皆寂寞,惟有饮者留其名。
+    陈王昔时宴平乐,斗酒十千恣欢谑。
+    主人何为言少钱,径须沽取对君酌。
+    五花马、千金裘,呼儿将出换美酒,
+    与尔同销万古愁。
+
+ diff --git a/src/collapsetext/demo/src/app/collapsetext/collapsetext-type.html b/src/collapsetext/demo/src/app/collapsetext/collapsetext-type.html new file mode 100644 index 0000000..219491e --- /dev/null +++ b/src/collapsetext/demo/src/app/collapsetext/collapsetext-type.html @@ -0,0 +1,4 @@ +

1.title

+ +

2.content

+ diff --git a/src/collapsetext/demo/src/app/collapsetext/webdoc/collapsetext-demos.js b/src/collapsetext/demo/src/app/collapsetext/webdoc/collapsetext-demos.js new file mode 100644 index 0000000..93d3d78 --- /dev/null +++ b/src/collapsetext/demo/src/app/collapsetext/webdoc/collapsetext-demos.js @@ -0,0 +1,65 @@ +export default { + column: '2', + demos: [ + { + demoId: 'collapsetext-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'collapsetext basic' + }, + desc: { + 'zh-CN': '

通过属性label配置头部文本内容。

', + 'en-US': 'collapsetext basic' + }, + apis: ['TiCollapsetextComponent.properties.label'] + }, + { + demoId: 'collapsetext-type', + name: { + 'zh-CN': '类型', + 'en-US': 'collapsetext type' + }, + desc: { + 'zh-CN': '

通过属性type配置是标题/内容下展,包含titlecontent两种类型。

', + 'en-US': 'collapsetext type' + }, + apis: ['TiCollapsetextComponent.properties.type'] + }, + { + demoId: 'collapsetext-highlight', + name: { + 'zh-CN': '高亮', + 'en-US': 'collapsetext highlight' + }, + desc: { + 'zh-CN': '

通过属性highlight配置默认状态是否高亮。

', + 'en-US': 'collapsetext highlight' + }, + apis: ['TiCollapsetextComponent.properties.highlight'] + }, + { + demoId: 'collapsetext-collapsed', + name: { + 'zh-CN': '折叠', + 'en-US': 'collapsetext collapsed' + }, + desc: { + 'zh-CN': + '

通过双向绑定的属性collapsed配置和获取是否处于折叠状态;当collapsed发生变化时触发collapsedChange事件。

', + 'en-US': 'collapsetext collapsed' + }, + apis: ['TiCollapsetextComponent.properties.collapsed', 'TiCollapsetextComponent.events.collapsedChange'] + }, + { + demoId: 'collapsetext-scene', + name: { + 'zh-CN': '常用场景', + 'en-US': 'collapsetext scene' + }, + desc: { + 'zh-CN': '

常用的使用场景。

', + 'en-US': 'collapsetext scene' + } + } + ] +}; diff --git a/src/collapsetext/demo/src/app/collapsetext/webdoc/collapsetext.cn.md b/src/collapsetext/demo/src/app/collapsetext/webdoc/collapsetext.cn.md new file mode 100644 index 0000000..32502e7 --- /dev/null +++ b/src/collapsetext/demo/src/app/collapsetext/webdoc/collapsetext.cn.md @@ -0,0 +1,15 @@ +--- +title: Collapsetext 下展文本 +--- + +# Collapsetext 下展文本 + +
+ +下展文本是控制文本展开的组件。 + +```typescript +import { TiCollapsetextModule } from '@opentiny/ng'; +``` + +
diff --git a/src/collapsetext/demo/src/app/collapsetext/webdoc/collapsetext.en.md b/src/collapsetext/demo/src/app/collapsetext/webdoc/collapsetext.en.md new file mode 100644 index 0000000..01d6e63 --- /dev/null +++ b/src/collapsetext/demo/src/app/collapsetext/webdoc/collapsetext.en.md @@ -0,0 +1,13 @@ +--- +title: Collapsetext +--- + +# Collapsetext + +
+ +```typescript +import { TiCollapsetextModule } from '@opentiny/ng'; +``` + +
diff --git a/src/collapsetext/demo/src/favicon.ico b/src/collapsetext/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/collapsetext/demo/src/index.html b/src/collapsetext/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/collapsetext/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/collapsetext/demo/src/main.ts b/src/collapsetext/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/collapsetext/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/collapsetext/demo/tsconfig.app.json b/src/collapsetext/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/collapsetext/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/collapsetext/lib/index.ts b/src/collapsetext/lib/index.ts new file mode 100644 index 0000000..17bba29 --- /dev/null +++ b/src/collapsetext/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiCollapsetextModule'; +export * from './src/TiCollapsetextComponent'; diff --git a/src/collapsetext/lib/ng-package.json b/src/collapsetext/lib/ng-package.json new file mode 100644 index 0000000..0431727 --- /dev/null +++ b/src/collapsetext/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/collapsetext", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/collapsetext/lib/package.json b/src/collapsetext/lib/package.json new file mode 100644 index 0000000..e7bce86 --- /dev/null +++ b/src/collapsetext/lib/package.json @@ -0,0 +1,8 @@ +{ + "name": "@opentiny/ng-collapsetext", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/collapsetext/lib/project.json b/src/collapsetext/lib/project.json new file mode 100644 index 0000000..47d2cda --- /dev/null +++ b/src/collapsetext/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/collapsetext/lib", + "sourceRoot": "src/collapsetext/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/collapsetext"], + "options": { + "project": "src/collapsetext/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/collapsetext"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js collapsetext" + }, + { + "command": "ng default-build collapsetext" + }, + { + "command": "node build/clear-default-theme.js collapsetext" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/collapsetext && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build collapsetext && ng pack collapsetext && node build/publish.js collapsetext --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/collapsetext/lib/src/TiCollapsetextComponent.ts b/src/collapsetext/lib/src/TiCollapsetextComponent.ts new file mode 100644 index 0000000..740fd55 --- /dev/null +++ b/src/collapsetext/lib/src/TiCollapsetextComponent.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; + +/** + * Collapsetext组件用于显示 标题下展/内容下展,状态可折叠或展开 + */ +@Component({ + selector: 'ti-collapsetext', + templateUrl: 'collapsetext.html', + styleUrls: ['collapsetext.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiCollapsetextComponent { + /** + * 显示的文本内容 + */ + @Input() label: string; + /** + * 下展的类型 + */ + @Input() type: 'title' | 'content' = 'title'; + /** + * 是否高亮 + */ + @Input() highlight: boolean = false; + /** + * 是否折叠 + */ + @Input() collapsed: boolean = true; + /** + * 折叠状态变化时触发的回调,参数:折叠状态 collapsed 的值 + */ + @Output() readonly collapsedChange: EventEmitter = new EventEmitter(); + + /** + * @ignore + * 切换展开状态方法 + */ + public changeCollapse($event: Event): void { + this.collapsed = !this.collapsed; + // 通知collapsed展开状态,确保collapsed状态双向绑定功能正常 + this.collapsedChange.emit(this.collapsed); + } +} diff --git a/src/collapsetext/lib/src/TiCollapsetextModule.ts b/src/collapsetext/lib/src/TiCollapsetextModule.ts new file mode 100644 index 0000000..e2d8702 --- /dev/null +++ b/src/collapsetext/lib/src/TiCollapsetextModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TiCollapsetextComponent } from './TiCollapsetextComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [TiCollapsetextComponent], + declarations: [TiCollapsetextComponent] +}) +export class TiCollapsetextModule {} + +export { TiCollapsetextComponent } from './TiCollapsetextComponent'; diff --git a/src/collapsetext/lib/src/collapsetext.html b/src/collapsetext/lib/src/collapsetext.html new file mode 100644 index 0000000..565151d --- /dev/null +++ b/src/collapsetext/lib/src/collapsetext.html @@ -0,0 +1,10 @@ +
+ + {{label}} + + +
diff --git a/src/collapsetext/lib/src/collapsetext.less b/src/collapsetext/lib/src/collapsetext.less new file mode 100644 index 0000000..aaf9515 --- /dev/null +++ b/src/collapsetext/lib/src/collapsetext.less @@ -0,0 +1,122 @@ +// 因为CSS Var补丁,处理:host内变量有问题。所以写为::ng-deep :root +::ng-deep :root { + --ti-collapsetext-label-color: var(--ti-common-color-text-primary); + --ti-collapsetext-label-color-highlight: var(--ti-common-color-text-highlight); + --ti-collapsetext-label-color-hover: var(--ti-common-color-text-link-hover); +} + +// 组件容器 +.ti3-collapsetext-container { + .flex-container(row, flex-start, center); + .user-select(none); + display: inline-flex; + cursor: pointer; +} + +// 文字显示(默认内容类型) +.ti3-collapsetext-label { + line-height: var(--ti-common-line-height-number); + &:hover { + color: var(--ti-collapsetext-label-color-hover); + & + .ti3-collapsetext-arrow { + border-top-color: var(--ti-collapsetext-label-color-hover); + } + } +} + +// 文字显示(标题类型) +.ti3-collapsetext-label-title { + font-size: var(--ti-common-font-size-2); +} + +// 文字高亮显示 +.ti3-collapsetext-label-highlight { + color: var(--ti-collapsetext-label-color-highlight); + & + .ti3-collapsetext-arrow { + border-top-color: var(--ti-collapsetext-label-color-highlight); + } +} + +// 三角箭头 +.ti3-collapsetext-arrow { + .triangle-down(8px, 5px, var(--ti-collapsetext-label-color)); + margin-left: var(--ti-common-space-2x); +} + +/******************************** 箭头动效 ************************************/ +// 展开动画:旋转 -180-0deg +.rotate-aniamtion(ti3-collapsetext-rotate-uncollapsed-keyframes, 0deg, 180deg); + +// 展开时动效 +.ti3-collapsetext-arrow { + .animation(ti3-collapsetext-rotate-uncollapsed-keyframes, 0.15s); +} + +// 收起动画:旋转 0-180deg +.rotate-aniamtion(ti3-collapsetext-rotate-collapsed-keyframes, -180deg, 0deg); + +// 收起时动效 +.ti3-collapsetext-arrow-collapsed { + .animation(ti3-collapsetext-rotate-collapsed-keyframes, 0.15s); +} + +.flex-container(@direction: row, @justify-content: flex-start, @align-items: stretch) { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: @direction; + -ms-flex-direction: @direction; + -moz-flex-direction: @direction; + flex-direction: @direction; + -webkit-justify-content: @justify-content; + -moz-justify-content: @justify-content; + -ms-justify-content: @justify-content; + justify-content: @justify-content; + -webkit-align-items: @align-items; + -moz-align-items: @align-items; + -ms-align-items: @align-items; + align-items: @align-items; +} + +.triangle-down(@triangle-width; @triangle-height; @triangle-color) { + width: 0; + height: 0; + border-left: calc(@triangle-width / 2) solid transparent; + border-right: calc(@triangle-width / 2) solid transparent; + border-top: @triangle-height solid @triangle-color; +} + +// 文字选择 +.user-select (@type:none) { + // 火狐 + -moz-user-select: @type; + // webkit浏览器 + -webkit-user-select: @type; + // IE10 + -ms-user-select: @type; + // 早期浏览器 + -khtml-user-select: @type; + user-select: @type; +} + +//定义一个旋转函数 +.rotate-aniamtion(@rotate-name,@from-rotate,@to-rotate) { + @keyframes @rotate-name { + from { + transform: rotate(@from-rotate); + -moz-transform: rotate(@from-rotate); + -ms-transform: rotate(@from-rotate); + -webkit-transform: rotate(@from-rotate); + } + to { + transform: rotate(@to-rotate); + -moz-transform: rotate(@to-rotate); + -ms-transform: rotate(@to-rotate); + -webkit-transform: rotate(@to-rotate); + } + } +} + +//动效公共的less方法 +.animation(@animate-name, @time, @timing-function: cubic-bezier(0.4, 0, 0.2, 1), @delay:0s, @count:1,@wards:forwards) { + animation: @animate-name @time @timing-function @delay @count @wards; +} diff --git a/src/copy/demo/karma.conf.js b/src/copy/demo/karma.conf.js new file mode 100644 index 0000000..22d9d1a --- /dev/null +++ b/src/copy/demo/karma.conf.js @@ -0,0 +1,41 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [{ type: 'html' }, { type: 'text-summary' }] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/copy/demo/project.json b/src/copy/demo/project.json new file mode 100644 index 0000000..0e4c52f --- /dev/null +++ b/src/copy/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/copy/demo", + "sourceRoot": "src/copy/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/copy", + "index": "src/copy/demo/src/index.html", + "main": "src/copy/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/copy/demo/tsconfig.app.json", + "assets": ["src/copy/demo/src/favicon.ico", "src/copy/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "copy-demo:build:production" + }, + "development": { + "browserTarget": "copy-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js copy" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/copy/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/copy/demo/tsconfig.spec.json", + "karmaConfig": "src/copy/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/copy/demo/src/app/AppComponent.ts b/src/copy/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/copy/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/copy/demo/src/app/AppModule.ts b/src/copy/demo/src/app/AppModule.ts new file mode 100644 index 0000000..9665a2a --- /dev/null +++ b/src/copy/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { CopyTestModule } from './copy/CopyTestModule'; + +@NgModule({ + imports: [ + CopyTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/copy/demo/src/app/IndexComponent.ts b/src/copy/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..495755b --- /dev/null +++ b/src/copy/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { CopyTestModule } from './copy/CopyTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = CopyTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/copy/demo/src/app/app.html b/src/copy/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/copy/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/copy/demo/src/app/copy/CopyBasicComponent.ts b/src/copy/demo/src/app/copy/CopyBasicComponent.ts new file mode 100644 index 0000000..be64ee7 --- /dev/null +++ b/src/copy/demo/src/app/copy/CopyBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './copy-basic.html' +}) +export class CopyBasicComponent { + content: string = '故事的小黄花'; +} diff --git a/src/copy/demo/src/app/copy/CopyDarkComponent.ts b/src/copy/demo/src/app/copy/CopyDarkComponent.ts new file mode 100644 index 0000000..4fe31c9 --- /dev/null +++ b/src/copy/demo/src/app/copy/CopyDarkComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './copy-dark.html' +}) +export class CopyDarkComponent {} diff --git a/src/copy/demo/src/app/copy/CopyEventComponent.ts b/src/copy/demo/src/app/copy/CopyEventComponent.ts new file mode 100644 index 0000000..0c8db15 --- /dev/null +++ b/src/copy/demo/src/app/copy/CopyEventComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './copy-event.html' +}) +export class CopyEventComponent { + myLogs: Array = []; + onCopy(): void { + this.myLogs = [...this.myLogs, '已复制']; + } +} diff --git a/src/copy/demo/src/app/copy/CopyTableComponent.ts b/src/copy/demo/src/app/copy/CopyTableComponent.ts new file mode 100644 index 0000000..996ead6 --- /dev/null +++ b/src/copy/demo/src/app/copy/CopyTableComponent.ts @@ -0,0 +1,54 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './copy-table.html' +}) +export class CopyTableComponent { + displayed: Array = []; + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'ID', + width: '50%' + }, + { + title: 'Name', + width: '50%' + } + ]; + private data: Array = []; + + ngOnInit(): void { + for (let j: number = 0; j < 5; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + trackByFn(item: any): number { + return item.id; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Dupont-Dupont-Dupont-Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const stringList: Array = ['bb8e9b52', 'df9ec29c', 'e34e8b0c', 'ee39cfcc']; + const name: string = nameList[id % nameList.length]; + const uuid: string = `${stringList[(id * 7) % 4]}-${stringList[(id * 11) % 4]}-${stringList[(id * 13) % 4]}-${ + stringList[(id * 17) % 4] + }`; + + return { + name, + uuid, + id + }; + } +} diff --git a/src/copy/demo/src/app/copy/CopyTestModule.ts b/src/copy/demo/src/app/copy/CopyTestModule.ts new file mode 100644 index 0000000..c034cf1 --- /dev/null +++ b/src/copy/demo/src/app/copy/CopyTestModule.ts @@ -0,0 +1,33 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { TiOverflowModule, TiTableModule, TiTipModule, TiCopyModule } from '@opentiny/ng'; +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; + +import { CopyBasicComponent } from './CopyBasicComponent'; +import { CopyDarkComponent } from './CopyDarkComponent'; +import { CopyTableComponent } from './CopyTableComponent'; +import { CopyTipComponent } from './CopyTipComponent'; +import { CopyEventComponent } from './CopyEventComponent'; + +@NgModule({ + imports: [ + CommonModule, + TiTipModule, + TiTableModule, + TiCopyModule, + TiOverflowModule, + DemoLogModule, + RouterModule.forChild(CopyTestModule.ROUTES) + ], + declarations: [CopyBasicComponent, CopyDarkComponent, CopyTableComponent, CopyTipComponent, CopyEventComponent] +}) +export class CopyTestModule { + public static readonly ROUTES: Routes = [ + { path: 'copy/copy-basic', component: CopyBasicComponent, data: { label: '基本使用' } }, + { path: 'copy/copy-dark', component: CopyDarkComponent, data: { label: '深色背景' } }, + { path: 'copy/copy-tip', component: CopyTipComponent, data: { label: '提示信息' } }, + { path: 'copy/copy-table', component: CopyTableComponent, data: { label: '在表格中使用' } }, + { path: 'copy/copy-event', component: CopyEventComponent, data: { label: '事件' } } + ]; +} diff --git a/src/copy/demo/src/app/copy/CopyTipComponent.ts b/src/copy/demo/src/app/copy/CopyTipComponent.ts new file mode 100644 index 0000000..5bfcf32 --- /dev/null +++ b/src/copy/demo/src/app/copy/CopyTipComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './copy-tip.html' +}) +export class CopyTipComponent { + content: string = `可以复制换行的内容\n任时光匆匆流去\n我只在乎你\n`; +} diff --git a/src/copy/demo/src/app/copy/copy-basic.html b/src/copy/demo/src/app/copy/copy-basic.html new file mode 100644 index 0000000..eddf5f6 --- /dev/null +++ b/src/copy/demo/src/app/copy/copy-basic.html @@ -0,0 +1,5 @@ + +
+ {{content}} + +
diff --git a/src/copy/demo/src/app/copy/copy-dark.html b/src/copy/demo/src/app/copy/copy-dark.html new file mode 100644 index 0000000..d1f28dc --- /dev/null +++ b/src/copy/demo/src/app/copy/copy-dark.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/copy/demo/src/app/copy/copy-event.html b/src/copy/demo/src/app/copy/copy-event.html new file mode 100644 index 0000000..db86a52 --- /dev/null +++ b/src/copy/demo/src/app/copy/copy-event.html @@ -0,0 +1,5 @@ +
+ 故事的小黄花 + +
+ diff --git a/src/copy/demo/src/app/copy/copy-table.html b/src/copy/demo/src/app/copy/copy-table.html new file mode 100644 index 0000000..ba8e024 --- /dev/null +++ b/src/copy/demo/src/app/copy/copy-table.html @@ -0,0 +1,22 @@ + +
+ + + + + + + + + + + + +
{{column.title}}
+ {{row.uuid}} + + + + {{row.name}}
+
+
diff --git a/src/copy/demo/src/app/copy/copy-tip.html b/src/copy/demo/src/app/copy/copy-tip.html new file mode 100644 index 0000000..ac3f47d --- /dev/null +++ b/src/copy/demo/src/app/copy/copy-tip.html @@ -0,0 +1,7 @@ + +

1.title: 图标的说明信息

+{{content}} + +

2.successTip:完成复制

+{{content}} + diff --git a/src/copy/demo/src/app/copy/webdoc/copy-demos.js b/src/copy/demo/src/app/copy/webdoc/copy-demos.js new file mode 100644 index 0000000..97e95d7 --- /dev/null +++ b/src/copy/demo/src/app/copy/webdoc/copy-demos.js @@ -0,0 +1,63 @@ +export default { + column: '2', + demos: [ + { + demoId: 'copy-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'copy basic' + }, + desc: { + 'zh-CN': '

通过属性content设置复制内容。content接口值为空时,不能复制。

', + 'en-US': '

copy basic

' + }, + apis: ['TiCopyComponent.properties.content'] + }, + { + demoId: 'copy-dark', + name: { + 'zh-CN': '深色背景', + 'en-US': 'copy dark' + }, + desc: { + 'zh-CN': '

通过属性dark配置深色背景复制。

', + 'en-US': '

copy dark

' + } + }, + { + demoId: 'copy-tip', + name: { + 'zh-CN': '提示信息', + 'en-US': 'copy tip' + }, + desc: { + 'zh-CN': '

通过属性title配置图标的说明信息;通过属性successTip配置完成复制提示信息。

', + 'en-US': '

copy tip

' + }, + apis: ['TiCopyComponent.properties.title', 'TiCopyComponent.properties.successTip'] + }, + { + demoId: 'copy-table', + name: { + 'zh-CN': '在表格中使用', + 'en-US': 'copy table' + }, + desc: { + 'zh-CN': '

通过元素ti-cell-icons包裹。

', + 'en-US': '

copy table

' + } + }, + { + demoId: 'copy-event', + name: { + 'zh-CN': '事件', + 'en-US': 'copy event' + }, + desc: { + 'zh-CN': '

当点击复制图标后触发copy事件。

', + 'en-US': '

copy event

' + }, + apis: ['TiCopyComponent.events.copy'] + } + ] +}; diff --git a/src/copy/demo/src/app/copy/webdoc/copy.cn.md b/src/copy/demo/src/app/copy/webdoc/copy.cn.md new file mode 100644 index 0000000..8451cc8 --- /dev/null +++ b/src/copy/demo/src/app/copy/webdoc/copy.cn.md @@ -0,0 +1,25 @@ +--- +title: Copy 复制 +--- + +# Copy 复制 + +
+ +复制组件,用于复制文本 + +```typescript +import { TiCopyModule } from '@opentiny/ng'; +``` + +
+ +
+ +复制组件,用于复制文本 + +```typescript +import { TiCopyModule } from '@opentiny/ng'; +``` + +
diff --git a/src/copy/demo/src/app/copy/webdoc/copy.en.md b/src/copy/demo/src/app/copy/webdoc/copy.en.md new file mode 100644 index 0000000..7fd994d --- /dev/null +++ b/src/copy/demo/src/app/copy/webdoc/copy.en.md @@ -0,0 +1,21 @@ +--- +title: Copy +--- + +# Copy + +
+ +```typescript +import { TiCopyModule } from '@opentiny/ng'; +``` + +
+ +
+ +```typescript +import { TiCopyModule } from '@opentiny/ng'; +``` + +
diff --git a/src/copy/demo/src/favicon.ico b/src/copy/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/copy/demo/src/index.html b/src/copy/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/copy/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/copy/demo/src/main.ts b/src/copy/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/copy/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/copy/demo/test.ts b/src/copy/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/copy/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/copy/demo/tsconfig.app.json b/src/copy/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/copy/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/copy/demo/tsconfig.spec.json b/src/copy/demo/tsconfig.spec.json new file mode 100644 index 0000000..e12f108 --- /dev/null +++ b/src/copy/demo/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": ["jasmine"] + }, + "files": ["./test.ts", "../../polyfills.ts"], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/copy/lib/index.ts b/src/copy/lib/index.ts new file mode 100644 index 0000000..386abb5 --- /dev/null +++ b/src/copy/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiCopyModule'; diff --git a/src/copy/lib/ng-package.json b/src/copy/lib/ng-package.json new file mode 100644 index 0000000..130c397 --- /dev/null +++ b/src/copy/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/copy", + "lib": { + "entryFile": "index.ts" + } +} diff --git a/src/copy/lib/package.json b/src/copy/lib/package.json new file mode 100644 index 0000000..4a81713 --- /dev/null +++ b/src/copy/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "@opentiny/ng-copy", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-popup": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/copy/lib/project.json b/src/copy/lib/project.json new file mode 100644 index 0000000..7b5f1cc --- /dev/null +++ b/src/copy/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/copy/lib", + "sourceRoot": "src/copy/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/copy"], + "options": { + "project": "src/copy/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/copy"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js copy" + }, + { + "command": "ng default-build copy" + }, + { + "command": "node build/clear-default-theme.js copy" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/copy && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build copy && ng pack copy && node build/publish.js copy --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/copy/lib/src/TiCopyComponent.ts b/src/copy/lib/src/TiCopyComponent.ts new file mode 100644 index 0000000..319c4bf --- /dev/null +++ b/src/copy/lib/src/TiCopyComponent.ts @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, Renderer2 } from '@angular/core'; + +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiPopUpRef, TiPopupService } from '@opentiny/ng-popup'; +import { Position } from '@opentiny/ng-utils'; +import { TiLocale } from '@opentiny/ng-locale'; +import { Subject, Subscription } from 'rxjs'; +import { delay, throttleTime } from 'rxjs/operators'; + +import { TiToastComponent } from './TiToastComponent'; + +/** + * 复制组件,用于复制文本 + * + */ +@Component({ + selector: 'ti-copy', + templateUrl: './copy.html', + styleUrls: ['./copy.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiCopyComponent extends TiBaseComponent { + constructor(protected hostRef: ElementRef, private Renderer: Renderer2, private tiPopup: TiPopupService) { + super(hostRef, Renderer); + } + @Input() content: string; + /** + * 复制后的即时提示信息(小黑框), + * + * 缺省值:复制成功/Copied successfully.(国际化),支持自定义 + */ + @Input() successTip: string; + /** + * 复制成功后的事件接口 + */ + @Output() readonly copy: EventEmitter = new EventEmitter(); + private clickObserve: Subject = new Subject(); + private clickSub: Subscription; + private hideToast: Subject = new Subject(); + private hideToastSub: Subscription; + private deleteToast: Subject = new Subject(); + private deleteToastSub: Subscription; + + private toast: TiPopUpRef; + private toastRef: any; + private hostSpace: number = 4; // toast距copy图标的距离 + + ngOnInit(): void { + super.ngOnInit(); + // 创建点击事件的observe + this.createClickObserve(); + // 创建hideToast的observe + this.createHideToastObserve(); + // 创建deleteToast的observe + this.createDeleteToastObserve(); + } + /** + * @ignore + * 图标点击事件 + */ + public onClick(): void { + if (!this.content) { + return; + } + this.clickObserve.next(null); + } + private createClickObserve(): void { + this.clickSub = this.clickObserve.pipe(throttleTime(2000)).subscribe((): void => { + // 创建textarea + const textarea: any = this.Renderer.createElement('textarea'); + this.Renderer.appendChild(document.body, textarea); + this.Renderer.setStyle(textarea, 'postion', 'absolute'); + this.Renderer.setStyle(textarea, 'top', '-9999px'); + this.Renderer.setStyle(textarea, 'left', '-9999px'); + this.Renderer.setProperty(textarea, 'value', this.content); + textarea.select(); + document.execCommand('Copy'); + this.Renderer.removeChild(document.body, textarea); + this.copy.emit(); + // 复制成功后,显示即时提示信息 + this.showToast(); + // 显示后隐藏 + this.hideToast.next(null); + }); + } + private createHideToastObserve(): void { + this.hideToastSub = this.hideToast.pipe(delay(1000)).subscribe(() => { + this.Renderer.addClass(this.toastRef.location.nativeElement, 'fadeout'); + // 隐藏后删除 + this.deleteToast.next(null); + }); + } + private createDeleteToastObserve(): void { + this.deleteToastSub = this.deleteToast.pipe(delay(300)).subscribe(() => { + this.toast.hide(); + }); + } + /** + * 复制成功后,显示toast提示 + */ + private showToast(): void { + this.toast = this.tiPopup.create(TiToastComponent); + this.toastRef = this.toast.show({ + content: this.successTip || TiLocale.getLocaleWords().tiCopy.successTip, + container: 'body' + }); + Position.setPosition({ + hostEle: this.hostRef.nativeElement, + targetEle: this.toastRef.location.nativeElement, + hostSpace: this.hostSpace, + fixMaxHeight: true, + determinPositionFn: (layout: any): string => { + if (layout.avilableLayout.right >= layout.targetLayout.width + this.hostSpace) { + return 'right'; + } else { + return 'left'; + } + } + }); + this.Renderer.addClass(this.toastRef.location.nativeElement, 'fadein'); + } + ngOnDestroy(): void { + if (this.toast) { + this.toast.hide(); + } + if (this.clickSub) { + this.clickSub.unsubscribe(); + } + if (this.hideToastSub) { + this.hideToastSub.unsubscribe(); + } + if (this.deleteToastSub) { + this.deleteToastSub.unsubscribe(); + } + } +} diff --git a/src/copy/lib/src/TiCopyModule.ts b/src/copy/lib/src/TiCopyModule.ts new file mode 100644 index 0000000..5a12244 --- /dev/null +++ b/src/copy/lib/src/TiCopyModule.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TiLocaleModule, TiLocale } from '@opentiny/ng-locale'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiPopupModule } from '@opentiny/ng-popup'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiCopyComponent } from './TiCopyComponent'; +import { TiToastComponent } from './TiToastComponent'; + +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, TiLocaleModule, TiPopupModule, TiIconModule, TiOutlineModule], + exports: [TiCopyComponent], + declarations: [TiCopyComponent, TiToastComponent], + entryComponents: [TiToastComponent] +}) +export class TiCopyModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiCopyComponent } from './TiCopyComponent'; diff --git a/src/copy/lib/src/TiToastComponent.ts b/src/copy/lib/src/TiToastComponent.ts new file mode 100644 index 0000000..699b9c2 --- /dev/null +++ b/src/copy/lib/src/TiToastComponent.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * @ignore + */ +@Component({ + selector: 'ti-toast', + template: '', + styleUrls: ['./toast.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiToastComponent {} diff --git a/src/copy/lib/src/copy.html b/src/copy/lib/src/copy.html new file mode 100644 index 0000000..ca60c92 --- /dev/null +++ b/src/copy/lib/src/copy.html @@ -0,0 +1,2 @@ + + diff --git a/src/copy/lib/src/copy.less b/src/copy/lib/src/copy.less new file mode 100644 index 0000000..bad3621 --- /dev/null +++ b/src/copy/lib/src/copy.less @@ -0,0 +1,22 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + color: var(--ti-common-color-icon-normal); + margin-left: var(--ti-common-space-2x); + cursor: pointer; + &:hover { + color: var(--ti-common-color-icon-hover); + } + &[dark] { + color: var(--ti-common-color-icon-darkbg-normal); + &:hover { + color: var(--ti-common-color-icon-darkbg-hover); + } + } +} +ti-table td :host { + visibility: hidden; +} +ti-table td:hover :host { + visibility: visible; +} diff --git a/src/copy/lib/src/i18n/TiCopyWords.ts b/src/copy/lib/src/i18n/TiCopyWords.ts new file mode 100644 index 0000000..bc95f33 --- /dev/null +++ b/src/copy/lib/src/i18n/TiCopyWords.ts @@ -0,0 +1,5 @@ +export interface TiCopyWords { + tiCopy: { + successTip: string; + }; +} diff --git a/src/copy/lib/src/i18n/en_US.ts b/src/copy/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..dffdaf4 --- /dev/null +++ b/src/copy/lib/src/i18n/en_US.ts @@ -0,0 +1,7 @@ +import { TiCopyWords } from './TiCopyWords'; + +export const en_US: TiCopyWords = { + tiCopy: { + successTip: 'Copied successfully.' + } +}; diff --git a/src/copy/lib/src/i18n/es_US.ts b/src/copy/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..2e26f1a --- /dev/null +++ b/src/copy/lib/src/i18n/es_US.ts @@ -0,0 +1,7 @@ +import { TiCopyWords } from './TiCopyWords'; + +export const es_US: TiCopyWords = { + tiCopy: { + successTip: 'Se copió.' + } +}; diff --git a/src/copy/lib/src/i18n/fr_FR.ts b/src/copy/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..5468419 --- /dev/null +++ b/src/copy/lib/src/i18n/fr_FR.ts @@ -0,0 +1,7 @@ +import { TiCopyWords } from './TiCopyWords'; + +export const fr_FR: TiCopyWords = { + tiCopy: { + successTip: 'Copie réussie.' + } +}; diff --git a/src/copy/lib/src/i18n/index.ts b/src/copy/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/copy/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/copy/lib/src/i18n/pt_BR.ts b/src/copy/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..9ca1074 --- /dev/null +++ b/src/copy/lib/src/i18n/pt_BR.ts @@ -0,0 +1,7 @@ +import { TiCopyWords } from './TiCopyWords'; + +export const pt_BR: TiCopyWords = { + tiCopy: { + successTip: 'Copiado com sucesso.' + } +}; diff --git a/src/copy/lib/src/i18n/zh_CN.ts b/src/copy/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..a446a95 --- /dev/null +++ b/src/copy/lib/src/i18n/zh_CN.ts @@ -0,0 +1,7 @@ +import { TiCopyWords } from './TiCopyWords'; + +export const zh_CN: TiCopyWords = { + tiCopy: { + successTip: '复制成功' + } +}; diff --git a/src/copy/lib/src/toast.less b/src/copy/lib/src/toast.less new file mode 100644 index 0000000..cfe528b --- /dev/null +++ b/src/copy/lib/src/toast.less @@ -0,0 +1,27 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + position: absolute; + // display: none; + background-color: var(--ti-common-color-bg-dark-deep); + border-radius: var(--ti-common-border-radius-1); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-gray); + top: -999px; // 添加定位后,不出滚动条,不影响页面结构 + left: -999px; + padding: 12px 16px; // IE下不能及时解析CSSvar,会导致第一次复制定位错误 + padding: calc(var(--ti-common-space-3x) - 1px) calc(var(--ti-common-space-4x) - 1px); + white-space: nowrap; + z-index: 5001; + &.fadein { + animation: fadein 200ms; + } + &.fadeout { + animation: fadeout 300ms forwards; + } +} + +// 定义淡入函数 +.fade-animation(fadein,0,1); +// 定义淡出函数 +.fade-animation(fadeout,1,0); diff --git a/src/crumb/demo/karma.conf.js b/src/crumb/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/crumb/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/crumb/demo/project.json b/src/crumb/demo/project.json new file mode 100644 index 0000000..caa1694 --- /dev/null +++ b/src/crumb/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/crumb/demo", + "sourceRoot": "src/crumb/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/crumb", + "index": "src/crumb/demo/src/index.html", + "main": "src/crumb/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/crumb/demo/tsconfig.app.json", + "assets": ["src/crumb/demo/src/favicon.ico", "src/crumb/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "crumb-demo:build:production" + }, + "development": { + "browserTarget": "crumb-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js crumb" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/crumb/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/crumb/demo/tsconfig.spec.json", + "karmaConfig": "src/crumb/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/crumb/demo/src/app/AppComponent.ts b/src/crumb/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/crumb/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/crumb/demo/src/app/AppModule.ts b/src/crumb/demo/src/app/AppModule.ts new file mode 100644 index 0000000..9c9a22c --- /dev/null +++ b/src/crumb/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { CrumbTestModule } from './crumb/CrumbTestModule'; + +@NgModule({ + imports: [ + CrumbTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/crumb/demo/src/app/IndexComponent.ts b/src/crumb/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..230530f --- /dev/null +++ b/src/crumb/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { CrumbTestModule } from './crumb/CrumbTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = CrumbTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/crumb/demo/src/app/app.html b/src/crumb/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/crumb/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/crumb/demo/src/app/crumb/CrumbBasicComponent.ts b/src/crumb/demo/src/app/crumb/CrumbBasicComponent.ts new file mode 100644 index 0000000..6e6fe47 --- /dev/null +++ b/src/crumb/demo/src/app/crumb/CrumbBasicComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { TiLink } from '@opentiny/ng'; + +@Component({ + templateUrl: './crumb-basic.html' +}) +export class CrumbBasicComponent { + items: Array = [ + { + label: '服装' + }, + { + label: '女装' + }, + { + label: '裙子' + }, + { + label: '连衣裙' + }, + { + label: '复古' + } + ]; +} diff --git a/src/crumb/demo/src/app/crumb/CrumbEventsComponent.ts b/src/crumb/demo/src/app/crumb/CrumbEventsComponent.ts new file mode 100644 index 0000000..12615b6 --- /dev/null +++ b/src/crumb/demo/src/app/crumb/CrumbEventsComponent.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { TiLink } from '@opentiny/ng'; + +@Component({ + templateUrl: './crumb-events.html' +}) +export class CrumbEventsComponent { + myLogs: Array = []; + items: Array = [ + { + label: '服装' + }, + { + label: '女装' + }, + { + label: '裙子' + }, + { + label: '连衣裙' + }, + { + label: '复古' + } + ]; + + onSelect(item: TiLink): void { + this.myLogs = [...this.myLogs, `onSelect() item = ${JSON.stringify(item)}`]; + } +} diff --git a/src/crumb/demo/src/app/crumb/CrumbHrefComponent.ts b/src/crumb/demo/src/app/crumb/CrumbHrefComponent.ts new file mode 100644 index 0000000..6a8d286 --- /dev/null +++ b/src/crumb/demo/src/app/crumb/CrumbHrefComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { TiLink } from '@opentiny/ng'; + +@Component({ + templateUrl: './crumb-href.html' +}) +export class CrumbHrefComponent { + items: Array = [ + { + label: '服装', + href: '', + target: '_blank' + }, + { + label: '女装', + href: '', + target: '_parent' + }, + { + label: '裙子', + href: '', + target: '_top' + }, + { + label: '连衣裙', + href: '' + }, + { + label: '复古' + } + ]; +} diff --git a/src/crumb/demo/src/app/crumb/CrumbRouterComponent.ts b/src/crumb/demo/src/app/crumb/CrumbRouterComponent.ts new file mode 100644 index 0000000..2f7e1e3 --- /dev/null +++ b/src/crumb/demo/src/app/crumb/CrumbRouterComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { TiLink } from '@opentiny/ng'; + +@Component({ + templateUrl: './crumb-router.html' +}) +export class CrumbRouterComponent { + myLogs: Array = []; + items: Array = [ + { + label: 'Home', + routerLink: '' + }, + { + label: 'Crumb', + routerLink: '', + queryParams: { + name: 'crumb' + } + }, + { + label: 'Crumb Router' + } + ]; +} diff --git a/src/crumb/demo/src/app/crumb/CrumbRouterTestComponent.ts b/src/crumb/demo/src/app/crumb/CrumbRouterTestComponent.ts new file mode 100644 index 0000000..66bb23f --- /dev/null +++ b/src/crumb/demo/src/app/crumb/CrumbRouterTestComponent.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; +import { TiLink } from '@opentiny/ng'; +import { ActivatedRoute, Router } from '@angular/router'; + +@Component({ + templateUrl: './crumb-router-test.html' +}) +export class CrumbRouterTestComponent { + constructor(private router: Router, private activeRoute: ActivatedRoute) {} + myLogs: Array = []; + items: Array = [ + { + label: '第一级' + }, + { + label: '第二级', + routerLink: 'router2', + // 参数传递 + queryParams: { + name: 'router' + } + }, + { + label: '第三级', + href: '#/crumb/crumb-router-test/router3' // 相对路径跳转 + }, + { + label: '第四级', + routerLink: 'router2' + }, + { + label: '最后一级' + } + ]; + + onSelect(item: any): void { + if (item.label === '第一级') { + this.router.navigate(['./router1'], { relativeTo: this.activeRoute }); + } + } +} diff --git a/src/crumb/demo/src/app/crumb/CrumbTestModule.ts b/src/crumb/demo/src/app/crumb/CrumbTestModule.ts new file mode 100644 index 0000000..ec6bbbe --- /dev/null +++ b/src/crumb/demo/src/app/crumb/CrumbTestModule.ts @@ -0,0 +1,59 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiCrumbModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { CrumbBasicComponent } from './CrumbBasicComponent'; +import { CrumbEventsComponent } from './CrumbEventsComponent'; +import { CrumbRouterComponent } from './CrumbRouterComponent'; +import { Router1Component } from './Router1Component'; +import { Router2Component } from './Router2Component'; +import { Router3Component } from './Router3Component'; +import { CrumbHrefComponent } from './CrumbHrefComponent'; +import { CrumbRouterTestComponent } from './CrumbRouterTestComponent'; + +@NgModule({ + imports: [CommonModule, TiCrumbModule, DemoLogModule, RouterModule.forChild(CrumbTestModule.ROUTES)], + declarations: [ + CrumbBasicComponent, + CrumbEventsComponent, + CrumbRouterComponent, + Router1Component, + Router2Component, + Router3Component, + CrumbHrefComponent, + CrumbRouterTestComponent + ] +}) +export class CrumbTestModule { + static readonly LINKS: Array = [{ label: 'Crumb' }]; + static readonly ROUTES: Routes = [ + { + path: 'crumb/crumb-basic', + component: CrumbBasicComponent + }, + { + path: 'crumb/crumb-events', + component: CrumbEventsComponent + }, + { + path: 'crumb/crumb-router-test', + component: CrumbRouterTestComponent, + children: [ + { path: 'router1', component: Router1Component }, + { path: 'router2', component: Router2Component }, + { path: 'router3', component: Router3Component } + ] + }, + { + path: 'crumb/crumb-href', + component: CrumbHrefComponent + }, + { + path: 'crumb/crumb-router', + component: CrumbRouterComponent + } + ]; +} diff --git a/src/crumb/demo/src/app/crumb/Router1Component.ts b/src/crumb/demo/src/app/crumb/Router1Component.ts new file mode 100644 index 0000000..72d5c30 --- /dev/null +++ b/src/crumb/demo/src/app/crumb/Router1Component.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + template: `
Welcome to router1
` +}) +export class Router1Component {} diff --git a/src/crumb/demo/src/app/crumb/Router2Component.ts b/src/crumb/demo/src/app/crumb/Router2Component.ts new file mode 100644 index 0000000..72fe7ef --- /dev/null +++ b/src/crumb/demo/src/app/crumb/Router2Component.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + template: `
Welcome to router2
` +}) +export class Router2Component {} diff --git a/src/crumb/demo/src/app/crumb/Router3Component.ts b/src/crumb/demo/src/app/crumb/Router3Component.ts new file mode 100644 index 0000000..89a92b3 --- /dev/null +++ b/src/crumb/demo/src/app/crumb/Router3Component.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + template: `
Welcome to router3
` +}) +export class Router3Component {} diff --git a/src/crumb/demo/src/app/crumb/crumb-basic.html b/src/crumb/demo/src/app/crumb/crumb-basic.html new file mode 100644 index 0000000..8ba7bd3 --- /dev/null +++ b/src/crumb/demo/src/app/crumb/crumb-basic.html @@ -0,0 +1 @@ + diff --git a/src/crumb/demo/src/app/crumb/crumb-events.html b/src/crumb/demo/src/app/crumb/crumb-events.html new file mode 100644 index 0000000..e805b27 --- /dev/null +++ b/src/crumb/demo/src/app/crumb/crumb-events.html @@ -0,0 +1,3 @@ + + + diff --git a/src/crumb/demo/src/app/crumb/crumb-href.html b/src/crumb/demo/src/app/crumb/crumb-href.html new file mode 100644 index 0000000..b6986b8 --- /dev/null +++ b/src/crumb/demo/src/app/crumb/crumb-href.html @@ -0,0 +1 @@ + diff --git a/src/crumb/demo/src/app/crumb/crumb-router-test.html b/src/crumb/demo/src/app/crumb/crumb-router-test.html new file mode 100644 index 0000000..1d2e973 --- /dev/null +++ b/src/crumb/demo/src/app/crumb/crumb-router-test.html @@ -0,0 +1,19 @@ +

1.描述

+

路由跳转测试, 支持href和routerLink 两种形式

+

使用routerLink实现路由跳转时,可设置queryParams属性传递参数

+

2.示例

+ +
+ +
+ + diff --git a/src/crumb/demo/src/app/crumb/crumb-router.html b/src/crumb/demo/src/app/crumb/crumb-router.html new file mode 100644 index 0000000..5dac317 --- /dev/null +++ b/src/crumb/demo/src/app/crumb/crumb-router.html @@ -0,0 +1 @@ + diff --git a/src/crumb/demo/src/app/crumb/webdoc/crumb-demos.js b/src/crumb/demo/src/app/crumb/webdoc/crumb-demos.js new file mode 100644 index 0000000..181a417 --- /dev/null +++ b/src/crumb/demo/src/app/crumb/webdoc/crumb-demos.js @@ -0,0 +1,63 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'crumb-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': '

Crumb 组件的最简用法。

', + 'en-US': '' + }, + apis: [ + 'TiCrumbComponent.properties.items', + 'TiLink.properties.label' + ], + }, + { + demoId: 'crumb-href', + name: { + 'zh-CN': '链接跳转', + 'en-US': 'href' + }, + desc: { + 'zh-CN': '

通过属性item.href配置导航项跳转链接地址;通过属性item.target配置导航项链接打开方式。

', + 'en-US': '' + }, + apis: [ + 'TiLink.properties.href', + 'TiLink.properties.target' + ], + }, + { + demoId: 'crumb-router', + name: { + 'zh-CN': '路由跳转', + 'en-US': 'router' + }, + desc: { + 'zh-CN': '

通过属性item.routerLink配置导航项跳转路由;通过属性item.queryParams配置导航项跳转路由参数。

', + 'en-US': '' + }, + apis: [ + 'TiLink.properties.routerLink', + 'TiLink.properties.queryParams' + ], + }, + { + demoId: 'crumb-events', + name: { + 'zh-CN': '事件', + 'en-US': 'events' + }, + desc: { + 'zh-CN': '

当选中非最后一级导航项的时候触发select事件,传递出去的参数为当前选中项的数据。

', + 'en-US': '' + }, + apis: ['TiCrumbComponent.events.select'], + } + ], +}; diff --git a/src/crumb/demo/src/app/crumb/webdoc/crumb.cn.md b/src/crumb/demo/src/app/crumb/webdoc/crumb.cn.md new file mode 100644 index 0000000..d63cdcd --- /dev/null +++ b/src/crumb/demo/src/app/crumb/webdoc/crumb.cn.md @@ -0,0 +1,24 @@ +--- +title: Crumb 面包屑 +--- +# Crumb 面包屑 + +
+ +用于显示当前页面在系统层级结构中位置的组件。 + +```typescript +import { TiCrumbModule } from '@opentiny/ng'; +``` + +
+ +
+ +用于显示当前页面在系统层级结构中位置的组件。 + +```typescript +import { TiCrumbModule } from '@opentiny/ng'; +``` + +
diff --git a/src/crumb/demo/src/app/crumb/webdoc/crumb.en.md b/src/crumb/demo/src/app/crumb/webdoc/crumb.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/crumb/demo/src/app/crumb/webdoc/crumb.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/crumb/demo/src/favicon.ico b/src/crumb/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/crumb/demo/src/index.html b/src/crumb/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/crumb/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/crumb/demo/src/main.ts b/src/crumb/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/crumb/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/crumb/demo/test.ts b/src/crumb/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/crumb/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/crumb/demo/tsconfig.app.json b/src/crumb/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/crumb/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/crumb/demo/tsconfig.spec.json b/src/crumb/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/crumb/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/crumb/lib/index.ts b/src/crumb/lib/index.ts new file mode 100644 index 0000000..2163897 --- /dev/null +++ b/src/crumb/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiCrumbModule'; diff --git a/src/crumb/lib/ng-package.json b/src/crumb/lib/ng-package.json new file mode 100644 index 0000000..f63bb21 --- /dev/null +++ b/src/crumb/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/crumb", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/crumb/lib/package.json b/src/crumb/lib/package.json new file mode 100644 index 0000000..26d8506 --- /dev/null +++ b/src/crumb/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-crumb", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/router": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/crumb/lib/project.json b/src/crumb/lib/project.json new file mode 100644 index 0000000..dbdbbb6 --- /dev/null +++ b/src/crumb/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/crumb/lib", + "sourceRoot": "src/crumb/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/crumb"], + "options": { + "project": "src/crumb/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/crumb"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js crumb" + }, + { + "command": "ng default-build crumb" + }, + { + "command": "node build/clear-default-theme.js crumb" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/crumb && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build crumb && ng pack crumb && node build/publish.js crumb --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/crumb/lib/src/TiCrumbComponent.ts b/src/crumb/lib/src/TiCrumbComponent.ts new file mode 100644 index 0000000..fd121a8 --- /dev/null +++ b/src/crumb/lib/src/TiCrumbComponent.ts @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, Output, Renderer2 } from '@angular/core'; +import { Params } from '@angular/router'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +export interface TiLink { + /** + * 必选,导航项文本 + */ + label: string; + /** + * 导航项链接地址 + */ + href?: string; + /** + * 导航项链接打开方式 + * @default '_self' + */ + target?: '_self' | '_blank' | '_parent' | '_top'; + /** + * 导航项跳转路由 + */ + routerLink?: string | Array; + /** + * 导航项跳转路由参数 + */ + queryParams?: Params; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} + +/** + * crumb锚点组件 + * + * 锚点组件除最后一级外,其余几级一般都为可跳转的链接,链接地址可以通过href属性设置; + * 用户也可通过添加事件,实现业务逻辑。 + * + */ + +@Component({ + selector: 'ti-crumb', + templateUrl: './crumb.html', + styleUrls: ['./crumb.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiCrumbComponent extends TiBaseComponent { + protected versionInfo: string = super.getVersion(packageInfo); + /** + * 必选,导航项数据集 + */ + @Input() items: Array; + /** + * 选中非最后一级导航项时触发的回调,参数:当前选中项数据 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + + constructor(private elementRef: ElementRef, private renderer2: Renderer2, private changeDetectorRef: ChangeDetectorRef) { + super(elementRef, renderer2); + } + + ngDoCheck(): void { + super.ngDoCheck(); + this.changeDetectorRef.markForCheck(); + } + + /** + * @ignore + */ + public onClick(item: TiLink): boolean { + this.select.emit(item); + // 不设置链接时,阻止跳转(a标签在click事件返回值为false时,会阻止默认行为) + if (!item.href) { + return false; + } + } +} diff --git a/src/crumb/lib/src/TiCrumbModule.ts b/src/crumb/lib/src/TiCrumbModule.ts new file mode 100644 index 0000000..08d0579 --- /dev/null +++ b/src/crumb/lib/src/TiCrumbModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; + +import { TiCrumbComponent } from './TiCrumbComponent'; + +@NgModule({ + imports: [CommonModule, RouterModule], + exports: [TiCrumbComponent], + declarations: [TiCrumbComponent] +}) +export class TiCrumbModule {} +export { TiCrumbComponent, TiLink } from './TiCrumbComponent'; diff --git a/src/crumb/lib/src/crumb.html b/src/crumb/lib/src/crumb.html new file mode 100644 index 0000000..52f04d3 --- /dev/null +++ b/src/crumb/lib/src/crumb.html @@ -0,0 +1,27 @@ + + + {{item.label}} + {{item.label}} + / + + {{item.label}} + diff --git a/src/crumb/lib/src/crumb.less b/src/crumb/lib/src/crumb.less new file mode 100644 index 0000000..d5828fc --- /dev/null +++ b/src/crumb/lib/src/crumb.less @@ -0,0 +1,34 @@ +:host { + line-height: var(--ti-common-line-height-number); +} + +// 用户设置href属性时文本样式 +.tp-crumb-label { + color: var(--ti-common-color-text-link); + text-decoration: none; + vertical-align: bottom; + &:hover { + text-decoration: underline; + text-decoration-color: var(--ti-common-color-text-link-hover); + color: var(--ti-common-color-text-link-hover); + } +} + +// 用户未设置href属性、最后一级时文本样式 +.tp-crumb-nohref-label { + color: var(--ti-common-color-text-primary); + font-weight: bold; + vertical-align: bottom; + text-decoration: none; + &:hover { + color: var(--ti-common-color-text-primary); + } +} + +// 两级文本间分割线样式 +.tp-crumb-divider { + display: inline-block; + width: var(--ti-common-size-4x); + color: var(--ti-common-color-text-weaken); + text-align: center; +} diff --git a/src/date/demo/karma.conf.js b/src/date/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/date/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/date/demo/project.json b/src/date/demo/project.json new file mode 100644 index 0000000..0e96fd0 --- /dev/null +++ b/src/date/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/date/demo", + "sourceRoot": "src/date/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/date", + "index": "src/date/demo/src/index.html", + "main": "src/date/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/date/demo/tsconfig.app.json", + "assets": ["src/date/demo/src/favicon.ico", "src/date/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "date-demo:build:production" + }, + "development": { + "browserTarget": "date-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js date" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/date/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/date/demo/tsconfig.spec.json", + "karmaConfig": "src/date/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/date/demo/src/app/AppComponent.ts b/src/date/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/date/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/date/demo/src/app/AppModule.ts b/src/date/demo/src/app/AppModule.ts new file mode 100644 index 0000000..812b8df --- /dev/null +++ b/src/date/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { DateTestModule } from './date/DateTestModule'; + +@NgModule({ + imports: [ + DateTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/date/demo/src/app/IndexComponent.ts b/src/date/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..182ab31 --- /dev/null +++ b/src/date/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { DateTestModule } from './date/DateTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = DateTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/date/demo/src/app/app.html b/src/date/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/date/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/date/demo/src/app/date/DateCleariconComponent.ts b/src/date/demo/src/app/date/DateCleariconComponent.ts new file mode 100644 index 0000000..45464d6 --- /dev/null +++ b/src/date/demo/src/app/date/DateCleariconComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './date-clearicon.html' +}) +export class DateCleariconComponent { + value: Date = new Date(2015, 8, 2); +} diff --git a/src/date/demo/src/app/date/DateCustomizeComponent.ts b/src/date/demo/src/app/date/DateCustomizeComponent.ts new file mode 100644 index 0000000..30f78a2 --- /dev/null +++ b/src/date/demo/src/app/date/DateCustomizeComponent.ts @@ -0,0 +1,38 @@ +import { Component } from '@angular/core'; +import { TiDateCustomizeOptions } from '@opentiny/ng'; + +@Component({ + templateUrl: './date-customize.html' +}) +export class DateCustomizeComponent { + nowTime: Date = new Date(); + nowYear: number = this.nowTime.getFullYear(); + nowMonth: number = this.nowTime.getMonth(); + nowDate: number = this.nowTime.getDate(); + value: Date = null; + customizeOptions: Array = [ + { + label: '今天', + value: { + begin: this.nowTime + } + }, + { + label: '明天', + value: { + begin: new Date(this.nowYear, this.nowMonth, this.nowDate + 1) + } + }, + { + label: '一周前', + value: { + begin: new Date(this.nowYear, this.nowMonth, this.nowDate - 6) + } + } + ]; + myLogs: Array = []; + + onCustomizeOptionClick(model: Date): void { + this.myLogs = [...this.myLogs, `customizeOptionClick() model = ${JSON.stringify(model)}`]; + } +} diff --git a/src/date/demo/src/app/date/DateDisabledComponent.ts b/src/date/demo/src/app/date/DateDisabledComponent.ts new file mode 100644 index 0000000..423b36c --- /dev/null +++ b/src/date/demo/src/app/date/DateDisabledComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './date-disabled.html' +}) +export class DateDisabledComponent { + disabled: boolean = true; + value: Date = new Date(2015, 8, 2); +} diff --git a/src/date/demo/src/app/date/DateDisableddaysComponent.ts b/src/date/demo/src/app/date/DateDisableddaysComponent.ts new file mode 100644 index 0000000..ff71580 --- /dev/null +++ b/src/date/demo/src/app/date/DateDisableddaysComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './date-disableddays.html' +}) +export class DateDisableddaysComponent { + value: Date = null; + nowTime: Date = new Date(); + nowYear: number = this.nowTime.getFullYear(); + nowMonth: number = this.nowTime.getMonth(); + nowDate: number = this.nowTime.getDate(); + disabledDays: Array = [ + new Date(this.nowYear, this.nowMonth, this.nowDate), + new Date(this.nowYear, this.nowMonth + 1, this.nowDate) + ]; +} diff --git a/src/date/demo/src/app/date/DateEventComponent.ts b/src/date/demo/src/app/date/DateEventComponent.ts new file mode 100644 index 0000000..02c6f0b --- /dev/null +++ b/src/date/demo/src/app/date/DateEventComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './date-event.html' +}) +export class DateEventComponent { + value: Date = null; + myLogs: Array = []; + + onNgModelChange(value: Date): void { + this.myLogs = [...this.myLogs, `onNgModelChange() value = ${value}`]; + } +} diff --git a/src/date/demo/src/app/date/DateFormComponent.ts b/src/date/demo/src/app/date/DateFormComponent.ts new file mode 100644 index 0000000..0e5b666 --- /dev/null +++ b/src/date/demo/src/app/date/DateFormComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { FormGroup, FormControl } from '@angular/forms'; + +@Component({ + templateUrl: './date-form.html' +}) +export class DateFormComponent { + myFormControls: FormGroup; + + ngOnInit(): void { + this.myFormControls = new FormGroup({ + // 初始化值为null + field1: new FormControl(null), + // 初始化值为正常日期 + field2: new FormControl(new Date(2025, 8, 27)) + }); + } + + onNgModelChange(model: Date): void { + console.log(model, 'model'); + } +} diff --git a/src/date/demo/src/app/date/DateFormatComponent.ts b/src/date/demo/src/app/date/DateFormatComponent.ts new file mode 100644 index 0000000..4e703e8 --- /dev/null +++ b/src/date/demo/src/app/date/DateFormatComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './date-format.html' +}) +export class DateFormatComponent { + dayFormat: string = 'yyyy-MM-dd'; + monthFormat: string = 'yyyy/M'; + seasonFormat: string = 'YYYY/QQ'; + yearFormat: string = 'yyyy'; + + dayValue: Date = null; + monthValue: Date = null; + seasonValue: Date = null; + yearValue: Date = null; +} diff --git a/src/date/demo/src/app/date/DateFormatTestComponent.ts b/src/date/demo/src/app/date/DateFormatTestComponent.ts new file mode 100644 index 0000000..09bd5bc --- /dev/null +++ b/src/date/demo/src/app/date/DateFormatTestComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './date-format-test.html' +}) +export class DateFormatTestComponent { + value: Date = new Date(2015, 8, 2); + value1: Date = new Date(); + format1: string = 'yyyy'; + value2: Date = new Date(1991, 6, 2); + format2: string = 'yyyy/M'; + value3: Date = new Date(2017, 8, 27); + format3: string = 'yyyy.MM.d'; + value4: Date = new Date(2021, 6, 1); + format4: string = 'YYYY/QQ'; + + clickFn(): void { + this.format3 = 'yyyy/MM/d'; + } + changeQuarterVal(): void { + this.value4 = new Date(2021, 9, 1); + } +} diff --git a/src/date/demo/src/app/date/DateMaxComponent.ts b/src/date/demo/src/app/date/DateMaxComponent.ts new file mode 100644 index 0000000..6bc3928 --- /dev/null +++ b/src/date/demo/src/app/date/DateMaxComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './date-max.html' +}) +export class DateMaxComponent { + value: Date = new Date(1992, 4, 12); + value1: Date = new Date(1994, 2, 8); + min: Date = new Date(1991, 6, 2); + max: Date | undefined = undefined; + maxValue1: Date = new Date(2014, 8, 27); + + maxValueClick(): void { + this.maxValue1 = new Date(1995, 3, 5); + } + maxValueClick2(): void { + this.maxValue1 = undefined; + } +} diff --git a/src/date/demo/src/app/date/DateMaxminComponent.ts b/src/date/demo/src/app/date/DateMaxminComponent.ts new file mode 100644 index 0000000..02be86a --- /dev/null +++ b/src/date/demo/src/app/date/DateMaxminComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './date-maxmin.html' +}) +export class DateMaxminComponent { + min: Date = new Date(2021, 11, 25); + max: Date = new Date(2023, 1, 14); + value1: Date = null; + value2: Date = null; + value3: Date = null; +} diff --git a/src/date/demo/src/app/date/DateMaxminTestComponent.ts b/src/date/demo/src/app/date/DateMaxminTestComponent.ts new file mode 100644 index 0000000..6cd6c41 --- /dev/null +++ b/src/date/demo/src/app/date/DateMaxminTestComponent.ts @@ -0,0 +1,54 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './date-maxmin-test.html' +}) +export class DateMaxminTestComponent { + nowTime: Date = new Date(); + delayTime: number = 60 * 60 * 1000 * 24 * 10; // 10天 + min: Date = new Date(); + max: Date = new Date(); + min1: Date = new Date(); + max1: Date = new Date(); + min2: Date = new Date(); + max2: Date = new Date(); + min3: Date = new Date(); + max3: Date = new Date(); + min4: Date = new Date(2029, 9, 30); + max4: Date = new Date(2020, 9, 30); + format: string; + format1: string = 'yyyy'; + value: Date; + value1: Date; + value2: Date; + value3: Date; + value4: Date = new Date(); + value5: Date = this.min4; + value6: Date = this.max4; + + changeFormat(): void { + this.format = 'yyyy'; + } + + changeFormat1(): void { + this.format = 'yyyy/MM'; + } + + changeFormat2(): void { + this.format = 'YYYY/QQ'; + } + + changeFormat3(): void { + this.format = 'yyyy/MM/dd'; + } + + onMaxAndMinClick(): void { + this.max1 = new Date(this.nowTime.getTime() - this.delayTime); + this.min1 = new Date(this.nowTime.getTime() + this.delayTime); + } + + onMaxAndMinClick1(): void { + this.max2 = new Date(this.nowTime.getTime() + this.delayTime); + this.min2 = new Date(this.nowTime.getTime() - this.delayTime); + } +} diff --git a/src/date/demo/src/app/date/DateMinComponent.ts b/src/date/demo/src/app/date/DateMinComponent.ts new file mode 100644 index 0000000..2435c1c --- /dev/null +++ b/src/date/demo/src/app/date/DateMinComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './date-min.html' +}) +export class DateMinComponent { + min: Date | undefined = undefined; + value: Date = new Date(1992, 4, 12); + + value1: Date = new Date(1994, 2, 8); + min1: Date = new Date(1996, 7, 1); + + value2: Date = new Date(1994, 2, 8); + min2: Date = new Date(1991, 6, 2); + max: Date = new Date(2015, 2, 6); + + minValueClick(): void { + this.min2 = new Date(1990, 3, 5); + } + minValueClick2(): void { + this.min2 = undefined; + } +} diff --git a/src/date/demo/src/app/date/DateNowdatetimeComponent.ts b/src/date/demo/src/app/date/DateNowdatetimeComponent.ts new file mode 100644 index 0000000..510daf6 --- /dev/null +++ b/src/date/demo/src/app/date/DateNowdatetimeComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './date-nowdatetime.html' +}) +export class DateNowdatetimeComponent { + nowDateTime: Date = new Date(2022, 1, 12); + value: Date = null; +} diff --git a/src/date/demo/src/app/date/DatePanelalignComponent.ts b/src/date/demo/src/app/date/DatePanelalignComponent.ts new file mode 100644 index 0000000..17e9039 --- /dev/null +++ b/src/date/demo/src/app/date/DatePanelalignComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './date-panelalign.html' +}) +export class DatePanelalignComponent { + panelAlign: string = 'right'; + value: Date = new Date(2015, 8, 2); +} diff --git a/src/date/demo/src/app/date/DateTestModule.ts b/src/date/demo/src/app/date/DateTestModule.ts new file mode 100644 index 0000000..9af1d95 --- /dev/null +++ b/src/date/demo/src/app/date/DateTestModule.ts @@ -0,0 +1,111 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiDateModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { DatePanelalignComponent } from './DatePanelalignComponent'; +import { DateValidationComponent } from './DateValidationComponent'; +import { DateValueComponent } from './DateValueComponent'; +import { DateMinComponent } from './DateMinComponent'; +import { DateFormatComponent } from './DateFormatComponent'; +import { DateDisabledComponent } from './DateDisabledComponent'; +import { DateMaxComponent } from './DateMaxComponent'; +import { DateNowdatetimeComponent } from './DateNowdatetimeComponent'; +import { DateFormComponent } from './DateFormComponent'; +import { DateMaxminComponent } from './DateMaxminComponent'; +import { DateEventComponent } from './DateEventComponent'; +import { DateCustomizeComponent } from './DateCustomizeComponent'; +import { DateDisableddaysComponent } from './DateDisableddaysComponent'; +import { DateValueTestComponent } from './DateValueTestComponent'; +import { DateFormatTestComponent } from './DateFormatTestComponent'; +import { DateMaxminTestComponent } from './DateMaxminTestComponent'; +import { DateCleariconComponent } from './DateCleariconComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiDateModule, + TiValidationModule, + DemoLogModule, + RouterModule.forChild(DateTestModule.ROUTES) + ], + declarations: [ + DatePanelalignComponent, + DateValidationComponent, + DateNowdatetimeComponent, + DateValueComponent, + DateMaxComponent, + DateMaxminComponent, + DateMinComponent, + DateFormatComponent, + DateEventComponent, + DateDisabledComponent, + DateCustomizeComponent, + DateDisableddaysComponent, + DateFormComponent, + DateValueTestComponent, + DateFormatTestComponent, + DateMaxminTestComponent, + DateCleariconComponent + ] +}) +export class DateTestModule { + static readonly LINKS: Array = [{ href: 'components/TiDateComponent.html', label: 'Date' }]; + static readonly ROUTES: Routes = [ + { + path: 'date/date-value', + component: DateValueComponent + }, + { + path: 'date/date-format', + component: DateFormatComponent + }, + { + path: 'date/date-max', + component: DateMaxComponent + }, + { + path: 'date/date-min', + component: DateMinComponent + }, + { + path: 'date/date-panelalign', + component: DatePanelalignComponent + }, + { + path: 'date/date-nowdatetime', + component: DateNowdatetimeComponent + }, + { + path: 'date/date-validation', + component: DateValidationComponent + }, + { + path: 'date/date-disabled', + component: DateDisabledComponent + }, + { + path: 'date/date-customize', + component: DateCustomizeComponent + }, + { + path: 'date/date-disableddays', + component: DateDisableddaysComponent + }, + { + path: 'date/date-event', + component: DateEventComponent + }, + { path: 'date/date-form', component: DateFormComponent }, + { path: 'date/date-maxmin', component: DateMaxminComponent }, + { path: 'date/date-value-test', component: DateValueTestComponent }, + { path: 'date/date-format-test', component: DateFormatTestComponent }, + { path: 'date/date-maxmin-test', component: DateMaxminTestComponent }, + { path: 'date/date-clearicon', component: DateCleariconComponent } + ]; +} diff --git a/src/date/demo/src/app/date/DateValidationComponent.ts b/src/date/demo/src/app/date/DateValidationComponent.ts new file mode 100644 index 0000000..090ef26 --- /dev/null +++ b/src/date/demo/src/app/date/DateValidationComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './date-validation.html' +}) +export class DateValidationComponent { + value: Date = new Date(2015, 8, 2); + validation: TiValidationConfig = { + tipPosition: 'top' + }; +} diff --git a/src/date/demo/src/app/date/DateValueComponent.ts b/src/date/demo/src/app/date/DateValueComponent.ts new file mode 100644 index 0000000..d3b0f98 --- /dev/null +++ b/src/date/demo/src/app/date/DateValueComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './date-value.html' +}) +export class DateValueComponent { + value1: Date = null; + value2: Date = new Date(2008, 7, 8); +} diff --git a/src/date/demo/src/app/date/DateValueTestComponent.ts b/src/date/demo/src/app/date/DateValueTestComponent.ts new file mode 100644 index 0000000..23ab291 --- /dev/null +++ b/src/date/demo/src/app/date/DateValueTestComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './date-value-test.html' +}) +export class DateValueTestComponent { + id: string = 'date'; + min: Date = new Date(1991, 6, 2); + max: Date = new Date(2017, 8, 27); + format: string = 'yyyy.M.d'; + value: string = ''; + + value1: any = null; + format1: string = 'yyyy-M-d'; + value2: Date = this.min; + format2: string = 'yyyy/M/d'; + value3: Date = this.max; + format3: string = 'yyyy-MM-dd'; + + ngModelChange(model: Date): void { + console.log(model, 'model'); + } +} diff --git a/src/date/demo/src/app/date/date-clearicon.html b/src/date/demo/src/app/date/date-clearicon.html new file mode 100644 index 0000000..64f1322 --- /dev/null +++ b/src/date/demo/src/app/date/date-clearicon.html @@ -0,0 +1 @@ + diff --git a/src/date/demo/src/app/date/date-customize.html b/src/date/demo/src/app/date/date-customize.html new file mode 100644 index 0000000..3e9fadc --- /dev/null +++ b/src/date/demo/src/app/date/date-customize.html @@ -0,0 +1,8 @@ + + + diff --git a/src/date/demo/src/app/date/date-disabled.html b/src/date/demo/src/app/date/date-disabled.html new file mode 100644 index 0000000..df9f609 --- /dev/null +++ b/src/date/demo/src/app/date/date-disabled.html @@ -0,0 +1 @@ + diff --git a/src/date/demo/src/app/date/date-disableddays.html b/src/date/demo/src/app/date/date-disableddays.html new file mode 100644 index 0000000..80644d3 --- /dev/null +++ b/src/date/demo/src/app/date/date-disableddays.html @@ -0,0 +1 @@ + diff --git a/src/date/demo/src/app/date/date-event.html b/src/date/demo/src/app/date/date-event.html new file mode 100644 index 0000000..39ccf21 --- /dev/null +++ b/src/date/demo/src/app/date/date-event.html @@ -0,0 +1,3 @@ + + + diff --git a/src/date/demo/src/app/date/date-form.html b/src/date/demo/src/app/date/date-form.html new file mode 100644 index 0000000..5ba452a --- /dev/null +++ b/src/date/demo/src/app/date/date-form.html @@ -0,0 +1,13 @@ +

1.描述

+

date 在响应式表单中使用的场景测试

+

2.示例

+
+
+
+

2.1 初始化值为null

+ +
+

2.2 初始化值为正常日期

+ +
+
diff --git a/src/date/demo/src/app/date/date-format-test.html b/src/date/demo/src/app/date/date-format-test.html new file mode 100644 index 0000000..a7d387c --- /dev/null +++ b/src/date/demo/src/app/date/date-format-test.html @@ -0,0 +1,29 @@ +

1.描述

+

format接口测试

+

2.示例

+

(2.1)不设置时,使用默认类型。中文:'yyyy/MM/dd';英文:"MMM dd, yyyy"。

+ +
+
+
+

(2.2)只设置年

+ +
+
+
+

(2.3)只设置年月

+ +
+
+
+

(2.4)动态变更

+ +
+
+

(2.5)季度面板展示

+ +
+季度面板值:{{value4}} +

+ + diff --git a/src/date/demo/src/app/date/date-format.html b/src/date/demo/src/app/date/date-format.html new file mode 100644 index 0000000..0fc1f80 --- /dev/null +++ b/src/date/demo/src/app/date/date-format.html @@ -0,0 +1,7 @@ + +
+ +
+ +
+ diff --git a/src/date/demo/src/app/date/date-max.html b/src/date/demo/src/app/date/date-max.html new file mode 100644 index 0000000..3d12020 --- /dev/null +++ b/src/date/demo/src/app/date/date-max.html @@ -0,0 +1,17 @@ +

1.描述

+

max接口测试

+

2.示例

+

(2.1)最大值设置为非字符串类型、非法日期时:最大值取默认值

+ +
+
+
+

(2.2)动态变更

+ +
+最小值:{{min | date: "yyyy-MM-dd"}} +
+最大值: {{ maxValue1 | date: "yyyy-MM-dd"}} +
+ + diff --git a/src/date/demo/src/app/date/date-maxmin-test.html b/src/date/demo/src/app/date/date-maxmin-test.html new file mode 100644 index 0000000..57bc620 --- /dev/null +++ b/src/date/demo/src/app/date/date-maxmin-test.html @@ -0,0 +1,51 @@ +

描述

+

min,max临界值测试

+

示例

+

1.max 接口测试,设置max为 new Date()

+
+ +

+当前日期:{{value}} +

+

2.min 接口测试,设置min为 new Date()

+
+ +

+当前日期:{{value1}} +

+

3.max min接口测试,设置max1,min1都为 new Date()

+
+ +

+当前日期:{{value2}} +

+

4.max min 接口测试,设置max2,min2都为 new Date()

+
+ +

+当前日期:{{value3}} +

+

5.初始有值且max min 接口测试,设置value4,max3,min3都为 new Date()

+
+ +

+当前日期:{{value4}} +

+切换日期格式: + + + + + + +

+

6.设置最小值年(2029年)为下一年面板的第一个值时,年份禁用问题测试

+
+ +

+当前日期:{{value5}} +

7.设置最大值年(2020年)为上一年面板的最后一个值时,年份禁用问题测试

+
+ +

+当前日期:{{value6}} diff --git a/src/date/demo/src/app/date/date-maxmin.html b/src/date/demo/src/app/date/date-maxmin.html new file mode 100644 index 0000000..95129b6 --- /dev/null +++ b/src/date/demo/src/app/date/date-maxmin.html @@ -0,0 +1,5 @@ + +
+ +
+ diff --git a/src/date/demo/src/app/date/date-min.html b/src/date/demo/src/app/date/date-min.html new file mode 100644 index 0000000..90efd1e --- /dev/null +++ b/src/date/demo/src/app/date/date-min.html @@ -0,0 +1,20 @@ +

1.描述

+

min接口测试

+

2.示例

+

(2.1)最小值设置为非字符串类型、非法日期时:最小值取默认值

+ +
+
+
+

(2.2)设置的min大于设置的value时:即value值非法,显示最小值

+ +
+
+
+

(2.3)动态变更

+ +
+最小值:{{min2 | date: "yyyy-MM-dd"}} +
+ + diff --git a/src/date/demo/src/app/date/date-nowdatetime.html b/src/date/demo/src/app/date/date-nowdatetime.html new file mode 100644 index 0000000..2243944 --- /dev/null +++ b/src/date/demo/src/app/date/date-nowdatetime.html @@ -0,0 +1 @@ + diff --git a/src/date/demo/src/app/date/date-panelalign.html b/src/date/demo/src/app/date/date-panelalign.html new file mode 100644 index 0000000..f7f5f80 --- /dev/null +++ b/src/date/demo/src/app/date/date-panelalign.html @@ -0,0 +1 @@ + diff --git a/src/date/demo/src/app/date/date-validation.html b/src/date/demo/src/app/date/date-validation.html new file mode 100644 index 0000000..0996582 --- /dev/null +++ b/src/date/demo/src/app/date/date-validation.html @@ -0,0 +1 @@ + diff --git a/src/date/demo/src/app/date/date-value-test.html b/src/date/demo/src/app/date/date-value-test.html new file mode 100644 index 0000000..1c7edea --- /dev/null +++ b/src/date/demo/src/app/date/date-value-test.html @@ -0,0 +1,22 @@ +

1.描述

+

value接口测试

+

导入

+

import {{ '{' }} TiDateModule {{ '}' }} from '@opentiny/ng';

+

2.示例

+

(2.1)设置为非字符串类型、非法日期时,显示空

+ +
+
+
+

(2.2)设置为null时,显示空白

+ +
+
+
+

(2.3)设置为最小日期时,正常显示

+ +
+
+
+

(2.4)设置为最大日期时,正常显示,modelChange测试

+ diff --git a/src/date/demo/src/app/date/date-value.html b/src/date/demo/src/app/date/date-value.html new file mode 100644 index 0000000..7d45459 --- /dev/null +++ b/src/date/demo/src/app/date/date-value.html @@ -0,0 +1,3 @@ + +
+ diff --git a/src/date/demo/src/app/date/webdoc/date-demos.js b/src/date/demo/src/app/date/webdoc/date-demos.js new file mode 100644 index 0000000..32317de --- /dev/null +++ b/src/date/demo/src/app/date/webdoc/date-demos.js @@ -0,0 +1,149 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'date-value', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': '

Date 组件的最简用法。

', + 'en-US': '' + } + }, + { + demoId: 'date-format', + name: { + 'zh-CN': '日期格式', + 'en-US': 'format' + }, + desc: { + 'zh-CN': '

通过属性format配置日期显示格式。

', + 'en-US': '' + }, + apis: ['TiDateComponent.properties.format'] + }, + { + demoId: 'date-maxmin', + name: { + 'zh-CN': '预设范围', + 'en-US': 'maxmin' + }, + desc: { + 'zh-CN': '

通过属性max配置可选择日期的最大值;通过属性min配置可选择日期的最小值。

', + 'en-US': '' + }, + apis: ['TiDateComponent.properties.max', 'TiDateComponent.properties.min'] + }, + { + demoId: 'date-panelalign', + name: { + 'zh-CN': '下拉面板对齐方向', + 'en-US': 'panelAlign' + }, + desc: { + 'zh-CN': '

通过属性panelAlign配置下拉面板对齐方向,包括leftright

', + 'en-US': '' + }, + apis: ['TiDateComponent.properties.panelAlign'] + }, + { + demoId: 'date-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled' + }, + desc: { + 'zh-CN': '

通过属性disabled配置是否为禁用状态。

', + 'en-US': '' + }, + apis: ['TiDateComponent.properties.disabled'] + }, + { + demoId: 'date-clearicon', + name: { + 'zh-CN': '清除功能', + 'en-US': 'clearIcon' + }, + desc: { + 'zh-CN': '

通过属性clearIcon配置选择框右侧是否显示清除的叉号图标。

', + 'en-US': '' + }, + apis: ['TiDateComponent.properties.clearIcon'] + }, + { + demoId: 'date-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event' + }, + desc: { + 'zh-CN': '

当日期框内值改变的时候触发ngModelChange事件。

', + 'en-US': '' + } + }, + { + demoId: 'date-customize', + name: { + 'zh-CN': '自定义下拉面板内容', + 'en-US': 'customize' + }, + desc: { + 'zh-CN': + '

通过属性customizeOptions自定义下拉面板左侧内容,设置可快捷选择的常用日期;当点击自定义下拉面板左侧内容时触发customizeOptionClick事件,传递出去的参数为选中的日期。

', + 'en-US': '' + }, + apis: [ + 'TiDateComponent.properties.customizeOptions', + 'TiDateCustomizeOptions.properties.label', + 'TiDateCustomizeOptions.properties.value', + 'TiDateValue.properties.begin' + ] + }, + { + demoId: 'date-disableddays', + name: { + 'zh-CN': '不可选择日期', + 'en-US': 'disabledDays' + }, + desc: { + 'zh-CN': '

通过属性disabledDays配置禁止选择的部分日期。

', + 'en-US': '' + }, + apis: ['TiDateComponent.properties.disabledDays'] + }, + { + demoId: 'date-validation', + name: { + 'zh-CN': '校验', + 'en-US': 'date validation' + }, + desc: { + 'zh-CN': '

通过指令tiValidation实现校验。

', + 'en-US': '' + } + }, + { + demoId: 'date-nowdatetime', + name: { + 'zh-CN': '自定义当前日期', + 'en-US': 'nowDateTime' + }, + desc: { + 'zh-CN': '

通过属性nowDateTime自定义当前日期,可用于矫正实际日期和计算机系统日期的偏差。

', + 'en-US': '' + }, + apis: ['TiDateComponent.properties.nowDateTime'] + } + ], + ignoreApis: [ + 'TiDateComponent.properties.isBeginFixed', + 'TiDateComponent.properties.isEndFixed', + 'TiDateComponent.events.dayClick', + 'TiDateComponent.events.okClick', + 'TiDateValue.properties.end', + 'TiDateValue.properties.timeZone' + ] +}; diff --git a/src/date/demo/src/app/date/webdoc/date.cn.md b/src/date/demo/src/app/date/webdoc/date.cn.md new file mode 100644 index 0000000..9d88c55 --- /dev/null +++ b/src/date/demo/src/app/date/webdoc/date.cn.md @@ -0,0 +1,29 @@ +--- +title: Date 日期 +--- +# Date 日期 + +
+ +选择或输入日期的组件。 + +```typescript +import { TiDateModule } from '@opentiny/ng'; +``` + +如需使用校验功能,请导入。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
+ +
+ +选择或输入日期的组件。 + +```typescript +import { TiDateModule } from '@opentiny/ng'; +``` +
diff --git a/src/date/demo/src/app/date/webdoc/date.en.md b/src/date/demo/src/app/date/webdoc/date.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/date/demo/src/app/date/webdoc/date.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/date/demo/src/favicon.ico b/src/date/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/date/demo/src/index.html b/src/date/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/date/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/date/demo/src/main.ts b/src/date/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/date/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/date/demo/test.ts b/src/date/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/date/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/date/demo/tsconfig.app.json b/src/date/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/date/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/date/demo/tsconfig.spec.json b/src/date/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/date/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/date/lib/index.ts b/src/date/lib/index.ts new file mode 100644 index 0000000..5fecca9 --- /dev/null +++ b/src/date/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiDateModule'; +export { TiDateCustomizeOptions } from '@opentiny/ng-datebase'; diff --git a/src/date/lib/ng-package.json b/src/date/lib/ng-package.json new file mode 100644 index 0000000..5ec3516 --- /dev/null +++ b/src/date/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/date", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/date/lib/package.json b/src/date/lib/package.json new file mode 100644 index 0000000..ceac4f5 --- /dev/null +++ b/src/date/lib/package.json @@ -0,0 +1,19 @@ +{ + "name": "@opentiny/ng-date", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-datepanel": "~1.0.0-beta.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@opentiny/ng-datedominator": "~1.0.0-beta.0", + "@opentiny/ng-dateedit": "~1.0.0-beta.0", + "@opentiny/ng-datebase": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/date/lib/project.json b/src/date/lib/project.json new file mode 100644 index 0000000..d576dc1 --- /dev/null +++ b/src/date/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/date/lib", + "sourceRoot": "src/date/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/date"], + "options": { + "project": "src/date/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/date"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js date" + }, + { + "command": "ng default-build date" + }, + { + "command": "node build/clear-default-theme.js date" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/date && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build date && ng pack date && node build/publish.js date --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/date/lib/src/TiDateComponent.ts b/src/date/lib/src/TiDateComponent.ts new file mode 100644 index 0000000..99588cc --- /dev/null +++ b/src/date/lib/src/TiDateComponent.ts @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ContentChild, SimpleChanges, TemplateRef, ChangeDetectionStrategy } from '@angular/core'; +import { TiDateUtil, TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiLocaleFormat } from '@opentiny/ng-locale'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiDateBaseComponent } from '@opentiny/ng-datebase'; +import packageInfo from '../package.json'; + +/** + * Date日期组件 + * + * Date组件提供了一种方便的显示和设置日期的方式 + * + */ +@Component({ + selector: 'ti-date', + templateUrl: './date.html', + styleUrls: ['./date.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-date-input-container]': 'true', + '(blur)': 'hidePanel()' + }, + providers: [TiFormComponent.getValueAccessor(TiDateComponent)] +}) +export class TiDateComponent extends TiDateBaseComponent { + /** + * @ignore + * 日期显示格式: date 组件的format为string类型 + */ + public format: string; + /** + * @ignore + * 标记date/datetime的类型 + */ + public isDatetime: boolean = false; + /** + * @ignore + * 用于标记是不是range + */ + public isRange: boolean = false; + protected versionInfo: string = super.getVersion(packageInfo); + /** + * @ignore + */ + @ContentChild('customize', { static: true }) + customizeTemplate: TemplateRef; + + ngOnInit(): void { + super.ngOnInit(); + this.setPlacehoder(); + } + + /** + * @ignore + * model值变化时的回调 + */ + ngOnModelChange(): void { + if (!TiDateUtil.isDate(this.model)) { + this.inputValue = ''; + + return; + } + this.formatValue(); + this.setPickerDate(); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + // format支持动态变更 + if (changes['format'] && !changes['format'].firstChange) { + // 如果新的 format 值为非法值,format值保持旧值不变 + if (!Util.isString(changes['format'].currentValue)) { + this.format = changes['format'].previousValue; + + return; + } + this.setFormatChange(); + } + // 验证最大值最小值,为了处理最大值和最小值从合法日期变为undefined 的情景 + if ((changes['max'] && !changes['max'].firstChange) || (changes['min'] && !changes['min'].firstChange)) { + this.validateMinAndMax(this.config, this.isDatetime); + } + } + + /** + * @ignore + */ + public setInputValue(value: Date): void { + let isDisabled: boolean = false; + if (Util.isArray(this.disabledDays) && this.disabledDays.length > 0) { + this.disabledDays.forEach((item: Date) => { + if (item.getTime() === value.getTime()) { + isDisabled = true; + } + }); + } + if (isDisabled) { + return; + } + if (this.isValidValue(value)) { + this.model = value; + } + } + /** + * @ignore + */ + public onKeydownFn(event: KeyboardEvent, val: any, pos: string): void { + if (event.keyCode === TiKeymap.KEY_ENTER || event.keyCode === TiKeymap.KEY_NUMPAD_ENTER) { + this.hideDrop(); + } + } + + private setFormatChange(): void { + this.formatValue(); + this.setPlacehoder(); + this.setDatepanelPicker(); + this.validateMinAndMax(this.config); + // 配置面板的接口值 + this.setPickerDate(); + } + + /** + * @ignore + * 配置时间日期面板接口 + */ + public setPickerDate(): void { + this.datePanel = { + value: { + begin: this.model, + end: null + }, + picker: this.datepanelPicker, + min: this.min, + max: this.max, + format: this.format, + nowDateTime: this.nowDateTime, + select: (): void => { + // 点击面板:新旧值相同不做处理,不相同,将新值赋给model + if (!TiDateUtil.isDateEqual(this.model, this.datePanel['value'].begin)) { + this.model = this.datePanel['value'].begin; + } + this.hideDrop(); + } + }; + } + + /** + * @ignore + * 根据model值和format接口,格式化显示时间日期 + */ + public formatValue(): void { + if (this.format === 'YYYY/QQ') { + this.inputValue = TiDateUtil.transformDateToQuarter(this.model); + } else { + this.inputValue = this.model !== null ? TiLocaleFormat.formatDate(this.model, this.format) : ''; + } + } + /** + * @ignore + * 判断是不是合法的model + */ + public isValidValue(value: Date): boolean { + if (TiDateUtil.isDate(value) && TiDateUtil.isBetweenMaxAndmin(value, this.min, this.max)) { + return true; + } + + return false; + } +} diff --git a/src/date/lib/src/TiDateModule.ts b/src/date/lib/src/TiDateModule.ts new file mode 100644 index 0000000..53729e8 --- /dev/null +++ b/src/date/lib/src/TiDateModule.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiDatePanelModule } from '@opentiny/ng-datepanel'; +import { TiButtonModule } from '@opentiny/ng-button'; +import { TiDateDominatorModule } from '@opentiny/ng-datedominator'; +import { TiDateComponent } from './TiDateComponent'; +import { TiLocaleModule } from '@opentiny/ng-locale'; +import { TiDateEditModule } from '@opentiny/ng-dateedit'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiDropModule, + TiButtonModule, + TiDateEditModule, + TiDatePanelModule, + TiDateDominatorModule, + TiLocaleModule + ], + exports: [TiDateComponent], + declarations: [TiDateComponent] +}) +export class TiDateModule {} +export { TiDateComponent } from './TiDateComponent'; diff --git a/src/date/lib/src/date-common.less b/src/date/lib/src/date-common.less new file mode 100644 index 0000000..e4a228a --- /dev/null +++ b/src/date/lib/src/date-common.less @@ -0,0 +1,37 @@ +.ti3-customize-time-label { + font-size: var(--ti-common-font-size-base); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-primary); + margin-bottom: 10px; + cursor: pointer; + list-style: none; + &:hover { + color: var(--ti-common-color-text-highlight); + } +} +.ti3-date-clear { + .clearfix(); + display: flex; +} +.ti3-datetime-select-btn { + float: right; + padding-right: var(--ti-common-space-2x); + line-height: 24px; + color: var(--ti-common-color-text-highlight); + cursor: pointer; +} +.ti3-datetime-select-btn-disabled { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; +} +.ti3-date-range-splitline { + padding: 0 6px; +} + +.ti3-tab-input { + width: 0; + height: 0; + position: absolute; + top: -9999px; + left: -9999px; +} diff --git a/src/date/lib/src/date.html b/src/date/lib/src/date.html new file mode 100644 index 0000000..74577c8 --- /dev/null +++ b/src/date/lib/src/date.html @@ -0,0 +1,70 @@ +{{ placeholder }} + +
+
+ +
+
+ + + +
+
+
+ + +
    +
  • + {{option.label}} +
  • +
+
diff --git a/src/date/lib/src/date.less b/src/date/lib/src/date.less new file mode 100644 index 0000000..14e7edb --- /dev/null +++ b/src/date/lib/src/date.less @@ -0,0 +1,39 @@ +@import '../../../themes/basic/base-all.less'; +@import '../../../themes/basic/compnent-container-border.less'; +@import './date-common.less'; + +ti-drop.ti3-date-picker { + --ti-date-picker-padding-horizon: var(--ti-common-space-4x); + --ti-date-picker-line-width: 1px; + --ti-date-picker-line-color: var(--ti-common-color-line-dividing); +} + +:host.ti3-date-input-container :extend(.ti3-compnent-container-border all) { + width: var(--ti-common-size-40x); +} + +.ti3-dropdown-container.ti3-date-picker { + padding: var(--ti-common-space-3x) var(--ti-date-picker-padding-horizon); + font-weight: var(--ti-common-font-weight-4); + font-size: var(--ti-common-font-size-base); + z-index: 10002; + &:focus { + outline: 0px; + } +} + +.ti3-date-customize-container { + padding-right: var(--ti-common-space-4x); + width: var(--ti-common-size-20x); + border-right: var(--ti-date-picker-line-width) var(--ti-common-border-style-solid) var(--ti-date-picker-line-color); +} + +.ti3-date-edit { + width: 100%; +} +::ng-deep .ti3-date-edit input[tiText] { + border-color: var(--ti-common-color-line-active) !important; +} +.ti3-date-panel-container { + padding-left: var(--ti-common-space-4x); +} diff --git a/src/datebase/lib/index.ts b/src/datebase/lib/index.ts new file mode 100644 index 0000000..bc2b2eb --- /dev/null +++ b/src/datebase/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiDateBaseModule'; diff --git a/src/datebase/lib/ng-package.json b/src/datebase/lib/ng-package.json new file mode 100644 index 0000000..85eb656 --- /dev/null +++ b/src/datebase/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/datebase", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/datebase/lib/package.json b/src/datebase/lib/package.json new file mode 100644 index 0000000..3386ee2 --- /dev/null +++ b/src/datebase/lib/package.json @@ -0,0 +1,17 @@ +{ + "name": "@opentiny/ng-datebase", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-datedominator": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-dateedit": "~1.0.0-beta.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-text": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/datebase/lib/project.json b/src/datebase/lib/project.json new file mode 100644 index 0000000..a1f0c97 --- /dev/null +++ b/src/datebase/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/datebase/lib", + "sourceRoot": "src/datebase/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/datebase"], + "options": { + "project": "src/datebase/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/datebase"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js datebase" + }, + { + "command": "ng default-build datebase" + }, + { + "command": "node build/clear-default-theme.js datebase" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/datebase && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build datebase && ng pack datebase && node build/publish.js datebase --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/datebase/lib/src/TiDateBaseComponent.ts b/src/datebase/lib/src/TiDateBaseComponent.ts new file mode 100644 index 0000000..e10b597 --- /dev/null +++ b/src/datebase/lib/src/TiDateBaseComponent.ts @@ -0,0 +1,1157 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/* eslint-disable no-param-reassign */ +import { + Component, + ElementRef, + EventEmitter, + HostListener, + Input, + NgZone, + Output, + QueryList, + Renderer2, + ViewChild, + ViewChildren, + Inject, + ChangeDetectorRef, + SimpleChanges +} from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +import { TiDateUtil, TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiDateDominatorComponent } from '@opentiny/ng-datedominator'; +import { TiDropComponent } from '@opentiny/ng-drop'; +import { TiDateEditComponent } from '@opentiny/ng-dateedit'; +import { TiButtonComponent } from '@opentiny/ng-button'; +import { TiTextComponent } from '@opentiny/ng-text'; +import packageInfo from '../package.json'; + +/** + * 时间日期范围类组件value接口 + */ +export interface TiDateValue { + /** + * 开始时间 + */ + begin?: Date; + /** + * 结束时间 + */ + end?: Date; + /** + * 当前时区,目前支持‘本地时区’和‘UTC/GMT’ + */ + timeZone?: string; +} + +export interface TiDateCustomizeOptions { + /** + * 自定义文本 + */ + label: string; + /** + * 自定义时间 + */ + value: TiDateValue; +} + +/** + * 时间日期格式化配置 + */ +export interface TiDatetimeFormat { + /** + * 日期格式化,默认配置'yyyy/MM/dd' + * + * 1.年可以设置为: + * + * yyyy — 四位数字表示年:例如:'2016'; + * + * 2.月可以设置为: + * + * MMMM — 月份英文全称(January - December); + * + * MMM — 月份英文简称 (Jan - Dec); + * + * MM — 两位数字(01 - 12); + * + * M — 开头不补零方式(1 - 12) + * + * 3.日可以设置为: + * + * dd — 两位数字(01 - 31); + * + * d — 开头不补零数字(1 - 31). + */ + date: string; + /** + * 时间格式化,默认配置'HH:mm:ss' + * + * 1.小时可以设置为: + * + * HH — 24 小时制,两位数字(00 - 23) + * + * H — 24 小时制,开头不补零数字(0 - 23) + * + * 2.分钟可以设置为: + * + * mm — 两位数字(00-59) + * + * m — 开头不补零数字(0-59) + * + * 3.秒可以设置为: + * + * ss — 两位数字(00-59) + * + * s — 开头不补零数字(0-59) + * + * 说明:开头补零是指当前时间是个位数字时,前边补零,始终保持两位数字 + */ + time: string; +} + +/** + * @ignore + * dateRange以及datetimeRange组件适配datepanel的接口类型 + */ +export interface TiPicker { + value?: TiDateValue; + type?: string; + picker?: string; + min?: Date; + max?: Date; + nowDateTime?: Date; + format?: string | TiDatetimeFormat; // 两种类型:date dateRange组件是string类型,datetime datetimeRange组件是DatetimeFormat类型 + select?(): void; // 月份/日期,点击事件回调函数 + selectTimeFn?(val: any): void; +} + +/** + * @ignore + * 配置time组件的接口 + */ +export interface TimeOptions { + value?: string; + min?: string; + max?: string; +} + +// lib编译要求@Component +@Component({ + template: '' +}) +export class TiDateBaseComponent extends TiFormComponent { + /** + * @ignore + * 下拉距离选择框0px + */ + private static readonly DOMINATOR_SPACE: number = -30; + /** + * 日期/日期时间下拉选择框距离浏览器上下边沿的距离为5px,开发者可在项目中整体配置 + */ + public static BROWSER_SPACE: number = 5; + protected versionInfo: string = super.getVersion(packageInfo); + /** + * 最小值,默认值为 new Date(1970, 0, 1, 0, 0, 0) + */ + @Input() min: Date = TiDateUtil.minDate(); + /** + * 最大值,默认值为 new Date(2099, 11, 31, 23, 59, 59) + */ + @Input() max: Date = TiDateUtil.maxDate(); + /** + * 日期、时间显示格式 + * + * 1.date 和 dateRange 组件中类型为 string,默认值为'yyyy/MM/dd'; + * + * 2.datetime 和 datatimerange 组件中类型为 TiDatetimeFormat,默认值为{date: 'yyyy/MM/dd', time: 'HH:mm:ss'} + * + */ + @Input() format: string | TiDatetimeFormat; + /** + * 自定义当前时间,默认值为当前系统时间 + */ + @Input() nowDateTime: Date; + /** + * 是否显示清除图标 + */ + @Input() clearIcon: boolean = true; + /** + * 下拉面板与输入框的对齐方式 + */ + @Input() panelAlign: 'left' | 'right' = 'left'; + /** + * 自定义下拉面板左侧内容,一般用于快速选择日期时间场景 + */ + @Input() customizeOptions: Array; + /** + * 自定义禁用日期,date 和 daterange 组件支持 + */ + @Input() disabledDays: Array; + /** + * 开始日期是否固定不可选,只有 daterange 组件支持 + */ + @Input() isBeginFixed: boolean = false; + /** + * 结束日期是否固定不可选,只有 daterange 组件支持 + */ + @Input() isEndFixed: boolean = false; + /** + * 点击下拉面板上的确认按钮时触发的回调,date 组件不支持 + */ + @Output() readonly okClick: EventEmitter = new EventEmitter(); + /** + * 点击日期面板上的日期触发的回调,date 和 datertime 组件不支持 + */ + @Output() readonly dayClick: EventEmitter = new EventEmitter(); + /** + * 点击自定义下拉面板左侧内容触发的回调 + */ + @Output() readonly customizeOptionClick: EventEmitter = new EventEmitter(); + /** + * @ignore + * dominator组件 + */ + @ViewChild(TiDateDominatorComponent, { static: true }) + dominatorCom: TiDateDominatorComponent; + /** + * @ignore + * drop下拉组件 + */ + @ViewChild(TiDropComponent, { static: true }) dropCom: TiDropComponent; + /** + * @ignore + * + * 配合tab快捷键操作的过度Dom + */ + @ViewChild('input', { static: true }) inputRef: ElementRef; + /** + * @ignore + * dateEdit组件 + */ + @ViewChildren(TiDateEditComponent) + dateEditComs: QueryList; + /** + * @ignore + * 时间编辑框 + */ + @ViewChildren(TiTextComponent) textComs: QueryList; + /** + * @ignore + * button组件 + */ + @ViewChildren(TiButtonComponent) buttonComs: QueryList; + + private readonly localeWords = TiLocale.getLocaleWords(); + + // 该基类主要用于实现公共变量接口声明,公共的事件,以及公共方法 + /** + * @ignore + * 保存旧日期显示格式 + */ + public oldFormat: string | TiDatetimeFormat; + /** + * @ignore + * 保存时间日期类组件词条 + */ + public dateWords = this.localeWords.tiDatedominator; + /** + * @ignore + * 保存旧model值:date类型或者datevalue类型 + */ + public oldModel: Date | TiDateValue; + /** + * @ignore + * 判断当前value值是不是合法的,不合法则确认按钮灰化 + */ + public inValidValue: boolean; + /** + * @ignore + * 标记点击叉号 + */ + public isClearClick: boolean; + /** + * @ignore + * 标志是日期还是时间日期 + */ + public isDatetime: boolean; + /** + * @ignore + * 标志是时间日期还是时间日期段 + */ + public isRange: boolean; + /** + * @ignore + * 时间面板dominator对应的placehoder + */ + public placeholder: string; + /** + * @ignore + * 标志是时间选择面板还是日期选择面板 + */ + public selectTime: boolean = false; + /** + * @ignore + * 对应每个时间日期组件的最大最小值的配置 + */ + public config: { min: Date; max: Date } = { + min: TiDateUtil.minDate(), + max: TiDateUtil.maxDate() + }; + /** + * @ignore + * model默认值 + */ + public defaultValue: TiDateValue = { + begin: null, + end: null + }; + /** + * @ignore + * 模板初始化 + */ + public datePanel: TiPicker = { + value: this.defaultValue + }; + + /** + * @ignore + * 面板宽度 + */ + public datePanelWidth: string = 'auto'; + /** + * @ignore + * 日期显示字符串 + */ + public inputValue: string = ''; + private documentKeydownListener: () => void; + /** + * @ignore + * 模板中使用 + */ + public browserSpace: string = TiDateBaseComponent.BROWSER_SPACE + 'px'; + /** + * @ignore 面板与dominator的距离 + */ + public dominatorSpace: string = TiDateBaseComponent.DOMINATOR_SPACE + 'px'; + /** + * @ignore + * 记录当前面板内部编辑区域焦点位置 + */ + public focusedPosition: string = 'begin'; + /** + * @ignore + * 时间编辑框placehoder + */ + public timeplaceholder: string = 'hh:mm:ss'; + /** + * @ignore + * 底部选择时间按钮禁用标志 + */ + public timeEditDisabled: boolean; + /** + * @ignore + * 标志是否存在左侧自定义时间区域 + */ + public hasCustomizeTime: boolean = false; + /** + * @ignore + * 可聚焦元素集合 + */ + public focusElementsArr: Array = []; + /** + * @ignore + * 保存用户设置的max值 + */ + public userSetMaxvalue: Date; + /** + * @ignore + * 保存用户设置的min值 + */ + public userSetMinvalue: Date; + /** + * @ignore + * 日期面板/时间面板类型 + */ + public datepanelPicker: + | 'onlyYear' + | 'year' + | 'onlyYearMonth' + | 'month' + | 'day' + | 'quarter' + | 'onlyHours' + | 'onlyHoursMinutes' + | 'seconds'; + + constructor( + protected hostRef: ElementRef, + protected render: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + private zone: NgZone, + @Inject(DOCUMENT) private document + ) { + super(hostRef, render, changeDetectorRef); + } + + private static getFocusElements(focusComs: any, focusElements: Array): Array { + for (const focusCom of focusComs.toArray()) { + focusElements = focusElements.concat(focusCom.getFocusableElems()); + } + + return focusElements; + } + + /** + * @ignore + * 组件快捷键处理tab键 enter键 + */ + @HostListener('keydown', ['$event']) public onKeydown(event: KeyboardEvent): void { + switch (event.keyCode) { + case TiKeymap.KEY_TAB: // TAB键 + this.hidePanel(); + break; + case TiKeymap.KEY_ENTER: // ENTER键(大键盘) + case TiKeymap.KEY_NUMPAD_ENTER: // ENTER键(数字小键盘) + this.responseEnter(); + break; + default: + break; + } + } + + /** + * @ignore + * writeValue区分是不是range组件: + */ + writeValue(value: any): void { + super.writeValue(value); + // 1.daterange和datetimerange组件的处理 + // 新旧值判断处理放在docheck中 + if (this.isRange) { + // value不是对象时,将value设置为null,输入框中显示空白 + if (!(value instanceof Object) || !this.isValidValue(value)) { + this.model = value?.timeZone + ? { + begin: null, + end: null, + timeZone: value?.timeZone + } + : null; + + return; + } + } + + // 2.date和datetime组件的处理,非法时间日期值统一置为null,显示空 + if (!this.isValidValue(value)) { + this.model = null; + + return; + } + + // 新旧值相同时不作处理; + if (TiDateUtil.isDateEqual(value, this.model)) { + return; + } + } + ngOnChanges(changes: SimpleChanges): void { + // 记录服务设置的最大值 + if (changes['max']) { + this.userSetMaxvalue = this.max; + } + + // 记录服务设置的最小值 + if (changes['min']) { + this.userSetMinvalue = this.min; + } + } + ngOnInit(): void { + super.ngOnInit(); + // 是否存在左侧自定义面板 + this.hasCustomizeTime = !Util.isUndefined(this.customizeOptions) && !Util.isNull(this.customizeOptions); + // format接口校验:对时间日期进行国际化处理 + this.validateFormat(this.isDatetime); + // 设置面板显示格式 + this.setPicker(); + // 最大最小值校验 + this.validateMinAndMax(this.config, this.isDatetime); + + this.zone.runOutsideAngular(() => { + // 避免不停触发变化检测 + // document上的Ecs快捷键功能 + this.documentKeydownListener = this.render.listen(this.document, 'keydown', this.keydownHandlerFn); + }); + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + // 定义变量存储聚焦元素; + let focusElements: Array = []; + + // 获取dateedit的聚焦元素 + focusElements = TiDateBaseComponent.getFocusElements(this.dateEditComs, focusElements); + // 获取时间编辑框聚焦元素 + if (this.textComs.length !== 0) { + for (const textCom of this.textComs.toArray()) { + focusElements = focusElements.concat(textCom.nativeElement); + } + } + + if (this.inputRef) { + focusElements = focusElements.concat(this.inputRef.nativeElement); + } + + // 获取确认取消按钮的聚焦元素 + for (const buttonCom of this.buttonComs.toArray()) { + focusElements = focusElements.concat(buttonCom.nativeElement); + } + this.focusElementsArr = focusElements; + this.setFocusableElems([this.dominatorCom.nativeElement].concat(this.dropCom.nativeElement).concat(focusElements)); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + if (this.documentKeydownListener) { + this.documentKeydownListener(); + } + } + + protected keydownHandlerFn = (event: KeyboardEvent): void => { + if (!this.dropCom.isShow) { + return; + } + if (event.keyCode === TiKeymap.KEY_ESCAPE) { + this.hidePanel(); + this.dominatorCom.focus(); + } + this.tabKeydown(event); + }; + + /** + * 实现按tab键时,focus在下拉面板中循环 + */ + private tabKeydown(event: KeyboardEvent): void { + if (this.focusElementsArr.length === 0) { + return; + } + let focusChanged: boolean = false; + if (event.keyCode === TiKeymap.KEY_TAB) { + const panelFocusElementsArr = this.focusElementsArr.filter((item: any) => !item.hasAttribute('disabled')); + if (event.shiftKey) { + if (event.target === panelFocusElementsArr[0]) { + focusChanged = true; + panelFocusElementsArr[panelFocusElementsArr.length - 1].focus(); + } + } else if (event.target === panelFocusElementsArr[panelFocusElementsArr.length - 1]) { + focusChanged = true; + panelFocusElementsArr[0].focus(); + } + } + if (focusChanged) { + event.preventDefault(); + event.stopPropagation(); + } + } + + // ----------------------------- 公共事件处理 ----------------------------- + /** + * @ignore + * 点击面板展开收起函数 + */ + public onShowClick(): void { + if (this.disabled) { + // 非灰化可点击展开面板 + return; + } + + if (this.dropCom.isShow || this.isClearClick) { + this.isClearClick = false; + this.dropCom.hide(); + } else { + // 配置时间日期面板接口 + this.setPickerDate(); + this.show(); + this.selectTime = false; // 面板打开初次总展示日期面板 + setTimeout(() => { + if (this.isBeginFixed && !this.isEndFixed) { + this.dateEditComs.last.focus(); // 开始值固定,则第二个编辑区域聚焦 + this.focusedPosition = 'end'; + } else { + this.dateEditComs.first.focus(); // 初始化打开面板,第一个编辑区域聚焦 + this.focusedPosition = 'begin'; + } + }, 0); + } + } + /** + * @ignore + * 左侧自定义时间文本点击事件 + */ + public customizeTimeClickFn(val: TiDateValue): void { + this.model = val; + this.dropCom.hide(); + this.customizeOptionClick.emit(this.model); + } + /** + * @ignore + * 点击选择时间,根据初始传入值设定下拉选择项 + */ + public selectTimeFn(): void { + if (this.timeEditDisabled) { + return; + } + this.selectTime = true; // 切换为时间选择面板 + this.textComs.first.nativeElement.focus(); + } + /** + * @ignore + */ + public selectDateFn(): void { + this.selectTime = false; + this.dateEditComs.first.nativeElement.focus(); + this.focusedPosition = 'begin'; + } + /** + * @ignore + */ + public dateEditClick(val: string): void { + if (this.isBeginFixed || this.isEndFixed) { + return; + } + this.focusedPosition = val; + this.selectTime = false; + } + /** + * @ignore + */ + public dateEditFocus(val: string): void { + this.focusedPosition = val; + this.selectTime = false; + + if (!this.isRange) { + return; + } + + const value: any = this.datePanel.value; + // 先选择开始日期,焦点会自动到结束日期,此时开始日期之前日期禁用,若不选择结束日期手动聚焦到开始日期时,需将开始日期之前日期取消禁用即恢复默认最小值,以便用户重新选择开始日期; + if (val === 'begin' && TiDateUtil.isDate(value.begin) && !TiDateUtil.isDate(value.end)) { + this.datePanel.min = this.min; + } else if (val === 'end' && TiDateUtil.isDate(value.end) && !TiDateUtil.isDate(value.begin)) { + // 先选择结束日期,与先选择结束日期同理; + this.datePanel.max = this.max; + } + } + /** + * @ignore + * 时间框聚焦 + */ + public timeFocus(val: string): void { + this.selectTime = true; + this.focusedPosition = val === 'begin' ? 'beginTime' : 'endTime'; + } + /** + * @ignore + */ + public setTimeFn(obj: any, beginTime: TimeOptions, endTime?: TimeOptions): void { + const timePosition: string = obj.timeOption; + const beginTimeArr: any = beginTime.value.split(':'); + let endTimeArr: any; + if (!Util.isUndefined(endTime)) { + endTimeArr = endTime.value.split(':'); + } + switch (timePosition) { + case 'beginHour': + beginTimeArr[0] = obj.val.label; + break; + case 'beginMinute': + beginTimeArr[1] = obj.val.label; + break; + case 'beginSecond': + beginTimeArr[2] = obj.val.label; + break; + case 'endHour': + endTimeArr[0] = obj.val.label; + break; + case 'endMinute': + endTimeArr[1] = obj.val.label; + break; + case 'endSecond': + endTimeArr[2] = obj.val.label; + break; + default: + break; + } + beginTime.value = beginTimeArr.join(':'); + if (!Util.isUndefined(endTime)) { + endTime.value = endTimeArr.join(':'); + } + } + + /** + * @ignore + * 点击删除按钮 + */ + public onIconClearClick(event: MouseEvent): void { + // 阻止事件冒泡:可以防止点击叉号引起面板的显示或者隐藏,但此处不能阻止事件冒泡, + // 事件不会冒泡到document上,会出现页面上其他下拉类组件面板不收起情况 + // 添加标志量,在onShowClick函数中处理 + if (this.disabled || !this.clearIcon) { + return; + } + this.isClearClick = true; + this.model = this.model?.timeZone + ? { + begin: null, + end: null, + timeZone: this.model?.timeZone + } + : null; + this.inputValue = ''; + // 时间日期面板显示则隐藏 + this.hidePanel(); + } + + /** + * @ignore + * 隐藏drop, 适用于drop中用户点击引起drop隐藏,阻止了整体失焦。 + */ + public hideDrop(): void { + // 阻止整体失去焦点 + this.dominatorCom.focus(); + this.dropCom.hide(); + } + /** + * @ignore + * 隐藏面板 + */ + public hidePanel(): void { + if (this.dropCom.isShow) { + this.dropCom.hide(); + } + } + + /** + * @ignore + * enter键的功能:如果面板展开不处理,面板收起则展开,设置datePanel指令的接口值 + */ + public responseEnter(): void { + if (this.dropCom.isShow) { + return; + } + + // 面板收起前是年或月面板,按enter键重新设置datePanel指令的接口值 + this.setPickerDate(); + + // 时间面板展开 + this.show(); + setTimeout(() => { + this.dateEditComs.first.nativeElement.focus(); + this.focusedPosition = 'begin'; + }, 0); + } + /** + * @ignore + * + * 设置按钮禁用场景下 过度input的样式(适配tab快捷键场景) + */ + public setInputStyle(val: boolean): void { + if (val) { + this.render.removeStyle(this.inputRef.nativeElement, 'display'); + this.render.setStyle(this.inputRef.nativeElement, 'opacity', '0'); + } else { + this.render.setStyle(this.inputRef.nativeElement, 'display', 'none'); + } + } + + /** + * @ignore + */ + public setPlacehoder(): void { + if (Util.isUndefined(this.format)) { + return; + } + + if (this.isDatetime) { + this.placeholder = this.dateWords.datetimePlaceholder; + return; + } + + if (this.format === 'YYYY/QQ') { + this.placeholder = this.isRange + ? `${this.dateWords.quarterRangeBeginLabel} ─ ${this.dateWords.quarterRangeEndLabel}` + : this.dateWords.quarterSelectPlaceholder; + + return; + } + // 如果只显示年 + if ((this.format as string).indexOf('M') === -1 && this.format !== 'mediumDate') { + this.placeholder = this.isRange + ? `${this.dateWords.yearRangeBeginLabel} ─ ${this.dateWords.yearRangeEndLabel}` + : this.dateWords.yearSelectPlaceholder; + } else if ((this.format as string).indexOf('d') === -1) { + // 如果只显示年月 + this.placeholder = this.isRange + ? `${this.dateWords.monthRangeBeginLabel} ─ ${this.dateWords.monthRangeEndLabel}` + : this.dateWords.monthSelectPlaceholder; + } else { + this.placeholder = this.isRange + ? `${this.dateWords.rangeBeginLabel} ─ ${this.dateWords.rangeEndLabel}` + : this.dateWords.datePlaceholder; + } + } + + private setPicker(): void { + if (this.isDatetime) { + this.setDatetimepanelPicker(); + } else { + this.setDatepanelPicker(); + } + } + + /** + * @ignore + * + * @param isDatetime 是否是日期时间组件 + * + * 设置日期时间和日期时间范围组件时间面板类型 + */ + public setDatetimepanelPicker(): string { + if (!this.format) { + return; + } + + this.format = this.format as TiDatetimeFormat; + + if (this.format.time.indexOf('m') === -1) { + this.datepanelPicker = 'onlyHours'; + } else if (this.format.time.indexOf('s') === -1) { + this.datepanelPicker = 'onlyHoursMinutes'; + } else { + this.datepanelPicker = 'seconds'; + } + } + + /** + * @ignore + * + * @param isDatetime 是否是日期时间组件 + * + * 设置日期和日期范围组件面板类型 + */ + public setDatepanelPicker(): void { + if (!this.format) { + return; + } + + this.format = this.format as string; + + // 如果只显示年 + if (this.format.indexOf('M') === -1 && this.format !== 'mediumDate') { + this.datepanelPicker = this.isRange ? 'onlyYear' : 'year'; + } else if (this.format.indexOf('d') === -1) { + // 如果只显示年月 + this.datepanelPicker = this.isRange ? 'onlyYearMonth' : 'month'; + } else { + this.datepanelPicker = 'day'; + } + + if (this.format === 'YYYY/QQ') { + this.datepanelPicker = 'quarter'; + } + } + + // ----------------------------- 公共方法处理 ----------------------------- + /** + * @ignore + * 判断是不是合法的DatetimeFormat: format为对象 {date:'', time: ''} + */ + public isValidDatetimeFormat(): boolean { + return this.format instanceof Object && Util.isString(this.format['date']) && Util.isString(this.format['time']); + } + + /** + * @ignore + * 最大值最小值校验 + */ + public validateMinAndMax(dateCofig: { min: Date; max: Date }, isDatetime?: boolean): void { + const maxChanged: Date = TiDateUtil.changeMaxTime(this.userSetMaxvalue, this.datepanelPicker, isDatetime); + this.max = TiDateUtil.isDate(maxChanged) ? maxChanged : dateCofig.max; + + const minChanged: Date = TiDateUtil.changeMinTime(this.userSetMinvalue, this.datepanelPicker, isDatetime); + this.min = TiDateUtil.isDate(minChanged) ? minChanged : dateCofig.min; + + // 最大最小值矛盾时,设置为默认值 + if (this.max.getTime() < this.min.getTime()) { + this.max = dateCofig.max; + this.min = dateCofig.min; + } + } + + /** + * @ignore + * 校验format接口 + */ + public validateFormat(isDatetime: boolean): void { + if (isDatetime) { + this.validateDatetimeFormat(); + } else { + this.validateDateFormat(); + } + } + + /** + * @ignore + * 校验format: string类型, + */ + public validateDateFormat(): void { + if (Util.isString(this.format)) { + return; + } + // TODO:如果配置时间日期国际化 + this.format = this.localeWords.tiDateedit['date']; + } + + /** + * @ignore + * 校验format: 对象类型{date:'', time:''} + */ + public validateDatetimeFormat(): void { + // TODO:初始化为ngLocale格式 + // 用户设置format不是一个对象,设置为国际配置的format或默认值 + if (!(this.format instanceof Object)) { + this.format = { + date: this.localeWords.tiDateedit['date'], + time: TiDateUtil.DEFAULT_TIME_FORMAT + }; + + return; + } + + // format 为联合类型:此处处理DatetimeFormat类型 + this.format = this.format as TiDatetimeFormat; + // 日期格式校验 + if (!Util.isString(this.format.date)) { + this.format.date = this.localeWords.tiDateedit['date']; + } + + // 日期格式校验 + if (!Util.isString(this.format.time)) { + this.format.time = TiDateUtil.DEFAULT_TIME_FORMAT; + } + + // 时间框提示文本按照小写显示 + this.timeplaceholder = this.format.time.toLowerCase(); + } + + /** + * @ignore + * 判断range类型的model是否变化:比较起始时间和结束时间 + */ + public rangeValueIsEqual(newValue: TiDateValue, oldValue: TiDateValue, isDatetime: boolean): boolean { + if (newValue === oldValue) { + return true; + } + + if (newValue instanceof Object && oldValue instanceof Object) { + if (isDatetime) { + return ( + TiDateUtil.isDatetimeEqual(newValue['begin'], oldValue['begin']) && TiDateUtil.isDatetimeEqual(newValue['end'], oldValue['end']) + ); + } else { + return TiDateUtil.isDateEqual(newValue['begin'], oldValue['begin']) && TiDateUtil.isDateEqual(newValue['end'], oldValue['end']); + } + } + + return false; + } + + // ----------------------------- 动态监听处理 ----------------------------- + /** + * @ignore + * model值动态变更:支持仅修改begin或end值 + */ + public setModel(isDatetime: boolean): void { + if (this.rangeValueIsEqual(this.model, this.oldModel as TiDateValue, isDatetime)) { + return; + } + + // model中begin或者end值的变更 + this.formatValue(); + + // 通过按钮改变model值:当面板处于打开状态时需要重新设置面板接口 + if (this.dropCom.isShow) { + this.setPickerDate(); + } + + this.oldModel = + this.model === null + ? null + : { + begin: this.model.begin, + end: this.model.end + }; + } + + /** + * @ignore + * datetime与datetimeRange组件format动态变更 + */ + public setFormat(): void { + if (this.format && this.format['date'] === this.oldFormat['date'] && this.format['time'] === this.oldFormat['time']) { + return; + } + + if (!this.isValidDatetimeFormat()) { + this.format = this.oldFormat; + + return; + } + if (this.isDatetime && this.format['time'] !== this.oldFormat['time']) { + this.format = this.format as TiDatetimeFormat; + // 时间框提示文本按照小写显示 + this.timeplaceholder = this.format.time.toLowerCase(); + } + this.formatValue(); + this.oldFormat = this.format; + this.setDatetimepanelPicker(); + this.validateMinAndMax(this.config, true); + } + + private show(): void { + if (this.renderer.parentNode(this.dropCom.nativeElement) === document.body) { + this.dropCom.show(); + } else { + // 使用setTimeout是为了保证datepanel渲染完成后再显示,否则首次打开时定位计算不准确 + setTimeout(() => { + this.dropCom.show(); + }, 0); + } + } + + /** + * @ignore + * 决定上下位置的函数 + */ + public determinPositionFn: (layout: any) => string = (layout: any) => { + const needHeight: number = layout.targetLayout.height + TiDateBaseComponent.DOMINATOR_SPACE + TiDateBaseComponent.BROWSER_SPACE; + if (layout.avilableLayout.bottom >= needHeight) { + // 下方空间足够,向下展开 + return this.panelAlign === 'left' ? 'bottom-left' : 'bottom-right'; + } else { + return this.panelAlign === 'left' ? 'top-left' : 'top-right'; + } + }; + /** + * @ignore + * 设置时间 + */ + public setDateTime(timeArr: Array): Date { + const hour = timeArr[0] ? parseInt(timeArr[0], 10) : 0; + const minute = timeArr[1] ? parseInt(timeArr[1], 10) : 0; + const second = timeArr[2] ? parseInt(timeArr[2], 10) : 0; + + return new Date(2020, 10, 30, hour, minute, second); + } + + /** + * @ignore + * TiDateRangeComponent 和 TiDatetimeRangeComponen 中使用 + */ + public rangeChange(changes: SimpleChanges): void { + // 验证最大值最小值,为了处理最大值和最小值从合法日期变为undefined 的情景 + if (changes['max'] && !changes['max'].firstChange) { + this.validateMinAndMax(this.config, this.isDatetime); + this.datePanel.max = this.max; + + // dayClick事件中(选择日期)动态变更最大值时,只有在点击确认按钮时需要更新model值,此处只处理面板未打开情况下变更最大值的场景 + if (TiDateUtil.isDate(this.model?.end) && TiDateUtil.isBigger(this.model.end, this.max) && !this.dropCom.isShow) { + this.model.end = this.max; + + return; + } + + if (TiDateUtil.isDate(this.datePanel.value.end) && TiDateUtil.isBigger(this.datePanel.value.end, this.max)) { + this.datePanel.value.end = this.max; + } + } + + if (changes['min'] && !changes['min'].firstChange) { + this.validateMinAndMax(this.config, this.isDatetime); + this.datePanel.min = this.min; + + // dayClick事件中(选择日期)动态变更最小值时,只有在点击确认按钮时需要更新model值,此处只处理面板未打开情况下变更最小值的场景 + if (TiDateUtil.isDate(this.model?.begin) && TiDateUtil.isSmaller(this.model.begin, this.min) && !this.dropCom.isShow) { + this.model.begin = this.min; + + return; + } + + if (TiDateUtil.isDate(this.datePanel.value.begin) && TiDateUtil.isSmaller(this.datePanel.value.begin, this.min)) { + this.datePanel.value.begin = this.min; + } + } + } + + // ----------------------------- 需要在子类中实现 ----------------------------- + /** + * @ignore + * 0.判断是不是合法的model + */ + protected isValidValue(value: any): boolean { + return true; + } + + /** + * @ignore + * 1.配置时间日期面板接口 + */ + protected setPickerDate(): void {} + + /** + * @ignore + * 2.根据format格式model值 + */ + protected formatValue(): void {} + + /** + * @ignore + * 3.根据当前值设置确认按钮的状态 + */ + protected setOkBtnState(): void {} + + /** + * @ignore + * 起始面板日期有变化时,要重新设置其time组件对应最大值 datetimeRange组件中实现此方法 + */ + protected setBeginTimeMaxValue(): void {} + + /** + * @ignore + * 起始面板日期有变化时,要重新设置其time组件对应最小值 datetimeRange组件中实现此方法 + */ + protected setBeginTimeMinValue(): void {} + + /** + * @ignore + * 结束日期有变化时,要重新设置其time组件对应最大值 datetimeRange组件中实现此方法 + */ + protected setEndTimeMaxValue(): void {} + + /** + * @ignore + * 结束日期有变化时,要重新设置其time组件对应最小值 datetimeRange组件中实现此方法 + */ + protected setEndTimeMinValue(): void {} +} diff --git a/src/datebase/lib/src/TiDateBaseModule.ts b/src/datebase/lib/src/TiDateBaseModule.ts new file mode 100644 index 0000000..197c5fa --- /dev/null +++ b/src/datebase/lib/src/TiDateBaseModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiDateBaseComponent } from './TiDateBaseComponent'; +// 在使用tiny npm包生产环境编辑时,要求基类也注册。 +/** + * @ignore + */ +@NgModule({ + imports: [], + exports: [], + declarations: [TiDateBaseComponent] +}) +export class TiDateBaseModule {} +export { TiDateBaseComponent, TimeOptions, TiPicker, TiDatetimeFormat, TiDateValue, TiDateCustomizeOptions } from './TiDateBaseComponent'; diff --git a/src/datedominator/demo/project.json b/src/datedominator/demo/project.json new file mode 100644 index 0000000..b33fbc2 --- /dev/null +++ b/src/datedominator/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/datedominator/demo", + "sourceRoot": "src/datedominator/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/datedominator", + "index": "src/datedominator/demo/src/index.html", + "main": "src/datedominator/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/datedominator/demo/tsconfig.app.json", + "assets": ["src/datedominator/demo/src/favicon.ico", "src/datedominator/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "datedominator-demo:build:production" + }, + "development": { + "browserTarget": "datedominator-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js datedominator" + } + ] + } + } + } +} diff --git a/src/datedominator/demo/src/app/AppComponent.ts b/src/datedominator/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/datedominator/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/datedominator/demo/src/app/AppModule.ts b/src/datedominator/demo/src/app/AppModule.ts new file mode 100644 index 0000000..0a586aa --- /dev/null +++ b/src/datedominator/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { DateDominatorTestModule } from './datedominator/DateDominatorTestModule'; + +@NgModule({ + imports: [ + DateDominatorTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/datedominator/demo/src/app/IndexComponent.ts b/src/datedominator/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..7075deb --- /dev/null +++ b/src/datedominator/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { DateDominatorTestModule } from './datedominator/DateDominatorTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = DateDominatorTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/datedominator/demo/src/app/app.html b/src/datedominator/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/datedominator/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/datedominator/demo/src/app/datedominator/DateDominatorComponent.ts b/src/datedominator/demo/src/app/datedominator/DateDominatorComponent.ts new file mode 100644 index 0000000..0bd0f27 --- /dev/null +++ b/src/datedominator/demo/src/app/datedominator/DateDominatorComponent.ts @@ -0,0 +1,48 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './datedominator.html', // 指定组件模板 + styleUrls: ['./datedominator.less'], // 样式路径 + encapsulation: ViewEncapsulation.None +}) +export class DateDominatorComponent { + containerWidth: string = '200px'; + model: string = '2018/12/5'; + model1: string = '2011-07-02 ─ 2017-09-27'; + format: string = 'yyyy-MM-dd'; + clear: boolean = true; + clear1: boolean = true; + disabled: boolean = true; + + clearClick(): void { + if (this.disabled) { + console.log('点击删除按钮'); + return; + } + this.clear = false; + this.model = null; + console.log('点击删除按钮'); + } + + clickFn(): void { + console.log('点击dominator'); + } + + clearClick1(): void { + this.clear1 = false; + this.model1 = null; + console.log('点击删除按钮'); + } + + clickFn1(): void { + console.log('点击dominator'); + } + + showLineClick(): void { + this.model1 = '2011-07-02 ─ 2017-09-27'; + } + + showLineClick1(): void { + this.model1 = ''; + } +} diff --git a/src/datedominator/demo/src/app/datedominator/DateDominatorTestModule.ts b/src/datedominator/demo/src/app/datedominator/DateDominatorTestModule.ts new file mode 100644 index 0000000..ae77abf --- /dev/null +++ b/src/datedominator/demo/src/app/datedominator/DateDominatorTestModule.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiDateDominatorModule } from '@opentiny/ng'; + +import { DateDominatorComponent } from './DateDominatorComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiDateDominatorModule, RouterModule.forChild(DateDominatorTestModule.ROUTES)], + declarations: [DateDominatorComponent] +}) +export class DateDominatorTestModule { + static readonly LINKS: Array = [ + { + href: 'components/TiDateDominatorComponent.html', + label: 'DateDominator' + } + ]; + static readonly ROUTES: Routes = [ + { + path: 'datedominator/datedominator-basic', + component: DateDominatorComponent + } + ]; +} diff --git a/src/datedominator/demo/src/app/datedominator/datedominator.html b/src/datedominator/demo/src/app/datedominator/datedominator.html new file mode 100644 index 0000000..f38b09a --- /dev/null +++ b/src/datedominator/demo/src/app/datedominator/datedominator.html @@ -0,0 +1,34 @@ +

1.date组件

+ +

2.dateRange组件

+ +

+ + +

3.dominator宽度不够时出...,鼠标悬浮出tip

+开始日期-结束日期 diff --git a/src/datedominator/demo/src/app/datedominator/datedominator.less b/src/datedominator/demo/src/app/datedominator/datedominator.less new file mode 100644 index 0000000..9d16cce --- /dev/null +++ b/src/datedominator/demo/src/app/datedominator/datedominator.less @@ -0,0 +1,28 @@ +.dominator-input-container { + display: inline-block; + vertical-align: middle; + height: 32px; + line-height: normal; + position: relative; + color: #333; + background-color: #fff; + -webkit-box-shadow: 0 1px 1px 0px #f6f6f6 inset; + box-shadow: 0 1px 1px 0px #f6f6f6 inset; + border: 1px solid #ccc; + border-radius: 2px; + -webkit-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; +} + +.date-width { + width: 200px !important; +} + +.range-width { + width: 262px !important; +} + +.daterange-width { + width: 100px !important; +} diff --git a/src/datedominator/demo/src/favicon.ico b/src/datedominator/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/datedominator/demo/src/index.html b/src/datedominator/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/datedominator/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/datedominator/demo/src/main.ts b/src/datedominator/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/datedominator/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/datedominator/demo/tsconfig.app.json b/src/datedominator/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/datedominator/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/datedominator/lib/index.ts b/src/datedominator/lib/index.ts new file mode 100644 index 0000000..5781cac --- /dev/null +++ b/src/datedominator/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiDateDominatorModule'; diff --git a/src/datedominator/lib/ng-package.json b/src/datedominator/lib/ng-package.json new file mode 100644 index 0000000..8a84427 --- /dev/null +++ b/src/datedominator/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/datedominator", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/datedominator/lib/package.json b/src/datedominator/lib/package.json new file mode 100644 index 0000000..08f1afe --- /dev/null +++ b/src/datedominator/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-datedominator", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/datedominator/lib/project.json b/src/datedominator/lib/project.json new file mode 100644 index 0000000..821799f --- /dev/null +++ b/src/datedominator/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/datedominator/lib", + "sourceRoot": "src/datedominator/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/datedominator"], + "options": { + "project": "src/datedominator/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/datedominator"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js datedominator" + }, + { + "command": "ng default-build datedominator" + }, + { + "command": "node build/clear-default-theme.js datedominator" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/datedominator && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build datedominator && ng pack datedominator && node build/publish.js datedominator --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/datedominator/lib/src/TiDateDominatorComponent.ts b/src/datedominator/lib/src/TiDateDominatorComponent.ts new file mode 100644 index 0000000..00b8145 --- /dev/null +++ b/src/datedominator/lib/src/TiDateDominatorComponent.ts @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ElementRef, + EventEmitter, + Input, + Output, + Renderer2, + ViewChild, + ViewEncapsulation, + ChangeDetectionStrategy +} from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +/** + * @ignore + */ +@Component({ + selector: 'ti-date-dominator', + templateUrl: './datedominator.html', + styleUrls: ['./datedominator.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '(mouseenter)': 'onMouseenter()', + '(mouseleave)': 'onMouseleave()', + '[class.ti3-date-dominator-container]': 'true' + }, + encapsulation: ViewEncapsulation.None, // 灰化设置样式 + providers: [TiFormComponent.getValueAccessor(TiDateDominatorComponent)] +}) +export class TiDateDominatorComponent extends TiFormComponent { + @Input() clearable: boolean; // 是否显示删除按钮 + @Input() format: string; // 该接口主要用于range组件中线的定位:用于计算宽度 + @Input() type: string; // 类型:'date','range' + @Input() isTime: boolean; // 是否是纯时间组件 + @Output() readonly clear: EventEmitter = new EventEmitter(); // 点击删除按钮的事件回调 + /** + * @ignore + * 是否显示清除图标 + */ + public showClear: boolean; + + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + super.ngOnInit(); + this.setFocusableElems([this.nativeElement]); + } + + onIconClearClick(event: MouseEvent): void { + this.clear.emit(event); + } + + onMouseenter(): void { + this.showClear = true; + } + onMouseleave(): void { + this.showClear = false; + } +} diff --git a/src/datedominator/lib/src/TiDateDominatorModule.ts b/src/datedominator/lib/src/TiDateDominatorModule.ts new file mode 100644 index 0000000..5553c10 --- /dev/null +++ b/src/datedominator/lib/src/TiDateDominatorModule.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiDateDominatorComponent } from './TiDateDominatorComponent'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule, TiIconModule, TiOverflowModule], + exports: [TiDateDominatorComponent], + declarations: [TiDateDominatorComponent] +}) +export class TiDateDominatorModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiDateDominatorComponent } from './TiDateDominatorComponent'; diff --git a/src/datedominator/lib/src/datedominator.html b/src/datedominator/lib/src/datedominator.html new file mode 100644 index 0000000..9b3fa7e --- /dev/null +++ b/src/datedominator/lib/src/datedominator.html @@ -0,0 +1,17 @@ + +
{{model}}
+ + +
+ +
+
+
diff --git a/src/datedominator/lib/src/datedominator.less b/src/datedominator/lib/src/datedominator.less new file mode 100644 index 0000000..8a6fa3d --- /dev/null +++ b/src/datedominator/lib/src/datedominator.less @@ -0,0 +1,102 @@ +@import '../../../themes/basic/base-all.less'; + +ti-date-dominator { + --ti-date-dominator-container-height: 26px; + --ti-date-icon-width: 35px; +} + +/* 时间日期显示框的样式 */ +.ti3-date-dominator-container { + display: inline-block; + height: var(--ti-date-dominator-container-height); + width: calc(100% + 2 * var(--ti-common-border-weight-normal)); + margin: 0 calc(-1 * var(--ti-common-border-weight-normal)); + vertical-align: middle; + line-height: normal; + position: relative; + color: var(--ti-common-color-text-primary); + border-left: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) transparent; + border-right: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) transparent; + cursor: pointer; + .box-sizing(border-box); + &:focus { + outline: none; + } + + // 日期显示文本样式 + & .ti3-date-show { + display: inline-block; + position: absolute; + left: 0; + height: 100%; + line-height: var(--ti-date-dominator-container-height); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + width: calc(100% - var(--ti-date-icon-width)); + text-align: left; + vertical-align: inherit; + background-color: transparent; + padding-left: var(--ti-common-space-10); + .box-sizing(border-box); + .user-select(); + + &:before { + content: ''; + display: inline-block; + height: 100%; + line-height: 100%; + vertical-align: middle; + } + } + + // placeholder 样式 + & .ti3-date-placeholder-container { + color: var(--ti-common-color-text-disabled); + width: calc(100% - var(--ti-date-icon-width)); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + } + + // 时间日期图标样式 + & .ti3-date-icon-container { + display: inline-block; + position: absolute; + top: 0; + right: 0; + left: calc(100% - var(--ti-date-icon-width)); + font-size: var(--ti-common-size-4x); + width: var(--ti-date-icon-width); + color: var(--ti-common-color-icon-normal); + text-align: right; + padding: var(--ti-common-space-0) var(--ti-common-space-10); + line-height: var(--ti-date-dominator-container-height); + cursor: pointer; + .box-sizing(border-box); + + &:before { + position: relative; + top: calc(50% - var(--ti-common-size-4x) / 2); + } + } +} + +// 非禁用状态的样式:hover,focus +.ti3-date-dominator-container:not([disabled]) { + & .ti3-date-icon-container:hover { + color: var(--ti-common-color-icon-hover); + } +} + +// 禁用状态样式 +.ti3-date-dominator-container[disabled] { + cursor: not-allowed; + color: var(--ti-common-color-text-disabled); + & .ti3-date-icon-container { + color: var(--ti-common-color-icon-disabled); + cursor: not-allowed; + } +} + +.ti3-date-icon-container.ti3-icon-close { + font-size: var(--ti-common-size-3x); +} diff --git a/src/datedominator/lib/src/i18n/TiDatedominatorWords.ts b/src/datedominator/lib/src/i18n/TiDatedominatorWords.ts new file mode 100644 index 0000000..8cf9208 --- /dev/null +++ b/src/datedominator/lib/src/i18n/TiDatedominatorWords.ts @@ -0,0 +1,21 @@ +export interface TiDatedominatorWords { + tiDatedominator: { + datePlaceholder: string; + datetimePlaceholder: string; + rangeBeginLabel: string; + rangeEndLabel: string; + selectTime: string; + selectDate: string; + yearRangeBeginLabel: string; + yearRangeEndLabel: string; + monthRangeBeginLabel: string; + monthRangeEndLabel: string; + quarterRangeEndLabel: string; + quarterRangeBeginLabel: string; + yearSelectPlaceholder: string; + monthSelectPlaceholder: string; + quarterSelectPlaceholder: string; + utcZone: string; + localZone: string; + }; +} diff --git a/src/datedominator/lib/src/i18n/en_US.ts b/src/datedominator/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..15c35f3 --- /dev/null +++ b/src/datedominator/lib/src/i18n/en_US.ts @@ -0,0 +1,23 @@ +import { TiDatedominatorWords } from './TiDatedominatorWords'; + +export const en_US: TiDatedominatorWords = { + tiDatedominator: { + datePlaceholder: 'Select a date.', + datetimePlaceholder: 'Select a date and time.', + rangeBeginLabel: 'Start Date', + rangeEndLabel: 'End Date', + selectTime: 'Select Time', + selectDate: 'Select Date', + yearRangeBeginLabel: 'Start year', + yearRangeEndLabel: 'End year', + monthRangeBeginLabel: 'Start month', + monthRangeEndLabel: 'End month', + quarterRangeEndLabel: 'End quarter', + quarterRangeBeginLabel: 'Start quarter', + yearSelectPlaceholder: 'Select a year.', + monthSelectPlaceholder: 'Select a month.', + quarterSelectPlaceholder: 'Select a quarter.', + utcZone: 'UTC/GMT', + localZone: 'Local time zone' + } +}; diff --git a/src/datedominator/lib/src/i18n/es_US.ts b/src/datedominator/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..cfcc7aa --- /dev/null +++ b/src/datedominator/lib/src/i18n/es_US.ts @@ -0,0 +1,23 @@ +import { TiDatedominatorWords } from './TiDatedominatorWords'; + +export const es_US: TiDatedominatorWords = { + tiDatedominator: { + datePlaceholder: 'Seleccione una fecha.', + datetimePlaceholder: 'Seleccione una fecha y hora.', + rangeBeginLabel: 'Inicio', + rangeEndLabel: 'Fin', + selectTime: 'Seleccionar hora', + selectDate: 'Seleccionar fecha', + yearSelectPlaceholder: 'Seleccione un año.', + monthSelectPlaceholder: 'Seleccione un mes.', + quarterSelectPlaceholder: 'Seleccione un trimestre.', + yearRangeBeginLabel: 'Año de inicio', + yearRangeEndLabel: 'Año de fin', + monthRangeBeginLabel: 'Mes de inicio', + monthRangeEndLabel: 'Mes de fin', + quarterRangeBeginLabel: 'Trimestre de inicio', + quarterRangeEndLabel: 'Trimestre de fin', + utcZone: 'UTC/GMT', + localZone: 'Local time zone' + } +}; diff --git a/src/datedominator/lib/src/i18n/fr_FR.ts b/src/datedominator/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..63906c3 --- /dev/null +++ b/src/datedominator/lib/src/i18n/fr_FR.ts @@ -0,0 +1,23 @@ +import { TiDatedominatorWords } from './TiDatedominatorWords'; + +export const fr_FR: TiDatedominatorWords = { + tiDatedominator: { + datePlaceholder: 'Sélectionnez une date.', + datetimePlaceholder: 'Sélectionnez une date et une heure.', + rangeBeginLabel: 'Date de début', + rangeEndLabel: 'Date de fin', + selectTime: `Sélectionner l'heure`, + selectDate: 'Sélectionner la date', + yearSelectPlaceholder: 'Sélectionnez une année.', + monthSelectPlaceholder: 'Sélectionnez un mois.', + quarterSelectPlaceholder: 'Sélectionnez un trimestre.', + yearRangeBeginLabel: 'Année de début', + yearRangeEndLabel: 'Année de fin', + monthRangeBeginLabel: 'Mois de début', + monthRangeEndLabel: 'Fin du mois', + quarterRangeBeginLabel: 'Début du trimestre', + quarterRangeEndLabel: 'Fin du trimestre', + utcZone: 'UTC/GMT', + localZone: 'Local time zone' + } +}; diff --git a/src/datedominator/lib/src/i18n/index.ts b/src/datedominator/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/datedominator/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/datedominator/lib/src/i18n/pt_BR.ts b/src/datedominator/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..1b6b8fb --- /dev/null +++ b/src/datedominator/lib/src/i18n/pt_BR.ts @@ -0,0 +1,23 @@ +import { TiDatedominatorWords } from './TiDatedominatorWords'; + +export const pt_BR: TiDatedominatorWords = { + tiDatedominator: { + datePlaceholder: 'Selecione uma data.', + datetimePlaceholder: 'Selecione uma data e um horário.', + rangeBeginLabel: 'Data de início', + rangeEndLabel: 'Data de término', + selectTime: 'Selecionar horário', + selectDate: 'Selecionar data', + yearSelectPlaceholder: 'Selecione um ano.', + monthSelectPlaceholder: 'Selecione um mês.', + quarterSelectPlaceholder: 'Selecione um trimestre.', + yearRangeBeginLabel: 'Ano de início', + yearRangeEndLabel: 'Ano de término', + monthRangeBeginLabel: 'Mês de início', + monthRangeEndLabel: 'Mês de término', + quarterRangeBeginLabel: 'Trimestre de início', + quarterRangeEndLabel: 'Trimestre de término', + utcZone: 'UTC/GMT', + localZone: 'Local time zone' + } +}; diff --git a/src/datedominator/lib/src/i18n/zh_CN.ts b/src/datedominator/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..3ccc89e --- /dev/null +++ b/src/datedominator/lib/src/i18n/zh_CN.ts @@ -0,0 +1,23 @@ +import { TiDatedominatorWords } from './TiDatedominatorWords'; + +export const zh_CN: TiDatedominatorWords = { + tiDatedominator: { + datePlaceholder: '请选择日期', + datetimePlaceholder: '请选择日期时间', + rangeBeginLabel: '开始日期', + rangeEndLabel: '结束日期', + selectTime: '选择时间', + selectDate: '选择日期', + yearRangeBeginLabel: '开始年份', + yearRangeEndLabel: '结束年份', + monthRangeBeginLabel: '开始月份', + monthRangeEndLabel: '结束月份', + quarterRangeEndLabel: '结束季度', + quarterRangeBeginLabel: '开始季度', + yearSelectPlaceholder: '请选择年份', + monthSelectPlaceholder: '请选择月份', + quarterSelectPlaceholder: '请选择季度', + utcZone: 'UTC/GMT', + localZone: '本地时区' + } +}; diff --git a/src/dateedit/demo/project.json b/src/dateedit/demo/project.json new file mode 100644 index 0000000..f5cc1ed --- /dev/null +++ b/src/dateedit/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/dateedit/demo", + "sourceRoot": "src/dateedit/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/dateedit", + "index": "src/dateedit/demo/src/index.html", + "main": "src/dateedit/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/dateedit/demo/tsconfig.app.json", + "assets": ["src/dateedit/demo/src/favicon.ico", "src/dateedit/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "dateedit-demo:build:production" + }, + "development": { + "browserTarget": "dateedit-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js dateedit" + } + ] + } + } + } +} diff --git a/src/dateedit/demo/src/app/AppComponent.ts b/src/dateedit/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/dateedit/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/dateedit/demo/src/app/AppModule.ts b/src/dateedit/demo/src/app/AppModule.ts new file mode 100644 index 0000000..2006928 --- /dev/null +++ b/src/dateedit/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { DateEditTestModule } from './dateedit/DateEditTestModule'; + +@NgModule({ + imports: [ + DateEditTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/dateedit/demo/src/app/IndexComponent.ts b/src/dateedit/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..14362a9 --- /dev/null +++ b/src/dateedit/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { DateEditTestModule } from './dateedit/DateEditTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = DateEditTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/dateedit/demo/src/app/app.html b/src/dateedit/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/dateedit/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/dateedit/demo/src/app/dateedit/DateEditComponent.ts b/src/dateedit/demo/src/app/dateedit/DateEditComponent.ts new file mode 100644 index 0000000..e2175d7 --- /dev/null +++ b/src/dateedit/demo/src/app/dateedit/DateEditComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './dateedit.html', // 指定组件模板 + styleUrls: ['./dateedit.less'] // 样式路径 +}) +export class DateEditComponent { + value: Date = new Date(2015, 1, 4); + format: string = 'yyyy-MM-dd'; + value1: Date = new Date(2017, 1, 23); + format1: string = 'yyyy.MM.dd'; + + blurFn(e: any): void { + console.log(e); + } + + ngModelChange(e: any): void { + console.log(e, 'change'); + } +} diff --git a/src/dateedit/demo/src/app/dateedit/DateEditFormatComponent.ts b/src/dateedit/demo/src/app/dateedit/DateEditFormatComponent.ts new file mode 100644 index 0000000..9700644 --- /dev/null +++ b/src/dateedit/demo/src/app/dateedit/DateEditFormatComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './dateedit-format.html', // 指定组件模板 + styleUrls: ['./dateedit.less'] // 样式路径 +}) +export class DateEditFormatComponent { + value: Date = new Date(2015, 1, 12); + value1: Date = new Date(2017, 1, 23); + value2: Date = new Date(2016, 12, 5); + value3: Date = new Date(2015, 1, 12); + format: string = ''; + format1: string = 'dd/MM/yyyy'; + + clickFn(): void { + this.format1 = 'yyyy/MM/dd'; + } + + clickFn1(): void { + this.format1 = 'yyyy.MM.dd'; + } +} diff --git a/src/dateedit/demo/src/app/dateedit/DateEditMaxComponent.ts b/src/dateedit/demo/src/app/dateedit/DateEditMaxComponent.ts new file mode 100644 index 0000000..d131ae5 --- /dev/null +++ b/src/dateedit/demo/src/app/dateedit/DateEditMaxComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './dateedit-max.html', // 指定组件模板 + styleUrls: ['./dateedit.less'] // 样式路径 +}) +export class DateEditMaxComponent { + value: Date = new Date(2015, 1, 23); + max: Date = new Date(2016, 1, 23); + value1: Date = new Date(2016, 10, 5); + max1: Date = new Date(2020, 10, 12); + value2: Date = new Date(2016, 10, 5); + + clickFn(): void { + this.max1 = new Date(2060, 10, 12); + } + + clickFn1(): void { + this.max1 = new Date(2019, 10, 12); + } +} diff --git a/src/dateedit/demo/src/app/dateedit/DateEditMinComponent.ts b/src/dateedit/demo/src/app/dateedit/DateEditMinComponent.ts new file mode 100644 index 0000000..ae5d751 --- /dev/null +++ b/src/dateedit/demo/src/app/dateedit/DateEditMinComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './dateedit-min.html', // 指定组件模板 + styleUrls: ['./dateedit.less'] // 样式路径 +}) +export class DateEditMinComponent { + value: Date = new Date(2015, 1, 23); + min: Date = new Date(2017, 1, 23); + value1: Date = new Date(2016, 10, 5); + min1: Date = new Date(2010, 10, 12); + value2: Date = new Date(2016, 10, 5); + + clickFn(): void { + this.min1 = new Date(2006, 10, 12); + } + + clickFn1(): void { + this.min1 = new Date(2019, 10, 12); + } +} diff --git a/src/dateedit/demo/src/app/dateedit/DateEditTestModule.ts b/src/dateedit/demo/src/app/dateedit/DateEditTestModule.ts new file mode 100644 index 0000000..dcb2515 --- /dev/null +++ b/src/dateedit/demo/src/app/dateedit/DateEditTestModule.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiDateEditModule } from '@opentiny/ng'; + +import { DateEditComponent } from './DateEditComponent'; +import { DateEditFormatComponent } from './DateEditFormatComponent'; +import { DateEditMaxComponent } from './DateEditMaxComponent'; +import { DateEditValueComponent } from './DateEditValueComponent'; +import { DateEditMinComponent } from './DateEditMinComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiDateEditModule, RouterModule.forChild(DateEditTestModule.ROUTES)], + declarations: [DateEditComponent, DateEditFormatComponent, DateEditMaxComponent, DateEditValueComponent, DateEditMinComponent] +}) +export class DateEditTestModule { + static readonly LINKS: Array = [{ href: 'components/TiDateEditComponent.html', label: 'DateEdit' }]; + static readonly ROUTES: Routes = [ + { path: 'dateedit/dateedit-basic', component: DateEditComponent }, + { path: 'dateedit/dateedit-value', component: DateEditValueComponent }, + { path: 'dateedit/dateedit-format', component: DateEditFormatComponent }, + { path: 'dateedit/dateedit-max', component: DateEditMaxComponent }, + { path: 'dateedit/dateedit-min', component: DateEditMinComponent } + ]; +} diff --git a/src/dateedit/demo/src/app/dateedit/DateEditValueComponent.ts b/src/dateedit/demo/src/app/dateedit/DateEditValueComponent.ts new file mode 100644 index 0000000..140e6a9 --- /dev/null +++ b/src/dateedit/demo/src/app/dateedit/DateEditValueComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './dateedit-value.html', // 指定组件模板 + styleUrls: ['./dateedit.less'] // 样式路径 +}) +export class DateEditValueComponent { + value: Date = null; + value1: Date = new Date(2016, 7, 9); + value2: Date = new Date(2016, 7, 9); + + min1: Date = new Date(2010, 3, 1); + max1: Date = new Date(2021, 6, 30); + + min2: Date = new Date(1992, 5, 30); + max2: Date = new Date(2099, 11, 6); + + clickFn(): void { + this.value2 = new Date(2099, 3, 1); + } + + clickFn1(): void { + this.value2 = new Date(2017, 9, 12); + } +} diff --git a/src/dateedit/demo/src/app/dateedit/dateedit-format.html b/src/dateedit/demo/src/app/dateedit/dateedit-format.html new file mode 100644 index 0000000..005d66d --- /dev/null +++ b/src/dateedit/demo/src/app/dateedit/dateedit-format.html @@ -0,0 +1,14 @@ +

1.format:设置为非字符串类型、不设置时,使用默认类型

+ +

+ +

2.format:设置为空

+ +

+

3.format:设置yyyy/MM

+ +

+

4.format:设置dd/MM/yyyy

+ + + diff --git a/src/dateedit/demo/src/app/dateedit/dateedit-max.html b/src/dateedit/demo/src/app/dateedit/dateedit-max.html new file mode 100644 index 0000000..12629ba --- /dev/null +++ b/src/dateedit/demo/src/app/dateedit/dateedit-max.html @@ -0,0 +1,12 @@ +

1.max:不设置、设置为非法日期时,默认最大值限制2099.12.31

+ +

+

2.max:设置的max小于设置的value时,以最大值显示;max:2016/2/23;value:2016/11/5

+ +

+

3.max:设置为正常日期值时,显示及用户操作改变时都必须小于这个最大值;max:2020/11/12;value:2016/11/5

+ +

+ + + diff --git a/src/dateedit/demo/src/app/dateedit/dateedit-min.html b/src/dateedit/demo/src/app/dateedit/dateedit-min.html new file mode 100644 index 0000000..d4a4436 --- /dev/null +++ b/src/dateedit/demo/src/app/dateedit/dateedit-min.html @@ -0,0 +1,12 @@ +

1.min:不设置、设置为非法日期时,默认最小值限制1970.1.1

+ +

+

2.min:设置的minValue大于设置的value时,以最小值显示;min:2017/2/23;value:2016/11/5

+ +

+

3.min:设置为正常日期值时,显示及用户操作改变时都必须大于这个最小值;min:2010/11/12;value:2016/11/5

+ +

+ + + diff --git a/src/dateedit/demo/src/app/dateedit/dateedit-value.html b/src/dateedit/demo/src/app/dateedit/dateedit-value.html new file mode 100644 index 0000000..792792b --- /dev/null +++ b/src/dateedit/demo/src/app/dateedit/dateedit-value.html @@ -0,0 +1,18 @@ +

1.设置为null时,显示空白

+ +

+ +

2.设置为最小日期时,正常显示;min:2010/4/1; max:2021/7/30

+ +

+ +

2.设置为最大日期时,正常显示;min:2010/4/1; max:2021/7/30

+ +

+ +

3.设置为中间日期时,正常显示;min:1992/6/30; max:2099/12/6; value:2016/8/9

+ +

+ + + diff --git a/src/dateedit/demo/src/app/dateedit/dateedit.html b/src/dateedit/demo/src/app/dateedit/dateedit.html new file mode 100644 index 0000000..31bc673 --- /dev/null +++ b/src/dateedit/demo/src/app/dateedit/dateedit.html @@ -0,0 +1,6 @@ +

1.dateEdit组件测试ngModelChange接口测试

+ + +

+

2.dateEdit组件测试blur接口测试

+ diff --git a/src/dateedit/demo/src/app/dateedit/dateedit.less b/src/dateedit/demo/src/app/dateedit/dateedit.less new file mode 100644 index 0000000..b2053d9 --- /dev/null +++ b/src/dateedit/demo/src/app/dateedit/dateedit.less @@ -0,0 +1,17 @@ +.ti3-date-input-container { + display: inline-block; + vertical-align: middle; + height: 32px; + width: 200px; + line-height: normal; + position: relative; + color: #333; + background-color: #fff; + -webkit-box-shadow: 0 1px 1px 0px #f6f6f6 inset; + box-shadow: 0 1px 1px 0px #f6f6f6 inset; + border: 1px solid #ccc; + border-radius: 2px; + -webkit-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; +} diff --git a/src/dateedit/demo/src/favicon.ico b/src/dateedit/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/dateedit/demo/src/index.html b/src/dateedit/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/dateedit/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/dateedit/demo/src/main.ts b/src/dateedit/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/dateedit/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/dateedit/demo/tsconfig.app.json b/src/dateedit/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/dateedit/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/dateedit/lib/index.ts b/src/dateedit/lib/index.ts new file mode 100644 index 0000000..6540534 --- /dev/null +++ b/src/dateedit/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiDateEditModule'; diff --git a/src/dateedit/lib/ng-package.json b/src/dateedit/lib/ng-package.json new file mode 100644 index 0000000..2fad220 --- /dev/null +++ b/src/dateedit/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/dateedit", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/dateedit/lib/package.json b/src/dateedit/lib/package.json new file mode 100644 index 0000000..a0b7d5c --- /dev/null +++ b/src/dateedit/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-dateedit", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/dateedit/lib/project.json b/src/dateedit/lib/project.json new file mode 100644 index 0000000..76ec3f6 --- /dev/null +++ b/src/dateedit/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/dateedit/lib", + "sourceRoot": "src/dateedit/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/dateedit"], + "options": { + "project": "src/dateedit/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/dateedit"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js dateedit" + }, + { + "command": "ng default-build dateedit" + }, + { + "command": "node build/clear-default-theme.js dateedit" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/dateedit && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build dateedit && ng pack dateedit && node build/publish.js dateedit --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/dateedit/lib/src/TiDateEditComponent.ts b/src/dateedit/lib/src/TiDateEditComponent.ts new file mode 100644 index 0000000..23e675f --- /dev/null +++ b/src/dateedit/lib/src/TiDateEditComponent.ts @@ -0,0 +1,335 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + HostBinding, + Input, + Output, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { TiBrowser, TiDateUtil, TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiLocale, TiLocaleFormat } from '@opentiny/ng-locale'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +/** + * @ignore + */ +@Component({ + selector: 'ti-date-edit', + templateUrl: './dateedit.html', + styleUrls: ['../../../text/lib/src/text.less'], // 引用text的less文件 + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiDateEditComponent)] +}) +export class TiDateEditComponent extends TiFormComponent { + @Input() format: string; // 设置日期显示格式 + @Input() min: Date; // 设置最小日期值 + @Input() max: Date; // 设置最大日期值 + @Input() disabled: boolean; // 支持禁用 + @Input() isEndValue: boolean = false; // 在时间段组件中是否代表结束时间 + + @Input() disabledDays: Array; + @Input() time: string; // 时间值 + /** + * 当失焦或按回车键时触发的回调 + * + * 参数:时间值 + * + */ + @Output() readonly inputChange: EventEmitter = new EventEmitter(); + @ViewChild('input', { static: true }) inputRef: ElementRef; + /** + * @ignore + */ + @HostBinding('style.display') display: string = 'inline-block'; + + protected oldMinvalue: Date; + protected oldMaxvalue: Date; + private oldModel: Date; + private oldInputvalue: string; + private inputFormat: string; + public inputValue: string = ''; + public placehoderText: string; + public MAX: Date = TiDateUtil.maxDate(); + public MIN: Date = TiDateUtil.minDate(); + protected versionInfo: string = super.getVersion(packageInfo); + + writeValue(value: Date): void { + super.writeValue(value); + // model为null时,输入框不显示内容 + if (value === null) { + return; + } + + // 传入的model值是非日期时,设置为当前日期 + if (!TiDateUtil.isDate(value)) { + this.model = new Date(); + } else if (TiDateUtil.isBigger(value, this.max)) { + // 如果大于最大值,设置为最大值 + this.model = this.max; + } else if (TiDateUtil.isSmaller(value, this.min)) { + // 小于最小值,设置为最小值 + this.model = this.min; + } + } + + ngOnModelChange(): void { + this.formatValue(); + } + + ngOnInit(): void { + super.ngOnInit(); + // 设置聚焦元素为input + this.setFocusableElems([this.inputRef.nativeElement]); + // format接口校验:对时间日期进行国际化处理 + this.validateFormat(); + + // 最大最小值校验 + this.validateMinAndMax(); + + if (!Util.isUndefined(this.format)) { + this.setPlacehoderText(); + } + + this.oldMinvalue = this.min; + this.oldMaxvalue = this.max; + } + + ngOnChanges(changes: SimpleChanges): void { + // format支持动态变更 + if (changes['format'] && !changes['format'].firstChange) { + // 新的format是非法时,format值保持之前值不变 + if (!Util.isString(changes['format'].currentValue) && !Util.isString(changes['format'].currentValue.date)) { + this.format = changes['format'].previousValue; + + return; + } + if (!Util.isString(changes['format'].currentValue)) { + this.format = changes['format'].currentValue.date; + } + this.setPlacehoderText(); + this.formatValue(); + } + } + /** + * 不同场景下设置日期输入框placehoder文本 + */ + private setPlacehoderText(): void { + // 以下是为了处理英文状态下,dateEdit输入时有问题,将其placeholder调整为国际化以后的值 + const englishFormatArr: any = this.format.match(/M/g); + const formatPureArr: any = this.format.split(/[-\/\.\_,\s]/).filter(this.filterEmptyFn); + // 确定其为英文显示月份 + if (Util.isArray(englishFormatArr) && englishFormatArr.length === 3) { + this.inputFormat = formatPureArr.length === 3 ? 'MM/dd/yyyy' : 'MM/yyyy'; + } else if (this.format === 'mediumDate') { + // 显示格式为"mediumDate" + this.inputFormat = TiLocale.getLocale() === TiLocale.ZH_CN ? 'yyyy年M月dd日' : 'MM/d/yyyy'; + } else { + this.inputFormat = this.format; + } + + // 日期框提示文本按照大写显示 + this.placehoderText = this.inputFormat.toUpperCase(); + } + + onFocus(): void { + // 在输入框聚焦时获取当前合法的model值: + this.oldModel = this.model; + } + + onBlur(): void { + if (this.oldInputvalue !== this.inputValue) { + this.handleInputValue(); + } + } + + onKeydown(event: KeyboardEvent): void { + if (event.keyCode === TiKeymap.KEY_ENTER || event.keyCode === TiKeymap.KEY_NUMPAD_ENTER) { + this.handleInputValue(); + } + } + /** + * @ignore + */ + handleInputValue(): void { + // blur时拿到输入框的值 + this.inputValue = this.inputRef.nativeElement.value; + // 校验输入框中值:得到合法的model值 + if (this.inputValue !== '') { + if (this.format === 'YYYY/QQ') { + this.getValidQuarterModel(); + } else { + this.getValidModel(); + } + } else { + this.model = null; // 当用户输入为空或者为null的情况,model赋值为null + } + + // 根据model值格式化 + this.formatValue(); + this.oldInputvalue = this.inputValue; + } + + isValidValue(value: Date): boolean { + // value值为null时会将输入框清空,是一个合法的value值 + if (value === null) { + return true; + } + + // value值为一个Date类型值并且在最大最小值范围内时,是一个合法值 + if (TiDateUtil.isDate(value) && !TiDateUtil.isBigger(value, this.max) && !TiDateUtil.isSmaller(value, this.min)) { + return true; + } + + return false; + } + /** + * @ignore + */ + public filterEmptyFn(value: any): any { + return value !== ''; + } + + // 日期格式校验 + private validateFormat(): void { + if (Util.isString(this.format) && this.format !== '') { + return; + } + // TODO:如果配置时间日期国际化 + this.format = TiLocale.getLocaleWords().tiDateedit['date']; + } + + // 最大值最小值校验 + private validateMinAndMax(): void { + // 最大值合法性校验:(时分秒:23:59:59) + const maxTimeChanged: Date = TiDateUtil.changeMaxTime(this.max); + this.max = TiDateUtil.isDate(this.max) ? maxTimeChanged : this.MAX; + + // 最小值合法性校验:(时分秒:0:0:0) + const minTimeChanged: Date = TiDateUtil.changeMinTime(this.min); + this.min = TiDateUtil.isDate(this.min) ? minTimeChanged : this.MIN; + + // 最大最小值矛盾时,设置为默认值 + if (this.max.getTime() < this.min.getTime()) { + this.max = this.MAX; + this.min = this.MIN; + } + } + + // 根据model值和format接口,格式化显示时间日期 + public formatValue(): void { + if (this.format === 'YYYY/QQ') { + this.inputValue = TiDateUtil.transformDateToQuarter(this.model); + } else { + this.inputValue = this.model !== null ? TiLocaleFormat.formatDate(this.model, this.inputFormat) : ''; + } + this.oldInputvalue = this.inputValue; + } + + // 校验输入框中的值 + private getValidModel(): void { + // 日期时间支持分隔符:空格 + const datetimeArr: Array = this.inputValue.trim().split(' '); + const time: string = datetimeArr[1] ? TiDateUtil.addColon(datetimeArr[1]).trim() : ''; + + let dateValue: any = this.getDateValue(datetimeArr, time); + + // 1.输入值为不合法日期:2018-3-34; + // 2.输入值不在最小值最大值范围内; + // 3.当前输入框中的值,和上次输入框中的值相同时; + const invalidDate: boolean = String(dateValue) === 'Invalid Date' || !this.isValidValue(dateValue) || this.isDisabledDays(dateValue); + this.model = invalidDate ? this.oldModel : new Date(TiDateUtil.getDateStr(dateValue)); + + if (!invalidDate) { + this.inputChange.emit(time); + } + } + + private getValidQuarterModel(): void { + const value: Date = TiDateUtil.getValidQuarterDate(this.inputValue, this.isEndValue); + this.model = this.isValidValue(value) ? value : this.oldModel; + this.oldModel = this.model; + } + /** + * @ignore + * 是否为禁用日期 + */ + public isDisabledDays(value: Date): boolean { + return TiDateUtil.isDisabledDays(this.disabledDays, value); + } + + /** + * 获取当前输入框的值 + * @param datetimeArr 输入框中的日期时间 + * @param time 用户输入的时间 + * @returns 处理后的日期时间 + */ + private getDateValue(datetimeArr: Array, time: string): any { + const datetime: string = time ? `${datetimeArr[0]} ${time}` : datetimeArr[0]; + + // 支持分隔符:中划线,下划线,点,斜杠 + const reg: RegExp = /[年月日\-/._]/; + const dateArr: Array = datetime.split(reg); + + // 以年月日结尾的格式最后一项为空字符串,在后续合法性校验 dateArr.length > 3 时造成逻辑错误,故需要删除最后一项 + if (/[年月日]/.test(datetime) && !dateArr[dateArr.length - 1].length) { + dateArr.pop(); + } + + const formatArr: Array = this.inputFormat.split(reg); + const arr: Array = []; // 保存format格式下年月日的顺序下标;年月日,日月年,月日年,年月。。。 + for (let i: number = 0; i < formatArr.length; i++) { + if (formatArr[i].indexOf('y') !== -1) { + arr[0] = i; + } else if (formatArr[i].indexOf('M') !== -1) { + arr[1] = i; + } else if (formatArr[i].indexOf('d') !== -1) { + arr[2] = i; + } + } + // 处理年月情况 + if (arr.length === 2) { + dateArr[arr[2]] = '1'; + } + // 处理只有年份情况 + if (arr.length === 1) { + dateArr[arr[1]] = '1'; + dateArr[arr[2]] = '1'; + } + + // 浏览器兼容性处理:对于非法日期表现不一致:eg:new Date('2018/13/12'),谷歌和火狐处理为非法日期对象,而IE浏览器会处理为2019/1/12; + // 获取当前月总天数 + const totalDays: number = new Date(parseInt(dateArr[arr[0]], 10), parseInt(dateArr[arr[1]], 10), 0).getDate(); + let dateValue: any; + + // 将时间日期转换成字符串,原因:2018-3-34 不合法日期,用new Date(2018, 2, 34)生成日期对象,会处理成 ==>2018/4/3 + // 将其转换成字符串形式:new Date('2018/2/34')生成日期对象,处理成 ==>Invalid Date对象 + dateValue = + (TiBrowser.isIE() && (parseInt(dateArr[arr[1]], 10) > 12 || parseInt(dateArr[arr[2]], 10) > totalDays)) || + String(new Date(parseInt(dateArr[arr[0]], 10), parseInt(dateArr[arr[1]], 10), parseInt(dateArr[arr[2]], 10))) === 'Invalid Date' || + dateArr.length > 3 + ? 'Invalid Date' + : new Date(`${dateArr[arr[0]]}/${dateArr[arr[1]]}/${dateArr[arr[2]]}`); + + // 时间日期组件,若当前输入日期与最小值相等时,需要与时间一起进行合法值校验 + if (!time && TiDateUtil.isDateEqual(dateValue, this.min)) { + dateValue = this.time ? new Date(`${TiDateUtil.getDateStr(dateValue)} ${this.time}:999`) : this.min; + } + + return dateValue; + } +} diff --git a/src/dateedit/lib/src/TiDateEditModule.ts b/src/dateedit/lib/src/TiDateEditModule.ts new file mode 100644 index 0000000..21b09cb --- /dev/null +++ b/src/dateedit/lib/src/TiDateEditModule.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiDateEditComponent } from './TiDateEditComponent'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule, FormsModule, TiTextModule], + exports: [TiDateEditComponent], + declarations: [TiDateEditComponent] +}) +export class TiDateEditModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiDateEditComponent } from './TiDateEditComponent'; diff --git a/src/dateedit/lib/src/dateedit.html b/src/dateedit/lib/src/dateedit.html new file mode 100644 index 0000000..9eba40b --- /dev/null +++ b/src/dateedit/lib/src/dateedit.html @@ -0,0 +1,14 @@ + diff --git a/src/dateedit/lib/src/i18n/TiDateeditWords.ts b/src/dateedit/lib/src/i18n/TiDateeditWords.ts new file mode 100644 index 0000000..d0f8de5 --- /dev/null +++ b/src/dateedit/lib/src/i18n/TiDateeditWords.ts @@ -0,0 +1,5 @@ +export interface TiDateeditWords { + tiDateedit: { + date: string; + }; +} diff --git a/src/dateedit/lib/src/i18n/en_US.ts b/src/dateedit/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..676d399 --- /dev/null +++ b/src/dateedit/lib/src/i18n/en_US.ts @@ -0,0 +1,7 @@ +import { TiDateeditWords } from './TiDateeditWords'; + +export const en_US: TiDateeditWords = { + tiDateedit: { + date: 'MMM dd, yyyy' + } +}; diff --git a/src/dateedit/lib/src/i18n/es_US.ts b/src/dateedit/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..08fb8fc --- /dev/null +++ b/src/dateedit/lib/src/i18n/es_US.ts @@ -0,0 +1,7 @@ +import { TiDateeditWords } from './TiDateeditWords'; + +export const es_US: TiDateeditWords = { + tiDateedit: { + date: 'dd/MM/yyyy' + } +}; diff --git a/src/dateedit/lib/src/i18n/fr_FR.ts b/src/dateedit/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..57fff0f --- /dev/null +++ b/src/dateedit/lib/src/i18n/fr_FR.ts @@ -0,0 +1,7 @@ +import { TiDateeditWords } from './TiDateeditWords'; + +export const fr_FR: TiDateeditWords = { + tiDateedit: { + date: 'dd/MM/yyyy' + } +}; diff --git a/src/dateedit/lib/src/i18n/index.ts b/src/dateedit/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/dateedit/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/dateedit/lib/src/i18n/pt_BR.ts b/src/dateedit/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..6acf682 --- /dev/null +++ b/src/dateedit/lib/src/i18n/pt_BR.ts @@ -0,0 +1,7 @@ +import { TiDateeditWords } from './TiDateeditWords'; + +export const pt_BR: TiDateeditWords = { + tiDateedit: { + date: 'dd/MM/yyyy' + } +}; diff --git a/src/dateedit/lib/src/i18n/zh_CN.ts b/src/dateedit/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..82a8243 --- /dev/null +++ b/src/dateedit/lib/src/i18n/zh_CN.ts @@ -0,0 +1,7 @@ +import { TiDateeditWords } from './TiDateeditWords'; + +export const zh_CN: TiDateeditWords = { + tiDateedit: { + date: 'yyyy/MM/dd' + } +}; diff --git a/src/datepanel/lib/index.ts b/src/datepanel/lib/index.ts new file mode 100644 index 0000000..8f57226 --- /dev/null +++ b/src/datepanel/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiDatePanelModule'; diff --git a/src/datepanel/lib/ng-package.json b/src/datepanel/lib/ng-package.json new file mode 100644 index 0000000..d1dfec2 --- /dev/null +++ b/src/datepanel/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/datepanel", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/datepanel/lib/package.json b/src/datepanel/lib/package.json new file mode 100644 index 0000000..32fac87 --- /dev/null +++ b/src/datepanel/lib/package.json @@ -0,0 +1,17 @@ +{ + "name": "@opentiny/ng-datepanel", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-datebase": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-list": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/datepanel/lib/project.json b/src/datepanel/lib/project.json new file mode 100644 index 0000000..b6e6e65 --- /dev/null +++ b/src/datepanel/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/datepanel/lib", + "sourceRoot": "src/datepanel/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/datepanel"], + "options": { + "project": "src/datepanel/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/datepanel"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js datepanel" + }, + { + "command": "ng default-build datepanel" + }, + { + "command": "node build/clear-default-theme.js datepanel" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/datepanel && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build datepanel && ng pack datepanel && node build/publish.js datepanel --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/datepanel/lib/src/TiDatePanelComponent.ts b/src/datepanel/lib/src/TiDatePanelComponent.ts new file mode 100644 index 0000000..ad7f450 --- /dev/null +++ b/src/datepanel/lib/src/TiDatePanelComponent.ts @@ -0,0 +1,1691 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ElementRef, + EventEmitter, + Input, + Output, + Renderer2, + SimpleChanges, + ViewChild, + ChangeDetectionStrategy, + ChangeDetectorRef +} from '@angular/core'; +import { TiDateUtil, Util } from '@opentiny/ng-utils'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiDateValue, TimeOptions } from '@opentiny/ng-datebase'; +import { TiDropComponent } from '@opentiny/ng-drop'; +import { TiListComponent } from '@opentiny/ng-list'; +import packageInfo from '../package.json'; +import { TiDatepanelWords } from './i18n/TiDatepanelWords'; + +/** + * @ignore + * 面板内部年月日 + */ +export interface TiDatePanel { + year: number; + month: number; + day: number; +} + +/** + * @ignore + * 面板头部显示文本接口 + */ +export interface TiPickerHeadText { + year?: string; + month?: string; + yearRange?: string; + endpanelYearRange?: string; + onlyYear?: string; +} + +/** + * @ignore + * 年月日数据的接口:state状态;value年月日值 + */ +export interface TiDateValueAndState { + state: string; // 面板中每个年月日具体的状态:日期:状态值为上个月日期,当前月日期,下个月日期 + value: number | string; // TODO:类型为string或number:此处处理 this.local.monthArr.indexOf(month.value + '') + isToday?: boolean; + isMonth?: boolean; + isYear?: boolean; +} + +/** + * @ignore + * 最大年最小年范围 + */ +export interface TiYearRange { + min: number; + max: number; +} +/** + * @ignore + */ +@Component({ + selector: 'ti-date-panel', + templateUrl: './datepanel.html', + styleUrls: ['./datepanel.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiDatePanelComponent extends TiBaseComponent { + @Input() value: TiDateValue; + @Output() readonly valueChange: EventEmitter = new EventEmitter(); + + @Input() picker: string = 'day'; // 设置年、月、日面板 + @Output() readonly pickerChange: EventEmitter = new EventEmitter(); + + @Input() nowDateTime: Date; + @Input() max: Date; + @Input() min: Date; + @Input() format: string; + @Input() timeFormat: string; + @Input() disabledDays: Array; + @Input() endTimeDisabled: boolean; + @Input() beginTimeDisabled: boolean; + /** + * @ignore + * 面板内部焦点位置标志量 + */ + @Input() focusedPosition: string; + /** + * @ignore + * 面板呈现选择时间或者选择日期 标志量 + */ + @Input() selectTime: boolean; + /** + * @ignore + * 开始时间 + */ + @Input() beginTime: TimeOptions; + /** + * @ignore + * 结束时间 + */ + @Input() endTime: TimeOptions; + /** + * @ignore + * 判断是单日期(时间)还是日期(时间)段 + */ + @Input() isRange: boolean; + /** + * @ignore + * 输入框的日期是否为手动输入 + */ + @Input() isInputValue: boolean = false; + /** + * 开始日期是否禁用 + */ + @Input() isBeginFixed: boolean = false; + /** + * 结束日期是否禁用 + */ + @Input() isEndFixed: boolean = false; + + // 日期面板,月份面板点击每个日期、月份的事件回调 + @Output() readonly select: EventEmitter = new EventEmitter(); + // 时间面板选择时分秒的事件回调 + @Output() readonly selectTimeFn: EventEmitter = new EventEmitter(); + + @ViewChild('next', { static: true }) nextRef: ElementRef; + @ViewChild('beginHourContainer', { static: true }) + private beginHourContainerRef: ElementRef; + @ViewChild('beginMinuteContainer', { static: true }) + private beginMinuteContainerRef: ElementRef; + @ViewChild('beginSecondContainer', { static: true }) + private beginSecondContainerRef: ElementRef; + @ViewChild('endHourContainer', { static: true }) + private endHourContainerRef: ElementRef; + @ViewChild('endMinuteContainer', { static: true }) + private endMinuteContainerRef: ElementRef; + @ViewChild('endSecondContainer', { static: true }) + private endSecondContainerRef: ElementRef; + private date: TiDatePanel; // 内部时间值:包括年月日 + public dayArr: Array> = []; + public nextdayArr: Array> = []; + public monthArr: Array> = []; + public endpanelMonthArr: Array> = []; + public yearArr: Array> = []; + public quartersArr: Array = []; + public nextQuartersArr: Array = []; + public endpanelYearArr: Array> = []; + public pickerValue: TiPickerHeadText = {}; // 面板头部显示文本数据 + public nextPickerValue: TiPickerHeadText = {}; // 面板头部显示文本数据 + public nextYearPanelClick: boolean = false; // 纯年份面板点击的是否是右侧面板 + public isMaxMonth: boolean; + public isMinMonth: boolean; + private isMaxYear: boolean; + private isMinYear: boolean; + private isMaxYearRange: boolean; + private isMinYearRange: boolean; + private oldValue: TiDateValue = { + // 当初始值为null时,做新旧值对比渲染时间面板 + begin: undefined, + end: undefined + }; + public hourOptions: Array = this.setOptions(24); + public minuteOptions: Array = this.setOptions(60); + public secondOptions: Array = this.setOptions(60); + public endHourOptions: Array = this.setOptions(24); + public endMinuteOptions: Array = this.setOptions(60); + public endSecondOptions: Array = this.setOptions(60); + public beginHour: any; + public beginMinute: any; + public beginSecond: any; + public endHour: any; + public endMinute: any; + public endSecond: any; + public isChinese: boolean = true; + public onlyHour: boolean; + public onlyHourMinute: boolean; + protected versionInfo: string = super.getVersion(packageInfo); + + public local: { + weekArr: Array; + weekTitleArr: Array; + monthArr: Array; + yearText: string; + } = { + weekArr: [], + weekTitleArr: [], + monthArr: [], + yearText: '' + }; + + constructor( + protected render: Renderer2, + protected hostRef: ElementRef, + public changeDetectorRef: ChangeDetectorRef, + public drop: TiDropComponent + ) { + super(hostRef, render); + } + + ngOnInit(): void { + super.ngOnInit(); + const datePanelWords: TiDatepanelWords = TiLocale.getLocaleWords().tiDatepanel; + this.isChinese = TiLocale.getLocale() === TiLocale.ZH_CN; + this.local = { + weekArr: datePanelWords['weekNamesAbb'], + weekTitleArr: datePanelWords['weeknamesTitle'], + monthArr: datePanelWords['monthNamesAbb'], + yearText: datePanelWords['yearSuffixLabel'] + }; + this.drop.dropSubject.subscribe(() => { + this.setValue(); + }); + this.renderer.listen(this.nativeElement, 'mousedown', (event: MouseEvent): void => { + event.preventDefault(); // 防止dominator blur行为 + }); + + // 初始化设置时间面板 + this.setTimePanel(); + } + + private changeEndTimeDisabled(changes: SimpleChanges): void { + if (changes['endTimeDisabled'] && !changes['endTimeDisabled'].firstChange) { + this.endHourOptions = this.setOptions(24, 'end'); + this.endMinuteOptions = this.setOptions(60, 'end'); + this.endSecondOptions = this.setOptions(60, 'end'); + } + } + + private changeBeginTimeDisabled(changes: SimpleChanges): void { + if (changes['beginTimeDisabled'] && !changes['beginTimeDisabled'].firstChange) { + this.hourOptions = this.setOptions(24); + this.minuteOptions = this.setOptions(60); + this.secondOptions = this.setOptions(60); + } + } + + private changePicker(changes: SimpleChanges): void { + if (changes['picker'] && !changes['picker'].firstChange) { + if (Util.isUndefined(this.date)) { + return; + } + // 设置下拉面板的数据 + this.setDatePanel(); + } + } + private changeScope(changes: SimpleChanges): void { + // 最大最小值更新时,需要更新面板来保证最大最小值外的禁用有效 + if ((changes['max'] && !changes['max'].firstChange) || (changes['min'] && !changes['min'].firstChange)) { + if (Util.isUndefined(this.date)) { + return; + } + this.setDatePanel(); + } + } + + private changesSelectTime(changes: SimpleChanges): void { + if (changes['selectTime'] && !changes['selectTime'].firstChange) { + this.setTimeValue(this.beginTime.value); + + if (this.endTime) { + this.setTimeValue(this.endTime.value, true); + } + setTimeout(() => { + this.setTimepanelPosition(); + if (this.endTime) { + this.setTimepanelPosition(true); + } + }, 0); + } + } + + private changeBeginTime(changes: SimpleChanges): void { + if (changes['beginTime'] && !changes['beginTime'].firstChange) { + this.setTimeValue(changes['beginTime'].currentValue.value); + this.setTimepanelPosition(); + } + } + + private changeEndTime(changes: SimpleChanges): void { + if (changes['endTime'] && !changes['endTime'].firstChange) { + this.setTimeValue(changes['endTime'].currentValue.value, true); + this.setTimepanelPosition(true); + } + } + + private changeEndValue(changes: SimpleChanges): void { + if (TiDateUtil.isDate(this.value.end) && changes['focusedPosition'].currentValue === 'end') { + const num1: number = this.date.year - (this.date.year % 10); + const num2: number = this.value.end.getFullYear() - (this.value.end.getFullYear() % 10); + // num2 > num1表示end值在当前面板不可见,所以切换到end聚焦时,需要面板跳转,否则不需要(针对纯年份场景) + const isHideEndvalue: boolean = num2 > num1; + // 面板的切换需要根据this.picker做不同处理 + this.date = { + year: + (this.picker === 'onlyYearMonth' && this.date.year < this.value.end.getFullYear()) || + (this.picker === 'quarter' && this.date.year < this.value.end.getFullYear()) + ? this.value.end.getFullYear() - 1 + : this.picker === 'onlyYear' && isHideEndvalue + ? this.value.end.getFullYear() - 10 + : this.value.end.getFullYear(), + // 同年同月场景下,点击切换焦点面板不需要切换 + month: + this.value.end.getMonth() === this.value.begin?.getMonth() && this.value.end.getFullYear() === this.value.begin?.getFullYear() + ? this.value.end.getMonth() + 1 + : this.value.end.getMonth(), + day: this.value.end.getDate() + }; + } + } + private changesFocusedPosition(changes: SimpleChanges): void { + if (changes['focusedPosition'] && !changes['focusedPosition'].firstChange) { + this.changeEndValue(changes); + if (TiDateUtil.isDate(this.value.begin) && changes['focusedPosition'].currentValue === 'begin') { + this.date = { + year: this.date.year < this.value.begin.getFullYear() ? this.value.begin.getFullYear() - 1 : this.value.begin.getFullYear(), + month: this.value.begin.getMonth() + 1, + day: this.value.begin.getDate() + }; + } + this.setDatePanel(); + } + } + + private changesTimeFormat(changes: SimpleChanges): void { + if (changes['timeFormat'] && !changes['timeFormat'].firstChange) { + this.setTimePanel(); + } + } + + ngOnChanges(changes: SimpleChanges): void { + this.changeEndTimeDisabled(changes); + + this.changeBeginTimeDisabled(changes); + + // format支持动态变更 + this.changePicker(changes); + + // 最大最小值更新时,需要更新面板来保证最大最小值外的禁用有效 + this.changeScope(changes); + + // 根据传入值确定时间下拉选择项 + this.changesSelectTime(changes); + + this.changeBeginTime(changes); + + this.changeEndTime(changes); + + // 焦点切换时,面板需要根据焦点位置切换对应展示 + this.changesFocusedPosition(changes); + + this.changesTimeFormat(changes); + } + + /** + * 设置开始时间/结束时间各面板滚动位置 + */ + private setTimepanelPosition(isEndTime?: boolean): void { + if (isEndTime) { + this.timeHandleFn(this.endHour, this.endHourContainerRef); + this.timeHandleFn(this.endMinute, this.endMinuteContainerRef); + this.timeHandleFn(this.endSecond, this.endSecondContainerRef); + } else { + this.timeHandleFn(this.beginHour, this.beginHourContainerRef); + this.timeHandleFn(this.beginMinute, this.beginMinuteContainerRef); + this.timeHandleFn(this.beginSecond, this.beginSecondContainerRef); + } + } + + /** + * 设置开始时间/结束时间 + */ + private setTimeValue(value: string, isEndTime?: boolean): void { + const timeArr: Array = value.split(':'); + timeArr.forEach((item: string, index: number): void => { + if (item.length === 1) { + timeArr[index] = '0' + item; + } + }); + + if (isEndTime) { + this.endHour = this.endHourOptions.find((item: any) => item.label === timeArr[0]); + this.endMinute = this.endMinuteOptions.find((item: any) => item.label === timeArr[1]); + this.endSecond = this.endSecondOptions.find((item: any) => item.label === timeArr[2]); + } else { + this.beginHour = this.hourOptions.find((item: any) => item.label === timeArr[0]); + this.beginMinute = this.minuteOptions.find((item: any) => item.label === timeArr[1]); + this.beginSecond = this.secondOptions.find((item: any) => item.label === timeArr[2]); + } + } + + private timeHandleFn(model: any, ele: ElementRef): void { + if (model === undefined) { + return; + } + const index: number = model.label; + this.render.setProperty(ele.nativeElement.children[0], 'scrollTop', index * 30); + // TODO 动画效果 + } + + ngDoCheck(): void { + super.ngDoCheck(); + + // 只有点击面板打开时,才会有接口值,需要渲染面板,在没有打开时间日期面板时不需要渲染面板数据 + // daterange组件,动态更新最大最小值时,也更新了面板的最大最小值,此时可获取到对应更新后的值,因此需要添加最小值的容错处理 + if (Util.isUndefined(this.value) || Util.isUndefined(this.max) || Util.isUndefined(this.min)) { + return; + } + + if ( + !TiDateUtil.isDateEqual(this.oldValue['begin'], this.value['begin']) || + !TiDateUtil.isDateEqual(this.oldValue['end'], this.value['end']) + ) { + this.setValue(); + this.oldValue = { + begin: this.value.begin, + end: this.value.end + }; + } + // onpush模式下datepanel.value属性发生变化,面板数据不更新 + this.changeDetectorRef.markForCheck(); + } + private setTime(year: number): void { + this.setYearMonthText(); + this.setPreNextState(year, this.date.month); + this.setDayMonthArr(this.picker); + this.setQuarterArr(); + this.setNextQuarterArr(); + } + // 处理左侧年后退按钮事件 + onPreYearClick(event: MouseEvent): void { + // 最小月,最小年,最小年时间段:左按钮灰化 + if (this.isPickerYearMinValue()) { + return; + } + + // 日期和月份面板 + if (this.picker === 'day' || this.picker === 'month' || this.picker === 'onlyYearMonth' || this.picker === 'quarter') { + const year: number = this.date.year - 1; // 年份减1 + if (year >= this.min.getFullYear()) { + // 当年份大于等于最小值 + this.date.year = year; + if (this.isSmallerThanMinMonth(year, this.date.month)) { + this.date.month = this.min.getMonth() + 1; + } + this.setTime(year); + } + + return; + } + + // 年面板 + if (this.picker === 'year' || this.picker === 'onlyYear') { + // 当前年减10 + const year: number = this.date.year - 10; // 年份减10 + const minValueRange: TiYearRange = this.getYearRange(this.min.getFullYear()); // 最小年:年面板的范围 + const currentYearRange: TiYearRange = this.getYearRange(year); // 当前年面板的范围 + // 当年份大于等于最小年面板的最小值是有效的 + if (year >= minValueRange.min) { + this.date.year = currentYearRange.min; // 默认跳转到年面板的第一个有效年 + // 设置下拉面板头部显示文本 + this.setYearMonthText(); + + // 设置左右键状态 + this.setYearRangePreNextState(); + + // 设置年份面板的值和状态 + this.setYearArr(); + + this.setEndpanelYearArr(); + } + } + } + + // 处理左侧月后退按钮事件 + onPreMonthClick(event: MouseEvent, tag?: string): void { + // 日期面板 + let month: number; + let year: number; + if (this.date.month === 1) { + month = 12; + year = this.date.year - 1; + } else { + month = this.date.month - 1; + year = this.date.year; + } + + // 最小月,最小年,最小年时间段:左按钮灰化 + if (this.isMinMonth) { + if (tag === 'onDayClick') { + this.date.month = month; + this.date.year = year; + } + + return; + } + + if (!this.isSmallerThanMinMonth(year, month)) { + this.date.month = month; + this.date.year = year; + this.setYearMonthText(); + this.setPreNextState(year, month); + this.setDayArr(); + this.setNextDayArr(); + } + } + // 处理右侧月前进按钮事件 + onNextMonthClick(event: MouseEvent): void { + // 最大月,最大年,最大年时间段 + if (this.isMaxMonth) { + return; + } + + let month: number; + let year: number; + if (this.date.month === 12) { + month = 1; + year = this.date.year + 1; + } else { + month = this.date.month + 1; + year = this.date.year; + } + + if (!this.isBiggerThanMaxMonth(year, month)) { + this.date.month = month; + this.date.year = year; + this.setYearMonthText(); + this.setPreNextState(year, month); + this.setDayArr(); + this.setNextDayArr(); + } + } + // 处理右侧年前进按钮事件 + onNextYearClick(event: MouseEvent): void { + // 最大月,最大年,最大年时间段 + if (this.isPickerYearMaxValue()) { + return; + } + + // 日期和月份面板 + if (this.picker === 'day' || this.picker === 'month' || this.picker === 'onlyYearMonth' || this.picker === 'quarter') { + const year: number = this.date.year + 1; // 年份加1 + if (year <= this.max.getFullYear()) { + // 判断是否小于等于最大年 + this.date.year = year; + if (this.isBiggerThanMaxMonth(year, this.date.month)) { + this.date.month = this.max.getMonth() + 1; + } + this.setTime(year); + } + + return; + } + + // 年面板 + if (this.picker === 'year' || this.picker === 'onlyYear') { + const year: number = this.date.year + 10; // 年份加10 + + const maxValueRange: TiYearRange = this.getYearRange(this.max.getFullYear()); + const currentYearRange: TiYearRange = this.getYearRange(year); + // 当年份小于等于最大年面板的最大值是有效的 + if (year <= maxValueRange.max) { + this.date.year = currentYearRange.min; + // 设置下拉面板头部显示文本 + this.setYearMonthText(); + + // 设置左右键状态 + this.setYearRangePreNextState(); + + // 设置年份面板的值和状态 + this.setYearArr(); + + this.setEndpanelYearArr(); + } + } + } + + // 面板头部文本点击事件 + onHeadTextClick(picker: string): void { + // 点击日期面板头部,跳转到月份面板 + this.picker = picker; + this.pickerChange.emit(picker); + } + + // 日期面板:日期点击事件 + onDayClick(isNext: boolean, day: TiDateValueAndState, event: MouseEvent): void { + // 输入框的日期标识为非手动输入 + this.isInputValue = false; + if (day.state === 'disable') { + return; + } + if (day.state === 'preMonth') { + // 如果是上个月的日期则跳转到上个月 + this.onPreMonthClick(event, 'onDayClick'); + } else if (day.state === 'nextMonth') { + // 如果是下个月的日期则跳转到下个月 + this.onNextMonthClick(event); + } + let dateSelect: Date; + const newDate: TiDatePanel = JSON.parse(JSON.stringify(this.date)); + if (isNext) { + newDate.year = this.date.month === 12 ? this.date.year + 1 : this.date.year; + newDate.month = this.date.month === 12 ? 1 : this.date.month + 1; + dateSelect = new Date(newDate.year, newDate.month - 1, Number(day.value)); + } else { + dateSelect = new Date(this.date.year, this.date.month - 1, day.value as number); + } + + if (this.beginTime || this.endTime) { + dateSelect = new Date(` ${TiDateUtil.getDateStr(dateSelect)} ${TiDateUtil.getTimeStr(new Date())}`); + } + + if (this.focusedPosition !== 'begin' && this.isRange) { + this.value.end = TiDateUtil.transformDateToExactDate(dateSelect, this.picker, true); + + if (this.endTime?.value) { + this.value.end = new Date(` ${TiDateUtil.getDateStr(this.value.end)} ${TiDateUtil.addColon(this.endTime.value)}`); + } + } else { + this.value.begin = dateSelect; + + if (this.beginTime?.value) { + this.value.begin = new Date(` ${TiDateUtil.getDateStr(this.value.begin)} ${TiDateUtil.addColon(this.beginTime.value)}`); + } + + if (TiDateUtil.isDate(this.value.end) && TiDateUtil.isSmaller(this.value.end, this.value.begin)) { + this.value.end = null; + } + } + + this.valueChange.emit(this.value); + // 事件回调:点击日期执行面板收起操作 + this.select.emit(day.state); + } + // 月份面板:月份点击事件 + onMonthClick(month: TiDateValueAndState, event: MouseEvent, isEndClick?: boolean): void { + // 输入框的日期标识为非手动输入 + this.isInputValue = false; + + // 月份灰化不可点击 + if (month.state === 'disable') { + return; + } + // 当前月值变更 + this.date.month = this.local.monthArr.indexOf(String(month.value)) + 1; + // 根据当前月更新面板头部月份显示 + this.pickerValue = this.pickerValue instanceof Object ? this.pickerValue : {}; + this.nextPickerValue = this.nextPickerValue instanceof Object ? this.nextPickerValue : {}; + this.pickerValue['month'] = this.local.monthArr[this.date.month - 1]; + this.nextPickerValue['month'] = this.date.month === 12 ? this.local.monthArr[0] : this.local.monthArr[this.date.month]; + // 无日期面板:点击月份将value值变更。将执行select事件回调 + // 有日期面板:点击跳转到日期面板,根据年月设置左右按钮状态和日期面板值; + if (this.format.indexOf('d') === -1) { + // 只显示年月 + const yearNum: number = isEndClick ? this.date.year + 1 : this.date.year; + this.setTiDateValue(new Date(yearNum, this.date.month - 1)); + this.valueChange.emit(this.value); + this.select.emit(); + } else { + this.picker = 'day'; + this.pickerChange.emit(this.picker); + } + } + // 年范围面板:年点击事件 + onYearClick(year: TiDateValueAndState, event: MouseEvent): void { + // 输入框的日期标识为非手动输入 + this.isInputValue = false; + if (year.state !== 'disable') { + // 纯年份面板需判断用户点选的是否为右侧面板,放置面板跳转。 + if (this.picker === 'onlyYear') { + const num1: number = this.date.year - (this.date.year % 10); + const num2: number = Number(year.value) - (Number(year.value) % 10); + this.nextYearPanelClick = num2 > num1; + } + this.date.year = year.value as number; + this.pickerValue = this.pickerValue instanceof Object ? this.pickerValue : {}; + this.nextPickerValue = this.nextPickerValue instanceof Object ? this.nextPickerValue : {}; + this.pickerValue['year'] = `${this.date.year}${this.local.yearText}`; + this.nextPickerValue['year'] = + this.date.month === 12 ? `${this.date.year + 1}${this.local.yearText}` : `${this.date.year}${this.local.yearText}`; + this.nextPickerValue['onlyYear'] = `${this.date.year + 1}${this.local.yearText}`; + if (this.format === 'YYYY/QQ') { + this.picker = 'quarter'; + this.nextPickerValue['year'] = `${this.date.year + 1}${this.local.yearText}`; + this.setQuarterArr(); + this.setNextQuarterArr(); + this.pickerChange.emit(this.picker); + + return; + } + // 只有年份面板 + if (this.format.indexOf('M') === -1) { + this.setTiDateValue(new Date(this.date.year, 0, 1)); + this.valueChange.emit(this.value); + this.select.emit(); + return; + } + // 年月格式 + if (this.format.indexOf('d') === -1) { + this.picker = 'onlyYearMonth'; + this.setDayMonthArr(this.picker); + this.pickerChange.emit(this.picker); + + return; + } + this.picker = 'month'; + this.pickerChange.emit(this.picker); + } + } + + public onQuarterClick(quarter: any, next?: boolean): void { + // 输入框的日期标识为非手动输入 + this.isInputValue = false; + if (quarter.state === 'disable') { + return; + } + const year: number = next ? this.date.year + 1 : this.date.year; + if (this.focusedPosition === 'begin') { + this.value.begin = TiDateUtil.transFormQuarterToDate(year, quarter.value); + if (this.value.begin.getTime() > this.value.end?.getTime()) { + this.value.end = null; + } + } else { + this.value.end = TiDateUtil.transFormQuarterToDate(year, quarter.value, true); + if (this.value.end.getTime() < this.value.begin?.getTime()) { + this.value.begin = null; + } + } + this.valueChange.emit(this.value); + this.select.emit(this.value); + } + + public onSelect(val: any, timeOption: string): void { + const obj: any = { + timeOption, + val + }; + this.selectTimeFn.emit(obj); + switch (timeOption) { + case 'beginHour': + this.timeHandleFn(val, this.beginHourContainerRef); + break; + case 'beginMinute': + this.timeHandleFn(val, this.beginMinuteContainerRef); + break; + case 'beginSecond': + this.timeHandleFn(val, this.beginSecondContainerRef); + break; + case 'endHour': + this.timeHandleFn(val, this.endHourContainerRef); + break; + case 'endMinute': + this.timeHandleFn(val, this.endMinuteContainerRef); + break; + case 'endSecond': + this.timeHandleFn(val, this.endSecondContainerRef); + break; + default: + break; + } + } + + // 分年月日三个面板:根据不同面板设置相应的值 + private setDatePanel(): void { + // 设置下拉面板头部显示文本 + this.setYearMonthText(); + + // 设置年月文本;设置左右键状态;设置下拉面板中日期 + switch (this.picker) { + case 'day': + this.setPreNextState(this.date.year, this.date.month); + + this.setDayArr(); + this.setNextDayArr(); + + break; + case 'month': + case 'onlyYearMonth': + this.setPreNextState(this.date.year, this.date.month); + this.setMonthArr(); + this.setEndpanelMonthArr(); + + break; + case 'year': + case 'onlyYear': + this.setYearRangePreNextState(); + + this.setYearArr(); + this.setEndpanelYearArr(); + + break; + case 'quarter': + this.setPreNextState(this.date.year, this.date.month); + + this.setQuarterArr(); + this.setNextQuarterArr(); + break; + default: + break; + } + } + private setTiDateValue(val: Date): void { + if (this.focusedPosition === 'begin') { + this.value.begin = val; + if (this.value.end !== null && TiDateUtil.isBigger(this.value.begin, this.value.end)) { + this.value.end = null; + } + } else { + this.value.end = TiDateUtil.transformDateToExactDate(val, this.picker, true); + if (this.value.begin !== null && TiDateUtil.isBigger(this.value.begin, this.value.end)) { + this.value.begin = null; + } + } + } + + /** + * @description 设置展示的日期 + */ + // eslint-disable-next-line complexity + private setValue(): void { + if (this.isRangeSelectStartOrEndTime()) { + this.nextYearPanelClick = false; + this.setDatePanel(); + return; + } + + // 1.根据接口值获取合法的当前值 + let date: Date; + let today: Date; + let nextDayPanelClick: boolean; // 年月日格式场景下是否点击的是右侧日期面板(daterange/datetimerange) + let nextMonthPanelClick: boolean; // 年月格式场景下是否点击的是右侧月份面板(daterange/datetimerange) + const isBeginFocused: boolean = this.focusedPosition === 'begin' || this.focusedPosition === 'beginTime'; // 当前焦点是否在开始日期框或开始时间框上 + const isEndFocused: boolean = this.focusedPosition === 'end' || this.focusedPosition === 'endTime'; // 当前焦点是否在结束日期框或结束时间框上 + + if (this.value instanceof Object && TiDateUtil.isDate(this.value.begin) && isBeginFocused) { + date = this.value.begin; + if (!TiDateUtil.isDate(this.value.end)) { + nextDayPanelClick = this.isRange && this.date?.month < this.value.begin.getMonth() + 1; + if (this.picker === 'onlyYearMonth') { + nextMonthPanelClick = this.date?.year < this.value.begin.getFullYear(); + } + if (this.picker === 'quarter' && this.isRange && this.date.year < this.value.begin.getFullYear()) { + nextMonthPanelClick = true; + } + } + } else if (this.value instanceof Object && TiDateUtil.isDate(this.value.end) && isEndFocused) { + date = this.value.end; + nextDayPanelClick = this.date?.month < this.value.end.getMonth() + 1; + if (this.picker === 'onlyYearMonth') { + nextMonthPanelClick = this.date?.year < this.value.end.getFullYear(); + } + if (this.picker === 'quarter' && TiDateUtil.isDate(this.value.begin)) { + nextMonthPanelClick = true; + } + } else { + // 1.1根据nowDateTime接口获取现在时间 + today = this.nowDateTime && TiDateUtil.isDate(this.nowDateTime) ? this.nowDateTime : new Date(); + // 1.2根据最大值最小值对获取的现在时间进行进一步判断 + const year: number = today.getFullYear(); + const month: number = today.getMonth() + 1; + const day: number = today.getDate(); + date = !this.isBiggerThanMaxDay(year, month, day) && !this.isSmallerThanMinDay(year, month, day) ? today : this.min; + } + + if (!TiDateUtil.isDate(date)) { + return; + } + + let dateYear: number = date.getFullYear(); + + if (this.nextYearPanelClick) { + dateYear = date.getFullYear() - 10; + } + + if (nextMonthPanelClick) { + dateYear = date.getFullYear() - 1; + } + // 2.将当前时间的年月日存在对象 + this.date = { + year: dateYear, + month: nextDayPanelClick ? date.getMonth() : date.getMonth() + 1, + day: date.getDate() + }; + this.nextYearPanelClick = false; + this.setDatePanel(); + } + + // 判断日期时间段组件是否已选择了起始时间或结束时间 + private isRangeSelectStartOrEndTime(): boolean { + return ( + this.value instanceof Object && + this.isRange && + !this.isInputValue && + ((TiDateUtil.isDate(this.value.begin) && !TiDateUtil.isDate(this.value.end)) || + (!TiDateUtil.isDate(this.value.begin) && TiDateUtil.isDate(this.value.end))) + ); + } + + /** + * @description 判断参数传来的日期是否大于最大日期:大于返回true,小于返回false + * @param year 年份 + * @param month 月份 + * @param day 日 + */ + private isBiggerThanMaxDay(year: number, month: number, day: number): boolean { + // 1.最大值不是合法的时间对象 + if (!TiDateUtil.isDate(this.max)) { + return false; + } + + // 2.重新生成一个新的最大值的原因:防止传入一个带时分的最大值 + let max: Date = this.max; + max = new Date(max.getFullYear(), max.getMonth(), max.getDate()); + if (max.getTime() < new Date(year, month - 1, day).getTime()) { + return true; + } + + return false; + } + + /** + * @description 判断参数传来的日期是否小于最小日期:小于返回true,大于返回false + * @param year 年份 + * @param month 月份 + * @param day 日 + */ + private isSmallerThanMinDay(year: number, month: number, day: number): boolean { + if (!TiDateUtil.isDate(this.min)) { + return false; + } + + // 防止传入一个带时分的最小值 + let min: Date = this.min; + min = new Date(min.getFullYear(), min.getMonth(), min.getDate()); + if (min.getTime() > new Date(year, month - 1, day).getTime()) { + return true; + } + + return false; + } + // 是否为禁用日期 + private isDisabledDay(year: number, month: number, day: number): boolean { + if (!Util.isArray(this.disabledDays)) { + return false; + } + let isDisabled: boolean = false; + this.disabledDays.forEach((item: Date) => { + if (item.getTime() === new Date(year, month - 1, day).getTime()) { + isDisabled = true; + } + }); + + return isDisabled; + } + + // 开始日期或者结束日期固定不允许修改时,超出范围的需要禁用 + private isFixedWithDisabled(year: number, month: number, day: number): boolean { + if ((!this.isBeginFixed && !this.isEndFixed) || this.value === null) { + return false; + } + // 开始日期有值并且禁用 + if (this.isBeginFixed) { + if (this.value.begin.getTime() > new Date(year, month - 1, day).getTime()) { + return true; + } + } + if (this.isEndFixed) { + if (this.value.end.getTime() < new Date(year, month - 1, day).getTime()) { + return true; + } + } + + return false; + } + + /** + * @description 设置下拉面板中年月文本 + */ + private setYearMonthText(): void { + // 1.大于等于最大年,设置年为最大年,月大于最大月,设置为最大月; + if (this.date.year >= this.max.getFullYear()) { + this.date.year = this.max.getFullYear(); + if (this.date.month > this.max.getMonth() + 1) { + this.date.month = this.max.getMonth() + 1; + } + } + + // 2.小于等于最小年,设置年为最小年,月份小于最小月设置为最小月; + if (this.date.year <= this.min.getFullYear()) { + this.date.year = this.min.getFullYear(); + if (this.date.month < this.min.getMonth() + 1) { + this.date.month = this.min.getMonth() + 1; + } + } + + // 3.根据年月值,拼成下拉面板中年月文本 + const start: number = this.date.year - (this.date.year % 10); + this.pickerValue = this.pickerValue instanceof Object ? this.pickerValue : {}; + this.nextPickerValue = this.nextPickerValue instanceof Object ? this.nextPickerValue : {}; + this.pickerValue['year'] = + this.date.month === 0 ? `${this.date.year - 1}${this.local.yearText}` : `${this.date.year}${this.local.yearText}`; + this.nextPickerValue['onlyYear'] = `${this.date.year + 1}${this.local.yearText}`; + this.nextPickerValue['year'] = + this.date.month === 12 ? `${this.date.year + 1}${this.local.yearText}` : `${this.date.year}${this.local.yearText}`; + if (this.format === 'YYYY/QQ') { + this.nextPickerValue['year'] = `${this.date.year + 1}${this.local.yearText}`; + } + this.pickerValue['month'] = this.date.month === 0 ? this.local.monthArr[11] : this.local.monthArr[this.date.month - 1]; + this.nextPickerValue['month'] = this.date.month === 12 ? this.local.monthArr[0] : this.local.monthArr[this.date.month]; + this.pickerValue['yearRange'] = `${start} - ${start + 9}`; + this.pickerValue['endpanelYearRange'] = `${start + 10} - ${start + 19}`; + } + + /** + * @description 设置上月、下月按钮状态:已经是最大、最小月时,将对应的按钮置灰 + * @param year 年份 + * @param month 月份 + */ + private setPreNextState(year: number, month: number): void { + this.isMaxMonth = this.isEqualToMaxMonth(year, month); + this.isMinMonth = this.isEqualToMinMonth(year, month); + this.isMaxYear = year === this.max.getFullYear(); + this.isMinYear = year === this.min.getFullYear(); + } + + /** + * @description 判断参数传来的年月是否等于最大月份:等于返回true,不等于返回false + * @param year 年份 + * @param month 月份 + */ + private isEqualToMaxMonth(year: number, month: number): boolean { + // 1.最大值不是合法的时间对象 + if (!TiDateUtil.isDate(this.max)) { + return false; + } + + // 2.重新生成一个新的最大值的原因:防止传入一个带时分的最大值 + let max: Date = this.max; + max = new Date(max.getFullYear(), max.getMonth(), 1); + if (max.getTime() === new Date(year, month - 1, 1).getTime()) { + return true; + } + + return false; + } + + /** + * @description 判断参数传来的年月是否等于最小月份:等于返回true,不等于返回false + * @param year 年份 + * @param month 月份 + */ + private isEqualToMinMonth(year: number, month: number): boolean { + if (!TiDateUtil.isDate(this.min)) { + return false; + } + + let min: Date = this.min; + min = new Date(min.getFullYear(), min.getMonth(), 1); + if (min.getTime() === new Date(year, month - 1, 1).getTime()) { + return true; + } + + return false; + } + /** + * @description 根据面板设置 dayArr或monthArr + * + */ + private setDayMonthArr(picker: string): void { + if (picker === 'day') { + this.setDayArr(); + this.setNextDayArr(); + } else if (picker === 'month' || picker === 'onlyYearMonth') { + this.setMonthArr(); + this.setEndpanelMonthArr(); + } + } + /** + * @description 根据当前年月值,设置下拉面板中显示的日数据 + */ + private setDayArr(): void { + // 获取当前年月 + const year: number = parseInt(String(this.date.year), 10); + const month: number = parseInt(String(this.date.month), 10); + + // 1:计算这个月1号是下拉面板第一行的第几个 + // 获取到这个月1号是周几 周一到周六:1-6,周天:0; + const begin: number = new Date(year, month - 1, 1).getDay(); + + // 2:将上个月的日期存入数组dateArr + const dateArr: Array = []; // 存储下拉面板中所有日的值和状态 + const preMonth: number = month === 1 ? 12 : month - 1; // 上个月 + const preMonthDays: number = new Date(year, preMonth, 0).getDate(); // 上个月总天数 + const preMonthStart: number = preMonthDays - begin + 1; // 获取到第一行的起始值 + let preMonthDayState: string = ''; // 上个月日期的状态 + // 本月是最小月时的上月日期置灰 + preMonthDayState = this.isMinMonth ? 'disable' : 'preMonth'; + + for (let i: number = 0; i < begin; i++) { + dateArr[i] = { + value: preMonthStart + i, + state: preMonthDayState + }; + if ( + this.isSmallerThanMinDay(year, month - 1, preMonthStart + i) || + this.isDisabledDay(year, month - 1, preMonthStart + i) || + this.isFixedWithDisabled(year, month - 1, preMonthStart + i) + ) { + dateArr[i].state = 'disable'; + } + } + + // 3:将本月的日存入数组dateArr + this.setToday(year, month, begin, dateArr); + + // 4:将下个月的日存入数组dateArr + let dayNum: number = 1; + const nextMonthDayState: string = this.isMaxMonth ? 'disable' : 'nextMonth'; // 下个月日期的状态 + for (let k: number = dateArr.length; k < 42; k++) { + dateArr[k] = { + value: dayNum, + state: nextMonthDayState + }; + + if ( + this.isBiggerThanMaxDay(year, month + 1, dayNum) || + this.isDisabledDay(year, month + 1, dayNum) || + this.isFixedWithDisabled(year, month + 1, dayNum) + ) { + dateArr[k].state = 'disable'; + } + + dayNum++; + } + + // 5:将dateArr组装成下拉面板中显示日的二维数组dayArr + for (let p: number = 0; p < dateArr.length / 7; p++) { + this.dayArr[p] = []; + for (let q: number = 0; q < 7; q++) { + this.dayArr[p].push(dateArr[q + p * 7]); + } + } + } + + private setToday(year: number, month: number, begin: number, dateArr: Array): void { + let dayNum: number = 1; + const monthDays: number = new Date(year, month, 0).getDate(); // 这个月总天数 + const length: number = begin + monthDays; + let state: string = ''; + + for (let j: number = begin; j < length; j++) { + if ( + this.isBiggerThanMaxDay(year, month, dayNum) || + this.isSmallerThanMinDay(year, month, dayNum) || + this.isDisabledDay(year, month, dayNum) || + this.isFixedWithDisabled(year, month, dayNum) + ) { + state = 'disable'; // 日样式置灰 + } else if (this.isEqualToValue('day', year, month, dayNum)) { + state = 'current'; // 当前选中 + } else if (this.isBetweenInRange('day', year, month, dayNum)) { + state = 'select'; // 在选中范围 + } else { + state = 'default'; // 没有选中 + } + + dateArr[j] = { + value: dayNum, + state, + isToday: this.isEqualToToday('day', year, month, dayNum) + }; + + dayNum++; + } + } + + /** + * @description 根据当前年月值,设置下拉面板中显示的日数据 + */ + private setNextDayArr(): void { + // 获取当前年月 + const sss: number = this.date.month === 12 ? 1 : this.date.month + 1; + const yyy: number = this.date.month === 12 ? this.date.year + 1 : this.date.year; + const year: number = parseInt(String(yyy), 10); + const month: number = parseInt(String(sss), 10); + + // 1:计算这个月1号是下拉面板第一行的第几个 + // 获取到这个月1号是周几 周一到周六:1-6,周天:0; + const begin: number = new Date(year, month - 1, 1).getDay(); + + // 2:将上个月的日期存入数组dateArr + const dateArr: Array = []; // 存储下拉面板中所有日的值和状态 + const preMonth: number = month === 1 ? 12 : month - 1; // 上个月 + const preMonthDays: number = new Date(year, preMonth, 0).getDate(); // 上个月总天数 + const preMonthStart: number = preMonthDays - begin + 1; // 获取到第一行的起始值 + + for (let i: number = 0; i < begin; i++) { + dateArr[i] = { + value: preMonthStart + i, + state: 'preMonth' + }; + + if ( + this.isBiggerThanMaxDay(year, month - 1, preMonthStart + i) || + this.isSmallerThanMinDay(year, month - 1, preMonthStart + i) || + this.isDisabledDay(year, month - 1, preMonthStart + i) || + this.isFixedWithDisabled(year, month - 1, preMonthStart + i) + ) { + dateArr[i].state = 'disable'; + } + } + + // 3:将本月的日存入数组dateArr + this.setToday(year, month, begin, dateArr); + + // 4:将下个月的日存入数组dateArr + let dayNum: number = 1; + const nextMonthDayState: string = this.isMaxMonth ? 'disable' : 'nextMonth'; // 下个月日期的状态 + for (let k: number = dateArr.length; k < 42; k++) { + dateArr[k] = { + value: dayNum, + state: nextMonthDayState + }; + + if ( + this.isBiggerThanMaxDay(year, month + 1, dayNum) || + this.isDisabledDay(year, month + 1, dayNum) || + this.isFixedWithDisabled(year, month + 1, dayNum) + ) { + dateArr[k].state = 'disable'; + } + + dayNum++; + } + + // 5:将dateArr组装成下拉面板中显示日的二维数组dayArr + for (let p: number = 0; p < dateArr.length / 7; p++) { + this.nextdayArr[p] = []; + for (let q: number = 0; q < 7; q++) { + this.nextdayArr[p].push(dateArr[q + p * 7]); + } + } + } + + /** + * @description 判断参数传来的日期是否begin值小于end的值,end值大于begion值 + * @param year 年份 + * @param month 月份 + * @param day 日 + */ + private isOutInRange(year: number, month: number, day: number): boolean { + const value: TiDateValue = this.value; + const curValue: number = new Date(year, month - 1, day).getTime(); + + if (!(value instanceof Object)) { + return false; + } + let end: Date = value.end; + let endValue: number; + if (TiDateUtil.isDate(value.end)) { + // 只取年月日:防止传入带有时分秒的值 + end = new Date(end.getFullYear(), end.getMonth(), end.getDate()); + endValue = end.getTime(); + + return curValue > endValue; + } + + return false; + } + + /** + * @description 判断参数传来的日期是否等于当前选中的日期:等于返回true,不等于返回false + * @param year 年份 + * @param month 月份 + * @param day 日 + */ + private isEqualToValue(type?: string, year?: number, month?: number, day?: number): boolean { + const beginVal: Date = this.value.begin; + const endVal: Date = this.value.end; + if (beginVal === null && endVal === null) { + return false; + } else { + let value1: Date; + let value2: Date; + let beginV: boolean; + let endV: boolean; + if (beginVal !== null) { + value1 = new Date(beginVal.getFullYear(), beginVal.getMonth(), beginVal.getDate()); + beginV = this.isEqualValue(type, value1, year, month, day); + } + if (endVal !== null) { + value2 = new Date(endVal.getFullYear(), endVal.getMonth(), endVal.getDate()); + endV = this.isEqualValue(type, value2, year, month, day); + } + + return beginV || endV; + } + } + + /** + * @description 判断参数传来的日期是否在当前选中日期的范围内,并且不等于当前选中的值:符合返回true,不符合返回false + * @param year 年份 + * @param month 月份 + * @param day 日 + */ + private isBetweenInRange(type?: string, year?: number, month?: number, day?: number): boolean { + const value: TiDateValue = this.value; + // value不是对象,begin或者end不是合法日期 + if (!(value instanceof Object) || !TiDateUtil.isDate(value.begin) || !TiDateUtil.isDate(value.end)) { + return false; + } + + let begin: Date = value.begin; + let end: Date = value.end; + let curValue: number; + + if (type === 'day') { + begin = new Date(begin.getFullYear(), begin.getMonth(), begin.getDate()); + end = new Date(end.getFullYear(), end.getMonth(), end.getDate()); + curValue = new Date(year, month - 1, day).getTime(); + } + + if (type === 'year') { + begin = new Date(begin.getFullYear()); + end = new Date(end.getFullYear()); + curValue = new Date(year).getTime(); + } + + if (type === 'month') { + begin = new Date(begin.getFullYear(), begin.getMonth()); + end = new Date(end.getFullYear(), end.getMonth()); + curValue = new Date(year, month - 1).getTime(); + } + + const beginValue: number = begin.getTime(); + const endValue: number = end.getTime(); + + if (beginValue < curValue && curValue <= endValue) { + return true; + } + + return false; + } + + /** + * @description 判断参数传来的日期是否等于当前日期时间 + * @param year 年份 + * @param month 月份 + * @param day 日 + */ + private isEqualToToday(type?: string, year?: number, month?: number, day?: number): boolean { + const today: Date = this.nowDateTime && TiDateUtil.isDate(this.nowDateTime) ? this.nowDateTime : new Date(); + + return this.isEqualValue(type, today, year, month, day); + } + + // 判断日期是当前日期或当前选中日期 + private isEqualValue(type?: string, value?: any, year?: number, month?: number, day?: number): boolean { + let _value: any; + if (type === 'year') { + // 年 + _value = value.getFullYear(); + if (_value === year) { + return true; + } + + return false; + } + + if (type === 'month') { + // 年、月 + _value = new Date(value.getFullYear(), value.getMonth()); + if (_value.getTime() === new Date(year, month - 1).getTime()) { + return true; + } + + return false; + } + // 年、月、日 + _value = new Date(value.getFullYear(), value.getMonth(), value.getDate()); + if (_value.getTime() === new Date(year, month - 1, day).getTime()) { + return true; + } + + return false; + } + + private setMonthValue(year: number): Array { + const innerMonth: Array = []; + let state: string = ''; + // 设置每个月份对应的值和状态 + for (let month: number = 1; month <= 12; month++) { + if (this.isSmallerThanMinMonth(year, month) || this.isBiggerThanMaxMonth(year, month)) { + state = 'disable'; // 月样式置灰 + } else if (this.isEqualToValue('month', year, month)) { + state = 'current'; // 当前选中 + } else if (this.isBetweenInRange('month', year, month)) { + state = 'select'; // 在选中范围 + } else { + state = 'default'; // 没有选中 + } + + innerMonth[month - 1] = { + value: this.local.monthArr[month - 1], + state, + isMonth: this.isEqualToToday('month', year, month) + }; + } + + return innerMonth; + } + + /** + * @description 根据当前年份和最大最小值,设置下拉面板中显示的月份状态 + */ + private setMonthArr(): void { + const year: number = this.date.year; + const innerMonth: Array = this.setMonthValue(year); + // 将月份数据格式化为二维数组 + this.monthArr = []; + for (let p: number = 0; p < 3; p++) { + this.monthArr[p] = []; + for (let q: number = 0; q < 4; q++) { + this.monthArr[p].push(innerMonth[q + p * 4]); + } + } + } + private setQuarterArr(): void { + const year: number = this.date.year; + this.setQuarterState(year, this.quartersArr); + } + + private setNextQuarterArr(): void { + const year: number = this.date.year + 1; + this.setQuarterState(year, this.nextQuartersArr); + } + /** + * 设置季节状态 + */ + private setQuarterState(year: number, quartersArr: any): void { + const arr: Array = ['Q1', 'Q2', 'Q3', 'Q4']; + const value: string = TiDateUtil.transformDateToQuarter(this.value.begin); + const value1: string = TiDateUtil.transformDateToQuarter(this.value.end); + const min: string = TiDateUtil.transformDateToQuarter(this.min); + const max: string = TiDateUtil.transformDateToQuarter(this.max); + let state: string = ''; + arr.forEach((val: string, index: number) => { + const quarter: string = `${year}/${val}`; + if (quarter < min || quarter > max) { + state = 'disable'; + } else if (quarter === value || quarter === value1) { + state = 'current'; + } else if (quarter > value && quarter < value1) { + state = 'select'; + } else { + state = 'default'; + } + quartersArr[index] = { + value: arr[index], + state + }; + }); + } + + /** + * @description 根据当前年份和最大最小值,设置下拉面板中显示的月份状态 + */ + private setEndpanelMonthArr(): void { + const year: number = this.date.year + 1; + const innerMonth: Array = this.setMonthValue(year); + // 将月份数据格式化为二维数组 + this.endpanelMonthArr = []; + for (let p: number = 0; p < 3; p++) { + this.endpanelMonthArr[p] = []; + for (let q: number = 0; q < 4; q++) { + this.endpanelMonthArr[p].push(innerMonth[q + p * 4]); + } + } + } + + /** + * @description 判断参数传来的年月是否小于最小月份:小于返回true,大于返回false + * @param year 年份 + * @param month 月份 + */ + private isSmallerThanMinMonth(year: number, month: number): boolean { + if (!TiDateUtil.isDate(this.min)) { + return false; + } + + let min: Date = this.min; + min = new Date(min.getFullYear(), min.getMonth(), 1); + if (min.getTime() > new Date(year, month - 1, 1).getTime()) { + return true; + } + + return false; + } + + /** + * @description 判断参数传来的年月是否大于最大月份:大于返回true,小于返回false + * @param year 年份 + * @param month 月份 + */ + private isBiggerThanMaxMonth(year: number, month: number): boolean { + if (!TiDateUtil.isDate(this.max)) { + return false; + } + + let max: Date = this.max; + max = new Date(max.getFullYear(), max.getMonth(), 1); + if (max.getTime() < new Date(year, month - 1, 1).getTime()) { + return true; + } + + return false; + } + + /** + * @description 设置按钮状态: + */ + private setYearRangePreNextState(): void { + const minValueRange: TiYearRange = this.getYearRange(this.min.getFullYear()); + const maxValueRange: TiYearRange = this.getYearRange(this.max.getFullYear()); + this.isMaxYearRange = this.date.year >= maxValueRange.min && this.date.year <= maxValueRange.max ? true : false; + this.isMinYearRange = this.date.year >= minValueRange.min && this.date.year <= minValueRange.max ? true : false; + } + + // 获取时间面板:最大值和最小值年面板范围 + private getYearRange(year: number): TiYearRange { + const minDateRange: number = year - (year % 10); + const maxDateRange: number = minDateRange + 9; + + return { + min: minDateRange, + max: maxDateRange + }; + } + /** + * 当前年份是否在最大最小值范围内 + * @param year 当前年 + * @returns + */ + private isNotBetweenMaxAndmin(year: number): boolean { + return year < this.min.getFullYear() || year > this.max.getFullYear() ? true : false; + } + + /** + * 设置当前年份面板值 + * @param innerYear 当前年月数据集合 + * @returns + */ + private setYearMonth(innerYear: Array): Array> { + const yearArr: Array> = []; + + for (let p: number = 0; p < 3; p++) { + yearArr[p] = []; + for (let q: number = 0; q < 4; q++) { + yearArr[p].push(innerYear[q + p * 4]); + } + } + + return yearArr; + } + private setYearValue(year: number): Array { + const innerYear: Array = []; // 存储年值 + // 2.数组的第一个为起始年 -1; + const start: number = year - 1; + const end: number = year + 9; + let index: number = 0; + let state: string = ''; + const preYearRange: string = this.isNotBetweenMaxAndmin(start) ? 'disable' : 'preYear'; + innerYear.push({ + state: preYearRange, + value: start + }); + // 3.起始年 - 最终年(起始年+9)(for遍历) + for (let i: number = year; i <= end; i++) { + index++; + if (this.isNotBetweenMaxAndmin(i)) { + state = 'disable'; + } else if (this.isEqualToValue('year', i)) { + state = 'current'; // 当前选中 + } else if (this.isBetweenInRange('year', i)) { + state = 'select'; // 在选中范围 + } else { + state = 'default'; + } + + innerYear[index] = { + value: i, + state, + isYear: this.isEqualToToday('year', i) + }; + } + + // 判断是不是大于最大年:大于最大年状态为灰化 + const nextYearRange: string = this.isNotBetweenMaxAndmin(end + 1) ? 'disable' : 'nextYear'; + innerYear.push({ + state: nextYearRange, + value: end + 1 + }); + + return innerYear; + } + + /** + * @description 根据当前年份和最大最小值,设置下拉面板中显示的年份状态 + */ + private setYearArr(): void { + // 1.获取当前年;算出起始年, + const year: number = this.date.year - (this.date.year % 10); + const innerYear: Array = this.setYearValue(year); + this.yearArr = this.setYearMonth(innerYear); + } + + /** + * @description 根据当前年份和最大最小值,设置下拉面板中显示的年份状态 + */ + private setEndpanelYearArr(): void { + // 1.获取当前年;算出起始年, + const year: number = this.date.year - (this.date.year % 10) + 10; + const innerYear: Array = this.setYearValue(year); + this.endpanelYearArr = this.setYearMonth(innerYear); + } + + // 设置左年后退按钮灰化 + isPickerYearMinValue(): boolean { + return this.picker === 'year' || this.picker === 'onlyYear' ? this.isMinYearRange : this.isMinYear; + } + // 设置右年前进按钮灰化 + isPickerYearMaxValue(): boolean { + return this.picker === 'year' || this.picker === 'onlyYear' ? this.isMaxYearRange : this.isMaxYear; + } + + public trackByFn(index: number, item: any): number { + return index; + } + + private setOptions(num: number, pos?: string): Array { + const options: Array = []; + for (let i: number = 0; i < num; i++) { + options[i] = { + label: i < 10 ? '0' + i : String(i), + disabled: pos === 'end' ? this.endTimeDisabled : this.beginTimeDisabled + }; + } + + return options; + } + + /** + * @ignore + * 时间选择框部分鼠标移出时去除hover样式 + */ + public onMouseleave(listCom: TiListComponent): void { + listCom.hoverOption = null; + } + /** + * @ignore + * 时间面板:根据格式显隐分秒面板 + */ + private setTimePanel(): void { + // 仅时 + if (this.timeFormat?.indexOf('m') === -1) { + this.onlyHour = true; + } else if (this.timeFormat?.indexOf('s') === -1) { + // 仅时分 + this.onlyHourMinute = true; + } else { + this.onlyHour = false; + this.onlyHourMinute = false; + } + } +} diff --git a/src/datepanel/lib/src/TiDatePanelModule.ts b/src/datepanel/lib/src/TiDatePanelModule.ts new file mode 100644 index 0000000..e328a4b --- /dev/null +++ b/src/datepanel/lib/src/TiDatePanelModule.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { TiDatePanelComponent } from './TiDatePanelComponent'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiListModule } from '@opentiny/ng-list'; +import { TiLocaleModule } from '@opentiny/ng-locale'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule, FormsModule, TiIconModule, TiListModule, TiLocaleModule], + exports: [TiDatePanelComponent], + declarations: [TiDatePanelComponent] +}) +export class TiDatePanelModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiDatePanelComponent, TiDatePanel, TiPickerHeadText, TiDateValueAndState, TiYearRange } from './TiDatePanelComponent'; diff --git a/src/datepanel/lib/src/datepanel.html b/src/datepanel/lib/src/datepanel.html new file mode 100644 index 0000000..cf244f5 --- /dev/null +++ b/src/datepanel/lib/src/datepanel.html @@ -0,0 +1,380 @@ + +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ {{pickerValue.month}} + + {{pickerValue.year}} + + {{pickerValue.month}} + {{pickerValue.yearRange}} +
+
+ {{nextPickerValue.month}} + + {{nextPickerValue.year}} + + {{nextPickerValue.onlyYear}} + + {{nextPickerValue.month}} + {{pickerValue.endpanelYearRange}} +
+
+
+
+ + +
+
+ + + + {{week}} + + + + + + +
+ {{day.value}} +
+ + + +
+
+ + + {{week}} + + + + + + +
+ {{day.value}} +
+ + + +
+
+ + +
+
+
+
+ {{month.value}} +
+
+
+
+ + +
+
+
+
+
+ {{month.value}} +
+
+
+
+
+
+
+
+ {{month.value}} +
+
+
+
+
+ + +
+
+
+
+ {{year.value}} +
+
+
+
+ +
+
+
+
+
+ {{year.value}} +
+
+
+
+
+
+
+
+ {{year.value}} +
+
+
+
+
+ +
+
+
+ {{ 'tiDatepanel.hour' | tiTranslate }} + +
+
+ {{ 'tiDatepanel.minute' | tiTranslate }} + +
+
+ {{ 'tiDatepanel.second' | tiTranslate }} + +
+
+
+
+ {{ 'tiDatepanel.hour' | tiTranslate }} + +
+
+ {{ 'tiDatepanel.minute' | tiTranslate }} + +
+
+ {{ 'tiDatepanel.second' | tiTranslate }} + +
+
+
+ + +
+
+
+
+ {{quarter.value}} +
+
+
+
+
+
+ {{quarter.value}} +
+
+
+
+ + diff --git a/src/datepanel/lib/src/datepanel.less b/src/datepanel/lib/src/datepanel.less new file mode 100644 index 0000000..42e4d76 --- /dev/null +++ b/src/datepanel/lib/src/datepanel.less @@ -0,0 +1,430 @@ +@import "../../../themes/basic/base-all.less"; + +:host { + --ti-date-panel-picker-header-height: 18px; + --ti-date-panel-picker-month-height: var(--ti-common-size-8x); + --ti-date-panel-picker-icon-width: var(--ti-common-size-4x); + --ti-date-panel-month-line-width: var(--ti-common-size-6x); + --ti-date-panel-picker-week-height: 18px; + --ti-date-panel-year-line-width: 30px; + --ti-date-panel-day-line-width: var(--ti-common-size-3x); + --ti-date-panel-day-container-width: var(--ti-common-size-9x); + --ti-date-panel-day-container-height: var(--ti-common-size-6x); + --ti-date-panel-picker-month-width: calc((var(--ti-date-panel-day-container-width) * 7) / 4); + --ti-date-panel-picker-line-bottom: var(--ti-common-space-base); //年月面板当前日期的下划线bottom + --ti-date-panel-day-picker-line-bottom: 2px; // 日期面板当前日期的下划线bottom +} +/** + * 当前日期状态下划线样式 + */ + .date-underline(@date-underline-bottom; @date-underline-left; @date-underline-width) { + position: relative; + &:after{ + content: ""; + position: absolute; + bottom: @date-underline-bottom; + left: @date-underline-left; + width: @date-underline-width; + height: 1px; + background: var(--ti-common-color-line-active); + } +} + +.ti3-date-picker-header { + height: var(--ti-date-panel-picker-header-height); + line-height: var(--ti-date-panel-picker-header-height) ; + margin: var(--ti-common-space-3x) var(--ti-common-space-0); + position: relative; + font-size: var(--ti-common-font-size-base); + .box-sizing(content-box); +} + +.ti3-date-picker-icons-prev, +.ti3-date-picker-icons-next { + position: absolute; + width: calc(var(--ti-date-panel-picker-icon-width)*2 + var(--ti-common-space-10)); + height: var(--ti-date-panel-picker-header-height); + line-height: var(--ti-date-panel-picker-header-height); + cursor: pointer; + display: block; + top: 0px; + padding: 0; + font-size: var(--ti-common-font-size-base); + color: var(--ti-common-color-icon-normal); + text-align: right; + .box-sizing(border-box); + .ti3-icon { + width: var(--ti-date-panel-picker-icon-width); + display: inline-block; + text-align: center; + &:hover, + &:active { + color: var(--ti-common-color-icon-hover); + } + } +} + +.ti3-date-picker-icons-prev { + left: 0px; + text-align: left; + padding-left: var(--ti-common-space-10); +} + +.ti3-date-picker-icons-next { + right: 0px; + text-align: right; + padding-right: var(--ti-common-space-10); +} + + + +/* 上下键灰化的样式 */ +.ti3-date-state-disabled { + cursor: not-allowed !important; + color: var(--ti-common-color-icon-disabled) !important; +} + + /* 下拉面板头部中间年月文本样式 */ +.ti3-header-year-month-picker { + margin: var(--ti-common-space-0) calc((var(--ti-date-panel-day-container-width) * 7 - (166px - 2px)) / 2); +} + +.ti3-date-picker-year-month { + font-weight: var(--ti-common-font-weight-7); + color: var(--ti-common-color-text-primary); + background-color: var(--ti-common-color-bg-white-normal); + cursor: pointer; + .user-select(); + + & span:hover{ + color: var(--ti-common-color-text-highlight); + } + } + + .ti3-date-picker-year-month-text { + text-align: center; + } + + .ti3-date-picker-container { + .clearfix(); + background-color: var(--ti-common-color-bg-white-normal); + font-size: var(--ti-common-font-size-base); + th { + font-weight: var(--ti-common-font-weight-4); + } + &.ti3-date-picker-year, + &.ti3-date-picker-month { + margin-top: 42px; + } + } + + + + /** + * 下拉面板中间周、日表格样式 + */ +.ti3-date-week { + height: var(--ti-date-panel-picker-week-height); + background-color: var(--ti-common-color-bg-white-normal); + color: var(--ti-common-color-text-disabled); + text-align: center; + border: 0; + font-weight: var(--ti-common-font-weight-4); + .user-select(); +} + +.ti3-date-day-tr { + height: var(--ti-date-panel-day-container-height); +} + +.ti3-date-today-current-td { + position: relative; +} + +.ti3-date-current-day { + background-color: var(--ti-common-color-bg-emphasize); + color: var(--ti-common-color-text-white); + text-align: center; + line-height: var(--ti-date-panel-day-container-height); + cursor: pointer; + display: block; + text-decoration: none; + padding: var(--ti-common-space-0); + margin-top: var(--ti-common-space-10); + height: var(--ti-date-panel-day-container-height); + min-width: var(--ti-date-panel-day-container-width); + .box-sizing(border-box); + .user-select(); +} + +.ti3-dateRange-select-day { + background-color: var(--ti-common-color-bg-light-normal); + color: var(--ti-common-color-text-primary); + text-align: center; + line-height: var(--ti-date-panel-day-container-height); + cursor: pointer; + display: block; + text-decoration: none; + padding: var(--ti-common-space-0); + margin-top: var(--ti-common-space-10); + height: var(--ti-date-panel-day-container-height); + min-width: var(--ti-date-panel-day-container-width); + .box-sizing(border-box); + .user-select(); + + &:hover{ + background-color: var(--ti-common-color-bg-light-emphasize); + } +} + +.ti3-date-default-day { + background-color: var(--ti-common-color-bg-white-normal); + color: var(--ti-common-color-text-primary); + text-align: center; + line-height: var(--ti-date-panel-day-container-height); + font-size: var(--ti-common-font-size-base); + cursor: pointer; + display: block; + text-decoration: none; + padding: var(--ti-common-space-0); + height: var(--ti-date-panel-day-container-height); + margin-top: var(--ti-common-space-10); + min-width: var(--ti-date-panel-day-container-width); + .box-sizing(border-box); + .user-select(); + + &:hover{ + background-color: var(--ti-common-color-bg-white-emphasize); + color: var(--ti-common-color-text-highlight); + } +} + +.ti3-date-today { + &.ti3-date-current-day { + border: 0; + background-color: var(--ti-common-color-bg-emphasize); + } + .date-underline(var(--ti-date-panel-day-picker-line-bottom); calc((var(--ti-date-panel-day-container-width) - var(--ti-date-panel-day-line-width))/2); var(--ti-date-panel-day-line-width)) +} + +.ti3-date-disable-day { + background-image: none; + color: var(--ti-common-color-line-normal); + text-align: center; + line-height: var(--ti-date-panel-day-container-height); + font-size: var(--ti-common-font-size-base); + display: block; + text-decoration: none; + background-color: var(--ti-common-color-bg-disabled); + padding: var(--ti-common-space-0); + margin-top: var(--ti-common-space-10); + height: var(--ti-date-panel-day-container-height); + min-width: var(--ti-date-panel-day-container-width); + cursor: not-allowed; + .box-sizing(border-box); + .user-select(); +} + +/* 相邻月份的天变为浅灰色 */ +.ti3-date-beside-day { + color: var(--ti-common-color-text-disabled); + text-align: center; + line-height: var(--ti-date-panel-day-container-height); + font-size: var(--ti-common-font-size-base); + cursor: pointer; + display: block; + text-decoration: none; + padding: var(--ti-common-space-0); + margin-top: var(--ti-common-space-10); + height: var(--ti-date-panel-day-container-height); + min-width: var(--ti-date-panel-day-container-width); + .box-sizing(border-box); + .user-select(); + &:hover { + background-color: var(--ti-common-color-bg-disabled); + } +} + + +/* 设置月、年面板的样式: */ +.ti3-date-month, .ti3-date-year { + text-align: center; + line-height: var(--ti-date-panel-picker-month-height); + cursor: pointer; + display: block; + text-decoration: none; + height: var(--ti-date-panel-picker-month-height); + width: var(--ti-date-panel-picker-month-width); + .box-sizing(border-box); + .user-select(); +} + +.ti3-date-default-month, .ti3-date-default-year { + background-color: var(--ti-common-color-bg-white-normal); + color: var(--ti-common-color-text-secondary); + + &:hover{ + color: var(--ti-common-color-text-highlight); + background-color: var(--ti-common-color-bg-white-emphasize); + } +} + +.ti3-date-current-month, .ti3-date-current-year { + background-color: var(--ti-common-color-bg-emphasize); + color: var(--ti-common-color-text-white); +} + +.ti3-date-disable-month, .ti3-date-disable-year{ + background-color: var(--ti-common-color-bg-disabled); + color: var(--ti-common-color-line-normal); + cursor: not-allowed; +} + +.ti3-date-select-month, .ti3-date-select-year { + background-color: var(--ti-common-color-bg-light-normal); + color: var(--ti-common-color-text-primary); + &:hover { + background-color: var(--ti-common-color-bg-light-emphasize); + } +} + +.ti3-date-preNext-year{ + background-color: var(--ti-common-color-bg-white-normal); + color:var(--ti-common-color-text-disabled); +} + +::ng-deep .ti3-datepanel-time-select-container { + display: flex; + height: 265px; + width: 250px; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + border-bottom: none; + margin-top: var(--ti-common-space-10); + .box-sizing(content-box); + & ul { + width: 100%; + height: 231px; + overflow-y: auto; + text-align: center; + & li { + text-align: center !important; + margin-top: 0 !important; + } + } +} + +.ti3-time-minuteselect-container, +.ti3-time-hourselect-container, +.ti3-time-secondselect-container { + flex: 1; + & ti-list { + width: 100%; + } +} + +.ti3-time-select-title { + width: 100%; + display: inline-block; + margin-left: calc(-1 * var(--ti-common-space-6)); + height: 30px; + line-height: 30px; + text-align: center; + font-size: var(--ti-common-font-size-base); + color: var(--ti-common-color-text-darkbg); +} +.ti3-endtime-select-container, +.ti3-time-select-panel { + display: flex; +} +.ti3-endtime-select-container { + margin-left: var(--ti-common-space-5x); +} +.ti3-endtime-select-none { + display: none; +} + +.ti3-date-picker-text { + padding-left:58px; + display: inline; +} +.ti3-date-picker-endpanel-text { + padding-left: 204px; + display: inline; +} + +.ti3-time-minuteselect-container, +.ti3-time-hourselect-container { + border-right: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); +} + +.ti3-date-year-month-picker-container { + display: flex; + margin-bottom: var(--ti-common-space-8x); + justify-content: space-around; +} + +.ti3-time-select-container-border { + border-bottom: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); +} + +.ti-date-nowMonth { + .date-underline(var(--ti-date-panel-picker-line-bottom); calc((var(--ti-date-panel-picker-month-width) - var(--ti-date-panel-month-line-width))/2); var(--ti-date-panel-month-line-width)); +} + +.ti-date-nowYear { + .date-underline(var(--ti-date-panel-picker-line-bottom); calc((var(--ti-date-panel-picker-month-width) - var(--ti-date-panel-year-line-width))/2); var(--ti-date-panel-year-line-width)); +} + +.ti3-date-picker-only-year-container, +.ti3-date-picker-year-month-container { + display: flex; +} + +.ti3-date-picker-right-zone { + padding-left: var(--ti-common-space-5x); +} + +.ti3-date-quarter{ + width: calc((7 * var(--ti-common-size-9x) / 2)); + height: var(--ti-common-size-16x); + margin-bottom: var(--ti-common-space-8x); + line-height: var(--ti-common-size-16x); + text-align: center; + display: inline-block; + color: var(--ti-common-color-text-primary); + cursor: pointer; + &:hover{ + color: var(--ti-common-color-text-highlight); + background-color: var(--ti-common-color-bg-white-emphasize); + } + +} +.ti3-date-quarter{ + &.ti3-date-quarter-disable { + color: var(--ti-common-color-text-disabled); + background-color: var(--ti-common-color-bg-disabled); + cursor: not-allowed; + &:hover { + color: var(--ti-common-color-text-disabled); + } + } + &.ti3-date-quarter-current { + background-color: var(--ti-common-color-bg-emphasize); + color: var(--ti-common-color-text-white); + } + &.ti3-date-quarter-select { + color: var(--ti-common-color-text-primary); + background-color: var(--ti-common-color-bg-light-normal); + &:hover { + background-color: var(--ti-common-color-bg-light-emphasize); + } + } +} + +.ti3-date-quarter-top { + margin-top: 30px; +} + +.ti3-date-picker-quarter-container { + width: calc(7 * var(--ti-common-size-9x)); +} diff --git a/src/datepanel/lib/src/i18n/TiDatepanelWords.ts b/src/datepanel/lib/src/i18n/TiDatepanelWords.ts new file mode 100644 index 0000000..831ce17 --- /dev/null +++ b/src/datepanel/lib/src/i18n/TiDatepanelWords.ts @@ -0,0 +1,11 @@ +export interface TiDatepanelWords { + tiDatepanel: { + weekNamesAbb: Array; + weeknamesTitle: Array; + monthNamesAbb: Array; + yearSuffixLabel: string; + hour: string; + minute: string; + second: string; + }; +} diff --git a/src/datepanel/lib/src/i18n/en_US.ts b/src/datepanel/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..11f0f66 --- /dev/null +++ b/src/datepanel/lib/src/i18n/en_US.ts @@ -0,0 +1,13 @@ +import { TiDatepanelWords } from './TiDatepanelWords'; + +export const en_US: TiDatepanelWords = { + tiDatepanel: { + weekNamesAbb: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + weeknamesTitle: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + monthNamesAbb: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + yearSuffixLabel: '', + hour: 'Hour', + minute: 'Minute', + second: 'Second' + } +}; diff --git a/src/datepanel/lib/src/i18n/es_US.ts b/src/datepanel/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..bae1c4f --- /dev/null +++ b/src/datepanel/lib/src/i18n/es_US.ts @@ -0,0 +1,13 @@ +import { TiDatepanelWords } from './TiDatepanelWords'; + +export const es_US: TiDatepanelWords = { + tiDatepanel: { + weekNamesAbb: ['Do.', 'Lu.', 'Ma.', 'Mi.', 'Ju.', 'Vi.', 'Sá.'], + weeknamesTitle: ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado'], + monthNamesAbb: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dec'], + yearSuffixLabel: '', + hour: 'Hora', + minute: 'Minuto', + second: 'Segundo' + } +}; diff --git a/src/datepanel/lib/src/i18n/fr_FR.ts b/src/datepanel/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..e14368d --- /dev/null +++ b/src/datepanel/lib/src/i18n/fr_FR.ts @@ -0,0 +1,13 @@ +import { TiDatepanelWords } from './TiDatepanelWords'; + +export const fr_FR: TiDatepanelWords = { + tiDatepanel: { + weekNamesAbb: ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'], + weeknamesTitle: ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'], + monthNamesAbb: ['Jan', 'Feb', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + yearSuffixLabel: '', + hour: 'Heure', + minute: 'Minute', + second: 'Second' + } +}; diff --git a/src/datepanel/lib/src/i18n/index.ts b/src/datepanel/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/datepanel/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/datepanel/lib/src/i18n/pt_BR.ts b/src/datepanel/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..a297511 --- /dev/null +++ b/src/datepanel/lib/src/i18n/pt_BR.ts @@ -0,0 +1,13 @@ +import { TiDatepanelWords } from './TiDatepanelWords'; + +export const pt_BR: TiDatepanelWords = { + tiDatepanel: { + weekNamesAbb: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'], + weeknamesTitle: ['Domingo', 'Segunda-feira', 'Terça-feira', 'Quarta-feira', 'Quinta-feira', 'Sexta-feira', 'Sábado'], + monthNamesAbb: ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'], + yearSuffixLabel: '', + hour: 'Hora', + minute: 'Minuto', + second: 'Segundo' + } +}; diff --git a/src/datepanel/lib/src/i18n/zh_CN.ts b/src/datepanel/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..3c6780c --- /dev/null +++ b/src/datepanel/lib/src/i18n/zh_CN.ts @@ -0,0 +1,13 @@ +import { TiDatepanelWords } from './TiDatepanelWords'; + +export const zh_CN: TiDatepanelWords = { + tiDatepanel: { + weekNamesAbb: ['日', '一', '二', '三', '四', '五', '六'], + weeknamesTitle: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'], + monthNamesAbb: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], + yearSuffixLabel: '年', + hour: '时', + minute: '分', + second: '秒' + } +}; diff --git a/src/daterange/demo/karma.conf.js b/src/daterange/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/daterange/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/daterange/demo/project.json b/src/daterange/demo/project.json new file mode 100644 index 0000000..ce49558 --- /dev/null +++ b/src/daterange/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/daterange/demo", + "sourceRoot": "src/daterange/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/daterange", + "index": "src/daterange/demo/src/index.html", + "main": "src/daterange/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/daterange/demo/tsconfig.app.json", + "assets": ["src/daterange/demo/src/favicon.ico", "src/daterange/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "daterange-demo:build:production" + }, + "development": { + "browserTarget": "daterange-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js daterange" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/daterange/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/daterange/demo/tsconfig.spec.json", + "karmaConfig": "src/daterange/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/daterange/demo/src/app/AppComponent.ts b/src/daterange/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/daterange/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/daterange/demo/src/app/AppModule.ts b/src/daterange/demo/src/app/AppModule.ts new file mode 100644 index 0000000..7c234bd --- /dev/null +++ b/src/daterange/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { DateRangeTestModule } from './daterange/DateRangeTestModule'; + +@NgModule({ + imports: [ + DateRangeTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/daterange/demo/src/app/IndexComponent.ts b/src/daterange/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..01494ba --- /dev/null +++ b/src/daterange/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { DateRangeTestModule } from './daterange/DateRangeTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = DateRangeTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/daterange/demo/src/app/app.html b/src/daterange/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/daterange/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/daterange/demo/src/app/daterange/DateRangeTestModule.ts b/src/daterange/demo/src/app/daterange/DateRangeTestModule.ts new file mode 100644 index 0000000..881f4cd --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DateRangeTestModule.ts @@ -0,0 +1,131 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiDateRangeModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { DaterangePanelalignComponent } from './DaterangePanelalignComponent'; +import { DaterangeValueComponent } from './DaterangeValueComponent'; +import { DaterangeFormatComponent } from './DaterangeFormatComponent'; +import { DaterangeMaxComponent } from './DaterangeMaxComponent'; +import { DaterangeMinComponent } from './DaterangeMinComponent'; +import { DaterangeDisabledComponent } from './DaterangeDisabledComponent'; +import { DaterangeValidationComponent } from './DaterangeValidationComponent'; +import { DaterangeNowdatetimeComponent } from './DaterangeNowdatetimeComponent'; +import { DaterangeCustomizeComponent } from './DaterangeCustomizeComponent'; +import { DaterangeDisableddaysComponent } from './DaterangeDisableddaysComponent'; +import { DaterangeFixedvalueComponent } from './DaterangeFixedvalueComponent'; +import { DaterangeEventComponent } from './DaterangeEventComponent'; +import { DaterangeIsallowbeginequalendComponent } from './DaterangeIsallowbeginequalendComponent'; +import { DaterangeValueTestComponent } from './DaterangeValueTestComponent'; +import { DaterangeFormatTestComponent } from './DaterangeFormatTestComponent'; +import { DaterangeMaxminComponent } from './DaterangeMaxminComponent'; +import { DaterangeFixedvalueTestComponent } from './DaterangeFixedvalueTestComponent'; +import { DaterangeMaxminTestComponent } from './DaterangeMaxminTestComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiValidationModule, + TiDateRangeModule, + DemoLogModule, + RouterModule.forChild(DateRangeTestModule.ROUTES) + ], + declarations: [ + DaterangePanelalignComponent, + DaterangeNowdatetimeComponent, + DaterangeValueComponent, + DaterangeFormatComponent, + DaterangeMaxComponent, + DaterangeMinComponent, + DaterangeEventComponent, + DaterangeDisabledComponent, + DaterangeCustomizeComponent, + DaterangeDisableddaysComponent, + DaterangeValidationComponent, + DaterangeFixedvalueComponent, + DaterangeIsallowbeginequalendComponent, + DaterangeValueTestComponent, + DaterangeFormatTestComponent, + DaterangeMaxminComponent, + DaterangeFixedvalueTestComponent, + DaterangeMaxminTestComponent + ] +}) +export class DateRangeTestModule { + static readonly LINKS: Array = [{ href: 'components/TiDateRangeComponent.html', label: 'DateRange' }]; + static readonly ROUTES: Routes = [ + { + path: 'daterange/daterange-value', + component: DaterangeValueComponent + }, + { + path: 'daterange/daterange-format', + component: DaterangeFormatComponent + }, + { + path: 'daterange/daterange-max', + component: DaterangeMaxComponent + }, + { + path: 'daterange/daterange-min', + component: DaterangeMinComponent + }, + { + path: 'daterange/daterange-panelalign', + component: DaterangePanelalignComponent + }, + { + path: 'daterange/daterange-nowdatetime', + component: DaterangeNowdatetimeComponent + }, + { + path: 'daterange/daterange-validation', + component: DaterangeValidationComponent + }, + { + path: 'daterange/daterange-disabled', + component: DaterangeDisabledComponent + }, + { + path: 'daterange/daterange-customize', + component: DaterangeCustomizeComponent + }, + { + path: 'daterange/daterange-disableddays', + component: DaterangeDisableddaysComponent + }, + { + path: 'daterange/daterange-fixedvalue', + component: DaterangeFixedvalueComponent + }, + { + path: 'daterange/daterange-isallowbeginequalend', + component: DaterangeIsallowbeginequalendComponent + }, + { + path: 'daterange/daterange-event', + component: DaterangeEventComponent + }, + { path: 'daterange/daterange-maxmin', component: DaterangeMaxminComponent }, + { + path: 'daterange/daterange-value-test', + component: DaterangeValueTestComponent + }, + { + path: 'daterange/daterange-format-test', + component: DaterangeFormatTestComponent + }, + { + path: 'daterange/daterange-fixedvalue-test', + component: DaterangeFixedvalueTestComponent + }, + { + path: 'daterange/daterange-maxmin-test', + component: DaterangeMaxminTestComponent + } + ]; +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeCustomizeComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeCustomizeComponent.ts new file mode 100644 index 0000000..bc2e014 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeCustomizeComponent.ts @@ -0,0 +1,44 @@ +import { Component } from '@angular/core'; +import { TiDateCustomizeOptions, TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-customize.html' +}) +export class DaterangeCustomizeComponent { + nowTime: Date = new Date(); + nowYear: number = this.nowTime.getFullYear(); + nowMonth: number = this.nowTime.getMonth(); + nowDate: number = this.nowTime.getDate(); + value: TiDateValue = { + begin: new Date(2022, 5, 1), + end: new Date(2023, 1, 4) + }; + customizeOptions: Array = [ + { + label: '最近一周', + value: { + begin: new Date(this.nowYear, this.nowMonth, this.nowDate - 6), + end: this.nowTime + } + }, + { + label: '最近一个月', + value: { + begin: new Date(this.nowYear, this.nowMonth - 1, this.nowDate), + end: this.nowTime + } + }, + { + label: '最近三个月', + value: { + begin: new Date(this.nowYear, this.nowMonth - 3, this.nowDate), + end: this.nowTime + } + } + ]; + myLogs: Array = []; + + onCustomizeOptionClick(model: TiDateValue): void { + this.myLogs = [...this.myLogs, `customizeOptionClick() model = ${JSON.stringify(model)}`]; + } +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeDisabledComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeDisabledComponent.ts new file mode 100644 index 0000000..50cdffd --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeDisabledComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-disabled.html' +}) +export class DaterangeDisabledComponent { + disabled: boolean = true; + value: TiDateValue = { + begin: new Date(2015, 3, 12), + end: new Date(2056, 2, 1) + }; +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeDisableddaysComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeDisableddaysComponent.ts new file mode 100644 index 0000000..468aaca --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeDisableddaysComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-disableddays.html' +}) +export class DaterangeDisableddaysComponent { + value: TiDateValue = null; + nowTime: Date = new Date(); + nowYear: number = this.nowTime.getFullYear(); + nowMonth: number = this.nowTime.getMonth(); + nowDate: number = this.nowTime.getDate(); + disabledDays: Array = [ + new Date(this.nowYear, this.nowMonth, this.nowDate - 6), + new Date(this.nowYear, this.nowMonth, this.nowDate), + new Date(this.nowYear, this.nowMonth, this.nowDate + 6), + new Date(this.nowYear, this.nowMonth + 2, this.nowDate) + ]; +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeEventComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeEventComponent.ts new file mode 100644 index 0000000..e3488e8 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeEventComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-event.html' +}) +export class DaterangeEventComponent { + value: TiDateValue = null; + myLogs: Array = []; + + onNgModelChange(value: TiDateValue): void { + this.myLogs = [...this.myLogs, `ngModelChange value = ${JSON.stringify(value)}`]; + } + + onDayClick(info: any): void { + this.myLogs.push(`onDayClick() info = value: ${info.value}, focusedPosition: ${info.focusedPosition}, + beginValue: ${info.beginValue}, endValue: ${info.endValue}`); + } + + onOkClick(value: Date): void { + this.myLogs = [...this.myLogs, `onOkClick() value = ${value}`]; + } +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeFixedvalueComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeFixedvalueComponent.ts new file mode 100644 index 0000000..5874a1a --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeFixedvalueComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-fixedvalue.html' +}) +export class DaterangeFixedvalueComponent { + isBeginFixed: boolean = true; + isEndFixed: boolean = true; + value1: TiDateValue = { + begin: new Date(2021, 8, 27), + end: new Date(2021, 9, 6) + }; + value2: TiDateValue = { + begin: new Date(2021, 8, 27), + end: new Date(2021, 9, 6) + }; +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeFixedvalueTestComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeFixedvalueTestComponent.ts new file mode 100644 index 0000000..de3a311 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeFixedvalueTestComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-fixedvalue-test.html' +}) +export class DaterangeFixedvalueTestComponent { + value: TiDateValue = { + begin: new Date(1990, 8, 27), + end: new Date(1990, 9, 6) + }; + isBeginFixed: boolean = true; + isEndFixed: boolean = true; + onChange(): void { + this.value.end = new Date(1990, 10, 6); + this.value = { ...this.value }; + } +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeFormatComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeFormatComponent.ts new file mode 100644 index 0000000..082d257 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeFormatComponent.ts @@ -0,0 +1,17 @@ +import { Component, ViewChild } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-format.html' +}) +export class DaterangeFormatComponent { + dayFormat: string = 'yyyy-MM-d'; + monthFormat: string = 'yyyy/M'; + seasonFormat: string = 'YYYY/QQ'; + yearFormat: string = 'yyyy'; + + dayValue: TiDateValue = null; + monthValue: TiDateValue = null; + seasonValue: TiDateValue = null; + yearValue: TiDateValue = null; +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeFormatTestComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeFormatTestComponent.ts new file mode 100644 index 0000000..e01990e --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeFormatTestComponent.ts @@ -0,0 +1,53 @@ +import { Component, ViewChild } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-format-test.html' +}) +export class DaterangeFormatTestComponent { + value: TiDateValue = { + begin: new Date(2017, 8, 27), + end: new Date(2018, 5, 6) + }; + + value1: TiDateValue = { + begin: new Date(2013, 10, 10), + end: new Date(2015, 5, 6) + }; + + value2: TiDateValue = { + begin: new Date(2016, 2, 25), + end: new Date(2018, 10, 16) + }; + + value3: TiDateValue = { + begin: new Date(2013, 10, 10), + end: new Date(2018, 10, 16) + }; + + value4: TiDateValue = { + begin: new Date(2013, 6, 1), + end: new Date(2018, 8, 30, 23, 59, 59) + }; + min: Date = new Date(2012, 0); + + format1: string = 'yyyy/MM/dd'; + format2: string = 'yyyy.MM'; + format3: string = 'mediumDate'; + format4: string = 'yyyy'; + format5: string = 'YYYY/QQ'; + + clickFn(): void { + this.format3 = 'yyyy.MM.dd'; + } + + okClick(event: TiDateValue): void { + console.log(event); + } + changeQuarterVal(): void { + this.value4 = { + begin: new Date(2013, 10, 1), + end: new Date(2018, 4, 30, 23, 59, 59) + }; + } +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeIsallowbeginequalendComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeIsallowbeginequalendComponent.ts new file mode 100644 index 0000000..419f546 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeIsallowbeginequalendComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-isallowbeginequalend.html' +}) +export class DaterangeIsallowbeginequalendComponent { + isAllowBeginEqualEnd: boolean = false; + value: TiDateValue = null; +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeMaxComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeMaxComponent.ts new file mode 100644 index 0000000..930bcfe --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeMaxComponent.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-max.html' +}) +export class DaterangeMaxComponent { + format: string = 'yyyy-MM-dd'; + value: TiDateValue = { + begin: new Date(1994, 2, 8), + end: new Date(2016, 5, 21) + }; + + value1: TiDateValue = { + begin: new Date(1992, 2, 21), + end: new Date(2016, 10, 15) + }; + + value2: TiDateValue; + + max: Date = new Date(2018, 10, 8); + max1: Date = new Date(2018, 10, 8); + + maxValueClick(): void { + this.max = new Date(2017, 2, 15); + } + maxValueClick2(): void { + this.max = undefined; + } + + onDayClick(obj: any): void { + if ((obj.focusedPosition === 'end' && obj.endValue === null) || obj.focusedPosition === 'begin') { + this.max1 = new Date(obj.beginValue?.getFullYear(), obj.beginValue?.getMonth() + 1, obj.beginValue?.getDate() + 6); + } + } +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeMaxminComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeMaxminComponent.ts new file mode 100644 index 0000000..cc2200c --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeMaxminComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-maxmin.html' +}) +export class DaterangeMaxminComponent { + min: Date = new Date(2021, 11, 25); + max: Date = new Date(2023, 1, 14); + value1: TiDateValue = null; + value2: TiDateValue = null; + value3: TiDateValue = null; +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeMaxminTestComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeMaxminTestComponent.ts new file mode 100644 index 0000000..49ac045 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeMaxminTestComponent.ts @@ -0,0 +1,47 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-maxmin-test.html' +}) +export class DaterangeMaxminTestComponent { + nowTime: Date = new Date(); + delayTime: number = 60 * 60 * 1000 * 24 * 10; // 10天 + min: Date = new Date(); + max: Date = new Date(); + min1: Date = new Date(); + max1: Date = new Date(); + min2: Date = new Date(); + max2: Date = new Date(); + format: string; + value: TiDateValue; + value1: TiDateValue; + value2: TiDateValue; + value3: TiDateValue; + + changeFormat(): void { + this.format = 'yyyy'; + } + + changeFormat1(): void { + this.format = 'yyyy/MM'; + } + + changeFormat2(): void { + this.format = 'YYYY/QQ'; + } + + changeFormat3(): void { + this.format = 'yyyy/MM/dd'; + } + + onMaxAndMinClick(): void { + this.max1 = new Date(this.nowTime.getTime() - this.delayTime); + this.min1 = new Date(this.nowTime.getTime() + this.delayTime); + } + + onMaxAndMinClick1(): void { + this.max2 = new Date(this.nowTime.getTime() + this.delayTime); + this.min2 = new Date(this.nowTime.getTime() - this.delayTime); + } +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeMinComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeMinComponent.ts new file mode 100644 index 0000000..1dd00e9 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeMinComponent.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-min.html' +}) +export class DaterangeMinComponent { + min: string = ''; + min1: Date = new Date(2016, 5, 21); + value: TiDateValue = { + begin: new Date(1994, 2, 8), + end: new Date(2016, 5, 21) + }; + + value1: TiDateValue = { + begin: new Date(2017, 4, 2), + end: new Date(2018, 4, 16) + }; + + value2: TiDateValue; + min2: Date = new Date(2021, 5, 21); + + minValueClick(): void { + this.min1 = new Date(2017, 2, 23); + } + minValueClick2(): void { + this.min1 = undefined; + } + + onDayClick(obj: any): void { + if ((obj.focusedPosition === 'begin' && obj.beginValue === null) || obj.focusedPosition === 'end') { + this.min2 = new Date(obj.endValue?.getFullYear(), obj.endValue?.getMonth() - 1, obj.endValue?.getDate()); + } + } +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeNowdatetimeComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeNowdatetimeComponent.ts new file mode 100644 index 0000000..7c8bbf3 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeNowdatetimeComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-nowdatetime.html' +}) +export class DaterangeNowdatetimeComponent { + nowDateTime: Date = new Date(2022, 5, 20); + value: TiDateValue = null; +} diff --git a/src/daterange/demo/src/app/daterange/DaterangePanelalignComponent.ts b/src/daterange/demo/src/app/daterange/DaterangePanelalignComponent.ts new file mode 100644 index 0000000..5611aa3 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangePanelalignComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-panelalign.html' +}) +export class DaterangePanelalignComponent { + panelAlign: string = 'right'; + value: TiDateValue = { + begin: new Date(2015, 3, 12), + end: new Date(2056, 2, 1) + }; +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeValidationComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeValidationComponent.ts new file mode 100644 index 0000000..8c6014d --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeValidationComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiDateValue, TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-validation.html' +}) +export class DaterangeValidationComponent { + value: TiDateValue = { + begin: new Date(2015, 8, 27), + end: new Date(2016, 5, 6) + }; + validation: TiValidationConfig = { + tipPosition: 'top' + }; +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeValueComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeValueComponent.ts new file mode 100644 index 0000000..b056e00 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeValueComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-value.html' +}) +export class DaterangeValueComponent { + value1: TiDateValue = null; + value2: TiDateValue = { + begin: new Date(2017, 8, 27), + end: new Date(2018, 5, 6) + }; +} diff --git a/src/daterange/demo/src/app/daterange/DaterangeValueTestComponent.ts b/src/daterange/demo/src/app/daterange/DaterangeValueTestComponent.ts new file mode 100644 index 0000000..a71cbca --- /dev/null +++ b/src/daterange/demo/src/app/daterange/DaterangeValueTestComponent.ts @@ -0,0 +1,48 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './daterange-value-test.html' +}) +export class DaterangeValueTestComponent { + id: string = 'daterange'; + min: Date = new Date(1991, 6, 2); + max: Date = new Date(2017, 11, 25); + format: string = 'yyyy-MM-dd'; + value: string = ''; + + value1: TiDateValue = { + begin: new Date(2015, 8, 27), + end: new Date(2016, 5, 6) + }; + + value2: TiDateValue = { + begin: new Date(2017, 8, 27), + end: new Date(2018, 5, 6) + }; + + valueClickFn(): void { + this.value2 = { + begin: new Date(2015, 10, 12), + end: new Date() + }; + } + + valueClickFn1(): void { + this.value2.begin = new Date(2013, 11, 10); + this.value2 = { ...this.value2 }; + } + + valueClickFn2(): void { + this.value2.end = new Date(2020, 11, 16); + this.value2 = { ...this.value2 }; + } + + valueClickFn3(): void { + this.value1 = null; + } + + ngModelChange(event: TiDateValue): void { + console.log(event, 'ngModelChange'); + } +} diff --git a/src/daterange/demo/src/app/daterange/daterange-customize.html b/src/daterange/demo/src/app/daterange/daterange-customize.html new file mode 100644 index 0000000..1865dd4 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-customize.html @@ -0,0 +1,8 @@ + + + diff --git a/src/daterange/demo/src/app/daterange/daterange-disabled.html b/src/daterange/demo/src/app/daterange/daterange-disabled.html new file mode 100644 index 0000000..bf933e6 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-disabled.html @@ -0,0 +1 @@ + diff --git a/src/daterange/demo/src/app/daterange/daterange-disableddays.html b/src/daterange/demo/src/app/daterange/daterange-disableddays.html new file mode 100644 index 0000000..e1032d7 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-disableddays.html @@ -0,0 +1 @@ + diff --git a/src/daterange/demo/src/app/daterange/daterange-event.html b/src/daterange/demo/src/app/daterange/daterange-event.html new file mode 100644 index 0000000..064b074 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-event.html @@ -0,0 +1,10 @@ + + + diff --git a/src/daterange/demo/src/app/daterange/daterange-fixedvalue-test.html b/src/daterange/demo/src/app/daterange/daterange-fixedvalue-test.html new file mode 100644 index 0000000..e5696e7 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-fixedvalue-test.html @@ -0,0 +1,11 @@ +

1.描述

+

开始或结束值固定测试用例

+

2.示例

+

(2.1)开始日期固定

+ + +

(2.2)结束日期固定

+ +

(2.3)动态变更结束日期

+ + diff --git a/src/daterange/demo/src/app/daterange/daterange-fixedvalue.html b/src/daterange/demo/src/app/daterange/daterange-fixedvalue.html new file mode 100644 index 0000000..f6fdba4 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-fixedvalue.html @@ -0,0 +1,9 @@ + +
+ diff --git a/src/daterange/demo/src/app/daterange/daterange-format-test.html b/src/daterange/demo/src/app/daterange/daterange-format-test.html new file mode 100644 index 0000000..784b7bf --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-format-test.html @@ -0,0 +1,36 @@ +

1.描述

+

format接口测试

+

2.示例

+

(2.1)不设置时,使用默认类型。中文:'yyyy/MM/dd';英文:"MMM dd, yyyy"。

+ +
+
+
+

(2.2)设置为"yyyy/MM/dd"

+ +
+
+
+

(2.3)只显示年月

+ +
+
+

(2.4)只显示年

+ +
+
+
+

(2.5)设置为"mediumDate"

+ +
+
+
+

(2.5)设置为"YYYY/QQ",显示季度区间

+ +
+季度面板开始值:{{value4?.begin}} +
+季度面板结束值:{{value4?.end}} +

+ + diff --git a/src/daterange/demo/src/app/daterange/daterange-format.html b/src/daterange/demo/src/app/daterange/daterange-format.html new file mode 100644 index 0000000..c1b5cd5 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-format.html @@ -0,0 +1,7 @@ + +
+ +
+ +
+ diff --git a/src/daterange/demo/src/app/daterange/daterange-isallowbeginequalend.html b/src/daterange/demo/src/app/daterange/daterange-isallowbeginequalend.html new file mode 100644 index 0000000..7e2a9d8 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-isallowbeginequalend.html @@ -0,0 +1 @@ + diff --git a/src/daterange/demo/src/app/daterange/daterange-max.html b/src/daterange/demo/src/app/daterange/daterange-max.html new file mode 100644 index 0000000..a18ff9e --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-max.html @@ -0,0 +1,24 @@ +

1.描述

+

max接口测试

+

2.示例

+

(2.1)最大值设置为非字符串类型、非法日期时:最大值取默认值

+ +
+
+
+

(2.2)设置为正常日期值时,初始、下拉面板显示及用户操作改变时都必须小于这个最大值

+ +
+
+最大值: {{ max | date: "y-MM-dd"}} +
+ + +
+
+
+

(2.3)dayClick事件中设置最大值(选择开始时间,更改最大值 10.1.11版本支持)

+最大值: {{ max1 | date: "y-MM-dd"}} +
+
+ diff --git a/src/daterange/demo/src/app/daterange/daterange-maxmin-test.html b/src/daterange/demo/src/app/daterange/daterange-maxmin-test.html new file mode 100644 index 0000000..a595d71 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-maxmin-test.html @@ -0,0 +1,42 @@ +

描述

+

min,max临界值测试

+

示例

+

1.max 接口测试,设置max为 new Date()

+
+ +

+开始日期:{{value?.begin}} +
+结束日期:{{value?.end}} +

+

2.min 接口测试,设置min为 new Date()

+
+ +

+开始日期:{{value1?.begin}} +
+结束日期:{{value1?.end}} +

+

3.max min 接口测试,设置max1,min1都为 new Date()

+
+ +

+开始日期:{{value2?.begin}} +
+结束日期:{{value2?.end}} +

+

4.max min 接口测试,设置max2,min2都为 new Date()

+
+ +

+开始日期:{{value3?.begin}} +
+结束日期:{{value3?.end}} +

+切换日期格式: + + + + + + diff --git a/src/daterange/demo/src/app/daterange/daterange-maxmin.html b/src/daterange/demo/src/app/daterange/daterange-maxmin.html new file mode 100644 index 0000000..bc30e1a --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-maxmin.html @@ -0,0 +1,5 @@ + +
+ +
+ diff --git a/src/daterange/demo/src/app/daterange/daterange-min.html b/src/daterange/demo/src/app/daterange/daterange-min.html new file mode 100644 index 0000000..16475d8 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-min.html @@ -0,0 +1,24 @@ +

1.描述

+

min接口测试

+

2.示例

+

(2.1)最小值设置为非字符串类型、非法日期时:最小值取默认值

+ +
+
+
+

(2.2)设置为正常日期值时,初始、下拉面板显示及用户操作改变时都必须大于这个最小值

+ +
+
+最小值:{{min1 | date: "yyyy-MM-dd"}} +
+ + +
+
+
+

(2.3)dayClick事件中设置最小值(选择结束时间,更改最小值 10.1.11版本支持)

+最小值: {{ min2 | date: "y-MM-dd"}} +
+
+ diff --git a/src/daterange/demo/src/app/daterange/daterange-nowdatetime.html b/src/daterange/demo/src/app/daterange/daterange-nowdatetime.html new file mode 100644 index 0000000..ef75fb9 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-nowdatetime.html @@ -0,0 +1 @@ + diff --git a/src/daterange/demo/src/app/daterange/daterange-panelalign.html b/src/daterange/demo/src/app/daterange/daterange-panelalign.html new file mode 100644 index 0000000..e8d5dce --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-panelalign.html @@ -0,0 +1 @@ + diff --git a/src/daterange/demo/src/app/daterange/daterange-validation.html b/src/daterange/demo/src/app/daterange/daterange-validation.html new file mode 100644 index 0000000..71aaca6 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-validation.html @@ -0,0 +1 @@ + diff --git a/src/daterange/demo/src/app/daterange/daterange-value-test.html b/src/daterange/demo/src/app/daterange/daterange-value-test.html new file mode 100644 index 0000000..c7d0bd6 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-value-test.html @@ -0,0 +1,26 @@ +

1.描述

+

value接口测试

+

导入

+

import {{ '{' }} TiDateRangeModule {{ '}' }} from '@opentiny/ng';

+

2.示例

+

(2.1)设置为非字符串类型、非法日期时,显示空

+ +
+
+
+

(2.2)begin和end都设置为合法的Date类型,以begin和end为输入框值

+ +
+
+ +
+
+

(2.3)动态更新

+ +
+
+
+ + + + diff --git a/src/daterange/demo/src/app/daterange/daterange-value.html b/src/daterange/demo/src/app/daterange/daterange-value.html new file mode 100644 index 0000000..f8db09f --- /dev/null +++ b/src/daterange/demo/src/app/daterange/daterange-value.html @@ -0,0 +1,2 @@ +
+ diff --git a/src/daterange/demo/src/app/daterange/webdoc/daterange-demos.js b/src/daterange/demo/src/app/daterange/webdoc/daterange-demos.js new file mode 100644 index 0000000..f4e8dcc --- /dev/null +++ b/src/daterange/demo/src/app/daterange/webdoc/daterange-demos.js @@ -0,0 +1,162 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'daterange-value', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': '

DateRange 组件的最简用法。

', + 'en-US': '' + } + }, + { + demoId: 'daterange-format', + name: { + 'zh-CN': '日期范围格式', + 'en-US': 'format' + }, + desc: { + 'zh-CN': '

通过属性format配置日期范围显示格式。

', + 'en-US': '' + }, + apis: ['TiDateRangeComponent.properties.format'] + }, + { + demoId: 'daterange-maxmin', + name: { + 'zh-CN': '预设范围', + 'en-US': 'maxmin' + }, + desc: { + 'zh-CN': '

通过属性max配置可选择日期范围的最大值;通过属性min配置可选择日期范围的最小值。

', + 'en-US': '' + }, + apis: ['TiDateRangeComponent.properties.max', 'TiDateRangeComponent.properties.min'] + }, + { + demoId: 'daterange-panelalign', + name: { + 'zh-CN': '下拉面板对齐方向', + 'en-US': 'panelAlign' + }, + desc: { + 'zh-CN': '

通过属性panelAlign配置下拉面板对齐方向,包括leftright

', + 'en-US': '' + }, + apis: ['TiDateRangeComponent.properties.panelAlign'] + }, + { + demoId: 'daterange-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled' + }, + desc: { + 'zh-CN': '

通过属性disabled配置是否为禁用状态。

', + 'en-US': '' + }, + apis: ['TiDateRangeComponent.properties.disabled'] + }, + { + demoId: 'daterange-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event' + }, + desc: { + 'zh-CN': + '当点击下拉面板上的日期时触发dayClick事件,传递出去的参数为包含valuefocusedPositionbeginValueendValue这四个属性的对象;当点击面板上的确认按钮时触发okClick事件,传递出去的参数为选中的日期。', + 'en-US': '' + }, + apis: ['TiDateRangeComponent.events.dayClick', 'TiDateRangeComponent.events.okClick'] + }, + { + demoId: 'daterange-fixedvalue', + name: { + 'zh-CN': '固定开始日期或者结束日期', + 'en-US': 'isBeginOrEndFixed' + }, + desc: { + 'zh-CN': + '

通过属性isBeginFixedisEndFixed配置是否固定开始日期或者结束日期;通过属性clearIcon配置选择框右侧是否显示清除的叉号图标。

', + 'en-US': '' + }, + apis: [ + 'TiDateRangeComponent.properties.isBeginFixed', + 'TiDateRangeComponent.properties.isEndFixed', + 'TiDateRangeComponent.properties.clearIcon' + ] + }, + { + demoId: 'daterange-isallowbeginequalend', + name: { + 'zh-CN': '不允许开始日期和结束日期相同', + 'en-US': 'isAllowBeginEqualEnd' + }, + desc: { + 'zh-CN': '

通过属性isAllowBeginEqualEnd配置是否允许结束日期与开始日期相同。

', + 'en-US': '' + }, + apis: ['TiDateRangeComponent.properties.isAllowBeginEqualEnd'] + }, + { + demoId: 'daterange-customize', + name: { + 'zh-CN': '自定义下拉面板内容', + 'en-US': 'customize' + }, + desc: { + 'zh-CN': + '

通过属性customizeOptions自定义下拉面板左侧内容,设置可快捷选择的常用日期范围;当点击自定义下拉面板左侧内容时触发customizeOptionClick事件,传递出去的参数为选中的日期范围。

', + 'en-US': '' + }, + apis: [ + 'TiDateRangeComponent.properties.customizeOptions', + 'TiDateCustomizeOptions.properties.label', + 'TiDateCustomizeOptions.properties.value', + 'TiDateValue.properties.begin', + 'TiDateValue.properties.end' + ] + }, + { + demoId: 'daterange-disableddays', + name: { + 'zh-CN': '不可选择日期', + 'en-US': 'disabledDays' + }, + desc: { + 'zh-CN': '

通过属性disabledDays配置禁止选择的部分日期。

', + 'en-US': '' + }, + apis: ['TiDateRangeComponent.properties.disabledDays'] + }, + { + demoId: 'daterange-validation', + name: { + 'zh-CN': '校验', + 'en-US': 'daterange validation' + }, + desc: { + 'zh-CN': '

通过指令tiValidation实现校验。

', + 'en-US': '' + } + }, + { + demoId: 'daterange-nowdatetime', + name: { + 'zh-CN': '自定义当前日期', + 'en-US': 'nowDateTime' + }, + desc: { + 'zh-CN': '

通过属性nowDateTime自定义当前日期,可用于矫正实际日期和计算机系统日期的偏差。

', + 'en-US': '' + }, + apis: ['TiDateRangeComponent.properties.nowDateTime'] + } + ], + ignoreApis: ['TiDateValue.properties.timeZone'] +}; diff --git a/src/daterange/demo/src/app/daterange/webdoc/daterange.cn.md b/src/daterange/demo/src/app/daterange/webdoc/daterange.cn.md new file mode 100644 index 0000000..36e9fbd --- /dev/null +++ b/src/daterange/demo/src/app/daterange/webdoc/daterange.cn.md @@ -0,0 +1,29 @@ +--- +title: DateRange 日期范围 +--- +# DateRange 日期范围 + +
+ +选择或输入日期范围的组件。 + +```typescript +import { TiDateRangeModule } from '@opentiny/ng'; +``` + +如需使用校验功能,请导入。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
+ +
+ +选择或输入日期范围的组件。 + +```typescript +import { TiDateRangeModule } from '@opentiny/ng'; +``` +
diff --git a/src/daterange/demo/src/app/daterange/webdoc/daterange.en.md b/src/daterange/demo/src/app/daterange/webdoc/daterange.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/daterange/demo/src/app/daterange/webdoc/daterange.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/daterange/demo/src/favicon.ico b/src/daterange/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/daterange/demo/src/index.html b/src/daterange/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/daterange/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/daterange/demo/src/main.ts b/src/daterange/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/daterange/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/daterange/demo/test.ts b/src/daterange/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/daterange/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/daterange/demo/tsconfig.app.json b/src/daterange/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/daterange/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/daterange/demo/tsconfig.spec.json b/src/daterange/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/daterange/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/daterange/lib/index.ts b/src/daterange/lib/index.ts new file mode 100644 index 0000000..5c65874 --- /dev/null +++ b/src/daterange/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiDateRangeModule'; +export { TiDateCustomizeOptions, TiDateValue } from '@opentiny/ng-datebase'; diff --git a/src/daterange/lib/ng-package.json b/src/daterange/lib/ng-package.json new file mode 100644 index 0000000..94683d7 --- /dev/null +++ b/src/daterange/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/daterange", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/daterange/lib/package.json b/src/daterange/lib/package.json new file mode 100644 index 0000000..27a9686 --- /dev/null +++ b/src/daterange/lib/package.json @@ -0,0 +1,19 @@ +{ + "name": "@opentiny/ng-daterange", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-datebase": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@opentiny/ng-datepanel": "~1.0.0-beta.0", + "@opentiny/ng-dateedit": "~1.0.0-beta.0", + "@opentiny/ng-datedominator": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/daterange/lib/project.json b/src/daterange/lib/project.json new file mode 100644 index 0000000..c1c6ea0 --- /dev/null +++ b/src/daterange/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/daterange/lib", + "sourceRoot": "src/daterange/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/daterange"], + "options": { + "project": "src/daterange/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/daterange"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js daterange" + }, + { + "command": "ng default-build daterange" + }, + { + "command": "node build/clear-default-theme.js daterange" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/daterange && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build daterange && ng pack daterange && node build/publish.js daterange --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/daterange/lib/src/TiDateRangeComponent.ts b/src/daterange/lib/src/TiDateRangeComponent.ts new file mode 100644 index 0000000..faf8878 --- /dev/null +++ b/src/daterange/lib/src/TiDateRangeComponent.ts @@ -0,0 +1,319 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, SimpleChanges, ChangeDetectionStrategy, Input } from '@angular/core'; +import { TiDateUtil, TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiDateBaseComponent, TiDateValue } from '@opentiny/ng-datebase'; +import { TiLocaleFormat } from '@opentiny/ng-locale'; +import packageInfo from '../package.json'; + +/** + * DateRange日期范围组件 + * + * DateRange组件提供了一种方便的显示和设置日期范围的方式 + * + */ +@Component({ + selector: 'ti-date-range', + templateUrl: './daterange.html', + styleUrls: ['./daterange.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-date-range-input-container]': 'true', + '(blur)': 'hidePanel()' + }, + // eslint-disable-next-line no-use-before-define + providers: [TiFormComponent.getValueAccessor(TiDateRangeComponent)] +}) +export class TiDateRangeComponent extends TiDateBaseComponent { + /** + * 是否允许结束日期和开始日期相同 + */ + @Input() isAllowBeginEqualEnd: boolean = true; + /** + * @ignore + * 日期显示格式: date组件的format为string类型 + */ + public format: string; + /** + * @ignore + * 标记date/datetime的类型 + */ + public isDatetime: boolean = false; + /** + * @ignore + * 用于标记是不是range + */ + public isRange: boolean = true; + /** + * @ignore + * 保存model值 + */ + public oldModel: TiDateValue = { + begin: null, + end: null + }; + /** + * @ignore + * 标记输入框中的时间是不是手动输入 + */ + public isInputValue: boolean = false; + + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + super.ngOnInit(); + this.setPlacehoder(); + } + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + // format支持动态变更 + if (changes['format'] && !changes['format'].firstChange) { + // 新的format是非法时,format值保持之前值不变 + if (!Util.isString(changes['format'].currentValue)) { + this.format = changes['format'].previousValue; + + return; + } + this.formatValue(); + this.setPlacehoder(); + this.setDatepanelPicker(); + this.validateMinAndMax(this.config); + } + this.rangeChange(changes); + } + + ngDoCheck(): void { + // 监听value值 + if (!Util.isUndefined(this.model)) { + if (!this.isValidValue(this.model)) { + return; + } + // 监听model值的变化 + this.setModel(this.isDatetime); + } + } + + /** + * @ignore + * 将value转换成format接口格式的字符串 + */ + public formatValue(): void { + if (this.model === null || (this.model.begin === null && this.model.end === null)) { + this.inputValue = ''; + } else { + let begin: string = TiLocaleFormat.formatDate(this.model.begin, this.format); + let end: string = TiLocaleFormat.formatDate(this.model.end, this.format); + // 注意:中划线用的是制表符中的中划线,与正常的中线区分开2017-2-12 - 2018-3-20+ + if (this.format === 'YYYY/QQ') { + begin = TiDateUtil.transformDateToQuarter(this.model.begin); + end = TiDateUtil.transformDateToQuarter(this.model.end); + } + this.inputValue = `${begin} ─ ${end}`; + } + } + /** + * @ignore + */ + onKeydownFn(event: KeyboardEvent, pos: string): void { + if (event.keyCode === TiKeymap.KEY_ENTER || event.keyCode === TiKeymap.KEY_NUMPAD_ENTER) { + this.dateEditBlur(pos); + } + } + /** + * @ignore + */ + dateEditBlur(pos: string): void { + // 输入值超出最大最小 认为非法 + const value: any = this.datePanel.value; + if (pos === 'begin') { + // 添加setTimeout原因:确保日期框失焦,this.focusedPosition获取到的是处于聚焦状态时的位置 + setTimeout(() => { + if (this.focusedPosition === 'end' && TiDateUtil.isDate(value.begin)) { + this.datePanel.min = value.begin; + + if (TiDateUtil.isDate(value.end)) { + this.datePanel.max = this.max; + } + } + // 绑定在模板上的值变了,需要手动触发变更检测 + this.changeDetectorRef.detectChanges(); + }, 0); + } else { + setTimeout(() => { + if (this.focusedPosition === 'begin' && TiDateUtil.isDate(value.end)) { + this.datePanel.max = value.end; + + if (TiDateUtil.isDate(value.begin)) { + this.datePanel.min = this.min; + } + } + this.changeDetectorRef.detectChanges(); + }, 0); + } + + // 添加setTimeout原因:点击另外输入框时,获取到最新的focusedPosition再做判断 + // 点击非禁用的确认按钮,需保证焦点在面板内部,否则点击document空白处无法关闭面板 + setTimeout(() => { + if ( + ((!TiDateUtil.isDate(value.begin) && this.focusedPosition !== 'end') || + (!TiDateUtil.isDate(value.end) && this.focusedPosition !== 'begin')) && + !this.inValidValue + ) { + this.dropCom.nativeElement.focus(); + } + this.setOkBtnState(); + }, 0); + } + /** + * @ignore + * 日期框值变化且是合法值时触发 + */ + onInputChange(): void { + this.isInputValue = true; + } + /** + * @ignore + * 确认按钮 + */ + public onOkClick(): void { + if (this.inValidValue) { + return; + } + + if (this.datePanel instanceof Object) { + // 新旧值不同更新model值 + if (!this.rangeValueIsEqual(this.model, this.datePanel['value'], this.isDatetime)) { + const endDate: Date = this.datePanel['value'].end; + const transformDateToExactDate: Date = TiDateUtil.transformDateToExactDate(endDate, this.datePanel.picker, true); + this.model = { + begin: this.datePanel['value'].begin, + // 结束日期应转化为对应具体日期时间,此处添加判断排除用户通过鼠标操作选择结束日期的场景 + end: TiDateUtil.isDatetimeEqual(endDate, transformDateToExactDate) ? endDate : transformDateToExactDate + }; + } + } + + this.hideDrop(); + + this.okClick.emit(this.model); + } + + /** + * @ignore + * 是否为禁用日期 + */ + public isDisabledDays(value: Date): boolean { + return TiDateUtil.isDisabledDays(this.disabledDays, value); + } + /** + * @ignore + * model值的合法性判断 + */ + public isValidValue(value: TiDateValue): boolean { + if (value === null) { + return true; + } + + return this.isValidRange(value) ? true : false; + } + + // 判断是不是合法范围 + private isValidRange(value: TiDateValue): boolean { + if (!(value instanceof Object) || !TiDateUtil.isDate(value.begin) || !TiDateUtil.isDate(value.end)) { + return false; + } + + const startDate: Date = new Date(value.begin.getFullYear(), value.begin.getMonth(), value.begin.getDate()); + const endDate: Date = new Date(value.end.getFullYear(), value.end.getMonth(), value.end.getDate()); + + return ( + startDate.getTime() <= endDate.getTime() && + TiDateUtil.isBetweenMaxAndmin(startDate, this.min, this.max) && + TiDateUtil.isBetweenMaxAndmin(endDate, this.min, this.max) + ); + } + /** + * @ignore + * 设置确认按钮的状态 + */ + public setOkBtnState(): void { + const date: TiDateValue = this.datePanel.value; + let endTime: number; + let beginTime: number; + if (TiDateUtil.isDate(date.end)) { + endTime = new Date(date.end.getFullYear(), date.end.getMonth(), date.end.getDate()).getTime(); + } + if (TiDateUtil.isDate(date.begin)) { + beginTime = new Date(date.begin.getFullYear(), date.begin.getMonth(), date.begin.getDate()).getTime(); + } + const isBeginSmallorEqualEnd: boolean = this.isAllowBeginEqualEnd ? endTime < beginTime : endTime <= beginTime; + this.inValidValue = + !(date instanceof Object) || !TiDateUtil.isDate(date.begin) || !TiDateUtil.isDate(date.end) || isBeginSmallorEqualEnd; + + if (this.buttonComs) { + this.setAttr(this.buttonComs.last.nativeElement, 'disabled', this.inValidValue); + this.setInputStyle(this.inValidValue); + } + } + + /** + * @ignore + * 配置时间日期面板接口 + */ + public setPickerDate(): void { + // 做深拷贝的原因:不让model和datePanel组件中value双向绑定, + // 因为下拉面板中日期变化时,不立即更新到输入框中 + let value: TiDateValue; + if (this.model === null || this.model === undefined) { + value = { + begin: null, + end: null + }; + } else { + value = { + begin: this.model.begin, + end: this.model.end + }; + } + + this.datePanel = { + value, + format: this.format, + max: this.max, + min: this.min, + picker: this.datepanelPicker, + select: (): void => { + const obj: any = { + value: this.datePanel.value[this.focusedPosition], + focusedPosition: this.focusedPosition, + beginValue: this.datePanel.value.begin, + endValue: this.datePanel.value.end + }; + this.dayClick.emit(obj); + // 初始化选择结束之后,如果开始未选择,焦点转移到开始日期编辑框 + if (this.focusedPosition === 'end' && this.datePanel.value.begin === null) { + this.focusedPosition = 'begin'; + this.dateEditComs.first.focus(); + } else { + if (!this.isEndFixed) { + this.focusedPosition = 'end'; + this.dateEditComs.last.focus(); + } + } + this.setOkBtnState(); + } + }; + + this.setOkBtnState(); + } +} diff --git a/src/daterange/lib/src/TiDateRangeModule.ts b/src/daterange/lib/src/TiDateRangeModule.ts new file mode 100644 index 0000000..1a67856 --- /dev/null +++ b/src/daterange/lib/src/TiDateRangeModule.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiLocaleModule } from '@opentiny/ng-locale'; +import { TiDateRangeComponent } from './TiDateRangeComponent'; +import { TiButtonModule } from '@opentiny/ng-button'; +import { TiDatePanelModule } from '@opentiny/ng-datepanel'; +import { TiDateEditModule } from '@opentiny/ng-dateedit'; +import { TiDateDominatorModule } from '@opentiny/ng-datedominator'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiLocaleModule, + TiDropModule, + TiButtonModule, + TiDatePanelModule, + TiDateEditModule, + TiDateDominatorModule + ], + exports: [TiDateRangeComponent], + declarations: [TiDateRangeComponent] +}) +export class TiDateRangeModule {} +export { TiDateRangeComponent } from './TiDateRangeComponent'; diff --git a/src/daterange/lib/src/daterange.html b/src/daterange/lib/src/daterange.html new file mode 100644 index 0000000..9111b91 --- /dev/null +++ b/src/daterange/lib/src/daterange.html @@ -0,0 +1,112 @@ +{{placeholder}} + +
+
+ +
+ +
+
+ + + + + +
+ +
+
+ +
+ +
    +
  • + {{option.label}} +
  • +
+
diff --git a/src/daterange/lib/src/daterange.less b/src/daterange/lib/src/daterange.less new file mode 100644 index 0000000..fd6299d --- /dev/null +++ b/src/daterange/lib/src/daterange.less @@ -0,0 +1,53 @@ +@import '../../../themes/basic/base-all.less'; +@import '../../../themes/basic/compnent-container-border.less'; +@import '../../../date/lib/src/date-common.less'; + +ti-drop.ti3-date-range-picker { + --ti-date-picker-padding-horizon: var(--ti-common-space-4x); + --ti-date-picker-line-width: 1px; + --ti-date-picker-line-color: var(--ti-common-color-line-dividing); +} +:host { + --ti-date-range-input-width: 240px; +} +:host.ti3-date-range-input-container :extend(.ti3-compnent-container-border all) { + width: var(--ti-date-range-input-width); +} + +.ti3-dropdown-container.ti3-date-range-picker { + padding: var(--ti-common-space-3x) var(--ti-date-picker-padding-horizon); + font-size: var(--ti-common-font-size-base); + z-index: 10002; + + &:focus { + outline: 0px; + } +} + +.ti3-date-range-edit { + display: inline-block; + width: calc(var(--ti-common-size-9x) * 7 - 4px); +} +::ng-deep .ti3-date-range-end-edit, +::ng-deep .ti3-date-range-begin-edit { + input[tiText] { + border-color: var(--ti-common-color-line-active) !important; + } +} +.ti3-date-range-customize-container { + float: left; + padding-right: var(--ti-common-space-4x); + margin-right: var(--ti-common-space-4x); + width: var(--ti-common-size-20x); + border-right: var(--ti-date-picker-line-width) var(--ti-common-border-style-solid) var(--ti-date-picker-line-color); +} + +::ng-deep :root .ti3-date-picker-footer-right { + margin-top: var(--ti-common-space-3x); + padding-top: var(--ti-common-space-3x); + border-top: var(--ti-date-picker-line-width) var(--ti-common-border-style-solid) var(--ti-date-picker-line-color); + width: 100%; +} +.ti3-date-picker-footer-btn { + float: right; +} diff --git a/src/datetime/demo/karma.conf.js b/src/datetime/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/datetime/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/datetime/demo/project.json b/src/datetime/demo/project.json new file mode 100644 index 0000000..92beccb --- /dev/null +++ b/src/datetime/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/datetime/demo", + "sourceRoot": "src/datetime/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/datetime", + "index": "src/datetime/demo/src/index.html", + "main": "src/datetime/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/datetime/demo/tsconfig.app.json", + "assets": ["src/datetime/demo/src/favicon.ico", "src/datetime/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "datetime-demo:build:production" + }, + "development": { + "browserTarget": "datetime-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js datetime" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/datetime/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/datetime/demo/tsconfig.spec.json", + "karmaConfig": "src/datetime/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/datetime/demo/src/app/AppComponent.ts b/src/datetime/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/datetime/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/datetime/demo/src/app/AppModule.ts b/src/datetime/demo/src/app/AppModule.ts new file mode 100644 index 0000000..ab92628 --- /dev/null +++ b/src/datetime/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { DatetimeTestModule } from './datetime/DatetimeTestModule'; + +@NgModule({ + imports: [ + DatetimeTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/datetime/demo/src/app/IndexComponent.ts b/src/datetime/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..1919787 --- /dev/null +++ b/src/datetime/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { DatetimeTestModule } from './datetime/DatetimeTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = DatetimeTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/datetime/demo/src/app/app.html b/src/datetime/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/datetime/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/datetime/demo/src/app/datetime/DatetimeCleariconComponent.ts b/src/datetime/demo/src/app/datetime/DatetimeCleariconComponent.ts new file mode 100644 index 0000000..e4eebce --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeCleariconComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './datetime-clearicon.html' +}) +export class DatetimeCleariconComponent { + value: Date = new Date(2024, 8, 2, 12, 2, 35); +} diff --git a/src/datetime/demo/src/app/datetime/DatetimeCustomizeComponent.ts b/src/datetime/demo/src/app/datetime/DatetimeCustomizeComponent.ts new file mode 100644 index 0000000..4fc2609 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeCustomizeComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiDateCustomizeOptions } from '@opentiny/ng'; +@Component({ + templateUrl: './datetime-customize.html' +}) +export class DatetimeCustomizeComponent { + nowTime: Date = new Date(); + nowYear: number = this.nowTime.getFullYear(); + nowMonth: number = this.nowTime.getMonth(); + nowDate: number = this.nowTime.getDate(); + nowHours: number = this.nowTime.getHours(); + nowMinutes: number = this.nowTime.getMinutes(); + nowSeconds: number = this.nowTime.getSeconds(); + value: Date = new Date(2013, 3, 12, 12, 23, 45); + customizeOptions: Array = [ + { + label: '今天', + value: { + begin: this.nowTime + } + }, + { + label: '明天', + value: { + begin: new Date(this.nowYear, this.nowMonth, this.nowDate + 1, this.nowHours, this.nowMinutes, this.nowSeconds) + } + }, + { + label: '一周前', + value: { + begin: new Date(this.nowYear, this.nowMonth, this.nowDate - 6, this.nowHours, this.nowMinutes, this.nowSeconds) + } + } + ]; + myLogs: Array = []; + + onCustomizeOptionClick(model: Date): void { + this.myLogs = [...this.myLogs, `customizeOptionClick() model = ${JSON.stringify(model)}`]; + } +} diff --git a/src/datetime/demo/src/app/datetime/DatetimeDisabledComponent.ts b/src/datetime/demo/src/app/datetime/DatetimeDisabledComponent.ts new file mode 100644 index 0000000..45523ce --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeDisabledComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './datetime-disabled.html' +}) +export class DatetimeDisabledComponent { + value: Date = new Date(2015, 8, 2, 12, 2, 35); + disabled: boolean = true; +} diff --git a/src/datetime/demo/src/app/datetime/DatetimeEventComponent.ts b/src/datetime/demo/src/app/datetime/DatetimeEventComponent.ts new file mode 100644 index 0000000..d90ead3 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeEventComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './datetime-event.html' +}) +export class DatetimeEventComponent { + myLogs: Array = []; + value: Date = null; + + onNgModelChange(value: Date): void { + this.myLogs = [...this.myLogs, `onNgModelChange() value = ${value}`]; + } + + onOkClick(value: Date): void { + this.myLogs = [...this.myLogs, `onOkClick() value = ${value}`]; + } +} diff --git a/src/datetime/demo/src/app/datetime/DatetimeFormatComponent.ts b/src/datetime/demo/src/app/datetime/DatetimeFormatComponent.ts new file mode 100644 index 0000000..e267489 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeFormatComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { TiDatetimeFormat } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetime-format.html' +}) +export class DatetimeFormatComponent { + secondValue: Date = null; + minutevalue: Date = null; + hourValue: Date = null; + secondFormat: TiDatetimeFormat = { + date: 'yyyy.MM.dd', + time: 'HH:mm:ss' + }; + + minuteFormat: TiDatetimeFormat = { + date: 'yyyy.MM.dd', + time: 'HH:mm' + }; + + hourFormat: TiDatetimeFormat = { + date: 'yyyy/MM/dd', + time: 'HH' + }; +} diff --git a/src/datetime/demo/src/app/datetime/DatetimeFormatTestComponent.ts b/src/datetime/demo/src/app/datetime/DatetimeFormatTestComponent.ts new file mode 100644 index 0000000..3f05c8f --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeFormatTestComponent.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { TiDatetimeFormat } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetime-format-test.html' +}) +export class DatetimeFormatTestComponent { + value: Date = new Date(); + value1: Date = new Date(2016, 2, 12, 10, 23, 45); + value2: Date = new Date(2016, 2, 12, 10, 23, 45); + value3: Date = new Date(2016, 2, 12, 10, 23, 45); + format: TiDatetimeFormat = { + date: 'yyyy.MM.dd', + time: 'HH:mm' + }; + + format1: TiDatetimeFormat = { + date: 'yyyy.MM.dd', + time: 'HH' + }; + + format2: TiDatetimeFormat = { + date: 'yyyy/MM/dd', + time: 'HH:mm:ss' + }; + + onFormatClick(): void { + this.format2 = { + date: 'yyyy.MM.dd', + time: 'HH:mm' + }; + } +} diff --git a/src/datetime/demo/src/app/datetime/DatetimeMaxComponent.ts b/src/datetime/demo/src/app/datetime/DatetimeMaxComponent.ts new file mode 100644 index 0000000..34ca047 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeMaxComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './datetime-max.html' +}) +export class DatetimeMaxComponent { + value: Date = new Date(2016, 2, 12, 10, 23, 45); + max: string = ''; + + value1: Date = new Date(2013, 2, 12, 10, 23, 45); + max1: Date = new Date(2010, 6, 30); + + value2: Date = new Date(2013, 2, 12, 10, 23, 45); + max2: Date = new Date(2016, 6, 30); + + onMaxClick(): void { + this.max2 = new Date(2019, 6, 30); + } + onMaxClick2(): void { + this.max2 = undefined; + } +} diff --git a/src/datetime/demo/src/app/datetime/DatetimeMaxminComponent.ts b/src/datetime/demo/src/app/datetime/DatetimeMaxminComponent.ts new file mode 100644 index 0000000..2bdb255 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeMaxminComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './datetime-maxmin.html' +}) +export class DatetimeMaxminComponent { + max: Date = new Date(2023, 1, 14); + min: Date = new Date(2021, 11, 25, 10, 12, 12); + value1: Date = null; + value2: Date = null; + value3: Date = null; +} diff --git a/src/datetime/demo/src/app/datetime/DatetimeMaxminTestComponent.ts b/src/datetime/demo/src/app/datetime/DatetimeMaxminTestComponent.ts new file mode 100644 index 0000000..73570c2 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeMaxminTestComponent.ts @@ -0,0 +1,60 @@ +import { Component } from '@angular/core'; +import { TiDatetimeFormat, TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetime-maxmin-test.html' +}) +export class DatetimeMaxminTestComponent { + nowTime: Date = new Date(); + delayTime: number = 10 * 60 * 1000; // 10分钟 + min: Date = new Date(); + max: Date = new Date(); + min1: Date = new Date(); + max1: Date = new Date(); + min2: Date = new Date(); + max2: Date = new Date(); + value: Date; + value1: Date; + value2: Date; + value3: Date; + format: TiDatetimeFormat; + + onMaxClick(): void { + this.max = new Date(this.nowTime.getTime() - this.delayTime); + } + + onMinClick(): void { + this.min = new Date(this.nowTime.getTime() + this.delayTime); + } + + changeFormat(): void { + this.format = { + date: 'yyyy.MM.dd', + time: 'HH' + }; + } + + changeFormat1(): void { + this.format = { + date: 'yyyy.MM.dd', + time: 'HH:mm' + }; + } + + changeFormat2(): void { + this.format = { + date: 'yyyy.MM.dd', + time: 'HH:mm:ss' + }; + } + + onMaxAndMinClick(): void { + this.max1 = new Date(this.nowTime.getTime() - this.delayTime); + this.min1 = new Date(this.nowTime.getTime() + this.delayTime); + } + + onMaxAndMinClick1(): void { + this.max2 = new Date(this.nowTime.getTime() + this.delayTime); + this.min2 = new Date(this.nowTime.getTime() - this.delayTime); + } +} diff --git a/src/datetime/demo/src/app/datetime/DatetimeMinComponent.ts b/src/datetime/demo/src/app/datetime/DatetimeMinComponent.ts new file mode 100644 index 0000000..798d84b --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeMinComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './datetime-min.html' +}) +export class DatetimeMinComponent { + value: Date = new Date(2016, 2, 12, 10, 23, 45); + min: string = ''; + + value1: Date = new Date(2013, 2, 12, 10, 23, 45); + min1: Date = new Date(2015, 6, 30); + + value2: Date = new Date(2018, 2, 12, 10, 23, 45); + min2: Date = new Date(2016, 5, 4); + + onMinClick(): void { + this.min2 = new Date(2013, 5, 20); + } + onMinClick2(): void { + this.min2 = undefined; + } +} diff --git a/src/datetime/demo/src/app/datetime/DatetimeNowdatetimeComponent.ts b/src/datetime/demo/src/app/datetime/DatetimeNowdatetimeComponent.ts new file mode 100644 index 0000000..08bca9a --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeNowdatetimeComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './datetime-nowdatetime.html' +}) +export class DatetimeNowdatetimeComponent { + nowDateTime: Date = new Date(2018, 10, 20); + value: Date = null; +} diff --git a/src/datetime/demo/src/app/datetime/DatetimePanelalignComponent.ts b/src/datetime/demo/src/app/datetime/DatetimePanelalignComponent.ts new file mode 100644 index 0000000..af3238b --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimePanelalignComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './datetime-panelalign.html' +}) +export class DatetimePanelalignComponent { + value: Date = new Date(2016, 2, 12, 10, 23, 45); +} diff --git a/src/datetime/demo/src/app/datetime/DatetimeTestModule.ts b/src/datetime/demo/src/app/datetime/DatetimeTestModule.ts new file mode 100644 index 0000000..b3e25e3 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeTestModule.ts @@ -0,0 +1,123 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiDatetimeModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { DatetimeValueComponent } from './DatetimeValueComponent'; +import { DatetimeFormatComponent } from './DatetimeFormatComponent'; +import { DatetimeMaxComponent } from './DatetimeMaxComponent'; +import { DatetimeMinComponent } from './DatetimeMinComponent'; +import { DatetimeDisabledComponent } from './DatetimeDisabledComponent'; +import { DatetimeValidationComponent } from './DatetimeValidationComponent'; +import { DatetimeNowdatetimeComponent } from './DatetimeNowdatetimeComponent'; +import { DatetimeCustomizeComponent } from './DatetimeCustomizeComponent'; +import { DatetimeEventComponent } from './DatetimeEventComponent'; +import { DatetimeValueTestComponent } from './DatetimeValueTestComponent'; +import { DatetimeFormatTestComponent } from './DatetimeFormatTestComponent'; +import { DatetimeMaxminComponent } from './DatetimeMaxminComponent'; +import { DatetimePanelalignComponent } from './DatetimePanelalignComponent'; +import { DatetimeCleariconComponent } from './DatetimeCleariconComponent'; +import { DatetimeMaxminTestComponent } from './DatetimeMaxminTestComponent'; +import { DatetimeTimezoneableComponent } from './DatetimeTimezoneableComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiDatetimeModule, + TiValidationModule, + DemoLogModule, + RouterModule.forChild(DatetimeTestModule.ROUTES) + ], + declarations: [ + DatetimeValueComponent, + DatetimeNowdatetimeComponent, + DatetimeFormatComponent, + DatetimeMaxComponent, + DatetimeMinComponent, + DatetimeEventComponent, + DatetimeDisabledComponent, + DatetimeValidationComponent, + DatetimeCustomizeComponent, + DatetimeValueTestComponent, + DatetimeFormatTestComponent, + DatetimeMaxminComponent, + DatetimePanelalignComponent, + DatetimeCleariconComponent, + DatetimeMaxminTestComponent, + DatetimeTimezoneableComponent + ] +}) +export class DatetimeTestModule { + static readonly LINKS: Array = [{ href: 'components/TiDatetimeComponent.html', label: 'Datetime' }]; + static readonly ROUTES: Routes = [ + { + path: 'datetime/datetime-value', + component: DatetimeValueComponent + }, + { + path: 'datetime/datetime-format', + component: DatetimeFormatComponent + }, + { + path: 'datetime/datetime-max', + component: DatetimeMaxComponent + }, + { + path: 'datetime/datetime-min', + component: DatetimeMinComponent + }, + { + path: 'datetime/datetime-nowdatetime', + component: DatetimeNowdatetimeComponent + }, + { + path: 'datetime/datetime-validation', + component: DatetimeValidationComponent + }, + { + path: 'datetime/datetime-disabled', + component: DatetimeDisabledComponent + }, + { + path: 'datetime/datetime-customize', + component: DatetimeCustomizeComponent + }, + { + path: 'datetime/datetime-event', + component: DatetimeEventComponent + }, + { + path: 'datetime/datetime-value-test', + component: DatetimeValueTestComponent + }, + { + path: 'datetime/datetime-format-test', + component: DatetimeFormatTestComponent + }, + { + path: 'datetime/datetime-maxmin', + component: DatetimeMaxminComponent + }, + { + path: 'datetime/datetime-panelalign', + component: DatetimePanelalignComponent + }, + { + path: 'datetime/datetime-clearicon', + component: DatetimeCleariconComponent + }, + { + path: 'datetime/datetime-timezoneable', + component: DatetimeTimezoneableComponent + }, + { + path: 'datetime/datetime-maxmin-test', + component: DatetimeMaxminTestComponent + } + ]; +} diff --git a/src/datetime/demo/src/app/datetime/DatetimeTimezoneableComponent.ts b/src/datetime/demo/src/app/datetime/DatetimeTimezoneableComponent.ts new file mode 100644 index 0000000..8b59eae --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeTimezoneableComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TiDateCustomizeOptions } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetime-timezoneable.html' +}) +export class DatetimeTimezoneableComponent { + nowTime: Date = new Date(); + nowYear: number = this.nowTime.getFullYear(); + nowMonth: number = this.nowTime.getMonth(); + nowDate: number = this.nowTime.getDate(); + nowHours: number = this.nowTime.getHours(); + nowMinutes: number = this.nowTime.getMinutes(); + nowSeconds: number = this.nowTime.getSeconds(); + value: Date = null; + value1: Date = new Date(2021, 4, 2, 1, 2, 12); + value2: Date = null; + timeZone: string; + timeZone1: string = 'UTC/GMT'; + timeZone2: string; + customizeOptions: Array = [ + { + label: '今天', + value: { + begin: this.nowTime + } + }, + { + label: '明天', + value: { + begin: new Date(this.nowYear, this.nowMonth, this.nowDate + 1, this.nowHours, this.nowMinutes, this.nowSeconds) + } + }, + { + label: '一周前', + value: { + begin: new Date(this.nowYear, this.nowMonth, this.nowDate - 6, this.nowHours, this.nowMinutes, this.nowSeconds) + } + } + ]; + + getCurrentTimeZone(timeZone: string): void { + this.timeZone = timeZone; + } + + getCurrentTimeZone2(timeZone) { + this.timeZone2 = timeZone; + } +} diff --git a/src/datetime/demo/src/app/datetime/DatetimeValidationComponent.ts b/src/datetime/demo/src/app/datetime/DatetimeValidationComponent.ts new file mode 100644 index 0000000..a68aaf8 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeValidationComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetime-validation.html' +}) +export class DatetimeValidationComponent { + value: Date = new Date(2016, 2, 12, 10, 23, 45); + validation: TiValidationConfig = { + tipPosition: 'top' + }; +} diff --git a/src/datetime/demo/src/app/datetime/DatetimeValueComponent.ts b/src/datetime/demo/src/app/datetime/DatetimeValueComponent.ts new file mode 100644 index 0000000..a44c603 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeValueComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './datetime-value.html' +}) +export class DatetimeValueComponent { + value1: Date = null; + value2: Date = new Date(); +} diff --git a/src/datetime/demo/src/app/datetime/DatetimeValueTestComponent.ts b/src/datetime/demo/src/app/datetime/DatetimeValueTestComponent.ts new file mode 100644 index 0000000..b61b983 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/DatetimeValueTestComponent.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { TiDatetimeFormat } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetime-value-test.html' +}) +export class DatetimeValueTestComponent { + id: string = 'datetime'; + value: string = ''; + value1: Date = null; + value2: Date = new Date(2013, 2, 12, 10, 23, 45); + value3: Date = new Date(2020, 10, 30, 2, 15, 12); + value4: Date = new Date(2015, 3, 20); + + format: TiDatetimeFormat = { + date: 'yyyy.MM.dd', + time: 'HH:mm:ss' + }; + min: Date = new Date(2013, 2, 12, 10, 23, 45); + max: Date = new Date(2020, 10, 30, 2, 15, 12); + + onValueClick(): void { + this.value4 = new Date(2015, 3, 12); + } + + onValueClick1(): void { + this.value4 = new Date(2050, 5, 20); + } + + ngModelChange(model: Date): void { + console.log(model); + } +} diff --git a/src/datetime/demo/src/app/datetime/datetime-clearicon.html b/src/datetime/demo/src/app/datetime/datetime-clearicon.html new file mode 100644 index 0000000..6196621 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-clearicon.html @@ -0,0 +1 @@ + diff --git a/src/datetime/demo/src/app/datetime/datetime-customize.html b/src/datetime/demo/src/app/datetime/datetime-customize.html new file mode 100644 index 0000000..930f7b3 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-customize.html @@ -0,0 +1,9 @@ + + + diff --git a/src/datetime/demo/src/app/datetime/datetime-disabled.html b/src/datetime/demo/src/app/datetime/datetime-disabled.html new file mode 100644 index 0000000..c027c02 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-disabled.html @@ -0,0 +1 @@ + diff --git a/src/datetime/demo/src/app/datetime/datetime-event.html b/src/datetime/demo/src/app/datetime/datetime-event.html new file mode 100644 index 0000000..8d4fc68 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-event.html @@ -0,0 +1,9 @@ + + + diff --git a/src/datetime/demo/src/app/datetime/datetime-format-test.html b/src/datetime/demo/src/app/datetime/datetime-format-test.html new file mode 100644 index 0000000..399b0f6 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-format-test.html @@ -0,0 +1,23 @@ +

1.描述

+

format接口测试

+

2.示例

+

(2.1)不设置、设置为非plainObject时,使用国际化中datetime格式或者默认值

+ +
+
+
+

(2.2)设置为plainObject(yyyy.MM.dd HH:mm)时

+ +
+
+
+

(2.2)设置为plainObject(yyyy.MM.dd HH)时

+ +
+
+
+

(2.3)动态变更

+ +

+ + diff --git a/src/datetime/demo/src/app/datetime/datetime-format.html b/src/datetime/demo/src/app/datetime/datetime-format.html new file mode 100644 index 0000000..ce66ccb --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-format.html @@ -0,0 +1,5 @@ + +
+ +
+ diff --git a/src/datetime/demo/src/app/datetime/datetime-max.html b/src/datetime/demo/src/app/datetime/datetime-max.html new file mode 100644 index 0000000..417084f --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-max.html @@ -0,0 +1,18 @@ +

1.描述

+

max接口测试

+

2.示例

+

(2.1)最大值设置为字符串类型、非法日期时:最大值为默认值

+ +
+
+
+

(2.2)设置的max小于设置的value时,显示空

+ +
+
+
+

(2.3)动态变更

+ +

+ + diff --git a/src/datetime/demo/src/app/datetime/datetime-maxmin-test.html b/src/datetime/demo/src/app/datetime/datetime-maxmin-test.html new file mode 100644 index 0000000..bc6c39f --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-maxmin-test.html @@ -0,0 +1,35 @@ +

描述

+

min,max临界值测试

+

示例

+

1.max 接口测试,设置max为 new Date()

+
+ +

+当前时间:{{value}} +

+

2.min 接口测试,设置min为 new Date()

+
+ +

+当前时间:{{value1}} +

+

3.max min接口测试,设置max1,min1都为 new Date()

+
+ +

+当前时间:{{value2}} +

+

4.max min接口测试,设置max2,min2都为 new Date()

+
+ +

+当前时间:{{value3}} +

+切换时间格式: + + + + + + + diff --git a/src/datetime/demo/src/app/datetime/datetime-maxmin.html b/src/datetime/demo/src/app/datetime/datetime-maxmin.html new file mode 100644 index 0000000..21846fb --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-maxmin.html @@ -0,0 +1,5 @@ + +
+ +
+ diff --git a/src/datetime/demo/src/app/datetime/datetime-min.html b/src/datetime/demo/src/app/datetime/datetime-min.html new file mode 100644 index 0000000..0ba2ec4 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-min.html @@ -0,0 +1,20 @@ +

1.描述

+

min接口测试

+

2.示例

+

(2.1)最小值设置为字符串类型、非法日期时:最小值为默认值

+ +
+
+
+

(2.2)设置的min大于设置的value时,显示空

+ +
+
+
+

(2.3)动态变更

+ +

+最小值: {{ min2 | date: "yyyy-MM-dd HH:mm:ss"}} +
+ + diff --git a/src/datetime/demo/src/app/datetime/datetime-nowdatetime.html b/src/datetime/demo/src/app/datetime/datetime-nowdatetime.html new file mode 100644 index 0000000..b952d48 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-nowdatetime.html @@ -0,0 +1 @@ + diff --git a/src/datetime/demo/src/app/datetime/datetime-panelalign.html b/src/datetime/demo/src/app/datetime/datetime-panelalign.html new file mode 100644 index 0000000..67943c2 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-panelalign.html @@ -0,0 +1 @@ + diff --git a/src/datetime/demo/src/app/datetime/datetime-timezoneable.html b/src/datetime/demo/src/app/datetime/datetime-timezoneable.html new file mode 100644 index 0000000..b34ae11 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-timezoneable.html @@ -0,0 +1,33 @@ + + +
+
Current timeZone: {{ timeZone }}
+
Current value: {{ value | json }}
+
+
+ + +
+
Current timeZone: {{ timeZone1 }}
+
Current value: {{ value1 | json }}
+
+
+ + +
+
Current timeZone: {{ timeZone2 }}
+
Current value: {{ value2 | json }}
+
diff --git a/src/datetime/demo/src/app/datetime/datetime-validation.html b/src/datetime/demo/src/app/datetime/datetime-validation.html new file mode 100644 index 0000000..e2b1e1f --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-validation.html @@ -0,0 +1 @@ + diff --git a/src/datetime/demo/src/app/datetime/datetime-value-test.html b/src/datetime/demo/src/app/datetime/datetime-value-test.html new file mode 100644 index 0000000..dc87a28 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-value-test.html @@ -0,0 +1,31 @@ +

1.描述

+

value接口测试

+

导入

+

import {{ '{' }} TiDatetimeModule {{ '}' }} from '@opentiny/ng';

+

2.示例

+

(2.1)设置为非Date类型和非null时,显示空

+ +
+
+
+

(2.2)设置为null时,显示空白

+ +
+
+
+

(2.3)设置为最小日期时,正常显示;

+ +
+
+
+

(2.4)设置为最大日期时,正常显示

+ +
+
+
+

(2.5)设置为中间日期时,正常显示

+ + +
+ + diff --git a/src/datetime/demo/src/app/datetime/datetime-value.html b/src/datetime/demo/src/app/datetime/datetime-value.html new file mode 100644 index 0000000..1320fa3 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/datetime-value.html @@ -0,0 +1,3 @@ + +
+ diff --git a/src/datetime/demo/src/app/datetime/webdoc/datetime-demos.js b/src/datetime/demo/src/app/datetime/webdoc/datetime-demos.js new file mode 100644 index 0000000..af457c1 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/webdoc/datetime-demos.js @@ -0,0 +1,157 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'datetime-value', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': '

Datetime 组件的最简用法。

', + 'en-US': '' + } + }, + { + demoId: 'datetime-format', + name: { + 'zh-CN': '日期时间格式', + 'en-US': 'format' + }, + desc: { + 'zh-CN': '

通过属性format配置日期时间显示格式。

', + 'en-US': '' + }, + apis: ['TiDatetimeComponent.properties.format', 'TiDatetimeFormat.properties.date', 'TiDatetimeFormat.properties.time'] + }, + { + demoId: 'datetime-maxmin', + name: { + 'zh-CN': '预设范围', + 'en-US': 'maxmin' + }, + desc: { + 'zh-CN': '

通过属性max配置配置可选择日期时间的最大值;通过属性min配置可选择日期时间的最小值。

', + 'en-US': '' + }, + apis: ['TiDatetimeComponent.properties.max', 'TiDatetimeComponent.properties.min'] + }, + { + demoId: 'datetime-panelalign', + name: { + 'zh-CN': '下拉面板对齐方向', + 'en-US': 'panelalign' + }, + desc: { + 'zh-CN': '

通过属性panelAlign配置下拉面板对齐方向,包括leftright

', + 'en-US': '' + }, + apis: ['TiDatetimeComponent.properties.panelAlign'] + }, + { + demoId: 'datetime-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled' + }, + desc: { + 'zh-CN': '

通过属性disabled配置是否为禁用状态。', + 'en-US': '' + }, + apis: ['TiDatetimeComponent.properties.disabled'] + }, + { + demoId: 'datetime-clearicon', + name: { + 'zh-CN': '清除功能', + 'en-US': 'clearIcon' + }, + desc: { + 'zh-CN': '

通过属性clearIcon配置是否显示清除的叉号图标。

', + 'en-US': '' + }, + apis: ['TiDatetimeComponent.properties.clearIcon'] + }, + { + demoId: 'datetime-customize', + name: { + 'zh-CN': '自定义下拉面板内容', + 'en-US': 'customize' + }, + desc: { + 'zh-CN': + '

通过属性customizeOptions自定义下拉面板左侧内容,设置可快捷选择的常用日期时间;当点击自定义下拉面板左侧内容时触发customizeOptionClick事件,传递出去的参数为选中的日期时间。

', + 'en-US': '' + }, + apis: [ + 'TiDatetimeComponent.properties.customizeOptions', + 'TiDateCustomizeOptions.properties.label', + 'TiDateCustomizeOptions.properties.value', + 'TiDateValue.properties.begin' + ] + }, + { + demoId: 'datetime-nowdatetime', + name: { + 'zh-CN': '自定义当前时间', + 'en-US': 'nowDateTime' + }, + desc: { + 'zh-CN': '

通过属性nowDateTime自定义当前日期时间,可用于矫正实际日期时间和计算机系统日期时间的偏差。

', + 'en-US': '' + }, + apis: ['TiDatetimeComponent.properties.nowDateTime'] + }, + + { + demoId: 'datetime-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event' + }, + desc: { + 'zh-CN': + '

当日期时间框内值改变的时候触发ngModelChange事件;当点击面板上的确认按钮时触发okClick事件,传递出去的参数为选中的日期时间。

', + 'en-US': '' + }, + apis: ['TiDatetimeComponent.events.okClick'] + }, + { + demoId: 'datetime-validation', + name: { + 'zh-CN': '校验', + 'en-US': 'datetime validation' + }, + desc: { + 'zh-CN': '

通过指令tiValidation实现校验。

', + 'en-US': '' + } + }, + { + demoId: 'datetime-timezoneable', + name: { + 'zh-CN': '时区选择', + 'en-US': 'timeZoneable' + }, + desc: { + 'zh-CN': + '

通过属性timeZoneable配置是否显示时区选择框,支持时区选择;通过属性timeZone配置时区选择框的当前选中值,包括本地时区、UTC/GMT;当时区选择框的选中值变化时,点击日期时间面板确定按钮或自定义下拉面板左侧内容后触发timeZoneChange事件,传递出去的参数为当前时区。

', + 'en-US': '' + }, + apis: [ + 'TiDatetimeComponent.properties.timeZoneable', + 'TiDatetimeComponent.properties.timeZone', + 'TiDatetimeComponent.events.timeZoneChange' + ] + } + ], + ignoreApis: [ + 'TiDatetimeComponent.properties.disabledDays', + 'TiDatetimeComponent.properties.isBeginFixed', + 'TiDatetimeComponent.properties.isEndFixed', + 'TiDatetimeComponent.events.dayClick', + 'TiDateValue.properties.end', + 'TiDateValue.properties.timeZone' + ] +}; diff --git a/src/datetime/demo/src/app/datetime/webdoc/datetime.cn.md b/src/datetime/demo/src/app/datetime/webdoc/datetime.cn.md new file mode 100644 index 0000000..8267e49 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/webdoc/datetime.cn.md @@ -0,0 +1,30 @@ +--- +title: Datetime 日期时间 +--- +# Datetime 日期时间 + +
+ +选择或输入日期时间的组件。   + +```typescript +import { TiDatetimeModule } from '@opentiny/ng'; +``` + +如需使用校验功能,请导入。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
+ +
+ +选择或输入日期时间的组件。   + +```typescript +import { TiDatetimeModule } from '@opentiny/ng'; +``` + +
diff --git a/src/datetime/demo/src/app/datetime/webdoc/datetime.en.md b/src/datetime/demo/src/app/datetime/webdoc/datetime.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/datetime/demo/src/app/datetime/webdoc/datetime.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/datetime/demo/src/favicon.ico b/src/datetime/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/datetime/demo/src/index.html b/src/datetime/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/datetime/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/datetime/demo/src/main.ts b/src/datetime/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/datetime/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/datetime/demo/test.ts b/src/datetime/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/datetime/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/datetime/demo/tsconfig.app.json b/src/datetime/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/datetime/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/datetime/demo/tsconfig.spec.json b/src/datetime/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/datetime/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/datetime/lib/index.ts b/src/datetime/lib/index.ts new file mode 100644 index 0000000..a3b7185 --- /dev/null +++ b/src/datetime/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiDatetimeModule'; +export { TiDateCustomizeOptions, TiDatetimeFormat } from '@opentiny/ng-datebase'; diff --git a/src/datetime/lib/ng-package.json b/src/datetime/lib/ng-package.json new file mode 100644 index 0000000..b6c465d --- /dev/null +++ b/src/datetime/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/datetime", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/datetime/lib/package.json b/src/datetime/lib/package.json new file mode 100644 index 0000000..11b8f9a --- /dev/null +++ b/src/datetime/lib/package.json @@ -0,0 +1,22 @@ +{ + "name": "@opentiny/ng-datetime", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-datebase": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@opentiny/ng-datepanel": "~1.0.0-beta.0", + "@opentiny/ng-dateedit": "~1.0.0-beta.0", + "@opentiny/ng-datedominator": "~1.0.0-beta.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-time": "~1.0.0-beta.0", + "@opentiny/ng-select": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/datetime/lib/project.json b/src/datetime/lib/project.json new file mode 100644 index 0000000..f032296 --- /dev/null +++ b/src/datetime/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/datetime/lib", + "sourceRoot": "src/datetime/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/datetime"], + "options": { + "project": "src/datetime/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/datetime"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js datetime" + }, + { + "command": "ng default-build datetime" + }, + { + "command": "node build/clear-default-theme.js datetime" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/datetime && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build datetime && ng pack datetime && node build/publish.js datetime --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/datetime/lib/src/TiDatetimeComponent.ts b/src/datetime/lib/src/TiDatetimeComponent.ts new file mode 100644 index 0000000..a18f8e7 --- /dev/null +++ b/src/datetime/lib/src/TiDatetimeComponent.ts @@ -0,0 +1,469 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, SimpleChanges, ChangeDetectionStrategy, Input, ViewChild, Output, EventEmitter } from '@angular/core'; +import { TiLocaleFormat } from '@opentiny/ng-locale'; +import { TiDateUtil, TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiDateBaseComponent, TiDatetimeFormat, TimeOptions, TiDateValue } from '@opentiny/ng-datebase'; +import packageInfo from '../package.json'; +import { TiSelectComponent } from '@opentiny/ng-select'; +/** + * Datetime日期时间组件 + * + * Datetime组件提供了一种方便的显示和设置日期时间的方式。 + * + */ +@Component({ + selector: 'ti-datetime', + templateUrl: './datetime.html', + styleUrls: ['./datetime.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-datetime-input-container]': 'true', + '(blur)': 'hidePanel()' + }, + providers: [TiFormComponent.getValueAccessor(TiDatetimeComponent)] +}) +export class TiDatetimeComponent extends TiDateBaseComponent { + /** + * 是否显示时区选择框(选项:‘本地时区’、‘UTC/GMT’) + */ + @Input() timeZoneable: boolean = false; + /** + * 时区选择框选中项,默认值为‘本地时区’ + */ + @Input() timeZone: string = this.dateWords.localZone; + /** + * 当时区选择框的选中值变化时,点击日期时间面板确定按钮后触发的回调,参数:当前时区 + */ + @Output() readonly timeZoneChange: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + @ViewChild(TiSelectComponent, { static: false }) selectComRef: TiSelectComponent; + /** + * @ignore + * 配置time组件的接口 + */ + public beginTime: TimeOptions = {}; + /** + * @ignore + * 日期显示格式: Datetime组件的format为DatetimeFormat类型 + */ + public format: TiDatetimeFormat; + /** + * @ignore + * 标记date/datetime的类型 + */ + public isDatetime: boolean = true; + /** + * @ignore + * 用于标记是不是range + */ + public isRange: boolean = false; + /** + * @ignore + * 旧时间值 + */ + public oldValue: any = ''; + private time: string = ''; // 日期框中的时间值 + /** + * @ignore + * 时区选择下拉建议项 + */ + public timeZoneOptions: Array = [{ label: this.dateWords.utcZone }, { label: this.dateWords.localZone }]; + private lastTimeZone: string; // 记录上次选择的时区 + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + super.ngOnInit(); + this.oldFormat = this.format; + this.setPlacehoder(); + + // 设置初始时区选择的选中项 + if (this.timeZoneable) { + this.lastTimeZone = this.timeZone; + } + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + + if (this.timeZoneable && this.selectComRef) { + // 将select dominator设置为可聚焦元素 + this.setFocusableElems( + [this.dominatorCom.nativeElement] + .concat(this.dropCom.nativeElement) + .concat(this.focusElementsArr) + .concat(this.selectComRef.dominatorCom.getFocusableElems()) + ); + } + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + + // 验证最大值最小值,为了处理最大值和最小值从合法日期变为undefined 的情景 + if ((changes['max'] && !changes['max'].firstChange) || (changes['min'] && !changes['min'].firstChange)) { + this.validateMinAndMax(this.config, this.isDatetime); + } + } + + ngDoCheck(): void { + // 监听format的变化 + this.setFormat(); + } + + /** + * @ignore + * model值变化时的回调 + */ + ngOnModelChange(): void { + if (!TiDateUtil.isDate(this.model)) { + this.inputValue = ''; + + return; + } + this.formatValue(); + this.setOkBtnState(); + this.setPickerDate(); + } + /** + * @ignore + * 确认按钮的点击事件处理 + */ + onOkClick(): void { + if (this.inValidValue) { + return; + } + + if (!TiDateUtil.isDate(this.datePanel['value']['begin'])) { + this.dropCom.hide(); + + return; + } + + // 获取时间的字符串 + const dateStr: string = TiDateUtil.getDateStr(this.datePanel['value']['begin']); + // 空格误删 + const newValue: Date = TiDateUtil.getLocalDate( + `${dateStr} ${TiDateUtil.addColon(this.beginTime['value'])}`, + this.timeZoneable, + this.timeZone + ); + this.recordEmitCurrentTimeZone(); + + // 点击确认按钮对比新旧值:相等不做处理,不相等将新值赋给model + if (!TiDateUtil.isDatetimeEqual(this.model, newValue)) { + this.model = newValue; + } + + this.hideDrop(); + + this.okClick.emit(this.model); + } + + /** + * @ignore + * 左侧自定义时间文本点击事件 + */ + public customizeTimeClickFn(val: TiDateValue): void { + this.recordEmitCurrentTimeZone(); + this.model = TiDateUtil.getLocalDate(`${val}`, this.timeZoneable, this.timeZone); + this.dropCom.hide(); + this.customizeOptionClick.emit(this.model); + } + + // 下拉面板选择不同时区后,通过元素点击事件关闭,记录并发出当前时区 + private recordEmitCurrentTimeZone(): void { + if (this.timeZoneable && this.lastTimeZone !== this.timeZone) { + this.lastTimeZone = this.timeZone; + this.timeZoneChange.emit(this.timeZone); + } + } + + /** + * @ignore + * 设置下拉面板中datePanel组件接口 + */ + private setDatePanel(): void { + this.datePanel = { + picker: 'day', + max: this.max, + min: this.min, + format: this.format, + nowDateTime: this.nowDateTime, + value: { + begin: TiDateUtil.getUtcDate(this.model, this.timeZoneable, this.timeZone), + end: null + }, + select: (): void => { + // 点击日期面板值,校验时间的最大值最小值 + // 原因:当选择的日期是最小或者最大时,需要校验当前的时间是否合法 + // 设定时间范围 + const minFormatTime: string = TiLocaleFormat.formatTime('2020/10/30 00:00:00', this.format.time); + const maxFormatTime: string = TiLocaleFormat.formatTime('2020/10/30 23:59:59', this.format.time); + if (TiDateUtil.isDateEqual(this.datePanel['value']['begin'], this.max)) { + this.beginTime['max'] = TiLocaleFormat.formatTime(this.max, this.format.time); + this.beginTime['min'] = minFormatTime; + } else if (TiDateUtil.isDateEqual(this.datePanel['value']['begin'], this.min)) { + this.beginTime['max'] = maxFormatTime; + this.beginTime['min'] = TiLocaleFormat.formatTime(this.min, this.format.time); + } else { + this.beginTime['min'] = minFormatTime; + this.beginTime['max'] = maxFormatTime; + } + this.setBeginTime(); + this.setOkBtnState(); + }, + selectTimeFn: (obj: any): void => { + this.setTimeFn(obj, this.beginTime); + this.setOkBtnState(); + } + }; + } + + /** + * @ignore + * 日期框值变化且是合法值时触发 + */ + onInputChange(time: string): void { + this.time = time; + } + + private setBeginTime(): void { + if (this.timeEditDisabled) { + this.beginTime['value'] = TiLocaleFormat.formatTime( + TiDateUtil.getUtcDate(new Date(), this.timeZoneable, this.timeZone), + this.format.time + ); + } + + // 日期框的时间值 + if (this.time) { + this.handleTimeValue(this.time); + this.time = ''; + } + + // 1.获取时间的字符串 + const dateStr: string = TiDateUtil.getDateStr(this.datePanel['value']['begin']); + // 空格误删 + const datetimeStr: Date = new Date(`${dateStr} ${TiDateUtil.addColon(this.beginTime['value'])}`); + // 最大值校验 + if (TiDateUtil.isBigger(datetimeStr, this.max)) { + this.beginTime['value'] = TiLocaleFormat.formatTime(this.max, this.format.time); + } + // 最小值校验 + if (TiDateUtil.isSmaller(datetimeStr, this.min)) { + this.beginTime['value'] = TiLocaleFormat.formatTime(this.min, this.format.time); + } + this.oldValue = this.beginTime['value']; + + this.timeEditDisabled = false; + } + /** + * @ignore + * @param event + * @param val + */ + onKeydownFn(event: KeyboardEvent, val: any): void { + if (event.keyCode === TiKeymap.KEY_ENTER || event.keyCode === TiKeymap.KEY_NUMPAD_ENTER) { + if (!TiDateUtil.isDate(val)) { + return; + } + this.setBeginTime(); + this.setOkBtnState(); + } + } + + /** + * @ignore + */ + public editBlur(val: any): void { + if (val !== null) { + this.setBeginTime(); + } else { + setTimeout(() => { + // 添加setTimeout原因:确保点击时间输入框时拿到的focusedPosition为beginTime + // 点击非禁用的确认按钮,需保证焦点在面板内部,否则点击document空白处无法关闭面板;如果焦点在select组件上,不做焦点转移,否则select下拉选项框无法收起 + if (this.focusedPosition === 'begin' && document.activeElement !== this.selectComRef?.dominatorCom.nativeElement) { + this.dropCom.nativeElement.focus(); + } + }, 0); + } + this.setOkBtnState(); + } + // 设置下拉面板中time组件接口 + private setTime(): void { + // value接口设置 + this.setTimeValue(); + + // maxValue接口设置 + this.setTimeMaxValue(); + + // minValue接口设置 + this.setTimeMinValue(); + } + + private setTimeValue(): void { + this.beginTime['value'] = TiDateUtil.isDate(this.model) + ? TiLocaleFormat.formatTime(TiDateUtil.getUtcDate(this.model, this.timeZoneable, this.timeZone), this.format.time) + : ''; + } + + private setTimeMaxValue(): void { + if (!TiDateUtil.isDate(this.model)) { + return; + } + + this.beginTime['max'] = TiDateUtil.isDateEqual(this.model, this.max) + ? TiLocaleFormat.formatTime(this.max, this.format.time) + : '23:59:59'; + } + + private setTimeMinValue(): void { + if (!TiDateUtil.isDate(this.model)) { + return; + } + + this.beginTime['min'] = TiDateUtil.isDateEqual(this.model, this.min) + ? TiLocaleFormat.formatTime(this.min, this.format.time) + : '00:00:00'; + } + + /** + * @ignore + */ + public isValidValue(value: Date): boolean { + if (TiDateUtil.isDate(value) && TiDateUtil.isBetweenMaxAndmin(value, this.min, this.max)) { + return true; + } + + return false; + } + + /** + * @ignore + */ + public setPickerDate(): void { + // 设置时区选择的选中项 + if (this.timeZoneable && this.timeZone !== this.lastTimeZone) { + this.timeZone = this.lastTimeZone; + } + + // 设置下拉面板中datePanel组件接口 + this.setDatePanel(); + + // 设置下拉面板中time组件接口 + this.setTime(); + + this.setOkBtnState(); + + this.timeEditDisabled = TiDateUtil.isDate(this.model) ? false : true; + + if (this.timeEditDisabled) { + this.selectTime = false; + } + } + + /** + * @ignore + * 设置确认按钮的状态 + */ + public setOkBtnState(): void { + // 输入中文冒号之后,及时转换为英文冒号 + const inputvalue: string = this.beginTime.value; + if (!Util.isUndefined(inputvalue)) { + this.beginTime.value = inputvalue.replace(':', ':'); + } + const date: Date = this.datePanel.value.begin; + if (!TiDateUtil.isDate(date)) { + this.inValidValue = true; + } else { + const dateTime: Date = new Date(`${TiDateUtil.getDateStr(date)} ${TiDateUtil.addColon(this.beginTime['value'])}`); + this.inValidValue = !TiDateUtil.isBetweenMaxAndmin(dateTime, this.min, this.max); + } + if (this.buttonComs) { + this.setAttr(this.buttonComs.last.nativeElement, 'disabled', this.inValidValue); + this.setInputStyle(this.inValidValue); + } + } + + /** + * @ignore + * 将value转换为format接口格式的字符串 + */ + public formatValue(): void { + if (this.model === null) { + this.inputValue = ''; + this.oldValue = ''; + } else { + // 空格误删 + const format: string = `${this.format.date} ${TiDateUtil.addAmPm(this.format.time)}`; + this.inputValue = TiLocaleFormat.formatDateTime(TiDateUtil.getUtcDate(this.model, this.timeZoneable, this.timeZone), format); + this.oldValue = TiLocaleFormat.formatTime(TiDateUtil.getUtcDate(this.model, this.timeZoneable, this.timeZone), this.format.time); + } + } + /** + * @ignore + * 时间框失焦事件 + */ + public timeBlur(): void { + this.handleTimeValue(); + } + /** + * @ignore + * 时间框enter事件 + */ + public timeKeydown(event: KeyboardEvent): void { + if (event.keyCode === TiKeymap.KEY_ENTER || event.keyCode === TiKeymap.KEY_NUMPAD_ENTER) { + this.handleTimeValue(); + } + } + /** + * @ignore + * time时间框输入内容处理 + */ + public handleTimeValue(value?: string): void { + // 输入区间是否存在小数点,非整数 + let isNotInteger: boolean = false; + this.beginTime = JSON.parse(JSON.stringify(this.beginTime)); + const timeValue: string = value ? value : this.beginTime.value; + const timeArr: Array = timeValue.split(TiDateUtil.COLON_REGEXP); + + timeArr.forEach((item: string, index: number): void => { + if (item.length === 1) { + timeArr[index] = '0' + item; + } + if (item && parseInt(item, 10) !== Number(item)) { + isNotInteger = true; + } + }); + + const processedValue: string = timeArr.join(':'); + let datetime: Date; + let dateVal: Date; + if (this.datePanel['value']['begin']) { + datetime = new Date(`2020/10/30 ${TiDateUtil.addColon(processedValue)}`); + dateVal = this.setDateTime(timeArr); + } + if (String(datetime) === 'Invalid Date' || String(dateVal) === 'Invalid Date' || timeArr.length > 3 || isNotInteger) { + this.beginTime.value = this.oldValue; + this.setOkBtnState(); + + return; + } + this.beginTime.value = TiLocaleFormat.formatTime(dateVal, this.format.time); + this.oldValue = this.beginTime.value; + this.setOkBtnState(); + } +} diff --git a/src/datetime/lib/src/TiDatetimeModule.ts b/src/datetime/lib/src/TiDatetimeModule.ts new file mode 100644 index 0000000..2c5588f --- /dev/null +++ b/src/datetime/lib/src/TiDatetimeModule.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { TiDropModule } from '@opentiny/ng-drop'; + +import { TiLocaleModule } from '@opentiny/ng-locale'; +import { TiDatetimeComponent } from './TiDatetimeComponent'; +import { TiButtonModule } from '@opentiny/ng-button'; +import { TiDatePanelModule } from '@opentiny/ng-datepanel'; +import { TiDateEditModule } from '@opentiny/ng-dateedit'; +import { TiDateDominatorModule } from '@opentiny/ng-datedominator'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiTimeModule } from '@opentiny/ng-time'; +import { TiSelectModule } from '@opentiny/ng-select'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiLocaleModule, + TiDropModule, + TiButtonModule, + TiDatePanelModule, + TiDateEditModule, + TiTextModule, + TiTimeModule, + TiDateDominatorModule, + TiSelectModule + ], + exports: [TiDatetimeComponent], + declarations: [TiDatetimeComponent] +}) +export class TiDatetimeModule {} +export { TiDatetimeComponent } from './TiDatetimeComponent'; diff --git a/src/datetime/lib/src/datetime.html b/src/datetime/lib/src/datetime.html new file mode 100644 index 0000000..bacc1ca --- /dev/null +++ b/src/datetime/lib/src/datetime.html @@ -0,0 +1,133 @@ +{{ placeholder }} + + + +
+
+ +
+ +
+ + + + + +
+
+ +
+ + +
    +
  • + {{option.label}} +
  • +
+
diff --git a/src/datetime/lib/src/datetime.less b/src/datetime/lib/src/datetime.less new file mode 100644 index 0000000..2f0af01 --- /dev/null +++ b/src/datetime/lib/src/datetime.less @@ -0,0 +1,104 @@ +@import '../../../themes/basic/base-all.less'; +@import '../../../themes/basic/compnent-container-border.less'; +@import '../../../date/lib/src/date-common.less'; + +ti-drop.ti3-datetime-picker { + --ti-date-picker-padding-horizon: var(--ti-common-space-4x); + --ti-date-picker-line-width: 1px; + --ti-date-picker-line-color: var(--ti-common-color-line-dividing); +} + +:host.ti3-datetime-input-container :extend(.ti3-compnent-container-border all) { + width: var(--ti-common-size-50x); +} + +.ti3-dropdown-container.ti3-datetime-picker { + padding: var(--ti-common-space-3x) var(--ti-date-picker-padding-horizon); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + z-index: 10002; + + &:focus { + outline: 0px; + } +} + +.ti3-datetime-picker-footer { + padding-bottom: var(--ti-common-space-10); + width: 100%; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + clear: both; + .box-sizing(border-box); + .user-select(); + .clearfix(); +} + +.ti3-date-picker-footer-right { + float: right; +} + +.ti3-datetime-btn-picker-footer { + margin-top: var(--ti-common-space-3x); + padding-top: var(--ti-common-space-3x); + width: 100%; + clear: both; + border-top: var(--ti-date-picker-line-width) var(--ti-common-border-style-solid) var(--ti-date-picker-line-color); + .box-sizing(border-box); + .user-select(); + .clearfix(); + + .ti3-datetime-picker-footer-btn { + float: right; + &:last-child { + margin-right: var(--ti-common-space-10); + } + } +} +.ti3-datetime-btn-picker-footer.ti3-datetime-select-time { + margin-top: 0; + border-top: none; +} + +.ti3-date-time-customize-contianer { + float: left; + padding-right: var(--ti-common-space-4x); + width: var(--ti-common-size-20x); + border-right: var(--ti-date-picker-line-width) var(--ti-common-border-style-solid) var(--ti-date-picker-line-color); +} + +.ti3-datetime-dateedit { + display: inline-block; + width: calc((var(--ti-common-size-9x) * 7 - var(--ti-common-space-2x)) / 2 - 2px); + margin-right: var(--ti-common-space-2x); +} +.ti3-datetime-timeedit { + display: inline-block; + width: calc((var(--ti-common-size-9x) * 7 - var(--ti-common-space-2x)) / 2 - 2px); +} +.ti3-datetime-panel-container { + float: right; + padding-left: var(--ti-common-space-4x); +} + +::ng-deep .ti3-datetime-dateedit-input input[tiText] { + border-color: var(--ti-common-color-line-active) !important; +} + +::ng-deep .ti3-datetime-timeedit-input { + border-color: var(--ti-common-color-line-active) !important; +} + +.ti3-datetime-picker-footer-right { + float: right; +} +.ti3-datetime-picker-footer-left { + float: left; + margin-left: calc(var(--ti-common-space-10) * -1); // select组件左边距是10px,设置margin-left是为了与面板其它内容左对齐 + ::ng-deep ti-select { + width: auto; + ti-dominator .ti3-overflow-padding { + padding-right: 0; + } + } +} diff --git a/src/datetimerange/demo/karma.conf.js b/src/datetimerange/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/datetimerange/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/datetimerange/demo/project.json b/src/datetimerange/demo/project.json new file mode 100644 index 0000000..ed8e119 --- /dev/null +++ b/src/datetimerange/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/datetimerange/demo", + "sourceRoot": "src/datetimerange/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/datetimerange", + "index": "src/datetimerange/demo/src/index.html", + "main": "src/datetimerange/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/datetimerange/demo/tsconfig.app.json", + "assets": ["src/datetimerange/demo/src/favicon.ico", "src/datetimerange/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "datetimerange-demo:build:production" + }, + "development": { + "browserTarget": "datetimerange-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js datetimerange" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/datetimerange/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/datetimerange/demo/tsconfig.spec.json", + "karmaConfig": "src/datetimerange/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/datetimerange/demo/src/app/AppComponent.ts b/src/datetimerange/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/datetimerange/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/datetimerange/demo/src/app/AppModule.ts b/src/datetimerange/demo/src/app/AppModule.ts new file mode 100644 index 0000000..5bc8b11 --- /dev/null +++ b/src/datetimerange/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { DatetimeRangeTestModule } from './datetimerange/DatetimeRangeTestModule'; + +@NgModule({ + imports: [ + DatetimeRangeTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/datetimerange/demo/src/app/IndexComponent.ts b/src/datetimerange/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..da4965c --- /dev/null +++ b/src/datetimerange/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { DatetimeRangeTestModule } from './datetimerange/DatetimeRangeTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = DatetimeRangeTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/datetimerange/demo/src/app/app.html b/src/datetimerange/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/datetimerange/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimeRangeTestModule.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimeRangeTestModule.ts new file mode 100644 index 0000000..ea4282d --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimeRangeTestModule.ts @@ -0,0 +1,140 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiDatetimeRangeModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { DatetimerangeValueComponent } from './DatetimerangeValueComponent'; +import { DatetimerangeFormatComponent } from './DatetimerangeFormatComponent'; +import { DatetimerangeMaxComponent } from './DatetimerangeMaxComponent'; +import { DatetimerangeMinComponent } from './DatetimerangeMinComponent'; +import { DatetimerangeDisabledComponent } from './DatetimerangeDisabledComponent'; +import { DatetimerangeValidationComponent } from './DatetimerangeValidationComponent'; +import { DatetimerangeNowdatetimeComponent } from './DatetimerangeNowdatetimeComponent'; +import { DatetimerangeCustomizeComponent } from './DatetimerangeCustomizeComponent'; +import { DatetimerangeManyTestComponent } from './DatetimerangeManyTestComponent'; +import { DatetimerangeEventComponent } from './DatetimerangeEventComponent'; +import { DatetimerangeIsallowbeginequalendComponent } from './DatetimerangeIsallowbeginequalendComponent'; +import { DatetimerangeTimezoneableComponent } from './DatetimerangeTimezoneableComponent'; +import { DatetimerangeValueTestComponent } from './DatetimerangeValueTestComponent'; +import { DatetimerangeFormatTestComponent } from './DatetimerangeFormatTestComponent'; +import { DatetimerangeMaxminComponent } from './DatetimerangeMaxminComponent'; +import { DatetimerangePanelalignComponent } from './DatetimerangePanelalignComponent'; +import { DatetimerangeCleariconComponent } from './DatetimerangeCleariconComponent'; +import { DatetimerangeMaxminTestComponent } from './DatetimerangeMaxminTestComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiValidationModule, + TiDatetimeRangeModule, + DemoLogModule, + RouterModule.forChild(DatetimeRangeTestModule.ROUTES) + ], + declarations: [ + DatetimerangeFormatComponent, + DatetimerangeNowdatetimeComponent, + DatetimerangeValueComponent, + DatetimerangeMaxComponent, + DatetimerangeMinComponent, + DatetimerangeDisabledComponent, + DatetimerangeEventComponent, + DatetimerangeValidationComponent, + DatetimerangeCustomizeComponent, + DatetimerangeManyTestComponent, + DatetimerangeIsallowbeginequalendComponent, + DatetimerangeTimezoneableComponent, + DatetimerangeValueTestComponent, + DatetimerangeFormatTestComponent, + DatetimerangeMaxminComponent, + DatetimerangePanelalignComponent, + DatetimerangeCleariconComponent, + DatetimerangeMaxminTestComponent + ] +}) +export class DatetimeRangeTestModule { + static readonly LINKS: Array = [ + { + href: 'components/TiDatetimeRangeComponent.html', + label: 'DatetimeRange' + } + ]; + static readonly ROUTES: Routes = [ + { + path: 'datetimerange/datetimerange-value', + component: DatetimerangeValueComponent + }, + { + path: 'datetimerange/datetimerange-format', + component: DatetimerangeFormatComponent + }, + { + path: 'datetimerange/datetimerange-max', + component: DatetimerangeMaxComponent + }, + { + path: 'datetimerange/datetimerange-min', + component: DatetimerangeMinComponent + }, + { + path: 'datetimerange/datetimerange-nowdatetime', + component: DatetimerangeNowdatetimeComponent + }, + { + path: 'datetimerange/datetimerange-validation', + component: DatetimerangeValidationComponent + }, + { + path: 'datetimerange/datetimerange-disabled', + component: DatetimerangeDisabledComponent + }, + { + path: 'datetimerange/datetimerange-customize', + component: DatetimerangeCustomizeComponent + }, + { + path: 'datetimerange/datetimerange-isallowbeginequalend', + component: DatetimerangeIsallowbeginequalendComponent + }, + { + path: 'datetimerange/datetimerange-timezoneable', + component: DatetimerangeTimezoneableComponent + }, + { + path: 'datetimerange/datetimerange-event', + component: DatetimerangeEventComponent + }, + { + path: 'datetimerange/datetimerange-many-test', + component: DatetimerangeManyTestComponent + }, + { + path: 'datetimerange/datetimerange-value-test', + component: DatetimerangeValueTestComponent + }, + { + path: 'datetimerange/datetimerange-format-test', + component: DatetimerangeFormatTestComponent + }, + { + path: 'datetimerange/datetimerange-maxmin', + component: DatetimerangeMaxminComponent + }, + { + path: 'datetimerange/datetimerange-panelalign', + component: DatetimerangePanelalignComponent + }, + { + path: 'datetimerange/datetimerange-clearicon', + component: DatetimerangeCleariconComponent + }, + { + path: 'datetimerange/datetimerange-maxmin-test', + component: DatetimerangeMaxminTestComponent + } + ]; +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeCleariconComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeCleariconComponent.ts new file mode 100644 index 0000000..11d79ff --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeCleariconComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-clearicon.html' +}) +export class DatetimerangeCleariconComponent { + value: TiDateValue = { + begin: new Date(2013, 10, 10, 23, 8, 45), + end: new Date(2015, 5, 6, 4, 51, 12) + }; +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeCustomizeComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeCustomizeComponent.ts new file mode 100644 index 0000000..ac179b0 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeCustomizeComponent.ts @@ -0,0 +1,47 @@ +import { Component } from '@angular/core'; +import { TiDateCustomizeOptions, TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-customize.html' +}) +export class DatetimerangeCustomizeComponent { + nowTime: Date = new Date(); + nowYear: number = this.nowTime.getFullYear(); + nowMonth: number = this.nowTime.getMonth(); + nowDate: number = this.nowTime.getDate(); + nowHours: number = this.nowTime.getHours(); + nowMinutes: number = this.nowTime.getMinutes(); + nowSeconds: number = this.nowTime.getSeconds(); + value: TiDateValue = { + begin: new Date(2016, 9, 27, 10, 20, 12), + end: new Date(2018, 5, 6, 15, 12, 12) + }; + customizeOptions: Array = [ + { + label: '最近一周', + value: { + end: this.nowTime, + begin: new Date(this.nowYear, this.nowMonth, this.nowDate - 6, this.nowHours, this.nowMinutes, this.nowSeconds) + } + }, + { + label: '最近一个月', + value: { + end: this.nowTime, + begin: new Date(this.nowYear, this.nowMonth - 1, this.nowDate, this.nowHours, this.nowMinutes, this.nowSeconds) + } + }, + { + label: '最近三个月', + value: { + end: this.nowTime, + begin: new Date(this.nowYear, this.nowMonth - 3, this.nowDate, this.nowHours, this.nowMinutes, this.nowSeconds) + } + } + ]; + myLogs: Array = []; + + onCustomizeOptionClick(model: TiDateValue): void { + this.myLogs = [...this.myLogs, `customizeOptionClick() model = ${JSON.stringify(model)}`]; + } +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeDisabledComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeDisabledComponent.ts new file mode 100644 index 0000000..a1a958c --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeDisabledComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-disabled.html' +}) +export class DatetimerangeDisabledComponent { + value: TiDateValue = { + begin: new Date(2013, 10, 10, 23, 8, 45), + end: new Date(2015, 5, 6, 4, 51, 12) + }; + disabled: boolean = true; +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeEventComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeEventComponent.ts new file mode 100644 index 0000000..3b3bbb5 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeEventComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-event.html' +}) +export class DatetimerangeEventComponent { + value: TiDateValue = null; + myLogs: Array = []; + + onNgModelChange(value: TiDateValue): void { + this.myLogs = [...this.myLogs, `onNgModelChange() value = ${JSON.stringify(value)}`]; + } + + onDayClick(info: any): void { + this.myLogs = [ + ...this.myLogs, + `onDayClick() info = value: ${info.value}, focusedPosition: ${info.focusedPosition}, + beginValue: ${info.beginValue}, endValue: ${info.endValue}` + ]; + } + + onOkClick(value: Date): void { + this.myLogs = [...this.myLogs, `onOkClick() value = ${JSON.stringify(value)}`]; + } +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeFormatComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeFormatComponent.ts new file mode 100644 index 0000000..6856765 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeFormatComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { TiDatetimeFormat, TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-format.html' +}) +export class DatetimerangeFormatComponent { + secondValue: TiDateValue = null; + minutevalue: TiDateValue = null; + hourValue: TiDateValue = null; + secondFormat: TiDatetimeFormat = { + date: 'yyyy/MM/dd', + time: 'HH:mm:ss' + }; + + minuteFormat: TiDatetimeFormat = { + date: 'yyyy.MM.dd', + time: 'HH:mm' + }; + + hourFormat: TiDatetimeFormat = { + date: 'yyyy/MM/dd', + time: 'HH' + }; +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeFormatTestComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeFormatTestComponent.ts new file mode 100644 index 0000000..93e2853 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeFormatTestComponent.ts @@ -0,0 +1,55 @@ +import { Component } from '@angular/core'; +import { TiDatetimeFormat, TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-format-test.html' +}) +export class DatetimerangeFormatTestComponent { + value: TiDateValue = { + begin: new Date(2017, 8, 27, 11, 12, 45), + end: new Date(2018, 5, 6, 4, 51, 12) + }; + + format: TiDatetimeFormat = { + date: 'yyyy/MM/dd', + time: 'HH:mm' + }; + + format1: TiDatetimeFormat = { + date: 'yyyy.MM.dd', + time: 'HH' + }; + + value1: TiDateValue = { + begin: new Date(2013, 10, 10, 23, 8, 45), + end: new Date(2015, 5, 6, 4, 51, 12) + }; + + value2: TiDateValue = { + begin: new Date(2016, 2, 25, 23, 12, 45), + end: new Date(2018, 10, 16, 4, 51, 12) + }; + + value3: TiDateValue = { + begin: new Date(2016, 2, 25, 23, 12, 45), + end: new Date(2018, 10, 16, 4, 51, 12) + }; + + onFormatClick(): void { + this.format1 = { + date: 'yyyy-MM-dd', + time: 'HH:mm:ss' + }; + } + + onFormatClick1(): void { + this.format1 = { + date: 'yyyy.MM.dd', + time: 'HH:mm' + }; + } + + okClick(event: TiDateValue): void { + console.log(event); + } +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeIsallowbeginequalendComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeIsallowbeginequalendComponent.ts new file mode 100644 index 0000000..64f27b1 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeIsallowbeginequalendComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-isallowbeginequalend.html' +}) +export class DatetimerangeIsallowbeginequalendComponent { + value: TiDateValue = null; +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeManyTestComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeManyTestComponent.ts new file mode 100644 index 0000000..03a4159 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeManyTestComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './datetimerange-many-test.html' +}) +export class DatetimerangeManyTestComponent { + items: Array = []; + + ngOnInit(): void { + for (let i: number = 0; i < 100; i++) { + this.items.push({ value: null }); + } + } +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeMaxComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeMaxComponent.ts new file mode 100644 index 0000000..bae447b --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeMaxComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-max.html' +}) +export class DatetimerangeMaxComponent { + nowTime: Date = new Date(); + value: TiDateValue = { + begin: new Date(1994, 2, 8), + end: new Date(2016, 5, 21) + }; + max: Date = null; + + value1: TiDateValue = { + begin: new Date(1992, 2, 21), + end: new Date(2016, 10, 15) + }; + + max1: Date = new Date(2018, 10, 15, 10, 20, 30); + + value2: TiDateValue = { + begin: new Date(1994, 2, 8), + end: new Date(2016, 6, 5) + }; + + value3: TiDateValue = { + begin: new Date(new Date().getTime() - 6 * 60 * 60 * 1000), + end: this.nowTime + }; + max3: Date = this.nowTime; + + max2: Date = new Date(2020, 6, 12, 2, 12, 23); + + value4: TiDateValue = null; + + max4: Date = new Date(2020, 6, 12, 2, 12, 23); + + maxValueClick(): void { + this.max2 = new Date(2030, 2, 15); + } + maxValueClick2(): void { + this.max2 = undefined; + } + + onDayClick(obj: any): void { + if ((obj.focusedPosition === 'end' && obj.endValue === null) || obj.focusedPosition === 'begin') { + this.max4 = new Date(obj.beginValue?.getFullYear(), obj.beginValue?.getMonth(), obj.beginValue?.getDate() + 6); + } + } +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeMaxminComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeMaxminComponent.ts new file mode 100644 index 0000000..d7cf66f --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeMaxminComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-maxmin.html' +}) +export class DatetimerangeMaxminComponent { + max: Date = new Date(2025, 10, 15, 10, 20, 30); + min: Date = new Date(2020, 10, 15, 10, 20, 30); + value1: TiDateValue = null; + value2: TiDateValue = null; + value3: TiDateValue = null; +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeMaxminTestComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeMaxminTestComponent.ts new file mode 100644 index 0000000..134196d --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeMaxminTestComponent.ts @@ -0,0 +1,60 @@ +import { Component } from '@angular/core'; +import { TiDatetimeFormat, TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-maxmin-test.html' +}) +export class DatetimerangeMaxminTestComponent { + nowTime: Date = new Date(); + delayTime: number = 10 * 60 * 1000; // 10分钟 + min: Date = new Date(); + max: Date = new Date(); + min1: Date = new Date(); + max1: Date = new Date(); + min2: Date = new Date(); + max2: Date = new Date(); + value: TiDateValue; + value1: TiDateValue; + value2: TiDateValue; + value3: TiDateValue; + format: TiDatetimeFormat; + + onMaxClick(): void { + this.max = new Date(this.nowTime.getTime() - this.delayTime); + } + + onMinClick(): void { + this.min = new Date(this.nowTime.getTime() + this.delayTime); + } + + onMaxAndMinClick(): void { + this.max1 = new Date(this.nowTime.getTime() - this.delayTime); + this.min1 = new Date(this.nowTime.getTime() + this.delayTime); + } + + onMaxAndMinClick1(): void { + this.max2 = new Date(this.nowTime.getTime() + this.delayTime); + this.min2 = new Date(this.nowTime.getTime() - this.delayTime); + } + + changeFormat(): void { + this.format = { + date: 'yyyy.MM.dd', + time: 'HH' + }; + } + + changeFormat1(): void { + this.format = { + date: 'yyyy.MM.dd', + time: 'HH:mm' + }; + } + + changeFormat2(): void { + this.format = { + date: 'yyyy.MM.dd', + time: 'HH:mm:ss' + }; + } +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeMinComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeMinComponent.ts new file mode 100644 index 0000000..a9afc82 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeMinComponent.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-min.html' +}) +export class DatetimerangeMinComponent { + nowTime: Date = new Date(); + min: Date = null; + value: TiDateValue = { + begin: new Date(1994, 2, 8, 1, 1, 1), + end: new Date(2016, 5, 21) + }; + + value1: TiDateValue = null; + min1: Date = new Date(2010, 10, 15, 10, 20, 30); + + value2: TiDateValue = { + begin: new Date(2017, 4, 2, 1, 2, 12), + end: new Date(20, 4, 16, 1, 12, 20) + }; + min2: Date = new Date(2010, 10, 15, 10, 20, 30); + + minValueClick(): void { + this.min1 = new Date(2013, 11, 23); + } + minValueClick2(): void { + this.min1 = undefined; + } + + onDayClick(obj: any): void { + if ((obj.focusedPosition === 'begin' && obj.beginValue === null) || obj.focusedPosition === 'end') { + this.min2 = new Date(obj.endValue?.getFullYear(), obj.endValue?.getMonth(), obj.endValue?.getDate() - 6); + } + } +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeNowdatetimeComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeNowdatetimeComponent.ts new file mode 100644 index 0000000..e1124e9 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeNowdatetimeComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-nowdatetime.html' +}) +export class DatetimerangeNowdatetimeComponent { + nowDateTime: Date = new Date(2018, 10, 20); + value: TiDateValue = null; +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangePanelalignComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangePanelalignComponent.ts new file mode 100644 index 0000000..5a3ccbd --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangePanelalignComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { TiDatetimeFormat, TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-panelalign.html' +}) +export class DatetimerangePanelalignComponent { + secondValue: TiDateValue = null; + minutevalue: TiDateValue = null; + hourValue: TiDateValue = null; + secondFormat: TiDatetimeFormat = { + date: 'yyyy/MM/dd', + time: 'HH:mm:ss' + }; + + minuteFormat: TiDatetimeFormat = { + date: 'yyyy.MM.dd', + time: 'HH:mm' + }; + + hourFormat: TiDatetimeFormat = { + date: 'yyyy/MM/dd', + time: 'HH' + }; +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeTimezoneableComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeTimezoneableComponent.ts new file mode 100644 index 0000000..4339d76 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeTimezoneableComponent.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; +import { TiDateValue, TiDateCustomizeOptions } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-timezoneable.html' +}) +export class DatetimerangeTimezoneableComponent { + nowTime: Date = new Date(); + nowYear: number = this.nowTime.getFullYear(); + nowMonth: number = this.nowTime.getMonth(); + nowDate: number = this.nowTime.getDate(); + nowHours: number = this.nowTime.getHours(); + nowMinutes: number = this.nowTime.getMinutes(); + nowSeconds: number = this.nowTime.getSeconds(); + value1: TiDateValue = null; + value2: TiDateValue = { + begin: new Date(2021, 4, 2, 1, 2, 12), + end: new Date(2023, 10, 15, 10, 20, 30), + timeZone: 'UTC/GMT' + }; + value3: TiDateValue = null; + customizeOptions: Array = [ + { + label: '最近一周', + value: { + end: this.nowTime, + begin: new Date(this.nowYear, this.nowMonth, this.nowDate - 6, this.nowHours, this.nowMinutes, this.nowSeconds) + } + }, + { + label: '最近一个月', + value: { + end: this.nowTime, + begin: new Date(this.nowYear, this.nowMonth - 1, this.nowDate, this.nowHours, this.nowMinutes, this.nowSeconds) + } + }, + { + label: '最近三个月', + value: { + end: this.nowTime, + begin: new Date(this.nowYear, this.nowMonth - 3, this.nowDate, this.nowHours, this.nowMinutes, this.nowSeconds) + } + } + ]; +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeValidationComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeValidationComponent.ts new file mode 100644 index 0000000..1a09312 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeValidationComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiDateValue, TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-validation.html' +}) +export class DatetimerangeValidationComponent { + value: TiDateValue = { + begin: new Date(2015, 8, 27), + end: new Date(2016, 5, 6) + }; + validation: TiValidationConfig = { + tipPosition: 'top' + }; +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeValueComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeValueComponent.ts new file mode 100644 index 0000000..df1f178 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeValueComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-value.html' +}) +export class DatetimerangeValueComponent { + value1: TiDateValue = null; + value2: TiDateValue = { + begin: new Date(2017, 8, 27, 11, 20, 12), + end: new Date(2018, 5, 6, 4, 12, 12) + }; +} diff --git a/src/datetimerange/demo/src/app/datetimerange/DatetimerangeValueTestComponent.ts b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeValueTestComponent.ts new file mode 100644 index 0000000..26881a8 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/DatetimerangeValueTestComponent.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; +import { TiDateValue } from '@opentiny/ng'; + +@Component({ + templateUrl: './datetimerange-value-test.html' +}) +export class DatetimerangeValueTestComponent { + id: string = 'datetimerange'; + value: string = ''; + value1: TiDateValue = { + begin: new Date(2017, 8, 27, 11, 20, 12), + end: new Date(2018, 5, 6, 4, 12, 12) + }; + + valueClickFn(): void { + this.value1 = { + begin: new Date(2016, 2, 15), + end: new Date(2017, 3, 15) + }; + } + + valueClickFn1(): void { + if (this.value1 === null) { + return; + } + this.value1.begin = new Date(2015, 4, 12); + this.value1 = { ...this.value1 }; + } + + valueClickFn2(): void { + if (this.value1 === null) { + return; + } + this.value1.end = new Date(2020, 6, 12, 12, 10, 30); + this.value1 = { ...this.value1 }; + } +} diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-clearicon.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-clearicon.html new file mode 100644 index 0000000..33e386e --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-clearicon.html @@ -0,0 +1 @@ + diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-customize.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-customize.html new file mode 100644 index 0000000..6801ff2 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-customize.html @@ -0,0 +1,9 @@ + + + diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-disabled.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-disabled.html new file mode 100644 index 0000000..386ef1f --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-disabled.html @@ -0,0 +1 @@ + diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-event.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-event.html new file mode 100644 index 0000000..d366cc0 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-event.html @@ -0,0 +1,10 @@ + + + diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-format-test.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-format-test.html new file mode 100644 index 0000000..5b42d34 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-format-test.html @@ -0,0 +1,24 @@ +

1.描述

+

format接口测试

+

2.示例

+

(2.1)不设置时,使用默认类型。中文:'yyyy/MM/dd';英文:"MMM dd, yyyy"。

+ +
+
+
+

(2.2)自定义设置为"yyyy/MM/dd HH:mm"

+ +
+
+
+

(2.2)自定义设置为"yyyy.MM.dd HH"

+ +
+
+
+

(2.3)动态变更

+ + +

+ + diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-format.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-format.html new file mode 100644 index 0000000..0d4aab5 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-format.html @@ -0,0 +1,5 @@ + +
+ +
+ diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-isallowbeginequalend.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-isallowbeginequalend.html new file mode 100644 index 0000000..1dbd75f --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-isallowbeginequalend.html @@ -0,0 +1,6 @@ + diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-many-test.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-many-test.html new file mode 100644 index 0000000..84099c7 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-many-test.html @@ -0,0 +1,6 @@ +

1.描述

+

页面上有100个TiDatetimeRangeComponent

+

2.示例

+ + + diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-max.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-max.html new file mode 100644 index 0000000..9e51ab9 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-max.html @@ -0,0 +1,39 @@ +

描述

+

max接口测试

+

示例

+

1.最大值设置为字符串类型、非法日期时:最大值为默认值

+
+ +
+
+

2.设置为正常日期值时,初始、下拉面板显示及用户操作改变时都必须小于这个最大值

+
+ +
+
+最大值: {{ max1 | date: "yyyy-MM-dd HH:mm:ss"}} +
+
+

3.动态改变最大值

+
+ +
+
+

4.最大值设置为new Date()

+
+ +
+
+最大值: {{ max2 | date: "yyyy-MM-dd HH:mm:ss"}} +
+
+ + +
+
+

5.dayClick事件中设置最大值(选择开始时间,更改最大值 10.1.13版本支持)

+
+最大值: {{ max4 | date: "yyyy-MM-dd HH:mm:ss" }} +
+
+ diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-maxmin-test.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-maxmin-test.html new file mode 100644 index 0000000..a5c5c9e --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-maxmin-test.html @@ -0,0 +1,43 @@ +

描述

+

min,max临界值测试

+

示例

+

1.max 接口测试,设置max为 new Date()

+
+ +

+开始时间:{{value?.begin}} +
+结束时间:{{value?.end}} +

+

2.min 接口测试,设置min为 new Date()

+
+ +

+开始时间:{{value?.begin}} +
+结束时间:{{value?.end}} +

+

3.max min 接口测试,设置min1,max1都为 new Date()

+
+ +

+开始时间:{{value2?.begin}} +
+结束时间:{{value2?.end}} +

+

4.max min 接口测试,设置min2,max2都为 new Date()

+
+ +

+开始时间:{{value3?.begin}} +
+结束时间:{{value3?.end}} +

+切换时间格式: + + + + + + + diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-maxmin.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-maxmin.html new file mode 100644 index 0000000..bb8d954 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-maxmin.html @@ -0,0 +1,5 @@ + +
+ +
+ diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-min.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-min.html new file mode 100644 index 0000000..7218e76 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-min.html @@ -0,0 +1,26 @@ +

描述

+

min接口测试

+

示例

+

1.最小值设置为字符串类型、非法日期时:最小值为默认值

+
+ +
+
+

2.设置为正常日期值时,显示正常

+
+ +
+
+最小值:{{min1 | date: "yyyy-MM-dd"}} +
+
+ + +
+
+

3.dayClick事件中设置最小值(选择结束时间,更改最小值 10.1.13版本支持)

+
+最小值: {{ min2 | date: "yyyy-MM-dd HH:mm:ss" }} +
+
+ diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-nowdatetime.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-nowdatetime.html new file mode 100644 index 0000000..314e9cd --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-nowdatetime.html @@ -0,0 +1 @@ + diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-panelalign.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-panelalign.html new file mode 100644 index 0000000..19734d2 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-panelalign.html @@ -0,0 +1 @@ + diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-timezoneable.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-timezoneable.html new file mode 100644 index 0000000..d55c9cd --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-timezoneable.html @@ -0,0 +1,20 @@ + +
+
Current value: {{ value1 | json }}
+
+
+ +
+
Current value: {{ value2 | json }}
+
+
+ +
+
Current value: {{ value3 | json }}
+
diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-validation.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-validation.html new file mode 100644 index 0000000..9fba694 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-validation.html @@ -0,0 +1 @@ + diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-value-test.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-value-test.html new file mode 100644 index 0000000..7c868c1 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-value-test.html @@ -0,0 +1,19 @@ +

1.描述

+

value接口测试,可设置样式,此处设置宽度为410px;

+

导入

+

import {{ '{' }} TiDatetimeRangeModule {{ '}' }} from '@opentiny/ng';

+

2.示例

+

(2.1)设置为null、字符串类型、非法日期时:输入框显示空白

+ +
+
+
+

(2.2)动态变更

+ +
+
+
+ + + + diff --git a/src/datetimerange/demo/src/app/datetimerange/datetimerange-value.html b/src/datetimerange/demo/src/app/datetimerange/datetimerange-value.html new file mode 100644 index 0000000..6fc4ca2 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/datetimerange-value.html @@ -0,0 +1,3 @@ + +
+ diff --git a/src/datetimerange/demo/src/app/datetimerange/webdoc/datetimerange-demos.js b/src/datetimerange/demo/src/app/datetimerange/webdoc/datetimerange-demos.js new file mode 100644 index 0000000..ed93d1e --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/webdoc/datetimerange-demos.js @@ -0,0 +1,162 @@ +export default { + column: '2', + demos: [ + { + demoId: 'datetimerange-value', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': '

DatetimeRange 组件的最简用法。

', + 'en-US': '' + } + }, + { + demoId: 'datetimerange-format', + name: { + 'zh-CN': '日期时间范围格式', + 'en-US': 'format' + }, + desc: { + 'zh-CN': '

通过属性format配置日期时间范围显示格式。

', + 'en-US': '' + }, + apis: ['TiDatetimeRangeComponent.properties.format', 'TiDatetimeFormat.properties.date', 'TiDatetimeFormat.properties.time'] + }, + { + demoId: 'datetimerange-maxmin', + name: { + 'zh-CN': '预设范围', + 'en-US': 'maxmin' + }, + desc: { + 'zh-CN': + '

通过属性max配置配置可选择日期时间范围的最大值;通过属性min配置可选择日期时间范围的最小值。

', + 'en-US': '' + }, + apis: ['TiDatetimeRangeComponent.properties.max', 'TiDatetimeRangeComponent.properties.min'] + }, + { + demoId: 'datetimerange-panelalign', + name: { + 'zh-CN': '下拉面板对齐方向', + 'en-US': 'panelAlign' + }, + desc: { + 'zh-CN': '

通过属性panelAlign配置下拉面板对齐方向,包括leftright

', + 'en-US': '' + }, + apis: ['TiDatetimeRangeComponent.properties.panelAlign'] + }, + { + demoId: 'datetimerange-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled' + }, + desc: { + 'zh-CN': '

通过属性disabled配置是否为禁用状态。

', + 'en-US': '' + }, + apis: ['TiDatetimeRangeComponent.properties.disabled'] + }, + { + demoId: 'datetimerange-clearicon', + name: { + 'zh-CN': '清除功能', + 'en-US': 'clearIcon' + }, + desc: { + 'zh-CN': '

通过属性clearIcon配置是否显示清除的叉号图标。

', + 'en-US': '' + }, + apis: ['TiDatetimeRangeComponent.properties.clearIcon'] + }, + { + demoId: 'datetimerange-customize', + name: { + 'zh-CN': '自定义下拉面板内容', + 'en-US': 'customize' + }, + desc: { + 'zh-CN': + '

通过属性customizeOptions自定义下拉面板左侧内容,设置可快捷选择的常用日期时间范围;当点击自定义下拉面板左侧内容时触发customizeOptionClick事件,传递出去的参数为选中的日期时间范围。

', + 'en-US': '' + }, + apis: [ + 'TiDatetimeRangeComponent.properties.customizeOptions', + 'TiDateCustomizeOptions.properties.label', + 'TiDateCustomizeOptions.properties.value', + 'TiDateValue.properties.begin', + 'TiDateValue.properties.end' + ] + }, + { + demoId: 'datetimerange-nowdatetime', + name: { + 'zh-CN': '自定义当前时间', + 'en-US': 'nowDateTime' + }, + desc: { + 'zh-CN': '

通过属性nowDateTime自定义当前时间,可用于矫正实际日期时间和计算机系统时间的偏差。

', + 'en-US': '' + }, + apis: ['TiDatetimeRangeComponent.properties.nowDateTime'] + }, + { + demoId: 'datetimerange-isallowbeginequalend', + name: { + 'zh-CN': '不允许开始时间和结束时间相同', + 'en-US': 'isAllowBeginEqualEnd' + }, + desc: { + 'zh-CN': '

通过属性isAllowBeginEqualEnd配置是否允许结束时间与开始时间相同。

', + 'en-US': '' + }, + apis: ['TiDatetimeRangeComponent.properties.isAllowBeginEqualEnd'] + }, + { + demoId: 'datetimerange-timezoneable', + name: { + 'zh-CN': '时区选择', + 'en-US': 'timeZoneable' + }, + desc: { + 'zh-CN': + '

通过属性timeZoneable配置是否显示时区选择框,支持时区选择;通过属性timeZone配置时区选择框的当前选中值,包括本地时区、UTC/GMT。

', + 'en-US': '' + }, + apis: ['TiDatetimeRangeComponent.properties.timeZoneable', 'TiDateValue.properties.timeZone'] + }, + { + demoId: 'datetimerange-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event' + }, + desc: { + 'zh-CN': + '

当日期时间范围框内值改变的时候触发ngModelChange事件;当点击下拉面板上的日期时触发dayClick事件,传递出去的参数为包括 value、focusedPosition、beginValue、endValue 这四个属性的对象;当点击面板上的确认按钮时触发okClick事件,传递出去的参数为选中的日期时间。

', + 'en-US': '' + }, + apis: ['TiDatetimeRangeComponent.events.dayClick', 'TiDatetimeRangeComponent.events.okClick'] + }, + { + demoId: 'datetimerange-validation', + name: { + 'zh-CN': '校验', + 'en-US': 'datetimerange validation' + }, + desc: { + 'zh-CN': '

通过指令tiValidation实现校验。

', + 'en-US': 'datetimerange validation description' + } + } + ], + ignoreApis: [ + 'TiDatetimeRangeComponent.properties.disabledDays', + 'TiDatetimeRangeComponent.properties.isBeginFixed', + 'TiDatetimeRangeComponent.properties.isEndFixed' + ] +}; diff --git a/src/datetimerange/demo/src/app/datetimerange/webdoc/datetimerange.cn.md b/src/datetimerange/demo/src/app/datetimerange/webdoc/datetimerange.cn.md new file mode 100644 index 0000000..979d431 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/webdoc/datetimerange.cn.md @@ -0,0 +1,30 @@ +--- +title: DatetimeRange 日期时间范围 +--- +# DatetimeRange 日期时间范围 + +
+ +选择或输入日期时间范围的组件。   + +```typescript +import { TiDatetimeRangeModule } from '@opentiny/ng'; +``` + +如需使用校验功能,请导入。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
+ +
+ +选择或输入日期时间范围的组件。   + +```typescript +import { TiDatetimeRangeModule } from '@opentiny/ng'; +``` + +
diff --git a/src/datetimerange/demo/src/app/datetimerange/webdoc/datetimerange.en.md b/src/datetimerange/demo/src/app/datetimerange/webdoc/datetimerange.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/datetimerange/demo/src/app/datetimerange/webdoc/datetimerange.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/datetimerange/demo/src/favicon.ico b/src/datetimerange/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/datetimerange/demo/src/index.html b/src/datetimerange/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/datetimerange/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/datetimerange/demo/src/main.ts b/src/datetimerange/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/datetimerange/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/datetimerange/demo/test.ts b/src/datetimerange/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/datetimerange/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/datetimerange/demo/tsconfig.app.json b/src/datetimerange/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/datetimerange/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/datetimerange/demo/tsconfig.spec.json b/src/datetimerange/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/datetimerange/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/datetimerange/lib/index.ts b/src/datetimerange/lib/index.ts new file mode 100644 index 0000000..10ec265 --- /dev/null +++ b/src/datetimerange/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiDatetimeRangeModule'; +export { TiDateCustomizeOptions, TiDatetimeFormat, TiDateValue } from '@opentiny/ng-datebase'; diff --git a/src/datetimerange/lib/ng-package.json b/src/datetimerange/lib/ng-package.json new file mode 100644 index 0000000..48e3c1d --- /dev/null +++ b/src/datetimerange/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/datetimerange", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/datetimerange/lib/package.json b/src/datetimerange/lib/package.json new file mode 100644 index 0000000..a5c6eda --- /dev/null +++ b/src/datetimerange/lib/package.json @@ -0,0 +1,22 @@ +{ + "name": "@opentiny/ng-datetimerange", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-datebase": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@opentiny/ng-datepanel": "~1.0.0-beta.0", + "@opentiny/ng-dateedit": "~1.0.0-beta.0", + "@opentiny/ng-time": "~1.0.0-beta.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-datedominator": "~1.0.0-beta.0", + "@opentiny/ng-select": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/datetimerange/lib/project.json b/src/datetimerange/lib/project.json new file mode 100644 index 0000000..8c8f6a5 --- /dev/null +++ b/src/datetimerange/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/datetimerange/lib", + "sourceRoot": "src/datetimerange/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/datetimerange"], + "options": { + "project": "src/datetimerange/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/datetimerange"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js datetimerange" + }, + { + "command": "ng default-build datetimerange" + }, + { + "command": "node build/clear-default-theme.js datetimerange" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/datetimerange && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build datetimerange && ng pack datetimerange && node build/publish.js datetimerange --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/datetimerange/lib/src/TiDatetimeRangeComponent.ts b/src/datetimerange/lib/src/TiDatetimeRangeComponent.ts new file mode 100644 index 0000000..f4459ce --- /dev/null +++ b/src/datetimerange/lib/src/TiDatetimeRangeComponent.ts @@ -0,0 +1,824 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input, SimpleChanges, ViewChild } from '@angular/core'; +import { TiDateUtil, TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiLocaleFormat } from '@opentiny/ng-locale'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiDateBaseComponent, TiDatetimeFormat, TiDateValue, TimeOptions } from '@opentiny/ng-datebase'; +import { TiSelectComponent } from '@opentiny/ng-select'; +import packageInfo from '../package.json'; +/** + * DatetimeRange日期时间范围组件 + * + * Datetime组件提供了一种方便的显示和设置日期时间范围的方式。 + * + */ +@Component({ + selector: 'ti-datetime-range', + templateUrl: './datetimerange.html', + styleUrls: ['./datetimerange.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-datetime-range-input-container]': 'true', + '(blur)': 'hidePanel()' + }, + providers: [TiFormComponent.getValueAccessor(TiDatetimeRangeComponent)] +}) +export class TiDatetimeRangeComponent extends TiDateBaseComponent { + /** + * 是否允许结束时间和开始时间相同 + */ + @Input() isAllowBeginEqualEnd: boolean = true; + /** + * 是否显示时区选择框(选项:‘本地时区’、‘UTC/GMT’),默认值为‘本地时区’ + */ + @Input() timeZoneable: boolean = false; + /** + * @ignore + */ + @ViewChild(TiSelectComponent, { static: false }) + selectComRef: TiSelectComponent; + + /** + * @ignore + * 保存model值 + */ + public oldModel: TiDateValue = { + begin: null, + end: null + }; + /** + * @ignore + * 对应Time组件的接口属性 + */ + public beginTime: TimeOptions = {}; + /** + * @ignore + * 对应Time组件的接口属性 + */ + public endTime: TimeOptions = {}; + /** + * @ignore + */ + public beginTimeDisabled: boolean; + /** + * @ignore + */ + public endTimeDisabled: boolean; + /** + * @ignore + * 时间日期的format + */ + public dominatorFormat: string; + /** + * @ignore + * 日期显示格式: Datetime组件的format为DatetimeFormat类型 + */ + public format: TiDatetimeFormat; + /** + * @ignore + * 标记date/datetime的类型 + */ + public isDatetime: boolean = true; + /** + * @ignore + * 用于标记是不是range + */ + public isRange: boolean = true; + /** + * @ignore + * 标记输入框中的时间是不是手动输入 + */ + public isInputValue: boolean = false; + /** + * @ignore + */ + public oldBegintimeValue: any = ''; + /** + * @ignore + */ + public oldEndtimeValue: any = ''; + /** + * @ignore + * placeholder提示文本 + */ + public placeholder: string = `${this.dateWords.rangeBeginLabel} ─ ${this.dateWords.rangeEndLabel}`; + /** + * @ignore + * 时区选择下拉建议项 + */ + public timeZoneOptions: Array = [{ label: this.dateWords.utcZone }, { label: this.dateWords.localZone }]; + /** + * @ignore + * 时区选择当前选中项 + */ + public selectedOption: string; + protected versionInfo: string = super.getVersion(packageInfo); + private lastTimeZone: string; // 记录上次选择的时区 + private beginDateTime: string = ''; // 开始日期框中的时间值 + private endDateTime: string = ''; // 结束日期框中的时间值 + + ngOnInit(): void { + super.ngOnInit(); + // 将日期与时间的format拼接为一个字符串 + this.dominatorFormat = `${this.format.date} ${this.format.time}`; + this.oldFormat = this.format; + // model有值时,需要记录初始时间值 + if (Util.isUndefined(this.model)) { + return; + } + if (this.model !== null) { + setTimeout((): void => { + this.oldBegintimeValue = TiLocaleFormat.formatTime(this.model.begin, this.format.time); + this.oldEndtimeValue = TiLocaleFormat.formatTime(this.model.end, this.format.time); + }, 0); + } + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + + if (this.timeZoneable && this.selectComRef) { + // 将select dominator设置为可聚焦元素 + this.setFocusableElems( + [this.dominatorCom.nativeElement] + .concat(this.dropCom.nativeElement) + .concat(this.focusElementsArr) + .concat(this.selectComRef.dominatorCom.getFocusableElems()) + ); + } + } + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + this.rangeChange(changes); + } + + ngDoCheck(): void { + if (!Util.isUndefined(this.model)) { + // 设置初始时区选择的选中项 + if (this.timeZoneable && !this.selectedOption) { + this.selectedOption = this.model?.timeZone; + this.lastTimeZone = this.selectedOption; + } + // 监听model值的变化 + this.setModel(this.isDatetime); + + // 监听format变化 + this.setFormat(); + } + } + + /** + * @ignore + * 确认按钮事件处理 + */ + public onOkClick(): void { + if (this.inValidValue) { + return; + } + + const beginDateStr: string = TiDateUtil.getDateStr(this.datePanel['value']['begin']); + // 中间空格误删 + const beginValue: Date = TiDateUtil.getLocalDate( + `${beginDateStr} ${TiDateUtil.addColon(this.beginTime['value'])}`, + this.timeZoneable, + this.selectedOption + ); + const endDateStr: string = TiDateUtil.getDateStr(this.datePanel['value']['end']); + // 中间空格误删 + const endValue: Date = TiDateUtil.getLocalDate( + `${endDateStr} ${TiDateUtil.addColon(this.endTime['value'], true)}`, + this.timeZoneable, + this.selectedOption + ); + + const newModel: TiDateValue = { + begin: beginValue, + end: endValue + }; + + if (this.selectedOption) { + newModel.timeZone = this.selectedOption; + this.lastTimeZone = this.selectedOption; + } + + // 新旧值不同更新model值 + if (!this.rangeValueIsEqual(this.model, newModel, this.isDatetime)) { + this.model = newModel; + } + + this.hideDrop(); + + this.okClick.emit(this.model); + } + + /** + * @ignore + * model值的合法性判断 + */ + public isValidValue(value: TiDateValue): boolean { + if (value === null || (value.begin === null && value.end === null)) { + return true; + } + + if ( + TiDateUtil.isDate(value['begin']) && + TiDateUtil.isDate(value['end']) && + value['begin'].getTime() <= value['end'].getTime() && + TiDateUtil.isBetweenMaxAndmin(value['begin'], this.min, this.max) && + TiDateUtil.isBetweenMaxAndmin(value['end'], this.min, this.max) + ) { + return true; + } + + return false; + } + + /** + * @ignore + * 将value转换成format接口格式的字符串 + */ + public formatValue(): void { + if (this.model === null || (this.model.begin === null && this.model.end === null)) { + this.inputValue = ''; + } else { + // 空格误删 + const format: string = `${this.format.date} ${TiDateUtil.addAmPm(this.format.time)}`; + const begin: string = TiLocaleFormat.formatDateTime( + TiDateUtil.getUtcDate(this.model.begin, this.timeZoneable, this.selectedOption), + format + ); + const end: string = TiLocaleFormat.formatDateTime( + TiDateUtil.getUtcDate(this.model.end, this.timeZoneable, this.selectedOption), + format + ); + // 注意:中划线用的是制表符中的中划线,与正常的中线区分开2017-2-12 - 2018-3-20 + this.inputValue = `${begin} ─ ${end}`; + } + } + + /** + * @ignore + * 配置时间日期面板接口 + */ + public setPickerDate(): void { + // 设置时区选择的选中项 + if (this.timeZoneable) { + this.selectedOption = this.lastTimeZone ? this.lastTimeZone : this.model?.timeZone || this.timeZoneOptions[1].label; + this.lastTimeZone = this.selectedOption; + } + + // 设置datepanel指令接口 + this.setDatePanelOptions(); + + // 设置time指令接口 + this.setTimeOptions(); + + // 设置OK按钮状态 + this.setOkBtnState(); + this.beginTimeDisabled = this.model === null || this.model['begin'] === null ? true : false; + this.endTimeDisabled = this.model === null || this.model['end'] === null ? true : false; + this.timeEditDisabled = this.beginTimeDisabled && this.endTimeDisabled; + } + + // 设置datepanel指令接口 + private setDatePanelOptions(): void { + let value: TiDateValue; + if (this.model === null || (this.model['begin'] === null && this.model['end'] === null)) { + value = { + begin: null, + end: null + }; + } else { + value = { + begin: TiDateUtil.getUtcDate(this.model.begin, this.timeZoneable, this.selectedOption), + end: TiDateUtil.getUtcDate(this.model.end, this.timeZoneable, this.selectedOption) + }; + } + + this.datePanel = { + value, + max: this.max, + min: this.min, + format: this.format, + picker: 'day', + select: (): void => { + const nowTime: string = TiLocaleFormat.formatTime( + TiDateUtil.getUtcDate(new Date(), this.timeZoneable, this.selectedOption), + this.format.time + ); + const isSameday: boolean = TiDateUtil.isDateEqual(this.datePanel.value.begin, this.datePanel.value.end); + + // 选择开始日期 + if (this.focusedPosition === 'begin') { + if (this.beginTimeDisabled) { + const isSmallerNowtime: boolean = nowTime > this.endTime.value; + + this.beginTime['value'] = isSameday && isSmallerNowtime ? this.endTime.value : nowTime; + this.beginTimeDisabled = false; + } + + const beginDateStr: string = TiDateUtil.getDateStr(this.datePanel['value']['begin']); + // 中间空格误删 + const beginValue: Date = new Date(`${beginDateStr} ${TiDateUtil.addColon(this.beginTime['value'])}`); + this.datePanel.value.begin = beginValue; + this.dayClick.emit({ + value: TiDateUtil.getLocalDate(`${beginValue}`, this.timeZoneable, this.selectedOption), + focusedPosition: this.focusedPosition, + beginValue: TiDateUtil.getLocalDate(`${this.datePanel.value.begin}`, this.timeZoneable, this.selectedOption), + endValue: TiDateUtil.getLocalDate(`${this.datePanel.value.end}`, this.timeZoneable, this.selectedOption) + }); + + if (this.datePanel.value.end === null) { + this.datePanel.min = this.datePanel.value.begin; + this.endTime.value = ''; + this.endTimeDisabled = true; + } + + this.timeEditDisabled = false; + this.dateEditComs.last.focus(); + this.focusedPosition = 'end'; + + // 先选择结束日期后,焦点由开始自动聚焦到结束面板上,需要此处处理 + if (TiDateUtil.isDate(value.end)) { + this.datePanel.min = value.begin; + this.datePanel.max = this.max; + } + } else { + if (this.endTimeDisabled) { + const isBiggerNowtime: boolean = nowTime < this.beginTime.value; + + this.endTime['value'] = isSameday && isBiggerNowtime ? this.beginTime.value : nowTime; + this.endTimeDisabled = false; + } + if (this.focusedPosition === 'end' && this.datePanel.value.begin === null) { + this.focusedPosition = 'begin'; + this.dateEditComs.first.focus(); + } + const endDateStr: string = TiDateUtil.getDateStr(this.datePanel['value']['end']); + // 中间空格误删 + const endValue: Date = new Date(`${endDateStr} ${TiDateUtil.addColon(this.endTime['value'], true)}`); + this.datePanel.value.end = endValue; + this.dayClick.emit({ + value: TiDateUtil.getLocalDate(`${endValue}`, this.timeZoneable, this.selectedOption), + focusedPosition: this.focusedPosition, + beginValue: TiDateUtil.getLocalDate(`${this.datePanel.value.begin}`, this.timeZoneable, this.selectedOption), + endValue: TiDateUtil.getLocalDate(`${this.datePanel.value.end}`, this.timeZoneable, this.selectedOption) + }); + + if (this.datePanel.value.begin === null) { + this.datePanel.max = this.datePanel.value.end; + } + + // 焦点结束面板上,有值情况下不会触发失焦事件,因此需要单独处理 + if (TiDateUtil.isDate(value.begin)) { + const isSmallerBeginTime: boolean = this.endTime.value < this.beginTime.value; + this.endTime.value = isSameday && isSmallerBeginTime ? this.beginTime.value : this.endTime.value; + } + } + this.validTime(); + this.setOkBtnState(); + }, + selectTimeFn: (obj: any): void => { + this.setTimeFn(obj, this.beginTime, this.endTime); + this.setOkBtnState(); + // 选择结束时分秒 焦点移动到结束时间框 + if (obj.timeOption.includes('end')) { + this.textComs.last.nativeElement.focus(); + } else { + // 选择开始时分秒 焦点移动到开始时间框 + this.textComs.first.nativeElement.focus(); + } + } + }; + } + + private validTime(): void { + const beginDateStr: string = this.datePanel.value.begin ? TiDateUtil.getDateStr(this.datePanel.value.begin) : ''; + const beginDateTime: Date = new Date(` ${beginDateStr} ${TiDateUtil.addColon(this.beginTime.value)}`); + const endDateStr: string = this.datePanel.value.end ? TiDateUtil.getDateStr(this.datePanel.value.end) : ''; + const endDateTime: Date = new Date(` ${endDateStr} ${TiDateUtil.addColon(this.endTime.value)}`); + if (TiDateUtil.isSmaller(beginDateTime, this.min)) { + this.beginTime.value = TiLocaleFormat.formatTime(this.min, this.format.time); + } + if (TiDateUtil.isSmaller(endDateTime, this.min)) { + this.endTime.value = TiLocaleFormat.formatTime(this.min, this.format.time); + } + if (TiDateUtil.isBigger(beginDateTime, this.max)) { + this.beginTime.value = TiLocaleFormat.formatTime(this.max, this.format.time); + } + if (TiDateUtil.isBigger(endDateTime, this.max)) { + this.endTime.value = TiLocaleFormat.formatTime(this.max, this.format.time); + } + this.oldBegintimeValue = this.beginTime['value']; + this.oldEndtimeValue = this.endTime['value']; + } + + /** + * @ignore + * 时间框失焦事件 + */ + public timeBlur(pos: string, val: string): void { + let timeVal: string = val; + this.beginTime = JSON.parse(JSON.stringify(this.beginTime)); + this.endTime = JSON.parse(JSON.stringify(this.endTime)); + // 输入区间是否存在小数点,非整数 + let isNotInteger: boolean = false; + const timeArr: Array = val.split(TiDateUtil.COLON_REGEXP); + timeArr.forEach((item: string, index: number): void => { + if (item.length === 1) { + timeArr[index] = '0' + item; + } + if (item && parseInt(item, 10) !== Number(item)) { + isNotInteger = true; + } + }); + timeVal = timeArr.join(':'); + const dateStr: string = '2020/10/30'; + if (pos === 'begin' && TiDateUtil.isDate(this.datePanel.value.begin)) { + const beginDateVal: Date = new Date(`${TiDateUtil.getDateStr(this.datePanel.value.begin)} ${TiDateUtil.addColon(timeVal)}`); + const isSmallerThanMin: boolean = TiDateUtil.isSmaller(beginDateVal, this.min); + const beginDate: Date = new Date(`${dateStr} ${TiDateUtil.addColon(timeVal)}`); + const dateVal: Date = this.setDateTime(timeArr); + + if ( + String(beginDate) === 'Invalid Date' || + String(dateVal) === 'Invalid Date' || + timeArr.length > 3 || + isNotInteger || + (!this.isBeginSmallerThanEnd('begin', timeVal) && TiDateUtil.isDate(this.datePanel.value.end)) || + isSmallerThanMin + ) { + this.beginTime.value = this.oldBegintimeValue; + } else { + this.beginTime.value = TiLocaleFormat.formatTime(dateVal, this.format.time); + this.oldBegintimeValue = this.beginTime.value; + } + + this.setDateDisabled('beginTime'); + } else if (pos === 'end' && TiDateUtil.isDate(this.datePanel.value.end)) { + const endDateVal: Date = new Date(`${TiDateUtil.getDateStr(this.datePanel.value.end)} ${TiDateUtil.addColon(timeVal)}`); + const isBiggerThanMax: boolean = TiDateUtil.isBigger(endDateVal, this.max); + const endDate: Date = new Date(`${dateStr} ${TiDateUtil.addColon(this.endTime['value'])}`); + const dateVal: Date = this.setDateTime(timeArr); + + if ( + String(endDate) === 'Invalid Date' || + String(dateVal) === 'Invalid Date' || + timeArr.length > 3 || + isNotInteger || + (!this.isBeginSmallerThanEnd('end', timeVal) && TiDateUtil.isDate(this.datePanel.value.begin)) || + isBiggerThanMax + ) { + this.endTime.value = this.oldEndtimeValue; + } else { + this.endTime.value = TiLocaleFormat.formatTime(dateVal, this.format.time); + this.oldEndtimeValue = this.endTime.value; + } + + this.setDateDisabled('endTime'); + } + this.setOkBtnState(); + } + /** + * @ignore + * 时间框enter事件 + */ + public timeKeydown(pos: string, val: string, event: KeyboardEvent): void { + if (event.keyCode === TiKeymap.KEY_ENTER || event.keyCode === TiKeymap.KEY_NUMPAD_ENTER) { + this.timeBlur(pos, val); + } + } + /** + * @ignore + * 日期框值变化且是合法值时触发 + */ + onInputChange(time: string, pos: string): void { + this.isInputValue = true; + if (pos === 'begin') { + this.beginDateTime = time; + } else { + this.endDateTime = time; + } + } + + /** + * @ignore + */ + public dateEditBlur(val: any, pos: string, timeVal?: string): void { + const value: any = this.datePanel.value; + if (val === null) { + setTimeout(() => { + // 添加setTimeout原因:确保点击时间输入框时拿到的focusedPosition为beginTime或者endTime + // 点击非禁用的确认按钮,需保证焦点在面板内部,否则点击document空白处无法关闭面板 + if ( + ((!TiDateUtil.isDate(value.begin) && this.focusedPosition !== 'end') || + (!TiDateUtil.isDate(value.end) && this.focusedPosition !== 'begin')) && + !this.inValidValue + ) { + this.dropCom.nativeElement.focus(); + } + this.setOkBtnState(); + }, 0); + + return; + } + + // 时间日期组件,若当前输入日期与最小值相等时,需要与时间一起进行合法值校验 + let dateValue: any = val; + + if (TiDateUtil.isDateEqual(dateValue, this.min)) { + dateValue = timeVal ? new Date(`${TiDateUtil.getDateStr(dateValue)} ${timeVal}:999`) : this.min; + } + + if (TiDateUtil.isBetweenMaxAndmin(dateValue, this.min, this.max)) { + if (pos === 'begin') { + if (this.beginTimeDisabled) { + this.beginTime['value'] = TiLocaleFormat.formatTime( + TiDateUtil.getUtcDate(new Date(), this.timeZoneable, this.selectedOption), + this.format.time + ); + this.beginTimeDisabled = false; + } + + const inputValue: string = this.dateEditComs.first.nativeElement.firstChild.value; + if (inputValue === '') { + return; + } + + if (this.beginDateTime) { + this.timeBlur('begin', this.beginDateTime); + this.beginDateTime = ''; + } + + this.setDateDisabled('begin'); + } else { + if (this.endTimeDisabled) { + this.endTime['value'] = TiLocaleFormat.formatTime( + TiDateUtil.getUtcDate(new Date(), this.timeZoneable, this.selectedOption), + this.format.time + ); + this.endTimeDisabled = false; + } + const inputValue: string = this.dateEditComs.last.nativeElement.firstChild.value; + if (inputValue === '') { + return; + } + + if (this.endDateTime) { + this.timeBlur('end', this.endDateTime); + this.endDateTime = ''; + } + + this.setDateDisabled('end'); + } + this.validTime(); + this.setOkBtnState(); + this.timeEditDisabled = this.beginTimeDisabled && this.endTimeDisabled; + } + } + + /** + * 设置日期禁用状态 + * 选择开始日期,开始日期前禁用;选择结束日期,结束日期后禁用 + */ + private setDateDisabled(pos: string): void { + const value: any = this.datePanel.value; + const isSameday: boolean = TiDateUtil.isDateEqual(value.begin, value.end); + + // 添加setTimeout原因:确保时间框或日期框失焦,this.focusedPosition获取到的是处于聚焦状态时的位置 + if (pos === 'begin' || pos === 'endTime') { + setTimeout(() => { + if (this.focusedPosition === 'end' || this.focusedPosition === 'beginTime') { + if (value.begin === null) { + return; + } + + this.datePanel.min = value.begin; + + if (TiDateUtil.isDate(value.end)) { + this.datePanel.max = this.max; + const isSmallerBeginTime: boolean = this.endTime.value < this.beginTime.value; + this.endTime.value = isSameday && isSmallerBeginTime ? this.beginTime.value : this.endTime.value; + // 时间有变化时,需要更新确认按钮状态 + this.setOkBtnState(); + } + } + + // 绑定在模板上的值变了,需要手动触发变更检测 + this.changeDetectorRef.markForCheck(); + }, 0); + } else { + setTimeout(() => { + if (this.focusedPosition === 'begin' || this.focusedPosition === 'endTime') { + if (value.end === null) { + return; + } + + this.datePanel.max = value.end; + + if (TiDateUtil.isDate(value.begin)) { + this.datePanel.min = this.min; + const isBiggerEndTime: boolean = this.beginTime.value > this.endTime.value; + this.beginTime.value = isSameday && isBiggerEndTime ? this.endTime.value : this.beginTime.value; + // 时间有变化时,需要更新确认按钮状态 + this.setOkBtnState(); + } + } + + // 绑定在模板上的值变了,需要手动触发变更检测 + this.changeDetectorRef.markForCheck(); + }, 0); + } + } + /** + * @ignore + */ + public onKeydownFn(event: KeyboardEvent, val: any, pos: string, timeVal: string): void { + if (event.keyCode === TiKeymap.KEY_ENTER || event.keyCode === TiKeymap.KEY_NUMPAD_ENTER) { + this.dateEditBlur(val, pos, timeVal); + } + } + + // 设置time指令接口 + private setTimeOptions(): void { + // 设置开始time指令接口 + this.setBeginTimeOptions(); + + // 设置结束time指令接口 + this.setEndTimeOptions(); + } + + // 设置开始面板time指令接口 + private setBeginTimeOptions(): void { + // value接口设置 + this.setBeginTimeValue(); + + // max接口设置 + this.setBeginTimeMaxValue(); + + // min接口设置 + this.setBeginTimeMinValue(); + } + + // time指令的起始面板的value接口设置 点击删除按钮:如果设置nowDateTime并且值为合法时间日期,时间设置为nowDateTime的时间 + private setBeginTimeValue(): void { + if (this.model === null || (this.model['begin'] === null && this.model['end'] === null)) { + this.beginTime['value'] = ''; + + return; + } + this.beginTime['value'] = TiLocaleFormat.formatTime( + TiDateUtil.getUtcDate(this.model.begin, this.timeZoneable, this.selectedOption), + this.format.time + ); + } + + /** + * @ignore + * time指令的起始面板的max接口设置 + */ + public setBeginTimeMaxValue(): void { + // 起始日期是最大日期时,时间组件最大值是max接口中的时间;其他情况最大值是""23:59:59"|"23:59"|"23" + const value: TiDateValue = this.datePanel['value']; + const maxFormatTime: string = TiLocaleFormat.formatTime('2020/10/30 23:59:59', this.format.time); + this.beginTime['max'] = + value instanceof Object && TiDateUtil.isDateEqual(this.max, value['begin']) + ? TiLocaleFormat.formatTime(this.max, this.format.time) + : maxFormatTime; + } + + /** + * @ignore + * time指令的起始面板的min接口设置 + */ + public setBeginTimeMinValue(): void { + // 起始日期是最小日期时,时间组件最小值是min接口中的时间;其他情况最大值是"00:00:00"|"00:00"|"00" + const value: TiDateValue = this.datePanel['value']; + const minFormatTime: string = TiLocaleFormat.formatTime('2020/10/30 00:00:00', this.format.time); + this.beginTime['min'] = + value instanceof Object && TiDateUtil.isDateEqual(this.min, value['begin']) + ? TiLocaleFormat.formatTime(this.min, this.format.time) + : minFormatTime; + } + + // 设置结束面板time指令接口 + private setEndTimeOptions(): void { + // value接口设置 + this.setEndTimeValue(); + + // max接口设置 + this.setEndTimeMaxValue(); + + // min接口设置 + this.setEndTimeMinValue(); + } + + // time指令的结束面板的value接口设置 + private setEndTimeValue(): void { + if (this.model === null || (this.model['begin'] === null && this.model['end'] === null)) { + this.endTime['value'] = ''; + + return; + } + this.endTime['value'] = TiLocaleFormat.formatTime( + TiDateUtil.getUtcDate(this.model.end, this.timeZoneable, this.selectedOption), + this.format.time + ); + } + + /** + * @ignore + * time指令的结束面板的max接口设置 + */ + public setEndTimeMaxValue(): void { + // 结束日期是最大日期时,时间组件最大值是max接口中的时间; 否则,时间组件最大值是"23:59:59"|"23:59"|"23" + const value: TiDateValue = this.datePanel['value']; + const maxFormatTime: string = TiLocaleFormat.formatTime('2020/10/30 23:59:59', this.format.time); + this.endTime['max'] = + value instanceof Object && TiDateUtil.isDateEqual(this.max, value['end']) + ? TiLocaleFormat.formatTime(this.max, this.format.time) + : maxFormatTime; + } + + /** + * @ignore + * time指令的结束面板的min接口设置 + */ + public setEndTimeMinValue(): void { + // 起始日期是最小日期时,时间组件最小值是min接口中的时间;其他情况最大值是"00:00:00"|"00:00"|"00" + const value: TiDateValue = this.datePanel['value']; + const minFormatTime: string = TiLocaleFormat.formatTime('2020/10/30 00:00:00', this.format.time); + this.endTime['min'] = + value instanceof Object && TiDateUtil.isDateEqual(this.min, value['end']) + ? TiLocaleFormat.formatTime(this.min, this.format.time) + : minFormatTime; + } + + /** + * @ignore + * 设置确认按钮的状态 + */ + public setOkBtnState(): void { + // 输入中文冒号之后,及时转换为英文冒号 + const inputvalue: string = this.beginTime.value; + const inputvalue1: string = this.endTime.value; + this.beginTime.value = inputvalue.replace(':', ':'); + this.endTime.value = inputvalue1.replace(':', ':'); + // 判断下拉面板中时间区间是否合法 + this.inValidValue = !this.isValidRange(); + if (this.buttonComs) { + this.setAttr(this.buttonComs.last.nativeElement, 'disabled', this.inValidValue); + this.setInputStyle(this.inValidValue); + } + } + + /** + * @ignore + * 左侧自定义时间文本点击事件 + */ + public customizeTimeClickFn(val: TiDateValue): void { + this.model = { + begin: TiDateUtil.getLocalDate(`${val.begin}`, this.timeZoneable, this.selectedOption), + end: TiDateUtil.getLocalDate(`${val.end}`, this.timeZoneable, this.selectedOption) + }; + + if (this.selectedOption) { + this.model.timeZone = this.selectedOption; + this.lastTimeZone = this.selectedOption; + } + this.dropCom.hide(); + this.customizeOptionClick.emit(this.model); + } + + // 判断下拉面板中时间区间是否合法 + private isValidRange(): boolean { + const date: TiDateValue = this.datePanel.value; + + return date instanceof Object && TiDateUtil.isDate(date.begin) && TiDateUtil.isDate(date.end) && this.isBeginSmallerThanEnd(); + } + + // 判断起始值和结束值都在最小值最大值之间 + private isBeginSmallerThanEnd(pos?: string, value?: string): boolean { + if (!TiDateUtil.isDate(this.datePanel.value.begin) || !TiDateUtil.isDate(this.datePanel.value.end)) { + return false; + } + + const beginTime: string = pos === 'begin' && value ? value : this.beginTime['value']; + const endTime: string = pos === 'end' && value ? value : this.endTime['value']; + // 组装起始日期时间 空格误删 + const begin: Date = new Date(`${TiDateUtil.getDateStr(this.datePanel.value.begin)} ${TiDateUtil.addColon(beginTime)}`); + const end: Date = new Date(`${TiDateUtil.getDateStr(this.datePanel.value.end)} ${TiDateUtil.addColon(endTime, true)}`); + + return this.isAllowBeginEqualEnd ? begin.getTime() <= end.getTime() : begin.getTime() < end.getTime(); + } +} diff --git a/src/datetimerange/lib/src/TiDatetimeRangeModule.ts b/src/datetimerange/lib/src/TiDatetimeRangeModule.ts new file mode 100644 index 0000000..d4b617b --- /dev/null +++ b/src/datetimerange/lib/src/TiDatetimeRangeModule.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiLocaleModule } from '@opentiny/ng-locale'; +import { TiDatetimeRangeComponent } from './TiDatetimeRangeComponent'; +import { TiButtonModule } from '@opentiny/ng-button'; +import { TiDatePanelModule } from '@opentiny/ng-datepanel'; +import { TiDateEditModule } from '@opentiny/ng-dateedit'; +import { TiTimeModule } from '@opentiny/ng-time'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiDateDominatorModule } from '@opentiny/ng-datedominator'; +import { TiSelectModule } from '@opentiny/ng-select'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiLocaleModule, + TiDropModule, + TiButtonModule, + TiDatePanelModule, + TiDateEditModule, + TiTimeModule, + TiTextModule, + TiDateDominatorModule, + TiSelectModule + ], + exports: [TiDatetimeRangeComponent], + declarations: [TiDatetimeRangeComponent] +}) +export class TiDatetimeRangeModule {} +export { TiDatetimeRangeComponent } from './TiDatetimeRangeComponent'; diff --git a/src/datetimerange/lib/src/datetimerange.html b/src/datetimerange/lib/src/datetimerange.html new file mode 100644 index 0000000..1897906 --- /dev/null +++ b/src/datetimerange/lib/src/datetimerange.html @@ -0,0 +1,167 @@ +{{placeholder}} + +
+
+ +
+ +
+ + + + + + + + +
+
+ +
+ +
    +
  • + {{option.label}} +
  • +
+
diff --git a/src/datetimerange/lib/src/datetimerange.less b/src/datetimerange/lib/src/datetimerange.less new file mode 100644 index 0000000..6cb4d52 --- /dev/null +++ b/src/datetimerange/lib/src/datetimerange.less @@ -0,0 +1,94 @@ +@import '../../../themes/basic/base-all.less'; +@import '../../../themes/basic/compnent-container-border.less'; +@import '../../../date/lib/src/date-common.less'; + +ti-drop.ti3-datetime-range-picker { + --ti-date-picker-padding-horizon: var(--ti-common-space-4x); + --ti-datetime-range-picker-footer-left: -10px; + --ti-date-picker-line-width: 1px; + --ti-date-picker-line-color: var(--ti-common-color-line-dividing); +} +:host { + --ti-datetime-range-input-width: 320px; +} +:host.ti3-datetime-range-input-container :extend(.ti3-compnent-container-border all) { + width: var(--ti-datetime-range-input-width); +} + +.ti3-dropdown-container.ti3-datetime-range-picker { + padding: var(--ti-common-space-3x) var(--ti-date-picker-padding-horizon); + font-size: var(--ti-common-font-size-base); + z-index: 10002; + + &:focus { + outline: 0px; + } +} + +.ti3-datetime-range-picker-footer { + margin-top: var(--ti-common-space-3x); + padding-top: var(--ti-common-space-10); + border-top: var(--ti-date-picker-line-width) var(--ti-common-border-style-solid) var(--ti-date-picker-line-color); + width: 100%; + clear: both; + .box-sizing(border-box); + .user-select(); + .clearfix(); + + .ti3-date-picker-footer-btn { + float: right; + &:last-child { + margin-right: var(--ti-common-space-10); + } + } +} +.ti3-datetime-range-picker-footer.ti3-datetime-range-select-time { + margin-top: 0; + border-top: none; +} + +.ti3-datetime-range-picker-footer-right { + float: right; +} + +.ti3-datetime-range-picker-footer-left { + float: left; + margin-left: var(--ti-datetime-range-picker-footer-left); // select组件左边距是10px,设置margin-left是为了与面板其它内容左对齐 + ::ng-deep ti-select { + width: auto; + ti-dominator .ti3-overflow-padding { + padding-right: 0; + } + } +} +.ti3-date-time-range-customize-container { + float: left; + padding-right: var(--ti-common-space-4x); + width: var(--ti-common-size-20x); + border-right: var(--ti-date-picker-line-width) var(--ti-common-border-style-solid) var(--ti-date-picker-line-color); +} +.ti3-date-time-range-dateedit { + display: inline-block; + width: calc(((var(--ti-common-size-9x) * 7 - var(--ti-common-space-2x)) / 2) - 2px); + margin-right: var(--ti-common-space-2x); +} +::ng-deep .ti3-date-time-range-end-edit, +::ng-deep .ti3-date-time-range-begin-edit { + input[tiText] { + border-color: var(--ti-common-color-line-active) !important; + } +} + +::ng-deep .ti3-date-time-range-endtime-edit, +::ng-deep .ti3-date-time-range-begintime-edit { + border-color: var(--ti-common-color-line-active) !important; +} + +.ti3-date-time-range-timeedit { + display: inline-block; + width: calc(((var(--ti-common-size-9x) * 7 - var(--ti-common-space-2x)) / 2) - 2px); +} +.ti3-datetime-range-panel-container { + float: right; + padding-left: var(--ti-common-space-4x); +} diff --git a/src/dominator/demo/project.json b/src/dominator/demo/project.json new file mode 100644 index 0000000..4f3769a --- /dev/null +++ b/src/dominator/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/dominator/demo", + "sourceRoot": "src/dominator/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/dominator", + "index": "src/dominator/demo/src/index.html", + "main": "src/dominator/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/dominator/demo/tsconfig.app.json", + "assets": ["src/dominator/demo/src/favicon.ico", "src/dominator/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "dominator-demo:build:production" + }, + "development": { + "browserTarget": "dominator-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js dominator" + } + ] + } + } + } +} diff --git a/src/dominator/demo/src/app/AppComponent.ts b/src/dominator/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/dominator/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/dominator/demo/src/app/AppModule.ts b/src/dominator/demo/src/app/AppModule.ts new file mode 100644 index 0000000..b08e0d5 --- /dev/null +++ b/src/dominator/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { DominatorTestModule } from './dominator/DominatorTestModule'; + +@NgModule({ + imports: [ + DominatorTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/dominator/demo/src/app/IndexComponent.ts b/src/dominator/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..53c7321 --- /dev/null +++ b/src/dominator/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { DominatorTestModule } from './dominator/DominatorTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = DominatorTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/dominator/demo/src/app/app.html b/src/dominator/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/dominator/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/dominator/demo/src/app/dominator/DominatorDefaultComponent.ts b/src/dominator/demo/src/app/dominator/DominatorDefaultComponent.ts new file mode 100644 index 0000000..f0657c0 --- /dev/null +++ b/src/dominator/demo/src/app/dominator/DominatorDefaultComponent.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './dominator-default.html' +}) +export class DominatorDefaultComponent { + isFocused: boolean = false; + disabled: boolean = false; + selected: any = { + id: 'haha', + name: '很长很长很长很长很长很长很长很长很长很长很长很长myemail@example.com很长很长', + pic: 'pic.png' + }; + searchWord: string = ''; + myLogs: Array = []; + onSearch(event: string): void { + this.searchWord = event; + this.myLogs = [...this.myLogs, `搜索内容:${event}`]; + } + + change1(): void { + this.selected = { + id: 'haha', + name: '很长很长很长很长很长很长很长很长很长很长很长很长myemail@example.com很长很长', + pic: 'pic.png' + }; + } + change2(): void { + this.selected = null; + } + change3(): void { + this.selected = { + id: 'haha', + name: '很长很长很长很长很长很长很长很长很长很长很长很长myemail@example.com很长很长', + pic: 'pic.png' + }; + } + change4(): void { + this.selected = undefined; + } + + changeDisabled(): void { + this.disabled = !this.disabled; + } +} diff --git a/src/dominator/demo/src/app/dominator/DominatorTestModule.ts b/src/dominator/demo/src/app/dominator/DominatorTestModule.ts new file mode 100644 index 0000000..3302248 --- /dev/null +++ b/src/dominator/demo/src/app/dominator/DominatorTestModule.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiDominatorModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { DominatorDefaultComponent } from './DominatorDefaultComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiDominatorModule, DemoLogModule, RouterModule.forChild(DominatorTestModule.ROUTES)], + declarations: [DominatorDefaultComponent] +}) +export class DominatorTestModule { + static readonly LINKS: Array = [{ href: 'components/TiDominatorComponent.html', label: 'Dominator' }]; + static readonly ROUTES: Routes = [{ path: 'dominator/dominator-basic', component: DominatorDefaultComponent }]; +} diff --git a/src/dominator/demo/src/app/dominator/dominator-default.html b/src/dominator/demo/src/app/dominator/dominator-default.html new file mode 100644 index 0000000..1d2b949 --- /dev/null +++ b/src/dominator/demo/src/app/dominator/dominator-default.html @@ -0,0 +1,57 @@ + + + + + + + +

Select组件数据双向绑定展示区域

+
+
+ 1.用例1的当前选中项为:
{{selected | json}}
+
+
+

+
+ + + + +

2.自定义选中项模板

+ + + {{item.id}}+{{item.name}}+{{item.pic}} + + +
+

3.inputtable=true

+

事件日志:

+ + + +
+ + + + + + + diff --git a/src/dominator/demo/src/favicon.ico b/src/dominator/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/dominator/demo/src/index.html b/src/dominator/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/dominator/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/dominator/demo/src/main.ts b/src/dominator/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/dominator/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/dominator/demo/tsconfig.app.json b/src/dominator/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/dominator/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/dominator/lib/index.ts b/src/dominator/lib/index.ts new file mode 100644 index 0000000..4fdadbd --- /dev/null +++ b/src/dominator/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiDominatorModule'; diff --git a/src/dominator/lib/ng-package.json b/src/dominator/lib/ng-package.json new file mode 100644 index 0000000..7d99456 --- /dev/null +++ b/src/dominator/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/dominator", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/dominator/lib/package.json b/src/dominator/lib/package.json new file mode 100644 index 0000000..b1c682d --- /dev/null +++ b/src/dominator/lib/package.json @@ -0,0 +1,16 @@ +{ + "name": "@opentiny/ng-dominator", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-tag": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/dominator/lib/project.json b/src/dominator/lib/project.json new file mode 100644 index 0000000..869d3db --- /dev/null +++ b/src/dominator/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/dominator/lib", + "sourceRoot": "src/dominator/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/dominator"], + "options": { + "project": "src/dominator/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/dominator"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js dominator" + }, + { + "command": "ng default-build dominator" + }, + { + "command": "node build/clear-default-theme.js dominator" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/dominator && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build dominator && ng pack dominator && node build/publish.js dominator --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/dominator/lib/src/TiDominatorComponent.ts b/src/dominator/lib/src/TiDominatorComponent.ts new file mode 100644 index 0000000..924aa21 --- /dev/null +++ b/src/dominator/lib/src/TiDominatorComponent.ts @@ -0,0 +1,277 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + Input, + NgZone, + Output, + Renderer2, + TemplateRef, + ViewChild, + ViewChildren, + QueryList +} from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiPositionType, Util } from '@opentiny/ng-utils'; +import { TiTagComponent } from '@opentiny/ng-tag'; +import packageInfo from '../package.json'; + +/** + * @ignore + */ +@Component({ + selector: 'ti-dominator', + templateUrl: './dominator.html', + styleUrls: ['./dominator.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-select-dominator-container]': 'true', + '[class.ti3-multiselect-container]': 'multiple' + }, + providers: [TiFormComponent.getValueAccessor(TiDominatorComponent)] +}) +export class TiDominatorComponent extends TiFormComponent { + // 删除 buttonStyle 接口,规范中按钮样式只有三角图标样式 + @Input() labelKey: string; // 呈现的内容(无删除按钮) TODO: 含删除按钮的场景,扩展为Array + @Input() placeholder: string; // 组件的预留文本 + @Input() multiple: boolean = false; // 单选多选 + @Input() selectedTipPosition: TiPositionType = 'auto'; // 选中项文本超出时tip展开方向 + /** + * @ignore + * tag的最大宽,TpSearchbox使用 + */ + @Input() tagMaxWidth: string; + /** + * 设置dominator最多显示几行,默认2行, multiple为true时生效 + */ + @Input() maxLine: number = 2; + /** + * 设置是否显示已选项的个数,默认不显示,multiple为true时生效 + * + */ + @Input() showSelectedNumber: boolean = false; + /** + * 设置是否显示已选中项个数的tip提示 + * + */ + @Input() showSelectedNumberTip: boolean = false; + /** + * 设置已选中项个数tip的方向 + * + */ + @Input() selectedNumberTipPosition: TiPositionType = 'bottom'; + /** + * 设置dominator 内容的最大高度,默认是两行的高度,multiple为true时生效,此参数设置后maxLine接口失效 + */ + @Input() maxHeight: string; + @Input() type: 'tagsinput' | string; + /** + * 下拉选择框是否开启清除已选项功能 + */ + @Input() clearable: boolean = false; + + @Input() tipMaxWidth: string; + /** + * @ignore + * 是否可输入 + */ + @Input() inputtable: boolean = false; + @Output() readonly delete: EventEmitter = new EventEmitter(); + /** + * 当下拉选择框开启清除已选项功能时,点击清除按钮时触发 + * + */ + @Output() readonly clear: EventEmitter = new EventEmitter(); + /** + * 传出输入内容 + */ + @Output() readonly inputChange: EventEmitter = new EventEmitter(); + + @ContentChild(TemplateRef, { static: true }) firstTemplate: TemplateRef; + @ContentChild('item', { static: true }) itemTemplate: TemplateRef; + @ContentChild('placeholder') placeholderTemplate: TemplateRef; + @ViewChild('input', { static: false }) inputRef: ElementRef; + @ViewChildren('tag') tagComs: QueryList; + public inputModel: string = ''; + public isInputFocus: boolean = false; // 控制输入框和dominator文本区域的样式状态 + public hasInputModel: boolean = false; // 控制输入框和dominator文本区域的样式状态 + protected versionInfo: string = super.getVersion(packageInfo); + + private isMouseIn: boolean = false; + private selectOptionLine: number; + constructor( + protected hostRef: ElementRef, + protected renderer: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + private zone: NgZone + ) { + super(hostRef, renderer, changeDetectorRef); + } + + ngOnInit(): void { + super.ngOnInit(); + // 每行tag的高度24px, +2 是因为 .ti3-select-dominator-text 元素上下padding + if (this.showSelectedNumber) { + this.selectOptionLine = this.maxLine === 1 ? 1 : this.maxLine - 1; + } else { + this.selectOptionLine = this.maxLine; + } + + this.maxHeight = this.multiple ? this.maxHeight : ''; + } + + ngDoCheck(): void { + super.ngDoCheck(); + // 修复问题:OnPush的综合搜索,绑定的ngModel值内容变化,引用未变时,综合搜索显示不变。 + // 详见tp-serchbox-table用例,更改表格某列过滤项,综合搜索应该立即改变。 + this.changeDetectorRef.markForCheck(); + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + if (this.inputRef?.nativeElement) { + this.setFocusableElems([this.nativeElement, this.inputRef.nativeElement]); + } else { + this.setFocusableElems([this.nativeElement]); + } + this.zone.runOutsideAngular(() => { + // 内部mousedown事件冒泡至根节点 + this.renderer.listen(this.nativeElement, 'mousedown', (event: MouseEvent): void => { + if (this.disabled) { + event.preventDefault(); // 不能聚焦,不能click + } + }); + if (this.clearable) { + this.renderer.listen(this.nativeElement, 'mouseenter', (): void => { + if (this.disabled || Util.isEmptyString(this.model)) { + return; + } + this.zone.run(() => { + this.isMouseIn = true; + this.changeDetectorRef.markForCheck(); + }); + }); + this.renderer.listen(this.nativeElement, 'mouseleave', (): void => { + if (this.isMouseIn) { + this.zone.run(() => { + this.isMouseIn = false; + this.changeDetectorRef.markForCheck(); + }); + } + }); + } + }); + } + /** + * 兼容旧版: + * 3.1.2版select只能内嵌一个模板,无命名。 + * 新版可以内嵌2个模板,示例书写要求都命名。 + * 但需要兼容旧版无命名测试用例。 + */ + ngAfterContentInit(): void { + super.ngAfterContentInit(); + // 如果item模板为空,那么把第一个出现的无标签(无footer等标签)模板作为item + if (!this.itemTemplate && this.firstTemplate !== this.placeholderTemplate) { + this.itemTemplate = this.firstTemplate; + } + } + + ngAfterViewChecked() { + super.ngAfterViewChecked(); + + if (this.multiple && this.tagComs.first?.nativeElement && !this.maxHeight) { + if (typeof getComputedStyle !== 'undefined') { + const tagStyle: CSSStyleDeclaration = getComputedStyle(this.tagComs.first?.nativeElement); + const dominatorStyle: CSSStyleDeclaration = getComputedStyle(this.nativeElement); + + const tagHeight = parseInt(tagStyle.height, 10); + const tagBorderWidth = parseInt(tagStyle.borderWidth, 10) * 2; + const dominatorMargin = parseInt(dominatorStyle.getPropertyValue('--ti-dominator-multiselect-tag-margin'), 10) * 2; + const dominatorPadding = parseInt(dominatorStyle.getPropertyValue('--ti-dominator-multiselect-container-padding'), 10) * 2; + + this.maxHeight = this.selectOptionLine * (tagHeight + tagBorderWidth + dominatorMargin) + dominatorPadding + 'px'; + this.changeDetectorRef.detectChanges(); + } + } + } + + /** + * @ignore 获取选中项呈现个数的tip匹配 + */ + public getTipConfig(): string { + return this.showSelectedNumberTip ? this.model?.map((item: any) => item[this.labelKey]).toString() : ''; + } + /** + * 用户点击叉号,删除一个项目 + * @param item + * @returns + */ + public onDelete(item: any): void { + if (item.disabled === true) { + return; + } + // 删除选中 + const index: number = this.model.indexOf(item); + if (index !== -1) { + this.model.splice(index, 1); + } + this.delete.emit({ + item: item, + model: this.model + }); + // 强行向外通知model改变。 + this.model = this.model.concat(); + } + /** + * @ignore + */ + public onClickClear(event: MouseEvent): void { + event.stopPropagation(); + this.model = this.multiple ? [] : null; + this.clear.emit(); + } + public showClear(): boolean { + const isShow: boolean = this.clearable && !this.disabled && (this.isMouseIn || this.focused); + + return this.multiple ? isShow && this.model && this.model.length !== 0 : isShow && this.model; + } + /** + * @ignore + */ + public onInputFocus(): void { + this.isInputFocus = true; + } + /** + * @ignore + */ + public onInputBlur(): void { + this.hasInputModel = false; + this.isInputFocus = false; + this.inputModel = ''; + } + /** + * @ignore + */ + public inputModelChange(event: string): void { + if (event?.length > 0) { + this.hasInputModel = true; + } else if (event?.length === 0) { + this.hasInputModel = false; + } + this.inputChange.emit(event); + } +} diff --git a/src/dominator/lib/src/TiDominatorModule.ts b/src/dominator/lib/src/TiDominatorModule.ts new file mode 100644 index 0000000..835928d --- /dev/null +++ b/src/dominator/lib/src/TiDominatorModule.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiDominatorComponent } from './TiDominatorComponent'; +import { TiTagModule } from '@opentiny/ng-tag'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiLocaleModule, TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule, FormsModule, TiIconModule, TiTipModule, TiLocaleModule, TiTagModule], + exports: [TiDominatorComponent], + declarations: [TiDominatorComponent] +}) +export class TiDominatorModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiDominatorComponent } from './TiDominatorComponent'; diff --git a/src/dominator/lib/src/dominator.html b/src/dominator/lib/src/dominator.html new file mode 100644 index 0000000..e6e68b2 --- /dev/null +++ b/src/dominator/lib/src/dominator.html @@ -0,0 +1,105 @@ + +
+ + +
+ + + + + {{model[labelKey]}} + + {{placeholder}} +
+
+ + + + + + + {{item[labelKey]}} + + + + + + + + + + + + +
{{placeholder}}
+
+
+ + + +
+ + +
+ + +
+ + +
+ {{ 'tiDominator.selected' | tiTranslate }}{{model.length}} +
+ + + + diff --git a/src/dominator/lib/src/dominator.less b/src/dominator/lib/src/dominator.less new file mode 100644 index 0000000..85aabf3 --- /dev/null +++ b/src/dominator/lib/src/dominator.less @@ -0,0 +1,188 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-dominator-container-height: var(--ti-common-size-7x); + --ti-dominator-container-border-size: calc(var(--ti-common-border-weight-normal) * 2); // 边框 + --ti-dominator-select-box-width: calc( + var(--ti-dominator-container-height) - var(--ti-dominator-container-border-size) + ); // 右侧区域的宽度与高度一致,容器高度 - 边框 + --ti-dominator-multiselect-selected-oneline-padding: 5px; // 单行时已选项个数上下padding + --ti-dominator-multiselect-container-padding: var(--ti-common-space-1); // 多选内间距 + --ti-dominator-multiselect-tag-margin: var(--ti-common-space-1); + --ti-dominator-fold-icon-width: var(--ti-common-size-2x); + --ti-dominator-fold-icon-height: calc(var(--ti-dominator-fold-icon-width) / 1.6); // 按照比例计算 8 / 5 = 1.6 + --ti-dominator-padding-vertical: var(--ti-common-space-0); // 配置化时高度需要去除上下间距 +} + +:host.ti3-select-dominator-container { + position: relative; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-transparent); + margin: calc(-1 * var(--ti-common-border-weight-normal)); + text-align: left; + height: var(--ti-dominator-container-height); + line-height: calc( + var(--ti-dominator-container-height) - var(--ti-dominator-container-border-size) - var(--ti-dominator-padding-vertical) + ); + width: ~'calc(100% + var(--ti-dominator-container-border-size))'; + display: inline-block; + vertical-align: middle; + text-align: left; + // 隔离作用,防止继承下来的white-space属性值影响 codeclub #1437 + white-space: normal; + .box-sizing(border-box); + cursor: pointer; + + &:focus { + outline: none; + } +} + +:host ti-tag { + margin: var(--ti-dominator-multiselect-tag-margin); + max-width: calc(100% - var(--ti-common-border-weight-normal) * 2 - var(--ti-dominator-multiselect-tag-margin) * 2); +} + +:host[disabled] { + &.ti3-select-dominator-container { + cursor: not-allowed !important; + } + .ti3-select-dominator-dropdown-btn:after { + border-top-color: var(--ti-common-color-icon-disabled) !important; + } + .ti3-select-dominator-text { + color: var(--ti-common-color-text-disabled); + } + .ti3-multiselect-box-cell { + border: 1px solid var(--ti-common-color-line-disabled); + } +} + +.ti3-overflow-padding { + padding: var(--ti-dominator-padding-vertical) 0 var(--ti-dominator-padding-vertical) var(--ti-common-space-10); + height: calc(100% - var(--ti-dominator-padding-vertical) * 2); // 需要设置该样式,因为当select选中项为空时,heihgt需要撑大 + .ellipsis(); +} +/** + * 输入框默认状态:透明不可见, + 聚焦后:dominator文本变为placeholder色,看起来像是输入框的placeholder + 输入后:变为白色背景,dominator文本不可见, + */ +.ti3-dominator-input { + display: inline-block; + position: absolute; + height: 100%; + width: calc(100% - var(--ti-dominator-select-box-width) - var(--ti-common-space-10)); + outline: none; + border: none; + vertical-align: top; + cursor: inherit; + box-sizing: border-box; + padding: var(--ti-common-space-10); + top: 0; + background-color: transparent; +} +.ti3-dominator-input-isediting { + background-color: var(--ti-common-color-bg-white-normal); +} + +.ti3-select-dominator-text { + height: 100%; + color: var(--ti-common-color-text-primary); + .box-sizing(border-box); + border: none; + display: inline-block; + border-radius: var(--ti-common-border-radius-normal); //为防止放大情况下,输入框内角不圆滑情况,因此此处加该样式 + text-decoration: none; + width: calc(100% - var(--ti-dominator-select-box-width)); + vertical-align: top; +} +// 考虑提出到最公共样式 +.ti3-placeholder { + color: var(--ti-common-color-text-disabled); +} + +.ti3-select-dominator-dropdown-btn { + width: var(--ti-dominator-select-box-width); + border-radius: var(--ti-common-border-radius-0) var(--ti-common-border-radius-normal) var(--ti-common-border-radius-normal) + var(--ti-common-border-radius-0); + height: calc(var(--ti-dominator-container-height) - var(--ti-dominator-container-border-size)); + float: right; + .box-sizing(border-box); + + &:after { + .triangle-down(var(--ti-dominator-fold-icon-width), var(--ti-dominator-fold-icon-height), var(--ti-common-color-icon-normal)); + content: ''; + position: absolute; + top: calc( + (var(--ti-dominator-container-height) - var(--ti-dominator-container-border-size) - var(--ti-dominator-fold-icon-height)) / 2 + ); // 下拉三角距上边框10px,不能动态计算,多选dominator的高是动态变化的 + right: calc(var(--ti-dominator-select-box-width) / 2 - var(--ti-dominator-fold-icon-width) / 2); + } +} +// 箭头图标是after伪元素,旋转的应该是伪元素 +:host[dropshow] .ti3-select-dominator-dropdown-btn::after { + .rotate(180deg); +} +.ti3-select-dominator-clear-btn { + &:extend(.ti3-select-dominator-dropdown-btn); + font-size: var(--ti-common-size-3x); + color: var(--ti-common-color-icon-normal); + text-align: center; + line-height: calc(var(--ti-dominator-container-height) - var(--ti-dominator-container-border-size)); + &:hover { + color: var(--ti-common-color-icon-hover); + } +} +/*********************以下是多选***********************************/ +:host.ti3-multiselect-container { + height: auto; + min-height: var(--ti-dominator-container-height); + line-height: 1; + overflow: hidden; + + & > .ti3-select-dominator-text { + max-height: calc( + 2 * var(--ti-dominator-container-height) - var(--ti-dominator-container-border-size) - + var(--ti-dominator-multiselect-container-padding) * 2 + ); + line-height: calc( + var(--ti-dominator-container-height) - var(--ti-dominator-container-border-size) - var(--ti-dominator-multiselect-container-padding) * + 2 + ); // 行高 = dominator总高 - 上下2px的border - 上下2px的padding + padding: var(--ti-dominator-multiselect-container-padding); + overflow-y: auto; + vertical-align: middle; + &::-webkit-scrollbar-track { + margin: 0; // 在dominator中定制没有上下边距 + } + &::after { + content: ''; + overflow: hidden; + display: block; + } + &.ti3-tags-input-container { + width: 100%; + } + } + & > .ti3-multiselect-container-oneline { + overflow: hidden; + } + + .ti3-multiselect-selected { + display: inline-block; + padding: var(--ti-common-space-base) 0 var(--ti-common-space-base) var(--ti-common-space-3x); + } + .ti3-multiselect-selected-oneline { + padding-top: var(--ti-dominator-multiselect-selected-oneline-padding); + padding-bottom: var(--ti-dominator-multiselect-selected-oneline-padding); // 一行时需要和tag的高度一直,不然上下会留灰色条 + position: absolute; + top: calc( + (var(--ti-dominator-select-box-width) - var(--ti-common-font-size-base)) / 2 - + var(--ti-dominator-multiselect-selected-oneline-padding) + ); // (dominator高度-文本的高度)/2 - 文本上padding + right: calc( + var(--ti-dominator-fold-icon-width) + var(--ti-common-space-2x) + var(--ti-common-space-base) + ); // 小三角的宽度8px + 右侧间距8px + 4px距离小三角间距 + background: linear-gradient(90deg, hsla(0, 0%, 100%, 0), var(--ti-common-color-bg-white-normal) var(--ti-common-space-3x)); + } +} diff --git a/src/dominator/lib/src/i18n/TiDominatorWords.ts b/src/dominator/lib/src/i18n/TiDominatorWords.ts new file mode 100644 index 0000000..365fc3d --- /dev/null +++ b/src/dominator/lib/src/i18n/TiDominatorWords.ts @@ -0,0 +1,5 @@ +export interface TiDominatorWords { + tiDominator: { + selected: string; + }; +} diff --git a/src/dominator/lib/src/i18n/en_US.ts b/src/dominator/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..be93dba --- /dev/null +++ b/src/dominator/lib/src/i18n/en_US.ts @@ -0,0 +1,7 @@ +import { TiDominatorWords } from './TiDominatorWords'; + +export const en_US: TiDominatorWords = { + tiDominator: { + selected: 'Selected' + } +}; diff --git a/src/dominator/lib/src/i18n/es_US.ts b/src/dominator/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..e312893 --- /dev/null +++ b/src/dominator/lib/src/i18n/es_US.ts @@ -0,0 +1,7 @@ +import { TiDominatorWords } from './TiDominatorWords'; + +export const es_US: TiDominatorWords = { + tiDominator: { + selected: 'Seleccionados' + } +}; diff --git a/src/dominator/lib/src/i18n/fr_FR.ts b/src/dominator/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..f45d1c9 --- /dev/null +++ b/src/dominator/lib/src/i18n/fr_FR.ts @@ -0,0 +1,7 @@ +import { TiDominatorWords } from './TiDominatorWords'; + +export const fr_FR: TiDominatorWords = { + tiDominator: { + selected: 'Sélectionné' + } +}; diff --git a/src/dominator/lib/src/i18n/index.ts b/src/dominator/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/dominator/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/dominator/lib/src/i18n/pt_BR.ts b/src/dominator/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..6938759 --- /dev/null +++ b/src/dominator/lib/src/i18n/pt_BR.ts @@ -0,0 +1,7 @@ +import { TiDominatorWords } from './TiDominatorWords'; + +export const pt_BR: TiDominatorWords = { + tiDominator: { + selected: 'Selecionados:' + } +}; diff --git a/src/dominator/lib/src/i18n/zh_CN.ts b/src/dominator/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..2daafeb --- /dev/null +++ b/src/dominator/lib/src/i18n/zh_CN.ts @@ -0,0 +1,7 @@ +import { TiDominatorWords } from './TiDominatorWords'; + +export const zh_CN: TiDominatorWords = { + tiDominator: { + selected: '已选' + } +}; diff --git a/src/drag/demo/project.json b/src/drag/demo/project.json new file mode 100644 index 0000000..2a429fd --- /dev/null +++ b/src/drag/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/drag/demo", + "sourceRoot": "src/drag/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/drag", + "index": "src/drag/demo/src/index.html", + "main": "src/drag/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/drag/demo/tsconfig.app.json", + "assets": ["src/drag/demo/src/favicon.ico", "src/drag/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "drag-demo:build:production" + }, + "development": { + "browserTarget": "drag-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js drag" + } + ] + } + } + } +} diff --git a/src/drag/demo/src/app/AppComponent.ts b/src/drag/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/drag/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/drag/demo/src/app/AppModule.ts b/src/drag/demo/src/app/AppModule.ts new file mode 100644 index 0000000..00c6952 --- /dev/null +++ b/src/drag/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { DragTestModule } from './drag/DragTestModule'; + +@NgModule({ + imports: [ + DragTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/drag/demo/src/app/IndexComponent.ts b/src/drag/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..24b2070 --- /dev/null +++ b/src/drag/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { DragTestModule } from './drag/DragTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = DragTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/drag/demo/src/app/app.html b/src/drag/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/drag/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/drag/demo/src/app/drag/DragBasicComponent.ts b/src/drag/demo/src/app/drag/DragBasicComponent.ts new file mode 100644 index 0000000..7eb19d8 --- /dev/null +++ b/src/drag/demo/src/app/drag/DragBasicComponent.ts @@ -0,0 +1,31 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiDraggableConfig, TiDraggableEvent } from '@opentiny/ng'; + +@Component({ + templateUrl: './drag-basic.html', + styleUrls: ['./dragTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class DragBasicComponent { + dragOption: TiDraggableConfig = { + drag: (event: TiDraggableEvent): void => { + if (event.position.left > 400) { + event.position.left = 400; // 容器的宽度 - 拖拽元素的宽度 + } + + if (event.position.left < 0) { + event.position.left = 0; + } + + if (event.position.top < -10) { + // 上margin 10px + event.position.top = -10; + } + + if (event.position.top > 470) { + // 容器的高度 - 拖拽元素的高度 - 下margin + event.position.top = 470; + } + } + }; +} diff --git a/src/drag/demo/src/app/drag/DragServiceComponent.ts b/src/drag/demo/src/app/drag/DragServiceComponent.ts new file mode 100644 index 0000000..25a25ba --- /dev/null +++ b/src/drag/demo/src/app/drag/DragServiceComponent.ts @@ -0,0 +1,37 @@ +import { Component, ElementRef, ViewChild, ViewEncapsulation } from '@angular/core'; +import { TiDraggableEvent, TiDragService } from '@opentiny/ng'; + +@Component({ + templateUrl: './drag-service.html', + styleUrls: ['./dragTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class DragServiceComponent { + @ViewChild('draggable', { static: true }) private draggableEle: ElementRef; + constructor(private dragService: TiDragService) {} + + ngAfterViewInit(): void { + this.dragService.create({ + helper: this.draggableEle.nativeElement, + drag: (event: TiDraggableEvent): void => { + if (event.position.left > 400) { + event.position.left = 400; // 容器的宽度 - 拖拽元素的宽度 + } + + if (event.position.left < 0) { + event.position.left = 0; + } + + if (event.position.top < -10) { + // 上margin 10px + event.position.top = -10; + } + + if (event.position.top > 470) { + // 容器的高度 - 拖拽元素的高度 - 下margin + event.position.top = 470; + } + } + }); + } +} diff --git a/src/drag/demo/src/app/drag/DragTestComponent.ts b/src/drag/demo/src/app/drag/DragTestComponent.ts new file mode 100644 index 0000000..adfc409 --- /dev/null +++ b/src/drag/demo/src/app/drag/DragTestComponent.ts @@ -0,0 +1,20 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiDraggableConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './drag-test.html', + styleUrls: ['./dragTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class DragTestComponent { + disabled: boolean = false; + options1: TiDraggableConfig = { + axis: 'x' + }; + options2: TiDraggableConfig = { + axis: 'y' + }; + changeDisable(): void { + this.disabled = !this.disabled; + } +} diff --git a/src/drag/demo/src/app/drag/DragTestModule.ts b/src/drag/demo/src/app/drag/DragTestModule.ts new file mode 100644 index 0000000..bd4a144 --- /dev/null +++ b/src/drag/demo/src/app/drag/DragTestModule.ts @@ -0,0 +1,29 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiDraggableModule } from '@opentiny/ng'; + +import { DragTestComponent } from './DragTestComponent'; +import { DragBasicComponent } from './DragBasicComponent'; +import { DragServiceComponent } from './DragServiceComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, ReactiveFormsModule, TiDraggableModule, RouterModule.forChild(DragTestModule.ROUTES)], + declarations: [DragTestComponent, DragBasicComponent, DragServiceComponent] +}) +export class DragTestModule { + static readonly LINKS: Array = [{ href: 'directives/TiDraggableDirective.html', label: 'Draggable' }]; + static readonly ROUTES: Routes = [ + { + path: 'drag/drag-basic', + component: DragBasicComponent + }, + { + path: 'drag/drag-service', + component: DragServiceComponent + }, + { path: 'drag/drag-test', component: DragTestComponent } + ]; +} diff --git a/src/drag/demo/src/app/drag/drag-basic.html b/src/drag/demo/src/app/drag/drag-basic.html new file mode 100644 index 0000000..414a3d7 --- /dev/null +++ b/src/drag/demo/src/app/drag/drag-basic.html @@ -0,0 +1,9 @@ +

描述

+

指令方式用法展示

+

导入

+

import {{ '{' }} TiDraggableModule {{ '}' }} from '@opentiny/ng';

+

start、drag、stop事件参数,参数类型为TiDraggableEvent

+

示例

+
+
absolute div
+
diff --git a/src/drag/demo/src/app/drag/drag-service.html b/src/drag/demo/src/app/drag/drag-service.html new file mode 100644 index 0000000..b884eb1 --- /dev/null +++ b/src/drag/demo/src/app/drag/drag-service.html @@ -0,0 +1,7 @@ +

描述

+

服务方式用法展示

+

新增start、drag、stop事件参数,参数类型为TiDraggableEvent

+

示例

+
+
absolute div
+
diff --git a/src/drag/demo/src/app/drag/drag-test.html b/src/drag/demo/src/app/drag/drag-test.html new file mode 100644 index 0000000..278c8db --- /dev/null +++ b/src/drag/demo/src/app/drag/drag-test.html @@ -0,0 +1,17 @@ +

1.描述

+

测试 tiDragDisabled/tiDraggable

+

2.示例

+

+ +

+
+
test disable
+ +
test axis = 'x'
+
test axis = 'y'
+
test absolute
+
test fixed
+
+
fixed-parent
+
+
diff --git a/src/drag/demo/src/app/drag/dragTest.less b/src/drag/demo/src/app/drag/dragTest.less new file mode 100644 index 0000000..f5c6f69 --- /dev/null +++ b/src/drag/demo/src/app/drag/dragTest.less @@ -0,0 +1,14 @@ +.container { + position: relative; + width: 500px; + height: 500px; + margin-left: 100px; + background-color: var(--ti-common-color-bg-normal); +} +.blockCls { + box-sizing: border-box; + border: 1px solid blue; + width: 100px; + height: 20px; + margin: 10px 0; +} diff --git a/src/drag/demo/src/favicon.ico b/src/drag/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/drag/demo/src/index.html b/src/drag/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/drag/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/drag/demo/src/main.ts b/src/drag/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/drag/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/drag/demo/tsconfig.app.json b/src/drag/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/drag/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/drag/lib/index.ts b/src/drag/lib/index.ts new file mode 100644 index 0000000..b92bae7 --- /dev/null +++ b/src/drag/lib/index.ts @@ -0,0 +1,3 @@ +export * from './src/TiDraggableModule'; +export * from './src/TiDragService'; +export * from './src/TiDragServiceModule'; diff --git a/src/drag/lib/ng-package.json b/src/drag/lib/ng-package.json new file mode 100644 index 0000000..1f90190 --- /dev/null +++ b/src/drag/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/drag", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/drag/lib/package.json b/src/drag/lib/package.json new file mode 100644 index 0000000..d1e76a8 --- /dev/null +++ b/src/drag/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-drag", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/drag/lib/project.json b/src/drag/lib/project.json new file mode 100644 index 0000000..5f0f9f4 --- /dev/null +++ b/src/drag/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/drag/lib", + "sourceRoot": "src/drag/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/drag"], + "options": { + "project": "src/drag/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/drag"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js drag" + }, + { + "command": "ng default-build drag" + }, + { + "command": "node build/clear-default-theme.js drag" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/drag && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build drag && ng pack drag && node build/publish.js drag --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/drag/lib/src/TiDragService.ts b/src/drag/lib/src/TiDragService.ts new file mode 100644 index 0000000..190d199 --- /dev/null +++ b/src/drag/lib/src/TiDragService.ts @@ -0,0 +1,199 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { Util } from '@opentiny/ng-utils'; +import { TiDragServiceModule } from './TiDragServiceModule'; +/** + * @ignore + * + * h5提供拖放特性,后续内部开发或服务开发建议优先使用原生拖放,故10.1.11起,在官网隐藏该组件。 + */ + +// Renderer2注入运行时报错。临时改法,只为通过编译:改为RendererFactory2注入 +// 服务的设计目标,并不涉及到渲染。 +// https://huncode.com/using-renderer2-inside-services-in-angular/ +/** + * 拖拽事件:start drag stop的参数类型 + * + * helper: 可拖拽元素 + * + * position: 设置给helper元素的位置信息 + */ +export interface TiDraggableEvent { + position: { + left: number; + top: number; + }; + helper: Element; +} +/** + * 拖拽配置项参数设置接口 + */ +export interface TiDraggableConfig { + /** + * 可拖拽元素,仅用于服务生成方式的配置 + */ + helper?: Element; + /** + * 拖拽生效元素,即只有当鼠标按下该元素时才可开始拖拽 + */ + handle?: Element; + /** + * 可拖拽坐标轴,可定义为x/y,即只可在x和Y方向拖拽 + */ + axis?: 'x' | 'y' | null; + /** + * 是否可拖拽,仅用于服务生成方式的配置 + */ + disabled?: boolean; + /** + * 拖拽开始事件 + * event参数 + */ + start?(event?: TiDraggableEvent): any; + /** + * 拖拽中事件 + * event参数 + */ + drag?(event?: TiDraggableEvent): any; // 在modal组件中调用该函数,有返回值position: {left: number, top: number} + /** + * 拖拽结束事件 + * event参数 + */ + stop?(event?: TiDraggableEvent): any; +} +/** + * 拖拽服务,适用于可拖拽元素不在html模板中显示声明的情况,除服务方式外,还提供了指令生成方式[TiDraggableDirective]{@link ../directives/TiDraggableDirective.html}, + * 使用该服务时需要引入模块TiDragServiceModule + * + */ +@Injectable({ + providedIn: TiDragServiceModule +}) +export class TiDragService { + private renderer: Renderer2; + constructor(rendererFactory: RendererFactory2, private tiRenderer: TiRenderer) { + this.renderer = rendererFactory.createRenderer(null, null); + } + /** + * 创建拖拽对象 + * + * 函数返回值为 + * { + * destroy: () => void + * } + */ + create(options: TiDraggableConfig): { + destroy(): void; + } { + const helper: Element = options.helper; // 可拖拽元素,原生HTML类型 + const handle: Element = options.handle || helper; // 拖拽生效元素,未定义情况下,默认为helper元素 + // 标志位,用于记录是否是拖拽引起的事件触发,在mousedown和mouseup中分别进行了置位 + let mouseStart: boolean = false; + // 记录初始位置信息 + let originalPagePos: { + pageX: number; + pageY: number; + }; + let originalPos: { + left: number; + top: number; + }; + // 设置给helper元素的位置信息,用于位置设置及事件中参数传递 + let position: { + left: number; + top: number; + }; + // 非定位元素情况下设置left/top无效,因此需先将其设置为relative定位 + // 修复SSR错误:ERROR ReferenceError: getComputedStyle is not defined + const pos: string = typeof getComputedStyle !== 'undefined' ? getComputedStyle(helper).position : ''; + if (!/(fixed|absolute|relative)/.test(pos)) { + this.renderer.setStyle(helper, 'position', 'relative'); + } + // 拖拽时需要用到的mouse事件句柄记录 + let mouseMoveEvt: Function; + let mouseUpEvt: Function; + this.renderer.listen(handle, 'mousedown', (event: MouseEvent) => { + if (options.disabled) { + // 灰化情况下不做处理 + return; + } + mouseStart = true; + // 初始拖拽时鼠标距离页面边距位置(包含滚动条在内) + originalPagePos = { + pageX: event.pageX, + pageY: event.pageY + }; + // 拖拽元素初始位置,由于获取到的位置为'xxpx',因此此处需要转化为数字 + originalPos = position = { + left: parseInt(getComputedStyle(helper).left, 10) || 0, + top: parseInt(getComputedStyle(helper).top, 10) || 0 + }; + // 触发初始拖拽事件 + triggerEvt('start'); + mouseMoveEvt = this.renderer.listen(document, 'mousemove', (evt: MouseEvent) => { + onMouseMove(evt); + }); + mouseUpEvt = this.renderer.listen(document, 'mouseup', (evt: MouseEvent) => { + onMouseUp(evt); + }); + }); + const onMouseMove: (evt: MouseEvent) => void = (evt: MouseEvent): void => { + // 防止非拖拽触发的mousemove事件 + if (!mouseStart) { + return; + } + evt.preventDefault(); // 阻止拖拽过程中的选中处理等操作 + const topPos: number = + options.axis === 'x' ? originalPos.top : evt.pageY - originalPagePos.pageY + originalPos.top - document.body.scrollTop; + const leftPos: number = + options.axis === 'y' ? originalPos.left : evt.pageX - originalPagePos.pageX + originalPos.left - document.body.scrollLeft; + position = { + left: leftPos, + top: topPos + }; + // 触发拖拽时间,该事件中处理拖拽过程中的位置纠正 + triggerEvt('drag'); + this.tiRenderer.setStyles(helper, { + left: `${position.left}px`, + top: `${position.top}px` + }); + }; + const onMouseUp: (evt: MouseEvent) => void = (evt: MouseEvent): void => { + mouseStart = false; + triggerEvt('stop'); + // 拖拽停止后,解绑document上的相关事件 + mouseMoveEvt(); + mouseUpEvt(); + }; + const triggerEvt: (evtType: string) => void = (evtType: string): void => { + if (options[evtType]) { + const ret: object = options[evtType]({ position, helper }); + if (typeof ret === 'object') { + position = { ...ret, ...position }; + } + } + }; + + return { + destroy: (): void => { + if (!Util.isUndefined(mouseMoveEvt)) { + mouseMoveEvt(); + } + if (!Util.isUndefined(mouseUpEvt)) { + mouseUpEvt(); + } + } + }; + } +} diff --git a/src/drag/lib/src/TiDragServiceModule.ts b/src/drag/lib/src/TiDragServiceModule.ts new file mode 100644 index 0000000..c78f1d8 --- /dev/null +++ b/src/drag/lib/src/TiDragServiceModule.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { CommonModule } from '@angular/common'; +// 命名含有Service的Module,都不是对外暴露的Service,是内部使用。外部使用此Service,导入TiDragModule即可 +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule, TiRendererModule] +}) +export class TiDragServiceModule {} diff --git a/src/drag/lib/src/TiDraggableDirective.ts b/src/drag/lib/src/TiDraggableDirective.ts new file mode 100644 index 0000000..7c4ed16 --- /dev/null +++ b/src/drag/lib/src/TiDraggableDirective.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { TiDraggableConfig, TiDragService } from './TiDragService'; +/** + * + * @ignore + * + * h5提供拖放特性,后续内部开发或服务开发建议优先使用原生拖放,故10.1.11起,在官网隐藏该组件。 + * + * 拖拽指令,除指令方式外,还提供了服务生成方式[TiDragService]{@link ../injectables/TiDragService.html} + * + */ +@Directive({ + selector: '[tiDraggable]' +}) +export class TiDraggableDirective implements OnInit, OnChanges, OnDestroy { + /** + * 拖拽配置项 + */ + @Input() tiDraggable: TiDraggableConfig; + /** + * 拖拽是否灰化,可动态修改 + */ + @Input() tiDragDisabled: boolean = false; + private dragInstance: { + destroy(): void; + }; + private options: TiDraggableConfig; + constructor(private hostEle: ElementRef, private dragService: TiDragService) {} + ngOnInit(): void { + this.options = { + ...this.tiDraggable, + helper: this.hostEle.nativeElement, + disabled: this.tiDragDisabled + }; + this.dragInstance = this.dragService.create(this.options); + } + ngOnChanges(changes: SimpleChanges): void { + // 通过直接修改options的方式修改disabled状态 + if (changes['tiDragDisabled'] && !changes['tiDragDisabled'].firstChange) { + this.options.disabled = this.tiDragDisabled; + } + } + ngOnDestroy(): void { + // 修复SSR错误:TypeError: Cannot read property 'destroy' of undefined + this.dragInstance && this.dragInstance.destroy(); + } +} diff --git a/src/drag/lib/src/TiDraggableModule.ts b/src/drag/lib/src/TiDraggableModule.ts new file mode 100644 index 0000000..f96ad87 --- /dev/null +++ b/src/drag/lib/src/TiDraggableModule.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiDraggableDirective } from './TiDraggableDirective'; +import { TiDragServiceModule } from './TiDragServiceModule'; +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule, TiDragServiceModule], + exports: [TiDraggableDirective, TiDragServiceModule], + declarations: [TiDraggableDirective] +}) +export class TiDraggableModule {} +export { TiDraggableDirective } from './TiDraggableDirective'; diff --git a/src/drop/demo/project.json b/src/drop/demo/project.json new file mode 100644 index 0000000..da1f43d --- /dev/null +++ b/src/drop/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/drop/demo", + "sourceRoot": "src/drop/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/drop", + "index": "src/drop/demo/src/index.html", + "main": "src/drop/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/drop/demo/tsconfig.app.json", + "assets": ["src/drop/demo/src/favicon.ico", "src/drop/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "drop-demo:build:production" + }, + "development": { + "browserTarget": "drop-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js drop" + } + ] + } + } + } +} diff --git a/src/drop/demo/src/app/AppComponent.ts b/src/drop/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/drop/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/drop/demo/src/app/AppModule.ts b/src/drop/demo/src/app/AppModule.ts new file mode 100644 index 0000000..ca7e525 --- /dev/null +++ b/src/drop/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { DropTestModule } from './drop/DropTestModule'; + +@NgModule({ + imports: [ + DropTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/drop/demo/src/app/IndexComponent.ts b/src/drop/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..8fb3f44 --- /dev/null +++ b/src/drop/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { DropTestModule } from './drop/DropTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = DropTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/drop/demo/src/app/app.html b/src/drop/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/drop/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/drop/demo/src/app/drop/DropAppendtobodyComponent.ts b/src/drop/demo/src/app/drop/DropAppendtobodyComponent.ts new file mode 100644 index 0000000..4ff8445 --- /dev/null +++ b/src/drop/demo/src/app/drop/DropAppendtobodyComponent.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { TiPositionType } from '@opentiny/ng'; + +@Component({ + templateUrl: './drop-appendtobody.html' +}) +export class DropAppendtobodyComponent { + positions: Array = [ + 'top', + 'top-left', + 'top-right', + 'bottom', + 'bottom-left', + 'bottom-right', + 'left', + 'left-top', + 'left-bottom', + 'right', + 'right-top', + 'right-bottom', + 'center' + ]; + position: TiPositionType = 'top'; + positionFn = () => { + return this.position; + }; + change(item): void { + this.position = item; + } +} diff --git a/src/drop/demo/src/app/drop/DropDefaultComponent.ts b/src/drop/demo/src/app/drop/DropDefaultComponent.ts new file mode 100644 index 0000000..88246b5 --- /dev/null +++ b/src/drop/demo/src/app/drop/DropDefaultComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './drop-default.html' +}) +export class DropDefaultComponent {} diff --git a/src/drop/demo/src/app/drop/DropTestModule.ts b/src/drop/demo/src/app/drop/DropTestModule.ts new file mode 100644 index 0000000..d616e1c --- /dev/null +++ b/src/drop/demo/src/app/drop/DropTestModule.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiDropModule } from '@opentiny/ng'; + +import { DropDefaultComponent } from './DropDefaultComponent'; +import { DropAppendtobodyComponent } from './DropAppendtobodyComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiDropModule, RouterModule.forChild(DropTestModule.ROUTES)], + declarations: [DropDefaultComponent, DropAppendtobodyComponent] +}) +export class DropTestModule { + static readonly LINKS: Array = [{ href: 'components/TiDropComponent.html', label: 'Drop' }]; + static readonly ROUTES: Routes = [ + { path: 'drop/drop-basic', component: DropDefaultComponent }, + { path: 'drop/drop-appendtobody', component: DropAppendtobodyComponent } + ]; +} diff --git a/src/drop/demo/src/app/drop/drop-appendtobody.html b/src/drop/demo/src/app/drop/drop-appendtobody.html new file mode 100644 index 0000000..bc4a0aa --- /dev/null +++ b/src/drop/demo/src/app/drop/drop-appendtobody.html @@ -0,0 +1,27 @@ +

描述

+

referElem:定位参考元素,接口传值表示drop不添加在body上,而是跟随宿主

+

示例

+ +改变位置: +

1.跟随宿主元素

+



+ + + + + +
+
+
+ +

2.宿主元素的祖先元素有fixed定位

+



+
+ + + + +
+
+
+
diff --git a/src/drop/demo/src/app/drop/drop-default.html b/src/drop/demo/src/app/drop/drop-default.html new file mode 100644 index 0000000..10f6144 --- /dev/null +++ b/src/drop/demo/src/app/drop/drop-default.html @@ -0,0 +1,251 @@ + +

Drop组件, 宽度可指定,高度随内容(但可以设置最大高度)。 如果浏览器可用空间不够,内部更改的最大高度值随浏览器空间

+ + +
+
+
+
+改变位置: + + + +

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+
+
+
+ +
+
+ + +

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+ +

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

hahah

+ + +
+                                  + +
+ +

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+

哈奥德发送到发送到发送到发送到发送到发送到发送到发送到到打发斯蒂芬

+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

hahah

diff --git a/src/drop/demo/src/favicon.ico b/src/drop/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/drop/demo/src/index.html b/src/drop/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/drop/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/drop/demo/src/main.ts b/src/drop/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/drop/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/drop/demo/tsconfig.app.json b/src/drop/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/drop/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/drop/lib/index.ts b/src/drop/lib/index.ts new file mode 100644 index 0000000..57f1484 --- /dev/null +++ b/src/drop/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiDropModule'; diff --git a/src/drop/lib/ng-package.json b/src/drop/lib/ng-package.json new file mode 100644 index 0000000..cf4312f --- /dev/null +++ b/src/drop/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/drop", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/drop/lib/package.json b/src/drop/lib/package.json new file mode 100644 index 0000000..76aa470 --- /dev/null +++ b/src/drop/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-drop", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/drop/lib/project.json b/src/drop/lib/project.json new file mode 100644 index 0000000..46d3e8b --- /dev/null +++ b/src/drop/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/drop/lib", + "sourceRoot": "src/drop/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/drop"], + "options": { + "project": "src/drop/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/drop"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js drop" + }, + { + "command": "ng default-build drop" + }, + { + "command": "node build/clear-default-theme.js drop" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/drop && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build drop && ng pack drop && node build/publish.js drop --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/drop/lib/src/TiDropComponent.ts b/src/drop/lib/src/TiDropComponent.ts new file mode 100644 index 0000000..59723e1 --- /dev/null +++ b/src/drop/lib/src/TiDropComponent.ts @@ -0,0 +1,362 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ApplicationRef, + ChangeDetectionStrategy, + Component, + ElementRef, + EmbeddedViewRef, + Input, + Renderer2, + TemplateRef, + ViewChild +} from '@angular/core'; +import { Position, TiBrowser, TiHostLayout, TiPositionResult, TiPositionType, Util } from '@opentiny/ng-utils'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { Subject } from 'rxjs'; +import packageInfo from '../package.json'; + +/** + * @ignore + * 纯下拉面板组件,只有面板,没有内容。因为有时朝上弹,dropdown/dropup合称drop + */ +@Component({ + selector: 'ti-drop', + templateUrl: './drop.html', + styleUrls: ['./drop.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-dropdown-container]': 'true', + '[class.ti3-dropdown-container-border]': 'theme ==="border"', + '[style.height]': 'panelHeight' + } +}) +export class TiDropComponent extends TiBaseComponent { + /** + * 设置下拉距离dominator的距离:默认没有间距 + */ + public static readonly DOMINATOR_SPACE: number = 4; + /** + * 控制下拉框距离浏览器上下边沿的距离为5px,预留一个余量,防止显示不了边框 + */ + private static readonly BROWSER_SPACE: number = 5; + + private onHeadChange: (e: CustomEvent) => void; // console头部高度变化时触发 + + private consoleDataService: any; + /** + * 下拉面板的最大显示宽度,该变量与下拉类组件保持一致 + * + * 1."justified"(默认): 下拉框的宽度与Select组件的宽度保持一致; + * + * 2."auto": 下拉框的宽度根据下拉选项的内容自动撑开; + * + * 3.表示宽度的字符串: 设置固定的下拉框宽度(不小于Select组件的宽度)。例如:"200px" + */ + @Input() panelWidth: string = 'justified'; // 面板宽度 + /** + * 面板最大高度 + */ + @Input() panelMaxHeight: string = '9999px'; // 面板最大高度 + /** + * 面板高度 + */ + @Input() panelHeight: string; + /** + * 参考位置元素 + */ + @Input() dominatorElem: any; // 参考位置元素 + /** + * 空间不足时,依然保持固定高度。 select search等是可变最大高度,日历是固定最大高度。 + * 再就是上下空间都不足时,日历从下方弹出。 + */ + @Input() fixMaxHeight: boolean = false; // 再就是上下空间都不足时,日历从下方弹出。 + /** + * 主题样式控制 + */ + @Input() theme: 'border' | 'noborder' = 'border'; + /** + * 距离宿主元素距离,像素值 + */ + @Input() dominatorSpace: string = TiDropComponent.DOMINATOR_SPACE + 'px'; + /** + * 距离浏览器上或下边沿的距离,像素值 + */ + @Input() browserSpace: string = TiDropComponent.BROWSER_SPACE + 'px'; + /** + * 在定位基础上的水平方向的偏移,向左偏移为负值,向右偏移为正值,像素值 + */ + @Input() hOffset: number = 0; + /** + * 准备去除 + */ + @Input() determinPositionFn: (layout: any) => string; + /** + * 面板对齐方式 + */ + @Input() panelAlign: 'left' | 'right' = 'left'; + /** + * @ignore + * 用于定位的参照元素,如果该接口有值,表示drop不添加在body上,而是跟随宿主元素 + */ + @Input() referElem: Element; + /** + * @ignore + */ + @ViewChild('dropTemplateRef', { static: true }) + dropTemplateRef: TemplateRef; + + /** + * @ignore + */ + public isUp: boolean; // 面板展开的方向 + /** + * @ignore + */ + public dropSubject: Subject = new Subject(); + private posHandles: Array<() => void> = []; + /** + * @ignore + * drop相对dominator的位置,暂不对外,buttonselect使用 + */ + public position: TiPositionType; + public isZIndexModified: boolean = false; + protected versionInfo: string = super.getVersion(packageInfo); + constructor(protected hostRef: ElementRef, protected renderer: Renderer2, private applicationRef: ApplicationRef) { + super(hostRef, renderer); + } + + /** + * 外部接口: 获取当前状态, 只读不写 + */ + public get isShow(): boolean { + return this.hostRef.nativeElement.style.display === 'block'; + } + + public consoleHeaderHeight: number = 0; + + ngOnInit(): void { + super.ngOnInit(); + // console头部高度变化需同步改变drop的定位 + const consoleContext = (window).getConsoleContext && (window).getConsoleContext(); + this.consoleDataService = consoleContext?.get && consoleContext.get({ name: 'safearea' }); + this.onHeadChange = (e: CustomEvent) => { + this.consoleHeaderHeight = e.detail.top; + }; + // 头部高度变化会触发此事件 + if (this.consoleDataService?.onChange) { + this.consoleDataService.onChange(this.onHeadChange); + } + } + + ngOnDestroy(): void { + // 从body上摘除。 + // 如果跟随宿主,宿主销毁的时候drop也会销毁 + this.renderer.removeChild(this.renderer.parentNode(this.hostRef.nativeElement), this.hostRef.nativeElement); + this.unlistenPosition(); + if (this.consoleDataService?.offChange) { + this.consoleDataService.offChange(this.onHeadChange); + } + } + + /** + * 切换面板状态:打开/关闭 + */ + public toggle(): void { + this.isShow ? this.hide() : this.show(); + } + /** + * 打开面板 + */ + public show(): void { + if (this.isShow) { + return; + } + // console初始化首次进来不会触发consoleDataService.onChange,需要根据getSafeArea()设置一次。 + if (this.consoleDataService?.getSafeArea) { + const safeArea: any = this.consoleDataService.getSafeArea(); + this.consoleHeaderHeight = safeArea.top; + } + // 插入DOM树,定位在极远处 + this.appendToDOMFarAway(); + // 先显示,才能再计算 + this.setShow(); + this.setPanelWidth(); + // 设置位置:居下优先,居上其次 + this.setPosition(); + + this.listenPosition(); + this.dropSubject.next(true); + } + /** + * 关闭面板 + */ + public hide(): void { + if (!this.isShow) { + return; + } + this.renderer.setStyle(this.hostRef.nativeElement, 'display', 'none'); + if (this.dominatorElem) { + this.renderer.removeAttribute(this.dominatorElem, 'dropshow'); + } + this.unlistenPosition(); + } + private appendToDOMFarAway(): void { + // 如果不在body上,那么挂在body上。(逻辑应该移入Position.setPosition吧,因为那位置就是按照body定的,不在body会出错。) + // 首次显示,需要添加到body或宿主元素上 + const parentNode: Element = this.referElem || document.body; + if (this.renderer.parentNode(this.hostRef.nativeElement) !== parentNode) { + if (this.referElem) { + const position: string = window.getComputedStyle(this.referElem).position; + if (position === 'static') { + this.renderer.setStyle(this.referElem, 'position', 'relative'); + } + } + this.renderer.appendChild(parentNode, this.hostRef.nativeElement); + const embeddedViewRef: EmbeddedViewRef = this.dropTemplateRef.createEmbeddedView(null); + this.applicationRef.attachView(embeddedViewRef); // 不做此处处理,ng-template中的标签不会解析 + Array.from(embeddedViewRef.rootNodes).forEach((item: any) => { + this.renderer.appendChild(this.hostRef.nativeElement, item); + }); + } + // 设置在极远处(逻辑应该移入Position.setPosition吧) + this.renderer.setStyle(this.hostRef.nativeElement, 'left', '-9999px'); + this.renderer.setStyle(this.hostRef.nativeElement, 'top', '-9999px'); + // 更新max-height为默认值panelMaxHeight,因为上次显示可能更改了max-height + this.renderer.setStyle(this.hostRef.nativeElement, 'max-height', this.panelMaxHeight); + // 设置width,当出现滚动条时,更新width为默认的panelWidth,避免滚动条宽度的影响 + if (this.panelWidth === 'auto') { + this.renderer.setStyle(this.hostRef.nativeElement, 'width', this.panelWidth); + } + } + private setShow(): void { + // 显示才能计算宽度 + this.renderer.setStyle(this.hostRef.nativeElement, 'display', 'block'); + if (this.dominatorElem) { + this.renderer.setAttribute(this.dominatorElem, 'dropshow', ''); + } + } + /** + * 监听位置变化, 并隐藏面板。打开面板时监听,关闭和销毁时取消监听。 + * TODO: 尝试hide改为rePosition + */ + private listenPosition(): void { + this.posHandles = Position.addPosChangeEvts(() => { + this.hide(); + }, this.renderer); + } + private unlistenPosition(): void { + Position.removePosChangeEvts(this.posHandles); + } + /** + * 确定元素的显示样式,包括位置、最大高度、向上或向下 + * @returns 定位结果对象 + */ + public setPosition(): TiHostLayout { + const dominatorSpace: number = parseInt(this.dominatorSpace, 10); + const browserSpace: number = parseInt(this.browserSpace, 10); + const determinPositionFn: (layout: any) => string = Util.isFunction(this.determinPositionFn) + ? this.determinPositionFn + : this.defaultDeterminPositionFn; + + // 设置位置 + const result: TiPositionResult = Position.setPosition({ + targetEle: this.hostRef.nativeElement, + hostEle: this.dominatorElem, + // position: undefined, // 可选参数 + referElem: this.referElem, + hostSpace: dominatorSpace, + browserSpace, + consoleHeaderHeight: this.consoleHeaderHeight, + fixMaxHeight: this.fixMaxHeight, + hOffset: this.hOffset, + determinPositionFn + }); + this.position = result.position; + if (typeof getComputedStyle !== 'undefined' && result.hostLayout.fixedAncestor && !this.isZIndexModified) { + this.isZIndexModified = true; + const fixedAncestorZindex: number = parseInt(getComputedStyle(result.hostLayout.fixedAncestor).zIndex, 10); + if (typeof fixedAncestorZindex === 'number' && fixedAncestorZindex > parseInt(getComputedStyle(this.nativeElement).zIndex, 10)) { + this.renderer.setStyle(this.nativeElement, 'z-index', fixedAncestorZindex); + } + } + + return result.hostLayout; + } + + public resetPosition(): void { + // 记录上一次dominator left值 + const hostLayout: TiHostLayout = this.setPosition(); + const dominatorLastLeft: number = hostLayout && hostLayout.left; + // 记录当前dominator left值 + const dominatorCurLeft: number = Position.getHostEleLayout(this.dominatorElem).left; + + // dominator发生水平位移且面板处于打开状态时,需重新定位 + // 主要场景:下拉面板数据量变动引起body出现竖向滚动条,dominator发生水平方向偏移; + if (dominatorLastLeft !== dominatorCurLeft) { + if (this.isShow) { + this.setPosition(); + } + } + } + + private setPanelWidth(): void { + const panelWidthNum: number = parseInt(this.panelWidth, 10); + const scrollWidth: number = this.hostRef.nativeElement.offsetWidth - this.hostRef.nativeElement.clientWidth; + const dominatorElemWidth: number = this.dominatorElem.offsetWidth; + let width: string; + if (!isNaN(panelWidthNum)) { + width = panelWidthNum + 'px'; + } else if (this.panelWidth === 'auto') { + width = 'auto'; + // Fix bug: 非IE下滚动条会覆盖部分内容 + if (!TiBrowser.isIE()) { + // 需要重置宽度设置,根据下拉面板的真实宽度确定是否需要增加滚动条宽度 + width = null; + // 有滚动条出现且文本较长时,需要再增加滚动条的宽度,否则内容显示不全 + if (scrollWidth) { + width = this.hostRef.nativeElement.offsetWidth + scrollWidth + 'px'; + } + } + const minWidth: string = dominatorElemWidth + 'px'; + this.renderer.setStyle(this.hostRef.nativeElement, 'min-width', minWidth); + } else { + // 默认宽度设置,包含justified + width = dominatorElemWidth + 'px'; + } + // 设置 + this.renderer.setStyle(this.hostRef.nativeElement, 'width', width); + } + + // 决定上下位置的函数 + private defaultDeterminPositionFn: (layout: any) => string = (layout: any) => { + const dominatorSpace: number = parseInt(this.dominatorSpace, 10); + const browserSpace: number = parseInt(this.browserSpace, 10); + const needHeight: number = layout.targetLayout.height + dominatorSpace + browserSpace; + if (layout.avilableLayout.bottom >= needHeight) { + // 下方空间足够,向下展开 + return this.panelAlign === 'left' ? 'bottom-left' : 'bottom-right'; + } else if (layout.avilableLayout.top >= needHeight) { + // 上方空间足够,向上展开 + return this.panelAlign === 'left' ? 'top-left' : 'top-right'; + } else if (layout.avilableLayout.bottom >= layout.avilableLayout.top) { + // 下方空间较大,则向下展开 + // 因为日历组件存在问题,最大高度不压缩,极端情况时会显示在top为负显示不全。所以日历组件尽量向下弹出。 + return this.panelAlign === 'left' ? 'bottom-left' : 'bottom-right'; + } + + return this.panelAlign === 'left' ? 'top-left' : 'top-right'; // 向上展开 + }; +} + +// offsetWidth属性可以返回对象的padding+border+width属性值之和, 是整数。与jQuery的outerWidth()完全相同,outerWidth(true)带有外边距。 +// Element.clientWidth 属性表示元素的内部宽度,以像素计。该属性包括内边距,但不包括垂直滚动条(如果有)、边框和外边距。 diff --git a/src/drop/lib/src/TiDropModule.ts b/src/drop/lib/src/TiDropModule.ts new file mode 100644 index 0000000..232673a --- /dev/null +++ b/src/drop/lib/src/TiDropModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiDropComponent } from './TiDropComponent'; +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule], + exports: [TiDropComponent], + declarations: [TiDropComponent] +}) +export class TiDropModule {} +export { TiDropComponent } from './TiDropComponent'; diff --git a/src/drop/lib/src/drop.html b/src/drop/lib/src/drop.html new file mode 100644 index 0000000..d79de8f --- /dev/null +++ b/src/drop/lib/src/drop.html @@ -0,0 +1,3 @@ + + + diff --git a/src/drop/lib/src/drop.less b/src/drop/lib/src/drop.less new file mode 100644 index 0000000..d6f2de7 --- /dev/null +++ b/src/drop/lib/src/drop.less @@ -0,0 +1,15 @@ +@import '../../../themes/basic/base-all.less'; +:host.ti3-dropdown-container { + display: none; + position: absolute; //与Tiny2 fix不同 + .box-sizing(border-box); + color: var(--ti-common-color-text-primary); + z-index: 100; + background-color: var(--ti-common-color-bg-white-normal); + box-shadow: var(--ti-common-shadow-2-down); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); +} +:host.ti3-dropdown-container-border { + border-radius: var(--ti-common-border-radius-normal); +} diff --git a/src/droplist/demo/project.json b/src/droplist/demo/project.json new file mode 100644 index 0000000..6de446d --- /dev/null +++ b/src/droplist/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/droplist/demo", + "sourceRoot": "src/droplist/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/droplist", + "index": "src/droplist/demo/src/index.html", + "main": "src/droplist/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/droplist/demo/tsconfig.app.json", + "assets": ["src/droplist/demo/src/favicon.ico", "src/droplist/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "droplist-demo:build:production" + }, + "development": { + "browserTarget": "droplist-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js droplist" + } + ] + } + } + } +} diff --git a/src/droplist/demo/src/app/AppComponent.ts b/src/droplist/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/droplist/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/droplist/demo/src/app/AppModule.ts b/src/droplist/demo/src/app/AppModule.ts new file mode 100644 index 0000000..96864a9 --- /dev/null +++ b/src/droplist/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { DroplistTestModule } from './droplist/DroplistTestModule'; + +@NgModule({ + imports: [ + DroplistTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/droplist/demo/src/app/IndexComponent.ts b/src/droplist/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..7b0cbf7 --- /dev/null +++ b/src/droplist/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { DroplistTestModule } from './droplist/DroplistTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = DroplistTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/droplist/demo/src/app/app.html b/src/droplist/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/droplist/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/droplist/demo/src/app/droplist/DroplistDefaultComponent.ts b/src/droplist/demo/src/app/droplist/DroplistDefaultComponent.ts new file mode 100644 index 0000000..6af3a88 --- /dev/null +++ b/src/droplist/demo/src/app/droplist/DroplistDefaultComponent.ts @@ -0,0 +1,47 @@ +import { AfterViewInit, Component, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { TiDroplistComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: 'droplist-default.html' +}) +export class DroplistDefaultComponent implements OnInit { + model: any; + model1: any; + model2: any; + model3: any; + options: Array = []; + options1: Array = []; + + ngOnInit(): void { + for (let i: number = 0; i < 5; i++) { + const option: any = { + id: String(i), + label: 'label_' + i, + pic: `pic${i}.png` + }; + this.options.push(option); + } + + for (let i: number = 0; i < 2000; i++) { + const option: any = { + id: String(i), + label: 'label_' + i, + pic: `pic${i}.png` + }; + this.options1.push(option); + } + } + + onFocus(droplist: TiDroplistComponent): void { + droplist.show(); + } + + onBlur(droplist: TiDroplistComponent): void { + droplist.hide(); + } + + changeNoData(): void { + this.options = []; + this.options1 = []; + } +} diff --git a/src/droplist/demo/src/app/droplist/DroplistTestModule.ts b/src/droplist/demo/src/app/droplist/DroplistTestModule.ts new file mode 100644 index 0000000..9d04b67 --- /dev/null +++ b/src/droplist/demo/src/app/droplist/DroplistTestModule.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiDroplistModule } from '@opentiny/ng'; + +import { DroplistDefaultComponent } from './DroplistDefaultComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiDroplistModule, RouterModule.forChild(DroplistTestModule.ROUTES)], + declarations: [DroplistDefaultComponent] +}) +export class DroplistTestModule { + static readonly LINKS: Array = [{ href: 'components/TiDropListComponent.html', label: 'TiDropList' }]; + static readonly ROUTES: Routes = [{ path: 'droplist/droplist-basic', component: DroplistDefaultComponent }]; +} diff --git a/src/droplist/demo/src/app/droplist/droplist-default.html b/src/droplist/demo/src/app/droplist/droplist-default.html new file mode 100644 index 0000000..8c402fd --- /dev/null +++ b/src/droplist/demo/src/app/droplist/droplist-default.html @@ -0,0 +1,31 @@ +

示例

+ +

1.basic 下拉没有滚动条:

+
+国家: + + +

+ +

2.basic 下拉有滚动条

+
+国家: + + +

+ +

3.虚拟滚动,没有滚动条:

+
+国家: + + +

+ +

4.虚拟滚动,有滚动条:

+
+国家: + + +

+ + diff --git a/src/droplist/demo/src/favicon.ico b/src/droplist/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/droplist/demo/src/index.html b/src/droplist/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/droplist/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/droplist/demo/src/main.ts b/src/droplist/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/droplist/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/droplist/demo/tsconfig.app.json b/src/droplist/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/droplist/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/droplist/lib/index.ts b/src/droplist/lib/index.ts new file mode 100644 index 0000000..0094ba1 --- /dev/null +++ b/src/droplist/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiDroplistModule'; diff --git a/src/droplist/lib/ng-package.json b/src/droplist/lib/ng-package.json new file mode 100644 index 0000000..645a9fa --- /dev/null +++ b/src/droplist/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/droplist", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/droplist/lib/package.json b/src/droplist/lib/package.json new file mode 100644 index 0000000..983c21f --- /dev/null +++ b/src/droplist/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "@opentiny/ng-droplist", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-list": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/droplist/lib/project.json b/src/droplist/lib/project.json new file mode 100644 index 0000000..fb27df4 --- /dev/null +++ b/src/droplist/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/droplist/lib", + "sourceRoot": "src/droplist/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/droplist"], + "options": { + "project": "src/droplist/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/droplist"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js droplist" + }, + { + "command": "ng default-build droplist" + }, + { + "command": "node build/clear-default-theme.js droplist" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/droplist && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build droplist && ng pack droplist && node build/publish.js droplist --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/droplist/lib/src/TiDroplistComponent.ts b/src/droplist/lib/src/TiDroplistComponent.ts new file mode 100644 index 0000000..789aa41 --- /dev/null +++ b/src/droplist/lib/src/TiDroplistComponent.ts @@ -0,0 +1,458 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + HostListener, + Input, + NgZone, + Output, + Renderer2, + SimpleChanges, + TemplateRef, + ViewChild +} from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiDropComponent } from '@opentiny/ng-drop'; +import { TiListComponent, TiListScrollLoad } from '@opentiny/ng-list'; +import { TiKeymap } from '@opentiny/ng-utils'; +import { TiPositionType } from '@opentiny/ng-utils'; +import { TiLocale } from '@opentiny/ng-locale'; +import packageInfo from '../package.json'; + +/** + * @ignore + * 下拉面板带有数组列表组件,用于aucomplete等。它有子类TiDropsearchComponent + */ +@Component({ + selector: 'ti-droplist', + templateUrl: './droplist.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiDroplistComponent)] +}) +export class TiDroplistComponent extends TiFormComponent { + /** + * 带搜索框情况下需要去除的高度 + */ + protected static readonly SEARCHBOX_EXCLUDE_HEIGHT: number = 28; // 下拉框中的搜索框的高度 + /** + * 默认高度8条 DEFAULT_LIST_MAX_HEIGHT + */ + protected defaultListMaxHeight: number = TiListComponent.OPTION_DEFAULT_HEIGHT * 8 + TiListComponent.CONTAINER_VERTICAL_PADDING; + /** + * 面板中,除去list外,其它元素的占位高度 + */ + protected heightExcludeList: number = 2; + protected versionInfo: string = super.getVersion(packageInfo); + /** + * drop面板最大高度 + */ + private dropMaxHeight: number; + /** + * drop面板自定义底部高度 + */ + private footerHeight: number = 0; + /** + * droplist依附的元素,需要使用该元素进行droplist的定位处理 + */ + @Input() dominatorElem: HTMLElement; + /** + * 是否多选 + */ + @Input() multiple: boolean = false; + /** + * 下拉面板的最大显示宽度,该变量与下拉类组件保持一致 + * + * 1."justified"(默认): 下拉框的宽度与Select组件的宽度保持一致; + * + * 2."auto": 下拉框的宽度根据下拉选项的内容自动撑开; + * + * 3.表示宽度的字符串: 设置固定的下拉框宽度(不小于Select组件的宽度)。例如:"200px" + */ + @Input() panelWidth: 'justified' | 'auto' | string = 'justified'; + /** + * 下拉面板的最大显示高度,溢出时则出滚动条,该变量名与下拉类组件保持一致 + */ + @Input() panelMaxHeight: string; + @Input() options: Array; + @Input() labelKey: string = 'label'; + @Input() noDataText: string = TiLocale.getLocaleWords().tiList.noDataText; + @Input() tipPosition: TiPositionType = 'right'; + @Input() dominatorSpace: string = TiDropComponent.DOMINATOR_SPACE + 'px'; + @Input() panelAlign: 'left' | 'right' = 'left'; + /** + * 内部接口,用作suggestion时type传入suggestion,默认值default + */ + @Input() type: 'default' | 'suggestion' = 'default'; + /** + * 大小样式,default/small. 默认值default + */ + @Input() size: 'default' | 'small' = 'default'; + /** + * 选中选项后面板是否保持显示, 默认值false + */ + @Input() isShowAfterSelect: boolean = false; + + /** + * 是否开启虚拟滚动, 默认值false + */ + @Input() virtual: boolean = false; + /** + * @ignore + * TODO: 暂不对外开放该接口,后续根据使用场景进行优化 + * 当开启虚拟滚动时,可配置单条选项的高度(单位是px), 默认值30 + */ + @Input() itemSize: number = TiListComponent.OPTION_DEFAULT_HEIGHT; + + @Input() tipMaxWidth: string; + /** + * @ignore + * + * + * idKey指定的属性的值相等时即认为 option 选项是选中的。选中项 ngModel 中的数据(modelOption对象)跟 options 数据集中的选项(option对象)之间对应相等关系的依据属性。当 + * modelOption中的 idKey 设置的属性的值 与 option 中的 idKey 设置的属性的值相等时,则认为 modelOption 和 option 是对应的相等关系,即认为 option 选项是选中的。 + * + * 默认当 modelOption === option 或者 modelOption 中的 labelKey 设置的属性的值 与 option 中的 labelKey 设置的属性的值相等时,则认为 option 选项是选中的。 + * + * 该接口暂不对外开放,后续如果业务场景labelKey对应的值确实有重复时,再对外开放该接口。 + */ + @Input() idKey: string; + /** + * @ignore + * 用于定位的参照元素,如果该接口有值,表示drop不添加在body上,而是跟随宿主元素 + */ + @Input() referElem: Element; + + /** + * 选中事件,向外通知option数据 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + /** + * 下拉列表滚动到底部的回调 + * + * + */ + @Output() readonly scrollToBottom: EventEmitter = new EventEmitter(); + + @ViewChild(TiDropComponent, { static: true }) dropCom: TiDropComponent; + @ViewChild(TiListComponent, { static: false }) listCom: TiListComponent; + @ViewChild('footer', { static: false }) footerElemRef: ElementRef; + /** + * 自定义list中的item的模板 + */ + @ContentChild(TemplateRef, { static: true }) itemTemplate: TemplateRef; + /** + * 储存donimator bottom旧值 + */ + private dominatorLastBottom: number = undefined; + /** + * 储存donimator left旧值 + */ + private dominatorLastLeft: number = undefined; + /** + * @ignore + * list 是否已初始化。在打开面板前 listInited 一直为false,第一次打开面板后 listInited 就一直为true + */ + public listInited: boolean = false; + /** + * 虚拟滚动容器的高度 + */ + private virtualScrollViewportMaxHeight: number; + /** + * 承接 CdkVirtualScrollViewport 实例上的 checkViewportSize 方法供组件在下拉框高度发生变化时调用 + */ + private checkVirtualScrollViewportSize: () => void; + private unlistenKeydownFn: () => void; + constructor( + protected hostRef: ElementRef, + protected render: Renderer2, + public changeDetectorRef: ChangeDetectorRef, + protected zone: NgZone + ) { + super(hostRef, render, changeDetectorRef); + } + + ngOnInit(): void { + super.ngOnInit(); + this.listenKeydown(this.dominatorElem); + } + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + // dominatorElem 改变后重新添加监听 + if (changes['dominatorElem'] && !changes['dominatorElem'].firstChange) { + this.unlistenKendown(); + this.listenKeydown(this.dominatorElem); + } + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + // 解除按键监听 + this.unlistenKendown(); + } + /** + * 当做生命周期用吧,监听模型值变化。包括writeValue和this.model=赋值 这两个时刻。 + * @param model ngModel值 + */ + protected ngOnModelChange(model: any): void { + super.ngOnModelChange(model); + if (!this.multiple || !this.dominatorElem) { + // 多选才会有dominator行高切换 + return; + } + if (this.dominatorLastBottom === undefined && this.dominatorElem) { + // 第一次只记录位置,不走进重定位逻辑 + // 修复SSR错误:ERROR TypeError: this.dominatorElem.getBoundingClientRect is not a function + if (typeof this.dominatorElem.getBoundingClientRect === 'function') { + this.dominatorLastBottom = this.dominatorElem.getBoundingClientRect().bottom; + this.dominatorLastLeft = this.dominatorElem.getBoundingClientRect().left; + } + + return; + } + + this.zone.runOutsideAngular(() => { + setTimeout(() => { + // TODO:这里setTimeout能否去除? + if (!this.dominatorElem) { + return; + } + // ngModel更新(点击checkbox,或者点击叉叉号)后,dominator有可能一行/两行变换,所以更新下拉面板位置。 + // 甚至donimator本身位置也会变动。(比如测试用例中元素变动,引起donimator本身变动 ???) + if ( + this.dominatorLastBottom !== this.dominatorElem.getBoundingClientRect().bottom || + this.dominatorLastLeft !== this.dominatorElem.getBoundingClientRect().left + ) { + if (this.dropCom.isShow) { + // 仅当高度变化时,重定位。 + this.rePosition(); + } + this.dominatorLastBottom = this.dominatorElem.getBoundingClientRect().bottom; + this.dominatorLastLeft = this.dominatorElem.getBoundingClientRect().left; + } + }, 0); + }); + } + + /** + * 监听keydown + * @param focusElem 焦点元素 + */ + protected listenKeydown(focusElem: HTMLElement): void { + if (!focusElem) { + return; + } + this.unlistenKeydownFn = this.renderer.listen(focusElem, 'keydown', (event: KeyboardEvent) => { + if (!this.isShow) { + return; + } + // 10.0.2删除 KEY_SPACE 空格快捷键的响应 + switch (event.keyCode) { + case TiKeymap.KEY_ESCAPE: // Esc键,仅可关闭 + case TiKeymap.KEY_TAB: // Tab键,仅可关闭 + event.preventDefault(); // 阻止触发blurFn + this.hide(); + break; + case TiKeymap.KEY_ARROW_UP: // 向上箭头,上移选项 + case TiKeymap.KEY_ARROW_DOWN: // 向下箭头,下移选项 + case TiKeymap.KEY_ENTER: // ENTER键 相当于click + case TiKeymap.KEY_NUMPAD_ENTER: // ENTER键(苹果数字小键盘) + this.listCom.onKeydown(event); + break; + default: + break; + } + // 如果droplist响应了按键,那么就不再冒泡。 + event.stopPropagation(); + }); + } + protected unlistenKendown(): void { + if (this.unlistenKeydownFn) { + this.unlistenKeydownFn(); + } + } + /** + * 外部接口: 获取当前状态, 只读不写 + */ + public get isShow(): boolean { + return this.dropCom.isShow; + } + + /** + * 打开面板 + */ + public show(): void { + if (!this.isShow) { + let inited: boolean = false; + if (!this.listInited) { + // 初始化 TiListComponent, ngIf 绑定 listInited 值 + this.listInited = true; + this.changeDetectorRef.detectChanges(); + this.processCdkVirtualScroll(); + inited = true; + } + // 每次打开面板前需要重置list的高度确保drop高度不受其影响 + this.initListMaxHeight(); + // 打开面板 + this.dropCom.show(); + // 根据drop的最大高度设置list的最大高度 + this.restyleListMaxHeight(); + + if (!this.listCom) { + return; + } + if (inited && this.model) { + // 首次打开面板时,在此处model值还没传递到listcom组件中去,如果直接调用 listCom.scrollToSelected 方法时,该方法中获取的 model(listComponent中) 值不准确, + // 为了保证 listCom.scrollToSelected 方法中 model 值准确,从而滚动条定位准确,这里需要延时处理下 + setTimeout(() => { + this.listCom.scrollToSelected(); + }, 0); + } else { + this.listCom.scrollToSelected(); + } + // IE滚动条Bug的监听 + this.listCom.listenIESrollbarBug(); + } + } + /** + * 关闭面板的处理 + */ + public hide(): void { + // this.listCom.hide(); // 隐藏时list也需要做处理:清除当前hover项状态 + if (this.isShow) { + this.dropCom.hide(); // 关闭面板 + // 解除IE滚动条Bug的监听 + if (this.listCom) { + this.listCom.unlistenIESrollbarBug(); + } + } + } + /** + * @ignore + */ + public onScrollToBottom(scrollLoad: TiListScrollLoad): void { + this.scrollToBottom.emit(scrollLoad); + } + /** + * 重新设置元素位置 + */ + public rePosition(optionsChange?: boolean): void { + if (!this.dropCom || !this.dropCom.isShow) { + return; + } + if (optionsChange) { + this.dropCom.resetPosition(); + } else { + this.dropCom.setPosition(); + } + + this.restyleListMaxHeight(); + } + /** + * 根据drop的压缩情况,设置list的max-height + */ + public restyleListMaxHeight(): void { + if (!this.listCom) { + return; + } + // 计算自定义底部的高度 + if (this.footerElemRef && this.footerElemRef.nativeElement) { + const rect: any = this.footerElemRef.nativeElement.getBoundingClientRect(); + if (rect.height) { + this.footerHeight = rect.height; + } + } + // 如果drop被压缩,则根据drop最大高度设置当前list最大高度。 + const dropCurMaxHeight: number = parseInt(this.dropCom.nativeElement.style.maxHeight, 10); + let dropMaxHeightAdapted: number = this.dropMaxHeight + this.footerHeight; + if (!isNaN(dropCurMaxHeight) && dropCurMaxHeight < dropMaxHeightAdapted) { + dropMaxHeightAdapted = dropCurMaxHeight; + const computedListMaxHeight: number = dropMaxHeightAdapted - this.heightExcludeList - this.footerHeight; + // 设置list max-height + this.renderer.setStyle(this.listCom.nativeElement, 'max-height', computedListMaxHeight + 'px'); + } + // 面板高度发生变化时需要虚拟滚动相关重新计算实际需要渲染的option选项 + if (this.virtual) { + const listMaxHeight: number = parseInt(this.listCom.nativeElement.style.maxHeight, 10); + if (listMaxHeight !== this.virtualScrollViewportMaxHeight && this.checkVirtualScrollViewportSize) { + this.virtualScrollViewportMaxHeight = listMaxHeight; + this.checkVirtualScrollViewportSize.apply(this.listCom.virtualScrollViewport); + } + } + } + // 初始化list最大高度 + private initListMaxHeight(): void { + // 此处做容错处理,如果list面板不存在,不用进行设置 + if (!this.listCom) { + return; + } + this.dropMaxHeight = this.panelMaxHeight ? parseInt(this.panelMaxHeight, 10) : this.defaultListMaxHeight + this.heightExcludeList; + this.renderer.setStyle(this.listCom.nativeElement, 'max-height', this.dropMaxHeight - this.heightExcludeList + 'px'); + } + // 鼠标或enter点击选项后,组件隐藏 + public onSelect(option: any): void { + this.select.emit(option); + // nextlevel只用在省市区regionselect组件场景,选中省、市级别面板不关闭 + if (!this.isShowAfterSelect && !this.multiple && !option.nextLevel) { + this.hide(); + } + } + + private processCdkVirtualScroll(): void { + if (!this.virtual || !this.listCom?.virtualScrollViewport) { + return; + } + + // 因为接下来组件会覆写 CdkVirtualScrollViewport 实例上的 checkViewportSize 方法,所以这里先承接 CdkVirtualScrollViewport 实例上 + // 的 checkViewportSize 方法供组件在下拉框高度发生变化时调用来重新计算实际需要渲染的option选项。 + if (!this.checkVirtualScrollViewportSize) { + this.checkVirtualScrollViewportSize = this.listCom.virtualScrollViewport.checkViewportSize; + } + + // 在 @angular/CDK 提供的虚拟滚动处理中,当 window 的 resize 和 orientationchange 事件触发时会调用 CdkVirtualScrollViewport 实例上的 checkViewportSize 方法, + // 而这时组件的下拉框display为none,那么计算虚拟滚动容器的高度是不准确的(计算出来的实际需要渲染的option选项也就是不准确的,影响下一次打开面板时面板中呈现的数据), + // 由于组件下拉框display为none时其实是不可见的,不需要做任何处理,所以这里直接覆写置空 CdkVirtualScrollViewport 实例上的 checkViewportSize 方法。 + if (this.listCom.virtualScrollViewport.checkViewportSize !== this.nullFn) { + this.listCom.virtualScrollViewport.checkViewportSize = this.nullFn; + } + + // 在 @angular/CDK 提供的虚拟滚动处理中,在 虚拟滚动容器初始化 / options改变 / window的resize和orientationchange事件 这三种场景下会计算实际需要渲染的option选项, + // 在这个计算过程中会调用其 CdkVirtualScrollViewport 实例上的 getViewportSize 方法来获取滚动容器的高度。 + // 由于下拉框中的options改变时,下拉框可能并没有打开(display为none),这时获取到的滚动容器的高度的高度是不准确的。 + // 因为上述原因,这里直接接管覆写了 CdkVirtualScrollViewport 实例上的 getViewportSize 方法。 + if (this.listCom.virtualScrollViewport.getViewportSize !== this.getCdkViewportSize) { + this.listCom.virtualScrollViewport.getViewportSize = this.getCdkViewportSize; + } + } + + private nullFn: () => void = (): void => {}; + // 虚拟滚动容器初始化,options改变, list的最大高度改变(打开面板或重定位) 时都会调用到该方法 + private getCdkViewportSize: () => number = (): number => { + if (!this.listCom) { + return; + } + const contentHeight: number = this.listCom.options?.length > 0 ? this.itemSize * this.listCom.options.length : 0; + if (isNaN(this.virtualScrollViewportMaxHeight)) { + return contentHeight; + } + const contentMaxHeight: number = + this.multiple && this.listCom?.selectAll && this.listCom.options?.length > 0 + ? this.virtualScrollViewportMaxHeight - TiListComponent.OPTION_DEFAULT_HEIGHT + : this.virtualScrollViewportMaxHeight; + + return contentHeight < contentMaxHeight ? contentHeight : contentMaxHeight; + }; +} diff --git a/src/droplist/lib/src/TiDroplistModule.ts b/src/droplist/lib/src/TiDroplistModule.ts new file mode 100644 index 0000000..b1218d8 --- /dev/null +++ b/src/droplist/lib/src/TiDroplistModule.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiDroplistComponent } from './TiDroplistComponent'; +import { TiListModule } from '@opentiny/ng-list'; +import { TiDropModule } from '@opentiny/ng-drop'; + +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule, FormsModule, TiListModule, TiDropModule], + exports: [TiDroplistComponent], + declarations: [TiDroplistComponent] +}) +export class TiDroplistModule {} +export { TiDroplistComponent } from './TiDroplistComponent'; diff --git a/src/droplist/lib/src/droplist.html b/src/droplist/lib/src/droplist.html new file mode 100644 index 0000000..e5f66c7 --- /dev/null +++ b/src/droplist/lib/src/droplist.html @@ -0,0 +1,34 @@ + + + + + + + + {{item[labelKey]}} + + + diff --git a/src/dropsearch/lib/index.ts b/src/dropsearch/lib/index.ts new file mode 100644 index 0000000..184621b --- /dev/null +++ b/src/dropsearch/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiDropsearchModule'; diff --git a/src/dropsearch/lib/ng-package.json b/src/dropsearch/lib/ng-package.json new file mode 100644 index 0000000..cee34e5 --- /dev/null +++ b/src/dropsearch/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/dropsearch", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/dropsearch/lib/package.json b/src/dropsearch/lib/package.json new file mode 100644 index 0000000..f8c7085 --- /dev/null +++ b/src/dropsearch/lib/package.json @@ -0,0 +1,17 @@ +{ + "name": "@opentiny/ng-dropsearch", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-searchbox": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-droplist": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-list": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/dropsearch/lib/project.json b/src/dropsearch/lib/project.json new file mode 100644 index 0000000..cdd0ec9 --- /dev/null +++ b/src/dropsearch/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/dropsearch/lib", + "sourceRoot": "src/dropsearch/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/dropsearch"], + "options": { + "project": "src/dropsearch/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/dropsearch"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js dropsearch" + }, + { + "command": "ng default-build dropsearch" + }, + { + "command": "node build/clear-default-theme.js dropsearch" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/dropsearch && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build dropsearch && ng pack dropsearch && node build/publish.js dropsearch --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/dropsearch/lib/src/TiDropsearchComponent.ts b/src/dropsearch/lib/src/TiDropsearchComponent.ts new file mode 100644 index 0000000..8eb5480 --- /dev/null +++ b/src/dropsearch/lib/src/TiDropsearchComponent.ts @@ -0,0 +1,282 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + Component, + ContentChild, + EventEmitter, + Input, + Output, + SimpleChanges, + TemplateRef, + ViewChild +} from '@angular/core'; +import { Subject, Subscription } from 'rxjs'; +import { debounceTime, switchMap } from 'rxjs/operators'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiSearchboxNotsearchComponent } from '@opentiny/ng-searchbox'; +import { TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiDropComponent } from '@opentiny/ng-drop'; +import { TiDroplistComponent } from '@opentiny/ng-droplist'; +import { TiLocale } from '@opentiny/ng-locale'; +import packageInfo from '../package.json'; +/** + * @ignore + * 下拉面板带有可搜索的数组组件,主要用于select,表格列设置。 + */ +@Component({ + selector: 'ti-dropsearch', + templateUrl: './dropsearch.html', + styleUrls: ['./dropsearch.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiDropsearchComponent)] +}) +export class TiDropsearchComponent extends TiDroplistComponent { + @Input() searchable: boolean = false; + @Input() searchKeys: Array; // 要搜索的字段数组, 默认是labelKey或者全部字段 + @Input() selectAll: boolean = false; // 多选模式下是否有全选功能 + /** + * 面板对齐方式, drop中的panelAlign接口透传出去 + */ + @Input() panelAlign: 'left' | 'right' = 'left'; + + /** + * @ignore + * dominator 到drop的距离 + */ + @Input() dominatorSpace: string = TiDropComponent.DOMINATOR_SPACE + 'px'; + /** + * + * 搜索时,收起下拉面板后是否保留搜索关键字, + */ + @Input() reserveSearchword: boolean = false; + + /** + * @ignore + * + * 后台搜索,传出搜索框的值 + */ + @Output() readonly beforeSearch: EventEmitter = new EventEmitter(); + + @ViewChild(TiSearchboxNotsearchComponent) + searchboxCom: TiSearchboxNotsearchComponent; + @ViewChild(TiDropComponent, { static: true }) dropCom: TiDropComponent; + @ContentChild('footer', { static: true }) footerTemplateRef: TemplateRef; + + public searchWord: string; // 搜索框内单词 + /** + * 搜索结果数组 + */ + public searchResult: Array = new Array(); + private oldSearchable: boolean = null; + public pagination: any; // 没有指定类型是因为如果引入TiPaginationComponent会因为循环引用导致报错 + public focusableElemsInSearch: Array = []; + public focusableElemsInPagination: Array = []; + /** + * @ignore select的beforeSearch有没有被监听,该值为true,dropsearch的beforeSearch才是真正意义的被监听 + */ + public hasBeforeSearchObservers: boolean; + + public isFocusableElemsInPaginationChange: boolean; + + protected versionInfo: string = super.getVersion(packageInfo); + + private searchwordChangeObserve: Subject = new Subject(); + private searchwordChangeSub: Subscription; + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + // 外部修改show属性的处理,初始定义在ngOnInit中处理 + if (changes['options'] && this.options && !changes['options'].firstChange) { + if (this.searchable && this.isShow) { + if (this.hasBeforeSearchObservers) { + this.setSearchResult(this.options); // 后台搜索时会给 options 赋新值 + } else { + this.filterSearchReslut(this.searchboxCom?.model); + } + } + } + + if (changes['searchable']) { + this.heightExcludeList = changes['searchable'].currentValue ? TiDroplistComponent.SEARCHBOX_EXCLUDE_HEIGHT : 2; + } + } + ngOnInit(): void { + super.ngOnInit(); + this.searchResult = this.options; + if (this.hasBeforeSearchObservers) { + this.createSearchwordChangeObserve(); + } + } + + ngDoCheck(): void { + super.ngDoCheck(); + // 修复问题:OnPush的综合搜索,点击value弹开面板,面板显示值是旧的,移动鼠标到面板可以刷新。 + this.changeDetectorRef.markForCheck(); + } + + ngAfterViewChecked(): void { + super.ngAfterViewChecked(); + let focusableElemsChange: boolean; + if (this.searchable !== this.oldSearchable) { + super.unlistenKendown(); + if (this.searchable) { + this.focusableElemsInSearch = this.searchboxCom?.getFocusableElems(); + focusableElemsChange = true; + super.listenKeydown(this.searchboxCom?.getFocusableElems()[0]); + } else { + this.focusableElemsInSearch = []; + super.listenKeydown(this.dominatorElem); + } + this.oldSearchable = this.searchable; + } + if (this.pagination && this.pagination.updateFocusableElems) { + this.focusableElemsInPagination = this.pagination.getFocusableElems(); + this.pagination.updateFocusableElems = false; + this.isFocusableElemsInPaginationChange = true; + focusableElemsChange = true; + } + + if (focusableElemsChange) { + this.setFocusableElems(this.focusableElemsInSearch?.concat(this.focusableElemsInPagination)); + } + } + + ngOnDestroy(): void { + this.searchwordChangeSub && this.searchwordChangeSub.unsubscribe(); + } + private createSearchwordChangeObserve(): void { + this.searchwordChangeSub = this.searchwordChangeObserve + .pipe( + debounceTime(500), + switchMap(() => { + this.beforeSearch.emit(); + + return 'complete'; + }) + ) + .subscribe(); + } + /** + * 覆盖父类show() + */ + public show(): void { + if (this.isShow) { + return; + } + // 清空搜索框并处理下拉列表内容 + if (this.searchable) { + // 更新searchResult的情况是:关闭之前清空搜索词,则后台搜索置为重新请求的数据,前台搜索置为初始值 + if (!this.reserveSearchword) { + this.searchResult = this.options; + } + this.changeDetectorRef.detectChanges(); + super.show(); + // 焦点转移至搜索框 + if (this.searchboxCom) { + this.searchboxCom.focus(); + } + } else { + super.show(); + } + } + /** + * 覆盖父类hide(), 有搜索框时焦点转移 + */ + public hide(): void { + if (this.isShow) { + super.hide(); + this.handleSearchOnHide(); + if (this.searchable) { + this.dominatorElem.focus(); + } + } + } + /** + * 仅hide,不带焦点转移。 + */ + public hideWithoutFocus(): void { + super.hide(); + this.handleSearchOnHide(); + } + + public searchWordChange(searchWord: string): void { + // 1.如果是后台搜索,则对外发送事件,由外部处理 + if (this.hasBeforeSearchObservers) { + this.searchwordChangeObserve.next(null); + + return; + } + // 2.如果是前台搜索,则由内部过滤搜索结果 + this.filterSearchReslut(searchWord); + } + /** + * 前台搜索时使用,查找搜索结果 + */ + private filterSearchReslut(searchWord: string): void { + if (this.options && this.options.length >= 0) { + // 搜索结果临时值。结果默认值,是原数据 + let searchResult: Array = this.options; + // 如果搜索词存在 + if (!Util.isEmptyString(searchWord)) { + // 如果是分组,则找出第二级item集合 + if (this.options[0] && this.listCom.isGroup(this.options[0])) { + searchResult = []; + this.options.forEach((group: any) => { + searchResult = searchResult.concat(group.children); + }); + } + // 在集合中搜索 + searchResult = searchResult.filter((option: any) => { + if (!this.searchKeys) { + // 没有定义searchKeys时,取labelKey + return Util.isString(option[this.labelKey]) && option[this.labelKey].toLowerCase().indexOf(searchWord.toLowerCase()) >= 0; + } else { + // 已定义searchKeys,任一条目匹配即可 + for (const searchKey of this.searchKeys) { + if (Util.isString(option[searchKey]) && option[searchKey].toLowerCase().indexOf(searchWord.toLowerCase()) >= 0) { + return true; + } + } + + return false; + } + }); + } + + this.setSearchResult(searchResult); + } + } + /** + * 搜索后,按需重新定位、设置hoverOption + */ + public setSearchResult(searchResult: Array): void { + const oldSearchResultLength: number = this.searchResult?.length; + this.searchResult = searchResult; + // list个数不同时,重定位下拉位置 + if (this.searchResult.length !== oldSearchResultLength) { + setTimeout(() => { + this.rePosition(true); + this.changeDetectorRef.markForCheck(); + }, 0); + } + } + + private handleSearchOnHide(): void { + if (this.searchable && this.searchWord && !this.reserveSearchword) { + this.searchWord = ''; + if (this.hasBeforeSearchObservers) { + this.searchwordChangeObserve.next(null); + } + } + } +} diff --git a/src/dropsearch/lib/src/TiDropsearchModule.ts b/src/dropsearch/lib/src/TiDropsearchModule.ts new file mode 100644 index 0000000..7adf6c6 --- /dev/null +++ b/src/dropsearch/lib/src/TiDropsearchModule.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiDropsearchComponent } from './TiDropsearchComponent'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiListModule } from '@opentiny/ng-list'; +import { TiSearchboxModule } from '@opentiny/ng-searchbox'; + +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule, FormsModule, TiDropModule, TiListModule, TiSearchboxModule], + exports: [TiDropsearchComponent], + declarations: [TiDropsearchComponent] +}) +export class TiDropsearchModule {} +export { TiDropsearchComponent } from './TiDropsearchComponent'; diff --git a/src/dropsearch/lib/src/dropsearch.html b/src/dropsearch/lib/src/dropsearch.html new file mode 100644 index 0000000..e608fac --- /dev/null +++ b/src/dropsearch/lib/src/dropsearch.html @@ -0,0 +1,52 @@ + + +
+ + +
+ + + + + + + {{item[labelKey]}} + + + +
+ +
+
+
diff --git a/src/dropsearch/lib/src/dropsearch.less b/src/dropsearch/lib/src/dropsearch.less new file mode 100644 index 0000000..b9b6ed7 --- /dev/null +++ b/src/dropsearch/lib/src/dropsearch.less @@ -0,0 +1,5 @@ +@import '../../../themes/basic/base-all.less'; +// 搜索框 +.ti3-dropdown-searchbox { + width: 100% !important; // 强制生效,原来searchbox默认宽度200px +} diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts new file mode 100644 index 0000000..1a9473a --- /dev/null +++ b/src/environments/environment.prod.ts @@ -0,0 +1,7 @@ +export const environment: any = { + production: true, + isWc: false // 是否构建成web component +}; + +window['DEPLOY_URL'] = ''; +window['PUBLIC_URL'] = ''; diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100644 index 0000000..6ddd76a --- /dev/null +++ b/src/environments/environment.ts @@ -0,0 +1,18 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment: any = { + production: false, + isWc: false // 是否构建成web component +}; + +window['DEPLOY_URL'] = ''; +window['PUBLIC_URL'] = ''; + +/* + * In development mode, to ignore zone related error stack frames such as + * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can + * import the following file, but please comment it out in production mode + * because it will have performance impact when throw error + */ diff --git a/src/environments/environment.wc.ts b/src/environments/environment.wc.ts new file mode 100644 index 0000000..5fd507c --- /dev/null +++ b/src/environments/environment.wc.ts @@ -0,0 +1,4 @@ +export const environment: any = { + production: true, + isWc: true // 是否构建成web component +}; diff --git a/src/foldtext/demo/karma.conf.js b/src/foldtext/demo/karma.conf.js new file mode 100644 index 0000000..22d9d1a --- /dev/null +++ b/src/foldtext/demo/karma.conf.js @@ -0,0 +1,41 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [{ type: 'html' }, { type: 'text-summary' }] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/foldtext/demo/project.json b/src/foldtext/demo/project.json new file mode 100644 index 0000000..70ff31e --- /dev/null +++ b/src/foldtext/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/foldtext/demo", + "sourceRoot": "src/foldtext/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/foldtext", + "index": "src/foldtext/demo/src/index.html", + "main": "src/foldtext/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/foldtext/demo/tsconfig.app.json", + "assets": ["src/foldtext/demo/src/favicon.ico", "src/foldtext/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "foldtext-demo:build:production" + }, + "development": { + "browserTarget": "foldtext-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js foldtext" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/foldtext/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/foldtext/demo/tsconfig.spec.json", + "karmaConfig": "src/foldtext/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/foldtext/demo/src/app/AppComponent.ts b/src/foldtext/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/foldtext/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/foldtext/demo/src/app/AppModule.ts b/src/foldtext/demo/src/app/AppModule.ts new file mode 100644 index 0000000..65e9fbc --- /dev/null +++ b/src/foldtext/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { FoldtextTestModule } from './foldtext/FoldtextTestModule'; + +@NgModule({ + imports: [ + FoldtextTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/foldtext/demo/src/app/IndexComponent.ts b/src/foldtext/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..9e0bb78 --- /dev/null +++ b/src/foldtext/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { FoldtextTestModule } from './foldtext/FoldtextTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = FoldtextTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/foldtext/demo/src/app/app.html b/src/foldtext/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/foldtext/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/foldtext/demo/src/app/foldtext/FoldtextBasicComponent.ts b/src/foldtext/demo/src/app/foldtext/FoldtextBasicComponent.ts new file mode 100644 index 0000000..4bbc1ca --- /dev/null +++ b/src/foldtext/demo/src/app/foldtext/FoldtextBasicComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './foldtext-basic.html' +}) +export class FoldtextBasicComponent { + texts: Array = []; + ngOnInit(): void { + this.texts = ['《送元二使安西》——王维(唐)', '渭城朝雨悒轻尘,', '客舍青青柳色新。', '劝君更尽一杯酒,', '西出阳关无故人。']; + } +} diff --git a/src/foldtext/demo/src/app/foldtext/FoldtextTableComponent.ts b/src/foldtext/demo/src/app/foldtext/FoldtextTableComponent.ts new file mode 100644 index 0000000..548dee1 --- /dev/null +++ b/src/foldtext/demo/src/app/foldtext/FoldtextTableComponent.ts @@ -0,0 +1,75 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; +import { Util } from '@opentiny/ng-utils'; + +@Component({ + templateUrl: './foldtext-table.html' +}) +export class FoldtextTableComponent { + // 表格模拟数据 + displayed: Array = []; + srcData: TiTableSrcData; + data: Array = []; + + columns: Array = [ + { + title: '名称', + width: '15%' + }, + { + title: '可用' + }, + { + title: '朝代' + }, + { + title: '是否使用' + }, + { + title: '详细内容', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 5; j++) { + this.data.push(this.createRandomRow(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + // 模拟数据 + private createRandomRow(id: number): TiTableRowData { + const nameList: Array = ['送元二使安西', '送元二使安西']; + const name: string = nameList[this.random(0, 2)] + '_' + this.random(0, 100); + + return { + name_id: { + name, + id: Util.getUniqueId('foldtext') + }, + access: '可用区' + this.random(0, 1000), + status: id % 5 === 0 ? '已冻结' : '使用中', + users: this.getAuthUsers(id), + id + }; + } + + private getAuthUsers(id: number): Array { + return ['《送元二使安西》——王维(唐)', '渭城朝雨悒轻尘,', '客舍青青柳色新。', '劝君更尽一杯酒,', '西出阳关无故人。']; + } + + // 模拟数据 + private random(start: number, end: number): number { + const span: number = end - start; + + return Math.floor(span * Math.random()); + } +} diff --git a/src/foldtext/demo/src/app/foldtext/FoldtextTestModule.ts b/src/foldtext/demo/src/app/foldtext/FoldtextTestModule.ts new file mode 100644 index 0000000..2ef93fe --- /dev/null +++ b/src/foldtext/demo/src/app/foldtext/FoldtextTestModule.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiFoldtextModule, TiTableModule, TiButtonModule } from '@opentiny/ng'; + +import { FoldtextBasicComponent } from './FoldtextBasicComponent'; +import { FoldtextTableComponent } from './FoldtextTableComponent'; + +@NgModule({ + imports: [CommonModule, TiFoldtextModule, TiTableModule, TiButtonModule, RouterModule.forChild(FoldtextTestModule.ROUTES)], + declarations: [FoldtextBasicComponent, FoldtextTableComponent] +}) +export class FoldtextTestModule { + public static readonly ROUTES: Routes = [ + { path: 'foldtext/foldtext-basic', component: FoldtextBasicComponent }, + { path: 'foldtext/foldtext-table', component: FoldtextTableComponent } + ]; +} diff --git a/src/foldtext/demo/src/app/foldtext/foldtext-basic.html b/src/foldtext/demo/src/app/foldtext/foldtext-basic.html new file mode 100644 index 0000000..1cd4ee0 --- /dev/null +++ b/src/foldtext/demo/src/app/foldtext/foldtext-basic.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/foldtext/demo/src/app/foldtext/foldtext-table.html b/src/foldtext/demo/src/app/foldtext/foldtext-table.html new file mode 100644 index 0000000..c161a24 --- /dev/null +++ b/src/foldtext/demo/src/app/foldtext/foldtext-table.html @@ -0,0 +1,25 @@ + +
+ + + + + + + + + + + + + + + +
{{column.title}}
+
{{row.name_id.name}}
+
{{row.name_id.id}}
+
{{row.access}}{{row.status}} + +
+
+
diff --git a/src/foldtext/demo/src/app/foldtext/webdoc/foldtext-demos.js b/src/foldtext/demo/src/app/foldtext/webdoc/foldtext-demos.js new file mode 100644 index 0000000..809a338 --- /dev/null +++ b/src/foldtext/demo/src/app/foldtext/webdoc/foldtext-demos.js @@ -0,0 +1,28 @@ +export default { + column: '1', + demos: [ + { + demoId: 'foldtext-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

foldtext 组件的最简用法。

', + 'en-US': '

Basic usage

' + }, + apis: ['TiFoldtextComponent.properties.texts', 'TiFoldtextComponent.properties.line'] + }, + { + demoId: 'foldtext-table', + name: { + 'zh-CN': '表格中的用法', + 'en-US': '' + }, + desc: { + 'zh-CN': '

foldtext组件在表格中的用法。

', + 'en-US': '

' + } + } + ] +}; diff --git a/src/foldtext/demo/src/app/foldtext/webdoc/foldtext.cn.md b/src/foldtext/demo/src/app/foldtext/webdoc/foldtext.cn.md new file mode 100644 index 0000000..7d81132 --- /dev/null +++ b/src/foldtext/demo/src/app/foldtext/webdoc/foldtext.cn.md @@ -0,0 +1,24 @@ +--- +title: Foldtext 折叠文本 +--- +# Foldtext 折叠文本 + +
+ +Foldtext 将多条文本数据进行折叠展示。   + +```typescript +import { TiFoldtextModule } from '@opentiny/ng'; +``` + +
+ +
+ +Foldtext 折叠文本组件。   + +```typescript +import { TiFoldtextModule } from '@opentiny/ng'; +``` + +
diff --git a/src/foldtext/demo/src/app/foldtext/webdoc/foldtext.en.md b/src/foldtext/demo/src/app/foldtext/webdoc/foldtext.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/foldtext/demo/src/app/foldtext/webdoc/foldtext.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/foldtext/demo/src/favicon.ico b/src/foldtext/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/foldtext/demo/src/index.html b/src/foldtext/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/foldtext/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/foldtext/demo/src/main.ts b/src/foldtext/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/foldtext/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/foldtext/demo/test.ts b/src/foldtext/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/foldtext/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/foldtext/demo/tsconfig.app.json b/src/foldtext/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/foldtext/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/foldtext/demo/tsconfig.spec.json b/src/foldtext/demo/tsconfig.spec.json new file mode 100644 index 0000000..e12f108 --- /dev/null +++ b/src/foldtext/demo/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": ["jasmine"] + }, + "files": ["./test.ts", "../../polyfills.ts"], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/foldtext/lib/index.ts b/src/foldtext/lib/index.ts new file mode 100644 index 0000000..75c3a14 --- /dev/null +++ b/src/foldtext/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiFoldtextModule'; +export * from './src/TiFoldtextComponent'; diff --git a/src/foldtext/lib/ng-package.json b/src/foldtext/lib/ng-package.json new file mode 100644 index 0000000..fe01211 --- /dev/null +++ b/src/foldtext/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/foldtext", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/foldtext/lib/package.json b/src/foldtext/lib/package.json new file mode 100644 index 0000000..35eb03f --- /dev/null +++ b/src/foldtext/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-foldtext", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/foldtext/lib/project.json b/src/foldtext/lib/project.json new file mode 100644 index 0000000..59f7e55 --- /dev/null +++ b/src/foldtext/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/foldtext/lib", + "sourceRoot": "src/foldtext/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/foldtext"], + "options": { + "project": "src/foldtext/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/foldtext"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js foldtext" + }, + { + "command": "ng default-build foldtext" + }, + { + "command": "node build/clear-default-theme.js foldtext" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/foldtext && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build foldtext && ng pack foldtext && node build/publish.js foldtext --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/foldtext/lib/src/TiFoldtextComponent.ts b/src/foldtext/lib/src/TiFoldtextComponent.ts new file mode 100644 index 0000000..8a95197 --- /dev/null +++ b/src/foldtext/lib/src/TiFoldtextComponent.ts @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, Renderer2, SimpleChanges } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiLocale } from '@opentiny/ng-locale'; + +/** + * 折叠文本 + */ +@Component({ + selector: 'ti-foldtext', + templateUrl: './foldtext.html', + styleUrls: ['./foldtext.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiFoldtextComponent extends TiBaseComponent { + localeWords: any = TiLocale.getLocaleWords().tiFoldtext; + + /** + * 文本内容 + */ + @Input() texts: Array = []; + /** + * 折叠时显示的行数 + */ + @Input() line: number = 2; + /** + * @ignore + * “查看全部” 词条 + */ + showMore: string = this.localeWords.showMore; + /** + * @ignore + * “收起” 词条 + */ + showLess: string = this.localeWords.showLess; + + /** + * @ignore + * 显示文本内容的高度 + */ + height: string; + /** + * @ignore + * 是否折叠 + */ + + folded: boolean = true; + // 表格中的默认font-size:12px, 默认line-height:1.5, 所以默认值取18px + private lineHeight: string = '18px'; + constructor(protected hostRef: ElementRef, private renderer2: Renderer2, private changeRef: ChangeDetectorRef) { + super(hostRef, renderer2); + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + + const CSSStyles: CSSStyleDeclaration = getComputedStyle(this.nativeElement); + const lineHeight: string = CSSStyles.getPropertyValue('line-height'); + + if (!Number.isNaN(parseFloat(lineHeight))) { + this.lineHeight = lineHeight; + } + + this.line = this.getLine(); + this.height = this.line * parseFloat(this.lineHeight) + 'px'; + // default模式下,解决ExpressionChangedAfterItHasBeenCheckedError报错问题 + this.changeRef.detectChanges(); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + + if ((changes['texts'] && !changes['texts'].firstChange) || (changes['line'] && !changes['line'].firstChange)) { + this.line = this.getLine(); + this.height = this.line * parseFloat(this.lineHeight) + 'px'; + } + } + /** + * @ignore + * 切换文本的点击事件 + */ + onClick(event: Event): void { + this.folded = !this.folded; + } + + // 当总行数小于折叠时显示行数时,设置折叠时显示行数为总行数 + private getLine(): number { + return this.texts?.length < this.line ? this.texts.length : this.line; + } +} diff --git a/src/foldtext/lib/src/TiFoldtextModule.ts b/src/foldtext/lib/src/TiFoldtextModule.ts new file mode 100644 index 0000000..2c3d473 --- /dev/null +++ b/src/foldtext/lib/src/TiFoldtextModule.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiFoldtextComponent } from './TiFoldtextComponent'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, TiOverflowModule, TiOutlineModule], + exports: [TiFoldtextComponent], + declarations: [TiFoldtextComponent] +}) +export class TiFoldtextModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiFoldtextComponent } from './TiFoldtextComponent'; diff --git a/src/foldtext/lib/src/foldtext.html b/src/foldtext/lib/src/foldtext.html new file mode 100644 index 0000000..f408104 --- /dev/null +++ b/src/foldtext/lib/src/foldtext.html @@ -0,0 +1,16 @@ +
+
+ +
+
+
+ {{folded? showMore:showLess}} ({{texts.length}}) +
diff --git a/src/foldtext/lib/src/foldtext.less b/src/foldtext/lib/src/foldtext.less new file mode 100644 index 0000000..ac152df --- /dev/null +++ b/src/foldtext/lib/src/foldtext.less @@ -0,0 +1,14 @@ +@import '../../../themes/basic/link-no-decoration.less'; + +.ti-foldtext-wraper { + overflow-y: hidden; + ::ng-deep a { + .ti-link-no-decoration(); + } +} +.ti-foldtext-toggle { + color: var(--ti-common-color-text-highlight); + &:hover { + cursor: pointer; + } +} diff --git a/src/foldtext/lib/src/i18n/TiFoldtextWords.ts b/src/foldtext/lib/src/i18n/TiFoldtextWords.ts new file mode 100644 index 0000000..11ea420 --- /dev/null +++ b/src/foldtext/lib/src/i18n/TiFoldtextWords.ts @@ -0,0 +1,6 @@ +export interface TiFoldtextWords { + tiFoldtext: { + showMore: string; + showLess: string; + }; +} diff --git a/src/foldtext/lib/src/i18n/en_US.ts b/src/foldtext/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..121357f --- /dev/null +++ b/src/foldtext/lib/src/i18n/en_US.ts @@ -0,0 +1,8 @@ +import { TiFoldtextWords } from './TiFoldtextWords'; + +export const en_US: TiFoldtextWords = { + tiFoldtext: { + showMore: 'Show more', + showLess: 'Show less' + } +}; diff --git a/src/foldtext/lib/src/i18n/es_US.ts b/src/foldtext/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..ebad442 --- /dev/null +++ b/src/foldtext/lib/src/i18n/es_US.ts @@ -0,0 +1,8 @@ +import { TiFoldtextWords } from './TiFoldtextWords'; + +export const es_US: TiFoldtextWords = { + tiFoldtext: { + showMore: 'Mostrar más', + showLess: 'Mostrar menos' + } +}; diff --git a/src/foldtext/lib/src/i18n/fr_FR.ts b/src/foldtext/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..bdc6b06 --- /dev/null +++ b/src/foldtext/lib/src/i18n/fr_FR.ts @@ -0,0 +1,8 @@ +import { TiFoldtextWords } from './TiFoldtextWords'; + +export const fr_FR: TiFoldtextWords = { + tiFoldtext: { + showMore: 'Voir plus', + showLess: 'Voir moins' + } +}; diff --git a/src/foldtext/lib/src/i18n/index.ts b/src/foldtext/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/foldtext/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/foldtext/lib/src/i18n/pt_BR.ts b/src/foldtext/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..fdfe507 --- /dev/null +++ b/src/foldtext/lib/src/i18n/pt_BR.ts @@ -0,0 +1,8 @@ +import { TiFoldtextWords } from './TiFoldtextWords'; + +export const pt_BR: TiFoldtextWords = { + tiFoldtext: { + showMore: 'Mostrar mais', + showLess: 'Mostrar menos' + } +}; diff --git a/src/foldtext/lib/src/i18n/zh_CN.ts b/src/foldtext/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..dd61cfc --- /dev/null +++ b/src/foldtext/lib/src/i18n/zh_CN.ts @@ -0,0 +1,8 @@ +import { TiFoldtextWords } from './TiFoldtextWords'; + +export const zh_CN: TiFoldtextWords = { + tiFoldtext: { + showMore: '查看全部', + showLess: '收起' + } +}; diff --git a/src/formfield/demo/karma.conf.js b/src/formfield/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/formfield/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/formfield/demo/project.json b/src/formfield/demo/project.json new file mode 100644 index 0000000..d1dc9b3 --- /dev/null +++ b/src/formfield/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/formfield/demo", + "sourceRoot": "src/formfield/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/formfield", + "index": "src/formfield/demo/src/index.html", + "main": "src/formfield/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/formfield/demo/tsconfig.app.json", + "assets": ["src/formfield/demo/src/favicon.ico", "src/formfield/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "formfield-demo:build:production" + }, + "development": { + "browserTarget": "formfield-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js formfield" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/formfield/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/formfield/demo/tsconfig.spec.json", + "karmaConfig": "src/formfield/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/formfield/demo/src/app/AppComponent.ts b/src/formfield/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/formfield/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/formfield/demo/src/app/AppModule.ts b/src/formfield/demo/src/app/AppModule.ts new file mode 100644 index 0000000..3333c25 --- /dev/null +++ b/src/formfield/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { FormfieldTestModule } from './formfield/FormfieldTestModule'; + +@NgModule({ + imports: [ + FormfieldTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/formfield/demo/src/app/IndexComponent.ts b/src/formfield/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..64459c8 --- /dev/null +++ b/src/formfield/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { FormfieldTestModule } from './formfield/FormfieldTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = FormfieldTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/formfield/demo/src/app/app.html b/src/formfield/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/formfield/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/formfield/demo/src/app/formfield/FormfieldColspanRowspanComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldColspanRowspanComponent.ts new file mode 100644 index 0000000..0ca4b9b --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldColspanRowspanComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-colspan-rowspan.html' +}) +export class FormfieldColspanRowspanComponent { + colsNumber: number = 3; + colsGap: Array = ['100px', '120px']; + checkboxValue: boolean = true; + inputValueyle: string = 'hello tiny'; +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldColspanRowspanTestComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldColspanRowspanTestComponent.ts new file mode 100644 index 0000000..271566f --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldColspanRowspanTestComponent.ts @@ -0,0 +1,44 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-colspan-rowspan-test.html' +}) +export class FormfieldColspanRowspanTestComponent { + public title: string = '单列表单'; + public item1: any = { + show: true, + label: '站点名称:', + required: true, + value: '' + }; + public item2: any = { + label: '站点地址:', + required: true, + value: '' + }; + public item3: any = { + show: false, + label: '国家:', + required: true, + verticalAlign: 'middle', + value: '' + }; + public item4: any = { + label: '分布式探测', + required: true, + verticalAlign: 'middle' + }; + public item5: any = { + show: true, + required: true, + verticalAlign: 'middle', + label: '类别' + }; + public button: any = { + okLabel: '确定', + cancelLabel: '取消' + }; + + public myOptions: Array = [{ label: '华北-北京一' }, { label: '华东-上海一' }]; + public mySelected: any = this.myOptions[0].label; +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldColswidthComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldColswidthComponent.ts new file mode 100644 index 0000000..3082af3 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldColswidthComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-colswidth.html' +}) +export class FormfieldColswidthComponent { + contryValue: string = '中国'; + sexValue: string = '男'; + IDValue: string = 'hello tiny'; + emailValue: string = 'hello@example.com'; + nickValue: string = 'tiny'; + + colsNumber: number = 3; + colsGap: Array = ['100px', '120px']; + colsWidth: Array = ['33.33%', '33.33%', '33.33%']; + + changeCols(n: number): void { + this.colsNumber = n; + this.colsWidth = new Array(n); + this.colsWidth.fill(`${String((100 / n).toFixed(2))}%`); + } +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldFooComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldFooComponent.ts new file mode 100644 index 0000000..8350f4d --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldFooComponent.ts @@ -0,0 +1,67 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { TiButtonItem } from '@opentiny/ng'; + +@Component({ + selector: 'ti-formfield-foo', + templateUrl: './formfield-foo.html' +}) +export class FormfieldFooComponent implements OnInit { + title: string = '单列表单'; + item1: any = { + show: true, + label: '时间:', + required: true, + options: [ + { + text: '近1小时', + value: 1 + }, + { + text: '近12小时', + value: 12 + }, + { + text: '近1天', + value: 24 + }, + { + text: '近3天', + value: 72 + } + ] + }; + item2: any = { + label: '容量', + required: true, + value: 'happy' + }; + item3: any = { + show: true, + label: '时光', + required: true + }; + + profileForm: FormGroup; + max: number = 10000; + min: number = 0; + + constructor(private fb: FormBuilder) {} + + ngOnInit(): void { + this.profileForm = this.fb.group({ + time: [undefined], + spinner: [16] + }); + + this.profileForm.get('time').valueChanges.subscribe((option: TiButtonItem) => { + console.log('value', option); + + this.item2.show = option.value > 12; + }); + + this.profileForm.get('spinner').valueChanges.subscribe((value) => { + console.log('spinner', value); + }); + } +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldIndexComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldIndexComponent.ts new file mode 100644 index 0000000..5806dac --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldIndexComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-index.html' +}) +export class FormfieldIndexComponent { + contryValue: string = '中国'; + sexValue: string = '男'; + IDValue: string = 'hello tiny'; + emailValue: string = 'hello@example.com'; + nickValue: string = 'tiny'; + order: Array = [6, 5, 4, 3, 2]; +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldLabelComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldLabelComponent.ts new file mode 100644 index 0000000..541f37f --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldLabelComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +const DEFAULTVALUE = 'hello tiny'; +@Component({ + templateUrl: './formfield-label.html' +}) +export class FormfieldLabelComponent { + labelName: string = '输入框:'; + labelWidth: string = '200px'; + + inputValue: string = DEFAULTVALUE; + customInputValue: string = DEFAULTVALUE; + labelWidthInputValue: string = DEFAULTVALUE; + + clickHandle(): void { + this.labelName = '自定义 Label:'; + } +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldLabelwidthComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldLabelwidthComponent.ts new file mode 100644 index 0000000..921ebb8 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldLabelwidthComponent.ts @@ -0,0 +1,59 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-labelwidth.html' +}) +export class FormfieldLabelwidthComponent { + labelWidth: string = '20px'; + labelWidth1: string = '30px'; + colsWidth: Array = ['100%']; + title: string = '表单1'; + title1: string = '表单2'; + title2: string = '表单3'; + items: any = [ + { + label: '姓名:', + required: true, + value: 'Rose', + index: 0 + }, + { + label: '爱好:', + required: true, + value: '看电影,冲浪,极限挑战', + index: 1 + }, + { + label: '年龄:', + required: false, + value: '18', + index: 3 + } + ]; + + items1: any = [ + { + label: '姓名:', + required: true, + value: 'Rose', + index: 0 + }, + { + label: '爱好:', + required: true, + value: '看电影,冲浪,极限挑战', + index: 1 + }, + { + label: '年龄:', + required: false, + value: '18', + index: 3 + } + ]; + + changeLabelwidth(): void { + this.items1[0].label = 'name 文本超过初始设置宽度'; + this.labelWidth = this.labelWidth1 = '160px'; + } +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldMultiColumnComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldMultiColumnComponent.ts new file mode 100644 index 0000000..c39206c --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldMultiColumnComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-multi-column.html' +}) +export class FormfieldMultiColumnComponent { + contryValue: string = '中国'; + sexValue: string = '男'; + IDValue: string = 'hello tiny'; + emailValue: string = 'hello@example.com'; + nickValue: string = 'tiny'; + colsNumber: number = 3; +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldNestFormfiledComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldNestFormfiledComponent.ts new file mode 100644 index 0000000..91370ea --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldNestFormfiledComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-nest-formfiled.html' +}) +export class FormfieldNestFormfiledComponent { + colsWidth: Array = ['100%']; + colsWidth1: Array = ['50%', '50%']; + colsNumber: number = 2; + item1: any = { + show: true, + label: '姓名:', + required: true + }; + item2: any = { + label: '输入框:', + required: true, + value: 'happy' + }; + item3: any = { + show: false, + label: 'span标签:', + required: true, + verticalAlign: 'middle' + }; + item4: any = { + label: '', + required: true, + verticalAlign: 'middle' + }; + item5: any = { + show: true, + required: true, + verticalAlign: 'middle' + }; + button: any = { + okLabel: '确定', + cancelLabel: '取消' + }; +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldNgforTestComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldNgforTestComponent.ts new file mode 100644 index 0000000..0e598d3 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldNgforTestComponent.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-ngfor-test.html' +}) +export class FormfieldNgforTestComponent { + items: any = [ + { + label: '姓名:', + required: true, + value: 'Rose', + index: 0 + }, + { + label: '爱好:', + required: true, + value: '看电影,冲浪,极限挑战', + index: 1 + }, + { + label: '年龄:', + required: false, + value: '18', + index: 3 + } + ]; + type: string = 'text'; + + addItems(): void { + this.items.splice(2, 0, { + label: '性别:', + required: true, + value: '女', + index: 2 + }); + } +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldRawgapComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldRawgapComponent.ts new file mode 100644 index 0000000..69a05e2 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldRawgapComponent.ts @@ -0,0 +1,38 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-rowgap.html' +}) +export class FormfieldRawgapComponent { + colsWidth: Array = ['50%', '50%']; + public item1: any = { + label: '姓名:', + required: true + }; + public item2: any = { + label: '描述:', + required: true + }; + public item3: any = { + label: 'span标签:', + required: false, + verticalAlign: 'middle' + }; + public item4: any = { + verticalAlign: 'middle', + required: false + }; + public item5: any = { + required: true, + verticalAlign: 'middle' + }; + public item6: any = { + required: false, + label: '输入框:', + value: 'd' + }; + public button: any = { + okLabel: '确定', + cancelLabel: '取消' + }; +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldRequiredComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldRequiredComponent.ts new file mode 100644 index 0000000..23079a3 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldRequiredComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-required.html' +}) +export class FormfieldRequiredComponent { + contryValue: string = '中国'; + sexValue: string = '男'; + IDValue: string = 'hello tiny'; + emailValue: string = 'hello@example.com'; + nickValue: string = 'tiny'; + nickRequired: boolean = true; + + changeState(): void { + this.nickRequired = !this.nickRequired; + } +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldRequiredspaceComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldRequiredspaceComponent.ts new file mode 100644 index 0000000..9ddc36d --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldRequiredspaceComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-requiredspace.html' +}) +export class FormfieldRequiredspaceComponent { + colsWidth: Array = ['50%', '50%']; + colsNumber: number = 2; + contryValue: string = '中国'; + sexValue: string = '男'; + IDValue: string = 'hello tiny'; + emailValue: string = 'hello@tiny.com'; + nickValue: string = 'tiny'; +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldRequiredspaceTestComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldRequiredspaceTestComponent.ts new file mode 100644 index 0000000..9dc7de9 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldRequiredspaceTestComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-requiredspace-test.html' +}) +export class FormfieldRequiredspaceTestComponent { + colsWidth: Array = ['50%', '50%']; + colsNumber: number = 2; + contryValue: string = '中国'; + sexValue: string = '男'; + IDValue: string = 'hello tiny'; + emailValue: string = 'hello@tiny.com'; + nickValue: string = 'tiny'; +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldShowComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldShowComponent.ts new file mode 100644 index 0000000..61315af --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldShowComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-show.html' +}) +export class FormfieldShowComponent { + contryValue: string = '中国'; + sexValue: string = '男'; + IDValue: string = 'hello tiny'; + emailValue: string = 'hello@example.com'; + nickValue: string = 'tiny'; + showInput: boolean = true; + showButton: boolean = true; + + changeState(key) { + this[`show${key}`] = !this[`show${key}`]; + } +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldSingleColumnComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldSingleColumnComponent.ts new file mode 100644 index 0000000..c43936e --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldSingleColumnComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-single-column.html' +}) +export class FormfieldSingleColumnComponent { + title: string = '单列表单'; + contryValue: string = '中国'; + sexValue: string = '男'; + IDValue: string = 'hello tiny'; + emailValue: string = 'hello@example.com'; + nickValue: string = 'tiny'; +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldTestComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldTestComponent.ts new file mode 100644 index 0000000..7d577e9 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldTestComponent.ts @@ -0,0 +1,144 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './formfield-test.html' +}) +export class FormfieldTestComponent { + title: string = '单列表单'; + item1: any = { + show: true, + label: '姓名:', + required: true, + showSpan: true + }; + item2: any = { + label: '输入框:', + required: true, + value: '' + }; + item3: any = { + show: false, + label: 'span标签:', + required: true, + verticalAlign: 'middle' + }; + item4: any = { + label: '', + required: true, + labelShow: true, + verticalAlign: 'middle' + }; + item5: any = { + show: true, + required: true, + verticalAlign: 'middle' + }; + item6: any = { + show: false, + required: true, + verticalAlign: 'middle', + options: [ + { + text: '近1小时', + value: 1 + }, + { + text: '近12小时', + value: 12 + }, + { + text: '近1天', + value: 24 + }, + { + text: '近3天', + value: 72 + } + ] + }; + button: any = { + okLabel: '确定', + cancelLabel: '取消' + }; + + onClick(): void { + console.log('fdgasdgasdg'); + } + + change(): void { + this.item5['show'] = !this.item5['show']; + this.item4['label'] = '重复测试:'; + this.item1['showSpan'] = !this.item1['showSpan']; + this.item6['show'] = !this.item6['show']; + } + + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldTestModule.ts b/src/formfield/demo/src/app/formfield/FormfieldTestModule.ts new file mode 100644 index 0000000..594363d --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldTestModule.ts @@ -0,0 +1,156 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { + TiButtongroupModule, + TiButtonModule, + TiFormfieldModule, + TiOverflowModule, + TiSpinnerModule, + TiTableModule, + TiTextareaModule, + TiTextModule, + TiCheckboxModule, + TiSelectModule +} from '@opentiny/ng'; + +import { FormfieldSingleColumnComponent } from './FormfieldSingleColumnComponent'; +import { FormfieldMultiColumnComponent } from './FormfieldMultiColumnComponent'; +import { FormfieldTextFormComponent } from './FormfieldTextFormComponent'; +import { FormfieldColspanRowspanComponent } from './FormfieldColspanRowspanComponent'; +import { FormfieldNgforTestComponent } from './FormfieldNgforTestComponent'; +import { FormfieldTestComponent } from './FormfieldTestComponent'; +import { FormfieldIndexComponent } from './FormfieldIndexComponent'; +import { FormfieldFooComponent } from './FormfieldFooComponent'; +import { FormfieldVerticalComponent } from './FormfieldVerticalComponent'; +import { FormfieldVerticalAlignComponent } from './FormfieldVerticalAlignComponent'; +import { FormfieldLabelComponent } from './FormfieldLabelComponent'; +import { FormfieldShowComponent } from './FormfieldShowComponent'; +import { FormfieldColswidthComponent } from './FormfieldColswidthComponent'; +import { FormfieldRequiredComponent } from './FormfieldRequiredComponent'; +import { FormfieldLabelwidthComponent } from './FormfieldLabelwidthComponent'; +import { FormfieldNestFormfiledComponent } from './FormfieldNestFormfiledComponent'; +import { FormfieldColspanRowspanTestComponent } from './FormfieldColspanRowspanTestComponent'; +import { FormfieldRequiredspaceComponent } from './FormfieldRequiredspaceComponent'; +import { FormfieldRequiredspaceTestComponent } from './FormfieldRequiredspaceTestComponent'; +import { FormfieldRawgapComponent } from './FormfieldRawgapComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiButtonModule, + TiButtongroupModule, + TiTextareaModule, + TiTextModule, + TiButtonModule, + TiSpinnerModule, + TiFormfieldModule, + TiOverflowModule, + TiTableModule, + TiCheckboxModule, + TiSelectModule, + RouterModule.forChild(FormfieldTestModule.ROUTES) + ], + declarations: [ + FormfieldSingleColumnComponent, + FormfieldMultiColumnComponent, + FormfieldTextFormComponent, + FormfieldColspanRowspanComponent, + FormfieldNgforTestComponent, + FormfieldIndexComponent, + FormfieldFooComponent, + FormfieldTestComponent, + FormfieldVerticalComponent, + FormfieldVerticalAlignComponent, + FormfieldLabelComponent, + FormfieldShowComponent, + FormfieldColswidthComponent, + FormfieldRequiredComponent, + FormfieldLabelwidthComponent, + FormfieldNestFormfiledComponent, + FormfieldColspanRowspanTestComponent, + FormfieldRequiredspaceComponent, + FormfieldRequiredspaceTestComponent, + FormfieldRawgapComponent + ] +}) +export class FormfieldTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiFormfieldComponent.html', label: 'Formfield' }, + { href: 'components/TiButtonItemComponent.html', label: 'ButtonItem' }, + { href: 'components/TiItemComponent.html', label: 'Item' }, + { href: 'components/TiItemLabelComponent.html', label: 'ItemLabel' } + ]; + static readonly ROUTES: Routes = [ + { + path: 'formfield/formfield-single-column', + component: FormfieldSingleColumnComponent + }, + { + path: 'formfield/formfield-multi-column', + component: FormfieldMultiColumnComponent + }, + { + path: 'formfield/formfield-text-form', + component: FormfieldTextFormComponent + }, + { + path: 'formfield/formfield-colspan-rowspan', + component: FormfieldColspanRowspanComponent + }, + { + path: 'formfield/formfield-index', + component: FormfieldIndexComponent + }, + { + path: 'formfield/formfield-vertical', + component: FormfieldVerticalComponent + }, + { + path: 'formfield/formfield-label', + component: FormfieldLabelComponent + }, + { + path: 'formfield/formfield-labelwidth', + component: FormfieldLabelwidthComponent + }, + { + path: 'formfield/formfield-show', + component: FormfieldShowComponent + }, + { + path: 'formfield/formfield-required', + component: FormfieldRequiredComponent + }, + { + path: 'formfield/formfield-colswidth', + component: FormfieldColswidthComponent + }, + { + path: 'formfield/formfield-nest-formfiled', + component: FormfieldNestFormfiledComponent + }, + { + path: 'formfield/formfield-ngfor-test', + component: FormfieldNgforTestComponent + }, + { path: 'formfield/formfield-test', component: FormfieldTestComponent }, + { + path: 'formfield/formfield-colspan-rowspan-test', + component: FormfieldColspanRowspanTestComponent + }, + { + path: 'formfield/formfield-requiredspace', + component: FormfieldRequiredspaceComponent + }, + { + path: 'formfield/formfield-requiredspace-test', + component: FormfieldRequiredspaceTestComponent + }, + { path: 'formfield/formfield-rowgap', component: FormfieldRawgapComponent } + ]; +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldTextFormComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldTextFormComponent.ts new file mode 100644 index 0000000..db9436e --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldTextFormComponent.ts @@ -0,0 +1,44 @@ +import { Component } from '@angular/core'; +const ITEMOPTS: Array = [ + { + label: '地域:', + value: '中国 zh-CN' + }, + { + label: '云服务器组:', + value: 'ecsg-23' + }, + { + label: '可用分区:', + value: '可用区1' + }, + { + label: '虚拟私有云:', + value: 'huaV' + }, + { + label: '规格:', + value: 'M1.TINY | 0.5G' + }, + { + label: '购买时长:', + value: '1年' + } +]; +@Component({ + templateUrl: './formfield-text-form.html' +}) +export class FormfieldTextFormComponent { + singleItem: any = { + title: '单列纯文本表单:', + textLineHeight: '22px', + ITEMOPTS + }; + multiItem: any = { + title: '多列纯文本表单:', + colsNumber: 3, + colsGap: ['100px', '100px'], + textLineHeight: '30px', + ITEMOPTS + }; +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldVerticalAlignComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldVerticalAlignComponent.ts new file mode 100644 index 0000000..c730f9c --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldVerticalAlignComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-vertical-align.html' +}) +export class FormfieldVerticalAlignComponent { + inputValue: string = 'hello tiny'; +} diff --git a/src/formfield/demo/src/app/formfield/FormfieldVerticalComponent.ts b/src/formfield/demo/src/app/formfield/FormfieldVerticalComponent.ts new file mode 100644 index 0000000..4d18ce2 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/FormfieldVerticalComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './formfield-vertical.html' +}) +export class FormfieldVerticalComponent { + contryValue: string = '中国'; + sexValue: string = '男'; + IDValue: string = 'hello tiny'; + emailValue: string = 'hello@example.com'; + nickValue: string = 'tiny'; +} diff --git a/src/formfield/demo/src/app/formfield/formfield-colspan-rowspan-test.html b/src/formfield/demo/src/app/formfield/formfield-colspan-rowspan-test.html new file mode 100644 index 0000000..012d2f8 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-colspan-rowspan-test.html @@ -0,0 +1,34 @@ +

描述

+

动态修改colspan(由3到1)时,第二行content区域宽度动态修改前后一致

+

示例

+
+ + + + + + + + + + + + + + + + + +
diff --git a/src/formfield/demo/src/app/formfield/formfield-colspan-rowspan.html b/src/formfield/demo/src/app/formfield/formfield-colspan-rowspan.html new file mode 100644 index 0000000..2b62f66 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-colspan-rowspan.html @@ -0,0 +1,41 @@ + + + + + + + + + 标签内容 + + + + + + + + + + + +

+ + + + + + + + + 标签内容 + + + + + + + + + + + diff --git a/src/formfield/demo/src/app/formfield/formfield-colswidth.html b/src/formfield/demo/src/app/formfield/formfield-colswidth.html new file mode 100644 index 0000000..d430291 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-colswidth.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/formfield/demo/src/app/formfield/formfield-foo.html b/src/formfield/demo/src/app/formfield/formfield-foo.html new file mode 100644 index 0000000..51de7ff --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-foo.html @@ -0,0 +1,12 @@ + + + + + + + + + + {{item3.label}} + 美好时光 + diff --git a/src/formfield/demo/src/app/formfield/formfield-index.html b/src/formfield/demo/src/app/formfield/formfield-index.html new file mode 100644 index 0000000..a74474f --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/formfield/demo/src/app/formfield/formfield-label.html b/src/formfield/demo/src/app/formfield/formfield-label.html new file mode 100644 index 0000000..2b5fe00 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-label.html @@ -0,0 +1,25 @@ +

1.通过label属性:

+ + + + + + +

2.通过 ti-item-label:

+ + + +
+ 自定义 + 结构: +
+
+ +
+
+

3.Label 宽度:

+ + + + + diff --git a/src/formfield/demo/src/app/formfield/formfield-labelwidth.html b/src/formfield/demo/src/app/formfield/formfield-labelwidth.html new file mode 100644 index 0000000..3dc8640 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-labelwidth.html @@ -0,0 +1,27 @@ +

描述

+

设置labelWidth接口,表单条目label的宽度

+

示例

+ + + + + +

+ + + + + +

+ + + + + + + + +

+ diff --git a/src/formfield/demo/src/app/formfield/formfield-multi-column.html b/src/formfield/demo/src/app/formfield/formfield-multi-column.html new file mode 100644 index 0000000..e9e8163 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-multi-column.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/formfield/demo/src/app/formfield/formfield-nest-formfiled.html b/src/formfield/demo/src/app/formfield/formfield-nest-formfiled.html new file mode 100644 index 0000000..ddd452f --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-nest-formfiled.html @@ -0,0 +1,177 @@ +

描述

+

表单嵌套使用

+

示例

+ + + + + + + + + item3 + + + {{item}} + + + + + + 测试1 + + + + + + item3 + + + + 测试标签dom + 按钮 + + {{item}} + + + + 存储地址: + + + + + + + + + + + + + + + + + + + item3 + + + {{item}} + + + + + + 测试1 + + + + + + item3 + + + + 测试标签dom + 按钮 + + {{item}} + + + + 存储地址: + + + + + + + + + + + + + + + + + + + item3 + + + {{item}} + + + + + + 测试1 + + + + + + item3 + + + + 测试标签dom + 按钮 + + {{item}} + + + + 存储地址: + + + + + + + + + + + + + + + + item3 + + + + + + 测试1 + + + + + + item3 + + + + 测试标签dom + 按钮 + + {{item}} + + + + 存储地址: + + + + + + diff --git a/src/formfield/demo/src/app/formfield/formfield-ngfor-test.html b/src/formfield/demo/src/app/formfield/formfield-ngfor-test.html new file mode 100644 index 0000000..9be520f --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-ngfor-test.html @@ -0,0 +1,6 @@ + + + +
{{item.value}}
+
+
diff --git a/src/formfield/demo/src/app/formfield/formfield-required.html b/src/formfield/demo/src/app/formfield/formfield-required.html new file mode 100644 index 0000000..50e6a33 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-required.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/formfield/demo/src/app/formfield/formfield-requiredspace-test.html b/src/formfield/demo/src/app/formfield/formfield-requiredspace-test.html new file mode 100644 index 0000000..29095ae --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-requiredspace-test.html @@ -0,0 +1,177 @@ +

描述

+

多个表单label对齐, requireSpace接口测试

+

示例

+

1.table布局

+

1.1 不设置requireSpace

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

1.2 设置requireSpace

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

2.grid布局

+

2.1 不设置requireSpace

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

2.2 设置requireSpace

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/formfield/demo/src/app/formfield/formfield-requiredspace.html b/src/formfield/demo/src/app/formfield/formfield-requiredspace.html new file mode 100644 index 0000000..ac13780 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-requiredspace.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/formfield/demo/src/app/formfield/formfield-rowgap.html b/src/formfield/demo/src/app/formfield/formfield-rowgap.html new file mode 100644 index 0000000..f80b89a --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-rowgap.html @@ -0,0 +1,61 @@ +

描述

+

配置 colsWidth 属性设置列宽且是带输入控件类表单时,相邻两行间的间距, 设置 rowGap 接口

+

示例

+ + + + + + + + + 不设置label + + + + 测试标签dom + + {{item}} + + + + 存储地址: + + + + + + + + + + + + + + + + + 不设置label + + + + 测试标签dom + + {{item}} + + + + 存储地址: + + + + + + + + diff --git a/src/formfield/demo/src/app/formfield/formfield-show.html b/src/formfield/demo/src/app/formfield/formfield-show.html new file mode 100644 index 0000000..563b795 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-show.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/formfield/demo/src/app/formfield/formfield-single-column.html b/src/formfield/demo/src/app/formfield/formfield-single-column.html new file mode 100644 index 0000000..e83234b --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-single-column.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/formfield/demo/src/app/formfield/formfield-test.html b/src/formfield/demo/src/app/formfield/formfield-test.html new file mode 100644 index 0000000..25f0e9a --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-test.html @@ -0,0 +1,65 @@ +
+ + + + + 测试1 + + + + + + item3 + + + + 测试标签dom + 按钮 + + {{item}} + + + + 存储地址: + + + + + + 存储地址: + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{column.title}}
{{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
+
+
+ + + +
+
diff --git a/src/formfield/demo/src/app/formfield/formfield-text-form.html b/src/formfield/demo/src/app/formfield/formfield-text-form.html new file mode 100644 index 0000000..d8aefbc --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-text-form.html @@ -0,0 +1,20 @@ + + + {{item.label}} + {{item.value}} + + +

+ + + {{item.label}} + {{item.value}} + + diff --git a/src/formfield/demo/src/app/formfield/formfield-vertical-align.html b/src/formfield/demo/src/app/formfield/formfield-vertical-align.html new file mode 100644 index 0000000..c5c2b82 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-vertical-align.html @@ -0,0 +1,38 @@ + + + + + + + + +

+ + + + + + + + +

+ + + + + + + + +

+ + + + + + + + + + + diff --git a/src/formfield/demo/src/app/formfield/formfield-vertical.html b/src/formfield/demo/src/app/formfield/formfield-vertical.html new file mode 100644 index 0000000..b87938a --- /dev/null +++ b/src/formfield/demo/src/app/formfield/formfield-vertical.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/formfield/demo/src/app/formfield/webdoc/formfield-demos.js b/src/formfield/demo/src/app/formfield/webdoc/formfield-demos.js new file mode 100644 index 0000000..9df7537 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/webdoc/formfield-demos.js @@ -0,0 +1,178 @@ +export default { + column: '1', + + demos: [ + { + demoId: 'formfield-single-column', + name: { + 'zh-CN': '基本使用', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

Formfield 组件的最简用法。通过属性title配置表单名称。

', + 'en-US': '

Basic usage

', + }, + apis: ['TiFormfieldComponent.properties.title', 'TiButtonItemComponent'], + }, + { + demoId: 'formfield-multi-column', + name: { + 'zh-CN': '多列表单', + 'en-US': '', + }, + desc: { + 'zh-CN': '

通过属性colsNumber配置表单列数。

', + 'en-US': '

', + }, + apis: ['TiFormfieldComponent.properties.colsNumber'], + }, + { + demoId: 'formfield-label', + name: { + 'zh-CN': '表单项标签', + 'en-US': '', + }, + desc: { + 'zh-CN': + '

支持两种方式配置表单项标签的内容:1.通过 ti-item 组件的属性label配置字符串变量;2.通过 ti-item-label 组件自定义表单项标签的内容。通过 ti-item 组件的属性labelWidth配置表单项标签的宽度。

', + 'en-US': '

', + }, + apis: [ + 'TiFormfieldComponent.properties.labelWidth', + 'TiItemComponent.properties.label', + ], + }, + { + demoId: 'formfield-colswidth', + name: { + 'zh-CN': '列宽', + 'en-US': '', + }, + desc: { + 'zh-CN': + '

通过属性colsWidth配置表单列宽;通过属性colsGap配置表单列间距。注意:若同时设置colsWidthcolsGap,则列间距为colsGap中第一个元素的值。

', + 'en-US': '

', + }, + apis: [ + 'TiFormfieldComponent.properties.colsGap', + 'TiFormfieldComponent.properties.colsWidth', + ], + }, + { + demoId: 'formfield-show', + name: { + 'zh-CN': '显示/隐藏表单项', + 'en-US': '', + }, + desc: { + 'zh-CN': + '

通过 ti-item、ti-button-item 组件的属性show配置是否外显表单项。

', + 'en-US': '

', + }, + apis: [ + 'TiItemComponent.properties.show', + 'TiButtonItemComponent.properties.show', + ], + }, + { + demoId: 'formfield-required', + name: { + 'zh-CN': '必填项', + 'en-US': '', + }, + desc: { + 'zh-CN': + '

通过 ti-item 组件的属性required配置是否为必填项。

', + 'en-US': '

', + }, + apis: ['TiItemComponent.properties.required'], + }, + { + demoId: 'formfield-vertical-align', + name: { + 'zh-CN': '垂直方向对齐方式', + 'en-US': '', + }, + desc: { + 'zh-CN': + '

通过属性verticalAlign配置表单在垂直方向的对齐方式,包括:topmiddlebottom。你也可以通过 ti-item、ti-button-item 组件的属性verticalAlign配置该表单项在垂直方向的对齐方式。

', + 'en-US': '

', + }, + apis: [ + 'TiFormfieldComponent.properties.verticalAlign', + 'TiButtonItemComponent.properties.verticalAlign', + 'TiItemComponent.properties.verticalAlign', + ], + }, + { + demoId: 'formfield-text-form', + name: { + 'zh-CN': '纯文本表单', + 'en-US': '', + }, + desc: { + 'zh-CN': + '

通过属性type配置表单类型,包括:default(默认)、text(纯文本表单)。通过属性textLineHeight配置纯文本表单的行高。

', + 'en-US': '

', + }, + apis: [ + 'TiFormfieldComponent.properties.textLineHeight', + 'TiFormfieldComponent.properties.type', + ], + }, + { + demoId: 'formfield-vertical', + name: { + 'zh-CN': '纵向布局', + 'en-US': '', + }, + desc: { + 'zh-CN': + '

通过 ti-item 组件的属性vertical配置表单为纵向布局。注意:纵向布局不适用于纯文本表单,纵向布局表单不支持通过verticalAlign配置表单项垂直对齐方式。

', + 'en-US': '

', + }, + }, + { + demoId: 'formfield-colspan-rowspan', + name: { + 'zh-CN': '表单合并', + 'en-US': '', + }, + desc: { + 'zh-CN': + '

通过 ti-item 组件的属性rowspan配置表单项可横跨的行数。通过 ti-item 组件的属性colspan配置表单项可横跨的列数。

', + 'en-US': '

', + }, + apis: [ + 'TiItemComponent.properties.colspan', + 'TiItemComponent.properties.rowspan', + ], + }, + { + demoId: 'formfield-index', + name: { + 'zh-CN': '表单项顺序', + 'en-US': '', + }, + desc: { + 'zh-CN': + '

通过 ti-item 组件的属性index配置 Item 的顺序,数值越大越靠后展示。默认按照 ti-item 的解析顺序展示。注意:若使用该属性自定义顺序,则需要为所有 ti-item 设置值。

', + 'en-US': '

', + }, + apis: ['TiItemComponent.properties.index'], + }, + { + demoId: 'formfield-requiredspace', + name: { + 'zh-CN': '多个表单文本对齐', + 'en-US': '', + }, + desc: { + 'zh-CN': '

通过requiredSpace配置必填图标是否占用空间,一般用于多个表单 label 对齐。

', + 'en-US': '

', + }, + apis: ['TiFormfieldComponent.properties.requiredSpace'], + }, + ], +}; diff --git a/src/formfield/demo/src/app/formfield/webdoc/formfield.cn.md b/src/formfield/demo/src/app/formfield/webdoc/formfield.cn.md new file mode 100644 index 0000000..55690d7 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/webdoc/formfield.cn.md @@ -0,0 +1,27 @@ +--- +title: Formfield 表单布局 +--- +# Formfield 表单布局 + +
+ +Formfield 是对多个表单元素布局的组件。   + ++ 支持单列、多列布局。 + +```typescript +import { TiFormfieldModule } from '@opentiny/ng'; +``` + +
+ +
+ +Formfield 是对多个表单元素布局的组件。   + ++ 支持单列、多列布局。 + +```typescript +import { TiFormfieldModule } from '@opentiny/ng'; +``` +
diff --git a/src/formfield/demo/src/app/formfield/webdoc/formfield.en.md b/src/formfield/demo/src/app/formfield/webdoc/formfield.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/formfield/demo/src/app/formfield/webdoc/formfield.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/formfield/demo/src/favicon.ico b/src/formfield/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/formfield/demo/src/index.html b/src/formfield/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/formfield/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/formfield/demo/src/main.ts b/src/formfield/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/formfield/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/formfield/demo/test.ts b/src/formfield/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/formfield/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/formfield/demo/tsconfig.app.json b/src/formfield/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/formfield/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/formfield/demo/tsconfig.spec.json b/src/formfield/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/formfield/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/formfield/lib/index.ts b/src/formfield/lib/index.ts new file mode 100644 index 0000000..9da333f --- /dev/null +++ b/src/formfield/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiFormfieldModule'; diff --git a/src/formfield/lib/ng-package.json b/src/formfield/lib/ng-package.json new file mode 100644 index 0000000..f01efce --- /dev/null +++ b/src/formfield/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/formfield", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/formfield/lib/package.json b/src/formfield/lib/package.json new file mode 100644 index 0000000..a33c374 --- /dev/null +++ b/src/formfield/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-formfield", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-include": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/formfield/lib/project.json b/src/formfield/lib/project.json new file mode 100644 index 0000000..ac94284 --- /dev/null +++ b/src/formfield/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/formfield/lib", + "sourceRoot": "src/formfield/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/formfield"], + "options": { + "project": "src/formfield/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/formfield"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js formfield" + }, + { + "command": "ng default-build formfield" + }, + { + "command": "node build/clear-default-theme.js formfield" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/formfield && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build formfield && ng pack formfield && node build/publish.js formfield --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/formfield/lib/src/TiButtonItemComponent.ts b/src/formfield/lib/src/TiButtonItemComponent.ts new file mode 100644 index 0000000..a92e2ce --- /dev/null +++ b/src/formfield/lib/src/TiButtonItemComponent.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ElementRef, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChange, + SimpleChanges, + ChangeDetectionStrategy +} from '@angular/core'; +import { TiFormfieldComponent } from './TiFormfieldComponent'; + +/** + * 本组件嵌在 tiFormfield 中使用,包裹着表单按钮 + */ +@Component({ + selector: 'ti-button-item', + templateUrl: './formfield-btn-item.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiButtonItemComponent implements OnInit, OnChanges, OnDestroy { + /** + * 每一个ti-item占4个td。第一个td用来显示“*”,第二个td用来显示label, + * 第三个td用来显示content, 第四个td是item间距。 + */ + private static readonly TD_NUM_PER_ITEM: number = 4; + /** + * 表单项是否显示 + */ + @Input() show: boolean = true; + /** + * 表单项在垂直方向的对齐方式,纵向布局表单不支持,优先级高于 ti-formfield 中的 verticalAlign 属性 + */ + @Input() verticalAlign: 'top' | 'middle' | 'bottom'; + /** + * @ignore + */ + public verticalAlignObj: { 'vertical-align'?: string } = {}; + /** + * @ignore + */ + public btnContent: Node; + /** + * @ignore + */ + public btnColspan: number; + constructor(private formfield: TiFormfieldComponent, private elementRef: ElementRef) {} + + ngOnInit(): void { + this.btnContent = this.elementRef.nativeElement; + this.btnColspan = (this.formfield.colsNum - 1) * TiButtonItemComponent.TD_NUM_PER_ITEM + 2; + if (this.verticalAlign) { + this.verticalAlignObj['vertical-align'] = this.verticalAlign; + } + + if (this.show) { + this.formfield.addBtnItem(this); + } + } + + ngOnChanges(changes: SimpleChanges): void { + const showObj: SimpleChange = changes['show']; + + if (showObj && !showObj.firstChange) { + if (showObj.currentValue) { + this.formfield.addBtnItem(this); + } else { + this.formfield.removeBtnItem(this); + } + } + } + + ngOnDestroy(): void { + this.formfield.removeBtnItem(this); + } +} diff --git a/src/formfield/lib/src/TiFormfieldComponent.ts b/src/formfield/lib/src/TiFormfieldComponent.ts new file mode 100644 index 0000000..f860149 --- /dev/null +++ b/src/formfield/lib/src/TiFormfieldComponent.ts @@ -0,0 +1,373 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Input, + Renderer2, + SimpleChange, + SimpleChanges +} from '@angular/core'; +import { Util } from '@opentiny/ng-utils'; +import { TiItemComponent } from './TiItemComponent'; +import { TiButtonItemComponent } from './TiButtonItemComponent'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { Subject } from 'rxjs'; +import packageInfo from '../package.json'; +/** + * 本组件实现对多个表单元素的布局,支持单列和多列布局。 + * 其中包含了tiFormfield、tiItem、tiItemLabel和tiButtonItem几个组件。 + * + */ +@Component({ + selector: 'ti-formfield', + templateUrl: './formfield.html', + styleUrls: ['./formfield.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiFormfieldComponent extends TiBaseComponent { + /** + * 默认两列的间距 + */ + private static readonly COLS_GAP: string = '70px'; + /** + * 表单类型,不设置时默认为带输入控件类表单;当为 text 时,为纯文本描述类表单 + */ + @Input() type: 'default' | 'text' = 'default'; + /** + * 表单标题 + */ + @Input() title: string; + /** + * 表单项标签宽度 + */ + @Input() labelWidth: string; + /** + * 表单展示的列数 + */ + @Input() colsNumber: number = 1; + /** + * 相邻两列的间距,仅多列表单支持 + */ + @Input() colsGap: Array = []; + /** + * @ignore + * + * 设置列宽且是带输入控件类表单时,相邻两行的间距 + * + * 目前仅有一个服务(CRM)场景需求,暂不对外开放 + */ + @Input() rowGap: Array = ['20px']; + /** + * 表单项在垂直方向的对齐方式,纵向布局表单不支持 + */ + @Input() verticalAlign: 'top' | 'middle' | 'bottom' = 'top'; + /** + * 纯文本表单中表单项的行高,默认为'40px' + */ + @Input() textLineHeight: string; + /** + * 各列宽度, 设置该属性后: 1.各列间距为 colsGap 中第一个元素的值; 2.不支持行合并列合并 + */ + @Input() colsWidth: Array = []; + + /** + * 必填图标是否占用空间 + */ + @Input() requiredSpace: boolean = false; + + /** + * @ignore + * 纯文本描述类表单的type的值 + */ + public readonly textType: string = 'text'; + /** + * @ignore + * ti-formfield中包含的所有ti-item的集合 + */ + public items: Array = []; + /** + * @ignore + * ti-formfield中包含的所有ti-btn-item的集合 + */ + public btnItems: Array = []; + /** + * @ignore + * 根据colsNum对 items 分组后的数据 + */ + public groupedItems: Array = []; + /** + * @ignore + * 表单的列数 + */ + public colsNum: number = 1; + /** + * @ignore + */ + public containterObj: { + 'vertical-align'?: string; + 'grid-template-columns'?: string; + 'grid-column-gap'?: string; + 'grid-row-gap'?: string; + } = {}; + /** + * @ignore + * 当前formfield下ti-item的总个数 + */ + public itemTotal: number = 0; + /** + * @ignore + * 表单是否为上下布局 + */ + public isVertical: boolean = false; + /** + * @ignore + * button样式 + */ + public buttonStyle: { 'margin-left'?: string }; + /** + * @ignore + */ + public labelWidthSubject: Subject = new Subject(); + + protected versionInfo: string = super.getVersion(packageInfo); + // 浏览器是否支持grid布局 + private isSupportGrid: boolean = + typeof window !== 'undefined' && (window as any).CSS && (window as any).CSS.supports && (window as any).CSS.supports('(display: grid)'); + /** + * @ignore + * 表单是否使用grid布局 + */ + public useGrid: boolean; + constructor(protected hostRef: ElementRef, protected renderer: Renderer2, public changeDetector: ChangeDetectorRef) { + super(hostRef, renderer); + } + + ngOnChanges(changes: SimpleChanges): void { + const labelWidthObj: SimpleChange = changes['labelWidth']; + const colsNumberObj: SimpleChange = changes['colsNumber']; + + if (labelWidthObj && !labelWidthObj.isFirstChange()) { + if (this.useGrid) { + this.labelWidth = this.labelWidth ? this.labelWidth : '80px'; + this.buttonStyle = this.isVertical ? {} : { 'margin-left': `calc(${this.labelWidth} + 20px)` }; // 20px padding + } + this.labelWidthSubject.next(this.labelWidth); + } + + if (colsNumberObj && !colsNumberObj.isFirstChange()) { + this.setColsNumber(); + this.groupedItems = this.chunkArray(this.items, this.colsNum); + } + + if (changes['colsWidth']) { + this.setColsWidth(); + } + } + + ngOnInit(): void { + super.ngOnInit(); + this.isVertical = this.nativeElement.hasAttribute('vertical'); + this.setColsNumber(); + // 统一设置表单内容的垂直对齐方式; + // tbody、tr、td的verticalAlign继承于此(table),所以此处可以 + // 统一设置表单内容的垂直对齐方式 + this.setFieldStyle(); + } + + private setColsNumber(): void { + if (this.colsNumber) { + const colsNumber: number = parseInt(String(this.colsNumber), 10); + if (!Number.isNaN(colsNumber) && colsNumber >= 1) { + this.colsNum = colsNumber; + } + } + } + + private setFieldStyle(): void { + if (!Util.isEmptyString(this.verticalAlign)) { + this.containterObj['vertical-align'] = this.verticalAlign; + } + } + + private setColsWidth(): void { + // 设置width接口时,配置grid,IE下不支持grid布局,存在兼容性问题 + this.useGrid = this.colsWidth && this.colsWidth.length > 0 && this.isSupportGrid; + + if (this.useGrid) { + this.labelWidth = this.labelWidth ? this.labelWidth : '80px'; // 给定宽度,确保content对齐; + for (let i: number = 0; i < this.colsWidth.length; i++) { + // 各列设置百分比且设置列间距时,需要重新计算各列宽度,设置宽度-各列间距总和 + const calcWidth: string = `calc( ${this.colsWidth[i]} - ${ + (parseFloat(this.colsGap[0]) * (this.colsWidth.length - 1)) / this.colsWidth.length + }px)`; + this.colsWidth[i] = /%$/.test(this.colsWidth[i]) && this.colsGap[0] ? calcWidth : this.colsWidth[i]; + } + + this.containterObj['grid-template-columns'] = `${this.colsWidth.join(' ')}`; + this.containterObj['grid-column-gap'] = this.colsGap[0]; // 列间距 grid-column-gap属性只支持一个值,取首项作为列间距 + this.containterObj['grid-row-gap'] = this.type === this.textType ? '0' : this.rowGap[0]; // 行间距 + this.buttonStyle = this.isVertical ? {} : { 'margin-left': `calc(${this.labelWidth} + 20px)` }; // 20px padding + } + } + + /** + * @ignore + * 需要在TiItemComponent.ts中调用 + */ + public addCntItem(item: TiItemComponent): void { + this.items.push(item); + this.items.sort((a: TiItemComponent, b: TiItemComponent) => { + return a.itemIndex - b.itemIndex; + }); + this.groupedItems = this.chunkArray(this.items, this.colsNum); + // 强制变检一次,消除报错(Expression has changed after it was checked) + this.changeDetector.detectChanges(); + } + /** + * @ignore + * 需要在 TiItemComponent.ts 中调用 + */ + public removeCntItem(item: TiItemComponent): void { + const itemIndex: number = this.items.indexOf(item); + if (itemIndex !== -1) { + this.items.splice(itemIndex, 1); + this.groupedItems = this.chunkArray(this.items, this.colsNum); + // 强制变检一次,消除报错(Expression has changed after it was checked) + // ti-item和ti-formfiled组件都销毁时,视图也已销毁,不需要再强制变检; + // 因此,强制变检需要在一定的条件下执行,消除服务报错(Attempt to use a destroyed view: detectChanges) + if (!this.changeDetector['destroyed']) { + this.changeDetector.detectChanges(); + } + } + } + /** + * @ignore + * 需要在 TiButtonItemComponent.ts 中调用 + */ + public addBtnItem(item: TiButtonItemComponent): void { + this.btnItems.push(item); + // 绑定在模板上的变量变更后,需要强制变更检测 + this.changeDetector.markForCheck(); + } + /** + * @ignore + * 需要在 TiButtonItemComponent.ts 中调用 + */ + public removeBtnItem(item: TiButtonItemComponent): void { + const itemIndex: number = this.btnItems.indexOf(item); + if (itemIndex !== -1) { + this.btnItems.splice(itemIndex, 1); + // 绑定在模板上的变量变更后,需要强制变更检测 + this.changeDetector.markForCheck(); + } + } + + private chunkArray(array: Array, size: number): Array { + const length: number = array ? array.length : 0; + if (!length || size < 1) { + return []; + } + + const result: Array> = []; + // 单列 + if (size === 1) { + array.forEach((item: TiItemComponent) => { + result.push([item]); + item.gapWidth = '0px'; + }); + // 多列 + } else { + const rowspanNums: Array = new Array(size); // 记录每一列的行合并的信息 + let onegroup: Array = []; + let colIndex: number = 0; + array.forEach((item: TiItemComponent, index: number) => { + while (rowspanNums[colIndex] > 0) { + rowspanNums[colIndex] = rowspanNums[colIndex] - 1; + colIndex++; + if (colIndex === size) { + result.push(onegroup); + onegroup = []; + colIndex = 0; + } + } + + onegroup.push(item); + // 如果该单元是行合并,则影响后续行 + if (item.rowspan > 1) { + rowspanNums[colIndex] = item.rowspan - 1; + } + // 如果该单元是列合并,则影响当前行 + if (item.colspan > 1) { + colIndex += item.colspan - 1; + } + + // 设置列间距 + if (colIndex < size - 1) { + // 设置宽度时,为使IE下有良好的呈现,取首项作为各列间距 + const gridColsGap: string = this.colsWidth && this.colsWidth.length > 0 && this.colsGap[0]; + item.gapWidth = gridColsGap ? this.colsGap[0] : this.colsGap[colIndex] || TiFormfieldComponent.COLS_GAP; + } else if (item.gapWidth !== '0px') { + // 动态更改colspan时需重置列间距,最后一列无需间距 + item.gapWidth = '0px'; + } + + colIndex++; + + if (colIndex === size || index === array.length - 1) { + result.push(onegroup); + onegroup = []; + colIndex = 0; + } + }); + } + + return result; + } + /** + * @ignore + * 需要在html模板中使用 + */ + public trackByFn(index: number, item: TiItemComponent): number { + return item.itemIndex; + } + + /** + * @ignore + * 每项图标是否占位,设置按钮及content样式 + */ + public getRequire(item: TiItemComponent, index: number): boolean { + let hasRequire: boolean; + + for (let i = 0; i < this.groupedItems.length; i++) { + const groupedItem: TiItemComponent = this.groupedItems[i]; + if (groupedItem[index % this.colsNum] && (groupedItem[index % this.colsNum].required || this.requiredSpace)) { + hasRequire = true; + } + } + + if (index === 0 && hasRequire) { + this.buttonStyle = { 'margin-left': `calc(${this.labelWidth} + 36px)` }; // 20 padding + 16 图标 + } + + if (hasRequire) { + item.contentStyle['width'] = item.hasLabel ? `calc(100% - ${this.labelWidth} - 36px)` : `calc(100% - 16px)`; + } else { + // TiItemComponent 初始化时,ti-item-label指令无法获取到label,导致宽度设置不正确,因此在此处设置宽度 + item.contentStyle['width'] = item.hasLabel ? `calc(100% - ${this.labelWidth} - 20px)` : '100%'; + } + + return hasRequire; + } +} diff --git a/src/formfield/lib/src/TiFormfieldModule.ts b/src/formfield/lib/src/TiFormfieldModule.ts new file mode 100644 index 0000000..fd411c4 --- /dev/null +++ b/src/formfield/lib/src/TiFormfieldModule.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIncludeModule } from '@opentiny/ng-include'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiFormfieldComponent } from './TiFormfieldComponent'; +import { TiItemComponent } from './TiItemComponent'; +import { TiItemLabelComponent } from './TiItemLabelComponent'; +import { TiButtonItemComponent } from './TiButtonItemComponent'; + +@NgModule({ + imports: [CommonModule, TiIconModule, TiIncludeModule], + exports: [TiFormfieldComponent, TiItemComponent, TiItemLabelComponent, TiButtonItemComponent], + declarations: [TiFormfieldComponent, TiItemComponent, TiItemLabelComponent, TiButtonItemComponent] +}) +export class TiFormfieldModule {} +export { TiFormfieldComponent } from './TiFormfieldComponent'; +export { TiItemComponent } from './TiItemComponent'; +export { TiItemLabelComponent } from './TiItemLabelComponent'; +export { TiButtonItemComponent } from './TiButtonItemComponent'; diff --git a/src/formfield/lib/src/TiItemComponent.ts b/src/formfield/lib/src/TiItemComponent.ts new file mode 100644 index 0000000..1b6ad37 --- /dev/null +++ b/src/formfield/lib/src/TiItemComponent.ts @@ -0,0 +1,203 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChange, + SimpleChanges +} from '@angular/core'; +import { TiFormfieldComponent } from './TiFormfieldComponent'; +import { Util } from '@opentiny/ng-utils'; +import { Subscription } from 'rxjs'; + +/** + * 本组件嵌在 tiFormfield 中使用,包裹着单个表单条目的所有内容 + */ +@Component({ + selector: 'ti-item', + templateUrl: './formfield-item.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiItemComponent implements OnInit, OnChanges, OnDestroy { + /** + * 表单项是否必填 + */ + @Input() required: boolean = false; + /** + * 表单项在垂直方向的对齐方式,纵向布局表单不支持,优先级高于 ti-formfield 中的 verticalAlign 属性 + */ + @Input() verticalAlign: 'top' | 'middle' | 'bottom'; + /** + * 表单项是否显示 + */ + @Input() show: boolean = true; + /** + * 合并行数 + */ + @Input() rowspan: number = 1; + /** + * 合并列数 + */ + @Input() colspan: number = 1; + /** + * 表单项在 ti-formfield 中的展示顺序,数值越大越靠后展示 + */ + @Input() index: number; + /** + * @ignore + * tiItem的索引值,用于控制其显示时的位置顺序 + */ + public itemIndex: number; + /** + * @ignore + */ + public verticalAlignObj: { 'vertical-align'?: string }; + /** + * @ignore + */ + public itemStyle: { 'grid-area'?: string }; + /** + * @ignore + */ + public labelStyle: { + 'vertical-align'?: string; + width?: string; + 'line-height'?: string; + }; + /** + * @ignore + */ + public contentStyle: { + 'vertical-align'?: string; + 'line-height'?: string; + width?: string; + }; + /** + * @ignore + */ + public element: HTMLElement; + /** + * @ignore + */ + public itemLabel: string | Node; + /** + * @ignore + */ + public hasLabel: boolean = false; + /** + * @ignore + */ + public gapWidth: string = '0px'; + private subscription: Subscription; + constructor(public formfield: TiFormfieldComponent, private elementRef: ElementRef, private changeDetector: ChangeDetectorRef) {} + + // 有两处改变label + // 1. label接口传入的值 + // 2. 在ti-item-label组件中修改label的值 + /** + * 表单项的 Label 内容 + */ + @Input() + set label(labelParm: string | Node) { + this.setItemLabel(labelParm); + } + + ngOnInit(): void { + this.formfield.itemTotal++; + this.decorateItem(); + if (this.show) { + this.formfield.addCntItem(this); + } + } + + ngOnChanges(changes: SimpleChanges): void { + const showObj: SimpleChange = changes['show']; + const requiredObj: SimpleChange = changes['required']; + if (showObj && !showObj.firstChange) { + if (showObj.currentValue) { + this.formfield.addCntItem(this); + } else { + this.formfield.removeCntItem(this); + } + } + + if (requiredObj && !requiredObj.firstChange) { + // onpush策略 tiItem 的required改变时无法触发 tiFormfield 的变化检测,需要手动给formfiled标志一下本次变化检测tiFormfield需要进行变化检测 + this.formfield.changeDetector.markForCheck(); + } + } + + ngOnDestroy(): void { + // 在外部将ti-iem销毁时,应将此item从items数组中去掉 + this.formfield.removeCntItem(this); + this.changeDetector.detach(); + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + private decorateItem(): void { + this.itemIndex = Util.isNumber(this.index) ? this.index : this.formfield.itemTotal - 1; + this.element = this.elementRef.nativeElement; + this.verticalAlignObj = Util.isUndefined(this.verticalAlign) ? {} : { 'vertical-align': this.verticalAlign }; + + // item-label的自定义的样式(垂直对齐方式、宽度) + this.labelStyle = { + 'vertical-align': this.verticalAlign, + width: this.formfield.labelWidth + }; + + this.subscription = this.formfield.labelWidthSubject.subscribe((value: string) => { + this.labelStyle.width = value; + }); + + // item-content的自定义的样式(垂直对齐方式) + this.contentStyle = { + 'vertical-align': this.verticalAlign + }; + // 为纯文本描述类表单时,可设置 item 的行高 + if (this.formfield.type === this.formfield.textType && !Util.isUndefined(this.formfield.textLineHeight)) { + const customLineStyle = { + 'line-height': this.formfield.textLineHeight, + 'padding-top': 0, + 'padding-bottom': 0 + }; + this.labelStyle = { ...(this.labelStyle || {}), ...customLineStyle }; + this.contentStyle = { ...(this.labelStyle || {}), ...customLineStyle }; + } + + // item 自定义行列合并样式 + if (this.formfield.useGrid) { + this.itemStyle = { + 'grid-area': `span ${this.rowspan} / span ${this.colspan}` + }; + } + } + + private setItemLabel(label: string | Node): void { + if (label) { + this.hasLabel = true; + this.itemLabel = label; + } else { + this.hasLabel = false; + } + // onpush策略 无法通知formfiled值变更,需要手动使formfiled强制变更检测一次 + this.formfield.changeDetector.detectChanges(); + this.changeDetector.detectChanges(); + } +} diff --git a/src/formfield/lib/src/TiItemLabelComponent.ts b/src/formfield/lib/src/TiItemLabelComponent.ts new file mode 100644 index 0000000..8ab8745 --- /dev/null +++ b/src/formfield/lib/src/TiItemLabelComponent.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterContentInit, ChangeDetectionStrategy, Component, ElementRef } from '@angular/core'; +import { TiItemComponent } from './TiItemComponent'; +/** + * 本组件嵌在 tiItem 中使用,包裹着单个表单条目中 label显示的dom片段 + */ +@Component({ + selector: 'ti-item-label', + templateUrl: './formfield-item-label.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiItemLabelComponent implements AfterContentInit { + constructor(private item: TiItemComponent, private elementRef: ElementRef) {} + + ngAfterContentInit(): void { + this.item.label = this.elementRef.nativeElement; + // onpush策略 无法通知formfiled值变更,需要手动使formfiled强制变更检测一次 + this.item.formfield.changeDetector.detectChanges(); + } +} diff --git a/src/formfield/lib/src/formfield-btn-item.html b/src/formfield/lib/src/formfield-btn-item.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/formfield/lib/src/formfield-btn-item.html @@ -0,0 +1 @@ + diff --git a/src/formfield/lib/src/formfield-item-label.html b/src/formfield/lib/src/formfield-item-label.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/formfield/lib/src/formfield-item-label.html @@ -0,0 +1 @@ + diff --git a/src/formfield/lib/src/formfield-item.html b/src/formfield/lib/src/formfield-item.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/formfield/lib/src/formfield-item.html @@ -0,0 +1 @@ + diff --git a/src/formfield/lib/src/formfield.html b/src/formfield/lib/src/formfield.html new file mode 100644 index 0000000..68f8dfd --- /dev/null +++ b/src/formfield/lib/src/formfield.html @@ -0,0 +1,112 @@ +
{{title}}
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+
+ + +
+ +
+
+
+
+ + +
+ + +
+
+
+
diff --git a/src/formfield/lib/src/formfield.less b/src/formfield/lib/src/formfield.less new file mode 100644 index 0000000..a2f5744 --- /dev/null +++ b/src/formfield/lib/src/formfield.less @@ -0,0 +1,114 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-formfield-item-padding-bottom: var(--ti-common-space-5x); + --ti-formfield-item-text-from-line-height: var(--ti-common-line-height-number); +} + +.ti3-form-title { + font-weight: var(--ti-common-font-weight-7); + line-height: var(--ti-common-size-5x); + font-size: var(--ti-common-font-size-1); + color: var(--ti-common-color-text-primary); + margin-bottom: var(--ti-common-space-5x); +} + +.ti3-form { + text-align: left; + & > tbody, + div { + vertical-align: inherit; + & tr { + vertical-align: inherit; + & > td { + vertical-align: inherit; // 此处与Tiny2设置不一致 + white-space: normal; //加此属性防止在tiny2表格中使用formfield导致td内容无法换行 + &.ti3-form-label { + white-space: nowrap; + } + } + } + } + + .ti3-form-grid-container { + display: grid; + .ti3-form-label, + .ti3-form-require, + .ti3-form-content { + padding-bottom: 0; + } + } + + .ti3-form-list { + & > .ti3-form-label, + & > .ti3-form-require, + & > .ti3-form-content, + & > td > .ti3-form-content { + // 上下表单布局content需要单独设置 + padding-bottom: 0; + } + } + + .ti3-form-require { + padding: 0 var(--ti-common-space-0) var(--ti-formfield-item-padding-bottom) 0; + .ti3-icon { + line-height: var(--ti-common-size-7x); + font-weight: normal; + color: var(--ti-common-color-error); + font-size: var(--ti-common-size-4x); + } + } + + .ti3-form-require-container { + width: var(--ti-common-size-4x); + display: inline-block; + } + + .ti3-form-label { + line-height: var(--ti-common-size-7x); + padding: 0 var(--ti-common-space-5x) var(--ti-formfield-item-padding-bottom) 0; + color: var(--ti-common-color-text-secondary); + font-size: var(--ti-common-font-size-base); + } + + .ti3-form-content { + word-break: break-word; + padding-bottom: var(--ti-formfield-item-padding-bottom); + } + + td.ti3-form-button-item, + .ti3-form-grid-container + .ti3-form-button-item { + padding-top: var(--ti-formfield-item-padding-bottom); + } + + .ti3-text-form { + & .ti3-text-form-label, + & .ti3-text-form-content { + line-height: var(--ti-formfield-item-text-from-line-height); + padding-top: var(--ti-common-space-10); + padding-bottom: var(--ti-common-space-10); + } + .ti3-text-form-label { + color: var(--ti-common-color-text-weaken); + } + } + + // 上下布局样式 + .ti3-form-top-label { + display: flex; + align-items: center; + color: var(--ti-common-color-text-secondary); + font-size: var(--ti-common-font-size-base); + padding-bottom: var(--ti-common-space-2x); + white-space: nowrap; + } + + .ti3-form-top-require { + color: var(--ti-common-color-error); + font-size: var(--ti-common-size-4x); + } + + .ti3-form-top-text { + line-height: var(--ti-common-line-height-number); + } +} diff --git a/src/grid/lib/index.ts b/src/grid/lib/index.ts new file mode 100644 index 0000000..ef9ae25 --- /dev/null +++ b/src/grid/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiGridModule'; diff --git a/src/grid/lib/ng-package.json b/src/grid/lib/ng-package.json new file mode 100644 index 0000000..a7b0227 --- /dev/null +++ b/src/grid/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/grid", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/grid/lib/package.json b/src/grid/lib/package.json new file mode 100644 index 0000000..98c3450 --- /dev/null +++ b/src/grid/lib/package.json @@ -0,0 +1,8 @@ +{ + "name": "@opentiny/ng-grid", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/grid/lib/project.json b/src/grid/lib/project.json new file mode 100644 index 0000000..27460e1 --- /dev/null +++ b/src/grid/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/grid/lib", + "sourceRoot": "src/grid/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/grid"], + "options": { + "project": "src/grid/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/grid"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js grid" + }, + { + "command": "ng default-build grid" + }, + { + "command": "node build/clear-default-theme.js grid" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/grid && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build grid && ng pack grid && node build/publish.js grid --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/grid/lib/src/TiGridComponent.ts b/src/grid/lib/src/TiGridComponent.ts new file mode 100644 index 0000000..842b5d6 --- /dev/null +++ b/src/grid/lib/src/TiGridComponent.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ViewEncapsulation } from '@angular/core'; + +/** + * @ignore + * + * 栅格样式组件 + * + * 10.0.4及之前的版本grid.less是全局的。10.1.0版本为了实现样式按需加载,将grid.less封装在一个组件模块,所以该组件实际只提供了样式。 + * + * 使用示例在 card 组件示例中 + * + */ +@Component({ + selector: '.ti-row', // 兼容延用栅格样式选择器 + template: '', + styleUrls: ['./grid.less'], + encapsulation: ViewEncapsulation.None +}) +export class TiGridComponent {} diff --git a/src/grid/lib/src/TiGridModule.ts b/src/grid/lib/src/TiGridModule.ts new file mode 100644 index 0000000..5836ae8 --- /dev/null +++ b/src/grid/lib/src/TiGridModule.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; + +import { TiGridComponent } from './TiGridComponent'; + +/** + * @ignore + * + */ +@NgModule({ + exports: [TiGridComponent], + declarations: [TiGridComponent] +}) +export class TiGridModule {} +export { TiGridComponent } from './TiGridComponent'; diff --git a/src/grid/lib/src/grid.less b/src/grid/lib/src/grid.less new file mode 100644 index 0000000..47bdf72 --- /dev/null +++ b/src/grid/lib/src/grid.less @@ -0,0 +1,108 @@ +@import '../../../themes/basic/base-all.less'; +// 根据规范,总共分四种分辨率 lg: 1760~1920【1920】,md:1600~1759【1680】, sm:1440~1599【1440】, xs:1280~1439【1280、1366】 +// 以下变量要在less 函数中使用,所以没有使用css-var + +@tp-grid-columns: 24; +@tp-grid-gutter-width: 20px; +@tp-screen-xs-min: 1280px; +@tp-screen-sm-min: 1440px; +@tp-screen-md-min: 1600px; +@tp-screen-lg-min: 1760px; + +.make-grid-columns(); + +.ti-row { + .make-row(); +} + +// 网格函数 +.make-row(@gutter: @tp-grid-gutter-width) { + margin-right: -@gutter; + .clearfix(); +} + +.make-grid(xs); + +@media (min-width: @tp-screen-sm-min) { + .make-grid(sm); +} + +@media (min-width: @tp-screen-md-min) { + .make-grid(md); +} + +@media (min-width: @tp-screen-lg-min) { + .make-grid(lg); +} + +.make-grid-columns() { + .col(@index) { + @item: ~'.ti3-col-xs-@{index}, .ti3-col-sm-@{index}, .ti3-col-md-@{index}, .ti3-col-lg-@{index}'; + .col((@index + 1), @item); + } + + .col(@index, @list) when (@index =< @tp-grid-columns) { + @item: ~'.ti3-col-xs-@{index}, .ti3-col-sm-@{index}, .ti3-col-md-@{index}, .ti3-col-lg-@{index}'; + .col((@index + 1), ~'@{list}, @{item}'); + } + + .col(@index, @list) when (@index > @tp-grid-columns) { + @{list} { + position: relative; + min-height: 1px; + } + } + + .col(1); +} + +.make-grid(@class, @gutter: @tp-grid-gutter-width) { + .float-grid-columns(@class); + .loop-grid-columns(@tp-grid-columns, @class, width, @gutter); +} + +.remake-grid(@class, @gutter: @tp-grid-gutter-width) { + .loop-grid-columns(@tp-grid-columns, @class, width, @gutter); +} + +.float-grid-columns(@class) { + .col(@index) { + @item: ~'.ti3-col-@{class}-@{index}'; + .col((@index + 1), @item); + } + + .col(@index, @list) when (@index =< @tp-grid-columns) { + @item: ~'.ti3-col-@{class}-@{index}'; + .col((@index + 1), ~'@{list}, @{item}'); + } + + .col(@index, @list) when (@index > @tp-grid-columns) { + @{list} { + float: left; + } + } + + .col(1); +} + +.loop-grid-columns(@index, @class, @type, @gutter) when (@index >= 0) { + .calc-grid-column(@index, @class, @type, @gutter); + .width-grid-column(@index, @class, @type, @gutter); + .loop-grid-columns((@index - 1), @class, @type, @gutter); +} + +.width-grid-column(@index, @class, @type, @gutter) when (@type = width) and (@index > 0) and (@gutter = 0) { + .ti3-col-@{class}-@{index} { + margin-right: @gutter; + width: percentage((@index / @tp-grid-columns)); + } +} + +.calc-grid-column(@index, @class, @type, @gutter) when (@type = width) and (@index > 0) and (@gutter > 0) { + @tmp-width: percentage((@index / @tp-grid-columns)); + + .ti3-col-@{class}-@{index} { + margin-right: @gutter; + width: calc(~'@{tmp-width} - @{gutter}'); + } +} diff --git a/src/guides/demo/project.json b/src/guides/demo/project.json new file mode 100644 index 0000000..6a6ce8b --- /dev/null +++ b/src/guides/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/guides/demo", + "sourceRoot": "src/guides/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/guides", + "index": "src/guides/demo/src/index.html", + "main": "src/guides/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/guides/demo/tsconfig.app.json", + "assets": ["src/guides/demo/src/favicon.ico", "src/guides/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "guides-demo:build:production" + }, + "development": { + "browserTarget": "guides-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js guides" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/guides/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/guides/demo/tsconfig.spec.json", + "karmaConfig": "src/guides/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/guides/demo/src/app/AppComponent.ts b/src/guides/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/guides/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/guides/demo/src/app/AppModule.ts b/src/guides/demo/src/app/AppModule.ts new file mode 100644 index 0000000..8be1e69 --- /dev/null +++ b/src/guides/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { GuidesTestModule } from './guides/GuidesTestModule'; + +@NgModule({ + imports: [ + GuidesTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/guides/demo/src/app/IndexComponent.ts b/src/guides/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..108e6f4 --- /dev/null +++ b/src/guides/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { GuidesTestModule } from './guides/GuidesTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = GuidesTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/guides/demo/src/app/app.html b/src/guides/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/guides/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/guides/demo/src/app/guides/GuidesBasicComponent.ts b/src/guides/demo/src/app/guides/GuidesBasicComponent.ts new file mode 100644 index 0000000..ea5d295 --- /dev/null +++ b/src/guides/demo/src/app/guides/GuidesBasicComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './guides-basic.html' +}) +export class GuidesBasicComponent { + items: Array = [ + { + label: 'First step', + content: 'The content of the first step' + }, + { + label: 'Second step', + content: 'The content of the second step' + } + ]; +} diff --git a/src/guides/demo/src/app/guides/GuidesGuidestepsComponent.ts b/src/guides/demo/src/app/guides/GuidesGuidestepsComponent.ts new file mode 100644 index 0000000..3623e83 --- /dev/null +++ b/src/guides/demo/src/app/guides/GuidesGuidestepsComponent.ts @@ -0,0 +1,70 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './guides-guidesteps.html' +}) +export class GuidesGuidestepsComponent { + activeIndex: number = 1; + cards: Array = [ + { + title: '步骤一', + content: '步骤一的步骤一的内容步骤一的内容步骤一的内容步骤一的内容内容', + isComplete: true, + items: [ + { + stepTitle: '小步骤一', + content: '第一步' + }, + { + stepTitle: '小步骤二', + content: '第二步' + } + ] + }, + { + title: '步骤二', + content: '步骤一的步骤一的内容步骤一的内容步骤一的内容步骤一的内容内容', + isComplete: false, + items: [ + { + stepTitle: '小步骤一', + content: '第一步' + }, + { + stepTitle: '小步骤二', + content: '第二步' + } + ] + }, + { + title: '步骤三', + content: '步骤一的步骤一的内容步骤一的内容步骤一的内容步骤一的内容内容', + isComplete: false, + items: [ + { + stepTitle: '小步骤一', + content: '第一步' + }, + { + stepTitle: '小步骤二', + content: '第二步' + } + ] + } + ]; + + clickShow1Fn(): void { + this.activeIndex = 0; + this.cards[0].isComplete = false; + this.cards[1].isComplete = false; + } + clickShow2Fn(): void { + this.activeIndex = 1; + this.cards[0].isComplete = true; + } + clickShow3Fn(): void { + this.activeIndex = 2; + this.cards[0].isComplete = true; + this.cards[1].isComplete = true; + } +} diff --git a/src/guides/demo/src/app/guides/GuidesTabComponent.ts b/src/guides/demo/src/app/guides/GuidesTabComponent.ts new file mode 100644 index 0000000..d19ff2a --- /dev/null +++ b/src/guides/demo/src/app/guides/GuidesTabComponent.ts @@ -0,0 +1,89 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './guides-tab.html' +}) +export class GuidesTabComponent { + card1: Array = [ + { + title: '第一项', + active: true, + activeIndex: 1, + items: [ + { + label: '第一项步骤一', + content: '步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一' + }, + { + label: '步骤二', + content: '步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二' + }, + { + label: '步骤三', + content: '步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三' + }, + { + label: '步骤四', + content: '步骤四步骤四步骤四步骤四步骤四步骤四步骤四步骤四步骤四步骤四步骤四步骤四步骤四步骤四步骤四步骤四步骤四步骤四' + } + ] + }, + { + title: '第二项', + items: [ + { + label: '第二项步骤一', + content: '步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一步骤一' + }, + { + label: '步骤二', + content: '步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二步骤二' + }, + { + label: '步骤三', + content: '步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三步骤三' + } + ] + } + ]; + card2: Array = [ + { + title: '第一项', + active: true, + items: [ + { + label: '第一项步骤一', + content: '提交订单' + }, + { + label: '步骤二', + content: '确认订单' + }, + { + label: '步骤三', + content: '完成付款' + } + ] + }, + { + title: '第二项', + items: [ + { + label: '第二项步骤一', + content: '提交订单' + }, + { + label: '步骤二', + content: '确认订单' + }, + { + label: '步骤三', + content: '完成付款' + } + ] + } + ]; + clickFn(): void { + console.log('触发点击事件'); + } +} diff --git a/src/guides/demo/src/app/guides/GuidesTestModule.ts b/src/guides/demo/src/app/guides/GuidesTestModule.ts new file mode 100644 index 0000000..8ccf867 --- /dev/null +++ b/src/guides/demo/src/app/guides/GuidesTestModule.ts @@ -0,0 +1,39 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiGuidesModule } from '@opentiny/ng-guides'; +import { TiTabModule } from '@opentiny/ng-tab'; +import { GuidesBasicComponent } from './GuidesBasicComponent'; +import { GuidesTabComponent } from './GuidesTabComponent'; +import { GuidesGuidestepsComponent } from './GuidesGuidestepsComponent'; +import { GuidesTypeComponent } from './GuidesTypeComponent'; + +@NgModule({ + imports: [CommonModule, TiGuidesModule, TiTabModule, RouterModule.forChild(GuidesTestModule.ROUTES)], + declarations: [GuidesBasicComponent, GuidesTabComponent, GuidesGuidestepsComponent, GuidesTypeComponent] +}) +export class GuidesTestModule { + static readonly ROUTES: Routes = [ + { + path: 'guides/guides-basic', + component: GuidesBasicComponent, + data: { label: '基础' } + }, + { + path: 'guides/guides-tab', + component: GuidesTabComponent, + data: { label: '与组件结合使用' } + }, + { + path: 'guides/guides-guidesteps', + component: GuidesGuidestepsComponent, + data: { label: '与组件结合使用' } + }, + { + path: 'guides/guides-type', + component: GuidesTypeComponent, + data: { label: '错误失败状态' } + } + ]; +} diff --git a/src/guides/demo/src/app/guides/GuidesTypeComponent.ts b/src/guides/demo/src/app/guides/GuidesTypeComponent.ts new file mode 100644 index 0000000..5e6ecbd --- /dev/null +++ b/src/guides/demo/src/app/guides/GuidesTypeComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './guides-type.html' +}) +export class GuidesTypeComponent { + activeIndex: number = 2; + steps: Array = [ + { + label: 'First step', + content: 'The content of the first step', + body: 'The body of the first step', + type: 'error' + }, + { + label: 'Second step', + content: 'The content of the second step', + body: 'The body of the second step', + type: 'info' + }, + { + label: 'Third step', + content: 'The content of the third step', + body: 'The body of the third step' + }, + { + label: 'Fourth step', + content: 'The content of the fourth step', + body: 'The body of the fourth step' + } + ]; +} diff --git a/src/guides/demo/src/app/guides/guides-basic.html b/src/guides/demo/src/app/guides/guides-basic.html new file mode 100644 index 0000000..175d3d8 --- /dev/null +++ b/src/guides/demo/src/app/guides/guides-basic.html @@ -0,0 +1,14 @@ +

1.基础样式

+ + + {{item.label}} + {{item.content}} + + +

2.小尺寸样式

+ + + {{item.label}} + {{item.content}} + + diff --git a/src/guides/demo/src/app/guides/guides-guidesteps.html b/src/guides/demo/src/app/guides/guides-guidesteps.html new file mode 100644 index 0000000..9ca8b37 --- /dev/null +++ b/src/guides/demo/src/app/guides/guides-guidesteps.html @@ -0,0 +1,22 @@ + + + + {{card.label}} + + + + +

操作指南

+ + + +
+
+
diff --git a/src/guides/demo/src/app/guides/guides-tab.html b/src/guides/demo/src/app/guides/guides-tab.html new file mode 100644 index 0000000..16c89dd --- /dev/null +++ b/src/guides/demo/src/app/guides/guides-tab.html @@ -0,0 +1,38 @@ +

1.描述

+

特殊场景展示

+

2.示例

+

2.1 guides和tab组件结合使用,且第一个页签下,卡片第二项为激活项

+

+ + + {{tab.title}} + + + {{item.label}} + {{item.content}} + + + + +

+ + + +

diff --git a/src/guides/demo/src/app/guides/guides-type.html b/src/guides/demo/src/app/guides/guides-type.html new file mode 100644 index 0000000..659c69a --- /dev/null +++ b/src/guides/demo/src/app/guides/guides-type.html @@ -0,0 +1,16 @@ +

1.基础样式

+ + + {{step.label}} + {{step.content}} + {{step.body}} + + +

2.小尺寸样式

+ + + {{step.label}} + {{step.content}} + {{step.body}} + + diff --git a/src/guides/demo/src/app/guides/webdoc/guides-demos.js b/src/guides/demo/src/app/guides/webdoc/guides-demos.js new file mode 100644 index 0000000..02e76d2 --- /dev/null +++ b/src/guides/demo/src/app/guides/webdoc/guides-demos.js @@ -0,0 +1,29 @@ +export default { + column: '1', + demos: [ + { + demoId: 'guides-basic', + name: { + 'zh-CN': '基础', + 'en-US': 'guides basic' + }, + desc: { + 'zh-CN': '

通过属性small配置小尺寸样式。

', + 'en-US': 'guides basic' + } + }, + { + demoId: 'guides-type', + name: { + 'zh-CN': '错误/失败状态', + 'en-US': 'guides type' + }, + desc: { + 'zh-CN': + '

通过属性activeIndex配置当前激活项;通过属性type配置错误/失败状态,包含infoerror两种类型。

', + 'en-US': 'guides type' + }, + apis: ['TiGuideComponent.properties.type', 'TiGuidesComponent.properties.activeIndex'] + } + ] +}; diff --git a/src/guides/demo/src/app/guides/webdoc/guides.cn.md b/src/guides/demo/src/app/guides/webdoc/guides.cn.md new file mode 100644 index 0000000..3961d7a --- /dev/null +++ b/src/guides/demo/src/app/guides/webdoc/guides.cn.md @@ -0,0 +1,15 @@ +--- +title: Guides 情景引导 +--- + +# Guides 情景引导 + +
+ +引导用户操作步骤的组件。 + +```typescript +import { TiGuidesModule } from '@opentiny/ng'; +``` + +
diff --git a/src/guides/demo/src/app/guides/webdoc/guides.en.md b/src/guides/demo/src/app/guides/webdoc/guides.en.md new file mode 100644 index 0000000..5bd3f78 --- /dev/null +++ b/src/guides/demo/src/app/guides/webdoc/guides.en.md @@ -0,0 +1,13 @@ +--- +title: Guides +--- + +# Guides + +
+ +```typescript +import { TiGuidesModule } from '@opentiny/ng'; +``` + +
diff --git a/src/guides/demo/src/favicon.ico b/src/guides/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d7a7af634ee6d643ac81d81feadff569ac25de3d GIT binary patch literal 300852 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 + + + + + + Tiny + + + + + + + + + diff --git a/src/guides/demo/src/main.ts b/src/guides/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/guides/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/guides/demo/tsconfig.app.json b/src/guides/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/guides/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/guides/lib/index.ts b/src/guides/lib/index.ts new file mode 100644 index 0000000..3ce1ebc --- /dev/null +++ b/src/guides/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiGuidesModule'; +export * from './src/TiGuidesComponent'; diff --git a/src/guides/lib/ng-package.json b/src/guides/lib/ng-package.json new file mode 100644 index 0000000..52deb1e --- /dev/null +++ b/src/guides/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/guides", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/guides/lib/package.json b/src/guides/lib/package.json new file mode 100644 index 0000000..98a2ced --- /dev/null +++ b/src/guides/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-guides", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/guides/lib/project.json b/src/guides/lib/project.json new file mode 100644 index 0000000..e980d24 --- /dev/null +++ b/src/guides/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/guides/lib", + "sourceRoot": "src/guides/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/guides"], + "options": { + "project": "src/guides/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/guides"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js guides" + }, + { + "command": "ng default-build guides" + }, + { + "command": "node build/clear-default-theme.js guides" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/guides && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build guides && ng pack guides && node build/publish.js guides --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/guides/lib/src/TiGuideBodyComponent.ts b/src/guides/lib/src/TiGuideBodyComponent.ts new file mode 100644 index 0000000..b7f0ed1 --- /dev/null +++ b/src/guides/lib/src/TiGuideBodyComponent.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Renderer2 } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiRenderer } from '@opentiny/ng-renderer'; + +import { TiGuideComponent } from './TiGuideComponent'; +import { TiGuidesComponent } from './TiGuidesComponent'; +/** + * TiGuideBodyComponent 是单个引导模块的body区域 + */ +@Component({ + selector: 'ti-guide-body', + templateUrl: './guide-body.html', + styleUrls: ['./guide-body.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiGuideBodyComponent extends TiBaseComponent { + constructor( + private elementRef: ElementRef, + private renderer2: Renderer2, + private tiRenderer: TiRenderer, + private guideComp: TiGuideComponent, + private guidesComp: TiGuidesComponent, + private changeDetectorRef: ChangeDetectorRef + ) { + super(elementRef, renderer2); + } + + ngOnInit(): void { + super.ngOnInit(); + this.guideComp.hasBody = true; + this.tiRenderer.insertAfter(this.nativeElement, this.guidesComp.nativeElement); + } + + ngDoCheck(): void { + // guides组件当前激活项变化时,需要手动更新,否则无法触发模板上绑定的事件并更新视图 + if (this.guidesComp.activeIndex !== this.guidesComp.lastActiveIndex) { + this.changeDetectorRef.markForCheck(); + } + } + + /** + * @ignore + * 展示激活项body + */ + public showActiveBody(): boolean { + return this.guidesComp.activeIndex === this.guideComp.cardIndex; + } +} diff --git a/src/guides/lib/src/TiGuideComponent.ts b/src/guides/lib/src/TiGuideComponent.ts new file mode 100644 index 0000000..e338f74 --- /dev/null +++ b/src/guides/lib/src/TiGuideComponent.ts @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, Renderer2 } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; + +import { TiGuidesComponent } from './TiGuidesComponent'; +/** + * TiGuideComponent 是单个引导模块组件 + */ +@Component({ + selector: 'ti-guide', + templateUrl: './guide.html', + styleUrls: ['./guide.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-guide-card-container]': 'true', + '[class.ti3-guide-card-undo]': 'isUndo(cardIndex)', + '[class.ti3-guide-card-active]': 'isActive(cardIndex)', + '[class.ti3-guide-card-done]': 'isDone(cardIndex)', + '[class.ti3-guide-card-error]': "type === 'error'" + } +}) +export class TiGuideComponent extends TiBaseComponent { + constructor( + protected elementRef: ElementRef, + protected renderer2: Renderer2, + private guidesComp: TiGuidesComponent, + private changeDetectorRef: ChangeDetectorRef + ) { + super(elementRef, renderer2); + } + /** + * 当前步骤的状态,默认值是 'info' + */ + @Input() type: 'info' | 'error' = 'info'; + + /** + * @ignore + * 下标值 + */ + public cardIndex: number = -1; + /** + * @ignore + * 是否含有body + */ + public hasBody: boolean = false; + + ngAfterContentInit(): void { + super.ngAfterContentInit(); + this.setCardIndex(); + this.cardIndex = this.nativeElement.cardIndex; + } + + ngDoCheck(): void { + // guides组件当前激活项变化时,需要手动更新,否则无法触发模板上绑定的事件并更新视图 + if (this.guidesComp.activeIndex !== this.guidesComp.lastActiveIndex) { + this.changeDetectorRef.markForCheck(); + } + } + + /** + * @ignore + * 设置步骤标志 + */ + private setCardIndex(): void { + const cardNum: number = this.guidesComp.nativeElement.children.length; + for (let i: number = 0; i < cardNum; i++) { + this.guidesComp.nativeElement.children[i].cardIndex = i; + if (cardNum <= 2) { + this.renderer.setStyle(this.guidesComp.nativeElement.children[i], 'max-width', '445px'); + } + } + } + + /** + * @ignore + * 是否待办项 + */ + public isUndo(index: number): boolean { + return this.guidesComp.activeIndex >= 0 && index > this.guidesComp.activeIndex; + } + /** + * @ignore + * 是否当前项 + */ + public isActive(index: number): boolean { + return this.guidesComp.activeIndex >= 0 && index === this.guidesComp.activeIndex; + } + /** + * @ignore + * 是否已完成 + */ + public isDone(index: number): boolean { + return this.guidesComp.activeIndex >= 0 && index < this.guidesComp.activeIndex; + } + /** + * @ignore + * 是否含有间距 + */ + public hasSpace(index: number): boolean { + return this.isActive(index) && this.hasBody; + } +} diff --git a/src/guides/lib/src/TiGuideContentComponent.ts b/src/guides/lib/src/TiGuideContentComponent.ts new file mode 100644 index 0000000..9390c5c --- /dev/null +++ b/src/guides/lib/src/TiGuideContentComponent.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { ChangeDetectionStrategy, Component } from '@angular/core'; +/** + * TiGuideContentComponent 是单个引导模块组件的content区域 + */ +@Component({ + selector: 'ti-guide-content', + template: ``, + styleUrls: ['./guide-content.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiGuideContentComponent {} diff --git a/src/guides/lib/src/TiGuideHeaderComponent.ts b/src/guides/lib/src/TiGuideHeaderComponent.ts new file mode 100644 index 0000000..bb47b9c --- /dev/null +++ b/src/guides/lib/src/TiGuideHeaderComponent.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { ChangeDetectionStrategy, Component } from '@angular/core'; +/** + * TiGuideHeaderComponent 是单个引导模块组件的标题头 + */ +@Component({ + selector: 'ti-guide-header', + template: ``, + styleUrls: ['./guide-header.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiGuideHeaderComponent {} diff --git a/src/guides/lib/src/TiGuidesComponent.ts b/src/guides/lib/src/TiGuidesComponent.ts new file mode 100644 index 0000000..0c381ef --- /dev/null +++ b/src/guides/lib/src/TiGuidesComponent.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Input, + Renderer2, + SimpleChange, + SimpleChanges +} from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +/** + * 情景引导组件 + */ +@Component({ + selector: 'ti-guides', + template: ``, + styleUrls: ['./guides.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiGuidesComponent extends TiBaseComponent { + /** + * 激活项的下标,默认值为 -1 表示无激活状态;显示的步骤下标小于该值为完成项,大于该值则为待办项。 + */ + @Input() activeIndex: number = -1; + /** + * @ignore + * 上次激活项 + */ + public lastActiveIndex: number; + constructor(protected elementRef: ElementRef, protected renderer2: Renderer2, private changeDetectorRef: ChangeDetectorRef) { + super(elementRef, renderer2); + } + + ngOnInit(): void { + this.lastActiveIndex = this.activeIndex; + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + const activeIndexObj: SimpleChange = changes['activeIndex']; + if (activeIndexObj && !activeIndexObj.isFirstChange()) { + this.lastActiveIndex = activeIndexObj.previousValue; + } + } +} diff --git a/src/guides/lib/src/TiGuidesModule.ts b/src/guides/lib/src/TiGuidesModule.ts new file mode 100644 index 0000000..d3d2d21 --- /dev/null +++ b/src/guides/lib/src/TiGuidesModule.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIconModule } from '@opentiny/ng-icon'; + +import { TiGuidesComponent } from './TiGuidesComponent'; +import { TiGuideComponent } from './TiGuideComponent'; +import { TiGuideHeaderComponent } from './TiGuideHeaderComponent'; +import { TiGuideContentComponent } from './TiGuideContentComponent'; +import { TiGuideBodyComponent } from './TiGuideBodyComponent'; + +@NgModule({ + imports: [CommonModule, TiIconModule], + exports: [TiGuidesComponent, TiGuideComponent, TiGuideHeaderComponent, TiGuideContentComponent, TiGuideBodyComponent], + declarations: [TiGuidesComponent, TiGuideComponent, TiGuideHeaderComponent, TiGuideContentComponent, TiGuideBodyComponent] +}) +export class TiGuidesModule {} +export { TiGuidesComponent } from './TiGuidesComponent'; +export { TiGuideComponent } from './TiGuideComponent'; +export { TiGuideHeaderComponent } from './TiGuideHeaderComponent'; +export { TiGuideContentComponent } from './TiGuideContentComponent'; +export { TiGuideBodyComponent } from './TiGuideBodyComponent'; diff --git a/src/guides/lib/src/guide-body.html b/src/guides/lib/src/guide-body.html new file mode 100644 index 0000000..0e03ead --- /dev/null +++ b/src/guides/lib/src/guide-body.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/guides/lib/src/guide-body.less b/src/guides/lib/src/guide-body.less new file mode 100644 index 0000000..75aef76 --- /dev/null +++ b/src/guides/lib/src/guide-body.less @@ -0,0 +1,11 @@ +@import '../../../themes/basic/mixins.less'; +.ti3-guide-body { + display: block; + font-size: var(--ti-common-font-size-base); + background: var(--ti-common-color-bg-white-normal); + box-shadow: var(--ti-common-shadow-1-down); + width: 100%; + padding: var(--ti-common-space-6x) var(--ti-common-space-6x) var(--ti-common-space-6x) var(--ti-common-space-6x); + .box-sizing(border-box); + margin-top: var(--ti-guide-body-space); // 内容区域和卡片非分离 +} diff --git a/src/guides/lib/src/guide-content.less b/src/guides/lib/src/guide-content.less new file mode 100644 index 0000000..6bc0836 --- /dev/null +++ b/src/guides/lib/src/guide-content.less @@ -0,0 +1,7 @@ +:host { + display: block; + background: var(--ti-common-color-bg-white-normal); + font-size: var(--ti-common-font-size-base); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-secondary); +} diff --git a/src/guides/lib/src/guide-header.less b/src/guides/lib/src/guide-header.less new file mode 100644 index 0000000..b937d98 --- /dev/null +++ b/src/guides/lib/src/guide-header.less @@ -0,0 +1,14 @@ +:host { + display: block; + background: var(--ti-common-color-bg-white-normal); + font-weight: bold; + font-size: var(--ti-common-font-size-1); + line-height: 20px; + color: var(--ti-common-color-text-primary); + + // 用户设置small属性卡片样式 + [small] & { + font-size: var(--ti-common-font-size-base); + font-weight: normal; + } +} diff --git a/src/guides/lib/src/guide.html b/src/guides/lib/src/guide.html new file mode 100644 index 0000000..ac39dc3 --- /dev/null +++ b/src/guides/lib/src/guide.html @@ -0,0 +1,18 @@ + + + + +
+ + + + {{cardIndex + 1}} +
+ + +
+ +
+ + +
diff --git a/src/guides/lib/src/guide.less b/src/guides/lib/src/guide.less new file mode 100644 index 0000000..5843698 --- /dev/null +++ b/src/guides/lib/src/guide.less @@ -0,0 +1,165 @@ +@import '../../../themes/basic/mixins.less'; + +// 卡片容器样式 +:host/* .ti3-guide-card-container */ { + position: relative; + display: inline-block; + margin-right: var(--ti-common-space-2x); + box-shadow: var(--ti-common-shadow-1-down); + background: var(--ti-common-color-bg-white-normal); + flex: 1; + &:hover { + box-shadow: var(--ti-common-shadow-3-down); + } + &:last-child { + margin-right: 0; + } +} + +// 卡片样式 +::ng-deep .ti3-guide-card { + background: var(--ti-common-color-bg-white-normal); + border-top: var(--ti-guide-card-border-top) solid var(--ti-common-color-line-active); + .box-sizing(border-box); + padding: var(--ti-common-space-6x) var(--ti-common-space-6x) var(--ti-common-space-5x); + // 待办项卡片 + :host.ti3-guide-card-undo & { + border-top-color: var(--ti-common-color-line-normal); + } + // 当前项卡片 + :host.ti3-guide-card-active & { + border-top-color: var(--ti-common-color-line-active); + [small] & { + border-top-color: var(--ti-common-color-warn); + } + } + // 已完成卡片 + :host.ti3-guide-card-done & { + border-top-color: var(--ti-common-color-success); + } + + // 错误/失败 + :host.ti3-guide-card-error & { + border-top-color: var(--ti-common-color-error-border); + [small] & { + border-top-color: var(--ti-common-color-error-border); + } + } + + // 用户设置small属性卡片样式 + :host & { + [small] & { + padding: var(--ti-common-space-3x) 0 var(--ti-common-space-3x) var(--ti-common-space-6x); + } + } +} + +// 卡片标志样式 +::ng-deep .ti3-guide-circle { + position: absolute; + top: -15px; + left: var(--ti-guide-circle-space-left); + width: var(--ti-guide-circle-size); + height: var(--ti-guide-circle-size); + border-radius: 50%; + border: var(--ti-guide-circle-border) solid var(--ti-common-color-line-active); + .box-sizing(border-box); + background: var(--ti-common-color-bg-white-normal); + & .ti3-guide-number { + display: inline-block; + color: var(--ti-common-color-text-highlight); + font-size: var(--ti-common-font-size-4); + width: 100%; + height: 100%; + line-height: var(--ti-common-line-height-number); + text-align: center; + } + // 待办项卡片 + :host.ti3-guide-card-undo & { + border-color: var(--ti-common-color-line-normal); + & .ti3-guide-number { + color: var(--ti-common-color-text-primary); + } + } + // 当前项卡片 + :host.ti3-guide-card-active & { + border-color: var(--ti-common-color-line-active); + background: var(--ti-common-color-icon-graybg-active); + & .ti3-guide-number { + color: var(--ti-common-color-text-white); + } + [small] & { + border-color: var(--ti-common-color-warn); + background: var(--ti-common-color-warn); + & .ti3-guide-number { + color: var(--ti-common-color-text-white); + } + } + } + // 已完成卡片 + :host.ti3-guide-card-done & { + border-color: var(--ti-common-color-success); + & .ti3-guide-number { + color: var(--ti-common-color-success); + line-height: var(--ti-guide-circle-size); // 为使图标居中显示 + } + } + + // 错误/失败 + :host.ti3-guide-card-error & { + border-color: var(--ti-common-color-error-border); + background-color: var(--ti-common-color-bg-white-normal); + & .ti3-guide-number { + color: var(--ti-common-color-error); + line-height: calc(var(--ti-guide-circle-size) - 2 * var(--ti-guide-circle-border)); // 为使图标居中显示 + } + + [small] & { + border-color: var(--ti-common-color-error-border); + background-color: var(--ti-common-color-bg-white-normal); + & .ti3-guide-number { + color: var(--ti-common-color-error); + } + } + } + + // 用户设置small属性卡片标志样式 + :host & { + [small] & { + width: var(--ti-guide-circle-size-small); + height: var(--ti-guide-circle-size-small); + top: -10px; + & .ti3-guide-number { + font-size: var(--ti-common-font-size-base); + line-height: 16px; + } + } + } +} + +// 卡片标志线 +.ti3-guide-line { + position: absolute; + width: calc(var(--ti-guide-circle-size) + 2 * var(--ti-guide-circle-white-space)); + height: var(--ti-guide-card-border-top); + background: var(--ti-common-color-bg-white-normal); + left: calc(var(--ti-guide-circle-space-left) - var(--ti-guide-circle-white-space)); + // 用户设置small属性卡片标志线样式 + :host & { + [small] & { + width: calc(var(--ti-guide-circle-size-small) + 2 * var(--ti-guide-circle-white-space)); + } + } +} + +// 有body时间距样式 +.ti3-guide-space { + width: 100%; + height: var(--ti-guide-body-space); + position: absolute; + bottom: calc(-1 * var(--ti-guide-body-space)); + background: transparent; + &.ti3-guide-space-active { + background: var(--ti-common-color-bg-white-normal); + } +} diff --git a/src/guides/lib/src/guides.less b/src/guides/lib/src/guides.less new file mode 100644 index 0000000..878c652 --- /dev/null +++ b/src/guides/lib/src/guides.less @@ -0,0 +1,16 @@ +@import '../../../themes/basic/mixins.less'; + +::ng-deep :root { + --ti-guide-circle-space-left: var(--ti-common-space-6x); // 圆圈距离卡片左边的距离是24px + --ti-guide-circle-white-space: 1px; + --ti-guide-circle-size: 32px; + --ti-guide-circle-size-small: 20px; + --ti-guide-card-border-top: 3px; + --ti-guide-body-space: var(--ti-common-space-2x); + --ti-guide-circle-border: 2px; +} +:host/* .ti-guides-container */ { + display: flex; + padding-top: 15px; + .box-sizing(border-box); +} diff --git a/src/guidesteps/demo/project.json b/src/guidesteps/demo/project.json new file mode 100644 index 0000000..ec0db6d --- /dev/null +++ b/src/guidesteps/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/guidesteps/demo", + "sourceRoot": "src/guidesteps/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/guidesteps", + "index": "src/guidesteps/demo/src/index.html", + "main": "src/guidesteps/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/guidesteps/demo/tsconfig.app.json", + "assets": ["src/guidesteps/demo/src/favicon.ico", "src/guidesteps/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "guidesteps-demo:build:production" + }, + "development": { + "browserTarget": "guidesteps-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js guidesteps" + } + ] + } + } + } +} diff --git a/src/guidesteps/demo/src/app/AppComponent.ts b/src/guidesteps/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/guidesteps/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/guidesteps/demo/src/app/AppModule.ts b/src/guidesteps/demo/src/app/AppModule.ts new file mode 100644 index 0000000..2500598 --- /dev/null +++ b/src/guidesteps/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { GuidestepsTestModule } from './guidesteps/GuidestepsTestModule'; + +@NgModule({ + imports: [ + GuidestepsTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/guidesteps/demo/src/app/IndexComponent.ts b/src/guidesteps/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..5727d93 --- /dev/null +++ b/src/guidesteps/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { GuidestepsTestModule } from './guidesteps/GuidestepsTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = GuidestepsTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/guidesteps/demo/src/app/app.html b/src/guidesteps/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/guidesteps/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/guidesteps/demo/src/app/guidesteps/GuidestepsBasicComponent.ts b/src/guidesteps/demo/src/app/guidesteps/GuidestepsBasicComponent.ts new file mode 100644 index 0000000..6476558 --- /dev/null +++ b/src/guidesteps/demo/src/app/guidesteps/GuidestepsBasicComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './guidesteps-basic.html' +}) +export class GuidestepsBasicComponent { + items: Array = [ + { + label: '小步骤一', + content: '小步骤一的内容小步骤一的内容小步骤一的内容小步骤一的内容小步骤一的内容' + }, + { + label: '小步骤二', + content: '小步骤二的内容小步骤二的内容小步骤二的内容小步骤二的内容小步骤二的内容' + }, + { + label: '小步骤三', + content: '小步骤三的内容小步骤三的内容小步骤三的内容小步骤三的内容小步骤三的内容' + } + ]; +} diff --git a/src/guidesteps/demo/src/app/guidesteps/GuidestepsIscompleteComponent.ts b/src/guidesteps/demo/src/app/guidesteps/GuidestepsIscompleteComponent.ts new file mode 100644 index 0000000..ebc5163 --- /dev/null +++ b/src/guidesteps/demo/src/app/guidesteps/GuidestepsIscompleteComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './guidesteps-iscomplete.html' +}) +export class GuidestepsIscompleteComponent { + isComplete: boolean = true; + items: Array = [ + { + label: '小步骤一', + content: '提交订单' + }, + { + label: '小步骤二', + content: '确认订单' + }, + { + label: '小步骤三', + content: '完成付款' + } + ]; +} diff --git a/src/guidesteps/demo/src/app/guidesteps/GuidestepsLargeComponent.ts b/src/guidesteps/demo/src/app/guidesteps/GuidestepsLargeComponent.ts new file mode 100644 index 0000000..4abe089 --- /dev/null +++ b/src/guidesteps/demo/src/app/guidesteps/GuidestepsLargeComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './guidesteps-large.html' +}) +export class GuidestepsLargeComponent { + items: Array = [ + { + label: '小步骤一', + content: '小步骤一的内容小步骤一的内容小步骤一的内容小步骤一的内容小步骤一的内容' + }, + { + label: '小步骤二', + content: '小步骤二的内容小步骤二的内容小步骤二的内容小步骤二的内容小步骤二的内容' + }, + { + label: '小步骤三', + content: '小步骤三的内容小步骤三的内容小步骤三的内容小步骤三的内容小步骤三的内容' + } + ]; +} diff --git a/src/guidesteps/demo/src/app/guidesteps/GuidestepsTestModule.ts b/src/guidesteps/demo/src/app/guidesteps/GuidestepsTestModule.ts new file mode 100644 index 0000000..3907c13 --- /dev/null +++ b/src/guidesteps/demo/src/app/guidesteps/GuidestepsTestModule.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiGuidestepsModule } from '@opentiny/ng'; + +import { GuidestepsBasicComponent } from './GuidestepsBasicComponent'; +import { GuidestepsIscompleteComponent } from './GuidestepsIscompleteComponent'; +import { GuidestepsLargeComponent } from './GuidestepsLargeComponent'; + +@NgModule({ + imports: [CommonModule, TiGuidestepsModule, RouterModule.forChild(GuidestepsTestModule.ROUTES)], + declarations: [GuidestepsBasicComponent, GuidestepsIscompleteComponent, GuidestepsLargeComponent] +}) +export class GuidestepsTestModule { + static readonly ROUTES: Routes = [ + { path: 'guidesteps/guidesteps-basic', component: GuidestepsBasicComponent }, + { path: 'guidesteps/guidesteps-iscomplete', component: GuidestepsIscompleteComponent }, + { path: 'guidesteps/guidesteps-large', component: GuidestepsLargeComponent } + ]; +} diff --git a/src/guidesteps/demo/src/app/guidesteps/guidesteps-basic.html b/src/guidesteps/demo/src/app/guidesteps/guidesteps-basic.html new file mode 100644 index 0000000..ee70304 --- /dev/null +++ b/src/guidesteps/demo/src/app/guidesteps/guidesteps-basic.html @@ -0,0 +1,6 @@ + + + {{item.label}} + {{item.content}} + + diff --git a/src/guidesteps/demo/src/app/guidesteps/guidesteps-iscomplete.html b/src/guidesteps/demo/src/app/guidesteps/guidesteps-iscomplete.html new file mode 100644 index 0000000..b088b87 --- /dev/null +++ b/src/guidesteps/demo/src/app/guidesteps/guidesteps-iscomplete.html @@ -0,0 +1,8 @@ + + + {{item.label}} + +

{{item.content}}

+
+
+
diff --git a/src/guidesteps/demo/src/app/guidesteps/guidesteps-large.html b/src/guidesteps/demo/src/app/guidesteps/guidesteps-large.html new file mode 100644 index 0000000..55f2658 --- /dev/null +++ b/src/guidesteps/demo/src/app/guidesteps/guidesteps-large.html @@ -0,0 +1,6 @@ + + + {{item.label}} + {{item.content}} + + diff --git a/src/guidesteps/demo/src/app/guidesteps/webdoc/guidesteps-demos.js b/src/guidesteps/demo/src/app/guidesteps/webdoc/guidesteps-demos.js new file mode 100644 index 0000000..6d3f612 --- /dev/null +++ b/src/guidesteps/demo/src/app/guidesteps/webdoc/guidesteps-demos.js @@ -0,0 +1,39 @@ +export default { + column: '2', + demos: [ + { + demoId: 'guidesteps-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

Guidesteps 组件最简单用法。

', + 'en-US': '' + } + }, + { + demoId: 'guidesteps-iscomplete', + name: { + 'zh-CN': '步骤完成', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

通过属性isComplete配置步骤是否完成。

', + 'en-US': '' + }, + apis: ['TiGuidestepComponent.properties.isComplete'] + }, + { + demoId: 'guidesteps-large', + name: { + 'zh-CN': 'large属性', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

通过属性large配置标题头字体变大以及修改标志点颜色。

', + 'en-US': '' + } + } + ] +}; diff --git a/src/guidesteps/demo/src/app/guidesteps/webdoc/guidesteps.cn.md b/src/guidesteps/demo/src/app/guidesteps/webdoc/guidesteps.cn.md new file mode 100644 index 0000000..f90e4e9 --- /dev/null +++ b/src/guidesteps/demo/src/app/guidesteps/webdoc/guidesteps.cn.md @@ -0,0 +1,15 @@ +--- +title: Guidesteps 小步骤引导 +--- + +# Guidesteps 小步骤引导 + +
+ +小步骤引导组件 + +```typescript +import { TiGuidestepsModule } from '@opentiny/ng'; +``` + +
diff --git a/src/guidesteps/demo/src/app/guidesteps/webdoc/guidesteps.en.md b/src/guidesteps/demo/src/app/guidesteps/webdoc/guidesteps.en.md new file mode 100644 index 0000000..efc6fda --- /dev/null +++ b/src/guidesteps/demo/src/app/guidesteps/webdoc/guidesteps.en.md @@ -0,0 +1,13 @@ +--- +title: Guidesteps +--- + +# Guidesteps + +
+ +```typescript +import { TiGuidestepsModule } from '@opentiny/ng'; +``` + +
diff --git a/src/guidesteps/demo/src/favicon.ico b/src/guidesteps/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/guidesteps/demo/src/index.html b/src/guidesteps/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/guidesteps/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/guidesteps/demo/src/main.ts b/src/guidesteps/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/guidesteps/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/guidesteps/demo/tsconfig.app.json b/src/guidesteps/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/guidesteps/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/guidesteps/lib/index.ts b/src/guidesteps/lib/index.ts new file mode 100644 index 0000000..8acdf78 --- /dev/null +++ b/src/guidesteps/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiGuidestepsModule'; +export * from './src/TiGuidestepsComponent'; diff --git a/src/guidesteps/lib/ng-package.json b/src/guidesteps/lib/ng-package.json new file mode 100644 index 0000000..4f228ef --- /dev/null +++ b/src/guidesteps/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/guidesteps", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/guidesteps/lib/package.json b/src/guidesteps/lib/package.json new file mode 100644 index 0000000..613df4a --- /dev/null +++ b/src/guidesteps/lib/package.json @@ -0,0 +1,9 @@ +{ + "name": "@opentiny/ng-guidesteps", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/guidesteps/lib/project.json b/src/guidesteps/lib/project.json new file mode 100644 index 0000000..dc53501 --- /dev/null +++ b/src/guidesteps/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/guidesteps/lib", + "sourceRoot": "src/guidesteps/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/guidesteps"], + "options": { + "project": "src/guidesteps/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/guidesteps"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js guidesteps" + }, + { + "command": "ng default-build guidesteps" + }, + { + "command": "node build/clear-default-theme.js guidesteps" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/guidesteps && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build guidesteps && ng pack guidesteps && node build/publish.js guidesteps --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/guidesteps/lib/src/TiGuidestepComponent.ts b/src/guidesteps/lib/src/TiGuidestepComponent.ts new file mode 100644 index 0000000..86628c5 --- /dev/null +++ b/src/guidesteps/lib/src/TiGuidestepComponent.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; + +/** + * TiGuidestepComponent 是单个小步骤模块组件 + */ +@Component({ + selector: 'ti-guidestep', + template: ``, + styleUrls: ['./guidestep.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiGuidestepComponent extends TiBaseComponent { + /** + * 当前步骤是否已经完成 + */ + @Input() isComplete: boolean = false; +} diff --git a/src/guidesteps/lib/src/TiGuidestepContentComponent.ts b/src/guidesteps/lib/src/TiGuidestepContentComponent.ts new file mode 100644 index 0000000..c3a4c95 --- /dev/null +++ b/src/guidesteps/lib/src/TiGuidestepContentComponent.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; + +@Component({ + selector: 'ti-guidestep-content', + template: ``, + styleUrls: ['./guidestep-content.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiGuidestepContentComponent extends TiBaseComponent {} diff --git a/src/guidesteps/lib/src/TiGuidestepHeaderComponent.ts b/src/guidesteps/lib/src/TiGuidestepHeaderComponent.ts new file mode 100644 index 0000000..0f75375 --- /dev/null +++ b/src/guidesteps/lib/src/TiGuidestepHeaderComponent.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { ChangeDetectionStrategy, Component, ElementRef, Renderer2 } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; + +import { TiGuidestepComponent } from './TiGuidestepComponent'; + +@Component({ + selector: 'ti-guidestep-header', + template: ``, + styleUrls: ['./guidestep-header.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti-guidestep-dot-complete]': 'guidestepComp.isComplete' + } +}) +export class TiGuidestepHeaderComponent extends TiBaseComponent { + constructor(private elementRef: ElementRef, private renderer2: Renderer2, public guidestepComp: TiGuidestepComponent) { + super(elementRef, renderer2); + } +} diff --git a/src/guidesteps/lib/src/TiGuidestepsComponent.ts b/src/guidesteps/lib/src/TiGuidestepsComponent.ts new file mode 100644 index 0000000..ab4f1b9 --- /dev/null +++ b/src/guidesteps/lib/src/TiGuidestepsComponent.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +@Component({ + selector: 'ti-guidesteps', + template: ``, + styleUrls: ['./guidesteps.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiGuidestepsComponent extends TiBaseComponent {} diff --git a/src/guidesteps/lib/src/TiGuidestepsModule.ts b/src/guidesteps/lib/src/TiGuidestepsModule.ts new file mode 100644 index 0000000..e8ae4be --- /dev/null +++ b/src/guidesteps/lib/src/TiGuidestepsModule.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TiGuidestepsComponent } from './TiGuidestepsComponent'; +import { TiGuidestepComponent } from './TiGuidestepComponent'; +import { TiGuidestepHeaderComponent } from './TiGuidestepHeaderComponent'; +import { TiGuidestepContentComponent } from './TiGuidestepContentComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [TiGuidestepsComponent, TiGuidestepComponent, TiGuidestepHeaderComponent, TiGuidestepContentComponent], + declarations: [TiGuidestepsComponent, TiGuidestepComponent, TiGuidestepHeaderComponent, TiGuidestepContentComponent] +}) +export class TiGuidestepsModule {} +export { TiGuidestepsComponent } from './TiGuidestepsComponent'; +export { TiGuidestepComponent } from './TiGuidestepComponent'; +export { TiGuidestepHeaderComponent } from './TiGuidestepHeaderComponent'; +export { TiGuidestepContentComponent } from './TiGuidestepContentComponent'; diff --git a/src/guidesteps/lib/src/guidestep-content.less b/src/guidesteps/lib/src/guidestep-content.less new file mode 100644 index 0000000..603dd9d --- /dev/null +++ b/src/guidesteps/lib/src/guidestep-content.less @@ -0,0 +1,7 @@ +// 步骤内容区域样式 +:host { + display: block; + background: var(--ti-common-color-bg-white-normal, #ffffff); + padding-left: calc(var(--guidestep-dot-size) + var(--guidestep-icon-space)); + padding-bottom: var(--ti-common-space-6x, 24px); +} diff --git a/src/guidesteps/lib/src/guidestep-header.less b/src/guidesteps/lib/src/guidestep-header.less new file mode 100644 index 0000000..2977583 --- /dev/null +++ b/src/guidesteps/lib/src/guidestep-header.less @@ -0,0 +1,33 @@ +// 步骤头部标题样式 +:host { + position: relative; + display: inline-block; + padding-left: calc(var(--guidestep-dot-size) + var(--guidestep-icon-space)); + padding-bottom: 8px; + font-size: var(--ti-common-font-size-base, 12px); + line-height: var(--ti-common-line-height-number, 1.5); + &:before { + content: ''; + position: absolute; + width: var(--guidestep-dot-size); + height: var(--guidestep-dot-size); + background-color: #8a8e99; // 无对应场景变量 + border: var(--guidestep-dot-border) solid #eef0f5; // 无对应场景变量 + border-radius: 50%; + left: 0; + top: calc((var(--guidestep-header-line-height) - (var(--guidestep-dot-outer-size))) / 2); + } + + // 用户设置large属性时样式 + [large] & { + font-size: var(--ti-common-font-size-2, 16px); + &:before { + background: var(--ti-common-color-icon-normal, #575d6c); + top: calc((var(--guidestep-header-large-line-height) - (var(--guidestep-dot-outer-size))) / 2); + } + } +} +// 步骤完成后,标志点为成功状态颜色 +:host.ti-guidestep-dot-complete:before { + background: var(--ti-common-color-success, #50d4ab); +} diff --git a/src/guidesteps/lib/src/guidestep.less b/src/guidesteps/lib/src/guidestep.less new file mode 100644 index 0000000..53b5026 --- /dev/null +++ b/src/guidesteps/lib/src/guidestep.less @@ -0,0 +1,39 @@ +// 纵向步骤间分割线样式 +:host { + display: block; + position: relative; + background: var(--ti-common-color-bg-white-normal, #ffffff); + &:before { + content: ''; + position: absolute; + width: var(--guidestep-line-width); + height: calc( + 100% - (var(--guidestep-dot-outer-size)) - var(--guidestep-line-dot-space) - + (var(--guidestep-header-line-height) - (var(--guidestep-dot-outer-size))) / 2 + ); + top: calc( + var(--guidestep-dot-outer-size) + var(--guidestep-line-dot-space) + + (var(--guidestep-header-line-height) - (var(--guidestep-dot-outer-size))) / 2 + ); + left: calc((var(--guidestep-dot-outer-size) - var(--guidestep-line-width)) / 2); + background: var(--ti-common-color-line-dividing, #dfe1e6); + } + &:last-child { + &:before { + background: transparent; + } + } + // 用户设置large属性时样式 + [large] & { + &:before { + top: calc( + var(--guidestep-dot-outer-size) + var(--guidestep-line-dot-space) + + (var(--guidestep-header-large-line-height) - (var(--guidestep-dot-outer-size))) / 2 + ); + height: calc( + 100% - (var(--guidestep-dot-outer-size)) - var(--guidestep-line-dot-space) - + (var(--guidestep-header-large-line-height) - (var(--guidestep-dot-outer-size))) / 2 + ); + } + } +} diff --git a/src/guidesteps/lib/src/guidesteps.less b/src/guidesteps/lib/src/guidesteps.less new file mode 100644 index 0000000..791520b --- /dev/null +++ b/src/guidesteps/lib/src/guidesteps.less @@ -0,0 +1,14 @@ +::ng-deep :root { + --guidestep-header-line-height: var(--ti-common-font-size-base, 12px) * var(--ti-common-line-height-number, 1.5); + --guidestep-header-large-line-height: var(--ti-common-font-size-2, 16px) * var(--ti-common-line-height-number, 1.5); + --guidestep-dot-size: 8px; + --guidestep-icon-space: var(--ti-common-space-2x, 8px); // 图标和标题文字之间的距离 + --guidestep-line-dot-space: var(--ti-common-space-base, 4px); + --guidestep-dot-border: 2px; + --guidestep-line-width: 1px; + --guidestep-dot-outer-size: var(--guidestep-dot-size) + 2 * var(--guidestep-dot-border); // 使用calc计算,IE下对应分割线无法显示 +} + +:host { + display: block; +} diff --git a/src/halfmodal/demo/karma.conf.js b/src/halfmodal/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/halfmodal/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/halfmodal/demo/project.json b/src/halfmodal/demo/project.json new file mode 100644 index 0000000..13991a8 --- /dev/null +++ b/src/halfmodal/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/halfmodal/demo", + "sourceRoot": "src/halfmodal/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/halfmodal", + "index": "src/halfmodal/demo/src/index.html", + "main": "src/halfmodal/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/halfmodal/demo/tsconfig.app.json", + "assets": ["src/halfmodal/demo/src/favicon.ico", "src/halfmodal/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "halfmodal-demo:build:production" + }, + "development": { + "browserTarget": "halfmodal-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js halfmodal" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/halfmodal/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/halfmodal/demo/tsconfig.spec.json", + "karmaConfig": "src/halfmodal/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/halfmodal/demo/src/app/AppComponent.ts b/src/halfmodal/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/halfmodal/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/halfmodal/demo/src/app/AppModule.ts b/src/halfmodal/demo/src/app/AppModule.ts new file mode 100644 index 0000000..c9d2410 --- /dev/null +++ b/src/halfmodal/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { HalfmodalTestModule } from './halfmodal/HalfmodalTestModule'; + +@NgModule({ + imports: [ + HalfmodalTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/halfmodal/demo/src/app/IndexComponent.ts b/src/halfmodal/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..91335bd --- /dev/null +++ b/src/halfmodal/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { HalfmodalTestModule } from './halfmodal/HalfmodalTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = HalfmodalTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/halfmodal/demo/src/app/app.html b/src/halfmodal/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/halfmodal/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/halfmodal/demo/src/app/halfmodal/HalfmodalAsyncComponent.ts b/src/halfmodal/demo/src/app/halfmodal/HalfmodalAsyncComponent.ts new file mode 100644 index 0000000..261f9b2 --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/HalfmodalAsyncComponent.ts @@ -0,0 +1,28 @@ +import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { TiHalfmodalComponent, TiHalfmodalService } from '@opentiny/ng'; +@Component({ + templateUrl: './halfmodal-async.html', + styleUrls: ['./halfmodalTest.less'] +}) +export class HalfmodalAsyncComponent { + constructor(private tiHalfmodalservice: TiHalfmodalService) {} + @ViewChild('service1', { static: false }) tempRef: TemplateRef; + click(halfmodal: TiHalfmodalComponent): void { + setTimeout((): void => { + halfmodal.show(); + }, 1000); + } + + click1(): void { + setTimeout((): void => { + this.tiHalfmodalservice.open(this.tempRef, { + backdrop: false, + width: '450px', + closeIcon: true, + dismiss: (): void => { + console.log('dismiss'); + } + }); + }, 1000); + } +} diff --git a/src/halfmodal/demo/src/app/halfmodal/HalfmodalBackdropComponent.ts b/src/halfmodal/demo/src/app/halfmodal/HalfmodalBackdropComponent.ts new file mode 100644 index 0000000..f63e387 --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/HalfmodalBackdropComponent.ts @@ -0,0 +1,12 @@ +import { Component, ViewChild } from '@angular/core'; +import { TiHalfmodalComponent } from '@opentiny/ng'; +@Component({ + templateUrl: './halfmodal-backdrop.html' +}) +export class HalfmodalBackdropComponent { + @ViewChild('modal', { static: true }) halfmodal: TiHalfmodalComponent; + + show(): void { + this.halfmodal.show(); + } +} diff --git a/src/halfmodal/demo/src/app/halfmodal/HalfmodalBasicComponent.ts b/src/halfmodal/demo/src/app/halfmodal/HalfmodalBasicComponent.ts new file mode 100644 index 0000000..571f84b --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/HalfmodalBasicComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiHalfmodalComponent } from '@opentiny/ng'; +@Component({ + templateUrl: './halfmodal-basic.html' +}) +export class HalfmodalBasicComponent { + steps: Array = [ + { + label: 'General' + }, + { + label: 'Host Group Family' + }, + { + label: 'Currency Policy' + } + ]; + activeStep: any = this.steps[2]; + + click(halfmodal: TiHalfmodalComponent): void { + halfmodal.show(); + } +} diff --git a/src/halfmodal/demo/src/app/halfmodal/HalfmodalBeforehideComponent.ts b/src/halfmodal/demo/src/app/halfmodal/HalfmodalBeforehideComponent.ts new file mode 100644 index 0000000..cc310c2 --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/HalfmodalBeforehideComponent.ts @@ -0,0 +1,22 @@ +import { Component, ViewChild } from '@angular/core'; +import { TiHalfmodalComponent } from '@opentiny/ng'; +@Component({ + templateUrl: './halfmodal-beforehide.html' +}) +export class HalfmodalBeforehideComponent { + @ViewChild('modal', { static: true }) halfmodal: TiHalfmodalComponent; + + show(): void { + this.halfmodal.show(); + } + + hide(): void { + this.halfmodal.hide(); + } + + beforeHide(): void { + if (window.confirm('您确定关闭弹窗吗?')) { + this.halfmodal.hide(); + } + } +} diff --git a/src/halfmodal/demo/src/app/halfmodal/HalfmodalContentComponent.ts b/src/halfmodal/demo/src/app/halfmodal/HalfmodalContentComponent.ts new file mode 100644 index 0000000..1f1fe99 --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/HalfmodalContentComponent.ts @@ -0,0 +1,128 @@ +import { Component, ViewChild } from '@angular/core'; +import { TiHalfmodalComponent } from '@opentiny/ng'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; +@Component({ + templateUrl: './halfmodal-content.html' +}) +export class HalfmodalContentComponent { + @ViewChild('modal', { static: true }) halfmodal: TiHalfmodalComponent; + myLogs: Array = []; + item1: any = { + show: true, + label: '姓名:', + required: true, + value: '' + }; + item2: any = { + label: '输入框:', + required: true, + value: '' + }; + item3: any = { + show: true, + label: 'span标签:', + required: true, + verticalAlign: 'middle' + }; + item4: any = { + label: '', + required: true, + labelShow: true, + verticalAlign: 'middle' + }; + item5: any = { + show: true, + required: true, + verticalAlign: 'middle' + }; + item6: any = { + show: true, + required: true, + verticalAlign: 'middle', + radioList: [ + { + key: '男人', + id: 'man' + }, + { + key: '女人', + id: 'woman' + } + ] + }; + + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + show(): void { + this.halfmodal.show(); + } + + close(): void { + this.halfmodal.hide(); + } + + dismiss(): void { + this.halfmodal.hide(); + } +} diff --git a/src/halfmodal/demo/src/app/halfmodal/HalfmodalModalComponent.ts b/src/halfmodal/demo/src/app/halfmodal/HalfmodalModalComponent.ts new file mode 100644 index 0000000..7ece14f --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/HalfmodalModalComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { TiHalfmodalComponent } from '@opentiny/ng'; +import { TiMessageService } from '@opentiny/ng'; +@Component({ + templateUrl: './halfmodal-modal.html', + styleUrls: ['./halfmodalTest.less'] +}) +export class HalfmodalModalComponent { + constructor(private message: TiMessageService) {} + click(halfmodal: TiHalfmodalComponent): void { + halfmodal.show(); + } + + openMessage(): void { + this.message.open({ + content: '操作弹窗不会关闭半屏弹窗' + }); + } +} diff --git a/src/halfmodal/demo/src/app/halfmodal/HalfmodalModalselectComponent.ts b/src/halfmodal/demo/src/app/halfmodal/HalfmodalModalselectComponent.ts new file mode 100644 index 0000000..acdb314 --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/HalfmodalModalselectComponent.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; +import { TiHalfmodalComponent } from '@opentiny/ng'; +import { TiModalService } from '@opentiny/ng'; +@Component({ + templateUrl: './halfmodal-modalselect.html', + styleUrls: ['./halfmodalTest.less'] +}) +export class HalfmodalModalselectComponent { + constructor(private tiModal: TiModalService) {} + + myOptions: Array = [ + { label: '美国', englishname: 'America' }, + { label: '巴西', englishname: 'Brazil' }, + { label: '加拿大', englishname: 'Canada' }, + { label: '中国', englishname: 'China' }, + { label: '法国', englishname: 'France' }, + { label: '德国', englishname: 'Germany' }, + { label: '日本', englishname: 'Japan' }, + { label: '韩国', englishname: 'South Korea' }, + { label: '土耳其', englishname: 'Turkey' }, + { label: '大不列颠和北爱兰联合王国', englishname: 'United Kingdom' } + ]; + mySelected1 = undefined; + click(halfmodal: TiHalfmodalComponent): void { + halfmodal.show(); + } + + show(content: any): void { + this.tiModal.open(content, { + id: 'myModal', // 定义id防止同一页面出现多个相同弹框 + // 模板上下文:一般通过context定义的是与弹出动作相关的数据,大部分数据还是建议在外部定义 + // 双向绑定的值,建议放在context对象中,每次打开弹窗都重新就行赋值。 + context: { + myOptions: this.myOptions, + mySelected1: this.myOptions[0] + } + }); + } +} diff --git a/src/halfmodal/demo/src/app/halfmodal/HalfmodalMultiComponent.ts b/src/halfmodal/demo/src/app/halfmodal/HalfmodalMultiComponent.ts new file mode 100644 index 0000000..70487ee --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/HalfmodalMultiComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './halfmodal-multi.html', + styleUrls: ['./halfmodalTest.less'] +}) +export class HalfmodalMultiComponent { + data: any = { + actionGuide: 'document', + mySelected1: {}, + myOptions: [{ label: '中国' }, { label: '美国' }, { label: '巴西' }, { label: '加拿大' }], + answer: (key: number) => { + this.answer = this.answerArray[key - 1]; + } + }; + + answerArray: Array = [ + { + q: '5乘以3等于几?', + a: '等于15' + }, + { + q: '1加1等于几?', + a: '等于2' + }, + { + q: '做人呐,最重要的是什么', + a: '是开心' + } + ]; + answer: any = {}; +} diff --git a/src/halfmodal/demo/src/app/halfmodal/HalfmodalServiceComponent.ts b/src/halfmodal/demo/src/app/halfmodal/HalfmodalServiceComponent.ts new file mode 100644 index 0000000..33d6fd5 --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/HalfmodalServiceComponent.ts @@ -0,0 +1,103 @@ +import { Component, ElementRef, TemplateRef, ViewChild } from '@angular/core'; +import { TiButtonItem } from '@opentiny/ng'; +import { TiHalfmodalService } from '@opentiny/ng'; +@Component({ + templateUrl: './halfmodal-service.html' +}) +export class HalfmodalServiceComponent { + constructor(private tiHalfmodalservice: TiHalfmodalService) {} + @ViewChild('service1', { static: false }) tempRef: TemplateRef; + @ViewChild('service2', { static: false }) templateRef: TemplateRef; + + myOptions: Array = selectOptions; + items: Array = buttongroupItems; + myLogs: Array = []; + + mySelected1: any = this.myOptions[1]; + click1(): void { + this.tiHalfmodalservice.open(this.tempRef, { + nomaskCloseable: false + }); + } + click2(): void { + this.tiHalfmodalservice.open(this.templateRef, { + width: '500px', + closeIcon: false, + clientRectTop: '100px' + }); + } + click3(): void { + this.tiHalfmodalservice.open(ContentComponent, { + backdrop: true, + context: { + text: 'Tiny', + data: { + s: this.mySelected1, + t: 'tiny', + b: this.items[0] + } + }, + close: (context: any): void => { + this.myLogs = [...this.myLogs, `on close = ${JSON.stringify(context)}`]; + }, + dismiss: (context: any): void => { + this.myLogs = [...this.myLogs, `on dismiss = ${JSON.stringify(context)}`]; + } + }); + } +} + +@Component({ + template: ` + Component + + This is a demo from {{ text }}! + + + + + + + + + + + + + + + + + + + + ` +}) +export class ContentComponent { + text: string; + options: Array = selectOptions; + items: Array = buttongroupItems; + data: any = { + myValue: '', + t: '', + s: undefined, + b: undefined + }; + dismiss(): void {} // 类似TiModalService,此处的dismiss和close只是为了编译,实际调用的是open()的参数的dismiss和close + close(): void {} +} + +export let selectOptions: Array = [ + { label: '美国', englishname: 'America' }, + { label: '巴西', englishname: 'Brazil' }, + { label: '加拿大', englishname: 'Canada' }, + { label: '中国', englishname: 'China' }, + { label: '法国', englishname: 'France' }, + { label: '德国', englishname: 'Germany' }, + { label: '日本', englishname: 'Japan' }, + { label: '韩国', englishname: 'South Korea' }, + { label: '土耳其', englishname: 'Turkey' }, + { label: '大不列颠和北爱兰联合王国', englishname: 'United Kingdom' } +]; + +export let buttongroupItems: Array = [{ text: '云服务器' }, { text: '裸金属服务器' }, { text: '虚拟IP地址' }]; diff --git a/src/halfmodal/demo/src/app/halfmodal/HalfmodalServiceTestComponent.ts b/src/halfmodal/demo/src/app/halfmodal/HalfmodalServiceTestComponent.ts new file mode 100644 index 0000000..0d7f61d --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/HalfmodalServiceTestComponent.ts @@ -0,0 +1,34 @@ +import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { TiHalfmodalService } from '@opentiny/ng'; +@Component({ + templateUrl: './halfmodal-service-test.html' +}) +export class HalfmodalServiceTestComponent { + constructor(private tiHalfmodalservice: TiHalfmodalService) {} + @ViewChild('service1', { static: false }) tempRef: TemplateRef; + @ViewChild('service2', { static: false }) templateRef: TemplateRef; + + public openModal(): void { + this.tiHalfmodalservice.open(this.tempRef, { + backdrop: true, + dismiss: (): void => { + console.log('dismiss'); + }, + close: (context: any): void => { + console.log('close'); + } + }); + } + public openModal1(): void { + this.tiHalfmodalservice.open(this.templateRef, { + width: '450px', + backdrop: true, + dismiss: (): void => { + console.log('dismiss'); + }, + close: (context: any): void => { + console.log('close'); + } + }); + } +} diff --git a/src/halfmodal/demo/src/app/halfmodal/HalfmodalTestModule.ts b/src/halfmodal/demo/src/app/halfmodal/HalfmodalTestModule.ts new file mode 100644 index 0000000..d8aa16e --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/HalfmodalTestModule.ts @@ -0,0 +1,110 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { + TiAccordionModule, + TiButtongroupModule, + TiButtonModule, + TiFormfieldModule, + TiHalfmodalModule, + TiIconModule, + TiMessageModule, + TiModalModule, + TiOverflowModule, + TiRadioModule, + TiSearchboxModule, + TiSelectModule, + TiStepsModule, + TiTableModule, + TiTabModule, + TiTextareaModule, + TiTextModule, + TiTipModule +} from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { HalfmodalBasicComponent } from './HalfmodalBasicComponent'; +import { HalfmodalMultiComponent } from './HalfmodalMultiComponent'; +import { HalfmodalBackdropComponent } from './HalfmodalBackdropComponent'; +import { HalfmodalBeforehideComponent } from './HalfmodalBeforehideComponent'; +import { HalfmodalModalselectComponent } from './HalfmodalModalselectComponent'; +import { HalfmodalContentComponent } from './HalfmodalContentComponent'; +import { HalfmodalModalComponent } from './HalfmodalModalComponent'; +import { HalfmodalAsyncComponent } from './HalfmodalAsyncComponent'; +import { ContentComponent, HalfmodalServiceComponent } from './HalfmodalServiceComponent'; +import { HalfmodalServiceTestComponent } from './HalfmodalServiceTestComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiAccordionModule, + TiButtonModule, + TiButtongroupModule, + TiMessageModule, + TiSearchboxModule, + TiSelectModule, + TiTabModule, + TiTipModule, + TiFormfieldModule, + TiModalModule, + TiRadioModule, + TiTableModule, + TiTextModule, + TiOverflowModule, + TiTextareaModule, + TiStepsModule, + TiIconModule, + TiHalfmodalModule, + DemoLogModule, + RouterModule.forChild(HalfmodalTestModule.ROUTES) + ], + declarations: [ + HalfmodalBasicComponent, + HalfmodalMultiComponent, + HalfmodalBackdropComponent, + HalfmodalServiceComponent, + HalfmodalModalComponent, + HalfmodalBeforehideComponent, + HalfmodalContentComponent, + HalfmodalModalselectComponent, + ContentComponent, + HalfmodalAsyncComponent, + HalfmodalServiceTestComponent + ], + entryComponents: [ContentComponent] +}) +export class HalfmodalTestModule { + public static readonly ROUTES: Routes = [ + { path: 'halfmodal/halfmodal-basic', component: HalfmodalBasicComponent }, + { + path: 'halfmodal/halfmodal-backdrop', + component: HalfmodalBackdropComponent + }, + { + path: 'halfmodal/halfmodal-beforehide', + component: HalfmodalBeforehideComponent + }, + { + path: 'halfmodal/halfmodal-content', + component: HalfmodalContentComponent + }, + { + path: 'halfmodal/halfmodal-service', + component: HalfmodalServiceComponent + }, + { + path: 'halfmodal/halfmodal-service-test', + component: HalfmodalServiceTestComponent + }, + { path: 'halfmodal/halfmodal-modal', component: HalfmodalModalComponent }, + { path: 'halfmodal/halfmodal-multi', component: HalfmodalMultiComponent }, + { + path: 'halfmodal/halfmodal-modalselect', + component: HalfmodalModalselectComponent + }, + { path: 'halfmodal/halfmodal-async', component: HalfmodalAsyncComponent } + ]; +} diff --git a/src/halfmodal/demo/src/app/halfmodal/halfmodal-async.html b/src/halfmodal/demo/src/app/halfmodal/halfmodal-async.html new file mode 100644 index 0000000..e7d5484 --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/halfmodal-async.html @@ -0,0 +1,10 @@ + +
这是第{{key}}条内容
+
+ +


+ + 头部区域 + 半屏弹框内容区域 + +
diff --git a/src/halfmodal/demo/src/app/halfmodal/halfmodal-backdrop.html b/src/halfmodal/demo/src/app/halfmodal/halfmodal-backdrop.html new file mode 100644 index 0000000..ed0ec14 --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/halfmodal-backdrop.html @@ -0,0 +1,5 @@ + +

这是一个带遮罩的弹窗

+
+ + diff --git a/src/halfmodal/demo/src/app/halfmodal/halfmodal-basic.html b/src/halfmodal/demo/src/app/halfmodal/halfmodal-basic.html new file mode 100644 index 0000000..2782f9b --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/halfmodal-basic.html @@ -0,0 +1,20 @@ + +

这是一个不带遮罩的弹窗

+
+ + +

这是一个不带遮罩的弹窗,点击页面其他区域不会关闭弹窗

+
+ + +

这是一个定制化的弹窗,宽度:500px 、距浏览器上边距:100px 、关闭图标:无

+
+ +

1.打开一个不带遮罩的弹窗,点击页面其他区域会关闭弹窗

+ + +

2.打开一个不带遮罩的弹窗,配置 nomaskCloseable 为 false ,点击页面其他区域不会关闭弹窗

+ + +

3.打开一个定制化的弹窗,宽度:500px 、距浏览器上边距:100px 、关闭图标:无

+ diff --git a/src/halfmodal/demo/src/app/halfmodal/halfmodal-beforehide.html b/src/halfmodal/demo/src/app/halfmodal/halfmodal-beforehide.html new file mode 100644 index 0000000..09485cd --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/halfmodal-beforehide.html @@ -0,0 +1,7 @@ + +

这个弹窗会朝外通知 beforeHide 事件

+
+ +
+ + diff --git a/src/halfmodal/demo/src/app/halfmodal/halfmodal-content.html b/src/halfmodal/demo/src/app/halfmodal/halfmodal-content.html new file mode 100644 index 0000000..1f5a744 --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/halfmodal-content.html @@ -0,0 +1,61 @@ + + 配置 + + + + + + + + + + item3 + + + + 测试标签dom + + {{item}} + + + + 存储地址: + + + + + + 性别: + +
+ +
+
+ + + + + + + + + + + + + + + + + +
{{column.title}}
{{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
+
+
+
+
+ + + + +
+ diff --git a/src/halfmodal/demo/src/app/halfmodal/halfmodal-modal.html b/src/halfmodal/demo/src/app/halfmodal/halfmodal-modal.html new file mode 100644 index 0000000..4a9112b --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/halfmodal-modal.html @@ -0,0 +1,7 @@ + + + +
这是第{{key}}条内容
+
+ +
diff --git a/src/halfmodal/demo/src/app/halfmodal/halfmodal-modalselect.html b/src/halfmodal/demo/src/app/halfmodal/halfmodal-modalselect.html new file mode 100644 index 0000000..7ee8dba --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/halfmodal-modalselect.html @@ -0,0 +1,16 @@ + + + +
这是第{{key}}条内容
+
+ + +
+ + + I'm a modal! + + + + + diff --git a/src/halfmodal/demo/src/app/halfmodal/halfmodal-multi.html b/src/halfmodal/demo/src/app/halfmodal/halfmodal-multi.html new file mode 100644 index 0000000..b10bfb7 --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/halfmodal-multi.html @@ -0,0 +1,58 @@ + +

1.描述

+

多层弹窗示例

+

PS: 多层弹窗的场景由帮助面板组件代替,该示例不对外呈现

+

2.示例

+ + + + + + + + +

1.打开弹窗(默认width:600px)

+ +
+ + +
+
使用指南
+ + + + 首次使用 +
+ 这是第{{key}}个问题 + 查看答案 +
+
+ + Static Header, initially expanded +

This content is straight in the template.

+
+
+ +
+
+ + + {{context.q}}
+ {{context.a}} +
diff --git a/src/halfmodal/demo/src/app/halfmodal/halfmodal-service-test.html b/src/halfmodal/demo/src/app/halfmodal/halfmodal-service-test.html new file mode 100644 index 0000000..ef026c1 --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/halfmodal-service-test.html @@ -0,0 +1,24 @@ +

1.描述

+

服务类型多层弹窗示例

+ + 第一层弹窗 + + + + + + + + + + + 第二层弹窗 + 文本 + + + + + + + +
diff --git a/src/halfmodal/demo/src/app/halfmodal/halfmodal-service.html b/src/halfmodal/demo/src/app/halfmodal/halfmodal-service.html new file mode 100644 index 0000000..2fa5389 --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/halfmodal-service.html @@ -0,0 +1,26 @@ + +

nomaskCloseable: false

+
+ + + Template + +

width: '500px'

+

closeIcon: false

+

clientRectTop: '100px'

+
+ +

这是半屏弹窗底部

+
+
+ +

1.打开一个不带遮罩的弹窗,配置 nomaskCloseable 为 false ,点击页面其他区域不会关闭弹窗

+ + +

2.打开一个定制化的弹窗,宽度:500px 、距浏览器上边距:100px 、关闭图标:无

+ + +

3.打开一个带遮罩的弹窗

+ + + diff --git a/src/halfmodal/demo/src/app/halfmodal/halfmodalTest.less b/src/halfmodal/demo/src/app/halfmodal/halfmodalTest.less new file mode 100644 index 0000000..3cbb39a --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/halfmodalTest.less @@ -0,0 +1,12 @@ +.dot { + display: inline-block; + margin: 0 4px 0 0; + width: 8px; + height: 8px; + border-radius: 50%; + background: #5e7ce0; +} +.list { + padding: 20px; + border-bottom: 1px solid #333; +} diff --git a/src/halfmodal/demo/src/app/halfmodal/webdoc/formfield.en.md b/src/halfmodal/demo/src/app/halfmodal/webdoc/formfield.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/webdoc/formfield.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/halfmodal/demo/src/app/halfmodal/webdoc/halfmodal-demos.js b/src/halfmodal/demo/src/app/halfmodal/webdoc/halfmodal-demos.js new file mode 100644 index 0000000..ba50915 --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/webdoc/halfmodal-demos.js @@ -0,0 +1,75 @@ +export default { + column: '2', + demos: [ + { + demoId: 'halfmodal-basic', + name: { + 'zh-CN': '基础', + 'en-US': 'halfmodal basic' + }, + desc: { + 'zh-CN': '

通过属性width配置半屏弹窗宽度;通过属性clientRectTop配置半屏弹窗顶部距窗口顶部的距离;通过属性closeIcon配置是否显示右上角的关闭按钮;通过属性nomaskCloseable配置不带遮罩层的场景下,点击页面是否允许关闭。

', + 'en-US': 'halfmodal basic' + }, + apis: [ + 'TiHalfmodalComponent.properties.width', + 'TiHalfmodalComponent.properties.clientRectTop', + 'TiHalfmodalComponent.properties.closeIcon', + 'TiHalfmodalComponent.properties.nomaskCloseable' + ] + }, + { + demoId: 'halfmodal-backdrop', + name: { + 'zh-CN': '遮罩', + 'en-US': 'halfmodal backdrop' + }, + desc: { + 'zh-CN': '

通过属性backdrop配置是否带有遮罩层。

', + 'en-US': 'halfmodal backdrop' + }, + apis: [ 'TiHalfmodalComponent.properties.backdrop' ] + }, + { + demoId: 'halfmodal-beforehide', + name: { + 'zh-CN': 'beforeHide事件', + 'en-US': 'halfmodal beforehide' + }, + desc: { + 'zh-CN': '

当半屏弹窗将要关闭的时候触发beforeHide事件,如果增加该事件需要业务手动关闭半屏弹窗。

', + 'en-US': 'halfmodal beforehide' + }, + apis: [ 'TiHalfmodalComponent.events.beforeHide' ] + }, + { + demoId: 'halfmodal-content', + name: { 'zh-CN': '使用头部、内容和底部标签', 'en-US': 'halfmodal content' }, + desc: { + 'zh-CN': '

可以结合ti-halfmodal-headerti-halfmodal-bodyti-halfmodal-footer标签使用。

', + 'en-US': 'halfmodal content' + }, + apis: [ + 'TiHalfmodalBodyComponent', + 'TiHalfmodalFooterComponent', + 'TiHalfmodalHeaderComponent' + ] + }, + { + demoId: 'halfmodal-service', + name: { + 'zh-CN': '服务方式生成半屏弹窗', + 'en-US': 'halfmodal service' + }, + desc: { + 'zh-CN': '

通过服务方式生成半屏弹窗。

', + 'en-US': 'halfmodal service' + }, + apis: [ + 'TiHalfmodalService', + 'TiHalfmodalRef', + 'TiHalfmodalConfig' + ] + } + ] +} \ No newline at end of file diff --git a/src/halfmodal/demo/src/app/halfmodal/webdoc/halfmodal.cn.md b/src/halfmodal/demo/src/app/halfmodal/webdoc/halfmodal.cn.md new file mode 100644 index 0000000..dbd9ac5 --- /dev/null +++ b/src/halfmodal/demo/src/app/halfmodal/webdoc/halfmodal.cn.md @@ -0,0 +1,27 @@ +--- +title: Halfmodal 半屏弹窗 +--- +# Halfmodal 半屏弹窗 + +
+ +Halfmodal 是从屏幕边缘滑出的浮层面板组件。   + ++ 半屏弹窗分为两种:1.不带遮罩层,默认点击弹窗外会关闭弹窗;2.带遮罩层,点击弹窗外不能关闭弹窗。 + +```typescript +import { TiHalfmodalModule } from '@opentiny/ng'; +``` + +
+ +
+ +Formfield 是对多个表单元素布局的组件。   + ++ 支持单列、多列布局。 + +```typescript +import { TiFormfieldModule } from '@opentiny/ng'; +``` +
diff --git a/src/halfmodal/demo/src/favicon.ico b/src/halfmodal/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/halfmodal/demo/src/index.html b/src/halfmodal/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/halfmodal/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/halfmodal/demo/src/main.ts b/src/halfmodal/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/halfmodal/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/halfmodal/demo/test.ts b/src/halfmodal/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/halfmodal/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/halfmodal/demo/tsconfig.app.json b/src/halfmodal/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/halfmodal/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/halfmodal/demo/tsconfig.spec.json b/src/halfmodal/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/halfmodal/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/halfmodal/lib/index.ts b/src/halfmodal/lib/index.ts new file mode 100644 index 0000000..3cb104d --- /dev/null +++ b/src/halfmodal/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiHalfmodalModule'; +export * from './src/TiHalfmodalService'; diff --git a/src/halfmodal/lib/ng-package.json b/src/halfmodal/lib/ng-package.json new file mode 100644 index 0000000..6e46960 --- /dev/null +++ b/src/halfmodal/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/halfmodal", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/halfmodal/lib/package.json b/src/halfmodal/lib/package.json new file mode 100644 index 0000000..b4ae957 --- /dev/null +++ b/src/halfmodal/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "@opentiny/ng-halfmodal", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/animations": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@opentiny/ng-popup": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/halfmodal/lib/project.json b/src/halfmodal/lib/project.json new file mode 100644 index 0000000..06be0f7 --- /dev/null +++ b/src/halfmodal/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/halfmodal/lib", + "sourceRoot": "src/halfmodal/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/halfmodal"], + "options": { + "project": "src/halfmodal/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/halfmodal"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js halfmodal" + }, + { + "command": "ng default-build halfmodal" + }, + { + "command": "node build/clear-default-theme.js halfmodal" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/halfmodal && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build halfmodal && ng pack halfmodal && node build/publish.js halfmodal --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/halfmodal/lib/src/TiHalfmodalBodyComponent.ts b/src/halfmodal/lib/src/TiHalfmodalBodyComponent.ts new file mode 100644 index 0000000..10ef8fe --- /dev/null +++ b/src/halfmodal/lib/src/TiHalfmodalBodyComponent.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +/** + * 右侧半屏弹窗内容区域 + * + */ +@Component({ + selector: 'ti-halfmodal-body', + template: '', + styleUrls: ['./halfmodal-body.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiHalfmodalBodyComponent extends TiBaseComponent { + protected versionInfo: string = super.getVersion(packageInfo); +} diff --git a/src/halfmodal/lib/src/TiHalfmodalComponent.ts b/src/halfmodal/lib/src/TiHalfmodalComponent.ts new file mode 100644 index 0000000..b3db3de --- /dev/null +++ b/src/halfmodal/lib/src/TiHalfmodalComponent.ts @@ -0,0 +1,300 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + Input, + NgZone, + Output, + Renderer2 +} from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiHalfmodalHeaderComponent } from './TiHalfmodalHeaderComponent'; +import packageInfo from '../package.json'; + +/** + * 半屏弹窗组件 + * + * 半屏弹窗分为两种:1.不带遮罩层,默认点击弹窗外会关闭弹窗;2.带遮罩层,点击弹窗外不能关闭弹窗。 + * + * 两种弹窗都可以通过点击右上角的关闭按钮或者hide()方法关闭弹窗 + * + * 注:第一类弹窗,点击弹窗外关闭的原理是在document上注册点击事件触发`TiHalfmodalComponent.hide()`,如果不想关闭弹窗可以阻止事件冒泡。 + * + */ +@Component({ + selector: 'ti-halfmodal', + templateUrl: './halfmodal.html', + styleUrls: ['./halfmodal.less'], + host: { + '[style.width]': 'width', + '[style.top]': 'clientRectTop', + '[style.right]': 'width ? "-" + width : "-600px"', + '[style.z-index]': 'backdrop? "1300":"900"', + '[hidden]': '!isShow' + }, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiHalfmodalComponent extends TiBaseComponent { + constructor( + protected elementRef: ElementRef, + protected renderer2: Renderer2, + protected zone: NgZone, + protected changeDetector: ChangeDetectorRef + ) { + super(elementRef, renderer2); + } + /** + * 弹窗宽度 + */ + @Input() width: string = '600px'; + /** + * 是否展示遮罩 + */ + @Input() backdrop: boolean = false; + /** + * 弹窗顶部距窗口顶部的距离 + */ + @Input() clientRectTop: string = '50px'; + /** + * 是否显示右上角的关闭按钮 + */ + @Input() closeIcon: boolean = true; + /** + * 不带遮罩层的场景下,点击页面是否允许关闭 + */ + @Input() nomaskCloseable: boolean = true; + /** + * 弹窗将要关闭的时候触发的回调 + */ + @Output() readonly beforeHide: EventEmitter = new EventEmitter(); + /** + * @ignore + * 头部区域 + */ + @ContentChild(TiHalfmodalHeaderComponent) + headElement: TiHalfmodalHeaderComponent; + /** + * @ignore + */ + public isShow: boolean = false; + /** + * @ignore + */ + public isWantShow: boolean = false; + /** + * @ignore + */ + public unlistenClick: () => void; + /** + * @ignore + */ + public unlistenMousedown: () => void; + protected versionInfo: string = super.getVersion(packageInfo); + /** + * mousedown的时候,鼠标对应的X坐标 + */ + private mousedownX: number; + /** + * mousedown的时候,鼠标对应的Y坐标 + */ + private mousedownY: number; + // 遮罩层元素 + private backdropEle: Element; + // 可聚焦元素 + private focusableElementsString: string = `a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), + button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), + textarea:not([disabled]):not([tabindex=\'-1\']), + iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]`; + private unListenDocumentKeydown: () => void; + private onHeadChange: (e: CustomEvent) => void; + private consoleDataService: any; + /** + * @description 向上查找最近的指定祖先元素 + * @param ele 子元素 + * @param parentNode 祖先元素标签名 + * @return 判断祖先元素或元素自身是否与 parentNodeName 相同,否则 undefined + */ + private static findClosest(ele: any, parentNodeName: string): Element | undefined { + if (Util.isNull(ele) || ele.nodeName === 'HTML') { + return undefined; + } else if (ele.nodeName === parentNodeName) { + return ele; + } else { + return this.findClosest(ele.parentNode, parentNodeName); + } + } + + ngOnInit(): void { + super.ngOnInit(); + const consoleContext: any = (window as any).getConsoleContext && (window as any).getConsoleContext(); + this.consoleDataService = consoleContext?.get && consoleContext.get({ name: 'safearea' }); + this.onHeadChange = (e: CustomEvent): void => { + this.renderer2.setStyle(this.nativeElement, 'top', e.detail.top + 'px'); + }; + // 头部高度变化会触发此事件 + if (this.consoleDataService?.onChange) { + this.consoleDataService.onChange(this.onHeadChange); + } + } + + /** + * 弹窗显示事件 + * @param element 通过事件触发halfModal显示的元素 + */ + public show(element?: Element): void { + if (this.headElement) { + this.renderer.addClass(this.nativeElement, 'ti3-halfmodal-layout-container'); + } + if (this.consoleDataService?.getSafeArea) { + const safeArea: any = this.consoleDataService.getSafeArea(); + this.renderer2.setStyle(this.nativeElement, 'top', safeArea.top + 'px'); + } + this.isShow = true; + this.isWantShow = true; + // 该组件为onpush变化检测策略。Angular默认不会为该组件以及子组件执行变化检测。但是此处设计模板变量的修改,所以手动触发变化检测。 + this.changeDetector.markForCheck(); + this.renderer2.addClass(this.nativeElement, 'ti3-halfmodal-animation'); + if (this.backdrop && !this.backdropEle) { + this.backdropEle = this.renderer2.createElement('div'); + this.renderer2.addClass(this.backdropEle, 'ti3-halfmodal-backdrop'); + this.renderer2.appendChild(document.body, this.backdropEle); + this.zone.runOutsideAngular(() => { + // 避免不停触发变化检测 + // 防止多次打开半屏弹窗的时候,事件注册多次。 + if (!this.unListenDocumentKeydown) { + this.unListenDocumentKeydown = this.renderer2.listen(document, 'keydown', (event: KeyboardEvent): void => { + switch (event.which) { + case TiKeymap.KEY_TAB: // tab键用于处理在提示框内循环获取焦点 + this.clickTab(event); + break; + default: + break; + } + }); + } + }); + } else { + // 让Angular不在执行变化检测。 + this.zone.runOutsideAngular((): void => { + if (!this.unlistenMousedown && this.nomaskCloseable) { + this.unlistenMousedown = this.renderer2.listen(document, 'mousedown', ($event: MouseEvent): void => { + this.mousedownX = $event.clientX; + this.mousedownY = $event.clientY; + }); + } + }); + // click事件涉及更新模板中的变量,所以不能增加runOutsideAngular。 + if (!this.unlistenClick) { + this.unlistenClick = this.renderer2.listen(document, 'click', ($event: MouseEvent): void => { + if ((element && element.contains($event.target as Node)) || this.isWantShow) { + this.isWantShow = false; + + return; + } + if ( + TiHalfmodalComponent.findClosest($event.target, 'TI-MESSAGE') || + TiHalfmodalComponent.findClosest($event.target, 'TI-MODAL-WRAPPER') || + TiHalfmodalComponent.findClosest($event.target, 'TI-MODAL-HEADER') || + TiHalfmodalComponent.findClosest($event.target, 'TI-MODAL-FOOTER') || + TiHalfmodalComponent.findClosest($event.target, 'TI-MODAL-BODY') || + TiHalfmodalComponent.findClosest($event.target, 'TI-DROP') + ) { + return; + } + + // 判断鼠标是否在弹窗范围内, 如果不在弹窗内部,则关闭半屏弹窗。 + if ( + this.nomaskCloseable && + (document.documentElement.clientWidth - this.mousedownX > this.nativeElement.offsetWidth || + document.documentElement.clientHeight - this.mousedownY > this.nativeElement.offsetHeight) + ) { + this.wantHide(); + // 该组件为onpush变化检测策略。Angular默认不会为该组件以及子组件执行变化检测。但是此处设计模板变量的修改,所以手动触发变化检测。 + this.changeDetector.markForCheck(); + } + }); + } + // 当用户异步调用show方法时,在打开半屏弹窗之后,需要重置isWantShow的值,防止出现点击两次才能关闭的情况。 + setTimeout((): void => { + this.isWantShow = false; + }, 0); + } + } + private clickTab(event: KeyboardEvent): void { + const introModal: HTMLElement = document.querySelector('.ti3-halfmodal-animation'); + const focusableElements: NodeList = introModal?.querySelectorAll(this.focusableElementsString); + Util.focusInDialogOnTabchange(event, focusableElements); + } + /** + * @ignore + * 即将关闭,如果监听beforeHide,则由业务关闭弹窗 + */ + public wantHide(): void { + if (this.beforeHide.observers.length > 0) { + this.beforeHide.emit(this); + } else { + this.hide(); + } + } + /** + * 弹窗隐藏事件 + */ + public hide(): void { + this.hideBackdrop(); + this.isShow = false; + // 该组件为onpush变化检测策略。Angular默认不会为该组件以及子组件执行变化检测。但是此处设计模板变量的修改,所以手动触发变化检测。 + this.changeDetector.markForCheck(); + if (this.unlistenClick) { + this.unlistenClick(); + this.unlistenClick = undefined; + } + + if (this.unlistenMousedown) { + this.unlistenMousedown(); + this.unlistenMousedown = undefined; + } + } + /** + * @ignore + */ + public hideBackdrop(): void { + if (this.backdropEle) { + this.renderer2.removeChild(document.body, this.backdropEle); + this.backdropEle = undefined; + } + } + + ngOnDestroy(): void { + this.hideBackdrop(); + if (this.unlistenClick) { + this.unlistenClick(); + this.unlistenClick = undefined; + } + if (this.unListenDocumentKeydown) { + this.unListenDocumentKeydown(); + this.unListenDocumentKeydown = undefined; + } + if (this.unlistenMousedown) { + this.unlistenMousedown(); + this.unlistenMousedown = undefined; + } + if (this.consoleDataService?.offChange) { + this.consoleDataService.offChange(this.onHeadChange); + } + } +} diff --git a/src/halfmodal/lib/src/TiHalfmodalContainerComponent.ts b/src/halfmodal/lib/src/TiHalfmodalContainerComponent.ts new file mode 100644 index 0000000..1baba4e --- /dev/null +++ b/src/halfmodal/lib/src/TiHalfmodalContainerComponent.ts @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, Renderer2, TemplateRef } from '@angular/core'; +import { animate, state, style, transition, trigger } from '@angular/animations'; + +import { TiHalfmodalComponent } from './TiHalfmodalComponent'; + +/** + * @ignore + */ +/** + * 通过service打开半屏弹窗时,该组件作为弹窗容器 + * + */ +@Component({ + selector: 'ti-halfmodalcontainer', + templateUrl: './halfmodal.html', + styleUrls: ['./halfmodal.less'], + host: { + '[style.width]': 'width', + '[style.top]': 'clientRectTop', + '[style.right]': '"-" + width', + '[class]': '"ti3-halfmodal-layout-container"', + '[hidden]': '!isShow', + '[@halfmodalAnimate]': 'isShow' + }, + animations: [ + // 由于动画创建需要在元数据中解析,因此动画定义必须在此处定义 + trigger('halfmodalAnimate', [ + state( + 'show', + style({ + right: 0 + }) + ), + transition('* => show', animate('0.25s')) + ]) + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiHalfmodalContainerComponent extends TiHalfmodalComponent { + constructor( + protected elementRef: ElementRef, + protected renderer2: Renderer2, + public zone: NgZone, + protected changeDetector: ChangeDetectorRef + ) { + super(elementRef, renderer2, zone, changeDetector); + } + + public dismiss: (halfmodal: TemplateRef | any, content: TemplateRef | any) => void; + public beforeClose: (content: TemplateRef | any) => void; + public halfmodal: TemplateRef | any; + public content: any; + public destroyCallback: (reason: boolean) => void; + + ngOnInit(): void { + super.ngOnInit(); + this.show(); + } + + /** + * 点击弹窗外和右上角叉号的触发事件 + */ + public wantHide(): void { + this.dismiss(this.halfmodal, this.content); + } +} diff --git a/src/halfmodal/lib/src/TiHalfmodalFooterComponent.ts b/src/halfmodal/lib/src/TiHalfmodalFooterComponent.ts new file mode 100644 index 0000000..ebd71d2 --- /dev/null +++ b/src/halfmodal/lib/src/TiHalfmodalFooterComponent.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +/** + * 右侧半屏弹窗底部区域 + * + */ +@Component({ + selector: 'ti-halfmodal-footer', + template: '', + styleUrls: ['./halfmodal-footer.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiHalfmodalFooterComponent extends TiBaseComponent { + protected versionInfo: string = super.getVersion(packageInfo); +} diff --git a/src/halfmodal/lib/src/TiHalfmodalHeaderComponent.ts b/src/halfmodal/lib/src/TiHalfmodalHeaderComponent.ts new file mode 100644 index 0000000..aaa011e --- /dev/null +++ b/src/halfmodal/lib/src/TiHalfmodalHeaderComponent.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +/** + * 右侧半屏弹窗头部区域 + * + */ +@Component({ + selector: 'ti-halfmodal-header', + template: ` +
`, + styleUrls: ['./halfmodal-header.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiHalfmodalHeaderComponent extends TiBaseComponent { + protected versionInfo: string = super.getVersion(packageInfo); +} diff --git a/src/halfmodal/lib/src/TiHalfmodalModule.ts b/src/halfmodal/lib/src/TiHalfmodalModule.ts new file mode 100644 index 0000000..b463c29 --- /dev/null +++ b/src/halfmodal/lib/src/TiHalfmodalModule.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiPopupModule } from '@opentiny/ng-popup'; +import { TiIconModule } from '@opentiny/ng-icon'; + +import { TiHalfmodalComponent } from './TiHalfmodalComponent'; +import { TiHalfmodalHeaderComponent } from './TiHalfmodalHeaderComponent'; +import { TiHalfmodalFooterComponent } from './TiHalfmodalFooterComponent'; +import { TiHalfmodalBodyComponent } from './TiHalfmodalBodyComponent'; +import { TiHalfmodalContainerComponent } from './TiHalfmodalContainerComponent'; + +@NgModule({ + imports: [CommonModule, TiOutlineModule, TiPopupModule, TiIconModule], + exports: [ + TiHalfmodalComponent, + TiHalfmodalHeaderComponent, + TiHalfmodalFooterComponent, + TiHalfmodalBodyComponent, + TiHalfmodalContainerComponent + ], + declarations: [ + TiHalfmodalComponent, + TiHalfmodalHeaderComponent, + TiHalfmodalFooterComponent, + TiHalfmodalBodyComponent, + TiHalfmodalContainerComponent + ] +}) +export class TiHalfmodalModule {} +export { TiHalfmodalComponent } from './TiHalfmodalComponent'; +export { TiHalfmodalHeaderComponent } from './TiHalfmodalHeaderComponent'; +export { TiHalfmodalBodyComponent } from './TiHalfmodalBodyComponent'; +export { TiHalfmodalFooterComponent } from './TiHalfmodalFooterComponent'; +export { TiHalfmodalContainerComponent } from './TiHalfmodalContainerComponent'; diff --git a/src/halfmodal/lib/src/TiHalfmodalService.ts b/src/halfmodal/lib/src/TiHalfmodalService.ts new file mode 100644 index 0000000..6c3e5fc --- /dev/null +++ b/src/halfmodal/lib/src/TiHalfmodalService.ts @@ -0,0 +1,177 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ComponentRef, Injectable, Renderer2, RendererFactory2, TemplateRef } from '@angular/core'; +import { TiPopUpRef, TiPopupService } from '@opentiny/ng-popup'; + +import { TiHalfmodalModule } from './TiHalfmodalModule'; +import { TiHalfmodalContainerComponent } from './TiHalfmodalContainerComponent'; +/** + * 通过TiHalfmodalService打开弹窗的配置参数 + */ +export interface TiHalfmodalConfig { + /** + * 弹窗宽度,默认600px + */ + width?: string; + /** + * 是否含有遮罩层,默认没有遮罩层 + */ + backdrop?: boolean; + /** + * 弹窗顶部距窗口顶部的距离 + */ + clientRectTop?: string; + /** + * 是否显示关闭图标 + */ + closeIcon?: boolean; + /** + * 弹窗内使用的上下文 + */ + context?: {}; + /** + * 不带遮罩层的场景下,点击页面是否允许关闭 + */ + nomaskCloseable?: boolean; + /** + * @ignore + * + * 弹窗将要关闭的时候触发的回调 + */ + beforeClose?(context: any, destroy: (reason: boolean) => void, reason: boolean): void; + /** + * 调用上下文中 close 的时候触发回调 + */ + close?(context: any): void; + /** + * 调用上下文中 dismiss 、无背景层的弹窗点击弹窗外,以及点击右上角关闭图标的时候触发的回调 + */ + dismiss?(context?: any): void; +} +/** + * 半屏弹窗实例,是通过Service的方式打开半屏弹窗时方法的返回值 + */ +export interface TiHalfmodalRef { + /** + * 通过 service 的方式打开半屏弹窗时方法返回的实例;弹窗内容为 TemplateRef 时为空;弹窗内容为组件时,是半屏弹窗内容的实例 + */ + content: ComponentRef; + /** + * 调用上下文中 close 的时候绑定的事件 + */ + close(): void; + /** + * 调用上下文中 dismiss 、无背景层的弹窗点击弹窗外,以及点击右上角关闭图标绑定的事件 + */ + dismiss(): void; +} + +@Injectable({ + providedIn: TiHalfmodalModule +}) +export class TiHalfmodalService { + private renderer2: Renderer2; + private openModals: Array = []; + constructor(private tiPopup: TiPopupService, protected rendererFactory: RendererFactory2) { + this.renderer2 = this.rendererFactory.createRenderer(null, null); + } + + public open(content: TemplateRef | any, config?: TiHalfmodalConfig): TiHalfmodalRef { + let halfmodalInstance: TiHalfmodalRef; + let halfmodalContainerComponent: TiPopUpRef; + let halfmodalComponentRef: ComponentRef; + let unListenWindowHashchange: () => void; + if (this.openModals.includes(content) && !config.backdrop) { + return; + } + const context: any = config.context; + /** + * 不同关闭方式的回调,包括事件注销,config中的事件函数回调 + */ + const destroyCallback: (reason: boolean, halfmodalRef?: TiPopUpRef, content?: TemplateRef | any) => void = ( + reason: boolean, + halfmodalRef?: TemplateRef | any, + modalContent?: TemplateRef | any + ): void => { + destroy(reason, halfmodalRef, modalContent); + }; + const destroy: (reason: boolean, halfmodalRef?: TiPopUpRef, content?: TemplateRef | any) => void = ( + reason: boolean, + halfmodalRef?: TiPopUpRef, + modalContent?: TemplateRef | any + ): void => { + if (halfmodalRef) { + halfmodalRef.hide(); + this.openModals.splice(this.openModals.indexOf(modalContent), 1); + } else { + halfmodalContainerComponent.hide(); + this.openModals.splice(this.openModals.indexOf(halfmodalComponentRef), 1); + halfmodalComponentRef.instance.isShow = false; + } + unListenWindowHashchange(); + if (typeof config.dismiss === 'function' && !reason) { + config.dismiss(context); + } else if (typeof config.close === 'function' && reason) { + config.close(context); + } + }; + /** + * hide和dismiss的区别在于,hide从TiHalfmodalContainerComponent中传入了参数,在多个halfmodal的情况下,能区分关闭的是哪一个 + */ + const hide: (halfmodal: TemplateRef | any, content?: TemplateRef | any) => void = ( + halfmodalRef: TemplateRef | any, + modalContent: TemplateRef | any + ): void => { + destroyCallback(false, halfmodalRef, modalContent); + }; + halfmodalContainerComponent = this.tiPopup.create(TiHalfmodalContainerComponent); + /** + * “确认”按钮的绑定事件 + */ + const close: () => void = (): void => { + destroyCallback(true); + }; + /** + * “取消”按钮、右上角叉号、点击弹窗外 的绑定事件 + */ + const dismiss: () => void = (): void => { + destroyCallback(false); + }; + halfmodalInstance = { + content: undefined, + close, + dismiss + }; + + halfmodalComponentRef = halfmodalContainerComponent.show({ + content, + container: 'body', + contentContext: { ...context, close, dismiss }, + context: { + ...config, + dismiss: hide, + halfmodal: halfmodalContainerComponent, + content, + destroyCallback + } + }); + unListenWindowHashchange = this.renderer2.listen('window', 'hashchange', (): void => { + destroyCallback(false); + unListenWindowHashchange(); + }); + this.openModals.push(content); + halfmodalComponentRef.instance.isShow = true; + halfmodalInstance.content = (halfmodalComponentRef as any).tiContentRef.componentRef; + + return halfmodalInstance; + } +} diff --git a/src/halfmodal/lib/src/halfmodal-body.less b/src/halfmodal/lib/src/halfmodal-body.less new file mode 100644 index 0000000..54de360 --- /dev/null +++ b/src/halfmodal/lib/src/halfmodal-body.less @@ -0,0 +1,6 @@ +:host { + word-wrap: break-word; + flex-grow: 1; + overflow-y: auto; + padding: var(--ti-common-space-5x) var(--ti-common-space-10x) 0 0; +} diff --git a/src/halfmodal/lib/src/halfmodal-footer.less b/src/halfmodal/lib/src/halfmodal-footer.less new file mode 100644 index 0000000..e549064 --- /dev/null +++ b/src/halfmodal/lib/src/halfmodal-footer.less @@ -0,0 +1,11 @@ +:host { + flex-shrink: 0; + background: var(--ti-common-color-bg-white-normal); + padding-right: var(--ti-common-space-10x); + text-align: right; +} + +.ti3-halfmodal-footer-line { + border-top: 1px solid var(--ti-common-color-line-dividing); + padding-bottom: var(--ti-common-space-5x); +} diff --git a/src/halfmodal/lib/src/halfmodal-header.less b/src/halfmodal/lib/src/halfmodal-header.less new file mode 100644 index 0000000..95b5e83 --- /dev/null +++ b/src/halfmodal/lib/src/halfmodal-header.less @@ -0,0 +1,11 @@ +:host { + font-size: var(--ti-common-font-size-2); + line-height: var(--ti-common-line-height-number); + font-weight: bold; + padding-right: var(--ti-common-space-10x); +} + +.ti3-halfmodal-header-line { + border-bottom: 1px solid var(--ti-common-color-line-dividing); + padding-top: var(--ti-common-space-5x); +} diff --git a/src/halfmodal/lib/src/halfmodal.html b/src/halfmodal/lib/src/halfmodal.html new file mode 100644 index 0000000..782106f --- /dev/null +++ b/src/halfmodal/lib/src/halfmodal.html @@ -0,0 +1,15 @@ +
+ +
+ +
+
diff --git a/src/halfmodal/lib/src/halfmodal.less b/src/halfmodal/lib/src/halfmodal.less new file mode 100644 index 0000000..80893f3 --- /dev/null +++ b/src/halfmodal/lib/src/halfmodal.less @@ -0,0 +1,56 @@ +@import '../../../themes/basic/base-all.less'; +:host/* .ti3-halfmodal-container */ { + position: fixed; + bottom: 0; + background-color: var(--ti-common-color-bg-white-normal); + padding: var(--ti-common-space-8x); + padding-right: var(--ti-common-space-10x); // 右侧间距40px + box-shadow: var(--ti-common-shadow-4-left); + overflow-y: auto; + .box-sizing(); + .ti3-halfmodal-close { + font-size: var(--ti-common-size-3x); + color: var(--ti-common-color-icon-normal); + position: absolute; + top: 20px; + right: 20px; + cursor: pointer; + &:hover { + color: var(--ti-common-color-icon-hover); + } + } +} + +::ng-deep .ti3-halfmodal-layout-container { + padding-right: 0; + .ti3-halfmodal-content, + ng-component { + // 通过service把一个component放入halfmodal,会产生一个ng-component标签,该标签也需要是flex布局 + display: flex; + flex-direction: column; + flex-wrap: nowrap; + width: 100%; + height: 100%; + } +} + +::ng-deep.ti3-halfmodal-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1200; + box-sizing: border-box; + background-color: #000000; + opacity: 0.2; +} + +::ng-deep.ti3-halfmodal-animation { + animation: show 0.25s forwards; +} +@keyframes show { + 100% { + right: 0; + } +} diff --git a/src/icon/demo/karma.conf.js b/src/icon/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/icon/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/icon/demo/project.json b/src/icon/demo/project.json new file mode 100644 index 0000000..29fa8e7 --- /dev/null +++ b/src/icon/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/icon/demo", + "sourceRoot": "src/icon/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/icon", + "index": "src/icon/demo/src/index.html", + "main": "src/icon/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/icon/demo/tsconfig.app.json", + "assets": ["src/icon/demo/src/favicon.ico", "src/icon/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "icon-demo:build:production" + }, + "development": { + "browserTarget": "icon-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js icon" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/icon/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/icon/demo/tsconfig.spec.json", + "karmaConfig": "src/icon/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/icon/demo/src/app/AppComponent.ts b/src/icon/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/icon/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/icon/demo/src/app/AppModule.ts b/src/icon/demo/src/app/AppModule.ts new file mode 100644 index 0000000..651af49 --- /dev/null +++ b/src/icon/demo/src/app/AppModule.ts @@ -0,0 +1,25 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { HttpClientModule } from '@angular/common/http'; +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { IconTestModule } from './icon/IconTestModule'; + +@NgModule({ + imports: [ + IconTestModule, + HttpClientModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/icon/demo/src/app/IndexComponent.ts b/src/icon/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..ca85ce3 --- /dev/null +++ b/src/icon/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { IconTestModule } from './icon/IconTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = IconTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/icon/demo/src/app/app.html b/src/icon/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/icon/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/icon/demo/src/app/icon/IconBasicComponent.ts b/src/icon/demo/src/app/icon/IconBasicComponent.ts new file mode 100644 index 0000000..1ff04a0 --- /dev/null +++ b/src/icon/demo/src/app/icon/IconBasicComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './icon-basic.html', + styleUrls: ['./icon.less'] +}) +export class IconBasicComponent {} diff --git a/src/icon/demo/src/app/icon/IconShowComponent.ts b/src/icon/demo/src/app/icon/IconShowComponent.ts new file mode 100644 index 0000000..86442bc --- /dev/null +++ b/src/icon/demo/src/app/icon/IconShowComponent.ts @@ -0,0 +1,63 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './icon-show.html', + styleUrls: ['./icon.less'] +}) +export class IconShowComponent { + iconNames: Array = [ + 'checkmark', + 'alert-warn-bg', + 'add1', + 'alert-prompt', + 'close', + 'info-circle', + 'warn', + 'exclamation-circle', + 'check-circle', + 'calendar', + 'angle-left', + 'angle-right', + 'config', + 'arrow-down', + 'arrow-right', + 'minus-square', + 'accordion-fold', + 'accordion-unfold', + 'search', + 'alert-success', + 'alert-warn', + 'location', + 'sort', + 'clear', + 'x-thin', + 'document', + 'file', + 'plus-square', + 'add', + 'reduce', + 'discount', + 'staricon', + 'alarm', + 'discount-sup', + 'slider-point', + 'filter', + 'sold-out', + 'delete', + 'sort-ascent', + 'sort-descent', + 'go-to-old-edition', + 'left-1', + 'left-2', + 'right-1', + 'right-2', + 'edit', + 'action-binding', + 'star', + 'cancel', + 'refresh-1', + 'time', + 'delete-1', + 'alert-error' + ]; +} diff --git a/src/icon/demo/src/app/icon/IconTestModule.ts b/src/icon/demo/src/app/icon/IconTestModule.ts new file mode 100644 index 0000000..d95812d --- /dev/null +++ b/src/icon/demo/src/app/icon/IconTestModule.ts @@ -0,0 +1,31 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiIconModule } from '@opentiny/ng'; + +import { IconBasicComponent } from './IconBasicComponent'; +import { IconShowComponent } from './IconShowComponent'; +import { SvgSetpathComponent } from './SvgSetpathComponent'; + +@NgModule({ + imports: [CommonModule, TiIconModule, RouterModule.forChild(IconTestModule.ROUTES)], + declarations: [IconBasicComponent, IconShowComponent, SvgSetpathComponent] +}) +export class IconTestModule { + static readonly LINKS: Array = [{ href: 'components/TiIconComponent.html', label: 'Icon' }]; + static readonly ROUTES: Routes = [ + { + path: 'icon/icon-basic', + component: IconBasicComponent + }, + { + path: 'icon/icon-show', + component: IconShowComponent + }, + { + path: 'icon/svg-setpath', + component: SvgSetpathComponent + } + ]; +} diff --git a/src/icon/demo/src/app/icon/SvgSetpathComponent.ts b/src/icon/demo/src/app/icon/SvgSetpathComponent.ts new file mode 100644 index 0000000..479184f --- /dev/null +++ b/src/icon/demo/src/app/icon/SvgSetpathComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { TiSvgComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './svg-setpath.html', + styleUrls: ['./icon.less'] +}) +export class SvgSetpathComponent { + constructor() { + TiSvgComponent.setPath('/assets/ionicons/'); + } +} diff --git a/src/icon/demo/src/app/icon/icon-basic.html b/src/icon/demo/src/app/icon/icon-basic.html new file mode 100644 index 0000000..46d55c7 --- /dev/null +++ b/src/icon/demo/src/app/icon/icon-basic.html @@ -0,0 +1,6 @@ +
+
+ +
checkmark
+
+
diff --git a/src/icon/demo/src/app/icon/icon-show.html b/src/icon/demo/src/app/icon/icon-show.html new file mode 100644 index 0000000..43f1b26 --- /dev/null +++ b/src/icon/demo/src/app/icon/icon-show.html @@ -0,0 +1,6 @@ +
+
+ +
{{iconName}}
+
+
diff --git a/src/icon/demo/src/app/icon/icon.less b/src/icon/demo/src/app/icon/icon.less new file mode 100644 index 0000000..4847895 --- /dev/null +++ b/src/icon/demo/src/app/icon/icon.less @@ -0,0 +1,32 @@ +.my-icon { + display: grid; + grid-gap: 7px 7px; + justify-content: space-between; + grid-template-columns: repeat(auto-fill, 135px); +} +.my-icon-list { + position: relative; + display: inline-block; + width: 135px; + height: 135px; + margin: 3px 0; + padding: 10px 0 0; + overflow: hidden; + text-align: center; + list-style: none; + background-color: inherit; + border-radius: 4px; + cursor: pointer; +} +.my-icon-size { + font-size: 30px; +} +.my-icon-name { + position: absolute; + bottom: 60px; + text-align: center; + left: 0; + width: 100%; + padding: 0 8px; + box-sizing: border-box; +} diff --git a/src/icon/demo/src/app/icon/svg-setpath.html b/src/icon/demo/src/app/icon/svg-setpath.html new file mode 100644 index 0000000..5840a83 --- /dev/null +++ b/src/icon/demo/src/app/icon/svg-setpath.html @@ -0,0 +1,6 @@ +
+
+ +
accessibility-outline
+
+
diff --git a/src/icon/demo/src/app/icon/ui3-icons.PNG b/src/icon/demo/src/app/icon/ui3-icons.PNG new file mode 100644 index 0000000000000000000000000000000000000000..4cb71e6429531c5afad2979f10d2daf934b1f156 GIT binary patch literal 21280 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 literal 0 HcmV?d00001 diff --git a/src/icon/demo/src/app/icon/webdoc/icon-demos.js b/src/icon/demo/src/app/icon/webdoc/icon-demos.js new file mode 100644 index 0000000..2352bed --- /dev/null +++ b/src/icon/demo/src/app/icon/webdoc/icon-demos.js @@ -0,0 +1,42 @@ +export default { + column: '1', + + demos: [ + { + demoId: 'icon-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'icon-basic', + }, + desc: { + 'zh-CN': '

通过属性name配置要显示的图标。

', + 'en-US': '

basic

', + }, + apis: ['TiIconComponent.properties.name'], + }, + { + demoId: 'icon-show', + name: { + 'zh-CN': '所有图标', + 'en-US': 'icon-show', + }, + desc: { + 'zh-CN': '

', + 'en-US': '

basic

', + }, + }, + { + demoId: 'svg-setpath', + name: { + 'zh-CN': '非本组件库图标', + 'en-US': 'svg-setpath', + }, + desc: { + 'zh-CN': + '

使用非本组件库图标时,采用 HTTP 请求的策略,需要在项目模块(建议在根模块)中引入HttpClientModule。本示例以使用 ionicons 图标库中的图标为例,参考如下步骤:
(1)npm i iconicons
(2)配置 angular.json,让安装到 node_modules 里的图标转移到 assets
{
"glob": "**/*"
"input": "node_modules/ionicons/dist/ionicons/svg",
"output": "/assets/ionicons/"
}

(3)在 ts 文件中使用setPath方法配置图标路径

', + 'en-US': '

setpath

', + }, + apis: ['TiSvgComponent.methods.setPath'], + }, + ], +}; diff --git a/src/icon/demo/src/app/icon/webdoc/icon.cn.md b/src/icon/demo/src/app/icon/webdoc/icon.cn.md new file mode 100644 index 0000000..2089199 --- /dev/null +++ b/src/icon/demo/src/app/icon/webdoc/icon.cn.md @@ -0,0 +1,23 @@ +--- +title: Icon 图标 +--- +# Icon图标 + +
+ +提供常用图标。   + +```typescript +import { TiIconModule } from '@opentiny/ng'; +``` + +
+ +
+ +提供常用图标。   + +```typescript +import { TiIconModule } from '@opentiny/ng'; +``` +
diff --git a/src/icon/demo/src/app/icon/webdoc/icon.en.md b/src/icon/demo/src/app/icon/webdoc/icon.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/icon/demo/src/app/icon/webdoc/icon.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/icon/demo/src/favicon.ico b/src/icon/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/icon/demo/src/index.html b/src/icon/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/icon/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/icon/demo/src/main.ts b/src/icon/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/icon/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/icon/demo/test.ts b/src/icon/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/icon/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/icon/demo/tsconfig.app.json b/src/icon/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/icon/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/icon/demo/tsconfig.spec.json b/src/icon/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/icon/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/icon/lib/index.ts b/src/icon/lib/index.ts new file mode 100644 index 0000000..b92bc29 --- /dev/null +++ b/src/icon/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiIconModule'; diff --git a/src/icon/lib/ng-package.json b/src/icon/lib/ng-package.json new file mode 100644 index 0000000..7c72e63 --- /dev/null +++ b/src/icon/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/icon", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/icon/lib/package.json b/src/icon/lib/package.json new file mode 100644 index 0000000..433ce1f --- /dev/null +++ b/src/icon/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-icon", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/common/http": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/icon/lib/project.json b/src/icon/lib/project.json new file mode 100644 index 0000000..2728696 --- /dev/null +++ b/src/icon/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/icon/lib", + "sourceRoot": "src/icon/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/icon"], + "options": { + "project": "src/icon/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/icon"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js icon" + }, + { + "command": "ng default-build icon" + }, + { + "command": "node build/clear-default-theme.js icon" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/icon && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build icon && ng pack icon && node build/publish.js icon --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/icon/lib/src/Ti-Icon.svg b/src/icon/lib/src/Ti-Icon.svg new file mode 100644 index 0000000..41d2baf --- /dev/null +++ b/src/icon/lib/src/Ti-Icon.svg @@ -0,0 +1,83 @@ + + + + + + +{ + "fontFamily": "ti3Font", + "description": "Font generated by IcoMoon.", + "majorVersion": 1, + "minorVersion": 0, + "version": "Version 1.0", + "fontId": "ti3Font", + "psName": "ti3Font", + "subFamily": "Regular", + "fullName": "ti3Font" +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/icon/lib/src/Ti-Icon.woff b/src/icon/lib/src/Ti-Icon.woff new file mode 100644 index 0000000000000000000000000000000000000000..27ab59122d68e517a5e6977b072c533f7c7cc6d8 GIT binary patch literal 9248 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

=GZ4DLr2RZ zhd#1fuf)WQ-h`O^t!xX>#G{t9HBS=(oBZ3PnDD9>)r(O}EI^gBa!O3lO0QS_Q$kG1 zXVu>ZLdFv(xNW$s+cMfIcE#y`<;jn&?jq4^s4&pE?l7QtH!f$U|@I|jqZ~C z)fL=){)!Ui(n4M>>-P&CV=ux!b6muA1;fO0RL?j{;Lu(JISo_^%OyWsN@it0 zc1gvtMT@6xQFd3WuDe0q-_X}F?)Cejh8}sPorjDL^M)o~JgBRS6NVF&oM?d{#-eF0SwkNZe8K>$>LXy3&Otj?DUI zIR%&g)u-`lmtPMyHaWl8(Y~WGFcObTw+fXNyCuWW(3#w!ukY-*xvHwOd3~mB*zI1i^73x?a9d{mx#7F-9!8+wSbP6qNxW2a@cy;c z>za-X4sKbtwuiS24jyS*SKX+UK{?DLR{Ij(0S?1;0-O}M*vr5bqd%&irHP~JCH2x# zno!Rk73393s)vYH2d{_R`|&@1db_pjzeoiZ2cEe!yRi7it|(wv6p-e(<)sH@w7Ykr zW5_S+O84sJOzqWOs;R`{#gHD*kp-3WOM!k1@4e-;xWs29f>Us7DG-~+=j|3*bpQxwGA;(g+A@t68KeO~`{{rgg#bXa;p`oyrtu+i|m;hfQ8tTpa1{>J1p z-C~+BJz)A}MWEuoigT4MmA^CJV*Z->eam&09hU#FuD9;y^9ZebTKn~fwz9R}ERQVY zIZcYsIZlCPS^&Ms=QqiJQReGFbFUbGL}2P&Wm*sZ$ueysKHS!0_H{;ZEXXO*0_Y!? zX$Jc7GOYvs%Q7wEj_qKX)`LG=rcJ<_O*d{zY}z$>5I=F*L=p(Q$Rs&9FnREBXz!kb zdk*b9yk~dljw2z+?43BE5&~2Ja4X1aYaB93;7HY`!d-mS5f9D|_qW7Tvd&qvYeP~rlj3w;k HwVwY4GM45q literal 0 HcmV?d00001 diff --git a/src/icon/lib/src/TiIconClassComponent.ts b/src/icon/lib/src/TiIconClassComponent.ts new file mode 100644 index 0000000..235f474 --- /dev/null +++ b/src/icon/lib/src/TiIconClassComponent.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ElementRef, ViewEncapsulation, ChangeDetectionStrategy } from '@angular/core'; +/** + * @ignore + * ti-icon组件,为了防止多个组件重复使用字体图标文件,导致最终用户打包文件成倍增大,因此封装该组件,确保最终打包只引用一份字体图标样式及文件 + * TODO:考虑逐步去除这个i封装组件,采用ti-icon封装 + */ +@Component({ + selector: ':not(ti-icon).ti3-icon', + template: '', + styleUrls: ['./icon.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None +}) +export class TiIconClassComponent { + public nativeElement: any; // 宿主元素 + constructor(protected hostRef: ElementRef) { + this.nativeElement = this.hostRef.nativeElement; + } +} diff --git a/src/icon/lib/src/TiIconComponent.ts b/src/icon/lib/src/TiIconComponent.ts new file mode 100644 index 0000000..dec2aa9 --- /dev/null +++ b/src/icon/lib/src/TiIconComponent.ts @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + Component, + Input, + SimpleChange, + SimpleChanges, + ViewEncapsulation, + ElementRef, + Renderer2 +} from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +/** + * + * Icon组件可以设置显示Icon。 + * + */ +@Component({ + selector: 'ti-icon', + template: '', + styleUrls: ['./icon.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, // 样式是加在标签上的,不是内部,所以非胶囊 + host: { + '[class.ti3-icon]': 'true' + } +}) +export class TiIconComponent extends TiBaseComponent { + /** + * icon名称 + */ + @Input() name: string; + protected versionInfo: string = super.getVersion(packageInfo); + + constructor(protected hostRef: ElementRef, protected renderer: Renderer2) { + super(hostRef, renderer); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + const nameChange: SimpleChange = changes['name']; + if (nameChange) { + this.renderer.removeClass(this.nativeElement, `ti3-icon-${nameChange.previousValue}`); + this.renderer.addClass(this.nativeElement, `ti3-icon-${nameChange.currentValue}`); + } + } +} +/* ti-icon组件,为了防止多个组件重复使用字体图标文件,导致最终用户打包文件成倍增大。 +因此封装该组件,确保最终打包只引用一份字体图标样式及文件 */ diff --git a/src/icon/lib/src/TiIconModule.ts b/src/icon/lib/src/TiIconModule.ts new file mode 100644 index 0000000..81e7f03 --- /dev/null +++ b/src/icon/lib/src/TiIconModule.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIconClassComponent } from './TiIconClassComponent'; +import { TiSvgComponent } from './TiSvgComponent'; +import { TiIconComponent } from './TiIconComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [TiSvgComponent, TiIconComponent, TiIconClassComponent], + declarations: [TiSvgComponent, TiIconComponent, TiIconClassComponent] +}) +export class TiIconModule {} +export { TiSvgComponent } from './TiSvgComponent'; +export { TiIconClassComponent } from './TiIconClassComponent'; +export { TiIconComponent } from './TiIconComponent'; diff --git a/src/icon/lib/src/TiSvgComponent.ts b/src/icon/lib/src/TiSvgComponent.ts new file mode 100644 index 0000000..58c23d5 --- /dev/null +++ b/src/icon/lib/src/TiSvgComponent.ts @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, SimpleChange, SimpleChanges, ElementRef, Renderer2, ChangeDetectionStrategy } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { Util } from '@opentiny/ng-utils'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { share } from 'rxjs/operators'; +import packageInfo from '../package.json'; + +// TODO:如果封装为胶囊样式,DOM内部多一层,比现在优劣? +/** + * Icon组件 + * + * Icon组件可以设置显示Icon。 + * + */ +@Component({ + selector: 'ti-svg', + template: '', + styleUrls: ['./svg.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiSvgComponent extends TiBaseComponent { + protected versionInfo: string = super.getVersion(packageInfo); + /** + * Icon基路径 + */ + private static path: string = ''; + /** + * 保存url和观察者,避免并发的重复请求。 + */ + private static urlObservableMap: Map> = new Map>(); + /** + * 保存url和Data,相同的url,只发一次http。 + */ + private static urlDataMap: Map = new Map(); + /** + * 图标名称 + */ + @Input() name: string; + + constructor(protected hostRef: ElementRef, protected renderer: Renderer2, private http: HttpClient) { + super(hostRef, renderer); + } + + /** + * 设置icon下载地址,默认是'' + * @param path + */ + public static setPath(path: string): void { + TiSvgComponent.path = path; + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + const nameChange: SimpleChange = changes['name']; + if (nameChange && !Util.isEmptyString(nameChange.currentValue)) { + // 拼接URL + const url: string = `${TiSvgComponent.path}${nameChange.currentValue}.svg`; + // 取出缓存数据 + const data: string = TiSvgComponent.urlDataMap.get(url); + // 如果曾经下载过,有缓存数据 + if (data) { + // 将挂在下 + this.nativeElement.innerHTML = data; + } else { + // 如果并没有下载过,无缓存数据 + // 取出观察者 + let ob: Observable = TiSvgComponent.urlObservableMap.get(url); + if (!ob) { + // 如果不存在观察者,则发起http请求。share()是为了避免每次订阅都发起http请求。 + ob = this.http.get(url, { responseType: 'text' }).pipe(share()); + // 保存ur和观察者 + TiSvgComponent.urlObservableMap.set(url, ob); + } + // 订阅http请求结果 + ob.subscribe((svgText: string): void => { + // 将挂在下 + this.nativeElement.innerHTML = svgText; + // 删除url和观察者 + TiSvgComponent.urlObservableMap.delete(url); + // 保存数据 + TiSvgComponent.urlDataMap.set(url, svgText); + }); + } + } + } +} +/* ti-svg组件,为了防止多个组件重复使用字体图标文件,导致最终用户打包文件成倍增大。 +因此封装该组件,确保最终打包只引用一份字体图标样式及文件 */ diff --git a/src/icon/lib/src/icon.less b/src/icon/lib/src/icon.less new file mode 100644 index 0000000..c8cabba --- /dev/null +++ b/src/icon/lib/src/icon.less @@ -0,0 +1,193 @@ +@font-face { + font-family: 'ti3Font'; + src: data-uri('./Ti-Icon.woff') format('woff'); + font-weight: normal; + font-style: normal; +} + +.ti3-icon { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'ti3Font' !important; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + text-rendering: auto; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.ti3-icon-refresh:before { + content: '\e900'; +} +.ti3-icon-checkmark:before { + content: '\e901'; +} +.ti3-icon-alert-warn-bg:before { + content: '\e902'; +} +.ti3-icon-add1:before { + content: '\e903'; +} +.ti3-icon-alert-prompt:before { + content: '\e904'; +} +.ti3-icon-close:before { + content: '\e905'; +} +.ti3-icon-info-circle:before { + content: '\e906'; +} +.ti3-icon-warn:before { + content: '\e907'; +} +.ti3-icon-exclamation-circle:before { + content: '\e908'; +} +.ti3-icon-check-circle:before { + content: '\e909'; +} +.ti3-icon-calendar:before { + content: '\e90a'; +} +.ti3-icon-angle-left:before { + content: '\e90b'; +} +.ti3-icon-angle-right:before { + content: '\e90c'; +} +.ti3-icon-config:before { + content: '\e90d'; +} +.ti3-icon-minus-square:before { + content: '\e90e'; +} +.ti3-icon-arrow-down:before { + content: '\e90f'; +} +.ti3-icon-arrow-right:before { + content: '\e910'; +} +.ti3-icon-accordion-fold:before { + content: '\e911'; +} +.ti3-icon-accordion-unfold:before { + content: '\e912'; +} +.ti3-icon-search:before { + content: '\e913'; +} +.ti3-icon-alert-success:before { + content: '\e914'; +} +.ti3-icon-alert-warn:before { + content: '\e915'; +} +.ti3-icon-location:before { + content: '\e916'; +} +.ti3-icon-sort:before { + content: '\e917'; +} +.ti3-icon-go-to-old-edition:before { + content: '\e918'; +} +.ti3-icon-left-1:before { + content: '\e919'; +} +.ti3-icon-clear:before { + content: '\e91a'; +} +.ti3-icon-x-thin:before { + content: '\e91b'; +} +.ti3-icon-document:before { + content: '\e91c'; +} +.ti3-icon-file:before { + content: '\e91d'; +} +.ti3-icon-plus-square:before { + content: '\e91e'; +} +.ti3-icon-add:before { + content: '\e91f'; +} +.ti3-icon-reduce:before { + content: '\e920'; +} +.ti3-icon-discount:before { + content: '\e921'; +} +.ti3-icon-staricon:before { + content: '\e922'; +} +.ti3-icon-left-2:before { + content: '\e924'; +} +.ti3-icon-alarm:before { + content: '\e925'; +} +.ti3-icon-discount-sup:before { + content: '\e926'; +} +.ti3-icon-slider-point:before { + content: '\e927'; +} +.ti3-icon-filter:before { + content: '\e928'; +} +.ti3-icon-right-1:before { + content: '\e929'; +} +.ti3-icon-right-2:before { + content: '\e92a'; +} +.ti3-icon-edit:before { + content: '\e92b'; +} +.ti3-icon-sold-out:before { + content: '\e92c'; +} +.ti3-icon-delete:before { + content: '\e92d'; +} +.ti3-icon-sort-ascent:before { + content: '\e92e'; +} +.ti3-icon-sort-descent:before { + content: '\e92f'; +} +.ti3-icon-action-binding:before { + content: '\e930'; +} +.ti3-icon-star:before { + content: '\e932'; +} +.ti3-icon-cancel:before { + content: '\e923'; +} +.ti3-icon-refresh-1:before { + content: '\e931'; +} +.ti3-icon-time:before { + content: '\e933'; +} +.ti3-icon-delete-1:before { + content: '\e934'; +} +.ti3-icon-checkmark-small:before { + content: '\e935'; +} +.ti3-icon-cloud-action-tip:before { + content: '\e936'; +} +.ti3-icon-cloud-action-user:before { + content: '\e937'; +} +.ti3-icon-alert-error:before { + content: '\e938'; +} diff --git a/src/icon/lib/src/svg.less b/src/icon/lib/src/svg.less new file mode 100644 index 0000000..d595c23 --- /dev/null +++ b/src/icon/lib/src/svg.less @@ -0,0 +1,11 @@ +:host { + display: inline-block; + vertical-align: -0.125em; + font-size: 1em; + ::ng-deep svg { + width: 1em; + height: 1em; + font-size: inherit; + fill: currentColor; + } +} diff --git a/src/iconaction/demo/project.json b/src/iconaction/demo/project.json new file mode 100644 index 0000000..d6569fc --- /dev/null +++ b/src/iconaction/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/iconaction/demo", + "sourceRoot": "src/iconaction/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/iconaction", + "index": "src/iconaction/demo/src/index.html", + "main": "src/iconaction/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/iconaction/demo/tsconfig.app.json", + "assets": ["src/iconaction/demo/src/favicon.ico", "src/iconaction/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "iconaction-demo:build:production" + }, + "development": { + "browserTarget": "iconaction-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js iconaction" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/iconaction/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/iconaction/demo/tsconfig.spec.json", + "karmaConfig": "src/iconaction/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/iconaction/demo/src/app/AppComponent.ts b/src/iconaction/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/iconaction/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/iconaction/demo/src/app/AppModule.ts b/src/iconaction/demo/src/app/AppModule.ts new file mode 100644 index 0000000..e114ebe --- /dev/null +++ b/src/iconaction/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { HttpClientModule } from '@angular/common/http'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { IconactionTestModule } from './iconaction/IconactionTestModule'; + +@NgModule({ + imports: [ + IconactionTestModule, + HttpClientModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/iconaction/demo/src/app/IndexComponent.ts b/src/iconaction/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..c6f5982 --- /dev/null +++ b/src/iconaction/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { IconactionTestModule } from './iconaction/IconactionTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = IconactionTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/iconaction/demo/src/app/app.html b/src/iconaction/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/iconaction/demo/src/app/app.html @@ -0,0 +1,3 @@ +

+ +
diff --git a/src/iconaction/demo/src/app/iconaction/IconactionBasicComponent.ts b/src/iconaction/demo/src/app/iconaction/IconactionBasicComponent.ts new file mode 100644 index 0000000..33966f9 --- /dev/null +++ b/src/iconaction/demo/src/app/iconaction/IconactionBasicComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './iconaction-basic.html' +}) +export class IconactionBasicComponent {} diff --git a/src/iconaction/demo/src/app/iconaction/IconactionDarkComponent.ts b/src/iconaction/demo/src/app/iconaction/IconactionDarkComponent.ts new file mode 100644 index 0000000..86014f4 --- /dev/null +++ b/src/iconaction/demo/src/app/iconaction/IconactionDarkComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './iconaction-dark.html', + styles: [ + ` + .dark-bg { + width: 200px; + background-color: #373c57; + } + ` + ] +}) +export class IconactionDarkComponent {} diff --git a/src/iconaction/demo/src/app/iconaction/IconactionDisabledComponent.ts b/src/iconaction/demo/src/app/iconaction/IconactionDisabledComponent.ts new file mode 100644 index 0000000..5a59091 --- /dev/null +++ b/src/iconaction/demo/src/app/iconaction/IconactionDisabledComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './iconaction-disabled.html', + styles: [ + ` + .dark-bg { + width: 200px; + background-color: #373c57; + } + ` + ] +}) +export class IconactionDisabledComponent {} diff --git a/src/iconaction/demo/src/app/iconaction/IconactionHrefComponent.ts b/src/iconaction/demo/src/app/iconaction/IconactionHrefComponent.ts new file mode 100644 index 0000000..2f8cf31 --- /dev/null +++ b/src/iconaction/demo/src/app/iconaction/IconactionHrefComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './iconaction-href.html' +}) +export class IconactionHrefComponent {} diff --git a/src/iconaction/demo/src/app/iconaction/IconactionTestModule.ts b/src/iconaction/demo/src/app/iconaction/IconactionTestModule.ts new file mode 100644 index 0000000..9506e0a --- /dev/null +++ b/src/iconaction/demo/src/app/iconaction/IconactionTestModule.ts @@ -0,0 +1,37 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiIconactionModule, TiSvgComponent, TiIconModule } from '@opentiny/ng'; +import { IconactionBasicComponent } from './IconactionBasicComponent'; +import { IconactionDarkComponent } from './IconactionDarkComponent'; +import { IconactionDisabledComponent } from './IconactionDisabledComponent'; +import { IconactionHrefComponent } from './IconactionHrefComponent'; + +@NgModule({ + imports: [CommonModule, TiIconModule, TiIconactionModule, RouterModule.forChild(IconactionTestModule.ROUTES)], + declarations: [IconactionBasicComponent, IconactionDarkComponent, IconactionDisabledComponent, IconactionHrefComponent] +}) +export class IconactionTestModule { + constructor() { + TiSvgComponent.setPath('/assets/ionicons/'); + } + static readonly ROUTES: Routes = [ + { + path: 'iconaction/iconaction-basic', + component: IconactionBasicComponent + }, + { + path: 'iconaction/iconaction-dark', + component: IconactionDarkComponent + }, + { + path: 'iconaction/iconaction-disabled', + component: IconactionDisabledComponent + }, + { + path: 'iconaction/iconaction-href', + component: IconactionHrefComponent + } + ]; +} diff --git a/src/iconaction/demo/src/app/iconaction/iconaction-basic.html b/src/iconaction/demo/src/app/iconaction/iconaction-basic.html new file mode 100644 index 0000000..95ce26d --- /dev/null +++ b/src/iconaction/demo/src/app/iconaction/iconaction-basic.html @@ -0,0 +1 @@ + diff --git a/src/iconaction/demo/src/app/iconaction/iconaction-dark.html b/src/iconaction/demo/src/app/iconaction/iconaction-dark.html new file mode 100644 index 0000000..90d36e2 --- /dev/null +++ b/src/iconaction/demo/src/app/iconaction/iconaction-dark.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/iconaction/demo/src/app/iconaction/iconaction-disabled.html b/src/iconaction/demo/src/app/iconaction/iconaction-disabled.html new file mode 100644 index 0000000..cdcd9a7 --- /dev/null +++ b/src/iconaction/demo/src/app/iconaction/iconaction-disabled.html @@ -0,0 +1,4 @@ + +
+ +
diff --git a/src/iconaction/demo/src/app/iconaction/iconaction-href.html b/src/iconaction/demo/src/app/iconaction/iconaction-href.html new file mode 100644 index 0000000..1ed55e9 --- /dev/null +++ b/src/iconaction/demo/src/app/iconaction/iconaction-href.html @@ -0,0 +1 @@ + diff --git a/src/iconaction/demo/src/app/iconaction/webdoc/iconaction-demos.js b/src/iconaction/demo/src/app/iconaction/webdoc/iconaction-demos.js new file mode 100644 index 0000000..edec075 --- /dev/null +++ b/src/iconaction/demo/src/app/iconaction/webdoc/iconaction-demos.js @@ -0,0 +1,53 @@ +export default { + column: '2', + demos: [ + { + demoId: 'iconaction-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': 'Iconaction 组件的最简用法。', + 'en-US': '' + }, + apis: ['TiIconactionComponent.properties.iconName', 'TiIconactionComponent.properties.label'] + }, + { + demoId: 'iconaction-dark', + name: { + 'zh-CN': '深色背景', + 'en-US': '' + }, + desc: { + 'zh-CN': '

通过属性dark适配深色背景。

', + 'en-US': '' + }, + apis: ['TiIconactionComponent.properties.dark'] + }, + { + demoId: 'iconaction-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': '' + }, + desc: { + 'zh-CN': '

通过属性disabled配置禁用。

', + 'en-US': '' + }, + apis: ['TiIconactionComponent.properties.disabled'] + }, + { + demoId: 'iconaction-href', + name: { + 'zh-CN': '链接跳转', + 'en-US': '' + }, + desc: { + 'zh-CN': '

通过属性href配置跳转链接地址;通过属性target配置链接打开方式。

', + 'en-US': '' + }, + apis: ['TiIconactionComponent.properties.href'] + } + ] +}; diff --git a/src/iconaction/demo/src/app/iconaction/webdoc/iconaction.cn.md b/src/iconaction/demo/src/app/iconaction/webdoc/iconaction.cn.md new file mode 100644 index 0000000..e943801 --- /dev/null +++ b/src/iconaction/demo/src/app/iconaction/webdoc/iconaction.cn.md @@ -0,0 +1,17 @@ +--- +title: Iconaction 图标文本链接 +--- + +# Iconaction 图标文本链接 + +
+ +以图标 + 文本的形式显示链接。 + +- 图标、文字以及链接地址都可配置。 + +```typescript +import { TiIconactionModule } from '@opentiny/ng'; +``` + +
diff --git a/src/iconaction/demo/src/app/iconaction/webdoc/iconaction.en.md b/src/iconaction/demo/src/app/iconaction/webdoc/iconaction.en.md new file mode 100644 index 0000000..b807397 --- /dev/null +++ b/src/iconaction/demo/src/app/iconaction/webdoc/iconaction.en.md @@ -0,0 +1,13 @@ +--- +title: Iconaction +--- + +# Iconaction + +
+ +```typescript +import { TiIconactionModule } from '@opentiny/ng'; +``` + +
diff --git a/src/iconaction/demo/src/favicon.ico b/src/iconaction/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d7a7af634ee6d643ac81d81feadff569ac25de3d GIT binary patch literal 300852 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 + + + + + + Tiny + + + + + + + + + diff --git a/src/iconaction/demo/src/main.ts b/src/iconaction/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/iconaction/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/iconaction/demo/tsconfig.app.json b/src/iconaction/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/iconaction/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/iconaction/lib/index.ts b/src/iconaction/lib/index.ts new file mode 100644 index 0000000..cd4bfad --- /dev/null +++ b/src/iconaction/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiIconactionModule'; +export * from './src/TiIconactionComponent'; diff --git a/src/iconaction/lib/ng-package.json b/src/iconaction/lib/ng-package.json new file mode 100644 index 0000000..a010466 --- /dev/null +++ b/src/iconaction/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/iconaction", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/iconaction/lib/package.json b/src/iconaction/lib/package.json new file mode 100644 index 0000000..7242a95 --- /dev/null +++ b/src/iconaction/lib/package.json @@ -0,0 +1,10 @@ +{ + "name": "@opentiny/ng-iconaction", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/iconaction/lib/project.json b/src/iconaction/lib/project.json new file mode 100644 index 0000000..f8d98ba --- /dev/null +++ b/src/iconaction/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/iconaction/lib", + "sourceRoot": "src/iconaction/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/iconaction"], + "options": { + "project": "src/iconaction/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/iconaction"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js iconaction" + }, + { + "command": "ng default-build iconaction" + }, + { + "command": "node build/clear-default-theme.js iconaction" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/iconaction && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build iconaction && ng pack iconaction && node build/publish.js iconaction --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/iconaction/lib/src/TiIconactionComponent.ts b/src/iconaction/lib/src/TiIconactionComponent.ts new file mode 100644 index 0000000..a02cf36 --- /dev/null +++ b/src/iconaction/lib/src/TiIconactionComponent.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { ChangeDetectionStrategy, Component, ElementRef, Input, ViewChild } from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +/** + * 组件描述 + */ +@Component({ + selector: 'ti-iconaction', + templateUrl: 'iconaction.html', + styleUrls: ['iconaction.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiIconactionComponent extends TiFormComponent { + /** + * 图标名称 + */ + @Input() iconName: string; + /** + * 显示的文本 + */ + @Input() label: string; + /** + * 跳转链接 + */ + @Input() href: string; + /** + * 跳转方式 + */ + @Input() target: string = '_blank'; + /** + * @ignore + */ + @ViewChild('a', { static: true }) aRef: ElementRef; + // tslint:disable-next-line: use-life-cycle-interface + ngOnInit(): void { + // 基类中做了设置宿主id的操作 + super.ngOnInit(); + this.setFocusableElems([this.aRef.nativeElement]); + } + /** + * @ignore + */ + public onClickFn(event: Event): void { + if (!this.href) { + event.preventDefault(); + } + } + /** + * @ignore + * 不传href时,禁用右键打开新窗口功能 + */ + public oncontextmenu(): boolean { + return !!this.href; + } +} diff --git a/src/iconaction/lib/src/TiIconactionModule.ts b/src/iconaction/lib/src/TiIconactionModule.ts new file mode 100644 index 0000000..57decca --- /dev/null +++ b/src/iconaction/lib/src/TiIconactionModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIconactionComponent } from './TiIconactionComponent'; +import { TiIconModule } from '@opentiny/ng-icon'; + +@NgModule({ + imports: [CommonModule, TiIconModule], + exports: [TiIconactionComponent], + declarations: [TiIconactionComponent] +}) +export class TiIconactionModule {} +export { TiIconactionComponent } from './TiIconactionComponent'; diff --git a/src/iconaction/lib/src/iconaction.html b/src/iconaction/lib/src/iconaction.html new file mode 100644 index 0000000..02bdb19 --- /dev/null +++ b/src/iconaction/lib/src/iconaction.html @@ -0,0 +1,13 @@ + + + {{label}} + diff --git a/src/iconaction/lib/src/iconaction.less b/src/iconaction/lib/src/iconaction.less new file mode 100644 index 0000000..d75db42 --- /dev/null +++ b/src/iconaction/lib/src/iconaction.less @@ -0,0 +1,74 @@ +@import '../../../themes/basic/base-all.less'; + +.ti-iconaction { + cursor: pointer; + display: inline-flex; + align-items: center; + text-decoration: none; +} + +// 深色背景下有问题未解决 +.ti-iconaction-icon { + font-size: var(--ti-common-font-size-2); + display: inline-flex; + margin-right: var(--ti-common-space-2x); + color: var(--ti-common-color-icon-normal); + .ti-iconaction:hover &, + .ti-iconaction:active &, + .ti-iconaction:focus & { + color: var(--ti-common-color-icon-hover); + .transition(color; 0.2s); + } + :host[dark] & { + color: var(--ti-common-color-icon-darkbg-normal); + } + :host[dark] .ti-iconaction:hover &, + :host[dark] .ti-iconaction:active &, + :host[dark] .ti-iconaction:focus & { + color: var(--ti-common-color-icon-darkbg-hover); + .transition(color; 0.2s); + } +} + +:host[disabled] { + & .ti-iconaction { + cursor: not-allowed; + outline: none; + } + & .ti-iconaction-icon { + color: var(--ti-common-color-icon-disabled) !important; + } + & .ti-iconaction-text { + color: var(--ti-common-color-text-disabled) !important; + } +} + +:host[dark][disabled] { + & .ti-iconaction-icon { + color: var(--ti-common-color-icon-darkbg-disabled) !important; + } + & .ti-iconaction-text { + color: var(--ti-common-color-text-weaken) !important; + } +} + +.ti-iconaction-text { + font-size: var(--ti-common-font-size-base); + line-height: var(--ti-common-line-height-number); + + color: var(--ti-common-color-text-primary); + .ti-iconaction:hover &, + .ti-iconaction:active &, + .ti-iconaction:focus & { + color: var(--ti-common-color-text-highlight); + .transition(color; 0.2s); + } + :host[dark] & { + color: var(--ti-common-color-text-gray); + } + :host[dark] .ti-iconaction:hover &, + :host[dark] .ti-iconaction:active &, + :host[dark] .ti-iconaction:focus & { + color: var(--ti-common-color-text-link-darkbg-hover); + } +} diff --git a/src/imagepreview/demo/project.json b/src/imagepreview/demo/project.json new file mode 100644 index 0000000..3c45961 --- /dev/null +++ b/src/imagepreview/demo/project.json @@ -0,0 +1,86 @@ +{ + "projectType": "application", + "root": "src/imagepreview/demo", + "sourceRoot": "src/imagepreview/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/imagepreview", + "index": "src/imagepreview/demo/src/index.html", + "main": "src/imagepreview/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/imagepreview/demo/tsconfig.app.json", + "assets": [ + "src/imagepreview/demo/src/favicon.ico", + "src/imagepreview/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/image", + "output": "/assets/image/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "imagepreview-demo:build:production" + }, + "development": { + "browserTarget": "imagepreview-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js imagepreview" + } + ] + } + } + } +} diff --git a/src/imagepreview/demo/src/app/AppComponent.ts b/src/imagepreview/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/imagepreview/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/imagepreview/demo/src/app/AppModule.ts b/src/imagepreview/demo/src/app/AppModule.ts new file mode 100644 index 0000000..7b6647f --- /dev/null +++ b/src/imagepreview/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ImagepreviewTestModule } from './imagepreview/ImagepreviewTestModule'; + +@NgModule({ + imports: [ + ImagepreviewTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/imagepreview/demo/src/app/IndexComponent.ts b/src/imagepreview/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..5def667 --- /dev/null +++ b/src/imagepreview/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ImagepreviewTestModule } from './imagepreview/ImagepreviewTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = ImagepreviewTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/imagepreview/demo/src/app/app.html b/src/imagepreview/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/imagepreview/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/imagepreview/demo/src/app/imagepreview/ImagepreviewBasicComponent.ts b/src/imagepreview/demo/src/app/imagepreview/ImagepreviewBasicComponent.ts new file mode 100644 index 0000000..883e029 --- /dev/null +++ b/src/imagepreview/demo/src/app/imagepreview/ImagepreviewBasicComponent.ts @@ -0,0 +1,39 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +import { TiFilePreviewInfo, TiImagepreviewComponent, TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: './imagepreview-basic.html', + styleUrls: ['./imagepreview.less'], + encapsulation: ViewEncapsulation.None // 要想设置的样式生效,此处必须配置成 ViewEncapsulation.None +}) +export class ImagepreviewBasicComponent { + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL']; + files: Array = [ + { + previewUrl: `${this.baseUrl}assets/image/1.jpg` + }, + { + previewUrl: `${this.baseUrl}assets/image/3.jpg` + }, + { + previewUrl: `${this.baseUrl}assets/image/1.jpg` + }, + { + previewUrl: `${this.baseUrl}assets/image/3.jpg` + } + ]; + + constructor(private tiModal: TiModalService) {} + + showImagepreview(): void { + this.tiModal.open(TiImagepreviewComponent, { + id: 'productPreviewModal', + modalClass: 'tp-product-preview-modal', + context: { + index: 0, // 当前文件索引 + fileList: this.files + } + }); + } +} diff --git a/src/imagepreview/demo/src/app/imagepreview/ImagepreviewTestModule.ts b/src/imagepreview/demo/src/app/imagepreview/ImagepreviewTestModule.ts new file mode 100644 index 0000000..73cc710 --- /dev/null +++ b/src/imagepreview/demo/src/app/imagepreview/ImagepreviewTestModule.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiImagepreviewModule } from '@opentiny/ng'; + +import { ImagepreviewBasicComponent } from './ImagepreviewBasicComponent'; + +@NgModule({ + imports: [CommonModule, TiImagepreviewModule, TiButtonModule, RouterModule.forChild(ImagepreviewTestModule.ROUTES)], + declarations: [ImagepreviewBasicComponent] +}) +export class ImagepreviewTestModule { + static readonly LINKS: Array = [{ href: 'components/TiImagepreviewComponent.html', label: 'Icon' }]; + static readonly ROUTES: Routes = [ + { + path: 'imagepreview/imagepreview-basic', + component: ImagepreviewBasicComponent + } + ]; +} diff --git a/src/imagepreview/demo/src/app/imagepreview/imagepreview-basic.html b/src/imagepreview/demo/src/app/imagepreview/imagepreview-basic.html new file mode 100644 index 0000000..d441949 --- /dev/null +++ b/src/imagepreview/demo/src/app/imagepreview/imagepreview-basic.html @@ -0,0 +1,6 @@ +

描述

+

图片大图预览组件

+

导入

+

import {{ '{' }} TiImagepreviewModule {{ '}' }} from '@opentiny/ng';

+

示例

+ diff --git a/src/imagepreview/demo/src/app/imagepreview/imagepreview.less b/src/imagepreview/demo/src/app/imagepreview/imagepreview.less new file mode 100644 index 0000000..bb22055 --- /dev/null +++ b/src/imagepreview/demo/src/app/imagepreview/imagepreview.less @@ -0,0 +1,9 @@ +.tp-product-preview-modal { + width: 640px !important; + .ti3-modal-close { + top: 0; + } + .ti3-image-preview-container { + margin-top: var(--ti-common-space-2x); + } +} diff --git a/src/imagepreview/demo/src/app/imagepreview/webdoc/imagepreview-demos.js b/src/imagepreview/demo/src/app/imagepreview/webdoc/imagepreview-demos.js new file mode 100644 index 0000000..b48c2e8 --- /dev/null +++ b/src/imagepreview/demo/src/app/imagepreview/webdoc/imagepreview-demos.js @@ -0,0 +1,47 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'appendToBody', + name: { + 'zh-CN': '附着在body上', + 'en-US': 'appendToBody', + }, + desc: { + 'zh-CN': + '

通过属性appendToBody配置下拉面板是否附着在body上。在有局部滚动条的场景下,如果配置成false,下拉面板和选择框不会分离', + 'en-US': '

button color

', + }, + tag: 'website-tiny-select-appendtobody', + codeFiles: ['select-appendtobody.html', 'SelectAppendtobodyComponent.ts'], + }, + { + demoId: 'beforeOpen', + name: { + 'zh-CN': '懒加载', + 'en-US': 'beforeOpen', + }, + desc: { + 'zh-CN': '下拉面板展开前触发的回调,一般用于懒加载场景。', + 'en-US': '

button color

', + }, + tag: 'website-tiny-select-lazy', + codeFiles: ['select-lazy.html', 'SelectLazyComponent.ts'], + }, + { + demoId: 'template', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'item', + }, + desc: { + 'zh-CN': + '通过placeholder配置选择框提示文本的模板。通过selected配置选择框中选中项的模板。通过item配置下拉列表中选项的模板。通过footer配置下拉列表底部的模板。', + 'en-US': '

item

', + }, + tag: 'website-tiny-select-template', + codeFiles: ['select-template.html', 'SelectTemplateComponent.ts'], + }, + ], +}; diff --git a/src/imagepreview/demo/src/app/imagepreview/webdoc/imagepreview.cn.md b/src/imagepreview/demo/src/app/imagepreview/webdoc/imagepreview.cn.md new file mode 100644 index 0000000..6caabe4 --- /dev/null +++ b/src/imagepreview/demo/src/app/imagepreview/webdoc/imagepreview.cn.md @@ -0,0 +1,27 @@ +--- +title: Select 选择器 +--- +# Select 选择器 + +
+ +Select 是提供选项菜单的组件。   + ++ 支持单选、多选、分组、搜索、懒加载等场景。 + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +Select 是提供选项菜单的组件。   + ++ 支持单选、多选、分组、搜索、懒加载等场景。 + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
diff --git a/src/imagepreview/demo/src/app/imagepreview/webdoc/imagepreview.en.md b/src/imagepreview/demo/src/app/imagepreview/webdoc/imagepreview.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/imagepreview/demo/src/app/imagepreview/webdoc/imagepreview.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/imagepreview/demo/src/favicon.ico b/src/imagepreview/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/imagepreview/demo/src/index.html b/src/imagepreview/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/imagepreview/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/imagepreview/demo/src/main.ts b/src/imagepreview/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/imagepreview/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/imagepreview/demo/tsconfig.app.json b/src/imagepreview/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/imagepreview/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/imagepreview/lib/index.ts b/src/imagepreview/lib/index.ts new file mode 100644 index 0000000..222675c --- /dev/null +++ b/src/imagepreview/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiImagepreviewModule'; diff --git a/src/imagepreview/lib/ng-package.json b/src/imagepreview/lib/ng-package.json new file mode 100644 index 0000000..2ea7169 --- /dev/null +++ b/src/imagepreview/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/imagepreview", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/imagepreview/lib/package.json b/src/imagepreview/lib/package.json new file mode 100644 index 0000000..2467bff --- /dev/null +++ b/src/imagepreview/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-imagepreview", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/platform-browser": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-modal": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/imagepreview/lib/project.json b/src/imagepreview/lib/project.json new file mode 100644 index 0000000..2c98be6 --- /dev/null +++ b/src/imagepreview/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/imagepreview/lib", + "sourceRoot": "src/imagepreview/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/imagepreview"], + "options": { + "project": "src/imagepreview/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/imagepreview"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js imagepreview" + }, + { + "command": "ng default-build imagepreview" + }, + { + "command": "node build/clear-default-theme.js imagepreview" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/imagepreview && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build imagepreview && ng pack imagepreview && node build/publish.js imagepreview --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/imagepreview/lib/src/TiImagepreviewComponent.ts b/src/imagepreview/lib/src/TiImagepreviewComponent.ts new file mode 100644 index 0000000..ee18305 --- /dev/null +++ b/src/imagepreview/lib/src/TiImagepreviewComponent.ts @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterViewInit, Component, ElementRef, Input, Renderer2, ViewChild, ChangeDetectionStrategy } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +// 文件预览信息接口 +export interface TiFilePreviewInfo { + /** + * 文件名称 + */ + name?: string; + + /** + * 预览地址 + */ + previewUrl?: string; + + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} + +/** + * @ignore + * + * + * 该组件主要用于图片放大预览 + * + */ +@Component({ + selector: 'ti-imagepreview', + templateUrl: './imagepreview.html', + styleUrls: ['./imagepreview.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiImagepreviewComponent extends TiBaseComponent implements AfterViewInit { + /** + * 需要预览的文件列表 + */ + @Input() fileList: Array = []; + + /** + * 是否展示文件名称,默认展示 + */ + @Input() showFileName: boolean = true; + + /** + * 默认展示的图片的索引 + */ + @Input() index: number = 0; + + /** + * @ignore 内部变量 + */ + @ViewChild('imgTemplate', { static: true }) imgTemplate: ElementRef; + /** + * @ignore 内部变量 + */ + @ViewChild('switchBtn', { static: true }) switchBtnTemplate: ElementRef; + + protected versionInfo: string = super.getVersion(packageInfo); + + constructor(public sanitizer: DomSanitizer, protected renderer: Renderer2, private elementRef: ElementRef) { + super(elementRef, renderer); + } + + ngAfterViewInit(): void { + this.addImgLoadListener(); + } + + /** + * @ignore 翻到上一张图片 + */ + public toNext(): void { + if (this.index === this.fileList.length - 1) { + return; + } + this.addImgLoadListener(); + this.index++; + } + + /** + * @ignore 翻到下一张图片 + */ + public toPrevious(): void { + if (this.index === 0) { + return; + } + this.addImgLoadListener(); + this.index--; + } + + // 由于图片高度不确定,需动态设置top值,所以增加load事件。 + private addImgLoadListener(): void { + this.imgTemplate.nativeElement.onload = (): void => { + const imgHeight: number = this.imgTemplate.nativeElement.height; + if (imgHeight !== 0) { + const top: number = (imgHeight - 48) / 2; + this.renderer.setStyle(this.switchBtnTemplate.nativeElement, 'top', top + 'px'); + } + // 防止循环触发load事件 + this.imgTemplate.nativeElement.onload = null; + }; + } +} diff --git a/src/imagepreview/lib/src/TiImagepreviewModule.ts b/src/imagepreview/lib/src/TiImagepreviewModule.ts new file mode 100644 index 0000000..7c2e11d --- /dev/null +++ b/src/imagepreview/lib/src/TiImagepreviewModule.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiImagepreviewComponent } from './TiImagepreviewComponent'; +import { TiModalModule } from '@opentiny/ng-modal'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +/** + * @ignore + */ +@NgModule({ + imports: [TiModalModule, CommonModule, TiIconModule, TiOutlineModule], + exports: [TiImagepreviewComponent], + declarations: [TiImagepreviewComponent] +}) +export class TiImagepreviewModule {} +export { TiImagepreviewComponent, TiFilePreviewInfo } from './TiImagepreviewComponent'; diff --git a/src/imagepreview/lib/src/imagepreview.html b/src/imagepreview/lib/src/imagepreview.html new file mode 100644 index 0000000..9f2098b --- /dev/null +++ b/src/imagepreview/lib/src/imagepreview.html @@ -0,0 +1,30 @@ + + {{fileList[index].name}} + + +
+ +
+
+ + +
+
diff --git a/src/imagepreview/lib/src/imagepreview.less b/src/imagepreview/lib/src/imagepreview.less new file mode 100644 index 0000000..67d054c --- /dev/null +++ b/src/imagepreview/lib/src/imagepreview.less @@ -0,0 +1,58 @@ +:host { + --ti-image-preview-icon-width: var(--ti-common-size-6x); + --ti-image-preview-container-margin: var(--ti-common-space-5x); +} + +.ti3-image-preview-modal-header { + padding: var(--ti-common-space-3x) var(--ti-common-space-5x) !important; + font-size: var(--ti-common-font-size-1) !important; + font-weight: var(--ti-common-font-weight-4); + line-height: 20px; +} +.ti3-image-preview-modal-body { + margin: 0 var(--ti-image-preview-container-margin) !important; + max-height: none !important; + // 因为弹窗的纵向滚动条需要完全靠右(issue #1902),但弹窗内容与边框也有间距,所以将弹窗的margin改为了padding + // 而预览图片时,不需要额外的间距,所以将padding设置为0。 + padding: 0; +} + +.ti3-image-preview-container { + margin: 0 auto var(--ti-image-preview-container-margin); + img { + display: inline-block; + width: 100%; + border-style: none; + } +} +.ti3-image-preview-icon-container { + position: absolute; + width: 100%; + .ti3-image-preview-icon { + display: inline-block; + width: var(--ti-image-preview-icon-width); + height: calc(var(--ti-image-preview-icon-width) * 2); + line-height: calc(var(--ti-image-preview-icon-width) * 2); + background: rgba(0, 0, 0, 0.2); + font-size: var(--ti-image-preview-icon-width); + color: #ffffff; + cursor: pointer; + border-radius: 2px; + &:hover, + &:focus { + background: rgba(0, 0, 0, 0.3); + } + } + .ti3-image-preview-icon-disabled { + background: rgba(0, 0, 0, 0.2) !important; + color: rgba(255, 255, 255, 0.4); + cursor: not-allowed; + outline: none; + } + .ti3-image-preview-icon-left { + float: left; + } + .ti3-image-preview-icon-right { + float: right; + } +} diff --git a/src/include/lib/index.ts b/src/include/lib/index.ts new file mode 100644 index 0000000..f7a5364 --- /dev/null +++ b/src/include/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiIncludeModule'; diff --git a/src/include/lib/ng-package.json b/src/include/lib/ng-package.json new file mode 100644 index 0000000..8b7bb53 --- /dev/null +++ b/src/include/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/include", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/include/lib/package.json b/src/include/lib/package.json new file mode 100644 index 0000000..78e3594 --- /dev/null +++ b/src/include/lib/package.json @@ -0,0 +1,10 @@ +{ + "name": "@opentiny/ng-include", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/include/lib/project.json b/src/include/lib/project.json new file mode 100644 index 0000000..7b0e259 --- /dev/null +++ b/src/include/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/include/lib", + "sourceRoot": "src/include/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/include"], + "options": { + "project": "src/include/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/include"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js include" + }, + { + "command": "ng default-build include" + }, + { + "command": "node build/clear-default-theme.js include" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/include && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build include && ng pack include && node build/publish.js include --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/include/lib/src/TiIncludeDirective.ts b/src/include/lib/src/TiIncludeDirective.ts new file mode 100644 index 0000000..7492e25 --- /dev/null +++ b/src/include/lib/src/TiIncludeDirective.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Input, OnChanges, Renderer2, SimpleChanges } from '@angular/core'; + +import { Util } from '@opentiny/ng-utils'; + +/** + * @ignore + * 此指令功能:清空宿主元素的子元素,将传入的string或Node放入(append) + * + * 类似angular.js的ng-bind-html功能 + */ +@Directive({ + selector: '[tiInclude]' +}) +export class TiIncludeDirective implements OnChanges { + @Input() tiInclude: string | Node; // 只支持string和Node两种类型 + private element: HTMLElement; + constructor(private elementRef: ElementRef, private renderer: Renderer2) { + this.element = this.elementRef.nativeElement; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['tiInclude'] && !Util.isUndefined(this.tiInclude)) { + // 清空宿主元素的子节点 + if (this.element.childNodes.length > 0) { + this.element.innerHTML = ''; + } + // 如果 tiInclude 为字符串,则当作文本节点处理 + const node: Node = Util.isString(this.tiInclude) ? this.renderer.createText(this.tiInclude as string) : this.tiInclude; + this.renderer.appendChild(this.element, node); + } + } +} diff --git a/src/include/lib/src/TiIncludeModule.ts b/src/include/lib/src/TiIncludeModule.ts new file mode 100644 index 0000000..d44f2df --- /dev/null +++ b/src/include/lib/src/TiIncludeModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIncludeDirective } from './TiIncludeDirective'; +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule], + exports: [TiIncludeDirective], + declarations: [TiIncludeDirective] +}) +export class TiIncludeModule {} +export { TiIncludeDirective } from './TiIncludeDirective'; diff --git a/src/inputnumber/demo/karma.conf.js b/src/inputnumber/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/inputnumber/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/inputnumber/demo/project.json b/src/inputnumber/demo/project.json new file mode 100644 index 0000000..6924d2e --- /dev/null +++ b/src/inputnumber/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/inputnumber/demo", + "sourceRoot": "src/inputnumber/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/inputnumber", + "index": "src/inputnumber/demo/src/index.html", + "main": "src/inputnumber/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/inputnumber/demo/tsconfig.app.json", + "assets": ["src/inputnumber/demo/src/favicon.ico", "src/inputnumber/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "inputnumber-demo:build:production" + }, + "development": { + "browserTarget": "inputnumber-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js inputnumber" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/inputnumber/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/inputnumber/demo/tsconfig.spec.json", + "karmaConfig": "src/inputnumber/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/inputnumber/demo/src/app/AppComponent.ts b/src/inputnumber/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/inputnumber/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/inputnumber/demo/src/app/AppModule.ts b/src/inputnumber/demo/src/app/AppModule.ts new file mode 100644 index 0000000..869a74a --- /dev/null +++ b/src/inputnumber/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { InputnumberTestModule } from './inputnumber/InputnumberTestModule'; + +@NgModule({ + imports: [ + InputnumberTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/inputnumber/demo/src/app/IndexComponent.ts b/src/inputnumber/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..41f1206 --- /dev/null +++ b/src/inputnumber/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { InputnumberTestModule } from './inputnumber/InputnumberTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = InputnumberTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/inputnumber/demo/src/app/app.html b/src/inputnumber/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/inputnumber/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberBasicComponent.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberBasicComponent.ts new file mode 100644 index 0000000..481751b --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './inputnumber-basic.html' +}) +export class InputnumberBasicComponent { + inputValue: number = 11111.45; +} diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberEventComponent.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberEventComponent.ts new file mode 100644 index 0000000..f53a469 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberEventComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './inputnumber-event.html' +}) +export class InputnumberEventComponent { + myLogs: Array = []; + value: number = 189.421; + + onModelChange(value: any): void { + this.myLogs = [...this.myLogs, `modelChange:${value}`]; + } + + onFocus(value: any): void { + this.myLogs = [...this.myLogs, `focus:${value}`]; + } + + onBlur(value: any): void { + this.myLogs = [...this.myLogs, `blur:${value}`]; + } +} diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberFocusComponent.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberFocusComponent.ts new file mode 100644 index 0000000..d160d3e --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberFocusComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './inputnumber-focus.html' +}) +export class InputnumberFocusComponent { + inputValue: number = 2345.565678; +} diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberFormatComponent.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberFormatComponent.ts new file mode 100644 index 0000000..2423dd8 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberFormatComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './inputnumber-format.html' +}) +export class InputnumberFormatComponent { + format: string = 'n2'; + formatValue: number = 33333.45678; +} diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberLoadComponent.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberLoadComponent.ts new file mode 100644 index 0000000..c3ce33b --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberLoadComponent.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './inputnumber-load.html' +}) +export class InputnumberLoadComponent { + inputValue: any = undefined; + inputValue1: any = 1234.56789; + changeA(): void { + this.inputValue = 123456.789; + } + changeB(): void { + this.inputValue = 123456.782199; + } + changeC(): void { + this.inputValue1 = 2222.4567; + } + changeInvalid(): void { + this.inputValue1 = 'dfasd'; + } + clear(): void { + this.inputValue1 = undefined; + } +} diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberLocaleableComponent.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberLocaleableComponent.ts new file mode 100644 index 0000000..752abd6 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberLocaleableComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './inputnumber-localeable.html' +}) +export class InputnumberLocaleableComponent { + localValue: number = 33333.45678; + localeable: boolean = false; +} diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberMaxlengthComponent.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberMaxlengthComponent.ts new file mode 100644 index 0000000..91282fa --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberMaxlengthComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './inputnumber-maxlength.html' +}) +export class InputnumberMaxlengthComponent { + inputValue: number = 2345.565678; +} diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberTestModule.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberTestModule.ts new file mode 100644 index 0000000..6167002 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberTestModule.ts @@ -0,0 +1,70 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiInputNumberModule, TiTextModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { InputnumberBasicComponent } from './InputnumberBasicComponent'; +import { InputnumberMaxlengthComponent } from './InputnumberMaxlengthComponent'; +import { InputnumberLoadComponent } from './InputnumberLoadComponent'; +import { InputnumberFocusComponent } from './InputnumberFocusComponent'; +import { InputnumberFormatComponent } from './InputnumberFormatComponent'; +import { InputnumberLocaleableComponent } from './InputnumberLocaleableComponent'; +import { InputnumberEventComponent } from './InputnumberEventComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiTextModule, + TiInputNumberModule, + TiButtonModule, + DemoLogModule, + RouterModule.forChild(InputnumberTestModule.ROUTES) + ], + declarations: [ + InputnumberBasicComponent, + InputnumberMaxlengthComponent, + InputnumberLoadComponent, + InputnumberFocusComponent, + InputnumberFormatComponent, + InputnumberLocaleableComponent, + InputnumberEventComponent + ] +}) +export class InputnumberTestModule { + static readonly LINKS: Array = [{ href: 'directives/TiInputNumberDirective.html', label: 'InputNumber' }]; + static readonly ROUTES: Routes = [ + { + path: 'inputnumber/inputnumber-basic', + component: InputnumberBasicComponent + }, + { + path: 'inputnumber/inputnumber-format', + component: InputnumberFormatComponent + }, + { + path: 'inputnumber/inputnumber-localeable', + component: InputnumberLocaleableComponent + }, + { + path: 'inputnumber/inputnumber-maxlength', + component: InputnumberMaxlengthComponent + }, + { + path: 'inputnumber/inputnumber-load', + component: InputnumberLoadComponent + }, + { + path: 'inputnumber/inputnumber-focus', + component: InputnumberFocusComponent + }, + { + path: 'inputnumber/inputnumber-event', + component: InputnumberEventComponent + } + ]; +} diff --git a/src/inputnumber/demo/src/app/inputnumber/inputnumber-basic.html b/src/inputnumber/demo/src/app/inputnumber/inputnumber-basic.html new file mode 100644 index 0000000..d70ad73 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/inputnumber-basic.html @@ -0,0 +1,4 @@ + +
+
Current value: {{ inputValue }}
+
diff --git a/src/inputnumber/demo/src/app/inputnumber/inputnumber-event.html b/src/inputnumber/demo/src/app/inputnumber/inputnumber-event.html new file mode 100644 index 0000000..ef85fad --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/inputnumber-event.html @@ -0,0 +1,11 @@ + + diff --git a/src/inputnumber/demo/src/app/inputnumber/inputnumber-focus.html b/src/inputnumber/demo/src/app/inputnumber/inputnumber-focus.html new file mode 100644 index 0000000..b7cb83c --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/inputnumber-focus.html @@ -0,0 +1,9 @@ +

1.autofocus方式聚焦:

+ +

+

2.focus()方式聚焦:

+ +

+ +  + diff --git a/src/inputnumber/demo/src/app/inputnumber/inputnumber-format.html b/src/inputnumber/demo/src/app/inputnumber/inputnumber-format.html new file mode 100644 index 0000000..de391df --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/inputnumber-format.html @@ -0,0 +1 @@ + diff --git a/src/inputnumber/demo/src/app/inputnumber/inputnumber-load.html b/src/inputnumber/demo/src/app/inputnumber/inputnumber-load.html new file mode 100644 index 0000000..b2f5ceb --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/inputnumber-load.html @@ -0,0 +1,20 @@ +

描述

+

inputnumber组件,数据加载

+

示例

+

1.使用典型场景:空数据->数据A->数据B

+
+ +

+ +   + +
+

2.使用典型场景:数据A->数据C->空数据(当用户给组件传递为非法值时,数据不会改变)

+
+ +

+ +   + +   + diff --git a/src/inputnumber/demo/src/app/inputnumber/inputnumber-localeable.html b/src/inputnumber/demo/src/app/inputnumber/inputnumber-localeable.html new file mode 100644 index 0000000..0d9b0e1 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/inputnumber-localeable.html @@ -0,0 +1 @@ + diff --git a/src/inputnumber/demo/src/app/inputnumber/inputnumber-maxlength.html b/src/inputnumber/demo/src/app/inputnumber/inputnumber-maxlength.html new file mode 100644 index 0000000..fa84f30 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/inputnumber-maxlength.html @@ -0,0 +1 @@ + diff --git a/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber-demos.js b/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber-demos.js new file mode 100644 index 0000000..db560f6 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber-demos.js @@ -0,0 +1,66 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'inputnumber-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

InputNumber 指令的最简用法。

', + 'en-US': '', + }, + }, + { + demoId: 'inputnumber-format', + name: { + 'zh-CN': '数字精度', + 'en-US': 'format', + }, + desc: { + 'zh-CN': + '

通过属性format配置小数点位数,使用“n+数字”方式设置,数字代表保留几位小数。不配置 format 时,默认最少保留 0 位小数,最多保留 3 位小数。

', + 'en-US': '', + }, + apis: ['TiInputNumberDirective.properties.format'], + }, + { + demoId: 'inputnumber-localeable', + name: { + 'zh-CN': '国际化', + 'en-US': 'localeable', + }, + desc: { + 'zh-CN': + '

通过属性localeable配置是否开启国际化。默认值为 true,开启国际化。

', + 'en-US': '', + }, + apis: ['TiInputNumberDirective.properties.localeable'], + }, + { + demoId: 'inputnumber-maxlength', + name: { + 'zh-CN': '可输入最大长度', + 'en-US': 'maxlength', + }, + desc: { + 'zh-CN': '

通过属性maxlength配置输入的最大字符数。

', + 'en-US': '', + }, + }, + { + demoId: 'inputnumber-event', + name: { + 'zh-CN': '事件', + 'en-US': 'events', + }, + desc: { + 'zh-CN': + '

当元素聚焦的时候触发focus事件。当元素失焦的时候触发blur事件。当元素内容发生变化的时候触发ngModelChange事件。

', + 'en-US': '', + }, + }, + ], +}; diff --git a/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.cn.md b/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.cn.md new file mode 100644 index 0000000..32b67f2 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.cn.md @@ -0,0 +1,27 @@ +--- +title: InputNumber 数字输入框 +--- +# InputNumber 数字输入框 + +
+ +InputNumber 是数字输入框指令。   + ++ 支持输入框数字保留精度、国际化等显示场景。 + +```typescript +import { TiInputNumberModule } from '@opentiny/ng'; +``` + +
+ +
+ +InputNumber 是数字输入框指令。   + ++ 支持输入框数字保留精度、国际化等显示场景。 + +```typescript +import { TiInputNumberModule } from '@cloud/tiny-config'; +``` +
diff --git a/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.en.md b/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/inputnumber/demo/src/favicon.ico b/src/inputnumber/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/inputnumber/demo/src/index.html b/src/inputnumber/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/inputnumber/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/inputnumber/demo/src/main.ts b/src/inputnumber/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/inputnumber/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/inputnumber/demo/test.ts b/src/inputnumber/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/inputnumber/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/inputnumber/demo/tsconfig.app.json b/src/inputnumber/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/inputnumber/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/inputnumber/demo/tsconfig.spec.json b/src/inputnumber/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/inputnumber/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/inputnumber/lib/index.ts b/src/inputnumber/lib/index.ts new file mode 100644 index 0000000..18fc0c7 --- /dev/null +++ b/src/inputnumber/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiInputNumberModule'; diff --git a/src/inputnumber/lib/ng-package.json b/src/inputnumber/lib/ng-package.json new file mode 100644 index 0000000..a6816eb --- /dev/null +++ b/src/inputnumber/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/inputnumber", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/inputnumber/lib/package.json b/src/inputnumber/lib/package.json new file mode 100644 index 0000000..3f0f99d --- /dev/null +++ b/src/inputnumber/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-inputnumber", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/forms": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/inputnumber/lib/project.json b/src/inputnumber/lib/project.json new file mode 100644 index 0000000..df423c9 --- /dev/null +++ b/src/inputnumber/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/inputnumber/lib", + "sourceRoot": "src/inputnumber/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/inputnumber"], + "options": { + "project": "src/inputnumber/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/inputnumber"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js inputnumber" + }, + { + "command": "ng default-build inputnumber" + }, + { + "command": "node build/clear-default-theme.js inputnumber" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/inputnumber && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build inputnumber && ng pack inputnumber && node build/publish.js inputnumber --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/inputnumber/lib/src/TiInputNumberDirective.ts b/src/inputnumber/lib/src/TiInputNumberDirective.ts new file mode 100644 index 0000000..2e107d7 --- /dev/null +++ b/src/inputnumber/lib/src/TiInputNumberDirective.ts @@ -0,0 +1,273 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { DefaultValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Directive, ElementRef, forwardRef, Input, Renderer2, Inject } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +import { TiLocaleFormat } from '@opentiny/ng-locale'; +import { Util } from '@opentiny/ng-utils'; +/** + * TiNumber 数字输入框指令 + * + * 该指令主要作用于输入框上,限制只能输入数字。用户可以通过设置 数字保留精度、是否国际化 来设置数字显示格式 + * + * 输入框处于焦点状态时,输入框中数字标准化显示。失去焦点时,根据用户配置是否支持国际化进行格式化显示 + * + * 目前JS可以解析的范围是[-2^53, 2^53],即16位数字。当超过16位整数时,此时数字范围已经超过JS解析方位,不能精确表示。 + * + */ +@Directive({ + // 指令元数据 注意:该指令和数字校验指令重复,后期开发tiny4需要修改组件名 + selector: '[tiNumber]', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TiInputNumberDirective), + multi: true + } + ], + host: { + '(input)': 'handleInput($event.target.value)', + '(focus)': 'focusFn();', + '(blur)': 'blurFn()' + } +}) +// The default ControlValueAccessor for writing a value and listening to changes on input elements. +// The accessor is used by the FormControlDirective, FormControlName, and NgModel directives. +export class TiInputNumberDirective extends DefaultValueAccessor { + /** + * 是否开启国际化 + */ + @Input() localeable: boolean = true; + /** + * 小数保留位数。使用 n +'数字' 形式,例如:'n4',代表保留4位小数。spinner 保持一致。 + * + * 不设置时,10.0.1 版本后小数保留位数最少 0 位,最多 3 位;10.0.0 版本前小数保留位数为 3 位 + */ + @Input() format: string; + private numberFormat: string = '1.0-3'; + private oldInputValue: string = ''; // 没有千位分隔符的input中的值 + private onChangeFn: (_: any) => void; + private element: HTMLInputElement; + private oldModel: number; + constructor(private renderer: Renderer2, private elementRef: ElementRef, @Inject(DOCUMENT) private document) { + super(renderer, elementRef, true); + this.element = this.elementRef.nativeElement; + } + + ngOnInit(): void { + if (!TiLocaleFormat.isInvalidFormat(this.format)) { + const precision: number = parseInt(this.format.slice(1), 10); + this.numberFormat = '1.' + precision + '-' + precision; + } + } + + // 检测当前值是否合法 如果合法 返回true;否则返回false. + private isValidInput(value: string): boolean { + const decimalSep: string = this.localeable ? TiLocaleFormat.getNumberSymbol('Decimal') : '.'; // 当前局点小数点的形式 + let regex: any; // regex是正则表达式,不明其类型,所以 + if (decimalSep === ',') { + regex = /^\-{0,1}\d{0,},{0,1}\d{0,}$/; + } else if (decimalSep === '.') { + regex = /^\-{0,1}\d{0,}\.{0,1}\d{0,}$/; + } + + return regex.test(value); + } + /** + * 功能描述:当该值合法,直接返回. 如果不合法,则返回之前保存的有效值。 + * @memberOf TiInputNumberDirective + */ + private getCorrectValue(value: string): string { + if (!this.isValidInput(value)) { + return this.oldInputValue; + } + + return value; + } + // 根据精度和是否支持国际化,格式化数字 + private formatValue(value: number): string { + if (Number.isNaN(value)) { + return; + } + // https://angular.cn/api/common/DecimalPipe + // digitsInfo: {minIntegerDigits}.{minFractionDigits}-{maxFractionDigits} + // minIntegerDigits: 小数点前最小位数 默认为1 + // minFractionDigits: 小数点后最小位数 默认0 + // maxFractionDigits: 小数点后最大位数 默认3 + const localeValue: string = TiLocaleFormat.formatNumber(value, this.numberFormat); + if (this.localeable) { + return this.document.activeElement === this.element ? TiLocaleFormat.deleteGroupSep(localeValue) : localeValue; + } else { + // 不支持国际化时,也可利用TiLocaleFormat.formatNumber精度处理能力 + return TiLocaleFormat.parseNumber(localeValue).toString(); + } + } + // 将国际化数字转换为标准化数字 + private parseValue(value: string): number { + return this.localeable ? TiLocaleFormat.parseNumber(value) : parseFloat(value); + } + // 将国际化数字的千位分隔符去掉(输入框聚焦状态需要) + private deleteGroupSepValue(formatValue: string): string { + return this.localeable ? TiLocaleFormat.deleteGroupSep(formatValue) : formatValue; + } + /** + * @ignore + * 实现继承来自父类的方法 + * Sets the "value" property on the input element. + * model->view + */ + writeValue(value: any): void { + // writeValue()方法会执行两遍。第一遍为null, 第二遍为用户配置的数据。 + if (value === null) { + return; + } + + // 由于10.0.0及之前版本中开发者只能给ngModel传入空字符串时才能清空值,但是该组件ngModel应该是传入number类型,所以要清空时应该设置undefined。 + // 10.0.1版本开始进行纠正,为了兼容旧版本,设置空字符串时也能清空。 + if (value === undefined || value === '') { + super.writeValue(''); + this.oldInputValue = ''; + this.oldModel = undefined; + return; + } + + let normalValue: number; + if (Number.isNaN(parseFloat(value))) { + if (this.oldModel === undefined) { + super.writeValue(''); + this.oldInputValue = ''; + } else { + const formatValue: string = this.formatValue(this.oldModel); + super.writeValue(formatValue); + this.oldInputValue = this.deleteGroupSepValue(formatValue); + } + normalValue = this.oldModel; + } else { + const formatValue: string = this.formatValue(value); + super.writeValue(formatValue); + this.oldInputValue = this.deleteGroupSepValue(formatValue); + normalValue = this.parseValue(formatValue); + } + + if (normalValue !== value) { + if (Util.isUndefined(this.onChangeFn)) { + // 在reactive-form中使用,初始化赋值调用writeValue时, + // 此时registerOnChange还未被调用,onChangeFn还未被赋值, + // 所以要使用setTimeout等onChangeFn被赋值后再调用 + setTimeout(() => { + this.changeModel(normalValue); + }, 0); + } else { + this.changeModel(normalValue); + } + } else { + this.oldModel = normalValue; + } + } + /** + * @ignore + * Registers a function called when the control value changes + * @param fn The callback function + * 注册当控件接收到change事件之后,调用的函数fn + * viewValue和model value值的同步 + */ + registerOnChange(fn: (value: any) => void): void { + this.onChangeFn = fn; + } + + /** + * @ignore + * view -> model + * 非法字符不能输入。如果是合法则更新,如果是非法,则设置为oldInputValue... + */ + public handleInput(value: string): void { + // IE9 cut delete backspace下支持这三种方式引起的改变。 + // text组件针对这三种方式做了兼容性处理,并且触发input事件 + // 所以 inputnumber无需再做兼容性处理 + this.parser(value); + } + + private parser(value: string): void { + const validationValue: string = this.getCorrectValue(value); + if (!this.isValidInput(value)) { + let cursorPos: number = this.element.selectionStart; + const diff: number = value.length - validationValue.length; + cursorPos = cursorPos - diff; + this.renderer.setProperty(this.element, 'value', this.oldInputValue); // Sets the "value" property on the input element. + this.element.setSelectionRange(cursorPos, cursorPos); + } else { + this.oldInputValue = validationValue; + const parseValue: number = this.parseValue(validationValue); + this.changeModel(Number.isNaN(parseValue) ? undefined : parseValue); + } + } + + /** + * @ignore + * 得到焦点数据标准化处理 + * @memberOf TiInputNumberDirective + * renderer.setProperty: Implement this callback to set the value of a property of an element in the DOM. + */ + public focusFn(): void { + if (this.element.value === '') { + return; + } + // 为了得到各个浏览器下正确的光标位置,所以添加延时处理 + if (this.localeable) { + setTimeout(() => { + const start: number = this.element.selectionStart; + const end: number = this.element.selectionEnd; + if (end - start === this.element.value.length) { + this.renderer.setProperty(this.element, 'value', TiLocaleFormat.deleteGroupSep(this.element.value)); + this.element.setSelectionRange(0, this.element.value.length); + } else { + const groupSep: string = TiLocaleFormat.getNumberSymbol('Group'); + const old: string = this.element.value; + const sub: string = old.substr(0, start); + const groupSepNum: number = sub.split(groupSep).length - 1; + + this.renderer.setProperty(this.element, 'value', TiLocaleFormat.deleteGroupSep(this.element.value)); + this.element.setSelectionRange(start - groupSepNum, start - groupSepNum); + } + }, 0); + } + } + + /** + * @ignore + * 失去焦点数字国际化处理 + * @memberOf TiInputNumberDirective + */ + public blurFn(): void { + const value: number = this.parseValue(this.element.value); + if (Number.isNaN(value)) { + this.renderer.setProperty(this.element, 'value', ''); + this.oldInputValue = ''; + return; + } + const formatValue: string = this.formatValue(value); + this.renderer.setProperty(this.element, 'value', formatValue); + this.oldInputValue = this.deleteGroupSepValue(formatValue); + const normalValue: number = this.parseValue(formatValue); + if (value !== normalValue) { + this.changeModel(normalValue); + } + } + + private changeModel(model: number): void { + if (model !== this.oldModel) { + this.onChangeFn(model); + this.oldModel = model; + } + } +} diff --git a/src/inputnumber/lib/src/TiInputNumberModule.ts b/src/inputnumber/lib/src/TiInputNumberModule.ts new file mode 100644 index 0000000..0bf105d --- /dev/null +++ b/src/inputnumber/lib/src/TiInputNumberModule.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiInputNumberDirective } from './TiInputNumberDirective'; + +@NgModule({ + imports: [CommonModule], + exports: [TiInputNumberDirective], + declarations: [TiInputNumberDirective] +}) +export class TiInputNumberModule {} +export { TiInputNumberDirective } from './TiInputNumberDirective'; diff --git a/src/intro/demo/karma.conf.js b/src/intro/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/intro/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/intro/demo/project.json b/src/intro/demo/project.json new file mode 100644 index 0000000..19336a9 --- /dev/null +++ b/src/intro/demo/project.json @@ -0,0 +1,98 @@ +{ + "projectType": "application", + "root": "src/intro/demo", + "sourceRoot": "src/intro/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/intro", + "index": "src/intro/demo/src/index.html", + "main": "src/intro/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/intro/demo/tsconfig.app.json", + "assets": [ + "src/intro/demo/src/favicon.ico", + "src/intro/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/food", + "output": "/assets/food/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "intro-demo:build:production" + }, + "development": { + "browserTarget": "intro-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js intro" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/intro/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/intro/demo/tsconfig.spec.json", + "karmaConfig": "src/intro/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/intro/demo/src/app/AppComponent.ts b/src/intro/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/intro/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/intro/demo/src/app/AppModule.ts b/src/intro/demo/src/app/AppModule.ts new file mode 100644 index 0000000..c65ff5c --- /dev/null +++ b/src/intro/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { IntroTestModule } from './intro/IntroTestModule'; + +@NgModule({ + imports: [ + IntroTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/intro/demo/src/app/IndexComponent.ts b/src/intro/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..2c30ca0 --- /dev/null +++ b/src/intro/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { IntroTestModule } from './intro/IntroTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = IntroTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/intro/demo/src/app/app.html b/src/intro/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/intro/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/intro/demo/src/app/intro/IntroBasicComponent.ts b/src/intro/demo/src/app/intro/IntroBasicComponent.ts new file mode 100644 index 0000000..8e0fc86 --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroBasicComponent.ts @@ -0,0 +1,66 @@ +import { AfterViewInit, Component, ElementRef, TemplateRef, ViewChild } from '@angular/core'; +import { TiIntroRef, TiIntroService, TiIntroStep } from '@opentiny/ng'; + +@Component({ + templateUrl: './intro-basic.html' +}) +export class IntroBasicComponent implements AfterViewInit { + @ViewChild('introStep1', { static: true }) introStep1: ElementRef; + @ViewChild('introStep2', { static: true }) introStep2: ElementRef; + @ViewChild('introStep3', { static: true }) introStep3: ElementRef; + @ViewChild('introStep4', { static: true }) introStep4: ElementRef; + + @ViewChild('step3Tem', { static: true }) public step3Tem: TemplateRef; + + constructor(private TiIntro: TiIntroService) {} + + intro: TiIntroRef; + + ngAfterViewInit(): void { + const steps: Array = [ + // 第一步没有指定element,则以弹窗的形式呈现 + { + step: 0, + title: '总览页标题', + content: '总览页内容' + }, + { + element: this.introStep1.nativeElement, + step: 1, + title: '第一条标题', + content: '可通过 position 属性设置提示的位置:此处设置为bottom-left', + position: 'bottom-left' + }, + { + element: this.introStep2.nativeElement, + step: 2, + title: '第二条标题', + content: '可通过 shape 属性设置高亮区域的形状: 此处设置为圆形', + shape: 'circle' + }, + { + element: this.introStep3.nativeElement, + step: 3, + title: '第三条标题', + content: this.step3Tem, + position: 'left' + }, + { + element: this.introStep4.nativeElement, + step: 4, + title: '第四条标题', + content: '第四条内容' + } + ]; + + this.intro = this.TiIntro.create({ + id: 'intro_id', + steps, + skipable: true // 可配置是否显示跳过按钮 + }); + } + + start(): void { + this.intro.start(); + } +} diff --git a/src/intro/demo/src/app/intro/IntroEventComponent.ts b/src/intro/demo/src/app/intro/IntroEventComponent.ts new file mode 100644 index 0000000..bc1a279 --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroEventComponent.ts @@ -0,0 +1,66 @@ +import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; +import { TiIntroRef, TiIntroService, TiIntroStep } from '@opentiny/ng'; + +@Component({ + templateUrl: './intro-event.html' +}) +export class IntroEventComponent implements AfterViewInit { + @ViewChild('introStep1', { static: true }) introStep1: ElementRef; + @ViewChild('introStep2', { static: true }) introStep2: ElementRef; + @ViewChild('introStep3', { static: true }) introStep3: ElementRef; + @ViewChild('introStep4', { static: true }) introStep4: ElementRef; + + constructor(private TiIntro: TiIntroService) {} + + intro: TiIntroRef; + myLogs: Array = []; + + ngAfterViewInit(): void { + const steps: Array = [ + { + step: 0, + title: '第一条标题', + content: '第一条内容' + }, + { + element: this.introStep2.nativeElement, + step: 1, + title: '第二条标题', + content: '第二条内容' + }, + { + element: this.introStep3.nativeElement, + step: 2, + title: '第三条标题', + content: '第三条内容', + position: 'left' + }, + { + element: this.introStep4.nativeElement, + step: 3, + title: '第四条标题', + content: '第四条内容' + } + ]; + + this.intro = this.TiIntro.create({ + steps, + finishButtonText: '马上开始', + beforeStep: (introRef: TiIntroRef, currentNumber: number): void => { + this.myLogs = [...this.myLogs, `beforeStep:即将进入新手引导的${currentNumber}步`]; + introRef.proceed(); + }, + onFinish: (): void => { + this.myLogs = [...this.myLogs, 'onFinish:看完所有引导信息']; + }, + onExit: (): void => { + this.myLogs = [...this.myLogs, 'onExit:退出引导']; + }, + skipable: true + }); + } + + start(): void { + this.intro.start(); + } +} diff --git a/src/intro/demo/src/app/intro/IntroModalComponent.ts b/src/intro/demo/src/app/intro/IntroModalComponent.ts new file mode 100644 index 0000000..f66b718 --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroModalComponent.ts @@ -0,0 +1,54 @@ +import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; +import { TiIntroRef, TiIntroService, TiIntroStep } from '@opentiny/ng'; + +@Component({ + templateUrl: './intro-modal.html' +}) +export class IntroModalComponent implements AfterViewInit { + @ViewChild('introStep1', { static: true }) introStep1: ElementRef; + @ViewChild('introStep2', { static: true }) introStep2: ElementRef; + @ViewChild('introStep3', { static: true }) introStep3: ElementRef; + + constructor(private TiIntro: TiIntroService) {} + intro: TiIntroRef; + + ngAfterViewInit(): void { + const steps: Array = [ + { + step: 0, + title: '总览页标题', + content: '总览页内容' + }, + { + element: this.introStep1.nativeElement, + step: 1, + title: '第一条标题', + content: '第一条内容', + position: 'bottom-left' + }, + { + element: this.introStep2.nativeElement, + step: 2, + title: '第二条标题', + content: '第二条内容' + }, + { + element: this.introStep3.nativeElement, + step: 3, + title: '第三条', + content: '第三条内容', + position: 'left' + } + ]; + + this.intro = this.TiIntro.create({ + steps + }); + + this.intro.start(); + } + + start(): void { + this.intro.start(); + } +} diff --git a/src/intro/demo/src/app/intro/IntroSkipableComponent.ts b/src/intro/demo/src/app/intro/IntroSkipableComponent.ts new file mode 100644 index 0000000..1240694 --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroSkipableComponent.ts @@ -0,0 +1,57 @@ +import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; +import { TiIntroRef, TiIntroService, TiIntroStep } from '@opentiny/ng'; + +@Component({ + templateUrl: './intro-skipable.html' +}) +export class IntroSkipableComponent implements AfterViewInit { + @ViewChild('introStep1', { static: true }) introStep1: ElementRef; + @ViewChild('introStep2', { static: true }) introStep2: ElementRef; + @ViewChild('introStep3', { static: true }) introStep3: ElementRef; + constructor(private TiIntro: TiIntroService) {} + intro: TiIntroRef; + ngAfterViewInit(): void { + const steps: Array = [ + { + element: this.introStep1.nativeElement, + step: 0, + title: '第一条标题', + content: '第一条内容', + position: 'bottom-left' + }, + { + element: this.introStep2.nativeElement, + step: 1, + title: '第二条标题', + content: '第二条内容', + position: 'bottom-left' + }, + { + element: this.introStep3.nativeElement, + step: 2, + title: '第三条标题', + content: '第三条内容', + position: 'left' + } + ]; + + this.intro = this.TiIntro.create({ + id: 'intro_id', + steps, + skipable: true + }); + + /** + * 此处使用延时,是因为组件示例的component和展示源码的tabs组件是兄弟元素, + * 组件ViewInit被调用时,tabs没有渲染完成,页面高度没有完全被撑开,遮罩层的高度不足 + * 实际业务不需要setTimeout + */ + setTimeout(() => { + this.intro.start(); + }, 0); + } + + start(): void { + this.intro.start(); + } +} diff --git a/src/intro/demo/src/app/intro/IntroTemplateComponent.ts b/src/intro/demo/src/app/intro/IntroTemplateComponent.ts new file mode 100644 index 0000000..c4fdfd8 --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroTemplateComponent.ts @@ -0,0 +1,58 @@ +import { AfterViewInit, Component, ElementRef, TemplateRef, ViewChild } from '@angular/core'; +import { TiIntroRef, TiIntroService, TiIntroStep } from '@opentiny/ng'; + +@Component({ + templateUrl: './intro-template.html' +}) +export class IntroTemplateComponent implements AfterViewInit { + @ViewChild('introStep1', { static: true }) introStep1: ElementRef; + @ViewChild('introStep2', { static: true }) introStep2: ElementRef; + @ViewChild('introStep3', { static: true }) introStep3: ElementRef; + + @ViewChild('introTem', { static: true }) introTem: TemplateRef; + @ViewChild('modalTemp') modalTemp: TemplateRef; + constructor(private TiIntro: TiIntroService) {} + intro: TiIntroRef; + isNotShowagain: boolean = false; + ngAfterViewInit(): void { + const steps: Array = [ + { + step: 0, + title: '总览页标题', + content: this.modalTemp + }, + { + element: this.introStep1.nativeElement, + step: 1, + title: '第一条标题', + content: '第一条内容', + position: 'bottom-left' + }, + { + element: this.introStep2.nativeElement, + step: 2, + title: '第二条标题', + content: this.introTem + }, + { + element: this.introStep3.nativeElement, + step: 3, + title: '第三条', + content: '第三条内容', + position: 'left' + } + ]; + + this.intro = this.TiIntro.create({ + steps + }); + + this.intro.start(); + } + + start(): void { + if (!this.isNotShowagain) { + this.intro.start(); + } + } +} diff --git a/src/intro/demo/src/app/intro/IntroTestModule.ts b/src/intro/demo/src/app/intro/IntroTestModule.ts new file mode 100644 index 0000000..3af8e9d --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroTestModule.ts @@ -0,0 +1,67 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiCheckboxModule, TiIconModule, TiIntroServiceModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { IntroBasicComponent } from './IntroBasicComponent'; +import { IntroTipComponent } from './IntroTipComponent'; +import { IntroEventComponent } from './IntroEventComponent'; +import { IntroModalComponent } from './IntroModalComponent'; +import { IntroTemplateComponent } from './IntroTemplateComponent'; +import { IntroTiscrollComponent } from './IntroTiscrollComponent'; +import { IntroSkipableComponent } from './IntroSkipableComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiButtonModule, + TiCheckboxModule, + TiIntroServiceModule, + TiIconModule, + DemoLogModule, + RouterModule.forChild(IntroTestModule.ROUTES) + ], + declarations: [ + IntroBasicComponent, + IntroTipComponent, + IntroEventComponent, + IntroModalComponent, + IntroTemplateComponent, + IntroTiscrollComponent, + IntroSkipableComponent + ] +}) +export class IntroTestModule { + static readonly LINKS: Array = [{ href: 'components/TiIntroComponent.html', label: 'Intro' }]; + static readonly ROUTES: Routes = [ + { + path: 'intro/intro-basic', + component: IntroBasicComponent + }, + { + path: 'intro/intro-tip', + component: IntroTipComponent + }, + { + path: 'intro/intro-modal', + component: IntroModalComponent + }, + { + path: 'intro/intro-template', + component: IntroTemplateComponent + }, + { + path: 'intro/intro-skipable', + component: IntroSkipableComponent + }, + { + path: 'intro/intro-event', + component: IntroEventComponent + }, + { path: 'intro/intro-tiscroll', component: IntroTiscrollComponent } + ]; +} diff --git a/src/intro/demo/src/app/intro/IntroTipComponent.ts b/src/intro/demo/src/app/intro/IntroTipComponent.ts new file mode 100644 index 0000000..c54dbb1 --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroTipComponent.ts @@ -0,0 +1,74 @@ +import { AfterViewInit, Component, ElementRef, TemplateRef, ViewChild } from '@angular/core'; +import { TiIntroRef, TiIntroService, TiIntroStep } from '@opentiny/ng'; + +@Component({ + templateUrl: './intro-tip.html' +}) +export class IntroTipComponent implements AfterViewInit { + @ViewChild('introStep1', { static: true }) introStep1: ElementRef; + @ViewChild('introStep2', { static: true }) introStep2: ElementRef; + @ViewChild('introStep3', { static: true }) introStep3: ElementRef; + @ViewChild('introStep4', { static: true }) introStep4: ElementRef; + @ViewChild('introStep5', { static: true }) introStep5: ElementRef; + @ViewChild('finish', { static: true }) finish: ElementRef; + + constructor(private TiIntro: TiIntroService) {} + intro: TiIntroRef; + ngAfterViewInit(): void { + const steps: Array = [ + { + element: this.introStep1.nativeElement, + step: 0, + title: '第一条标题', + content: '第一条内容', + position: 'bottom-left' + }, + { + element: this.introStep2.nativeElement, + step: 1, + title: '第二条标题', + content: '第二条内容' + }, + { + element: this.introStep3.nativeElement, + step: 2, + title: '第三条', + content: '第三条内容', + position: 'left' + }, + { + element: this.introStep4.nativeElement, + step: 3, + content: '第四条内容,以tip形式显示' + }, + { + element: this.introStep5.nativeElement, + step: 3, + shape: 'circle', + content: '第五条内容,以tip形式显示' + }, + { + element: this.finish.nativeElement, + step: 3, + isAction: true // 标识为结束该步的操作按钮 + } + ]; + + this.intro = this.TiIntro.create({ + steps + }); + + /** + * 此处使用延时,是因为组件示例的component和展示源码的tabs组件是兄弟元素, + * 组件ViewInit被调用时,tabs没有渲染完成,页面高度没有完全被撑开,遮罩层的高度不足 + * 实际业务不需要setTimeout + */ + setTimeout(() => { + this.intro.start(); + }, 0); + } + + start(): void { + this.intro.start(); + } +} diff --git a/src/intro/demo/src/app/intro/IntroTiscrollComponent.ts b/src/intro/demo/src/app/intro/IntroTiscrollComponent.ts new file mode 100644 index 0000000..29f58f1 --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroTiscrollComponent.ts @@ -0,0 +1,37 @@ +import { AfterViewInit, Component, ElementRef, TemplateRef, ViewChild } from '@angular/core'; +import { TiIntroRef, TiIntroService, TiIntroStep, TiMessageService } from '@opentiny/ng'; + +@Component({ + templateUrl: './intro-tiscroll.html' +}) +export class IntroTiscrollComponent implements AfterViewInit { + @ViewChild('introStep1', { static: true }) introStep1: ElementRef; + @ViewChild('introStep2', { static: true }) introStep2: ElementRef; + constructor(private TiIntro: TiIntroService, tiMessage: TiMessageService) {} + intro: TiIntroRef; + ngAfterViewInit(): void { + const steps: Array = [ + { + element: this.introStep1.nativeElement, + step: 0, + title: '第一条标题', + content: '第一条内容', + position: 'bottom-left' + }, + { + element: this.introStep2.nativeElement, + step: 1, + title: '第二条标题', + content: '第二条内容' + } + ]; + + this.intro = this.TiIntro.create({ + id: 'intro_id', + steps + }); + } + start(): void { + this.intro.start(); + } +} diff --git a/src/intro/demo/src/app/intro/intro-basic.html b/src/intro/demo/src/app/intro/intro-basic.html new file mode 100644 index 0000000..d760eb8 --- /dev/null +++ b/src/intro/demo/src/app/intro/intro-basic.html @@ -0,0 +1,10 @@ + + +
+ 1.位置 + 2.形状 + 3.模板 + 4:最后一步 +
+ + 自定义模板 diff --git a/src/intro/demo/src/app/intro/intro-event.html b/src/intro/demo/src/app/intro/intro-event.html new file mode 100644 index 0000000..980a3c0 --- /dev/null +++ b/src/intro/demo/src/app/intro/intro-event.html @@ -0,0 +1,10 @@ + + +
+ 第一步 + 第二步 + 第三步 + 第四步 +
+ + diff --git a/src/intro/demo/src/app/intro/intro-modal.html b/src/intro/demo/src/app/intro/intro-modal.html new file mode 100644 index 0000000..f39bfcc --- /dev/null +++ b/src/intro/demo/src/app/intro/intro-modal.html @@ -0,0 +1,22 @@ +

描述

+

第一步是总览页

+

示例

+ + +
+ 1号新功能 + 2号新功能 + 3号新功能 +
+ diff --git a/src/intro/demo/src/app/intro/intro-skipable.html b/src/intro/demo/src/app/intro/intro-skipable.html new file mode 100644 index 0000000..994f802 --- /dev/null +++ b/src/intro/demo/src/app/intro/intro-skipable.html @@ -0,0 +1,24 @@ +

描述

+

是否显示跳过按钮(可跳过/退出引导),设置skipable属性

+

示例

+ + +
+ 1号新功能 + 2号新功能 + 3号新功能 +
+ diff --git a/src/intro/demo/src/app/intro/intro-template.html b/src/intro/demo/src/app/intro/intro-template.html new file mode 100644 index 0000000..3fd18b2 --- /dev/null +++ b/src/intro/demo/src/app/intro/intro-template.html @@ -0,0 +1,29 @@ +

描述

+

自定义content内容的功能

+

示例

+ +下次是否显示引导信息:{{!isNotShowagain}} + +
+ 1号新功能 + 2号新功能 + 3号新功能 +
+ + +
+ +
+ + + diff --git a/src/intro/demo/src/app/intro/intro-tip.html b/src/intro/demo/src/app/intro/intro-tip.html new file mode 100644 index 0000000..07113b9 --- /dev/null +++ b/src/intro/demo/src/app/intro/intro-tip.html @@ -0,0 +1,45 @@ +

描述

+

第一步是总览页,并且同时显示多个引导信息

+

只支持在最后一步显示多个引导信息,且是以tip的形式呈现,需要传入关闭引导的元素

+

示例

+ + + +
+ 1号新功能 + 2号新功能(固定定位) + 3号新功能(transform定位) + 4号新功能(不在视口) + 5号 +
+ + + + diff --git a/src/intro/demo/src/app/intro/intro-tiscroll.html b/src/intro/demo/src/app/intro/intro-tiscroll.html new file mode 100644 index 0000000..12d38da --- /dev/null +++ b/src/intro/demo/src/app/intro/intro-tiscroll.html @@ -0,0 +1,27 @@ +

描述

+

在控制台输入触发tiScroll的代码,intro不能消失

+

示例

+ + +

控制台触发tiScroll事件

+

+ const event = document.createEvent('HTMLEvents');
+ event.initEvent('tiScroll')
+ document.dispatchEvent(event);
+

+ +
+ 1号新功能 + 2号新功能 +
+ diff --git a/src/intro/demo/src/app/intro/webdoc/intro-demos.js b/src/intro/demo/src/app/intro/webdoc/intro-demos.js new file mode 100644 index 0000000..e8ba0e8 --- /dev/null +++ b/src/intro/demo/src/app/intro/webdoc/intro-demos.js @@ -0,0 +1,47 @@ +export default { + column: '2', + demos: [ + { + demoId: 'intro-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

通过create方法创建 intro 实例。

', + 'en-US': '

basic

', + }, + apis: [ + 'TiIntroService.methods.create', + 'TiIntroStep.properties.element', + 'TiIntroStep.properties.step', + 'TiIntroStep.properties.shape', + 'TiIntroStep.properties.title', + 'TiIntroStep.properties.content', + 'TiIntroStep.properties.position', + 'TiIntroStep.properties.isAction', + ], + }, + { + demoId: 'intro-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event', + }, + desc: { + 'zh-CN': + '

当每一步打开前的时候触发beforeStep事件,当完成引导的时候触发onFinish事件,当退出引导的时候触发onExit事件。

', + 'en-US': '

event

', + }, + apis: [ + 'TiIntroConfig.properties.steps', + 'TiIntroConfig.properties.skipable', + 'TiIntroConfig.methods.beforeStep', + 'TiIntroConfig.methods.onExit', + 'TiIntroConfig.methods.onFinish', + 'TiIntroConfig.properties.finishButtonText', + 'TiIntroConfig.properties.id', + ], + }, + ], +}; diff --git a/src/intro/demo/src/app/intro/webdoc/intro.cn.md b/src/intro/demo/src/app/intro/webdoc/intro.cn.md new file mode 100644 index 0000000..e7670ce --- /dev/null +++ b/src/intro/demo/src/app/intro/webdoc/intro.cn.md @@ -0,0 +1,30 @@ +--- +title: Intro 新手引导 +--- +# Intro 新手引导 + +
+ +Intro 是提供服务中内容(入口)新增和变动等指引提醒的组件。   + +```typescript +import { TiIntroServiceModule } from '@opentiny/ng'; +``` + +需要在项目中(建议在根模块)中导入`BrowserAnimationsModule`。 + +```typescript +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +``` + +
+ +
+ +Intro 是提供服务中内容(入口)新增和变动等指引提醒的组件。   + +```typescript +import { TiIntroServiceModule } from '@opentiny/ng'; +``` + +
diff --git a/src/intro/demo/src/app/intro/webdoc/intro.en.md b/src/intro/demo/src/app/intro/webdoc/intro.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/intro/demo/src/app/intro/webdoc/intro.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/intro/demo/src/favicon.ico b/src/intro/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/intro/demo/src/index.html b/src/intro/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/intro/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/intro/demo/src/main.ts b/src/intro/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/intro/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/intro/demo/test.ts b/src/intro/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/intro/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/intro/demo/tsconfig.app.json b/src/intro/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/intro/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/intro/demo/tsconfig.spec.json b/src/intro/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/intro/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/intro/lib/index.ts b/src/intro/lib/index.ts new file mode 100644 index 0000000..060772c --- /dev/null +++ b/src/intro/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiIntroModule'; +export * from './src/TiIntroService'; diff --git a/src/intro/lib/ng-package.json b/src/intro/lib/ng-package.json new file mode 100644 index 0000000..ddd4a1f --- /dev/null +++ b/src/intro/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/intro", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/intro/lib/package.json b/src/intro/lib/package.json new file mode 100644 index 0000000..54cd9c8 --- /dev/null +++ b/src/intro/lib/package.json @@ -0,0 +1,17 @@ +{ + "name": "@opentiny/ng-intro", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-modal": "~1.0.0-beta.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/intro/lib/project.json b/src/intro/lib/project.json new file mode 100644 index 0000000..82a424b --- /dev/null +++ b/src/intro/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/intro/lib", + "sourceRoot": "src/intro/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/intro"], + "options": { + "project": "src/intro/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/intro"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js intro" + }, + { + "command": "ng default-build intro" + }, + { + "command": "node build/clear-default-theme.js intro" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/intro && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build intro && ng pack intro && node build/publish.js intro --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/intro/lib/src/TiIntroInterface.ts b/src/intro/lib/src/TiIntroInterface.ts new file mode 100644 index 0000000..1216692 --- /dev/null +++ b/src/intro/lib/src/TiIntroInterface.ts @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiPositionType } from '@opentiny/ng-utils'; +import { TemplateRef } from '@angular/core'; + +export type TiIntroShape = 'rect' | 'circle'; + +/** + * 某一步的intro信息对象 + */ +export interface TiIntroStep { + /** + * 被引导元素,第一步没有指定 element 则以弹窗的形式呈现 + */ + element?: any; + /** + * 必选,第几步引导,从 0 开始 + */ + step: number; + /** + * 元素被引导时高亮区域的形状 + */ + shape?: TiIntroShape; + /** + * 引导信息标题 + */ + title?: string; + /** + * 引导信息内容 + */ + content?: string | TemplateRef; + /** + * 引导信息方向 + */ + position?: TiPositionType; + /** + * @ignore + * 是否为关闭多个 tip 的操作元素 + */ + isAction?: boolean; +} + +export interface TiIntroConfig { + /** + * 组件整体 id + */ + id?: string; + /** + * 必选,引导数据集 + */ + steps: Array; + /** + * 最后一步引导信息的重要按钮文本 + */ + finishButtonText?: string; + /** + * 是否显示跳过按钮 + */ + skipable?: boolean; + /** + * 每一步引导前触发的回调 + */ + beforeStep?(TiIntroRef: TiIntroRef, currentNumber?: number): void; + /** + * 完成引导信息时触发的回调 + */ + onFinish?(): void; + /** + * 退出引导触发的回调 + */ + onExit?(): void; +} +export interface TiIntroRef { + /** + * 开始新手引导(intro)的方法 + * + * **函数类型:**() => void; + */ + start(): void; + /** + * 结束新手引导(intro)的方法 + * + * **函数类型:**() => void; + */ + end(): void; + /** + * 跳转至某一步的方法 + * + * **函数类型:**() => void; + */ + step(number: number): void; + /** + * 在beforeStep中调用introRef.proceed(),以便进入该步,否则不会进入 + * + * **函数类型:**() => void; + */ + proceed(): void; +} diff --git a/src/intro/lib/src/TiIntroModule.ts b/src/intro/lib/src/TiIntroModule.ts new file mode 100644 index 0000000..052268b --- /dev/null +++ b/src/intro/lib/src/TiIntroModule.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TiButtonModule } from '@opentiny/ng-button'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiModalModule } from '@opentiny/ng-modal'; +import { TiTipServiceModule } from '@opentiny/ng-tip'; +import { TiLocaleModule, TiLocale } from '@opentiny/ng-locale'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiIntromodalComponent } from './TiIntromodalComponent'; +import { TiIntrotipComponent } from './TiIntrotipComponent'; +import { locales } from './i18n'; + +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule, TiButtonModule, TiIconModule, TiLocaleModule, TiModalModule, TiTipServiceModule, TiOutlineModule], + declarations: [TiIntrotipComponent, TiIntromodalComponent], + entryComponents: [TiIntrotipComponent, TiIntromodalComponent] +}) +export class TiIntroServiceModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiIntroConfig, TiIntroRef, TiIntroStep, TiIntroShape } from './TiIntroInterface'; diff --git a/src/intro/lib/src/TiIntroService.ts b/src/intro/lib/src/TiIntroService.ts new file mode 100644 index 0000000..3c3ba42 --- /dev/null +++ b/src/intro/lib/src/TiIntroService.ts @@ -0,0 +1,456 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +import { TiIntroServiceModule } from './TiIntroModule'; +import { TiModalRef } from '@opentiny/ng-modal'; +import { TiModalService } from '@opentiny/ng-modal'; +import { TiTipService } from '@opentiny/ng-tip'; +import { TiTipRef } from '@opentiny/ng-tip'; +import { Position, TiKeymap, TiLog, TiPositionEventType, Util } from '@opentiny/ng-utils'; + +import { TiIntromodalComponent } from './TiIntromodalComponent'; +import { TiIntrotipComponent } from './TiIntrotipComponent'; +import { TiIntroConfig, TiIntroRef, TiIntroShape, TiIntroStep } from './TiIntroInterface'; + +/** + * 新手引导组件,适用于服务中内容(入口)新增和变动等指引提醒 + * + *

使用此组件时需要开发者在项目模块(建议在根模块)中引入BrowserAnimationsModule。 + * 这是因为此组件中使用了 TiModalService,TiModalService需要BrowserAnimationsModule + * (具体原因可以查看 [TiModalService]{@link ../injectables/TiModalService.html}), + * 但是 BrowserAnimationsModule 不能在懒加载模块被重复引入,所以需要开发者来引入BrowserAnimationsModule,保证其引入一次。

+ * + */ +@Injectable({ + providedIn: TiIntroServiceModule +}) +export class TiIntroService { + // 视觉规范调整:modal、halfmodal、intro的蒙层统一调整为1200;弹窗体、intro的气泡是1300 + private static readonly BACKDROP_Z_INDEX: number = 1200; + private static readonly TIP_Z_INDEX: number = 1300; + private render: Renderer2; + private unListenWindowResize: () => void; + private unListenWindowHashchange: () => void; + private closeMultiTipEleClickListener: () => void; + private unListenDocumentKeydown: () => void; + // 可聚焦元素 + private focusableElementsString: string = `a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), + button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), + textarea:not([disabled]):not([tabindex=\'-1\']), + iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]`; + constructor( + private tipService: TiTipService, + private modalService: TiModalService, + rendererFactory: RendererFactory2, + @Inject(DOCUMENT) private document + ) { + this.render = rendererFactory.createRenderer(null, null); + } + /** + * intro的公共配置,不是默认配置,不能修改 + */ + private commonConfig: any = { + trigger: 'manual', + theme: 'white', + maxWidth: '400px', + zIndex: TiIntroService.TIP_Z_INDEX, + registerVisibilityChangeEvent: false, + /** + * position在定位时注册了三个全局事件,intro都不需要,用空数组覆盖position中的事件类型数组 + */ + positionEventTypes: [] + }; + + private static clearCircle(x: number, y: number, radius: number, stepClear: number, ctx: any): void { + const calcWidth: number = radius - stepClear; + const calcHeight: number = Math.sqrt(radius * radius - calcWidth * calcWidth); + + const posX: number = x - calcWidth; + const posY: number = y - calcHeight; + + const widthX: number = calcWidth * 2; + const heightY: number = calcHeight * 2; + + if (stepClear < radius) { + ctx.clearRect(posX, posY, widthX, heightY); + const newStepClear: number = stepClear + 1; + TiIntroService.clearCircle(x, y, radius, newStepClear, ctx); + } + } + + private static scrollToViewport(element: any): void { + // 修复SSR错误:ERROR TypeError: element.getBoundingClientRect is not a function + if (typeof element.getBoundingClientRect !== 'function') { + return; + } + const layout: any = element.getBoundingClientRect(); + + const scrollTop: number = document.body.scrollTop || document.documentElement.scrollTop; + // 修复SSR报错:ERROR ReferenceError: window is not defined + if (typeof window === 'undefined') { + return; + } + if (layout.top < 0) { + // 上边界溢出屏幕 + window.scrollTo({ top: layout.top + scrollTop - 20 }); + } else if (layout.bottom > document.documentElement.clientHeight) { + // 下边界溢出屏幕 + window.scrollTo({ + top: scrollTop + layout.bottom + 20 - document.documentElement.clientHeight + }); + } + } + + /** + * 创建intro实例 + */ + public create(config: TiIntroConfig): TiIntroRef { + let introRef: TiIntroRef; // create方法创建并返回的intro实例 + const steps: Array = config.steps; + let introModalRef: TiModalRef; // modal实例,用于隐藏弹窗 + let introTipRef: TiTipRef; // 是生成的tip,有show & hide + let introMultiTipRefs: Array = []; // 多个tip场景下生成的tip实例数组 + let curStep: number = 0; // 在整个create方法中,标识当前步数 + let onResize: () => void; // 广义的resize事件,包括正常的resize和console UI顶部的弹出内容出现或隐藏 + let isAddConsoleDataOnChange: boolean = false; // 是否监听了 consoleDataService 的 OnChange 事件 + let consoleDataService: any; + const lastStep: number = steps[steps.length - 1].step; // 最后一TiIntroStep的step属性值,即总步数 + // 关闭多个tip的操作元素 + const closeMultiTipEle: Element = + steps.filter((item: TiIntroStep) => item.isAction).length > 0 + ? steps.filter((item: TiIntroStep) => item.isAction)[0].element + : undefined; + // 先隐藏closeMultiTipEle,并注册事件 + if (closeMultiTipEle) { + this.render.setStyle(closeMultiTipEle, 'display', 'none'); + } + this.unListenDocumentKeydown = this.render.listen(document, 'keydown', (event: KeyboardEvent): void => { + switch (event.which) { + case TiKeymap.KEY_TAB: // tab键用于处理在提示框内循环获取焦点 + this.clickTab(event); + break; + default: + break; + } + }); + + /** + * @param flag boolean true:完成intro引导;false:未完成intro引导,但是退出 + */ + const close: (flag: boolean) => void = (flag: boolean): void => { + if (introMultiTipRefs.length > 0) { + introMultiTipRefs.forEach((item: TiTipRef) => { + this.hidePreStep(item); + }); + introMultiTipRefs = []; + this.render.setStyle(closeMultiTipEle, 'display', 'none'); + } else { + this.hidePreStep(introTipRef); + } + + this.destroyBackdrop(); + + if (flag && typeof config.onFinish === 'function') { + config.onFinish(); + } else if (!flag && typeof config.onExit === 'function') { + config.onExit(); + } + if (this.unListenWindowResize) { + this.unListenWindowResize(); + this.unListenWindowResize = undefined; + } + if (this.unListenWindowHashchange) { + this.unListenWindowHashchange(); + this.unListenWindowHashchange = undefined; + } + if (this.closeMultiTipEleClickListener) { + this.closeMultiTipEleClickListener(); + } + if (this.unListenDocumentKeydown) { + this.unListenDocumentKeydown(); + this.unListenDocumentKeydown = undefined; + } + if (consoleDataService?.offChange) { + consoleDataService.offChange(onResize); + } + }; + + /** + * 无效步数条件: + * 1.不是有效数字 + * 2.等于currentNumber,等于0除外 + * 3.小于0 或 大于 totalNumber + * 使用curStep 和 lastStep,没有提为private方法 + */ + const isInvalidStepNum: (number: number) => boolean = (num: number): boolean => { + return isNaN(num) || (curStep === num && num !== 0) || num < 0 || num > lastStep; + }; + /** + * 即将进入某一步的引导 + * + * 需要在进入前做一些合法性判断,清除上一步信息等 + */ + const wantStep: (number: number) => void = (num: number): void => { + if (isInvalidStepNum(num)) { + TiLog.error('stepNumber is not valid'); + + return; + } + this.hidePreStep(introTipRef); + curStep = num; + // 如果是在console环境下,进入第一步时,onChange的回调函数传入onResize事件,会在console变化时,重新绘制intro和backdrop + if ((window).getConsoleContext && !isAddConsoleDataOnChange) { + consoleDataService = (window).getConsoleContext()?.get({ name: 'safearea' }); + // 头部高度变化会触发此事件 + if (consoleDataService?.onChange) { + consoleDataService.onChange(onResize); + isAddConsoleDataOnChange = true; + } + } + if (typeof config.beforeStep === 'function') { + config.beforeStep(introRef, num); + } else { + goStep(num); + } + // 延时为了保证遮罩dom已经绘制完毕。 + setTimeout((): void => { + // 在引导的时候,把焦点调整为遮罩层。为后续把焦点圈在提示框内做准备。 + const backdropEle: HTMLElement = document.querySelector('#ti3-intro-backdrop'); + if (backdropEle) { + backdropEle.focus(); + } + }, 100); + }; + + // 开始某一步的引导 + const goStep: (number: number) => void = (num: number): void => { + const currentSteps: Array = steps.filter((item: TiIntroStep) => item.step === num); + // curIntroStep curIntroStep + let currentStep: TiIntroStep; + if (currentSteps.length === 1) { + currentStep = currentSteps[0]; + } + // 先关闭之前的canvas + this.destroyBackdrop(); + /** + * 具体的引导步骤分为3种 + * 1.总览页: 一个正常弹窗,用modal实现 intromodal + * 2.分布页:带按钮的tip,用tipService实现 + * 3.提示: 没有按钮的普通tip,用tipService实现 + */ + // 第一种情况,弹窗 + if (currentStep && !currentStep.element) { + introModalRef = this.modalService.open(TiIntromodalComponent, { + id: 'ti3-intro-modal', + context: { + title: currentStep.title, + content: currentStep.content, + totalNumber: lastStep, + currentNumber: curStep, + finishButtonText: config.finishButtonText, + id: config.id + '_' + num, + close: (): void => { + introModalRef.close(); + }, + wantStep, + skipable: config.skipable + }, + dismiss(): void { + close(false); + }, + draggable: false + }); + + return; + } + // 如果有弹窗的话,关闭弹窗 + if (introModalRef) { + introModalRef._remove(); + introModalRef = undefined; + } + // 重新绘制canvas + this.drawBackdrop(); + + // 第二种情况,带按钮的tip + if (currentStep && currentStep.title) { + TiIntroService.scrollToViewport(currentStep.element); + introTipRef = this.tipService.create(currentStep.element, { + ...this.commonConfig, + position: currentStep.position + }); + introTipRef.show(TiIntrotipComponent, { + title: currentStep.title, + content: currentStep.content, + totalNumber: lastStep, + currentNumber: curStep, + finishButtonText: config.finishButtonText, + id: config.id + '_' + num, + close, + wantStep, + skipable: config.skipable + }); + + this.highlight(currentStep.element, currentStep.shape); + } + + // 第三种情况,普通tip,并且是多个,有一个是关闭按钮。和第二种情况是if else 的关系,重新列为一个if分支是为了让三种情况称为并列的if + if (currentSteps && currentSteps.length > 1) { + currentSteps.forEach((item: TiIntroStep): void => { + if (item.isAction) { + this.render.setStyle(item.element, 'display', 'block'); + this.closeMultiTipEleClickListener = this.render.listen(item.element, 'click', () => { + close(true); + }); + } else { + TiIntroService.scrollToViewport(item.element); + const introtipRef: TiTipRef = this.tipService.create(item.element, { + ...this.commonConfig, + position: item.position + }); + introtipRef.show(item.content); + introMultiTipRefs.push(introtipRef); + + this.highlight(item.element, item.shape); + } + }); + } + // 修复SSR报错:ERROR ReferenceError: window is not defined + if (!this.unListenWindowResize && typeof window !== 'undefined') { + this.unListenWindowResize = this.render.listen(window, 'resize', () => { + onResize(); + }); + } + if (!this.unListenWindowHashchange && typeof window !== 'undefined') { + this.unListenWindowHashchange = this.render.listen(window, 'hashchange', () => { + close(false); + }); + } + }; + // 定义resize事件 + onResize = () => { + this.hidePreStep(introTipRef); + this.destroyBackdrop(); + goStep(curStep); + }; + + // 对外暴露的intro实例 + introRef = { + start(): void { + wantStep(0); + }, + end(): void { + close(true); + }, + step: (number: number): void => { + wantStep(number); + }, + proceed(): void { + goStep(curStep); + } + }; + + return introRef; + } + /** + * @ignore + */ + public clickTab(event: KeyboardEvent): void { + const introModal: HTMLElement = document.querySelector('.ti3-intromodal-wrapper'); + const focusableElements: NodeList = introModal?.querySelectorAll(this.focusableElementsString); + + Util.focusInDialogOnTabchange(event, focusableElements); + } + private destroyBackdrop(): void { + const canvas: any = this.document.body.querySelector('#ti3-intro-backdrop'); + + if (canvas) { + // canvas.remove() IE报错;canvas.removeNode() Chrome报错;可以使用removeChild方法 + canvas.parentNode.removeChild(canvas); + } + } + + private hidePreStep(introTipRef: TiTipRef): void { + if (introTipRef) { + introTipRef.hide(); + } + } + + private drawBackdrop(): void { + if (typeof document === 'undefined') { + return; + } + const canvas: any = this.render.createElement('canvas'); + this.render.setStyle(canvas, 'z-index', TiIntroService.BACKDROP_Z_INDEX); + this.render.setStyle(canvas, 'position', 'absolute'); + this.render.setStyle(canvas, 'top', 0); + this.render.setStyle(canvas, 'left', 0); + this.render.setStyle(canvas, 'outline', 'none'); + this.render.setAttribute(canvas, 'tabindex', '0'); // 非表单元素想要使用focus()函数聚焦,需要设置tabindex属性。 + canvas.id = 'ti3-intro-backdrop'; + const ctx: any = canvas.getContext('2d'); + // _drawBackDrop(canvas, ctx) { + // 获取整个页面宽高,用于计算canvas宽高 + // 获取整个页面的宽高,包括滚动条部分及body margin等 + let browserWidth: number = this.document.documentElement.scrollWidth; + let browserHeight: number = this.document.documentElement.scrollHeight; + // 获取整个页面可视区域宽高,页面不出滚动条情况下,该值原则上应该是与上述browser宽高相等的 + // 但是由于浏览器的计算差异,可能存在不相等的情况 + const clientWidth: number = this.document.documentElement.clientWidth; + const clientHeight: number = this.document.documentElement.clientHeight; + + // 由于在IE下该宽高计算不精确,可能会导致页面本身无滚动条的情况下, + // 当使用该值设置canvas宽度后,出现滚动条,因此需要对宽高进行特殊处理 + // 根据多次试验经验,发现当实际页面实际宽度为小数时,clientWidth取值为去掉小数点后的值, + // scrollWidth则会自动+1,导致使用scrollWidth设置会出现滚动条, + // 因此,通过两者之差小于1的方式判断该情况下,取小值设置canvas宽度即可解决该问题 + if (browserWidth - clientWidth <= 1) { + browserWidth = clientWidth; + } + // 纵向情况下处理同上 + if (browserHeight - clientHeight <= 1) { + browserHeight = clientHeight; + } + + canvas.width = browserWidth; + canvas.height = browserHeight; + ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; + ctx.fillRect(0, 0, browserWidth, browserHeight); + + this.render.appendChild(this.document.body, canvas); + } + private highlight(element: any, shape: TiIntroShape): void { + const layout: any = Position.getHostEleLayout(element, element); + const canvas: any = this.document.body.querySelector('#ti3-intro-backdrop'); + // 修复SSR报错:ERROR TypeError: Cannot read property 'getContext' of undefined + if (!canvas) { + return; + } + const ctx: any = canvas.getContext('2d'); + const highlightLayout: any = { ...layout }; + if (layout.fixedAncestor) { + // 如果被高亮的元素是fixed定位,以元素相对于视口的位置为高亮区域的定位 + this.render.setStyle(canvas, 'position', 'fixed'); + highlightLayout.top = element.getBoundingClientRect().top; + highlightLayout.left = element.getBoundingClientRect().left; + } + if (shape === 'circle') { + const radius: number = (highlightLayout.width < highlightLayout.height ? highlightLayout.width : highlightLayout.height) / 2; + const x: number = highlightLayout.left + highlightLayout.width / 2; + const y: number = highlightLayout.top + highlightLayout.height / 2; + TiIntroService.clearCircle(x, y, radius, 1, ctx); + } else { + ctx.clearRect(highlightLayout.left, highlightLayout.top, highlightLayout.width, highlightLayout.height); + } + } +} diff --git a/src/intro/lib/src/TiIntromodalComponent.ts b/src/intro/lib/src/TiIntromodalComponent.ts new file mode 100644 index 0000000..8b604fd --- /dev/null +++ b/src/intro/lib/src/TiIntromodalComponent.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, OnInit, TemplateRef, ChangeDetectionStrategy } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * @ignore + */ +@Component({ + selector: 'ti-intromodal', + templateUrl: './intromodal.html', + styleUrls: ['./intro.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-intromodal-wrapper]': 'true' + } +}) +export class TiIntromodalComponent extends TiBaseComponent implements OnInit { + @Input() title: string; + @Input() content: string | TemplateRef; + @Input() image: string; + @Input() totalNumber: number; + @Input() currentNumber: number; + @Input() finishButtonText: string; + @Input() skipable: boolean; + @Input() close: (flag?: boolean) => void; + @Input() wantStep: (num: number) => void; + + public totalArray: Array; // 通过totalNumber生成的数组 + public isStringContent: boolean; // content是否为string + public templateContent: TemplateRef; // 用户传入的templateRef类型content + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + super.ngOnInit(); + this.totalArray = new Array(this.totalNumber + 1); + this.isStringContent = typeof this.content === 'string'; + this.templateContent = this.content instanceof TemplateRef ? this.content : undefined; + } + + /** + * @ignore + * 模板中实际调用的是Modal服务dismiss方法,并非此处定义的方法;在此处定义dismiss方法只是为了避免生产环境打包时报错 + */ + public dismiss(): void {} +} diff --git a/src/intro/lib/src/TiIntrotipComponent.ts b/src/intro/lib/src/TiIntrotipComponent.ts new file mode 100644 index 0000000..be935e9 --- /dev/null +++ b/src/intro/lib/src/TiIntrotipComponent.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { TiIntromodalComponent } from './TiIntromodalComponent'; + +/** + * @ignore + */ +@Component({ + selector: 'ti-intromodal', + templateUrl: './introtip.html', + styleUrls: ['./intro.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-intromodal-wrapper]': 'true' + } +}) +export class TiIntrotipComponent extends TiIntromodalComponent {} diff --git a/src/intro/lib/src/i18n/TiIntroWords.ts b/src/intro/lib/src/i18n/TiIntroWords.ts new file mode 100644 index 0000000..017f6a7 --- /dev/null +++ b/src/intro/lib/src/i18n/TiIntroWords.ts @@ -0,0 +1,8 @@ +export interface TiIntroWords { + tiIntro: { + skip: string; + previous: string; + next: string; + finish: string; + }; +} diff --git a/src/intro/lib/src/i18n/en_US.ts b/src/intro/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..3cdc71c --- /dev/null +++ b/src/intro/lib/src/i18n/en_US.ts @@ -0,0 +1,10 @@ +import { TiIntroWords } from './TiIntroWords'; + +export const en_US: TiIntroWords = { + tiIntro: { + skip: 'Skip', + previous: 'Previous', + next: 'Next', + finish: 'Finish' + } +}; diff --git a/src/intro/lib/src/i18n/es_US.ts b/src/intro/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..19a0709 --- /dev/null +++ b/src/intro/lib/src/i18n/es_US.ts @@ -0,0 +1,10 @@ +import { TiIntroWords } from './TiIntroWords'; + +export const es_US: TiIntroWords = { + tiIntro: { + skip: 'Omitir', + previous: 'Anterior', + next: 'Siguiente', + finish: 'Finalizar' + } +}; diff --git a/src/intro/lib/src/i18n/fr_FR.ts b/src/intro/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..020e7fa --- /dev/null +++ b/src/intro/lib/src/i18n/fr_FR.ts @@ -0,0 +1,10 @@ +import { TiIntroWords } from './TiIntroWords'; + +export const fr_FR: TiIntroWords = { + tiIntro: { + skip: 'Sauter', + previous: 'Précédent', + next: 'Suivant', + finish: 'Terminer' + } +}; diff --git a/src/intro/lib/src/i18n/index.ts b/src/intro/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/intro/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/intro/lib/src/i18n/pt_BR.ts b/src/intro/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..64947dd --- /dev/null +++ b/src/intro/lib/src/i18n/pt_BR.ts @@ -0,0 +1,10 @@ +import { TiIntroWords } from './TiIntroWords'; + +export const pt_BR: TiIntroWords = { + tiIntro: { + skip: 'Pular', + previous: 'Anterior', + next: 'Próximo', + finish: 'Finalizar' + } +}; diff --git a/src/intro/lib/src/i18n/zh_CN.ts b/src/intro/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..d7dd29a --- /dev/null +++ b/src/intro/lib/src/i18n/zh_CN.ts @@ -0,0 +1,10 @@ +import { TiIntroWords } from './TiIntroWords'; + +export const zh_CN: TiIntroWords = { + tiIntro: { + skip: '跳过', + previous: '上一步', + next: '下一步', + finish: '立即体验' + } +}; diff --git a/src/intro/lib/src/intro.less b/src/intro/lib/src/intro.less new file mode 100644 index 0000000..d27059c --- /dev/null +++ b/src/intro/lib/src/intro.less @@ -0,0 +1,87 @@ +:host { + --ti-intro-step-list-item-width: 5px; + --ti-intro-modal-padding-horizontal: calc(var(--ti-common-space-base) * 12); + --ti-intro-tip-container-width: 400px; +} +.ti3-intro-steplist-item() { + background-color: var(--ti-common-color-line-normal); + float: left; + width: var(--ti-intro-step-list-item-width); + height: var(--ti-intro-step-list-item-width); + border-radius: calc(var(--ti-intro-step-list-item-width) * 0.5); // IE下 不识别 "/ 2",改为" * 0.5" + margin: 0 3px; + cursor: pointer; + &:last-child { + margin-right: 0; + } + &.ti-step-showed { + background-color: var(--ti-common-color-line-hover); + } + &.active { + width: var(--ti-common-size-5x); + } +} +::ng-deep ti-tip-container ti-intromodal { + display: block; + // 366px = 400(最终宽度) - 16 * 2 (tip的横向padding) - 1 * 2(border); + width: calc(var(--ti-intro-tip-container-width) - var(--ti-common-space-4x) * 2 - 2px); +} +:host.ti3-intromodal-wrapper { + .ti3-intro-close { + position: absolute; + top: 12px; + right: 12px; + cursor: pointer; + } + ti-modal-header { + text-align: center; + padding: var(--ti-common-space-8x) var(--ti-common-space-10x) var(--ti-common-space-3x); + } + ti-modal-body.ti3-intro-body { + text-align: center; + padding: 0 var(--ti-intro-modal-padding-horizontal); + .ti3-intro-steplist-wrapper { + margin-top: var(--ti-common-space-3x); + line-height: 0; + font-size: 0; + } + .ti3-intro-steplist-container { + display: inline-block; + .ti3-intro-steplist-item { + .ti3-intro-steplist-item(); + } + } + } + .ti3-intro-header { + font-size: var(--ti-common-font-size-2); + line-height: var(--ti-common-line-height-number); + font-weight: var(--ti-common-font-weight-7); + color: var(--ti-common-color-text-primary); + margin-right: var(--ti-common-space-10); + } + + .ti3-intro-body { + padding: var(--ti-common-space-3x) 0 var(--ti-common-space-8x); + color: var(--ti-common-color-text-secondary); + line-height: var(--ti-common-line-height-number); + } + + .ti3-intro-footer { + .ti3-intro-steplist-container { + float: left; + margin-top: var(--ti-common-space-2x); + .ti3-intro-steplist-item { + .ti3-intro-steplist-item(); + } + } + .ti3-intro-button-container { + float: right; + button { + margin-right: var(--ti-common-space-2x); + &:last-child { + margin-right: 0; + } + } + } + } +} diff --git a/src/intro/lib/src/intromodal.html b/src/intro/lib/src/intromodal.html new file mode 100644 index 0000000..e238f57 --- /dev/null +++ b/src/intro/lib/src/intromodal.html @@ -0,0 +1,32 @@ +{{title}} + + + {{content}} + +
+
+ + + +
+
+ +
+ + + +
+
diff --git a/src/intro/lib/src/introtip.html b/src/intro/lib/src/introtip.html new file mode 100644 index 0000000..16059b3 --- /dev/null +++ b/src/intro/lib/src/introtip.html @@ -0,0 +1,49 @@ + +
{{title}}
+
+ + {{content}} + +
+
+ diff --git a/src/ip/demo/karma.conf.js b/src/ip/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/ip/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/ip/demo/project.json b/src/ip/demo/project.json new file mode 100644 index 0000000..78bf546 --- /dev/null +++ b/src/ip/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/ip/demo", + "sourceRoot": "src/ip/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/ip", + "index": "src/ip/demo/src/index.html", + "main": "src/ip/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/ip/demo/tsconfig.app.json", + "assets": ["src/ip/demo/src/favicon.ico", "src/ip/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "ip-demo:build:production" + }, + "development": { + "browserTarget": "ip-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js ip" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/ip/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/ip/demo/tsconfig.spec.json", + "karmaConfig": "src/ip/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/ip/demo/src/app/AppComponent.ts b/src/ip/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/ip/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/ip/demo/src/app/AppModule.ts b/src/ip/demo/src/app/AppModule.ts new file mode 100644 index 0000000..5fc8911 --- /dev/null +++ b/src/ip/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { IpTestModule } from './ip/IpTestModule'; + +@NgModule({ + imports: [ + IpTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/ip/demo/src/app/IndexComponent.ts b/src/ip/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..ac04b5f --- /dev/null +++ b/src/ip/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { IpTestModule } from './ip/IpTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = IpTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/ip/demo/src/app/app.html b/src/ip/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/ip/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/ip/demo/src/app/ip/IpBasicComponent.ts b/src/ip/demo/src/app/ip/IpBasicComponent.ts new file mode 100644 index 0000000..add193d --- /dev/null +++ b/src/ip/demo/src/app/ip/IpBasicComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './ip-basic.html' +}) +export class IpBasicComponent { + ipv4Value: string = ''; + ipv6Value: string = '0:0:0:0:0:0:0:1'; + ipv6ValueAbbreviated: string = '::1'; +} diff --git a/src/ip/demo/src/app/ip/IpDisabledComponent.ts b/src/ip/demo/src/app/ip/IpDisabledComponent.ts new file mode 100644 index 0000000..67f7de6 --- /dev/null +++ b/src/ip/demo/src/app/ip/IpDisabledComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './ip-disabled.html' +}) +export class IpDisabledComponent { + ipv4Value: string = '127.0.0.1'; + disabled: boolean = true; +} diff --git a/src/ip/demo/src/app/ip/IpFormcontrolComponent.ts b/src/ip/demo/src/app/ip/IpFormcontrolComponent.ts new file mode 100644 index 0000000..102fa4d --- /dev/null +++ b/src/ip/demo/src/app/ip/IpFormcontrolComponent.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; + +@Component({ + templateUrl: './ip-formcontrol.html' +}) +export class IpFormcontrolComponent implements OnInit { + ipForm: FormGroup; + constructor(private fb: FormBuilder) {} + ngOnInit(): void { + this.ipForm = this.fb.group({ + ipValue: '127.0.0.1' + }); + } +} diff --git a/src/ip/demo/src/app/ip/IpTestModule.ts b/src/ip/demo/src/app/ip/IpTestModule.ts new file mode 100644 index 0000000..6576e22 --- /dev/null +++ b/src/ip/demo/src/app/ip/IpTestModule.ts @@ -0,0 +1,45 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiButtonModule, TiIpModule, TiValidationModule } from '@opentiny/ng'; + +import { IpBasicComponent } from './IpBasicComponent'; +import { IpValidComponent } from './IpValidComponent'; +import { IpDisabledComponent } from './IpDisabledComponent'; +import { IpFormcontrolComponent } from './IpFormcontrolComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiValidationModule, + TiButtonModule, + TiIpModule, + RouterModule.forChild(IpTestModule.ROUTES) + ], + declarations: [IpBasicComponent, IpValidComponent, IpDisabledComponent, IpFormcontrolComponent] +}) +export class IpTestModule { + static readonly LINKS: Array = [{ href: 'components/TiIpComponent.html', label: 'Ip' }]; + static readonly ROUTES: Routes = [ + { + path: 'ip/ip-basic', + component: IpBasicComponent + }, + { + path: 'ip/ip-formcontrol', + component: IpFormcontrolComponent + }, + { + path: 'ip/ip-disabled', + component: IpDisabledComponent + }, + { + path: 'ip/ip-valid', + component: IpValidComponent + } + ]; +} diff --git a/src/ip/demo/src/app/ip/IpValidComponent.ts b/src/ip/demo/src/app/ip/IpValidComponent.ts new file mode 100644 index 0000000..98b3dca --- /dev/null +++ b/src/ip/demo/src/app/ip/IpValidComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './ip-valid.html' +}) +export class IpValidComponent { + ipv4Value: string = ''; + ipv6Value: string = '0:0:0:0:0:0:0:1'; +} diff --git a/src/ip/demo/src/app/ip/ip-basic.html b/src/ip/demo/src/app/ip/ip-basic.html new file mode 100644 index 0000000..97eb27f --- /dev/null +++ b/src/ip/demo/src/app/ip/ip-basic.html @@ -0,0 +1,16 @@ +

1.IPv4

+ +
+
Current Value: {{ ipv4Value }}
+
+ +

2.IPv6

+ +
+
Current Value: {{ ipv6Value }}
+
+
+ +
+
Current Value: {{ ipv6ValueAbbreviated }}
+
diff --git a/src/ip/demo/src/app/ip/ip-disabled.html b/src/ip/demo/src/app/ip/ip-disabled.html new file mode 100644 index 0000000..2926367 --- /dev/null +++ b/src/ip/demo/src/app/ip/ip-disabled.html @@ -0,0 +1 @@ + diff --git a/src/ip/demo/src/app/ip/ip-formcontrol.html b/src/ip/demo/src/app/ip/ip-formcontrol.html new file mode 100644 index 0000000..1078ad8 --- /dev/null +++ b/src/ip/demo/src/app/ip/ip-formcontrol.html @@ -0,0 +1,6 @@ +
+ +
+
+
Current Value: {{ ipForm.value.ipValue }}
+
diff --git a/src/ip/demo/src/app/ip/ip-valid.html b/src/ip/demo/src/app/ip/ip-valid.html new file mode 100644 index 0000000..3bc1efa --- /dev/null +++ b/src/ip/demo/src/app/ip/ip-valid.html @@ -0,0 +1,9 @@ + +
+
Current Value: {{ ipv4Value }}
+
+
+ +
+
Current Value: {{ ipv6Value }}
+
diff --git a/src/ip/demo/src/app/ip/webdoc/ip-demos.js b/src/ip/demo/src/app/ip/webdoc/ip-demos.js new file mode 100644 index 0000000..dac5e53 --- /dev/null +++ b/src/ip/demo/src/app/ip/webdoc/ip-demos.js @@ -0,0 +1,53 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'ip-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'ip basic', + }, + desc: { + 'zh-CN': + '

通过属性version配置 ip 版本号;IPv6 支持缩略格式,例如 1111::8888。

', + 'en-US': '

ip basic

', + }, + apis: ['TiIpComponent.properties.version'], + }, + { + demoId: 'ip-formcontrol', + name: { + 'zh-CN': '响应式表单', + 'en-US': 'ip formcontrol', + }, + desc: { + 'zh-CN': '

响应式表单的用法。

', + 'en-US': '

ip formcontrol

', + }, + }, + { + demoId: 'ip-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'ip disabled', + }, + desc: { + 'zh-CN': '

通过属性disabled配置是否禁用。

', + 'en-US': '

ip disabled

', + }, + apis: ['TiIpComponent.properties.disabled'], + }, + { + demoId: 'ip-valid', + name: { + 'zh-CN': '表单校验', + 'en-US': 'ip valid', + }, + desc: { + 'zh-CN': '

表单校验的用法。

', + 'en-US': '

ip valid

', + }, + }, + ], +}; diff --git a/src/ip/demo/src/app/ip/webdoc/ip.cn.md b/src/ip/demo/src/app/ip/webdoc/ip.cn.md new file mode 100644 index 0000000..eede21a --- /dev/null +++ b/src/ip/demo/src/app/ip/webdoc/ip.cn.md @@ -0,0 +1,23 @@ +--- +title: IP 输入IP +--- +# IP 输入IP + +
+ +Ip 是显示和设置IP地址的组件。   + +```typescript +import { TiIpModule } from '@opentiny/ng'; +``` + +
+ +
+ +Ip 是显示和设置IP地址的组件。   + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
diff --git a/src/ip/demo/src/app/ip/webdoc/ip.en.md b/src/ip/demo/src/app/ip/webdoc/ip.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/ip/demo/src/app/ip/webdoc/ip.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/ip/demo/src/favicon.ico b/src/ip/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/ip/demo/src/index.html b/src/ip/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/ip/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/ip/demo/src/main.ts b/src/ip/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/ip/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/ip/demo/test.ts b/src/ip/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/ip/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/ip/demo/tsconfig.app.json b/src/ip/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/ip/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/ip/demo/tsconfig.spec.json b/src/ip/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/ip/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/ip/lib/index.ts b/src/ip/lib/index.ts new file mode 100644 index 0000000..610d250 --- /dev/null +++ b/src/ip/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiIpModule'; diff --git a/src/ip/lib/ng-package.json b/src/ip/lib/ng-package.json new file mode 100644 index 0000000..353978f --- /dev/null +++ b/src/ip/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/ip", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/ip/lib/package.json b/src/ip/lib/package.json new file mode 100644 index 0000000..2adba1d --- /dev/null +++ b/src/ip/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-ip", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/forms": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/ip/lib/project.json b/src/ip/lib/project.json new file mode 100644 index 0000000..2f88f7f --- /dev/null +++ b/src/ip/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/ip/lib", + "sourceRoot": "src/ip/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/ip"], + "options": { + "project": "src/ip/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/ip"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js ip" + }, + { + "command": "ng default-build ip" + }, + { + "command": "node build/clear-default-theme.js ip" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/ip && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build ip && ng pack ip && node build/publish.js ip --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/ip/lib/src/TiIpComponent.ts b/src/ip/lib/src/TiIpComponent.ts new file mode 100644 index 0000000..789be79 --- /dev/null +++ b/src/ip/lib/src/TiIpComponent.ts @@ -0,0 +1,464 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/* eslint-disable no-eq-null */ +/* eslint-disable eqeqeq */ +import { Component, ElementRef, Input, QueryList, Renderer2, ViewChildren, ChangeDetectionStrategy } from '@angular/core'; +import { TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +// 下面注释,可以避免编译lib时正则报错。原理未知,副作用未知。 +// @dynamic +/** + * Ip组件 + * + * Ip组件提供了一种方便的显示和设置IP地址的功能。 + * + */ +@Component({ + selector: 'ti-ip', + templateUrl: './ip.html', + styleUrls: ['./ip.less'], + providers: [TiFormComponent.getValueAccessor(TiIpComponent)], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti_input_ip_container_ipv4]': 'version !== 6', + '[class.ti_input_ip_container_ipv6]': 'version === 6', + '(paste)': 'pasteHandle($event)', + '(blur)': 'blurHandle($event)' + } +}) +export class TiIpComponent extends TiFormComponent { + protected versionInfo: string = super.getVersion(packageInfo); + // ipv4 配置参数 + private static readonly ipv4Version: number = 4; + // 采用自研的ipv4正则表达式 + private static readonly ipv4Reg: RegExp = + /^(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))$/i; + private static readonly ipv4 = { + ipvCheckReg: /[\D]/g, + label: '...', + placeReg: /\s/g, + separator: '.', + ipValueArray: new Array(4).fill('') + }; + // ipv6 配置参数 + private static readonly ipv6Version: number = 6; + // 采用自研的ipv6正则表达式 + private static readonly ipv6Reg: RegExp = + /^(((([\da-f]{1,4}):){7}([\da-f]{1,4}))|(((([\da-f]{1,4}):){1,7}:)|((([\da-f]{1,4}):){6}:([\da-f]{1,4}))|((([\da-f]{1,4}):){5}:(([\da-f]{1,4}):)?([\da-f]{1,4}))|((([\da-f]{1,4}):){4}:(([\da-f]{1,4}):){0,2}([\da-f]{1,4}))|((([\da-f]{1,4}):){3}:(([\da-f]{1,4}):){0,3}([\da-f]{1,4}))|((([\da-f]{1,4}):){2}:(([\da-f]{1,4}):){0,4}([\da-f]{1,4}))|((([\da-f]{1,4}):){1}:(([\da-f]{1,4}):){0,5}([\da-f]{1,4}))|(::(([\da-f]{1,4}):){0,6}([\da-f]{1,4}))|(::([\da-f]{1,4})?))|(((([\da-f]{1,4}):){6}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){5}:(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){4}:(([\da-f]{1,4}):)?(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){3}:(([\da-f]{1,4}):){0,2}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){2}:(([\da-f]{1,4}):){0,3}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|(([\da-f]{1,4})::(([\da-f]{1,4}):){0,4}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|(::(([\da-f]{1,4}):){0,5}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))))$/i; + private static readonly ipv6 = { + ipvCheckReg: /[^a-f\d]/gi, + label: ':::::::', + placeReg: /\s/g, + separator: ':', + ipValueArray: new Array(8).fill('') + }; + private static getGlbInfo(ver: number): any { + return ver === 4 ? TiIpComponent.ipv4 : TiIpComponent.ipv6; + } + + /** + * Ip 版本号 + */ + @Input() version: number = 4; + /** + * @ignore + */ + @ViewChildren('ipInput') ipInputs: QueryList; + /** + * @ignore + * 存放 ip 组件各个input框值的数组 + */ + public ipsValues: Array; + + /** + * @ignore + */ + public pasteHandle = (event: any) => { + if (this.disabled) { + return; + } + event.preventDefault(); // 阻止默认的input原生的粘贴事件(把粘贴板的值粘贴到input框) + let pasteValue: string = ''; + if (window['clipboardData'] && window['clipboardData'].getData) { + // for IE + pasteValue = window['clipboardData'].getData('Text'); + } else { + // For other browser + try { + pasteValue = event.clipboardData.getData('Text'); + } catch (err) {} + } + const unmaskpaste: string = this.unmask(this.version, pasteValue); + if (this.isValidIP(this.version, unmaskpaste)) { + this.updateValue(unmaskpaste); // 粘贴之后更新值 + this.ipsValues = this.splitToIPArray(this.version, this.model); // 内部的操作手动更新数组 + } + }; + /** + * @ignore + */ + public blurHandle = (event: any) => { + const ipValue: string = this.generateIPValue(0); + if (ipValue === '') { + this.updateValue(ipValue); + } else { + const fixedVaule: string = this.blurFixed(this.version, ipValue); + this.updateValue(this.mask(this.version, fixedVaule)); + } + }; + private blurFixed(ver: number, ip: string): string { + const arr: Array = this.splitToIPArray(ver, ip); + for (let i: number = 0; i < arr.length; i++) { + if (arr[i] === '') { + arr[i] = '0'; + } + } + + return this.joinToIPValue(ver, arr); + } + + // 组件声明周期钩子--start + + ngOnInit(): void { + super.ngOnInit(); + this.init(); + } + + private init(): void { + // 默认是IPv4 + this.version = parseInt(String(this.version), 10) === TiIpComponent.ipv6Version ? TiIpComponent.ipv6Version : TiIpComponent.ipv4Version; + // 初始化ipValueArray 赋值新的数组 + this.ipsValues = TiIpComponent.getGlbInfo(this.version).ipValueArray.concat(); + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + // 处理 聚焦 并使第一段ip获焦(放在这个钩子的原因:在oninit中拿不到 QueryList 返回的结果集) + const elements = []; + this.ipInputs.forEach((item) => { + elements.push(item.nativeElement); + }); + // 推荐在onInit()时调用setFocusableElems(), 但是ngFor/ngIf中的元素在ngAfterViewInit()才能获取到 + this.setFocusableElems(elements); + } + // 组件声明周期钩子--end + + // 实现ControlValueAccessor接口--start + /** + * @ignore + */ + writeValue(value: any): void { + if (value === '') { + super.writeValue(value); + // 对空值做特殊处理,为空时清空IP框 + this.ipsValues = TiIpComponent.getGlbInfo(this.version).ipValueArray.concat(); + } else if (value === this.joinToIPValue(this.version, this.ipsValues)) { + // 和当前IP框的值相同不处理 + } else if (this.isValidIP(this.version, value) || this.isInputting(this.version, value)) { + super.writeValue(this.mask(this.version, value)); + this.ipsValues = this.splitToIPArray(this.version, this.model); + } else { + // 非法不处理 恢复之前值 + super.writeValue(this.joinToIPValue(this.version, this.ipsValues)); + } + } + // 实现ControlValueAccessor接口--end + + // 组件交互方法集合--start + /** + * @ignore + */ + public keydown(e: any, index: number): void { + const ipInputs = this.ipInputs; + const keyCode = e.keyCode; + this.keydownHandle(e, ipInputs['_results'][index], index, this.version); + // 按下键为左或右键时,不做重新赋值,避免IE浏览器下重新赋值之后光标位置不准确问题 + if (keyCode === TiKeymap.KEY_ARROW_LEFT || keyCode === TiKeymap.KEY_ARROW_RIGHT) { + return; + } + } + /** + * @ignore + */ + public change(index: number): void { + const ipValues: string = this.generateIPValue(index); + this.updateValue(ipValues); + } + + // 组件交互方法集合--end + + // 内部公共方法集合--start + /** + * @description 将 ip 数组拼接成ip值 + * @param: ver ip版本 + * @param: arr ip字符串中数字组成的数组 + * @returns + */ + private joinToIPValue(ver: number, arr: Array): string { + return arr.join(TiIpComponent.getGlbInfo(ver).separator); + } + /** + * @description 将 ip 值分割成ip数组 + * @param: ver ip版本 + * @param: ip ip字符串 + */ + private splitToIPArray(ver: number, ip: string): Array { + const res: Array = ip ? ip.split(TiIpComponent.getGlbInfo(ver).separator) : []; + + return res; + } + private isInputting(ver: number, value: string): boolean { + const arr: Array = this.splitToIPArray(ver, value); + + return arr.indexOf('') >= 0; + } + // 判断当前ip值是否合法 + private isValidIP(ver: number, ip: string): boolean { + if (ip === null || ip === undefined) { + return false; + } + const rgx = ver === TiIpComponent.ipv4Version ? TiIpComponent.ipv4Reg : TiIpComponent.ipv6Reg; + return rgx.test(ip); + } + /** + * @description IP需要去除前面的0,例如:,“010.0.0.0”经过该函数处理后变为:“10.0.0.0”;IPv6支持缩略格式 + * @param: ver ip版本 + * @param: ip ip字符串 + * @returns + */ + private mask(ver: number, ip: string): string { + const ipInfo = TiIpComponent.getGlbInfo(ver); + const part = ip.split(ipInfo.separator); + const partLength = part.length; + if (ver === 4) { + for (let i: number = 0; i < partLength; i++) { + if (part[i] !== '') { + let partNumber = parseInt(part[i], 10); + // parseInt(part[i], 10) /= 1; + partNumber /= 1; // 将part[j]转化为整数,目的是消除前面的0 + part[i] = String(partNumber); // 再转化为字符串 + } + } + } else { + // IPv6支持缩略格式,即任何由全0组成的1个或多个16位段的单个连续的字符串 都可以用一个双冒号“::”,且“::”只出现一次;例如:2001:d02::14:95 + // 用‘0’补齐缩略格式缺少个数 + while (part.length < 8) { + let index = part.indexOf(''); + part.splice(index, 0, '0'); + } + + part.forEach((partItem, index) => { + // 查找是否有1-9和字母,如果没有,替换为0,如果有,去除前面的0 + if (partItem.match(/[1-9a-f]/i)) { + part[index] = partItem.replace(/^0{1,3}/i, ''); + } else if (partItem !== '0') { + part[index] = '0'; + } + }); + } + + return part.join(ipInfo.separator); + } + /** + * @description 去掉空格后的IP地址;例如“ 10.0 .0.0 ”进过该函数处理后返回:“10.0.0” + * @param: ver ip版本 + * @param: ip ip字符串 + */ + private unmask(ver: number, ip: string): string { + const iPInfo = TiIpComponent.getGlbInfo(ver); + + return ip.replace(iPInfo.placeReg, ''); + } + + /** + * @description 获取当前ip段输入的值并进行判断,合法时更新到当前ip段输入框内,并拼接返回整体ip值 + * @param: index 当前输入框的下标 + */ + private generateIPValue(index: number): string { + const ipInfo = TiIpComponent.getGlbInfo(this.version); + const cellArray = this.ipInputs; + let value = cellArray['_results'][index].nativeElement.value; + if (ipInfo.ipvCheckReg.test(value)) { + value = value.replace(ipInfo.ipvCheckReg, ''); + } + + // ipv4下,当前输入框值大于255时,强制转换成255 + if (this.version === TiIpComponent.ipv4Version && value > 255) { + value = 255; + } + for (let i: number = 0; i < this.ipsValues.length; i++) { + if (i === index) { + this.ipsValues[i] = value; + cellArray['_results'][index].nativeElement.value = value; + } + } + let ipValues; + ipValues = this.ipsValues.join(ipInfo.separator); + if (ipValues === ipInfo.label) { + ipValues = ''; + } + return ipValues; + } + // 更新value值 //TODO: 考虑去除这个函数,用writeValue代替 + private updateValue(newValue: string): void { + if (newValue === this.model) { + return; + } + // 记录ip的值在value中,方便用户获取 + this.model = newValue; + this.writeValue(newValue); + } + // keydown事件触发时,判断并设置组件光标位置 + private keydownHandle(e: any, thisInput: ElementRef, index: number, ver: number): void { + const ipInputsLength = this.ipInputs.length; + const nextCell = index < ipInputsLength - 1 ? this.ipInputs['_results'][index + 1] : undefined; + const prevCell = index > 0 ? this.ipInputs['_results'][index - 1] : undefined; + + const keyCode = e.keyCode; + // 键盘码110代表. 190代表 >.---当键盘码为这两种时,跳至下一输入框中,并选中该输入框中的值 + if (keyCode === TiKeymap.KEY_NUMPAD_DECIMAL || keyCode === TiKeymap.KEY_PERIOD) { + if (!Util.isUndefined(nextCell)) { + // 非最后一个输入框时,跳至下一输入框 + nextCell.nativeElement.focus(); + nextCell.nativeElement.select(); + } + e.preventDefault(); // 不论是否为最后一个输入框,防止默认事件发生,否则输入框中会出现 ... + return; + } + // 光标后移到下一段ip + if (this.isMoveToNext(e, thisInput, nextCell, ver)) { + nextCell.nativeElement.focus(); + this.caret(nextCell, 0); // 设置光标位置 + } + + // 光标前移到上一段ip + if (this.isMoveToPrevous(keyCode, thisInput, prevCell)) { + this.caret(prevCell, prevCell.nativeElement.value.length); // 设置光标位置 + e.preventDefault(); // 防止默认事件发生,否则输入框中的回退光标位置会有问题 + } + } + // 判断当前是否需要将光标移至后一段ip + private isMoveToNext(e: any, thisInput: ElementRef, nextCell: ElementRef, ver: number): boolean { + // 如果下一段ip不存在直接返回false + if (Util.isUndefined(nextCell)) { + return false; + } + // 如果输入为合法数字,输入框中已有3个值,光标位于输入框末尾,并且存在下一输入框, 则跳至下一输入框 + let isFullToNext; + if (ver !== TiIpComponent.ipv6Version) { + isFullToNext = this.checkIpv4MoveToNext(thisInput, e); + } else { + isFullToNext = this.checkIpv6MoveToNext(thisInput, e); + } + // 当为向右键时→,且光标位置在该输入框末尾。则直接跳到下一输入框中 + const keyCode = e.keyCode; + const isRightToNext = keyCode === TiKeymap.KEY_ARROW_RIGHT && this.caret(thisInput)[0] === thisInput.nativeElement.value.length; + return isFullToNext || isRightToNext; + } + // 判断当前是否需要将光标移至前一段ip + private isMoveToPrevous(keyCode: number, thisInput: ElementRef, prevCell: ElementRef): boolean { + // 1.当为向左键(37),且光标位置位于输入框起始位置时,则跳至前一输入框中; + // 2.当为backspace键,且已回删至输入框起始位置 跳至前已输入框中 + // 3.输入框处于选中状态时,按backspace键不跳到前一输入框中 + return ( + !Util.isUndefined(prevCell) && + ((keyCode === TiKeymap.KEY_ARROW_LEFT && this.caret(thisInput)[0] === 0) || // 按左键 + (keyCode === TiKeymap.KEY_BACKSPACE && + this.caret(thisInput)[0] === 0 && // 按后退键并且删除完 + this.caret(thisInput)[0] === this.caret(thisInput)[1])) + ); // 输入框处于选中状态按后退键不跳到前一个输入框 + } + + // 判断当前段ipv4输入框是否已满3,末端是合法字符,且光标位置在此段ip最后 + private checkIpv4MoveToNext(thisInput: ElementRef, e: any): boolean { + return thisInput.nativeElement.value.length === 3 && this.isNumeric(e) && this.caret(thisInput)[0] === 3; + } + // 判断当前段ipv6输入框是否已满4未合法字符,且光标位置在此段ip最后 + private checkIpv6MoveToNext(thisInput: ElementRef, e: any): boolean { + return thisInput.nativeElement.value.length === 4 && this.isHexaDecimal(e) && this.caret(thisInput)[0] === 4; + } + // 判断输入是否为ipv4合法字符 + private isNumeric(e: any): boolean { + if (e.shiftKey || e.ctrlKey) { + return false; + } + const code = e.keyCode; + return (code >= TiKeymap.KEY_0 && code <= TiKeymap.KEY_9) || (code >= TiKeymap.KEY_NUMPAD_0 && code <= TiKeymap.KEY_NUMPAD_9); + } + // 判断输入是否为ipv6合法字符 + private isHexaDecimal(e: any): boolean { + const keyCode = e.keyCode; + // ctrl键组合其它情况,输入框中不会输入有效字符,返回 + if (e.ctrlKey) { + return false; + } + // 输入有效字符情况:1)a~f 2)有效数字 + const isValidInput = + (keyCode >= TiKeymap.KEY_A && keyCode <= TiKeymap.KEY_F) || + (keyCode >= TiKeymap.KEY_NUMPAD_0 && keyCode <= TiKeymap.KEY_NUMPAD_9) || + (keyCode >= TiKeymap.KEY_0 && keyCode <= TiKeymap.KEY_9); + return isValidInput; + } + + /** + * @description 获取或设置光标位置 + * @param: ele 光标所在dom + * @param: s 开始位置 + * @param: e 结束位置 + */ + private caret(ele: ElementRef, s?: number, e?: number): any { + const element = ele.nativeElement; + const setPosition = function (el, start, end) { + if (el.setSelectionRange) { + el.focus(); + el.setSelectionRange(start, end); + } else if (el.createTextRange) { + const range = el.createTextRange(); + range.collapse(true); + range.moveEnd('character', end); + range.moveStart('character', start); + range.select(); + } + }; + + if (s != null && e != null) { + // setting range + return setPosition(element, s, e); + } else if (s != null) { + // setting position + return setPosition(element, s, s); + } + + // getting + if (element.createTextRange && document['selection']) { + const r = document['selection'].createRange().duplicate(); + + const end = element.value.lastIndexOf(r.text) + r.text.length; + + r.moveEnd('character', element.value.length); + const start = r.text === '' ? element.value.length : element.value.lastIndexOf(r.text); + + return [start, end]; + } + + return [element.selectionStart, element.selectionEnd]; + } + /** + * @ignore + * ngFor遍历的 trackBy函数,防止数据更新导致所有DOM重新渲染 + */ + public trackByFn(index: any, item: any): any { + return index; + } + // 内部公共方法集合--end +} diff --git a/src/ip/lib/src/TiIpModule.ts b/src/ip/lib/src/TiIpModule.ts new file mode 100644 index 0000000..896cad9 --- /dev/null +++ b/src/ip/lib/src/TiIpModule.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIpComponent } from './TiIpComponent'; +import { TiTextModule } from '@opentiny/ng-text'; +@NgModule({ + imports: [CommonModule, FormsModule, TiTextModule], + exports: [TiIpComponent], + declarations: [TiIpComponent] +}) +export class TiIpModule {} +export { TiIpComponent } from './TiIpComponent'; diff --git a/src/ip/lib/src/ip.html b/src/ip/lib/src/ip.html new file mode 100644 index 0000000..ed1415e --- /dev/null +++ b/src/ip/lib/src/ip.html @@ -0,0 +1,20 @@ +{{version === 4 ? '.' : ':'}} diff --git a/src/ip/lib/src/ip.less b/src/ip/lib/src/ip.less new file mode 100644 index 0000000..91abfa7 --- /dev/null +++ b/src/ip/lib/src/ip.less @@ -0,0 +1,64 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-ip-container-height: var(--ti-common-size-7x); + --ti-ipv4-container-width: var(--ti-common-size-40x); + --ti-ipv6-container-width: 380px; + --ti-ip-division-width: var(--ti-common-size-base); +} + +.ti_input_ip_container (@width, @inputWidth) { + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + background-color: var(--ti-common-color-bg-white-normal); + display: inline-block; + width: @width; + height: var(--ti-ip-container-height); + display: flex; + align-items: center; + .box-sizing(border-box); + .form-border-animat-init(); + .ti_input_ip_anchor[tiText] { + text-align: center; + height: calc(var(--ti-ip-container-height) - var(--ti-common-border-weight-normal) * 2); + vertical-align: middle; + padding: 0; + // 由于IE无法正确解析 @{number} -1情况下的样式,因此此处新定义了divisionNum来做算式处理 + width: @inputWidth; + background-color: var(--ti-common-color-transparent); + } + .ti_input_ip_division { + color: var(--ti-common-color-icon-normal); + font-weight: var(--ti-common-font-weight-7); + display: inline-block; + width: var(--ti-ip-division-width); + text-align: center; + font-family: 'Helvetica', 'Myriad', Arial, 'Microsoft Yahei', '????', 'SimSun', Tahoma; + } + &:hover { + border-color: var(--ti-common-color-line-hover); + //默认状态下的hover动画 + .form-border-animat-enter(); + } + &[tiFocused] { + border-color: var(--ti-common-color-line-active); + } + &[disabled] { + background-color: var(--ti-common-color-bg-disabled); + border-color: var(--ti-common-color-line-disabled); + .ti_input_ip_octet, + .ti_input_ip_octet_v6, + .ti_input_ip_division { + color: var(--ti-common-color-icon-disabled); + cursor: not-allowed; + } + } +} + +:host.ti_input_ip_container_ipv4 { + .ti_input_ip_container(var(--ti-ipv4-container-width), calc((100% - var(--ti-ip-division-width) * 3) / 4)); +} + +:host.ti_input_ip_container_ipv6 { + .ti_input_ip_container(var(--ti-ipv6-container-width), calc((100% - var(--ti-ip-division-width) * 7) / 8)); +} diff --git a/src/ipsection/demo/karma.conf.js b/src/ipsection/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/ipsection/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/ipsection/demo/project.json b/src/ipsection/demo/project.json new file mode 100644 index 0000000..6b2890f --- /dev/null +++ b/src/ipsection/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/ipsection/demo", + "sourceRoot": "src/ipsection/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/ipsection", + "index": "src/ipsection/demo/src/index.html", + "main": "src/ipsection/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/ipsection/demo/tsconfig.app.json", + "assets": ["src/ipsection/demo/src/favicon.ico", "src/ipsection/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "ipsection-demo:build:production" + }, + "development": { + "browserTarget": "ipsection-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js ipsection" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/ipsection/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/ipsection/demo/tsconfig.spec.json", + "karmaConfig": "src/ipsection/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/ipsection/demo/src/app/AppComponent.ts b/src/ipsection/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/ipsection/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/ipsection/demo/src/app/AppModule.ts b/src/ipsection/demo/src/app/AppModule.ts new file mode 100644 index 0000000..74f5502 --- /dev/null +++ b/src/ipsection/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { IpsectionTestModule } from './ipsection/IpsectionTestModule'; + +@NgModule({ + imports: [ + IpsectionTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/ipsection/demo/src/app/IndexComponent.ts b/src/ipsection/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..29b2ffe --- /dev/null +++ b/src/ipsection/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { IpsectionTestModule } from './ipsection/IpsectionTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = IpsectionTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/ipsection/demo/src/app/app.html b/src/ipsection/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/ipsection/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionBasicComponent.ts b/src/ipsection/demo/src/app/ipsection/IpsectionBasicComponent.ts new file mode 100644 index 0000000..6ae6279 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionBasicComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiIpsectionConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './ipsection-basic.html' +}) +export class IpsectionBasicComponent { + ipValue: string = '127.0.0.1'; + ipSubnetmaskValue: string = '127.0.0.1/255'; + configs: Array = [ + { section: 0, bold: true }, + { section: 1, options: ['0'] }, + { section: 2, options: ['0'] }, + { section: 3 } + ]; + subnetmaskconfigs: Array = [ + { section: 0 }, + { section: 1, options: ['0'] }, + { section: 2, options: ['0'] }, + { section: 3 }, + { section: 4, options: ['198', '134', '255'] } + ]; +} diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionDisabledComponent.ts b/src/ipsection/demo/src/app/ipsection/IpsectionDisabledComponent.ts new file mode 100644 index 0000000..0077842 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionDisabledComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { TiIpsectionConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './ipsection-disabled.html' +}) +export class IpsectionDisabledComponent { + ipValue: string = '127.0.0.1'; + disabled: boolean = true; + configs: Array = [ + { section: 0, bold: true }, + { section: 1, options: ['0'] }, + { section: 2, options: ['0'] }, + { section: 3 } + ]; + configs1: Array = [ + { section: 0, disabled: false }, + { section: 1, options: ['0'], disabled: true }, + { section: 2, options: ['0'] }, + { section: 3, disabled: true } + ]; +} diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionEventsComponent.ts b/src/ipsection/demo/src/app/ipsection/IpsectionEventsComponent.ts new file mode 100644 index 0000000..0ac09a6 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionEventsComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiIpsectionConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './ipsection-events.html' +}) +export class IpsectionEventsComponent { + myLogs: Array = []; + ipValue: string = '127.0.0.1/255'; + configs: Array = [ + { section: 0, bold: true }, + { section: 1, options: ['0'] }, + { section: 2 }, + { section: 3, options: ['0'] }, + { section: 4 } + ]; + onBlur(event: FocusEvent): void { + this.myLogs = [...this.myLogs, `失焦事件 当前IP值为${this.ipValue}`]; + } + onChange(event: any): void { + this.myLogs = [...this.myLogs, `change事件 当前IP值为${event}`]; + } +} diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionFocusComponent.ts b/src/ipsection/demo/src/app/ipsection/IpsectionFocusComponent.ts new file mode 100644 index 0000000..08a8022 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionFocusComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { TiIpsectionConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './ipsection-focus.html' +}) +export class IpsectionFocusComponent { + ipValue: string = '127.0.0.1'; + ipValue1: string = '127.0.0.1'; + configs: Array = [ + { section: 0, bold: true }, + { section: 1, options: ['0'] }, + { section: 2, options: ['0'] }, + { section: 3 } + ]; + configs1: Array = [ + { section: 0, options: ['0'], disabled: true }, + { section: 1 }, + { section: 2, options: ['0'], disabled: true }, + { section: 3, disabled: true } + ]; +} diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionTestComponent.ts b/src/ipsection/demo/src/app/ipsection/IpsectionTestComponent.ts new file mode 100644 index 0000000..93102cd --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionTestComponent.ts @@ -0,0 +1,43 @@ +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { TiIpsectionConfig, TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './ipsection-test.html' +}) +export class IpsectionTestComponent { + ipValue: string; + control: FormControl = new FormControl(); + configs: Array = [ + { + section: 0, + bold: true, + validation: { + tip: '建议输入196-220', + tipPosition: 'top-right' + }, + validationRules: TiValidators.required + }, + { + section: 1, + validation: { + tip: '该网段为输入类型', + tipPosition: 'top' + } + }, + { + section: 2, + bold: true, + validation: { + tip: '建议输入196-220', + tipPosition: 'top-right' + } + }, + { + section: 3, + validation: { + tip: '该网段为输入类型' + } + } + ]; +} diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionTestModule.ts b/src/ipsection/demo/src/app/ipsection/IpsectionTestModule.ts new file mode 100644 index 0000000..7455d02 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionTestModule.ts @@ -0,0 +1,68 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiButtonModule, TiIpsectionModule, TiTextModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { IpsectionBasicComponent } from './IpsectionBasicComponent'; +import { IpsectionFocusComponent } from './IpsectionFocusComponent'; +import { IpsectionValidComponent } from './IpsectionValidComponent'; +import { IpsectionValidFormgroupComponent } from './IpsectionValidFormgroupComponent'; +import { IpsectionDisabledComponent } from './IpsectionDisabledComponent'; +import { IpsectionEventsComponent } from './IpsectionEventsComponent'; +import { IpsectionTestComponent } from './IpsectionTestComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiValidationModule, + TiButtonModule, + TiTextModule, + TiIpsectionModule, + DemoLogModule, + RouterModule.forChild(IpsectionTestModule.ROUTES) + ], + declarations: [ + IpsectionBasicComponent, + IpsectionFocusComponent, + IpsectionValidComponent, + IpsectionValidFormgroupComponent, + IpsectionDisabledComponent, + IpsectionEventsComponent, + IpsectionTestComponent + ] +}) +export class IpsectionTestModule { + static readonly LINKS: Array = [{ href: 'components/TiIpsectionComponent.html', label: 'Ipsection' }]; + static readonly ROUTES: Routes = [ + { + path: 'ipsection/ipsection-basic', + component: IpsectionBasicComponent + }, + { + path: 'ipsection/ipsection-disabled', + component: IpsectionDisabledComponent + }, + { + path: 'ipsection/ipsection-valid', + component: IpsectionValidComponent + }, + { + path: 'ipsection/ipsection-valid-formgroup', + component: IpsectionValidFormgroupComponent + }, + { + path: 'ipsection/ipsection-events', + component: IpsectionEventsComponent + }, + { + path: 'ipsection/ipsection-focus', + component: IpsectionFocusComponent + }, + { path: 'ipsection/ipsection-test', component: IpsectionTestComponent } + ]; +} diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionValidComponent.ts b/src/ipsection/demo/src/app/ipsection/IpsectionValidComponent.ts new file mode 100644 index 0000000..fbcb509 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionValidComponent.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; +import { TiIpsectionConfig, TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './ipsection-valid.html' +}) +export class IpsectionValidComponent { + ipValue: string = '127.0.0.1'; + configs: Array = [ + { + section: 0, + validation: { + tip: '建议输入127-220', + tipPosition: 'top-right' + }, + validationRules: TiValidators.required + }, + { + section: 1, + validation: { + tip: '该网段为输入类型' + }, + validationRules: [TiValidators.required, TiValidators.rangeValue(0, 255)] + }, + { section: 2, options: ['0'] }, + { + section: 3, + validationRules: TiValidators.rangeValue(0, 255) + } + ]; +} diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionValidFormgroupComponent.ts b/src/ipsection/demo/src/app/ipsection/IpsectionValidFormgroupComponent.ts new file mode 100644 index 0000000..1dbb0b6 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionValidFormgroupComponent.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { TiIpsectionConfig, TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './ipsection-valid-formgroup.html' +}) +export class IpsectionValidFormgroupComponent { + formGroup: FormGroup; + constructor(private fb: FormBuilder) { + this.formGroup = this.fb.group({ + ipsection: new FormControl('127.0.0.1') + }); + } + configs: Array = [ + { + section: 0, + validation: { + tip: '建议输入127-220', + tipPosition: 'top-right' + }, + validationRules: TiValidators.required + }, + { + section: 1, + validation: { + tip: '该网段为输入类型' + }, + validationRules: [TiValidators.required, TiValidators.rangeValue(0, 255)] + }, + { section: 2, options: ['0'] }, + { + section: 3, + validationRules: TiValidators.rangeValue(0, 255) + } + ]; +} diff --git a/src/ipsection/demo/src/app/ipsection/ipsection-basic.html b/src/ipsection/demo/src/app/ipsection/ipsection-basic.html new file mode 100644 index 0000000..8dafbcf --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/ipsection-basic.html @@ -0,0 +1,11 @@ +

不含子网掩码

+ +
+
Current Value: {{ ipValue }}
+
+
+

含有子网掩码

+ +
+
Current Value: {{ ipSubnetmaskValue }}
+
diff --git a/src/ipsection/demo/src/app/ipsection/ipsection-disabled.html b/src/ipsection/demo/src/app/ipsection/ipsection-disabled.html new file mode 100644 index 0000000..c4e8fd8 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/ipsection-disabled.html @@ -0,0 +1,5 @@ +

整体禁用

+ +

+

各网段禁用

+ diff --git a/src/ipsection/demo/src/app/ipsection/ipsection-events.html b/src/ipsection/demo/src/app/ipsection/ipsection-events.html new file mode 100644 index 0000000..f282c61 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/ipsection-events.html @@ -0,0 +1,13 @@ +

描述

+

事件接口示例

+

示例

+

+ +

事件日志:

+ diff --git a/src/ipsection/demo/src/app/ipsection/ipsection-focus.html b/src/ipsection/demo/src/app/ipsection/ipsection-focus.html new file mode 100644 index 0000000..318bf24 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/ipsection-focus.html @@ -0,0 +1,12 @@ +

描述

+

设置autofoucus属性,默认聚焦第一个非灰化元素

+

示例

+

1.不设置autofocus

+ +

+

当前Ip地址为:{{ipValue}}

+
+

2.设置autofocus且设置灰化项

+ +

+

当前Ip地址为:{{ipValue1}}

diff --git a/src/ipsection/demo/src/app/ipsection/ipsection-test.html b/src/ipsection/demo/src/app/ipsection/ipsection-test.html new file mode 100644 index 0000000..2c1400e --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/ipsection-test.html @@ -0,0 +1,12 @@ +

描述

+

场景测试,测试组件功能是否正常

+

示例

+

不设置初始值

+

1.1 模板式表单(值为 undefined)

+ +

+

当前Ip地址为:{{ipValue}}

+

1.2.响应式表单,使用formControl绑定值(值为 null)

+ +

+

当前Ip地址为:{{control.value}}

diff --git a/src/ipsection/demo/src/app/ipsection/ipsection-valid-formgroup.html b/src/ipsection/demo/src/app/ipsection/ipsection-valid-formgroup.html new file mode 100644 index 0000000..dc9718e --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/ipsection-valid-formgroup.html @@ -0,0 +1,6 @@ +
+ +
+
+
Current Value: {{ formGroup.value.ipsection }}
+
diff --git a/src/ipsection/demo/src/app/ipsection/ipsection-valid.html b/src/ipsection/demo/src/app/ipsection/ipsection-valid.html new file mode 100644 index 0000000..fe6feb2 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/ipsection-valid.html @@ -0,0 +1,4 @@ + +
+
Current Value: {{ ipValue }}
+
diff --git a/src/ipsection/demo/src/app/ipsection/webdoc/ipsection-demos.js b/src/ipsection/demo/src/app/ipsection/webdoc/ipsection-demos.js new file mode 100644 index 0000000..5df8a04 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/webdoc/ipsection-demos.js @@ -0,0 +1,66 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'ipsection-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'ipsection basic', + }, + desc: { + 'zh-CN': '

通过属性configs配置组件各网段的渲染数据。', + 'en-US': '

ipsection basic

', + }, + apis: [ + 'TiIpsectionComponent.properties.configs', + 'TiIpsectionConfig.properties.section', + 'TiIpsectionConfig.properties.options', + 'TiIpsectionConfig.properties.bold', + ], + }, + { + demoId: 'ipsection-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'ipsection disabled', + }, + desc: { + 'zh-CN': + '通过属性disabled配置整体是否为禁用状态;通过属性configs配置各网段是否为禁用状态。', + 'en-US': '

ipsection disabled

', + }, + apis: [ + 'TiIpsectionComponent.properties.disabled', + 'TiIpsectionConfig.properties.disabled', + ], + }, + { + demoId: 'ipsection-valid', + name: { + 'zh-CN': '表单校验', + 'en-US': 'ipsection valid', + }, + desc: { + 'zh-CN': + '通过属性configs配置组件各网段的校验规则和 tip 等配置项。', + 'en-US': '

ipsection valid

', + }, + apis: [ + 'TiIpsectionConfig.properties.validation', + 'TiIpsectionConfig.properties.validationRules', + ], + }, + { + demoId: 'ipsection-valid-formgroup', + name: { + 'zh-CN': '响应式表单', + 'en-US': 'ipsection valid formgroup', + }, + desc: { + 'zh-CN': '响应式表单的用法。', + 'en-US': '

ipsection valid formgroup

', + }, + }, + ], +}; diff --git a/src/ipsection/demo/src/app/ipsection/webdoc/ipsection.cn.md b/src/ipsection/demo/src/app/ipsection/webdoc/ipsection.cn.md new file mode 100644 index 0000000..e6594eb --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/webdoc/ipsection.cn.md @@ -0,0 +1,28 @@ +--- +title: IPsection IP分段 +--- +# IPsection IP分段 + +
+ +IPsection 是对 IP 各网段分开处理的组件。   + ++ 各网段有下拉框和输入框两种类型。 + +```typescript +import { TiIpsectionModule } from '@opentiny/ng';; +``` + +
+ +
+ +IPsection 是对 IP 各网段分开处理的组件。   + ++ 各网段有下拉框和输入框两种类型。 + +```typescript +import { TiIpsectionModule } from '@opentiny/ng';; +``` + +
diff --git a/src/ipsection/demo/src/app/ipsection/webdoc/ipsection.en.md b/src/ipsection/demo/src/app/ipsection/webdoc/ipsection.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/webdoc/ipsection.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/ipsection/demo/src/favicon.ico b/src/ipsection/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/ipsection/demo/src/index.html b/src/ipsection/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/ipsection/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/ipsection/demo/src/main.ts b/src/ipsection/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/ipsection/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/ipsection/demo/test.ts b/src/ipsection/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/ipsection/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/ipsection/demo/tsconfig.app.json b/src/ipsection/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/ipsection/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/ipsection/demo/tsconfig.spec.json b/src/ipsection/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/ipsection/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/ipsection/lib/index.ts b/src/ipsection/lib/index.ts new file mode 100644 index 0000000..9f0b4c9 --- /dev/null +++ b/src/ipsection/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiIpsectionModule'; diff --git a/src/ipsection/lib/ng-package.json b/src/ipsection/lib/ng-package.json new file mode 100644 index 0000000..bd5016c --- /dev/null +++ b/src/ipsection/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/ipsection", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/ipsection/lib/package.json b/src/ipsection/lib/package.json new file mode 100644 index 0000000..9867542 --- /dev/null +++ b/src/ipsection/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-ipsection", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-validation": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-select": "~1.0.0-beta.0", + "@opentiny/ng-text": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/ipsection/lib/project.json b/src/ipsection/lib/project.json new file mode 100644 index 0000000..884285d --- /dev/null +++ b/src/ipsection/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/ipsection/lib", + "sourceRoot": "src/ipsection/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/ipsection"], + "options": { + "project": "src/ipsection/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/ipsection"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js ipsection" + }, + { + "command": "ng default-build ipsection" + }, + { + "command": "node build/clear-default-theme.js ipsection" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/ipsection && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build ipsection && ng pack ipsection && node build/publish.js ipsection --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/ipsection/lib/src/TiIpsectionComponent.ts b/src/ipsection/lib/src/TiIpsectionComponent.ts new file mode 100644 index 0000000..8fada88 --- /dev/null +++ b/src/ipsection/lib/src/TiIpsectionComponent.ts @@ -0,0 +1,267 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Injector, + Input, + Renderer2, + SimpleChanges +} from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiValidationConfig } from '@opentiny/ng-validation'; +import { AbstractControl, FormBuilder, FormControl, FormGroup, NgControl, NgModel, ValidationErrors, ValidatorFn } from '@angular/forms'; +import packageInfo from '../package.json'; +export interface TiIpsectionConfig { + /** + * 网段唯一标识 + */ + section: number; + /** + * 网段为 select 类型的下拉数据集 + */ + options?: Array; + /** + * 网段为 input 类型时文本是否加粗 + */ + bold?: boolean; + /** + * 网段为 input 类型时的校验提示信息 + */ + validation?: TiValidationConfig; + /** + * 网段为 input 类型时的校验规则,10.1.15 支持 ValidatorFn 类型 + */ + validationRules?: Array | ValidatorFn; + /** + * 网段是否禁用 + */ + disabled?: boolean; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} +/** + * Ipsection组件,Ip各网段分开处理,各网段有下拉类和输入类两种类型; + * + * + */ +@Component({ + selector: 'ti-ipsection', + templateUrl: 'ipsection.html', + styleUrls: ['./ipsection.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-ipsection-container]': 'true', + '(blur)': 'onBlur($event)' + }, + providers: [TiFormComponent.getValueAccessor(TiIpsectionComponent)] +}) +export class TiIpsectionComponent extends TiFormComponent { + /** + * Ip 各网段数据集 + */ + @Input() configs: Array; + /** + * @ignore + * 存放 input 控件实例数组 + */ + private controls: Array = []; + /** + * @ignore + * 格式化输入 + */ + public maskInput: string = '000'; + /** + * @ignore + * 存放各网段 ip 值的数组 + */ + public ipValues: Array = []; + /** + * input控件 FormGroup 实例,服务可通过该实例动态更改某一FormControl实例相关属性(value,disabled,校验规则等) + */ + public formGroup: FormGroup; + protected versionInfo: string = super.getVersion(packageInfo); + + constructor( + protected hostRef: ElementRef, + protected renderer2: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + private fb: FormBuilder, + private injector: Injector + ) { + super(hostRef, renderer2, changeDetectorRef); + this.formGroup = fb.group({}); + } + + /** + * 将ip值分割为数组 + */ + private static splitToIPArray(ipValue: string): Array { + const ipArray: Array = ipValue ? ipValue.replace(/\//, '.').split('.') : []; + + return ipArray; + } + + /** + * 将ip数组拼接为ip值 + */ + private static joinToIPValue(ipArray: Array): string { + let ipValue: string = ipArray.join('.'); + if (ipArray.length > 4) { + ipValue = ipValue.replace(/(.*)\./, '$1/'); + } + + return ipValue; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['configs'] && !changes['configs'].firstChange) { + this.initIpValue(); + } + } + + ngOnInit(): void { + super.ngOnInit(); + // 创建表单控件实例 + for (let i: number = 0; i < this.configs.length; i++) { + if (!this.configs[i].options) { + this.controls[i] = new FormControl(); + this.formGroup.addControl(`input_${i}`, this.controls[i]); + // input类型网段值更改时触发事件 + this.controls[i].valueChanges.subscribe((value: any) => { + this.updateIpValue(i, value); + }); + } + } + } + + ngAfterViewInit(): void { + const itemElems: HTMLCollection = this.hostRef.nativeElement.getElementsByClassName('ti3-ipsection'); + const focusElements: Array = []; + // 获取ipsection对应控件 + const ipsectionControl: any = this.injector.get(NgControl); + // 添加tiGroup属性,方便业务整体校验时校验组件内部formGroup,获取正确的错误信息 + if (ipsectionControl) { + const control: any = ipsectionControl.control; + control.tiGroup = this.formGroup; + control.setValidators(this.validate.bind(this)); + } + + // 设置可聚焦元素 + for (let i: number = 0; i < this.configs.length; i++) { + if (itemElems[i].firstChild) { + focusElements.push(itemElems[i].firstChild); + } else { + this.setAttr(itemElems[i], 'disabled', this.configs[i].disabled || this.disabled); + // 此处添加formControlName|name属性原因:方便业务整体校验时通过formControlName|name找到对应元素 + const controlName: string = ipsectionControl instanceof NgModel ? 'name' : 'formControlName'; + this.renderer.setAttribute(itemElems[i], controlName, `input_${i}`); + focusElements.push(itemElems[i]); + } + } + this.setFocusableElems(focusElements); + } + + ngDoCheck(): void { + super.ngDoCheck(); + this.changeDetectorRef.markForCheck(); + } + + /** + * @ignore + * 获取model值,并进行初始化处理 + */ + writeValue(value: any): void { + super.writeValue(value); + this.ipValues = TiIpsectionComponent.splitToIPArray(value); + + this.initIpValue(); + } + /** + * @ignore + * 整体失焦触发事件 + */ + onBlur(event: any): void { + this.formatValue(); + } + + /** + * @ignore + * select类型网段值更改时触发事件 + */ + onChange(i: number, value: any): void { + this.updateIpValue(i, value); + } + + /** + * 初始化ip值 + */ + private initIpValue(): void { + for (let i: number = 0; i < this.configs.length; i++) { + if (this.configs[i].options) { + // 初始显示默认选中项,若value值在options存在,则显示该值,否则显示下拉配置项第一项 + this.configs[i].selected = this.configs[i].options.find((item) => item === this.ipValues[i]) + ? this.ipValues[i] + : this.configs[i].options[0]; + this.updateIpValue(i, this.configs[i].selected); + } else { + // 初始设置value,disabled及校验规则 + this.ipValues[i] = this.ipValues[i] ? parseInt(this.ipValues[i], 10).toString() : ''; + this.controls[i].setValue(this.ipValues[i]); + this.controls[i].setValidators(this.configs[i].validationRules); + this.configs[i].disabled || this.disabled ? this.controls[i].disable() : this.controls[i].enable(); + } + } + } + + /** + * 初始及失焦处理输入框值,清除Ip网段前面的0 + */ + private formatValue(): void { + for (let i: number = 0; i < this.configs.length; i++) { + if (!this.configs[i].options) { + this.controls[i].setValue(parseInt(this.controls[i].value, 10).toString()); + } + } + } + + /** + * 更新model值 + */ + private updateIpValue(index?: number, value?: any): void { + if (this.ipValues.length <= 0 || this.ipValues[index] === value) { + return; + } + + this.ipValues.splice(index, 1, value); + // 解决model值更新后ExpressionChangedAfterItHasBeenCheckedError报错问题,onpush正常default报错 + setTimeout(() => { + const ipValue: string = TiIpsectionComponent.joinToIPValue(this.ipValues); + this.model = ipValue === '...' || ipValue === '.../' ? '' : ipValue; + }, 0); + } + + // 自定义验证器,同步input表单控件组的状态 + private validate(control: AbstractControl): ValidationErrors | null { + return this.formGroup.invalid + ? { + 'input-formGroup': { + value: this.formGroup.value + } + } + : null; + } +} diff --git a/src/ipsection/lib/src/TiIpsectionModule.ts b/src/ipsection/lib/src/TiIpsectionModule.ts new file mode 100644 index 0000000..5c8b2f7 --- /dev/null +++ b/src/ipsection/lib/src/TiIpsectionModule.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TiIpsectionComponent } from './TiIpsectionComponent'; +import { TiSelectModule } from '@opentiny/ng-select'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiValidationModule } from '@opentiny/ng-validation'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiSelectModule, TiTextModule, ReactiveFormsModule, TiValidationModule], + exports: [TiIpsectionComponent], + declarations: [TiIpsectionComponent] +}) +export class TiIpsectionModule {} +export { TiIpsectionComponent, TiIpsectionConfig } from './TiIpsectionComponent'; diff --git a/src/ipsection/lib/src/ipsection.html b/src/ipsection/lib/src/ipsection.html new file mode 100644 index 0000000..969452d --- /dev/null +++ b/src/ipsection/lib/src/ipsection.html @@ -0,0 +1,35 @@ + + +
+ +
+
+ + + {{option}} + + +
+
diff --git a/src/ipsection/lib/src/ipsection.less b/src/ipsection/lib/src/ipsection.less new file mode 100644 index 0000000..cb20651 --- /dev/null +++ b/src/ipsection/lib/src/ipsection.less @@ -0,0 +1,52 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-ipsection-dot-size: 2px; + --ti-ipsection-input-width: 50px; + --ti-ipsection-select-width: 70px; +} + +:host.ti3-ipsection-container { + display: inline-block; + // ipsection整体校验时无需红色背景 + @{tiny-invalid-class} { + background-color: transparent; + } +} + +.ti3-ipsection-input { + width: var(--ti-ipsection-input-width); + text-align: center; +} + +.ti3-ipsection-select { + width: var(--ti-ipsection-select-width); +} + +.ti3-ipsection-text-blod { + font-weight: var(--ti-common-font-weight-7); +} + +.ti3-ipsection-division { + display: inline-block; + vertical-align: middle; + background: var(--ti-common-color-icon-normal); +} + +.ti3-ipsection-dot { + width: var(--ti-ipsection-dot-size); + height: var(--ti-ipsection-dot-size); + border-radius: 50%; + .box-sizing(); + margin: 0 var(--ti-common-space-2x); +} + +.ti3-ipsection-maskdivision { + width: 18px; + height: 1px; + transform: rotate(110deg); +} + +.ti3-ipsection-division-disabled { + background: var(--ti-common-color-icon-disabled); +} diff --git a/src/labeleditor/demo/project.json b/src/labeleditor/demo/project.json new file mode 100644 index 0000000..d2a9d1b --- /dev/null +++ b/src/labeleditor/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/labeleditor/demo", + "sourceRoot": "src/labeleditor/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/labeleditor", + "index": "src/labeleditor/demo/src/index.html", + "main": "src/labeleditor/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/labeleditor/demo/tsconfig.app.json", + "assets": ["src/labeleditor/demo/src/favicon.ico", "src/labeleditor/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "labeleditor-demo:build:production" + }, + "development": { + "browserTarget": "labeleditor-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js labeleditor" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/labeleditor/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/labeleditor/demo/tsconfig.spec.json", + "karmaConfig": "src/labeleditor/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/labeleditor/demo/src/app/AppComponent.ts b/src/labeleditor/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/labeleditor/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/labeleditor/demo/src/app/AppModule.ts b/src/labeleditor/demo/src/app/AppModule.ts new file mode 100644 index 0000000..1074c19 --- /dev/null +++ b/src/labeleditor/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { LabeleditorTestModule } from './labeleditor/LabeleditorTestModule'; + +@NgModule({ + imports: [ + LabeleditorTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/labeleditor/demo/src/app/IndexComponent.ts b/src/labeleditor/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..78f3f13 --- /dev/null +++ b/src/labeleditor/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { LabeleditorTestModule } from './labeleditor/LabeleditorTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = LabeleditorTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/labeleditor/demo/src/app/app.html b/src/labeleditor/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/labeleditor/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorAutotipComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorAutotipComponent.ts new file mode 100644 index 0000000..bc38657 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorAutotipComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-autotip.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorAutotipComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + iconTip: string = '请点击进行编辑'; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorBasicComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorBasicComponent.ts new file mode 100644 index 0000000..23b6761 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorBasicComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-basic.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorBasicComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorDisabledComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorDisabledComponent.ts new file mode 100644 index 0000000..c68ca46 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorDisabledComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-disabled.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorDisabledComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + iconTip: string = '当前为受限状态,不可编辑'; + disabled: boolean = true; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorEventsComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorEventsComponent.ts new file mode 100644 index 0000000..e5eefb1 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorEventsComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './labeleditor-events.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorEventsComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + myLogs: Array = []; + confirm(val: string): void { + this.myLogs = [...this.myLogs, `on confirm`]; + } + cancel(): void { + this.myLogs = [...this.myLogs, `on cancel`]; + } + editor(): void { + this.myLogs = [...this.myLogs, `on editor`]; + } +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorIcontipcontextComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorIcontipcontextComponent.ts new file mode 100644 index 0000000..9af0c63 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorIcontipcontextComponent.ts @@ -0,0 +1,49 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-iconTipContext.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorIconTipContextComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + componentClass: any = TemplateComponent; + myLogs: Array = []; + // 定义tip显示内容的上下文 + tipContext: any = { + label: 'Click here to edit!', // 该属性与TemplateComponent组件中的@Input属性定义对应 + outputs: { + // 定义在outputs对象中的属性(例如click)与TemplateComponent组件中的@Output属性定义对应 + click: ($event: string): void => { + this.myLogs = [...this.myLogs, `on ${$event}`]; + } + } + }; + + iconContext: string = '这是一个 template'; +} +// 自定义组件,此处为了方便demo展示与tip生成组件写在同一文件,项目中请将组件单独写在一个文件中 +@Component({ + template: ` + + {{ label }} +
+ +
+ ` +}) +export class TemplateComponent { + @Input() label: string; + @Output() readonly click: EventEmitter = new EventEmitter(); + + onClick(): void { + this.click.emit('click'); + } +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlengthComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlengthComponent.ts new file mode 100644 index 0000000..93f4204 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlengthComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-maxlength.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorMaxlengthComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + label1: string = '限制文本长度'; + maxlength: number = 10; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlineComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlineComponent.ts new file mode 100644 index 0000000..5f48c10 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlineComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-maxline.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorMaxlineComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorMultilineSizeComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorMultilineSizeComponent.ts new file mode 100644 index 0000000..c8bce70 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorMultilineSizeComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-multiline-size.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 250px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorMultilineSizeComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorResizeComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorResizeComponent.ts new file mode 100644 index 0000000..f0367d8 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorResizeComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-resize.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorResizeComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorTestModule.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorTestModule.ts new file mode 100644 index 0000000..7730fc1 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorTestModule.ts @@ -0,0 +1,70 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TiIconModule, TiLabeleditorModule, TiButtonModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { LabeleditorBasicComponent } from './LabeleditorBasicComponent'; +import { LabeleditorAutotipComponent } from './LabeleditorAutotipComponent'; +import { LabeleditorResizeComponent } from './LabeleditorResizeComponent'; +import { LabeleditorMaxlengthComponent } from './LabeleditorMaxlengthComponent'; +import { LabeleditorEventsComponent } from './LabeleditorEventsComponent'; +import { LabeleditorValidationComponent } from './LabeleditorValidationComponent'; +import { LabeleditorDisabledComponent } from './LabeleditorDisabledComponent'; +import { LabeleditorMultilineSizeComponent } from './LabeleditorMultilineSizeComponent'; +import { LabeleditorMaxlineComponent } from './LabeleditorMaxlineComponent'; +import { LabeleditorValidationAsyncComponent } from './LabeleditorValidationAsyncComponent'; +import { LabeleditorIconTipContextComponent, TemplateComponent } from './LabeleditorIcontipcontextComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiIconModule, + TiButtonModule, + ReactiveFormsModule, + TiLabeleditorModule, + TiValidationModule, + DemoLogModule, + RouterModule.forChild(LabeleditorTestModule.ROUTES) + ], + declarations: [ + LabeleditorBasicComponent, + LabeleditorAutotipComponent, + LabeleditorResizeComponent, + LabeleditorMaxlengthComponent, + LabeleditorValidationComponent, + LabeleditorEventsComponent, + LabeleditorDisabledComponent, + LabeleditorMaxlineComponent, + LabeleditorMultilineSizeComponent, + TemplateComponent, + LabeleditorValidationAsyncComponent, + LabeleditorIconTipContextComponent + ], + entryComponents: [TemplateComponent] +}) +export class LabeleditorTestModule { + static readonly ROUTES: Routes = [ + { path: 'labeleditor/labeleditor-basic', component: LabeleditorBasicComponent, data: { label: '基础' } }, + { path: 'labeleditor/labeleditor-autotip', component: LabeleditorAutotipComponent, data: { label: '智能提示' } }, + { + path: 'labeleditor/labeleditor-iconTipContext', + component: LabeleditorIconTipContextComponent, + data: { label: '图标提示为插槽或组件' } + }, + { path: 'labeleditor/labeleditor-resize', component: LabeleditorResizeComponent, data: { label: '多行文本框调整大小' } }, + { path: 'labeleditor/labeleditor-maxline', component: LabeleditorMaxlineComponent, data: { label: '最大行数' } }, + { path: 'labeleditor/labeleditor-maxlength', component: LabeleditorMaxlengthComponent, data: { label: '文本长度' } }, + { path: 'labeleditor/labeleditor-validation', component: LabeleditorValidationComponent, data: { label: '校验' } }, + { path: 'labeleditor/labeleditor-validation-async', component: LabeleditorValidationAsyncComponent, data: { label: '异步校验' } }, + { path: 'labeleditor/labeleditor-disabled', component: LabeleditorDisabledComponent, data: { label: '禁用' } }, + { path: 'labeleditor/labeleditor-events', component: LabeleditorEventsComponent, data: { label: '事件' } }, + { + path: 'labeleditor/labeleditor-multiline-size', + component: LabeleditorMultilineSizeComponent, + data: { label: '多行文本框可设置宽高' } + } + ]; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationAsyncComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationAsyncComponent.ts new file mode 100644 index 0000000..a812d2a --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationAsyncComponent.ts @@ -0,0 +1,56 @@ +import { Component } from '@angular/core'; +import { AbstractControl, AsyncValidatorFn, FormControl, ValidationErrors } from '@angular/forms'; +import { TiValidationDirective } from '@opentiny/ng'; +import { Observable, of } from 'rxjs'; +import { catchError, debounceTime, delay, distinctUntilChanged, first, map, switchMap } from 'rxjs/operators'; + +@Component({ + templateUrl: './labeleditor-validation-async.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorValidationAsyncComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + asyncValidatorRules: any = [CustomAsyncValidators.isRightUserName()]; + control: FormControl = new FormControl(this.label); +} + +export class CustomAsyncValidators { + // 自定义异步校验规则 + static isRightUserName(): AsyncValidatorFn { + return (control: AbstractControl): Promise | Observable => { + if (control.valueChanges) { + // 初始时control中可能没有valueChanges属性 + return control.valueChanges.pipe( + debounceTime(TiValidationDirective.ASYNC_DEBOUNCE_TIME), // 防抖处理(输入停顿后再进行校验) + distinctUntilChanged(), // 防止对前后相同的值进行校验 + switchMap((value: string) => CustomAsyncValidators.isRight(value)), // 进行后台请求校验 + map((isRight: boolean) => { + // 拿到后台返回值 + // 异步校验需要在校验错误信息中通过 tiAsyncErrorMessage 属性来设置校验错误提示信息 + return isRight ? { rightUserName: { actualValue: control.value, tiAsyncErrorMessage: '用户名不正确' } } : null; + }), + first(), // complete control.valueChanges + catchError(() => null) + ); + } else { + return of(null); + } + }; + } + + // 模拟后台异步请求 + static isRight(value: string): Observable { + return of(value !== 'tiny').pipe( + delay(2000), + catchError(() => of(false)) + ); + } +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationComponent.ts new file mode 100644 index 0000000..53df308 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { TiValidationConfig, TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './labeleditor-validation.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorValidationComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + label1: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + control: FormControl = new FormControl(this.label); + validation: TiValidationConfig = { + tip: '请输入' + }; + validationRules: any = [TiValidators.equal('aa'), TiValidators.required]; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-autotip.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-autotip.html new file mode 100644 index 0000000..ba4f109 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-autotip.html @@ -0,0 +1,2 @@ + + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-basic.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-basic.html new file mode 100644 index 0000000..b70c6b3 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-basic.html @@ -0,0 +1,5 @@ +

1.no multiline

+ +

2.multiline: true

+ + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-disabled.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-disabled.html new file mode 100644 index 0000000..77fe39a --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-disabled.html @@ -0,0 +1,2 @@ + + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-events.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-events.html new file mode 100644 index 0000000..a824483 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-events.html @@ -0,0 +1,10 @@ + + + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-icontipcontext.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-icontipcontext.html new file mode 100644 index 0000000..a04c772 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-icontipcontext.html @@ -0,0 +1,25 @@ +

1.component

+ + + +

2.template

+ + + + + {{context}} + + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-maxlength.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-maxlength.html new file mode 100644 index 0000000..cb26a4d --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-maxlength.html @@ -0,0 +1,11 @@ +

1.multiline: true + maxlength: 125

+ + +

2.multiline: false + maxlength: 10

+ diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-maxline.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-maxline.html new file mode 100644 index 0000000..e426321 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-maxline.html @@ -0,0 +1 @@ + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-multiline-size.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-multiline-size.html new file mode 100644 index 0000000..4a348c4 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-multiline-size.html @@ -0,0 +1,9 @@ + + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-resize.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-resize.html new file mode 100644 index 0000000..edf44a3 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-resize.html @@ -0,0 +1,24 @@ +

1.resize: vertical,只能垂直方向展开

+ + +

2.resize: horizontal,只能水平方向展开

+ + +

3.resize: both,水平/垂直方向都可展开

+ + +

4.resize: none,水平/垂直方向都不可展开

+ + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-validation-async.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-validation-async.html new file mode 100644 index 0000000..e47d3e3 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-validation-async.html @@ -0,0 +1,4 @@ +

1.模板表单

+ +

2.响应式表单

+ diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-validation.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-validation.html new file mode 100644 index 0000000..1e959cc --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-validation.html @@ -0,0 +1,17 @@ +

1.模板式表单

+ + +

2.响应式表单

+ + diff --git a/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor-demos.js b/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor-demos.js new file mode 100644 index 0000000..47afb63 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor-demos.js @@ -0,0 +1,150 @@ +export default { + column: '2', + demos: [ + { + demoId: 'labeleditor-basic', + name: { + 'zh-CN': '基础', + 'en-US': 'labeleditor basic' + }, + desc: { + 'zh-CN': '

通过属性multiline配置是多/单行文本编辑。

', + 'en-US': 'labeleditor basic' + }, + apis: ['TiLabeleditorComponent.properties.multiline'] + }, + { + demoId: 'labeleditor-maxline', + name: { + 'zh-CN': '最大行数', + 'en-US': 'labeleditor maxline' + }, + desc: { + 'zh-CN': '

通过属性maxline配置最大行数,该属性只在autoTiptrue时生效。

', + 'en-US': 'labeleditor maxline' + }, + apis: ['TiLabeleditorComponent.properties.maxLine'] + }, + { + demoId: 'labeleditor-autotip', + name: { + 'zh-CN': '智能提示', + 'en-US': 'labeleditor autotip' + }, + desc: { + 'zh-CN': '

通过属性autoTip配置编辑完成状态时文本是否自动溢出并提示。

', + 'en-US': 'labeleditor autotip' + }, + apis: ['TiLabeleditorComponent.properties.autoTip'] + }, + { + demoId: 'labeleditor-icontipcontext', + name: { + 'zh-CN': '图标提示为模板或组件', + 'en-US': 'labeleditor iconTipContext' + }, + desc: { + 'zh-CN': '

通过属性iconTip配置编辑文本图标提示内容;通过属性iconTipContext配置图标提示上下文。

', + 'en-US': 'labeleditor iconTipContext' + }, + apis: ['TiLabeleditorComponent.properties.iconTip', 'TiLabeleditorComponent.properties.iconTipContext'] + }, + { + demoId: 'labeleditor-maxlength', + name: { + 'zh-CN': '文本长度', + 'en-US': 'labeleditor maxlength' + }, + desc: { + 'zh-CN': '

通过属性maxlength配置文本最大长度。

', + 'en-US': 'labeleditor maxlength' + }, + apis: ['TiLabeleditorComponent.properties.maxlength'] + }, + { + demoId: 'labeleditor-resize', + name: { + 'zh-CN': '多行文本框调整大小', + 'en-US': 'labeleditor resize' + }, + desc: { + 'zh-CN': + '

通过属性resize配置多行文本编辑框调整的方向,包含both、none、vertical、horizontal四种类型

', + 'en-US': 'labeleditor resize' + }, + apis: ['TiLabeleditorComponent.properties.resize'] + }, + { + demoId: 'labeleditor-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'labeleditor disabled' + }, + desc: { + 'zh-CN': '

通过属性disabled配置禁用状态。

', + 'en-US': 'labeleditor disabled' + }, + apis: ['TiLabeleditorComponent.properties.disabled'] + }, + { + demoId: 'labeleditor-events', + name: { + 'zh-CN': '事件', + 'en-US': 'labeleditor events' + }, + desc: { + 'zh-CN': + '

当点击编辑图标时触发editor事件;当点击完成图标时触发confirm事件;当点击取消图标时触发cancel事件。

', + 'en-US': 'labeleditor events' + }, + apis: ['TiLabeleditorComponent.events.editor', 'TiLabeleditorComponent.events.confirm', 'TiLabeleditorComponent.events.cancel'] + }, + { + demoId: 'labeleditor-multiline-size', + name: { + 'zh-CN': '多行文本框可设置宽高', + 'en-US': 'labeleditor multiline size' + }, + desc: { + 'zh-CN': '

通过属性widthheight配置多行文本编辑框的宽高。

', + 'en-US': 'labeleditor multiline size' + }, + apis: ['TiLabeleditorComponent.properties.width', 'TiLabeleditorComponent.properties.height'] + }, + { + demoId: 'labeleditor-validation', + name: { + 'zh-CN': '同步校验', + 'en-US': 'labeleditor validation' + }, + desc: { + 'zh-CN': + '

通过属性validation配置校验相关信息,详见接口TiValidationConfig;通过属性validationRules配置校验规则。

', + 'en-US': 'labeleditor validation' + }, + apis: [ + 'TiLabeleditorComponent.properties.validation', + 'TiLabeleditorComponent.properties.validationRules', + 'TiValidationConfig.properties.errorMessage', + 'TiValidationConfig.properties.errorMessageWrapper', + 'TiValidationConfig.properties.passwordConfig', + 'TiValidationConfig.properties.tip', + 'TiValidationConfig.properties.tipMaxWidth', + 'TiValidationConfig.properties.tipPosition', + 'TiValidationConfig.properties.type' + ] + }, + { + demoId: 'labeleditor-validation-async', + name: { + 'zh-CN': '异步校验', + 'en-US': 'labeleditor validation async' + }, + desc: { + 'zh-CN': '

通过属性asyncValidatorRules配置单行文本编辑时异步校验规则。

', + 'en-US': 'labeleditor validation async' + }, + apis: ['TiLabeleditorComponent.properties.asyncValidatorRules'] + } + ] +}; diff --git a/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.cn.md b/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.cn.md new file mode 100644 index 0000000..68f90ed --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.cn.md @@ -0,0 +1,15 @@ +--- +title: Labeleditor 可编辑文本 +--- + +# Labeleditor 可编辑文本 + +
+ +主要实现了编辑文本功能的组件。 + +```typescript +import { TiLabeleditorModule } from '@opentiny/ng'; +``` + +
diff --git a/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.en.md b/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.en.md new file mode 100644 index 0000000..19e93ef --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.en.md @@ -0,0 +1,13 @@ +--- +title: Labeleditor +--- + +# Labeleditor + +
+ +```typescript +import { TiLabeleditorModule } from '@opentiny/ng'; +``` + +
diff --git a/src/labeleditor/demo/src/favicon.ico b/src/labeleditor/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d7a7af634ee6d643ac81d81feadff569ac25de3d GIT binary patch literal 300852 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 + + + + + + Tiny + + + + + + + + + diff --git a/src/labeleditor/demo/src/main.ts b/src/labeleditor/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/labeleditor/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/labeleditor/demo/tsconfig.app.json b/src/labeleditor/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/labeleditor/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/labeleditor/lib/index.ts b/src/labeleditor/lib/index.ts new file mode 100644 index 0000000..5e44df5 --- /dev/null +++ b/src/labeleditor/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiLabeleditorModule'; +export * from './src/TiLabeleditorComponent'; diff --git a/src/labeleditor/lib/ng-package.json b/src/labeleditor/lib/ng-package.json new file mode 100644 index 0000000..306040f --- /dev/null +++ b/src/labeleditor/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/labeleditor", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/labeleditor/lib/package.json b/src/labeleditor/lib/package.json new file mode 100644 index 0000000..cd219b9 --- /dev/null +++ b/src/labeleditor/lib/package.json @@ -0,0 +1,18 @@ +{ + "name": "@opentiny/ng-labeleditor", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@angular/forms": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-validation": "~1.0.0-beta.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-textarea": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/labeleditor/lib/project.json b/src/labeleditor/lib/project.json new file mode 100644 index 0000000..1e236a3 --- /dev/null +++ b/src/labeleditor/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/labeleditor/lib", + "sourceRoot": "src/labeleditor/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/labeleditor"], + "options": { + "project": "src/labeleditor/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/labeleditor"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js labeleditor" + }, + { + "command": "ng default-build labeleditor" + }, + { + "command": "node build/clear-default-theme.js labeleditor" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/labeleditor && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build labeleditor && ng pack labeleditor && node build/publish.js labeleditor --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/labeleditor/lib/src/TiLabeleditorComponent.ts b/src/labeleditor/lib/src/TiLabeleditorComponent.ts new file mode 100644 index 0000000..1befcad --- /dev/null +++ b/src/labeleditor/lib/src/TiLabeleditorComponent.ts @@ -0,0 +1,259 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AsyncValidatorFn, FormControl, ValidatorFn } from '@angular/forms'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Input, + Output, + Renderer2, + TemplateRef, + ViewChild +} from '@angular/core'; +import { Subscription } from 'rxjs'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiValidationConfig } from '@opentiny/ng-validation'; +import { TiTextComponent } from '@opentiny/ng-text'; +import { TiTextareaComponent } from '@opentiny/ng-textarea'; +import { Util } from '@opentiny/ng-utils'; +/** + * 可编辑文本组件,主要实现了编辑文本的功能。组件有两种状态:编辑状态和非编辑状态。 + * 在非编辑状态:点击右侧笔图标时,切换到编辑状态。 + * 在编辑状态:点击右侧对号图标时,切换到非编辑状态, + * 并且将输入框中内容显示在非编辑状态的文本中; + * 点击右侧叉图标时,切换到非编辑状态,非编辑状态的文本保持之前的不变,用户输入不生效。 + * + * ../tinyplus3demo/#/labeleditor/labeleditor-all + */ +@Component({ + selector: 'ti-labeleditor', + templateUrl: 'labeleditor.html', + styleUrls: ['./labeleditor.less'], + host: { + '[class.ti3-labeleditor-editing]': 'isEditing' + }, + providers: [TiFormComponent.getValueAccessor(TiLabeleditorComponent)], + changeDetection: ChangeDetectionStrategy.OnPush +}) +// extends TiFormComponent +export class TiLabeleditorComponent extends TiFormComponent { + constructor(protected elementRef: ElementRef, protected renderer2: Renderer2, protected changeDetectorRef: ChangeDetectorRef) { + super(elementRef, renderer2, changeDetectorRef); + } + /** + * 是否超出显示 tip + */ + @Input() autoTip: boolean = true; + /** + * 是否为多行编辑 + */ + @Input() multiline: boolean; + /** + * 允许调整文本框大小的方向: + * + * none(不可调整组件大小): + * + * vertical(仅可调整垂直方向的大小,即调整组件的高度) + * + * horizontal(仅可调节水平方向的大小,即调整组件的宽度) + * + * both(水平和垂直方向均可调节,宽高都可调节) + */ + @Input() resize: 'none' | 'vertical' | 'horizontal' | 'both' = 'both'; + /** + * 文本最大长度 + */ + @Input() maxlength: number; + /** + * 编辑图标 tip 内容 + */ + @Input() iconTip: string | TemplateRef | any; + /** + * 图标提示内容对应的上下文,tip 内容类型为 templateRef 或 Component 形式时使用 + */ + @Input() iconTipContext: any; + /** + * 校验相关信息 + */ + @Input() validation: TiValidationConfig; + /** + * 用户自定义同步校验规则 + * + * 接口值被当作 new FormControl() 的第二个参数 + * + * 接口类型与 new FormControl() 的第二个参数格式一致,详情参考 angular 表单部分 + */ + @Input() validationRules: Array; + /** + * + * 用户自定义异步校验规则(仅支持单行编辑) + * + * 接口值被当作 new FormControl() 的第三个参数 + * + * 接口类型与 new FormControl() 的第三个参数格式一致,详情参考 angular 表单部分 + */ + @Input() asyncValidatorRules: Array; + /** + * 文本域的宽度 + */ + @Input() width: string = '200px'; + /** + * 文本域的高度 + */ + @Input() height: string; + /** + * 非编辑状态的文本最大行数 + */ + @Input() maxLine: number = 3; + /** + * 点击编辑按钮触发的回调 + */ + @Output() readonly editor: EventEmitter = new EventEmitter(); + /** + * 点击确认按钮触发的回调 + */ + @Output() readonly confirm: EventEmitter = new EventEmitter(); + /** + * 点击取消按钮触发的回调 + */ + @Output() readonly cancel: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + @ViewChild('textarea', { static: false }) textareaComp: TiTextareaComponent; + /** + * @ignore + */ + @ViewChild('text', { static: false }) textComp: TiTextComponent; + /** + * @ignore + */ + @ViewChild('confirmIcon') confirmIconEle: ElementRef; + /** + * @ignore + */ + @ViewChild('edit', { static: false }) editEle: ElementRef; + /** + * @ignore + */ + public isEditing: boolean = false; // 是否处于编辑状态 + /** + * @ignore + */ + public value: string; // value内容 + /** + * @ignore + */ + public oldValue: string; // value内容 + /** + * @ignore + */ + public emptyValue: string = '--'; // 内容为空时显示‘--’ + /** + * @ignore + */ + public control: FormControl; + /** + * @ignore + */ + public valueInvalid: boolean = false; + private subscription: Subscription; + // tslint:disable-next-line:use-life-cycle-interface + ngOnInit(): void { + super.ngOnInit(); + // 如果显示文本框长度的话,文本框高度要加上最大长度文本的高度 18px + this.height = this.height ? this.height : this.maxlength ? '98px' : '80px'; + this.asyncValidatorRules = this.multiline ? null : this.asyncValidatorRules; + this.control = new FormControl(this.model, this.validationRules, this.asyncValidatorRules); + // 校验提示tip默认top方向展开 + if (this.validation) { + this.validation.tipPosition = Util.isUndefined(this.validation.tipPosition) ? 'top' : this.validation.tipPosition; + } + this.subscription = this.control.statusChanges.subscribe((status: string) => { + this.valueInvalid = status === 'INVALID' || status === 'PENDING'; + // 手动触发变更 + this.changeDetectorRef.markForCheck(); + }); + } + /** + * @ignore + */ + // tslint:disable-next-line:typedef + ngOnModelChange() { + if (this.model) { + this.control.setValue(this.model.slice(0, this.maxlength)); + } else { + this.control.setValue(''); + } + } + + // tslint:disable-next-line:use-life-cycle-interface + ngOnDestroy(): void { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + /** + * @ignore + * 点击编辑图标事件 + */ + public onClickEdit(): void { + if (this.disabled) { + return; + } + + this.isEditing = true; + this.oldValue = this.model; + this.editor.emit(this.model); + if (this.model) { + this.control.setValue(this.model.slice(0, this.maxlength)); + } + // 这里使用setTimeout是为了延时能够获取到编辑态的输入框 + setTimeout((): void => { + if (this.multiline) { + this.textareaComp.nativeElement.focus(); + if (Util.isUndefined(this.maxlength)) { + this.renderer2.removeAttribute(this.textareaComp.nativeElement, 'maxlength'); + } else { + this.renderer2.setAttribute(this.textareaComp.nativeElement, 'maxlength', String(this.maxlength)); + } + } else { + this.textComp.nativeElement.focus(); + } + }, 0); + } + /** + * @ignore + * 点击确认图标事件 + */ + public onClickConfirm(): void { + if (this.valueInvalid) { + return; + } + this.isEditing = false; + this.model = this.control.value; + this.confirm.emit(this.model); + } + /** + * @ignore + * 点击取消图标事件 + */ + public onClickCancel(): void { + this.isEditing = false; + this.cancel.emit(this.model); + this.control.setValue(this.oldValue); + } +} diff --git a/src/labeleditor/lib/src/TiLabeleditorModule.ts b/src/labeleditor/lib/src/TiLabeleditorModule.ts new file mode 100644 index 0000000..b5e6fe7 --- /dev/null +++ b/src/labeleditor/lib/src/TiLabeleditorModule.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TiLabeleditorComponent } from './TiLabeleditorComponent'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiTextareaModule } from '@opentiny/ng-textarea'; +import { TiValidationModule } from '@opentiny/ng-validation'; +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiOutlineModule, + TiIconModule, + TiOverflowModule, + TiIconModule, + TiTextareaModule, + TiTipModule, + TiTextModule, + TiValidationModule + ], + exports: [TiLabeleditorComponent], + declarations: [TiLabeleditorComponent] +}) +export class TiLabeleditorModule {} +export { TiLabeleditorComponent } from './TiLabeleditorComponent'; diff --git a/src/labeleditor/lib/src/labeleditor.html b/src/labeleditor/lib/src/labeleditor.html new file mode 100644 index 0000000..ce753ce --- /dev/null +++ b/src/labeleditor/lib/src/labeleditor.html @@ -0,0 +1,103 @@ + + + + + + + {{ model ? model : emptyValue}} + + + + + + + + + + + + + + + + + + diff --git a/src/labeleditor/lib/src/labeleditor.less b/src/labeleditor/lib/src/labeleditor.less new file mode 100644 index 0000000..a32d116 --- /dev/null +++ b/src/labeleditor/lib/src/labeleditor.less @@ -0,0 +1,65 @@ +.ti3-labeleditor-icon { + vertical-align: top; + line-height: 28px; + margin-left: var(--ti-common-space-2x); + cursor: pointer; +} +.ti3-icon-confirm { + color: var(--ti-common-color-success); +} +.ti3-icon-cancel { + color: var(--ti-common-color-error); +} + +.ti3-confirm-disabled.ti3-icon-confirm { + color: var(--ti-common-color-icon-disabled); + cursor: not-allowed; +} + +.ti3-labeleditor-container { + position: relative; + display: inline-block; +} +.ti3-labeleditor-textarea-container { + display: inline-block; +} + +.ti3-labeleditor-icon-container .ti3-labeleditor-icon { + vertical-align: middle; +} + +.ti3-editor-icon, +::ng-deep span[tiOverflowEndicon] { + font-size: var(--ti-common-font-size-2); + margin-left: var(--ti-common-space-2x); + color: var(--ti-common-color-icon-normal); + cursor: pointer; + &:hover { + color: var(--ti-common-color-icon-hover); + } +} + +// 校验失败提示需要换行呈现 +.ti3-editor-errorMessageWrapper { + line-height: 16px; + display: block; +} + +// 禁用样式 +:host[disabled] { + .ti3-editor-icon, + ::ng-deep span[tiOverflowEndicon] { + color: var(--ti-common-color-icon-disabled); + cursor: not-allowed; + } +} + +:host { + word-wrap: break-word; + word-break: break-word; +} + +:hostti-labeleditor.ti3-labeleditor-editing { + white-space: nowrap; + line-height: 0; // 编辑状态,同步失焦校验 异步校验消除中间的空白 +} diff --git a/src/layout/demo/karma.conf.js b/src/layout/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/layout/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/layout/demo/project.json b/src/layout/demo/project.json new file mode 100644 index 0000000..66093cd --- /dev/null +++ b/src/layout/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/layout/demo", + "sourceRoot": "src/layout/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/layout", + "index": "src/layout/demo/src/index.html", + "main": "src/layout/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/layout/demo/tsconfig.app.json", + "assets": ["src/layout/demo/src/favicon.ico", "src/layout/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "layout-demo:build:production" + }, + "development": { + "browserTarget": "layout-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js layout" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/layout/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/layout/demo/tsconfig.spec.json", + "karmaConfig": "src/layout/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/layout/demo/src/app/AppComponent.ts b/src/layout/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/layout/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/layout/demo/src/app/AppModule.ts b/src/layout/demo/src/app/AppModule.ts new file mode 100644 index 0000000..cf90703 --- /dev/null +++ b/src/layout/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { LayoutTestModule } from './layout/LayoutTestModule'; + +@NgModule({ + imports: [ + LayoutTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/layout/demo/src/app/IndexComponent.ts b/src/layout/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..4fb3937 --- /dev/null +++ b/src/layout/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { LayoutTestModule } from './layout/LayoutTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = LayoutTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/layout/demo/src/app/app.html b/src/layout/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/layout/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/layout/demo/src/app/layout/LayoutBasicComponent.ts b/src/layout/demo/src/app/layout/LayoutBasicComponent.ts new file mode 100644 index 0000000..661c7f7 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutBasicComponent.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './layout-basic.html', + styleUrls: ['./layout-basic.less'] +}) +export class LayoutBasicComponent { + // 左侧菜单配置 + marginLeft: string = '192px'; + config: any = { + serviceName: '菜单栏', + reloadState: true, // 初始设置为true + toggleable: true, + collapsed: false, // 默认展开,当设置为true时会收起 + collapsedChangeFn: (collapsed: boolean): void => { + this.marginLeft = collapsed ? '0' : '192px'; + } + }; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + active: TiLeftmenuItem; +} diff --git a/src/layout/demo/src/app/layout/LayoutBasicSimpleComponent.ts b/src/layout/demo/src/app/layout/LayoutBasicSimpleComponent.ts new file mode 100644 index 0000000..f9794a1 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutBasicSimpleComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './layout-basic-simple.html', + styleUrls: ['./layout-simple.less'] +}) +export class LayoutBasicSimpleComponent {} diff --git a/src/layout/demo/src/app/layout/LayoutBasicSimpleResponsiveComponent.ts b/src/layout/demo/src/app/layout/LayoutBasicSimpleResponsiveComponent.ts new file mode 100644 index 0000000..d4fede8 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutBasicSimpleResponsiveComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './layout-basic-simple-responsive.html', + styleUrls: ['./layout-simple.less'] +}) +export class LayoutBasicSimpleResponsiveComponent {} diff --git a/src/layout/demo/src/app/layout/LayoutDetailColumnComponent.ts b/src/layout/demo/src/app/layout/LayoutDetailColumnComponent.ts new file mode 100644 index 0000000..db86749 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutDetailColumnComponent.ts @@ -0,0 +1,61 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './layout-detail-column.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutDetailColumnComponent { + public items1: Array = [ + { + id: '1', + label: 'ECS-name-1' + } + ]; + public textForm1: any = { + // 多列 + colsNumber: 2, + colsGap: ['150px'], + + fieldVerticalAlign: 'middle', + + firstItem: { + label: 'ID:', + value: '9fd3121221-fa4112' + }, + secondItem: { + label: '名称:', + value: '某某产品' + }, + thirdItem: { + label: '描述:', + value: 'some support services' + }, + fourthItem: { + label: '启动盘:', + value: '是' + }, + fifthItem: { + label: '创建时间:', + value: '2015-06-20 21:13' + }, + sixthItem: { + label: '挂载点:', + value: '/dev/happy' + } + }; + public tabs = [ + { + title: '基本信息', + content: + "Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse." + }, + { title: '云硬盘', content: 'Dynamic content 2' }, + { title: '网卡', content: 'Dynamic content 2' }, + { title: '安全组', content: 'Dynamic content 2' }, + { title: '弹性公网IP', content: 'Dynamic content 2' }, + { title: '监控', content: 'Dynamic content 2' }, + { title: '标签', content: 'Dynamic content 2' } + ]; +} diff --git a/src/layout/demo/src/app/layout/LayoutDetailComponent.ts b/src/layout/demo/src/app/layout/LayoutDetailComponent.ts new file mode 100644 index 0000000..9138701 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutDetailComponent.ts @@ -0,0 +1,69 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './layout-detail.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutDetailComponent { + public items: Array = [ + { + id: '1', + label: 'ECS-name-1' + }, + { + id: '2', + label: 'ECS-name-2' + }, + { + id: '3', + label: 'ECS-name-3' + } + ]; + public textForm1: any = { + // 多列 + colsNumber: 2, + colsGap: ['50%', '50%'], + + fieldVerticalAlign: 'middle', + + firstItem: { + label: 'ID:', + value: '9fd3121221-fa4112' + }, + secondItem: { + label: '名称:', + value: '某某产品' + }, + thirdItem: { + label: '描述:', + value: 'some support services' + }, + fourthItem: { + label: '启动盘:', + value: '是' + }, + fifthItem: { + label: '创建时间:', + value: '2015-06-20 21:13' + }, + sixthItem: { + label: '挂载点:', + value: '/dev/happy' + } + }; + public tabs = [ + { + title: '基本信息', + content: + "Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse." + }, + { title: '云硬盘', content: 'Dynamic content 2' }, + { title: '网卡', content: 'Dynamic content 2' }, + { title: '安全组', content: 'Dynamic content 2' }, + { title: '弹性公网IP', content: 'Dynamic content 2' }, + { title: '监控', content: 'Dynamic content 2' }, + { title: '标签', content: 'Dynamic content 2' } + ]; +} diff --git a/src/layout/demo/src/app/layout/LayoutListComponent.ts b/src/layout/demo/src/app/layout/LayoutListComponent.ts new file mode 100644 index 0000000..bda86d7 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutListComponent.ts @@ -0,0 +1,157 @@ +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { TiActionmenuItem, TiLeftmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './layout-list.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutListComponent implements OnInit { + public marginLeft: string = '192px'; + public config: any = { + serviceName: '云服务器控制台', + reloadState: true, // 初始设置为true + toggleable: true, + collapsed: false, // 默认展开,当设置为true时会收起 + collapsedChangeFn: (collapsed: boolean): void => { + this.marginLeft = collapsed ? '0' : '192px'; + } + }; + public items: Array = [ + { + label: '总览', + children: [ + { + label: '二级菜单1' + }, + { + label: '二级菜单2' + } + ] + }, + { + label: '弹性云服务器', + children: [ + { + label: '二级菜单' + }, + { + label: '二级' + } + ] + }, + { + label: '裸金属服务器' + } + ]; + public active: TiLeftmenuItem; + + public displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + public srcData: TiTableSrcData; + private data: Array = []; + public columns: Array = [ + { + label: 'first name', + width: '25%' + }, + { + label: 'last name', + width: '20%' + }, + { + label: 'birth date', + width: '20%' + }, + { + label: 'state', + width: '15%' + }, + { + label: 'email', + width: '20%' + } + ]; + public currentPage: number = 1; + public totalNumber: number = 62; + public pageSize: { options: Array; size: number } = { + options: [10, 20, 50, 100], + size: 10 + }; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、过滤、分页等操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为$scope.displayed显示出来 + // 本示例中,开发者没有设置分页、过滤和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + public onSelect(item: any): void { + console.log('onSelect()'); + console.log(item); + } + + public dataToItemsFn: (data: any) => Array = (data: any) => { + let items: Array = [ + { + label: '启用', + association: 'switch', + disabled: true + }, + { + label: '禁用', + association: 'switch' + }, + { + label: '制作镜像' + }, + { + label: '这是一个很长的选项' + }, + { + label: '制作镜像2', + disabled: true + } + ]; + if (data.state === '已停止') { + items[0].disabled = false; + items[1].disabled = true; + items = [items[0], items[1]]; + } + + return items; + }; + + private createRandomItem(id: number): any { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[Math.floor(Math.random() * 4)]; + const lastName: string = familyName[Math.floor(Math.random() * 4)]; + const age: number = Math.floor(Math.random() * 100); + const state: string = ['已启动', '已停止'][Math.floor(Math.random() * 2)]; + + return { + firstName, + lastName, + age, + state, + id + }; + } + + public trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/layout/demo/src/app/layout/LayoutListLargedataComponent.ts b/src/layout/demo/src/app/layout/LayoutListLargedataComponent.ts new file mode 100644 index 0000000..ba575bf --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutListLargedataComponent.ts @@ -0,0 +1,153 @@ +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { TiActionmenuItem, TiLeftmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './layout-list-largedata.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutListLargedataComponent implements OnInit { + public marginLeft: string = '192px'; + public config: any = { + serviceName: '云服务器控制台', + reloadState: true, // 初始设置为true + toggleable: true, + collapsed: false, // 默认展开,当设置为true时会收起 + collapsedChangeFn: (collapsed: boolean): void => { + this.marginLeft = collapsed ? '0' : '192px'; + } + }; + public items: Array = [ + { + label: '总览', + children: [ + { + label: '二级菜单1' + }, + { + label: '二级菜单2' + } + ] + }, + { + label: '弹性云服务器', + children: [ + { + label: '二级菜单' + }, + { + label: '二级' + } + ] + }, + { + label: '裸金属服务器' + } + ]; + public active: TiLeftmenuItem; + + public displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + public srcData: TiTableSrcData; + private data: Array = []; + public columns: Array = [ + { + label: 'first name', + width: '25%' + }, + { + label: 'last name', + width: '20%' + }, + { + label: 'birth date', + width: '20%' + }, + { + label: 'state', + width: '15%' + }, + { + label: 'email', + width: '20%' + } + ]; + public currentPage: number = 1; + public totalNumber: number = 800; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、过滤、分页等操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为$scope.displayed显示出来 + // 本示例中,开发者没有设置分页、过滤和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + public onSelect(item: any): void { + console.log('onSelect()'); + console.log(item); + } + + public dataToItemsFn: (data: any) => Array = (data: any) => { + let items: Array = [ + { + label: '启用', + association: 'switch', + disabled: true + }, + { + label: '禁用', + association: 'switch' + }, + { + label: '制作镜像' + }, + { + label: '这是一个很长的选项' + }, + { + label: '制作镜像2', + disabled: true + } + ]; + if (data.state === '已停止') { + items[0].disabled = false; + items[1].disabled = true; + items = [items[0], items[1]]; + } + + return items; + }; + + private createRandomItem(id: number): any { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[Math.floor(Math.random() * 4)]; + const lastName: string = familyName[Math.floor(Math.random() * 4)]; + const age: number = Math.floor(Math.random() * 100); + const state: string = ['已启动', '已停止'][Math.floor(Math.random() * 2)]; + + return { + firstName, + lastName, + age, + state, + id + }; + } + + public trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/layout/demo/src/app/layout/LayoutMultiColumnComponent.ts b/src/layout/demo/src/app/layout/LayoutMultiColumnComponent.ts new file mode 100644 index 0000000..6064cf2 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutMultiColumnComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './layout-multi-column.html', + styleUrls: ['./layout-column.less'] +}) +export class LayoutMultiColumnComponent {} diff --git a/src/layout/demo/src/app/layout/LayoutOverviewComponent.ts b/src/layout/demo/src/app/layout/LayoutOverviewComponent.ts new file mode 100644 index 0000000..e4b1e96 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutOverviewComponent.ts @@ -0,0 +1,49 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './layout-overview.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutOverviewComponent { + // 左侧菜单配置 + public marginLeft: string = '192px'; + public config: any = { + serviceName: '云服务器控制台', + reloadState: true, // 初始设置为true + toggleable: true, + collapsed: false, // 默认展开,当设置为true时会收起 + collapsedChangeFn: (collapsed: boolean): void => { + this.marginLeft = collapsed ? '0' : '192px'; + } + }; + public items: Array = [ + { + label: '总览', + children: [ + { + label: '二级菜单1' + }, + { + label: '二级菜单2' + } + ] + }, + { + label: '弹性云服务器', + children: [ + { + label: '二级菜单' + }, + { + label: '二级' + } + ] + }, + { + label: '裸金属服务器' + } + ]; + public active: TiLeftmenuItem; +} diff --git a/src/layout/demo/src/app/layout/LayoutOverviewVerticalComponent.ts b/src/layout/demo/src/app/layout/LayoutOverviewVerticalComponent.ts new file mode 100644 index 0000000..534815c --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutOverviewVerticalComponent.ts @@ -0,0 +1,49 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './layout-overview-vertical.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutOverviewVerticalComponent { + // 左侧菜单配置 + public marginLeft: string = '192px'; + public config: any = { + serviceName: '云服务器控制台', + reloadState: true, // 初始设置为true + toggleable: true, + collapsed: false, // 默认展开,当设置为true时会收起 + collapsedChangeFn: (collapsed: boolean): void => { + this.marginLeft = collapsed ? '0' : '192px'; + } + }; + public items: Array = [ + { + label: '总览', + children: [ + { + label: '二级菜单1' + }, + { + label: '二级菜单2' + } + ] + }, + { + label: '弹性云服务器', + children: [ + { + label: '二级菜单' + }, + { + label: '二级' + } + ] + }, + { + label: '裸金属服务器' + } + ]; + public active: TiLeftmenuItem; +} diff --git a/src/layout/demo/src/app/layout/LayoutPurchaseComponent.ts b/src/layout/demo/src/app/layout/LayoutPurchaseComponent.ts new file mode 100644 index 0000000..3a807ba --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutPurchaseComponent.ts @@ -0,0 +1,52 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './layout-purchase.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutPurchaseComponent { + // 返回标题 + public items: Array = [ + { + label: 'item1 test 测试长标题 测试长标题 测试长标题' + } + ]; + + // 步骤 + public steps: Array = [ + { + label: 'General' + }, + { + label: 'Host Group' + }, + { + label: 'Policy' + }, + { + label: 'Names' + } + ]; + public activeStep: any = this.steps[0]; + + public format: string = 'N0'; + public spinnerValue: number = 1; + + public myOptions: Array = [ + { label: '一个月' }, + { label: '二个月' }, + { label: '三个月' }, + { label: '四个月' }, + { label: '五个月' }, + { label: '六个月' }, + { label: '七个月' }, + { label: '八个月' }, + { label: '九个月' }, + { label: '一年' }, + { label: '两年' }, + { label: '三年' } + ]; + + public mySelected1: any = this.myOptions[3]; +} diff --git a/src/layout/demo/src/app/layout/LayoutPurchaseResponsiveChangeComponent.ts b/src/layout/demo/src/app/layout/LayoutPurchaseResponsiveChangeComponent.ts new file mode 100644 index 0000000..bf3f864 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutPurchaseResponsiveChangeComponent.ts @@ -0,0 +1,56 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './layout-purchase-responsive-change.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutPurchaseResponsiveChangeComponent { + public responsive: boolean = false; + // 返回标题 + public items: Array = [ + { + label: 'item1 test 测试长标题 测试长标题 测试长标题' + } + ]; + + // 步骤 + public steps: Array = [ + { + label: 'General' + }, + { + label: 'Host Group' + }, + { + label: 'Policy' + }, + { + label: 'Names' + } + ]; + public activeStep: any = this.steps[0]; + public format: string = 'N0'; + public spinnerValue: number = 1; + + public myOptions: Array = [ + { label: '一个月' }, + { label: '二个月' }, + { label: '三个月' }, + { label: '四个月' }, + { label: '五个月' }, + { label: '六个月' }, + { label: '七个月' }, + { label: '八个月' }, + { label: '九个月' }, + { label: '一年' }, + { label: '两年' }, + { label: '三年' } + ]; + + public mySelected1: any = this.myOptions[3]; + + public changeResponsive(): void { + this.responsive = !this.responsive; + } +} diff --git a/src/layout/demo/src/app/layout/LayoutPurchaseResponsiveComponent.ts b/src/layout/demo/src/app/layout/LayoutPurchaseResponsiveComponent.ts new file mode 100644 index 0000000..b66c689 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutPurchaseResponsiveComponent.ts @@ -0,0 +1,51 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './layout-purchase-responsive.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutPurchaseResponsiveComponent { + // 返回标题 + public items: Array = [ + { + label: 'item1 test 测试长标题 测试长标题 测试长标题' + } + ]; + + // 步骤 + public steps: Array = [ + { + label: 'General' + }, + { + label: 'Host Group' + }, + { + label: 'Policy' + }, + { + label: 'Names' + } + ]; + public activeStep: any = this.steps[0]; + public format: string = 'N0'; + public spinnerValue: number = 1; + + public myOptions: Array = [ + { label: '一个月' }, + { label: '二个月' }, + { label: '三个月' }, + { label: '四个月' }, + { label: '五个月' }, + { label: '六个月' }, + { label: '七个月' }, + { label: '八个月' }, + { label: '九个月' }, + { label: '一年' }, + { label: '两年' }, + { label: '三年' } + ]; + + public mySelected1: any = this.myOptions[3]; +} diff --git a/src/layout/demo/src/app/layout/LayoutPurchaseSimpleComponent.ts b/src/layout/demo/src/app/layout/LayoutPurchaseSimpleComponent.ts new file mode 100644 index 0000000..7ccb713 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutPurchaseSimpleComponent.ts @@ -0,0 +1,14 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './layout-purchase-simple.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutPurchaseSimpleComponent { + public items: Array = [ + { + label: 'item1 test 测试长标题 测试长标题 测试长标题' + } + ]; +} diff --git a/src/layout/demo/src/app/layout/LayoutPurchaseSimpleResponsiveComponent.ts b/src/layout/demo/src/app/layout/LayoutPurchaseSimpleResponsiveComponent.ts new file mode 100644 index 0000000..061e534 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutPurchaseSimpleResponsiveComponent.ts @@ -0,0 +1,14 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './layout-purchase-simple-responsive.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutPurchaseSimpleResponsiveComponent { + public items: Array = [ + { + label: 'item1 test 测试长标题 测试长标题 测试长标题' + } + ]; +} diff --git a/src/layout/demo/src/app/layout/LayoutSingleComponent.ts b/src/layout/demo/src/app/layout/LayoutSingleComponent.ts new file mode 100644 index 0000000..da71aab --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutSingleComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './layout-single.html', + styleUrls: ['./layout-simple.less'] +}) +export class LayoutSingleComponent {} diff --git a/src/layout/demo/src/app/layout/LayoutTestModule.ts b/src/layout/demo/src/app/layout/LayoutTestModule.ts new file mode 100644 index 0000000..0583def --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutTestModule.ts @@ -0,0 +1,160 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { + TiActionmenuModule, + TiAlertModule, + TiButtonModule, + TiFormfieldModule, + TiLayoutModule, + TiLeftmenuModule, + TiPaginationModule, + TiSelectModule, + TiSpinnerModule, + TiStepsModule, + TiSubtitleModule, + TiTableModule, + TiTabModule +} from '@opentiny/ng'; + +import { LayoutOverviewComponent } from './LayoutOverviewComponent'; +import { LayoutBasicComponent } from './LayoutBasicComponent'; +import { LayoutBasicSimpleComponent } from './LayoutBasicSimpleComponent'; +import { LayoutBasicSimpleResponsiveComponent } from './LayoutBasicSimpleResponsiveComponent'; +import { LayoutSingleComponent } from './LayoutSingleComponent'; +import { LayoutMultiColumnComponent } from './LayoutMultiColumnComponent'; +import { LayoutListComponent } from './LayoutListComponent'; +import { LayoutPurchaseComponent } from './LayoutPurchaseComponent'; +import { LayoutPurchaseSimpleComponent } from './LayoutPurchaseSimpleComponent'; +import { LayoutPurchaseResponsiveComponent } from './LayoutPurchaseResponsiveComponent'; +import { LayoutPurchaseSimpleResponsiveComponent } from './LayoutPurchaseSimpleResponsiveComponent'; +import { LayoutDetailComponent } from './LayoutDetailComponent'; +import { LayoutDetailColumnComponent } from './LayoutDetailColumnComponent'; +import { LayoutOverviewVerticalComponent } from './LayoutOverviewVerticalComponent'; +import { LayoutListLargedataComponent } from './LayoutListLargedataComponent'; +import { LayoutPurchaseResponsiveChangeComponent } from './LayoutPurchaseResponsiveChangeComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiActionmenuModule, + TiAlertModule, + TiButtonModule, + TiLeftmenuModule, + TiPaginationModule, + TiStepsModule, + TiTableModule, + TiSelectModule, + TiSpinnerModule, + TiLayoutModule, + TiSubtitleModule, + TiFormfieldModule, + TiTabModule, + RouterModule.forChild(LayoutTestModule.ROUTES) + ], + declarations: [ + LayoutOverviewComponent, + LayoutBasicComponent, + LayoutBasicSimpleComponent, + LayoutBasicSimpleResponsiveComponent, + LayoutSingleComponent, + LayoutMultiColumnComponent, + LayoutListComponent, + LayoutPurchaseComponent, + LayoutPurchaseSimpleComponent, + LayoutPurchaseResponsiveComponent, + LayoutPurchaseSimpleResponsiveComponent, + LayoutDetailComponent, + LayoutDetailColumnComponent, + LayoutOverviewVerticalComponent, + LayoutListLargedataComponent, + LayoutPurchaseResponsiveChangeComponent + ] +}) +export class LayoutTestModule { + public static readonly ROUTES: Routes = [ + { + path: 'layout/layout-overview', + component: LayoutOverviewComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-basic', + component: LayoutBasicComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-basic-simple', + component: LayoutBasicSimpleComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-basic-simple-responsive', + component: LayoutBasicSimpleResponsiveComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-overview-vertical', + component: LayoutOverviewVerticalComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-list', + component: LayoutListComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-list-largedata', + component: LayoutListLargedataComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-purchase', + component: LayoutPurchaseComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-purchase-simple', + component: LayoutPurchaseSimpleComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-purchase-responsive', + component: LayoutPurchaseResponsiveComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-purchase-simple-responsive', + component: LayoutPurchaseSimpleResponsiveComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-detail', + component: LayoutDetailComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-detail-column', + component: LayoutDetailColumnComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-single', + component: LayoutSingleComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-multi-column', + component: LayoutMultiColumnComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-purchase-responsive-change', + component: LayoutPurchaseResponsiveChangeComponent, + data: { less: 'layout' } + } + ]; +} diff --git a/src/layout/demo/src/app/layout/layout-basic-simple-responsive.html b/src/layout/demo/src/app/layout/layout-basic-simple-responsive.html new file mode 100644 index 0000000..1a7810c --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-basic-simple-responsive.html @@ -0,0 +1,14 @@ + +
Header
+
+ + + + +
Content
+
+ +
Content
+
+
+
diff --git a/src/layout/demo/src/app/layout/layout-basic-simple.html b/src/layout/demo/src/app/layout/layout-basic-simple.html new file mode 100644 index 0000000..c6ae06f --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-basic-simple.html @@ -0,0 +1,14 @@ + +
Header
+
+ + + + +
Content
+
+ +
Content
+
+
+
diff --git a/src/layout/demo/src/app/layout/layout-basic.html b/src/layout/demo/src/app/layout/layout-basic.html new file mode 100644 index 0000000..a64eb68 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-basic.html @@ -0,0 +1,31 @@ + +
+ + +
{{config.serviceName}}
+
+ + +
{{m1.label}}
+
+ + {{m2.label}} + +
+
+ + + Header + + Content + Aside + + +
diff --git a/src/layout/demo/src/app/layout/layout-basic.less b/src/layout/demo/src/app/layout/layout-basic.less new file mode 100644 index 0000000..b203a19 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-basic.less @@ -0,0 +1,30 @@ +.layout-content { + background: #eee !important; + padding: 20px 20px 0 20px !important; +} + +.layout-header { + background: #fff; + margin-bottom: 12px; + text-align: center; +} + +.basic-section { + width: calc(100% - 183px - 12px); + text-align: center; + line-height: 300px; +} + +.basic-aside { + width: 183px; + margin-left: 12px; + text-align: center; + line-height: 300px; +} + +// 示例呈现效果,开发者不用关注 +.clear-transform { + transform: scale(1, 1); + height: 460px; + overflow: auto; +} diff --git a/src/layout/demo/src/app/layout/layout-column.less b/src/layout/demo/src/app/layout/layout-column.less new file mode 100644 index 0000000..d2d83ed --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-column.less @@ -0,0 +1,18 @@ +.text-color { + color: #fff; +} + +.column-left { + width: calc(45% - 12px); +} + +.column-right { + width: 55%; +} + +.content-section { + background-color: #eee; + height: 200px; + text-align: center; + line-height: 200px; +} diff --git a/src/layout/demo/src/app/layout/layout-detail-column.html b/src/layout/demo/src/app/layout/layout-detail-column.html new file mode 100644 index 0000000..6adbbbc --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-detail-column.html @@ -0,0 +1,67 @@ + + + + + +
+ + + +
+ + + + + + + + + + + + + {{textForm1.firstItem.value}} + + + +
{{textForm1.secondItem.label}}
+
+ {{textForm1.secondItem.value}} +
+ + {{textForm1.thirdItem.label}} + {{textForm1.thirdItem.value}} + + + {{textForm1.fourthItem.value}} + + + {{textForm1.fifthItem.label}} + {{textForm1.fifthItem.value}} + + + {{textForm1.sixthItem.label}} + {{textForm1.sixthItem.value}} + +
+
+ +
Detail Content
+
+ +
Detail Content
+
+
+ + + +
+ linklist +
+
+ +
Detail Content
+
+
+
+
diff --git a/src/layout/demo/src/app/layout/layout-detail.html b/src/layout/demo/src/app/layout/layout-detail.html new file mode 100644 index 0000000..1609fa0 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-detail.html @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + {{textForm1.firstItem.value}} + + + +
{{textForm1.secondItem.label}}
+
+ {{textForm1.secondItem.value}} +
+ + {{textForm1.thirdItem.label}} + {{textForm1.thirdItem.value}} + + + {{textForm1.fourthItem.value}} + + + {{textForm1.fifthItem.label}} + {{textForm1.fifthItem.value}} + + + {{textForm1.sixthItem.label}} + {{textForm1.sixthItem.value}} + +
+
+ +
+ + + +
+
Detail Content
+
+ +
Detail Content
+
+
+
diff --git a/src/layout/demo/src/app/layout/layout-list-largedata.html b/src/layout/demo/src/app/layout/layout-list-largedata.html new file mode 100644 index 0000000..97f8a22 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-list-largedata.html @@ -0,0 +1,57 @@ + + + + +
{{config.serviceName}}
+
+ + +
{{m1.label}}
+
+ + {{m2.label}} + +
+
+ + + + + + descandbuy + + 我是业务侧自定义内容 + + + + + + + + + + + + + + + + + + + +
{{column.label}}
{{row.firstName}}{{row.lastName}}{{row.age}}{{row.state}} + +
+ +
+
+
+
diff --git a/src/layout/demo/src/app/layout/layout-list.html b/src/layout/demo/src/app/layout/layout-list.html new file mode 100644 index 0000000..8b6e497 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-list.html @@ -0,0 +1,56 @@ + + + + +
{{config.serviceName}}
+
+ + +
{{m1.label}}
+
+ + {{m2.label}} + +
+
+ + + + + descandbuy + + 我是业务侧自定义内容 + + + + + + + + + + + + + + + + + + + +
{{column.label}}
{{row.firstName}}{{row.lastName}}{{row.age}}{{row.state}} + +
+ +
+
+
+
diff --git a/src/layout/demo/src/app/layout/layout-multi-column.html b/src/layout/demo/src/app/layout/layout-multi-column.html new file mode 100644 index 0000000..6815f30 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-multi-column.html @@ -0,0 +1,21 @@ + + +
Header
+
+ + + + +
Content
+
+ +
Content
+
+
+ + +
Content
+
+
+
+
diff --git a/src/layout/demo/src/app/layout/layout-overview-vertical.html b/src/layout/demo/src/app/layout/layout-overview-vertical.html new file mode 100644 index 0000000..b7e6628 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-overview-vertical.html @@ -0,0 +1,36 @@ + + + + + +
{{config.serviceName}}
+
+ + +
{{m1.label}}
+
+ + {{m2.label}} + +
+
+ + + + + descandbuy + + + 资源列表 resources + + linklist + + + diff --git a/src/layout/demo/src/app/layout/layout-overview.html b/src/layout/demo/src/app/layout/layout-overview.html new file mode 100644 index 0000000..f6aeeff --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-overview.html @@ -0,0 +1,38 @@ + + + + +
{{config.serviceName}}
+
+ + +
{{m1.label}}
+
+ + {{m2.label}} + +
+
+ + + + + descandbuy + + + + + 资源列表 resources + + + linklist + + + diff --git a/src/layout/demo/src/app/layout/layout-purchase-responsive-change.html b/src/layout/demo/src/app/layout/layout-purchase-responsive-change.html new file mode 100644 index 0000000..4fbf479 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-purchase-responsive-change.html @@ -0,0 +1,41 @@ + + + + + + + + +
+ + + + + + + + + + + +
+ +
+ buy content1 + +
+
+ +
buy content1
+
+ +
buy content1
+
+ +
buy content1
+
+
+
+
+
购买浮层 buylayer
+
diff --git a/src/layout/demo/src/app/layout/layout-purchase-responsive.html b/src/layout/demo/src/app/layout/layout-purchase-responsive.html new file mode 100644 index 0000000..5ebb4d1 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-purchase-responsive.html @@ -0,0 +1,40 @@ + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
buy content1
+
+ +
buy content1
+
+ +
buy content1
+
+ +
buy content1
+
+
+
+
+
购买浮层 buylayer
+
diff --git a/src/layout/demo/src/app/layout/layout-purchase-simple-responsive.html b/src/layout/demo/src/app/layout/layout-purchase-simple-responsive.html new file mode 100644 index 0000000..c153904 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-purchase-simple-responsive.html @@ -0,0 +1,26 @@ + + + + + + + + +
+ + + + + + + + + +
buy content1
+
+ +
buy content1
+
+
+
+
diff --git a/src/layout/demo/src/app/layout/layout-purchase-simple.html b/src/layout/demo/src/app/layout/layout-purchase-simple.html new file mode 100644 index 0000000..2f6d58b --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-purchase-simple.html @@ -0,0 +1,21 @@ + + + +
+ + + + + + + + + +
buy content1
+
+ +
buy content1
+
+
+
+
diff --git a/src/layout/demo/src/app/layout/layout-purchase.html b/src/layout/demo/src/app/layout/layout-purchase.html new file mode 100644 index 0000000..1342c56 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-purchase.html @@ -0,0 +1,34 @@ + + + +
+ + + + + + + + + + + + +
+ +
buy content1
+
+ +
buy content1
+
+ +
buy content1
+
+ +
buy content1
+
+
+
+
+
购买浮层 buylayer
+
diff --git a/src/layout/demo/src/app/layout/layout-simple.less b/src/layout/demo/src/app/layout/layout-simple.less new file mode 100644 index 0000000..b0654a2 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-simple.less @@ -0,0 +1,14 @@ +.text-color { + color: #fff; +} + +.padding-bottom { + padding-bottom: 12px; +} + +.content-section { + background-color: #eee; + height: 200px; + text-align: center; + line-height: 200px; +} diff --git a/src/layout/demo/src/app/layout/layout-single.html b/src/layout/demo/src/app/layout/layout-single.html new file mode 100644 index 0000000..01ea615 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-single.html @@ -0,0 +1,14 @@ + + +
Header
+
+ + + +
Content
+
+ +
Content
+
+
+
diff --git a/src/layout/demo/src/app/layout/layout.less b/src/layout/demo/src/app/layout/layout.less new file mode 100644 index 0000000..85b0012 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout.less @@ -0,0 +1,21 @@ +html, +body { + height: 100%; + background-color: #eef0f5; +} + +#section { + width: calc(100% - 183px - 12px); +} + +#aside { + width: 183px; + margin-left: 12px; +} +#column_left { + width: calc(45% - 12px); +} + +#column_right { + width: 55%; +} diff --git a/src/layout/demo/src/app/layout/webdoc/layout-demos.js b/src/layout/demo/src/app/layout/webdoc/layout-demos.js new file mode 100644 index 0000000..ae6e104 --- /dev/null +++ b/src/layout/demo/src/app/layout/webdoc/layout-demos.js @@ -0,0 +1,90 @@ +export default { + column: '1', + demos: [ + { + demoId: 'layout-basic', + name: { + 'zh-CN': '基本布局', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': '', + 'en-US': '

basic

' + }, + codeFiles: [ + 'layout-basic.html', + 'LayoutBasicComponent.ts', + 'layout-basic.less' + ] + }, + { + demoId: 'layout-single', + name: { + 'zh-CN': '单列布局', + 'en-US': 'single' + }, + desc: { + 'zh-CN': '', + 'en-US': '

single

' + }, + codeFiles: [ + 'layout-single.html', + 'LayoutSingleComponent.ts', + 'layout-simple.less' + ] + }, + { + demoId: 'layout-multi-column', + name: { + 'zh-CN': '多列布局', + 'en-US': 'multiColumn' + }, + desc: { + 'zh-CN': '', + 'en-US': '

multiColumn

' + }, + codeFiles: [ + 'layout-multi-column.html', + 'LayoutMultiColumnComponent.ts', + 'layout-column.less' + ] + }, + { + demoId: 'layout-basic-simple', + name: { + 'zh-CN': '固定居中布局', + 'en-US': 'basic-simple' + }, + desc: { + 'zh-CN': '

固定居中布局内容区宽度一直固定为1192px

', + 'en-US': '

basic-simple

' + }, + codeFiles: [ + 'layout-basic-simple.html', + 'LayoutBasicSimpleComponent.ts', + 'layout-simple.less' + ] + }, + { + demoId: 'layout-basic-simple-responsive', + name: { + 'zh-CN': '响应式布局', + 'en-US': 'basic-simple-responsive' + }, + desc: { + 'zh-CN': + '

响应式布局内容区宽度自适应策略:2560 ≥ 分辨率 > 1920 ,内容区宽度固定为1832px1920 ≥ 分辨率 ≥ 1280,左右间距固定为 20px,内容区宽度自适应;分辨率 < 1280时,显示不下则出滚动条。

', + 'en-US': '

basic-simple-responsive

' + }, + apis: [ + 'TiLayoutContentComponent.properties.responsive', + 'TiLayoutHeaderComponent.properties.responsive' + ], + codeFiles: [ + 'layout-basic-simple-responsive.html', + 'LayoutBasicSimpleResponsiveComponent.ts', + 'layout-simple.less' + ] + }, + ], +} diff --git a/src/layout/demo/src/app/layout/webdoc/layout.cn.md b/src/layout/demo/src/app/layout/webdoc/layout.cn.md new file mode 100644 index 0000000..de5536e --- /dev/null +++ b/src/layout/demo/src/app/layout/webdoc/layout.cn.md @@ -0,0 +1,23 @@ +--- +title: Layout 布局 +--- +# Layout 布局 + +
+ +布局组件,提供整个页面内容区域的布局。   + +```typescript +import { TiLayoutModule } from '@opentiny/ng'; +``` + +
+ +
+ +布局组件,提供整个页面内容区域的布局。   + +```typescript +import { TiLayoutModule } from '@opentiny/ng'; +``` +
diff --git a/src/layout/demo/src/app/layout/webdoc/layout.en.md b/src/layout/demo/src/app/layout/webdoc/layout.en.md new file mode 100644 index 0000000..b1366b4 --- /dev/null +++ b/src/layout/demo/src/app/layout/webdoc/layout.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiLayoutModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiLayoutModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/layout/demo/src/favicon.ico b/src/layout/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/layout/demo/src/index.html b/src/layout/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/layout/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/layout/demo/src/main.ts b/src/layout/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/layout/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/layout/demo/test.ts b/src/layout/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/layout/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/layout/demo/tsconfig.app.json b/src/layout/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/layout/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/layout/demo/tsconfig.spec.json b/src/layout/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/layout/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/layout/lib/index.ts b/src/layout/lib/index.ts new file mode 100644 index 0000000..070b473 --- /dev/null +++ b/src/layout/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiLayoutModule'; diff --git a/src/layout/lib/ng-package.json b/src/layout/lib/ng-package.json new file mode 100644 index 0000000..7dde01e --- /dev/null +++ b/src/layout/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/layout", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/layout/lib/package.json b/src/layout/lib/package.json new file mode 100644 index 0000000..b0fa160 --- /dev/null +++ b/src/layout/lib/package.json @@ -0,0 +1,9 @@ +{ + "name": "@opentiny/ng-layout", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/layout/lib/project.json b/src/layout/lib/project.json new file mode 100644 index 0000000..d45109d --- /dev/null +++ b/src/layout/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/layout/lib", + "sourceRoot": "src/layout/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/layout"], + "options": { + "project": "src/layout/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/layout"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js layout" + }, + { + "command": "ng default-build layout" + }, + { + "command": "node build/clear-default-theme.js layout" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/layout && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build layout && ng pack layout && node build/publish.js layout --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/layout/lib/src/TiLayoutColumnComponent.ts b/src/layout/lib/src/TiLayoutColumnComponent.ts new file mode 100644 index 0000000..e639977 --- /dev/null +++ b/src/layout/lib/src/TiLayoutColumnComponent.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * 布局组件,提供整个页面内容区域的布局 + * + * 内容容器 + * + */ +@Component({ + selector: 'ti-layout-column', + template: '', + styleUrls: ['./layout-column.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLayoutColumnComponent {} diff --git a/src/layout/lib/src/TiLayoutContentBodyComponent.ts b/src/layout/lib/src/TiLayoutContentBodyComponent.ts new file mode 100644 index 0000000..e356f8d --- /dev/null +++ b/src/layout/lib/src/TiLayoutContentBodyComponent.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * 布局组件,提供整个页面内容区域的布局 + * + * 内容主体 + * + */ +@Component({ + selector: 'ti-layout-content-body', + template: '', + styleUrls: ['./layout-content-body.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLayoutContentBodyComponent {} diff --git a/src/layout/lib/src/TiLayoutContentComponent.ts b/src/layout/lib/src/TiLayoutContentComponent.ts new file mode 100644 index 0000000..5f83974 --- /dev/null +++ b/src/layout/lib/src/TiLayoutContentComponent.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +/** + * 布局组件,提供整个页面内容区域的布局 + * + * 内容容器 + * + */ +@Component({ + selector: 'ti-layout-content', + template: '', + styleUrls: ['./layout-content.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-layout-content]': 'true', + // 这里用 false 判断是为了兼容 10.1.10 及之前的版本,之前使用方式是业务在ti-layout-content标签上直接添加responsive属性(非input接口)来使用响应式布局。 + // 所以responsive接口的值为true或undefined(以标签属性的方式使用)时都应该是响应式布局,只有为false时才不是响应式布局。 + '[class.ti3-layout-purchase-responsive]': 'responsive !== false' + } +}) +export class TiLayoutContentComponent { + /** + * 是否为响应式布局 + */ + @Input() responsive: boolean = false; +} diff --git a/src/layout/lib/src/TiLayoutContentHeaderComponent.ts b/src/layout/lib/src/TiLayoutContentHeaderComponent.ts new file mode 100644 index 0000000..01b8fb5 --- /dev/null +++ b/src/layout/lib/src/TiLayoutContentHeaderComponent.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * 布局组件,提供整个页面内容区域的布局 + * + * 内容头部 + * + */ +@Component({ + selector: 'ti-layout-content-header', + template: '', + styleUrls: ['./layout-content-header.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLayoutContentHeaderComponent {} diff --git a/src/layout/lib/src/TiLayoutHeaderComponent.ts b/src/layout/lib/src/TiLayoutHeaderComponent.ts new file mode 100644 index 0000000..2612157 --- /dev/null +++ b/src/layout/lib/src/TiLayoutHeaderComponent.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +/** + * 布局组件,提供整个页面内容区域的布局 + * + * 头部 + * + */ +@Component({ + selector: 'ti-layout-header', + template: '
', + styleUrls: ['./layout-header.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-layout-header]': 'true', + // 这里用 false 判断是为了兼容 10.1.10 及之前的版本,之前使用方式是业务在ti-layout-header标签上直接添加responsive属性(非input接口)来使用响应式布局。 + // 所以responsive接口的值为true或undefined(以标签属性的方式使用)时都应该是响应式布局,只有为false时才不是响应式布局。 + '[class.ti3-layout-purchase-responsive]': 'responsive !== false' + } +}) +export class TiLayoutHeaderComponent { + /** + * 是否为响应式布局 + */ + @Input() responsive: boolean = false; +} diff --git a/src/layout/lib/src/TiLayoutModule.ts b/src/layout/lib/src/TiLayoutModule.ts new file mode 100644 index 0000000..5f12f9e --- /dev/null +++ b/src/layout/lib/src/TiLayoutModule.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiLayoutHeaderComponent } from './TiLayoutHeaderComponent'; +import { TiLayoutContentComponent } from './TiLayoutContentComponent'; +import { TiLayoutSectionComponent } from './TiLayoutSectionComponent'; +import { TiLayoutContentBodyComponent } from './TiLayoutContentBodyComponent'; +import { TiLayoutContentHeaderComponent } from './TiLayoutContentHeaderComponent'; +import { TiLayoutColumnComponent } from './TiLayoutColumnComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [ + TiLayoutContentComponent, + TiLayoutContentBodyComponent, + TiLayoutHeaderComponent, + TiLayoutSectionComponent, + TiLayoutContentHeaderComponent, + TiLayoutColumnComponent + ], + declarations: [ + TiLayoutContentComponent, + TiLayoutContentBodyComponent, + TiLayoutHeaderComponent, + TiLayoutSectionComponent, + TiLayoutContentHeaderComponent, + TiLayoutColumnComponent + ] +}) +export class TiLayoutModule {} +export { TiLayoutContentComponent } from './TiLayoutContentComponent'; +export { TiLayoutContentBodyComponent } from './TiLayoutContentBodyComponent'; +export { TiLayoutHeaderComponent } from './TiLayoutHeaderComponent'; +export { TiLayoutSectionComponent } from './TiLayoutSectionComponent'; +export { TiLayoutContentHeaderComponent } from './TiLayoutContentHeaderComponent'; +export { TiLayoutColumnComponent } from './TiLayoutColumnComponent'; diff --git a/src/layout/lib/src/TiLayoutSectionComponent.ts b/src/layout/lib/src/TiLayoutSectionComponent.ts new file mode 100644 index 0000000..7ace9d6 --- /dev/null +++ b/src/layout/lib/src/TiLayoutSectionComponent.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * 布局组件,提供整个页面内容区域的布局 + * + * 内部分区 + * + */ +@Component({ + selector: 'ti-layout-section', + template: '', + styleUrls: ['./layout-section.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-layout-section]': 'true' + } +}) +export class TiLayoutSectionComponent {} diff --git a/src/layout/lib/src/layout-column.less b/src/layout/lib/src/layout-column.less new file mode 100644 index 0000000..6b94c33 --- /dev/null +++ b/src/layout/lib/src/layout-column.less @@ -0,0 +1,11 @@ +.ti3-layout-content[adaptive] ti-layout-content-body :host { + width: 100%; // flex布局下column容器宽度默认等分 + + &:not(:first-child) { + margin-left: var(--ti-common-space-3x); + } + + ::ng-deep .ti3-layout-section { + padding: var(--ti-common-space-5x) var(--ti-common-space-6x); + } +} diff --git a/src/layout/lib/src/layout-content-body.less b/src/layout/lib/src/layout-content-body.less new file mode 100644 index 0000000..cf3fa21 --- /dev/null +++ b/src/layout/lib/src/layout-content-body.less @@ -0,0 +1,39 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-buylayer-height: 70px; +} + +/**内容块**/ +.ti3-layout-content[leftmenu] :host { + width: 100%; + .flex-item(1); + .flex-container(); + padding-bottom: var(--ti-common-space-5x); + &[vertical] { + // 有leftmenu场景时,内容区竖向排列 + .flex-container(column); + } +} + +.ti3-layout-content:not([leftmenu]) :host { + display: block; + padding-bottom: calc(var(--ti-buylayer-height) + var(--ti-common-space-3x)); + &.ti3-no-buylayer { + padding-bottom: var(--ti-common-space-3x); + } + + &.ti3-no-steps ::ng-deep .ti3-layout-section:first-child { + margin-top: var(--ti-common-space-3x); + } +} + +.ti3-layout-content[adaptive] :host { + padding: var(--ti-common-space-3x) 0 var(--ti-common-space-5x); + + &[columns] { + // 多列模式下使用flex布局 + // .flex(); + .flex-container(); + } +} diff --git a/src/layout/lib/src/layout-content-header.less b/src/layout/lib/src/layout-content-header.less new file mode 100644 index 0000000..5572c3f --- /dev/null +++ b/src/layout/lib/src/layout-content-header.less @@ -0,0 +1,5 @@ +/**内容头*/ +.ti3-layout-content[leftmenu] :host { + display: block; + padding: var(--ti-common-space-4x) 0; +} diff --git a/src/layout/lib/src/layout-content.less b/src/layout/lib/src/layout-content.less new file mode 100644 index 0000000..de1e335 --- /dev/null +++ b/src/layout/lib/src/layout-content.less @@ -0,0 +1,68 @@ +@import '../../../themes/basic/base-all.less'; +@import './layout.less'; + +:host.ti3-layout-content[leftmenu] { + padding: 0 var(--ti-common-space-5x); + zoom: 1; + height: 100%; + .flex-container(column); +} + +:host.ti3-layout-content:not([leftmenu]) { + // 宽度固定居中,适用于购买页面固定居中布局 + display: block; + width: var(--ti-min-width); + margin: 0 auto; +} + +/**宽度自适应,左右间距固定, 适用于详情页面,详情页面可能有leftmnu,也可能没有。**/ +:host.ti3-layout-content[adaptive] { + width: 100%; + padding: 0 var(--ti-common-space-5x); + box-sizing: border-box; +} + +// 固定居中时,<1280时,出滚动条, 固定宽度1192 + 48 + 20 * 2 = 1280 +@media screen and (max-width: 1280px) { + ::ng-deep .ti3-layout-header:not([adaptive]):not(.ti3-layout-purchase-responsive) { + width: calc(var(--ti-min-width) + var(--ti-common-space-5x) * 2); + .ti3-layout-header-content { + margin: 0 var(--ti-common-space-5x); + } + } + :host.ti3-layout-content:not([leftmenu]):not([adaptive]):not(.ti3-layout-purchase-responsive) { + width: var(--ti-min-width); + margin: 0 var(--ti-common-space-5x); + } +} + +// 宽度响应式,适用于购买页面响应式布局: +// 2560 ≥ 分辨率 >1920 ,卡片固定宽度为1832px。 +// 1920 ≥ 分辨率 ≥ 1280,卡片距离左右间距20px +// 例:1920分辨率下卡片宽度1832px;1280分辨率下卡片宽度1192px +// 分辨率<1280时,显示不下则出滚动条。 +@media screen and (min-width: 1920px) { + ::ng-deep .ti3-layout-header.ti3-layout-purchase-responsive { + .ti3-layout-header-content { + margin: 0 calc(50% - var(--ti-max-width) / 2); + } + } + :host.ti3-layout-content.ti3-layout-purchase-responsive:not([leftmenu]) { + width: var(--ti-max-width); + margin: 0 auto; + } +} +@media screen and (max-width: 1920px) { + // 当为1920px时,max-width: 1920px和min-width: 1920px都生效,所以要注意这两个的样式优先级 + ::ng-deep .ti3-layout-header.ti3-layout-purchase-responsive { + min-width: calc(var(--ti-min-width) + var(--ti-common-space-5x) * 2); + .ti3-layout-header-content { + margin: 0 var(--ti-common-space-5x); + } + } + :host.ti3-layout-content.ti3-layout-purchase-responsive:not([leftmenu]) { + min-width: var(--ti-min-width); + width: auto; + margin: 0 var(--ti-common-space-5x); + } +} diff --git a/src/layout/lib/src/layout-header.less b/src/layout/lib/src/layout-header.less new file mode 100644 index 0000000..8943649 --- /dev/null +++ b/src/layout/lib/src/layout-header.less @@ -0,0 +1,31 @@ +@import './layout.less'; + +:host { + --ti-layout-header-height: var(--ti-common-size-10x); +} +// 头部容器 +:host { + display: block; + position: relative; + background-color: var(--ti-common-color-bg-dark-normal); + height: var(--ti-layout-header-height); + line-height: var(--ti-layout-header-height); + box-shadow: none; + border-top: none; + + // 直接给.ti3-layout-header设置带有百分比计算的左右padding来使header内容居中时,有竖向滚动条时刷新页面或者由前一个页面没有竖向滚动条页面调到该页面,这时 IE 下就会出现横向滚动条。 + // 发现用带有百分比计算的值来设置左右padding时存在该问题,但是margin就不会存在该问题。所以此处给.ti3-layout-header内再添加一层容器.ti3-layout-header-content,设置其margin来规避该问题。 + .ti3-layout-header-content { + height: 100%; + } + + // 宽度固定居中,适用于购买固定居中布局 + &:not([adaptive]):not(.ti3-layout-purchase-responsive) .ti3-layout-header-content { + margin: 0 calc(50% - var(--ti-min-width) / 2); + } + + // 宽度自适应,左右间距固定,适用于详情页面布局 + &[adaptive] .ti3-layout-header-content { + margin: 0 var(--ti-common-space-5x); + } +} diff --git a/src/layout/lib/src/layout-section.less b/src/layout/lib/src/layout-section.less new file mode 100644 index 0000000..855af36 --- /dev/null +++ b/src/layout/lib/src/layout-section.less @@ -0,0 +1,30 @@ +/**内容分片*/ +.ti3-layout-content[leftmenu] ti-layout-content-body :host { + display: block; + padding: var(--ti-common-space-5x) var(--ti-common-space-6x); + background: var(--ti-common-color-bg-white-normal); + box-shadow: var(--ti-common-shadow-1-down); +} + +.ti3-layout-content[leftmenu] ti-layout-content-body[vertical] :host { + margin-bottom: var(--ti-common-space-3x); + + &:last-child { + margin-bottom: 0; + } +} + +.ti3-layout-content:not([leftmenu]) ti-layout-content-body :host { + display: block; + padding: var(--ti-common-space-6x) var(--ti-common-space-8x); + background-color: var(--ti-common-color-bg-white-normal); + margin-top: var(--ti-common-space-3x); + box-shadow: var(--ti-common-shadow-1-down); + + &:first-child { + margin-top: 0; + } +} +.ti3-layout-content[adaptive] ti-layout-content-body :host { + padding: var(--ti-common-space-5x) var(--ti-common-space-6x); +} diff --git a/src/layout/lib/src/layout.less b/src/layout/lib/src/layout.less new file mode 100644 index 0000000..ba516b2 --- /dev/null +++ b/src/layout/lib/src/layout.less @@ -0,0 +1,4 @@ +:host { + --ti-min-width: 1192px; + --ti-max-width: 1832px; +} diff --git a/src/leftmenu/demo/karma.conf.js b/src/leftmenu/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/leftmenu/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/leftmenu/demo/project.json b/src/leftmenu/demo/project.json new file mode 100644 index 0000000..f4918f3 --- /dev/null +++ b/src/leftmenu/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/leftmenu/demo", + "sourceRoot": "src/leftmenu/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/leftmenu", + "index": "src/leftmenu/demo/src/index.html", + "main": "src/leftmenu/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/leftmenu/demo/tsconfig.app.json", + "assets": ["src/leftmenu/demo/src/favicon.ico", "src/leftmenu/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "leftmenu-demo:build:production" + }, + "development": { + "browserTarget": "leftmenu-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js leftmenu" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/leftmenu/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/leftmenu/demo/tsconfig.spec.json", + "karmaConfig": "src/leftmenu/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/leftmenu/demo/src/app/AppComponent.ts b/src/leftmenu/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/leftmenu/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/leftmenu/demo/src/app/AppModule.ts b/src/leftmenu/demo/src/app/AppModule.ts new file mode 100644 index 0000000..47caeed --- /dev/null +++ b/src/leftmenu/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { LeftmenuTestModule } from './leftmenu/LeftmenuTestModule'; + +@NgModule({ + imports: [ + LeftmenuTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/leftmenu/demo/src/app/IndexComponent.ts b/src/leftmenu/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..e4dc489 --- /dev/null +++ b/src/leftmenu/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { LeftmenuTestModule } from './leftmenu/LeftmenuTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = LeftmenuTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/leftmenu/demo/src/app/app.html b/src/leftmenu/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/leftmenu/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuActiveChangeComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuActiveChangeComponent.ts new file mode 100644 index 0000000..0ec85d2 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuActiveChangeComponent.ts @@ -0,0 +1,41 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-active-change.html', + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuActiveChangeComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + myLogs: Array = []; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } + + activeChange(event: TiLeftmenuItem): void { + this.myLogs = [...this.myLogs, `activeChange() event=${event}`]; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuBasicComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuBasicComponent.ts new file mode 100644 index 0000000..a98cea4 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuBasicComponent.ts @@ -0,0 +1,36 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-basic.html', + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuBasicComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuCandeactivateComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuCandeactivateComponent.ts new file mode 100644 index 0000000..f485a47 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuCandeactivateComponent.ts @@ -0,0 +1,32 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-candeactivate.html', + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuCandeactivateComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuCollapsedComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuCollapsedComponent.ts new file mode 100644 index 0000000..11b887e --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuCollapsedComponent.ts @@ -0,0 +1,37 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-collapsed.html', + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuCollapsedComponent { + marginLeft: string = '0'; + reloadState: boolean = true; + collapsed: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuDisabledComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuDisabledComponent.ts new file mode 100644 index 0000000..15e812a --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuDisabledComponent.ts @@ -0,0 +1,38 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-disabled.html', + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuDisabledComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'], + disabled: true + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'], + disabled: true + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuDividingComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuDividingComponent.ts new file mode 100644 index 0000000..23bc7ac --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuDividingComponent.ts @@ -0,0 +1,38 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-dividing.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuDividingComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + showDividingLine: true, + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'] + } + ]; + + active: TiLeftmenuItem = this.items[1]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuFootComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuFootComponent.ts new file mode 100644 index 0000000..f84d332 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuFootComponent.ts @@ -0,0 +1,37 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-foot.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuFootComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] // 相对路由路径 + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuGroupComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuGroupComponent.ts new file mode 100644 index 0000000..8fde0c7 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuGroupComponent.ts @@ -0,0 +1,49 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-group.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuGroupComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '分组一', + isGroup: true, + children: [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + } + ] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '分组二', + isGroup: true, + children: [ + { + label: '菜单二', + router: ['router2'] + } + ] + } + ]; + + active: TiLeftmenuItem = this.items[1].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuHrefComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuHrefComponent.ts new file mode 100644 index 0000000..546185f --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuHrefComponent.ts @@ -0,0 +1,36 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-href.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuHrefComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + toggleable: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '首页', + href: '' + } + ] + }, + { + label: '地图', + href: '' + } + ]; + active: TiLeftmenuItem = this.items[0].children[0]; + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuIdComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuIdComponent.ts new file mode 100644 index 0000000..7b355e5 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuIdComponent.ts @@ -0,0 +1,65 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-id.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuIdComponent { + leftmenuId: string = 'leftmenu'; + headId: string = 'leftmenu_head'; + footId: string = 'leftmenu_foot'; + + headLabel: string = '弹性云服务器 ECS'; + footLabel: string = '返回旧版(自定义)'; + marginLeft: string = '192px'; + collapsed: boolean = false; // 默认展开,当设置为true时会收起 + toggleable: boolean = true; + reloadState: boolean = true; + items: Array = [ + { + id: 'hardDisk', + label: '云硬盘', + children: [ + { + id: 'disk', + label: '磁盘', + router: ['./router11'] // 相对路由路径 + }, + { + id: 'snapshot', + label: '快照', + router: ['./router12'] + } + ] + }, + { + id: 'mirror', + label: '镜像服务', + router: ['router2'] + }, + { + id: 'scale', + label: '弹性伸缩', + children: [ + { + id: 'scaleInstance', + label: '伸缩实例', + router: ['router31'] // 相对路由路径 + }, + { + id: 'scaleBandwidth', + label: '伸缩带宽', + router: ['router32'] // 绝对路由路径 + } + ] + } + ]; + + active: TiLeftmenuItem = this.items[1]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '192px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuNoRouterComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuNoRouterComponent.ts new file mode 100644 index 0000000..546360f --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuNoRouterComponent.ts @@ -0,0 +1,34 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-no-router.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuNoRouterComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuParamsComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuParamsComponent.ts new file mode 100644 index 0000000..99eaf38 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuParamsComponent.ts @@ -0,0 +1,43 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-params.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuParamsComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['router11'] + }, + { + label: '子菜单1.2', + router: ['router12'] + } + ] + }, + { + label: '带参数路由', + router: ['Routerparams', 1], + routerExtras: { + queryParams: { + locale: 'zh-cn' + }, + fragment: 'tiny' + } + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuReloadStateComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuReloadStateComponent.ts new file mode 100644 index 0000000..9d057fe --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuReloadStateComponent.ts @@ -0,0 +1,35 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-reload-state.html', + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuReloadStateComponent { + marginLeft: string = '200px'; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuRouterlistComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuRouterlistComponent.ts new file mode 100644 index 0000000..9e1fd07 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuRouterlistComponent.ts @@ -0,0 +1,38 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-routerlist.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuRouterlistComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '携带多个路由', + router: ['./router2'], + routerList: [['routerC']] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[1]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuScrollComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuScrollComponent.ts new file mode 100644 index 0000000..0842fe1 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuScrollComponent.ts @@ -0,0 +1,63 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-scroll.html', + styleUrls: ['./leftmenuTest.less'], + styles: ['body {height: 200px;}'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuScrollComponent { + headLabel: string = '弹性云服务器 ECS'; + marginLeft: string = '192px'; + collapsed: boolean = false; + toggleable: boolean = true; + reloadState: boolean = true; + myOptions: Array = [ + { label: '美国', englishname: 'America' }, + { label: '巴西', englishname: 'Brazil' } + ]; + items: Array = [ + { + label: '云硬盘', + children: [ + { + label: '磁盘', + router: ['./router11'] + }, + { + label: '快照', + router: ['./router12'] + } + ] + }, + { + label: '镜像服务', + router: ['router2'] + }, + { + label: '弹性伸缩', + children: [ + { + label: '伸缩带宽', + router: ['./router31'] // 绝对路由路径 + } + ] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + // 需要业务侧在菜单收起/展开时,控制右侧内容的位置。3.1.3版本leftMenu默认宽度修改为192px + this.marginLeft = isHide ? '0' : '192px'; + } + + clickLevel1(m1: any): void { + console.log(`点击一级菜单${JSON.stringify(m1)}`); + } + + clickLevel2(m2: any): void { + console.log(`点击二级菜单${JSON.stringify(m2)}`); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuSecurityComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuSecurityComponent.ts new file mode 100644 index 0000000..666bebb --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuSecurityComponent.ts @@ -0,0 +1,57 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-security.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuSecurityComponent { + headLabel: string = '弹性云服务器 ECS'; + marginLeft: string = '192px'; + collapsed: boolean = false; // 默认展开,当设置为true时会收起 + toggleable: boolean = true; + reloadState: boolean = true; + items: Array = [ + { + label: '一级菜单', + children: [ + { + label: '首页', + href: '' // 跳转至新页面打开链接 + }, + { + label: 'XSS攻击脚本', + // 点击该项时js脚本alert('Hi XSS')不会执行 + href: "javascript:alert('Hi XSS')" + } + ] + }, + { + label: '原页面重定向', + // 点击该项时原页面不会被重定向 + href: '#/leftmenu/opener' + }, + { + label: '弹性伸缩', + children: [ + { + label: '伸缩实例', + router: ['router31'] // 相对路由路径 + }, + { + label: '伸缩带宽', + router: ['router32'] // 绝对路由路径 + } + ] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + // 需要业务侧在菜单收起\展开时,控制右侧内容的位置 + // 3.1.3版本leftMenu默认宽度修改为192px + this.marginLeft = isHide ? '0' : '192px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuTestModule.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuTestModule.ts new file mode 100644 index 0000000..0ac2fa0 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuTestModule.ts @@ -0,0 +1,277 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiLeftmenuModule, TiModalModule, TiSelectModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { LeftmenuNoRouterComponent } from './LeftmenuNoRouterComponent'; +import { LeftmenuBasicWebsiteViewComponent } from './website-views/LeftmenuBasicWebsiteViewComponent'; +import { LeftmenuRouterlistWebsiteViewComponent } from './website-views/LeftmenuRouterlistWebsiteViewComponent'; +import { LeftmenuParamsWebsiteViewComponent } from './website-views/LeftmenuParamsWebsiteViewComponent'; +import { LeftmenuHrefWebsiteViewComponent } from './website-views/LeftmenuHrefWebsiteViewComponent'; +import { LeftmenuGroupWebsiteViewComponent } from './website-views/LeftmenuGroupWebsiteViewComponent'; +import { LeftmenuFootWebsiteViewComponent } from './website-views/LeftmenuFootWebsiteViewComponent'; +import { LeftmenuDividingWebsiteViewComponent } from './website-views/LeftmenuDividingWebsiteViewComponent'; +import { LeftmenuNoRouterWebsiteViewComponent } from './website-views/LeftmenuNoRouterWebsiteViewComponent'; +import { LeftmenuReloadStateWebsiteViewComponent } from './website-views/LeftmenuReloadStateWebsiteViewComponent'; +import { LeftmenuDisabledWebsiteViewComponent } from './website-views/LeftmenuDisabledWebsiteViewComponent'; +import { LeftmenuActiveChangeWebsiteViewComponent } from './website-views/LeftmenuActiveChangeWebsiteViewComponent'; +import { LeftmenuCollapsedWebsiteViewComponent } from './website-views/LeftmenuCollapsedWebsiteViewComponent'; +import { LeftmenuToggleableWebsiteViewComponent } from './website-views/LeftmenuToggleableWebsiteViewComponent'; + +import { LeftmenuBasicComponent } from './LeftmenuBasicComponent'; +import { LeftmenuRouterlistComponent } from './LeftmenuRouterlistComponent'; +import { LeftmenuParamsComponent } from './LeftmenuParamsComponent'; +import { LeftmenuReloadStateComponent } from './LeftmenuReloadStateComponent'; +import { LeftmenuHrefComponent } from './LeftmenuHrefComponent'; +import { LeftmenuGroupComponent } from './LeftmenuGroupComponent'; +import { LeftmenuFootComponent } from './LeftmenuFootComponent'; +import { LeftmenuDividingComponent } from './LeftmenuDividingComponent'; +import { LeftmenuIdComponent } from './LeftmenuIdComponent'; +import { LeftmenuSecurityComponent } from './LeftmenuSecurityComponent'; +import { LeftmenuScrollComponent } from './LeftmenuScrollComponent'; +import { LeftmenuCollapsedComponent } from './LeftmenuCollapsedComponent'; +import { LeftmenuToggleableComponent } from './LeftmenuToggleableComponent'; +import { LeftmenuActiveChangeComponent } from './LeftmenuActiveChangeComponent'; +import { LeftmenuDisabledComponent } from './LeftmenuDisabledComponent'; +import { LeftmenuCandeactivateComponent } from './LeftmenuCandeactivateComponent'; + +import { Router11Component } from './Router11Component'; +import { Router12Component } from './Router12Component'; +import { Router2Component } from './Router2Component'; +import { Router31Component } from './Router31Component'; +import { Router32Component } from './Router32Component'; + +import { RouterAComponent } from './RouterAComponent'; +import { RouterBComponent } from './RouterBComponent'; +import { RouterCComponent } from './RouterCComponent'; +import { RouterDComponent } from './RouterDComponent'; +import { OpenerComponent } from './OpenerComponent'; +import { RouterparamsComponent } from './RouterparamsComponent'; + +import { UnsaveGuard } from './unsave'; +@NgModule({ + imports: [ + CommonModule, + TiLeftmenuModule, + TiModalModule, + TiButtonModule, + TiSelectModule, + DemoLogModule, + RouterModule.forChild(LeftmenuTestModule.ROUTES) + ], + providers: [UnsaveGuard], + declarations: [ + LeftmenuBasicComponent, + LeftmenuBasicWebsiteViewComponent, + LeftmenuRouterlistWebsiteViewComponent, + LeftmenuParamsWebsiteViewComponent, + LeftmenuHrefWebsiteViewComponent, + LeftmenuGroupWebsiteViewComponent, + LeftmenuFootWebsiteViewComponent, + LeftmenuDividingWebsiteViewComponent, + LeftmenuNoRouterWebsiteViewComponent, + LeftmenuReloadStateWebsiteViewComponent, + LeftmenuDisabledWebsiteViewComponent, + LeftmenuActiveChangeWebsiteViewComponent, + LeftmenuCollapsedWebsiteViewComponent, + LeftmenuToggleableWebsiteViewComponent, + LeftmenuNoRouterComponent, + Router11Component, + Router12Component, + Router2Component, + Router31Component, + Router32Component, + RouterAComponent, + RouterBComponent, + RouterCComponent, + RouterDComponent, + LeftmenuRouterlistComponent, + LeftmenuParamsComponent, + LeftmenuReloadStateComponent, + LeftmenuCollapsedComponent, + LeftmenuToggleableComponent, + LeftmenuHrefComponent, + LeftmenuGroupComponent, + LeftmenuFootComponent, + LeftmenuDividingComponent, + LeftmenuCandeactivateComponent, + LeftmenuIdComponent, + LeftmenuSecurityComponent, + OpenerComponent, + RouterparamsComponent, + LeftmenuScrollComponent, + LeftmenuActiveChangeComponent, + LeftmenuDisabledComponent + ] +}) +export class LeftmenuTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiLeftmenuComponent.html', label: 'Leftmenu' }, + { href: 'components/TiLeftmenuHeadComponent.html', label: 'LeftmenuHead' }, + { href: 'components/TiLeftmenuItemComponent.html', label: 'LeftmenuItem' }, + { + href: 'components/TiLeftmenuLevel1Component.html', + label: 'LeftmenuLevel1' + }, + { + href: 'components/TiLeftmenuLevel2Component.html', + label: 'LeftmenuLevel2' + } + ]; + static readonly ROUTES: Routes = [ + { + path: 'leftmenu/leftmenu-basic', + component: LeftmenuBasicComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerD', component: RouterDComponent }, + { path: 'routerC', component: RouterCComponent } + ] + }, + { + path: 'leftmenu/leftmenu-routerlist', + component: LeftmenuRouterlistComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerA', component: RouterAComponent }, + { path: 'routerB', component: RouterBComponent }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/leftmenu-params', + component: LeftmenuParamsComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31/:id', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerA', component: RouterAComponent }, + { path: 'routerB', component: RouterBComponent }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent }, + { path: 'Routerparams/:id', component: RouterparamsComponent } + ] + }, + { + path: 'leftmenu/leftmenu-href', + component: LeftmenuHrefComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router31', component: Router31Component }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/leftmenu-group', + component: LeftmenuGroupComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/leftmenu-foot', + component: LeftmenuFootComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/leftmenu-dividing', + component: LeftmenuDividingComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/leftmenu-no-router', + component: LeftmenuNoRouterComponent + }, + { + path: 'leftmenu/leftmenu-id', + component: LeftmenuIdComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/leftmenu-candeactivate', + component: LeftmenuCandeactivateComponent, + children: [ + { + path: 'router11', + component: Router11Component, + canDeactivate: [UnsaveGuard] + }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/leftmenu-security', + component: LeftmenuSecurityComponent, + children: [ + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/opener', + component: OpenerComponent + }, + { + path: 'leftmenu/leftmenu-scroll', + component: LeftmenuScrollComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component } + ] + } + ]; +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuToggleableComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuToggleableComponent.ts new file mode 100644 index 0000000..3eb6a07 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuToggleableComponent.ts @@ -0,0 +1,32 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-toggleable.html', + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuToggleableComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; +} diff --git a/src/leftmenu/demo/src/app/leftmenu/OpenerComponent.ts b/src/leftmenu/demo/src/app/leftmenu/OpenerComponent.ts new file mode 100644 index 0000000..33da98b --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/OpenerComponent.ts @@ -0,0 +1,32 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +@Component({ + template: ` +
+

恶意攻击网站

+

opener类型安全漏洞

+

如果在项目中需要 打开新标签 进行跳转一般会有两种方式

+

1) HTML -> a标签, target = _blank

+

2) JS -> window.open('')

+

+ 这两种方式看起来没有问题,但是存在漏洞。通过这两种方式打开的页面可以使用 window.opener 来访问源页面的 window 对象。 场景:A 页面通过 + a标签 或 window.open 方式,打开 B 页面。但是 B 页面存在恶意代码如下: window.opener.location.replace('') + 【此代码仅针对打开新标签有效】 此时,用户正在浏览新标签页,但是原来网站的标签页已经被导航到了其他页面。 + 恶意网站可以伪造一个足以欺骗用户的页面,使得进行恶意破坏。 即使在跨域状态下 opener 仍可以调用 location.replace 方法。 +

+
+ + ` +}) +export class OpenerComponent implements OnInit { + myLogs: Array = []; + ngOnInit(): void { + try { + window.opener.location.replace(''); + } catch (error) { + if (error.name === 'TypeError') { + this.myLogs = [...this.myLogs, `已做安全处理:已经将 opner 赋值为 null,这里无法拿到 opener 的引用`]; + } + } + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/Router11Component.ts b/src/leftmenu/demo/src/app/leftmenu/Router11Component.ts new file mode 100644 index 0000000..99f7914 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/Router11Component.ts @@ -0,0 +1,7 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + template: `

子菜单 1.1 的内容

` +}) +export class Router11Component {} diff --git a/src/leftmenu/demo/src/app/leftmenu/Router12Component.ts b/src/leftmenu/demo/src/app/leftmenu/Router12Component.ts new file mode 100644 index 0000000..af7dd8b --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/Router12Component.ts @@ -0,0 +1,25 @@ +import { Component, OnInit } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; +@Component({ + template: ` +

子菜单 1.2 的内容

+ + 弹窗标题 + +
弹窗内容
+
+ + + +
+ + ` +}) +export class Router12Component { + constructor(private tiModal: TiModalService) {} + show(content: string): void { + this.tiModal.open(content, { + modalClass: 'modal-class' + }); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/Router2Component.ts b/src/leftmenu/demo/src/app/leftmenu/Router2Component.ts new file mode 100644 index 0000000..f7bbe91 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/Router2Component.ts @@ -0,0 +1,24 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +@Component({ + template: ` +

欢迎来到镜像服务页面

+ 点击跳转到帮助中心页面 + ` +}) +export class Router2Component implements OnInit { + constructor(private activeRoute: ActivatedRoute) {} + + ngOnInit(): void { + console.log('router2Test Init', this.activeRoute.routeConfig.path); + // 这种是直接获取queryParam + this.activeRoute.queryParams.subscribe( + (res: any) => { + console.log('queryParams', res); + }, + (err: any) => { + console.log('网络错误'); + } + ); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/Router31Component.ts b/src/leftmenu/demo/src/app/leftmenu/Router31Component.ts new file mode 100644 index 0000000..7b9f526 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/Router31Component.ts @@ -0,0 +1,26 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + template: ` +

子菜单 3.1 的内容

+ 点击跳转到使用指南页面 + ` +}) +export class Router31Component implements OnInit { + token: string; + queryParams: string; + constructor(private activeRoute: ActivatedRoute) {} + ngOnInit(): void { + console.log('router31Test Init'); + + this.activeRoute.paramMap.subscribe((paramMap: any) => { + this.token = JSON.stringify(paramMap.params); + }); + + // 这种是直接获取queryParam + this.activeRoute.queryParamMap.subscribe((res: any) => { + this.queryParams = JSON.stringify(res.params); + }); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/Router32Component.ts b/src/leftmenu/demo/src/app/leftmenu/Router32Component.ts new file mode 100644 index 0000000..bd170ce --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/Router32Component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +@Component({ + template: `

欢迎来到伸缩带宽页面

` +}) +export class Router32Component implements OnInit { + label: string; + constructor(private activeRoute: ActivatedRoute) {} + + ngOnInit(): void { + console.log('router32Test Init', this.activeRoute.routeConfig.path); + this.label = 'init'; + + // 这种是直接获取queryParam + this.activeRoute.queryParams.subscribe( + (res: any) => { + console.log('queryParams', res); + }, + (err: any) => { + console.log('网络错误'); + } + ); + } + + clickFn(): void { + this.label = `${this.label}xxxx`; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/RouterAComponent.ts b/src/leftmenu/demo/src/app/leftmenu/RouterAComponent.ts new file mode 100644 index 0000000..59ba986 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/RouterAComponent.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +@Component({ + template: `

欢迎来到问题咨询页面

` +}) +export class RouterAComponent implements OnInit { + constructor(private activeRoute: ActivatedRoute) {} + + ngOnInit(): void { + console.log('RouterAComponent Init', this.activeRoute.routeConfig.path); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/RouterBComponent.ts b/src/leftmenu/demo/src/app/leftmenu/RouterBComponent.ts new file mode 100644 index 0000000..4a97f50 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/RouterBComponent.ts @@ -0,0 +1,16 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +@Component({ + template: ` +
Welcome to router B!
+ go to routerA(in router32 routerList) + ` +}) +export class RouterBComponent implements OnInit { + label: string; + constructor(private activeRoute: ActivatedRoute) {} + + ngOnInit(): void { + console.log('RouterBComponent Init', this.activeRoute.routeConfig.path); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/RouterCComponent.ts b/src/leftmenu/demo/src/app/leftmenu/RouterCComponent.ts new file mode 100644 index 0000000..76d999f --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/RouterCComponent.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +@Component({ + template: `

欢迎来到帮助中心页面

` +}) +export class RouterCComponent implements OnInit { + constructor(private activeRoute: ActivatedRoute) {} + + ngOnInit(): void { + console.log('RouterCComponent Init', this.activeRoute.routeConfig.path); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/RouterDComponent.ts b/src/leftmenu/demo/src/app/leftmenu/RouterDComponent.ts new file mode 100644 index 0000000..041a724 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/RouterDComponent.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +@Component({ + template: `

欢迎来到使用指南页面

` +}) +export class RouterDComponent implements OnInit { + constructor(private activeRoute: ActivatedRoute) {} + + ngOnInit(): void { + console.log('RouterDComponent Init RouterDComponentRouterDComponent', this.activeRoute.routeConfig.path); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/RouterparamsComponent.ts b/src/leftmenu/demo/src/app/leftmenu/RouterparamsComponent.ts new file mode 100644 index 0000000..3845bf1 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/RouterparamsComponent.ts @@ -0,0 +1,27 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + template: ` +

参数: {{ token }}

+

参数: {{ queryParams }}

+

参数:{{ fragment }}

+ ` +}) +export class RouterparamsComponent implements OnInit { + token: string; + queryParams: string; + fragment: string; + constructor(private activeRoute: ActivatedRoute) {} + ngOnInit(): void { + this.activeRoute.paramMap.subscribe((paramMap: any) => { + this.token = JSON.stringify(paramMap.params); + }); + this.activeRoute.queryParamMap.subscribe((res: any) => { + this.queryParams = JSON.stringify(res.params); + }); + this.activeRoute.fragment.subscribe((res: any) => { + this.fragment = JSON.stringify(res); + }); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-active-change.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-active-change.html new file mode 100644 index 0000000..12640cb --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-active-change.html @@ -0,0 +1,21 @@ + + + +
{{ m1.label }}
+
+ + {{ m2.label }} + +
+
+
+ +
+

事件日志:

+ diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-basic.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-basic.html new file mode 100644 index 0000000..5085b6c --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-basic.html @@ -0,0 +1,13 @@ + + + +
{{ m1.label }}
+
+ + {{ m2.label }} + +
+
+
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-candeactivate.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-candeactivate.html new file mode 100644 index 0000000..c02afb7 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-candeactivate.html @@ -0,0 +1,13 @@ + + + +
{{ m1.label }}
+
+ + {{ m2.label }} + +
+
+
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-collapsed.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-collapsed.html new file mode 100644 index 0000000..8911bc0 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-collapsed.html @@ -0,0 +1,19 @@ + + + +
{{ m1.label }}
+
+ + {{ m2.label }} + +
+
+
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-disabled.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-disabled.html new file mode 100644 index 0000000..aac1f5b --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-disabled.html @@ -0,0 +1,13 @@ + + + +
{{ m1.label }}
+
+ + {{ m2.label }} + +
+
+
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-dividing.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-dividing.html new file mode 100644 index 0000000..aac1f5b --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-dividing.html @@ -0,0 +1,13 @@ + + + +
{{ m1.label }}
+
+ + {{ m2.label }} + +
+
+
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-foot.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-foot.html new file mode 100644 index 0000000..c37b7b7 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-foot.html @@ -0,0 +1,19 @@ + + + +
菜单标题内容
+
+ +
{{ m1.label }}
+
+ + {{ m2.label }} + +
+ +
菜单底部内容
+
+
+
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-group.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-group.html new file mode 100644 index 0000000..b71cba1 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-group.html @@ -0,0 +1,29 @@ + + + + + {{ group.label }} + + + +
{{ m1.label }}
+
+ + {{ m2.label }} + +
+
+ + +
{{ group.label }}
+
+ + {{ m2.label }} + +
+
+
+ +
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-href.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-href.html new file mode 100644 index 0000000..aac1f5b --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-href.html @@ -0,0 +1,13 @@ + + + +
{{ m1.label }}
+
+ + {{ m2.label }} + +
+
+
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-id.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-id.html new file mode 100644 index 0000000..c5f7de8 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-id.html @@ -0,0 +1,29 @@ + + + +
{{headLabel}}
+
+ + +
{{m1.label}}
+
+ + {{m2.label}} + +
+ +
{{footLabel}}
+
+
+ + +
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-no-router.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-no-router.html new file mode 100644 index 0000000..ad1c290 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-no-router.html @@ -0,0 +1,23 @@ + + + +
{{ m1.label }}
+
+ + {{ m2.label }} + +
+
+
+
子菜单 1.1 的内容
+
子菜单 1.2 的内容
+
+

菜单二的内容

+
+
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-params.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-params.html new file mode 100644 index 0000000..aac1f5b --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-params.html @@ -0,0 +1,13 @@ + + + +
{{ m1.label }}
+
+ + {{ m2.label }} + +
+
+
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-reload-state.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-reload-state.html new file mode 100644 index 0000000..22f3bca --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-reload-state.html @@ -0,0 +1,13 @@ + + + +
{{ m1.label }}
+
+ + {{ m2.label }} + +
+
+
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-routerlist.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-routerlist.html new file mode 100644 index 0000000..5085b6c --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-routerlist.html @@ -0,0 +1,13 @@ + + + +
{{ m1.label }}
+
+ + {{ m2.label }} + +
+
+
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-scroll.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-scroll.html new file mode 100644 index 0000000..dba4208 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-scroll.html @@ -0,0 +1,26 @@ + + + + 请选出您所在国家: + + + + +
{{m1.label}}
+
+ + {{m2.label}} + +
+
+ + +
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-security.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-security.html new file mode 100644 index 0000000..2c6f0d9 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-security.html @@ -0,0 +1,25 @@ + + + +
{{headLabel}}
+
+ + +
{{m1.label}}
+
+ + {{m2.label}} + +
+
+ + +
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-toggleable.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-toggleable.html new file mode 100644 index 0000000..62c85f5 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-toggleable.html @@ -0,0 +1,13 @@ + + + +
{{ m1.label }}
+
+ + {{ m2.label }} + +
+
+
+ +
diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenuTest.less b/src/leftmenu/demo/src/app/leftmenu/leftmenuTest.less new file mode 100644 index 0000000..68ecf14 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenuTest.less @@ -0,0 +1,9 @@ +body { + background-color: #eef0f5; +} +.clear-transform { + background-color: #f5f2f0; + transform: scale(1, 1); + height: 460px; + overflow: auto; +} diff --git a/src/leftmenu/demo/src/app/leftmenu/unsave.ts b/src/leftmenu/demo/src/app/leftmenu/unsave.ts new file mode 100644 index 0000000..9e70543 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/unsave.ts @@ -0,0 +1,8 @@ +import { CanDeactivate } from '@angular/router'; +import { TiLeftmenuLevel2Component } from '@opentiny/ng'; + +export class UnsaveGuard implements CanDeactivate { + canDeactivate(component: TiLeftmenuLevel2Component) { + return window.confirm('确定要离开当前菜单吗?'); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu-demos.js b/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu-demos.js new file mode 100644 index 0000000..26401b8 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu-demos.js @@ -0,0 +1,163 @@ +export default { + column: '1', + demos: [ + { + demoId: 'leftmenu-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': '' + }, + desc: { + 'zh-CN': '

Leftmenu 组件的最简用法。

', + 'en-US': '

leftmenu basic

' + }, + apis: [ + 'TiLeftmenuComponent.properties.active', + 'TiLeftmenuLevel1Component.properties.item', + 'TiLeftmenuLevel2Component.properties.item' + ] + }, + { + demoId: 'leftmenu-routerlist', + name: { + 'zh-CN': '同一菜单下挂载多个路由', + 'en-US': '' + }, + desc: { + 'zh-CN': '

支持在同一菜单下挂载多个路由。

', + 'en-US': '

' + } + }, + { + demoId: 'leftmenu-params', + name: { + 'zh-CN': '路由参数', + 'en-US': '' + }, + desc: { + 'zh-CN': + '

通过TiLeftmenuItem实例的routerExtras属性配置路由参数,更多配置请参考 Angular 官方文档。注意:如果需要在路由路径中携带参数,请在注册路由时使用冒号 : 表示路径参数,例如下方示例中定义了 /:id。更多路由配置请参考 Angular 官方文档

', + 'en-US': '

' + }, + codeFiles: ['leftmenu-params.html', 'LeftmenuParamsComponent.ts', 'RouterparamsComponent.ts'] + }, + { + demoId: 'leftmenu-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': '' + }, + desc: { + 'zh-CN': '

通过属性disabled配置菜单的禁用状态。

', + 'en-US': '

' + }, + apis: ['TiLeftmenuLevel1Component.properties.disabled', 'TiLeftmenuLevel2Component.properties.disabled'] + }, + { + demoId: 'leftmenu-reload-state', + name: { + 'zh-CN': '路由刷新', + 'en-US': '' + }, + desc: { + 'zh-CN': '

不设置属性reloadState,则点击当前已激活菜单时右侧内容区不会刷新。

', + 'en-US': '

' + }, + apis: ['TiLeftmenuComponent.properties.reloadState'] + }, + { + demoId: 'leftmenu-collapsed', + name: { + 'zh-CN': '面板水平方向展开状态', + 'en-US': '' + }, + desc: { + 'zh-CN': + '

通过属性collapsed配置菜单面板水平方向的展开状态。当菜单面板水平方向折叠/展开状态改变的时候触发collapsedChange事件,传递参数为改变后面板是否处于折叠状态,true 表示面板被折叠,false 表示面板被展开。

', + 'en-US': '

' + }, + apis: ['TiLeftmenuComponent.properties.collapsed', 'TiLeftmenuComponent.events.collapsedChange'] + }, + { + demoId: 'leftmenu-toggleable', + name: { + 'zh-CN': '不可折叠面板', + 'en-US': '' + }, + desc: { + 'zh-CN': '

通过属性toggleable配置菜单面板是否可以水平方向折叠。

', + 'en-US': '

' + }, + apis: ['TiLeftmenuComponent.properties.toggleable'] + }, + { + demoId: 'leftmenu-active-change', + name: { + 'zh-CN': '切换激活菜单', + 'en-US': '' + }, + desc: { + 'zh-CN': '

支持通过监听菜单的 click 事件来拿到用户点击菜单的回调

', + 'en-US': '

' + }, + apis: ['TiLeftmenuComponent.events.activeChange'] + }, + { + demoId: 'leftmenu-href', + name: { + 'zh-CN': '链接跳转', + 'en-US': '' + }, + desc: { + 'zh-CN': '

通过TiLeftmenuItem实例的href属性配置链接地址。

', + 'en-US': '

' + } + }, + { + demoId: 'leftmenu-group', + name: { + 'zh-CN': '分组展示', + 'en-US': '' + }, + desc: { + 'zh-CN': '

通过 LeftmenuGroup 组件对路由进行分组。

', + 'en-US': '

' + } + }, + { + demoId: 'leftmenu-foot', + name: { + 'zh-CN': '自定义标题及底部', + 'en-US': '' + }, + desc: { + 'zh-CN': '

通过 LeftmenuHead 组件配置菜单底部内容;通过LeftmenuFoot 组件配置菜单底部内容。

', + 'en-US': '

' + } + }, + { + demoId: 'leftmenu-dividing', + name: { + 'zh-CN': '分割线', + 'en-US': '' + }, + desc: { + 'zh-CN': '

通过TiLeftmenuItem实例的showDividingLine属性配置分割线。注意:该属性仅对一级菜单生效。

', + 'en-US': '

' + } + }, + { + demoId: 'leftmenu-no-router', + name: { + 'zh-CN': '关闭路由模式', + 'en-US': '' + }, + desc: { + 'zh-CN': + '

通过属性routable配置是否启用路由模式。注意:本组件路由使用 @angular/router,你可以通过 Angular 官方文档 查看更多关于路由的内容。

', + 'en-US': '

' + }, + apis: ['TiLeftmenuComponent.properties.routable'] + } + ] +}; diff --git a/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.cn.md b/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.cn.md new file mode 100644 index 0000000..54417d6 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.cn.md @@ -0,0 +1,36 @@ +--- +title: Leftmenu 左侧菜单 +--- +# Leftmenu 左侧菜单 + +
+ +Leftmenu 是提供页面导航的菜单列表。   + ++ 支持二级菜单。 + ++ 支持菜单分组。 + ++ 支持头部、底部区域自定义。 + +```typescript +import { TiLeftmenuModule } from '@opentiny/ng'; +``` + +
+ +
+ +Leftmenu 是提供页面导航的菜单列表。   + ++ 支持二级菜单。 + ++ 支持菜单分组。 + ++ 支持头部、底部区域自定义。 + +```typescript +import { TiLeftmenuModule } from '@opentiny/ng'; +``` + +
diff --git a/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.en.md b/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuActiveChangeWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuActiveChangeWebsiteViewComponent.ts new file mode 100644 index 0000000..96b8e46 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuActiveChangeWebsiteViewComponent.ts @@ -0,0 +1,39 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-active-change-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuActiveChangeWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + myLogs: Array = []; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } + + itemClickHandle(event: TiLeftmenuItem): void { + this.myLogs = [...this.myLogs, `itemClickHandle() event=${JSON.stringify(event)}`]; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuBasicWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuBasicWebsiteViewComponent.ts new file mode 100644 index 0000000..fffc8cd --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuBasicWebsiteViewComponent.ts @@ -0,0 +1,34 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-basic-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuBasicWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuCollapsedWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuCollapsedWebsiteViewComponent.ts new file mode 100644 index 0000000..c105596 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuCollapsedWebsiteViewComponent.ts @@ -0,0 +1,35 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-collapsed-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuCollapsedWebsiteViewComponent { + marginLeft: string = '0'; + collapsed: boolean = true; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDisabledWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDisabledWebsiteViewComponent.ts new file mode 100644 index 0000000..b5a0b36 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDisabledWebsiteViewComponent.ts @@ -0,0 +1,36 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-disabled-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuDisabledWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + disabled: true + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二', + disabled: true + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDividingWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDividingWebsiteViewComponent.ts new file mode 100644 index 0000000..427352e --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDividingWebsiteViewComponent.ts @@ -0,0 +1,35 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-dividing-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuDividingWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + showDividingLine: true, + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuFootWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuFootWebsiteViewComponent.ts new file mode 100644 index 0000000..06fca92 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuFootWebsiteViewComponent.ts @@ -0,0 +1,34 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-foot-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuFootWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuGroupWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuGroupWebsiteViewComponent.ts new file mode 100644 index 0000000..05352b7 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuGroupWebsiteViewComponent.ts @@ -0,0 +1,46 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-group-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuGroupWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '分组一', + isGroup: true, + children: [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + } + ] + }, + { + label: '分组二', + isGroup: true, + children: [ + { + label: '菜单二' + } + ] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuHrefWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuHrefWebsiteViewComponent.ts new file mode 100644 index 0000000..103041c --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuHrefWebsiteViewComponent.ts @@ -0,0 +1,36 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-href-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuHrefWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '首页', + href: '' + } + ] + }, + { + label: '地图', + href: '' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuNoRouterWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuNoRouterWebsiteViewComponent.ts new file mode 100644 index 0000000..1922112 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuNoRouterWebsiteViewComponent.ts @@ -0,0 +1,34 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-no-router-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuNoRouterWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuParamsWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuParamsWebsiteViewComponent.ts new file mode 100644 index 0000000..60582e0 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuParamsWebsiteViewComponent.ts @@ -0,0 +1,34 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-params-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuParamsWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '带参数路由' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuReloadStateWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuReloadStateWebsiteViewComponent.ts new file mode 100644 index 0000000..5d03cd8 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuReloadStateWebsiteViewComponent.ts @@ -0,0 +1,33 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-reload-state-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuReloadStateWebsiteViewComponent { + marginLeft: string = '200px'; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuRouterlistWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuRouterlistWebsiteViewComponent.ts new file mode 100644 index 0000000..fe2e0c1 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuRouterlistWebsiteViewComponent.ts @@ -0,0 +1,42 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-routerlist-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuRouterlistWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '携带多个路由' + } + ]; + showD: boolean = false; + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } + linkToD(): void { + this.showD = true; + } + clickLevel2(m2: any): void { + if (m2 !== this.items[1]) { + this.showD = false; + } + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuToggleableWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuToggleableWebsiteViewComponent.ts new file mode 100644 index 0000000..47585ed --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuToggleableWebsiteViewComponent.ts @@ -0,0 +1,30 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-toggleable-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuToggleableWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-active-change-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-active-change-website-view.html new file mode 100644 index 0000000..42c2092 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-active-change-website-view.html @@ -0,0 +1,27 @@ +
+ + + + {{ m1.label }} + + + {{ m2.label }} + + + +
+
子菜单 1.1 的内容
+
子菜单 1.2 的内容
+
+

菜单二的内容

+
+
+
+

事件日志:

+ diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-basic-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-basic-website-view.html new file mode 100644 index 0000000..111890d --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-basic-website-view.html @@ -0,0 +1,25 @@ +
+ + + + {{ m1.label }} + + + {{ m2.label }} + + + +
+
子菜单 1.1 的内容
+
子菜单 1.2 的内容
+
+

菜单二的内容

+
+
+
diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-collapsed-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-collapsed-website-view.html new file mode 100644 index 0000000..eb85ab8 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-collapsed-website-view.html @@ -0,0 +1,26 @@ +
+ + + + {{ m1.label }} + + + {{ m2.label }} + + + +
+
子菜单 1.1 的内容
+
子菜单 1.2 的内容
+
+

菜单二的内容

+
+
+
diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-disabled-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-disabled-website-view.html new file mode 100644 index 0000000..bd9b9a8 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-disabled-website-view.html @@ -0,0 +1,25 @@ +
+ + + + {{ m1.label }} + + + {{ m2.label }} + + + +
+
子菜单 1.1 的内容
+
子菜单 1.2 的内容
+
+

菜单二的内容

+
+
+
diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-dividing-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-dividing-website-view.html new file mode 100644 index 0000000..d3a0461 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-dividing-website-view.html @@ -0,0 +1,26 @@ +
+ + +
菜单标题
+
+ + + {{ m1.label }} + + + {{ m2.label }} + + +
+
+
子菜单 1.1 的内容
+
子菜单 1.2 的内容
+
菜单二的内容
+
+
diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-foot-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-foot-website-view.html new file mode 100644 index 0000000..50b046e --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-foot-website-view.html @@ -0,0 +1,29 @@ +
+ + +
菜单标题内容
+
+ + + {{ m1.label }} + + + {{ m2.label }} + + + +
菜单底部内容
+
+
+
+
子菜单 1.1 的内容
+
子菜单 1.2 的内容
+
菜单二的内容
+
+
diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-group-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-group-website-view.html new file mode 100644 index 0000000..20143ba --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-group-website-view.html @@ -0,0 +1,40 @@ +
+ + + + + {{ group.label }} + + + +
{{ m1.label }}
+
+ + {{ m2.label }} + +
+
+ + +
{{ group.label }}
+
+ + {{ m2.label }} + +
+
+
+
+
子菜单 1.1 的内容
+
子菜单 1.2 的内容
+
+

菜单二的内容

+
+
+
diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-href-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-href-website-view.html new file mode 100644 index 0000000..00fa627 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-href-website-view.html @@ -0,0 +1,23 @@ +
+ + + + {{ m1.label }} + + + {{ m2.label }} + + + +
+
子菜单 1.1 的内容
+
子菜单 1.2 的内容
+
菜单二的内容
+
+
diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-no-router-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-no-router-website-view.html new file mode 100644 index 0000000..cd099c2 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-no-router-website-view.html @@ -0,0 +1,28 @@ +
+ + +
菜单标题
+
+ + + {{ m1.label }} + + + {{ m2.label }} + + +
+
+
子菜单 1.1 的内容
+
子菜单 1.2 的内容
+
+

菜单二的内容

+
+
+
diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-params-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-params-website-view.html new file mode 100644 index 0000000..b8600fe --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-params-website-view.html @@ -0,0 +1,27 @@ +
+ + + + {{ m1.label }} + + + {{ m2.label }} + + + +
+
子菜单 1.1 的内容
+
子菜单 1.2 的内容
+
+

参数: {{ '{' }}"id":"1"{{ '}' }}

+

参数: {{ '{' }}"locale":"zh-cn"{{ '}' }}

+

参数:"tiny"

+
+
+
diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-reload-state-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-reload-state-website-view.html new file mode 100644 index 0000000..bc95ea4 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-reload-state-website-view.html @@ -0,0 +1,19 @@ +
+ + + + {{ m1.label }} + + + {{ m2.label }} + + + +
+
子菜单 1.1 的内容
+
子菜单 1.2 的内容
+
+

菜单二的内容

+
+
+
diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-routerlist-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-routerlist-website-view.html new file mode 100644 index 0000000..842a232 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-routerlist-website-view.html @@ -0,0 +1,27 @@ +
+ + + + {{ m1.label }} + + + {{ m2.label }} + + + +
+
子菜单 1.1 的内容
+
子菜单 1.2 的内容
+
+

菜单二的内容

+

点击跳转到使用路由 D 所指向的页面

+

这里是路由 D 所指向的页面

+
+
+
diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-toggleable-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-toggleable-website-view.html new file mode 100644 index 0000000..563d7a4 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-toggleable-website-view.html @@ -0,0 +1,19 @@ +
+ + + + {{ m1.label }} + + + {{ m2.label }} + + + +
+
子菜单 1.1 的内容
+
子菜单 1.2 的内容
+
+

菜单二的内容

+
+
+
diff --git a/src/leftmenu/demo/src/favicon.ico b/src/leftmenu/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/leftmenu/demo/src/index.html b/src/leftmenu/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/leftmenu/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/leftmenu/demo/src/main.ts b/src/leftmenu/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/leftmenu/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/leftmenu/demo/test.ts b/src/leftmenu/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/leftmenu/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/leftmenu/demo/tsconfig.app.json b/src/leftmenu/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/leftmenu/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/leftmenu/demo/tsconfig.spec.json b/src/leftmenu/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/leftmenu/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/leftmenu/lib/index.ts b/src/leftmenu/lib/index.ts new file mode 100644 index 0000000..287f1e6 --- /dev/null +++ b/src/leftmenu/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiLeftmenuModule'; diff --git a/src/leftmenu/lib/ng-package.json b/src/leftmenu/lib/ng-package.json new file mode 100644 index 0000000..64970cd --- /dev/null +++ b/src/leftmenu/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/leftmenu", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/leftmenu/lib/package.json b/src/leftmenu/lib/package.json new file mode 100644 index 0000000..9efac38 --- /dev/null +++ b/src/leftmenu/lib/package.json @@ -0,0 +1,16 @@ +{ + "name": "@opentiny/ng-leftmenu", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/router": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/platform-browser": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/leftmenu/lib/project.json b/src/leftmenu/lib/project.json new file mode 100644 index 0000000..8964023 --- /dev/null +++ b/src/leftmenu/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/leftmenu/lib", + "sourceRoot": "src/leftmenu/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/leftmenu"], + "options": { + "project": "src/leftmenu/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/leftmenu"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js leftmenu" + }, + { + "command": "ng default-build leftmenu" + }, + { + "command": "node build/clear-default-theme.js leftmenu" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/leftmenu && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build leftmenu && ng pack leftmenu && node build/publish.js leftmenu --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/leftmenu/lib/src/TiLeftmenuComponent.ts b/src/leftmenu/lib/src/TiLeftmenuComponent.ts new file mode 100644 index 0000000..95a8875 --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuComponent.ts @@ -0,0 +1,513 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ElementRef, + EventEmitter, + Input, + NgZone, + Optional, + Output, + Renderer2, + SimpleChange, + SimpleChanges, + ViewEncapsulation, + Inject, + ChangeDetectionStrategy, + IterableDiffer, + IterableDiffers +} from '@angular/core'; +import { ActivatedRoute, NavigationEnd, NavigationExtras, Router, UrlTree } from '@angular/router'; +import { DOCUMENT } from '@angular/common'; +import { Subscription } from 'rxjs'; + +import { Util } from '@opentiny/ng-utils'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiLeftmenuLevel1Component } from './TiLeftmenuLevel1Component'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import packageInfo from '../package.json'; + +/** + * 左侧菜单各项数据接口 + * + * 是TiLeftmenuLevel1 和 TiLeftmenuLevel2 组件的 item 接口的数据类型 + */ +export interface TiLeftmenuItem { + /** + * 设置当前菜单项对应的路由,与 Router.navigate方法的第一个参数一样 + */ + router?: Array; + /** + * 设置当前菜单项对应路由的配置信息,与 Router.navigate方法的第二个参数一样 + */ + routerExtras?: NavigationExtras; + /** + * 设置一个router路由列表,当该列表中的路由被激活时,其对应的菜单项被激活处于高亮状态。 + * 其使用场景为多个路由对应同一个左侧菜单项。 + * + * routerList 数组中每一个元素与 Router.navigate方法的第一个参数一样 + */ + routerList?: Array>; // 数组中元素也为数组 + /** + * 链接地址,点击后路由激活项不变,在新页面打开配置地址。 + * + */ + href?: string; + /** + * 设置分界线(只支持一级菜单),在设置该属性菜单项的底部生成一条区块分界线。 + * + */ + showDividingLine?: boolean; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} +/** + * TiLeftmenu组件提供了一种方便灵活实现页面布局左侧菜单的方式,支持两级菜单。 + * + * 其内部包含5个组件: + * + * TiLeftmenu : 左侧菜单最外层的指令; + * + * TiLeftmenuGroup: 菜单分组指令; + * + * TileftmenuGroupItem:分组文本内容指令; + * + * TiLeftmenuHead : 菜单头部内容指令; + * + * TiLeftmenuLevel1 :一级菜单指令; + * + * TiLeftmenuItem :一级菜单文本内容指令; + * + * TiLeftmenuLevel2 :二级菜单指令。 + * + */ +@Component({ + selector: 'ti-leftmenu', + templateUrl: './leftmenu.html', + styleUrls: ['./leftmenu.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-leftmenu-container]': 'true', + '[class.ti3-leftmenu-hide]': 'collapsed' + }, + encapsulation: ViewEncapsulation.None +}) +export class TiLeftmenuComponent extends TiBaseComponent { + /** + * 设置激活菜单项,值必须是 TiLeftmenuItem 类型,且需要是 TiLeftmenuLevel1 或 TiLeftmenuLevel2 的成员之一。 + * 注意:该属性必须双向绑定 + */ + @Input() active: TiLeftmenuItem; + /** + * 激活菜单项改变时向外通知 + * @ignore + */ + + @Output() readonly activeChange: EventEmitter = new EventEmitter(); + /** + * 当重复点击当前激活菜单项时,通过此属性控制路由刷新。 + * 注意:该属性必须双向绑定 + */ + @Input() reloadState: boolean = true; + /** + * 右侧内容区是否需要刷新的状态值改变时向外通知 + * @ignore + */ + @Output() readonly reloadStateChange: EventEmitter = new EventEmitter(); + // routable接口在3.0.3版本起对外隐藏,10.0.1重新开放 + /** + * 是否使用路由来控制菜单对应内容显示/隐藏,默认使用路由。 + */ + @Input() routable: boolean = true; + /** + * 是否开启左侧菜单面板可折叠功能,默认开启 + */ + @Input() toggleable: boolean = true; + /** + * 左侧菜单面板是否为收起状态,用于设置面板收起/展开状态,默认展开 + */ + @Input() collapsed: boolean = false; + /** + * 左侧菜单面板收起/展开状态改变时向外通知 + */ + @Output() readonly collapsedChange: EventEmitter = new EventEmitter(); + /** + * @ignore + * tiLeftmenu中包含的 TiLeftmenuLevel1Component 实例集合 + */ + public level1Items: Array = []; + protected versionInfo: string = super.getVersion(packageInfo); + private navigationSubscription: Subscription; + /** + * @ignore + */ + public readonly resolvedPromise: Promise = Promise.resolve(null); + /** + * 标志量,是否为url改变(需要去改变active) + */ + private isUrlChange: boolean = false; + /** + * @ignore + */ + private panelContainer: any; + + private onHeadChange: (e: CustomEvent) => void; // console头部高度变化时触发 + + private consoleDataService: any; + /** + * 滚动条的宽度, 在 TiLeftmenuFoot中也有使用 + * @ignore + */ + public scrollWidth: number; + private itemsDiffer: IterableDiffer; + private headElement: HTMLElement; + private placeholderEle: HTMLElement; // 占位元素 + constructor( + elementRef: ElementRef, + public renderer2: Renderer2, + @Optional() private router: Router, + @Optional() private activeRoute: ActivatedRoute, + private iterableDiffers: IterableDiffers, + private tiRenderer: TiRenderer, + private zone: NgZone, + @Inject(DOCUMENT) private document + ) { + super(elementRef, renderer2); + } + + // 判断item的router是否是父路由下默认激活的子路由(即子路由与父路由路径一致) + private static isDefaultActiveChildRoute(item: TiLeftmenuItem): boolean { + // 默认激活的子路由的router可设为[]或[''] + return !item.router || item.router.length === 0 || item.router[0] === ''; + } + + ngOnInit(): void { + super.ngOnInit(); + // 初始时右侧内容区默认显示,需使外部用户的reloadState值为true。 + // 双向绑定数据时,对初始传入的值立即修改并传出时会报错, + // 此处参考ngModel源码setValue的处理,使用promise延后执行时序 + this.resolvedPromise.then(() => { + this.reloadStateChange.emit(true); + }); + this.scrollWidth = this.getScrollbarSize('Y'); + this.itemsDiffer = this.iterableDiffers.find(this.level1Items || []).create(); + + // console头部高度变化需同步改变leftMenu的定位 + const consoleContext = (window).getConsoleContext && (window).getConsoleContext(); + this.consoleDataService = consoleContext?.get && consoleContext.get({ name: 'safearea' }); + this.onHeadChange = (e: CustomEvent) => { + this.renderer2.setStyle(this.nativeElement, 'top', e.detail.top + 'px'); + this.renderer2.setStyle(this.nativeElement, 'left', e.detail.left + 'px'); + // COC场景下console左侧无sidebar,但是CUI工具会添加left:48px important, + // 通过renderer2.setStyle方式添加的important不生效(原因不清楚),所以通过添加class增加样式权重 + this.changeLeftMenuLeftPosition(e.detail.left); + }; + // console初始化首次进来不会触发consoleDataService.onChange,需要根据getSafeArea()设置一次。 + if (this.consoleDataService?.getSafeArea) { + const safeArea = this.consoleDataService.getSafeArea(); + this.renderer2.setStyle(this.nativeElement, 'top', safeArea.top + 'px'); + this.renderer2.setStyle(this.nativeElement, 'left', safeArea.left + 'px'); + // COC场景下console左侧无sidebar,但是CUI工具会添加left:48px important, + // 通过renderer2.setStyle方式添加的important不生效(原因不清楚),所以通过添加class增加样式权重 + this.changeLeftMenuLeftPosition(safeArea.left); + } + // 头部高度变化会触发此事件 + if (this.consoleDataService?.onChange) { + this.consoleDataService.onChange(this.onHeadChange); + } + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + + if (!this.routable) { + return; + } + + this.changeLeftmenuState(); + // 从非leftMenu处跳转改变状态时,leftmenu能同步修改激活状态 + this.navigationSubscription = this.router.events.subscribe((evt: any) => { + if (evt instanceof NavigationEnd) { + const newActiveItem: TiLeftmenuItem = this.getActiveItemByUrl(this.level1Items); + if (newActiveItem && newActiveItem !== this.active) { + this.active = newActiveItem; + this.activeChange.emit(this.active); + this.isUrlChange = true; + } + } + }); + this.headElement = this.nativeElement.querySelector('.ti3-leftmenu-head'); + if (!this.headElement) { + return; // 如果头部元素ti3-leftmenu-head不存在,return停止以下克隆头部元素的操作 + } + this.panelContainer = this.nativeElement.querySelector('.ti3-leftmenu-panel-container'); + + let headElementRect: any; + const headParentnode: HTMLElement = this.renderer2.parentNode(this.headElement); + + let headEleWidth: number; + let headEleHeight: number; + this.zone.runOutsideAngular((): void => { + // 避免不停触发变化检测 + this.renderer2.listen(this.panelContainer, 'scroll', (): void => { + const scrollTop: number = this.panelContainer.scrollTop; + if (!headElementRect) { + headElementRect = this.headElement.getBoundingClientRect(); + headEleWidth = headElementRect.width; + headEleHeight = headElementRect.height; + } + if (scrollTop > 0) { + if (!this.placeholderEle) { + this.placeholderEle = this.renderer2.createElement('div'); + this.renderer2.setStyle(this.placeholderEle, 'height', `${headEleHeight}px`); + + this.renderer2.setStyle(this.headElement, 'width', `${headEleWidth}px`); + this.renderer2.insertBefore(headParentnode, this.placeholderEle, this.headElement); + + this.renderer2.addClass(this.headElement, 'ti3-leftmenu-head-clone'); + this.nativeElement.appendChild(this.headElement); + } + } else if (this.placeholderEle) { + this.renderer2.removeClass(this.headElement, 'ti3-leftmenu-head-clone'); + this.renderer2.insertBefore(headParentnode, this.headElement, this.placeholderEle); + this.renderer2.setStyle(this.headElement, 'width', '100%'); + + this.placeholderEle.parentNode.removeChild(this.placeholderEle); + this.placeholderEle = undefined; + } + }); + }); + } + + ngOnChanges(changes: SimpleChanges): void { + const activeObj: SimpleChange = changes['active']; + if (activeObj && !activeObj.firstChange) { + for (const level1Item of this.level1Items) { + level1Item.setShowChildren(); + level1Item.setActiveState(); + } + + if (this.routable && !this.isUrlChange) { + this.link(); + } + + this.isUrlChange = false; + } + // 监听到collapsed接口的值发生改变 + const collapsedObj: SimpleChange = changes['collapsed']; + if (collapsedObj && !collapsedObj.firstChange) { + this.collapsedHead(); + } + } + + ngAfterViewChecked(): void { + if (this.level1Items.length > 0 && this.routable) { + const differ: any = this.itemsDiffer.diff(this.level1Items); + if (differ) { + this.changeLeftmenuState(); + } + } + } + + ngOnDestroy(): void { + if (this.navigationSubscription) { + this.navigationSubscription.unsubscribe(); + } + if (this.consoleDataService?.offChange) { + this.consoleDataService.offChange(this.onHeadChange); + } + } + + private changeLeftMenuLeftPosition(val: number) { + if (val === 0) { + this.renderer2.addClass(this.nativeElement, 'ti3-leftmenu-panel-no-sidebar'); + } else { + this.renderer2.removeClass(this.nativeElement, 'ti3-leftmenu-panel-no-sidebar'); + } + } + + private changeLeftmenuState(): void { + // 直接输入url(刷新页面)改变状态时,leftmenu能同步修改激活状态 + const urlActiveItem: TiLeftmenuItem = this.getActiveItemByUrl(this.level1Items); + if (urlActiveItem) { + // 双向绑定数据时,对初始传入的值立即修改并传出时会报错, + // 此处参考ngModel源码setValue的处理,使用promise延后执行时序 + this.resolvedPromise.then(() => { + if (urlActiveItem !== this.active) { + this.isUrlChange = true; + this.active = urlActiveItem; + this.activeChange.emit(this.active); + } + }); + } else { + this.link(); + } + } + /** + * @ignore + * 模板中使用 + */ + public toggleClickFn(): void { + this.collapsed = !this.collapsed; + this.collapsedChange.emit(this.collapsed); + } + + // 头部元素显示和隐藏的方法 + private collapsedHead(): void { + if (this.placeholderEle) { + if (this.collapsed) { + this.renderer2.setStyle(this.headElement, 'display', 'none'); + } else { + this.renderer2.removeStyle(this.headElement, 'display'); + } + } + // leftmenu展开/收起状态完成后,再触发事件 + setTimeout(() => { + Util.trigger(document, 'tiReLayoutX'); + }, 0); + } + + private getActiveItemByUrl(arr: Array): TiLeftmenuItem { + let resultItem: TiLeftmenuItem; + for (const component of arr) { + if (this.isActived(component.item)) { + resultItem = component.item; + if (TiLeftmenuComponent.isDefaultActiveChildRoute(component.item)) { + continue; + } else { + return resultItem; + } + } + + if (component.children && component.children.length > 0) { + const result: TiLeftmenuItem = this.getActiveItemByUrl(component.children); + if (result) { + return result; + } + } + } + + return resultItem; + } + + private isActived(item: TiLeftmenuItem): boolean { + const relativeRoute: NavigationExtras = this.getRelativeRoute(item.routerExtras); + if (this.isMatchCurrentUrl(item.router, relativeRoute)) { + return true; + } + + if (Util.isArray(item.routerList) && item.routerList.length > 0) { + for (const router of item.routerList) { + if (this.isMatchCurrentUrl(router, relativeRoute)) { + return true; + } + } + } + + return false; + } + + private isMatchCurrentUrl(router: Array, relativeRoute: NavigationExtras): boolean { + if (!Util.isArray(router)) { + return false; + } + + const itemUrlTree: UrlTree = this.router.createUrlTree(router, relativeRoute); + + return this.router.isActive(itemUrlTree, false); + } + + private link(): void { + if (this.active) { + this.navigate(this.active); + } + } + /** + * @ignore + */ + public navigate(item: TiLeftmenuItem): void { + if (TiLeftmenuComponent.isDefaultActiveChildRoute(item)) { + // routeConfig 可能为null + if (!this.activeRoute.routeConfig) { + return; + } + const path: string = this.activeRoute.routeConfig.path; + const url: string = this.router.routerState.snapshot.url; + const rootUrl: string = url.split(path)[0]; + // 绝对路径跳转 + this.router.navigate([rootUrl + path], item.routerExtras); + } else { + this.router.navigate(item.router, this.getNavigationExtras(item.routerExtras)); + } + } + + /** + * @ignore + * 点击当前选中项时,触发内容区重载 + * @param item 当前点击项 + */ + public triggerReload(item: TiLeftmenuItem): void { + // 使reloadState由false变为true,配合内容区的ngIf做到内容区的重载 + this.reloadStateChange.emit(false); + setTimeout(() => { + this.reloadStateChange.emit(true); + // 如果当前路由就是item.router对应路由,则此导航不作用(路由相同); + // 如果当前路由是item.routerList中的路由,则需要跳转到item.router对应路由 + this.navigate(item); + }, 0); + } + + private getNavigationExtras(routerExtras: Object): Object { + return { relativeTo: this.activeRoute, ...routerExtras }; + } + + private getRelativeRoute(routerExtras: Object): NavigationExtras { + if (!routerExtras || !routerExtras['relativeTo']) { + return { relativeTo: this.activeRoute }; + } + + return { relativeTo: routerExtras['relativeTo'] }; + } + /** + * 得到浏览器滚动条的宽度和高度 + * 代表滚动条的方向,取值为X or Y + */ + private getScrollbarSize(direction: string): number { + const outerDiv: any = this.renderer.createElement('div'); + const innerDiv: any = this.renderer.createElement('div'); + + this.renderer.setStyle(innerDiv, 'width', '100%'); + this.renderer.setStyle(innerDiv, 'height', '100%'); + + this.renderer.appendChild(outerDiv, innerDiv); + this.tiRenderer.setStyles(outerDiv, { + position: 'absolute', + top: '-9999px', + left: '-9999px', + width: '100px', + height: '100px', + overflow: 'hidden' + }); + + this.renderer.appendChild(this.document.body, outerDiv); + + const field: string = direction === 'X' ? 'height' : 'width'; + const noScrollSize: number = typeof innerDiv.getBoundingClientRect === 'function' ? innerDiv.getBoundingClientRect()[field] : 0; + this.renderer.setStyle(outerDiv, 'overflow', 'scroll'); + const withScrollSize: number = typeof innerDiv.getBoundingClientRect === 'function' ? innerDiv.getBoundingClientRect()[field] : 0; + this.document.body.removeChild(outerDiv); + + return parseFloat((noScrollSize - withScrollSize).toFixed(1)); + } +} diff --git a/src/leftmenu/lib/src/TiLeftmenuFootComponent.ts b/src/leftmenu/lib/src/TiLeftmenuFootComponent.ts new file mode 100644 index 0000000..c624396 --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuFootComponent.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiLeftmenuComponent } from './TiLeftmenuComponent'; +import { Component, ElementRef, Renderer2, ChangeDetectionStrategy } from '@angular/core'; +@Component({ + selector: 'ti-leftmenu-foot', + templateUrl: './leftmenu-foot.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-leftmenu-foot]': 'true' + } +}) +export class TiLeftmenuFootComponent { + constructor(private renderer2: Renderer2, private elementRef: ElementRef, private tiLeftmenu: TiLeftmenuComponent) {} + + ngAfterViewInit(): void { + const templateDom: any = this.renderer2.createElement('div'); + const panelDom: any = this.tiLeftmenu.nativeElement.querySelector('.ti3-leftmenu-panel'); + const hostElement: any = this.elementRef.nativeElement; + // 修复SSR错误:ERROR ReferenceError: getComputedStyle is not defined + const spaceRight: string = + typeof getComputedStyle === 'function' ? getComputedStyle(hostElement).getPropertyValue('stroke-width') : '0'; + + this.renderer2.setStyle(templateDom, 'height', hostElement.offsetHeight + 'px'); + this.renderer2.appendChild(panelDom, templateDom); + + this.renderer2.setStyle(hostElement, 'width', `calc(100% - ${this.tiLeftmenu.scrollWidth}px)`); + this.renderer2.setStyle(hostElement, 'marginRight', `${this.tiLeftmenu.scrollWidth}px`); + this.renderer2.setStyle(hostElement, 'paddingRight', `calc(${spaceRight} - ${this.tiLeftmenu.scrollWidth}px)`); + } +} diff --git a/src/leftmenu/lib/src/TiLeftmenuGroupComponent.ts b/src/leftmenu/lib/src/TiLeftmenuGroupComponent.ts new file mode 100644 index 0000000..0535079 --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuGroupComponent.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiLeftmenuLevel1Component } from './TiLeftmenuLevel1Component'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, Renderer2 } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * TiLeftmenuGroup 是分组菜单组件,嵌在 TiLeftmenu组件中使用。 + * + * + */ +@Component({ + selector: 'ti-leftmenu-group', + templateUrl: './leftmenu-group.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLeftmenuGroupComponent extends TiBaseComponent { + /** + * @ignore + * 设置当前分组菜单项是否展开 + */ + @Input() expanded: boolean = false; + /** + * @ignore + * 当前TiLeftmenuGroup下的所有TiLeftmenuLevel1的集合 + */ + public children: Array = []; + protected versionInfo: string = super.getVersion(packageInfo); + + constructor(elementRef: ElementRef, renderer2: Renderer2, public changeRef: ChangeDetectorRef) { + super(elementRef, renderer2); + } + + ngAfterViewInit(): void { + if (this.nativeElement.parentNode.parentNode.nodeName === 'TI-LEFTMENU-LEVEL1') { + this.renderer.addClass(this.nativeElement, 'ti3-leftmenu-level2-group'); + } + } + + /** + * @ignore + * 在模板中使用,当前分组中的子项是否有激活项 + */ + public hasActivedChild(): boolean { + if (!this.children || this.children.length <= 0) { + return false; + } + + for (const level1 of this.children) { + if (level1.isActived || level1.hasActivedChildren()) { + return true; + } + } + + return false; + } +} diff --git a/src/leftmenu/lib/src/TiLeftmenuGroupItemComponent.ts b/src/leftmenu/lib/src/TiLeftmenuGroupItemComponent.ts new file mode 100644 index 0000000..3ca58ab --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuGroupItemComponent.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +/** + * TiLeftmenuGroupItem 是分组菜单文本内容组件,嵌在 TiLeftmenuGroupComponent组件中使用,其包裹的元素会作为 leftmenu 的分组菜单项内容显示 + * + * + */ +@Component({ + selector: 'ti-leftmenu-group-item', + templateUrl: './leftmenu-group-item.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLeftmenuGroupItemComponent {} diff --git a/src/leftmenu/lib/src/TiLeftmenuHeadComponent.ts b/src/leftmenu/lib/src/TiLeftmenuHeadComponent.ts new file mode 100644 index 0000000..8b4367c --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuHeadComponent.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +/** + * TiLeftmenuHead是菜单头部组件,其包裹的元素会作为leftmenu的菜单头部 + */ +@Component({ + selector: 'ti-leftmenu-head', + templateUrl: './leftmenu-head.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-leftmenu-head]': 'true' + } +}) +export class TiLeftmenuHeadComponent {} diff --git a/src/leftmenu/lib/src/TiLeftmenuItemComponent.ts b/src/leftmenu/lib/src/TiLeftmenuItemComponent.ts new file mode 100644 index 0000000..0c335f0 --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuItemComponent.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ElementRef, ChangeDetectionStrategy } from '@angular/core'; +/** + * TiLeftmenuItem 是一级菜单文本内容组件,嵌在 TiLeftmenuLevel1Component组件中使用,其包裹的元素会作为 leftmenu 的一级菜单的菜单项内容显示 + */ +@Component({ + selector: 'ti-leftmenu-item', + templateUrl: './leftmenu-item.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-leftmenu-level1-label]': 'element.textContent!==""' + } +}) +export class TiLeftmenuItemComponent { + /** + * @ignore + */ + public element: any; + constructor(elementRef: ElementRef) { + this.element = elementRef.nativeElement; + } +} diff --git a/src/leftmenu/lib/src/TiLeftmenuLevel1Component.ts b/src/leftmenu/lib/src/TiLeftmenuLevel1Component.ts new file mode 100644 index 0000000..491d7e5 --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuLevel1Component.ts @@ -0,0 +1,261 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + AfterViewInit, + ChangeDetectorRef, + ChangeDetectionStrategy, + Component, + ContentChild, + ElementRef, + Input, + OnDestroy, + OnInit, + Optional, + Renderer2, + SecurityContext +} from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; + +import { TiLeftmenuComponent, TiLeftmenuItem } from './TiLeftmenuComponent'; +import { TiLeftmenuGroupComponent } from './TiLeftmenuGroupComponent'; +import { TiLeftmenuLevel2Component } from './TiLeftmenuLevel2Component'; +import { TiLeftmenuItemComponent } from './TiLeftmenuItemComponent'; +import { Util } from '@opentiny/ng-utils'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * TiLeftmenuLevel1Component 是一级菜单组件,嵌在 TiLeftmenu 中使用 + * + */ +@Component({ + selector: 'ti-leftmenu-level1', + templateUrl: './leftmenu-level1.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-leftmenu-level1]': 'true', + '[class.ti3-leftmenu-level1-dividing]': 'item.showDividingLine' + } +}) +export class TiLeftmenuLevel1Component extends TiBaseComponent implements AfterViewInit, OnDestroy, OnInit { + /** + * 设置当前菜单项的内容信息 + * + */ + @Input() item: TiLeftmenuItem; + /** + * 是否禁用 + */ + @Input() disabled: boolean = false; + /** + * @ignore + */ + @ContentChild(TiLeftmenuItemComponent, { static: false }) + leftmenuItem: TiLeftmenuItemComponent; + /** + * @ignore + * 当前 TiLeftmenuLevel1下所有的 TiLeftmenuLevel2 的集合 + */ + public children: Array = []; + /** + * @ignore + * 模板中使用,二级菜单是否展开 + */ + public showChildren: boolean = false; + /** + * @ignore + * 模板中使用,是否是激活状态 + */ + public isActived: boolean = false; + protected versionInfo: string = super.getVersion(packageInfo); + /** + * @ignore + */ + private leftmenu: TiLeftmenuComponent; + private element: any; + private oldHref: string; + constructor( + leftmenu: TiLeftmenuComponent, + private elementRef: ElementRef, + renderer2: Renderer2, + private changeRef: ChangeDetectorRef, + @Optional() private leftmenuGroup: TiLeftmenuGroupComponent, + private domSanitizer: DomSanitizer + ) { + super(elementRef, renderer2); + this.element = this.elementRef.nativeElement; + this.leftmenu = leftmenu; + } + + ngOnInit(): void { + super.ngOnInit(); + this.oldHref = this.item?.href; + if (this.leftmenuGroup) { + this.leftmenuGroup.children.push(this); + } + } + + ngAfterViewInit(): void { + this.setShowChildren(); + this.setActiveState(); + this.leftmenu.level1Items.push(this); + this.changeRef.detectChanges(); + } + + ngDoCheck(): void { + if (this.item?.href !== this.oldHref) { + this.oldHref = this.item?.href; + this.changeRef.detectChanges(); + } + } + + ngOnDestroy(): void { + const index: number = this.leftmenu.level1Items.indexOf(this); + if (this.leftmenuGroup) { + const groupIndex: number = this.leftmenuGroup.children.indexOf(this); + if (groupIndex !== -1) { + this.leftmenuGroup.children.splice(groupIndex, 1); + } + } + + if (index !== -1) { + this.leftmenu.level1Items.splice(index, 1); + } + } + /** + * @ignore + * 模板中使用,点击一级菜单项时调用 + */ + public selectFn(): void { + if (this.disabled) { + return; + } + if (this.item.href) { + this.openHref(this.item.href); + this.element.querySelector('.ti3-leftmenu-level1-item').blur(); + + return; + } + const oldactive: TiLeftmenuItem = this.leftmenu.active; + + if (this.hasChildren()) { + this.showChildren = !this.showChildren; + this.isActived = !this.showChildren && this.hasActivedChildren(); + if (this.showChildren) { + for (const level1Items of this.leftmenu.level1Items) { + if (level1Items === this) { + continue; + } + if (level1Items.showChildren) { + level1Items.showChildren = false; + if (level1Items.hasActivedChildren()) { + level1Items.isActived = true; + } + // onpush模式下showChildren变更,视图未刷新 + level1Items.changeRef.markForCheck(); + break; + } + } + } + } else { + if (this.leftmenu.routable) { + // 点击当前已经激活的item时,刷新对应路由 + if (this.item === oldactive) { + this.leftmenu.triggerReload(this.item); + } else { + // 点击其他项需要进行跳转,来触发路由守卫,实际是否跳转取决于路由守卫返回值。 + this.leftmenu.navigate(this.item); + } + } else { + if (this.item !== oldactive) { + this.leftmenu.active = this.item; + this.leftmenu.activeChange.emit(this.item); + this.isActived = true; + } + } + } + // leftmenuGroup的模板中绑定的 hasActivedChild() 方法中用到了level1 的 isActived 值 + if (this.leftmenuGroup) { + this.leftmenuGroup.changeRef.markForCheck(); + } + } + /** + * @ignore + * 在 TiLeftmenuComponent.ts 中调用了 + */ + public setShowChildren(): void { + // 初始化时,子菜单中有当前选中状态时,显示子菜单 + this.showChildren = this.hasActivedChildren(); + this.changeRef.markForCheck(); + } + + /** + * @ignore + * 在模板上使用 + */ + public hasChildren(): boolean { + return this.children && this.children.length > 0; + } + /** + * @ignore + * 在模板上使用 + */ + public hasActivedChildren(): boolean { + if (!this.hasChildren()) { + return false; + } + + const result: Array = this.children.filter((level2: TiLeftmenuLevel2Component) => { + return level2.item === this.leftmenu.active; + }); + + return result.length > 0; + } + + /** + * @ignore + * 设置当前菜单是否处于激活状态,下边两种情况下将当前菜单设置为激活状态: + * + * 有子菜单时,当子菜单有有激活状态项并且子菜单关闭状态情况下; + * + * 没有子菜单时,当前菜单就是激活菜单项。 + */ + public setActiveState(): void { + this.isActived = (this.hasActivedChildren() && !this.showChildren) || this.item === this.leftmenu.active; + // onpush模式下,active状态视图未及时更新 + this.changeRef.markForCheck(); + // leftmenuGroup的模板中绑定的 hasActivedChild() 方法中用到了level1 的 isActived 值 + if (this.leftmenuGroup) { + this.leftmenuGroup.changeRef.markForCheck(); + } + } + + /** + * @ignore + */ + public onStateIconClick(): void { + if (!this.disabled) { + Util.trigger(this.leftmenuItem?.element, 'click'); + } + } + + /** + * @ignore + * 如果有链接,跳转新页面 + */ + public openHref(href: string): void { + const newTab: any = window.open(this.domSanitizer.sanitize(SecurityContext.URL, href), '_blank'); + // IE 下有时 newTab 不存在 + if (newTab) { + newTab.opener = null; + } + } +} diff --git a/src/leftmenu/lib/src/TiLeftmenuLevel2Component.ts b/src/leftmenu/lib/src/TiLeftmenuLevel2Component.ts new file mode 100644 index 0000000..3f23776 --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuLevel2Component.ts @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Inject, + Input, + NgZone, + OnDestroy, + OnInit, + Renderer2 +} from '@angular/core'; +import { TiLeftmenuComponent, TiLeftmenuItem } from './TiLeftmenuComponent'; +import { TiLeftmenuLevel1Component } from './TiLeftmenuLevel1Component'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { DOCUMENT } from '@angular/common'; +import packageInfo from '../package.json'; +/** + * TiLeftmenuLevel2Component 是二级菜单组件,嵌在 TiLeftmenuLevel1 中使用 + */ +@Component({ + selector: 'ti-leftmenu-level2', + templateUrl: './leftmenu-level2.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-leftmenu-level2-disabled]': 'disabled', + '[class.ti3-leftmenu-level2]': 'true', + '[class.ti3-leftmenu-level2-active]': 'this.item === this.leftmenu.active', + '[attr.tabindex]': 'disabled ? -1 : 0', + '[id]': 'appendId("leftmenu_level2")', + '(click)': 'selectFn()', + '(keyup.enter)': 'selectFn()' + } +}) +export class TiLeftmenuLevel2Component extends TiBaseComponent implements OnInit, OnDestroy, AfterViewInit { + /** + * 设置当前菜单项的内容信息 + */ + @Input() item: TiLeftmenuItem; + /** + * 是否禁用 + */ + @Input() disabled: boolean = false; + /** + * @ignore + */ + public leftmenu: TiLeftmenuComponent; + public level2Ele: any; + public renderer: any; + protected versionInfo: string = super.getVersion(packageInfo); + private oldHref: string; + private documentVisibilitychangeListener: () => void; + + constructor( + elementRef: ElementRef, + renderer2: Renderer2, + leftmenu: TiLeftmenuComponent, + private level1: TiLeftmenuLevel1Component, + private zone: NgZone, + @Inject(DOCUMENT) private document, + private changeRef: ChangeDetectorRef + ) { + super(elementRef, renderer2); + this.leftmenu = leftmenu; + this.level2Ele = elementRef; + this.renderer = renderer2; + } + + ngOnInit(): void { + super.ngOnInit(); + this.level1.children.push(this); + this.oldHref = this.item?.href; + } + ngAfterViewInit(): void { + this.zone.runOutsideAngular(() => { + let outlineColor: string; + const transparentColor: string = 'rgba(0, 0, 0, 0)'; // 透明色,跟transparent色值一致 + this.renderer.listen(this.level2Ele.nativeElement, 'mousedown', (): void => { + // 当点击的时候,不需要有outline。 + this.renderer.setStyle(this.level2Ele.nativeElement, 'outlineColor', 'transparent'); + }); + this.renderer.listen(this.level2Ele.nativeElement, 'blur', (): void => { + // blur时仅能读取到outlineColor,作为窗口切换前的状态标志。 + outlineColor = getComputedStyle(this.level2Ele.nativeElement).outlineColor; + // 恢复outline原生状态 + this.renderer.setStyle(this.level2Ele.nativeElement, 'outline', ''); + }); + this.documentVisibilitychangeListener = this.renderer.listen(this.document, 'visibilitychange', (): void => { + if ( + this.document.visibilityState === 'visible' && + outlineColor === transparentColor && + this.document.activeElement === this.level2Ele.nativeElement + ) { + this.renderer.setStyle(this.level2Ele.nativeElement, 'outlineColor', 'transparent'); + } + }); + }); + } + + ngDoCheck(): void { + if (this.item?.href !== this.oldHref) { + this.oldHref = this.item?.href; + this.changeRef.detectChanges(); + } + } + /** + * @ignore + * 模板中使用,点击二级菜单项时调用 + */ + public selectFn(): void { + if (this.disabled) { + return; + } + if (this.item.href) { + this.level1.openHref(this.item.href); + this.level2Ele.nativeElement.blur(); // 为了没有outline。 + + return; + } + + if (this.leftmenu.routable) { + // 点击当前已经激活的item时,刷新对应路由 + if (this.leftmenu.active === this.item) { + this.leftmenu.triggerReload(this.item); + } else { + // 点击其他项需要进行跳转,来触发路由守卫,实际是否跳转取决于路由守卫返回值。 + this.leftmenu.navigate(this.item); + } + } else { + if (this.leftmenu.active !== this.item) { + this.leftmenu.active = this.item; + this.leftmenu.activeChange.emit(this.item); + } + } + } + + ngOnDestroy(): void { + const index: number = this.level1.children.indexOf(this); + if (index !== -1) { + this.level1.children.splice(index, 1); + } + if (this.documentVisibilitychangeListener) { + this.documentVisibilitychangeListener(); + } + } +} diff --git a/src/leftmenu/lib/src/TiLeftmenuModule.ts b/src/leftmenu/lib/src/TiLeftmenuModule.ts new file mode 100644 index 0000000..73c1ac1 --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuModule.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiLeftmenuComponent } from './TiLeftmenuComponent'; +import { TiLeftmenuHeadComponent } from './TiLeftmenuHeadComponent'; +import { TiLeftmenuGroupItemComponent } from './TiLeftmenuGroupItemComponent'; +import { TiLeftmenuGroupComponent } from './TiLeftmenuGroupComponent'; +import { TiLeftmenuLevel1Component } from './TiLeftmenuLevel1Component'; +import { TiLeftmenuItemComponent } from './TiLeftmenuItemComponent'; +import { TiLeftmenuLevel2Component } from './TiLeftmenuLevel2Component'; +import { TiLeftmenuFootComponent } from './TiLeftmenuFootComponent'; +import { TiOutlineModule } from '@opentiny/ng-outline'; + +@NgModule({ + imports: [CommonModule, TiIconModule, TiRendererModule, TiOutlineModule], + exports: [ + TiLeftmenuComponent, + TiLeftmenuHeadComponent, + TiLeftmenuLevel1Component, + TiLeftmenuItemComponent, + TiLeftmenuLevel2Component, + TiLeftmenuGroupComponent, + TiLeftmenuGroupItemComponent, + TiLeftmenuFootComponent + ], + declarations: [ + TiLeftmenuComponent, + TiLeftmenuHeadComponent, + TiLeftmenuLevel1Component, + TiLeftmenuItemComponent, + TiLeftmenuLevel2Component, + TiLeftmenuGroupComponent, + TiLeftmenuGroupItemComponent, + TiLeftmenuFootComponent + ] +}) +export class TiLeftmenuModule {} +export * from './TiLeftmenuComponent'; +export { TiLeftmenuComponent, TiLeftmenuItem } from './TiLeftmenuComponent'; +export { TiLeftmenuHeadComponent } from './TiLeftmenuHeadComponent'; +export { TiLeftmenuLevel1Component } from './TiLeftmenuLevel1Component'; +export { TiLeftmenuItemComponent } from './TiLeftmenuItemComponent'; +export { TiLeftmenuLevel2Component } from './TiLeftmenuLevel2Component'; +export { TiLeftmenuGroupComponent } from './TiLeftmenuGroupComponent'; +export { TiLeftmenuGroupItemComponent } from './TiLeftmenuGroupItemComponent'; +export { TiLeftmenuFootComponent } from './TiLeftmenuFootComponent'; diff --git a/src/leftmenu/lib/src/leftmenu-foot.html b/src/leftmenu/lib/src/leftmenu-foot.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu-foot.html @@ -0,0 +1 @@ + diff --git a/src/leftmenu/lib/src/leftmenu-group-item.html b/src/leftmenu/lib/src/leftmenu-group-item.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu-group-item.html @@ -0,0 +1 @@ + diff --git a/src/leftmenu/lib/src/leftmenu-group.html b/src/leftmenu/lib/src/leftmenu-group.html new file mode 100644 index 0000000..f5a0655 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu-group.html @@ -0,0 +1,4 @@ +
+ +
+ diff --git a/src/leftmenu/lib/src/leftmenu-head.html b/src/leftmenu/lib/src/leftmenu-head.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu-head.html @@ -0,0 +1 @@ + diff --git a/src/leftmenu/lib/src/leftmenu-item.html b/src/leftmenu/lib/src/leftmenu-item.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu-item.html @@ -0,0 +1 @@ + diff --git a/src/leftmenu/lib/src/leftmenu-level1.html b/src/leftmenu/lib/src/leftmenu-level1.html new file mode 100644 index 0000000..ab1ce08 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu-level1.html @@ -0,0 +1,28 @@ +
+ + + + + +
+
+ +
diff --git a/src/leftmenu/lib/src/leftmenu-level2.html b/src/leftmenu/lib/src/leftmenu-level2.html new file mode 100644 index 0000000..ff39310 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu-level2.html @@ -0,0 +1,3 @@ + + + diff --git a/src/leftmenu/lib/src/leftmenu.html b/src/leftmenu/lib/src/leftmenu.html new file mode 100644 index 0000000..0cd8e62 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu.html @@ -0,0 +1,17 @@ +
+
+ +
+
+ +
diff --git a/src/leftmenu/lib/src/leftmenu.less b/src/leftmenu/lib/src/leftmenu.less new file mode 100644 index 0000000..99ba898 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu.less @@ -0,0 +1,340 @@ +@import '../../../themes/basic/base-all.less'; +ti-leftmenu { + --ti-leftmenu-toggle-width: var(--ti-common-size-3x); + --ti-leftmenu-level2-left-line: 30px; // 二级菜单的左侧边线位置 + --ti-leftmenu-panel-left-padding: 2px; // 菜单容器的左右内边距 + --ti-leftmenu-width: 192px; + --ti-leftment-leftmenu-head-text-align: left; + --ti-leftmenu-angle-normal-width: calc(var(--ti-leftmenu-angle-normal-height) * 1.6); //三角按钮宽度 + --ti-leftmenu-angle-normal-height: 5px; //三角按钮高度 + --ti-leftmenu-group-padding-top: 18px; + --ti-leftmenu-group-padding-bottom: 2px; +} + +.ti3-leftmenu-container { + display: block; + position: fixed; + top: 0px; + left: 0px; + bottom: 0px; + z-index: 990; // 需要被halfmodal覆盖,halfmodal为999,halfmodal的遮罩为998 + width: var(--ti-leftmenu-width); + background-color: var(--ti-common-color-bg-white-normal); + box-shadow: var(--ti-common-shadow-1-right); + + &.ti3-leftmenu-hide { + width: 0px !important; + & .ti3-leftmenu-panel-container { + height: 0px !important; // 处理ie下面板关闭出现竖向滚动条 + } + & .ti3-leftmenu-foot { + padding: 0; + margin: 0; + } + & .ti3-leftmenu-head { + padding: 0; + margin: 0; + } + } +} + +.ti3-leftmenu-panel-no-sidebar { + left: 0 !important; +} + +.ti3-leftmenu-panel-container { + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + width: 100%; + overflow-y: auto; + overflow-x: hidden; + .box-sizing(border-box); +} + +.ti3-leftmenu-panel { + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + width: 100%; + padding: 0 var(--ti-leftmenu-panel-left-padding); + overflow-y: visible; + overflow-x: visible; + font-size: var(--ti-common-font-size-base); + color: var(--ti-common-color-text-secondary); + .box-sizing(border-box); +} + +.ti3-leftmenu-toggle { + position: absolute; + top: 50%; + width: var(--ti-leftmenu-toggle-width); + height: 80px; + background-color: var(--ti-common-color-bg-white-normal); + border-radius: 0 var(--ti-leftmenu-toggle-width) var(--ti-leftmenu-toggle-width) 0; + transform: translateY(-50%); + cursor: pointer; + &:after { + display: block; + content: ''; + position: relative; + // 小三角旋转后,中心点不变,因此以未旋转的三角形上下左右居中定位,旋转后仍在中间位置 + top: calc(50% - var(--ti-leftmenu-angle-normal-height) / 2); + left: calc(50% - var(--ti-leftmenu-angle-normal-width) / 2); + .triangle-down(var(--ti-leftmenu-angle-normal-width), var(--ti-leftmenu-angle-normal-height), var(--ti-common-color-icon-normal)); + transform: rotateZ(90deg); + } + &.ti3-leftmenu-toggle-show { + left: auto; + right: calc(-1 * var(--ti-leftmenu-toggle-width)); + } + &.ti3-leftmenu-toggle-hide { + left: 0px; + right: auto; + &:after { + transform: rotateZ(270deg); + } + } +} +.ti3-leftmenu-head { + display: block; + .box-sizing(border-box); + padding: var(--ti-common-space-5x) var(--ti-common-space-9x) var(--ti-common-space-5x) var(--ti-common-space-5x); + margin-bottom: 10px; + overflow: hidden; + font-size: var(--ti-common-font-size-1); + line-height: var(--ti-common-size-5x); + color: var(--ti-common-color-text-primary); + text-align: var(--ti-leftment-leftmenu-head-text-align); + + &:after { + display: block; + position: relative; + bottom: -20px; + content: ''; + width: 100%; + border-bottom: 1px solid var(--ti-common-color-line-dividing); + } +} +.ti3-leftmenu-head-clone { + position: absolute; + top: 0; + left: var(--ti-leftmenu-panel-left-padding); + background: var(--ti-common-color-bg-white-normal); + z-index: 3; +} + +.ti3-leftmenu-level1 { + display: block; + padding: 0; + margin: 0; +} + +.ti3-leftmenu-level1-dividing { + position: relative; + &:after { + .group-dividng(auto, 0); + } +} +.group-dividng(@top: auto, @bottom: auto) { + position: absolute; + left: var(--ti-common-space-9x); + top: @top; + bottom: @bottom; + border: 1px dashed var(--ti-common-color-line-dividing); + width: calc(100% - var(--ti-common-space-9x) - var(--ti-common-space-5x)); + content: ''; +} + +.ti3-leftmenu-level1-item { + position: relative; + cursor: pointer; + display: block; + line-height: var(--ti-common-line-height-number); + .box-sizing(border-box); + & .ti3-leftmenu-triangle.ti3-leftmenu-triangle-down { + .rotate(0); + } + & .ti3-leftmenu-triangle.ti3-leftmenu-triangle-up { + .rotate(180deg); + } + &.ti3-leftmenu-level1-active-item { + color: var(--ti-common-color-text-highlight); + } + &:not(.ti3-leftmenu-level1-disabled):hover { + background-color: var(--ti-common-color-bg-white-emphasize); + color: var(--ti-common-color-text-highlight); + + & .ti3-leftmenu-triangle.ti3-leftmenu-triangle-down { + border-top-color: var(--ti-common-color-icon-hover); + } + & .ti3-leftmenu-triangle.ti3-leftmenu-triangle-up { + border-top-color: var(--ti-common-color-icon-hover); + } + } +} + +.ti3-leftmenu-level1-active-item, +.ti3-leftmenu-level2-active { + position: relative; + & .ti3-leftmenu-triangle.ti3-leftmenu-triangle-down { + border-top-color: var(--ti-common-color-icon-hover); // 设置小三角的颜色 + } +} +// 激活菜单的左侧高亮边线 +.ti3-leftmenu-level1-active-item:before, +.ti3-leftmenu-level2-active:before { + content: ''; + position: absolute; + top: var(--ti-common-space-10); + z-index: 1; + width: 2px; + bottom: var(--ti-common-space-10); + background: var(--ti-common-color-line-active); +} +.ti3-leftmenu-level1-active-item:before { + left: var(--ti-common-space-10); +} +.ti3-leftmenu-level2-active:before { + left: var(--ti-leftmenu-level2-left-line); +} + +.ti3-leftmenu-level1-label { + padding: var(--ti-common-space-10) var(--ti-common-space-9x) var(--ti-common-space-10) var(--ti-common-space-5x); + display: block; + word-break: normal; +} + +.ti3-leftmenu-level1-state-icon { + position: absolute; + top: ~'calc(50% - 11px)'; + right: 9px; + font-size: 12px; +} + +.ti3-leftmenu-level2 { + position: relative; + display: block; + line-height: var(--ti-common-line-height-number); + padding: var(--ti-common-space-10) var(--ti-common-space-9x) var(--ti-common-space-10) var(--ti-common-space-10x); + cursor: pointer; + .box-sizing(border-box); + word-break: normal; + + // 二级菜单的左侧灰色边线 + &:after { + display: block; + content: ''; + position: absolute; + top: 0px; + bottom: 0px; + left: var(--ti-leftmenu-level2-left-line); + border-left: 1px solid var(--ti-common-color-line-dividing); + border-right: 1px solid var(--ti-common-color-line-dividing); + } + + &:not(.ti3-leftmenu-level2-disabled):hover { + background-color: var(--ti-common-color-bg-white-emphasize); + color: var(--ti-common-color-text-highlight); + } + + &.ti3-leftmenu-level2-active { + color: var(--ti-common-color-text-highlight); + } +} + +.ti3-leftmenu-level1-disabled, +.ti3-leftmenu-level2-disabled { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; +} + +.ti3-leftmenu-level2:first-of-type:after { + border-top: 1px solid var(--ti-common-color-line-dividing); + top: var(--ti-common-space-10); +} + +.ti3-leftmenu-level2:last-of-type:after { + border-bottom: 1px solid var(--ti-common-color-line-dividing); + bottom: var(--ti-common-space-10); +} + +// 向下的小三角,该组件中所有方向的小三角都是由该三角旋转生成的 +.ti3-leftmenu-triangle { + position: absolute; + right: 14px; + top: calc(50% - var(--ti-leftmenu-angle-normal-height) / 2); + content: ''; + .triangle-down(var(--ti-leftmenu-angle-normal-width), var(--ti-leftmenu-angle-normal-height), var(--ti-common-color-icon-normal)); +} + +.ti3-leftmenu-href { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + font-size: 16px; +} + +/************封装后的leftmenu样式start************/ +.ti3-leftmenu-logo { + height: 60px; + + .ti3-leftmenu-logo-ficon { + font-size: 60px; + line-height: 60px; + color: var(--ti-common-color-text-primary); + vertical-align: middle; + } +} + +.ti3-leftmenu-content { + background: #ffffff; + position: absolute; + top: 0; + bottom: 0; + left: 240px; + right: 0; + overflow: auto; +} + +.ti3-leftmenu-group-wrapper { + position: relative; + color: var(--ti-common-color-text-weaken); + line-height: var(--ti-common-line-height-number); + padding: var(--ti-leftmenu-group-padding-top) var(--ti-common-space-9x) var(--ti-leftmenu-group-padding-bottom) var(--ti-common-space-5x); +} + +.ti3-leftmenu-level2-group .ti3-leftmenu-group-wrapper { + padding-left: var(--ti-leftmenu-level2-left-line); + padding-top: var(--ti-common-space-10); +} + +.ti3-leftmenu-foot { + position: absolute; + bottom: 0; + text-align: var(--ti-leftment-leftmenu-head-text-align); + padding: var(--ti-common-space-10) 0 var(--ti-common-space-10) var(--ti-common-space-5x); + stroke-width: var(--ti-common-space-9x); // 在 TS 中读取。 + .box-sizing(border-box); + color: var(--ti-common-color-text-link); + overflow: hidden; + background: var(--ti-common-color-bg-white-normal); + z-index: 3; + line-height: var(--ti-common-line-height-number); + &:hover { + cursor: pointer; + color: var(--ti-common-color-text-link-hover); + } + &:before { + position: relative; + top: calc(-1 * var(--ti-common-space-10)); + left: 0; + content: ''; + border-bottom: 1px solid var(--ti-common-color-line-dividing); + display: block; + } +} +/************封装后的leftmenu样式end************/ diff --git a/src/linkbutton/demo/karma.conf.js b/src/linkbutton/demo/karma.conf.js new file mode 100644 index 0000000..22d9d1a --- /dev/null +++ b/src/linkbutton/demo/karma.conf.js @@ -0,0 +1,41 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [{ type: 'html' }, { type: 'text-summary' }] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/linkbutton/demo/project.json b/src/linkbutton/demo/project.json new file mode 100644 index 0000000..1629e29 --- /dev/null +++ b/src/linkbutton/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/linkbutton/demo", + "sourceRoot": "src/linkbutton/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/linkbutton", + "index": "src/linkbutton/demo/src/index.html", + "main": "src/linkbutton/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/linkbutton/demo/tsconfig.app.json", + "assets": ["src/linkbutton/demo/src/favicon.ico", "src/linkbutton/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "linkbutton-demo:build:production" + }, + "development": { + "browserTarget": "linkbutton-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js linkbutton" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/linkbutton/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/linkbutton/demo/tsconfig.spec.json", + "karmaConfig": "src/linkbutton/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/linkbutton/demo/src/app/AppComponent.ts b/src/linkbutton/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/linkbutton/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/linkbutton/demo/src/app/AppModule.ts b/src/linkbutton/demo/src/app/AppModule.ts new file mode 100644 index 0000000..951afe7 --- /dev/null +++ b/src/linkbutton/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { LinkbuttonTestModule } from './linkbutton/LinkbuttonTestModule'; + +@NgModule({ + imports: [ + LinkbuttonTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/linkbutton/demo/src/app/IndexComponent.ts b/src/linkbutton/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..b802109 --- /dev/null +++ b/src/linkbutton/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { LinkbuttonTestModule } from './linkbutton/LinkbuttonTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = LinkbuttonTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/linkbutton/demo/src/app/app.html b/src/linkbutton/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/linkbutton/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/linkbutton/demo/src/app/linkbutton/LinkbuttonBasicComponent.ts b/src/linkbutton/demo/src/app/linkbutton/LinkbuttonBasicComponent.ts new file mode 100644 index 0000000..036a1c2 --- /dev/null +++ b/src/linkbutton/demo/src/app/linkbutton/LinkbuttonBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './linkbutton-basic.html' +}) +export class LinkbuttonBasicComponent { + iconName: string = 'sold-out'; +} diff --git a/src/linkbutton/demo/src/app/linkbutton/LinkbuttonTestModule.ts b/src/linkbutton/demo/src/app/linkbutton/LinkbuttonTestModule.ts new file mode 100644 index 0000000..1243423 --- /dev/null +++ b/src/linkbutton/demo/src/app/linkbutton/LinkbuttonTestModule.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { TiLinkbuttonModule } from '@opentiny/ng'; + +import { LinkbuttonBasicComponent } from './LinkbuttonBasicComponent'; +@NgModule({ + imports: [CommonModule, TiLinkbuttonModule, RouterModule.forChild(LinkbuttonTestModule.ROUTES)], + declarations: [LinkbuttonBasicComponent] +}) +export class LinkbuttonTestModule { + public static readonly ROUTES: Routes = [{ path: 'linkbutton/linkbutton-basic', component: LinkbuttonBasicComponent }]; +} diff --git a/src/linkbutton/demo/src/app/linkbutton/linkbutton-basic.html b/src/linkbutton/demo/src/app/linkbutton/linkbutton-basic.html new file mode 100644 index 0000000..27d0e24 --- /dev/null +++ b/src/linkbutton/demo/src/app/linkbutton/linkbutton-basic.html @@ -0,0 +1 @@ +购买 diff --git a/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton-demos.js b/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton-demos.js new file mode 100644 index 0000000..357564a --- /dev/null +++ b/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton-demos.js @@ -0,0 +1,25 @@ +export default { + column: '2', + demos: [ + { + demoId: 'linkbutton-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

Linkbutton组件的最简用法。

', + 'en-US': '

Basic usage

' + }, + apis: ['TiLinkbuttonComponent.properties.iconName'] + } + ], + ignoreApis: [ + 'TiLinkbuttonComponent.properties.disabled', + 'TiLinkbuttonComponent.properties.tabindex', + 'TiLinkbuttonComponent.events.blur', + 'TiLinkbuttonComponent.events.focus', + 'TiLinkbuttonComponent.methods.blur', + 'TiLinkbuttonComponent.methods.focus' + ] +}; diff --git a/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.cn.md b/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.cn.md new file mode 100644 index 0000000..b918a81 --- /dev/null +++ b/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.cn.md @@ -0,0 +1,24 @@ +--- +title: Linkbutton 链接按钮 +--- +# Linkbutton 链接按钮 + +
+ +Linkbutton 链接按钮组件。   + +```typescript +import { TiLinkbuttonModule } from '@opentiny/ng'; +``` + +
+ +
+ +Linkbutton 链接按钮组件。   + +```typescript +import { TiLinkbuttonModule } from '@opentiny/ng'; +``` + +
diff --git a/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.en.md b/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
+ +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
+ +
+ +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
+ + diff --git a/src/linkbutton/demo/src/favicon.ico b/src/linkbutton/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/linkbutton/demo/src/index.html b/src/linkbutton/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/linkbutton/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/linkbutton/demo/src/main.ts b/src/linkbutton/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/linkbutton/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/linkbutton/demo/test.ts b/src/linkbutton/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/linkbutton/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/linkbutton/demo/tsconfig.app.json b/src/linkbutton/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/linkbutton/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/linkbutton/demo/tsconfig.spec.json b/src/linkbutton/demo/tsconfig.spec.json new file mode 100644 index 0000000..e12f108 --- /dev/null +++ b/src/linkbutton/demo/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": ["jasmine"] + }, + "files": ["./test.ts", "../../polyfills.ts"], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/linkbutton/lib/index.ts b/src/linkbutton/lib/index.ts new file mode 100644 index 0000000..6a5b61d --- /dev/null +++ b/src/linkbutton/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiLinkbuttonModule'; diff --git a/src/linkbutton/lib/ng-package.json b/src/linkbutton/lib/ng-package.json new file mode 100644 index 0000000..51cc62e --- /dev/null +++ b/src/linkbutton/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/linkbutton", + "lib": { + "entryFile": "index.ts" + } +} diff --git a/src/linkbutton/lib/package.json b/src/linkbutton/lib/package.json new file mode 100644 index 0000000..e119863 --- /dev/null +++ b/src/linkbutton/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-linkbutton", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/linkbutton/lib/project.json b/src/linkbutton/lib/project.json new file mode 100644 index 0000000..0cf7638 --- /dev/null +++ b/src/linkbutton/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/linkbutton/lib", + "sourceRoot": "src/linkbutton/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/linkbutton"], + "options": { + "project": "src/linkbutton/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/linkbutton"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js linkbutton" + }, + { + "command": "ng default-build linkbutton" + }, + { + "command": "node build/clear-default-theme.js linkbutton" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/linkbutton && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build linkbutton && ng pack linkbutton && node build/publish.js linkbutton --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/linkbutton/lib/src/TiLinkbuttonComponent.ts b/src/linkbutton/lib/src/TiLinkbuttonComponent.ts new file mode 100644 index 0000000..87760a8 --- /dev/null +++ b/src/linkbutton/lib/src/TiLinkbuttonComponent.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; + +/** + * 链接按钮组件 linkbutton + */ +@Component({ + selector: '[tiLinkbutton]', + templateUrl: './linkbutton.html', + styleUrls: ['./linkbutton.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLinkbuttonComponent extends TiFormComponent { + /** + * icon名称 + */ + @Input() iconName: string; +} diff --git a/src/linkbutton/lib/src/TiLinkbuttonModule.ts b/src/linkbutton/lib/src/TiLinkbuttonModule.ts new file mode 100644 index 0000000..01eadc0 --- /dev/null +++ b/src/linkbutton/lib/src/TiLinkbuttonModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiLinkbuttonComponent } from './TiLinkbuttonComponent'; +import { TiIconModule } from '@opentiny/ng-icon'; + +@NgModule({ + imports: [CommonModule, TiIconModule], + exports: [TiLinkbuttonComponent], + declarations: [TiLinkbuttonComponent] +}) +export class TiLinkbuttonModule {} +export { TiLinkbuttonComponent } from './TiLinkbuttonComponent'; diff --git a/src/linkbutton/lib/src/linkbutton.html b/src/linkbutton/lib/src/linkbutton.html new file mode 100644 index 0000000..01ea778 --- /dev/null +++ b/src/linkbutton/lib/src/linkbutton.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/linkbutton/lib/src/linkbutton.less b/src/linkbutton/lib/src/linkbutton.less new file mode 100644 index 0000000..785add2 --- /dev/null +++ b/src/linkbutton/lib/src/linkbutton.less @@ -0,0 +1,33 @@ +@import '../../../themes/basic/base-all.less'; + +::ng-deep :root { + --ti-link-btn-height: 24px; +} + +:host { + display: inline-block; + text-decoration: none; + height: var(--ti-link-btn-height); + line-height: calc(var(--ti-link-btn-height) - 2px); // border上下2px + font-size: var(--ti-common-font-size-base); + color: var(--ti-common-color-text-secondary); + .box-sizing(border-box); + border: 1px solid var(--ti-common-color-line-normal); + padding: 0 var(--ti-common-space-10); + + &:link, + &:active { + text-decoration: none; + color: var(--ti-common-color-text-secondary); + } + &:hover { + cursor: pointer; + text-decoration: none; + border-color: var(--ti-common-color-line-active); + color: var(--ti-common-color-icon-hover); + } +} + +.ti-link-btn-icon { + margin-right: var(--ti-common-space-base); +} diff --git a/src/list/demo/project.json b/src/list/demo/project.json new file mode 100644 index 0000000..a50e9e1 --- /dev/null +++ b/src/list/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/list/demo", + "sourceRoot": "src/list/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/list", + "index": "src/list/demo/src/index.html", + "main": "src/list/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/list/demo/tsconfig.app.json", + "assets": ["src/list/demo/src/favicon.ico", "src/list/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "list-demo:build:production" + }, + "development": { + "browserTarget": "list-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js list" + } + ] + } + } + } +} diff --git a/src/list/demo/src/app/AppComponent.ts b/src/list/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/list/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/list/demo/src/app/AppModule.ts b/src/list/demo/src/app/AppModule.ts new file mode 100644 index 0000000..bd9e4aa --- /dev/null +++ b/src/list/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ListTestModule } from './list/ListTestModule'; + +@NgModule({ + imports: [ + ListTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/list/demo/src/app/IndexComponent.ts b/src/list/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..4b9f3a4 --- /dev/null +++ b/src/list/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ListTestModule } from './list/ListTestModule'; + +@Component({ + template: ` + + + + + + + +
{{ routes[0]?.path.split('/')[0] }} demo
+ + {{ path2name(route.path) }} + +
` +}) +export class IndexComponent { + routes: any = ListTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/list/demo/src/app/app.html b/src/list/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/list/demo/src/app/app.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/list/demo/src/app/list/ListDefaultComponent.ts b/src/list/demo/src/app/list/ListDefaultComponent.ts new file mode 100644 index 0000000..fc52837 --- /dev/null +++ b/src/list/demo/src/app/list/ListDefaultComponent.ts @@ -0,0 +1,32 @@ +import { Component, HostBinding } from '@angular/core'; + +@Component({ + templateUrl: './list-default.html' +}) +export class ListDefaultComponent { + options: Array = []; + selected: any; + options1: Array = []; + selected1: any; + constructor() { + for (let i: number = 0; i < 20; i++) { + const option: any = { + id: String(i), + label: `很长很长很长很长很长很长很长很长很长很长很长很长myemail${i}@example.com很长很长`, + pic: `pic${i}.png'`, + disabled: i % 2 === 0 + }; + this.options.push(option); + } + this.selected = this.options[0]; + for (let i: number = 0; i < 20000; i++) { + const option: any = { + id: String(i), + label: `myemail${i}@example.com很长很长`, + pic: `pic${i}.png'`, + disabled: i % 2 === 0 + }; + this.options1.push(option); + } + } +} diff --git a/src/list/demo/src/app/list/ListGroupComponent.ts b/src/list/demo/src/app/list/ListGroupComponent.ts new file mode 100644 index 0000000..855418a --- /dev/null +++ b/src/list/demo/src/app/list/ListGroupComponent.ts @@ -0,0 +1,92 @@ +import { Component, HostBinding } from '@angular/core'; + +@Component({ + templateUrl: './list-group.html' +}) +export class ListGroupComponent { + options: Array = [ + { + id: 'group1', + label: 'Group1', + children: [ + { + id: '11', + label: '1111' + }, + { + id: '12', + label: '2222' + } + ] + }, + { + id: 'group2', + label: 'Group2', + children: [ + { + id: '21', + label: '3333ds' + }, + { + id: '22', + label: '4444' + } + ] + }, + { + id: 'group3', + label: 'Group3', + children: [ + { + id: '31', + label: '65476543' + }, + { + id: '32', + label: '231356' + }, + { + id: '33', + label: '23were 1356' + } + ] + }, + { + id: 'group4', + label: 'Group4', + children: [ + { + id: '41', + label: '刷卡了U的防护哈哈' + }, + { + id: '42', + label: '手动if玩儿' + }, + { + id: '43', + label: '2如价格的开发' + } + ] + }, + { + id: 'group5', + label: 'Group5', + children: [ + { + id: '51', + label: '问我我李开复路口IE我让' + }, + { + id: '52', + label: '我欧文UR了我二' + }, + { + id: '53', + label: '未入库虑多敷第三方了' + } + ] + } + ]; + selected: any = this.options[0].children[0]; +} diff --git a/src/list/demo/src/app/list/ListMultiComponent.ts b/src/list/demo/src/app/list/ListMultiComponent.ts new file mode 100644 index 0000000..8650b3a --- /dev/null +++ b/src/list/demo/src/app/list/ListMultiComponent.ts @@ -0,0 +1,32 @@ +import { Component, HostBinding } from '@angular/core'; + +@Component({ + templateUrl: './list-multi.html' +}) +export class ListMultiComponent { + options: Array = []; + options1: Array = []; + selected: any; + selected1: any; + constructor() { + for (let i: number = 0; i < 20; i++) { + const option: any = { + id: String(i), + label: `很长很长很长很长很长很长很长很长很长很长很长很长myemail${i}@example.com很长很长`, + pic: `pic${i}.png'`, + disabled: i % 2 === 0 + }; + this.options.push(option); + } + for (let i: number = 0; i < 20000; i++) { + const option: any = { + id: String(i), + label: `myemail${i}@example.com很长很长`, + pic: `pic${i}.png'`, + disabled: i % 6 === 0 + }; + this.options1.push(option); + } + this.selected = [this.options[0]]; + } +} diff --git a/src/list/demo/src/app/list/ListSelectallComponent.ts b/src/list/demo/src/app/list/ListSelectallComponent.ts new file mode 100644 index 0000000..e2233b1 --- /dev/null +++ b/src/list/demo/src/app/list/ListSelectallComponent.ts @@ -0,0 +1,181 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './list-selectall.html' +}) +export class ListSelectallComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + myOptions1: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil', + disabled: true + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: true + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey', + disabled: true + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + myOptions2: Array = [ + { + label: '北美洲', + children: [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + } + ] + }, + { + label: '南美洲', + children: [ + { + label: '巴西', + englishname: 'Brazil', + disabled: false + } + ] + }, + { + label: '亚洲', + children: [ + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + } + ] + }, + { + label: '欧洲', + children: [ + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ] + } + ]; + myOptions3: Array = []; + + mySelecteds1: any = [this.myOptions[3]]; + mySelecteds2: any = [this.myOptions1[1], this.myOptions1[7]]; + mySelecteds3: any = [this.myOptions2[0].children[0], this.myOptions2[3].children[3]]; + mySelecteds4: any; + ngOnInit(): void { + for (let i: number = 0; i < 1000; i++) { + const option: any = { + id: String(i), + label: 'label_' + i, + pic: `pic${i}.png` + }; + this.myOptions3.push(option); + } + } +} diff --git a/src/list/demo/src/app/list/ListTestModule.ts b/src/list/demo/src/app/list/ListTestModule.ts new file mode 100644 index 0000000..fd7c7ad --- /dev/null +++ b/src/list/demo/src/app/list/ListTestModule.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { TiListModule } from '@opentiny/ng'; + +import { ListGroupComponent } from './ListGroupComponent'; +import { ListMultiComponent } from './ListMultiComponent'; +import { ListTipComponent } from './ListTipComponent'; +import { ListSelectallComponent } from './ListSelectallComponent'; +import { ListDefaultComponent } from './ListDefaultComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiListModule, RouterModule.forChild(ListTestModule.ROUTES)], + declarations: [ListDefaultComponent, ListGroupComponent, ListMultiComponent, ListTipComponent, ListSelectallComponent] +}) +export class ListTestModule { + static readonly LINKS: Array = [{ href: 'components/TiListComponent.html', label: 'List' }]; + static readonly ROUTES: Routes = [ + { path: 'list', component: ListDefaultComponent }, + { path: 'list/group', component: ListGroupComponent }, + { path: 'list/multi', component: ListMultiComponent }, + { path: 'list/tip', component: ListTipComponent }, + { path: 'list/selectall', component: ListSelectallComponent } + ]; +} diff --git a/src/list/demo/src/app/list/ListTipComponent.ts b/src/list/demo/src/app/list/ListTipComponent.ts new file mode 100644 index 0000000..1d47925 --- /dev/null +++ b/src/list/demo/src/app/list/ListTipComponent.ts @@ -0,0 +1,21 @@ +import { Component, HostBinding } from '@angular/core'; + +@Component({ + templateUrl: './list-tip.html' +}) +export class ListTipComponent { + options: Array = []; + selected: any; + constructor() { + for (let i: number = 0; i < 20; i++) { + const option: any = { + id: String(i), + label: `很长很长很长很长很长很长很长很长很长很长很长很长myemail${i}@example.com很长很长`, + pic: `pic${i}.png'`, + disabled: i % 2 === 0 + }; + this.options.push(option); + } + this.selected = this.options[0]; + } +} diff --git a/src/list/demo/src/app/list/list-default.html b/src/list/demo/src/app/list/list-default.html new file mode 100644 index 0000000..d65258a --- /dev/null +++ b/src/list/demo/src/app/list/list-default.html @@ -0,0 +1,46 @@ + +

Select组件数据双向绑定展示区域

+
+
+ 1.用例1的当前选中项为:
{{selected | json}}
+

+ 2.用例2的当前选中项为:
{{selected | json}}
+
+
+

+ + + +
+
+ + +
+
+ +
+ + + {{i}}+{{item.pic}}+{{item.label}} + + +
+ +
+ +
+ + +
+ +
+


+ + +


原生Select参考: + diff --git a/src/list/demo/src/app/list/list-group.html b/src/list/demo/src/app/list/list-group.html new file mode 100644 index 0000000..e2ea6c5 --- /dev/null +++ b/src/list/demo/src/app/list/list-group.html @@ -0,0 +1,35 @@ + +

Select组件 Group数据

+

Select组件数据双向绑定展示区域

+
+
+ 1.用例1的当前选中项为:
{{selected | json}}
+

+ 2.用例2的当前选中项为:
{{selected | json}}
+
+
+

+ + + +
+ +
+ +
+ + + {{i}}+{{item.pic}}+{{item.label}} + + +
+


+ + +


原生Select参考: + diff --git a/src/list/demo/src/app/list/list-multi.html b/src/list/demo/src/app/list/list-multi.html new file mode 100644 index 0000000..0451122 --- /dev/null +++ b/src/list/demo/src/app/list/list-multi.html @@ -0,0 +1,64 @@ + +

Select组件数据双向绑定展示区域

+
+
+ 1.用例1的当前选中项为:
{{selected | json}}
+

+ 2.用例2的当前选中项为:
{{selected | json}}
+
+
+

+ + + +
+
+ + +
+
+ +
+ + + {{i}}+{{item.pic}}+{{item.label}} + + +
+ +
+ + +
+ + +
+ + +
+


+ + +


原生Select参考: + diff --git a/src/list/demo/src/app/list/list-selectall.html b/src/list/demo/src/app/list/list-selectall.html new file mode 100644 index 0000000..f72aa7d --- /dev/null +++ b/src/list/demo/src/app/list/list-selectall.html @@ -0,0 +1,40 @@ +

描述

+

List组件默认没有全选功能,当多选时设置 selectAll 接口为 true 时开启全选功能。

+

示例

+ +

1.基本:

+
+ +
+

选中项:{{mySelecteds1 | json}}

+
+ +

2.选项中有禁用的:

+
+ +
+

选中项:{{mySelecteds2 | json}}

+
+ +

3.分组:

+
+ +
+

选中项:{{mySelecteds3 | json}}

+
+ +

4.虚拟滚动:

+
+ + +
+

选中项:{{mySelecteds4 | json}}

+
diff --git a/src/list/demo/src/app/list/list-tip.html b/src/list/demo/src/app/list/list-tip.html new file mode 100644 index 0000000..9ceb6a8 --- /dev/null +++ b/src/list/demo/src/app/list/list-tip.html @@ -0,0 +1,41 @@ + +

Select组件数据双向绑定展示区域

+
+
+ 1.用例1的当前选中项为:
{{selected | json}}
+

+ 2.用例2的当前选中项为:
{{selected | json}}
+
+
+

+ + + +
+
+ + +
+
+ +
+ + + {{i}}+{{item.pic}}+{{item.label}} + + +
+ +
+ +
+


+ + +


原生Select参考: + diff --git a/src/list/demo/src/favicon.ico b/src/list/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/list/demo/src/index.html b/src/list/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/list/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/list/demo/src/main.ts b/src/list/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/list/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/list/demo/tsconfig.app.json b/src/list/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/list/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/list/lib/index.ts b/src/list/lib/index.ts new file mode 100644 index 0000000..12d5fbf --- /dev/null +++ b/src/list/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiListModule'; diff --git a/src/list/lib/ng-package.json b/src/list/lib/ng-package.json new file mode 100644 index 0000000..f48833c --- /dev/null +++ b/src/list/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/list", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/list/lib/package.json b/src/list/lib/package.json new file mode 100644 index 0000000..f59bd12 --- /dev/null +++ b/src/list/lib/package.json @@ -0,0 +1,16 @@ +{ + "name": "@opentiny/ng-list", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/cdk": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-loading": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/list/lib/project.json b/src/list/lib/project.json new file mode 100644 index 0000000..aec2ef0 --- /dev/null +++ b/src/list/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/list/lib", + "sourceRoot": "src/list/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/list"], + "options": { + "project": "src/list/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/list"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js list" + }, + { + "command": "ng default-build list" + }, + { + "command": "node build/clear-default-theme.js list" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/list && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build list && ng pack list && node build/publish.js list --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/list/lib/src/TiListComponent.ts b/src/list/lib/src/TiListComponent.ts new file mode 100644 index 0000000..ba7865b --- /dev/null +++ b/src/list/lib/src/TiListComponent.ts @@ -0,0 +1,764 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ContentChild, + ElementRef, + EventEmitter, + HostListener, + Input, + NgZone, + Output, + Renderer2, + SimpleChange, + SimpleChanges, + TemplateRef, + ViewChild, + ChangeDetectorRef, + ChangeDetectionStrategy +} from '@angular/core'; +import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { Subscription } from 'rxjs'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiBrowser, TiKeymap, TiPositionType } from '@opentiny/ng-utils'; +import { TiLocale } from '@opentiny/ng-locale'; // 获取词条 +import packageInfo from '../package.json'; + +/** + * scrollToBottom 事件回调参数 + * + */ +export interface TiListScrollLoad { + /** + * 是否正在加载数据 + */ + loading?: boolean; + /** + * scroll 事件的 event 参数 + */ + event?: Event; +} + +/** + * @ignore + * 列表组件,使用者有menu、droplist + */ +@Component({ + selector: 'ti-list', + templateUrl: './list.html', + styleUrls: ['./list.less', './list-multi.less'], + providers: [TiFormComponent.getValueAccessor(TiListComponent)], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-virtual-scroll-list]': 'virtual' + } +}) +export class TiListComponent extends TiFormComponent { + public static readonly OPTION_DEFAULT_HEIGHT: number = 30; // 每项的默认高度, 单位是px + public static readonly CONTAINER_VERTICAL_PADDING: number = 8; // list容器(出滚动条的容器)的上下padding总和值(上下各4px) + protected static readonly SCROLL_TOP_TIME: number = 200; // 设置选中项滚动到TOP所需时间。Chorme下简单测试。TODO:其他浏览器、复杂数据 + + // 列表数据配置 + @Input() multiple: boolean = false; // 是否多选 + @Input() options: Array; // 下拉选项的全部数据 + @Input() labelKey: string = 'label'; // 要显示的字段 + @Input() tipPosition: TiPositionType = 'right'; + @Input() noDataText: string; // 无数据时的显示文本 + @Input() hideEmptyOption: boolean = false; // 过滤空字符串选项 + @Input() selectAll: boolean = false; // 多选模式下是否有全选功能 + /** + * 用于配置是否显示children字段数据,大部分场景需要显示children,cascader不需要 + */ + @Input() showChildren: boolean = true; + /** + * @ignore + * 适配内部time类组件,点选滚动效果,暂不开放。 + */ + @Input() scrollTop: number = 0; + /** + * 是否开启虚拟滚动, 默认值false + */ + @Input() virtual: boolean = false; + /** + * @ignore + * TODO: 暂不对外开放该接口,后续根据使用场景进行优化 + * 当开启虚拟滚动时,可配置单条选项的高度(单位是px), 默认值30 + */ + @Input() itemSize: number = TiListComponent.OPTION_DEFAULT_HEIGHT; + + @Input() tipMaxWidth: string; + /** + * @ignore + * + * + * idKey指定的属性的值相等时即认为 option 选项是选中的。选中项 ngModel 中的数据(modelOption对象)跟 options 数据集中的选项(option对象)之间对应相等关系的依据属性。当 + * modelOption中的 idKey 设置的属性的值 与 option 中的 idKey 设置的属性的值相等时,则认为 modelOption 和 option 是对应的相等关系,即认为 option 选项是选中的。 + * + * 默认当 modelOption === option 或者 modelOption 中的 labelKey 设置的属性的值 与 option 中的 labelKey 设置的属性的值相等时,则认为 option 选项是选中的。 + * + * 该接口暂不对外开放,后续如果业务场景labelKey对应的值确实有重复时,再对外开放该接口。 + */ + @Input() idKey: string; + + /** + * 选中事件,向外通知option数据 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + @Output() readonly hover: EventEmitter = new EventEmitter(); + /** + * 下拉列表滚动到底部的回调 + * + * + */ + @Output() readonly scrollToBottom: EventEmitter = new EventEmitter(); + /** + * 鼠标移入选项的回调,与hover不同,hover是hoverOption改变的回调 + */ + @Output() readonly optionMouseenter: EventEmitter = new EventEmitter(); + // item模板 + @ContentChild(TemplateRef, { static: true }) itemTemplate: TemplateRef; + @ViewChild('ulRef', { static: false }) ulRef: ElementRef; + @ViewChild(CdkVirtualScrollViewport, { static: false }) + virtualScrollViewport: CdkVirtualScrollViewport; + + /** + * 需要阻止失焦标记: 点击面板时,会触发失焦,需要阻止失焦。 + * IE兼容性问题: 点击滚动条,触发失焦,面板会收起 + * 修复方案:点击list面板区域(包含滚动条)后,给失焦元素强制获取焦点。修改Form基类不触发组件整体失焦。 + */ + private stopBlur: boolean = false; + private unlistenIEScrollbarBugFns: Array<() => void> = []; + public selectAllState: boolean; + public selectAllText: string = TiLocale.getLocaleWords().tiList.selectAll; + public optionSelectAll: string = 'all'; + private scrollEventSub: Subscription; + public scrollLoadInfo: TiListScrollLoad = { loading: false }; + protected versionInfo: string = super.getVersion(packageInfo); + + private _hoverOption: any; + public get hoverOption(): any { + return this._hoverOption; + } + public set hoverOption(hoverOption: any) { + if (this._hoverOption !== hoverOption) { + this._hoverOption = hoverOption; + // onpush策略 异步获取数据更改hoverOption,需要手动更新视图 + this.changeDetectorRef.markForCheck(); + this.hover.emit(hoverOption); + } + } + protected optionScrollTopLastTime: number = 0; + // 判断是否禁用的函数: 函数类型是(any) => boolean,赋值是(item: any) => {} + @Input() isDisabledFn: (item: any) => boolean = (item: any) => { + return item && item.disabled === true; + }; // 这个分号是书写正确的 + constructor(protected hostRef: ElementRef, protected renderer: Renderer2, protected cd: ChangeDetectorRef, protected zone: NgZone) { + super(hostRef, renderer, cd); + } + + // 在Selected使用List时,Selected的聚焦元素是Dominator和Searchbox,它们接收按键,并调用list来处理。 + // 在表格列设置时,List自己聚焦,List处理按键。 + @HostListener('keydown', ['$event']) onKeydown(event: KeyboardEvent): void { + // 10.0.2删除 KEY_SPACE 空格快捷键的响应 + switch (event.keyCode) { + case TiKeymap.KEY_ENTER: // ENTER键 相当于click + case TiKeymap.KEY_NUMPAD_ENTER: // ENTER键(苹果数字小键盘) + if (this.hoverOption) { + this.selectOption(this.hoverOption); + } + break; + case TiKeymap.KEY_ESCAPE: // Esc键,仅可关闭 + case TiKeymap.KEY_TAB: // Tab键,仅可关闭 + break; + case TiKeymap.KEY_ARROW_UP: // 向上箭头,上移选项 + event.preventDefault(); // 防止上下按键默认行为:移动滚动条 + this.nextOption(true); + break; + case TiKeymap.KEY_ARROW_DOWN: // 向下箭头,下移选项 + event.preventDefault(); // 防止上下按键默认行为:移动滚动条 + this.nextOption(false); + break; + default: + break; + } + } + + ngOnInit(): void { + this.zone.runOutsideAngular(() => { + this.renderer.listen(this.nativeElement, 'mousedown', (event: MouseEvent): void => { + // Select时,点击滚动条和点击面板,不要触发默认行为:dominator blur + // 现在,list默认不聚焦。在所有场合,点击面板或者滚动条,都不触发默认焦点事件。 + event.preventDefault(); // 防止dominator blur行为 + // list自身可以落焦点时,需要聚焦的默认行为。 + }); + + // list内部滚动条会引起外部滚动条事件触发,引起弹框内的下拉组件无法使用鼠标拖动滚动条,因此此处阻止事件冒泡 + this.renderer.listen(this.nativeElement, 'mousewheel', (event: MouseEvent): void => { + event.stopPropagation(); + }); + this.renderer.listen(this.nativeElement, 'DOMMouseScroll', (event: MouseEvent): void => { + event.stopPropagation(); + }); + }); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + if (this.multiple && this.selectAll) { + const optionsChange: SimpleChange = changes['options']; + if (optionsChange) { + this.setSelectedAllState(); + } + } + // 全选时,selectAll改变且当前值为true,更新全选框状态 + const selectAllChange: SimpleChange = changes['selectAll']; + if (this.multiple && selectAllChange && selectAllChange.currentValue) { + this.setSelectedAllState(); + } + // time及datetime/datetimeRange组件使用list,点选时候需要滚动效果 + if (changes['scrollTop'] && !changes['scrollTop'].firstChange) { + this.setScrollFn(); + } + if (changes['options']) { + this.setHoverOption(); + } + } + + ngAfterViewInit(): void { + if (this.scrollTop > 0) { + this.setScrollFn(); + } + if (this.scrollToBottom.observers.length > 0 && this.virtualScrollViewport) { + this.scrollEventSub = this.virtualScrollViewport.elementScrolled().subscribe((event: Event): void => { + if ((event.target as any).scrollTop + (event.target as any).clientHeight >= (event.target as any).scrollHeight) { + this.scrollLoadInfo.event = event; + this.zone.run(() => { + this.scrollToBottom.emit(this.scrollLoadInfo); + }); + } + }); + } + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + // 解除IE滚动条Bug的监听 + this.unlistenIESrollbarBug(); + if (this.scrollEventSub) { + this.scrollEventSub.unsubscribe(); + } + } + + writeValue(model: any): void { + super.writeValue(model); + if (this.multiple && this.selectAll) { + this.setSelectedAllState(); + } + } + // time及datetime/datetimeRange组件使用list,点选时候需要滚动效果 + private setScrollFn(): void { + if (TiBrowser.isIE()) { + this.renderer.setProperty(this.hostRef.nativeElement, 'scrollTop', this.scrollTop); + return; + } + this.hostRef.nativeElement.scrollTo(0, this.scrollTop); + } + + // 下拉选项的鼠标按下操作 + public onMousedownItem(event: MouseEvent, own: HTMLElement): void { + event.preventDefault(); + event.stopPropagation(); + own.classList.add('ti3-menu-panel-list-active'); + } + + // 下拉选项的鼠标按下后的抬起操作 + public onMouseupItem(event: MouseEvent, own: HTMLElement): void { + this.removeClass(event, own); + } + + // 下拉选项的鼠标离开时的操作 + public onMouseleaveItem(event: MouseEvent, own: HTMLElement): void { + this.removeClass(event, own); + } + // 移除li元素上的ti3-menu-panel-list-active类 + public removeClass(event: MouseEvent, own: HTMLElement): void { + event.preventDefault(); + event.stopPropagation(); + own.classList.remove('ti3-menu-panel-list-active'); + } + + // 下拉选项的单击操作(包括组名所在的
  • 元素) + public onClickItem(event: MouseEvent, option: any): void { + // Selected时,点击选项,不要触发默认行为:dominator blur。 + event.preventDefault(); // 防止dominator blur行为 + // Group节点和禁用节点,无需响应。 + if (this.isGroup(option) || this.isDisabledFn(option)) { + event.stopPropagation(); + + return; + } + // 只处理左键和中键 + if (event.button !== TiKeymap.MOUSE_LEFT_BUTTON && event.button !== TiKeymap.MOUSE_MIDDLE_BUTTON) { + // Tiny2这里逻辑错误TODO: + return; + } + + // 点击选中项 + this.selectOption(option); + // 更新hover + this.hoverOption = option; + } + + public getVirtualScrollViewportHeight(): string { + let height: number = TiListComponent.CONTAINER_VERTICAL_PADDING; // 这里的8是上下各4px的padding + if (!this.options || this.options.length === 0) { + return height + 'px'; + } + height += this.options.length * this.itemSize; + if (this.selectAll && this.multiple) { + height += TiListComponent.OPTION_DEFAULT_HEIGHT; // 全选项的高度 + } + + return height + 'px'; + } + + private selectOption(option: any): void { + // 全选 + if (this.multiple && this.selectAll && option === this.optionSelectAll) { + this.selectAllOptions(); + this.select.emit(option); + + return; + } + + let index: number = -1; + if (this.multiple) { + // 多选 + if (!this.model) { + // 如果为空值 + this.model = []; + } + // 是否选中,取反 + index = this.getIndex(option, this.model); + if (index === -1) { + this.model.push(option); // 点击下拉项选中,选中项按用户操作顺序显示 + } else if (index !== -1) { + this.model.splice(index, 1); + } + // 强行向外通知model改变。 + this.model = this.model.concat(); + // 设置全选状态 + if (this.selectAll) { + this.setSelectedAllState(); + } + } else { + // 单选 + this.model = option; + } + this.select.emit(option); + } + + private selectAllOptions(): void { + if (!this.model) { + // 如果为空值 + this.model = []; + } + const listOptions: Array = this.getListOptions(); + let isChange: boolean = false; + if (this.selectAllState === false || this.selectAllState === null) { + listOptions.forEach((item: any) => { + if (this.getIndex(item, this.model) === -1 && this.isSelectable(item)) { + // 不在当前选中项且为可选数据项(排除禁用项和组名项) + // 点击全选按钮时的选中值顺序与option的顺序一致 + const insertIndex: number = this.getInsertIndex(item); + this.model.splice(insertIndex, 0, item); + isChange = true; + } + }); + + this.selectAllState = true; + } else { + listOptions.forEach((item: any) => { + const index: number = this.getIndex(item, this.model); + if (index !== -1 && !this.isDisabledFn(item)) { + this.model.splice(index, 1); + isChange = true; + } + }); + this.selectAllState = false; + } + if (isChange) { + // model的值在以上循环中,多次变化,或者不变,根据isChange,一次性触发变化检测 + this.model = this.model.concat(); + } + } + + public setSelectedAllState(): void { + if (!this.model || this.model.length === 0 || !this.options || this.options.length === 0) { + this.selectAllState = false; + + return; + } + const listOptions: Array = this.getListOptions(); + const selectedOptions: Array = listOptions.filter((item: any) => { + return this.isSelectable(item) && this.isSelected(item); + }); + if (selectedOptions.length === 0) { + this.selectAllState = false; + + return; + } + const selectableOptions: Array = listOptions.filter((item: any) => { + return this.isSelectable(item); + }); + this.selectAllState = selectedOptions.length === selectableOptions.length ? true : null; + } + + // 获取新插入元素的插入次序,该次序与list集合中的元素顺序保持一致 + private getInsertIndex(option: any): number { + const listOptions: Array = this.getListOptions(); + const indexOfList: number = listOptions.indexOf(option); // 当前元素在list中的排序 + // 将该元素与已选中在list中的次序进行比较,如果该元素在某选中项次序之后,则将其插入该元素之后 + for (let i: number = this.model.length; i > 0; i--) { + const itemIndexOfList: number = this.getIndex(this.model[i - 1], listOptions); + if (itemIndexOfList < indexOfList) { + return i; + } + } + + return 0; + } + public onMouseenterItem(event: MouseEvent, option: any): void { + // 是可选项,且不是在选中项置Top时鼠标经过。 + if (this.isSelectable(option) && new Date().getTime() - this.optionScrollTopLastTime > TiListComponent.SCROLL_TOP_TIME) { + this.hoverOption = option; // 更新hover + this.optionMouseenter.emit(option); + } + } + // 寻找下一个可选项。参数:向上/向下 + public nextOption(isUp: boolean): void { + const listOptions: Array = this.getListOptions(); + if (this.multiple && this.selectAll) { + listOptions.unshift(this.optionSelectAll); + } + if (listOptions.filter((x: any) => this.isSelectable(x)).length === 0) { + // 没有可选中的项目 + return; + } + let nextOption: any = this.hoverOption; + let curIndex: number = listOptions.indexOf(nextOption); + do { + if (isUp) { + if (curIndex > 0) { + // 找到了,且不是第一个元素 + curIndex = curIndex - 1; + } else { + // -1没有找到, 0第一个元素 + curIndex = listOptions.length - 1; // 跳到第一个元素。 + } + } else { + if (curIndex < listOptions.length - 1) { + // -1没有找到, 和不是最后一个 + curIndex = curIndex + 1; + } else { + // length - 1 最后一个元素 + curIndex = 0; // 跳到第一个元素。 + } + } + nextOption = listOptions[curIndex]; + } while (!(this.isSelectable(nextOption) || nextOption === this.hoverOption)); // 找到停止,或者寻找一圈停止 + // 没有找到结果 + if (nextOption === null || nextOption === this.hoverOption) { + return; + } + // 改变hover选项。 + this.hoverOption = nextOption; + this.cd.markForCheck(); + // 更新滚动条 + this.optionScollTop(this.hoverOption); + } + protected getListOptions(): Array { + let listOptions: Array = []; // 装有所有选项的数组 + if (this.options) { + this.options.forEach((x: any) => { + listOptions.push(x); + if (this.isGroup(x)) { + listOptions = listOptions.concat(x.children); + } + }); + } + + return listOptions; + } + // 滚动到选中项元素,多选定位到第一个选中项元素,无选中项时定义到第一项元素; + // 适用于每次下拉打开的场景 + public scrollToSelected(): void { + // 获取当前选中项 + let selectedItem: any; + if (this.multiple) { + selectedItem = this.model && this.model.length > 0 ? this.model[0] : null; + } else { + selectedItem = this.model; + } + if (selectedItem) { + // 有选中项时,自动定位到第一个选中元素 + this.scrollToEle(selectedItem, true); + } else { + // 无选中项时,定位在第一项元素 + if (this.virtual) { + this.virtualScrollViewport.elementRef.nativeElement.scrollTop = 0; + } else { + this.nativeElement.scrollTop = 0; + } + } + this.setHoverOption(); + } + /** + * 设置hoverOption + * 用于数据更新时,非鼠标或快捷键操作时 + */ + private setHoverOption(): void { + if (this.options?.length > 0) { + if (this.hasSelectedinList()) { + this.hoverOption = this.getHoverOptionInModel(); + } else { + if (this.multiple && this.selectAll) { + this.hoverOption = this.optionSelectAll; + } else { + if (this.options[0]?.children) { + this.hoverOption = this.getGroupHoverOption(); + } else { + this.hoverOption = this.options.find((option: any) => !this.isDisabledFn(option)); + } + } + } + } else { + this.hoverOption = undefined; + } + } + + /** + * 单选有model的时候,单选的model不会是禁用项,hoverOption应当是model, + * 多选有model的时候,hoverOption是第一个非禁用的model,如果model全部禁用,hoverOption是第一个禁用项往下的第一个可用项。 + */ + private getHoverOptionInModel(): any { + return this.multiple + ? this.options[ + this.getIndex( + this.model.find((item: any) => !this.isDisabledFn(item)), + this.options + ) + ] || + this.options.find((item: any, index: number) => { + return index > this.getIndex(this.model[0], this.options) && !this.isDisabledFn(item); + }) + : this.options[this.getIndex(this.model, this.options)] || this.model; + } + /** + * 判断当前list中是否有选中项,过滤后的数据有可能不含有选中项 + */ + private hasSelectedinList(): boolean { + const listOption: Array = this.getListOptions(); + if (this.multiple && this.model?.length > 0) { + return !!this.model.find((item: any) => this.getIndex(item, listOption) !== -1); + } else if (this.model) { + return this.getIndex(this.model, listOption) !== -1; + } else { + return false; + } + } + + // 分组情况下,找到hoverOption + private getGroupHoverOption(): any { + let groupHoverOption: any; + this.options.find((option: any): boolean => { + if (option.children.length > 0) { + groupHoverOption = option.children.find((child: any) => !this.isDisabledFn(child)); + } + + return groupHoverOption ? true : false; // 这个return只是为了结束find方法 + }); + + return groupHoverOption; // 这个return是为了返回查找结果 + } + private scrollToEle(option: any, isScrollBySelected: boolean = false): void { + if (!option || !this.ulRef) { + // 第一次model值改变时, ulRef不存在。 + return; + } + const listOptions: Array = this.getListOptions(); + if (this.multiple && this.selectAll) { + listOptions.unshift(this.optionSelectAll); + } + const curIndex: number = this.getIndex(option, listOptions); + if (curIndex < 2 && isScrollBySelected) { + if (this.virtual) { + this.virtualScrollViewport.elementRef.nativeElement.scrollTop = 0; + } else { + this.nativeElement.scrollTop = 0; + } + + return; // 前两项不需要滚动 + } + // 下拉框有滚动条时,若上下选项足够,选中项为下拉框的第二项,这样可以展示选中项的前后选项 + const scrollTopOptionIndex: number = Math.max(curIndex - 1, 0); + if (this.virtual) { + this.virtualScrollViewport.elementRef.nativeElement.scrollTop = this.itemSize * scrollTopOptionIndex; + } else { + const ele: HTMLElement = this.ulRef.nativeElement.getElementsByTagName('LI')[scrollTopOptionIndex]; + if (ele) { + this.nativeElement.scrollTop = ele.offsetTop; + } + } + } + private optionScollTop(option: any): void { + this.optionScrollTopLastTime = new Date().getTime(); + this.scrollToEle(option); + } + + // 是组数据项 + public isGroup(item: any): boolean { + return item && Object.prototype.hasOwnProperty.call(item, 'children') && this.showChildren; + } + // 是可选数据项 + protected isSelectable(item: any): boolean { + return !this.isGroup(item) && !this.isDisabledFn(item); + } + // 是已选中数据项 + public isSelected(item: any): boolean { + if (!this.model) { + return false; + } + + if (this.multiple) { + return this.getIndex(item, this.model) !== -1; + } + + return this.isEqual(this.model, item); + } + + // 选中项与options数据集中的选项的对应关系 + private isEqual(modelOption: any, option: any): boolean { + if (this.idKey) { + return modelOption[this.idKey] === option[this.idKey]; + } + + return ( + modelOption === option || + (modelOption[this.labelKey] !== undefined && + option[this.labelKey] !== undefined && + modelOption[this.labelKey] === option[this.labelKey]) + ); + } + + private getIndex(item: any, arr: Array): number { + if (!arr || !item) { + return -1; + } + for (let i: number = 0; i < arr.length; i++) { + if (this.isEqual(arr[i], item)) { + return i; + } + } + + return -1; + } + /** + * 修复点击IE滚动条,面板关闭。 + * 打开面板时监听,关闭和销毁面板时取消监听。 + */ + public listenIESrollbarBug(): void { + // IE兼容性涉及知识,见本文件底部。 + // IE兼容性问题: 点击滚动条,触发失焦,面板会收起 + // 修复方案:点击list面板区域(包含滚动条)后,给失焦元素强制获取焦点。修改Form基类不触发组件整体失焦。 + if (TiBrowser.isIE()) { + // 点击面板,阻止Blur + this.unlistenIEScrollbarBugFns.push( + this.renderer.listen(this.nativeElement, 'mousedown', (event: FocusEvent) => { + this.stopBlur = true; // 标记,以供focusin消耗 + // 阻止冒泡到document,因为document会把标志位改回false。和下面修复双击滚动条Bug配合 + event.stopPropagation(); + }) + ); + // 修复bug:双击滚动条,再点击面板之外,不失焦。 + this.unlistenIEScrollbarBugFns.push( + this.renderer.listen(document, 'mousedown', (event: FocusEvent) => { + // 点击面板之外区域,不阻止blur + this.stopBlur = false; + }) + ); + + // IE事件顺序:focusout->foucsin->blur->focus, 支持冒泡的focusin/focusout, 不支持冒泡的是focus/blur + this.unlistenIEScrollbarBugFns.push( + this.renderer.listen(document.body, 'focusin', (event: FocusEvent) => { + if (this.stopBlur && event.relatedTarget !== document.body) { + // 本来不需要!==document, 但连续多次点击太快,会发生:刚点击一下,上次的dominator->body focusin, 会进来 + + // 清零,因为下面的聚焦事件,又会触发focusin事件,并不想处理 + this.stopBlur = false; // 一般情况这里置为false是可以的。 + + // focusin事件,target是聚焦元素。relatedTarget是失焦元素。 + // 强制让失焦元素,重新获得焦点。 + if (event.relatedTarget) { + (event.relatedTarget as HTMLElement).focus(); + } + } + }) + ); + } + } + public unlistenIESrollbarBug(): void { + if (TiBrowser.isIE()) { + // 解绑监听 + for (const x of this.unlistenIEScrollbarBugFns) { + x(); + } + } + } +} +/* +IE兼容性问题:点击面板滚动条,触发失焦,无法阻止。 +解决方案:给失焦元素强制获取焦点。修改Form基类不触发组件整体失焦。 + +涉及知识点: +点击div面板的滚动条,浏览器会失焦,焦点到body。event.preventDefault();,可以阻止默认失焦行为。 +点击div面板的滚动条,浏览器会失焦,焦点到body。无法阻止。(问题产生) + +Chrome浏览器事件顺序: +blur->focusout->focus->foucsin +IE浏览器事件顺序: +focusout->focusin->blur->focus + +IE浏览器,在blur事件里,失焦元素.focus(), 会先显示失焦css,后显示聚焦css,会闪一下。 +IE浏览器,在focusout事件里,失焦元素.focus(), 无效。 +IE浏览器,在focusin事件里,失焦元素.focus(), 会一直显示聚焦css,不会闪。(作为解决问题的方案) + +Chrome blur事件时,document.activeElement是body +IE blur事件时,document.activeElement是将要聚焦的元素。 +IE 如果在focusin里,失焦元素.focus(),那么blur时document.activeElement刚好是这个失焦并强制聚焦的元素。 + +第一版方案: +强制聚焦,用了延时处理 +整体onBlur时,IE特殊处理,不闭合面板 +闭合面板时,处理标志位。 +解决双击滚动条bug,利用监听面板之外点击。 + +第二版方案: +强制聚焦,不用延时。 +修复Form基类,不再触发整体失焦/聚焦。 +闭合面板时,处理标志位 (TODO: 设法去除。尽量集中对标志位的管理) +解决双击滚动条bug,利用监听面板之外点击。(TODO: 设法去除。尽量集中对标志位的管理) + +第三版方案: +面板打开时监听mouse,面板闭合时不再监听 + + */ diff --git a/src/list/lib/src/TiListModule.ts b/src/list/lib/src/TiListModule.ts new file mode 100644 index 0000000..97e883c --- /dev/null +++ b/src/list/lib/src/TiListModule.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ScrollingModule } from '@angular/cdk/scrolling'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiListComponent } from './TiListComponent'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiLoadingModule } from '@opentiny/ng-loading'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule, ScrollingModule, TiIconModule, TiTipModule, TiLoadingModule], + exports: [TiListComponent], + declarations: [TiListComponent] +}) +export class TiListModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiListComponent, TiListScrollLoad } from './TiListComponent'; diff --git a/src/list/lib/src/i18n/TiListWords.ts b/src/list/lib/src/i18n/TiListWords.ts new file mode 100644 index 0000000..a4ca7d1 --- /dev/null +++ b/src/list/lib/src/i18n/TiListWords.ts @@ -0,0 +1,6 @@ +export interface TiListWords { + tiList: { + noDataText: string; + selectAll: string; + }; +} diff --git a/src/list/lib/src/i18n/en_US.ts b/src/list/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..178e723 --- /dev/null +++ b/src/list/lib/src/i18n/en_US.ts @@ -0,0 +1,8 @@ +import { TiListWords } from './TiListWords'; + +export const en_US: TiListWords = { + tiList: { + noDataText: 'No data available', + selectAll: '(Select all)' + } +}; diff --git a/src/list/lib/src/i18n/es_US.ts b/src/list/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..e74ede7 --- /dev/null +++ b/src/list/lib/src/i18n/es_US.ts @@ -0,0 +1,8 @@ +import { TiListWords } from './TiListWords'; + +export const es_US: TiListWords = { + tiList: { + noDataText: 'No hay datos disponibles', + selectAll: '(Seleccionar todo)' + } +}; diff --git a/src/list/lib/src/i18n/fr_FR.ts b/src/list/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..0dddc0a --- /dev/null +++ b/src/list/lib/src/i18n/fr_FR.ts @@ -0,0 +1,8 @@ +import { TiListWords } from './TiListWords'; + +export const fr_FR: TiListWords = { + tiList: { + noDataText: 'Aucune donnée disponible', + selectAll: '(Sélectionner tout)' + } +}; diff --git a/src/list/lib/src/i18n/index.ts b/src/list/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/list/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/list/lib/src/i18n/pt_BR.ts b/src/list/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..d84556a --- /dev/null +++ b/src/list/lib/src/i18n/pt_BR.ts @@ -0,0 +1,8 @@ +import { TiListWords } from './TiListWords'; + +export const pt_BR: TiListWords = { + tiList: { + noDataText: 'Nenhum dado disponível', + selectAll: '(Selecionar tudo)' + } +}; diff --git a/src/list/lib/src/i18n/zh_CN.ts b/src/list/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..cd76235 --- /dev/null +++ b/src/list/lib/src/i18n/zh_CN.ts @@ -0,0 +1,8 @@ +import { TiListWords } from './TiListWords'; + +export const zh_CN: TiListWords = { + tiList: { + noDataText: '暂无数据', + selectAll: '(全选)' + } +}; diff --git a/src/list/lib/src/list-multi.less b/src/list/lib/src/list-multi.less new file mode 100644 index 0000000..93ba7da --- /dev/null +++ b/src/list/lib/src/list-multi.less @@ -0,0 +1,53 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-list-multi-checkbox-container-size: var(--ti-common-size-4x); + --ti-list-multi-checkbox-container-line-height: calc( + var(--ti-list-multi-checkbox-container-size) - var(--ti-common-border-weight-normal) * 2 + ); + --ti-list-multi-checkbox-bg-color: var(--ti-common-color-bg-white-normal); + --ti-list-multi-checkbox-checked-bg-color: var(--ti-common-color-bg-emphasize); + --ti-list-multi-checkbox-icon-color: var(--ti-common-color-icon-white); +} +// 以下是多选使用的样式:复选框基础样式 +.ti3-multi-select-checkbox { + width: var(--ti-list-multi-checkbox-container-size); + height: var(--ti-list-multi-checkbox-container-size); + background-color: var(--ti-list-multi-checkbox-bg-color); + display: inline-block; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + .box-sizing; + color: var(--ti-list-multi-checkbox-icon-color); + line-height: var(--ti-list-multi-checkbox-container-line-height); + text-align: center; + margin-right: var(--ti-common-space-10); + cursor: pointer; +} + +// 正常选中 +.ti3-dropdown-option .ti3-multi-select-checkbox-selected { + border-color: var(--ti-common-color-line-active); + background: var(--ti-list-multi-checkbox-checked-bg-color); +} + +// 半选 +.ti3-dropdown-option .ti3-multi-select-checkbox-indeterminate { + border-color: var(--ti-common-color-line-active); + border-width: 5px; + vertical-align: sub; + &::before { + content: ''; + } +} + +// 灰化 / 灰化选中 +.ti3-dropdown-option-disabled .ti3-multi-select-checkbox { + border-color: var(--ti-common-color-line-disabled); + background: var(--ti-common-color-bg-disabled); + color: var(--ti-common-color-bg-disabled); + cursor: not-allowed; + &.ti3-multi-select-checkbox-selected { + color: var(--ti-common-color-icon-disabled); + } +} diff --git a/src/list/lib/src/list.html b/src/list/lib/src/list.html new file mode 100644 index 0000000..e4a28a4 --- /dev/null +++ b/src/list/lib/src/list.html @@ -0,0 +1,108 @@ + + + +
      + + + + + + + + + + +
    +
    +
    +
    + +
      + + + + + + + + + + + + +
    +
    + + + +
    +
    + + +
  • +
    + + + + + {{item[labelKey]}} +
    +
  • + + + +
  • +
    + + {{selectAllText}} +
    +
  • +
    diff --git a/src/list/lib/src/list.less b/src/list/lib/src/list.less new file mode 100644 index 0000000..65b3fc9 --- /dev/null +++ b/src/list/lib/src/list.less @@ -0,0 +1,149 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-list-container-vertical-padding: var(--ti-common-space-base); // list 滚动容器的上下内边距 + --ti-list-option-container-height: 30px; // 下拉项的行高 + --ti-list-fold-icon-width: var(--ti-common-size-2x); + --ti-list-fold-icon-height: calc(var(--ti-list-fold-icon-width) / 1.6); // 按照比例计算 8 / 5 = 1.6 + --ti-list-option-bg-color-active: var(--ti-common-color-bg-emphasize); + --ti-list-fold-icon-color-active: var(--ti-common-color-icon-white); + --ti-list-option-tworow-text-color: var(--ti-common-color-text-weaken); + --ti-list-option-group-title-color: var(--ti-common-color-text-weaken); +} + +:host { + display: inline-block; + + overflow-y: auto; + overflow-x: hidden; + position: relative; // 设置为非static之后,内部li定位滚动条时方便 + padding: var(--ti-list-container-vertical-padding) var(--ti-common-space-0); + box-sizing: border-box; + + &:focus { + //阻止聚焦时浏览器默认蓝框样式 + outline: none; + } +} + +.ti3-dropdown-group > .ti3-dropdown-option { + padding-left: var(--ti-common-space-3x); +} +.ti3-overflow-padding { + padding: var(--ti-common-space-0) var(--ti-common-space-10); + .ellipsis(); +} + +.ti3-dropdown-option { + list-style: none; + cursor: pointer; + color: inherit; + text-align: left; + line-height: var(--ti-common-line-height-number); + // 下拉框选项给一个默认的背景色(#fff,浏览器默认背景色),为了解决火狐浏览器下下拉选项的省略号是一条横线 issue#9436 + background-color: var(--ti-common-color-bg-white-normal); + padding: var(--ti-common-space-6) var(--ti-common-space-0); + /** + * tiTextweaken:对外暴露的属性,定义弱化信息的文本颜色 + * + * 此处用于select组件选项两行信息的场景下 + */ + ::ng-deep [tiTextweaken] { + color: var(--ti-list-option-tworow-text-color); + } +} +.ti3-dropdown-option:first-child { + margin-top: 0; +} + +.ti3-dropdown-group-list { + font-weight: bold; + color: var(--ti-list-option-group-title-color); + box-sizing: border-box; + height: var(--ti-list-option-container-height); + line-height: var(--ti-list-option-container-height); + text-align: left; +} +.ti3-dropdown-group-list-cascader { + position: relative; + padding-right: var(--ti-common-space-4x); + // 级联有下一级数据时,需要小三角箭头 + &::after { + content: ''; + position: absolute; + display: block; + top: 50%; + right: calc(var(--ti-common-space-3x) - 2px); // 视觉标注是12px,因为有旋转,需要减去2px + transform: translateY(-50%) rotate(90deg); + .triangle-up(var(--ti-list-fold-icon-width), var(--ti-list-fold-icon-height), var(--ti-common-color-icon-normal)); + } +} +.ti3-dropdown-option-selected.ti3-dropdown-group-list-cascader::after { + border-bottom-color: var(--ti-common-color-icon-white); +} +.ti3-dropdown-option-disabled.ti3-dropdown-group-list-cascader::after { + border-bottom-color: var(--ti-common-color-icon-disabled); +} + +.ti3-dropdown-option.ti3-dropdown-option-disabled, +.ti3-dropdown-option.ti3-dropdown-option-disabled:hover, +.ti3-dropdown-option.ti3-dropdown-option-disabled ::ng-deep [tiTextweaken], +.ti3-dropdown-option.ti3-dropdown-option-disabled ::ng-deep [tiTextweaken]:hover { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; +} + +.ti3-dropdown-no-data { + padding: var(--ti-common-space-6) var(--ti-common-space-10); + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + line-height: var(--ti-common-line-height-number); +} + +.ti3-dropdown-option-hover, +.ti3-dropdown-option-hover ::ng-deep [tiTextweaken] { + background-color: var(--ti-common-color-bg-white-emphasize); + color: var(--ti-common-color-text-highlight); +} +// 做修改 +.ti3-dropdown-option-selected, +.ti3-dropdown-option-selected ::ng-deep [tiTextweaken] { + background-color: var(--ti-list-option-bg-color-active); + color: var(--ti-common-color-text-white); +} +.ti3-drop-list-ul { + overflow-y: auto; + overflow-x: hidden; +} + +.ti3-list-scroll-to-bottom-loading-container { + position: absolute; + bottom: -8px; + width: calc(100% - 17px); + text-align: center; +} + +// 虚拟滚动 +:host.ti3-virtual-scroll-list { + display: block; + overflow: hidden; // 滚动容器为 .ti3-virtual-scroll-list-viewport + padding: var(--ti-common-space-0); +} +.ti3-virtual-scroll-list-viewport { + height: 100%; + width: 100%; + max-height: inherit; // 由于在下拉类组件中会给ti-list设置最大高度,这里直接继承ti-list的最大高度 + min-height: 38px; // 无数据时限制最小高度 无数据内容高度30px + 容器上下padding和8px + padding: var(--ti-list-container-vertical-padding) var(--ti-common-space-0); + box-sizing: border-box; +} +::ng-deep .ti3-virtual-scroll-list-viewport .cdk-virtual-scroll-content-wrapper { + width: 100%; + top: var(--ti-list-container-vertical-padding); +} +::ng-deep .ti3-virtual-scroll-list-viewport .cdk-virtual-scroll-spacer { + top: calc(var(--ti-list-container-vertical-padding) * 2); +} +::ng-deep .ti3-virtual-scroll-list-viewport.ti3-virtual-scroll-list-viewport-with-selectall .cdk-virtual-scroll-spacer { + top: calc(var(--ti-list-container-vertical-padding) * 2 + 30px); +} diff --git a/src/loading/demo/karma.conf.js b/src/loading/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/loading/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/loading/demo/project.json b/src/loading/demo/project.json new file mode 100644 index 0000000..d43ea4d --- /dev/null +++ b/src/loading/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/loading/demo", + "sourceRoot": "src/loading/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/loading", + "index": "src/loading/demo/src/index.html", + "main": "src/loading/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/loading/demo/tsconfig.app.json", + "assets": ["src/loading/demo/src/favicon.ico", "src/loading/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "loading-demo:build:production" + }, + "development": { + "browserTarget": "loading-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js loading" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/loading/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/loading/demo/tsconfig.spec.json", + "karmaConfig": "src/loading/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/loading/demo/src/app/AppComponent.ts b/src/loading/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/loading/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/loading/demo/src/app/AppModule.ts b/src/loading/demo/src/app/AppModule.ts new file mode 100644 index 0000000..9863337 --- /dev/null +++ b/src/loading/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { LoadingTestModule } from './loading/LoadingTestModule'; + +@NgModule({ + imports: [ + LoadingTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/loading/demo/src/app/IndexComponent.ts b/src/loading/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..0672756 --- /dev/null +++ b/src/loading/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { LoadingTestModule } from './loading/LoadingTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = LoadingTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/loading/demo/src/app/app.html b/src/loading/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/loading/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/loading/demo/src/app/loading/LoadingAreaComponent.ts b/src/loading/demo/src/app/loading/LoadingAreaComponent.ts new file mode 100644 index 0000000..2d5abf1 --- /dev/null +++ b/src/loading/demo/src/app/loading/LoadingAreaComponent.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './loading-area.html' +}) +export class LoadingAreaComponent { + showBlockLoading: boolean = false; + showFullscreenLoading: boolean = false; + showFullscreenLoadingWithMask: boolean = false; + + onClickShowBlockLoading(): void { + this.showBlockLoading = true; + setTimeout(() => { + this.showBlockLoading = false; // 3s后关闭 + }, 3000); + } + + onClickShowFullscreenLoading(): void { + this.showFullscreenLoading = true; + setTimeout(() => { + this.showFullscreenLoading = false; // 3s后关闭 + }, 3000); + } + + onClickShowFullscreenLoadingWithMask(): void { + this.showFullscreenLoadingWithMask = true; + setTimeout(() => { + this.showFullscreenLoadingWithMask = false; // 3s后关闭 + }, 3000); + } +} diff --git a/src/loading/demo/src/app/loading/LoadingBasicComponent.ts b/src/loading/demo/src/app/loading/LoadingBasicComponent.ts new file mode 100644 index 0000000..708bc41 --- /dev/null +++ b/src/loading/demo/src/app/loading/LoadingBasicComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './loading-basic.html' +}) +export class LoadingBasicComponent {} diff --git a/src/loading/demo/src/app/loading/LoadingSizeComponent.ts b/src/loading/demo/src/app/loading/LoadingSizeComponent.ts new file mode 100644 index 0000000..ef9bc78 --- /dev/null +++ b/src/loading/demo/src/app/loading/LoadingSizeComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './loading-size.html' +}) +export class LoadingSizeComponent {} diff --git a/src/loading/demo/src/app/loading/LoadingTestModule.ts b/src/loading/demo/src/app/loading/LoadingTestModule.ts new file mode 100644 index 0000000..3f5f205 --- /dev/null +++ b/src/loading/demo/src/app/loading/LoadingTestModule.ts @@ -0,0 +1,36 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiLoadingModule } from '@opentiny/ng'; + +import { LoadingBasicComponent } from './LoadingBasicComponent'; +import { LoadingTypeComponent } from './LoadingTypeComponent'; +import { LoadingSizeComponent } from './LoadingSizeComponent'; +import { LoadingAreaComponent } from './LoadingAreaComponent'; + +@NgModule({ + imports: [TiLoadingModule, TiButtonModule, RouterModule.forChild(LoadingTestModule.ROUTES)], + declarations: [LoadingTypeComponent, LoadingSizeComponent, LoadingBasicComponent, LoadingAreaComponent] +}) +export class LoadingTestModule { + static readonly LINKS: Array = [{ href: 'components/TiLoadingComponent.html', label: 'Loading' }]; + static readonly ROUTES: Routes = [ + { + path: 'loading/loading-basic', + component: LoadingBasicComponent + }, + { + path: 'loading/loading-type', + component: LoadingTypeComponent + }, + { + path: 'loading/loading-size', + component: LoadingSizeComponent + }, + { + path: 'loading/loading-area', + component: LoadingAreaComponent, + data: { label: '区域加载' } + } + ]; +} diff --git a/src/loading/demo/src/app/loading/LoadingTypeComponent.ts b/src/loading/demo/src/app/loading/LoadingTypeComponent.ts new file mode 100644 index 0000000..224012c --- /dev/null +++ b/src/loading/demo/src/app/loading/LoadingTypeComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './loading-type.html' +}) +export class LoadingTypeComponent {} diff --git a/src/loading/demo/src/app/loading/loading-area.html b/src/loading/demo/src/app/loading/loading-area.html new file mode 100644 index 0000000..34194a8 --- /dev/null +++ b/src/loading/demo/src/app/loading/loading-area.html @@ -0,0 +1,9 @@ + + + + +
    + +
    + + diff --git a/src/loading/demo/src/app/loading/loading-basic.html b/src/loading/demo/src/app/loading/loading-basic.html new file mode 100644 index 0000000..99bfd7d --- /dev/null +++ b/src/loading/demo/src/app/loading/loading-basic.html @@ -0,0 +1 @@ + diff --git a/src/loading/demo/src/app/loading/loading-size.html b/src/loading/demo/src/app/loading/loading-size.html new file mode 100644 index 0000000..e8c054b --- /dev/null +++ b/src/loading/demo/src/app/loading/loading-size.html @@ -0,0 +1,6 @@ +

    + +

    + +

    + diff --git a/src/loading/demo/src/app/loading/loading-type.html b/src/loading/demo/src/app/loading/loading-type.html new file mode 100644 index 0000000..7aaf5f7 --- /dev/null +++ b/src/loading/demo/src/app/loading/loading-type.html @@ -0,0 +1 @@ + diff --git a/src/loading/demo/src/app/loading/webdoc/loading-demos.js b/src/loading/demo/src/app/loading/webdoc/loading-demos.js new file mode 100644 index 0000000..c7398d1 --- /dev/null +++ b/src/loading/demo/src/app/loading/webdoc/loading-demos.js @@ -0,0 +1,54 @@ +export default { + column: '2', + demos: [ + { + demoId: 'loading-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    Loading 组件的最简用法。

    ', + 'en-US': '

    ' + } + }, + { + demoId: 'loading-type', + name: { + 'zh-CN': '加载类型', + 'en-US': 'Type' + }, + desc: { + 'zh-CN': '

    通过属性type配置加载显示类型,包括defaultweak

    ', + 'en-US': '

    ' + }, + apis: ['TiLoadingComponent.properties.type'] + }, + { + demoId: 'loading-size', + name: { + 'zh-CN': '大小', + 'en-US': 'Size' + }, + desc: { + 'zh-CN': + '

    通过属性size配置加载图标的大小,包括smallmiddlelarge,支持直接配置值。

    ', + 'en-US': '

    ' + }, + apis: ['TiLoadingComponent.properties.size'] + }, + { + demoId: 'loading-area', + name: { + 'zh-CN': '区域加载', + 'en-US': 'Area' + }, + desc: { + 'zh-CN': + '

    通过配置属性areablockfullscreen设置局部区域加载状态或全屏加载状态,通过配置showMasktrue显示遮罩层,遮罩层会阻止交互。

    ', + 'en-US': '

    ' + }, + apis: ['TiLoadingComponent.properties.area', 'TiLoadingComponent.properties.showMask'] + } + ] +}; diff --git a/src/loading/demo/src/app/loading/webdoc/loading.cn.md b/src/loading/demo/src/app/loading/webdoc/loading.cn.md new file mode 100644 index 0000000..351161b --- /dev/null +++ b/src/loading/demo/src/app/loading/webdoc/loading.cn.md @@ -0,0 +1,22 @@ +--- +title: Loading 加载 +--- +# Loading 加载 + +
    + +Loading 是加载数据时显示动效的组件。 + +```typescript +import { TiLoadingModule } from '@opentiny/ng'; +``` +
    + +
    + +Loading 是加载数据时显示动效的组件。 + +```typescript +import { TiLoadingModule } from '@opentiny/ng'; +``` +
    diff --git a/src/loading/demo/src/app/loading/webdoc/loading.en.md b/src/loading/demo/src/app/loading/webdoc/loading.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/loading/demo/src/app/loading/webdoc/loading.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/loading/demo/src/favicon.ico b/src/loading/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/loading/demo/src/index.html b/src/loading/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/loading/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/loading/demo/src/main.ts b/src/loading/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/loading/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/loading/demo/test.ts b/src/loading/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/loading/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/loading/demo/tsconfig.app.json b/src/loading/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/loading/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/loading/demo/tsconfig.spec.json b/src/loading/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/loading/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/loading/lib/index.ts b/src/loading/lib/index.ts new file mode 100644 index 0000000..eff0a58 --- /dev/null +++ b/src/loading/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiLoadingModule'; diff --git a/src/loading/lib/ng-package.json b/src/loading/lib/ng-package.json new file mode 100644 index 0000000..6c0c0ea --- /dev/null +++ b/src/loading/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/loading", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/loading/lib/package.json b/src/loading/lib/package.json new file mode 100644 index 0000000..3a1b3f8 --- /dev/null +++ b/src/loading/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-loading", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/loading/lib/project.json b/src/loading/lib/project.json new file mode 100644 index 0000000..660828d --- /dev/null +++ b/src/loading/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/loading/lib", + "sourceRoot": "src/loading/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/loading"], + "options": { + "project": "src/loading/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/loading"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js loading" + }, + { + "command": "ng default-build loading" + }, + { + "command": "node build/clear-default-theme.js loading" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/loading && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build loading && ng pack loading && node build/publish.js loading --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/loading/lib/src/TiLoadingComponent.ts b/src/loading/lib/src/TiLoadingComponent.ts new file mode 100644 index 0000000..4f2e180 --- /dev/null +++ b/src/loading/lib/src/TiLoadingComponent.ts @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * Loading组件用于加载场景,提供了两种类型 + * + * + */ +@Component({ + selector: 'ti-loading', + templateUrl: './loading.html', + styleUrls: ['./loading.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLoadingComponent extends TiBaseComponent { + /** + * @ignore + * 弱类型有4个圈 + */ + public items: Array = []; + protected versionInfo: string = super.getVersion(packageInfo); + + /** + * 加载类型 + */ + @Input() type: 'default' | 'weak' = 'default'; // 加载类型 + + /** + * 展示区域 + */ + @Input() area: 'default' | 'fullscreen' | 'block' = 'default'; + + /** + * 是否展示遮罩 + * + * 全屏场景下可以控制是否展示遮罩层 + */ + @Input() showMask: boolean = false; + + /** + * 加载尺寸 + */ + @Input() size: 'small' | 'middle' | 'large' | string = 'small'; // 加载尺寸 + + ngOnInit(): void { + super.ngOnInit(); + this.items.length = 4; + } + + /** + * @ignore + * 根据用户传入尺寸获取border的宽度 + */ + getBorderWidth(size: string): string { + return (parseInt(size, 10) * 0.3) / 2 + 'px'; + } +} diff --git a/src/loading/lib/src/TiLoadingModule.ts b/src/loading/lib/src/TiLoadingModule.ts new file mode 100644 index 0000000..4e17490 --- /dev/null +++ b/src/loading/lib/src/TiLoadingModule.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiLoadingComponent } from './TiLoadingComponent'; +import { TiLoadingfailComponent } from './TiLoadingfailComponent'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, TiIconModule], + exports: [TiLoadingComponent, TiLoadingfailComponent], + declarations: [TiLoadingComponent, TiLoadingfailComponent] +}) +export class TiLoadingModule { + constructor() { + const words = (window as any).tiWords; + TiLocale.setTiWords(locales); + } +} +export { TiLoadingComponent } from './TiLoadingComponent'; +export { TiLoadingfailComponent } from './TiLoadingfailComponent'; diff --git a/src/loading/lib/src/TiLoadingfailComponent.ts b/src/loading/lib/src/TiLoadingfailComponent.ts new file mode 100644 index 0000000..9aee383 --- /dev/null +++ b/src/loading/lib/src/TiLoadingfailComponent.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, EventEmitter, Output } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiLocale } from '@opentiny/ng-locale'; +import packageInfo from '../package.json'; +/** + * @ignore + * + * 加载失败,可以重新加载 + * + * + */ +@Component({ + selector: 'ti-loadingfail', + templateUrl: './loadingfail.html', + styleUrls: ['./loadingfail.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLoadingfailComponent extends TiBaseComponent { + @Output() readonly reload: EventEmitter = new EventEmitter(); + + /** + * @ignore + */ + protected versionInfo: string = super.getVersion(packageInfo); + public tiLoadingWords: any = TiLocale.getLocaleWords().tiLoading; + + /** + * @ignore + * + * 重新加载的回调事件 + */ + onReload(event: MouseEvent): void { + this.reload.emit(event); + } +} diff --git a/src/loading/lib/src/i18n/TiLoadingWords.ts b/src/loading/lib/src/i18n/TiLoadingWords.ts new file mode 100644 index 0000000..3dbcd17 --- /dev/null +++ b/src/loading/lib/src/i18n/TiLoadingWords.ts @@ -0,0 +1,6 @@ +export interface TiLoadingWords { + tiLoading: { + loadingfail: string; + reload: string; + }; +} diff --git a/src/loading/lib/src/i18n/en_US.ts b/src/loading/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..2a48ce3 --- /dev/null +++ b/src/loading/lib/src/i18n/en_US.ts @@ -0,0 +1,8 @@ +import { TiLoadingWords } from './TiLoadingWords'; + +export const en_US: TiLoadingWords = { + tiLoading: { + loadingfail: 'Loading failed. ', + reload: 'Reload' + } +}; diff --git a/src/loading/lib/src/i18n/es_US.ts b/src/loading/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..dbaba34 --- /dev/null +++ b/src/loading/lib/src/i18n/es_US.ts @@ -0,0 +1,8 @@ +import { TiLoadingWords } from './TiLoadingWords'; + +export const es_US: TiLoadingWords = { + tiLoading: { + loadingfail: 'Error al cargar. ', + reload: 'Volver a cargar' + } +}; diff --git a/src/loading/lib/src/i18n/fr_FR.ts b/src/loading/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..1404dc8 --- /dev/null +++ b/src/loading/lib/src/i18n/fr_FR.ts @@ -0,0 +1,8 @@ +import { TiLoadingWords } from './TiLoadingWords'; + +export const fr_FR: TiLoadingWords = { + tiLoading: { + loadingfail: 'Le chargement a échoué. ', + reload: 'Recharger' + } +}; diff --git a/src/loading/lib/src/i18n/index.ts b/src/loading/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/loading/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/loading/lib/src/i18n/pt_BR.ts b/src/loading/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..21c8475 --- /dev/null +++ b/src/loading/lib/src/i18n/pt_BR.ts @@ -0,0 +1,8 @@ +import { TiLoadingWords } from './TiLoadingWords'; + +export const pt_BR: TiLoadingWords = { + tiLoading: { + loadingfail: 'Erro no carregamento. ', + reload: 'Recarregar' + } +}; diff --git a/src/loading/lib/src/i18n/zh_CN.ts b/src/loading/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..1bdaaa0 --- /dev/null +++ b/src/loading/lib/src/i18n/zh_CN.ts @@ -0,0 +1,8 @@ +import { TiLoadingWords } from './TiLoadingWords'; + +export const zh_CN: TiLoadingWords = { + tiLoading: { + loadingfail: '加载失败,', + reload: '重新加载' + } +}; diff --git a/src/loading/lib/src/loading.html b/src/loading/lib/src/loading.html new file mode 100644 index 0000000..594e0e4 --- /dev/null +++ b/src/loading/lib/src/loading.html @@ -0,0 +1,20 @@ + +
    +
    + +
    + +
    diff --git a/src/loading/lib/src/loading.less b/src/loading/lib/src/loading.less new file mode 100644 index 0000000..ffa98b4 --- /dev/null +++ b/src/loading/lib/src/loading.less @@ -0,0 +1,116 @@ +:host { + --ti-loading-weak-size: 5px; // 弱类型加载尺寸 + --ti-backdrop-bg-color: #000000; // 遮罩层颜色 +} + +.ti3-loading-size(@size) { + width: @size; + height: @size; + border-width: calc(@size * 0.3 / 2); // 内圈外径 = 0.7 * 外圈外径;故边框粗 = 0.3 * 外圈直径 /2 +} + +.ti3-loading-default { + display: inline-block; + box-sizing: border-box; + border-style: var(--ti-common-border-style-solid); + border-color: var(--ti-common-color-line-active); + border-radius: var(--ti-common-border-radius-3); + border-right-color: transparent; + animation: default-animation 0.8s linear 0s infinite; + &.ti3-loading-default-small { + .ti3-loading-size(var(--ti-common-size-3x)); // s类型尺寸 + } + &.ti3-loading-default-middle { + .ti3-loading-size(var(--ti-common-size-6x)); // m类型尺寸 + } + &.ti3-loading-default-large { + .ti3-loading-size(var(--ti-common-size-8x)); // l类型尺寸 + } +} + +.ti3-loading-weak-box { + height: var(--ti-loading-weak-size); + width: calc(var(--ti-loading-weak-size) * 4 + var(--ti-common-space-2x) * 3); +} + +.ti3-loading-weak { + display: inline-block; + width: var(--ti-loading-weak-size); + height: var(--ti-loading-weak-size); + border-radius: var(--ti-common-border-radius-3); + background-color: var(--ti-common-color-line-active); + opacity: 0.7; + animation: weak-animation 1.2s ease-in-out infinite; + &:not(:last-child) { + margin-right: var(--ti-common-space-2x); + } + &:first-child { + animation-delay: 0s; + } + &:nth-child(2) { + animation-delay: 0.2s; + } + &:nth-child(3) { + animation-delay: 0.4s; + } + &:nth-child(4) { + animation-delay: 0.6s; + } +} + +.ti3-loading-fullscreen-mask { + position: fixed; + z-index: 1200; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--ti-backdrop-bg-color); + opacity: 0.2; +} + +.ti3-loading-fullscreen { + position: fixed; + z-index: 1200; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; +} + +.ti3-loading-block { + position: absolute; + z-index: 1200; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; +} + +// 定义默认loading旋转动画 +@keyframes default-animation { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +// 定义弱loading动画 +@keyframes weak-animation { + 0% { + opacity: 0.2; + } + 25% { + opacity: 0.5; + } + 50% { + opacity: 0.7; + } + 100% { + opacity: 0.2; + } +} diff --git a/src/loading/lib/src/loadingfail.html b/src/loading/lib/src/loadingfail.html new file mode 100644 index 0000000..562b0fb --- /dev/null +++ b/src/loading/lib/src/loadingfail.html @@ -0,0 +1,12 @@ + +{{tiLoadingWords.loadingfail}} + {{tiLoadingWords.reload}} + + diff --git a/src/loading/lib/src/loadingfail.less b/src/loading/lib/src/loadingfail.less new file mode 100644 index 0000000..8223b3d --- /dev/null +++ b/src/loading/lib/src/loadingfail.less @@ -0,0 +1,36 @@ +:host { + --ti-loadingfail-icon-size: var(--ti-common-size-4x); + --ti-loadingfail-line-height: calc(var(--ti-common-font-size-base) * var(--ti-common-line-height-number)); + --ti-loadingfail-margin-vertical: calc((var(--ti-loadingfail-line-height) - var(--ti-loadingfail-icon-size)) / 2); +} + +:host { + display: inline-flex; + align-items: flex-start; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + line-height: var(--ti-common-line-height-number); +} + +.ti3-loadingfail-icon { + display: inline-block; + color: var(--ti-common-color-text-white); + width: var(--ti-loadingfail-icon-size); + height: var(--ti-loadingfail-icon-size); + line-height: var(--ti-loadingfail-icon-size); + font-size: calc(var(--ti-loadingfail-icon-size) * 0.75); + border-radius: var(--ti-common-border-radius-3); + text-align: center; + background-color: var(--ti-common-color-error); + margin: var(--ti-loadingfail-margin-vertical) var(--ti-common-space-2x) var(--ti-loadingfail-margin-vertical) var(--ti-common-space-0); + flex-shrink: 0; +} + +.ti3-loadingfail-link { + color: var(--ti-common-color-text-link); + text-decoration: none; + &:hover { + color: var(--ti-common-color-text-link-hover); + text-decoration: underline; + } +} diff --git a/src/locale/demo/karma.conf.js b/src/locale/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/locale/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/locale/demo/project.json b/src/locale/demo/project.json new file mode 100644 index 0000000..322a1f3 --- /dev/null +++ b/src/locale/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/locale/demo", + "sourceRoot": "src/locale/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/locale", + "index": "src/locale/demo/src/index.html", + "main": "src/locale/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/locale/demo/tsconfig.app.json", + "assets": ["src/locale/demo/src/favicon.ico", "src/locale/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "locale-demo:build:production" + }, + "development": { + "browserTarget": "locale-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js locale" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/locale/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/locale/demo/tsconfig.spec.json", + "karmaConfig": "src/locale/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/locale/demo/src/app/AppComponent.ts b/src/locale/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/locale/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/locale/demo/src/app/AppModule.ts b/src/locale/demo/src/app/AppModule.ts new file mode 100644 index 0000000..1a93347 --- /dev/null +++ b/src/locale/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { LocaleTestModule } from './locale/LocaleTestModule'; + +@NgModule({ + imports: [ + LocaleTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/locale/demo/src/app/IndexComponent.ts b/src/locale/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..5571792 --- /dev/null +++ b/src/locale/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { LocaleTestModule } from './locale/LocaleTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = LocaleTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/locale/demo/src/app/app.html b/src/locale/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/locale/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/locale/demo/src/app/locale/LocaleBasicComponent.ts b/src/locale/demo/src/app/locale/LocaleBasicComponent.ts new file mode 100644 index 0000000..3b762ac --- /dev/null +++ b/src/locale/demo/src/app/locale/LocaleBasicComponent.ts @@ -0,0 +1,61 @@ +import { Component } from '@angular/core'; +import { TiLocale } from '@opentiny/ng'; + +interface LocaleWords { + tiLocaleKey: string; + testPassWord: string; + testMaxValue: string; + testRangeValue: string; +} + +@Component({ + templateUrl: './locale-basic.html' +}) +export class LocaleBasicComponent { + // 用户自定义词条 + private static myzh_CN: LocaleWords = { + tiLocaleKey: 'zh-CN', + testPassWord: '密码输入不符合要求,请重新输入', + testMaxValue: '输入值不能超过{0}', + testRangeValue: '输入值必须在{0}到{1}之间' + }; + + // 用户自定义词条 + private static myen_US: LocaleWords = { + tiLocaleKey: 'en-US', + testPassWord: 'Invalid password.', + testMaxValue: 'Enter a value less than or equal to {0}.', + testRangeValue: 'Enter a value from {0} to {1}.' + }; + + constructor() { + const words = (window as any).tiWords; + // 添加用户的国际化词条 + TiLocale.setWords({ + 'zh-CN': { ...words['zh-CN'], ...LocaleBasicComponent.myzh_CN }, + 'en-US': { ...words['en-US'], ...LocaleBasicComponent.myen_US } + }); + this.setValues(); + } + + testPassWord: string; + testMaxValue: string; + testRangeValue: string; + okBtn: string; + + changeLocale(localeKey: string): void { + TiLocale.setLocale(localeKey); + this.setValues(); + } + + setValues(): void { + this.testPassWord = this.setLocaleValue('testPassWord'); + this.testMaxValue = this.setLocaleValue('testMaxValue', [1]); + this.testRangeValue = this.setLocaleValue('testRangeValue', [1, 2]); + this.okBtn = this.setLocaleValue('tiButton.ok'); + } + + setLocaleValue(key: string, params?: Array): string { + return TiLocale.translate(key, params); + } +} diff --git a/src/locale/demo/src/app/locale/LocaleFormatComponent.ts b/src/locale/demo/src/app/locale/LocaleFormatComponent.ts new file mode 100644 index 0000000..0bbd40e --- /dev/null +++ b/src/locale/demo/src/app/locale/LocaleFormatComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { TiLocaleFormat } from '@opentiny/ng'; +@Component({ + templateUrl: './locale-format.html' +}) +export class LocaleFormatComponent { + formatedNumber: string = TiLocaleFormat.formatNumber(1598565912.6574, '1.2-2'); + formatedNumber1: string = TiLocaleFormat.formatNumber(1000, '1.1-1'); + formatedDate: string = TiLocaleFormat.formatDate(new Date()); + formatedTime: string = TiLocaleFormat.formatTime(new Date()); + formatedDateTime: string = TiLocaleFormat.formatDateTime(new Date(), '', '+0430'); +} diff --git a/src/locale/demo/src/app/locale/LocaleReloadComponent.ts b/src/locale/demo/src/app/locale/LocaleReloadComponent.ts new file mode 100644 index 0000000..291c97a --- /dev/null +++ b/src/locale/demo/src/app/locale/LocaleReloadComponent.ts @@ -0,0 +1,70 @@ +import { Component } from '@angular/core'; +import { TiLocale } from '@opentiny/ng'; + +interface LocaleWords { + tiLocaleKey: string; + testPassWord: string; + testMaxValue: string; + testRangeValue: string; +} + +@Component({ + templateUrl: './locale-reload.html' +}) +export class LocaleReloadComponent { + // 用户自定义词条 + private static myzh_CN: LocaleWords = { + tiLocaleKey: 'zh-CN', + testPassWord: '密码输入不符合要求,请重新输入', + testMaxValue: '输入值不能超过{0}', + testRangeValue: '输入值必须在{0}到{1}之间' + }; + + // 用户自定义词条 + private static myen_US: LocaleWords = { + tiLocaleKey: 'en-US', + testPassWord: 'Invalid password.', + testMaxValue: 'Enter a value less than or equal to {0}.', + testRangeValue: 'Enter a value from {0} to {1}.' + }; + + constructor() { + const words = (window as any).tiWords; + // 添加用户的国际化词条 + TiLocale.setWords({ + 'zh-CN': { ...words['zh-CN'], ...LocaleReloadComponent.myzh_CN }, + 'en-US': { ...words['en-US'], ...LocaleReloadComponent.myen_US } + }); + + TiLocale.setLocale(this.getCookie('localeKey')); + } + + changeLocale(localeKey: string): void { + TiLocale.setLocale(localeKey); + } + + setLocaleValue(key: string, params?: Array): string { + return TiLocale.translate(key, params); + } + + setLocaleAndRefresh(localeKey: string): void { + this.changeLocale(localeKey); + document.cookie = `localeKey=${localeKey}`; + location.reload(); + } + + getCookie(key: string): string { + const name: string = key + '='; + const splitedCookie: Array = document.cookie.split(';'); + for (let word of splitedCookie) { + while (word.charAt(0) === ' ') { + word = word.substring(1); + } + if (word.indexOf(name) === 0) { + return word.substring(name.length, word.length); + } + } + + return ''; + } +} diff --git a/src/locale/demo/src/app/locale/LocaleTestModule.ts b/src/locale/demo/src/app/locale/LocaleTestModule.ts new file mode 100644 index 0000000..82d015c --- /dev/null +++ b/src/locale/demo/src/app/locale/LocaleTestModule.ts @@ -0,0 +1,35 @@ +import { LOCALE_ID, NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule, registerLocaleData } from '@angular/common'; + +import { TiButtonModule, TiLocaleModule } from '@opentiny/ng'; + +import { LocaleBasicComponent } from './LocaleBasicComponent'; +import { LocaleFormatComponent } from './LocaleFormatComponent'; +import { LocaleReloadComponent } from './LocaleReloadComponent'; +import localeZh from '@angular/common/locales/zh'; + +@NgModule({ + imports: [CommonModule, TiLocaleModule, TiButtonModule, RouterModule.forChild(LocaleTestModule.ROUTES)], + declarations: [LocaleBasicComponent, LocaleFormatComponent, LocaleReloadComponent], + providers: [{ provide: LOCALE_ID, useValue: 'zh' }] +}) +export class LocaleTestModule { + constructor() { + registerLocaleData(localeZh, 'zh'); // 用于设置时间日期、数字格式化信息的配置,组件内部使用的规则语种与国际化语种设置一致,具体使用参考angular国际化设置文档 + } + static readonly ROUTES: Routes = [ + { + path: 'locale/locale-basic', + component: LocaleBasicComponent + }, + { + path: 'locale/locale-reload', + component: LocaleReloadComponent + }, + { + path: 'locale/locale-format', + component: LocaleFormatComponent + } + ]; +} diff --git a/src/locale/demo/src/app/locale/locale-basic.html b/src/locale/demo/src/app/locale/locale-basic.html new file mode 100644 index 0000000..cd5a58d --- /dev/null +++ b/src/locale/demo/src/app/locale/locale-basic.html @@ -0,0 +1,7 @@ +

    {{ testPassWord }}

    +

    {{ testMaxValue }}

    +

    {{ testRangeValue }}

    +

    {{ okBtn }}

    + + + diff --git a/src/locale/demo/src/app/locale/locale-format.html b/src/locale/demo/src/app/locale/locale-format.html new file mode 100644 index 0000000..4d17fff --- /dev/null +++ b/src/locale/demo/src/app/locale/locale-format.html @@ -0,0 +1,5 @@ +

    1.数字,保留2位:{{ formatedNumber }}

    +

    2.数字,保留1位:{{ formatedNumber1 }}

    +

    3.日期:{{ formatedDate }}

    +

    4.时间:{{ formatedTime }}

    +

    5.时间日期:{{ formatedDateTime }}

    diff --git a/src/locale/demo/src/app/locale/locale-reload.html b/src/locale/demo/src/app/locale/locale-reload.html new file mode 100644 index 0000000..9abee68 --- /dev/null +++ b/src/locale/demo/src/app/locale/locale-reload.html @@ -0,0 +1,6 @@ +

    {{ 'testPassWord' | tiTranslate }}

    +

    {{ 'testMaxValue' | tiTranslate: [1] }}

    +

    {{ 'testRangeValue' | tiTranslate: [1,2] }}

    +

    {{ 'tiButton.ok' | tiTranslate }}

    + + diff --git a/src/locale/demo/src/app/locale/webdoc/locale-demos.js b/src/locale/demo/src/app/locale/webdoc/locale-demos.js new file mode 100644 index 0000000..c84b231 --- /dev/null +++ b/src/locale/demo/src/app/locale/webdoc/locale-demos.js @@ -0,0 +1,40 @@ +export default { + column: '2', + demos: [ + { + demoId: 'locale-basic', + name: { + 'zh-CN': '基础用法', + 'en-US': 'locale Basic', + }, + desc: { + 'zh-CN': '语言切换,页面不刷新场景。', + 'en-US': '

    locale Basic

    ', + }, + }, + { + demoId: 'locale-reload', + name: { + 'zh-CN': '过滤器', + 'en-US': 'locale reload', + }, + desc: { + 'zh-CN': + '使用过滤器做国际化转换,此方式只支持刷新页面切换语言,用户可结合 cookie 进行语言切换', + 'en-US': 'locale reload', + }, + apis: ['TiLocale.methods.setLocale'], + }, + { + demoId: 'locale-format', + name: { + 'zh-CN': '格式化', + 'en-US': 'format', + }, + desc: { + 'zh-CN': '使用组件 API 格式化本地数字、货币、时间日期。', + 'en-US': 'locale format', + }, + }, + ], +}; diff --git a/src/locale/demo/src/app/locale/webdoc/locale.cn.md b/src/locale/demo/src/app/locale/webdoc/locale.cn.md new file mode 100644 index 0000000..98ea608 --- /dev/null +++ b/src/locale/demo/src/app/locale/webdoc/locale.cn.md @@ -0,0 +1,28 @@ +--- +title: TiLocale 国际化语言 +--- +# TiLocale 国际化语言 + +
    + +Tiny 组件中使用的国际化词条配置方法类。   + ++ 可通过在启动模块中通过配置具体的国际化语种的方式设置组件的国际化。 + +```typescript +import { TiLocaleModule } from '@opentiny/ng'; +``` + +
    + +
    + +Tiny 组件中使用的国际化词条配置方法类。   + ++ 可通过在启动模块中通过配置具体的国际化语种的方式设置组件的国际化。 + +```typescript +import { TiLocaleModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/locale/demo/src/app/locale/webdoc/locale.en.md b/src/locale/demo/src/app/locale/webdoc/locale.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/locale/demo/src/app/locale/webdoc/locale.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/locale/demo/src/favicon.ico b/src/locale/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/locale/demo/src/index.html b/src/locale/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/locale/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/locale/demo/src/main.ts b/src/locale/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/locale/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/locale/demo/test.ts b/src/locale/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/locale/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/locale/demo/tsconfig.app.json b/src/locale/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/locale/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/locale/demo/tsconfig.spec.json b/src/locale/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/locale/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/locale/lib/index.ts b/src/locale/lib/index.ts new file mode 100644 index 0000000..d432ba7 --- /dev/null +++ b/src/locale/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiLocaleModule'; diff --git a/src/locale/lib/ng-package.json b/src/locale/lib/ng-package.json new file mode 100644 index 0000000..e2df576 --- /dev/null +++ b/src/locale/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/locale", + "lib": { + "entryFile": "index.ts" + } +} diff --git a/src/locale/lib/package.json b/src/locale/lib/package.json new file mode 100644 index 0000000..13fef71 --- /dev/null +++ b/src/locale/lib/package.json @@ -0,0 +1,10 @@ +{ + "name": "@opentiny/ng-locale", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/locale/lib/project.json b/src/locale/lib/project.json new file mode 100644 index 0000000..6aca8a8 --- /dev/null +++ b/src/locale/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/locale/lib", + "sourceRoot": "src/locale/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/locale"], + "options": { + "project": "src/locale/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/locale"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js locale" + }, + { + "command": "ng default-build locale" + }, + { + "command": "node build/clear-default-theme.js locale" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/locale && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build locale && ng pack locale && node build/publish.js locale --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/locale/lib/src/TiLocale.ts b/src/locale/lib/src/TiLocale.ts new file mode 100644 index 0000000..48308e7 --- /dev/null +++ b/src/locale/lib/src/TiLocale.ts @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Util } from '@opentiny/ng-utils'; + +declare let global: any; + +export class TiLocale { + /** + * 英文语种关键字,关键字均使用中划线形式命名,确保和浏览器保持一致 + */ + public static readonly EN_US: string = 'en-US'; + /** + * 语种关键字 + */ + public static readonly ZH_CN: string = 'zh-CN'; + /** + * 语种关键字 + */ + public static readonly ES_US: string = 'es-US'; + /** + * 语种关键字 + */ + public static readonly FR_FR: string = 'fr-FR'; + /** + * 语种关键字 + */ + public static readonly PT_BR: string = 'pt-BR'; + + // private static localeKey: string = TiLocale.ZH_CN; // 语种关键字 + + /** + * Typescript没有静态代码段,所以这样代替静态代码段 + */ + protected static staticCode: void = TiLocale.initWordsAndLocale(); + /** + * 静态代码段执行:如过word和locale未初始化,那么执行初始化。 + */ + private static initWordsAndLocale(): void { + if (!TiLocale.getWords()) { + // 默认所有语言都打包进来 + TiLocale.setWords({}); + } + if (!TiLocale.getLocale()) { + TiLocale.setLocale(TiLocale.ZH_CN); + } + } + + /** + * Sets words + * @param Words 全量的语言包,由语言名称:语言词条包组成。 + * + * @returns words + */ + public static setWords(words: any): void { + // 设计缺陷:如果tiWords放在类成员变量上,会更好。window在SSR环境找不到。 + if (typeof window !== 'undefined') { + (window as any).tiWords = words; + } else if (typeof global !== 'undefined') { + (global as any).tiWords = words; + } + } + public static getWords(): any { + if (typeof window !== 'undefined') { + return (window as any).tiWords; + } else if (typeof global !== 'undefined') { + return (global as any).tiWords; + } + } + /** + * 设置组件国际化语种 + * @param locale 国际化字符,可通过TiLocale.ZH_CN/TiLocale.EN_US等语种关键字设置参数 + */ + public static setLocale(locale: string): void { + if (typeof window !== 'undefined') { + (window as any).tiLocale = locale; + } else if (typeof global !== 'undefined') { + (global as any).tiLocale = locale; + } + } + public static getLocale(): string { + let locale: string; + if (typeof window !== 'undefined') { + locale = (window as any).tiLocale; + } else if (typeof global !== 'undefined') { + locale = (global as any).tiLocale; + } + + return locale ? locale : TiLocale.ZH_CN; + } + + /** + * 设置组件国际化语种 + * @param localeKey 国际化字符,可通过TiLocale.ZH_CN/TiLocale.EN_US等语种关键字设置参数 + */ + + /** + * @ignore + * 获取当前语言下,组件国际化语种对应的词条集合 + */ + public static getLocaleWords(): any { + return TiLocale.getWords()[TiLocale.getLocale()]; + } + /** + * @ignore + * 获取单个词条的国际化文本/对象 + * 参数: + */ + public static translate(keyValue: string, params?: Array): any { + const keyArr: Array = keyValue.split('.'); + let value: any = TiLocale.getLocaleWords(); + keyArr.forEach((key: string) => { + value = value[key]; + }); + + return Util.formatEntry(value, params); + } + /** + * @ignore + * 获取各组件的词条追加在 tiWords 中 + */ + public static setTiWords(locales): void { + const { en_US, es_US, fr_FR, pt_BR, zh_CN } = locales; + const words = (window as any).tiWords; + TiLocale.setWords({ + 'zh-CN': { ...words['zh-CN'], ...zh_CN }, + 'en-US': { ...words['en-US'], ...en_US }, + 'es-US': { ...words['es-US'], ...es_US }, + 'fr-FR': { ...words['fr-FR'], ...fr_FR }, + 'pt-BR': { ...words['pt-BR'], ...pt_BR } + }); + } +} diff --git a/src/locale/lib/src/TiLocaleFormat.ts b/src/locale/lib/src/TiLocaleFormat.ts new file mode 100644 index 0000000..7d8f4d2 --- /dev/null +++ b/src/locale/lib/src/TiLocaleFormat.ts @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * @ignore + */ +import { formatDate, formatNumber, getLocaleCurrencySymbol, getLocaleId, getLocaleNumberSymbol, NumberSymbol } from '@angular/common'; +import { TiLocale } from './TiLocale'; +export class TiLocaleFormat { + private static readonly TIME_FORMAT: string = 'mediumTime'; // 'h:mm:ss a' (e.g. 9:03:01 AM) + private static readonly DATE_FORMAT: string = 'mediumDate'; // 'MMM d, y' (e.g. Jun 15, 2015). + private static readonly DATETIME_FORMAT: string = 'medium'; // 'MMM d, y, h:mm:ss a' (e.g. Jun 15, 2015, 9:03:01 AM) + private static readonly DEFAULT_LOCALE: string = 'en'; + /** + * 获取语种关键字,默认为'en' + * @return 语种关键字 + */ + private static getLocaleKey(): string { + let locale: string = TiLocale.getLocale(); + try { + // local信息未注册情况下,getLocaleId该方法会报错,因此使用try catch的方式 + getLocaleId(locale); + } catch (error) { + locale = TiLocaleFormat.DEFAULT_LOCALE; + } + + return locale; + } + /** + * @description 对国际化数字进行处理(包含小数点) + * @param: number 国际化数字 + * @param: isIntegerValid 只针对integer的情况 + * @return 返回数字信息 + */ + private static parseNumWithDecimal(number: string, isIntegerValid?: boolean): any { + const groupSep: string = TiLocaleFormat.getNumberSymbol('Group'); + const decimalSep: string = TiLocaleFormat.getNumberSymbol('Decimal'); + const numberReg: RegExp = new RegExp('^(?:-?\\d*|-?\\d(?:\\' + groupSep + '?\\d*)+)?(?:\\' + decimalSep + '\\d*)?$'); + const numberFormat: string = number; + + if (!numberReg.test(numberFormat)) { + return NaN; + } + const groupReg: RegExp = new RegExp('\\' + groupSep, 'g'); + const decimalReg: RegExp = new RegExp('\\' + decimalSep, 'g'); + const numberFormatString: string = numberFormat.replace(groupReg, '').replace(decimalReg, '.'); + const numberFloat: number = parseFloat(numberFormatString); + /* + * *只针对integer的情况 + * 截取转换前的小数位字符串numFormatSlice + * 转换前的小数位字符串长度numFormatCount + * 转换后的小数位为0,就添加小数位个数的0,返回值是string类型 + * */ + + if (isIntegerValid && numberFormatString.indexOf('.') !== -1) { + return numberFormatString; + } + + return numberFloat; + } + /** + * 获取数字规则信息 + * key 规则关键字 + */ + public static getNumberSymbol(key: string): string { + const standardNumber: number = 10000.0; + const localeNumber: string = TiLocaleFormat.formatNumber(standardNumber, '1.1-1'); + + return key === 'Group' ? localeNumber.charAt(2) : localeNumber.charAt(6); + } + /** + * @description 对数字进行国际化处理 + * @param: number 数字 + * @param: numberFormat 数字格式化形式 + * @return 返回格式化后的信息 + */ + public static formatNumber(number: number, format: string): string { + let options: any; + if (format) { + options = { + minimumIntegerDigits: format.substring(0, format.indexOf('.')), + minimumFractionDigits: format.substring(format.indexOf('.') + 1, format.indexOf('-')), + maximumFractionDigits: format.substring(format.indexOf('-') + 1) + }; + } + + // TODO: new Intl.NumberFormat es西语下整数部分有四位时,本地化数字没有千位分隔 + const res: string = new Intl.NumberFormat(TiLocale.getLocale(), options).format(number); + + return res; + } + + /** + * @description 对国际化数字进行标准化处理 + * @param: number 国际化数字 + * @return 返回数字信息 + */ + public static parseNumber(number: string): number { + return parseFloat(TiLocaleFormat.parseNumWithDecimal(number)); + } + + /** + * @description 对时间进行国际化处理 + * @param: time 时间 + * @param: format 时间格式 + * @return 返回格式化后的信息 + */ + public static formatTime(time: string | number | Date, format?: string, timezone?: string): string { + return formatDate(time, format || TiLocaleFormat.TIME_FORMAT, TiLocaleFormat.getLocaleKey(), timezone); + } + + /** + * @description 对日期进行国际化处理 + * @param date 日期 + * @param format 日期格式 + * @return 返回格式化后的信息 + */ + public static formatDate(date: Date, format?: string, timezone?: string): string { + return formatDate(date, format || TiLocaleFormat.DATE_FORMAT, TiLocaleFormat.getLocaleKey(), timezone); + } + + /** + * @description 对时间日期进行国际化处理 + * @param: 时间日期 + * @param: 时间日期格式 + * @return 返回格式化后的信息 + */ + public static formatDateTime(dateTime: Date, format?: string, timezone?: string): string { + return formatDate(dateTime, format || TiLocaleFormat.DATETIME_FORMAT, TiLocaleFormat.getLocaleKey(), timezone); + } + + /** + * 功能描述:去除千位分隔符,得到标准化数字 + * value: 带有千位分隔符的数据 + */ + public static deleteGroupSep(value: any): string { + let res: any; + res = String(value); + const groupSep: string = TiLocaleFormat.getNumberSymbol('Group'); + const groupSepReg: any = new RegExp('\\' + groupSep, 'g'); + + return res.replace(groupSepReg, ''); + } + /** + * 检测format接口配置是否合法。目前spinner和inputnumber组件会用到 + */ + public static isInvalidFormat(format: string): boolean { + if (typeof format !== 'string' || format.charAt(0).toUpperCase() !== 'N' || Number.isNaN(parseInt(format.slice(1), 10))) { + return true; + } + + return false; + } +} diff --git a/src/locale/lib/src/TiLocaleModule.ts b/src/locale/lib/src/TiLocaleModule.ts new file mode 100644 index 0000000..0aa05ca --- /dev/null +++ b/src/locale/lib/src/TiLocaleModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiTranslatePipe } from './TiTranslatePipe'; +/** + * @ignore + */ +@NgModule({ + declarations: [TiTranslatePipe], + exports: [TiTranslatePipe] +}) +export class TiLocaleModule {} +export { TiLocale } from './TiLocale'; +export { TiLocaleFormat } from './TiLocaleFormat'; +export { TiTranslatePipe } from './TiTranslatePipe'; diff --git a/src/locale/lib/src/TiTranslatePipe.ts b/src/locale/lib/src/TiTranslatePipe.ts new file mode 100644 index 0000000..563d8ce --- /dev/null +++ b/src/locale/lib/src/TiTranslatePipe.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Pipe, PipeTransform } from '@angular/core'; +import { TiLocale } from './TiLocale'; + +/** + * @ignore + */ +@Pipe({ + name: 'tiTranslate' +}) +export class TiTranslatePipe implements PipeTransform { + transform(keyValue: string, params?: Array): string { + return TiLocale.translate(keyValue, params); + } +} diff --git a/src/menu/demo/karma.conf.js b/src/menu/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/menu/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/menu/demo/project.json b/src/menu/demo/project.json new file mode 100644 index 0000000..86ff6ce --- /dev/null +++ b/src/menu/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/menu/demo", + "sourceRoot": "src/menu/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/menu", + "index": "src/menu/demo/src/index.html", + "main": "src/menu/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/menu/demo/tsconfig.app.json", + "assets": ["src/menu/demo/src/favicon.ico", "src/menu/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "menu-demo:build:production" + }, + "development": { + "browserTarget": "menu-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js menu" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/menu/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/menu/demo/tsconfig.spec.json", + "karmaConfig": "src/menu/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/menu/demo/src/app/AppComponent.ts b/src/menu/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/menu/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/menu/demo/src/app/AppModule.ts b/src/menu/demo/src/app/AppModule.ts new file mode 100644 index 0000000..52a29ae --- /dev/null +++ b/src/menu/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { MenuTestModule } from './menu/MenuTestModule'; + +@NgModule({ + imports: [ + MenuTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/menu/demo/src/app/IndexComponent.ts b/src/menu/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..63e610e --- /dev/null +++ b/src/menu/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { MenuTestModule } from './menu/MenuTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = MenuTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/menu/demo/src/app/app.html b/src/menu/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/menu/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/menu/demo/src/app/menu/MenuBasicComponent.ts b/src/menu/demo/src/app/menu/MenuBasicComponent.ts new file mode 100644 index 0000000..59e8a96 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuBasicComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-basic.html' +}) +export class MenuBasicComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuBeforeopenComponent.ts b/src/menu/demo/src/app/menu/MenuBeforeopenComponent.ts new file mode 100644 index 0000000..516926c --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuBeforeopenComponent.ts @@ -0,0 +1,46 @@ +import { Component } from '@angular/core'; +import { TiMenuComponent, TiMenuItem } from '@opentiny/ng'; +@Component({ + templateUrl: './menu-beforeopen.html' +}) +export class MenuBeforeopenComponent { + items: Array = []; + onBeforeOpen(menucomp: TiMenuComponent): void { + setTimeout(() => { + // 模拟一秒后才获取到菜单数据 + this.items = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; + menucomp.open(); + }, 1000); + } +} diff --git a/src/menu/demo/src/app/menu/MenuBorderComponent.ts b/src/menu/demo/src/app/menu/MenuBorderComponent.ts new file mode 100644 index 0000000..c778c1b --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuBorderComponent.ts @@ -0,0 +1,55 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; +@Component({ + templateUrl: './menu-border.html' +}) +export class MenuBorderComponent { + disabled: boolean = false; + panelAlign: string = 'left'; + options: Array = [ + { + label: '一级菜单', + tip: '远程登录', + tipPosition: 'right', + click(): void { + console.log(this.label + 'clicked'); + } + }, + { + label: '变更规格', + disabled: true + }, + { + label: '制作镜像', + tip: '远程登录', + tipPosition: 'left' + }, + { + label: '云服务器这是一个很长的选项只有云服务器处于关机状态才能执行该操作' + }, + { + label: '规格' + }, + { + label: '镜像', + disabled: true + } + ]; + + onSelect(item: TiMenuItem): void { + if (typeof item.click === 'function') { + item.click(); + + return; + } + console.log(item); + } + + changeDisabled(): void { + this.disabled = !this.disabled; + } + + changePanelAlign(): void { + this.panelAlign = 'right'; + } +} diff --git a/src/menu/demo/src/app/menu/MenuButtoncolorComponent.ts b/src/menu/demo/src/app/menu/MenuButtoncolorComponent.ts new file mode 100644 index 0000000..1261ef7 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuButtoncolorComponent.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; +@Component({ + templateUrl: './menu-buttoncolor.html' +}) +export class MenuButtoncolorComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuDefaultComponent.ts b/src/menu/demo/src/app/menu/MenuDefaultComponent.ts new file mode 100644 index 0000000..d09b764 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuDefaultComponent.ts @@ -0,0 +1,78 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-default.html' +}) +export class MenuDefaultComponent { + labelKey: string = 'title'; + panelMaxWidth: string = '200px'; + + options: Array = []; + options1: Array = this.options; + options2: Array = this.options; + constructor() { + const num: number = 5; + + for (let i: number = 1; i < num; i++) { + const option: TiMenuItem = { + label: '', + title: '', + disabled: false, + children: [] + }; + if (i === 1) { + option.label = '定制行。。。。。。。。。。。。。。。' + '。。。。。。。。。。。。。。。。。。。。。。。'; + option.title = '这是定制行:显示指定key'; + option.disabled = true; + } else { + option.label = `第${i}条item1`; + option.title = `第${i}条title1`; + option.disabled = false; + } + + option.children = []; + for (let j: number = 1; j < num; j++) { + const option1: TiMenuItem = { + label: '', + title: '', + disabled: false, + children: [] + }; + + if (j === 2) { + option1.label = '定制行。。。。。。。。。。。。'; + option1.title = '定制行。。。。。。。。。。。。'; + option1.disabled = false; + } else { + option1.label = `第${j}条item2`; + option1.title = `第${j}条title2`; + option1.disabled = true; + } + + option1.children = []; + for (let k: number = 1; k < num; k++) { + const option2: TiMenuItem = { + label: '', + title: '', + disabled: false, + groupId: '' + }; + option2.label = `第${k}条item3`; + option2.title = `第${k}条title3`; + option2.disabled = false; + option2.groupId = k < 2 ? 'a' : k > 3 ? 'b' : 'c'; + option1.children.push(option2); + } + + option.children.push(option1); + } + + this.options.push(option); + } + } + + onSelect(item: TiMenuItem): void { + console.log(item); + } +} diff --git a/src/menu/demo/src/app/menu/MenuDisabledComponent.ts b/src/menu/demo/src/app/menu/MenuDisabledComponent.ts new file mode 100644 index 0000000..86881c7 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuDisabledComponent.ts @@ -0,0 +1,42 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; +@Component({ + templateUrl: './menu-disabled.html' +}) +export class MenuDisabledComponent { + disabled: boolean = true; + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机', + disabled: true + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组', + disabled: true + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuEventComponent.ts b/src/menu/demo/src/app/menu/MenuEventComponent.ts new file mode 100644 index 0000000..2ef86ab --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuEventComponent.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-event.html' +}) +export class MenuEventComponent { + myLogs: Array = []; + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; + + onSelect(item: any): void { + this.myLogs = [...this.myLogs, `onSelect() event = ${JSON.stringify(item)}`]; + } +} diff --git a/src/menu/demo/src/app/menu/MenuGroupComponent.ts b/src/menu/demo/src/app/menu/MenuGroupComponent.ts new file mode 100644 index 0000000..918fcb2 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuGroupComponent.ts @@ -0,0 +1,42 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; +@Component({ + templateUrl: './menu-group.html' +}) +export class MenuGroupComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机', + groupId: 'action' + }, + { + id: 'powerOff', + label: '关机', + groupId: 'action' + }, + { + id: 'reboot', + label: '重启', + groupId: 'action' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuIdComponent.ts b/src/menu/demo/src/app/menu/MenuIdComponent.ts new file mode 100644 index 0000000..79b5a28 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuIdComponent.ts @@ -0,0 +1,76 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-id.html' +}) +export class MenuIdComponent { + panelMaxWidth: string = '200px'; + + options: Array = []; + options1: Array = this.options; + + constructor() { + const num: number = 5; + + for (let i: number = 1; i < num; i++) { + const option: TiMenuItem = { + label: '', + title: '', + disabled: false, + id: i, + children: [] + }; + if (i === 1) { + option.label = '定制行。。。。。。。。。。。。。。。' + '。。。。。。。。。。。。。。。。。。。。。。。'; + option.title = '这是定制行:显示指定key'; + option.disabled = true; + } else { + option.label = `第${i}条item1`; + option.title = `第${i}条title1`; + option.disabled = false; + } + + option.children = []; + for (let j: number = 1; j < num; j++) { + const option1: TiMenuItem = { + label: '', + title: '', + id: `${i}_${j}`, + disabled: false, + children: [] + }; + + if (j === 2) { + option1.label = '定制行。。。。。。。。。。。。'; + option1.title = '定制行。。。。。。。。。。。。'; + option1.disabled = false; + } else { + option1.label = `第${j}条item2`; + option1.title = `第${j}条title2`; + option1.disabled = true; + } + + option1.children = []; + for (let k: number = 1; k < num; k++) { + const option2: TiMenuItem = { + label: '', + title: '', + id: `${i}_${j}_${k}`, + disabled: false, + groupId: '' + }; + option2.label = `第${k}条item3`; + option2.title = `第${k}条title3`; + option2.disabled = false; + option2.groupId = k < 2 ? 'a' : k > 3 ? 'b' : 'c'; + option1.children.push(option2); + } + + option.children.push(option1); + } + + this.options.push(option); + } + } +} diff --git a/src/menu/demo/src/app/menu/MenuLabelkeyComponent.ts b/src/menu/demo/src/app/menu/MenuLabelkeyComponent.ts new file mode 100644 index 0000000..e330008 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuLabelkeyComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-labelkey.html' +}) +export class MenuLabelkeyComponent { + items: Array = [ + { + id: 'telnet', + title: '远程登录' + }, + { + id: 'powerOn', + title: '开机' + }, + { + id: 'powerOff', + title: '关机' + }, + { + id: 'reboot', + title: '重启' + }, + { + id: 'setting', + title: '网络设置', + children: [ + { + id: 'modify', + title: '更改安全组' + }, + { + id: 'toggle', + title: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuPanelalignComponent.ts b/src/menu/demo/src/app/menu/MenuPanelalignComponent.ts new file mode 100644 index 0000000..e5dc99d --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuPanelalignComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-panelalign.html' +}) +export class MenuPanelalignComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuPanelstyleComponent.ts b/src/menu/demo/src/app/menu/MenuPanelstyleComponent.ts new file mode 100644 index 0000000..8b45f0f --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuPanelstyleComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-panelstyle.html' +}) +export class MenuPanelstyleComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuTempleteComponent.ts b/src/menu/demo/src/app/menu/MenuTempleteComponent.ts new file mode 100644 index 0000000..4950a86 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuTempleteComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-templete.html' +}) +export class MenuTempleteComponent { + items: Array = [ + { + id: 'refresh', + label: '刷新', + iconName: 'refresh', + tipIconName: 'warn', + customTip: '确定要刷新吗?', + tipPosition: 'left' + }, + { + id: 'delete', + label: '删除', + iconName: 'delete-1', + tipIconName: 'warn', + customTip: '确定要删除吗?', + tipPosition: 'left' + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuTempleteTestComponent.ts b/src/menu/demo/src/app/menu/MenuTempleteTestComponent.ts new file mode 100644 index 0000000..13fd570 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuTempleteTestComponent.ts @@ -0,0 +1,74 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-templete-test.html' +}) +export class MenuTempleteTestComponent { + labelKey: string = 'title'; + panelMaxWidth: string = '200px'; + + options: Array = []; + options1: Array = this.options; + options2: Array = this.options; + constructor() { + const num: number = 5; + + for (let i: number = 1; i < num; i++) { + const option: TiMenuItem = { + label: '', + title: '', + disabled: false, + children: [] + }; + if (i === 1) { + option.label = '定制行。。。。。。。。。。。。。。。' + '。。。。。。。。。。。。。。。。。。。。。。。'; + option.title = '这是定制行:显示指定key'; + option.disabled = true; + } else { + option.label = `第${i}条item1`; + option.title = `第${i}条title1`; + option.disabled = false; + } + + option.children = []; + for (let j: number = 1; j < num; j++) { + const option1: TiMenuItem = { + label: '', + title: '', + disabled: false, + children: [] + }; + + if (j === 2) { + option1.label = '定制行。。。。。。。。。。。。'; + option1.title = '定制行。。。。。。。。。。。。'; + option1.disabled = false; + } else { + option1.label = `第${j}条item2`; + option1.title = `第${j}条title2`; + option1.disabled = true; + } + + option1.children = []; + for (let k: number = 1; k < num; k++) { + const option2: TiMenuItem = { + label: '', + title: '', + disabled: false, + groupId: '' + }; + option2.label = `第${k}条item3`; + option2.title = `第${k}条title3`; + option2.disabled = false; + option2.groupId = k < 2 ? 'a' : k > 3 ? 'b' : 'c'; + option1.children.push(option2); + } + + option.children.push(option1); + } + + this.options.push(option); + } + } +} diff --git a/src/menu/demo/src/app/menu/MenuTestModule.ts b/src/menu/demo/src/app/menu/MenuTestModule.ts new file mode 100644 index 0000000..1b61351 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuTestModule.ts @@ -0,0 +1,100 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { TiIconModule, TiMenuModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { MenuBorderComponent } from './MenuBorderComponent'; +import { MenuBasicComponent } from './MenuBasicComponent'; +import { MenuDisabledComponent } from './MenuDisabledComponent'; +import { MenuGroupComponent } from './MenuGroupComponent'; +import { MenuPanelalignComponent } from './MenuPanelalignComponent'; +import { MenuBeforeopenComponent } from './MenuBeforeopenComponent'; +import { MenuPanelstyleComponent } from './MenuPanelstyleComponent'; +import { MenuTipsComponent } from './MenuTipsComponent'; +import { MenuIdComponent } from './MenuIdComponent'; +import { MenuLabelkeyComponent } from './MenuLabelkeyComponent'; +import { MenuEventComponent } from './MenuEventComponent'; +import { MenuTempleteComponent } from './MenuTempleteComponent'; +import { MenuTempleteTestComponent } from './MenuTempleteTestComponent'; +import { MenuButtoncolorComponent } from './MenuButtoncolorComponent'; +import { MenuDefaultComponent } from './MenuDefaultComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiMenuModule, TiIconModule, DemoLogModule, RouterModule.forChild(MenuTestModule.ROUTES)], + declarations: [ + MenuBorderComponent, + MenuBasicComponent, + MenuDisabledComponent, + MenuGroupComponent, + MenuPanelalignComponent, + MenuBeforeopenComponent, + MenuPanelstyleComponent, + MenuIdComponent, + MenuTipsComponent, + MenuLabelkeyComponent, + MenuEventComponent, + MenuTempleteComponent, + MenuTempleteTestComponent, + MenuButtoncolorComponent, + MenuDefaultComponent + ] +}) +export class MenuTestModule { + static readonly LINKS: Array = [{ href: 'components/TiMenuComponent.html', label: 'Menu' }]; + static readonly ROUTES: Routes = [ + { + path: 'menu/menu-basic', + component: MenuBasicComponent + }, + { + path: 'menu/menu-labelkey', + component: MenuLabelkeyComponent + }, + { + path: 'menu/menu-disabled', + component: MenuDisabledComponent + }, + { + path: 'menu/menu-group', + component: MenuGroupComponent + }, + { + path: 'menu/menu-panelalign', + component: MenuPanelalignComponent + }, + { + path: 'menu/menu-beforeopen', + component: MenuBeforeopenComponent + }, + { + path: 'menu/menu-panelstyle', + component: MenuPanelstyleComponent + }, + { + path: 'menu/menu-tips', + component: MenuTipsComponent + }, + { + path: 'menu/menu-border', + component: MenuBorderComponent + }, + { + path: 'menu/menu-templete', + component: MenuTempleteComponent + }, + { + path: 'menu/menu-event', + component: MenuEventComponent + }, + { path: 'menu/menu-id', component: MenuIdComponent }, + { path: 'menu/menu-templete-test', component: MenuTempleteTestComponent }, + { path: 'menu/menu-buttoncolor', component: MenuButtoncolorComponent }, + { + path: 'menu/menu-default', + component: MenuDefaultComponent + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuTipsComponent.ts b/src/menu/demo/src/app/menu/MenuTipsComponent.ts new file mode 100644 index 0000000..4d43d2e --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuTipsComponent.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-tips.html' +}) +export class MenuTipsComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机', + disabled: true, + tip: '已冻结,不能执行此操作。请联系客服。', + tipPosition: 'left' + }, + { + id: 'reboot', + label: '重启', + tip: '重新启动服务器。', + tipPosition: 'right' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/menu-basic.html b/src/menu/demo/src/app/menu/menu-basic.html new file mode 100644 index 0000000..858c366 --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-basic.html @@ -0,0 +1,2 @@ +更多 +更多 diff --git a/src/menu/demo/src/app/menu/menu-beforeopen.html b/src/menu/demo/src/app/menu/menu-beforeopen.html new file mode 100644 index 0000000..2f4cedf --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-beforeopen.html @@ -0,0 +1 @@ +更多 diff --git a/src/menu/demo/src/app/menu/menu-border.html b/src/menu/demo/src/app/menu/menu-border.html new file mode 100644 index 0000000..2c0dc25 --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-border.html @@ -0,0 +1,10 @@ +

    描述

    +

    Menu菜单组件: 有边框的Menu组件(原buttonselect组件)展示,需添加hasborder属性

    +

    示例

    + +
    + 更多 +
    + + + diff --git a/src/menu/demo/src/app/menu/menu-buttoncolor.html b/src/menu/demo/src/app/menu/menu-buttoncolor.html new file mode 100644 index 0000000..129ddce --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-buttoncolor.html @@ -0,0 +1 @@ +更多 diff --git a/src/menu/demo/src/app/menu/menu-default.html b/src/menu/demo/src/app/menu/menu-default.html new file mode 100644 index 0000000..678ffd2 --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-default.html @@ -0,0 +1,38 @@ +

    描述

    +

    Menu菜单组件: 基础显示

    +

    导入

    +

    import {{ '{' }} TiMenuModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    + +

    1.默认显示label

    +
    + 导出 +
    + +

    2.labelKey:指定要显示文字的键值

    +
    + 导出 +
    + +

    3.自定义 item 模板(添加 #item 标签

    +
    + + 导出 + + + 自定义item模板 - index: {{i}} + item的label: {{item.label}} + + +
    + +

    4.兼容旧版模板测试(10.0.3 之前版本)

    +

    自定义 item 模板(未添加 #item 标签

    +
    + + 导出 + + + 自定义item模板 - index: {{i}} + item的label: {{item.label}} + + +
    diff --git a/src/menu/demo/src/app/menu/menu-disabled.html b/src/menu/demo/src/app/menu/menu-disabled.html new file mode 100644 index 0000000..e369b53 --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-disabled.html @@ -0,0 +1,3 @@ +更多 +更多 +更多 diff --git a/src/menu/demo/src/app/menu/menu-event.html b/src/menu/demo/src/app/menu/menu-event.html new file mode 100644 index 0000000..52c1414 --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-event.html @@ -0,0 +1,3 @@ +更多 + + diff --git a/src/menu/demo/src/app/menu/menu-group.html b/src/menu/demo/src/app/menu/menu-group.html new file mode 100644 index 0000000..05871e1 --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-group.html @@ -0,0 +1 @@ +更多 diff --git a/src/menu/demo/src/app/menu/menu-id.html b/src/menu/demo/src/app/menu/menu-id.html new file mode 100644 index 0000000..19629ef --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-id.html @@ -0,0 +1,13 @@ +

    描述

    +

    id属性和组件id配合使用,用于自定义每个菜单的id

    +

    示例

    + +

    1.默认:不设置id,设置item.id

    +
    + 更多 +
    + +

    2.设置id,设置item.id

    +
    + 更多 +
    diff --git a/src/menu/demo/src/app/menu/menu-labelkey.html b/src/menu/demo/src/app/menu/menu-labelkey.html new file mode 100644 index 0000000..b13a8c3 --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-labelkey.html @@ -0,0 +1 @@ +更多 diff --git a/src/menu/demo/src/app/menu/menu-panelalign.html b/src/menu/demo/src/app/menu/menu-panelalign.html new file mode 100644 index 0000000..7aa871d --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-panelalign.html @@ -0,0 +1 @@ +更多 diff --git a/src/menu/demo/src/app/menu/menu-panelstyle.html b/src/menu/demo/src/app/menu/menu-panelstyle.html new file mode 100644 index 0000000..1b7430b --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-panelstyle.html @@ -0,0 +1 @@ +更多 diff --git a/src/menu/demo/src/app/menu/menu-templete-test.html b/src/menu/demo/src/app/menu/menu-templete-test.html new file mode 100644 index 0000000..5e4d8de --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-templete-test.html @@ -0,0 +1,14 @@ +

    描述

    +

    自定义模板测试

    +

    示例

    +

    1.兼容旧版模板测试(10.0.3 之前版本)

    +

    自定义 item 模板(未添加 #item 标签

    +
    + + 更多 + + + 自定义item模板 - index: {{i}} + item的label: {{item.label}} + + +
    diff --git a/src/menu/demo/src/app/menu/menu-templete.html b/src/menu/demo/src/app/menu/menu-templete.html new file mode 100644 index 0000000..aea783c --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-templete.html @@ -0,0 +1,11 @@ + + 更多 + + + {{item.label}} + + + + {{item.customTip}} + + diff --git a/src/menu/demo/src/app/menu/menu-tips.html b/src/menu/demo/src/app/menu/menu-tips.html new file mode 100644 index 0000000..441185f --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-tips.html @@ -0,0 +1 @@ +更多 diff --git a/src/menu/demo/src/app/menu/webdoc/menu-demos.js b/src/menu/demo/src/app/menu/webdoc/menu-demos.js new file mode 100644 index 0000000..736ccde --- /dev/null +++ b/src/menu/demo/src/app/menu/webdoc/menu-demos.js @@ -0,0 +1,166 @@ +export default { + column: '2', + demos: [ + { + demoId: 'menu-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Menu 组件的基本用法。', + 'en-US': '

    basic

    ', + }, + apis: [ + 'TiMenuComponent.properties.items', + 'TiMenuItem.properties.children', + 'TiMenuItem.properties.id', + 'TiMenuItem.properties.label', + ], + }, + { + demoId: 'menu-buttoncolor', + name: { + 'zh-CN': '按钮颜色', + 'en-US': '', + }, + desc: { + 'zh-CN': + '

    通过属性color配置按钮下拉类型菜单的按钮颜色,包括defaultdanger。', + 'en-US': '', + }, + apis: ['TiMenuComponent.properties.color'], + }, + { + demoId: 'menu-labelkey', + name: { + 'zh-CN': '显示文本', + 'en-US': 'labelkey', + }, + desc: { + 'zh-CN': '通过属性labelKey配置要显示文本的键值。', + 'en-US': '

    labelKey

    ', + }, + apis: ['TiMenuComponent.properties.labelKey'], + }, + { + demoId: 'menu-group', + name: { + 'zh-CN': '分组', + 'en-US': 'groupId', + }, + desc: { + 'zh-CN': '通过item.groupId配置下拉菜单项分组。', + 'en-US': '

    groupId

    ', + }, + apis: ['TiMenuItem.properties.groupId'], + }, + { + demoId: 'menu-panelalign', + name: { + 'zh-CN': '下拉面板对齐方向', + 'en-US': 'panelAlign', + }, + desc: { + 'zh-CN': + '通过属性panelAlign配置下拉面板对齐方向,包括leftright。', + 'en-US': '

    panelAlign

    ', + }, + apis: ['TiMenuComponent.properties.panelAlign'], + }, + { + demoId: 'menu-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled', + }, + desc: { + 'zh-CN': + '

    通过属性disabled配置是否为禁用状态;通过属性item.disabled配置当前下拉选项是否为禁用状态。', + 'en-US': '

    disabled

    ', + }, + apis: [ + 'TiMenuComponent.properties.disabled', + 'TiMenuItem.properties.disabled', + ], + }, + { + demoId: 'menu-tips', + name: { + 'zh-CN': '提示信息', + 'en-US': 'menu tips', + }, + desc: { + 'zh-CN': + '通过属性item.tip配置当前菜单项提示信息;通过属性item.tipPosition配置当前菜单项提示信息弹出方向;通过属性tipMaxWidth配置各菜单项提示信息最大宽度。', + 'en-US': '

    menu tips description

    ', + }, + apis: [ + 'TiMenuItem.properties.tip', + 'TiMenuItem.properties.tipPosition', + 'TiMenuComponent.properties.tipMaxWidth', + ], + }, + { + demoId: 'menu-panelstyle', + name: { + 'zh-CN': '下拉面板样式', + 'en-US': 'menu panelstyle', + }, + desc: { + 'zh-CN': + '通过属性panelMaxWidth配置下拉面板最大宽度;通过属性panelMaxHeight配置下拉面板最大高度。', + 'en-US': '

    menu panelstyle description

    ', + }, + apis: [ + 'TiMenuComponent.properties.panelMaxWidth', + 'TiMenuComponent.properties.panelMaxHeight', + ], + }, + { + demoId: 'menu-beforeopen', + name: { + 'zh-CN': '懒加载', + 'en-US': 'lazy', + }, + desc: { + 'zh-CN': + '当下拉面板展开前触发beforeOpen事件,传递出去的参数为组件实例(TiMenuComponent)。', + 'en-US': '

    lazy

    ', + }, + apis: [ + 'TiMenuComponent.events.beforeOpen', + 'TiMenuComponent.methods.open', + ], + }, + { + demoId: 'menu-templete', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'custom template', + }, + desc: { + 'zh-CN': + '通过#item配置下拉面板中选项的模板;通过#tip配置当前菜单项提示信息的模板。', + 'en-US': '

    custom template description

    ', + }, + apis: [ + 'TiMenuComponent.slots.itemTemplate', + 'TiMenuComponent.slots.tipTemplate', + ], + }, + { + demoId: 'menu-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event', + }, + desc: { + 'zh-CN': + '当选中菜单项时触发select事件,传递出去的参数为当前选中项的数据。', + 'en-US': '

    event

    ', + }, + apis: ['TiMenuComponent.events.select'], + }, + ], +}; diff --git a/src/menu/demo/src/app/menu/webdoc/menu.cn.md b/src/menu/demo/src/app/menu/webdoc/menu.cn.md new file mode 100644 index 0000000..4a1efd8 --- /dev/null +++ b/src/menu/demo/src/app/menu/webdoc/menu.cn.md @@ -0,0 +1,24 @@ +--- +title: Menu 下拉菜单 +--- +# Menu 下拉菜单 + +
    + +提供下拉菜单列表、按钮类型下拉菜单列表的组件。   + +```typescript +import { TiMenuModule } from '@opentiny/ng'; +``` + +
    + +
    + +提供下拉菜单列表、按钮类型下拉菜单列表的组件。   + +```typescript +import { TiMenuModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/menu/demo/src/app/menu/webdoc/menu.en.md b/src/menu/demo/src/app/menu/webdoc/menu.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/menu/demo/src/app/menu/webdoc/menu.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/menu/demo/src/favicon.ico b/src/menu/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/menu/demo/src/index.html b/src/menu/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/menu/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/menu/demo/src/main.ts b/src/menu/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/menu/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/menu/demo/test.ts b/src/menu/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/menu/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/menu/demo/tsconfig.app.json b/src/menu/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/menu/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/menu/demo/tsconfig.spec.json b/src/menu/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/menu/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/menu/lib/index.ts b/src/menu/lib/index.ts new file mode 100644 index 0000000..7643404 --- /dev/null +++ b/src/menu/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiMenuModule'; diff --git a/src/menu/lib/ng-package.json b/src/menu/lib/ng-package.json new file mode 100644 index 0000000..448e7b0 --- /dev/null +++ b/src/menu/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/menu", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/menu/lib/package.json b/src/menu/lib/package.json new file mode 100644 index 0000000..a8cbb39 --- /dev/null +++ b/src/menu/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-menu", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-list": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/menu/lib/project.json b/src/menu/lib/project.json new file mode 100644 index 0000000..c45abc4 --- /dev/null +++ b/src/menu/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/menu/lib", + "sourceRoot": "src/menu/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/menu"], + "options": { + "project": "src/menu/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/menu"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js menu" + }, + { + "command": "ng default-build menu" + }, + { + "command": "node build/clear-default-theme.js menu" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/menu && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build menu && ng pack menu && node build/publish.js menu --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/menu/lib/src/TiMenuComponent.ts b/src/menu/lib/src/TiMenuComponent.ts new file mode 100644 index 0000000..6c4e47c --- /dev/null +++ b/src/menu/lib/src/TiMenuComponent.ts @@ -0,0 +1,395 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ContentChild, + ElementRef, + EventEmitter, + Input, + Output, + QueryList, + SimpleChanges, + TemplateRef, + ViewChild, + ViewChildren, + ChangeDetectionStrategy +} from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiDropComponent } from '@opentiny/ng-drop'; +import { TiMenuListComponent } from './TiMenuListComponent'; +import { TiKeymap } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; +import { TiMenuItem } from './TiMenuItem'; +// TODO: beforOpen延时打开,需要动态转圈等待 + +/** + * Menu菜单组件 + * + * 提供了一种方便的生成菜单下拉列表的方式,提供分组、自定义内容等功能。 + * + */ +@Component({ + selector: 'ti-menu', + templateUrl: './menu.html', + styleUrls: ['./menu.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '(blur)': 'onBlur()', + '[class.ti-menu-danger-button]': 'color === "danger"' + } +}) +export class TiMenuComponent extends TiFormComponent { + // 本应继承自BaseComp,但是想要A标签焦点功能 + /** + * 下拉面板与按钮对齐方式 + */ + @Input() panelAlign: 'left' | 'right' = 'right'; + /** + * 必选,下拉面板数据集 + */ + @Input() items: Array; + /** + * 下拉面板最大宽度,超过时内容换行显示 + */ + @Input() panelMaxWidth: string = '9999px'; + /** + * 下拉面板最大高度,超过时面板出现竖向滚动条 + */ + @Input() panelMaxHeight: string = '9999px'; + /** + * 下拉面板要展示的键值 + */ + @Input() labelKey: string = 'label'; + /** + * 按钮颜色(配置 buttonselect 属性时生效) + */ + @Input() color: 'default' | 'danger' = 'default'; + /** + * tip 最大宽度 + */ + @Input() tipMaxWidth: string; + /** + * @ignore + */ + @ViewChild('mytoggle', { static: true }) mytoggleRef: ElementRef; + /** + * @ignore + */ + @ViewChildren(TiDropComponent) dropComs: QueryList; + /** + * @ignore + */ + @ViewChildren(TiMenuListComponent) listComs: QueryList; + /** + * @ignore + */ + @ContentChild(TemplateRef, { static: true }) firstTemplate: TemplateRef; + /** + * 下拉选项区域的模板 + */ + @ContentChild('item', { static: true }) itemTemplate: TemplateRef; + /** + * tip 提示区域的模板 + */ + @ContentChild('tip', { static: false }) tipTemplate: TemplateRef; + /** + * 选中菜单项时触发的回调,参数:当前选中项数据 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + /** + * 打开面板前触发的回调,一般用于懒加载场景,参数:组件实例 TiMenuComponent + */ + @Output() readonly beforeOpen: EventEmitter = new EventEmitter(); + /** + * @ignore + * itemsArr[0]存放根面板内容,itemArr[1]存放次级面板内容,itemArr[2]存放次次级面板内容 + */ + public itemsArr: Array> = []; + /** + * @ignore + * 与开关距离 + */ + public dominatorSpace: string = '10px'; + /** + * @ignore + * 设置该属性时为buttonselect组件 + */ + public buttonSelect: boolean; + /** + * menulist最大高度 + */ + public menulistMaxHeight: number; + protected versionInfo: string = super.getVersion(packageInfo); + ngOnInit(): void { + // 基类中做了设置宿主id的操作 + super.ngOnInit(); + // 10.1.17 版本之前在标签上写 hasborder 属性会呈现按钮下拉样式,10.1.17 版本之后修改为 buttonselect 属性,也兼容之前的写法 + this.buttonSelect = this.nativeElement.hasAttribute('hasborder') || this.nativeElement.hasAttribute('buttonselect'); + } + ngOnChanges(changes: SimpleChanges): void { + this.setFocusableElems([this.mytoggleRef.nativeElement]); + super.ngOnChanges(changes); + if (changes['items']) { + // 外部修改show属性的处理,初始定义在ngOnInit中处理 + this.itemsArr[0] = this.items; + } + } + /** + * 兼容旧版: + * 10.0.3 版本之前只能内嵌一个模板,无命名。 + * 新版可以内嵌两个模板,示例书写要求都命名(#item,#tip)。 + * 但需要兼容旧版无命名测试用例。 + */ + ngAfterContentInit(): void { + super.ngAfterContentInit(); + // 如果 item 模板为空 && 存在第一个模板,那么把第一个出现的 “非#tip 标签” 的模板作为 item 模板 + if ( + !this.itemTemplate && + this.firstTemplate && + this.firstTemplate.elementRef.nativeElement !== (this.tipTemplate && this.tipTemplate.elementRef.nativeElement) + ) { + this.itemTemplate = this.firstTemplate; + } + } + /** + * 打开面板 + */ + public open(): void { + if (this.disabled) { + return; + } + // 打开面板前重置menulist最大高度 + this.initListMaxHeight(); + this.dropComs.first.show(); + // 使用beforeOpen事件:异步加载数据,调用open事件,需要手动触发变更检测 + if (this.beforeOpen.observers.length > 0) { + this.changeDetectorRef.markForCheck(); + } + + this.listComs.first.hoverOption = null; + // 根据drop的最大高度设置menulist最大高度 + this.restyleListMaxHeight(); + // IE滚动条Bug的监听 + this.listComs.toArray().forEach((listCom) => { + listCom.listenIESrollbarBug(); + }); + } + /** + * 关闭面板 + */ + public close(): void { + this.dropComs.first.hide(); + this.onHover(null, 0); + // 解除IE滚动条Bug的监听 + this.listComs.toArray().forEach((listCom) => { + listCom.unlistenIESrollbarBug(); + }); + } + /** + * @ignore + * + * 切换面板状态:打开/关闭 + */ + public toggle(): void { + this.dropComs.first.isShow ? this.close() : this.open(); + } + /** + * @ignore + */ + public onKeydown(event: KeyboardEvent): void { + switch (event.keyCode) { + case TiKeymap.KEY_SPACE: // 原生SPACE键仅可打开。但Tiny2,3都跟Enter键保持一致 + case TiKeymap.KEY_ENTER: // ENTER键 相当于click + case TiKeymap.KEY_NUMPAD_ENTER: // ENTER键(苹果数字小键盘) + // 第一级已打开,但无选中项。或者第一级没有打开。相当于鼠标点击逻辑。 + if ((this.dropComs.first.isShow && !this.listComs.toArray()[0].hoverOption) || !this.dropComs.first.isShow) { + this.onMousedown(null); // 相当于鼠标点击 + break; // 注意:break在if内,如果if不满足,则走到default,list处理按键 + } + // 上面的回车键, 可能继续走到下面分支。 + // eslint-disable-next-line no-fallthrough + case TiKeymap.KEY_ARROW_LEFT: // 左右箭头,焦点在左右面板转移 + case TiKeymap.KEY_ARROW_RIGHT: // 左右箭头,焦点在左右面板转移 + case TiKeymap.KEY_ARROW_UP: // 上下箭头,上下移动选中项 + case TiKeymap.KEY_ARROW_DOWN: // 上下箭头,上下移动选中项 + for (let i = this.dropComs.length - 1; i >= 0; i--) { + // 寻找末尾面板且有hover,list处理按键 + if (this.dropComs.toArray()[i].isShow && this.listComs.toArray()[i].hoverOption) { + this.onKeydownLastHoverList(event, i); + + // break; //这里触发了for循环的break;并没有走到switch的break;所以改为return; + return; + } + } + // 没有hover的面板,那么第一级面板(肯定是展开的)处理按键 + this.listComs.first.onKeydown(event); + break; + default: + break; + } + } + private onKeydownLastHoverList(event: KeyboardEvent, i: number): void { + switch (event.keyCode) { + case TiKeymap.KEY_ARROW_LEFT: // 左右箭头,焦点在左右面板转移 + case TiKeymap.KEY_ARROW_RIGHT: // 左右箭头,焦点在左右面板转移 + if ( + (this.panelAlign === 'right' && event.keyCode === TiKeymap.KEY_ARROW_LEFT) || + (this.panelAlign === 'left' && event.keyCode === TiKeymap.KEY_ARROW_RIGHT) + ) { + // 次级面板方向键 + if (i + 1 < this.dropComs.length && this.dropComs.toArray()[i + 1].isShow) { + // 次级存在且展开 + this.listComs.toArray()[i + 1].nextOption(false); // 次级面板hover由null改为下一个 + } + } else { + // 上级面板方向键 + this.listComs.toArray()[i].hoverOption = null; + } + break; + case TiKeymap.KEY_SPACE: // 原生SPACE键仅可打开。但Tiny2,3都跟Enter键保持一致 + case TiKeymap.KEY_ENTER: // ENTER键 相当于click + case TiKeymap.KEY_NUMPAD_ENTER: // ENTER键(苹果数字小键盘) + if (!this.listComs.toArray()[i].isGroup(this.listComs.toArray()[i].hoverOption)) { + this.listComs.toArray()[i].onKeydown(event); + } + break; + default: // 上下键等,和上面的回车键也会走到这里。 + this.listComs.toArray()[i].onKeydown(event); + break; + } + } + /** + * @ignore + */ + public onMousedown(event: MouseEvent): void { + if (this.dropComs.first.isShow) { + // 已打开,则关闭 + this.close(); + } else if (this.beforeOpen.observers.length === 0) { + // 下拉菜单收起,且无beforeOpen,则打开 + this.open(); + } else { + // 下拉菜单收起,有beforeOpen + this.beforeOpen.emit(this); + } + } + /** + * @ignore + */ + public onSelect(panelIndex: number): void { + this.select.emit(this.listComs.toArray()[panelIndex].model); + this.close(); + } + /** + * @ignore + */ + public onBlur(): void { + this.close(); + } + /** + * @ignore + */ + public onHover(item: any, panelIndex: number): void { + // 当item为空,表示鼠标移出当前面板,也需要关闭次级面板。 + if (panelIndex + 1 === this.dropComs.length) { + // 当前hover面板,已经是最后一级面板 + return; + } + const dropComArr: Array = this.dropComs.toArray(); + const listComArr: Array = this.listComs.toArray(); + if (item && listComArr[panelIndex].isGroup(item) && !listComArr[panelIndex].isDisabledFn(item)) { + // 根据数据,是否展开下一级 + this.itemsArr[panelIndex + 1] = item.children; // 下一级数据,赋新值。 + listComArr[panelIndex + 1].hoverOption = null; // 下一级面板赋空hoverOption,会触发onHover,进而关闭下一级面板的后续面板。 + // 赋新值以后,等模板有了新的尺寸,再弹出。 + setTimeout(() => { + const currentPanel: HTMLElement = dropComArr[panelIndex].nativeElement; // 当前面板元素 + const nextPanel: HTMLElement = dropComArr[panelIndex + 1].nativeElement; // 下一级面板元素 + + dropComArr[panelIndex + 1].show(); // 打开下一级面板 + // 位置调整,X方向 // TODO: 使用Position是否可行? + const leftOffset: number = + this.panelAlign === 'left' ? currentPanel.offsetLeft + currentPanel.offsetWidth : currentPanel.offsetLeft - nextPanel.offsetWidth; + nextPanel.style.left = `${leftOffset}px`; + // 位置调整,Y方向 + const itemIndex: number = this.itemsArr[panelIndex].indexOf(item); + const itemElem: any = listComArr[panelIndex].nativeElement.getElementsByTagName('LI')[itemIndex]; + // 当前li距离面板顶部的距离 = 当前li距离可视区顶部的距离 - 当前面板到可视区顶部的距离 + const itemElmTop: number = itemElem.getBoundingClientRect().top - currentPanel.getBoundingClientRect().top; + // 当前li距离页面底部的距离 = 可视区窗口高度 + 文档滚动高度 - 当前面板的offsetTop - 当前li距离面板顶部的距离 + const bottomOffset: number = + document.documentElement.clientHeight + document.documentElement.scrollTop - currentPanel.offsetTop - itemElmTop; + // 下一级面板的的高度 + const itemHeight: number = nextPanel.offsetHeight; + // 底部空间能放下下一级面板:下一级面板的top = 当前面板的offsetTop + 当前li距离面板顶部的距离 - 面板的上下留白 + const nextPanelTop: number = currentPanel.offsetTop + itemElmTop - 4; + if (bottomOffset > itemHeight) { + nextPanel.style.top = nextPanelTop + 'px'; + + return; + } + + // 底部空间不足,和浏览器底部对齐 + nextPanel.style.top = nextPanelTop + bottomOffset - itemHeight + 'px'; + }, 0); + } else { + // 当hover到具体项目(非分组)或 null(移出当前面板),需要闭合所有后续面板 + for (let i: number = panelIndex + 1; i < dropComArr.length; i++) { + dropComArr[i].hide(); + } + } + } + /** + * @ignore + */ + public onMouseoutDrop(event: MouseEvent, panelIndex: number): void { + // 默认需要隐藏, 除非鼠标进入面板区域。 + for (let i: number = 0; i < this.dropComs.length; i++) { + const dropRect: DOMRect = this.dropComs.toArray()[i].nativeElement.getBoundingClientRect(); + // 因为慢慢移出本面板时,移出鼠标事件依然停留在本面板内(1px误差),所以面板rect四周范围要更小1px。 + if ( + dropRect.left < event.clientX && + event.clientX < dropRect.right && + dropRect.top < event.clientY && + event.clientY < dropRect.bottom + ) { + return; + } + } + // 给第一级面板置无选中,那么次级面板自然会关闭。 + this.listComs.first.hoverOption = null; // 会触发到上面this.onHover(null, 0) + } + /** + * @ignore + * + * 初始化menulist最大高度 + */ + public initListMaxHeight(): void { + this.menulistMaxHeight = parseInt(this.panelMaxHeight, 10); + this.listComs.toArray().forEach((listCom) => { + this.renderer.setStyle(listCom.nativeElement, 'max-height', this.menulistMaxHeight + 'px'); + }); + } + /** + * @ignore + * + * 考虑drop的压缩情况,设置menulist的max-height + */ + public restyleListMaxHeight(): void { + const dropCurMaxHeight: number = parseInt(this.dropComs.first.nativeElement.style.maxHeight, 10); + if (!isNaN(dropCurMaxHeight) && dropCurMaxHeight < this.menulistMaxHeight) { + this.listComs.toArray().forEach((listCom) => { + this.renderer.setStyle(listCom.nativeElement, 'max-height', dropCurMaxHeight + 'px'); + }); + } + } +} diff --git a/src/menu/lib/src/TiMenuItem.ts b/src/menu/lib/src/TiMenuItem.ts new file mode 100644 index 0000000..a3e08f0 --- /dev/null +++ b/src/menu/lib/src/TiMenuItem.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiPositionType } from '@opentiny/ng-utils'; +/** + * MenuItem是item项的数据接口 + */ +export interface TiMenuItem { + /** + * 菜单项文本 + */ + label?: string; + /** + * 菜单项 tip 提示内容 + */ + tip?: string; + /** + * 菜单项 tip 提示方向,默认值为‘right’ + */ + tipPosition?: TiPositionType; + /** + * 菜单项是否禁用 + */ + disabled?: boolean; + /** + * 菜单项的子菜单项,最多支持 3 级菜单 + */ + children?: Array; + /** + * 分组 id,当 id 不同时在当前菜单项上方增加分割线 + */ + groupId?: string; + /** + * 菜单项 id + */ + id?: any; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} diff --git a/src/menu/lib/src/TiMenuListComponent.ts b/src/menu/lib/src/TiMenuListComponent.ts new file mode 100644 index 0000000..8347fae --- /dev/null +++ b/src/menu/lib/src/TiMenuListComponent.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TiListComponent } from '@opentiny/ng-list'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiMenuItem } from './TiMenuItem'; +import packageInfo from '../package.json'; +/** + * @ignore + */ +@Component({ + selector: 'ti-menulist', + templateUrl: './menulist.html', + styleUrls: ['./menulist.less' /* , './menu.less' */], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiMenuListComponent)] +}) +export class TiMenuListComponent extends TiListComponent { + @Input() panelAlign: 'left' | 'right' = 'right'; // 设置下拉菜单面板与按钮对齐方式。这里是为了决定了面板上箭头方向。 + protected versionInfo: string = super.getVersion(packageInfo); + protected isSelectable(item: TiMenuItem): boolean { + // 与普通list区别:group也是可以选中的 + return !this.isDisabledFn(item); + } + protected getListOptions(): Array { + // 与普通list区别:次级节点不会放入本级别。 + return this.options; + } + public hasBorder(item: TiMenuItem, items: Array): boolean { + // 分组带线 + const index: number = items.indexOf(item); + const indexPre: number = index === 0 ? 0 : index - 1; + + return item.groupId !== items[indexPre].groupId; + } + public onMouseenterItem(event: MouseEvent, option: any): void { + // 不是在选中项置Top时鼠标经过。 + if (new Date().getTime() - this.optionScrollTopLastTime > TiListComponent.SCROLL_TOP_TIME) { + this.hoverOption = option; // 更新hover + } + } + // 防止继承list的ngOnChanges,初始化hoverOption变更触发hover事件,导致menu组件中的onhover事件报错 + ngOnChanges(): void {} +} diff --git a/src/menu/lib/src/TiMenuModule.ts b/src/menu/lib/src/TiMenuModule.ts new file mode 100644 index 0000000..c8fcff5 --- /dev/null +++ b/src/menu/lib/src/TiMenuModule.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiMenuComponent } from './TiMenuComponent'; +import { TiMenuListComponent } from './TiMenuListComponent'; + +@NgModule({ + imports: [CommonModule, TiDropModule, TiTipModule], + exports: [TiMenuComponent, TiMenuListComponent], + declarations: [TiMenuComponent, TiMenuListComponent] +}) +export class TiMenuModule {} +export { TiMenuComponent } from './TiMenuComponent'; +export { TiMenuListComponent } from './TiMenuListComponent'; +export { TiMenuItem } from './TiMenuItem'; diff --git a/src/menu/lib/src/menu.html b/src/menu/lib/src/menu.html new file mode 100644 index 0000000..cd51ca7 --- /dev/null +++ b/src/menu/lib/src/menu.html @@ -0,0 +1,56 @@ + + + + + + + + + + +
    + + + + {{item[labelKey]}} +
    +
    +
    +
    +
    diff --git a/src/menu/lib/src/menu.less b/src/menu/lib/src/menu.less new file mode 100644 index 0000000..8ac94fc --- /dev/null +++ b/src/menu/lib/src/menu.less @@ -0,0 +1,115 @@ +@import '../../../themes/basic/base-all.less'; +@import '../../../themes/basic/compnent-container-border.less'; +@import '../../../button/lib/src/button.less'; + +:host { + --ti-menu-triangle-width: var(--ti-common-size-2x); + --ti-menu-container-height: var(--ti-common-size-7x); +} + +:host { + display: inline-block; +} + +.ti3-menu-toggle-menu { + text-decoration: none; + display: inline-block; + color: var(--ti-common-color-text-link); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + cursor: pointer; + .user-select(); + &:focus { + color: var(--ti-common-color-text-link); + } + + &:hover { + color: var(--ti-common-color-text-link-hover); + + &:after { + border-top-color: var(--ti-common-color-text-link-hover); + } + } + + &:active { + color: var(--ti-common-color-text-link-hover); + &:after { + border-top-color: var(--ti-common-color-text-link-hover); + } + } + + &:after { + .triangle-down(var(--ti-menu-triangle-width), calc(var(--ti-menu-triangle-width) / 1.6), var(--ti-common-color-text-link)); + content: ''; + margin-left: var(--ti-common-space-base); + display: inline-block; + position: relative; + top: -1px; + } + + &.ti3-menu-toggle-menu-up:after { + .rotate(180deg); + } +} + +.ti3-menu-panel-container { + border-radius: var(--ti-common-border-radius-normal); + background-color: var(--ti-common-color-bg-white-normal); + box-shadow: var(--ti-common-shadow-2-down); //增加阴影尺寸 兼容IE下横向偏移量为 0 时,阴影半径无渲染的情况 + position: absolute; + list-style: none; + z-index: 1060; + display: none; + .box-sizing(border-box); +} + +:host[hasborder], +:host[buttonselect] { + &:extend(.ti3-compnent-container-border all); + & .ti3-menu-toggle-menu { + padding-left: var(--ti-common-space-5x); + padding-right: var(--ti-common-space-4x); + height: var(--ti-menu-container-height); + line-height: var(--ti-menu-container-height); + color: var(--ti-common-color-text-primary); + margin: -1px; + &:hover, + &:focus { + color: var(--ti-common-color-text-primary); + } + &:after, + &:hover:after { + border-top-color: var(--ti-common-color-icon-normal); + } + } + &:hover:not([disabled]) { + border-color: var(--ti-common-color-line-active); + } +} + +// danger按钮样式 +:host[buttonselect].ti-menu-danger-button { + & .ti3-menu-toggle-menu { + &:extend(.ti3-btn-danger all); + border-radius: var(--ti-common-border-radius-normal); + &:after, + &:hover:after { + border-top-color: var(--ti-common-color-icon-white); + } + } +} + +:host[disabled] .ti3-menu-toggle-menu { + cursor: not-allowed; + color: var(--ti-common-color-text-disabled) !important; + &:after { + border-top-color: var(--ti-common-color-icon-disabled) !important; + } +} + +.ti3-menu-border-drop { + & ti-menulist { + max-height: inherit; //继承最大高度 + overflow-y: auto; //超过最大高度时产生竖向滚动条 + } +} diff --git a/src/menu/lib/src/menulist.html b/src/menu/lib/src/menulist.html new file mode 100644 index 0000000..52d9a57 --- /dev/null +++ b/src/menu/lib/src/menulist.html @@ -0,0 +1,39 @@ +
      + + + +
    + + +
    +
    + + + +
  • +
    + + + + {{item[labelKey]}} +
  • +
    diff --git a/src/menu/lib/src/menulist.less b/src/menu/lib/src/menulist.less new file mode 100644 index 0000000..feb11eb --- /dev/null +++ b/src/menu/lib/src/menulist.less @@ -0,0 +1,103 @@ +@import '../../../list/lib/src/list.less'; + +:host { + --ti-menulist-triangle-width: var(--ti-common-size-2x); + --ti-menulist-triangle-margin-left: 3px; +} + +.ti3-menu-groupId { + border-top: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); +} + +.ti3-menu-panel-list { + text-decoration: none; + display: block; + position: relative; + padding: var(--ti-common-space-6) var(--ti-common-space-5x); + clear: both; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-primary); + background-color: var(--ti-common-color-bg-white-normal); + white-space: inherit; + cursor: pointer; + .user-select(); + .box-sizing(border-box); + + &.ti3-menu-panel-list-active { + color: var(--ti-common-color-text-white); + background: var(--ti-common-color-bg-emphasize); + + > .ti3-menu-list-left-icon { + border-bottom-color: var(--ti-common-color-text-white); + } + + > .ti3-menu-list-right-icon { + border-bottom-color: var(--ti-common-color-text-white); + } + } + + &.ti3-menu-panel-list-disabled { + cursor: not-allowed; + background-color: var(--ti-common-color-bg-white-normal); + + ::ng-deep * { + color: var(--ti-common-color-text-disabled) !important; + } + + > .ti3-menu-list-left-icon { + border-bottom-color: var(--ti-common-color-text-disabled); + } + + > .ti3-menu-list-right-icon { + border-bottom-color: var(--ti-common-color-text-disabled); + } + } + + &.ti3-menu-panel-list-selected-item { + &:extend(.ti3-menu-panel-list-active all); + } +} + +.ti3-menu-panel-list-hover { + color: var(--ti-common-color-text-highlight); + background: var(--ti-common-color-bg-white-emphasize); + + > .ti3-menu-list-left-icon { + border-bottom-color: var(--ti-common-color-text-highlight); + } + + > .ti3-menu-list-right-icon { + border-bottom-color: var(--ti-common-color-text-highlight); + } +} + +.ti3-menu-list-icon { + content: ''; + display: inline-block; + position: absolute; + top: 50%; + .triangle-up(var(--ti-menulist-triangle-width), calc(var(--ti-menulist-triangle-width) / 1.6), var(--ti-common-color-icon-normal)); + margin-left: var(--ti-menulist-triangle-margin-left); + vertical-align: middle; +} + +.ti3-menu-list-left-icon { + left: 5px; + transform: translateY(-50%) rotate(270deg); +} + +.ti3-menu-list-right-icon { + right: 5px; + transform: translateY(-50%) rotate(90deg); +} + +.ti3-menu-panel-list-tooltip { + position: absolute; + top: 0px; + left: 0px; + display: inline-block; + width: 100%; + height: 100%; +} diff --git a/src/message/demo/karma.conf.js b/src/message/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/message/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/message/demo/project.json b/src/message/demo/project.json new file mode 100644 index 0000000..124abf2 --- /dev/null +++ b/src/message/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/message/demo", + "sourceRoot": "src/message/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/message", + "index": "src/message/demo/src/index.html", + "main": "src/message/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/message/demo/tsconfig.app.json", + "assets": ["src/message/demo/src/favicon.ico", "src/message/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "message-demo:build:production" + }, + "development": { + "browserTarget": "message-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js message" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/message/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/message/demo/tsconfig.spec.json", + "karmaConfig": "src/message/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/message/demo/src/app/AppComponent.ts b/src/message/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/message/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/message/demo/src/app/AppModule.ts b/src/message/demo/src/app/AppModule.ts new file mode 100644 index 0000000..ee9512d --- /dev/null +++ b/src/message/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { MessageTestModule } from './message/MessageTestModule'; + +@NgModule({ + imports: [ + MessageTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/message/demo/src/app/IndexComponent.ts b/src/message/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..9a362f8 --- /dev/null +++ b/src/message/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { MessageTestModule } from './message/MessageTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = MessageTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/message/demo/src/app/app.html b/src/message/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/message/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/message/demo/src/app/message/MessageBasicComponent.ts b/src/message/demo/src/app/message/MessageBasicComponent.ts new file mode 100644 index 0000000..0ce6035 --- /dev/null +++ b/src/message/demo/src/app/message/MessageBasicComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { TiMessageService, TiModalRef } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-basic.html' +}) +export class MessageBasicComponent { + constructor(private tiMessage: TiMessageService) {} + showMsg(): void { + this.tiMessage.open({ + content: 'this is a message', + close(messageRef: TiModalRef): void { + console.log('on close', messageRef); + }, + dismiss(messageRef: TiModalRef): void { + console.log('on dismiss', messageRef); + } + }); + } +} diff --git a/src/message/demo/src/app/message/MessageBtnComponent.ts b/src/message/demo/src/app/message/MessageBtnComponent.ts new file mode 100644 index 0000000..b5fe43f --- /dev/null +++ b/src/message/demo/src/app/message/MessageBtnComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; +import { TiMessageService } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-btn.html' +}) +export class MessageBtnComponent { + constructor(private tiMessage: TiMessageService) {} + showMsg(): void { + const modalInstance: any = this.tiMessage.open({ + content: 'this is a message', + close: (): void => {}, + okButton: { + show: true, + disabled: false, + primary: true, + text: '确定', + autofocus: true, + click: (): void => { + // 在这里可以添加你的业务代码 + // 使用按钮自定义click时,需要显式调用close或者dismiss + modalInstance.close(); + } + }, + cancelButton: { + // 取消按钮的设置和okbutton的属性设置是一致的,参考okbutton设置即可。 + show: false // 是否显示,默认是true + } + }); + } + + showMsg1(): void { + const modalInstance: any = this.tiMessage.open({ + content: 'this is a message', + close: (): void => {}, + okButton: { + show: true + }, + cancelButton: { + show: true, + disabled: true, + primary: false, + text: '取消', + autofocus: false, + click: (): void => { + modalInstance.close(); + } + } + }); + } +} diff --git a/src/message/demo/src/app/message/MessageBtnTestComponent.ts b/src/message/demo/src/app/message/MessageBtnTestComponent.ts new file mode 100644 index 0000000..636390a --- /dev/null +++ b/src/message/demo/src/app/message/MessageBtnTestComponent.ts @@ -0,0 +1,66 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiMessageService, TiModalRef } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-btn-test.html', + styles: ['.modal-class { width:500px !important; }'], + encapsulation: ViewEncapsulation.None +}) +export class MessageBtnTestComponent { + constructor(private tiMessage: TiMessageService) {} + showDefaultBtn(): void { + this.tiMessage.open({ + content: 'this is a message' + }); + } + showFalseBtn(): void { + this.tiMessage.open({ + content: 'this is a message', + cancelButton: { + show: false + } + }); + } + showBtnClick(): void { + const modalInstance: TiModalRef = this.tiMessage.open({ + modalClass: 'modal-class', + content: 'this is a message', + okButton: { + click: (): void => { + console.log('ok btn click'); + modalInstance.close(); + } + }, + cancelButton: { + click: (): void => { + console.log('cancel btn click'); + modalInstance.dismiss(); + } + } + }); + } + showBtnTxt(): void { + this.tiMessage.open({ + content: 'this is a message', + okButton: { + text: 'text changed' + } + }); + } + showBtnFocus(): void { + this.tiMessage.open({ + content: 'this is a message', + cancelButton: { + autofocus: true + } + }); + } + showBtnPrimary(): void { + this.tiMessage.open({ + content: 'this is a message', + cancelButton: { + primary: true + } + }); + } +} diff --git a/src/message/demo/src/app/message/MessageContentComponent.ts b/src/message/demo/src/app/message/MessageContentComponent.ts new file mode 100644 index 0000000..710b1c8 --- /dev/null +++ b/src/message/demo/src/app/message/MessageContentComponent.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; +import { TiMessageService } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-content.html' +}) +export class MessageContentComponent { + constructor(private tiMessage: TiMessageService) {} + showString(): void { + this.tiMessage.open({ + // v10.1.3及之前的版本存在XSS攻击(html类型)风险, v10.1.4 版本做了安全处理,已不存在XSS攻击风险,建议业务尽快升级版本。 + content: 'this is a message' + }); + } + showTemplate(content: string): void { + this.tiMessage.open({ + content, + context: { + contentName: 'ng-template content' + } + }); + } + showComp(): void { + this.tiMessage.open({ + content: TestComponent, + context: { + contentName: 'component' + } + }); + } +} + +// 弹框内部内容组件定义 +@Component({ + template: ` I'm a {{ contentName }} msg! ` +}) +export class TestComponent { + contentName: string = ''; +} diff --git a/src/message/demo/src/app/message/MessageIdComponent.ts b/src/message/demo/src/app/message/MessageIdComponent.ts new file mode 100644 index 0000000..dafccda --- /dev/null +++ b/src/message/demo/src/app/message/MessageIdComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { TiMessageService, TiModalRef } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-id.html' +}) +export class MessageIdComponent { + constructor(private tiMessage: TiMessageService) {} + showMsg(): void { + this.tiMessage.open({ + id: 'message_id', + content: 'this is a message_id', + close(messageRef: TiModalRef): void { + console.log('on close', messageRef); + }, + dismiss(messageRef: TiModalRef): void { + console.log('on dismiss', messageRef); + } + }); + } +} diff --git a/src/message/demo/src/app/message/MessageSecurityComponent.ts b/src/message/demo/src/app/message/MessageSecurityComponent.ts new file mode 100644 index 0000000..9fbfbcc --- /dev/null +++ b/src/message/demo/src/app/message/MessageSecurityComponent.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { TiMessageService } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-security.html' +}) +export class MessageSecurityComponent { + constructor(private tiMessage: TiMessageService, private domSanitizer: DomSanitizer) {} + showString1(): void { + this.tiMessage.open({ + // 10.1.3及之前版本该接口存在XSS攻击风险;该接口在10.1.4版本已经做了安全处理,js脚本不会执行。 + content: `hello链接1链接2` + }); + } + + showString2(): void { + this.tiMessage.open({ + // 组件内部用的是Angular提供的 DomSanitizer.sanitize 方法做防XSS攻击安全处理的,它会把style设置会过滤掉,所以建议使用class的方式添加样式; + // 如果一定要使用style方式,且能确保传入的html字符串片段是安全的,可以使用Angular提供的 DomSanitizer 上的 bypassSecurityTrustHtml 方法去掉angular的安全过滤处理。 + content: this.domSanitizer.bypassSecurityTrustHtml(`hello链接`) + }); + } +} diff --git a/src/message/demo/src/app/message/MessageTestModule.ts b/src/message/demo/src/app/message/MessageTestModule.ts new file mode 100644 index 0000000..e291645 --- /dev/null +++ b/src/message/demo/src/app/message/MessageTestModule.ts @@ -0,0 +1,79 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiMessageModule } from '@opentiny/ng'; + +import { MessageContentComponent, TestComponent } from './MessageContentComponent'; +import { MessageTypeComponent } from './MessageTypeComponent'; +import { MessageBtnComponent } from './MessageBtnComponent'; +import { MessageTitleComponent } from './MessageTitleComponent'; +import { MessageBasicComponent } from './MessageBasicComponent'; +import { MessageBtnTestComponent } from './MessageBtnTestComponent'; +import { MessageIdComponent } from './MessageIdComponent'; +import { MessageSecurityComponent } from './MessageSecurityComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiMessageModule, + TiButtonModule, + RouterModule.forChild(MessageTestModule.ROUTES) + ], + declarations: [ + MessageContentComponent, + MessageBasicComponent, + MessageBtnTestComponent, + MessageTypeComponent, + TestComponent, + MessageBtnComponent, + MessageTitleComponent, + MessageIdComponent, + MessageSecurityComponent + ], + entryComponents: [TestComponent] +}) +export class MessageTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiMessageComponent.html', label: 'Message' }, + { + href: 'components/TiContentWrapperComponent.html', + label: 'ContentWrapper' + }, + { + href: 'directives/TiTranscludeDirective.html', + label: 'TiTranscludeDirective' + } + ]; + static readonly ROUTES: Routes = [ + { + path: 'message/message-basic', + component: MessageBasicComponent + }, + { + path: 'message/message-type', + component: MessageTypeComponent + }, + { + path: 'message/message-title', + component: MessageTitleComponent + }, + { + path: 'message/message-content', + component: MessageContentComponent + }, + { + path: 'message/message-btn', + component: MessageBtnComponent + }, + { path: 'message/message-btn-test', component: MessageBtnTestComponent }, + { + path: 'message/message-id', + component: MessageIdComponent + }, + { path: 'message/message-security', component: MessageSecurityComponent } + ]; +} diff --git a/src/message/demo/src/app/message/MessageTitleComponent.ts b/src/message/demo/src/app/message/MessageTitleComponent.ts new file mode 100644 index 0000000..7465887 --- /dev/null +++ b/src/message/demo/src/app/message/MessageTitleComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiMessageService } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-title.html' +}) +export class MessageTitleComponent { + constructor(private tiMessage: TiMessageService) {} + showMessage(): void { + this.tiMessage.open({ + title: 'this is message title', + content: 'message content' + }); + } +} diff --git a/src/message/demo/src/app/message/MessageTypeComponent.ts b/src/message/demo/src/app/message/MessageTypeComponent.ts new file mode 100644 index 0000000..e218d39 --- /dev/null +++ b/src/message/demo/src/app/message/MessageTypeComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { TiMessageService } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-type.html' +}) +export class MessageTypeComponent { + constructor(private tiMessage: TiMessageService) {} + showDefault(): void { + this.tiMessage.open({ + content: 'confirm' + }); + } + showPrompt(): void { + this.tiMessage.open({ + type: 'prompt', + content: 'prompt' + }); + } + showWarn(): void { + this.tiMessage.open({ + type: 'warn', + content: 'warn' + }); + } + showError(): void { + this.tiMessage.open({ + type: 'error', + content: 'error' + }); + } +} diff --git a/src/message/demo/src/app/message/message-basic.html b/src/message/demo/src/app/message/message-basic.html new file mode 100644 index 0000000..8b62534 --- /dev/null +++ b/src/message/demo/src/app/message/message-basic.html @@ -0,0 +1,6 @@ +

    描述

    +

    基本使用,只定义内容,其余采用默认值

    +

    导入

    +

    import {{ '{' }} TiMessageModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    + diff --git a/src/message/demo/src/app/message/message-btn-test.html b/src/message/demo/src/app/message/message-btn-test.html new file mode 100644 index 0000000..cecf713 --- /dev/null +++ b/src/message/demo/src/app/message/message-btn-test.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/message/demo/src/app/message/message-btn.html b/src/message/demo/src/app/message/message-btn.html new file mode 100644 index 0000000..b1c93c1 --- /dev/null +++ b/src/message/demo/src/app/message/message-btn.html @@ -0,0 +1 @@ + diff --git a/src/message/demo/src/app/message/message-content.html b/src/message/demo/src/app/message/message-content.html new file mode 100644 index 0000000..c9348a8 --- /dev/null +++ b/src/message/demo/src/app/message/message-content.html @@ -0,0 +1,6 @@ + + {{context.contentName}} + + + + diff --git a/src/message/demo/src/app/message/message-id.html b/src/message/demo/src/app/message/message-id.html new file mode 100644 index 0000000..1315dc2 --- /dev/null +++ b/src/message/demo/src/app/message/message-id.html @@ -0,0 +1,4 @@ +

    描述

    +

    设置组件Id

    +

    示例

    + diff --git a/src/message/demo/src/app/message/message-security.html b/src/message/demo/src/app/message/message-security.html new file mode 100644 index 0000000..7b28fd6 --- /dev/null +++ b/src/message/demo/src/app/message/message-security.html @@ -0,0 +1,10 @@ +

    描述

    +

    + 内容通过content属性定义,支持传入字符串/template/组件形式。其中传入字符串形式时,是支持传入html字符串片段的, + v10.1.3及之前的版本存在XSS攻击风险,v10.1.4版本做了安全处理,v10.1.4及之后版本不存在XSS攻击风险。 +

    +

    示例

    + + +

    + diff --git a/src/message/demo/src/app/message/message-title.html b/src/message/demo/src/app/message/message-title.html new file mode 100644 index 0000000..bbc046a --- /dev/null +++ b/src/message/demo/src/app/message/message-title.html @@ -0,0 +1 @@ + diff --git a/src/message/demo/src/app/message/message-type.html b/src/message/demo/src/app/message/message-type.html new file mode 100644 index 0000000..b4584a0 --- /dev/null +++ b/src/message/demo/src/app/message/message-type.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/message/demo/src/app/message/webdoc/message-demos.js b/src/message/demo/src/app/message/webdoc/message-demos.js new file mode 100644 index 0000000..f23eb16 --- /dev/null +++ b/src/message/demo/src/app/message/webdoc/message-demos.js @@ -0,0 +1,68 @@ +export default { + column: '2', + demos: [ + { + demoId: 'message-type', + name: { + 'zh-CN': '消息弹框类型', + 'en-US': 'type', + }, + desc: { + 'zh-CN': + '

    通过属性type配置消息弹框类型,包含confirmpromptwarnerror类型。

    ', + 'en-US': '

    button color

    ', + }, + apis: ['TiMessageConfig.properties.type'], + }, + { + demoId: 'message-title', + name: { + 'zh-CN': '自定义标题', + 'en-US': 'title', + }, + desc: { + 'zh-CN': '

    通过属性title配置消息弹框标题。

    ', + 'en-US': '

    title

    ', + }, + apis: ['TiMessageConfig.properties.title'], + }, + { + demoId: 'message-content', + name: { + 'zh-CN': '内容', + 'en-US': 'content', + }, + desc: { + 'zh-CN': + '

    通过content配置内容,支持设置为字符串/ template 模板/组件形式。

    ', + 'en-US': 'content', + }, + apis: [ + 'TiMessageConfig.properties.content', + 'TiMessageConfig.properties.context', + ], + }, + { + demoId: 'message-btn', + name: { + 'zh-CN': '确认、取消按钮配置', + 'en-US': 'button', + }, + desc: { + 'zh-CN': + '

    通过okButton配置确认按钮,通过cancelButton配置取消按钮。

    ', + 'en-US': 'button', + }, + apis: [ + 'TiMessageConfig.properties.okButton', + 'TiMessageConfig.properties.cancelButton', + 'TiMessageButtonConfig.properties.text', + 'TiMessageButtonConfig.properties.primary', + 'TiMessageButtonConfig.properties.show', + 'TiMessageButtonConfig.properties.autofocus', + 'TiMessageButtonConfig.properties.disabled', + 'TiMessageButtonConfig.methods.click', + ], + }, + ], +}; diff --git a/src/message/demo/src/app/message/webdoc/message.cn.md b/src/message/demo/src/app/message/webdoc/message.cn.md new file mode 100644 index 0000000..7030fe1 --- /dev/null +++ b/src/message/demo/src/app/message/webdoc/message.cn.md @@ -0,0 +1,34 @@ +--- +title: Message 消息弹框 +--- +# Message 消息弹框 + +
    + +Message 是提供消息弹框的组件。   + ++ 提供服务方式供业务使用。使用该服务时需要引入模块`TiMessageModule`,通过调用`TiMessageService.open(config)`方法生成弹出框。 + +```typescript +import { TiMessageModule } from '@opentiny/ng'; +``` + +需要在项目中(建议在根模块)中导入`BrowserAnimationsModule`。 + +```typescript +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +``` + +
    + +
    + +Message 是提供消息弹框的组件。   + ++ 提供服务方式供业务使用。使用该服务时需要引入模块`TiMessageModule`,通过调用`TiMessageService.open(config)`方法生成弹出框。 + +```typescript +import { TiMessageModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/message/demo/src/app/message/webdoc/message.en.md b/src/message/demo/src/app/message/webdoc/message.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/message/demo/src/app/message/webdoc/message.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/message/demo/src/favicon.ico b/src/message/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/message/demo/src/index.html b/src/message/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/message/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/message/demo/src/main.ts b/src/message/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/message/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/message/demo/test.ts b/src/message/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/message/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/message/demo/tsconfig.app.json b/src/message/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/message/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/message/demo/tsconfig.spec.json b/src/message/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/message/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/message/lib/index.ts b/src/message/lib/index.ts new file mode 100644 index 0000000..69ca838 --- /dev/null +++ b/src/message/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiMessageModule'; +export * from './src/TiMessageService'; diff --git a/src/message/lib/ng-package.json b/src/message/lib/ng-package.json new file mode 100644 index 0000000..d393c26 --- /dev/null +++ b/src/message/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/message", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/message/lib/package.json b/src/message/lib/package.json new file mode 100644 index 0000000..fd838a5 --- /dev/null +++ b/src/message/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "@opentiny/ng-message", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-modal": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-popup": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/message/lib/project.json b/src/message/lib/project.json new file mode 100644 index 0000000..72b0a13 --- /dev/null +++ b/src/message/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/message/lib", + "sourceRoot": "src/message/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/message"], + "options": { + "project": "src/message/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/message"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js message" + }, + { + "command": "ng default-build message" + }, + { + "command": "node build/clear-default-theme.js message" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/message && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build message && ng pack message && node build/publish.js message --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/message/lib/src/TiContentWrapperComponent.ts b/src/message/lib/src/TiContentWrapperComponent.ts new file mode 100644 index 0000000..7b8c682 --- /dev/null +++ b/src/message/lib/src/TiContentWrapperComponent.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +/** + * @ignore + */ +@Component({ + selector: 'ti-content-wrapper', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiContentWrapperComponent {} diff --git a/src/message/lib/src/TiMessageComponent.html b/src/message/lib/src/TiMessageComponent.html new file mode 100644 index 0000000..10e26bd --- /dev/null +++ b/src/message/lib/src/TiMessageComponent.html @@ -0,0 +1,39 @@ + +
    + +
    + +
    +
    + + + diff --git a/src/message/lib/src/TiMessageComponent.ts b/src/message/lib/src/TiMessageComponent.ts new file mode 100644 index 0000000..06e2f84 --- /dev/null +++ b/src/message/lib/src/TiMessageComponent.ts @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { TiLocale } from '@opentiny/ng-locale'; +import { Util } from '@opentiny/ng-utils'; +import { TiMessageButtonConfig } from './TiMessageInterface'; + +/** + * messge类型设置 + */ +export type TiMessageType = 'prompt' | 'warn' | 'error' | 'confirm'; +/** + * @ignore + * message模板组件定义 + */ +@Component({ + selector: 'ti-message', + templateUrl: './TiMessageComponent.html', + styleUrls: ['./message.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiMessageComponent implements OnInit { + // 常量配置 + private CONST_CONFIG: any = { + validType: ['confirm', 'error', 'warn', 'prompt'], + defaultType: 'confirm' + }; + @Input() id: string; + @Input() title: string; + @Input() type: TiMessageType; + @Input() content: any; + @Input() context: any; + @Input() okButton: TiMessageButtonConfig; + @Input() cancelButton: TiMessageButtonConfig; + @Input() close: () => void; + @Input() dismiss: () => void; + public contentConfig: any; // 内容区域属性配置 + private readonly localeWords = TiLocale.getLocaleWords(); + + /** + * 设置按钮属性(除primary和autofocus外) + * @param options 外部配置属性 + * @param defaults 按钮text和click属性 + * @returns 合并后的属性 + */ + private static setBtn( + options: TiMessageButtonConfig, + defaults: { + text?: string; + click(): void; + } + ): TiMessageButtonConfig { + const defaultOpts: TiMessageButtonConfig = { + show: true, + disabled: false, + ...defaults + }; + if (typeof options === 'object') { + return { ...defaultOpts, ...options }; + } + + return defaultOpts; + } + + ngOnInit(): void { + this.setType(); + this.setTitle(); + this.setBtns(); + this.setContent(); + } + private setType(): void { + const validType: Array = this.CONST_CONFIG.validType; + const defaultType: TiMessageType = this.CONST_CONFIG.defaultType; + if (validType.indexOf(this.type) === -1) { + this.type = defaultType; + } + } + private setTitle(): void { + if (typeof this.title === 'string') { + return; + } + // 不同类型的message有默认title + this.title = this.localeWords.tiMessage[this.type]; + } + /** + * 设置按钮属性: + * 对按钮的设置支持Object类型 + * 为Object类型时,覆盖和扩展默认设置 + */ + private setBtns(): void { + this.okButton = TiMessageComponent.setBtn(this.okButton, { + text: this.localeWords.tiMessage.ok, + click: (): void => { + this.close(); + } + }); + this.cancelButton = TiMessageComponent.setBtn(this.cancelButton, { + text: this.localeWords.tiMessage.cancel, + click: (): void => { + this.dismiss(); + } + }); + + this.setBtnUniqueState('autofocus'); + this.setBtnUniqueState('primary'); + } + /** + * 设置按钮属性,确保默认状态只在一个按钮生效 + * @param prop 按钮属性 + */ + private setBtnUniqueState(prop: string): void { + if (Util.isUndefined(this.okButton[prop]) && Util.isUndefined(this.cancelButton[prop])) { + this.okButton[prop] = true; + } + } + private setContent(): void { + this.contentConfig = { + content: this.content, + context: this.context + }; + } + public setId(suffix: string): string { + if (Util.isUndefined(this.id)) { + return ''; + } + + return `${this.id}${suffix}`; + } +} diff --git a/src/message/lib/src/TiMessageInterface.ts b/src/message/lib/src/TiMessageInterface.ts new file mode 100644 index 0000000..3c7c5b7 --- /dev/null +++ b/src/message/lib/src/TiMessageInterface.ts @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TemplateRef } from '@angular/core'; +import { TiModalConfig } from '@opentiny/ng-modal'; +// 外部可传入的message配置项 +/** + * 消息组件配置项接口 + * + * 该接口用于消息弹框组件[open]{@link TiMessageService#open}方法的参数的定义 + */ +export interface TiMessageConfig extends TiModalConfig { + /** + * 消息弹框类型 + * @default 'confirm' + */ + type?: 'confirm' | 'warn' | 'error' | 'prompt'; + /** + * 消息弹框标题 + */ + title?: string; + /** + * 消息弹框内容,支持字符串、 template 模板、组件格式 + */ + content?: string | TemplateRef | any; + /** + * 内容区域的上下文,为对象形式 + */ + context?: any; + /** + * 底部 ok 按钮配置 + */ + okButton?: TiMessageButtonConfig; + /** + * 底部 cancel 按钮配置 + */ + cancelButton?: TiMessageButtonConfig; +} + +/** + * 消息弹框底部按钮的配置项接口 + * + * 该接口用于定义消息弹框的[okButton]{@link ../injectables/TiMessageService.html#okButton} + * 或[cancelButton]{@link ../injectables/TiMessageService.html#cancelButton}的接口类型 + */ +export interface TiMessageButtonConfig { + /** + * 按钮文本 + * + * @default okButton为确认/OK(国际化),cancelButton为取消/Cancel(国际化) + */ + text?: string; + /** + * 是否显示 + * + * @default true + */ + show?: boolean; + /** + * 是否禁用 + * + * @default false + */ + disabled?: boolean; + /** + * 是否为主按钮,主按钮为较醒目的按钮样式 + * + * @default okButton为主按钮 + */ + primary?: boolean; + /** + * 是否自动聚焦 + * + * @default okButton默认聚焦 + */ + autofocus?: boolean; + /** + * 点击事件 + */ + click?(): void; +} diff --git a/src/message/lib/src/TiMessageModule.ts b/src/message/lib/src/TiMessageModule.ts new file mode 100644 index 0000000..c785d06 --- /dev/null +++ b/src/message/lib/src/TiMessageModule.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiMessageComponent } from './TiMessageComponent'; +import { TiContentWrapperComponent } from './TiContentWrapperComponent'; +import { TiTranscludeDirective } from './TiTranscludeDirective'; +import { TiModalModule } from '@opentiny/ng-modal'; +import { TiButtonModule } from '@opentiny/ng-button'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule, TiModalModule, TiButtonModule, TiIconModule], + declarations: [TiMessageComponent, TiTranscludeDirective, TiContentWrapperComponent], + entryComponents: [TiMessageComponent, TiContentWrapperComponent] +}) +export class TiMessageModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiMessageButtonConfig, TiMessageConfig } from './TiMessageInterface'; +export { TiMessageType } from './TiMessageComponent'; diff --git a/src/message/lib/src/TiMessageService.ts b/src/message/lib/src/TiMessageService.ts new file mode 100644 index 0000000..0acaff5 --- /dev/null +++ b/src/message/lib/src/TiMessageService.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * 该类对外暴露,以服务的方式提供方法生成message组件 + * open({ + * id:message id,string类型,用于生成唯一的按钮可测试id,并且防止弹框多次打开(同modal组件) + * type:message类型,支持'prompt' | 'warn' | 'error' | 'confirm',默认为confirm + * title:头部title,string类型,可支持string形式的DOM片段 + * content:内容区域DOM,支持类型为:TemplateRef | Function | string + * context:内容区域上下文,类型为Object + * okButton:{ + * text:string,按钮文本显示 + * click: Function,点击回调 + * primary: boolean,默认为true,程序中保证了两个按钮中只有一个可为true + * autofocus: boolean,默认为true,程序中保证了两个按钮中只有一个可为true + * show: boolean,标识按钮是否显示,默认为true + * } + * cancleBtn:{ + * text:string,按钮文本显示 + * click: Function,点击回调 + * primary: boolean,默认为false,程序中保证了两个按钮中只有一个可为true + * autofocus: boolean,默认为false,程序中保证了两个按钮中只有一个可为true + * show: boolean,标识按钮是否显示,默认为true + * } + * }) + */ +import { Injectable } from '@angular/core'; +import { TiModalRef } from '@opentiny/ng-modal'; +import { TiModalService } from '@opentiny/ng-modal'; +import { TiMessageComponent } from './TiMessageComponent'; +import { TiMessageModule } from './TiMessageModule'; +import { TiMessageConfig } from './TiMessageInterface'; + +/** + * 消息弹框组件,使用服务方式生成,使用该服务时需要引入模块TiMessageModule,通过调用TiMessageService.open(config)方法生成弹出框 + * + *

    使用此组件时需要开发者在项目模块(建议在根模块) + * 中引入BrowserAnimationsModule。这是因为此组件中使用了Angular动画,需要引入BrowserAnimationsModule, + * 但是 BrowserAnimationsModule 不能在懒加载模块被重复引入,所以需要开发者来引入BrowserAnimationsModule,保证其引入一次。

    + * + */ +@Injectable({ + providedIn: TiMessageModule +}) +export class TiMessageService { + constructor(private tiModal: TiModalService) {} + /** + * 生成消息弹框方法 + * [config] 弹框配置信息 + * 返回 弹框实例信息,使用该实例信息可调用弹框的close等方法;内容为component类型时,也可通过content属性获取内容结果数据 + */ + public open(config?: TiMessageConfig): TiModalRef { + const { type, title, content, context, okButton, cancelButton, ...modalConfig }: TiMessageConfig = config; + + return this.tiModal.open(TiMessageComponent, { + context: { + id: config.id, + type, + title, + content, + context, + okButton, + cancelButton + }, + modalClass: 'ti3-msg-default-width', + ...modalConfig + }); + } +} diff --git a/src/message/lib/src/TiTranscludeDirective.ts b/src/message/lib/src/TiTranscludeDirective.ts new file mode 100644 index 0000000..fc37a9c --- /dev/null +++ b/src/message/lib/src/TiTranscludeDirective.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; +import { TiPopupService } from '@opentiny/ng-popup'; +import { TiContentWrapperComponent } from './TiContentWrapperComponent'; +/** + * @ignore + * 生成并且compile内容为宿主元素的子元素 + */ +@Directive({ + selector: '[tiTransclude]' +}) +export class TiTranscludeDirective implements OnInit, OnDestroy { + @Input() tiTransclude: { + content: string | TemplateRef | any; + context?: any; + }; + private wrapperRef: any; + constructor(private eleRef: ElementRef, private popService: TiPopupService) {} + ngOnInit(): void { + this.wrapperRef = this.popService.create(TiContentWrapperComponent); + this.wrapperRef.show({ + content: this.tiTransclude.content, + contentContext: this.tiTransclude.context, + container: this.eleRef + }); + } + ngOnDestroy(): void { + this.wrapperRef.hide(); + } +} diff --git a/src/message/lib/src/i18n/TiMessageWords.ts b/src/message/lib/src/i18n/TiMessageWords.ts new file mode 100644 index 0000000..7773961 --- /dev/null +++ b/src/message/lib/src/i18n/TiMessageWords.ts @@ -0,0 +1,10 @@ +export interface TiMessageWords { + tiMessage: { + prompt: string; + warn: string; + confirm: string; + error: string; + ok: string; + cancel: string; + }; +} diff --git a/src/message/lib/src/i18n/en_US.ts b/src/message/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..e6f5e37 --- /dev/null +++ b/src/message/lib/src/i18n/en_US.ts @@ -0,0 +1,12 @@ +import { TiMessageWords } from './TiMessageWords'; + +export const en_US: TiMessageWords = { + tiMessage: { + prompt: 'Information', + warn: 'Warning', + confirm: 'Confirm', + error: 'Error', + ok: 'OK', + cancel: 'Cancel' + } +}; diff --git a/src/message/lib/src/i18n/es_US.ts b/src/message/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..43d4d4a --- /dev/null +++ b/src/message/lib/src/i18n/es_US.ts @@ -0,0 +1,12 @@ +import { TiMessageWords } from './TiMessageWords'; + +export const es_US: TiMessageWords = { + tiMessage: { + prompt: 'Información', + warn: 'Advertencia', + confirm: 'Confirmar', + error: 'Error', + ok: 'Aceptar', + cancel: 'Cancelar' + } +}; diff --git a/src/message/lib/src/i18n/fr_FR.ts b/src/message/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..ed2813a --- /dev/null +++ b/src/message/lib/src/i18n/fr_FR.ts @@ -0,0 +1,12 @@ +import { TiMessageWords } from './TiMessageWords'; + +export const fr_FR: TiMessageWords = { + tiMessage: { + prompt: 'Information', + warn: 'Attention', + confirm: 'Confirmer', + error: 'Erreur', + ok: 'OK', + cancel: 'Annuler' + } +}; diff --git a/src/message/lib/src/i18n/index.ts b/src/message/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/message/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/message/lib/src/i18n/pt_BR.ts b/src/message/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..d4c200f --- /dev/null +++ b/src/message/lib/src/i18n/pt_BR.ts @@ -0,0 +1,12 @@ +import { TiMessageWords } from './TiMessageWords'; + +export const pt_BR: TiMessageWords = { + tiMessage: { + prompt: 'Informação', + warn: 'Aviso', + confirm: 'Confirmar', + error: 'Erro', + ok: 'OK', + cancel: 'Cancelar' + } +}; diff --git a/src/message/lib/src/i18n/zh_CN.ts b/src/message/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..098cb76 --- /dev/null +++ b/src/message/lib/src/i18n/zh_CN.ts @@ -0,0 +1,12 @@ +import { TiMessageWords } from './TiMessageWords'; + +export const zh_CN: TiMessageWords = { + tiMessage: { + prompt: '提示', + warn: '警告', + confirm: '确认', + error: '错误', + ok: '确认', + cancel: '取消' + } +}; diff --git a/src/message/lib/src/message.less b/src/message/lib/src/message.less new file mode 100644 index 0000000..40030e7 --- /dev/null +++ b/src/message/lib/src/message.less @@ -0,0 +1,53 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-message-icon-size: var(--ti-common-size-6x); + --ti-message-icon-margin-right: var(--ti-common-space-2x); +} + +::ng-deep ti-modal-wrapper { + --ti-message-width: 400px; +} + +::ng-deep .ti3-modal-dialog.ti3-msg-default-width { + width: var(--ti-message-width); +} + +ti-modal-header.ti3-msg-header { + padding-bottom: var(--ti-common-space-3x); +} +.ti3-msg-icon { + float: left; + line-height: normal; + font-size: var(--ti-message-icon-size); + margin-right: var(--ti-message-icon-margin-right); + &.ti3-icon-check-circle { + color: var(--ti-common-color-success); + } + &.ti3-msg-icon-warn { + color: var(--ti-common-color-warn); + } + &.ti3-icon-info-circle { + color: var(--ti-common-color-prompt); + } + &.ti3-msg-icon-error { + color: var(--ti-common-color-error); + } +} + +.ti3-msg-content-wrapper { + overflow: inherit; + font-size: var(--ti-common-font-size-base); + color: var(--ti-common-color-text-secondary); + line-height: var(--ti-common-line-height-number); + word-wrap: break-word; +} + +.ti3-msg-content-title { + font-size: var(--ti-common-font-size-3); + line-height: 26px; + color: var(--ti-common-color-text-primary); + font-weight: var(--ti-common-font-weight-7); + display: inline-block; + width: calc(100% - var(--ti-message-icon-size) - var(--ti-message-icon-margin-right)); +} diff --git a/src/modal/demo/karma.conf.js b/src/modal/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/modal/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/modal/demo/project.json b/src/modal/demo/project.json new file mode 100644 index 0000000..f993e10 --- /dev/null +++ b/src/modal/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/modal/demo", + "sourceRoot": "src/modal/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/modal", + "index": "src/modal/demo/src/index.html", + "main": "src/modal/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/modal/demo/tsconfig.app.json", + "assets": ["src/modal/demo/src/favicon.ico", "src/modal/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "modal-demo:build:production" + }, + "development": { + "browserTarget": "modal-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js modal" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/modal/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/modal/demo/tsconfig.spec.json", + "karmaConfig": "src/modal/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/modal/demo/src/app/AppComponent.ts b/src/modal/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/modal/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/modal/demo/src/app/AppModule.ts b/src/modal/demo/src/app/AppModule.ts new file mode 100644 index 0000000..ec368ac --- /dev/null +++ b/src/modal/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ModalTestModule } from './modal/ModalTestModule'; + +@NgModule({ + imports: [ + ModalTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/modal/demo/src/app/IndexComponent.ts b/src/modal/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..afe13cc --- /dev/null +++ b/src/modal/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ModalTestModule } from './modal/ModalTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = ModalTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/modal/demo/src/app/app.html b/src/modal/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/modal/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/modal/demo/src/app/modal/ModalAnimationComponent.ts b/src/modal/demo/src/app/modal/ModalAnimationComponent.ts new file mode 100644 index 0000000..0489d80 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalAnimationComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-animation.html' +}) +export class ModalAnimationComponent { + constructor(private tiModal: TiModalService) {} + + show(content: string): void { + this.tiModal.open(content, { + animation: false + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalBackdropComponent.ts b/src/modal/demo/src/app/modal/ModalBackdropComponent.ts new file mode 100644 index 0000000..4662f23 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalBackdropComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-backdrop.html' +}) +export class ModalBackdropComponent { + constructor(private tiModal: TiModalService) {} + + show(content: string): void { + this.tiModal.open(content, { + backdrop: false + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalClassComponent.ts b/src/modal/demo/src/app/modal/ModalClassComponent.ts new file mode 100644 index 0000000..a4147c6 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalClassComponent.ts @@ -0,0 +1,17 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-class.html', + styles: ['.modal-class { width: 300px !important; }'], + encapsulation: ViewEncapsulation.None // 要想设置的样式生效,此处必须配置成 ViewEncapsulation.None +}) +export class ModalClassComponent { + constructor(private tiModal: TiModalService) {} + + show(content: string): void { + this.tiModal.open(content, { + modalClass: 'modal-class' + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalCloseIconComponent.ts b/src/modal/demo/src/app/modal/ModalCloseIconComponent.ts new file mode 100644 index 0000000..bc69571 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalCloseIconComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-close-icon.html' +}) +export class ModalCloseIconComponent { + constructor(private tiModal: TiModalService) {} + + show(content: string): void { + this.tiModal.open(content, { + closeIcon: false + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalConfigTestComponent.ts b/src/modal/demo/src/app/modal/ModalConfigTestComponent.ts new file mode 100644 index 0000000..fd6226c --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalConfigTestComponent.ts @@ -0,0 +1,57 @@ +import { Component } from '@angular/core'; +import { TiModalRef, TiModalService, TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './modal-config-test.html' +}) +export class ModalConfigTestComponent { + constructor(private tiModal: TiModalService) {} + // 定义公共变量接收弹窗实例 + openModal: TiModalRef; + // 弹窗外部的公共变量 + myNumber: number = 1; + myValue: string = ''; + myValidationConfig: TiValidationConfig = { + tip: 'should input email', + type: 'change' + }; + show(content: string): void { + // 弹窗打开,会返回一个弹窗实例,使用一个公共变量去接收它,从而获得弹窗实例 + this.openModal = this.tiModal.open(content, { + id: 'myModal', // 弹框id,设置id可防止同一页面生成多个相同弹框的问题 + animation: false, // 控制弹窗显示/隐藏是否带动画效果,不设置使用默认值,默认值为true,即表示带有动效 + draggable: false, // 控制弹窗是否可拖拽,不设置使用默认值,默认值为true,即表示可拖拽 + closeOnEsc: false, // 控制是否可通过ESC快捷键关闭弹窗,不设置使用默认值,默认值为true,即默认可通过ESC快捷键关闭弹窗 + backdrop: false, // 控制是否显示遮罩,不设置使用默认值,默认值为true,即默认展示遮罩 + closeIcon: false, // 控制头部是否有关闭按钮,不设置使用默认值,默认值为true,即默认头部有关闭按钮, + headerAlign: 'center', // 头部内容水平对齐位置,可设置的值有'center' | 'left' | 'right',默认值为left,表示左对齐,可参考“header自定义”示例 + modalClass: '', // 弹框样式,可通过该样式定义弹框的宽高等,可参考“modalClass功能”示例 + context: { + // 向弹框传递的数据,当弹框内容为组件或者template时,使用方法不同。可分别参考“弹框内容为组件形式”和“弹框内容为组件形式”示例 + headerTitle: "I'm a modal!", + // 在弹窗自定义的button,来关闭弹窗。 + closeModal: (): void => { + this.openModal.close(); + }, + // 自定义方法,此方法来改变弹窗外部的变量this.myNumber + changMyNumber: (): void => { + this.myNumber++; + } + }, + // beforeClose可以阻止弹框的关闭,需要手动调用modalRef.destroy(reason)来关闭弹框 + beforeClose: (modalRef: TiModalRef, reason: boolean): void => { + console.log(reason); + // 如果不调用modalRef.destroy(reason),无法关闭弹框 + // reason为true时,回调close() + // reason为false时,回调dismiss() + modalRef.destroy(reason); + }, + close: (): void => { + console.log('close'); + }, + dismiss: (): void => { + console.log('dismiss'); + } + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalContentCompComponent.ts b/src/modal/demo/src/app/modal/ModalContentCompComponent.ts new file mode 100644 index 0000000..2ad9544 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalContentCompComponent.ts @@ -0,0 +1,58 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { TiModalRef, TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: './modal-content-comp.html' +}) +export class ModalContentCompComponent { + constructor(private tiModal: TiModalService) {} + myLogs: Array = []; + show(): void { + this.tiModal.open(ModalTestComponent, { + id: 'myModal1', // 定义id防止同一页面出现多个相同弹框 + // 弹框内容组件上下文属性定义 + context: { + name: 'component', + bodyText: 'component modal body', + outputs: { + // 输出属性定义在这里 + buttonClick: (): void => { + this.myLogs = [...this.myLogs, 'buttonClick事件']; + } + } + }, + close: (modalRef: TiModalRef): void => {}, + dismiss: (modalRef: TiModalRef): void => {} + }); + } +} + +// 组件定义,此处为了示例方便呈现写在一个文件,业务请新建文件定义组件 +@Component({ + template: ` + + I'm a {{ name }} modal! + + + {{ bodyText }} + + + + + + + ` +}) +export class ModalTestComponent { + // 组件对外接口,modal可通过context属性设置/获取这些接口属性 + @Input() bodyText: string = ''; + @Input() name: string = ''; + @Output() readonly buttonClick: EventEmitter = new EventEmitter(); + // 模板中实际调用的是Modal服务提供的close和dismiss方法,并非此处定义的方法; + // 在此处定义close和dismiss方法只是为了避免生产环境打包时报错 + close(): void {} + dismiss(): void {} + onClick(): void { + this.buttonClick.emit(); + } +} diff --git a/src/modal/demo/src/app/modal/ModalContentTempComponent.ts b/src/modal/demo/src/app/modal/ModalContentTempComponent.ts new file mode 100644 index 0000000..5256243 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalContentTempComponent.ts @@ -0,0 +1,15 @@ +import { Component, TemplateRef } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-content-temp.html' +}) +export class ModalContentTempComponent { + constructor(private tiModal: TiModalService) {} + + show(content: TemplateRef): void { + this.tiModal.open(content, { + id: 'myModal' + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalDraggableComponent.ts b/src/modal/demo/src/app/modal/ModalDraggableComponent.ts new file mode 100644 index 0000000..2842c04 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalDraggableComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-draggable.html' +}) +export class ModalDraggableComponent { + constructor(private tiModal: TiModalService) {} + + show(content: string): void { + this.tiModal.open(content, { + draggable: false + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalEscComponent.ts b/src/modal/demo/src/app/modal/ModalEscComponent.ts new file mode 100644 index 0000000..378fa68 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalEscComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-esc.html' +}) +export class ModalEscComponent { + constructor(private tiModal: TiModalService) {} + + show(content: string): void { + this.tiModal.open(content, { + closeOnEsc: false + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalEventComponent.ts b/src/modal/demo/src/app/modal/ModalEventComponent.ts new file mode 100644 index 0000000..7585718 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalEventComponent.ts @@ -0,0 +1,25 @@ +import { Component, TemplateRef } from '@angular/core'; +import { TiModalRef, TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: './modal-event.html' +}) +export class ModalEventComponent { + constructor(private tiModal: TiModalService) {} + myLogs: Array = []; + showModal(modalTemplate: TemplateRef): void { + this.tiModal.open(modalTemplate, { + id: 'myModal', + // 只有点击确认按钮时弹窗关闭 + beforeClose: (modalRef: TiModalRef, reason: boolean): void => { + if (reason) { + modalRef.destroy(reason); + } else { + this.myLogs = [...this.myLogs, '关闭不了']; + } + }, + close: (): void => {}, + dismiss: (): void => {} + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalHeaderAlignComponent.ts b/src/modal/demo/src/app/modal/ModalHeaderAlignComponent.ts new file mode 100644 index 0000000..46e710f --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalHeaderAlignComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-header-align.html' +}) +export class ModalHeaderAlignComponent { + constructor(private tiModal: TiModalService) {} + + show(content: string): void { + this.tiModal.open(content, { + headerAlign: 'center' + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalHeaderStyleComponent.ts b/src/modal/demo/src/app/modal/ModalHeaderStyleComponent.ts new file mode 100644 index 0000000..60011e6 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalHeaderStyleComponent.ts @@ -0,0 +1,16 @@ +import { Component, TemplateRef } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: './modal-header-style.html' +}) +export class ModalHeaderStyleComponent { + constructor(private tiModal: TiModalService) {} + modalName: string = 'aaa'; + show(content: TemplateRef): void { + this.tiModal.open(content, { + closeIcon: false, // 隐藏上方关闭按钮 + headerAlign: 'center' // 设置头部标题居中对齐 + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalTestModule.ts b/src/modal/demo/src/app/modal/ModalTestModule.ts new file mode 100644 index 0000000..5d19f3d --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalTestModule.ts @@ -0,0 +1,133 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { + TiButtonModule, + TiFormfieldModule, + TiIpModule, + TiModalModule, + TiSelectModule, + TiSpinnerModule, + TiTextareaModule, + TiTextModule, + TiValidationModule +} from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { ModalContentTempComponent } from './ModalContentTempComponent'; +import { ModalContentCompComponent, ModalTestComponent } from './ModalContentCompComponent'; +import { ModalEventComponent } from './ModalEventComponent'; +import { ModalHeaderStyleComponent } from './ModalHeaderStyleComponent'; +import { ModalClassComponent } from './ModalClassComponent'; +import { ModalTwoTestComponent } from './ModalTwoTestComponent'; +import { ModalTwoBackdropComponent } from './ModalTwoBackdropComponent'; +import { ModalConfigTestComponent } from './ModalConfigTestComponent'; +import { TestComponent } from './TestComponent'; +import { ModalAnimationComponent } from './ModalAnimationComponent'; +import { ModalBackdropComponent } from './ModalBackdropComponent'; +import { ModalCloseIconComponent } from './ModalCloseIconComponent'; +import { ModalDraggableComponent } from './ModalDraggableComponent'; +import { ModalHeaderAlignComponent } from './ModalHeaderAlignComponent'; +import { ModalEscComponent } from './ModalEscComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiIpModule, + TiModalModule, + TiButtonModule, + TiSelectModule, + TiTextareaModule, + TiFormfieldModule, + TiTextModule, + TiValidationModule, + TiSpinnerModule, + DemoLogModule, + RouterModule.forChild(ModalTestModule.ROUTES) + ], + declarations: [ + ModalConfigTestComponent, + ModalContentTempComponent, + ModalContentCompComponent, + TestComponent, + ModalTestComponent, + ModalClassComponent, + ModalTwoTestComponent, + ModalEventComponent, + ModalHeaderStyleComponent, + ModalHeaderStyleComponent, + ModalTwoBackdropComponent, + ModalAnimationComponent, + ModalBackdropComponent, + ModalCloseIconComponent, + ModalDraggableComponent, + ModalHeaderAlignComponent, + ModalEscComponent + ], + entryComponents: [TestComponent, ModalTestComponent] +}) +export class ModalTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiModalComponent.html', label: 'Modal' }, + { href: 'components/TiBackdropComponent.html', label: 'Backdrop' }, + { href: 'components/TiModalBodyComponent.html', label: 'ModalBody' }, + { href: 'components/TiModalHeaderComponent.html', label: 'ModalHeader' }, + { href: 'components/TiModalFooterComponent.html', label: 'ModalFooter' } + ]; + static readonly ROUTES: Routes = [ + { + path: 'modal/modal-content-comp', + component: ModalContentCompComponent + }, + { + path: 'modal/modal-content-temp', + component: ModalContentTempComponent + }, + { + path: 'modal/modal-class', + component: ModalClassComponent + }, + { + path: 'modal/modal-animation', + component: ModalAnimationComponent + }, + { + path: 'modal/modal-backdrop', + component: ModalBackdropComponent + }, + { + path: 'modal/modal-close-icon', + component: ModalCloseIconComponent + }, + { + path: 'modal/modal-draggable', + component: ModalDraggableComponent + }, + { + path: 'modal/modal-header-align', + component: ModalHeaderAlignComponent + }, + { + path: 'modal/modal-event', + component: ModalEventComponent + }, + { + path: 'modal/modal-header-style', + component: ModalHeaderStyleComponent + }, + { + path: 'modal/modal-config-test', + component: ModalConfigTestComponent + }, + { + path: 'modal/modal-esc', + component: ModalEscComponent + }, + { path: 'modal/modal-two-backdrop', component: ModalTwoBackdropComponent }, + { path: 'modal/modal-two-test', component: ModalTwoTestComponent } + ]; +} diff --git a/src/modal/demo/src/app/modal/ModalTwoBackdropComponent.ts b/src/modal/demo/src/app/modal/ModalTwoBackdropComponent.ts new file mode 100644 index 0000000..99e2175 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalTwoBackdropComponent.ts @@ -0,0 +1,25 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TestComponent } from './TestComponent'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-two-backdrop.html' +}) +export class ModalTwoBackdropComponent { + constructor(private tiModal: TiModalService, protected changeDetectorRef: ChangeDetectorRef) {} + myModal: TestComponent; + show(): void { + this.tiModal.open(TestComponent, { + id: 'myModal', + backdrop: false // 第一个弹窗没有遮罩 + }); + setTimeout(() => { + this.tiModal.open(TestComponent, { + id: 'myModalId', + backdrop: true // 第二个弹窗有遮罩 + }); + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + }, 0); + } +} diff --git a/src/modal/demo/src/app/modal/ModalTwoTestComponent.ts b/src/modal/demo/src/app/modal/ModalTwoTestComponent.ts new file mode 100644 index 0000000..b75635e --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalTwoTestComponent.ts @@ -0,0 +1,78 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TestComponent } from './TestComponent'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-two-test.html' +}) +export class ModalTwoTestComponent { + constructor(private tiModal: TiModalService, protected changeDetectorRef: ChangeDetectorRef) {} + myModal: TestComponent; + show(): void { + this.tiModal.open(TestComponent, { + id: 'myModal' + }); + setTimeout(() => { + this.tiModal.open(TestComponent, {}); + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + }, 0); + } + showModal(): void { + this.tiModal.open(TestComponent, { + id: 'myModalId' + }); + setTimeout(() => { + this.tiModal.open(TestComponent, { + id: 'myModalId' + }); + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + }, 0); + } + showMoreModals(): void { + setTimeout(() => { + this.tiModal.open(TestComponent, { + // id: 'myModal' + }); + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + }, 0); + setTimeout(() => { + this.tiModal.open(TestComponent, { + // id: 'myModal' + }); + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + }, 0); + setTimeout(() => { + this.tiModal.open(TestComponent, { + // id: 'myModal' + }); + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + setTimeout(() => { + console.log('close modals'); + const openedModals = this.tiModal.openedModals; + for (let i = 0; i < openedModals.length; i++) { + openedModals[i].dismiss(); + } + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + }, 20000); + }, 0); + } + closeOneShowAnother(): void { + this.tiModal.open(TestComponent, { + close: (): void => { + setTimeout(() => { + this.tiModal.open(TestComponent, { + id: 'myModal' + }); + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + }, 500); // 300ms~600ms + } + }); + } +} diff --git a/src/modal/demo/src/app/modal/TestComponent.ts b/src/modal/demo/src/app/modal/TestComponent.ts new file mode 100644 index 0000000..feaa736 --- /dev/null +++ b/src/modal/demo/src/app/modal/TestComponent.ts @@ -0,0 +1,72 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { TiValidationConfig } from '@opentiny/ng'; + +@Component({ + template: ` + + I'm a {{ name }} modal! + + + body + + + + + + + + + + + + + + + + + + + + + + + + `, + encapsulation: ViewEncapsulation.None +}) +export class TestComponent { + @Input() myValue: string = ''; + @Input() name: string = ''; + value: string = 'sdf'; + placeholder: string = '请输入'; + myValue1: string = ''; + options: Array = []; + label: string = 'aaa'; + constructor() { + for (let i: number = 0; i < 20; i++) { + const option: Object = { + id: String(i), + label: `myemail${i}@example.com很长很长`, + pic: `pic${i}.png`, + disabled: i % 2 === 1 && i > 10 + }; + this.options.push(option); + } + } + myValidationConfig: TiValidationConfig = { + tip: 'should input email', + type: 'change' + }; + tipValue: string = ''; + close(): void {} + dismiss(): void {} +} diff --git a/src/modal/demo/src/app/modal/modal-animation.html b/src/modal/demo/src/app/modal/modal-animation.html new file mode 100644 index 0000000..b645039 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-animation.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-backdrop.html b/src/modal/demo/src/app/modal/modal-backdrop.html new file mode 100644 index 0000000..b645039 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-backdrop.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-class.html b/src/modal/demo/src/app/modal/modal-class.html new file mode 100644 index 0000000..7729540 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-class.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-close-icon.html b/src/modal/demo/src/app/modal/modal-close-icon.html new file mode 100644 index 0000000..b645039 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-close-icon.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-config-test.html b/src/modal/demo/src/app/modal/modal-config-test.html new file mode 100644 index 0000000..06ae2dd --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-config-test.html @@ -0,0 +1,24 @@ +

    描述

    +

    + modal组件,对外提供TiModalService服务,开发者将TiModalService注入到组件中,可得到服务实例tiModal。
    + 可以调用tiModal.open(TemplateRef, TiModalConfig)方法来打开弹框。
    + 该用例介绍tiModal.open方法的第二个参数的使用方式,参数说明见ModalConfigTestComponent.ts文件。 +

    +

    示例

    + + {{ context.headerTitle }} + + body + + + + + + + + + + + + +
    通过弹窗内部的按钮,改变弹窗外面的变量myNumber的当前值:{{myNumber}}
    diff --git a/src/modal/demo/src/app/modal/modal-content-comp.html b/src/modal/demo/src/app/modal/modal-content-comp.html new file mode 100644 index 0000000..4795567 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-content-comp.html @@ -0,0 +1,2 @@ + + diff --git a/src/modal/demo/src/app/modal/modal-content-temp.html b/src/modal/demo/src/app/modal/modal-content-temp.html new file mode 100644 index 0000000..b645039 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-content-temp.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-draggable.html b/src/modal/demo/src/app/modal/modal-draggable.html new file mode 100644 index 0000000..b645039 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-draggable.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-esc.html b/src/modal/demo/src/app/modal/modal-esc.html new file mode 100644 index 0000000..b645039 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-esc.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-event.html b/src/modal/demo/src/app/modal/modal-event.html new file mode 100644 index 0000000..311a01e --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-event.html @@ -0,0 +1,11 @@ + + modal title + body content + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-header-align.html b/src/modal/demo/src/app/modal/modal-header-align.html new file mode 100644 index 0000000..b645039 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-header-align.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-header-style.html b/src/modal/demo/src/app/modal/modal-header-style.html new file mode 100644 index 0000000..6b99129 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-header-style.html @@ -0,0 +1,22 @@ +

    描述

    +

    弹框头部关闭按钮可自定义,标题也可定义位置

    +

    示例

    + + + + I'm a {{modalName}} modal! + 跳过 + + + body content + + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-two-backdrop.html b/src/modal/demo/src/app/modal/modal-two-backdrop.html new file mode 100644 index 0000000..c6ba457 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-two-backdrop.html @@ -0,0 +1,6 @@ +

    描述

    +

    + 此场景修复的问题:两个弹窗同时打开,第一个弹窗没有遮罩,第二个弹窗有遮罩的场景。当关闭两个弹窗后,遮罩无法正常关闭的问题。10.1.13版本进行了修复 +

    +

    示例

    + diff --git a/src/modal/demo/src/app/modal/modal-two-test.html b/src/modal/demo/src/app/modal/modal-two-test.html new file mode 100644 index 0000000..854ba43 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-two-test.html @@ -0,0 +1,7 @@ +TODO:页面滚动条/modal内滚动条,单独写用例吧? +
    + + + + +
    diff --git a/src/modal/demo/src/app/modal/webdoc/modal-demos.js b/src/modal/demo/src/app/modal/webdoc/modal-demos.js new file mode 100644 index 0000000..5c7c2f8 --- /dev/null +++ b/src/modal/demo/src/app/modal/webdoc/modal-demos.js @@ -0,0 +1,138 @@ +export default { + column: '2', + demos: [ + { + demoId: 'modal-content-temp', + name: { + 'zh-CN': '弹出框内容为 template 形式', + 'en-US': 'template', + }, + desc: { + 'zh-CN': '

    使用了ng-template方式自定义弹出框模板。

    ', + 'en-US': '

    template

    ', + }, + apis: ['TiModalConfig.properties.id'], + }, + { + demoId: 'modal-content-comp', + name: { + 'zh-CN': '弹出框内容定义为组件形式', + 'en-US': 'component', + }, + desc: { + 'zh-CN': + '

    弹出框内容定义为组件形式时,弹出框中的内容组件需要在ngModule中定义其为entryComponents

    ', + 'en-US': '

    component

    ', + }, + apis: [ + 'TiModalConfig.properties.context', + 'TiModalConfig.methods.close', + 'TiModalConfig.methods.dismiss', + ], + }, + { + demoId: 'modal-class', + name: { + 'zh-CN': '自定义弹出框样式', + 'en-US': 'modalClass', + }, + desc: { + 'zh-CN': '

    通过modalClass自定义弹出框样式。

    ', + 'en-US': 'modalClass', + }, + apis: ['TiModalConfig.properties.modalClass'], + }, + { + demoId: 'modal-animation', + name: { + 'zh-CN': '显示/隐藏不带动画效果', + 'en-US': 'animation', + }, + desc: { + 'zh-CN': + '

    通过animation控制弹出框显示/隐藏是否带动画效果。

    ', + 'en-US': 'animation', + }, + apis: ['TiModalConfig.properties.animation'], + }, + { + demoId: 'modal-backdrop', + name: { + 'zh-CN': '不呈现模态背景', + 'en-US': 'backdrop', + }, + desc: { + 'zh-CN': '

    通过backdrop控制是否呈现模态背景。

    ', + 'en-US': 'backdrop', + }, + apis: ['TiModalConfig.properties.backdrop'], + }, + { + demoId: 'modal-close-icon', + name: { + 'zh-CN': '头部不呈现关闭按钮', + 'en-US': 'closeIcon', + }, + desc: { + 'zh-CN': '

    通过closeIcon头部是否有关闭按钮。

    ', + 'en-US': 'closeIcon', + }, + apis: ['TiModalConfig.properties.closeIcon'], + }, + { + demoId: 'modal-draggable', + name: { + 'zh-CN': '不可拖拽', + 'en-US': 'draggable', + }, + desc: { + 'zh-CN': '

    通过draggable控制弹出框是否可拖拽。

    ', + 'en-US': 'draggable', + }, + apis: ['TiModalConfig.properties.draggable'], + }, + { + demoId: 'modal-esc', + name: { + 'zh-CN': 'Esc 键弹出框不关闭', + 'en-US': 'closeOnEsc', + }, + desc: { + 'zh-CN': + '

    通过closeOnEsc控制按 Esc 键弹出框是否关闭。

    ', + 'en-US': 'closeOnEsc', + }, + apis: ['TiModalConfig.properties.closeOnEsc'], + }, + { + demoId: 'modal-header-align', + name: { + 'zh-CN': '头部水平居中', + 'en-US': 'headerAlign', + }, + desc: { + 'zh-CN': + '

    通过headerAlign配置头部内容水平对齐方式。

    ', + 'en-US': 'headerAlign', + }, + apis: ['TiModalConfig.properties.headerAlign'], + }, + { + demoId: 'modal-event', + name: { + 'zh-CN': '事件', + 'en-US': 'beforeClose', + }, + desc: { + 'zh-CN': + '

    通过beforeClose配置关闭弹出框前的回调函数。

    ', + 'en-US': 'beforeClose', + }, + apis: [ + 'TiModalConfig.methods.beforeClose', + 'TiModalConfig.methods.close', + 'TiModalConfig.methods.dismiss', + ], + }, + ], +}; diff --git a/src/modal/demo/src/app/modal/webdoc/modal.cn.md b/src/modal/demo/src/app/modal/webdoc/modal.cn.md new file mode 100644 index 0000000..f3ae981 --- /dev/null +++ b/src/modal/demo/src/app/modal/webdoc/modal.cn.md @@ -0,0 +1,34 @@ +--- +title: Modal 弹出框 +--- +# Modal 弹出框 + +
    + +Modal 是提供模态对话框的组件。   + ++ 弹出框组件提供服务方式供业务使用,使用该服务时需要引入模块`TiModalModule`,开发者通过调用`TiModalService.open`方法生成弹出框。 + +```typescript +import { TiModalModule } from '@opentiny/ng'; +``` + +需要在项目中(建议在根模块)中导入`BrowserAnimationsModule`。 + +```typescript +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +``` + +
    + +
    + +Modal 是提供模态对话框的组件。   + ++ 弹出框组件提供服务方式供业务使用,使用该服务时需要引入模块`TiModalModule`,开发者通过调用`TiModalService.open`方法生成弹出框。 + +```typescript +import { TiModalModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/modal/demo/src/app/modal/webdoc/modal.en.md b/src/modal/demo/src/app/modal/webdoc/modal.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/modal/demo/src/app/modal/webdoc/modal.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/modal/demo/src/favicon.ico b/src/modal/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/modal/demo/src/index.html b/src/modal/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/modal/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/modal/demo/src/main.ts b/src/modal/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/modal/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/modal/demo/test.ts b/src/modal/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/modal/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/modal/demo/tsconfig.app.json b/src/modal/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/modal/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/modal/demo/tsconfig.spec.json b/src/modal/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/modal/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/modal/lib/index.ts b/src/modal/lib/index.ts new file mode 100644 index 0000000..c68c346 --- /dev/null +++ b/src/modal/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiModalModule'; +export * from './src/TiModalService'; diff --git a/src/modal/lib/ng-package.json b/src/modal/lib/ng-package.json new file mode 100644 index 0000000..e042faa --- /dev/null +++ b/src/modal/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/modal", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/modal/lib/package.json b/src/modal/lib/package.json new file mode 100644 index 0000000..b292be5 --- /dev/null +++ b/src/modal/lib/package.json @@ -0,0 +1,18 @@ +{ + "name": "@opentiny/ng-modal", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/router": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/animations": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-popup": "~1.0.0-beta.0", + "@opentiny/ng-drag": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/modal/lib/project.json b/src/modal/lib/project.json new file mode 100644 index 0000000..b975f65 --- /dev/null +++ b/src/modal/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/modal/lib", + "sourceRoot": "src/modal/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/modal"], + "options": { + "project": "src/modal/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/modal"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js modal" + }, + { + "command": "ng default-build modal" + }, + { + "command": "node build/clear-default-theme.js modal" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/modal && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build modal && ng pack modal && node build/publish.js modal --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/modal/lib/src/TiBackdropComponent.ts b/src/modal/lib/src/TiBackdropComponent.ts new file mode 100644 index 0000000..7226e01 --- /dev/null +++ b/src/modal/lib/src/TiBackdropComponent.ts @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Input, Renderer2 } from '@angular/core'; +import { animate, state, style, transition, trigger } from '@angular/animations'; + +/** + * @ignore + */ +export const animateStyle: string = '0.3s cubic-bezier(0.40, 0.00, 0.20, 1.00)'; +/** + * @ignore + */ +@Component({ + selector: 'ti-backdrop', + template: `
    `, + styleUrls: ['./backdrop.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger('backdropAnimate', [ + state( + 'show', + style({ + opacity: 0.2 + }) + ), + state( + 'hide, void', + style({ + opacity: 0 + }) + ), + transition('show => hide', animate(animateStyle)), + transition('hide => show', animate(animateStyle)) + ]) + ] +}) +export class TiBackdropComponent { + constructor(protected renderer: Renderer2, protected changeDetectorRef: ChangeDetectorRef) {} + + protected _showState: string; // 遮罩的显示状态,用于控制动画的状态切换 + + set showState(value: string) { + if (this._showState !== value) { + this._showState = value; + // onpush 模式下,点击 ok / cancel 按钮关闭弹窗和遮罩时;改变模板变量 showState,需要手动触发变更检测来强制更新。 + this.changeDetectorRef.markForCheck(); + } + } + + get showState(): string { + return this._showState; + } + + public animation: boolean; // 是否开启动画 + public index: number = 0; // 多个弹出框情况下,zindex层级设置辅助 + public destroy: () => void; + // 在微前端,多个子应用之间url路径跳转,等到弹出框隐藏结束后,this.destroy() 已经无效。 + @HostListener('window:hashchange', ['$event']) onhashchange(event: HashChangeEvent): void { + if (event.newURL !== event.oldURL) { + this.destroy(); + } + } + onAnimationDone($event: any): void { + // 弹框隐藏动画结束后,销毁弹框DOM + if ($event.toState === 'hide') { + this.destroy(); + + return; + } + } +} diff --git a/src/modal/lib/src/TiBackdropNoAnimationComponent.ts b/src/modal/lib/src/TiBackdropNoAnimationComponent.ts new file mode 100644 index 0000000..fd7e569 --- /dev/null +++ b/src/modal/lib/src/TiBackdropNoAnimationComponent.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { TiBackdropComponent } from './TiBackdropComponent'; +/** + * @ignore + */ +@Component({ + selector: 'ti-backdrop-no-animation', + template: `
    `, + styleUrls: ['./backdrop.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiBackdropNoAnimationComponent extends TiBackdropComponent { + set showState(value: string) { + this._showState = value; + this.onAnimationDone({ toState: value }); + } +} diff --git a/src/modal/lib/src/TiModalBodyComponent.ts b/src/modal/lib/src/TiModalBodyComponent.ts new file mode 100644 index 0000000..dcaea1a --- /dev/null +++ b/src/modal/lib/src/TiModalBodyComponent.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +/** + * 弹框body容器标签,可将弹框body内容定义在该容器内 + */ +@Component({ + selector: 'ti-modal-body', + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-modal-body]': 'true' + } +}) +export class TiModalBodyComponent {} diff --git a/src/modal/lib/src/TiModalComponent.html b/src/modal/lib/src/TiModalComponent.html new file mode 100644 index 0000000..3c19d34 --- /dev/null +++ b/src/modal/lib/src/TiModalComponent.html @@ -0,0 +1,16 @@ +
    +
    +
    + + +
    +
    +
    diff --git a/src/modal/lib/src/TiModalComponent.ts b/src/modal/lib/src/TiModalComponent.ts new file mode 100644 index 0000000..f9eacd6 --- /dev/null +++ b/src/modal/lib/src/TiModalComponent.ts @@ -0,0 +1,351 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + AfterContentInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + HostListener, + Inject, + Optional, + Renderer2, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { NavigationEnd, Router, RouterEvent } from '@angular/router'; +import { filter } from 'rxjs/operators'; +import { Subscription } from 'rxjs'; +import { DOCUMENT } from '@angular/common'; + +import { animate, state, style, transition, trigger } from '@angular/animations'; +import { TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * @ignore + */ +export const animateStyle: string = '0.3s cubic-bezier(0.40, 0.00, 0.20, 1.00)'; +/** + * @ignore + */ +@Component({ + selector: 'ti-modal-wrapper', + templateUrl: './TiModalComponent.html', + styleUrls: ['./modal.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + animations: [ + // 由于动画创建需要在元数据中解析,因此动画定义必须在此处定义 + trigger('modalAnimate', [ + state( + 'show', + style({ + opacity: 1 + }) + ), + state( + 'hide, void', + style({ + opacity: 0 + }) + ), + transition('show => hide', animate(animateStyle)), + transition('hide => show', animate(animateStyle)) + ]), + trigger('modalPosAnimate', [ + state( + 'show', + style({ + transform: 'translate(0, 0px)' + }) + ), + state( + 'hide, void', + style({ + transform: 'translate(0, -6px)' + }) + ), + transition('show => hide', animate(animateStyle)), + transition('hide => show', animate(animateStyle)) + ]) + ] +}) +export class TiModalComponent extends TiBaseComponent implements AfterContentInit { + /** + * 规范刷新,弹出框整体的最大高度为660px(按照最小分辨率1280*768计算) + */ + private static readonly MODAL_MAX_HEIGHT: number = 660; + // 可通过tab聚焦的元素选择器列表 + private static readonly tababbleSelector: string = `a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), + button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), + textarea:not([disabled]):not([tabindex=\'-1\']), + iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]`; + + @ViewChild('modalContent', { static: true }) private contentRef: ElementRef; + @ViewChild('modalEle', { static: true }) protected modalEleRef: ElementRef; + + protected _showState: string; // 组件显示状态,用于控制动画的状态切换 + set showState(value: string) { + if (this._showState !== value) { + this._showState = value; + // onpush模式下,点击ok/cancel按钮关闭面板。改变模板变量showState,却没有触发更新。强制更新。 + this.changeDetectorRef.markForCheck(); + } + } + get showState(): string { + return this._showState; + } + public animation: boolean; // 是否有动画 + public draggable: boolean; // 是否可拖拽 + public closeOnEsc: boolean; // 是否可通过ESC键关闭弹框 + public modalInstance: any; // 组件实例,可使用该实例调用方法控制组件行为 + public index: number = 0; // 多个弹出框情况下,zindex层级设置辅助 + public closeIcon: boolean; // 是否有头部关闭按钮 + public modalClass: string; // 弹框样式设置 + public headerAlign: 'center' | 'left' | 'right'; // 头部对齐方式 + public dragOptions: any; // 拖拽配置 + protected versionInfo: string = super.getVersion(packageInfo); + private focusableElements: Array = []; // 弹框中可聚焦元素列表 + private modalSubscribe: Subscription; + constructor( + ele: ElementRef, + private tiRenderer: TiRenderer, + protected renderer: Renderer2, + @Optional() private router: Router, + protected changeDetectorRef: ChangeDetectorRef, + @Inject(DOCUMENT) private document + ) { + super(ele, renderer); + } + + private static preventEvent(event: Event): void { + event.preventDefault(); + event.stopPropagation(); + } + + // 路由切换,关闭弹出框: 前进/后退/更改url等跳转,通过routerLink/navigate路由跳转监听不到 + @HostListener('window:hashchange', ['$event']) onhashchange(event: HashChangeEvent): void { + if (event.newURL !== event.oldURL) { + this.modalInstance._remove(); + } + } + + // 缩放时重置弹框位置 + @HostListener('window:resize') onResize(): void { + this.tiRenderer.setStyles(this.contentRef.nativeElement, { + left: 0, + top: 0 + }); + } + // 键盘事件处理 + @HostListener('document:keydown', ['$event']) onKeydown(event: KeyboardEvent): void { + switch (event.which) { + case TiKeymap.KEY_ESCAPE: // close on ESC + if (this.closeOnEsc) { + this.dismissModal(); + } + break; + case TiKeymap.KEY_TAB: // tab键用于处理在弹框内循环获取焦点 + this.onTabChange(event); + break; + default: + break; + } + } + // 弹出框外围出现的滚动条滚动会导致下拉等定位在body上的组件相对于宿主元素定位偏移, + // 因此触发tiScroll事件做处理,事件包括: + // 整个页面滚动条滚轮滚动事件, mousewheel处理Chrome和IE下事件,DOMMouseScroll处理firefox下事件 + // 整个页面滚动条鼠标滚动事件 + @HostListener('document:mousewheel') + @HostListener('document:DOMMouseScroll') + @HostListener('window:scroll') + onTiScroll(): void { + Util.trigger(document, 'tiScroll'); + } + + ngAfterContentInit(): void { + this.dragOptions = { + // 由于此处需要获取ngContent中的内容,因此在该时机中进行处理 + // 这时弹出框内容DOM还未解析,所以需要使用标签选择器('ti-modal-header')来获取元素 + handle: this.nativeElement.querySelector('ti-modal-header'), + drag: (opts: { + position: { + left: number; + top: number; + }; + helper: Element; + }): { left: number; top: number } => { + Util.trigger(document, 'tiScroll'); + // 当前浏览器可视区域的宽高 + const curBrowseWidth: number = document.documentElement.clientWidth; + const curBrowseheight: number = document.documentElement.clientHeight; + // 元素原始位置(由于元素本身left和top实时变化,因此取其父元素位置) + const eleRect: ClientRect | DOMRect = opts.helper.parentElement.getBoundingClientRect(); + // 元素距页面可视区域的可用位置范围 + const minLeft: number = -eleRect.left + document.body.scrollLeft; + const maxLeft: number = Math.max(curBrowseWidth + minLeft - eleRect.width, minLeft); + const minTop: number = -eleRect.top + document.body.scrollTop; + const maxTop: number = Math.max(curBrowseheight + minTop - eleRect.height, minTop); + const position: { left: number; top: number } = opts.position; + // 元素left位置根据可用left范围计算 + if (position.left < minLeft) { + position.left = minLeft; + } else if (position.left > maxLeft) { + position.left = maxLeft; + } + // 元素top位置根据可用top范围计算 + if (position.top < minTop) { + position.top = minTop; + } else if (position.top > maxTop) { + position.top = maxTop; + } + + return position; + } + }; + // 监听路由变化,关闭弹出框:hashchange监听不到通过routerLink/navigate路由跳转的场景 + this.modalSubscribe = this.router?.events.pipe(filter((event: RouterEvent) => event instanceof NavigationEnd)).subscribe(() => { + this.modalInstance._remove(); + }); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + this.modalSubscribe && this.modalSubscribe.unsubscribe(); + } + + public dismissModal(): void { + this.modalInstance.dismiss(); + } + + public onAnimationDone($event: any): void { + // 弹框toState有三种状态:'show'/'hide'/null + // 弹框显示动画结束后,重新将焦点聚焦在弹框元素/弹框中 + if ($event.toState === 'show') { + this.reStyleModal(); + this.focusInModal(); + + return; + } + // 弹框隐藏动画结束后,销毁弹框DOM + if ($event.toState === 'hide') { + this.modalInstance._remove(); + } + } + + // 弹框打开时,设置弹框元素样式 + private reStyleModal(): void { + const parentEle: Element = this.nativeElement.querySelector('.ti3-modal-header'); + if (this.draggable !== false) { + this.renderer.setStyle(parentEle, 'cursor', 'move'); + } + if (this.headerAlign === 'center' || this.headerAlign === 'right') { + this.renderer.setStyle(parentEle, 'text-align', this.headerAlign); + } + + this.renderer.addClass(this.document.body, 'ti3-modal-open'); + this.setMaxHeight(); + } + /** + * 设置弹框高度: + * 根据UCD3.0规范,modal最大高度不能超过弹框宽度,所以需要获取到modal的宽度值,用其减去modal的header和footer的高度 + * 后设为body的最大高度,当高度超出时,ti-modal-body垂直方向上出现滚动条 + * 10.1.1变更说明: + * 根据规范变更,弹框高度可以超过弹框宽度,同时弹出框的最大高度是660px。 + */ + private setMaxHeight(): void { + // 修复SSR问题:ERROR TypeError: modalHeaderEle.getBoundingClientRect is not a function + const modalHeaderEle: Element = this.nativeElement.querySelector('.ti3-modal-header'); + const modalHeaderHeight: number = + modalHeaderEle && typeof modalHeaderEle.getBoundingClientRect === 'function' ? modalHeaderEle.getBoundingClientRect().height : 0; + const modalFooterEle: Element = this.nativeElement.querySelector('.ti3-modal-footer'); + const modalFooterHeight: number = + modalFooterEle && typeof modalFooterEle.getBoundingClientRect === 'function' ? modalFooterEle.getBoundingClientRect().height : 0; + const modalBodyMaxHeight: number = TiModalComponent.MODAL_MAX_HEIGHT - modalHeaderHeight - modalFooterHeight; + this.renderer.setStyle(this.nativeElement.querySelector('.ti3-modal-body'), 'maxHeight', modalBodyMaxHeight + 'px'); + } + private focusInModal(): void { + /** + * 首先判断新打开的modal是否已存在获取焦点的元素 + * 如果存在,则需要对modal元素再次设置焦点 + * 否则,对modal容器设置焦点 + */ + const activeElement: any = this.document.activeElement; + const isfocusedInModal: boolean = activeElement && this.nativeElement.contains(activeElement); + if (isfocusedInModal) { + /** + * 对已经设置焦点的元素,再次设置焦点,是为了解决以下两个问题: + * 1>首次加载时,已经获取焦点的元素,其输入tip提示位置偏移 + * 2>首次加载时,IE浏览器中,已经获取焦点的元素input,光标位置出现偏移 + */ + if (activeElement) { + activeElement.blur(); + // tiautofocusinmodal属性需要在change校验中使用(ChangeCheck.ts) + this.renderer.setAttribute(activeElement, 'tiautofocusinmodal', ''); + activeElement.focus(); + this.renderer.removeAttribute(activeElement, 'tiautofocusinmodal'); + } + } else { + // 弹出框内未找到已聚焦元素情况下,优先处理 input 和 textarea 原生标签autofocus聚焦 + // (input,textarea.button原生标签的autofocus弹出框打开时无效(chrome是从第二次打开无效),因此要做此处理) + let focusEle: any = this.nativeElement.querySelector(`input[autofocus]:not([disabled]):not([tabindex=\'-1\']), + textarea[autofocus]:not([disabled]):not([tabindex=\'-1\']), button[autofocus]:not([disabled]):not([tabindex=\'-1\'])`); + // 如果通过上述处理,聚焦元素仍未获取到的情况下,则可聚焦元素为当前弹框 + focusEle = focusEle || this.modalEleRef.nativeElement; + focusEle.focus(); + } + } + // 用于处理tab键行为,确保tab时只可在弹框内循环获取焦点 + private onTabChange(event: KeyboardEvent): void { + const focusableElementList: Array = this.getFocusableElesInModal(); // 获取当前弹出框DOM中可以获取焦点的元素列表 + const focusableListLength: number = focusableElementList.length; + if (focusableListLength === 0) { + return; + } + const target: EventTarget = event.target; // 事件对象元素 + const firstFocusableEle: any = focusableElementList[0]; // 第一个可获取焦点的元素 + const lastFocusableEle: any = focusableElementList[focusableListLength - 1]; // 最后一个可获取焦点的元素 + // 按下tab+shift键时,如果当前已获取焦点元素是弹出框中的第一个可获取焦点元素或弹框本身,则聚焦最后一个元素 + if (event.shiftKey) { + if (target === firstFocusableEle || target === this.modalEleRef.nativeElement) { + lastFocusableEle.focus(); + TiModalComponent.preventEvent(event); // 阻止默认事件及继续冒泡,确保此处手动focus生效 + } + + return; + } + // 按下tab键时,如当前已获取焦点元素是弹框中最后一个可获取焦点元素,则聚焦第一个元素 + if (target === lastFocusableEle) { + firstFocusableEle.focus(); + TiModalComponent.preventEvent(event); + } + } + private getFocusableElesInModal(): Array { + if (this.focusableElements.length !== 0) { + return this.focusableElements; + } + const contentEle: Element = this.contentRef.nativeElement; + if (contentEle) { + const elements: NodeListOf = contentEle.querySelectorAll(TiModalComponent.tababbleSelector); + [].forEach.call(elements, (element: any) => { + // 找出页面真实显示的元素,只有显示的元素才可tab聚焦 + if (getComputedStyle(element).display !== 'none' && element.offsetWidth > 0 && element.offsetHeight > 0) { + this.focusableElements.push(element); + } + }); + } + + return this.focusableElements; + } +} diff --git a/src/modal/lib/src/TiModalComponentNoAnimation.html b/src/modal/lib/src/TiModalComponentNoAnimation.html new file mode 100644 index 0000000..88f9516 --- /dev/null +++ b/src/modal/lib/src/TiModalComponentNoAnimation.html @@ -0,0 +1,15 @@ +
    +
    +
    + + +
    +
    +
    diff --git a/src/modal/lib/src/TiModalFooterComponent.ts b/src/modal/lib/src/TiModalFooterComponent.ts new file mode 100644 index 0000000..893fe70 --- /dev/null +++ b/src/modal/lib/src/TiModalFooterComponent.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +/** + * 弹框header容器标签,可将弹框底部内容定义在该容器内 + */ +@Component({ + selector: 'ti-modal-footer', + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-modal-footer]': 'true' + } +}) +export class TiModalFooterComponent {} diff --git a/src/modal/lib/src/TiModalHeaderComponent.ts b/src/modal/lib/src/TiModalHeaderComponent.ts new file mode 100644 index 0000000..32163ee --- /dev/null +++ b/src/modal/lib/src/TiModalHeaderComponent.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +/** + * 弹框header容器标签,可将弹框头部内容定义在该容器内 + */ +@Component({ + selector: 'ti-modal-header', + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-modal-header]': 'true' + } +}) +export class TiModalHeaderComponent {} diff --git a/src/modal/lib/src/TiModalInterface.ts b/src/modal/lib/src/TiModalInterface.ts new file mode 100644 index 0000000..dd33c11 --- /dev/null +++ b/src/modal/lib/src/TiModalInterface.ts @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ComponentRef, Injector } from '@angular/core'; + +/** + * 弹出框组件生成配置参数接口 + * + * 该接口用于弹出框组件[open]{@link TiModalService#open}方法的参数的定义 + */ +export interface TiModalConfig { + /** + * 弹出框 id 唯一标识,避免同一页面生成多个相同弹出框 + */ + id?: string; + /** + * 头部内容水平对齐方向 + * + * @default 'left' + */ + headerAlign?: 'center' | 'left' | 'right'; // 头部内容水平对齐位置 + /** + * 是否有关闭按钮 + * + * @default true + */ + closeIcon?: boolean; + /** + * 弹出框样式定义,该样式定义在弹出框整体元素上,可通过该样式定义弹出框的宽高等 + */ + modalClass?: string; + /** + * 是否显示模态背景 + * + * @default true + */ + backdrop?: boolean; + /** + * 是否可拖拽 + * + * @default true + */ + draggable?: boolean; + /** + * 弹出框显示/隐藏是否带动画效果 + * + * @default true + */ + animation?: boolean; + /** + * 弹出框内容上下文,为自定义对象形式 + */ + context?: any; + /** + * 是否可通过 ESC 快捷键关闭弹出框 + * + * @default true + */ + closeOnEsc?: boolean; + /** + * 弹出框内容为组件时,内容组件的父级注入器 + */ + parentInjector?: Injector; + /** + * 关闭弹出框前的回调函数,可通过 beforeClose 事件阻止弹出框关闭:在 beforeClose 中通过 modalRef 调用 destroy 时,关闭弹出框,否则不关闭 + * + * 参数:TiModalRef:弹出框实例 reason:弹出框关闭原因(调用 close 关闭时,reason 为 true;调用 dismiss 关闭时,reason 为 false) + * + */ + beforeClose?(modalRef: TiModalRef, reason: boolean): void; + /** + * 关闭弹出框的事件回调(一般点确认时会进入该回调),参数:modalRef 弹出框实例 + */ + close?(modalRef: TiModalRef): void; + /** + * 关闭弹出框的事件回调(一般点击取消或右上角X关闭时会进入该回调),参数:modalRef 弹出框实例 + */ + dismiss?(modalRef: TiModalRef): void; +} + +/** + * 弹出框组件实例对象接口 + * + * 该接口用于弹出框组件[open]{@link TiModalService#open}方法的返回值定义 + */ +export interface TiModalRef { + /** + * 弹出框内容实例,适用于component形式的内容数据获取,具体用法可参考示例 + */ + content: ComponentRef; // 弹出框内容实例,可通过该实例获取content对应的相关数据,component形式的模板数据获取需要使用该方式 + /** + * @ignore + */ + _id: string; // 弹出框ID存储,该id用来标识弹出框的唯一性 + /** + * @ignore + */ + _backdrop: boolean; // 用于标记当前弹出框是否有遮罩 + /** + * 弹出框关闭方法,主要用在点击"确认(OK)"按钮时关闭弹出框 + * + * **函数类型:**() => void; + */ + close(): void; // 弹出框实例的方法,用来关闭弹出框,主要用在点击"确认(OK)"按钮时关闭弹出框 + /** + * 弹出框关闭方法,主要用在点击"取消(Cancel)"或右上方X按钮时关闭弹出框 + * + * **函数类型:**() => void; + */ + dismiss(): void; // 弹出框实例的方法,用来关闭弹出框,与close()的区别是:dismiss()主要用在点击"取消(Cancel)"按钮时关闭弹出框 + /** + * 弹出框销毁方法 + * + * **函数类型:**(reason: boolean) => void; + * + * **参数:** + * + * 弹出框关闭原因 boolean,同beforeClose参数中reason的意义 + */ + destroy(reason: boolean): void; // 销毁弹出框,外部调用 + /** + * @ignore + * 弹出框销毁方法,无关取消/关闭事件等 + */ + _remove(): void; +} diff --git a/src/modal/lib/src/TiModalModule.ts b/src/modal/lib/src/TiModalModule.ts new file mode 100644 index 0000000..b15e4d7 --- /dev/null +++ b/src/modal/lib/src/TiModalModule.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiPopupModule } from '@opentiny/ng-popup'; +import { TiModalComponent } from './TiModalComponent'; +import { TiModalNoAnimationComponent } from './TiModalNoAnimationComponent'; +import { TiBackdropComponent } from './TiBackdropComponent'; +import { TiBackdropNoAnimationComponent } from './TiBackdropNoAnimationComponent'; +import { TiModalHeaderComponent } from './TiModalHeaderComponent'; +import { TiModalBodyComponent } from './TiModalBodyComponent'; +import { TiModalFooterComponent } from './TiModalFooterComponent'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiDraggableModule } from '@opentiny/ng-drag'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; + +@NgModule({ + imports: [CommonModule, TiDraggableModule, TiIconModule, TiPopupModule, TiOutlineModule, TiRendererModule], + exports: [ + TiModalComponent, + TiModalNoAnimationComponent, + TiBackdropNoAnimationComponent, + TiModalHeaderComponent, + TiModalFooterComponent, + TiModalBodyComponent + ], + declarations: [ + TiModalComponent, + TiModalNoAnimationComponent, + TiBackdropNoAnimationComponent, + TiModalHeaderComponent, + TiModalFooterComponent, + TiModalBodyComponent, + TiBackdropComponent + ], + entryComponents: [TiModalComponent, TiModalNoAnimationComponent, TiBackdropNoAnimationComponent, TiBackdropComponent] +}) +export class TiModalModule {} + +export * from './TiModalInterface'; +export { TiModalHeaderComponent } from './TiModalHeaderComponent'; +export { TiModalFooterComponent } from './TiModalFooterComponent'; +export { TiModalBodyComponent } from './TiModalBodyComponent'; +export { TiModalComponent } from './TiModalComponent'; +export { TiModalNoAnimationComponent } from './TiModalNoAnimationComponent'; +export { TiBackdropNoAnimationComponent } from './TiBackdropNoAnimationComponent'; diff --git a/src/modal/lib/src/TiModalNoAnimationComponent.ts b/src/modal/lib/src/TiModalNoAnimationComponent.ts new file mode 100644 index 0000000..a10d8ff --- /dev/null +++ b/src/modal/lib/src/TiModalNoAnimationComponent.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ViewEncapsulation, ChangeDetectionStrategy } from '@angular/core'; +import { TiModalComponent } from './TiModalComponent'; +/** + * @ignore + */ +@Component({ + selector: 'ti-modal-wrapper-no-animation', + templateUrl: './TiModalComponentNoAnimation.html', + styleUrls: ['./modal.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None +}) +export class TiModalNoAnimationComponent extends TiModalComponent { + set showState(value: string) { + this._showState = value; + this.onAnimationDone({ toState: value }); + } +} diff --git a/src/modal/lib/src/TiModalService.ts b/src/modal/lib/src/TiModalService.ts new file mode 100644 index 0000000..1376c6e --- /dev/null +++ b/src/modal/lib/src/TiModalService.ts @@ -0,0 +1,244 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ComponentRef, Injectable, TemplateRef } from '@angular/core'; +import { TiPopUpRef, TiPopupService } from '@opentiny/ng-popup'; +import { TiBackdropComponent } from './TiBackdropComponent'; +import { TiBackdropNoAnimationComponent } from './TiBackdropNoAnimationComponent'; +import { TiModalComponent } from './TiModalComponent'; +import { TiModalNoAnimationComponent } from './TiModalNoAnimationComponent'; +import { TiBrowser, Util } from '@opentiny/ng-utils'; +import { TiModalModule } from './TiModalModule'; +import { TiModalConfig, TiModalRef } from './TiModalInterface'; + +/** + * 弹框组件提供服务方式供业务使用,使用该服务时需要引入模块TiModalModule,开发者通过调用TiModalService.open方法生成弹出框 + * + *

    使用此组件时需要开发者在项目模块(建议在根模块) + * 中引入BrowserAnimationsModule。这是因为此组件中使用了Angular动画,需要引入BrowserAnimationsModule, + * 但是 BrowserAnimationsModule 不能在懒加载模块被重复引入,所以需要开发者来引入BrowserAnimationsModule,保证其引入一次。

    + * + */ +@Injectable({ + providedIn: TiModalModule +}) +export class TiModalService { + /** + * 页面中存在的所有弹框实例,用于处理业务中无法明确当前有多少有效弹出框场景 + */ + public openedModals: Array = []; // 所有当前处于打开状态弹框缓存数组 + private backdropComponentRef: ComponentRef = null; // 单个页面中多个弹框出现时,只允许存在一个遮罩层,因此使用该标志位用来缓存遮罩实例 + private defaultConfig: TiModalConfig = { + backdrop: true, + draggable: true, + animation: true, + closeOnEsc: true, + closeIcon: true + }; + constructor(private popService: TiPopupService) {} + + /** + * 生成弹框方法 + * + * @param content 弹框内容,支持TemplateRef及组件形式,内容由ti-modal-header、ti-modal-body及ti-modal-footer组件构成弹框内容整体。 + * 不支持字符串形式,如果误传入字符串形式,不仅会有报错,还会存在XSS攻击风险,不过XSS攻击风险已在10.1.4版本已处理。 + * @param [config] 弹框配置信息 + * @returns 弹框实例信息,使用该实例信息可调用弹框的close等方法;弹框内容为component类型时,也可通过该实例信息的content属性获取弹框内容数据 + */ + public open(content: TemplateRef | any, config?: TiModalConfig): TiModalRef { + let modalInstance: TiModalRef; // 弹框最终返回的实例对象 + let modalComponentRef: ComponentRef; // 弹框组件实例,使用该实例获取弹框DOM元素及做属性赋值操作 + const modalConfig: TiModalConfig = { ...this.defaultConfig, ...config }; // 弹框配置合并 + let modal: { + modalComponentRef: ComponentRef; + hide(): void; + }; // generateModal方法返回值 + + // 通过id唯一标识防止重复打开相同弹框,id重复情况下返回先前实例 + if (modalConfig && !Util.isUndefined(modalConfig.id)) { + const index: number = this.openedModals.findIndex((item: TiModalRef) => { + return item._id === modalConfig.id; + }); + if (index !== -1) { + return this.openedModals[index]; + } + } + // 根据beforeClose返回处理弹出框的销毁:业务在beforeClose中调用弹框销毁 + const destroy: (reason: boolean) => void = (reason: boolean): void => { + if (typeof modalConfig.beforeClose === 'function') { + modalConfig.beforeClose(modalInstance, reason); + } else { + destroyModal(reason); + } + }; + // 销毁弹框:通过调用组件的动画状态方式实现动画处理 + const destroyModal: (reason?: boolean) => void = (reason?: boolean): void => { + if (reason) { + if (typeof modalConfig.close === 'function') { + modalConfig.close(modalInstance); + } + } else { + if (typeof modalConfig.dismiss === 'function') { + modalConfig.dismiss(modalInstance); + } + } + modalComponentRef.instance.showState = 'hide'; + }; + // 销毁弹框实体:弹框动画执行完成后调用该方法 + const removeModalEle: () => void = (): void => { + // 已经销毁,不做处理 + if (modalInstance === null) { + return; + } + + // 销毁弹框实体DOM + modal.hide(); + // 移除缓存实例 + const index: number = this.openedModals.indexOf(modalInstance); + this.openedModals.splice(index, 1); + modalInstance = null; + + // 销毁backdrop + destroyBackdrop(); + }; + const destroyBackdrop: () => void = (): void => { + // backdrop定义为false时,不进行处理 + // backdropComponentRef 已经销毁 + if (!modalConfig.backdrop || !this.backdropComponentRef) { + return; + } + // 修改backdrop zIndex,确保有多个弹出框情况下,最外层弹出框不被遮挡 + this.backdropComponentRef.instance.index = this.openedModals.length - 1; + /** + * 问题:两个弹出框,第一个打开的弹出框无遮罩,再此基础上打开第二个弹出框,第二个弹出框有遮罩。 + * 此时关闭第二个弹出框,关闭是有遮罩的弹出框,此处判断还有一个弹出框实例,遮罩没有关闭。 + * 关闭第一个弹出框时,因为没有设置遮罩,modalConfig.backdrop为false,关闭遮罩的逻辑就没执行,此时的现象是两个弹出框已经关闭了,但是遮罩无法关闭。 + * 解决:过滤出有遮罩的弹出框,如果还有打开的弹出框,配置了遮罩,则不关闭遮罩, + * 如果打开的弹出框,没有配置遮罩,那就直接关闭遮罩。 + */ + // 当前还有其它弹框情况下且其他弹出框也有遮罩时,不用销毁遮罩实例 + if (this.getOpenedModalsWithBackdrop().length !== 0) { + return; + } + this.backdropComponentRef.instance.showState = 'hide'; + }; + // 将当前要打开的modal放到openedModals列表中 + modalInstance = { + _id: modalConfig && modalConfig.id, + _backdrop: modalConfig && modalConfig.backdrop, + close(): void { + destroy(true); + }, + dismiss(): void { + destroy(false); + }, + destroy(reason: boolean): void { + destroyModal(reason); + }, + _remove: removeModalEle, + content: null + }; + modal = this.generateModal(content, modalConfig, modalInstance); + // 生成弹框window,并返回对应的组件实例 + modalComponentRef = modal.modalComponentRef; + // 生成模态背景backdrop + this.backdropComponentRef = this.generateBackdrop(modalConfig); + + return modalInstance; + } + + // 在打开的弹出框中,获取有遮罩的弹出框集合 + private getOpenedModalsWithBackdrop(): Array { + let backdropModal: Array = []; + backdropModal = this.openedModals.filter((item: TiModalRef) => { + return item._backdrop; + }); + + return backdropModal; + } + + private generateModal( + content: any, + config: TiModalConfig, + modalInstance: TiModalRef + ): { + modalComponentRef: ComponentRef; + hide(): void; + } { + const { context, ...modalConfig }: TiModalConfig = config; + // tiModalComponent生成需要使用到的上下文 + const modalContext: any = { + modalInstance, + index: this.openedModals.length, + ...modalConfig + }; + // 内容部分需要使用到的上下文 + const contentContext: any = { + close: modalInstance.close, + dismiss: modalInstance.dismiss, + ...context + }; + let modalComponent: any; + modalComponent = + TiBrowser.isIE() && TiBrowser.version() === 9 + ? this.popService.create(TiModalNoAnimationComponent) + : this.popService.create(TiModalComponent); + const modalComponentRef: any = modalComponent.show({ + content, + context: modalContext, + contentContext, + contentParentInjector: config.parentInjector, + container: 'body' + }); + // 控制元素动画呈现,元素呈现后修改动画状态才会有动画效果,因此在此处处理 + modalComponentRef.instance.showState = 'show'; + modalInstance.content = modalComponentRef.tiContentRef.componentRef; + this.openedModals.push(modalInstance); + + return { modalComponentRef, hide: modalComponent.hide }; + } + private generateBackdrop(config: TiModalConfig): ComponentRef { + // 配置中不需要backdrop,不生成 + if (!config.backdrop) { + return this.backdropComponentRef; + } + + if (this.backdropComponentRef) { + // backdrop已存在的情况下,修改其zIndex,并确保不生成backdrop + if (this.backdropComponentRef.instance.showState === 'show') { + this.backdropComponentRef.instance.index = this.openedModals.length - 1; + + return this.backdropComponentRef; + // 如果backdrop正在销毁(动画执行期间)时再新生成一个backdrop,这时应该将旧的backdrop立即销毁 + } else { + this.backdropComponentRef.destroy(); + } + } + const backdropComponent: TiPopUpRef = + TiBrowser.isIE() && TiBrowser.version() === 9 + ? this.popService.create(TiBackdropNoAnimationComponent) + : this.popService.create(TiBackdropComponent); + const backdropRef: ComponentRef = backdropComponent.show({ + context: { + animation: config.animation, + destroy: (): void => { + backdropComponent.hide(); + this.backdropComponentRef = null; + } + }, + container: 'body' + }); + // 控制元素动画呈现,元素呈现后修改动画状态才会有动画效果,因此在此处处理 + backdropRef.instance.showState = 'show'; + + return backdropRef; + } +} diff --git a/src/modal/lib/src/backdrop.less b/src/modal/lib/src/backdrop.less new file mode 100644 index 0000000..0efc0a1 --- /dev/null +++ b/src/modal/lib/src/backdrop.less @@ -0,0 +1,17 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-backdrop-bg-color: #000000; // 遮罩层颜色 +} + +.ti3-modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: @zindex-modal-background; + box-sizing: border-box; + background-color: var(--ti-backdrop-bg-color); + opacity: 0.2; +} diff --git a/src/modal/lib/src/modal.less b/src/modal/lib/src/modal.less new file mode 100644 index 0000000..e35ece8 --- /dev/null +++ b/src/modal/lib/src/modal.less @@ -0,0 +1,114 @@ +@import '../../../themes/basic/base-all.less'; + +ti-modal-wrapper { + --ti-modal-width: 550px; +} + +// Container that the modal scrolls within +.ti3-modal { + /* Positioning */ + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: @zindex-modal; + + /* Display & Box Model */ + .box-sizing(border-box); + overflow: hidden; + + /* Other */ + -webkit-overflow-scrolling: touch; + // Prevent Chrome on Windows from adding a focus outline. + outline: 0; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); +} + +.ti3-modal-open .ti3-modal { + overflow-x: hidden; + overflow-y: auto; +} + +// Shell div to position the modal with bottom padding +.ti3-modal-dialog { + position: relative; + .box-sizing(border-box); + width: auto; + margin: 10px; +} + +// Actual modal +.ti3-modal-content { + position: relative; + .box-sizing(border-box); + .border-radius(var(--ti-common-border-radius-normal)); + background-color: var(--ti-common-color-bg-white-normal); + //更改阴影颜色 + .box-shadow(var(--ti-common-shadow-4-down)); + background-clip: padding-box; + // Remove focus outline from opened modal + outline: 0; +} + +// Modal header +.ti3-modal-header { + /* Display & Box Model */ + .box-sizing(border-box); + padding: var(--ti-common-space-8x) var(--ti-common-space-8x) var(--ti-common-space-7x); + color: var(--ti-common-color-text-primary); + font-size: var(--ti-common-font-size-3); + font-weight: var(--ti-common-font-weight-7); + line-height: 100%; + display: block; + &.ti3-modal-title-center { + text-align: center; + } +} + +.ti3-modal-close { + position: absolute; + top: var(--ti-common-space-2x); + right: var(--ti-common-space-5x); + .box-sizing(border-box); + color: var(--ti-common-color-icon-normal); + cursor: pointer; + font: normal normal normal var(--ti-common-size-3x) / var(--ti-common-size-9x) ti3Font; + text-transform: none; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + &:hover { + color: var(--ti-common-color-icon-hover); + } +} + +// Modal body(sibling of .modal-header and .modal-footer) +.ti3-modal-body { + position: relative; + overflow-y: auto; + .box-sizing(border-box); + display: block; + padding: var(--ti-common-space-0) var(--ti-common-space-8x); +} + +// Footer (for actions) +.ti3-modal-footer { + padding: var(--ti-common-space-7x) var(--ti-common-space-8x) var(--ti-common-space-8x); + text-align: center; // center align buttons + display: block; + + & button:nth-of-type(2) { + margin-left: var(--ti-common-space-10); // 默认与HEC规范保持一致 + } +} + +// Scale up the modal +@media screen and (min-width: 768px) { + // Automatically set modal's width for larger viewports + .ti3-modal-dialog { + width: var(--ti-modal-width); + margin: 150px auto 0; + } +} diff --git a/src/nav/demo/karma.conf.js b/src/nav/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/nav/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/nav/demo/project.json b/src/nav/demo/project.json new file mode 100644 index 0000000..c3ba393 --- /dev/null +++ b/src/nav/demo/project.json @@ -0,0 +1,98 @@ +{ + "projectType": "application", + "root": "src/nav/demo", + "sourceRoot": "src/nav/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/nav", + "index": "src/nav/demo/src/index.html", + "main": "src/nav/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/nav/demo/tsconfig.app.json", + "assets": [ + "src/nav/demo/src/favicon.ico", + "src/nav/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/nav_logo", + "output": "/assets/nav_logo/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "nav-demo:build:production" + }, + "development": { + "browserTarget": "nav-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js nav" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/nav/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/nav/demo/tsconfig.spec.json", + "karmaConfig": "src/nav/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/nav/demo/src/app/AppComponent.ts b/src/nav/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/nav/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/nav/demo/src/app/AppModule.ts b/src/nav/demo/src/app/AppModule.ts new file mode 100644 index 0000000..c58c586 --- /dev/null +++ b/src/nav/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { NavTestModule } from './nav/NavTestModule'; + +@NgModule({ + imports: [ + NavTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/nav/demo/src/app/IndexComponent.ts b/src/nav/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..6e546db --- /dev/null +++ b/src/nav/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { NavTestModule } from './nav/NavTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = NavTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/nav/demo/src/app/app.html b/src/nav/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/nav/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/nav/demo/src/app/nav/NavActiveComponent.ts b/src/nav/demo/src/app/nav/NavActiveComponent.ts new file mode 100644 index 0000000..4afdf98 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavActiveComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-active.html' +}) +export class NavActiveComponent { + activeId: string = 'button'; + items: TiNavMenuItem[] = [ + { label: '首页', id: 'home' }, + { label: '组件总览', id: 'component' }, + { + label: '使用指南', + id: 'doc', + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start' }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本框', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ]; + items2: TiNavMenuItem[] = [ + { label: '首页', id: 'home' }, + { + label: '组件总览', + id: 'component', + children: [ + { + label: '使用指南', + id: 'doc', + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start' }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本框', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ] + } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavAlignComponent.ts b/src/nav/demo/src/app/nav/NavAlignComponent.ts new file mode 100644 index 0000000..07e1ba7 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavAlignComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-align.html' +}) +export class NavAlignComponent { + items: TiNavMenuItem[] = [ + { id: '1', label: '首页' }, + { id: '2', label: '文档' }, + { id: '3', label: '组件' }, + { id: '4', label: '关于' } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavBasicComponent.ts b/src/nav/demo/src/app/nav/NavBasicComponent.ts new file mode 100644 index 0000000..900b91d --- /dev/null +++ b/src/nav/demo/src/app/nav/NavBasicComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-basic.html' +}) +export class NavBasicComponent { + items: TiNavMenuItem[] = [ + { id: '1', label: '首页' }, + { id: '2', label: '文档' }, + { id: '3', label: '组件' }, + { id: '4', label: '关于' } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavDisabledComponent.ts b/src/nav/demo/src/app/nav/NavDisabledComponent.ts new file mode 100644 index 0000000..3ff0e34 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavDisabledComponent.ts @@ -0,0 +1,85 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-disabled.html' +}) +export class NavDisabledComponent { + items: TiNavMenuItem[] = [ + { label: '首页', id: 'home' }, + { label: '组件总览', id: 'component', disabled: true }, + { + label: '使用指南', + id: 'doc', + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start', disabled: true }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + disabled: true, + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本框', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ]; + items2: TiNavMenuItem[] = [ + { label: '首页', id: 'home', disabled: true }, + { + label: '组件总览', + id: 'component', + children: [ + { + label: '使用指南', + id: 'doc', + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start', disabled: true }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + disabled: true, + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本框', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ] + } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavEventComponent.ts b/src/nav/demo/src/app/nav/NavEventComponent.ts new file mode 100644 index 0000000..b800ca8 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavEventComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-event.html' +}) +export class NavEventComponent { + logs: string[] = []; + onSelect({ item, itemPath }: { item: TiNavMenuItem; itemPath: TiNavMenuItem[] }) { + this.logs.push(`选择:${item.label},菜单路径:${itemPath.map((item) => item.label).join(' -> ')}`); + } + + activeId: string = 'home'; + items: TiNavMenuItem[] = [ + { label: '首页', id: 'home' }, + { label: '组件总览', id: 'component' }, + { + label: '使用指南', + id: 'doc', + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start' }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本框', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavLeftComponent.ts b/src/nav/demo/src/app/nav/NavLeftComponent.ts new file mode 100644 index 0000000..9082e15 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavLeftComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-left.html' +}) +export class NavLeftComponent { + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL']; + logoUrl: string = `${this.baseUrl}assets/nav_logo/logo.png`; + items: TiNavMenuItem[] = [ + { id: '1', label: '首页' }, + { id: '2', label: '文档' }, + { id: '3', label: '组件' }, + { id: '4', label: '关于' } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavRightComponent.ts b/src/nav/demo/src/app/nav/NavRightComponent.ts new file mode 100644 index 0000000..154de61 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavRightComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-right.html' +}) +export class NavRightComponent { + items: TiNavMenuItem[] = [ + { id: '1', label: '首页' }, + { id: '2', label: '文档' }, + { id: '3', label: '组件' }, + { id: '4', label: '关于' } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavSelectableComponent.ts b/src/nav/demo/src/app/nav/NavSelectableComponent.ts new file mode 100644 index 0000000..91e6ecd --- /dev/null +++ b/src/nav/demo/src/app/nav/NavSelectableComponent.ts @@ -0,0 +1,91 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-selectable.html' +}) +export class NavSelectableComponent { + activeId: string = 'component'; + items: TiNavMenuItem[] = [ + { label: '首页', id: 'home' }, + { label: '组件总览', id: 'component' }, + { + label: '使用指南', + id: 'doc', + selectable: false, + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start' }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + selectable: false, + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + selectable: false, + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本框', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ]; + items2: TiNavMenuItem[] = [ + { label: '首页', id: 'home' }, + { + label: '组件总览', + id: 'component', + selectable: false, + children: [ + { + label: '使用指南', + id: 'doc', + selectable: false, + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start' }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + selectable: false, + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + selectable: false, + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本框', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ] + } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavSubmenuComponent.ts b/src/nav/demo/src/app/nav/NavSubmenuComponent.ts new file mode 100644 index 0000000..b9f6a2f --- /dev/null +++ b/src/nav/demo/src/app/nav/NavSubmenuComponent.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-submenu.html' +}) +export class NavSubmenuComponent { + items: TiNavMenuItem[] = [ + { label: '首页' }, + { label: '组件总览' }, + { + label: '使用指南', + children: [{ label: '介绍' }, { label: '快速上手' }, { label: '主题配置' }, { label: '国际化' }, { label: '常见问题' }] + }, + { + label: '表单选择', + children: [{ label: '按钮' }, { label: '选择器' }, { label: '单选框' }, { label: '复选框' }] + }, + { + label: '表单文本', + children: [{ label: '文本框' }, { label: '多行文本框' }, { label: '自动补全' }, { label: '搜索框' }] + } + ]; + + items2: TiNavMenuItem[] = [ + { label: '首页' }, + { + label: '组件总览', + children: [ + { + label: '使用指南', + children: [{ label: '介绍' }, { label: '快速上手' }, { label: '主题配置' }, { label: '国际化' }, { label: '常见问题' }] + }, + { + label: '表单选择', + children: [{ label: '按钮' }, { label: '选择器' }, { label: '单选框' }, { label: '复选框' }] + }, + { + label: '表单文本', + children: [{ label: '文本框' }, { label: '多行文本框' }, { label: '自动补全' }, { label: '搜索框' }] + } + ] + } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavTemplateComponent.ts b/src/nav/demo/src/app/nav/NavTemplateComponent.ts new file mode 100644 index 0000000..5b92f67 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavTemplateComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-template.html' +}) +export class NavTemplateComponent { + items: TiNavMenuItem[] = [ + { label: '首页', id: 'home' }, + { + label: '组件总览', + id: 'component', + children: [ + { + label: '使用指南', + icon: 'check-circle', + id: 'doc', + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start' }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ] + } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavTestModule.ts b/src/nav/demo/src/app/nav/NavTestModule.ts new file mode 100644 index 0000000..cb885ec --- /dev/null +++ b/src/nav/demo/src/app/nav/NavTestModule.ts @@ -0,0 +1,100 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { TiButtonModule, TiIconModule, TiMenuModule, TiNavModule } from '@opentiny/ng'; +import { NavActiveComponent } from './NavActiveComponent'; +import { NavAlignComponent } from './NavAlignComponent'; +import { NavBasicComponent } from './NavBasicComponent'; +import { NavDisabledComponent } from './NavDisabledComponent'; +import { NavEventComponent } from './NavEventComponent'; +import { NavLeftComponent } from './NavLeftComponent'; +import { NavTemplateComponent } from './NavTemplateComponent'; +import { NavRightComponent } from './NavRightComponent'; +import { NavSelectableComponent } from './NavSelectableComponent'; +import { NavSubmenuComponent } from './NavSubmenuComponent'; +import { NavThemeComponent } from './NavThemeComponent'; +import { NavWidthComponent } from './NavWidthComponent'; + +@NgModule({ + imports: [CommonModule, TiNavModule, TiMenuModule, TiButtonModule, TiIconModule, RouterModule.forChild(NavTestModule.ROUTES)], + declarations: [ + NavBasicComponent, + NavWidthComponent, + NavThemeComponent, + NavLeftComponent, + NavRightComponent, + NavAlignComponent, + NavSubmenuComponent, + NavActiveComponent, + NavSelectableComponent, + NavDisabledComponent, + NavTemplateComponent, + NavEventComponent + ] +}) +export class NavTestModule { + static readonly LINKS: Array<{ href: string; label: string }> = [{ href: 'components/TiNavComponent.html', label: 'Nav' }]; + + static readonly ROUTES: Routes = [ + { + path: 'nav/nav-basic', + component: NavBasicComponent, + data: { label: '基础' } + }, + { + path: 'nav/nav-width', + component: NavWidthComponent, + data: { label: '自定义宽度' } + }, + { + path: 'nav/nav-theme', + component: NavThemeComponent, + data: { label: '深色主题' } + }, + { + path: 'nav/nav-left', + component: NavLeftComponent, + data: { label: '左侧Logo区' } + }, + { + path: 'nav/nav-right', + component: NavRightComponent, + data: { label: '右侧操作区' } + }, + { + path: 'nav/nav-align', + component: NavAlignComponent, + data: { label: '菜单区对齐' } + }, + { + path: 'nav/nav-submenu', + component: NavSubmenuComponent, + data: { label: '子菜单' } + }, + { + path: 'nav/nav-active', + component: NavActiveComponent, + data: { label: '激活项' } + }, + { + path: 'nav/nav-selectable', + component: NavSelectableComponent, + data: { label: '可选择项' } + }, + { + path: 'nav/nav-disabled', + component: NavDisabledComponent, + data: { label: '禁用项' } + }, + { + path: 'nav/nav-template', + component: NavTemplateComponent, + data: { label: '使用模板渲染标签' } + }, + { + path: 'nav/nav-event', + component: NavEventComponent, + data: { label: '事件' } + } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavThemeComponent.ts b/src/nav/demo/src/app/nav/NavThemeComponent.ts new file mode 100644 index 0000000..7b813e9 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavThemeComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-theme.html' +}) +export class NavThemeComponent { + items: TiNavMenuItem[] = [ + { id: '1', label: '首页' }, + { id: '2', label: '文档' }, + { id: '3', label: '组件' }, + { id: '4', label: '关于' } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavWidthComponent.ts b/src/nav/demo/src/app/nav/NavWidthComponent.ts new file mode 100644 index 0000000..2e6c09b --- /dev/null +++ b/src/nav/demo/src/app/nav/NavWidthComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-width.html' +}) +export class NavWidthComponent { + items: TiNavMenuItem[] = [ + { id: '1', label: '首页' }, + { id: '2', label: '文档' }, + { id: '3', label: '组件' }, + { id: '4', label: '关于' } + ]; +} diff --git a/src/nav/demo/src/app/nav/nav-active.html b/src/nav/demo/src/app/nav/nav-active.html new file mode 100644 index 0000000..6f2802f --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-active.html @@ -0,0 +1,19 @@ +
    + + TinyNG + + +
    + +
    + + TinyNG + + +
    + + diff --git a/src/nav/demo/src/app/nav/nav-align.html b/src/nav/demo/src/app/nav/nav-align.html new file mode 100644 index 0000000..e2d3fc1 --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-align.html @@ -0,0 +1,23 @@ + + TinyNG + + + + + + + + TinyNG + + + + + + + + TinyNG + + + + + diff --git a/src/nav/demo/src/app/nav/nav-basic.html b/src/nav/demo/src/app/nav/nav-basic.html new file mode 100644 index 0000000..2d797f7 --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-basic.html @@ -0,0 +1,9 @@ + + TinyNG + + + + + + + diff --git a/src/nav/demo/src/app/nav/nav-disabled.html b/src/nav/demo/src/app/nav/nav-disabled.html new file mode 100644 index 0000000..cab6b0f --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-disabled.html @@ -0,0 +1,19 @@ +
    + + TinyNG + + +
    + +
    + + TinyNG + + +
    + + diff --git a/src/nav/demo/src/app/nav/nav-event.html b/src/nav/demo/src/app/nav/nav-event.html new file mode 100644 index 0000000..ca59135 --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-event.html @@ -0,0 +1,16 @@ +
    + + TinyNG + + + +
    +

    {{log}}

    +
    +
    + + diff --git a/src/nav/demo/src/app/nav/nav-left.html b/src/nav/demo/src/app/nav/nav-left.html new file mode 100644 index 0000000..d2fb5ea --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-left.html @@ -0,0 +1,15 @@ + + + + TinyNG + + + + + diff --git a/src/nav/demo/src/app/nav/nav-right.html b/src/nav/demo/src/app/nav/nav-right.html new file mode 100644 index 0000000..2e70140 --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-right.html @@ -0,0 +1,16 @@ + + TinyNG + + + + 欢迎使用TinyNG + 访问主页 + + + + + diff --git a/src/nav/demo/src/app/nav/nav-selectable.html b/src/nav/demo/src/app/nav/nav-selectable.html new file mode 100644 index 0000000..6f2802f --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-selectable.html @@ -0,0 +1,19 @@ +
    + + TinyNG + + +
    + +
    + + TinyNG + + +
    + + diff --git a/src/nav/demo/src/app/nav/nav-submenu.html b/src/nav/demo/src/app/nav/nav-submenu.html new file mode 100644 index 0000000..9462bd9 --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-submenu.html @@ -0,0 +1,33 @@ +
    + + TinyNG + + +
    + +
    + + TinyNG + + +
    + +
    + + TinyNG + + +
    + +
    + + TinyNG + + +
    + + diff --git a/src/nav/demo/src/app/nav/nav-template.html b/src/nav/demo/src/app/nav/nav-template.html new file mode 100644 index 0000000..1d22ba8 --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-template.html @@ -0,0 +1,17 @@ +
    + + TinyNG + + + {{ item.label }} + + + + +
    + + diff --git a/src/nav/demo/src/app/nav/nav-theme.html b/src/nav/demo/src/app/nav/nav-theme.html new file mode 100644 index 0000000..ff11021 --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-theme.html @@ -0,0 +1,9 @@ + + TinyNG + + + + + TinyNG + + diff --git a/src/nav/demo/src/app/nav/nav-width.html b/src/nav/demo/src/app/nav/nav-width.html new file mode 100644 index 0000000..a56939a --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-width.html @@ -0,0 +1,4 @@ + + TinyNG + + diff --git a/src/nav/demo/src/app/nav/webdoc/nav-demos.js b/src/nav/demo/src/app/nav/webdoc/nav-demos.js new file mode 100644 index 0000000..89ee92f --- /dev/null +++ b/src/nav/demo/src/app/nav/webdoc/nav-demos.js @@ -0,0 +1,149 @@ +export default { + column: '1', + demos: [ + { + demoId: 'nav-basic', + name: { + 'zh-CN': '基础', + 'en-US': 'nav basic' + }, + desc: { + 'zh-CN': '

    提供 Nav 组件的基本使用示例,其中包含左侧站点标题,中间导航菜单,右侧按钮。

    ', + 'en-US': '

    nav basic

    ' + } + }, + { + demoId: 'nav-width', + name: { + 'zh-CN': '自定义宽度', + 'en-US': 'nav width' + }, + desc: { + 'zh-CN': '

    通过属性原生属性可以设置导航栏宽度,参考 CSS 的width属性。', + 'en-US': '

    nav width

    ' + } + }, + { + demoId: 'nav-theme', + name: { + 'zh-CN': '深色主题', + 'en-US': 'nav theme' + }, + desc: { + 'zh-CN': + '

    通过属性theme配置 nav 组件主题,提供了深色dark和浅色light两种选项,默认使用light浅色主题。

    ', + 'en-US': '

    nav theme

    ' + }, + apis: ['TiNavComponent.properties.theme'] + }, + { + demoId: 'nav-left', + name: { + 'zh-CN': '左侧 Logo 区', + 'en-US': 'nav left' + }, + desc: { + 'zh-CN': '

    提供一个容器用来放置站点 Logo 图片和站点标题,预设了img标签的样式,也可根据需要自定义内容。

    ', + 'en-US': '

    nav left

    ' + } + }, + { + demoId: 'nav-right', + name: { + 'zh-CN': '右侧操作区', + 'en-US': 'nav right' + }, + desc: { + 'zh-CN': '

    右侧操作区用于放置a标签、span标签和button标签等元素。

    ', + 'en-US': '

    nav right

    ' + } + }, + { + demoId: 'nav-align', + name: { + 'zh-CN': '菜单区对齐', + 'en-US': 'nav align' + }, + desc: { + 'zh-CN': + '

    导航菜单区域采用了display: flex布局,可以通过修改justify-content属性来设置导航菜单元素的对齐方式,参考 CSS 的justfy-content属性。

    ', + 'en-US': '

    nav align

    ' + } + }, + { + demoId: 'nav-submenu', + name: { + 'zh-CN': '子菜单', + 'en-US': 'nav submenu' + }, + desc: { + 'zh-CN': + '

    通过属性items传入菜单项数组,根据菜单对象的children属性嵌套层数决定了展开子菜单的方式,二级嵌套采用下拉菜单,三级嵌套采用下拉面板。

    ', + 'en-US': '

    nav submenu

    ' + }, + apis: ['TiNavMenuComponent.properties.items'] + }, + { + demoId: 'nav-active', + name: { + 'zh-CN': '激活项', + 'en-US': 'nav active' + }, + desc: { + 'zh-CN': '

    通过传入activeId修改当前激活项,被激活的菜单项及其父级菜单会有高亮显示。

    ', + 'en-US': '

    nav active

    ' + }, + apis: ['TiNavMenuComponent.properties.activeId'] + }, + { + demoId: 'nav-selectable', + name: { + 'zh-CN': '可选择项', + 'en-US': 'nav selectable' + }, + desc: { + 'zh-CN': + '

    通过在items中设置selectable控制对应菜单项是否可被选中,默认为true,被标记为不可选中的项目不会触发选择事件,建议在一级和二级菜单项上根据需要使用。

    ', + 'en-US': '

    nav selectable

    ' + } + }, + { + demoId: 'nav-disabled', + name: { + 'zh-CN': '禁用项', + 'en-US': 'nav disabled' + }, + desc: { + 'zh-CN': + '

    通过在items中设置disabled控制对应菜单项及其子项是否处于已禁用状态,默认为false

    ', + 'en-US': '

    nav disabled

    ' + } + }, + { + demoId: 'nav-template', + name: { + 'zh-CN': '使用模板渲染标签', + 'en-US': 'nav template' + }, + desc: { + 'zh-CN': + '

    提供了item插槽来自定义 item 标签的内容,用户可以编写自定义模板,从模板上下文中获取标签对象item: TiNavMenuItem,可以在传入的items中添加需要的自定义数据,例如这里的示例添加了icon字段,并根据是否有icon字段来展示图标。

    ', + 'en-US': '

    nav template

    ' + }, + apis: ['TiNavMenuComponent.slots.itemTemplate'] + }, + { + demoId: 'nav-event', + name: { + 'zh-CN': '事件', + 'en-US': 'nav event' + }, + desc: { + 'zh-CN': + '

    选择菜单项将触发选择事件,并返回对象,其中item为被选择的对象数据,itemPath是选项数组,保存了从顶层到底层被选择的每一级对象数据。

    ', + 'en-US': '

    nav event

    ' + }, + apis: ['TiNavMenuComponent.events.select'] + } + ] +}; diff --git a/src/nav/demo/src/app/nav/webdoc/nav.cn.md b/src/nav/demo/src/app/nav/webdoc/nav.cn.md new file mode 100644 index 0000000..462adee --- /dev/null +++ b/src/nav/demo/src/app/nav/webdoc/nav.cn.md @@ -0,0 +1,25 @@ +--- +title: Nav 导航栏 +--- + +# Nav 导航栏 + +
    + +Nav 组件提供了页面顶部导航栏,包含了左侧logo区域、中间的导航菜单和右侧操作区域 + +```typescript +import { TiNavModule } from '@opentiny/ng'; +``` + +
    + +
    + +Nav 组件提供了页面顶部导航栏,包含了左侧logo区域、中间的导航菜单和右侧操作区域 + +```typescript +import { TiNavModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/nav/demo/src/app/nav/webdoc/nav.en.md b/src/nav/demo/src/app/nav/webdoc/nav.en.md new file mode 100644 index 0000000..e1a4208 --- /dev/null +++ b/src/nav/demo/src/app/nav/webdoc/nav.en.md @@ -0,0 +1,7 @@ +--- +title: Nav +--- + +# Nav + +The nav component provides a navigation bar at the top of the page, including the logo area on the left, the navigation menu in the middle, and the operation area on the right. diff --git a/src/nav/demo/src/favicon.ico b/src/nav/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/nav/demo/src/index.html b/src/nav/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/nav/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/nav/demo/src/main.ts b/src/nav/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/nav/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/nav/demo/test.ts b/src/nav/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/nav/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/nav/demo/tsconfig.app.json b/src/nav/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/nav/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/nav/demo/tsconfig.spec.json b/src/nav/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/nav/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/nav/lib/index.ts b/src/nav/lib/index.ts new file mode 100644 index 0000000..930968a --- /dev/null +++ b/src/nav/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiNavModule'; diff --git a/src/nav/lib/ng-package.json b/src/nav/lib/ng-package.json new file mode 100644 index 0000000..cab05b2 --- /dev/null +++ b/src/nav/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/nav", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/nav/lib/package.json b/src/nav/lib/package.json new file mode 100644 index 0000000..ec26573 --- /dev/null +++ b/src/nav/lib/package.json @@ -0,0 +1,9 @@ +{ + "name": "@opentiny/ng-nav", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/nav/lib/project.json b/src/nav/lib/project.json new file mode 100644 index 0000000..cc49048 --- /dev/null +++ b/src/nav/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/nav/lib", + "sourceRoot": "src/nav/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/nav"], + "options": { + "project": "src/nav/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/nav"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js nav" + }, + { + "command": "ng default-build nav" + }, + { + "command": "node build/clear-default-theme.js nav" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/nav && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build nav && ng pack nav && node build/publish.js nav --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/nav/lib/src/TiNavComponent.ts b/src/nav/lib/src/TiNavComponent.ts new file mode 100644 index 0000000..081fbda --- /dev/null +++ b/src/nav/lib/src/TiNavComponent.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +/** + * Nav 顶部导航组件 + * + * 提供页面顶部导航组件,包含了logo区域、菜单区域、操作区域 + */ +@Component({ + selector: 'ti-nav', + templateUrl: 'nav.html', + styleUrls: ['./nav.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'ti3-nav', + '[class.ti3-nav-light]': 'theme === "light"', + '[class.ti3-nav-dark]': 'theme === "dark"' + } +}) +export class TiNavComponent { + /** + * 主题,light|dark 默认light + */ + @Input() theme: 'light' | 'dark' = 'light'; +} diff --git a/src/nav/lib/src/TiNavLeftComponent.ts b/src/nav/lib/src/TiNavLeftComponent.ts new file mode 100644 index 0000000..f2fd705 --- /dev/null +++ b/src/nav/lib/src/TiNavLeftComponent.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * NavLeft 顶部导航logo区域组件 + * + * 提供顶部导航logo区域,由logo图片和文本标题组成,可以监听click点击事件 + */ +@Component({ + selector: 'ti-nav-left', + templateUrl: './navleft.html', + styleUrls: ['./navleft.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'ti3-nav-left' + } +}) +export class TiNavLeftComponent {} diff --git a/src/nav/lib/src/TiNavMenuComponent.ts b/src/nav/lib/src/TiNavMenuComponent.ts new file mode 100644 index 0000000..3d8e04e --- /dev/null +++ b/src/nav/lib/src/TiNavMenuComponent.ts @@ -0,0 +1,257 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ContentChild, EventEmitter, Input, Output, SimpleChanges, TemplateRef } from '@angular/core'; +import { TiNavMenuItem } from './interface'; + +/** + * @ignore + * + * 增强菜单项 + * + * 为一级菜单增加了子菜单展开标记和原始菜单项 + */ +interface TiNavMenuItemClone extends TiNavMenuItem { + /** + * 是否展开子菜单 + */ + isExpand: boolean; + /** + * 展开模式: menu 菜单模式展开, panel 面板模式展开, undefined 不展开 + */ + expandMode?: 'menu' | 'panel' | undefined; + /** + * 原始菜单项 + */ + item: TiNavMenuItem; +} + +/** + * NavMenu 顶部导航菜单组件 + * + * 提供页面顶部导航菜单,包含一级菜单、二级菜单 + */ +@Component({ + selector: 'ti-nav-menu', + templateUrl: './navmenu.html', + styleUrls: ['./navmenu.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'ti3-nav-menu' + } +}) +export class TiNavMenuComponent { + constructor() {} + + /** + * 菜单标签占位文本区域的模板 + */ + @ContentChild('item', { static: true }) itemTemplate: TemplateRef; + + /** + * 菜单项 + */ + @Input() items: Array = []; + + /** + * @ignore + * + * 增强的菜单项 + */ + itemsClone: Array = []; + + /** + * 传入需要激活的选项的id + */ + @Input() activeId: string; + + /** + * @ignore + * + * 当前激活项目列表 + */ + activePath: Array = []; + + /** + * 选择菜单项触发回调 + */ + @Output() select = new EventEmitter<{ + item: TiNavMenuItem; + itemPath: Array; + }>(); + + /** + * @ignore + * + * 监听数据变化 + * + * @param changes 发生变动的数据 + */ + ngOnChanges(changes: SimpleChanges): void { + // 当出入菜单项列表发生改变时,更新增强菜单项列表 + if (changes.items) { + // 增强的菜单项,增加了expand表示是否展开子菜单,默认为false,增加了item用来保存原始item + const itemsClone = JSON.parse(JSON.stringify(changes.items.currentValue)) as Array; + this.checkDefault(itemsClone, 'default'); + this.itemsClone = itemsClone.map((item) => { + return { + ...item, + isExpand: false, + expandMode: this.getExpandMode(item), + item + } as TiNavMenuItemClone; + }); + } + if (changes.activeId) { + this.activePath = this.getPathById(this.items, changes.activeId.currentValue); + } + } + + /** + * @ignore + * + * 导航菜单点击事件 + * + * 点击导航菜单,传入鼠标事件,被点击的原始菜单项 item,激活 id 数组 + * + * @param event 鼠标点击事件 + * @param item 被点击的项目 + * @param activePath 激活id数组 + * @param isParentDisabled 父级是否被禁用 + */ + onClick(event: PointerEvent, item: TiNavMenuItem, activePath: Array, isParentDisabled = false): void { + event.stopPropagation(); + // 禁用项 和 不可选中项 不触发选中回调 + if (!item.disabled && !isParentDisabled && item.selectable) { + // 触发外部选择菜单事件回调 + this.select.emit({ item, itemPath: activePath }); + this.activePath = activePath; + } + } + + /** + * @ignore + * + * 鼠标移入菜单项事件 + * + * 当鼠标移入时,展开子菜单 + * + * @param event 鼠标事件 + * @param id 一级菜单id + */ + onMouseenter(event: MouseEvent, id: string): void { + // 根据一级菜单 id 值寻找对应项,更新它的 expand 值为 true + this.handleToggleExpand(id, true); + } + + /** + * @ignore + * + * 鼠标移出菜单项事件 + * + * 当鼠标移出时,关闭子菜单 + * + * @param event 鼠标事件 + * @param id 一级菜单id + */ + onMouseleave(event: MouseEvent, id: string): void { + // 根据一级菜单 id 值寻找对应项,更新它的 expand 值为 false + this.handleToggleExpand(id, false); + } + + /** + * 子菜单开关 + * + * @param id 一级菜单 id 值 + * @param expand 新的展开值 + */ + private handleToggleExpand(id: string, expand: boolean): void { + const item = this.itemsClone.find((item) => item.id === id && !item.disabled); + // 二级菜单展开和关闭逻辑 + if (item) { + item.isExpand = expand; + } + } + + /** + * 获取子菜单展开模式 + * + * @param item + * @returns + */ + private getExpandMode(item: TiNavMenuItem): string { + let res; + for (let subItem of item.children || []) { + if (subItem.children && subItem.children.length > 0) { + return 'panel'; + } + res = 'menu'; + } + return res; + } + + /** + * 检查默认值,如果没有则进行初始化 + * + * @param items + * @param id + */ + private checkDefault(items: Array, id: string): void { + let count = 0; + for (const item of items) { + // 设置唯一id + if (item.id === undefined) { + item.id = `${id}-${count}`; + count += 1; + } + // 设置是否可选,默认true + if (item.selectable === undefined) { + item.selectable = true; + } + // 设置是否禁用,默认false + if (item.disabled === undefined) { + item.disabled = false; + } + // 遍历子元素 + if (item.children) { + this.checkDefault(item.children, item.id); + } + } + } + + /** + * 嵌套遍历获取path + * + * @param list 表单对象列表 + * @param id 需要匹配的id + * @returns 匹配的路径 + */ + private getPathById(list: Array, id: string): Array { + for (const item of list) { + // 禁用项不能作为默认激活项 + if (item.disabled) { + continue; + } + // 匹配成功返回path + if (item.id === id) { + return [item]; + } + // 匹配子菜单 + const res = this.getPathById(item.children || [], id); + if (res.length > 0) { + // 子菜单匹配成功,返回拼接后的path + return [item, ...res]; + } + } + // 匹配失败返回空匹配 + return []; + } +} diff --git a/src/nav/lib/src/TiNavModule.ts b/src/nav/lib/src/TiNavModule.ts new file mode 100644 index 0000000..fb87aa2 --- /dev/null +++ b/src/nav/lib/src/TiNavModule.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { TiNavComponent } from './TiNavComponent'; +import { TiNavLeftComponent } from './TiNavLeftComponent'; +import { TiNavMenuComponent } from './TiNavMenuComponent'; +import { TiNavRightComponent } from './TiNavRightComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [TiNavComponent, TiNavLeftComponent, TiNavRightComponent, TiNavMenuComponent], + declarations: [TiNavComponent, TiNavLeftComponent, TiNavRightComponent, TiNavMenuComponent] +}) +export class TiNavModule {} + +export { TiNavComponent } from './TiNavComponent'; +export { TiNavLeftComponent } from './TiNavLeftComponent'; +export { TiNavRightComponent } from './TiNavRightComponent'; +export { TiNavMenuComponent } from './TiNavMenuComponent'; +export { TiNavMenuItem } from './interface'; diff --git a/src/nav/lib/src/TiNavRightComponent.ts b/src/nav/lib/src/TiNavRightComponent.ts new file mode 100644 index 0000000..20827c9 --- /dev/null +++ b/src/nav/lib/src/TiNavRightComponent.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * NavRight 组件 + * + * 提供顶部导航栏右侧操作区域展示,可添加任意行内元素 + */ +@Component({ + selector: 'ti-nav-right', + templateUrl: './navright.html', + styleUrls: ['./navright.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiNavRightComponent {} diff --git a/src/nav/lib/src/common.less b/src/nav/lib/src/common.less new file mode 100644 index 0000000..827777e --- /dev/null +++ b/src/nav/lib/src/common.less @@ -0,0 +1,18 @@ +:host { + --ti-nav-color-text-active: var(--ti-common-color-text-highlight); + --ti-nav-color-text-white: var(--ti-common-color-text-gray); + --ti-nav-color-bg-active: var(--ti-common-color-bg-emphasize); + &.ti3-nav-light { + --ti-nav-color-bg: var(--ti-common-color-bg-white-normal); + --ti-nav-color-text: var(--ti-common-color-text-secondary); + --ti-nav-color-brand: var(--ti-common-color-text-primary); + --ti-nav-color-text-disabled: var(--ti-common-color-text-disabled); + } + &.ti3-nav-dark { + --ti-nav-color-bg: var(--ti-common-color-bg-navigation); + --ti-nav-color-text: var(--ti-common-color-text-darkbg); + --ti-nav-color-brand: var(--ti-common-color-text-white); + --ti-nav-color-text-disabled: var(--ti-common-color-text-darkbg-disabled); + } + --ti-nav-height: var(--ti-common-size-16x); +} diff --git a/src/nav/lib/src/interface.ts b/src/nav/lib/src/interface.ts new file mode 100644 index 0000000..4d8ac07 --- /dev/null +++ b/src/nav/lib/src/interface.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * TiNavMenuItem 是顶部导航栏一级菜单项数据接口 + */ +export interface TiNavMenuItem { + /** + * 菜单项文本 + */ + label: string; + /** + * 菜单项id值,确保唯一 + * + * @default 初始化自动生成唯一id + */ + id?: string; + /** + * 是否禁用 + * + * @default false + */ + disabled?: boolean; + /** + * 是否可选中 + * + * @default true + */ + selectable?: boolean; + /** + * 子菜单 + */ + children?: TiNavMenuItem[]; + /** + * 其他参数,可自定义 + */ + [key: string]: any; +} diff --git a/src/nav/lib/src/nav.html b/src/nav/lib/src/nav.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/nav/lib/src/nav.html @@ -0,0 +1 @@ + diff --git a/src/nav/lib/src/nav.less b/src/nav/lib/src/nav.less new file mode 100644 index 0000000..fc773f0 --- /dev/null +++ b/src/nav/lib/src/nav.less @@ -0,0 +1,16 @@ +@import './common.less'; + +:host { + &.ti3-nav { + box-sizing: border-box; + position: relative; + z-index: 1002; + display: flex; + justify-content: space-between; + width: auto; + height: var(--ti-nav-height); + padding: var(--ti-common-space-0) var(--ti-common-space-6x); + box-shadow: var(--ti-common-shadow-2-down); + background-color: var(--ti-nav-color-bg); + } +} diff --git a/src/nav/lib/src/navleft.html b/src/nav/lib/src/navleft.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/nav/lib/src/navleft.html @@ -0,0 +1 @@ + diff --git a/src/nav/lib/src/navleft.less b/src/nav/lib/src/navleft.less new file mode 100644 index 0000000..37e0bd9 --- /dev/null +++ b/src/nav/lib/src/navleft.less @@ -0,0 +1,15 @@ +@import './common.less'; + +:host { + &.ti3-nav-left { + box-sizing: border-box; + flex-shrink: 0; + display: flex; + cursor: pointer; + color: var(--ti-nav-color-brand); + + font-size: var(--ti-common-font-size-3); + line-height: var(--ti-nav-height); + font-weight: var(--ti-common-font-weight-5); + } +} diff --git a/src/nav/lib/src/navmenu.html b/src/nav/lib/src/navmenu.html new file mode 100644 index 0000000..f5b60b9 --- /dev/null +++ b/src/nav/lib/src/navmenu.html @@ -0,0 +1,72 @@ + +
    + +
    + +
    + + +
      + +
    • + +
    • +
    + + +
    +
    + +
    + +
    + + +
    + +
    +
    +
    +
    + + {{ item.label }} diff --git a/src/nav/lib/src/navmenu.less b/src/nav/lib/src/navmenu.less new file mode 100644 index 0000000..7469284 --- /dev/null +++ b/src/nav/lib/src/navmenu.less @@ -0,0 +1,186 @@ +@import './common.less'; + +:host.ti3-nav-menu { + flex-grow: 1; + flex-shrink: 0; + display: flex; + flex-direction: row; + justify-content: flex-start; + padding: var(--ti-common-space-0) var(--ti-common-space-10x); +} + +.ti3-nav-menu-item-label { + position: relative; + margin: var(--ti-common-space-0) var(--ti-common-space-5x); + font-size: var(--ti-common-font-size-2); + font-weight: var(--ti-common-font-weight-4); + line-height: var(--ti-nav-height); + flex-shrink: 0; + color: var(--ti-nav-color-text); + cursor: default; + + &.ti3-nav-selectable { + cursor: pointer; + } + + &[disabled] { + color: var(--ti-nav-color-text-disabled); + cursor: not-allowed; + .ti3-nav-submenu-icon { + border-top-color: var(--ti-nav-color-text-disabled); + } + } + + &::before { + content: ''; + height: 3px; + width: 100%; + background-color: transparent; + position: absolute; + bottom: 0; + left: 0; + transform-origin: center; + transform: scaleX(0); + transition: transform 0.2s ease; + } + + &.ti3-nav-submenu-label::after { + content: ''; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid var(--ti-nav-color-text); + margin-left: 4px; + display: inline-block; + position: relative; + top: -3px; + } + + &.ti3-nav-active:not([disabled]), + &:hover:not([disabled]) { + color: var(--ti-nav-color-text-active); + &::before { + transform: scaleX(1); + background-color: var(--ti-nav-color-text-active); + } + &.ti3-nav-submenu-label::after { + border-top-color: var(--ti-nav-color-text-active); + } + } +} + +.ti3-nav-submenu { + position: absolute; + width: fit-content; + cursor: default; + padding: 0; + background-color: var(--ti-nav-color-bg); + box-shadow: var(--ti-common-shadow-2-down); + top: var(--ti-nav-height); + left: 50%; + transform: translateX(-50%); + height: 0; + overflow: hidden; + transition: all 0.2s ease; + &.expand { + padding: var(--ti-common-space-5x) var(--ti-common-space-0); + height: fit-content; + } +} + +.ti3-nav-submenu-item { + word-break: keep-all; + font-size: var(--ti-common-font-size-1); + font-weight: var(--ti-common-font-weight-4); + line-height: var(--ti-common-line-height-number); + color: var(--ti-nav-color-text); + padding: var(--ti-common-space-10) var(--ti-common-space-6x); + background-color: inherit; + cursor: default; + + &.ti3-nav-selectable { + cursor: pointer; + } + &[disabled] { + color: var(--ti-nav-color-text-disabled); + cursor: not-allowed; + } + + &:hover:not([disabled]) { + color: var(--ti-nav-color-text-active); + } + + &.ti3-nav-active:not([disabled]) { + color: var(--ti-nav-color-text-white); + background-color: var(--ti-nav-color-bg-active); + } +} + +.ti3-nav-panel { + position: absolute; + top: var(--ti-nav-height); + left: 0; + right: 0; + padding: 0; + background-color: var(--ti-common-color-bg-gray); + box-shadow: var(--ti-common-shadow-2-down); + display: flex; + flex-direction: row; + justify-content: center; + flex-wrap: wrap; + transform: scaleY(0); + transform-origin: top; + transition: transform 0.2s ease; + + &.expand { + transform: scaleY(1); + padding: var(--ti-common-space-3x) var(--ti-common-space-5x) var(--ti-common-space-10x) var(--ti-common-space-5x); + } +} + +.ti3-nav-panel-list { + flex-shrink: 0; + width: 255px; + & > * { + padding: var(--ti-common-space-2x) var(--ti-common-space-10); + } +} + +.ti3-nav-panel-list-title { + font-size: var(--ti-common-font-size-2); + line-height: var(--ti-common-line-height-number); + color: var(--ti-nav-color-text-light); + font-weight: var(--ti-common-font-weight-6); + cursor: default; + + &.ti3-nav-selectable { + cursor: pointer; + } + &[disabled] { + cursor: not-allowed; + color: var(--ti-nav-color-text-disabled); + } +} + +.ti3-nav-panel-item { + font-size: var(--ti-common-font-size-1); + line-height: var(--ti-common-line-height-number); + color: var(--ti-nav-color-text-light-brand); + font-weight: var(--ti-common-font-weight-4); + cursor: default; + + &.ti3-nav-selectable { + cursor: pointer; + } + + &[disabled] { + cursor: not-allowed; + color: var(--ti-nav-color-text-disabled); + } + + &:hover:not([disabled]), + &.ti3-nav-active:not([disabled]) { + color: var(--ti-nav-color-text-active); + } +} diff --git a/src/nav/lib/src/navright.html b/src/nav/lib/src/navright.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/nav/lib/src/navright.html @@ -0,0 +1 @@ + diff --git a/src/nav/lib/src/navright.less b/src/nav/lib/src/navright.less new file mode 100644 index 0000000..92725dd --- /dev/null +++ b/src/nav/lib/src/navright.less @@ -0,0 +1,11 @@ +@import './common.less'; + +:host { + box-sizing: border-box; + flex-shrink: 0; + display: flex; + align-items: center; + font-size: var(--ti-common-font-size-1); + font-weight: var(--ti-common-font-weight-4); + color: var(--ti-nav-color-text); +} diff --git a/src/ng/demo/karma.conf.js b/src/ng/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/ng/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/ng/demo/project.json b/src/ng/demo/project.json new file mode 100644 index 0000000..0cb3dc0 --- /dev/null +++ b/src/ng/demo/project.json @@ -0,0 +1,120 @@ +{ + "projectType": "application", + "root": "src/ng/demo", + "sourceRoot": "src/ng/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/ng", + "index": "src/ng/demo/src/index.html", + "main": "src/ng/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/ng/demo/tsconfig.app.json", + "assets": [ + "src/ng/demo/src/favicon.ico", + "src/ng/demo/src/assets", + { + "glob": "**/*", + "input": "node_modules/ionicons/dist/ionicons/svg", + "output": "/assets/ionicons/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "wc": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "30MB", + "maximumError": "30MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "30MB", + "maximumError": "30MB" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.wc.ts" + } + ], + "outputHashing": "none" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "ng-demo:build:production" + }, + "development": { + "browserTarget": "ng-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js ng" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/ng/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/ng/demo/tsconfig.spec.json", + "karmaConfig": "src/ng/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "assets": ["src/ng/demo/src/favicon.ico", "src/ng/demo/src/assets"], + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/ng/demo/src/app/AppComponent.ts b/src/ng/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..3364119 --- /dev/null +++ b/src/ng/demo/src/app/AppComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { ActivatedRouteSnapshot, ChildActivationEnd, Router } from '@angular/router'; +// import { TiLog } from '@cloud/tiny3'; +import { environment } from '../../../../environments/environment'; + +export interface DemoTab { + title: string; + src: any; + active?: boolean; +} + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent { + constructor(router: Router) { + // 日志控制 + // TiLog.setLevel(environment.production ? TiLog.LEVEL_OFF : TiLog.LEVEL_LOG); + + router.events.subscribe((event: any) => { + if (event instanceof ChildActivationEnd) { + const snapshot: ActivatedRouteSnapshot = event.snapshot; + const routerPath: string = snapshot.firstChild.routeConfig.path; + + // 设置网页标题 + const pathArray: Array = routerPath.split('/'); + document.getElementsByTagName('title')[0].innerText = pathArray[pathArray.length - 1]; + } + }); + } +} diff --git a/src/ng/demo/src/app/AppModule.ts b/src/ng/demo/src/app/AppModule.ts new file mode 100644 index 0000000..a2d3f51 --- /dev/null +++ b/src/ng/demo/src/app/AppModule.ts @@ -0,0 +1,32 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { HttpClientModule } from '@angular/common/http'; +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { DemoModules } from './DemoModules'; +import { TiLocale } from '@opentiny/ng'; + +@NgModule({ + imports: [ + DemoModules.allModules, + HttpClientModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, // 因为modal,message,collapse,accordion组件使用了angular动画,所以需要引入此模块 + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule { + constructor() { + // TiLocale.setLocale(TiLocale.EN_US); // 设置国际化语种 + } +} diff --git a/src/ng/demo/src/app/AppWcModule.ts b/src/ng/demo/src/app/AppWcModule.ts new file mode 100644 index 0000000..86c26ec --- /dev/null +++ b/src/ng/demo/src/app/AppWcModule.ts @@ -0,0 +1,5394 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { Component, Injector, NgModule } from '@angular/core'; +import { createCustomElement } from '@angular/elements'; +import { DemoModules } from './DemoModules'; +import { HttpClientModule } from '@angular/common/http'; + +import { AccordionBasicComponent } from './../../../../accordion/demo/src/app/accordion/AccordionBasicComponent'; + +import { AccordionClassComponent } from './../../../../accordion/demo/src/app/accordion/AccordionClassComponent'; + +import { AccordionClickToggleComponent } from './../../../../accordion/demo/src/app/accordion/AccordionClickToggleComponent'; + +import { AccordionCloseOthersComponent } from './../../../../accordion/demo/src/app/accordion/AccordionCloseOthersComponent'; + +import { AccordionDisabledComponent } from './../../../../accordion/demo/src/app/accordion/AccordionDisabledComponent'; + +import { AccordionOpenComponent } from './../../../../accordion/demo/src/app/accordion/AccordionOpenComponent'; + +import { ActionmenuBasicComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuBasicComponent'; + +import { ActionmenuDataComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuDataComponent'; + +import { ActionmenuData2Component } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuData2Component'; + +import { ActionmenuDisabledComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuDisabledComponent'; + +import { ActionmenuDividerComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuDividerComponent'; + +import { ActionmenuEventComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuEventComponent'; + +import { ActionmenuFocusComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuFocusComponent'; + +import { ActionmenuIdComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuIdComponent'; + +import { ActionmenuItemsChangeComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuItemsChangeComponent'; + +import { ActionmenuItemsComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuItemsComponent'; + +import { ActionmenuLabelkeyComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuLabelkeyComponent'; + +import { ActionmenuManyComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuManyComponent'; + +import { ActionmenuMenutextComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuMenutextComponent'; + +import { ActionmenuPanelstyleComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuPanelstyleComponent'; + +import { ActionmenuShownumComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuShownumComponent'; + +import { ActionmenuSpaceComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuSpaceComponent'; + +import { ActionmenuTableComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuTableComponent'; + +import { ActionmenuTempleteTestComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuTempleteTestComponent'; + +import { ActionmenuTempleteComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuTempleteComponent'; + +import { ActionmenuTipsTestComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuTipsTestComponent'; + +import { ActionmenuTipsComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuTipsComponent'; + +import { AlertBoxshadowComponent } from './../../../../alert/demo/src/app/alert/AlertBoxshadowComponent'; + +import { AlertDarkthemeComponent } from './../../../../alert/demo/src/app/alert/AlertDarkthemeComponent'; + +import { AlertDismissComponent } from './../../../../alert/demo/src/app/alert/AlertDismissComponent'; + +import { AlertEventComponent } from './../../../../alert/demo/src/app/alert/AlertEventComponent'; + +import { AlertIconComponent } from './../../../../alert/demo/src/app/alert/AlertIconComponent'; + +import { AlertMessagesComponent } from './../../../../alert/demo/src/app/alert/AlertMessagesComponent'; + +import { AlertOpenTestComponent } from './../../../../alert/demo/src/app/alert/AlertOpenTestComponent'; + +import { AlertOpenComponent } from './../../../../alert/demo/src/app/alert/AlertOpenComponent'; + +import { AlertSizeComponent } from './../../../../alert/demo/src/app/alert/AlertSizeComponent'; + +import { AlertTriggerScrollComponent } from './../../../../alert/demo/src/app/alert/AlertTriggerScrollComponent'; + +import { AlertTypeComponent } from './../../../../alert/demo/src/app/alert/AlertTypeComponent'; + +import { AnchorBasicComponent } from './../../../../anchor/demo/src/app/anchor/AnchorBasicComponent'; + +import { AnchorEventsComponent } from './../../../../anchor/demo/src/app/anchor/AnchorEventsComponent'; + +import { AnchorIdComponent } from './../../../../anchor/demo/src/app/anchor/AnchorIdComponent'; + +import { AnchorItemsComponent } from './../../../../anchor/demo/src/app/anchor/AnchorItemsComponent'; + +import { AnchorOffsettopComponent } from './../../../../anchor/demo/src/app/anchor/AnchorOffsettopComponent'; + +import { AnchorScrolltargetComponent } from './../../../../anchor/demo/src/app/anchor/AnchorScrolltargetComponent'; + +import { AnchorSpeedComponent } from './../../../../anchor/demo/src/app/anchor/AnchorSpeedComponent'; + +import { AnchorTemplateComponent } from './../../../../anchor/demo/src/app/anchor/AnchorTemplateComponent'; + +import { AnchorTestComponent } from './../../../../anchor/demo/src/app/anchor/AnchorTestComponent'; + +import { AutocompleteAppendtobodyComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteAppendtobodyComponent'; + +import { AutocompleteBasicComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteBasicComponent'; + +import { AutocompleteClearableComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteClearableComponent'; + +import { AutocompleteDisabledComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteDisabledComponent'; + +import { AutocompleteEventsComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteEventsComponent'; + +import { AutocompleteLabelkeyComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteLabelkeyComponent'; + +import { AutocompleteMaxlengthComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteMaxlengthComponent'; + +import { AutocompletePanelSizeComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompletePanelSizeComponent'; + +import { AutocompleteTemplateComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteTemplateComponent'; + +import { AutocompleteTestComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteTestComponent'; + +import { AutocompleteTipComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteTipComponent'; + +import { AutocompleteValidComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteValidComponent'; + +import { AutocompleteGroupComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteGroupComponent'; + +import { AvatarImageErrorTestComponent } from './../../../../avatar/demo/src/app/avatar/AvatarImageErrorTestComponent'; + +import { AvatarImageComponent } from './../../../../avatar/demo/src/app/avatar/AvatarImageComponent'; + +import { AvatarShapeComponent } from './../../../../avatar/demo/src/app/avatar/AvatarShapeComponent'; + +import { AvatarSizeComponent } from './../../../../avatar/demo/src/app/avatar/AvatarSizeComponent'; + +import { AvatarStyleComponent } from './../../../../avatar/demo/src/app/avatar/AvatarStyleComponent'; + +import { AvatarTextComponent } from './../../../../avatar/demo/src/app/avatar/AvatarTextComponent'; + +import { ButtonColorComponent } from './../../../../button/demo/src/app/button/ButtonColorComponent'; + +import { ButtonDisabledComponent } from './../../../../button/demo/src/app/button/ButtonDisabledComponent'; + +import { ButtonEventComponent } from './../../../../button/demo/src/app/button/ButtonEventComponent'; + +import { ButtonFocusComponent } from './../../../../button/demo/src/app/button/ButtonFocusComponent'; + +import { ButtonHasborderTestComponent } from './../../../../button/demo/src/app/button/ButtonHasborderTestComponent'; + +import { ButtonHasborderComponent } from './../../../../button/demo/src/app/button/ButtonHasborderComponent'; + +import { ButtonIconComponent } from './../../../../button/demo/src/app/button/ButtonIconComponent'; + +import { ButtonLoadingComponent } from './../../../../button/demo/src/app/button/ButtonLoadingComponent'; + +import { ButtonOnlyiconComponent } from './../../../../button/demo/src/app/button/ButtonOnlyiconComponent'; + +import { ButtonSizeComponent } from './../../../../button/demo/src/app/button/ButtonSizeComponent'; + +import { ButtonTipComponent } from './../../../../button/demo/src/app/button/ButtonTipComponent'; + +import { ButtongroupActiveclassComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupActiveclassComponent'; + +import { ButtongroupBeforeclickComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupBeforeclickComponent'; + +import { ButtongroupDeselectableComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupDeselectableComponent'; + +import { ButtongroupDisabledComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupDisabledComponent'; + +import { ButtongroupEnumComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupEnumComponent'; + +import { ButtongroupEventComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupEventComponent'; + +import { ButtongroupFocusComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupFocusComponent'; + +import { ButtongroupIdTestComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupIdTestComponent'; + +import { ButtongroupIdComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupIdComponent'; + +import { ButtongroupItemsTestComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupItemsTestComponent'; + +import { ButtongroupItemsComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupItemsComponent'; + +import { ButtongroupManyComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupManyComponent'; + +import { ButtongroupMinwidthComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupMinwidthComponent'; + +import { ButtongroupMultiTypeComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupMultiTypeComponent'; + +import { ButtongroupMultilineComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupMultilineComponent'; + +import { ButtongroupMultipleComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupMultipleComponent'; + +import { ButtongroupRadioTypeComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupRadioTypeComponent'; + +import { ButtongroupReactiveFormsComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupReactiveFormsComponent'; + +import { ButtongroupSupTestComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupSupTestComponent'; + +import { ButtongroupSupComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupSupComponent'; + +import { ButtongroupTemplateComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupTemplateComponent'; + +import { ButtongroupTipComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupTipComponent'; + +import { ButtongroupValuekeyTestComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupValuekeyTestComponent'; + +import { ButtongroupValuekeyComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupValuekeyComponent'; + +import { ButtonselectBasicComponent } from './../../../../buttonselect/demo/src/app/buttonselect/ButtonselectBasicComponent'; + +import { ButtonselectLabelkeyComponent } from './../../../../buttonselect/demo/src/app/buttonselect/ButtonselectLabelkeyComponent'; + +import { CardAddComponent } from './../../../../card/demo/src/app/card/CardAddComponent'; + +import { CardBasicComponent } from './../../../../card/demo/src/app/card/CardBasicComponent'; + +import { CardGridComponent } from './../../../../card/demo/src/app/card/CardGridComponent'; + +import { CardGrid2Component } from './../../../../card/demo/src/app/card/CardGrid2Component'; + +import { CardHeaderComponent } from './../../../../card/demo/src/app/card/CardHeaderComponent'; + +import { CascaderBasicComponent } from './../../../../cascader/demo/src/app/cascader/CascaderBasicComponent'; + +import { CascaderClearableComponent } from './../../../../cascader/demo/src/app/cascader/CascaderClearableComponent'; + +import { CascaderDisabledComponent } from './../../../../cascader/demo/src/app/cascader/CascaderDisabledComponent'; + +import { CascaderEventsComponent } from './../../../../cascader/demo/src/app/cascader/CascaderEventsComponent'; + +import { CascaderIdkeyComponent } from './../../../../cascader/demo/src/app/cascader/CascaderIdkeyComponent'; + +import { CascaderItemTestComponent } from './../../../../cascader/demo/src/app/cascader/CascaderItemTestComponent'; + +import { CascaderLabelkeyComponent } from './../../../../cascader/demo/src/app/cascader/CascaderLabelkeyComponent'; + +import { CascaderOnlyselectleafComponent } from './../../../../cascader/demo/src/app/cascader/CascaderOnlyselectleafComponent'; + +import { CascaderPanelComponent } from './../../../../cascader/demo/src/app/cascader/CascaderPanelComponent'; + +import { CascaderSearchComponent } from './../../../../cascader/demo/src/app/cascader/CascaderSearchComponent'; + +import { CascaderShowalllevelComponent } from './../../../../cascader/demo/src/app/cascader/CascaderShowalllevelComponent'; + +import { CascaderTriggerComponent } from './../../../../cascader/demo/src/app/cascader/CascaderTriggerComponent'; + +import { CascaderValidComponent } from './../../../../cascader/demo/src/app/cascader/CascaderValidComponent'; + +import { CascaderValuekeyComponent } from './../../../../cascader/demo/src/app/cascader/CascaderValuekeyComponent'; + +import { CheckboxBasicComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxBasicComponent'; + +import { CheckboxDisabledComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxDisabledComponent'; + +import { CheckboxEventComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxEventComponent'; + +import { CheckboxFocusedComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxFocusedComponent'; + +import { CheckboxGroupComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxGroupComponent'; + +import { CheckboxGroupDirectionComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxGroupDirectionComponent'; + +import { CheckboxGroupLabelkeyComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxGroupLabelkeyComponent'; + +import { CheckboxGroupLevelComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxGroupLevelComponent'; + +import { CheckboxGroupLinewrapComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxGroupLinewrapComponent'; + +import { CheckboxGroupValidationComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxGroupValidationComponent'; + +import { CheckboxGroupValuekeyComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxGroupValuekeyComponent'; + +import { CheckboxIndeterminateComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxIndeterminateComponent'; + +import { CheckboxLabelComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxLabelComponent'; + +import { CollapseBasicComponent } from './../../../../collapse/demo/src/app/collapse/CollapseBasicComponent'; + +import { CollapseEventComponent } from './../../../../collapse/demo/src/app/collapse/CollapseEventComponent'; + +import { CollapseboxBasicComponent } from './../../../../collapsebox/demo/src/app/collapsebox/CollapseboxBasicComponent'; + +import { CollapseboxCloseableComponent } from './../../../../collapsebox/demo/src/app/collapsebox/CollapseboxCloseableComponent'; + +import { CollapseboxEventComponent } from './../../../../collapsebox/demo/src/app/collapsebox/CollapseboxEventComponent'; + +import { CollapsebuttonBasicComponent } from './../../../../collapsebutton/demo/src/app/collapsebutton/CollapsebuttonBasicComponent'; + +import { CollapsebuttonCustomtextComponent } from './../../../../collapsebutton/demo/src/app/collapsebutton/CollapsebuttonCustomtextComponent'; + +import { CollapsebuttonSearchcountComponent } from './../../../../collapsebutton/demo/src/app/collapsebutton/CollapsebuttonSearchcountComponent'; + +import { CollapsebuttonEventComponent } from './../../../../collapsebutton/demo/src/app/collapsebutton/CollapsebuttonEventComponent'; + +import { CollapsetextBasicComponent } from './../../../../collapsetext/demo/src/app/collapsetext/CollapsetextBasicComponent'; + +import { CollapsetextTypeComponent } from './../../../../collapsetext/demo/src/app/collapsetext/CollapsetextTypeComponent'; + +import { CollapsetextHighlightComponent } from './../../../../collapsetext/demo/src/app/collapsetext/CollapsetextHighlightComponent'; + +import { CollapsetextCollapsedComponent } from './../../../../collapsetext/demo/src/app/collapsetext/CollapsetextCollapsedComponent'; + +import { CollapsetextSceneComponent } from './../../../../collapsetext/demo/src/app/collapsetext/CollapsetextSceneComponent'; + +import { CopyBasicComponent } from './../../../../copy/demo/src/app/copy/CopyBasicComponent'; +import { CopyDarkComponent } from './../../../../copy/demo/src/app/copy/CopyDarkComponent'; + +import { CopyTipComponent } from './../../../../copy/demo/src/app/copy/CopyTipComponent'; + +import { CopyTableComponent } from './../../../../copy/demo/src/app/copy/CopyTableComponent'; + +import { CopyEventComponent } from './../../../../copy/demo/src/app/copy/CopyEventComponent'; + +import { CrumbBasicComponent } from './../../../../crumb/demo/src/app/crumb/CrumbBasicComponent'; + +import { CrumbEventsComponent } from './../../../../crumb/demo/src/app/crumb/CrumbEventsComponent'; + +import { CrumbHrefComponent } from './../../../../crumb/demo/src/app/crumb/CrumbHrefComponent'; + +import { CrumbRouterTestComponent } from './../../../../crumb/demo/src/app/crumb/CrumbRouterTestComponent'; + +import { CrumbRouterComponent } from './../../../../crumb/demo/src/app/crumb/CrumbRouterComponent'; + +import { DateCleariconComponent } from './../../../../date/demo/src/app/date/DateCleariconComponent'; + +import { DateCustomizeComponent } from './../../../../date/demo/src/app/date/DateCustomizeComponent'; + +import { DateDisabledComponent } from './../../../../date/demo/src/app/date/DateDisabledComponent'; + +import { DateDisableddaysComponent } from './../../../../date/demo/src/app/date/DateDisableddaysComponent'; + +import { DateEventComponent } from './../../../../date/demo/src/app/date/DateEventComponent'; + +import { DateFormComponent } from './../../../../date/demo/src/app/date/DateFormComponent'; + +import { DateFormatTestComponent } from './../../../../date/demo/src/app/date/DateFormatTestComponent'; + +import { DateFormatComponent } from './../../../../date/demo/src/app/date/DateFormatComponent'; + +import { DateMaxComponent } from './../../../../date/demo/src/app/date/DateMaxComponent'; + +import { DateMaxminTestComponent } from './../../../../date/demo/src/app/date/DateMaxminTestComponent'; + +import { DateMaxminComponent } from './../../../../date/demo/src/app/date/DateMaxminComponent'; + +import { DateMinComponent } from './../../../../date/demo/src/app/date/DateMinComponent'; + +import { DateNowdatetimeComponent } from './../../../../date/demo/src/app/date/DateNowdatetimeComponent'; + +import { DatePanelalignComponent } from './../../../../date/demo/src/app/date/DatePanelalignComponent'; + +import { DateValidationComponent } from './../../../../date/demo/src/app/date/DateValidationComponent'; + +import { DateValueTestComponent } from './../../../../date/demo/src/app/date/DateValueTestComponent'; + +import { DateValueComponent } from './../../../../date/demo/src/app/date/DateValueComponent'; + +import { DaterangeCustomizeComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeCustomizeComponent'; + +import { DaterangeDisabledComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeDisabledComponent'; + +import { DaterangeDisableddaysComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeDisableddaysComponent'; + +import { DaterangeEventComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeEventComponent'; + +import { DaterangeFixedvalueTestComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeFixedvalueTestComponent'; + +import { DaterangeFixedvalueComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeFixedvalueComponent'; + +import { DaterangeFormatTestComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeFormatTestComponent'; + +import { DaterangeFormatComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeFormatComponent'; + +import { DaterangeIsallowbeginequalendComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeIsallowbeginequalendComponent'; + +import { DaterangeMaxComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeMaxComponent'; + +import { DaterangeMaxminTestComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeMaxminTestComponent'; + +import { DaterangeMaxminComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeMaxminComponent'; + +import { DaterangeMinComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeMinComponent'; + +import { DaterangeNowdatetimeComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeNowdatetimeComponent'; + +import { DaterangePanelalignComponent } from './../../../../daterange/demo/src/app/daterange/DaterangePanelalignComponent'; + +import { DaterangeValidationComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeValidationComponent'; + +import { DaterangeValueTestComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeValueTestComponent'; + +import { DaterangeValueComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeValueComponent'; + +import { DatetimeCleariconComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeCleariconComponent'; + +import { DatetimeCustomizeComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeCustomizeComponent'; + +import { DatetimeDisabledComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeDisabledComponent'; + +import { DatetimeEventComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeEventComponent'; + +import { DatetimeFormatTestComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeFormatTestComponent'; + +import { DatetimeFormatComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeFormatComponent'; + +import { DatetimeMaxComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeMaxComponent'; + +import { DatetimeMaxminTestComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeMaxminTestComponent'; + +import { DatetimeMaxminComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeMaxminComponent'; + +import { DatetimeMinComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeMinComponent'; + +import { DatetimeNowdatetimeComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeNowdatetimeComponent'; + +import { DatetimePanelalignComponent } from './../../../../datetime/demo/src/app/datetime/DatetimePanelalignComponent'; + +import { DatetimeValidationComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeValidationComponent'; + +import { DatetimeValueTestComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeValueTestComponent'; + +import { DatetimeValueComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeValueComponent'; + +import { DatetimeTimezoneableComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeTimezoneableComponent'; + +import { DatetimerangeCleariconComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeCleariconComponent'; + +import { DatetimerangeCustomizeComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeCustomizeComponent'; + +import { DatetimerangeDisabledComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeDisabledComponent'; + +import { DatetimerangeEventComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeEventComponent'; + +import { DatetimerangeFormatTestComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeFormatTestComponent'; + +import { DatetimerangeFormatComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeFormatComponent'; + +import { DatetimerangeIsallowbeginequalendComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeIsallowbeginequalendComponent'; + +import { DatetimerangeManyTestComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeManyTestComponent'; + +import { DatetimerangeMaxComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeMaxComponent'; + +import { DatetimerangeMaxminTestComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeMaxminTestComponent'; + +import { DatetimerangeMaxminComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeMaxminComponent'; + +import { DatetimerangeMinComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeMinComponent'; + +import { DatetimerangeNowdatetimeComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeNowdatetimeComponent'; + +import { DatetimerangePanelalignComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangePanelalignComponent'; + +import { DatetimerangeTimezoneableComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeTimezoneableComponent'; + +import { DatetimerangeValidationComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeValidationComponent'; + +import { DatetimerangeValueTestComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeValueTestComponent'; + +import { DatetimerangeValueComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeValueComponent'; + +import { GuidesBasicComponent } from './../../../../guides/demo/src/app/guides/GuidesBasicComponent'; + +import { GuidesTabComponent } from './../../../../guides/demo/src/app/guides/GuidesTabComponent'; + +import { GuidesGuidestepsComponent } from './../../../../guides/demo/src/app/guides/GuidesGuidestepsComponent'; + +import { GuidesTypeComponent } from './../../../../guides/demo/src/app/guides/GuidesTypeComponent'; + +import { FormfieldColspanRowspanTestComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldColspanRowspanTestComponent'; + +import { FormfieldColspanRowspanComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldColspanRowspanComponent'; + +import { FormfieldColswidthComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldColswidthComponent'; + +import { FormfieldFooComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldFooComponent'; + +import { FormfieldIndexComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldIndexComponent'; + +import { FormfieldLabelComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldLabelComponent'; + +import { FormfieldLabelwidthComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldLabelwidthComponent'; + +import { FormfieldMultiColumnComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldMultiColumnComponent'; + +import { FormfieldNestFormfiledComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldNestFormfiledComponent'; + +import { FormfieldNgforTestComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldNgforTestComponent'; + +import { FormfieldRequiredComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldRequiredComponent'; + +import { FormfieldRequiredspaceComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldRequiredspaceComponent'; + +import { FormfieldShowComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldShowComponent'; + +import { FormfieldSingleColumnComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldSingleColumnComponent'; + +import { FormfieldTestComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldTestComponent'; + +import { FormfieldTextFormComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldTextFormComponent'; + +import { FormfieldVerticalAlignComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldVerticalAlignComponent'; + +import { FormfieldVerticalComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldVerticalComponent'; + +import { FoldtextBasicComponent } from './../../../../foldtext/demo/src/app/foldtext/FoldtextBasicComponent'; + +import { FoldtextTableComponent } from './../../../../foldtext/demo/src/app/foldtext/FoldtextTableComponent'; + +import { GuidestepsBasicComponent } from './../../../../guidesteps/demo/src/app/guidesteps/GuidestepsBasicComponent'; + +import { GuidestepsIscompleteComponent } from './../../../../guidesteps/demo/src/app/guidesteps/GuidestepsIscompleteComponent'; + +import { GuidestepsLargeComponent } from './../../../../guidesteps/demo/src/app/guidesteps/GuidestepsLargeComponent'; + +import { HalfmodalAsyncComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalAsyncComponent'; + +import { HalfmodalBackdropComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalBackdropComponent'; + +import { HalfmodalBasicComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalBasicComponent'; + +import { HalfmodalBeforehideComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalBeforehideComponent'; + +import { HalfmodalContentComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalContentComponent'; + +import { HalfmodalModalComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalModalComponent'; + +import { HalfmodalModalselectComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalModalselectComponent'; + +import { HalfmodalMultiComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalMultiComponent'; + +import { HalfmodalServiceTestComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalServiceTestComponent'; + +import { HalfmodalServiceComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalServiceComponent'; + +import { IconBasicComponent } from './../../../../icon/demo/src/app/icon/IconBasicComponent'; + +import { SvgSetpathComponent } from '../../../../icon/demo/src/app/icon/SvgSetpathComponent'; + +import { IconShowComponent } from './../../../../icon/demo/src/app/icon/IconShowComponent'; + +import { IconactionBasicComponent } from './../../../../iconaction/demo/src/app/iconaction/IconactionBasicComponent'; + +import { IconactionDarkComponent } from './../../../../iconaction/demo/src/app/iconaction/IconactionDarkComponent'; + +import { IconactionDisabledComponent } from './../../../../iconaction/demo/src/app/iconaction/IconactionDisabledComponent'; + +import { IconactionHrefComponent } from './../../../../iconaction/demo/src/app/iconaction/IconactionHrefComponent'; + +import { ImagepreviewBasicComponent } from './../../../../imagepreview/demo/src/app/imagepreview/ImagepreviewBasicComponent'; + +import { InputnumberBasicComponent } from './../../../../inputnumber/demo/src/app/inputnumber/InputnumberBasicComponent'; + +import { InputnumberEventComponent } from './../../../../inputnumber/demo/src/app/inputnumber/InputnumberEventComponent'; + +import { InputnumberFocusComponent } from './../../../../inputnumber/demo/src/app/inputnumber/InputnumberFocusComponent'; + +import { InputnumberFormatComponent } from './../../../../inputnumber/demo/src/app/inputnumber/InputnumberFormatComponent'; + +import { InputnumberLoadComponent } from './../../../../inputnumber/demo/src/app/inputnumber/InputnumberLoadComponent'; + +import { InputnumberLocaleableComponent } from './../../../../inputnumber/demo/src/app/inputnumber/InputnumberLocaleableComponent'; + +import { InputnumberMaxlengthComponent } from './../../../../inputnumber/demo/src/app/inputnumber/InputnumberMaxlengthComponent'; + +import { IntroBasicComponent } from './../../../../intro/demo/src/app/intro/IntroBasicComponent'; + +import { IntroEventComponent } from './../../../../intro/demo/src/app/intro/IntroEventComponent'; + +import { IntroModalComponent } from './../../../../intro/demo/src/app/intro/IntroModalComponent'; + +import { IntroSkipableComponent } from './../../../../intro/demo/src/app/intro/IntroSkipableComponent'; + +import { IntroTemplateComponent } from './../../../../intro/demo/src/app/intro/IntroTemplateComponent'; + +import { IntroTipComponent } from './../../../../intro/demo/src/app/intro/IntroTipComponent'; + +import { IntroTiscrollComponent } from './../../../../intro/demo/src/app/intro/IntroTiscrollComponent'; + +import { IpBasicComponent } from './../../../../ip/demo/src/app/ip/IpBasicComponent'; + +import { IpDisabledComponent } from './../../../../ip/demo/src/app/ip/IpDisabledComponent'; + +import { IpFormcontrolComponent } from './../../../../ip/demo/src/app/ip/IpFormcontrolComponent'; + +import { IpValidComponent } from './../../../../ip/demo/src/app/ip/IpValidComponent'; + +import { IpsectionBasicComponent } from './../../../../ipsection/demo/src/app/ipsection/IpsectionBasicComponent'; + +import { IpsectionDisabledComponent } from './../../../../ipsection/demo/src/app/ipsection/IpsectionDisabledComponent'; + +import { IpsectionEventsComponent } from './../../../../ipsection/demo/src/app/ipsection/IpsectionEventsComponent'; + +import { IpsectionFocusComponent } from './../../../../ipsection/demo/src/app/ipsection/IpsectionFocusComponent'; + +import { IpsectionTestComponent } from './../../../../ipsection/demo/src/app/ipsection/IpsectionTestComponent'; + +import { IpsectionValidFormgroupComponent } from './../../../../ipsection/demo/src/app/ipsection/IpsectionValidFormgroupComponent'; + +import { IpsectionValidComponent } from './../../../../ipsection/demo/src/app/ipsection/IpsectionValidComponent'; + +import { LabeleditorBasicComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorBasicComponent'; + +import { LabeleditorAutotipComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorAutotipComponent'; + +import { LabeleditorIconTipContextComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorIcontipcontextComponent'; + +import { LabeleditorResizeComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorResizeComponent'; + +import { LabeleditorMaxlineComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorMaxlineComponent'; + +import { LabeleditorMaxlengthComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorMaxlengthComponent'; + +import { LabeleditorValidationComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorValidationComponent'; + +import { LabeleditorValidationAsyncComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorValidationAsyncComponent'; + +import { LabeleditorDisabledComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorDisabledComponent'; + +import { LabeleditorEventsComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorEventsComponent'; + +import { LabeleditorMultilineSizeComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorMultilineSizeComponent'; + +import { LayoutBasicSimpleResponsiveComponent } from './../../../../layout/demo/src/app/layout/LayoutBasicSimpleResponsiveComponent'; + +import { LayoutBasicSimpleComponent } from './../../../../layout/demo/src/app/layout/LayoutBasicSimpleComponent'; + +import { LayoutBasicComponent } from './../../../../layout/demo/src/app/layout/LayoutBasicComponent'; + +import { LayoutDetailColumnComponent } from './../../../../layout/demo/src/app/layout/LayoutDetailColumnComponent'; + +import { LayoutDetailComponent } from './../../../../layout/demo/src/app/layout/LayoutDetailComponent'; + +import { LayoutListLargedataComponent } from './../../../../layout/demo/src/app/layout/LayoutListLargedataComponent'; + +import { LayoutListComponent } from './../../../../layout/demo/src/app/layout/LayoutListComponent'; + +import { LayoutMultiColumnComponent } from './../../../../layout/demo/src/app/layout/LayoutMultiColumnComponent'; + +import { LayoutOverviewVerticalComponent } from './../../../../layout/demo/src/app/layout/LayoutOverviewVerticalComponent'; + +import { LayoutOverviewComponent } from './../../../../layout/demo/src/app/layout/LayoutOverviewComponent'; + +import { LayoutPurchaseResponsiveChangeComponent } from './../../../../layout/demo/src/app/layout/LayoutPurchaseResponsiveChangeComponent'; + +import { LayoutPurchaseResponsiveComponent } from './../../../../layout/demo/src/app/layout/LayoutPurchaseResponsiveComponent'; + +import { LayoutPurchaseSimpleResponsiveComponent } from './../../../../layout/demo/src/app/layout/LayoutPurchaseSimpleResponsiveComponent'; + +import { LayoutPurchaseSimpleComponent } from './../../../../layout/demo/src/app/layout/LayoutPurchaseSimpleComponent'; + +import { LayoutPurchaseComponent } from './../../../../layout/demo/src/app/layout/LayoutPurchaseComponent'; + +import { LayoutSingleComponent } from './../../../../layout/demo/src/app/layout/LayoutSingleComponent'; + +import { LeftmenuActiveChangeWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuActiveChangeWebsiteViewComponent'; + +import { LeftmenuBasicWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuBasicWebsiteViewComponent'; + +import { LeftmenuCollapsedWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuCollapsedWebsiteViewComponent'; + +import { LeftmenuDisabledWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDisabledWebsiteViewComponent'; + +import { LeftmenuDividingWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDividingWebsiteViewComponent'; + +import { LeftmenuFootWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuFootWebsiteViewComponent'; + +import { LeftmenuGroupWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuGroupWebsiteViewComponent'; + +import { LeftmenuHrefWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuHrefWebsiteViewComponent'; + +import { LeftmenuNoRouterWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuNoRouterWebsiteViewComponent'; + +import { LeftmenuParamsWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuParamsWebsiteViewComponent'; + +import { LeftmenuReloadStateWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuReloadStateWebsiteViewComponent'; + +import { LeftmenuRouterlistWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuRouterlistWebsiteViewComponent'; + +import { LeftmenuToggleableWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuToggleableWebsiteViewComponent'; + +import { LoadingAreaComponent } from './../../../../loading/demo/src/app/loading/LoadingAreaComponent'; + +import { LoadingBasicComponent } from './../../../../loading/demo/src/app/loading/LoadingBasicComponent'; + +import { LoadingSizeComponent } from './../../../../loading/demo/src/app/loading/LoadingSizeComponent'; + +import { LoadingTypeComponent } from './../../../../loading/demo/src/app/loading/LoadingTypeComponent'; + +import { LocaleBasicComponent } from './../../../../locale/demo/src/app/locale/LocaleBasicComponent'; + +import { LocaleFormatComponent } from './../../../../locale/demo/src/app/locale/LocaleFormatComponent'; + +import { LocaleReloadComponent } from './../../../../locale/demo/src/app/locale/LocaleReloadComponent'; + +import { LinkbuttonBasicComponent } from './../../../../linkbutton/demo/src/app/linkbutton/LinkbuttonBasicComponent'; + +import { MenuBasicComponent } from './../../../../menu/demo/src/app/menu/MenuBasicComponent'; + +import { MenuBeforeopenComponent } from './../../../../menu/demo/src/app/menu/MenuBeforeopenComponent'; + +import { MenuBorderComponent } from './../../../../menu/demo/src/app/menu/MenuBorderComponent'; + +import { MenuButtoncolorComponent } from './../../../../menu/demo/src/app/menu/MenuButtoncolorComponent'; + +import { MenuDefaultComponent } from './../../../../menu/demo/src/app/menu/MenuDefaultComponent'; + +import { MenuDisabledComponent } from './../../../../menu/demo/src/app/menu/MenuDisabledComponent'; + +import { MenuEventComponent } from './../../../../menu/demo/src/app/menu/MenuEventComponent'; + +import { MenuGroupComponent } from './../../../../menu/demo/src/app/menu/MenuGroupComponent'; + +import { MenuIdComponent } from './../../../../menu/demo/src/app/menu/MenuIdComponent'; + +import { MenuLabelkeyComponent } from './../../../../menu/demo/src/app/menu/MenuLabelkeyComponent'; + +import { MenuPanelalignComponent } from './../../../../menu/demo/src/app/menu/MenuPanelalignComponent'; + +import { MenuPanelstyleComponent } from './../../../../menu/demo/src/app/menu/MenuPanelstyleComponent'; + +import { MenuTempleteTestComponent } from './../../../../menu/demo/src/app/menu/MenuTempleteTestComponent'; + +import { MenuTempleteComponent } from './../../../../menu/demo/src/app/menu/MenuTempleteComponent'; + +import { MenuTipsComponent } from './../../../../menu/demo/src/app/menu/MenuTipsComponent'; + +import { MessageBasicComponent } from './../../../../message/demo/src/app/message/MessageBasicComponent'; + +import { MessageBtnTestComponent } from './../../../../message/demo/src/app/message/MessageBtnTestComponent'; + +import { MessageBtnComponent } from './../../../../message/demo/src/app/message/MessageBtnComponent'; + +import { MessageContentComponent } from './../../../../message/demo/src/app/message/MessageContentComponent'; + +import { MessageIdComponent } from './../../../../message/demo/src/app/message/MessageIdComponent'; + +import { MessageSecurityComponent } from './../../../../message/demo/src/app/message/MessageSecurityComponent'; + +import { MessageTitleComponent } from './../../../../message/demo/src/app/message/MessageTitleComponent'; + +import { MessageTypeComponent } from './../../../../message/demo/src/app/message/MessageTypeComponent'; + +import { ModalAnimationComponent } from './../../../../modal/demo/src/app/modal/ModalAnimationComponent'; + +import { ModalBackdropComponent } from './../../../../modal/demo/src/app/modal/ModalBackdropComponent'; + +import { ModalClassComponent } from './../../../../modal/demo/src/app/modal/ModalClassComponent'; + +import { ModalCloseIconComponent } from './../../../../modal/demo/src/app/modal/ModalCloseIconComponent'; + +import { ModalConfigTestComponent } from './../../../../modal/demo/src/app/modal/ModalConfigTestComponent'; + +import { ModalContentCompComponent } from './../../../../modal/demo/src/app/modal/ModalContentCompComponent'; + +import { ModalContentTempComponent } from './../../../../modal/demo/src/app/modal/ModalContentTempComponent'; + +import { ModalDraggableComponent } from './../../../../modal/demo/src/app/modal/ModalDraggableComponent'; + +import { ModalEscComponent } from './../../../../modal/demo/src/app/modal/ModalEscComponent'; + +import { ModalEventComponent } from './../../../../modal/demo/src/app/modal/ModalEventComponent'; + +import { ModalHeaderAlignComponent } from './../../../../modal/demo/src/app/modal/ModalHeaderAlignComponent'; + +import { ModalHeaderStyleComponent } from './../../../../modal/demo/src/app/modal/ModalHeaderStyleComponent'; + +import { ModalTwoBackdropComponent } from './../../../../modal/demo/src/app/modal/ModalTwoBackdropComponent'; + +import { ModalTwoTestComponent } from './../../../../modal/demo/src/app/modal/ModalTwoTestComponent'; + +import { NavActiveComponent } from './../../../../nav/demo/src/app/nav/NavActiveComponent'; + +import { NavAlignComponent } from './../../../../nav/demo/src/app/nav/NavAlignComponent'; + +import { NavBasicComponent } from './../../../../nav/demo/src/app/nav/NavBasicComponent'; + +import { NavDisabledComponent } from './../../../../nav/demo/src/app/nav/NavDisabledComponent'; + +import { NavEventComponent } from './../../../../nav/demo/src/app/nav/NavEventComponent'; + +import { NavLeftComponent } from './../../../../nav/demo/src/app/nav/NavLeftComponent'; + +import { NavRightComponent } from './../../../../nav/demo/src/app/nav/NavRightComponent'; + +import { NavSelectableComponent } from './../../../../nav/demo/src/app/nav/NavSelectableComponent'; + +import { NavSubmenuComponent } from './../../../../nav/demo/src/app/nav/NavSubmenuComponent'; + +import { NavTemplateComponent } from './../../../../nav/demo/src/app/nav/NavTemplateComponent'; + +import { NavThemeComponent } from './../../../../nav/demo/src/app/nav/NavThemeComponent'; + +import { NavWidthComponent } from './../../../../nav/demo/src/app/nav/NavWidthComponent'; + +import { NotificationAnimationComponent } from './../../../../notification/demo/src/app/notification/NotificationAnimationComponent'; + +import { NotificationBasicComponent } from './../../../../notification/demo/src/app/notification/NotificationBasicComponent'; + +import { NotificationCloseComponent } from './../../../../notification/demo/src/app/notification/NotificationCloseComponent'; + +import { NotificationConfigComponent } from './../../../../notification/demo/src/app/notification/NotificationConfigComponent'; + +import { NotificationDurationComponent } from './../../../../notification/demo/src/app/notification/NotificationDurationComponent'; + +import { NotificationEventsComponent } from './../../../../notification/demo/src/app/notification/NotificationEventsComponent'; + +import { NotificationHoverPauseComponent } from './../../../../notification/demo/src/app/notification/NotificationHoverPauseComponent'; + +import { NotificationNameComponent } from './../../../../notification/demo/src/app/notification/NotificationNameComponent'; + +import { NotificationPositionComponent } from './../../../../notification/demo/src/app/notification/NotificationPositionComponent'; + +import { NotificationTemplateComponent } from './../../../../notification/demo/src/app/notification/NotificationTemplateComponent'; + +import { NotificationTypeComponent } from './../../../../notification/demo/src/app/notification/NotificationTypeComponent'; + +import { OverflowDestoryComponent } from './../../../../overflow/demo/src/app/overflow/OverflowDestoryComponent'; + +import { OverflowDirectiveComponent } from './../../../../overflow/demo/src/app/overflow/OverflowDirectiveComponent'; + +import { OverflowMaxlineComponent } from './../../../../overflow/demo/src/app/overflow/OverflowMaxlineComponent'; + +import { OverflowMaxwidthComponent } from './../../../../overflow/demo/src/app/overflow/OverflowMaxwidthComponent'; + +import { OverflowPositionComponent } from './../../../../overflow/demo/src/app/overflow/OverflowPositionComponent'; + +import { OverflowServiceComponent } from './../../../../overflow/demo/src/app/overflow/OverflowServiceComponent'; + +import { OverflowTestComponent } from './../../../../overflow/demo/src/app/overflow/OverflowTestComponent'; + +import { OverflowTipcontentComponent } from './../../../../overflow/demo/src/app/overflow/OverflowTipcontentComponent'; + +import { PhonenumberBasicComponent } from './../../../../phonenumber/demo/src/app/phonenumber/PhonenumberBasicComponent'; + +import { PhonenumberDisabledComponent } from './../../../../phonenumber/demo/src/app/phonenumber/PhonenumberDisabledComponent'; + +import { PhonenumberEventComponent } from './../../../../phonenumber/demo/src/app/phonenumber/PhonenumberEventComponent'; + +import { PhonenumberValidComponent } from './../../../../phonenumber/demo/src/app/phonenumber/PhonenumberValidComponent'; + +import { PhonenumberCountryComponent } from './../../../../phonenumber/demo/src/app/phonenumber/PhonenumberCountryComponent'; + +import { PaginationAutohideComponent } from './../../../../pagination/demo/src/app/pagination/PaginationAutohideComponent'; + +import { PaginationDisabledComponent } from './../../../../pagination/demo/src/app/pagination/PaginationDisabledComponent'; + +import { PaginationEventComponent } from './../../../../pagination/demo/src/app/pagination/PaginationEventComponent'; + +import { PaginationFixedComponent } from './../../../../pagination/demo/src/app/pagination/PaginationFixedComponent'; + +import { PaginationLoadingComponent } from './../../../../pagination/demo/src/app/pagination/PaginationLoadingComponent'; + +import { PaginationPageselectwidthComponent } from './../../../../pagination/demo/src/app/pagination/PaginationPageselectwidthComponent'; + +import { PaginationPagesizeComponent } from './../../../../pagination/demo/src/app/pagination/PaginationPagesizeComponent'; + +import { PaginationShowgotolinkComponent } from './../../../../pagination/demo/src/app/pagination/PaginationShowgotolinkComponent'; + +import { PaginationShowlastpageComponent } from './../../../../pagination/demo/src/app/pagination/PaginationShowlastpageComponent'; + +import { PaginationShowtotalnumberComponent } from './../../../../pagination/demo/src/app/pagination/PaginationShowtotalnumberComponent'; + +import { PaginationTypeComponent } from './../../../../pagination/demo/src/app/pagination/PaginationTypeComponent'; + +import { PathfieldItemsComponent } from './../../../../path/demo/src/app/path/PathfieldItemsComponent'; + +import { PathfieldIspanelComponent } from './../../../../path/demo/src/app/path/PathfieldIspanelComponent'; + +import { PathfieldPanelwidthComponent } from './../../../../path/demo/src/app/path/PathfieldPanelwidthComponent'; + +import { PathfieldEditableComponent } from './../../../../path/demo/src/app/path/PathfieldEditableComponent'; + +import { PathfieldEventComponent } from '../../../../path/demo/src/app/path/PathfieldEventComponent'; + +import { PathListComponent } from './../../../../path/demo/src/app/path/PathListComponent'; + +import { PathSelectComponent } from './../../../../path/demo/src/app/path/PathSelectComponent'; + +import { PopconfirmBasicComponent } from './../../../../popconfirm/demo/src/app/popconfirm/PopconfirmBasicComponent'; + +import { PopconfirmDefineComponent } from './../../../../popconfirm/demo/src/app/popconfirm/PopconfirmDefineComponent'; + +import { PopconfirmEventComponent } from './../../../../popconfirm/demo/src/app/popconfirm/PopconfirmEventComponent'; + +import { PopconfirmTableDefineComponent } from './../../../../popconfirm/demo/src/app/popconfirm/PopconfirmTableDefineComponent'; + +import { PopconfirmTableComponent } from './../../../../popconfirm/demo/src/app/popconfirm/PopconfirmTableComponent'; + +import { ProgressbarAnimationComponent } from './../../../../progressbar/demo/src/app/progressbar/ProgressbarAnimationComponent'; + +import { ProgressbarBasicComponent } from './../../../../progressbar/demo/src/app/progressbar/ProgressbarBasicComponent'; + +import { ProgressbarClassComponent } from './../../../../progressbar/demo/src/app/progressbar/ProgressbarClassComponent'; + +import { ProductpreviewBasicComponent } from './../../../../productpreview/demo/src/app/productpreview/ProductpreviewBasicComponent'; + +import { RadioBasicComponent } from './../../../../radio/demo/src/app/radio/RadioBasicComponent'; + +import { RadioDarkComponent } from './../../../../radio/demo/src/app/radio/RadioDarkComponent'; + +import { RadioDisabledComponent } from './../../../../radio/demo/src/app/radio/RadioDisabledComponent'; + +import { RadioEventComponent } from './../../../../radio/demo/src/app/radio/RadioEventComponent'; + +import { RadioFocusComponent } from './../../../../radio/demo/src/app/radio/RadioFocusComponent'; + +import { RadioGroupComponent } from './../../../../radio/demo/src/app/radio/RadioGroupComponent'; + +import { RadioGroupDirectionComponent } from './../../../../radio/demo/src/app/radio/RadioGroupDirectionComponent'; + +import { RadioGroupLabelkeyComponent } from './../../../../radio/demo/src/app/radio/RadioGroupLabelkeyComponent'; + +import { RadioGroupLinewrapComponent } from './../../../../radio/demo/src/app/radio//RadioGroupLinewrapComponent'; + +import { RadioGroupValidationComponent } from './../../../../radio/demo/src/app/radio//RadioGroupValidationComponent'; + +import { RadioGroupValuekeyComponent } from './../../../../radio/demo/src/app/radio/RadioGroupValuekeyComponent'; + +import { RadioLabelComponent } from './../../../../radio/demo/src/app/radio/RadioLabelComponent'; + +import { RateBasicComponent } from './../../../../rate/demo/src/app/rate/RateBasicComponent'; + +import { RateDisabledComponent } from './../../../../rate/demo/src/app/rate/RateDisabledComponent'; + +import { RateEventComponent } from './../../../../rate/demo/src/app/rate/RateEventComponent'; + +import { RateIdComponent } from './../../../../rate/demo/src/app/rate/RateIdComponent'; + +import { RateLoadComponent } from './../../../../rate/demo/src/app/rate/RateLoadComponent'; + +import { RightsBasicComponent } from './../../../../rights/demo/src/app/rights/RightsBasicComponent'; + +import { RightsTypeComponent } from './../../../../rights/demo/src/app/rights/RightsTypeComponent'; + +import { ScoreBasicComponent } from './../../../../score/demo/src/app/score/ScoreBasicComponent'; + +import { ScoreEventsComponent } from './../../../../score/demo/src/app/score/ScoreEventsComponent'; + +import { ScoreLimittextComponent } from './../../../../score/demo/src/app/score/ScoreLimittextComponent'; + +import { ScorePaddingComponent } from './../../../../score/demo/src/app/score/ScorePaddingComponent'; + +import { SearchboxAppendtobodyComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxAppendtobodyComponent'; + +import { SearchboxBasicComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxBasicComponent'; + +import { SearchboxDisabledComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxDisabledComponent'; + +import { SearchboxEventComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxEventComponent'; + +import { SearchboxMaxlengthComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxMaxlengthComponent'; + +import { SearchboxNotsearchComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxNotsearchComponent'; + +import { SearchboxOptionsComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxOptionsComponent'; + +import { SearchboxPanelsizeComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxPanelsizeComponent'; + +import { SearchboxReactiveComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxReactiveComponent'; + +import { SearchboxSuggestComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxSuggestComponent'; + +import { SearchboxTemplateComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxTemplateComponent'; + +import { SearchboxTestComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxTestComponent'; + +import { SearchboxTrimmedComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxTrimmedComponent'; + +import { SearchboxValidComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxValidComponent'; + +import { SearchboxVirtualscrollComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxVirtualscrollComponent'; + +import { SelectgroupBasicComponent } from './../../../../selectgroup/demo/src/app/selectgroup/SelectgroupBasicComponent'; + +import { SelectgroupMultipleComponent } from './../../../../selectgroup/demo/src/app/selectgroup/SelectgroupMultipleComponent'; + +import { SelectgroupValuekeyComponent } from './../../../../selectgroup/demo/src/app/selectgroup/SelectgroupValuekeyComponent'; + +import { SelectgroupTemplateComponent } from './../../../../selectgroup/demo/src/app/selectgroup/SelectgroupTemplateComponent'; + +import { SelectgroupSelectComponent } from './../../../../selectgroup/demo/src/app/selectgroup/SelectgroupSelectComponent'; + +import { SelectAppendtobodyComponent } from './../../../../select/demo/src/app/select/SelectAppendtobodyComponent'; + +import { SelectBasicComponent } from './../../../../select/demo/src/app/select/SelectBasicComponent'; + +import { SelectBeforesearchTestComponent } from './../../../../select/demo/src/app/select/SelectBeforesearchTestComponent'; + +import { SelectBeforesearchComponent } from './../../../../select/demo/src/app/select/SelectBeforesearchComponent'; + +import { SelectChangeSelectallComponent } from './../../../../select/demo/src/app/select/SelectChangeSelectallComponent'; + +import { SelectClearableComponent } from './../../../../select/demo/src/app/select/SelectClearableComponent'; + +import { SelectDisabledComponent } from './../../../../select/demo/src/app/select/SelectDisabledComponent'; + +import { SelectDisabledfocusComponent } from './../../../../select/demo/src/app/select/SelectDisabledfocusComponent'; + +import { SelectEnumComponent } from './../../../../select/demo/src/app/select/SelectEnumComponent'; + +import { SelectEventComponent } from './../../../../select/demo/src/app/select/SelectEventComponent'; + +import { SelectFocusComponent } from './../../../../select/demo/src/app/select/SelectFocusComponent'; + +import { SelectGroupComponent } from './../../../../select/demo/src/app/select/SelectGroupComponent'; + +import { SelectIdComponent } from './../../../../select/demo/src/app/select/SelectIdComponent'; + +import { SelectIdkeyComponent } from './../../../../select/demo/src/app/select/SelectIdkeyComponent'; + +import { SelectInputComponent } from './../../../../select/demo/src/app/select/SelectInputComponent'; + +import { SelectLabelkeyComponent } from './../../../../select/demo/src/app/select/SelectLabelkeyComponent'; + +import { SelectLazyComponent } from './../../../../select/demo/src/app/select/SelectLazyComponent'; + +import { SelectLeakComponent } from './../../../../select/demo/src/app/select/SelectLeakComponent'; + +import { SelectLoadComponent } from './../../../../select/demo/src/app/select/SelectLoadComponent'; + +import { SelectManyComponent } from './../../../../select/demo/src/app/select/SelectManyComponent'; + +import { SelectMaxlineComponent } from './../../../../select/demo/src/app/select/SelectMaxlineComponent'; + +import { SelectMuchComponent } from './../../../../select/demo/src/app/select/SelectMuchComponent'; + +import { SelectMultiComponent } from './../../../../select/demo/src/app/select/SelectMultiComponent'; + +import { SelectNoborderComponent } from './../../../../select/demo/src/app/select/SelectNoborderComponent'; + +import { SelectNodataComponent } from './../../../../select/demo/src/app/select/SelectNodataComponent'; + +import { SelectNoemptyComponent } from './../../../../select/demo/src/app/select/SelectNoemptyComponent'; + +import { SelectNullComponent } from './../../../../select/demo/src/app/select/SelectNullComponent'; + +import { SelectPaginBeforesearchComponent } from './../../../../select/demo/src/app/select/SelectPaginBeforesearchComponent'; + +import { SelectPaginationComponent } from './../../../../select/demo/src/app/select/SelectPaginationComponent'; + +import { SelectPanelComponent } from './../../../../select/demo/src/app/select/SelectPanelComponent'; + +import { SelectReservesearchwordComponent } from './../../../../select/demo/src/app/select/SelectReservesearchwordComponent'; + +import { SelectScrollLoadComponent } from './../../../../select/demo/src/app/select/SelectScrollLoadComponent'; + +import { SelectSearchComponent } from './../../../../select/demo/src/app/select/SelectSearchComponent'; + +import { SelectSearchkeysComponent } from './../../../../select/demo/src/app/select/SelectSearchkeysComponent'; + +import { SelectSelectallComponent } from './../../../../select/demo/src/app/select/SelectSelectallComponent'; + +import { SelectShowselectednumberComponent } from './../../../../select/demo/src/app/select/SelectShowselectednumberComponent'; + +import { SelectSmallComponent } from './../../../../select/demo/src/app/select/SelectSmallComponent'; + +import { SelectTagComponent } from './../../../../select/demo/src/app/select/SelectTagComponent'; + +import { SelectTemplateComponent } from './../../../../select/demo/src/app/select/SelectTemplateComponent'; + +import { SelectTipComponent } from './../../../../select/demo/src/app/select/SelectTipComponent'; + +import { SelectTiscrollComponent } from './../../../../select/demo/src/app/select/SelectTiscrollComponent'; + +import { SelectTworowComponent } from './../../../../select/demo/src/app/select/SelectTworowComponent'; + +import { SelectValidComponent } from './../../../../select/demo/src/app/select/SelectValidComponent'; + +import { SelectValidgroupComponent } from './../../../../select/demo/src/app/select/SelectValidgroupComponent'; + +import { SelectValuekeyTestComponent } from './../../../../select/demo/src/app/select/SelectValuekeyTestComponent'; + +import { SelectValuekeyComponent } from './../../../../select/demo/src/app/select/SelectValuekeyComponent'; + +import { SelectVirtualscrollMultiComponent } from './../../../../select/demo/src/app/select/SelectVirtualscrollMultiComponent'; + +import { SelectVirtualscrollComponent } from './../../../../select/demo/src/app/select/SelectVirtualscrollComponent'; + +import { SkeletonPageComponent } from './../../../../skeleton/demo/src/app/skeleton/SkeletonPageComponent'; + +import { SkeletonTitleComponent } from './../../../../skeleton/demo/src/app/skeleton/SkeletonTitleComponent'; + +import { SkeletonTypeComponent } from './../../../../skeleton/demo/src/app/skeleton/SkeletonTypeComponent'; + +import { SliderEventComponent } from './../../../../slider/demo/src/app/slider/SliderEventComponent'; + +import { SliderFormcontrolComponent } from './../../../../slider/demo/src/app/slider/SliderFormcontrolComponent'; + +import { SliderHiddenComponent } from './../../../../slider/demo/src/app/slider/SliderHiddenComponent'; + +import { SliderLimitsComponent } from './../../../../slider/demo/src/app/slider/SliderLimitsComponent'; + +import { SliderRatiosComponent } from './../../../../slider/demo/src/app/slider/SliderRatiosComponent'; + +import { SliderScalesComponent } from './../../../../slider/demo/src/app/slider/SliderScalesComponent'; + +import { SliderTemplateComponent } from './../../../../slider/demo/src/app/slider/SliderTemplateComponent'; + +import { SliderTipComponent } from './../../../../slider/demo/src/app/slider/SliderTipComponent'; + +import { SpinnerBasicTestComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerBasicTestComponent'; + +import { SpinnerBasicComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerBasicComponent'; + +import { SpinnerCorrectableComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerCorrectableComponent'; + +import { SpinnerDisabledComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerDisabledComponent'; + +import { SpinnerEventComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerEventComponent'; + +import { SpinnerFormatComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerFormatComponent'; + +import { SpinnerLoadComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerLoadComponent'; + +import { SpinnerLocaleableComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerLocaleableComponent'; + +import { SpinnerMaxMinComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerMaxMinComponent'; + +import { SpinnerMaxlengthComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerMaxlengthComponent'; + +import { SpinnerStepComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerStepComponent'; + +import { SpinnerStepfnComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerStepfnComponent'; + +import { SpinnerTipTestComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerTipTestComponent'; + +import { SpinnerTipComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerTipComponent'; + +import { SpinnerValidationTestComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerValidationTestComponent'; + +import { SpinnerValidationComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerValidationComponent'; + +import { StepsActiveComponent } from './../../../../steps/demo/src/app/steps/StepsActiveComponent'; + +import { StepsAdaptiveTestComponent } from './../../../../steps/demo/src/app/steps/StepsAdaptiveTestComponent'; + +import { StepsAdaptiveComponent } from './../../../../steps/demo/src/app/steps/StepsAdaptiveComponent'; + +import { StepsBaseComponent } from './../../../../steps/demo/src/app/steps/StepsBaseComponent'; + +import { StepsBeforeComponent } from './../../../../steps/demo/src/app/steps/StepsBeforeComponent'; + +import { StepsClickableComponent } from './../../../../steps/demo/src/app/steps/StepsClickableComponent'; + +import { StepsEventsComponent } from './../../../../steps/demo/src/app/steps/StepsEventsComponent'; + +import { StepsLabelComponent } from './../../../../steps/demo/src/app/steps/StepsLabelComponent'; + +import { StepsMaxwidthComponent } from './../../../../steps/demo/src/app/steps/StepsMaxwidthComponent'; + +import { StepsTemplateComponent } from './../../../../steps/demo/src/app/steps/StepsTemplateComponent'; + +import { SubtitleBasicComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleBasicComponent'; + +import { SubtitleBeforeSearchComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleBeforeSearchComponent'; + +import { SubtitleDarkComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleDarkComponent'; + +import { SubtitleEventComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleEventComponent'; + +import { SubtitleIdkeyComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleIdkeyComponent'; + +import { SubtitleItemsComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleItemsComponent'; + +import { SubtitleMaxwidthComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleMaxwidthComponent'; + +import { SubtitlePanelwidthComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitlePanelwidthComponent'; + +import { SubtitleRouteComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleRouteComponent'; + +import { SubtitleScrollLoadComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleScrollLoadComponent'; + +import { SubtitleSearchableComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleSearchableComponent'; + +import { SubtitleTargetComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleTargetComponent'; + +import { SubtitleTipPositionComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleTipPositionComponent'; + +import { SwiperActiveindexComponent } from './../../../../swiper/demo/src/app/swiper/SwiperActiveindexComponent'; + +import { SwiperAutoplayComponent } from './../../../../swiper/demo/src/app/swiper/SwiperAutoplayComponent'; + +import { SwiperBasicComponent } from './../../../../swiper/demo/src/app/swiper/SwiperBasicComponent'; + +import { SwiperEventsComponent } from './../../../../swiper/demo/src/app/swiper/SwiperEventsComponent'; + +import { SwiperIndicatorpositionComponent } from './../../../../swiper/demo/src/app/swiper/SwiperIndicatorpositionComponent'; + +import { SwiperLoopComponent } from './../../../../swiper/demo/src/app/swiper/SwiperLoopComponent'; + +import { SwiperShowcardnumTestComponent } from './../../../../swiper/demo/src/app/swiper/SwiperShowcardnumTestComponent'; + +import { SwiperShowcardnumComponent } from './../../../../swiper/demo/src/app/swiper/SwiperShowcardnumComponent'; + +import { SwitchBasicComponent } from './../../../../switch/demo/src/app/switch/SwitchBasicComponent'; + +import { SwitchBeforeComponent } from './../../../../switch/demo/src/app/switch/SwitchBeforeComponent'; + +import { SwitchDisabledComponent } from './../../../../switch/demo/src/app/switch/SwitchDisabledComponent'; + +import { SwitchEventComponent } from './../../../../switch/demo/src/app/switch/SwitchEventComponent'; + +import { SwitchExplanationComponent } from './../../../../switch/demo/src/app/switch/SwitchExplanationComponent'; + +import { SwitchFocusComponent } from './../../../../switch/demo/src/app/switch/SwitchFocusComponent'; + +import { SwitchIdComponent } from './../../../../switch/demo/src/app/switch/SwitchIdComponent'; + +import { SwitchLoadComponent } from './../../../../switch/demo/src/app/switch/SwitchLoadComponent'; + +import { SwitchTemplateComponent } from './../../../../switch/demo/src/app/switch/SwitchTemplateComponent'; + +import { TabBasicComponent } from './../../../../tab/demo/src/app/tab/TabBasicComponent'; + +import { TabBeforeactivechangeComponent } from './../../../../tab/demo/src/app/tab/TabBeforeactivechangeComponent'; + +import { TabContentCompComponent } from './../../../../tab/demo/src/app/tab/TabContentCompComponent'; + +import { TabCustomHeadComponent } from './../../../../tab/demo/src/app/tab/TabCustomHeadComponent'; + +import { TabDarkComponent } from './../../../../tab/demo/src/app/tab/TabDarkComponent'; + +import { TabDefaultTestComponent } from './../../../../tab/demo/src/app/tab/TabDefaultTestComponent'; + +import { TabLazyLoadComponent } from './../../../../tab/demo/src/app/tab/TabLazyLoadComponent'; + +import { TabLevel2TestComponent } from './../../../../tab/demo/src/app/tab/TabLevel2TestComponent'; + +import { TabLevel2Component } from './../../../../tab/demo/src/app/tab/TabLevel2Component'; + +import { TabOverflowComponent } from './../../../../tab/demo/src/app/tab/TabOverflowComponent'; + +import { TabRouteComponent } from './../../../../tab/demo/src/app/tab/TabRouteComponent'; + +import { TabScrollComponent } from './../../../../tab/demo/src/app/tab/TabScrollComponent'; + +import { TabSmallComponent } from './../../../../tab/demo/src/app/tab/TabSmallComponent'; + +import { TableActionmenuComponent } from './../../../../table/demo/src/app/table/TableActionmenuComponent'; + +import { TableBasicTestComponent } from './../../../../table/demo/src/app/table/TableBasicTestComponent'; + +import { TableBasicComponent } from './../../../../table/demo/src/app/table/TableBasicComponent'; + +import { TableCellTipComponent } from './../../../../table/demo/src/app/table/TableCellTipComponent'; + +import { TableCelliconsColsresizableComponent } from './../../../../table/demo/src/app/table/TableCelliconsColsresizableComponent'; + +import { TableCheckboxPaginationHeadmenuComponent } from './../../../../table/demo/src/app/table/TableCheckboxPaginationHeadmenuComponent'; + +import { TableCheckboxPaginationComponent } from './../../../../table/demo/src/app/table/TableCheckboxPaginationComponent'; + +import { TableCheckboxComponent } from './../../../../table/demo/src/app/table/TableCheckboxComponent'; + +import { TableColAlignComponent } from './../../../../table/demo/src/app/table/TableColAlignComponent'; + +import { TableColalignSortResizableTestComponent } from './../../../../table/demo/src/app/table/TableColalignSortResizableTestComponent'; + +import { TableColsResizableComponent } from './../../../../table/demo/src/app/table/TableColsResizableComponent'; + +import { TableColsToggleDetailsComponent } from './../../../../table/demo/src/app/table/TableColsToggleDetailsComponent'; + +import { TableColsToggleTestComponent } from './../../../../table/demo/src/app/table/TableColsToggleTestComponent'; + +import { TableColsToggleComponent } from './../../../../table/demo/src/app/table/TableColsToggleComponent'; + +import { TableColsresizableBasicComponent } from './../../../../table/demo/src/app/table/TableColsresizableBasicComponent'; + +import { TableColsresizableColstoggleFixedheadComponent } from './../../../../table/demo/src/app/table/TableColsresizableColstoggleFixedheadComponent'; + +import { TableColsresizableColstoggleComponent } from './../../../../table/demo/src/app/table/TableColsresizableColstoggleComponent'; + +import { TableColsresizableLoadfailComponent } from './../../../../table/demo/src/app/table/TableColsresizableLoadfailComponent'; + +import { TableColsresizableSortHeadfilterComponent } from './../../../../table/demo/src/app/table/TableColsresizableSortHeadfilterComponent'; + +import { TableColsresizableSortComponent } from './../../../../table/demo/src/app/table/TableColsresizableSortComponent'; + +import { TableColumnFixedComponent } from './../../../../table/demo/src/app/table/TableColumnFixedComponent'; + +import { TableColumnfixedCheckboxComponent } from './../../../../table/demo/src/app/table/TableColumnfixedCheckboxComponent'; + +import { TableColumnfixedColstoggleComponent } from './../../../../table/demo/src/app/table/TableColumnfixedColstoggleComponent'; + +import { TableColumnfixedEditrowComponent } from './../../../../table/demo/src/app/table/TableColumnfixedEditrowComponent'; + +import { TableColumnfixedFixedheadColsresizablePaginationComponent } from './../../../../table/demo/src/app/table/TableColumnfixedFixedheadColsresizablePaginationComponent'; + +import { TableColumnfixedHeadfixedComponent } from './../../../../table/demo/src/app/table/TableColumnfixedHeadfixedComponent'; + +import { TableColumnfixedLeftmenuComponent } from './../../../../table/demo/src/app/table/TableColumnfixedLeftmenuComponent'; + +import { TableColumnfixedNodataComponent } from './../../../../table/demo/src/app/table/TableColumnfixedNodataComponent'; + +import { TableColumnfixedPaginationComponent } from './../../../../table/demo/src/app/table/TableColumnfixedPaginationComponent'; + +import { TableColumnfixedResizableComponent } from './../../../../table/demo/src/app/table/TableColumnfixedResizableComponent'; + +import { TableComprehensiveComponent } from './../../../../table/demo/src/app/table/TableComprehensiveComponent'; + +import { TableDetailsCloseotherdetailsComponent } from './../../../../table/demo/src/app/table/TableDetailsCloseotherdetailsComponent'; + +import { TableDetailsNesttableComponent } from './../../../../table/demo/src/app/table/TableDetailsNesttableComponent'; + +import { TableDetailsPaginationComponent } from './../../../../table/demo/src/app/table/TableDetailsPaginationComponent'; + +import { TableDetailsComponent } from './../../../../table/demo/src/app/table/TableDetailsComponent'; + +import { TableDynamicDetailsComponent } from './../../../../table/demo/src/app/table/TableDynamicDetailsComponent'; + +import { TableEditallTestComponent } from './../../../../table/demo/src/app/table/TableEditallTestComponent'; + +import { TableEditallComponent } from './../../../../table/demo/src/app/table/TableEditallComponent'; + +import { TableEditrowTestComponent } from './../../../../table/demo/src/app/table/TableEditrowTestComponent'; + +import { TableEditrowComponent } from './../../../../table/demo/src/app/table/TableEditrowComponent'; + +import { TableFilterStrictComponent } from './../../../../table/demo/src/app/table/TableFilterStrictComponent'; + +import { TableFilterComponent } from './../../../../table/demo/src/app/table/TableFilterComponent'; + +import { TableFixedHeadColsResizableComponent } from './../../../../table/demo/src/app/table/TableFixedHeadColsResizableComponent'; + +import { TableFixedHeadInAccordionComponent } from './../../../../table/demo/src/app/table/TableFixedHeadInAccordionComponent'; + +import { TableFixedHeadNodataComponent } from './../../../../table/demo/src/app/table/TableFixedHeadNodataComponent'; + +import { TableFixedHeadPaginationDetailsComponent } from './../../../../table/demo/src/app/table/TableFixedHeadPaginationDetailsComponent'; + +import { TableFixedHeadComponent } from './../../../../table/demo/src/app/table/TableFixedHeadComponent'; + +import { TableFixedheadColsresizablePaginationDetailsComponent } from './../../../../table/demo/src/app/table/TableFixedheadColsresizablePaginationDetailsComponent'; + +import { TableFixheadScrollComponent } from './../../../../table/demo/src/app/table/TableFixheadScrollComponent'; + +import { TableGroupComponent } from './../../../../table/demo/src/app/table/TableGroupComponent'; + +import { TableGuideComponent } from './../../../../table/demo/src/app/table/TableGuideComponent'; + +import { TableHeadFilterDatetimeTestComponent } from './../../../../table/demo/src/app/table/TableHeadFilterDatetimeTestComponent'; + +import { TableHeadFilterDatetimeComponent } from './../../../../table/demo/src/app/table/TableHeadFilterDatetimeComponent'; + +import { TableHeadFilterMultiValuekeyComponent } from './../../../../table/demo/src/app/table/TableHeadFilterMultiValuekeyComponent'; + +import { TableHeadFilterMultiComponent } from './../../../../table/demo/src/app/table/TableHeadFilterMultiComponent'; + +import { TableHeadFilterTestComponent } from './../../../../table/demo/src/app/table/TableHeadFilterTestComponent'; + +import { TableHeadFilterValuekeyComponent } from './../../../../table/demo/src/app/table/TableHeadFilterValuekeyComponent'; + +import { TableHeadFilterVirtualscrollComponent } from './../../../../table/demo/src/app/table/TableHeadFilterVirtualscrollComponent'; + +import { TableHeadFilterComponent } from './../../../../table/demo/src/app/table/TableHeadFilterComponent'; + +import { TableLoadFailComponent } from './../../../../table/demo/src/app/table/TableLoadFailComponent'; + +import { TableNodataSimpleComponent } from './../../../../table/demo/src/app/table/TableNodataSimpleComponent'; + +import { TableNodataTestComponent } from './../../../../table/demo/src/app/table/TableNodataTestComponent'; + +import { TableNodataComponent } from './../../../../table/demo/src/app/table/TableNodataComponent'; + +import { TableOverflowLinkComponent } from './../../../../table/demo/src/app/table/TableOverflowLinkComponent'; + +import { TablePagiWithFilterComponent } from './../../../../table/demo/src/app/table/TablePagiWithFilterComponent'; + +import { TablePaginationComponent } from './../../../../table/demo/src/app/table/TablePaginationComponent'; + +import { TableRadioTestComponent } from './../../../../table/demo/src/app/table/TableRadioTestComponent'; + +import { TableRadioComponent } from './../../../../table/demo/src/app/table/TableRadioComponent'; + +import { TableRowDrag2Component } from './../../../../table/demo/src/app/table/TableRowDrag2Component'; + +import { TableRowspanComponent } from './../../../../table/demo/src/app/table/TableRowspanComponent'; + +import { TableSearchComponent } from './../../../../table/demo/src/app/table/TableSearchComponent'; + +import { TableServerPagiSearchSortComponent } from './../../../../table/demo/src/app/table/TableServerPagiSearchSortComponent'; + +import { TableServerPagiComponent } from './../../../../table/demo/src/app/table/TableServerPagiComponent'; + +import { TableSmallComponent } from './../../../../table/demo/src/app/table/TableSmallComponent'; + +import { TableSoldoutComponent } from './../../../../table/demo/src/app/table/TableSoldoutComponent'; + +import { TableSortBasicComponent } from './../../../../table/demo/src/app/table/TableSortBasicComponent'; + +import { TableSortComparefnLocaleComponent } from './../../../../table/demo/src/app/table/TableSortComparefnLocaleComponent'; + +import { TableSortComparefnComponent } from './../../../../table/demo/src/app/table/TableSortComparefnComponent'; + +import { TableSortDetailsComponent } from './../../../../table/demo/src/app/table/TableSortDetailsComponent'; + +import { TableSortResetComponent } from './../../../../table/demo/src/app/table/TableSortResetComponent'; + +import { TableSortTestComponent } from './../../../../table/demo/src/app/table/TableSortTestComponent'; + +import { TableSortComponent } from './../../../../table/demo/src/app/table/TableSortComponent'; + +import { TableStorageConfigComponent } from './../../../../table/demo/src/app/table/TableStorageConfigComponent'; + +import { TableStorageFilterComponent } from './../../../../table/demo/src/app/table/TableStorageFilterComponent'; + +import { TableStorageServeComponent } from './../../../../table/demo/src/app/table/TableStorageServeComponent'; + +import { TableStorageComponent } from './../../../../table/demo/src/app/table/TableStorageComponent'; + +import { TableTreeMulitiselectComponent } from './../../../../table/demo/src/app/table/TableTreeMulitiselectComponent'; + +import { TableTreeUnknowdeepthComponent } from './../../../../table/demo/src/app/table/TableTreeUnknowdeepthComponent'; + +import { TableTreeComponent } from './../../../../table/demo/src/app/table/TableTreeComponent'; + +import { TableVirtualscrollBasicComponent } from './../../../../table/demo/src/app/table/TableVirtualscrollBasicComponent'; + +import { TableVirtualscrollComprehensiveComponent } from './../../../../table/demo/src/app/table/TableVirtualscrollComprehensiveComponent'; + +import { TableVirtualscrollSizesComponent } from './../../../../table/demo/src/app/table/TableVirtualscrollSizesComponent'; + +import { TableVirtualscrollTreeComponent } from './../../../../table/demo/src/app/table/TableVirtualscrollTreeComponent'; + +import { TableVirtualscrollComponent } from './../../../../table/demo/src/app/table/TableVirtualscrollComponent'; + +import { TagBasicComponent } from './../../../../tag/demo/src/app/tag/TagBasicComponent'; + +import { TagDefaultComponent } from './../../../../tag/demo/src/app/tag/TagDefaultComponent'; + +import { TagDisabledComponent } from './../../../../tag/demo/src/app/tag/TagDisabledComponent'; + +import { TagEditComponent } from './../../../../tag/demo/src/app/tag/TagEditComponent'; + +import { TagsinputBasicComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputBasicComponent'; + +import { TagsinputDisabledComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputDisabledComponent'; + +import { TagsinputEventsComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputEventsComponent'; + +import { TagsinputLabelkeyComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputLabelkeyComponent'; + +import { TagsinputNullComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputNullComponent'; + +import { TagsinputPanelwidthComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputPanelwidthComponent'; + +import { TagsinputSeparatorsComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputSeparatorsComponent'; + +import { TagsinputReactiveComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputReactiveComponent'; + +import { TagsinputSuggestionComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputSuggestionComponent'; + +import { TagsinputTemplateComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputTemplateComponent'; + +import { TagsinputValidComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputValidComponent'; + +import { TagsinputValuekeyComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputValuekeyComponent'; + +import { TagsinputMaxlengthComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputMaxlengthComponent'; + +import { TextBasicComponent } from './../../../../text/demo/src/app/text/TextBasicComponent'; + +import { TextClearComponent } from './../../../../text/demo/src/app/text/TextClearComponent'; + +import { TextDisabledComponent } from './../../../../text/demo/src/app/text/TextDisabledComponent'; + +import { TextEventsComponent } from './../../../../text/demo/src/app/text/TextEventsComponent'; + +import { TextFocusComponent } from './../../../../text/demo/src/app/text/TextFocusComponent'; + +import { TextMaskinputComponent } from './../../../../text/demo/src/app/text/TextMaskinputComponent'; + +import { TextNoborderTestComponent } from './../../../../text/demo/src/app/text/TextNoborderTestComponent'; + +import { TextPasswordVisibleComponent } from './../../../../text/demo/src/app/text/TextPasswordVisibleComponent'; + +import { TextPasswordComponent } from './../../../../text/demo/src/app/text/TextPasswordComponent'; + +import { TextReactiveComponent } from './../../../../text/demo/src/app/text/TextReactiveComponent'; + +import { TextReadonlyComponent } from './../../../../text/demo/src/app/text/TextReadonlyComponent'; + +import { TextareaAutofocusComponent } from './../../../../textarea/demo/src/app/textarea/TextareaAutofocusComponent'; + +import { TextareaDisabledComponent } from './../../../../textarea/demo/src/app/textarea/TextareaDisabledComponent'; + +import { TextareaMaxlengthComponent } from './../../../../textarea/demo/src/app/textarea/TextareaMaxlengthComponent'; + +import { TextareaNoneComponent } from './../../../../textarea/demo/src/app/textarea/TextareaNoneComponent'; + +import { TextareaResizeComponent } from './../../../../textarea/demo/src/app/textarea/TextareaResizeComponent'; + +import { TextareaScrollComponent } from './../../../../textarea/demo/src/app/textarea/TextareaScrollComponent'; + +import { TextareaValidComponent } from './../../../../textarea/demo/src/app/textarea/TextareaValidComponent'; + +import { TextareaWidthComponent } from './../../../../textarea/demo/src/app/textarea/TextareaWidthComponent'; + +import { TimeCleariconComponent } from './../../../../time/demo/src/app/time/TimeCleariconComponent'; + +import { TimeDisabledComponent } from './../../../../time/demo/src/app/time/TimeDisabledComponent'; + +import { TimeEventComponent } from './../../../../time/demo/src/app/time/TimeEventComponent'; + +import { TimeFormatComponent } from './../../../../time/demo/src/app/time/TimeFormatComponent'; + +import { TimeMaxComponent } from './../../../../time/demo/src/app/time/TimeMaxComponent'; + +import { TimeMaxminComponent } from './../../../../time/demo/src/app/time/TimeMaxminComponent'; + +import { TimeMinComponent } from './../../../../time/demo/src/app/time/TimeMinComponent'; + +import { TimeOptionDisabledComponent } from './../../../../time/demo/src/app/time/TimeOptionDisabledComponent'; + +import { TimePanelalignComponent } from './../../../../time/demo/src/app/time/TimePanelalignComponent'; + +import { TimeReactiveComponent } from './../../../../time/demo/src/app/time/TimeReactiveComponent'; + +import { TimeValidationComponent } from './../../../../time/demo/src/app/time/TimeValidationComponent'; + +import { TimelineBasicComponent } from './../../../../timeline/demo/src/app/timeline/TimelineBasicComponent'; + +import { TimelineDarkComponent } from './../../../../timeline/demo/src/app/timeline/TimelineDarkComponent'; + +import { TimelineHelptipComponent } from './../../../../timeline/demo/src/app/timeline/TimelineHelptipComponent'; + +import { TimelineMultiComponent } from './../../../../timeline/demo/src/app/timeline/TimelineMultiComponent'; + +import { TimelineTempleteComponent } from './../../../../timeline/demo/src/app/timeline/TimelineTempleteComponent'; + +import { TimelineTestComponent } from './../../../../timeline/demo/src/app/timeline/TimelineTestComponent'; + +import { TimelineTypeComponent } from './../../../../timeline/demo/src/app/timeline/TimelineTypeComponent'; + +import { TipBasicComponent } from './../../../../tip/demo/src/app/tip/TipBasicComponent'; + +import { TipContentCompComponent } from './../../../../tip/demo/src/app/tip/TipContentCompComponent'; + +import { TipContentTemplateComponent } from './../../../../tip/demo/src/app/tip/TipContentTemplateComponent'; + +import { TipEmptyComponent } from './../../../../tip/demo/src/app/tip/TipEmptyComponent'; + +import { TipHasArrowComponent } from './../../../../tip/demo/src/app/tip/TipHasArrowComponent'; + +import { TipLongTextPositionComponent } from './../../../../tip/demo/src/app/tip/TipLongTextPositionComponent'; + +import { TipMaxWidthComponent } from './../../../../tip/demo/src/app/tip/TipMaxWidthComponent'; + +import { TipPositionTestComponent } from './../../../../tip/demo/src/app/tip/TipPositionTestComponent'; + +import { TipPositionComponent } from './../../../../tip/demo/src/app/tip/TipPositionComponent'; + +import { TipServiceDestroyComponent } from './../../../../tip/demo/src/app/tip/TipServiceDestroyComponent'; + +import { TipServiceComponent } from './../../../../tip/demo/src/app/tip/TipServiceComponent'; + +import { TipTriggerComponent } from './../../../../tip/demo/src/app/tip/TipTriggerComponent'; + +import { TipValidPositionTestComponent } from './../../../../tip/demo/src/app/tip/TipValidPositionTestComponent'; + +import { TipZindexComponent } from './../../../../tip/demo/src/app/tip/TipZindexComponent'; + +import { TransferBasicComponent } from './../../../../transfer/demo/src/app/transfer/TransferBasicComponent'; + +import { TransferDisabledComponent } from './../../../../transfer/demo/src/app/transfer/TransferDisabledComponent'; + +import { TransferEventComponent } from './../../../../transfer/demo/src/app/transfer/TransferEventComponent'; + +import { TransferIdComponent } from './../../../../transfer/demo/src/app/transfer/TransferIdComponent'; + +import { TransferIdkeyComponent } from './../../../../transfer/demo/src/app/transfer/TransferIdkeyComponent'; + +import { TransferLabelkeyComponent } from './../../../../transfer/demo/src/app/transfer/TransferLabelkeyComponent'; + +import { TransferLazyComponent } from './../../../../transfer/demo/src/app/transfer/TransferLazyComponent'; + +import { TransferLoadComponent } from './../../../../transfer/demo/src/app/transfer/TransferLoadComponent'; + +import { TransferNodatatextComponent } from './../../../../transfer/demo/src/app/transfer/TransferNodatatextComponent'; + +import { TransferPaginationComponent } from './../../../../transfer/demo/src/app/transfer/TransferPaginationComponent'; + +import { TransferPlaceholderComponent } from './../../../../transfer/demo/src/app/transfer/TransferPlaceholderComponent'; + +import { TransferSearchableComponent } from './../../../../transfer/demo/src/app/transfer/TransferSearchableComponent'; + +import { TransferSearchkeysComponent } from './../../../../transfer/demo/src/app/transfer/TransferSearchkeysComponent'; + +import { TransferSizeComponent } from './../../../../transfer/demo/src/app/transfer/TransferSizeComponent'; + +import { TransferTableComponent } from './../../../../transfer/demo/src/app/transfer/TransferTableComponent'; + +import { TransferTitlesComponent } from './../../../../transfer/demo/src/app/transfer/TransferTitlesComponent'; + +import { TreeBeforeExpandComponent } from './../../../../tree/demo/src/app/tree/TreeBeforeExpandComponent'; + +import { TreeBeforeMoreComponent } from './../../../../tree/demo/src/app/tree/TreeBeforeMoreComponent'; + +import { TreeChangedbycheckboxComponent } from './../../../../tree/demo/src/app/tree/TreeChangedbycheckboxComponent'; + +import { TreeCheckRelationComponent } from './../../../../tree/demo/src/app/tree/TreeCheckRelationComponent'; + +import { TreeDisabledComponent } from './../../../../tree/demo/src/app/tree/TreeDisabledComponent'; + +import { TreeDragBeforedropComponent } from './../../../../tree/demo/src/app/tree/TreeDragBeforedropComponent'; + +import { TreeDragComponent } from './../../../../tree/demo/src/app/tree/TreeDragComponent'; + +import { TreeEventComponent } from './../../../../tree/demo/src/app/tree/TreeEventComponent'; + +import { TreeIconComponent } from './../../../../tree/demo/src/app/tree/TreeIconComponent'; + +import { TreeLoadComponent } from './../../../../tree/demo/src/app/tree/TreeLoadComponent'; + +import { TreeManyComponent } from './../../../../tree/demo/src/app/tree/TreeManyComponent'; + +import { TreeMultiselectComponent } from './../../../../tree/demo/src/app/tree/TreeMultiselectComponent'; + +import { TreeOperateComponent } from './../../../../tree/demo/src/app/tree/TreeOperateComponent'; + +import { TreeParentcheckableComponent } from './../../../../tree/demo/src/app/tree/TreeParentcheckableComponent'; + +import { TreeRadioselectComponent } from './../../../../tree/demo/src/app/tree/TreeRadioselectComponent'; + +import { TreeSearchComponent } from './../../../../tree/demo/src/app/tree/TreeSearchComponent'; + +import { TreeShortcutkeyComponent } from './../../../../tree/demo/src/app/tree/TreeShortcutkeyComponent'; + +import { TreeSmallComponent } from './../../../../tree/demo/src/app/tree/TreeSmallComponent'; + +import { TreeTemplateComponent } from './../../../../tree/demo/src/app/tree/TreeTemplateComponent'; + +import { TreeUtilComponent } from './../../../../tree/demo/src/app/tree/TreeUtilComponent'; + +import { TreeVirtualscrollDragComponent } from './../../../../tree/demo/src/app/tree/TreeVirtualscrollDragComponent'; + +import { TreeVirtualscrollSmallComponent } from './../../../../tree/demo/src/app/tree/TreeVirtualscrollSmallComponent'; + +import { TreeVirtualscrollComponent } from './../../../../tree/demo/src/app/tree/TreeVirtualscrollComponent'; + +import { TreeselectBasicComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectBasicComponent'; + +import { TreeselectBeforeExpandComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectBeforeExpandComponent'; + +import { TreeselectBeforeMoreComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectBeforeMoreComponent'; + +import { TreeselectClearableComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectClearableComponent'; + +import { TreeselectDisabledComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectDisabledComponent'; + +import { TreeselectDropmaxheightComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectDropmaxheightComponent'; + +import { TreeselectEventComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectEventComponent'; + +import { TreeselectFocusComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectFocusComponent'; + +import { TreeselectLabelkeyComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectLabelkeyComponent'; + +import { TreeselectLazyloadComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectLazyloadComponent'; + +import { TreeselectLoadComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectLoadComponent'; + +import { TreeselectMaxlineComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectMaxlineComponent'; + +import { TreeselectModalComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectModalComponent'; + +import { TreeselectMultiComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectMultiComponent'; + +import { TreeselectNodataComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectNodataComponent'; + +import { TreeselectOptionsChangeComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectOptionsChangeComponent'; + +import { TreeselectPanelwidthComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectPanelwidthComponent'; + +import { TreeselectSearchComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectSearchComponent'; + +import { TreeselectSelectallComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectSelectallComponent'; + +import { TreeselectValidationComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectValidationComponent'; + +import { UploadAutoUploadComponent } from './../../../../upload/demo/src/app/upload/UploadAutoUploadComponent'; + +import { UploadBasicComponent } from './../../../../upload/demo/src/app/upload/UploadBasicComponent'; + +import { UploadBatchSendComponent } from './../../../../upload/demo/src/app/upload/UploadBatchSendComponent'; + +import { UploadBeforeremoveComponent } from './../../../../upload/demo/src/app/upload/UploadBeforeremoveComponent'; + +import { UploadButtonTestComponent } from './../../../../upload/demo/src/app/upload/UploadButtonTestComponent'; + +import { UploadButtonComponent } from './../../../../upload/demo/src/app/upload/UploadButtonComponent'; + +import { UploadCaseTestComponent } from './../../../../upload/demo/src/app/upload/UploadCaseTestComponent'; + +import { UploadChangesComponent } from './../../../../upload/demo/src/app/upload/UploadChangesComponent'; + +import { UploadChunksizeComponent } from './../../../../upload/demo/src/app/upload/UploadChunksizeComponent'; + +import { UploadCustomComponent } from './../../../../upload/demo/src/app/upload/UploadCustomComponent'; + +import { UploadEventComponent } from './../../../../upload/demo/src/app/upload/UploadEventComponent'; + +import { UploadFilterComponent } from './../../../../upload/demo/src/app/upload/UploadFilterComponent'; + +import { UploadFormDataComponent } from './../../../../upload/demo/src/app/upload/UploadFormDataComponent'; + +import { UploadInitfilesTestComponent } from './../../../../upload/demo/src/app/upload/UploadInitfilesTestComponent'; + +import { UploadInputFieldTestComponent } from './../../../../upload/demo/src/app/upload/UploadInputFieldTestComponent'; + +import { UploadPropsComponent } from './../../../../upload/demo/src/app/upload/UploadPropsComponent'; + +import { UploadServiceTestComponent } from './../../../../upload/demo/src/app/upload/UploadServiceTestComponent'; + +import { UploadServiceComponent } from './../../../../upload/demo/src/app/upload/UploadServiceComponent'; + +import { UploadSingleComponent } from './../../../../upload/demo/src/app/upload/UploadSingleComponent'; + +import { UploadimageAutoUploadComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageAutoUploadComponent'; + +import { UploadimageBasicComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageBasicComponent'; + +import { UploadimageChangesComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageChangesComponent'; + +import { UploadimageDeletableComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageDeletableComponent'; + +import { UploadimageDisabledComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageDisabledComponent'; + +import { UploadimageDragComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageDragComponent'; + +import { UploadimageEventComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageEventComponent'; + +import { UploadimageFilterComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageFilterComponent'; + +import { UploadimageInitfilesComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageInitfilesComponent'; + +import { UploadimageMaxcountComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageMaxcountComponent'; + +import { UploadimageTemplateComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageTemplateComponent'; + +import { BrowserUsageComponent } from './../../../../utils/demo/src/app/browser/BrowserUsageComponent'; + +import { KeymapUsageComponent } from './../../../../utils/demo/src/app/keymap/KeymapUsageComponent'; + +import { LogUsageComponent } from './../../../../utils/demo/src/app/log/LogUsageComponent'; + +import { ThemeBasicComponent } from './../../../../utils/demo/src/app/theme/ThemeBasicComponent'; + +import { ValidationAsyncCheckTestComponent } from './../../../../validation/demo/src/app/validation/ValidationAsyncCheckTestComponent'; + +import { ValidationAsyncCheckComponent } from './../../../../validation/demo/src/app/validation/ValidationAsyncCheckComponent'; + +import { ValidationBasicControlComponent } from './../../../../validation/demo/src/app/validation/ValidationBasicControlComponent'; + +import { ValidationBasicDirectiveComponent } from './../../../../validation/demo/src/app/validation/ValidationBasicDirectiveComponent'; + +import { ValidationBlurCheckComponent } from './../../../../validation/demo/src/app/validation/ValidationBlurCheckComponent'; + +import { ValidationErrorMsgComponent } from './../../../../validation/demo/src/app/validation/ValidationErrorMsgComponent'; + +import { ValidationFormGroupConfigComponent } from './../../../../validation/demo/src/app/validation/ValidationFormGroupConfigComponent'; + +import { ValidationFormGroupTestComponent } from './../../../../validation/demo/src/app/validation/ValidationFormGroupTestComponent'; + +import { ValidationFormGroupComponent } from './../../../../validation/demo/src/app/validation/ValidationFormGroupComponent'; + +import { ValidationParamChangeComponent } from './../../../../validation/demo/src/app/validation/ValidationParamChangeComponent'; + +import { ValidationPwdCheckComponent } from './../../../../validation/demo/src/app/validation/ValidationPwdCheckComponent'; + +import { ValidationRulesCustomDirectiveComponent } from './../../../../validation/demo/src/app/validation/ValidationRulesCustomDirectiveComponent'; + +import { ValidationRulesCustomComponent } from './../../../../validation/demo/src/app/validation/ValidationRulesCustomComponent'; + +import { ValidationRulesTestComponent } from './../../../../validation/demo/src/app/validation/ValidationRulesTestComponent'; + +import { ValidationTemplateFormNestedComponent } from './../../../../validation/demo/src/app/validation/ValidationTemplateFormNestedComponent'; + +import { ValidationTipComponent } from './../../../../validation/demo/src/app/validation/ValidationTipComponent'; + +import { ValidationTiscrollComponent } from './../../../../validation/demo/src/app/validation/ValidationTiscrollComponent'; + +@Component({ + selector: `app-root`, + template: `` +}) +export class AppComponent {} + +const WCS: any = [ + { + selector: 'website-tiny-accordion-basic', + component: AccordionBasicComponent + }, + + { + selector: 'website-tiny-accordion-class', + component: AccordionClassComponent + }, + + { + selector: 'website-tiny-accordion-click-toggle', + component: AccordionClickToggleComponent + }, + + { + selector: 'website-tiny-accordion-close-others', + component: AccordionCloseOthersComponent + }, + + { + selector: 'website-tiny-accordion-disabled', + component: AccordionDisabledComponent + }, + + { + selector: 'website-tiny-accordion-open', + component: AccordionOpenComponent + }, + + { + selector: 'website-tiny-actionmenu-basic', + component: ActionmenuBasicComponent + }, + + { + selector: 'website-tiny-actionmenu-data', + component: ActionmenuDataComponent + }, + + { + selector: 'website-tiny-actionmenu-data2', + component: ActionmenuData2Component + }, + + { + selector: 'website-tiny-actionmenu-disabled', + component: ActionmenuDisabledComponent + }, + + { + selector: 'website-tiny-actionmenu-divider', + component: ActionmenuDividerComponent + }, + + { + selector: 'website-tiny-actionmenu-event', + component: ActionmenuEventComponent + }, + + { + selector: 'website-tiny-actionmenu-focus', + component: ActionmenuFocusComponent + }, + + { selector: 'website-tiny-actionmenu-id', component: ActionmenuIdComponent }, + + { + selector: 'website-tiny-actionmenu-items-change', + component: ActionmenuItemsChangeComponent + }, + + { + selector: 'website-tiny-actionmenu-items', + component: ActionmenuItemsComponent + }, + + { + selector: 'website-tiny-actionmenu-labelkey', + component: ActionmenuLabelkeyComponent + }, + + { + selector: 'website-tiny-actionmenu-many', + component: ActionmenuManyComponent + }, + + { + selector: 'website-tiny-actionmenu-menutext', + component: ActionmenuMenutextComponent + }, + + { + selector: 'website-tiny-actionmenu-panelstyle', + component: ActionmenuPanelstyleComponent + }, + + { + selector: 'website-tiny-actionmenu-shownum', + component: ActionmenuShownumComponent + }, + + { + selector: 'website-tiny-actionmenu-space', + component: ActionmenuSpaceComponent + }, + + { + selector: 'website-tiny-actionmenu-table', + component: ActionmenuTableComponent + }, + + { + selector: 'website-tiny-actionmenu-templete-test', + component: ActionmenuTempleteTestComponent + }, + + { + selector: 'website-tiny-actionmenu-templete', + component: ActionmenuTempleteComponent + }, + + { + selector: 'website-tiny-actionmenu-tips-test', + component: ActionmenuTipsTestComponent + }, + + { + selector: 'website-tiny-actionmenu-tips', + component: ActionmenuTipsComponent + }, + + { + selector: 'website-tiny-alert-boxshadow', + component: AlertBoxshadowComponent + }, + + { + selector: 'website-tiny-alert-darktheme', + component: AlertDarkthemeComponent + }, + + { selector: 'website-tiny-alert-dismiss', component: AlertDismissComponent }, + + { selector: 'website-tiny-alert-event', component: AlertEventComponent }, + + { selector: 'website-tiny-alert-icon', component: AlertIconComponent }, + + { + selector: 'website-tiny-alert-messages', + component: AlertMessagesComponent + }, + + { + selector: 'website-tiny-alert-open-test', + component: AlertOpenTestComponent + }, + + { selector: 'website-tiny-alert-open', component: AlertOpenComponent }, + + { selector: 'website-tiny-alert-size', component: AlertSizeComponent }, + + { + selector: 'website-tiny-alert-trigger-scroll', + component: AlertTriggerScrollComponent + }, + + { selector: 'website-tiny-alert-type', component: AlertTypeComponent }, + + { selector: 'website-tiny-anchor-basic', component: AnchorBasicComponent }, + + { selector: 'website-tiny-anchor-events', component: AnchorEventsComponent }, + + { selector: 'website-tiny-anchor-id', component: AnchorIdComponent }, + + { selector: 'website-tiny-anchor-items', component: AnchorItemsComponent }, + + { + selector: 'website-tiny-anchor-offsettop', + component: AnchorOffsettopComponent + }, + + { + selector: 'website-tiny-anchor-scrolltarget', + component: AnchorScrolltargetComponent + }, + + { selector: 'website-tiny-anchor-speed', component: AnchorSpeedComponent }, + + { + selector: 'website-tiny-anchor-template', + component: AnchorTemplateComponent + }, + + { selector: 'website-tiny-anchor-test', component: AnchorTestComponent }, + + { + selector: 'website-tiny-autocomplete-appendtobody', + component: AutocompleteAppendtobodyComponent + }, + + { + selector: 'website-tiny-autocomplete-basic', + component: AutocompleteBasicComponent + }, + + { + selector: 'website-tiny-autocomplete-clearable', + component: AutocompleteClearableComponent + }, + + { + selector: 'website-tiny-autocomplete-disabled', + component: AutocompleteDisabledComponent + }, + + { + selector: 'website-tiny-autocomplete-events', + component: AutocompleteEventsComponent + }, + + { + selector: 'website-tiny-autocomplete-labelkey', + component: AutocompleteLabelkeyComponent + }, + + { + selector: 'website-tiny-autocomplete-maxlength', + component: AutocompleteMaxlengthComponent + }, + + { + selector: 'website-tiny-autocomplete-panel-size', + component: AutocompletePanelSizeComponent + }, + + { + selector: 'website-tiny-autocomplete-template', + component: AutocompleteTemplateComponent + }, + + { + selector: 'website-tiny-autocomplete-test', + component: AutocompleteTestComponent + }, + + { + selector: 'website-tiny-autocomplete-tip', + component: AutocompleteTipComponent + }, + + { + selector: 'website-tiny-autocomplete-valid', + component: AutocompleteValidComponent + }, + + { selector: 'website-tiny-autocomplete-group', component: AutocompleteGroupComponent }, + + { + selector: 'website-tiny-avatar-image-error-test', + component: AvatarImageErrorTestComponent + }, + + { selector: 'website-tiny-avatar-image', component: AvatarImageComponent }, + + { selector: 'website-tiny-avatar-shape', component: AvatarShapeComponent }, + + { selector: 'website-tiny-avatar-size', component: AvatarSizeComponent }, + + { selector: 'website-tiny-avatar-style', component: AvatarStyleComponent }, + + { selector: 'website-tiny-avatar-text', component: AvatarTextComponent }, + + { selector: 'website-tiny-button-color', component: ButtonColorComponent }, + + { + selector: 'website-tiny-button-disabled', + component: ButtonDisabledComponent + }, + + { selector: 'website-tiny-button-event', component: ButtonEventComponent }, + + { selector: 'website-tiny-button-focus', component: ButtonFocusComponent }, + + { + selector: 'website-tiny-button-hasborder-test', + component: ButtonHasborderTestComponent + }, + + { + selector: 'website-tiny-button-hasborder', + component: ButtonHasborderComponent + }, + + { selector: 'website-tiny-button-icon', component: ButtonIconComponent }, + + { + selector: 'website-tiny-button-loading', + component: ButtonLoadingComponent + }, + + { + selector: 'website-tiny-button-onlyicon', + component: ButtonOnlyiconComponent + }, + + { selector: 'website-tiny-button-size', component: ButtonSizeComponent }, + + { selector: 'website-tiny-button-tip', component: ButtonTipComponent }, + + { + selector: 'website-tiny-buttongroup-activeclass', + component: ButtongroupActiveclassComponent + }, + + { + selector: 'website-tiny-buttongroup-beforeclick', + component: ButtongroupBeforeclickComponent + }, + + { + selector: 'website-tiny-buttongroup-deselectable', + component: ButtongroupDeselectableComponent + }, + + { + selector: 'website-tiny-buttongroup-disabled', + component: ButtongroupDisabledComponent + }, + + { + selector: 'website-tiny-buttongroup-enum', + component: ButtongroupEnumComponent + }, + + { + selector: 'website-tiny-buttongroup-event', + component: ButtongroupEventComponent + }, + + { + selector: 'website-tiny-buttongroup-focus', + component: ButtongroupFocusComponent + }, + + { + selector: 'website-tiny-buttongroup-id-test', + component: ButtongroupIdTestComponent + }, + + { + selector: 'website-tiny-buttongroup-id', + component: ButtongroupIdComponent + }, + + { + selector: 'website-tiny-buttongroup-items-test', + component: ButtongroupItemsTestComponent + }, + + { + selector: 'website-tiny-buttongroup-items', + component: ButtongroupItemsComponent + }, + + { + selector: 'website-tiny-buttongroup-many', + component: ButtongroupManyComponent + }, + + { + selector: 'website-tiny-buttongroup-minwidth', + component: ButtongroupMinwidthComponent + }, + + { + selector: 'website-tiny-buttongroup-multi-type', + component: ButtongroupMultiTypeComponent + }, + + { + selector: 'website-tiny-buttongroup-multiline', + component: ButtongroupMultilineComponent + }, + + { + selector: 'website-tiny-buttongroup-multiple', + component: ButtongroupMultipleComponent + }, + + { + selector: 'website-tiny-buttongroup-radio-type', + component: ButtongroupRadioTypeComponent + }, + + { + selector: 'website-tiny-buttongroup-reactive-forms', + component: ButtongroupReactiveFormsComponent + }, + + { + selector: 'website-tiny-buttongroup-sup-test', + component: ButtongroupSupTestComponent + }, + + { + selector: 'website-tiny-buttongroup-sup', + component: ButtongroupSupComponent + }, + + { + selector: 'website-tiny-buttongroup-template', + component: ButtongroupTemplateComponent + }, + + { + selector: 'website-tiny-buttongroup-tip', + component: ButtongroupTipComponent + }, + + { + selector: 'website-tiny-buttongroup-valuekey-test', + component: ButtongroupValuekeyTestComponent + }, + + { + selector: 'website-tiny-buttongroup-valuekey', + component: ButtongroupValuekeyComponent + }, + + { + selector: 'website-tiny-buttonselect-basic', + component: ButtonselectBasicComponent + }, + + { + selector: 'website-tiny-buttonselect-labelkey', + component: ButtonselectLabelkeyComponent + }, + + { selector: 'website-tiny-card-add', component: CardAddComponent }, + + { selector: 'website-tiny-card-basic', component: CardBasicComponent }, + + { selector: 'website-tiny-card-grid', component: CardGridComponent }, + + { selector: 'website-tiny-card-grid2', component: CardGrid2Component }, + + { selector: 'website-tiny-card-header', component: CardHeaderComponent }, + + { + selector: 'website-tiny-cascader-basic', + component: CascaderBasicComponent + }, + + { + selector: 'website-tiny-cascader-clearable', + component: CascaderClearableComponent + }, + + { + selector: 'website-tiny-cascader-disabled', + component: CascaderDisabledComponent + }, + + { + selector: 'website-tiny-cascader-events', + component: CascaderEventsComponent + }, + + { + selector: 'website-tiny-cascader-idkey', + component: CascaderIdkeyComponent + }, + + { + selector: 'website-tiny-cascader-item-test', + component: CascaderItemTestComponent + }, + + { + selector: 'website-tiny-cascader-labelkey', + component: CascaderLabelkeyComponent + }, + + { + selector: 'website-tiny-cascader-onlyselectleaf', + component: CascaderOnlyselectleafComponent + }, + + { + selector: 'website-tiny-cascader-panel', + component: CascaderPanelComponent + }, + + { + selector: 'website-tiny-cascader-search', + component: CascaderSearchComponent + }, + + { + selector: 'website-tiny-cascader-showalllevel', + component: CascaderShowalllevelComponent + }, + + { + selector: 'website-tiny-cascader-trigger', + component: CascaderTriggerComponent + }, + + { + selector: 'website-tiny-cascader-valid', + component: CascaderValidComponent + }, + + { + selector: 'website-tiny-cascader-valuekey', + component: CascaderValuekeyComponent + }, + + { + selector: 'website-tiny-checkbox-basic', + component: CheckboxBasicComponent + }, + + { + selector: 'website-tiny-checkbox-disabled', + component: CheckboxDisabledComponent + }, + + { + selector: 'website-tiny-checkbox-event', + component: CheckboxEventComponent + }, + + { + selector: 'website-tiny-checkbox-focused', + component: CheckboxFocusedComponent + }, + + { + selector: 'website-tiny-checkbox-group', + component: CheckboxGroupComponent + }, + + { + selector: 'website-tiny-checkbox-group-direction', + component: CheckboxGroupDirectionComponent + }, + + { + selector: 'website-tiny-checkbox-group-labelkey', + component: CheckboxGroupLabelkeyComponent + }, + + { + selector: 'website-tiny-checkbox-group-level', + component: CheckboxGroupLevelComponent + }, + + { + selector: 'website-tiny-checkbox-group-linewrap', + component: CheckboxGroupLinewrapComponent + }, + + { + selector: 'website-tiny-checkbox-group-validation', + component: CheckboxGroupValidationComponent + }, + + { + selector: 'website-tiny-checkbox-group-valuekey', + component: CheckboxGroupValuekeyComponent + }, + + { + selector: 'website-tiny-checkbox-indeterminate', + component: CheckboxIndeterminateComponent + }, + + { + selector: 'website-tiny-checkbox-label', + component: CheckboxLabelComponent + }, + + { + selector: 'website-tiny-collapse-basic', + component: CollapseBasicComponent + }, + + { + selector: 'website-tiny-collapse-event', + component: CollapseEventComponent + }, + + { + selector: 'website-tiny-collapsebox-basic', + component: CollapseboxBasicComponent + }, + + { + selector: 'website-tiny-collapsebox-closeable', + component: CollapseboxCloseableComponent + }, + + { + selector: 'website-tiny-collapsebox-event', + component: CollapseboxEventComponent + }, + + { + selector: 'website-tiny-collapsebutton-basic', + component: CollapsebuttonBasicComponent + }, + { + selector: 'website-tiny-collapsebutton-customtext', + component: CollapsebuttonCustomtextComponent + }, + { + selector: 'website-tiny-collapsebutton-searchcount', + component: CollapsebuttonSearchcountComponent + }, + { + selector: 'website-tiny-collapsebutton-event', + component: CollapsebuttonEventComponent + }, + + { selector: 'website-tiny-collapsetext-basic', component: CollapsetextBasicComponent }, + + { selector: 'website-tiny-collapsetext-type', component: CollapsetextTypeComponent }, + + { selector: 'website-tiny-collapsetext-highlight', component: CollapsetextHighlightComponent }, + + { selector: 'website-tiny-collapsetext-collapsed', component: CollapsetextCollapsedComponent }, + + { selector: 'website-tiny-collapsetext-scene', component: CollapsetextSceneComponent }, + + { + selector: 'website-tiny-copy-basic', + component: CopyBasicComponent + }, + { + selector: 'website-tiny-copy-dark', + component: CopyDarkComponent + }, + { + selector: 'website-tiny-copy-tip', + component: CopyTipComponent + }, + { + selector: 'website-tiny-copy-table', + component: CopyTableComponent + }, + { + selector: 'website-tiny-copy-event', + component: CopyEventComponent + }, + + { selector: 'website-tiny-crumb-basic', component: CrumbBasicComponent }, + + { selector: 'website-tiny-crumb-events', component: CrumbEventsComponent }, + + { selector: 'website-tiny-crumb-href', component: CrumbHrefComponent }, + + { + selector: 'website-tiny-crumb-router-test', + component: CrumbRouterTestComponent + }, + + { selector: 'website-tiny-crumb-router', component: CrumbRouterComponent }, + + { + selector: 'website-tiny-date-clearicon', + component: DateCleariconComponent + }, + + { + selector: 'website-tiny-date-customize', + component: DateCustomizeComponent + }, + + { selector: 'website-tiny-date-disabled', component: DateDisabledComponent }, + + { + selector: 'website-tiny-date-disableddays', + component: DateDisableddaysComponent + }, + + { selector: 'website-tiny-date-event', component: DateEventComponent }, + + { selector: 'website-tiny-date-form', component: DateFormComponent }, + + { + selector: 'website-tiny-date-format-test', + component: DateFormatTestComponent + }, + + { selector: 'website-tiny-date-format', component: DateFormatComponent }, + + { selector: 'website-tiny-date-max', component: DateMaxComponent }, + + { + selector: 'website-tiny-date-maxmin-test', + component: DateMaxminTestComponent + }, + + { selector: 'website-tiny-date-maxmin', component: DateMaxminComponent }, + + { selector: 'website-tiny-date-min', component: DateMinComponent }, + + { + selector: 'website-tiny-date-nowdatetime', + component: DateNowdatetimeComponent + }, + + { + selector: 'website-tiny-date-panelalign', + component: DatePanelalignComponent + }, + + { + selector: 'website-tiny-date-validation', + component: DateValidationComponent + }, + + { + selector: 'website-tiny-date-value-test', + component: DateValueTestComponent + }, + + { selector: 'website-tiny-date-value', component: DateValueComponent }, + + { + selector: 'website-tiny-daterange-customize', + component: DaterangeCustomizeComponent + }, + + { + selector: 'website-tiny-daterange-disabled', + component: DaterangeDisabledComponent + }, + + { + selector: 'website-tiny-daterange-disableddays', + component: DaterangeDisableddaysComponent + }, + + { + selector: 'website-tiny-daterange-event', + component: DaterangeEventComponent + }, + + { + selector: 'website-tiny-daterange-fixedvalue-test', + component: DaterangeFixedvalueTestComponent + }, + + { + selector: 'website-tiny-daterange-fixedvalue', + component: DaterangeFixedvalueComponent + }, + + { + selector: 'website-tiny-daterange-format-test', + component: DaterangeFormatTestComponent + }, + + { + selector: 'website-tiny-daterange-format', + component: DaterangeFormatComponent + }, + + { + selector: 'website-tiny-daterange-isallowbeginequalend', + component: DaterangeIsallowbeginequalendComponent + }, + + { selector: 'website-tiny-daterange-max', component: DaterangeMaxComponent }, + + { + selector: 'website-tiny-daterange-maxmin-test', + component: DaterangeMaxminTestComponent + }, + + { + selector: 'website-tiny-daterange-maxmin', + component: DaterangeMaxminComponent + }, + + { selector: 'website-tiny-daterange-min', component: DaterangeMinComponent }, + + { + selector: 'website-tiny-daterange-nowdatetime', + component: DaterangeNowdatetimeComponent + }, + + { + selector: 'website-tiny-daterange-panelalign', + component: DaterangePanelalignComponent + }, + + { + selector: 'website-tiny-daterange-validation', + component: DaterangeValidationComponent + }, + + { + selector: 'website-tiny-daterange-value-test', + component: DaterangeValueTestComponent + }, + + { + selector: 'website-tiny-daterange-value', + component: DaterangeValueComponent + }, + + { + selector: 'website-tiny-datetime-clearicon', + component: DatetimeCleariconComponent + }, + + { + selector: 'website-tiny-datetime-customize', + component: DatetimeCustomizeComponent + }, + + { + selector: 'website-tiny-datetime-disabled', + component: DatetimeDisabledComponent + }, + + { + selector: 'website-tiny-datetime-event', + component: DatetimeEventComponent + }, + + { + selector: 'website-tiny-datetime-format-test', + component: DatetimeFormatTestComponent + }, + + { + selector: 'website-tiny-datetime-format', + component: DatetimeFormatComponent + }, + + { selector: 'website-tiny-datetime-max', component: DatetimeMaxComponent }, + + { + selector: 'website-tiny-datetime-maxmin-test', + component: DatetimeMaxminTestComponent + }, + + { + selector: 'website-tiny-datetime-maxmin', + component: DatetimeMaxminComponent + }, + + { selector: 'website-tiny-datetime-min', component: DatetimeMinComponent }, + + { + selector: 'website-tiny-datetime-nowdatetime', + component: DatetimeNowdatetimeComponent + }, + + { + selector: 'website-tiny-datetime-panelalign', + component: DatetimePanelalignComponent + }, + + { + selector: 'website-tiny-datetime-validation', + component: DatetimeValidationComponent + }, + + { + selector: 'website-tiny-datetime-value-test', + component: DatetimeValueTestComponent + }, + + { + selector: 'website-tiny-datetime-value', + component: DatetimeValueComponent + }, + + { selector: 'website-tiny-datetime-timezoneable', component: DatetimeTimezoneableComponent }, + + { + selector: 'website-tiny-datetimerange-clearicon', + component: DatetimerangeCleariconComponent + }, + + { + selector: 'website-tiny-datetimerange-customize', + component: DatetimerangeCustomizeComponent + }, + + { + selector: 'website-tiny-datetimerange-disabled', + component: DatetimerangeDisabledComponent + }, + + { + selector: 'website-tiny-datetimerange-event', + component: DatetimerangeEventComponent + }, + + { + selector: 'website-tiny-datetimerange-format-test', + component: DatetimerangeFormatTestComponent + }, + + { + selector: 'website-tiny-datetimerange-format', + component: DatetimerangeFormatComponent + }, + + { + selector: 'website-tiny-datetimerange-isallowbeginequalend', + component: DatetimerangeIsallowbeginequalendComponent + }, + + { + selector: 'website-tiny-datetimerange-many-test', + component: DatetimerangeManyTestComponent + }, + + { + selector: 'website-tiny-datetimerange-max', + component: DatetimerangeMaxComponent + }, + + { + selector: 'website-tiny-datetimerange-maxmin-test', + component: DatetimerangeMaxminTestComponent + }, + + { + selector: 'website-tiny-datetimerange-maxmin', + component: DatetimerangeMaxminComponent + }, + + { + selector: 'website-tiny-datetimerange-min', + component: DatetimerangeMinComponent + }, + + { + selector: 'website-tiny-datetimerange-nowdatetime', + component: DatetimerangeNowdatetimeComponent + }, + + { + selector: 'website-tiny-datetimerange-panelalign', + component: DatetimerangePanelalignComponent + }, + + { + selector: 'website-tiny-datetimerange-timezoneable', + component: DatetimerangeTimezoneableComponent + }, + + { + selector: 'website-tiny-datetimerange-validation', + component: DatetimerangeValidationComponent + }, + + { + selector: 'website-tiny-datetimerange-value-test', + component: DatetimerangeValueTestComponent + }, + + { + selector: 'website-tiny-datetimerange-value', + component: DatetimerangeValueComponent + }, + + { + selector: 'website-tiny-formfield-colspan-rowspan-test', + component: FormfieldColspanRowspanTestComponent + }, + + { selector: 'website-tiny-guides-basic', component: GuidesBasicComponent }, + + { selector: 'website-tiny-guides-tab', component: GuidesTabComponent }, + + { + selector: 'website-tiny-guides-guidesteps', + component: GuidesGuidestepsComponent + }, + + { selector: 'website-tiny-guides-type', component: GuidesTypeComponent }, + + { + selector: 'website-tiny-formfield-colspan-rowspan', + component: FormfieldColspanRowspanComponent + }, + + { + selector: 'website-tiny-formfield-colswidth', + component: FormfieldColswidthComponent + }, + + { selector: 'website-tiny-formfield-foo', component: FormfieldFooComponent }, + + { + selector: 'website-tiny-formfield-index', + component: FormfieldIndexComponent + }, + + { + selector: 'website-tiny-formfield-label', + component: FormfieldLabelComponent + }, + + { + selector: 'website-tiny-formfield-labelwidth', + component: FormfieldLabelwidthComponent + }, + + { + selector: 'website-tiny-formfield-multi-column', + component: FormfieldMultiColumnComponent + }, + + { + selector: 'website-tiny-formfield-nest-formfiled', + component: FormfieldNestFormfiledComponent + }, + + { + selector: 'website-tiny-formfield-ngfor-test', + component: FormfieldNgforTestComponent + }, + + { + selector: 'website-tiny-formfield-required', + component: FormfieldRequiredComponent + }, + + { + selector: 'website-tiny-formfield-requiredspace', + component: FormfieldRequiredspaceComponent + }, + + { + selector: 'website-tiny-formfield-show', + component: FormfieldShowComponent + }, + + { + selector: 'website-tiny-formfield-single-column', + component: FormfieldSingleColumnComponent + }, + + { + selector: 'website-tiny-formfield-test', + component: FormfieldTestComponent + }, + + { + selector: 'website-tiny-formfield-text-form', + component: FormfieldTextFormComponent + }, + + { + selector: 'website-tiny-formfield-vertical-align', + component: FormfieldVerticalAlignComponent + }, + + { + selector: 'website-tiny-formfield-vertical', + component: FormfieldVerticalComponent + }, + + { + selector: 'website-tiny-foldtext-basic', + component: FoldtextBasicComponent + }, + + { + selector: 'website-tiny-foldtext-table', + component: FoldtextTableComponent + }, + + { + selector: 'website-tiny-guidesteps-basic', + component: GuidestepsBasicComponent + }, + + { + selector: 'website-tiny-guidesteps-iscomplete', + component: GuidestepsIscompleteComponent + }, + + { + selector: 'website-tiny-guidesteps-large', + component: GuidestepsLargeComponent + }, + + { + selector: 'website-tiny-halfmodal-async', + component: HalfmodalAsyncComponent + }, + + { + selector: 'website-tiny-halfmodal-backdrop', + component: HalfmodalBackdropComponent + }, + + { + selector: 'website-tiny-halfmodal-basic', + component: HalfmodalBasicComponent + }, + + { + selector: 'website-tiny-halfmodal-beforehide', + component: HalfmodalBeforehideComponent + }, + + { + selector: 'website-tiny-halfmodal-content', + component: HalfmodalContentComponent + }, + + { + selector: 'website-tiny-halfmodal-modal', + component: HalfmodalModalComponent + }, + + { + selector: 'website-tiny-halfmodal-modalselect', + component: HalfmodalModalselectComponent + }, + + { + selector: 'website-tiny-halfmodal-multi', + component: HalfmodalMultiComponent + }, + + { + selector: 'website-tiny-halfmodal-service-test', + component: HalfmodalServiceTestComponent + }, + + { + selector: 'website-tiny-halfmodal-service', + component: HalfmodalServiceComponent + }, + + { selector: 'website-tiny-icon-basic', component: IconBasicComponent }, + + { selector: 'website-tiny-svg-setpath', component: SvgSetpathComponent }, + + { selector: 'website-tiny-icon-show', component: IconShowComponent }, + + { selector: 'website-tiny-iconaction-basic', component: IconactionBasicComponent }, + + { selector: 'website-tiny-iconaction-dark', component: IconactionDarkComponent }, + + { selector: 'website-tiny-iconaction-disabled', component: IconactionDisabledComponent }, + + { selector: 'website-tiny-iconaction-href', component: IconactionHrefComponent }, + + { + selector: 'website-tiny-imagepreview-basic', + component: ImagepreviewBasicComponent + }, + + { + selector: 'website-tiny-inputnumber-basic', + component: InputnumberBasicComponent + }, + + { + selector: 'website-tiny-inputnumber-event', + component: InputnumberEventComponent + }, + + { + selector: 'website-tiny-inputnumber-focus', + component: InputnumberFocusComponent + }, + + { + selector: 'website-tiny-inputnumber-format', + component: InputnumberFormatComponent + }, + + { + selector: 'website-tiny-inputnumber-load', + component: InputnumberLoadComponent + }, + + { + selector: 'website-tiny-inputnumber-localeable', + component: InputnumberLocaleableComponent + }, + + { + selector: 'website-tiny-inputnumber-maxlength', + component: InputnumberMaxlengthComponent + }, + + { selector: 'website-tiny-intro-basic', component: IntroBasicComponent }, + + { selector: 'website-tiny-intro-event', component: IntroEventComponent }, + + { selector: 'website-tiny-intro-modal', component: IntroModalComponent }, + + { + selector: 'website-tiny-intro-skipable', + component: IntroSkipableComponent + }, + + { + selector: 'website-tiny-intro-template', + component: IntroTemplateComponent + }, + + { selector: 'website-tiny-intro-tip', component: IntroTipComponent }, + + { + selector: 'website-tiny-intro-tiscroll', + component: IntroTiscrollComponent + }, + + { selector: 'website-tiny-ip-basic', component: IpBasicComponent }, + + { selector: 'website-tiny-ip-disabled', component: IpDisabledComponent }, + + { + selector: 'website-tiny-ip-formcontrol', + component: IpFormcontrolComponent + }, + + { selector: 'website-tiny-ip-valid', component: IpValidComponent }, + + { + selector: 'website-tiny-ipsection-basic', + component: IpsectionBasicComponent + }, + + { + selector: 'website-tiny-ipsection-disabled', + component: IpsectionDisabledComponent + }, + + { + selector: 'website-tiny-ipsection-events', + component: IpsectionEventsComponent + }, + + { + selector: 'website-tiny-ipsection-focus', + component: IpsectionFocusComponent + }, + + { + selector: 'website-tiny-ipsection-test', + component: IpsectionTestComponent + }, + + { + selector: 'website-tiny-ipsection-valid-formgroup', + component: IpsectionValidFormgroupComponent + }, + + { + selector: 'website-tiny-ipsection-valid', + component: IpsectionValidComponent + }, + { selector: 'website-tiny-labeleditor-basic', component: LabeleditorBasicComponent }, + + { selector: 'website-tiny-labeleditor-autotip', component: LabeleditorAutotipComponent }, + + { selector: 'website-tiny-labeleditor-icontipcontext', component: LabeleditorIconTipContextComponent }, + + { selector: 'website-tiny-labeleditor-resize', component: LabeleditorResizeComponent }, + + { selector: 'website-tiny-labeleditor-maxline', component: LabeleditorMaxlineComponent }, + + { selector: 'website-tiny-labeleditor-maxlength', component: LabeleditorMaxlengthComponent }, + + { selector: 'website-tiny-labeleditor-validation', component: LabeleditorValidationComponent }, + + { selector: 'website-tiny-labeleditor-validation-async', component: LabeleditorValidationAsyncComponent }, + + { selector: 'website-tiny-labeleditor-disabled', component: LabeleditorDisabledComponent }, + + { selector: 'website-tiny-labeleditor-events', component: LabeleditorEventsComponent }, + + { selector: 'website-tiny-labeleditor-multiline-size', component: LabeleditorMultilineSizeComponent }, + + { + selector: 'website-tiny-layout-basic-simple-responsive', + component: LayoutBasicSimpleResponsiveComponent + }, + + { + selector: 'website-tiny-layout-basic-simple', + component: LayoutBasicSimpleComponent + }, + + { selector: 'website-tiny-layout-basic', component: LayoutBasicComponent }, + + { + selector: 'website-tiny-layout-detail-column', + component: LayoutDetailColumnComponent + }, + + { selector: 'website-tiny-layout-detail', component: LayoutDetailComponent }, + + { + selector: 'website-tiny-layout-list-largedata', + component: LayoutListLargedataComponent + }, + + { selector: 'website-tiny-layout-list', component: LayoutListComponent }, + + { + selector: 'website-tiny-layout-multi-column', + component: LayoutMultiColumnComponent + }, + + { + selector: 'website-tiny-layout-overview-vertical', + component: LayoutOverviewVerticalComponent + }, + + { + selector: 'website-tiny-layout-overview', + component: LayoutOverviewComponent + }, + + { + selector: 'website-tiny-layout-purchase-responsive-change', + component: LayoutPurchaseResponsiveChangeComponent + }, + + { + selector: 'website-tiny-layout-purchase-responsive', + component: LayoutPurchaseResponsiveComponent + }, + + { + selector: 'website-tiny-layout-purchase-simple-responsive', + component: LayoutPurchaseSimpleResponsiveComponent + }, + + { + selector: 'website-tiny-layout-purchase-simple', + component: LayoutPurchaseSimpleComponent + }, + + { + selector: 'website-tiny-layout-purchase', + component: LayoutPurchaseComponent + }, + + { selector: 'website-tiny-layout-single', component: LayoutSingleComponent }, + + { + selector: 'website-tiny-leftmenu-active-change', + component: LeftmenuActiveChangeWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-basic', + component: LeftmenuBasicWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-collapsed', + component: LeftmenuCollapsedWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-disabled', + component: LeftmenuDisabledWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-dividing', + component: LeftmenuDividingWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-foot', + component: LeftmenuFootWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-group', + component: LeftmenuGroupWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-href', + component: LeftmenuHrefWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-no-router', + component: LeftmenuNoRouterWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-params', + component: LeftmenuParamsWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-reload-state', + component: LeftmenuReloadStateWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-routerlist', + component: LeftmenuRouterlistWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-toggleable', + component: LeftmenuToggleableWebsiteViewComponent + }, + + { selector: 'website-tiny-loading-area', component: LoadingAreaComponent }, + + { selector: 'website-tiny-loading-basic', component: LoadingBasicComponent }, + + { selector: 'website-tiny-loading-size', component: LoadingSizeComponent }, + + { selector: 'website-tiny-loading-type', component: LoadingTypeComponent }, + + { selector: 'website-tiny-locale-basic', component: LocaleBasicComponent }, + + { selector: 'website-tiny-locale-format', component: LocaleFormatComponent }, + + { selector: 'website-tiny-locale-reload', component: LocaleReloadComponent }, + + { selector: 'website-tiny-linkbutton-basic', component: LinkbuttonBasicComponent }, + + { selector: 'website-tiny-menu-basic', component: MenuBasicComponent }, + + { + selector: 'website-tiny-menu-beforeopen', + component: MenuBeforeopenComponent + }, + + { selector: 'website-tiny-menu-border', component: MenuBorderComponent }, + + { + selector: 'website-tiny-menu-buttoncolor', + component: MenuButtoncolorComponent + }, + + { selector: 'website-tiny-menu-default', component: MenuDefaultComponent }, + + { selector: 'website-tiny-menu-disabled', component: MenuDisabledComponent }, + + { selector: 'website-tiny-menu-event', component: MenuEventComponent }, + + { selector: 'website-tiny-menu-group', component: MenuGroupComponent }, + + { selector: 'website-tiny-menu-id', component: MenuIdComponent }, + + { selector: 'website-tiny-menu-labelkey', component: MenuLabelkeyComponent }, + + { + selector: 'website-tiny-menu-panelalign', + component: MenuPanelalignComponent + }, + + { + selector: 'website-tiny-menu-panelstyle', + component: MenuPanelstyleComponent + }, + + { + selector: 'website-tiny-menu-templete-test', + component: MenuTempleteTestComponent + }, + + { selector: 'website-tiny-menu-templete', component: MenuTempleteComponent }, + + { selector: 'website-tiny-menu-tips', component: MenuTipsComponent }, + + { selector: 'website-tiny-message-basic', component: MessageBasicComponent }, + + { + selector: 'website-tiny-message-btn-test', + component: MessageBtnTestComponent + }, + + { selector: 'website-tiny-message-btn', component: MessageBtnComponent }, + + { + selector: 'website-tiny-message-content', + component: MessageContentComponent + }, + + { selector: 'website-tiny-message-id', component: MessageIdComponent }, + + { + selector: 'website-tiny-message-security', + component: MessageSecurityComponent + }, + + { selector: 'website-tiny-message-title', component: MessageTitleComponent }, + + { selector: 'website-tiny-message-type', component: MessageTypeComponent }, + + { + selector: 'website-tiny-modal-animation', + component: ModalAnimationComponent + }, + + { + selector: 'website-tiny-modal-backdrop', + component: ModalBackdropComponent + }, + + { selector: 'website-tiny-modal-class', component: ModalClassComponent }, + + { + selector: 'website-tiny-modal-close-icon', + component: ModalCloseIconComponent + }, + + { + selector: 'website-tiny-modal-config-test', + component: ModalConfigTestComponent + }, + + { + selector: 'website-tiny-modal-content-comp', + component: ModalContentCompComponent + }, + + { + selector: 'website-tiny-modal-content-temp', + component: ModalContentTempComponent + }, + + { + selector: 'website-tiny-modal-draggable', + component: ModalDraggableComponent + }, + + { selector: 'website-tiny-modal-esc', component: ModalEscComponent }, + + { selector: 'website-tiny-modal-event', component: ModalEventComponent }, + + { + selector: 'website-tiny-modal-header-align', + component: ModalHeaderAlignComponent + }, + + { + selector: 'website-tiny-modal-header-style', + component: ModalHeaderStyleComponent + }, + + { + selector: 'website-tiny-modal-two-backdrop', + component: ModalTwoBackdropComponent + }, + + { selector: 'website-tiny-modal-two-test', component: ModalTwoTestComponent }, + + { selector: 'website-tiny-nav-active', component: NavActiveComponent }, + + { selector: 'website-tiny-nav-align', component: NavAlignComponent }, + + { selector: 'website-tiny-nav-basic', component: NavBasicComponent }, + + { selector: 'website-tiny-nav-disabled', component: NavDisabledComponent }, + + { selector: 'website-tiny-nav-event', component: NavEventComponent }, + + { selector: 'website-tiny-nav-left', component: NavLeftComponent }, + + { selector: 'website-tiny-nav-right', component: NavRightComponent }, + + { + selector: 'website-tiny-nav-selectable', + component: NavSelectableComponent + }, + + { selector: 'website-tiny-nav-submenu', component: NavSubmenuComponent }, + + { selector: 'website-tiny-nav-template', component: NavTemplateComponent }, + + { selector: 'website-tiny-nav-theme', component: NavThemeComponent }, + + { selector: 'website-tiny-nav-width', component: NavWidthComponent }, + + { + selector: 'website-tiny-notification-animation', + component: NotificationAnimationComponent + }, + + { + selector: 'website-tiny-notification-basic', + component: NotificationBasicComponent + }, + + { + selector: 'website-tiny-notification-close', + component: NotificationCloseComponent + }, + + { + selector: 'website-tiny-notification-config', + component: NotificationConfigComponent + }, + + { + selector: 'website-tiny-notification-duration', + component: NotificationDurationComponent + }, + + { + selector: 'website-tiny-notification-events', + component: NotificationEventsComponent + }, + + { + selector: 'website-tiny-notification-hover-pause', + component: NotificationHoverPauseComponent + }, + + { + selector: 'website-tiny-notification-name', + component: NotificationNameComponent + }, + + { + selector: 'website-tiny-notification-position', + component: NotificationPositionComponent + }, + + { + selector: 'website-tiny-notification-template', + component: NotificationTemplateComponent + }, + + { + selector: 'website-tiny-notification-type', + component: NotificationTypeComponent + }, + + { + selector: 'website-tiny-overflow-destory', + component: OverflowDestoryComponent + }, + + { + selector: 'website-tiny-overflow-directive', + component: OverflowDirectiveComponent + }, + + { + selector: 'website-tiny-overflow-maxline', + component: OverflowMaxlineComponent + }, + + { + selector: 'website-tiny-overflow-maxwidth', + component: OverflowMaxwidthComponent + }, + + { + selector: 'website-tiny-overflow-position', + component: OverflowPositionComponent + }, + + { + selector: 'website-tiny-overflow-service', + component: OverflowServiceComponent + }, + + { selector: 'website-tiny-overflow-test', component: OverflowTestComponent }, + + { + selector: 'website-tiny-overflow-tipcontent', + component: OverflowTipcontentComponent + }, + + { selector: 'website-tiny-phonenumber-basic', component: PhonenumberBasicComponent }, + + { selector: 'website-tiny-phonenumber-disabled', component: PhonenumberDisabledComponent }, + + { selector: 'website-tiny-phonenumber-event', component: PhonenumberEventComponent }, + + { selector: 'website-tiny-phonenumber-valid', component: PhonenumberValidComponent }, + + { selector: 'website-tiny-phonenumber-country', component: PhonenumberCountryComponent }, + + { + selector: 'website-tiny-pagination-autohide', + component: PaginationAutohideComponent + }, + + { + selector: 'website-tiny-pagination-disabled', + component: PaginationDisabledComponent + }, + + { + selector: 'website-tiny-pagination-event', + component: PaginationEventComponent + }, + + { + selector: 'website-tiny-pagination-fixed', + component: PaginationFixedComponent + }, + + { + selector: 'website-tiny-pagination-loading', + component: PaginationLoadingComponent + }, + + { + selector: 'website-tiny-pagination-pageselectwidth', + component: PaginationPageselectwidthComponent + }, + + { + selector: 'website-tiny-pagination-pagesize', + component: PaginationPagesizeComponent + }, + + { + selector: 'website-tiny-pagination-showgotolink', + component: PaginationShowgotolinkComponent + }, + + { + selector: 'website-tiny-pagination-showlastpage', + component: PaginationShowlastpageComponent + }, + + { + selector: 'website-tiny-pagination-showtotalnumber', + component: PaginationShowtotalnumberComponent + }, + + { + selector: 'website-tiny-pagination-type', + component: PaginationTypeComponent + }, + { selector: 'website-tiny-pathfield-items', component: PathfieldItemsComponent }, + + { selector: 'website-tiny-pathfield-ispanel', component: PathfieldIspanelComponent }, + + { selector: 'website-tiny-pathfield-panelwidth', component: PathfieldPanelwidthComponent }, + + { selector: 'website-tiny-pathfield-editable', component: PathfieldEditableComponent }, + + { selector: 'website-tiny-pathfield-event', component: PathfieldEventComponent }, + + { selector: 'website-tiny-path-list', component: PathListComponent }, + + { selector: 'website-tiny-path-select', component: PathSelectComponent }, + + { + selector: 'website-tiny-popconfirm-basic', + component: PopconfirmBasicComponent + }, + + { + selector: 'website-tiny-popconfirm-define', + component: PopconfirmDefineComponent + }, + + { + selector: 'website-tiny-popconfirm-event', + component: PopconfirmEventComponent + }, + + { + selector: 'website-tiny-popconfirm-table-define', + component: PopconfirmTableDefineComponent + }, + + { + selector: 'website-tiny-popconfirm-table', + component: PopconfirmTableComponent + }, + + { + selector: 'website-tiny-progressbar-animation', + component: ProgressbarAnimationComponent + }, + + { + selector: 'website-tiny-progressbar-basic', + component: ProgressbarBasicComponent + }, + + { + selector: 'website-tiny-progressbar-class', + component: ProgressbarClassComponent + }, + + { + selector: 'website-tiny-productpreview-basic', + component: ProductpreviewBasicComponent + }, + + { selector: 'website-tiny-radio-basic', component: RadioBasicComponent }, + + { selector: 'website-tiny-radio-dark', component: RadioDarkComponent }, + + { + selector: 'website-tiny-radio-disabled', + component: RadioDisabledComponent + }, + + { selector: 'website-tiny-radio-event', component: RadioEventComponent }, + + { selector: 'website-tiny-radio-focus', component: RadioFocusComponent }, + + { selector: 'website-tiny-radio-group', component: RadioGroupComponent }, + + { + selector: 'website-tiny-radio-group-direction', + component: RadioGroupDirectionComponent + }, + + { + selector: 'website-tiny-radio-group-labelkey', + component: RadioGroupLabelkeyComponent + }, + + { + selector: 'website-tiny-radio-group-linewrap', + component: RadioGroupLinewrapComponent + }, + + { + selector: 'website-tiny-radio-group-validation', + component: RadioGroupValidationComponent + }, + + { + selector: 'website-tiny-radio-group-valuekey', + component: RadioGroupValuekeyComponent + }, + + { selector: 'website-tiny-radio-label', component: RadioLabelComponent }, + + { selector: 'website-tiny-rate-basic', component: RateBasicComponent }, + + { selector: 'website-tiny-rate-disabled', component: RateDisabledComponent }, + + { selector: 'website-tiny-rate-event', component: RateEventComponent }, + + { selector: 'website-tiny-rate-id', component: RateIdComponent }, + + { selector: 'website-tiny-rate-load', component: RateLoadComponent }, + + { selector: 'website-tiny-rights-basic', component: RightsBasicComponent }, + + { selector: 'website-tiny-rights-type', component: RightsTypeComponent }, + + { + selector: 'website-tiny-score-basic', + component: ScoreBasicComponent + }, + + { + selector: 'website-tiny-score-events', + component: ScoreEventsComponent + }, + + { + selector: 'website-tiny-score-limittext', + component: ScoreLimittextComponent + }, + + { + selector: 'website-tiny-score-padding', + component: ScorePaddingComponent + }, + + { + selector: 'website-tiny-searchbox-appendtobody', + component: SearchboxAppendtobodyComponent + }, + + { + selector: 'website-tiny-searchbox-basic', + component: SearchboxBasicComponent + }, + + { + selector: 'website-tiny-searchbox-disabled', + component: SearchboxDisabledComponent + }, + + { + selector: 'website-tiny-searchbox-event', + component: SearchboxEventComponent + }, + + { + selector: 'website-tiny-searchbox-maxlength', + component: SearchboxMaxlengthComponent + }, + + { + selector: 'website-tiny-searchbox-notsearch', + component: SearchboxNotsearchComponent + }, + + { + selector: 'website-tiny-searchbox-options', + component: SearchboxOptionsComponent + }, + + { + selector: 'website-tiny-searchbox-panelsize', + component: SearchboxPanelsizeComponent + }, + + { + selector: 'website-tiny-searchbox-reactive', + component: SearchboxReactiveComponent + }, + + { + selector: 'website-tiny-searchbox-suggest', + component: SearchboxSuggestComponent + }, + + { + selector: 'website-tiny-searchbox-template', + component: SearchboxTemplateComponent + }, + + { + selector: 'website-tiny-searchbox-test', + component: SearchboxTestComponent + }, + + { + selector: 'website-tiny-searchbox-trimmed', + component: SearchboxTrimmedComponent + }, + + { + selector: 'website-tiny-searchbox-valid', + component: SearchboxValidComponent + }, + + { + selector: 'website-tiny-searchbox-virtualscroll', + component: SearchboxVirtualscrollComponent + }, + + { selector: 'website-tiny-selectgroup-basic', component: SelectgroupBasicComponent }, + + { selector: 'website-tiny-selectgroup-multiple', component: SelectgroupMultipleComponent }, + + { selector: 'website-tiny-selectgroup-valuekey', component: SelectgroupValuekeyComponent }, + + { selector: 'website-tiny-selectgroup-template', component: SelectgroupTemplateComponent }, + + { selector: 'website-tiny-selectgroup-select', component: SelectgroupSelectComponent }, + + { + selector: 'website-tiny-select-appendtobody', + component: SelectAppendtobodyComponent + }, + + { selector: 'website-tiny-select-basic', component: SelectBasicComponent }, + + { + selector: 'website-tiny-select-beforesearch-test', + component: SelectBeforesearchTestComponent + }, + + { + selector: 'website-tiny-select-beforesearch', + component: SelectBeforesearchComponent + }, + + { + selector: 'website-tiny-select-change-selectall', + component: SelectChangeSelectallComponent + }, + + { + selector: 'website-tiny-select-clearable', + component: SelectClearableComponent + }, + + { + selector: 'website-tiny-select-disabled', + component: SelectDisabledComponent + }, + + { + selector: 'website-tiny-select-disabledfocus', + component: SelectDisabledfocusComponent + }, + + { selector: 'website-tiny-select-enum', component: SelectEnumComponent }, + + { selector: 'website-tiny-select-event', component: SelectEventComponent }, + + { selector: 'website-tiny-select-focus', component: SelectFocusComponent }, + + { selector: 'website-tiny-select-group', component: SelectGroupComponent }, + + { selector: 'website-tiny-select-id', component: SelectIdComponent }, + + { selector: 'website-tiny-select-idkey', component: SelectIdkeyComponent }, + + { selector: 'website-tiny-select-input', component: SelectInputComponent }, + + { + selector: 'website-tiny-select-labelkey', + component: SelectLabelkeyComponent + }, + + { selector: 'website-tiny-select-lazy', component: SelectLazyComponent }, + + { selector: 'website-tiny-select-leak', component: SelectLeakComponent }, + + { selector: 'website-tiny-select-load', component: SelectLoadComponent }, + + { selector: 'website-tiny-select-many', component: SelectManyComponent }, + + { + selector: 'website-tiny-select-maxline', + component: SelectMaxlineComponent + }, + + { selector: 'website-tiny-select-much', component: SelectMuchComponent }, + + { selector: 'website-tiny-select-multi', component: SelectMultiComponent }, + + { + selector: 'website-tiny-select-noborder', + component: SelectNoborderComponent + }, + + { selector: 'website-tiny-select-nodata', component: SelectNodataComponent }, + + { + selector: 'website-tiny-select-noempty', + component: SelectNoemptyComponent + }, + + { selector: 'website-tiny-select-null', component: SelectNullComponent }, + + { + selector: 'website-tiny-select-pagin-beforesearch', + component: SelectPaginBeforesearchComponent + }, + + { + selector: 'website-tiny-select-pagination', + component: SelectPaginationComponent + }, + + { selector: 'website-tiny-select-panel', component: SelectPanelComponent }, + + { + selector: 'website-tiny-select-reservesearchword', + component: SelectReservesearchwordComponent + }, + + { + selector: 'website-tiny-select-scroll-load', + component: SelectScrollLoadComponent + }, + + { selector: 'website-tiny-select-search', component: SelectSearchComponent }, + + { + selector: 'website-tiny-select-searchkeys', + component: SelectSearchkeysComponent + }, + + { + selector: 'website-tiny-select-selectall', + component: SelectSelectallComponent + }, + + { + selector: 'website-tiny-select-showselectednumber', + component: SelectShowselectednumberComponent + }, + + { selector: 'website-tiny-select-small', component: SelectSmallComponent }, + + { selector: 'website-tiny-select-tag', component: SelectTagComponent }, + + { + selector: 'website-tiny-select-template', + component: SelectTemplateComponent + }, + + { selector: 'website-tiny-select-tip', component: SelectTipComponent }, + + { + selector: 'website-tiny-select-tiscroll', + component: SelectTiscrollComponent + }, + + { selector: 'website-tiny-select-tworow', component: SelectTworowComponent }, + + { selector: 'website-tiny-select-valid', component: SelectValidComponent }, + + { + selector: 'website-tiny-select-validgroup', + component: SelectValidgroupComponent + }, + + { + selector: 'website-tiny-select-valuekey-test', + component: SelectValuekeyTestComponent + }, + + { + selector: 'website-tiny-select-valuekey', + component: SelectValuekeyComponent + }, + + { + selector: 'website-tiny-select-virtualscroll-multi', + component: SelectVirtualscrollMultiComponent + }, + + { + selector: 'website-tiny-select-virtualscroll', + component: SelectVirtualscrollComponent + }, + + { selector: 'website-tiny-skeleton-page', component: SkeletonPageComponent }, + + { + selector: 'website-tiny-skeleton-title', + component: SkeletonTitleComponent + }, + + { selector: 'website-tiny-skeleton-type', component: SkeletonTypeComponent }, + + { selector: 'website-tiny-slider-event', component: SliderEventComponent }, + + { + selector: 'website-tiny-slider-formcontrol', + component: SliderFormcontrolComponent + }, + + { selector: 'website-tiny-slider-hidden', component: SliderHiddenComponent }, + + { selector: 'website-tiny-slider-limits', component: SliderLimitsComponent }, + + { selector: 'website-tiny-slider-ratios', component: SliderRatiosComponent }, + + { selector: 'website-tiny-slider-scales', component: SliderScalesComponent }, + + { + selector: 'website-tiny-slider-template', + component: SliderTemplateComponent + }, + + { selector: 'website-tiny-slider-tip', component: SliderTipComponent }, + + { + selector: 'website-tiny-spinner-basic-test', + component: SpinnerBasicTestComponent + }, + + { selector: 'website-tiny-spinner-basic', component: SpinnerBasicComponent }, + + { + selector: 'website-tiny-spinner-correctable', + component: SpinnerCorrectableComponent + }, + + { + selector: 'website-tiny-spinner-disabled', + component: SpinnerDisabledComponent + }, + + { selector: 'website-tiny-spinner-event', component: SpinnerEventComponent }, + + { + selector: 'website-tiny-spinner-format', + component: SpinnerFormatComponent + }, + + { selector: 'website-tiny-spinner-load', component: SpinnerLoadComponent }, + + { + selector: 'website-tiny-spinner-localeable', + component: SpinnerLocaleableComponent + }, + + { + selector: 'website-tiny-spinner-max-min', + component: SpinnerMaxMinComponent + }, + + { + selector: 'website-tiny-spinner-maxlength', + component: SpinnerMaxlengthComponent + }, + + { selector: 'website-tiny-spinner-step', component: SpinnerStepComponent }, + + { + selector: 'website-tiny-spinner-stepfn', + component: SpinnerStepfnComponent + }, + + { + selector: 'website-tiny-spinner-tip-test', + component: SpinnerTipTestComponent + }, + + { selector: 'website-tiny-spinner-tip', component: SpinnerTipComponent }, + + { + selector: 'website-tiny-spinner-validation-test', + component: SpinnerValidationTestComponent + }, + + { + selector: 'website-tiny-spinner-validation', + component: SpinnerValidationComponent + }, + + { selector: 'website-tiny-steps-active', component: StepsActiveComponent }, + + { + selector: 'website-tiny-steps-adaptive-test', + component: StepsAdaptiveTestComponent + }, + + { + selector: 'website-tiny-steps-adaptive', + component: StepsAdaptiveComponent + }, + + { selector: 'website-tiny-steps-base', component: StepsBaseComponent }, + + { selector: 'website-tiny-steps-before', component: StepsBeforeComponent }, + + { + selector: 'website-tiny-steps-clickable', + component: StepsClickableComponent + }, + + { selector: 'website-tiny-steps-events', component: StepsEventsComponent }, + + { selector: 'website-tiny-steps-label', component: StepsLabelComponent }, + + { + selector: 'website-tiny-steps-maxwidth', + component: StepsMaxwidthComponent + }, + + { + selector: 'website-tiny-steps-template', + component: StepsTemplateComponent + }, + + { + selector: 'website-tiny-subtitle-basic', + component: SubtitleBasicComponent + }, + + { + selector: 'website-tiny-subtitle-before-search', + component: SubtitleBeforeSearchComponent + }, + + { selector: 'website-tiny-subtitle-dark', component: SubtitleDarkComponent }, + + { + selector: 'website-tiny-subtitle-event', + component: SubtitleEventComponent + }, + + { + selector: 'website-tiny-subtitle-idkey', + component: SubtitleIdkeyComponent + }, + + { + selector: 'website-tiny-subtitle-items', + component: SubtitleItemsComponent + }, + + { + selector: 'website-tiny-subtitle-maxwidth', + component: SubtitleMaxwidthComponent + }, + + { + selector: 'website-tiny-subtitle-panelwidth', + component: SubtitlePanelwidthComponent + }, + + { + selector: 'website-tiny-subtitle-route', + component: SubtitleRouteComponent + }, + + { + selector: 'website-tiny-subtitle-scroll-load', + component: SubtitleScrollLoadComponent + }, + + { + selector: 'website-tiny-subtitle-searchable', + component: SubtitleSearchableComponent + }, + + { + selector: 'website-tiny-subtitle-target', + component: SubtitleTargetComponent + }, + + { + selector: 'website-tiny-subtitle-tip-position', + component: SubtitleTipPositionComponent + }, + + { + selector: 'website-tiny-swiper-activeindex', + component: SwiperActiveindexComponent + }, + + { + selector: 'website-tiny-swiper-autoplay', + component: SwiperAutoplayComponent + }, + + { selector: 'website-tiny-swiper-basic', component: SwiperBasicComponent }, + + { selector: 'website-tiny-swiper-events', component: SwiperEventsComponent }, + + { + selector: 'website-tiny-swiper-indicatorposition', + component: SwiperIndicatorpositionComponent + }, + + { selector: 'website-tiny-swiper-loop', component: SwiperLoopComponent }, + + { + selector: 'website-tiny-swiper-showcardnum-test', + component: SwiperShowcardnumTestComponent + }, + + { + selector: 'website-tiny-swiper-showcardnum', + component: SwiperShowcardnumComponent + }, + + { selector: 'website-tiny-switch-basic', component: SwitchBasicComponent }, + + { selector: 'website-tiny-switch-before', component: SwitchBeforeComponent }, + + { + selector: 'website-tiny-switch-disabled', + component: SwitchDisabledComponent + }, + + { selector: 'website-tiny-switch-event', component: SwitchEventComponent }, + + { + selector: 'website-tiny-switch-explanation', + component: SwitchExplanationComponent + }, + + { selector: 'website-tiny-switch-focus', component: SwitchFocusComponent }, + + { selector: 'website-tiny-switch-id', component: SwitchIdComponent }, + + { selector: 'website-tiny-switch-load', component: SwitchLoadComponent }, + + { + selector: 'website-tiny-switch-template', + component: SwitchTemplateComponent + }, + + { selector: 'website-tiny-tab-basic', component: TabBasicComponent }, + + { + selector: 'website-tiny-tab-beforeactivechange', + component: TabBeforeactivechangeComponent + }, + + { + selector: 'website-tiny-tab-content-comp', + component: TabContentCompComponent + }, + + { + selector: 'website-tiny-tab-custom-head', + component: TabCustomHeadComponent + }, + + { selector: 'website-tiny-tab-dark', component: TabDarkComponent }, + + { + selector: 'website-tiny-tab-default-test', + component: TabDefaultTestComponent + }, + + { selector: 'website-tiny-tab-lazy-load', component: TabLazyLoadComponent }, + + { + selector: 'website-tiny-tab-level2-test', + component: TabLevel2TestComponent + }, + + { selector: 'website-tiny-tab-level2', component: TabLevel2Component }, + + { selector: 'website-tiny-tab-overflow', component: TabOverflowComponent }, + + { selector: 'website-tiny-tab-route', component: TabRouteComponent }, + + { selector: 'website-tiny-tab-scroll', component: TabScrollComponent }, + + { selector: 'website-tiny-tab-small', component: TabSmallComponent }, + + { + selector: 'website-tiny-table-actionmenu', + component: TableActionmenuComponent + }, + + { + selector: 'website-tiny-table-basic-test', + component: TableBasicTestComponent + }, + + { selector: 'website-tiny-table-basic', component: TableBasicComponent }, + + { selector: 'website-tiny-table-cell-tip', component: TableCellTipComponent }, + + { + selector: 'website-tiny-table-cellicons-colsresizable', + component: TableCelliconsColsresizableComponent + }, + + { + selector: 'website-tiny-table-checkbox-pagination-headmenu', + component: TableCheckboxPaginationHeadmenuComponent + }, + + { + selector: 'website-tiny-table-checkbox-pagination', + component: TableCheckboxPaginationComponent + }, + + { + selector: 'website-tiny-table-checkbox', + component: TableCheckboxComponent + }, + + { + selector: 'website-tiny-table-col-align', + component: TableColAlignComponent + }, + + { + selector: 'website-tiny-table-colalign-sort-resizable-test', + component: TableColalignSortResizableTestComponent + }, + + { + selector: 'website-tiny-table-cols-resizable', + component: TableColsResizableComponent + }, + + { + selector: 'website-tiny-table-cols-toggle-details', + component: TableColsToggleDetailsComponent + }, + + { + selector: 'website-tiny-table-cols-toggle-test', + component: TableColsToggleTestComponent + }, + + { + selector: 'website-tiny-table-cols-toggle', + component: TableColsToggleComponent + }, + + { + selector: 'website-tiny-table-colsresizable-basic', + component: TableColsresizableBasicComponent + }, + + { + selector: 'website-tiny-table-colsresizable-colstoggle-fixedhead', + component: TableColsresizableColstoggleFixedheadComponent + }, + + { + selector: 'website-tiny-table-colsresizable-colstoggle', + component: TableColsresizableColstoggleComponent + }, + + { + selector: 'website-tiny-table-colsresizable-loadfail', + component: TableColsresizableLoadfailComponent + }, + + { + selector: 'website-tiny-table-colsresizable-sort-headfilter', + component: TableColsresizableSortHeadfilterComponent + }, + + { + selector: 'website-tiny-table-colsresizable-sort', + component: TableColsresizableSortComponent + }, + + { + selector: 'website-tiny-table-column-fixed', + component: TableColumnFixedComponent + }, + + { + selector: 'website-tiny-table-columnfixed-checkbox', + component: TableColumnfixedCheckboxComponent + }, + + { + selector: 'website-tiny-table-columnfixed-colstoggle', + component: TableColumnfixedColstoggleComponent + }, + + { + selector: 'website-tiny-table-columnfixed-editrow', + component: TableColumnfixedEditrowComponent + }, + + { + selector: 'website-tiny-table-columnfixed-fixedhead-colsresizable-pagination', + component: TableColumnfixedFixedheadColsresizablePaginationComponent + }, + + { + selector: 'website-tiny-table-columnfixed-headfixed', + component: TableColumnfixedHeadfixedComponent + }, + + { + selector: 'website-tiny-table-columnfixed-leftmenu', + component: TableColumnfixedLeftmenuComponent + }, + + { + selector: 'website-tiny-table-columnfixed-nodata', + component: TableColumnfixedNodataComponent + }, + + { + selector: 'website-tiny-table-columnfixed-pagination', + component: TableColumnfixedPaginationComponent + }, + + { + selector: 'website-tiny-table-columnfixed-resizable', + component: TableColumnfixedResizableComponent + }, + + { + selector: 'website-tiny-table-comprehensive', + component: TableComprehensiveComponent + }, + + { + selector: 'website-tiny-table-details-closeotherdetails', + component: TableDetailsCloseotherdetailsComponent + }, + + { + selector: 'website-tiny-table-details-nesttable', + component: TableDetailsNesttableComponent + }, + + { + selector: 'website-tiny-table-details-pagination', + component: TableDetailsPaginationComponent + }, + + { selector: 'website-tiny-table-details', component: TableDetailsComponent }, + + { + selector: 'website-tiny-table-dynamic-details', + component: TableDynamicDetailsComponent + }, + + { + selector: 'website-tiny-table-editall-test', + component: TableEditallTestComponent + }, + + { selector: 'website-tiny-table-editall', component: TableEditallComponent }, + + { + selector: 'website-tiny-table-editrow-test', + component: TableEditrowTestComponent + }, + + { selector: 'website-tiny-table-editrow', component: TableEditrowComponent }, + + { + selector: 'website-tiny-table-filter-strict', + component: TableFilterStrictComponent + }, + + { selector: 'website-tiny-table-filter', component: TableFilterComponent }, + + { + selector: 'website-tiny-table-fixed-head-cols-resizable', + component: TableFixedHeadColsResizableComponent + }, + + { + selector: 'website-tiny-table-fixed-head-in-accordion', + component: TableFixedHeadInAccordionComponent + }, + + { + selector: 'website-tiny-table-fixed-head-nodata', + component: TableFixedHeadNodataComponent + }, + + { + selector: 'website-tiny-table-fixed-head-pagination-details', + component: TableFixedHeadPaginationDetailsComponent + }, + + { + selector: 'website-tiny-table-fixed-head', + component: TableFixedHeadComponent + }, + + { + selector: 'website-tiny-table-fixedhead-colsresizable-pagination-details', + component: TableFixedheadColsresizablePaginationDetailsComponent + }, + + { + selector: 'website-tiny-table-fixhead-scroll', + component: TableFixheadScrollComponent + }, + + { selector: 'website-tiny-table-group', component: TableGroupComponent }, + + { selector: 'website-tiny-table-guide', component: TableGuideComponent }, + + { + selector: 'website-tiny-table-head-filter-datetime-test', + component: TableHeadFilterDatetimeTestComponent + }, + + { + selector: 'website-tiny-table-head-filter-datetime', + component: TableHeadFilterDatetimeComponent + }, + + { + selector: 'website-tiny-table-head-filter-multi-valuekey', + component: TableHeadFilterMultiValuekeyComponent + }, + + { + selector: 'website-tiny-table-head-filter-multi', + component: TableHeadFilterMultiComponent + }, + + { + selector: 'website-tiny-table-head-filter-test', + component: TableHeadFilterTestComponent + }, + + { + selector: 'website-tiny-table-head-filter-valuekey', + component: TableHeadFilterValuekeyComponent + }, + + { + selector: 'website-tiny-table-head-filter-virtualscroll', + component: TableHeadFilterVirtualscrollComponent + }, + + { + selector: 'website-tiny-table-head-filter', + component: TableHeadFilterComponent + }, + + { + selector: 'website-tiny-table-load-fail', + component: TableLoadFailComponent + }, + + { + selector: 'website-tiny-table-nodata-simple', + component: TableNodataSimpleComponent + }, + + { + selector: 'website-tiny-table-nodata-test', + component: TableNodataTestComponent + }, + + { selector: 'website-tiny-table-nodata', component: TableNodataComponent }, + + { + selector: 'website-tiny-table-overflow-link', + component: TableOverflowLinkComponent + }, + + { + selector: 'website-tiny-table-pagi-with-filter', + component: TablePagiWithFilterComponent + }, + + { + selector: 'website-tiny-table-pagination', + component: TablePaginationComponent + }, + + { + selector: 'website-tiny-table-radio-test', + component: TableRadioTestComponent + }, + + { selector: 'website-tiny-table-radio', component: TableRadioComponent }, + + { + selector: 'website-tiny-table-row-drag2', + component: TableRowDrag2Component + }, + + { selector: 'website-tiny-table-rowspan', component: TableRowspanComponent }, + + { selector: 'website-tiny-table-search', component: TableSearchComponent }, + + { + selector: 'website-tiny-table-server-pagi-search-sort', + component: TableServerPagiSearchSortComponent + }, + + { + selector: 'website-tiny-table-server-pagi', + component: TableServerPagiComponent + }, + + { selector: 'website-tiny-table-small', component: TableSmallComponent }, + + { selector: 'website-tiny-table-soldout', component: TableSoldoutComponent }, + + { + selector: 'website-tiny-table-sort-basic', + component: TableSortBasicComponent + }, + + { + selector: 'website-tiny-table-sort-comparefn-locale', + component: TableSortComparefnLocaleComponent + }, + + { + selector: 'website-tiny-table-sort-comparefn', + component: TableSortComparefnComponent + }, + + { + selector: 'website-tiny-table-sort-details', + component: TableSortDetailsComponent + }, + + { + selector: 'website-tiny-table-sort-reset', + component: TableSortResetComponent + }, + + { + selector: 'website-tiny-table-sort-test', + component: TableSortTestComponent + }, + + { selector: 'website-tiny-table-sort', component: TableSortComponent }, + + { + selector: 'website-tiny-table-storage-config', + component: TableStorageConfigComponent + }, + + { + selector: 'website-tiny-table-storage-filter', + component: TableStorageFilterComponent + }, + + { + selector: 'website-tiny-table-storage-serve', + component: TableStorageServeComponent + }, + + { selector: 'website-tiny-table-storage', component: TableStorageComponent }, + + { + selector: 'website-tiny-table-tree-mulitiselect', + component: TableTreeMulitiselectComponent + }, + + { + selector: 'website-tiny-table-tree-unknowdeepth', + component: TableTreeUnknowdeepthComponent + }, + + { selector: 'website-tiny-table-tree', component: TableTreeComponent }, + + { + selector: 'website-tiny-table-virtualscroll-basic', + component: TableVirtualscrollBasicComponent + }, + + { + selector: 'website-tiny-table-virtualscroll-comprehensive', + component: TableVirtualscrollComprehensiveComponent + }, + + { + selector: 'website-tiny-table-virtualscroll-sizes', + component: TableVirtualscrollSizesComponent + }, + + { + selector: 'website-tiny-table-virtualscroll-tree', + component: TableVirtualscrollTreeComponent + }, + + { + selector: 'website-tiny-table-virtualscroll', + component: TableVirtualscrollComponent + }, + + { selector: 'website-tiny-tag-basic', component: TagBasicComponent }, + + { selector: 'website-tiny-tag-default', component: TagDefaultComponent }, + + { selector: 'website-tiny-tag-disabled', component: TagDisabledComponent }, + + { selector: 'website-tiny-tag-edit', component: TagEditComponent }, + + { + selector: 'website-tiny-tagsinput-basic', + component: TagsinputBasicComponent + }, + + { + selector: 'website-tiny-tagsinput-disabled', + component: TagsinputDisabledComponent + }, + + { + selector: 'website-tiny-tagsinput-events', + component: TagsinputEventsComponent + }, + + { + selector: 'website-tiny-tagsinput-labelkey', + component: TagsinputLabelkeyComponent + }, + + { + selector: 'website-tiny-tagsinput-null', + component: TagsinputNullComponent + }, + + { + selector: 'website-tiny-tagsinput-panelwidth', + component: TagsinputPanelwidthComponent + }, + + { + selector: 'website-tiny-tagsinput-separators', + component: TagsinputSeparatorsComponent + }, + + { + selector: 'website-tiny-tagsinput-reactive', + component: TagsinputReactiveComponent + }, + + { + selector: 'website-tiny-tagsinput-suggestion', + component: TagsinputSuggestionComponent + }, + + { + selector: 'website-tiny-tagsinput-template', + component: TagsinputTemplateComponent + }, + + { + selector: 'website-tiny-tagsinput-valid', + component: TagsinputValidComponent + }, + + { + selector: 'website-tiny-tagsinput-valuekey', + component: TagsinputValuekeyComponent + }, + + { + selector: 'website-tiny-tagsinput-maxlength', + component: TagsinputMaxlengthComponent + }, + + { selector: 'website-tiny-text-basic', component: TextBasicComponent }, + + { selector: 'website-tiny-text-clear', component: TextClearComponent }, + + { selector: 'website-tiny-text-disabled', component: TextDisabledComponent }, + + { selector: 'website-tiny-text-events', component: TextEventsComponent }, + + { selector: 'website-tiny-text-focus', component: TextFocusComponent }, + + { + selector: 'website-tiny-text-maskinput', + component: TextMaskinputComponent + }, + + { + selector: 'website-tiny-text-noborder-test', + component: TextNoborderTestComponent + }, + + { + selector: 'website-tiny-text-password-visible', + component: TextPasswordVisibleComponent + }, + + { selector: 'website-tiny-text-password', component: TextPasswordComponent }, + + { selector: 'website-tiny-text-reactive', component: TextReactiveComponent }, + + { selector: 'website-tiny-text-readonly', component: TextReadonlyComponent }, + + { + selector: 'website-tiny-textarea-autofocus', + component: TextareaAutofocusComponent + }, + + { + selector: 'website-tiny-textarea-disabled', + component: TextareaDisabledComponent + }, + + { + selector: 'website-tiny-textarea-maxlength', + component: TextareaMaxlengthComponent + }, + + { selector: 'website-tiny-textarea-none', component: TextareaNoneComponent }, + + { + selector: 'website-tiny-textarea-resize', + component: TextareaResizeComponent + }, + + { + selector: 'website-tiny-textarea-scroll', + component: TextareaScrollComponent + }, + + { + selector: 'website-tiny-textarea-valid', + component: TextareaValidComponent + }, + + { + selector: 'website-tiny-textarea-width', + component: TextareaWidthComponent + }, + + { + selector: 'website-tiny-time-clearicon', + component: TimeCleariconComponent + }, + + { selector: 'website-tiny-time-disabled', component: TimeDisabledComponent }, + + { selector: 'website-tiny-time-event', component: TimeEventComponent }, + + { selector: 'website-tiny-time-format', component: TimeFormatComponent }, + + { selector: 'website-tiny-time-max', component: TimeMaxComponent }, + + { selector: 'website-tiny-time-maxmin', component: TimeMaxminComponent }, + + { selector: 'website-tiny-time-min', component: TimeMinComponent }, + + { + selector: 'website-tiny-time-option-disabled', + component: TimeOptionDisabledComponent + }, + + { + selector: 'website-tiny-time-panelalign', + component: TimePanelalignComponent + }, + + { selector: 'website-tiny-time-reactive', component: TimeReactiveComponent }, + + { + selector: 'website-tiny-time-validation', + component: TimeValidationComponent + }, + + { + selector: 'website-tiny-timeline-basic', + component: TimelineBasicComponent + }, + + { selector: 'website-tiny-timeline-dark', component: TimelineDarkComponent }, + + { + selector: 'website-tiny-timeline-helptip', + component: TimelineHelptipComponent + }, + + { + selector: 'website-tiny-timeline-multi', + component: TimelineMultiComponent + }, + + { + selector: 'website-tiny-timeline-templete', + component: TimelineTempleteComponent + }, + + { selector: 'website-tiny-timeline-test', component: TimelineTestComponent }, + + { selector: 'website-tiny-timeline-type', component: TimelineTypeComponent }, + + { selector: 'website-tiny-tip-basic', component: TipBasicComponent }, + + { + selector: 'website-tiny-tip-content-comp', + component: TipContentCompComponent + }, + + { + selector: 'website-tiny-tip-content-template', + component: TipContentTemplateComponent + }, + + { selector: 'website-tiny-tip-empty', component: TipEmptyComponent }, + + { selector: 'website-tiny-tip-has-arrow', component: TipHasArrowComponent }, + + { + selector: 'website-tiny-tip-long-text-position', + component: TipLongTextPositionComponent + }, + + { selector: 'website-tiny-tip-max-width', component: TipMaxWidthComponent }, + + { + selector: 'website-tiny-tip-position-test', + component: TipPositionTestComponent + }, + + { selector: 'website-tiny-tip-position', component: TipPositionComponent }, + + { + selector: 'website-tiny-tip-service-destroy', + component: TipServiceDestroyComponent + }, + + { selector: 'website-tiny-tip-service', component: TipServiceComponent }, + + { selector: 'website-tiny-tip-trigger', component: TipTriggerComponent }, + + { + selector: 'website-tiny-tip-valid-position-test', + component: TipValidPositionTestComponent + }, + + { selector: 'website-tiny-tip-zindex', component: TipZindexComponent }, + + { + selector: 'website-tiny-transfer-basic', + component: TransferBasicComponent + }, + + { + selector: 'website-tiny-transfer-disabled', + component: TransferDisabledComponent + }, + + { + selector: 'website-tiny-transfer-event', + component: TransferEventComponent + }, + + { selector: 'website-tiny-transfer-id', component: TransferIdComponent }, + + { + selector: 'website-tiny-transfer-idkey', + component: TransferIdkeyComponent + }, + + { + selector: 'website-tiny-transfer-labelkey', + component: TransferLabelkeyComponent + }, + + { selector: 'website-tiny-transfer-lazy', component: TransferLazyComponent }, + + { selector: 'website-tiny-transfer-load', component: TransferLoadComponent }, + + { + selector: 'website-tiny-transfer-nodatatext', + component: TransferNodatatextComponent + }, + + { + selector: 'website-tiny-transfer-pagination', + component: TransferPaginationComponent + }, + + { + selector: 'website-tiny-transfer-placeholder', + component: TransferPlaceholderComponent + }, + + { + selector: 'website-tiny-transfer-searchable', + component: TransferSearchableComponent + }, + + { + selector: 'website-tiny-transfer-searchkeys', + component: TransferSearchkeysComponent + }, + + { selector: 'website-tiny-transfer-size', component: TransferSizeComponent }, + + { + selector: 'website-tiny-transfer-table', + component: TransferTableComponent + }, + + { + selector: 'website-tiny-transfer-titles', + component: TransferTitlesComponent + }, + + { + selector: 'website-tiny-tree-before-expand', + component: TreeBeforeExpandComponent + }, + + { + selector: 'website-tiny-tree-before-more', + component: TreeBeforeMoreComponent + }, + + { + selector: 'website-tiny-tree-changedbycheckbox', + component: TreeChangedbycheckboxComponent + }, + + { + selector: 'website-tiny-tree-check-relation', + component: TreeCheckRelationComponent + }, + + { selector: 'website-tiny-tree-disabled', component: TreeDisabledComponent }, + + { + selector: 'website-tiny-tree-drag-beforedrop', + component: TreeDragBeforedropComponent + }, + + { selector: 'website-tiny-tree-drag', component: TreeDragComponent }, + + { selector: 'website-tiny-tree-event', component: TreeEventComponent }, + + { selector: 'website-tiny-tree-icon', component: TreeIconComponent }, + + { selector: 'website-tiny-tree-load', component: TreeLoadComponent }, + + { selector: 'website-tiny-tree-many', component: TreeManyComponent }, + + { + selector: 'website-tiny-tree-multiselect', + component: TreeMultiselectComponent + }, + + { selector: 'website-tiny-tree-operate', component: TreeOperateComponent }, + + { + selector: 'website-tiny-tree-parentcheckable', + component: TreeParentcheckableComponent + }, + + { + selector: 'website-tiny-tree-radioselect', + component: TreeRadioselectComponent + }, + + { selector: 'website-tiny-tree-search', component: TreeSearchComponent }, + + { + selector: 'website-tiny-tree-shortcutkey', + component: TreeShortcutkeyComponent + }, + + { selector: 'website-tiny-tree-small', component: TreeSmallComponent }, + + { selector: 'website-tiny-tree-template', component: TreeTemplateComponent }, + + { selector: 'website-tiny-tree-util', component: TreeUtilComponent }, + + { + selector: 'website-tiny-tree-virtualscroll-drag', + component: TreeVirtualscrollDragComponent + }, + + { + selector: 'website-tiny-tree-virtualscroll-small', + component: TreeVirtualscrollSmallComponent + }, + + { + selector: 'website-tiny-tree-virtualscroll', + component: TreeVirtualscrollComponent + }, + + { + selector: 'website-tiny-treeselect-basic', + component: TreeselectBasicComponent + }, + + { + selector: 'website-tiny-treeselect-before-expand', + component: TreeselectBeforeExpandComponent + }, + + { + selector: 'website-tiny-treeselect-before-more', + component: TreeselectBeforeMoreComponent + }, + + { + selector: 'website-tiny-treeselect-clearable', + component: TreeselectClearableComponent + }, + + { + selector: 'website-tiny-treeselect-disabled', + component: TreeselectDisabledComponent + }, + + { + selector: 'website-tiny-treeselect-dropmaxheight', + component: TreeselectDropmaxheightComponent + }, + + { + selector: 'website-tiny-treeselect-event', + component: TreeselectEventComponent + }, + + { + selector: 'website-tiny-treeselect-focus', + component: TreeselectFocusComponent + }, + + { + selector: 'website-tiny-treeselect-labelkey', + component: TreeselectLabelkeyComponent + }, + + { + selector: 'website-tiny-treeselect-lazyload', + component: TreeselectLazyloadComponent + }, + + { + selector: 'website-tiny-treeselect-load', + component: TreeselectLoadComponent + }, + + { + selector: 'website-tiny-treeselect-maxline', + component: TreeselectMaxlineComponent + }, + + { + selector: 'website-tiny-treeselect-modal', + component: TreeselectModalComponent + }, + + { + selector: 'website-tiny-treeselect-multi', + component: TreeselectMultiComponent + }, + + { + selector: 'website-tiny-treeselect-nodata', + component: TreeselectNodataComponent + }, + + { + selector: 'website-tiny-treeselect-options-change', + component: TreeselectOptionsChangeComponent + }, + + { + selector: 'website-tiny-treeselect-panelwidth', + component: TreeselectPanelwidthComponent + }, + + { + selector: 'website-tiny-treeselect-search', + component: TreeselectSearchComponent + }, + + { + selector: 'website-tiny-treeselect-selectall', + component: TreeselectSelectallComponent + }, + + { + selector: 'website-tiny-treeselect-validation', + component: TreeselectValidationComponent + }, + + { + selector: 'website-tiny-upload-auto-upload', + component: UploadAutoUploadComponent + }, + + { selector: 'website-tiny-upload-basic', component: UploadBasicComponent }, + + { + selector: 'website-tiny-upload-batch-send', + component: UploadBatchSendComponent + }, + + { + selector: 'website-tiny-upload-beforeremove', + component: UploadBeforeremoveComponent + }, + + { + selector: 'website-tiny-upload-button-test', + component: UploadButtonTestComponent + }, + + { selector: 'website-tiny-upload-button', component: UploadButtonComponent }, + + { + selector: 'website-tiny-upload-case-test', + component: UploadCaseTestComponent + }, + + { + selector: 'website-tiny-upload-changes', + component: UploadChangesComponent + }, + + { + selector: 'website-tiny-upload-chunksize', + component: UploadChunksizeComponent + }, + + { selector: 'website-tiny-upload-custom', component: UploadCustomComponent }, + + { selector: 'website-tiny-upload-event', component: UploadEventComponent }, + + { selector: 'website-tiny-upload-filter', component: UploadFilterComponent }, + + { + selector: 'website-tiny-upload-form-data', + component: UploadFormDataComponent + }, + + { + selector: 'website-tiny-upload-initfiles-test', + component: UploadInitfilesTestComponent + }, + + { + selector: 'website-tiny-upload-input-field-test', + component: UploadInputFieldTestComponent + }, + + { selector: 'website-tiny-upload-props', component: UploadPropsComponent }, + + { + selector: 'website-tiny-upload-service-test', + component: UploadServiceTestComponent + }, + + { + selector: 'website-tiny-upload-service', + component: UploadServiceComponent + }, + + { selector: 'website-tiny-upload-single', component: UploadSingleComponent }, + + { + selector: 'website-tiny-uploadimage-auto-upload', + component: UploadimageAutoUploadComponent + }, + + { + selector: 'website-tiny-uploadimage-basic', + component: UploadimageBasicComponent + }, + + { + selector: 'website-tiny-uploadimage-changes', + component: UploadimageChangesComponent + }, + + { + selector: 'website-tiny-uploadimage-deletable', + component: UploadimageDeletableComponent + }, + + { + selector: 'website-tiny-uploadimage-disabled', + component: UploadimageDisabledComponent + }, + + { + selector: 'website-tiny-uploadimage-drag', + component: UploadimageDragComponent + }, + + { + selector: 'website-tiny-uploadimage-event', + component: UploadimageEventComponent + }, + + { + selector: 'website-tiny-uploadimage-filter', + component: UploadimageFilterComponent + }, + + { + selector: 'website-tiny-uploadimage-initfiles', + component: UploadimageInitfilesComponent + }, + + { + selector: 'website-tiny-uploadimage-maxcount', + component: UploadimageMaxcountComponent + }, + + { + selector: 'website-tiny-uploadimage-template', + component: UploadimageTemplateComponent + }, + + { selector: 'website-tiny-browser-usage', component: BrowserUsageComponent }, + + { selector: 'website-tiny-keymap-usage', component: KeymapUsageComponent }, + + { selector: 'website-tiny-log-usage', component: LogUsageComponent }, + + { selector: 'website-tiny-theme-basic', component: ThemeBasicComponent }, + + { + selector: 'website-tiny-validation-async-check-test', + component: ValidationAsyncCheckTestComponent + }, + + { + selector: 'website-tiny-validation-async-check', + component: ValidationAsyncCheckComponent + }, + + { + selector: 'website-tiny-validation-basic-control', + component: ValidationBasicControlComponent + }, + + { + selector: 'website-tiny-validation-basic-directive', + component: ValidationBasicDirectiveComponent + }, + + { + selector: 'website-tiny-validation-blur-check', + component: ValidationBlurCheckComponent + }, + + { + selector: 'website-tiny-validation-error-msg', + component: ValidationErrorMsgComponent + }, + + { + selector: 'website-tiny-validation-form-group-config', + component: ValidationFormGroupConfigComponent + }, + + { + selector: 'website-tiny-validation-form-group-test', + component: ValidationFormGroupTestComponent + }, + + { + selector: 'website-tiny-validation-form-group', + component: ValidationFormGroupComponent + }, + + { + selector: 'website-tiny-validation-param-change', + component: ValidationParamChangeComponent + }, + + { + selector: 'website-tiny-validation-pwd-check', + component: ValidationPwdCheckComponent + }, + + { + selector: 'website-tiny-validation-rules-custom-directive', + component: ValidationRulesCustomDirectiveComponent + }, + + { + selector: 'website-tiny-validation-rules-custom', + component: ValidationRulesCustomComponent + }, + + { + selector: 'website-tiny-validation-rules-test', + component: ValidationRulesTestComponent + }, + + { + selector: 'website-tiny-validation-template-form-nested', + component: ValidationTemplateFormNestedComponent + }, + + { + selector: 'website-tiny-validation-tip', + component: ValidationTipComponent + }, + + { + selector: 'website-tiny-validation-tiscroll', + component: ValidationTiscrollComponent + } +]; + +@NgModule({ + imports: [ + DemoModules.allModules, + HttpClientModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule // 因为modal,message,collapse,accordion组件使用了angular动画,所以需要引入此模块 + ], + declarations: [AppComponent], + bootstrap: [AppComponent] +}) +export class AppModule { + constructor(private injector: Injector) { + // 国际化功能开启,测试其它语种的显示,可打开这段注释 + // 设置当前语言,默认为中文 + // TiLocale.setLocale(TiLocale.EN_US); // 设置国际化语种 + for (const item of WCS) { + if (!customElements.get(item.selector)) { + const el: any = createCustomElement(item.component, { + injector: this.injector + }); + customElements.define(item.selector, el); + } + } + } + + ngDoBootstrap(): void {} +} diff --git a/src/ng/demo/src/app/DemoModules.ts b/src/ng/demo/src/app/DemoModules.ts new file mode 100644 index 0000000..95ae083 --- /dev/null +++ b/src/ng/demo/src/app/DemoModules.ts @@ -0,0 +1,204 @@ +import { NgModule } from '@angular/core'; +// 以下按照字母排序 + +import { AccordionTestModule } from '../../../../accordion/demo/src/app/accordion/AccordionTestModule'; +import { ActionmenuTestModule } from '../../../../actionmenu/demo/src/app/actionmenu/ActionmenuTestModule'; +import { AlertTestModule } from '../../../../alert/demo/src/app/alert/AlertTestModule'; +import { AnchorTestModule } from '../../../../anchor/demo/src/app/anchor/AnchorTestModule'; +import { AutocompleteTestModule } from '../../../../autocomplete/demo/src/app/autocomplete/AutocompleteTestModule'; +import { AvatarTestModule } from '../../../../avatar/demo/src/app/avatar/AvatarTestModule'; +import { ButtonTestModule } from '../../../../button/demo/src/app/button/ButtonTestModule'; +import { ButtonGroupTestModule } from '../../../../buttongroup/demo/src/app/buttongroup/ButtonGroupTestModule'; +import { ButtonselectTestModule } from '../../../../buttonselect/demo/src/app/buttonselect/ButtonselectTestModule'; +import { CardTestModule } from '../../../../card/demo/src/app/card/CardTestModule'; +import { CascaderTestModule } from '../../../../cascader/demo/src/app/cascader/CascaderTestModule'; +import { CheckboxTestModule } from '../../../../checkbox/demo/src/app/checkbox/CheckboxTestModule'; +import { CollapseTestModule } from '../../../../collapse/demo/src/app/collapse/CollapseTestModule'; +import { CollapseboxTestModule } from '../../../../collapsebox/demo/src/app/collapsebox/CollapseboxTestModule'; +import { CollapsebuttonTestModule } from '../../../../collapsebutton/demo/src/app/collapsebutton/CollapsebuttonTestModule'; +import { CollapsetextTestModule } from '../../../../collapsetext/demo/src/app/collapsetext/CollapsetextTestModule'; +import { CopyTestModule } from '../../../../copy/demo/src/app/copy/CopyTestModule'; +import { CrumbTestModule } from '../../../../crumb/demo/src/app/crumb/CrumbTestModule'; +import { DateTestModule } from '../../../../date/demo/src/app/date/DateTestModule'; +import { DateDominatorTestModule } from '../../../../datedominator/demo/src/app/datedominator/DateDominatorTestModule'; +import { DateEditTestModule } from '../../../../dateedit/demo/src/app/dateedit/DateEditTestModule'; +import { DateRangeTestModule } from '../../../../daterange/demo/src/app/daterange/DateRangeTestModule'; +import { DatetimeTestModule } from '../../../../datetime/demo/src/app/datetime/DatetimeTestModule'; +import { DatetimeRangeTestModule } from '../../../../datetimerange/demo/src/app/datetimerange/DatetimeRangeTestModule'; +import { DominatorTestModule } from '../../../../dominator/demo/src/app/dominator/DominatorTestModule'; +import { DragTestModule } from '../../../../drag/demo/src/app/drag/DragTestModule'; +import { DropTestModule } from '../../../../drop/demo/src/app/drop/DropTestModule'; +import { DroplistTestModule } from '../../../../droplist/demo/src/app/droplist/DroplistTestModule'; +import { GuidesTestModule } from '../../../../guides/demo/src/app/guides/GuidesTestModule'; +import { FormfieldTestModule } from '../../../../formfield/demo/src/app/formfield/FormfieldTestModule'; +import { FoldtextTestModule } from '../../../../foldtext/demo/src/app/foldtext/FoldtextTestModule'; +import { GuidestepsTestModule } from '../../../../guidesteps/demo/src/app/guidesteps/GuidestepsTestModule'; +import { HalfmodalTestModule } from '../../../../halfmodal/demo/src/app/halfmodal/HalfmodalTestModule'; +import { IconTestModule } from '../../../../icon/demo/src/app/icon/IconTestModule'; +import { IconactionTestModule } from '../../../../iconaction/demo/src/app/iconaction/IconactionTestModule'; +import { ImagepreviewTestModule } from '../../../../imagepreview/demo/src/app/imagepreview/ImagepreviewTestModule'; +import { InputnumberTestModule } from '../../../../inputnumber/demo/src/app/inputnumber/InputnumberTestModule'; +import { IntroTestModule } from '../../../../intro/demo/src/app/intro/IntroTestModule'; +import { IpTestModule } from '../../../../ip/demo/src/app/ip/IpTestModule'; +import { IpsectionTestModule } from '../../../../ipsection/demo/src/app/ipsection/IpsectionTestModule'; +import { LabeleditorTestModule } from '../../../../labeleditor/demo/src/app/labeleditor/LabeleditorTestModule'; +import { LayoutTestModule } from '../../../../layout/demo/src/app/layout/LayoutTestModule'; +import { LeftmenuTestModule } from '../../../../leftmenu/demo/src/app/leftmenu/LeftmenuTestModule'; +import { ListTestModule } from '../../../../list/demo/src/app/list/ListTestModule'; +import { LoadingTestModule } from '../../../../loading/demo/src/app/loading/LoadingTestModule'; +import { LocaleTestModule } from '../../../../locale/demo/src/app/locale/LocaleTestModule'; +import { LinkbuttonTestModule } from '../../../../linkbutton/demo/src/app/linkbutton/LinkbuttonTestModule'; +import { MenuTestModule } from '../../../../menu/demo/src/app/menu/MenuTestModule'; +import { MessageTestModule } from '../../../../message/demo/src/app/message/MessageTestModule'; +import { ModalTestModule } from '../../../../modal/demo/src/app/modal/ModalTestModule'; +import { NavTestModule } from '../../../../nav/demo/src/app/nav/NavTestModule'; +import { NotificationTestModule } from '../../../../notification/demo/src/app/notification/NotificationTestModule'; +import { OverflowTestModule } from '../../../../overflow/demo/src/app/overflow/OverflowTestModule'; +import { PhonenumberTestModule } from '../../../../phonenumber/demo/src/app/phonenumber/PhonenumberTestModule'; +import { PaginationTestModule } from '../../../../pagination/demo/src/app/pagination/PaginationTestModule'; +import { PathTestModule } from '../../../../path/demo/src/app/path/PathTestModule'; +import { PopconfirmTestModule } from '../../../../popconfirm/demo/src/app/popconfirm/PopconfirmTestModule'; +import { ProgressbarTestModule } from '../../../../progressbar/demo/src/app/progressbar/ProgressbarTestModule'; +import { ProgresspieTestModule } from '../../../../progresspie/demo/src/app/progresspie/ProgresspieTestModule'; +import { ProductpreviewTestModule } from '../../../../productpreview/demo/src/app/productpreview/ProductpreviewTestModule'; +import { RadioTestModule } from '../../../../radio/demo/src/app/radio/RadioTestModule'; +import { RateTestModule } from '../../../../rate/demo/src/app/rate/RateTestModule'; +import { RightsTestModule } from '../../../../rights/demo/src/app/rights/RightsTestModule'; +import { ScoreTestModule } from '../../../../score/demo/src/app/score/ScoreTestModule'; +import { SearchboxTestModule } from '../../../../searchbox/demo/src/app/searchbox/SearchboxTestModule'; +import { SelectTestModule } from '../../../../select/demo/src/app/select/SelectTestModule'; +import { SelectgroupTestModule } from '../../../../selectgroup/demo/src/app/selectgroup/SelectgroupTestModule'; +import { SkeletonTestModule } from '../../../../skeleton/demo/src/app/skeleton/SkeletonTestModule'; +import { SliderTestModule } from '../../../../slider/demo/src/app/slider/SliderTestModule'; +import { SpinnerTestModule } from '../../../../spinner/demo/src/app/spinner/SpinnerTestModule'; +import { StepsTestModule } from '../../../../steps/demo/src/app/steps/StepsTestModule'; +import { SubtitleTestModule } from '../../../../subtitle/demo/src/app/subtitle/SubtitleTestModule'; +import { SwiperTestModule } from '../../../../swiper/demo/src/app/swiper/SwiperTestModule'; +import { SwitchTestModule } from '../../../../switch/demo/src/app/switch/SwitchTestModule'; +import { TabTestModule } from '../../../../tab/demo/src/app/tab/TabTestModule'; +import { TableTestModule } from '../../../../table/demo/src/app/table/TableTestModule'; +import { TagTestModule } from '../../../../tag/demo/src/app/tag/TagTestModule'; +import { TagsInputTestModule } from '../../../../tagsinput/demo/src/app/tagsinput/TagsInputTestModule'; +import { TextTestModule } from '../../../../text/demo/src/app/text/TextTestModule'; +import { TextareaTestModule } from '../../../../textarea/demo/src/app/textarea/TextareaTestModule'; +import { TimeTestModule } from '../../../../time/demo/src/app/time/TimeTestModule'; +import { TimelineTestModule } from '../../../../timeline/demo/src/app/timeline/TimelineTestModule'; +import { TipTestModule } from '../../../../tip/demo/src/app/tip/TipTestModule'; +import { TransferTestModule } from '../../../../transfer/demo/src/app/transfer/TransferTestModule'; +import { TreeTestModule } from '../../../../tree/demo/src/app/tree/TreeTestModule'; +import { TreeselectTestModule } from '../../../../treeselect/demo/src/app/treeselect/TreeselectTestModule'; +import { UploadTestModule } from '../../../../upload/demo/src/app/upload/UploadTestModule'; +import { UploadimageTestModule } from '../../../../upload/demo/src/app/uploadimage/UploadimageTestModule'; +import { BrowserTestModule } from '../../../../utils/demo/src/app/browser/BrowserTestModule'; +import { KeymapTestModule } from '../../../../utils/demo/src/app/keymap/KeymapTestModule'; +import { LogTestModule } from '../../../../utils/demo/src/app/log/LogTestModule'; +import { ThemeTestModule } from '../../../../utils/demo/src/app/theme/ThemeTestModule'; +import { ValidationTestModule } from '../../../../validation/demo/src/app/validation/ValidationTestModule'; +import { ZoomTestModule } from '../../../../zoom/demo/src/app/zoom/ZoomTestModule'; + +@NgModule({ + imports: DemoModules.allModules, + exports: DemoModules.allModules, + declarations: [] +}) +export class DemoModules { + // 增加新的测试模块,需配置此数组。 + static allModules: Array = [ + AccordionTestModule, + ActionmenuTestModule, + AlertTestModule, + AnchorTestModule, + AutocompleteTestModule, + AvatarTestModule, + ButtonTestModule, + ButtonGroupTestModule, + ButtonselectTestModule, + CardTestModule, + CascaderTestModule, + CheckboxTestModule, + CollapseTestModule, + CollapseboxTestModule, + CollapsebuttonTestModule, + CollapsetextTestModule, + CopyTestModule, + CrumbTestModule, + DateTestModule, + DateDominatorTestModule, + DateEditTestModule, + DateRangeTestModule, + DatetimeTestModule, + DatetimeRangeTestModule, + DominatorTestModule, + DragTestModule, + DropTestModule, + DroplistTestModule, + GuidesTestModule, + FormfieldTestModule, + FoldtextTestModule, + GuidestepsTestModule, + HalfmodalTestModule, + IconTestModule, + IconactionTestModule, + ImagepreviewTestModule, + InputnumberTestModule, + IntroTestModule, + IpTestModule, + IpsectionTestModule, + LabeleditorTestModule, + LayoutTestModule, + LeftmenuTestModule, + ListTestModule, + LoadingTestModule, + LocaleTestModule, + LinkbuttonTestModule, + MenuTestModule, + MessageTestModule, + ModalTestModule, + NavTestModule, + NotificationTestModule, + OverflowTestModule, + PaginationTestModule, + PathTestModule, + PhonenumberTestModule, + PopconfirmTestModule, + ProgressbarTestModule, + ProgresspieTestModule, + ProductpreviewTestModule, + RadioTestModule, + RateTestModule, + RightsTestModule, + ScoreTestModule, + SearchboxTestModule, + SelectTestModule, + SelectgroupTestModule, + SkeletonTestModule, + SliderTestModule, + SpinnerTestModule, + StepsTestModule, + SubtitleTestModule, + SwiperTestModule, + SwitchTestModule, + TabTestModule, + TableTestModule, + TagTestModule, + TagsInputTestModule, + TextTestModule, + TextareaTestModule, + TimeTestModule, + TimelineTestModule, + TipTestModule, + TransferTestModule, + TreeTestModule, + TreeselectTestModule, + UploadTestModule, + UploadimageTestModule, + // 一下四个顺序特殊 Utils Test Module + BrowserTestModule, + KeymapTestModule, + LogTestModule, + ThemeTestModule, + ValidationTestModule, + ZoomTestModule + // 以上按照字母排序,禁止在末尾添加 + ]; +} diff --git a/src/ng/demo/src/app/IndexComponent.ts b/src/ng/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..517186d --- /dev/null +++ b/src/ng/demo/src/app/IndexComponent.ts @@ -0,0 +1,58 @@ +import { Component, Inject } from '@angular/core'; +import { DemoModules } from './DemoModules'; +import { DOCUMENT } from '@angular/common'; + +@Component({ + template: ` +

    TinyNG All Demo Index

    + + + + + + + + + +
    NameDemo
    {{ module.ROUTES[0]?.path.split('/')[0] }} + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + allModules: Array = DemoModules.allModules; + + constructor(@Inject(DOCUMENT) private document) { + // 设置网页标题 + this.document.getElementsByTagName('title')[0].innerText = 'ui-ng all demo Index'; + } + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/ng/demo/src/app/app.html b/src/ng/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/ng/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/ng/demo/src/assets/browser/chrome.PNG b/src/ng/demo/src/assets/browser/chrome.PNG new file mode 100644 index 0000000000000000000000000000000000000000..c126db4d537bab6d98fa826e0c4b5e334223e80c GIT binary patch literal 1994 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 literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/browser/firefox.PNG b/src/ng/demo/src/assets/browser/firefox.PNG new file mode 100644 index 0000000000000000000000000000000000000000..c3770fc0fd95186523861acc3af5b4185ad7c964 GIT binary patch literal 2185 zcmV;42zK|0P)2(*XP~Y(4XP#xv}lixY)bn>*U4e&DrVZ(c}wm z|29Ye+(rLDK>ILJ{4!1d*+>85L;pNN|J6wUL_XaSU;p4j|2RbdRWtZ4SNSef{xeGA z5NiIyOaI(H|4BUmOgR5rF#ok9d{NB`zU|DGARJn33Gu<9RQ|IkbSU@clt z$M-8;?jv3HDp~(&JO9-;|70rvawPw+9kM~|xjf|S7-ajTQ2nh;{?A1J)jt1FH~(uY z|DqfJfg1m*8UK|SY+>kTUf*g^)NVbLgP6f>8RI zOaGxh|EV|s%Q64DBmcb^n@RAdM(&77>7_c=b~M!Gle+4Qr}D*$2)st{`>py?&G_zuCAvJwPk)t+zZ&000FTNkllJNs4NCP&0uIVd^)tA-{U0e zgyt>~OkK!gF^Jq4k*()V;++Oz*a*pD;$ZvbQYnkYBEvkH>_*;?sdsBW-+#_@6UUxd z?|=#kvE8Skp+N|SOr+k)r`a>A#d*$2Fge(N_U!o;YUp4ihoz?<_$;p%+DWNsoJyst zTxxc%kJCIoVMl^vc6NXY*8Rz`u(06Z;G@auT%jF}s%*cy>`c>zu3k268WN69qPz=_ zcZ7va#XB0Dd_ZW26B>JpAr=Z z>)*l=MBzFcn@Ok>r;`)k#BjN02$=cEi1*RO0T+$OieFQAdL0lO0KIt!dwq&wa?Q+W zW@c2GtRD`lt#C0X!O_uV`g9;`L^a9l7Ayc0{QCO?8q=Q%OAv6w&(y(E03yyw#Nll5 z@ZiLope0-S1}%VW68k=2u>Ad*XbIKL4ZqpWhK2$wt3ysA5fG1@RnTT(eJ`(|prCEr z3U_c>EUBL#BJjfThZwHgC^%?GPHt|l+^)xAFE1|;g^B6)QkK8O&yUFj;D#=?kG-z0 zp+F$8&W{ID<#C|L$&-%`2L=Xud2QWV7@8~;3S-1bu*g*MD0E3|cMw3Vtl~ITHBTQN zIkJ5D^0jN%0@)fGy5p-*E*Fa>5++MZqYebH+rZu49zT{KuBv8dQc_5WxA*$>U;+yY zLPNvvm$LwPAK&dxJu%<%R`rl4SZXgGjX0HP&wc?{RC zBY<6626O|qJrG@ox)c{AfB*^!*}naF_^H~QoSgFN=-AkpSSA-ebXN;Hg>C=@M1ES) zhRJhRteA;RNJ!H0@bH)Lay+VU)W*if!uwDI(EZAI9SWU}&c(LA_AC_y5HQY3Njp#N zyjGRR`EjFsF(mOjh@tjQ)X^ab`)8VKYQ*Hpb0KGEXBQWjlsZvf-u$x4%F4yHV#yvH zyt38Qge8a==-zTQHH`p*g7Kp3PTASn^8*4ZDk{s%#e;C5L9)=)BM2RYt7&R#L`1}f z4XG(b7ZdXH^WmEdB;fAtXyOaMcivbnJ=lT{Mq!tvy?`yK=S9~&`=Uikl9`#=1#3)|q;EcPA`qp?(xEJY==e3bE6M%R(o8k1sf{2eOu&tvo+hBI zEKqoKoZMHGmiB4>(|ev2#h+pU1|5w+a6CK;-Tj_Ip?HTiuUiHw%4pUU3kzZoAfyPV zRjg2az*^RoM2Z^6pEZRrfRMtW)}~Fx#h2j$TBeaf(jNz87VH583NdJtk&(IiWfiPd z-S(06qkxzrgPH&p!#JOBd#=?#3UOCB8HZ-fA-)mBx3#t1jkS@+ULatIfZ9UmhG1={ zdNzXu2sIDy%ErOi-y5>)?0G<_jC~tb)v)$7T0>P;wd>%a*#GDkDfuT43vZV#00000 LNkvXXu0mjfsXdH7 literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/browser/safari.PNG b/src/ng/demo/src/assets/browser/safari.PNG new file mode 100644 index 0000000000000000000000000000000000000000..e4b424308a8f7b56f8fe12c1a3bdd92144adcd9f GIT binary patch literal 2402 zcmV-o37z(dP)ut%<>BDq+9y`=iJ-d!NSA(`uODJ zFV5y!8ve?%UPQ!^6WOto|3b`31n`1CiSzZqNav!~ucB^YQNjn%v#n*aB$P19i{= zc*+5az;(O%k;V28lj|~rFewRquvI3-8FmI0jSpjiPZyo z)c~T=GJwqho5>oQ|6Q{B7OwXqe(MBK-UWl(19R3EXw?Cl(*csu0gBB(d(F|!$juTF zF`N9F#PVI6>kXdj42b3mkK_iP;tPY}2WZ<&eAWSf&`yER0fET@lD+|%z5Y>7O|bj9 z)cQ}H_YZ~f5w!3Ny6gse>j{?RbEM$`xZMSc+ya5tcbU**l+MV=$!wCx2e0Q%n&tz! z|uHQ_P-W6}$UX<9iudV+{MB57pi@N+ruKXdC{9c;-7^M1Pr1TY?^boD@ z3ZLaBisOZ^;2(qF8+hCrVcaNc*#=(O0kGC-PK&X!R(B^KYl|0LShtljl2;-yyZqj;hRBip&9U$O>|{3s|iPk*biIop6qu8E}#c zO^!8Rgw11ZQcY=pGeg}gE9NI7v>P78=KemP{G-b9E1T{zf$f{hk2fSa-enxwrs|w~BMC34@^@h@O>tj$(a{wQ_jtS6JgKDfJ%~ch25;x!yXt)gheE zOP#hghnGiykHUO=vq4##DKFzKEa56CAoai)0000WbW%=J06_kOnmpxSM+oQJx^N%y z=g7rt@aEgbyqlZVwS$8ngcvpH000I3Nkl#E>#b0(x&d)rL8eFwO)GY za_O5P+RgUZ|Ka63{N9)EeUJ5jQ_Ze#<#C(PCN8hFo?ZQKT+zluQ_0<@AiK#Fit@@T z{*KwLO$%BW*Eltq3^;IDyMQ*8voFUDZQP^-3d0Z|=ff~KAXou8k|?*V;Sycdl5)7i z4lf_#Ll6u@5GcHku-lPB>#F|HmCadqR7dLg3I!j>U>L*c3L+GvD? zAO-2f@Ql#u6lQSDNh%O744&3kT?w*rML!nSYv~aX72cv}UN~Lp>?xFsHRg={k znCD#iUTT@dG!cby99xJ4j5H8J0>n(FhnYhM+L@Mv(*)9S+Tk+Y>&bMbXF|7w4Q=CD z>5>*&=nX?_7tXfOqha3?6HhE^j0O)bQLlbeiXo!$;P3J>liz#!GH>4hdxEZ^X?jk7 zsjEMYPsocO?>~AuqE@T#JyR%T5A2U$+@DNH(@`ip!*9$c;d zc(n_*PABLyc1^f2J+~sP-d~KznNmrwXJsZqUzUU2i0yqC84+}oLI(OQe2uvx3AGQ9 zbRZg~HF}mcn@p3(<6y^P`@16m*r3no?Q}Gqrb(o?t!+?La#SJ?gU`pB&F0rld-Yq> zp43g6Oft|l;dmiDD?-{~S4GoIG(wlOKGtISIw8j!vHka6uNQ2Vi9f{IULo$&sOBT!bpkp~;+2V+xBe{pGP$zrjvK5aQ2srdO55*0U3 zQ$OiUF>9PbyNvA}ZRf_CfSk)&EH0ncU|`mmgp)*jF4A}qJ4*zN1a*v^A8va_M@O4} z4s4Ihr7dS1@kk(XhN11PB#@A&yubF)c67L)ETit{)Po%Qh6p=R)@R;QNh)vqn_j~sk!m+B8x7I^Mp0ENigp5|+S;cU? z`{woDG%U4YRkx9-TZt^kWeIDYFuJRlO^QNqy3TE}1SZUH1h~uskl_Lw^B_yRp!5Ba z8-o%lZo?3C9zYSyhD-4t=&OIFLp&e_a6lN*UUA1i?*#3)`cb5(ySH7q6Z{uH0mr^) Up2I=xw*UYD07*qoM6N<$f(qu@l>h($ literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/food/cake.png b/src/ng/demo/src/assets/food/cake.png new file mode 100644 index 0000000000000000000000000000000000000000..f4fe737be989057ef38b733375720273a250b2e7 GIT binary patch literal 4044 zcmV;-4>RzIP)HE^-_|f9{(BbaesP@m_0RaK>?dbZ^;_~d~`S$bd*{Au_<$eK_VVrv3JLtw

    7{1q1~8`1SGMs}~g(_}1t5^zixH?EBg36A=>q-S7O{?EBW| z^Wm-N)};0C>G;{{{rvj-;PC$0>Khsw|Ni~``}OMBrZ_Y-`u6bn^X+$KZT;5h_vEh` z77_mB_V&`_^W?8qNlzvtBq1Ib92yiA6AtRi+WYps`tG;-=&|H!QUCw{`}p$m@b3Ql zz_YQg?%%7ftEqT)b6rnWL_E`C;ySAOF>er>@)T73_kDP^% zmw<|jhlY%Kg07-{eQR@^l5k{JUFmF7Eh-@N^YQW2<>k}j?9t)c*w@p*&egie&A7$B zthn;)w!){h?c%PGeu%WGf`V~*a&T^LZE9<0W{rboSXNYWX;DBvJVrq=H7zmh+34Qe z*U-+&|Ng_m!NAS2zsRh*-_fD4m7uASoz=&goSK-GhK`SoioCIjgMod9fns=YR#Q?? zT~|XpIygloA1NXl_2KaE?dk02;oi;J$j8P1_PyE9pM`^gqnLJ=jcjmUWq^5Ie05pe zYgB7yO~qhLPfbfiIX*l$D)sgB=h@`t%G%e+)4j2|<=v{WnWU+gp`o0doQ#)dWnx@f zSV~Ao=i{`%x{#KWkgr!oOhz{C;Oxo7!0zU>wy3hKpsA>vcZhyr{dr!oRz*)pH$pux zST83#ARE`ysLso$vsgx0g>ZH^K`5nc(cZG zX_OyI9)a-S2N0`WW_-R#**G%`0tSinOb2lmw*2HOe_mB`y;4xqv`{FdX(`9sgzeb` zL5e1;bpNp))g){d_Heq*;0XqUS}3--572&TAws|pXXAiJD>M>`r&_%2J#2U5(l3&` zYol%CDwXWm2+($Z`+Gisu-cCtPhKXstwTy19*Gfdz{Iro=)^Olm1b9p+p!ZUAc)$D98 zf1{DE3gR$qF%bES7W02;B8=mB+&vG^?HrE}UAt}UyzTCwdDB>Jv@whQViLAy+o&|l zl4ZEWZ>44o1k21Hw2-1QzaU~sgcT`DMi5115ClO%U+6m&L_tCQ)(<_KMSsBQdxJfD zICwpu_lM8>j>|i<*?RZKlhdbZ;Xk>FgUgn@aE9jTvJI;Wi;MGDExTmlxL=>Ye}D3u z+S8;BRzeOL(xvF ztziH5(p~l~X46vO87439&?XwOZ&7JZ(ESDc0e?WtFc;{J3SJNo7fu05Vmj%@ z03z7f-r*`w*|-Z2?=Kdeb})ad!xDWrJ~^&H_P=-@0Kg-8FjN=z&?R=yxuUIAUV+%lnpcYR1ke@D;p@^fuArL?1uuWV$vnX_Cnfc&; zu5&nsiAUtwfAEas{)VF}{q+ZqdU&4Y8&6nL2_+Fb+F!tJ-(}BP40zto+Z;EfVo3E5 zPKop&v6H^RE)4>NAkQPU>T%s%1H#C2FJiTB+OWZ7e8^VpyeV)s%@WLx)Pi_TgQ6&U zDqNvymC^yV0jX1=(2$Pkbqzijx8v;=#&D>vG+*B#)Si<7uLipO3zb6_r$ev1kY}`` zU^49?FuN&Mc6SE}5IsDsVS4SKj z)s0y=&l4gNRs^k*7H^+$c{k?g8ikv$Uv`W&0DzR9RBhM;0dW#)a#dSU1VB=TLuI~s z75tq59^$hzk+6sxtIHi1=^dUiR(Qz`p&}fk)vCi|?`Y*3t_sprbE<^T1Wc^wS>2dS z{?REsovI?pInZzQ?pc*<2yQa(dG9(EfPkkwMb&tq&-Awu7C@BcK~3o`hz==;F6l)q zz@%zAjsZ~L6xF{iGz4cD^Npg(WFSEeU66aeqHL7H*}sBn+j#ACjKZ-*Se}0TGAj|R z4@MmO48qss?X!g>CjvYOVjjSFbD!XA$qpC45U;E%CH%DgV�LnAv~{=RkK`^9!Hw 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 Z{Q!wKTOZyy_a^`V002ovPDHLkV1jk*mAe1{ literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/food/cola.png b/src/ng/demo/src/assets/food/cola.png new file mode 100644 index 0000000000000000000000000000000000000000..efcc14a34aa5cce474b45211db0d45cb0b696a49 GIT binary patch literal 4115 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 Ri-iCH002ovPDHLkV1l|G5Gnuw literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/food/fried_chicken.png b/src/ng/demo/src/assets/food/fried_chicken.png new file mode 100644 index 0000000000000000000000000000000000000000..cbcdf42b5e789f56ba6d2c90162d07cd0ccee884 GIT binary patch literal 6156 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 Y10*L&q3N0zg8%>k07*qoM6N<$f`2_9PXGV_ literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/food/hamburger.png b/src/ng/demo/src/assets/food/hamburger.png new file mode 100644 index 0000000000000000000000000000000000000000..eba9ed8671809cf5336f407858f7f98685fd1939 GIT binary patch literal 7032 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 VREX{n;HwT8QAHJmw=|B&|JhLKFqZ%T literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/image/2.jpg b/src/ng/demo/src/assets/image/2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5382f6d5472e51d8b3709a8eb99904b0ca8b9035 GIT binary patch literal 46016 zcmb4}V{{!r*Qify+h~l&Nn_hOv2EK)W7~FeVl`}R8y zc=qi5%-^-Yy8v`KDOo811Oxy8@vi{>ZUV#su+T8DFwn5DFtBiNu<(fJh=>RXh}fuT z$mn?3`1p9(xVVHQ)MSK2Ux;yW$(hK%(9qH|&=Zg`voh1MQq$4X{l^Fd92^`XJR%k% zA{HGXE+O6jxBMLhV8BCaLux}oPy!$^AfPZH{tg3(0RTuysQ+~L{|Yn=6f6J|4g&t4 zmJkB)Pxt?7At0du|3>q71Aq(#0f0n@LjPBfz-P*zD$$4wiK*mwy?e2vcA?3`wi(V2 z-Gh{u)Q5WQnv*@Bdfu;1`?sy9>GhKwM%hEwVwV2Q&}~*ppj#u~<&etWdenf)*z}K? zO0oq{mic4+2DtV1%?r}Si+U8HusF6B>82%nCJ;|>exta;xl50C{|CrbC`fC|Hm(Ou z<9B65@SUla+!7b!_!BDEk6Z|AkKr(fZ7IOj9yt%q`0mSb*ZEk$Xr?K3sJ`CgEYw1+ zVaiILpr0qkn<$OX9_d>}(L{ae6{h8aEO<7r%t!q!Lf?kw5*e9bsQwU#&e?b+@R-jn z2hHWUBat6-j6iN)IX$mi-3*wZSe84+)Iu?MpG~y2!(HK1nWr}Xac93{a3?E(c_7%F zzqh0A%2>!9BzIxEZ{_A0fNVAQ_?aM-k1xHGx$^}*bRZL=+WI^vZjbO`uP#^hDhtV@ zc)cBn<(>dSz&}RJUNs24#ITcDjacrO1g|{zE+l#9MNI9N#oa*>pG{ZK@fm{B#aO*4 z4Rj2cG~`e0&-&_rrptH_PBz=r5*ipaMoaFUcgOpzCWkC))R#$Z^ESVHB%?ajI|zwJ zs<)9U*-Z7NDqQ?tsNbYW$clrKL%q>H=?(AZ?H-Q%7EpS?qNyh=v(GLpJb&Yw5V*j`XrH(+JU^^bBrVU>d<&lej0z+=@yfLZOu&&&s2+DayWd^deGcJLFeGb}Nm zs&kx|#7YEre*|Pki?1ey%-yr{c4Ys?nxhVt5VOK`3}E7gj3Y9+Zlib1r@R9PI$vLI zff+6Pgcm3%2YXO@aM3f)&Swe&7*;30cLMWjaXAJHz_TYW> z(oNx?6CCwC%ldLi`8_^IL{4yYXyQ6px3&PtOyRL5SDw?*gYs(|Im|Pp%&<6KK(8PcueIOWHTvn|}Js>B{-oj>W`pa5M`?_r9N4nMbM55AJ@VS zY5;YA@Wcw79!YT(bD4vWCJ2=BM;g$YVK35f^EO(11+C8nbLusH3|2*tNbqbWhjO=^ zxmk}|Z$@-~PujC*ol%QVRxID;X=x-mvy>}){Ym%M|2~I>xY2)#5&`LKLCrE`{!NaK_1rZC6Q_f}Eck!YYaEW%Y1@0YW(iDdK zeRY`pjga>mRUI8}9%^4%m>lpKE0zRVD9_|1d5NxmulsNA$lM9ebY>R3h}L&odv;wH zZ~GKrTxXx6eMavUs<;mr%@nHLI595ogJI-I48Pg?^Vw{y_vq4WJ%}WR&jbUy3~yyG zOLQ^Nzp`&-o>@eSiZJUVheuQBw6P1w5Uo9GS%EgQw5~aiW8~b79Kt;XYaBs12r$J~ z{&70pn2VQf_8Y0D`CDn7ag06e;|<7xle%`{QggVy-E5U!+3Lr?V%k{_^Tc8DS z1D_09`8lTfGhZ6wxD$UV&2TSY3q0uySl1;RK7h}(5UIWLcFqi?>+YVp=b8C&uCtZr z78boKN#-nt>@irs@6kh)HC48*#<^(5h;G_WwpBJlj%aMgGr0`4MY~LC9(`HJ7H!hX zx$H?-Pd5JglbyTTGN+}!!Iey!k@q{hCxs51(pVTvF^I`JQ&w|8wm6+s^uWL5{x#95 z2XSKH;$YsaS0SQlUV?rk&69y&skmBP!za}YRrxrpq0jF3_RDpR2UyW_p}9Fbu&CAl zW@kcKt4m#APPtJ1I{m@6_9umf*QqBjP|$;y(I_4Stu-jXFF~gWo3gZSY++G!TfM5V z47wa_ti4Q+m^|y)l2TW~2ibNLqZG+f_||=uWIr2n>f=0%39XW7#$c$fgW`M>0t=NKF$ zBj=ijNvEDWe@-=l$x4MRUM*9(E8Xi~3d)sg-IR%_NI&}^h8e{ONGzVj^tKCFxLnizzA`i_K&-sIe*GS$t+1 z>xRF%jH4H~W@B5==3%Q|+2pvVhEy%Nz!UdIB(IdzMYL$YChJMs!y)H`SBVE8o`0;_ zXnQw-2z-w9To;}*=R6#G4raec?`4r3$Iev7(9L!Sklg-U24}2H;W$k_*lOl>CHO7V zvWc2`eoxhASMS|_eH4k^DbWs zY%Om|iC@+P_^WLt!<)2AHf@+cOAjPp;YmaRMEe6*u6`0G-=Sd=+aGUZ~PGsUeoa z=(f`BNw*kmUlbHh5IXxBp@2}q)D>+)JBoOduaPWj{aI%-ODcvBa_m{`6@%k{0r>{p zUWXQnV8Dtag`bb!R<&o;DnkQ~g=KWG0#RB~dI>n%zK&N#P(6kw zhNNd>;;I0TwEd43P6iNORJ}8>9?Pfan+`H(m-JLV0{cDQzS!PiNqhRaXa!Z?3*vn%<^Lhg62N(A`{c1nYe`_V(hlDp<-BCw}|fu?EzUSgb|h_&}yE1 zI3<>Tg)Q+LiKV11{rdJogTN}r7iS9J0N|A@jGZxsg`|472A}bJ&VuzO=nky6f4?xh z_^VS|WuPhpzErMA!M!Br3tMv%@kpwHuWD=xhDQUv{S06A%5zWOkhK6e$X0bI2WAMX zmV{tH$#5iry{2u%Ty3*Roh3idPn^zEW;ja8cSxlfjOE6#l2f%gBM{dUkJWCl!iV;) zS8{v*y{zx+YQ59ak7Hd1?WseZg!Rf?r{e;XcAG560TVqB!`?e5)l?63p0J$L+RT{6 zHfEw$4`_yOi+Q_#M@A(8dE{;%ghkklT_nfA*dLblu{8*hAY6%{axsU>;N%v>7wm8f zi1mqu!CKCfvz9F%J5W0V$MQ0;W8+}kPTryVcYJ`2u7&J@u`gGV3#pZn5Tx&)sZ3Vo z*>jtAK1AC=n<3A;8Gjb8gbusxpF%R``SmQ5V;h1^TISzeg?=f>xm{bv3TOX@s&~k> z$+g^=L$zOA7jwot)5GIALoeC*DIv>ni})8HS&bQQ7G57QS*pgwzdA>d(-Ua|(wt~I zE2gkO91yz6BkpW*A|H4CS(Ou>QHg{ay+Ep5X{FztN#x}eZ#lY;>PNEQliO+xlg9m9 z)J0~^F@)vnz^B>sx1jcn)KHm&K}eljt+X(5g}_5KZnIs&Bi|c+LDXQWb%9sTK@`r# zsPB>cG1XTTw9b$gfteP8J174+?&&%1t$x~}sz>2`es_;YW~4+($LJNtb05muBjdv` zPLgGWLyT1$&ROeDX06|UTPP2L90=CM=4rB)Nc z2sqta;hKX)@i5@HZDsDZ=69rsyxnmw?9=q0ME^VPr|F><4%V6bnLLke+65Z^p`5zH z!cr(>hGs-BnSW=#s<&jL0V$-ar^4xg=ms)e z#ALr`*mDDYPIGRoBgrjmJeJmDGnHm7^Y|nq>(>Ct)FlU$H5*}b%JOac0bBp$>&fz9 zbbZ78`#H88YZ-ogz1J*SUqUKO;ahzd)JSMPu<-E!vP32RfR8AIDJT5vkl8fT2YN1AY@u;Or zGnM6I(6T4P&9enea)V(Au;gPyDmuqiz!a!sd)8nRmZ3{wu4R`ZP#y#kFS4;p`@b`+|qbqts_*=4mD z)U#jZIJABs>BSzVIc425`Xh+9i*Li6qA-;d}Co+!(K=mtDc={-NOHty^O+ zu}3AaCUl2KI0?HKNm@8*V}$nh)ZlK5H0dgd7;lfYZoGpErrhwyPu2PQ4vN@~A(XCe zE7yMI{pE-Ji#;*_UN@A~W%4tk_SPus4g3;S73h>*njtLn#aeJ60wK&ZV7M@{G1?=0 zMGi?W-Ik2wN;9Sof~40-i`j`F(L4T(i1_P>j=C(np2CyXkGr>4KC$wuTGh!V@&m2;wi6T@$(I_OX9*8T0jlW$yG7A7)minq)poy}9Lq%$ zR7C-DHsqICgvV%O0jg;efSmvr_iG|BPV7Y+>hJ8pL zVIK9g7a4)b!~{+D-) z(w?QQ`&s!hsq>N9!Hf z30==N&y|T-V0)T@eva$VdI)QOSSn%eSD)zPHROP}!vGdilB^%l3)&p*S2ONert(j) zPv+;0`7^K^DGz_6Dzj`)5eDPnb8dq!J7bEhuUA{LkZ;|7%S+c(>0D%6j6oFcdI?jA z!9{~vK0df0YqphI!ETxQZRR8G)N5x@a8ofNQ`xgbv$Mmhtme=1uY{kd2`;}khDy1f zP!p*J+of!+{(MxvAhezsDnQk$_cp8A>E#-l%GcdEm?D2|QCkadCo*)-P7E7|RX(=x zzHPo}$UezYT;Q_4w9%}QwI5$ds&}sLXLBm8%zdF5`=-5prriPU;<42xe&i_4W!D?QzP)X)sB8!7GuI@h5iyEWG?SC$F0%TA_` zj()->Up~Waqxvw%?ZKxb%d4{fe*X%ROF;Us79T3Ef zMOLyanrQE5a4)bM2-2EyVFf05I;!ctQVkRzdb!lx#PM5ezeL79De`b`d`;<)Ur^5- zW`CF)q+E@fA z%w-eVc(+PhZBJMo9jYHpj5#O?Rn ztL{Ava5^pybaZ;kMYY=11mx~j?aI;|lC1}NYG+GkC9qb!pq~xs1+J@9eKxaLx<;l} zlplxFl3n$wSNml5-dJ0;_>oi@vs<~z#j-6nf90#(>=dEn?n_L6Q3*Nx6UDRPxmj&? z3gN5&wGW@3h*b9uBZk?BZY1_+rzYyV?Fc_SZ3&-gZzj$?*x-vy5hby4jCaiC0~F!x z={Zcm`z1q4v2I&Tg?4vK(y1BPAur{@k;uKRgC{EHDmh;>{FXO6N6WwrHIY%_D>WFT zCEL2m$XDmM9154VdWBu@Si!ivisj0*F^Bv19JtnmQ;ZDKu(O-)Jcyg<+mO|{$UR=! z*4bJ-cTr|)J5Y3+AV%2BaRAN#E-U2D<)jv`2J(L`gnSH6Uo*E*e7gIUsbn3OB-#Wf zXk%0g*8T0qqQRbWQ2aQ|#L!4YrcwRpNByc(ojK#=xgzO6%JXaXu3lSi_Bx5vsEWQo zye1zdL3<#?M3eE<0dr30suA%kILt(ZnxI-sjmzSs^DZ~3G`_7!g$;oHfFC8gW z&f&Vzn{w0XMT!(D&g=reBdi(EJB#J*a60br^t2_br&A+N9!Jn!NjV3a<& zkP7kRBJ~(Nia7O0@Qt3Hm|(vnXHSFmDCW_j#xdJLQcHIO)+d+mEY~M8c`7!uwC$NR zltnzmDBET}az=8hh_}{vO&mFgTLl(Zr;&J?iNKc{_=aM<9KiGS4|9FF{rzDr{xkQd z0a`QI@+^$@zWpQb7va$CWoCGvIm)($&viXHZoo1)+34J+24{2eh)eCRAN(0P?d!n? z?ekNjRBW&jw|u5SKSALK{UdNK{sTCj3+pIl6@SfcmxYtJ9?uLny#{c@jMF{F=DeJo zw4W|}zvFNcm`2I9H1_YzcSUhDM$|YKJ0G3~#!k<)bFk^I+f5%dgj!NGluesL^QOzZ zPxEeDdWe#C=Cf1GnxNUggxgr-_Gx1H&PXZd9>aJs)w#B>Nv zr>nkY;s0vE>6soB{>N*m&;kF2IsZjE|7AK*&=8O?09Xu6bSyY*9CBRNf8+-KAJBn- z1ejO+oCDrdBj^R}FW9H&$|DH4NT&|nzG$mHC(dnN;n4^A6FqLFx~7d`Kp53ssmTi6 zH+AKGjRl+DaN_uWIp0q-<3ChB47%f+2+R&${3Ex`g0=A6fNV;5Uv3(=2F~?=2iy5> ziMIwch3hlTQ7BMr)Y-kfp=VMnxMs>-#*fUpCG82u<0+J|!B5ZhIqG6v+rhWC&;p0f zaTWCz?S%1AR!zA5w+pcP7nJVF$05~L{)MmQbaY0M3)7rxAICek8&qx~_IWS@<}b@` zW}kr$w6)5IXCrHl*9dRj7!8wz;erpXVt-lzM>*vs--$Am7FE;W{13X~WF=+G#$!La za?)Z048^7Adb%On6rsz*A@GY-7*sUMv||1Ajva1!L10O;vCZ?=zG++p-0Zi{DHp`) z9IQy<1s}5`_GRWWO#SK8?c4ROImkzc;5A85rv`b?`jSP5j9UDh+RjSul+XD=V1H5*DM5jF_P|)*_nML5GuB+_S?LAd?SX# ztq7>CnJ1ey$+#^r-0u9~vH$I8o5UmG+J-_t-=LRWr`udV1xHVW8oI&A57f|y_WyZK zcM59xvt{re(NkaH*c>tEB{cpkXjONqwAR+>=I%xtq*ZI(L)72VdcHw!tQQ!xbu4_} zK`J4SC7O?9!>q`2e^Z~LvthUQ%BE?RXX^ZB+3&;uPCf){f7y6k%V%%&d)1P}#-P$e zTw3q2Ark*h@}8i=F;QSk^=kfhz#eC^YEDB%@i|^##ulfp~t<{dY}#7rc*Dtekt$ zv~c+JWIR&x`TDN86;o6dBXZAee#Lt5F>Dj$VD2dxHN8AfKdSks`{p{$9~xN#`pUSy2gt? zs<;M;8s9!Mu|yH>`(jnQ=Jr!{%70ok*cq<2enJ|NX=3o$I)$k;uw-z}fSX7bmuAPHdZm-^TOTe@`vncO7?h?Gf1YYg7Lq1iU3}@0HbzLv?7mzSGYqSvmvD0dp5gi>X*|%iVx^cHK-f&>{Dil5- z1?1yx(cwpi_q8aUYgA8sfj_X}uq4+v*y>%pICUbDhn;CU9BV$}N^ZaLzv70FKdR^1 z>PzggTh_1V&T5oL6~WOFfzZq>{|o3lsdjUpwxB=m`7P0f(qJX{5VP3y{PGrX{IAJR z82_dqoY+-^t%q;?j9m6LG(d!EKSLDP#y6j7+;O4BLvcnqPNO?tcu218yT@Dki>3xM zK7%ZThMrJg4+=b#b-j(57+;V)BsniyZbc#fIVsf8DkOX@+^RP~11= zuL5rR2}JZI7&L|9f-sClQxwviq802`WF<5;{_G@4Dn}$OUx}wdPI9)EHaRru!J>Xs_59+siiF9a z@zM6ITlyJK`_PR|Da+v@xwng-+^TJf80sUJW%F`iHr;-M<(P&VXcpZ?At4$ezMy@- zlRi5mgz(_{_`iUp#vL;O?Fq8Ep6^ElzNjKpSr}A*G}00`lUC$`O570ODrHkWsYlGvjxi#<8YIDOqOJu%5>d^E> z?4_eqWDv!gAZeWsf}mU*TidM8jjIY>-7tzhxlp`QzeQK<^Xp}-4 z$t0I#p?%{(D8M)Cu{-o7DO*W0w5cn6a;**huFr>EV*AUP3qgQFejY4VNR+y;aAD#s ziEzThb|y@n1I{*lVs802&KT@`Z(|460eLuzNHOGj;YD84_3BQ76_-v*5<>y#Q+d^R z`FK&DR8ECdyqJ?{XtuP8&%PViiNou|_lKI1zJ0or-7r>NX+4bh{>YPK+GAm13`pUd zhH}kF`k&GqbEdi*011!g$kGGd_^2L_&(fH1u6!yrN^G@FY!vtJ3$Ew*Hb{6v8&)$H zv-~L8pqnUBRY{JTUg+Nyg{6H3)IkYNT3E2u6U#ql)KS6J(1V*g4s{$@Scag0z(SoW zD{>+6{Q8_a^TKiF!~8*2@(FFuoR>py=}b|{0YYB)I`n1MZ_p~WYRG38pV`tD{7}6>z2mry0Q;jG{VI1Qwgpaxg*!=;o%c{%7zII9hn?Dpw-%yM9|ox zT7P9s%Suq8KM{dp^4QhqLYi;P%9kc!Iv-djPlc5 z_(=6Gws(VTLFcBG9O0XbZp^_X7rsL!z2r`WhGHEX`Oj4e-cjJ{6)Ac%RA~U>Oa=yd ziBx(qgo~JEF@<|J*Ivzv$d};EWNB*MJ1)9N=wdq?y^tO24q~_G22@F|tioJnpE@*N zFN;FD&{FIrG#OA8A z?2eP8D`Dqsd=YNcTfiv)HOE>@(T*)zR2{RJKhkv#$mXRV!-R0%M3`Ds!qZ#Cuo76j zEYZ=SdKtW*FVRyn)@@;uu7rt|6`gi1Z&b^9_O52f)oR!g`?3G>@$OSuK0>19&9qRg z_6T3?ae?HL9$XXZaq5X!y~4R|j`ve}zt<21Y%*#XOg&tl*&oW=jO?wECmnAU zGwg4=f~q^fTz^I8;ktp+N$pDq;>S|1#Zi^gx`tHn<9(<#XH% z=_{MEUPtZB#?yt&-Za(3c&i>3qzHi?C2y9iA3+0Xdnx9SD08U!3wX-uT{U7A2@xw? ztgy`<5@$2|iYVUri&MH<`}&f@Ryeyn;q~bGLsf=EQdNZ$Me_Bytlw|B!_tz$tVPIigwL-`lPJF!fonw_yECx zrQ!Z-K1VTUIU7XK*Z9K>E<@7AD}77*kspM2iO1aR9(LGucDmrUI?K#Z$xFz?GoM}` zyp?iY(O1J@^=~kA^}?nOXL~aivE)_>$P%%8vR#!Eh?B6I!vDB*xNJe z$`6JKXG3Aa=i)?oy!sCkK3Ob0iZRz)EbFD4*P$nc4)R*Hx+A{5I0*>1)brbBpnZRtQT?k~#DVa&YFA`vcXwH0`i*NfSbf zdirfq{J{$?dp7*WN(?=Q9URlMXHW6C6in7M?l0ZP)5<&7dLw@UPS>fjAU(WeiIOar zqp!BxuX!u(6Jl0Rb===AJDB}Qf|wIeJOmLI*AQ3cD6vu16xaLk0<$q08bgJgux9d8 zulf5Fxi|+$&2nDg;)d45l<~gIX~G}O{mORbw)-izA(+a&wPvya$EA#zald(hr>(W+gwO{V(7l@`8nx}rUo`k?)@}lfNSC`L(DIMt5~zmf3KeNCZE;^{<%o@&~F52 z5iAk+KiU%u3*RKhoAz@NP*zb-@6T5jDRzQTwg~1>{Vf*sonK~=`R&FdDL)(GAuE_i zAG#O{s#>i-$w2qM_m|jIPmo7<8&1XH3IcxtS=q78GbXiK1;tWGge^hl!{+(q@j&2P;Av@5SzlN`6GMqzyI(_KtO)%oWYsy z^R9)_U#r?X(8(=)Lx?uE;0;Ms%3W<3Zo%`DxC5oc-tZI34Yuv~q1mhBi5=aoEwiUB#d*Okvg6v=U<0}M+qsG1dQTRjb zyHFy~j-nlT4tetKkyQwI(K0`fIqS;t9@&=chV2hwH9;b^N5q@g@&orWanRV@+`D>8 zN6g}gZN@kEmJU_c^(=@m(eW7X5Z&7VFTp z`g`+okr=>s(IN~Mo&$~LjY8=FzG<(q6>p;!n>3u-YruA~7wT}JP+-vcH|{&oOC%QU z5EoeVJ#(>*UKW`4(xKMnE*2BTy5earY55nxsTSIH_JxP3D)dLQRAcfSy5pd7P}yqP z7$#L&!I(p4M=N)}d&av~AIzjH_jsdK5YLj&Rj~i?=Rf-yv%=ASOw0Q!X$#_JY-O_` zL{ml?9XR}H27m&}$)#@9Ux1EprDa%P3=_U#<4+Ak5yTVV?@J{x%fGOofRK(>PModG zfaZgynoDk^vU)jYh3z2`$&f{DG>Ip(>qNJvxoQ>v z0b>Y8yij~V@>K^hqA0FctjIEyvP6ql;ZpScqNSEe2F9cAu%>*NVSupU}%gb|1F3G96}-mEanC+5gaYlJ(bO}J{P1t7cw{quZ&sJ)9M`BK?_7U3Xnlu*lh>t|0mo~X1jzm!U{V>y@b z-7u_8wq+@cRt;s>vHGF|?}W0M;stt;4$}n5;uoA^7wJ5@D9Hnq&?qJmFeyEvY;Cp7 zR5vb;@`7&eb~xy0jrfoBhteIM8e^CV){hfSaV%Y78-vda;Kap^+dt1o!s}Ij45+8} z{sk1ZZ*VDeQ&TN=1iOC7PWeF?N5aQ!3nM4~GQFJGXaG{S#<)Jr)p!t-kApaRm6Z#| zY`0e5{omi;kN$0zw7-D5(Yrtse?RIap5EUZdUE#1_;|c}%E}y)qI?l;qAi5RYxUz* z)RP6Tl?ntmsj=D4b}S6}x;GI`Adb1N%NEb` zXfsB4yYjhwoQ#M(v)>ueZdyOS3g`CqMSOQ%I^ajjD6G4*^OxtTPxu;{Ed*#v7hzPZ zL9=UdE*?5x?tsl8d_FfvVrGicGUdTPjWzLhIzjd+vcId_uVe#(bzJByuA#X zx66ADH19f$J$G^u$|QfHPqtDLtR*e3Fbf?L^THB1m%cK57r3K0UdGgHc3$*H9&7CC zp|!>iaPN@Ndjm2G z+(yVL)?p52aa$a_n7;s@w%wjF9=+v!)vZs*LLL#?Y(@hlt-4Jh z8{$dX0~}`{jgU~UAqJfa@R5UgRboS-Jr~38JunOHynkldZk9S>0~hAU`!rd|4oeFa z1e0wGZFPi16xKpWll*jU{8zoBAIuMx{l0^}@7V{KklB|8B*YwH-^N*TsDfFipUN_k zic34$R?beNK*OIb;uwM+>y!;tmdwLq&nNvqr+uora|>mI^qzP+WK& znYO|7{~TrCF6Gt2pZ0DM+Dl=MX>q)dVLKmuxSQj|gvI;qfDCrglV+_7BTWim6Hb}3 z@583(y8&_abP2BB!@GilG(@QCL&RDDqTZ}(&)wM8Ntdyku)OB63;h(#7qiLKKiZDO zkrn32=#H(6L#_8IgjkSfd_#1xKEhmD;ja(q(gL;Lx9j{NM#-g5cp(=>R_c+!c7hc= z;02o8xU<>b{7$}Kg1YaZ>xtF*yN z!wjdTvCs$Kj9m0W=XHGT3%LgpDwObZ{$(}8M?hOpY<{|6pd6P`OOaI30QWg&M#6j~ z;)XA8HzKYbc0z~0*T`^hK9X+Xb$796m5+(lUqDv~>Zr*VOKyD{2X@5x$km9KKD&6H zl!&pQKkrh5}jbSE0un1e7j6ztmpbI8Wt}Hvr)!K zt<+`ruvM1}3kq6iYVE+xRk7J=iK~qL`$^p16utv9;qmaR&{f*(N|q$G?v@o754*`u z!|obfa{`ram1r;)G zPx_H;#R#zX626s?Fbxqg3zG!ZVcs&-^wc*_xM1=%k@(tJTl&KlnY$@>Nd(s36TX|e*xLq!RP_TRRjDQ?k;)V zC-ECR<`AjowrxPj6+@tc&Q_&*Jn;Npm}xD8o=LW6qg@wx?yj3^2NFS7CpHXh2%E2s z&LiNOj0hC+81l}lqhs*Dm)5EkMvD|7jCEl2dH45+ugP+?CbIfkl(eE{*zTaM;fu2l zHAH?zl@r6!sTLs*w09+C8^2yHDMJGwr5tCPcSvFK?GT*ZQTCK1>i{yqq?k zJphj2-Qc6CTEVZoxIt&A)~vm$q#4GE7eveY!8}8(zz19o3;# z`@EuVK_-hA^**NyH~GtNJB1;3*=)Oru4+`a$tkZ^rY|F=ON1J_T%AraP=IT8WzF)R z@%uVaiBUqyAn2?#YS)RDK8Q!t!x;4bgLn2R^>Scr{zWOQH^4 zVPPyJIdFp{0|)cDOSN^EZ;f?psgLv&299am!oQpOq4@#w#lWFG*LHeh|K{dTFLqJ& z4V$22%zO-n9m}?lX4oIGzW}c{+!j$+&^j~n7$Xlh*0tWdw3^-XJXY}nTE)!kzxfB6 zg)urmyT~ne`FuuFwzqjZu1Z}S717rMzla(T9}jRDs#1m@d-oVkHkPRg(bnNbC*NkO zJ%^3y_6im&ic_4m+}rqtl)0xUUS_U&wlhcc%j4lxKQGeERxE8|ENFvP_i6IdV$xzH z?H;zqx26bKLOR^W6R8$f7Jwk0^4%#>VDY5KVGN zjcMF9n&yjX_lmc9%~2K>+GJCaQFTXB9_*4KhJ|Q@eH{^AoLl#O)Z=z8!sK0Twdyi` zB`f5CkMZWrxt)X2YWtyZE>Ze<_SXncjG|h}~V2Sr{h1&y0Wd1ZLEOayhZwJ6$3B38WBe zD#X9Edv2*Fj3r5T(8>lwSqA}GbbGLCuk>a;){Vk2c+*uSwPEqz3D_z7|X?v&o3=#ve~-J*NM@I(qYBv zTnP=j?IzvAJljrZE#PIVdl!H#VDc<<8==Jp5ZY%6uO zoFQNv3@`|_t!ycLs1EL-Dgy>SJ+uuI(T+tFTeo`MQe>OEK%FWBk6SOcmxX*VG5*2G zBc+>(-2TEHLgq^BlI?ucdW6a_^DT70EJkYag}KHJfdd%Cfp&87vU*de{Vqw_mJpQ; zq+H(9%|tl$C3`dC9aNe36A<)XZSVGd@V3`=PxhNtpBPI)#r#n|MUS=e?*k`_-5Fl6 z!a)IiP81J^d@q&(qnYfer+a!mgq>T%gmsCH_}fT3{0+g~)r+DqH7}=JD-n>g z{^bM$ujbAsApb+N;ECS>Srdn{G#$P;umj0XpZ7YtI9*Eke8`EdLD1f^S-713F8~;l ztmr5VQtKtrRw~e@Y&*U)?Wpc(A5v4uWb6QP=*%_e@zznM&=Vo>Crl>NEw=799G%7) zhSX9AZrAf552+8}*x{5lFl3ub>(i=!2U}?2vTB!iAN-R(c%-UP9M-w-3bAy9m_(2; zTp%U^(0h>@f)z=S!Rmya2W+1~h?rGpZdFB>FNA4kZ=J7B$OSW}z2ma6L2KMy z!SE}a?{`+d#WYZZv)p%-zV>3kY|yF7MH({vf^hWNdjcLPBwPne#V@an=P{@SMj>n1 zV9KVg{;`yK+X0-6fnt@%h%=A6NEGdK_)5)Lbi}Gna*w@86c@o%f!VoIcNM^RW+dl@ zO}U2+i`N?Tk0y)%qsfpEP>|42uuyRSI5Hf>KWYqx0gXw)CJKY4YJyJbf=&K~T`dGw zESZ(VwYX_u0f$Q56iz}Nmzq<=EOhVY|G;J_VF2&$D+&3(RPaoZ(@OEz_3 zv0ot^>SgU4Z5~{nnnV9qLG{vVgKQeaoM!*B^uySA>8!1QY#*CQfN+t_`~Cj4u#B4~ zqs+x^;qJ`CD!mQuR^aE5$6O_6tUGRj`hD2uV~zuWNXtbDOE7W=9V_k*V0OhyV{%G? zpKUS+2{DZZ)VSM#ZQzKYftkp5vhLiV(TCt=y;YzsgRq)5vHBK<;lsMwPb^|gK$;|C z#6-EBt1OdoMq2aCyiJq0Zd&m;HYs@Ybheys4nmpTt}x^T*Qm+sma9zJ*tTwp`f4zm zb;kEZT=D3%qCSUPX}=`(^PrG7qK69uMI~PB9-znbj-O#!Q{5U< zgif!f$G6w@&LZS8zxePDBN@ZBZUn?lhAIGDjfe8y`p4yxBoGnqv8GDB+z1b;6TLCK zgPmxDX~|J59FVp9wOIWbou*5GXD!y7XK-nfay-V9JevV4k!?ShpAsvV4E(4y&sJ}1 z;#xQZ1Vc*=r|b`bzC{nLr}w(TyfS^06NtX)6UW5uqF7!-1aCi{Z~nkyAqzY28w#iu zwiu>u77+Xeuv~K%Q0wmO>ngG?;~XV9N@ESRZk{jMoH;OZKbEgnc6I1fa55W8`oTgb zu}ZEw%0kTawP3@mvodH5N^yN^TJrIcTT|n1+Z#k z`?TpyF`8X$kXR5ph$!fS(eWAw#oWz1p5_SaEc1U8NqTR=`Df{=h`2TV0fgqGG{hwV zu!KlQm*Ob%vZB5}=)qdJTc&+uOxzk#9snLbMC8MOPtwFp47f&=dw!`^3l`a)t6t#f zx!*DMUw-+~B<%u!lWqe@{z6<>!}kE-kwUTqxZ*NU0ULMyP5i5PZcT5%Y_<2k)oZ z!*@(%j;2s}@@IUB{s@+xeIjCLHT^sQZLIafPt)biYKzlWF6t;U9lcX5m3NnkFIci3 z1-k1IPfmJIKXolhEK52X0WgzB z>!P$|J=Ok7O>V)zk%B2!RBgB9B6kD$vxdYS9Bz}1fhe2Kb1#rzXsX5987iwt`V*^G zmvK5EC%s=Si3FssKFbgkTi3->hQk~e-us=Q*Nc~b)sSC-a7mL5+iag5av|a)xybVI z$)T3t8)=H|%#wWO78&qN&akhkJag+e31H{hnioFUP&2jsoVI=k6~!w;^T-o@`3tyU zQuGD>Qq*y>WMcvo*$B|!6wv*I4vp20DTa5fv>o!SN`2YN3xFuFpsFVL26bv|D+aY= z4}XM*YzOtoy7m{q@h@0X1yk0exlPMWsK8y}WHREYbP7nDqFxTE` zBRL1kHH!Tf952x8?J61&-E*7DJ9s1w)fBHQk14G(Wt%WtL%7XYZ^Ekg`K;jH_3I#) zGMf1_wv9!*??Z<5hy0M()Vq!^o;y;0pH_*<`DaY%h*Q17^6EQ#{jlJG>lps@PenJu zxz>|ga_WkrB8O>8y%3)@3$F^`06X719DjvI0L@MCfipb}myv#2j>@=ym?UaV!jiEZAmt}SllnO4~bw3Pu9r<1E zjG;#r7SS*}Gr(4xosBU7r+T;kG@XjgvYO4Atqk>0e$*gP%?@_+1vVzA^)(HuerZh} zHvg(^qip$y#BX5JguoM}P`Fc9JCQ>xJ@nng0o0mEMcBuNt_c0sNzum~`$iqtJO~}u zVEa2n;T;><+=`*6x*FNmzFLN!4cugXT_kOv)R$kyKNWQ&X&zr;@F9J;fL*Q#8`zlN zp#Laefxj#U4nt@#RGrjv`~C&=Y*tC2dO08p&Uphj=^F0=$(NSr5VSk2JTZ`C;T)DA zU@3Gvezs~8$cmT)~p%cb{$-wYPg>`OkSM z-U;YV$KOiY5U?4?nr@E8x$t$%e*u?ftG4A`iA)b>JBgwnCh`gW`*~MD$I7kY-v0rE zKzzS0T3DLE<&OmZ-G`*pRseSBHv#G*+MAUyox4iaf|4`o6^01l$~6f59nHrQwbd=p zN?I!iU~Yro9m)RyC{DClsV{Z5oFdoxvHC|~J>$LUqNbH5vC0p~8@Y4n1j_}X8sF7n z<9YNRf;B-DrY*C8zolVoHc^&^IBv6gwf4JTUD)>OcH)>-LLv4_h!V zZ&MH39;&G*L6{^fWR3|@#$~6QEk4UNS~8-G-94DY#d@*zX-9}vzMBD@dd614J4Ce( z5rZW1##=2C+Rc=3$ATuEtu<9)TMF%bE=D`%eTUD}Ycpoky;uycIshh{L<8wqDr#(3 z*m~eiE|Xp2o+OkTYHCuw-9c^Kkt-e(ey2{sEW4%;=3;RP{vApY8!TJ#eUwK_9NGe# zo6cf;sGIyWt5nzXwzeMm$rJvmMQ?b5zzJ@^ZUfq_^4kqir0BCl(SeIDBw5k66#7Ljpnfyr+Z1| z5SSlTr740%M|anxw#L>an@}j)tbJjp!>?_}cvR1^!9VN-dvz4iWu&$X=jvu`Rc6V4 z{?Jo(_Ggf&-_&sp*kW;7@MW5pQBNc>tw*`UuC%Ed&8tb{>Nv@dez199n=5Uq&FhSl z{*iRcLT&}yEcrZz2ORsEdTKAZP%ZC;8@jG~LVQHNTI2ak6V~gij4`Z&m%eS6*Ej@lHq%1&vf zr$%4(Qx1c~t)LJc$zSlS!Af+se2RGQnT6eR1+yF|>V5u#RdFJ=Hovc7FVfmWRMzv3 z1o0g?TlCtBDo(7eY^u#N{jI>W*c?hVNtAI22M`>WQ;{vxKJY&Xn3d{&)s-|@F=FlL zCXZS#(`oG#YgC&`T04)gP-de>KG9n(`ildiZ&R^GKR4`PufX+MTd`SvE>%;jIp|6B zCngH5qK!u86k^BpUqKU3{5f8kZB=1Y*v;Gw5^?Tx2aF#UzgkIPi$zr_{D92|hv{xY zU^C})#xyn)xbqA9IYv|HYO^-lTe?r}Vpr2vz>Mb_q#uX2!87yGb>30!| z!D}sU;24YHuIj;+0|b9ic?0^G>26O26MGV2Z3Ahk0ITWQ-`K~~oibeEp2RLdJfZY6 zZC2f>{{YA~gdT&DEjkS=7anmZE_>yPAhLw(=nu4ow+;RT^1|4mc z?I1cf8285}1{zQY0j7LH+!ffMf=}}{aKF$A<)zi3;YLA?rEMsrZn@Q;_k{SJR1z#r zq@P2MOuns3ifvJ4j?DG}#cMSFB}@40D^*s(VW!iaxafUOdWgFXjOMn~xhSA+Gs>yf z$G+Fu`BMWMx|8pY3FHpZtI--Ot%WE)WK4qR^n!UJc!sk_b87BgJ7;}~Slr5kg+Mw3 z{_|6)6x(`O>OnNs$Ynv(J%NcuMeMAnVylDCp*_#jfh#qsw6dcO!~>6}bICEB5Tw*P z4#bGy63jpyGGg=u9B^X#z+yPs(wmTgquF~za9zn`*;RO!)CzeIf#|q0?&xaz@h~z> za&U@FbsVlIT>J_9_z=YeZezF9OMEw9q`O4#MO50hx0Zfp!Q_({Tj3gS7}A1UQBW?^ zcu`0Q%fa?Nzj>sp@NYX)zd2?TOZ7Mnp!gy(MUF!2z9!%L0FKng0Omsq-9A6WNmEPP#H2r_w5nQQ9baTx&Qf9!HU(8W61LhUvn87H!il8d!EjZMTcEX4lj zpp|IoMzH5$9YY@>gaquV$zJsK;hT&?wB`(uM1!+2m>QlVJ>n2TqH(_0P?LVImr z{{YDPjWP0SEcU-rc7$BypQ(tRD{^nnm{x+nWdM=>a=kULQ+{$rThx&c*}XCHj@ggA zmdGyI1bs1>w%!W>+qRSYbcU^aTuL^%QryDBI9!nm%h1Aftlk=k6YjQ~*p5vdUeN1n+ zigcA&_kosv$Pc0JOgddPCd&$?q^o%3PO1L@>Fa7Ek_mB^a5~_26UH1= z?@(0(g)7op+JbEax^!mm>O^+DA|L?uXdD3Flytd{iSH1)2+#0h+iIOXB|k z{B>TRL1SL@DOKOtOLUBM^#G0=<|Bx;V*OtW*aMyC>)gxrS!S#)2sjf}PN{Vot8iEQ zsu%o;6IbFEpuC-mIl-DlBDBSp9s&cBOm7F%8@#dRp0iNekgcY%oSrtW>8MA>qe$aT z{{Y>`0(gIkAT`{K{>CyhcMc^Q{T8oDX3)n43H1`~B|78cwxBwCi-#WlA^!lTH5@e* z-cCT|s!f^H>TEk#yMOFq zb-PR~E4Gk?j)Nzk@&v#^)UhDC?V*3v$FKN%O{PL|Q?s%4o^2XX$0=X@W<69JYG4hO z5n=4CM+Q^j+B8a!BTm^TvHZFIvihr9t}J)dpf951sgHZ;lyxXWWBgo4)mx~)gsBd9 zx2c+E8hS3d#g=8uh2e1=HFfxVrlzK`P+3{yk<;oi8k%pq@KGx{8-@u3jwh1T3du%q zHp@Mi3J2~<#Cq?ocm-?n+x$nYBUPgnz9_8#Aa?_kF1Z?m(1GS-s2`>_ipMn~=JSN4 zoZx<(2P;AHbO*CBhD76*`J zKw)-;gOa8(ih9b^f?_D)pm>a>nMV^xL9&Yf06aI6PsDr#5Kg1++o)}{*3NtFjp4t- zI=k*it&H&PeyYng6eBopB{tlJDsAn_c04HdmFegLy*WRkzU}Trv^(8-3+x6@?q$x6 zwwGk>p6BT;t;3Bs&+Kpq)@p97D_VovyvsZy(CVz!AI;7sdin)5psJqi5Dd*Dc(RIh zcGAh{ySIMAUtQ^@G+%hg{7e8P9wAdLfk`79NErqVMu3K`?4cKHo!pTQfrgYZr$*Kr zi?AfI0D!SHRq5>Eueg`=+MvhPYA8*n#6WRh>L-qWc}IdPfa2z6R|+%vrHg845jeAu zqqy`w!>p@QUd@58MSmy;BL@Hz^#hE`mD#SVa;t4nM^oPuO?}$yZWcPL1Oa+84&OmF zR0Fh*o3%2BT2ZBJD)*9(kzsVru&=E7`(8Ctjn@b#nK_Yp=aYTbHtJh1xq z6IFdilTsyPg(I{&gmU(=!Io*0kX8iys!K2WntHy+|@T^Dlg5z?Sc3NRWj zok=V>l>%{O)cB?QC|sv@G3sKr;9@p_JxB-T{Lt$4*4oD;m_G3P_SD#=%CS8})ad^J z_|nI{&4%grhYY!1d1|SPd;T@IU2#1VqUY4iU@-&K4!^V{c~}a9B~5@yZorV zN#@H;@l&-Z*S(Ks6xY)C0pWRP2{kY|r~H)P$2`v*hQtc`!be>#D$&wo5tvG2c*{6|k;~vyh$gPx_t=6}Ea(N>(cLU(iOOw=7kWh@4?JZ{vD1bzCEuV zadn__Ow;&P2CW(^Dx4o&!&in~eJoMJWRcHc_s}dt$a}6ef#0RUbyPN&6K89aP4>hdI zV48xef&L;6aVgVW*7+vaW5ev)qx{J*Dz@^>Na1i458M6o6)Y)IquJG&NNzpPVGZn7 zk^=W|2^sWJ-d9VjI_BPhvt<6~po?{dFa>?jshVoh^^d6_QUlZ{+wwe)iwTy3Iaz~&t?O3&ySOLgi zX!O48*$}S^qp*v8>t&Zc*zBVVEN2Xe3jY98A7YnW0W05zV4_o{RtsP;`9Q5T7UTxo z4VgAtOr+_&_aa;4n%BYErK^^2_?l`3x^E6`x6lAJGJs6g`IqW6EXipZHG7HT4^`R? zO)9;n;!kt^k%!_G%Py>Ak?0I>`UTWntOQ*Jwh?HkHbx8{C#fx_S8FqZ+cyc}9uLN| zR{E_{bL9+eU+KV!sAaagnW+OOvakG^uht>9v=mYg4DIv3?Jw5Q*6Xgain^0yVN~y_V+AXEs zn{ua2`opEoiOY;&`};$xWwTVNV~lbHvb|ERMY`rCUMqXQM^H)qCmp4B>RzCPFG9^K z)ZETlnX!aenr({b@d;vf^idZ>YB*8mDauV(irag;X3b5DR!>z?Dx*xcS!hPEgO2#` z{buVp!Xu{C9pcqp_A2r#N|iz5j<}y?(>iB!ANBN$;eHWztE-jc-HiKxi`jkI#{NCT z7kKN6*K&c3`v{5Rswz3ypceg4Km3*JE!LR3Re%DmD9HCF4PbU4F3_a?A~w5Bg@IyA ze&?i0ZgpdlxxtjE^N3OyZa&~g@f|UvPD>^j1<$8hsSmZ=83Ji^^qTs)Z~*rbs~2i_ z7+^cbxVtb^s0?m1DRneES%H$qIiwOl5d5OE+di{c`5YbWOFv*`$zCkAm7A#(Nl4AN z2M4HWq@i$KLi|UUr@$RCgu#w_gQIltTSKd>t(kZL(uLlfAc%CBr{Iyo~AO6Eq zuBT6FnzNwBtjZg8wo+{i$(A*3rJ*d-&_@ec+mr4`C+IOWx-{Rb2&_j7Yit?!2Os8h zJT|*3sSTR#Q9mVAj3__wJDA#GTBnmg@23-9PX%u6(DR-1~uyKqSn!m6kn0m~B(*1+6U^ardsFY~SbV8Ct$Ch)^XGYhkj zJ)nI?qf>D;t^(E%uR#+wp0re$K@O(+5cS`ZApZcRiW5I5E&l-f29HH$o~TGMN?WR9 zxQ9Q}tTyYh)tUE<`^Q}=1od1AaB;E%z4kCo&n*feV6JyS5SH)4gr z4pbWbWe=zo?9T2wM$l;L@cOjmt$1MI@jOng{vo8M+CyB1ww9h4q}zogaq@2#$q&73W_lJ;EwfcA$}mFo2u zHloHmg+qi^@1I^}&v{C=qW}w&)rZ%w5!?%Iw4x3UIX|>SPbW)W0)ysS{{S(^{QFA7 zrFky+8;Rfn+lbnW+}2njAbZ)5Ut!os{_KKDAOe4tW2yjvda>*TRW8Z2sTkyNPoRvt zh9wmM^@veaoE#jqH*RzwHa39{nMEnfw+2&ER8)&tDFDka)iJQJt($R);Z#>r^{^Iv zbye*P*uvA$e7?dPY$CjtFOfX0I8Sk(a}w=l;yMltD_gkXN!$BJ)A$1rxbqiceITpD zR2KZ*e`)rw8e!W!dk7bMV~pWMYdlu8AmC((EUPDwPzgD=>J z`HMv!Ic=}aRjD4?5w$vAxo~!|5IAdJmQjWFkM@o99QPyAU;II7t*zTld_S+a5xgk{ z8YVBjSxuc4LP++#iGbZh|u8%QNgG`|NZV5ybAp%HDM4djnVZt*H@ucEOp z6IH4&YHlK1O)k1O$2kcvyHdvLiZs~-9})eTVgk!`z9Uan0fZV|3L|zAT^t^Gm09L$ zZe&0?=ZwLnR9Ww+ie%UXcvetv9s;!&*Al%*8XMhf&{^&O%vnSRU& zMdgA+UP(A5#|gTAi7(^AHXLL0;maQB#80m7Prz(t@`6krnB_l&%+3)G3`21ET~X5z$$JSkXd z@S>HtrpGuAyb+$ixt68QNK?ooCy^{?37ZB#VVS>uZp1!+sFwJB%KreGx0Gv?c@K+w ziP3IC3ipc2X|ti|ObB-`?nK%E10?r@cnooaEJZI#3?gePA=hh;HkeCg92X}6AkpYl z5sRDx#vokxfWGzM&$S0H%qg5CH4}hD#2*9wnpndu17-?PSXEjI={rCiLs??cZ_~1r zer3t4W>ad6@G@~cOG|RyQ?YFRVu65_YMlnRPfnjya5o^pbM7WA4R)WC(bKqrt}ZAD zNFTq}2BC#QvWKem0K~BkHX>{U?cdwjO)F3m+V-bV$<7R>oW7pd{ak^}HP)M06vi+c zd0AZxmCa=SY@D%=tGsSvDNa>XMWM~9&Ay|SW9iRH_S&1x+My19RU_tuO{X^A0~<0z zfFmLNWxy+yj+d)y4$uziWr!*aaOV=O7ll!$(Nl|T;oSBU%y+kA6<$AhO9MY2%OjQK zz_{~B3I^jm-x^xi@{E@Hb`TvhgWQklJgo)991lsmU4N-C0gkPRmECJvNo~h(?+Mm# zW3&YgVY!&J7g}1z1yI3yJF@W1Ldxi=%M3Y>ziN=6pP-4ePL{U* zbF?|iLR&JI3cdPUkzM{x8U%AGeAo=uLHHWj&2Pe}Pvpf>)6e>5(_ z{a#BU%7_$T@F7u^g^$c2Z1ENIm7Di62=p@@u%u{Xe`V)bsK1{u>3kZaVCN78AjAy` z(-_9h+>m9IyKUHO?u1P)tX678O-LCa@D^))F}CV0vef7|%OCruKT8A)dbY+w$CV$J zS{%7TgFQ^LjJooNzBndVs8Y8KahXZO8`(L)3Ob3UE;&1h>Sc8ouDtDU=9TSTg;~Aj zI(;d#%NAXN31h}jv{lnF)BvaXdFvIMB;~W;u#c|S_V6=W=wLYgBdh;BA=Tg zH8&da&nn%Z&h;2$-fb$#!*_6d z#CuCt!7lCYWBs67yOPB@Xzx;+K1U;g`n}9^Qv>r8{U)cwLt2E|i}{CTmH3rjWh@wu zsAV^RE&+E8D;}AeogLasw%Gbt;jz(7O(%y`syjijFF;lWci`UFw}Y{y=t%XRdrY^w#%?^&rRDs|wF-65!u zu2l6a)Ij{y8ICe=og4?!Jj46XAf zN98TbjQucD&*dpKiX9G_@4G=<*7 zX9XXFF3{YTsluKJFfGer`IKOY7Uy(u5TRg1eMJ{qSdte5Gij(2+lV>(_RQJkEkkH* zu`S8KIFD*Rp@`Z^BO|nZh!uhXoVR&xxl|j7>RX^5rdo-^acRPzSjDLs$JeY{?I^96 zALSn26-XUPhO7i#&gT#d)d|l+X5B)ljj3^3_Gl^4y>*P(4jj)i)K%&LCKt>0078Pn z>Tsb4bbz_1ao8M z6!3@#Z&^k5+g){)7~#E4wv4j$?j@mesq*FvQ*dlQp^a`qQVhLWNTHu6^LCdg9$hM} z>w(0v@^7%R{+0e?^pt2R0dS4!)uy)RZ?jOT*xLND`U$93Ah|~#;FtBx)E!v5wPU#c z)9sqNE@jw9=7OqK7oZr7wh#>5IWT5m@O~knA_y5w4U!gl79KyaKLC~jWm=0WWIJ3i zAi{hmu*W1QoEU8^2#V-b*3(gAyHpe-c4>LSj|vwruKD6xM`Zs1rrMN0%f!`Ik5Q_@ zSLOEtKAD&E+z%Uy^!6~@N8$`@J4o(FtS?(5HdkpY98!U~RU60k5k*&4hdYm$v90sm z4l7{U9`WtAqyuRv6@HmHFg}S;5~#l*_r&rw)oGadj&R4=7%Xda$;D4+&1dQZu%1qv zPT@ec{{SNXkrPSgSQUK0e7eEx0OY_a8jNJxOnS7-rloMqL1u$bTAY$woS#@~P|PYJ)QJa+AX~V6wU$oc{oIJRaqC2TkwtVt7URXG%t#Iqsrqu0q2I zS0f$eHu!Q{gCR3p1MUtp3Q|ECDg|U@f<|Go-xQ$`sY-#zY|ZxgQRvJHpV^Ghil&$z z5bncAI@{a@HeTfu*epcxkV_q!d?OMDA-_bLf=29RcExQX9YdhEQDYpA$~eG@c8yk| zQ4k2t;yYN97XS!N^(XHS+ffHBXW6yY)XkP8XA;|zgsE_V#-$rSnvI!s@w=&*{RhKzvhb0l0)RPEM_1MS>!mn?vJ4@aLoy#63y9rK$v|Z$W zz;`mI2HVd23H;@MG4v9OF8ON&p3J#0D-g+br?L(d8Lrk+cH*S&tAQKM1?@1mh}70{ zfrP1P0TeI5d08+5N}R!n2|1WE9{@+hFfcL^{e@_xGUil=awy#{7UOWl8vRWy?5oI$ zr|{HJJ5E{yoS2PqIA~Sys6xkKh2kysF!M0#IQq?1RsyY1ZsVXcQ|Gmn;4$RwfY#gc ztp5Prk=XhfZ8kl3*Z7QQnwpI{+^+l?YS>X6`Ka4@=)g-KuzQ2|diWNBluc9umB$Bh`4ru(n6 zmd*izd_I*tJ)BMYza@JvAh%VH)d*ifZhx7$fYOzLjkCzq2leP-H*I72j5hB<{IT5M z+>$U)w0oBvsp@5wCF2^QOH1fB*zvhVz2KiT_W#09r_R@l{%j>nR-qjY8V(*$y4z8ei%5! z`Mw-{cdzzCIWW=UbSk+3xP|;GqJKz(m^2boDm+|37{Zx`IPsIeGcwa+Jh=_$dy@o+=SNs4hQ+lwP}=bTlRcQ zY9l3BbTCzLsH1(HWU0qMdW^@@c!H{z36cmNk)EAFQm$)pVB?Yx1WL939c5LI$MUx* zk3&5!N}ymyeY(V>#BSAGg{!~it&j6dJokHF{{R;N4$`BjY=ZUd<-4JYZTfent><SO6sV;qsoVg=BP?dS$X2Ul&2`T+grbt;|Q@?q0yw4pj~Ai|2auL%~) z2t1z9TY8q~Xw34(&UpDx^)cG!H=H0s-%}5g?4miU`nHnn$^-_R)EofBl09=0oj$8q zcP`baUt(i<+%DU0ZK!=0))!kCXKw+0ly^SnEp*dT*SU}!D)LLTKWSre&zHrnIm`9h z1V*^gN(HD4upPjJ*=3r8iW9GB61K1gEnxB!YtiM|v;5Yk$ zH0zS~z2ppKS89)-g4e*Moip-+Wj5GN!m5;iDR46mor>FY1yh>Wu!7_WRDwIqEIv^Q z$$+^86+V-g2!ol4hWmzOe`S-2Qfuh*2q$>KnedwMasr8i04H<>h!MXw;sNa|4tF62 zTT`h{Yd_^HHsO^^{{Z(^0b>H*Q)2Hwlf<$zt zz*wq+Fk8HA{w;vEF_A5=R;gj#P&g#_6Hk?{%JAgy%NcIIk(O6NS3O{-M@7``7x^VX z62+zvJrXeR=?^ZUYfvyh-Z zNrdlZU|E|Vwq;t4J5|Qfh2jr7z^C&ZO6t@TXJY};J2~ihivAf|2Fqp}d-7q?7A`Q( zFeQSAQ_C^HmIGxyZ9p;S_eB9CoWv z9sdARLw4ILAygk&>PdOBvmYd>g_{l3Oti>=ufql%voL>aS-BaB1Q23sd>W-!=(8t? z9f){4EDb?8CbVG05Z*5Vje|mCzB?O6zr@-)jE=C6iU+dI0pg{i=h(9mKCg zrl-!UHW7{we^|?}s`A`8`CLYAOTFCL*!sfDYT5aa%)aL*c6xh7)K=wGm2OTV)u3=N ztg3O2@f%r9W1#C6pA88?9&0w^9j+yp0CNzawao;gG10}JLAR#Ipp}$wAsZj1D z12_1cHB(+ZpcXw_gMtgc4d^??O&Kar%-crprW2^Hw)(Mx>xByZXDmly7hLP2rAY${ zgZ|2Xi#qGzF__yNuwxzgtmFJr*r(*-u~IT~#VBv03r* z)SVV-(z10dtbp=Z;d2|y3#LQ~&^9n3&c|{{lrt%2GM80E+|XA~9Eb%5DFn*HH_Sve zxjVxzpz8xDAi>@%1Be7OxnMi6HyYN zPW_>#ZCpB(>$HIpo!zKM$8ZETvDv5T+{-di2bBGfALtCevOv1OuEUaj5`HhF2la5(fq3lin_Ro#zM^b(6fT9~Nn->8XgVQKKRs+VvBUDcIkehrnbuRizX z>^$e(pZ1pOx73^5liP(5Du>i+6|L^7a;xo=FovxwDgnVGlkR2g2&tBu`U^iZu>SyQ zr_#B5h1!b`?o4usl~O9JvM*L{*(aFc3OlAxP=lmJWm{+cU?s_Sg z+lF8icP-v>pp_%kMe{-iA_}Ng6KZXQl0F4&Bt}je3}+@Q@wyLRfG}g>&-P))&}E6@ zm07Sb$b{%qBeV)kJygrtf~w>RmnD&R`^p-FWxy5(j%J~%V6LdWwsgeBI2KyRIohA^ zH);u?FvEZ>>F!B0T#J&Gs&{nc58g9;_iw9!Re{2^t?E8w(97&`nDu1T5FX47dmki_ z{Y!P4d+as6iM)Gp`-yc$t)qfZ9k0W9-(f!{Dbr%r*eq@B>+RS8M?_ndtMHmX;1Wf1NldI{!V=CbfkPbl$ zvHpTL8hs#pZm|5%(pN^5Qk6ZWcy?|Jzx+*2q?_(alfDNFC=KM)?$s&h0}KBE_A^gH z5tUT~g&n0tGkmW)d7+)Exw;7STHHM^DUP8J@4 zR3}}beo;Qs?(EzzB;x`B;Tn6*nR{+zRrOKS%U28z0DxGe^`C^ZEr>xd@*|_PC862D zjFxg>hD^w=p0HE0p^;0aGRKzH7-`0?HT@S0__4V)DN+0e$}cR zZfq)#pajcqsYbe$)Lc2v20wEsO407BwF}W|azq-WcWpkTCuU~GK7wiML*`j+Y(2&o z>mJLMv~1T_IL(LxkLk&m8*$|&+c-an!d|po;@dgoBlKTv^_03gYS(L+5ReWbc4FtJ zNW$$iQo%xU?cbSAHai;C&@NcFR%0FX&foyO0rU`~Z*(BA=z9nk!YdAQWrqsBkT$4P z-lU&l7OK@3@(7xZIHI<}1QQa=jaBDi;w^q7TG%kHz%$6Zcn5ytT7erZ9gmvB8yERU zdA{`&uGHuYbO3iUq)@9~=kCX+>><)F>QbdFIkLn4f;~(f>|>rmv%k?}c!rzET1~D- zaz@lq>P*ts)(T7NF6hjpEo=S}H5TD0r4!eDs*03fsHEI`Ff14L92rtE0>*Cmtpw3p z#dRKQ`yj?Yd7vZklG5JxW+_v(fjHqBQFD~HQYtw5kEap5#i(57jQ&+lvZLUB8ivMK z^IWK60Sh)*6Mj|`Y61CX)dFsKHp9;l^BFnJBoItEB+MPGU_t?DxQ=*%)J?)+vOeMG z;64Hmfp__3x#ifeqFz(gzWafxgMRpz(oXt{ z;lmG&PUGW7vWqE-&6Egz_g9O7}q;4_u$>L^{GGVQT z8f6DFa;)6H5%;n2E7CuhGSFzRM>Z&xc!rfqxo#i>D+@(*KPh7?VA=DDKgtBqP;IPK zY`40SKgdeAu}vfSW3n$IA=_vc=~KH{_&=~-Os&iPlTEGEmG58(r2hbkzvN9rUdgT0 z(Le|LnqSUz9{YGx^)pFz#ATkk{{SDOG_fOCvN=XRl z&HCd}Zre}FP9oILf1H)pNejkFnugZbZkKh5yVHo3S*&eITP;DjZT0LUn=f{(C=3{r z{IU}Y7)VA9!rG^G~HxtFI@fT#N zZLgp!z{k|W`TK5&BKKh6pWaZqab}e%V4Xp3X3%TG)ZH)yX4>I`sqUg06LO0z&!Wvh zeS(BgJGCZS$O|o7Jryx7$$HYTJzI&b)Uu-LG^(GN?R=P~#cORA9-tEu^gbU|Web4h zDgIoT%df7WkW)gS9ihj-WWme<8e--mGH}scb&Is~F!KZq^9uzAZmT2j2q5^LW_}x( z0~Wh$Q)v+_sc>^KkOa>{L57mV2X4m(UFLbKcEQ}93}EEGZBlXg*4^0wX0`vWrXZH%pv zjpq!+pHmV1Ua?{|n}`8gS^PVCU#v=h7#k2TL5Xg+#1a0`Q9={J;%eUIRH+#=kX&bv zSOw(j9qT8LF*p*9T~3aC)F*}~BtTlytTGsd-Q3MyjSXKSvv+QJ_Lk}N+G?t|4n`a& zZ>-YORaG^GowClrN}Bs?c4MdGC)e6lW|i`X3UB~0qS#0WVa#v)S5foWLF2S)O`ciH zTn6FvhVP$9LWUS=f00g#hgJx?pAz@d>TV9)0a-YXuFl2xr~+AH?x_p6b&SZiJ#M6S zm$ibT_5=`7Z{wQO9(qc7wwI^^Ip5wnps`A@NAg5QP%GMd9A*}%hhh|oVm8IMBRvVh zmeS-fv-~V2%U~x>Tc_zVo2tX;>8bvcMx+L)6aN6Jh%Im_8v(?&+Ua*hTF`9D^_Df_ z)?bl$QhJg1FigNC%qxkT8Ui>oY4~$7VzQZ(SV9lG?=km_M{)AaQkZU8(e zIn1)XO?r@WtT_|yI#db+J|WU-0Q)Q!(|;*aCj;EiJ#{wQX=1B|$El8@y6O$Rxe5nwQ}u~O7bl(n0CeDU z={0^^t&Z(UU_yb*8LG9jX$T+MeUP(U*qU84ikk4OM+-bRL8;rR)u3WB0O}0WT=+|C zZ)OpNjz-o~(Q%O$+JQj80frDWlSS%Vsa8E=UzE92U~*%%9(8HCGo9(wOG2X=v*dD9WE!8+SqmIqr_7Ji@nbe;{AHkarGEA!V zET@)-pgcm!veqn_-M6-VaTcJsmFW}aQ0{pGS*X@|jq?(6Vbu6;(`cww+<;1nPM=S8wv&WaQcD^37kX}V zOX?qp5b?PG04{oDK*_&XUsm}Tc9bn-8_JeF#}cgvi>{l*9-~Y22ceov*X{aL{{Xf; zlAWTj;_2N&9^?Jx)C9LvQEJ%ywkAk{@G;(1RUp@KONUkLW~$9ddvXzaOb>yMGIIzg zbjTrKRbpUgaz5!UCd6IcUh+&2pe*4{URXCb&3L&hsE$AZabfLRV;O#K< zID*xEpfQQ22F+3~MIAV6>X`^<{Ib<{T|(}F zHfghOv6ms_29|eR0W5Q!_z^F&b%lwQ)sUc)Wnt{9;=wV%9d>Qz@G3(rVKk*%~F8+pP10FP2*ww|}Nd1csO9;za@%7-ATrqxx!eb_ipAYk=iYXD&!nPr?)sPNhFD+uU;?m-^|(+AmL*8v3vBvYSj96Y2!W*O}a&+ z$OPc(0`2~WKB@ebu=!x_cl9vqE=@gSG$&GJ8f`Nwfae?t9|Kf~p#EIXvJs=ylJsXB z%KSgX)5QEZ1EhcN)2P9o9Zeu;sSdV(d07&i`MPc^Kg;@O+AY~_9%481dPLIF5juUr zg8>E<-GhkYw!^~CK|gnMq4$iKA{~&U`iOyMlB&1Z3mHa=#ud%1NA^ay>xk|^s1`F* zQmS0w2w5_{#IO=r6A_e_Q#Y~L1_$+lPHo_x05eRyzv7YD9wHlZ`g%%s*o4aLO_n1s zWelVN6IFv-p>i@z)Yi_`qXZpTiK|c>=ErY&sIZXRy(Nmf@Wo@-f>Q(DRaIVNE?a{T z_>WZwv15TPI(u#|fkDRLLoq(U_b??m#jk1S@dyMDU^-*>GTlH4Q3+c1CoDkfV%+uD zOJ=HpB#+Bq)7}kT#jwVTi3%I}nEFS!6)X`qjytN3q{LSj`Ap#Cf_(%JWqfUOU|pl8 zPI&!<7wPLNyKqm}uLd~Ow!1+hl)(iD^J961G%WSE^5$YgW(p7z>i%Mze z!zY6*@cWeM(AeXkZWef#kL=WVmFg0{6;xqXd9EqaR#|(rIxql6ByzxrJmmtifbovf zmBYKB+v^37x||Unr}A|i)O}#BP1JK)=K?dnHY-%$m>FJ{kmx%7%(UUA+~OUC7)J06 zEn%7X^ET~@^@65jye3ylAC_6CMqNF5B5L(_v)GN3Eaa6x*&>{@dfV`hCFfSV>@ZzQ zZZU37BUYpykRW2D%I&q3rd!m(U6?Ak^%C@{R)7HQH46U#jb{hGA*!V+g?kb^#j$gH zC_kW*Pwpm={v)7OJ-<}ZS6erfFivoKhbr3+E{?I(4Z;Xu#) zm|E%%2y}Dw1|7E7oWCqjatVwy=~m6Gpb?J76}xuaHy^uqFzK|Q0;*J!ev+%c(_zaF z4q&<5SL-jd5~(glB&_ntE|CM0YnF^HH4ObGr^IP`?FapYt456OjLtF0iXBBL0UcF@ zyB-0;mEHhst3tb2u;nXUNl+ znRAc26oqMo0ZeWnB0b{=p_+T!FFZPon-zGP3h5%1Ou*I>zXSb-A*SUZ&ofS$Xc+`Y zOc`4SijM5441D0I3j*-0#eH*(M)N^79AouIDsbd4^p^lUq}rW;mn-JbS^a$_#QB^s z#~B=Fw>{%M4QD1z-j_F)&u@x1FGl$J_ynGIY&EZx0%D!uhYbmG9UH zv}%x*EWP62qc@5OPg$mR8|YjaQ{3XD2uDduGMD7%nWdl>Wl@ubiJ<{iRDyXTHCKhK zmKn|^jl3;qd+{y&r_4UhEzVU)%qRv&FgloTwL}_iHs3)nsrS?Kj_WMA=Ed7MnL{wf z5)2_swFAQJ!|vq5haxb*aXjS^s>*bnJfV>7b%Rx$TMc&~orwO^7769Dp)k5E%D}=S zgO!eq-yx2wLsmeHmi)-%5rM=l{D6IC!ciLp&nYm^hb9o4ot=hqNjM*{i+Mgk|m?(ce1*=n*r*5 z6>%#y;r!K7+p^r8dSLf4!7XKn%T~o!%c1j8Op$CU%19+9;Vb(O05r-g37btWacfb5t`j#L;;?pYKLpkvZmMwvAAyDd$E zWq)P67i098?ux;)VvICbd@#a#%|^gg(%P~~C$x5No%7rXzEu~2>|!2QlpfvU*ZJ_o z^8>C(D+Vwkmm7}}Qjqoa#H&Y%fI*F{M*lua5JTZ@X%YDK9kCS?w&ofV44x(lg4dJDgJ001Mjh4>` zytPSu5=Rrw%wy>03c^QhjRsY-1aU@U|0VDwm;BK%7@K+U54iQa6fNoTB2NX2mq+* z9sH2&r~x}S%~f}+YF*8a01BL(dI@IFo~Nz)lHGCd8SDTd+6U$`rKz6f3<5Sia(Lom zg?-xtrW6r^-tg9?a0vtf^qJM?9gHaCD&%9Yys#z^Wr5@w=G1o(x$hdpCu0%oF2kr| z6F(@If_FaPIR_kx_Rj{qd1QH9jKop2rb(O51R!A9|pAXZARo_^N z;#CS>j(d_Pmv;Kb1SYix35toE9f61wB#4ZMnZDNm&A^lQaAsjbxSQ(+XNa1r+%n`D zO+m_3V$*Q|V1w}E`#epq2%G6amv33`v$F z63PHPa57+5!P!_nf1bniQP9MvDVHZ3im{g(nCxUj(Ln3A{QM#%$?cOLjWO9qoFjIQH5>^zv|+PpacZX#7( z;hZxg5C(bM7gerz`NGn64@JZdLshjd!SxX+sF9Ym^(H;eSg@@G3usDf z9%(uA3dE`RJMMCN&Hn&DnS}xu+06CPO5z{Sofz|A5 z2dJKHSinV5@&O88cy`O+1u@9Etsf}CE9%jkeO#2ub_4mF(8qRss8YxR`wYtKO{2e4 zoCpVq++N+v0Xvj%oxiMFg+ohO8*}D4@9ZO56_Fcuf0`pJ)?BM+q4k7TenY-8*@CgV zg?JmG0Fwc2+%YhP&IG27K>>#1Iz_46Rlx>ZfAdBs1k8Nk!l#ZJJCzP|07NG51JOB!d6)Oi03w4kez^r%9v?ZpRZH>AWMgx z%VW1`PNMwQ%hR6Hx>b=7md1BtGd2mWK>Vsqs=)Tx%TPYV?mooNC&hf1RuzH^204r6 z8|xXVkOAl-ir)4hkOtg$mIbj!P&6t$v?u&R1=5=9P|9(JCzHl_pM#r9}2Y3C>TZoz2=;P zR=$F8wsb)%(}P-YM0fEJRGx-4msy|GVFp!ut!{9}U0Q~zO{xC?sLazT!>=<mNx`8-tO_LH_XF zso8ZEqK@*fF_>Dao|uXgDth$!qgaZIj33OytgA_C`Tqcsd!9&@YCKYgrI4kl$AO*( zGEbqKs7lKnhQKlGAg1O*PSpg4>`Z%kxSuZ=Iqwm}S`HJP&FwdpP>ca>ryoE`{{Sg& zr8ksX=e=1&kAABh0!Dq{M9Jl#+R4Fr zr+ZHG^ZuDuI&fjrQpr@d#xCbDrTiQgn6Ug=74G>Tyfluv`?#2Sn4kvl9=6Ju z9YF_~o(YA4mR6<;0}E1kcZstbM#-V8ajEka9ECX_*y^?SiZnWFbvL&}tx1%4anFzV zjdWR#G_-ytzrSVu=8C&jJU2k8{{Zsd+nE0Vu!Yr29$;1A6D(nXp<^G+Vk7}F2H&g> zm@WGVsLWZzu;32DEfle-swDnm!~y$?AZdpG?h7)+*UzuVA3VrJ*XwEWJpS zn|^n-{YHIageb^CJdSh3O&^Kbq184ix96>xVBmEUf}39qI|by7@Fh76U0dhcsJxf% zar6_+(mQ2x&$WNgF$%S4szD$rZj!S6;SpPq4_{fQZ7@W}z&9~Hfa+$aS96e!R)sOK zd!KU^tKQaRdymcg#k>||8wb3rK}y(nH=)OA=7wsU>5(-y>K$!G-Re6_-S(=MJ##Lj zMmJPL^aRVD*{?i6!vZqGz>_wZJ{q45)Ghi;TDSEQuM^Q7UbTbT2n} zAaoxeGNL7zcc4v1msH~6-g%3Uyf=I@vt$V31RNL$fza`oUbkUX8hcn0rY!+;%l`uwv)puqr$v7 zxH^3j?A|~3Y?f)_I={uWMw*WIUf7-!p=CjPYi}J&aF7*UsuVyJ8N?(VnD-_xAy1fn zPiQXX$uSa_6{cESy~!j4+F4p96$WFTX0%>1&TB)3Pebc98;RcR^3~BYF9e=R`M=D{ zwJPYvgDD%BnZ2^e-ppGWl}57u zRUB6SjV_>N*DKhsNE#8yg$@i4$eWs7qS}h=YxI?C6Gu}OoDl$h$|lz% z?_kZ5F%}ej%&ahotgILbq-AC(B?wqo0b+jcx5aPrv=lY3-1JaA&p|vB!Flmp{{ZTG zvHt+l(AcbdGtUv$(e^zkO(u-=(`lD#pRPGF`)V4W616lsZn?9m`U&A44odh=m})g6 z{{TJQ{{TTXz9(UAP3+##3le;ye1ZYVKeT%nE%L9VR89s2!&Q=IV|*;y$_BTcEx2 zAqNNUVEiy}#$a$^D>=b14}f6-MvY1wF%a18^JM+Yb$6v2TAtgS;Z->&_MRKyDdztG z5nON8bs7yZz3>#``p=6FQJ;U!BV~e<(g|~EwO^ma~sIy7n6pb}ZF!%oe<*<91t?_Hh>1t-~_CE>) z@UP+E9m?yqY2^OCscwX6xFe>^U(Ul>l=o4}c{t#?ds)2*8JjrfQPnm^hk;Tr{$jWnrFk z1^8XOLuCVGy0yYf((>f@oGDfy*okyFy|l;MDY&`tZ2MSk+0Am2!31%{5||zN!9-V3hL8Mtna3727Z&o zd<#@*@hTpd2Z!v~?g)?M4-Wn!Pq%0rV(zpoyiZYdmt_BBRKstF(5b z?0y$m(`g5%Qwb!KpSpK|23VW`IVASLm46Lr&}*>%CwgSn-3Oun0C67V`%a;D3mzp zC0XDDD;UI8NWH{T`4M``{K9sKtI$HZ+OfhF?Hc9JaR{c=>$GPOs5+f5%%}6$=^FhZ z)ag{%fMvq+SKkm7%AO!b!x;2fx9KfvZ$rbO!4pg3T4CY8#oAQ08-b`F?;46vhPaDwGHCi5;tv^x$?mgz2^dtD2_+SfsCe+`l3C_Y# zQ3ITV3O6S(Eb&hQe9%1I8990%r_>(Ccu(<_KW@+;vFow(T<3p{{V?}gHu^#*C^ySPo%z=#r!vM8zUKvq-6YZtS6UiWJ=vS!=Ry>tV3|JwitZF zYcc!0d_WNlQZoQvM@dlXBe5-~w~~4p;#Hh)+$HxTtCL%|1p3WJ?a6QMR2kw3NZm!F z{2&=<&z+}G-ng6j+ws83lxVbr#k^5LKyjv_zn%X8Q67R{;+pr>-sPC^x$GsLMS5y@ zdk$=~vMi#t8eJbKoE9Bi&nzk*&E92xJj?=S*S5p_g9Gb4-D>yeCT_OL_y^N7Zt1m; zz`{(+P6QJZFc>lL0({sV3=jYu4h#=TKg_Xw@ZO>?fcUP>U@zgnV3E zqzFt7mR5gg)#@-$9SpS=ix1>+!GXAGM~rwq{{X_?GE1Y@MwOe;a!1s5gGH&;e+7RO zo%%;5H1;>4=s(;`zlHo!^tzDdr}?O~^mK&2D`I0`k2Ogi^G&!pPLk_2Dw=gF2Y*Sn zvreoBW7UlET)j+9P-YK?1mMfA{^njPA)lG3YYPT5R&2mriF$RHM^ZtBe&{?C64d4e zazuS80gEd3GOD?2(2ny>tzW}+Wtu%okXQzej7=@X)FC9Puny9jbG6$|cj;+juFX|q z+PZI8EMoc%ODX*so+m|~QwF>C%+)=qS<{S@^8$Y;^)th~L>(wQ0w377tg&51061g8 z62At~wx3Ui)j0fD{{Ueg{aV(5n_aLy$>2myMS49s4Mvm&4f>m_H|}8Ar@5O@ecs)G zn{@V(+~RU)4yIrjei(uGkt<@0fN2om|U&J57`ad7gJ~ed!UvE$c{253803!bY z3R~g6EaiM_N6mM4_F*pY?-#p4u0prca4p*ez z2gOfO5SKf8Pb?AT&4_l_8}tafNZHKXV6)+gpMx(XO}sR!<&Z znpbrF=C?*MVmqGXiA|$rsbx>=+9iX#d*H#Z1Ib$-)DY*r5$qi2LAvlrO~!E zAZp0z?=Mf|8fz2>XIJYKz5g^!+=+y`f0Ln{6!@Xard_-YTfQnlv8>`fc=<(yx13`xo-2cr#MAI~~0GLiCS00TQcd zuCxJ9=l$VLLW2jEp?(~EI>HHX~P^X znXLZv?A|M@)3+NfJVOtP>AuL|HS5i6XO_DZav{`sWt3g~r2hazE=Ak5x90|3{6pZU zL#jd@Ku~_23>pnhmFjcmuVWITKpe}hBmu;*>HwMJ(qaO?acn|JloN|M6T|4;@~(YM z(jz~YjL$ODiR32jPZpk5zT@4dPu&if?>tXsnau@)nQZUX10_k zhlI8oy|R|TD>TB*+p&p7>{vGJ%5>^TtxJ05c`U_fp+EL1lO55wI8`%Fs_o6JqBWM_ zE(fG4piD3jpN?}lFb%-ZkdN##W=!MZiIV|sp!Aq*Kt$e52>XD8lRnF>qS4OF0fw*Q z?O7cy32*ffyn6n0ET`0PNP^20Gi_Pnnr!a1o!DTBq#CCc)T*T9O6p;47|NUx1@`@< zlFZ#q4BN{5K4Z7MT2l%kCIUSmCy$76gEx|4w#zccI=Qb+QMvxS0qSL57#LJm{cn4^ zr9a^HZ*vJHnTs*xlgU4Lm_mjTZeDO23)HM?i6SlFsS&jHu}+Z*+hCS;qQHHOv;r=n zTzY{i{$*;+r_18?CNaQP{*ut4Lp~;;i~!M?{LTRPl-FIQ$zhJsK|WdqS(YTgt|7$Z z;nsDXW&s~_FhK<8;BhhV+QrXBpEgfnm`Oj@6Ov|2P7EPTJi}(M!u2PgKSNoMb)aqH z0ZND!NP|y$7Cq&5s(EY7a`tWwf?+=E7NNCZST}qD>kD_{1X36722u>+uqLMZ+PYI7 zsyoeXRU2$o0CY)hKb;63r|zGCaUC0da}At;!Y;CtjBb}`U94ZpDZFCduwLZSL7JT_ zWp_1p@)nMP1djOV$#qBS~4aCAal4ERf1#8f=M{Zeh7Pk`u zaXwfl1`#^q9Q+6dAA$Rvd^vzF$+nHhg6F5Ogv=+$gA102Q{f%}O?9rQ;AkKL8ZKIWPzafOPwzsVBIMY-c+{ zN^iH7Ve0WTS_iy@)CR~(*Zo>$$siA5+wPIUf)?~=`7LS!g~`9R5xvW>#|m={KC6%@0LP3C2uN zFgb;nC*zFHvkmYg;C=`0<1Gu>kJMn_wMWXl1N4F&pnNk4nI;Ro7eWZ)w{`yjIJHyq z!$z%3E>szy@og9ewGW8tW>Lzlvrd=aQT@Yxw`sB}RE9UudktH3dVI5P(X#c_89E&ps{oFTMO`M0O$u zV~8^cKXaMS-#GvtlRpjQ0UN0hEo0!RozkyZjW;O~k1H)ucFjsLI5O0`50L|+!08H{ z_KVQL1kXrTWPEoiAWFRE3`$~CeNMY+)UG+k4->gox?LBune#agE%VAA(E|m_f?Tem5;l^~)(tKApiHs~@s$GgwI3 zOEC5mUAU1&-1;e=CvJ8v;&`E7nJxXy@d9$v#2Ie0XgQht{C)fwS_ZS2i%?-P@GcGv zw??JZw-a-Pc8Pj|8Y>A~1H?k7 zEdK!eLXP839;=+?tWK_d@e5tP%|l&pdDq}RID#3K`?YwMk6UnQ0KR|>@OJ|n7I#63^|51WlEX&3K+_%LdB+?oPgp`$a7$oSz9W%Sk-Db)J$=W#tRf= zz*b~J%EU*Yk_5C2bd2LHV^_piPD~~D0}(i55r+rv3n?O1aDTL&m>e;cn^Q5gY zpAI4W;9v+!kYC*_?j#NiQ&L$vU zrVBbtm3LJNzROM`xX3>eP=%MNf0uIOw7XTM+@R97^|L*HV;f1W+o!+IKt61CGWt&z zvGy|iYue2TCJe$355t^^gM$HCkuYFCa-S5_u_!m{KKc)p7tOMa)g9Zl4SoQXWNC)A^-`g{I2i8{FKu4tVZ6mbarzU4R zWxKyLV~m(~nCou_I$1z@!?85@0Lye6;=B|aZ46kNbG zt+a>Cvm}F@G^^)ywAKDD^A# zG}dEKL@~o+s97;;GRJ7uGmEgpDo?}sxuPm!@E@K|%Fm6oPK`?n> zg9$hjoRa`iK4=KRFcjd4wTLEeZn4P#7)tVHV&^^PEBG>@#AZhMh%uNt*h%#;1|>-| zEJ-jzJ~g_hDcqVX9r2$>VHg+$#5=d4vph_&Ud41hm2d-1B}$=n0C^3eU6x6=jZ25?tLDY7uj?YHM%` z8F60XTU579x!WAJ)4`4h%46VW7FgC2b2g9~AjSkNMj_kq`91-Zj`QSgpzfNNXY#>w z={0mKJ@qm#sSxrZCTHM7KyVBWu)-!AA{x%uF)pqe-U2@ISh!&;Y>W&*`D%o>PQ)_9 zm|;URpMdxRAcheEm=mn$;B`zR1ci7e1y~v=f|WQhs}Z_&Ifu4PbH*4)XU6Dk88BEx zO!6f~a8yc9{xK?1!UT=C1UNaH71}$yN7?(g)ObPN# zDKLE4?G+TH(N~)_PPr3F;yQ09PJvW+n_aMY`@JSc49v*EoWiY@jZ+0<7UU4t{?ItV z`|4#ngdm8#?GU45Y+$5?g>ndlbRQ3BSF{r!0~@*cHs~FIf~7%-nt#Wv$6m4LlJxHwFcPNaiUYng++s0|*QOXD~G0ghH;wKt>EePZKu$ zvFd@Z0|%_#Q22nP39yl#BU`3(QOirqx0*?YZZL>`7@W=~CPc`9z}dtMG9|P(5a0)g z5Qwb-{=jW9PdJ-rv_K9-RVNX57_nj=rdE0wpuv#^X~~Js8WmhCM{;?L<`XgSDA^I! zHtmMh&e1kES+l$-cxkv}eDe4&63hbeEEx?0uUU!DS-o=wsKy;TkS0lsX@I~rLKUcd z09Z(AhGCe7whtKCSw|DpL6{G~AfNxl03{Is0s;a90RRF50|5X4000000Rj;aAp;UI zAQM3ZFd`!|asS!?2mt~C0Y3m~6m=9u5)igW`1P_eNb?PrIh~H@V)12`S!ZD~%M^ss z)f6OFSl%d6I-O28PK%6v6xWEeu;e#qV(emT%quono@`9)DGEiFMoe4FEOO0!E4>`w zrY=b)NBK3TMpm?TI}NOQAz|S$NW4((2tq7UWO*oDiJj2oKZg;BO2~1Xyluww$2S5q zOp?}(h^Db0l9Q6my4n*RV>J12@1sE;KHsS!gogiy=E zB2hy{Gs4coV#}2glRM?1#84v<9E<%-gejd=R>wt%fd*P*LTw6mh9QY;SW-ILr@EF~X!yDUK{qZ0SNER>?)Jo}v=162ct2&62A}lIsOJgG^XLLCvK4dvL9B^dSJrPPMV#3i0X7YxvCnk##)j!zM zSdJ{VA(1>IvN|GA;b-Gx<6`1Mie48;;@pbR@pxJk{!sq_#`W-@#S|rn{{RI2E9D6^h=~%@#57N2RV*yFEKxMk!cWX5Hb?pwlya?& z;)*Cm=!)4CauM)={U3r%@ePRCLKC8H&I6YV5f(cMiTraRt0;=G#)d3M(hEl+4+s@( zo{<5u$V}{Z6pJd(6-buK9W)r3BHD*juE|D_n2452T06>zwo8ddHjaaZ5YG%+ zCNW_W7xc{!5EG&^PeY?b-HSUa$nmbD;C6IkjV+UvvdGBek7g%C#vx*4b5-{|CqgJ` ziqR!#rYyqje4S9a(D{)rM=cdtiXr|eQnEu9WwNM6mw$6vLU}~QBqS;tvWi%-A{2HY zNMe5t2(N|lG(4#ZmL^SzOiWK_Y&KYsi#Ya0XqGgx%MskB#Txo!!z1ZfM8$=yb}ivV zQO8q^fYBvPmR@5bWKh0a!x>`HVwsxK1$g#HlQB2sR&N&en z@PtuG4TwX;aNhKKL`;Z}QB*&bp-58rvhfB%=wreohR(ub$~+RWDiRbdimbFakKtl2 znHdaap*%$5!@@#X7)+OP|gvhci<5tM=A*doU zuL;SW*%qI|8OSe`AvzF}Wh^w=)rlgIlt|Vk;XI-Y;At`P#FXOE9#)97(IkZE?T8U5 z`^u`=;xjp9jT~rz zr9zdO!&%|^kd_emR|Sox(uyc?8WBWLkdUyjv-E3nqIxm@ z8lOk|iMMZ}DTuBpLq!#YC5pmBh~jKEl|uL#^m@BxM4EWi(%wr&z>6bK3W^>TiSTSh zU%=ut@nYwqt5H!HIICvivuQ0gR{CW)jpMP1LPEn9BF)LHQMDUK9GjW4E8(^gAOIK+RKY`gd_rkM>}i5dR@ zMKf~4Br+1dj~OW6giwU2MB*rp6@-MCpxO3_=n>OMgc0>xpJ$>|{D&(N_&aknuW8+y zadp|ZGDAdqb}D7iio{knjnW%LL_kPFq7zW;`n_MfD;Z_;mxhAug8WAg%{J1eCuI~$ z1A(xLq2Y{0iIePUkP&1gInliu+TBtUC~+O5al4F>qSENo@MuY*IhgTyiXLA_=v|H$ zg%*gRK2NTBcLd?iaX*q5aBQ4o@jBSbv#Yl#uZKH6S zWSt!i(Wvc+Lqu?gjp$M@g%ETh3~4dJp;)YhQdz8B4suelYbK58w#X_*)h|aH^woHd zVzCw3JStv_X^V>?Vt8U=Xz7u3%i$($v+S{HGBK>mM4N1ASVGHflqgyhYm+qI5Ybs= zU92`+!eWyUn=2FSQDYFAHuYlQ*|~}HT`@ZliW(9cAh6KN=%EP}(RkWVp}Zu>-Edgi zg*2Cj=J8w^4={$h;IiA1R?k`&rp>XUJ?+Z@mO7y zks;zgiz4_kiNvbKmx>i3zQ}kiwPw*F_Bg-Ll?bLTu-RwopQ7mX5=?kRIMC6sdQOKL z#jO6w`GvxY;GZOU;U#2{$k86W7Yk<5DiyK)KJ-gMQ5;!D)+jP6kj3#VGHu1mjSo&g!RYD#04RD3^gTKnp@?4!#Dss5GE8`8-Na&%7A_JgiVW=w zP*NLAq=yqx5)imwH;$-Zk3_QZWCh1A!HI}*9C#@-EkSA_O&gG!HzRU4(iD3VE{i6~t3!ddv20A;LXkeu#%ZCh@mP3F hOn73$B*?Z!icH+dzEg@!Wey0E66=J#%mlGV|JekVBiR4| literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/image/3.jpg b/src/ng/demo/src/assets/image/3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4897cccf8e95b64502a1f95cf6f5b4ed11eb5f9b GIT binary patch literal 100875 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%0AF4SkvITPC+z=^Sv^M2G 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 z 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 literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/nav_logo/logo.png b/src/ng/demo/src/assets/nav_logo/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..34f8774cb14fe6fb2352e487c4c6d16952abd11d GIT binary patch literal 6847 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 literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/favicon.ico b/src/ng/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/index.html b/src/ng/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/ng/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/ng/demo/src/main.ts b/src/ng/demo/src/main.ts new file mode 100644 index 0000000..656f1eb --- /dev/null +++ b/src/ng/demo/src/main.ts @@ -0,0 +1,49 @@ +import { enableProdMode } from '@angular/core'; +// import { TiTheme } from '@cloud/tiny3'; + +import { AppModule } from './app/AppModule'; +import { AppModule as WcAppModule } from './app/AppWcModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +// if (environment.production) { +// // 生产环境 +// // 主题名称 +// const themename: string = 'default'; +// // 加载主题CSS文件。只有生产环境支持在线切换皮肤,所以基础CSS在angular.json中配置,主题CSS在代码中加载,之后再应用。 +// // 会从assets/tiny3/themes/theme-${theme}.css 加载CSS文件,放在head link +// const link: HTMLLinkElement = TiTheme.loadCss( +// `assets/tiny3/themes/theme-${themename}.css`, +// 'tiny3theme' +// ); + +// // 原生支持CSSVars +// // 在Chrome下,新加入的CSS载入太迟,CSS样式生效迟,overflow等需要计算宽度的组件有问题,所以要等CSS加载完成后才启动App +// link.addEventListener( +// 'load', +// () => { +// // 监听DOMContentLoaded,是ng add @nguniversal/express-engine生成的。但是,在当前环境下,css load事件之后,再也没有DOMContentLoaded事件了,所以不需要监听。 +// // document.addEventListener('DOMContentLoaded', () => { +// TiTheme.bootstrapModule(environment.isWc ? WcAppModule : AppModule); +// // }); +// }, +// false +// ); +// link.addEventListener( +// 'error', +// () => { +// TiTheme.bootstrapModule(environment.isWc ? WcAppModule : AppModule); +// }, +// false +// ); +// } else { +// TiTheme.bootstrapModule(AppModule); +// } + +platformBrowserDynamic() + .bootstrapModule(environment.isWc ? WcAppModule : AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/ng/demo/src/webdoc/LICENSE b/src/ng/demo/src/webdoc/LICENSE new file mode 100644 index 0000000..0a8f884 --- /dev/null +++ b/src/ng/demo/src/webdoc/LICENSE @@ -0,0 +1,103 @@ +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. + +Section 1 – Definitions. + +a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. + +b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. + +c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + +d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. + +e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. + +f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. + +g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. + +h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. + +i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. + +j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. + +k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. + +Section 2 – Scope. + +a. License grant. +1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: + A. reproduce and Share the Licensed Material, in whole or in part; and + B. produce, reproduce, and Share Adapted Material. +2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. +3. Term. The term of this Public License is specified in Section 6(a). +4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. +5. Downstream recipients. +A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. +B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. +6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). + +b. Other rights. +1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. +2. Patent and trademark rights are not licensed under this Public License. +3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. + +Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following conditions. + +a. Attribution. +1. If You Share the Licensed Material (including in modified form), You must: +A. retain the following if it is supplied by the Licensor with the Licensed Material: + i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); + ii. a copyright notice; + iii. a notice that refers to this Public License; + iv. a notice that refers to the disclaimer of warranties; + v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; +B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and +C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. +2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. +3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. +4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. + +Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: + +a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; + +b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and +c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. +For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. + +Section 5 – Disclaimer of Warranties and Limitation of Liability. + +a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. +b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. +c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. + +Section 6 – Term and Termination. + +a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. +b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: +1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or +2. upon express reinstatement by the Licensor. +For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + +c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. +d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +Section 7 – Other Terms and Conditions. + +a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. +b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. + +Section 8 – Interpretation. + +a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. +b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. +c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. +d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. diff --git a/src/ng/demo/src/webdoc/faq-en.md b/src/ng/demo/src/webdoc/faq-en.md new file mode 100644 index 0000000..e69de29 diff --git a/src/ng/demo/src/webdoc/faq.md b/src/ng/demo/src/webdoc/faq.md new file mode 100644 index 0000000..e85d42e --- /dev/null +++ b/src/ng/demo/src/webdoc/faq.md @@ -0,0 +1,79 @@ +--- +title: 常见问题 | TinyNG +--- + +# 常见问题 +## 1. 运行报错:Can't bind to 'ngModel' since it isn't a known property of 'ti-xxxxx'? + +使用 `ngModel`,需要引入`FormsModule` + +```typescript +import { FormsModule } from '@angular/forms'; +@NgModule({ + imports: [ + ... + FormsModule, + ... + ], +}) +``` + +## 2. 编译报错:Can't bind to 'yyyyy' since it isn't a known property of 'ti-xxxxx'? +``` + If 'ti-xxxxx' is an Angular component and it has 'options' input, then verify that it is part of this module. + If 'ti-xxxxx' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. + To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. +``` +导入组件对应的`Module`即可。 + +## 3. 怎样找到`TinyNG`组件对应的模块名? + +在查看具体组件的示例时,页面顶部会显示该组件依赖的`Module`。 + +## 4. 运行报错:ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked? + +这是 Angular 调试环境在提示:Angular 已经绘制完模板了,但是在`ngAfterViewInit()`或之后,业务代码又去改变模板变量了。 + +两种解决方案:一、优化代码组织方式,提前更改模板变量。二、更改模板变量之后,去强制刷新模板(参考此文)。 + +## 5. 使用`TinyNG`库开发业务时,如何单步调试呢? + +TinyNG 库内置了 soucemap,很方便单步调试。单步调试方法: + +目前 Angular CLI 7.0 版,`ng serve --vendor-source-map`(官方已废弃此参数) + +Angular CLI 7.2 版,`ng serve --source-map=true` + +## 6. 我写的跟官网一模一样,为什么我这里出错了? + +请使用`Angular CLI`新建一个项目,复制官网代码,查看组件表现是否和官网一致。 + +## 7. 页面局部有滚动条,滚动该滚动条时其内部的下拉框错位? + +下拉框组件内部在 document 上注册了`tiScroll`事件,当监听到该事件时,组件会使下拉框消失。 + +开发者需要在对应的局部容器的滚动条滚动事件回调中触发 document上的`tiScroll`事件,使下拉框在拖动局部滚动条时消失。 + +```typescript +const event: Event = document.createEvent('HTMLEvents'); +event.initEvent('tiScroll', false, true); +element.dispatchEvent(event); +``` + +## 8. 运行报错: can't bind 'fromGroup'? + +使用响应式表单指令,需要引入`ReactiveFormsModule`, 可参考:[响应式表单](https://angular.io/guide/reactive-forms) + +```typescript +import { ReactiveFormsModule } from '@angular/forms'; +@NgModule({ +imports: [ +... +ReactiveFormsModule, +... +], +``` + +## 9. 部分组件接口支持传入 html 片段字符串,但是 html 中标签上的 style 样式设置不生效是为什么? + +为了防止`XSS`攻击,组件内部用的是 Angular 提供的`DomSanitizer.sanitize`方法做防`XSS`攻击安全处理的,它会把`style`设置会过滤掉,所以建议使用`class`的方式添加样式,如果一定要使用`style`方式,且能确保传入的`html`字符串片段是安全的,可以使用 Angular 提供的`DomSanitizer`上的`bypassSecurityTrustHtml`方法去掉 angular 的安全过滤处理,具体可参考:`message/message-security`。 diff --git a/src/ng/demo/src/webdoc/getstart-en.md b/src/ng/demo/src/webdoc/getstart-en.md new file mode 100644 index 0000000..e69de29 diff --git a/src/ng/demo/src/webdoc/getstart.md b/src/ng/demo/src/webdoc/getstart.md new file mode 100644 index 0000000..770517d --- /dev/null +++ b/src/ng/demo/src/webdoc/getstart.md @@ -0,0 +1,89 @@ +--- +title: 快速上手 | TinyNG +--- + +# 快速上手 + +`TinyNG`是基于 Angular + TypeScript 的 Web UI 组件库,本文将介绍在一个项目中如何使用`TinyNG`组件。 + +> 在使用`TinyNG`组件库之前,建议您提前学习 [HTML](https://www.w3school.com.cn/h.asp)、[CSS](https://www.w3school.com.cn/css/index.asp)、[TypeScript](https://www.tslang.cn/)、[Angular](https://angular.cn/docs)。 + +## 第一个本地实例 + +如果您已经拥有一个 Angular 应用项目,请直接进入[安装](#%E5%AE%89%E8%A3%85)步骤。 + +如果没用,我们强烈建议使用官方的`@angular/cli`工具创建项目,它在项目的构建、开发、调试、打包部署等环节贡献突出,对开发者帮助很大。 + +#### 安装脚手架工具 + +> 如果你想了解更多 CLI 工具链的功能和命令,建议访问 [@angular/cli](https://angular.cn/cli) 了解更多。 + +```bash +$ npm install -g @angular/cli +``` + +#### 创建一个项目 + +> 在创建项目之前,请确保 `@angular/cli` 已被成功安装。 + +通过 `@angular/cli` 在当前目录下新建一个名为 MYAPP 的 Angular 项目,并自动安装好相应依赖。 + +```bash +ng new MYAPP +``` + +## 安装及使用 + +#### 进入项目根目录,使用 npm 安装 `TinyNG` + +```bash +cd MYAPP +npm install @opentiny/ng @angular/cdk +``` +#### 引入模块 + +在使用某个组件前,需要引入对应的模块,此处以引入`Button`组件为例。 + +```typescript +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; // 部分组件依赖angular动画,可能需要引入animations模块 +import { NgModule } from '@angular/core'; +import { TiButtonModule } from '@opentiny/ng'; // 导入某个组件的模块 +import { AppComponent } from './app.component'; + +@NgModule({ + declarations: [AppComponent], + imports: [ + BrowserModule, + BrowserAnimationsModule, + TiButtonModule // 引入该模块 + ], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +#### 引入样式文件 + +在`angular.json`文件中, 引入`TinyNG`组件库样式文件。 + +```json +{ + "build": { + "styles": [ + "node_modules/@opentiny/ng/themes/styles.css", // 基础样式 + "node_modules/@opentiny/ng/themes/theme-default.css", // 主题样式 + "src/styles.css" + ] + } +} +``` + +`TinyNG`组件库内置了 5 套主题,分别是: theme-default.css、theme-blue.css、theme-green.css、theme-purple.css、theme-red.css,您可以根据业务场景进行选择,也可以在[主题配置](./themedoc)中选择更多样式配置方式。 + + +## 启动开发调试 + +```bash +ng serve --open +``` diff --git a/src/ng/demo/src/webdoc/images/basecolor1.png b/src/ng/demo/src/webdoc/images/basecolor1.png new file mode 100644 index 0000000000000000000000000000000000000000..d21bdfdd74ab8aebca72b41685f0de192c8c6a81 GIT binary patch literal 28643 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 F{vZ26yYm15 literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/webdoc/images/demo.png b/src/ng/demo/src/webdoc/images/demo.png new file mode 100644 index 0000000000000000000000000000000000000000..0b619c1e5336db72bf51e0d2817e66b3af0e55cd GIT binary patch literal 2042 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 E13VYUqyPW_ literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/webdoc/images/theme.png b/src/ng/demo/src/webdoc/images/theme.png new file mode 100644 index 0000000000000000000000000000000000000000..441612a5163dfa00a95f4d73c5ac0958ff7f87fc GIT binary patch literal 18370 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
    Edge | Firefox
    Firefox | Chrome
    Chrome | Safari
    Safari | +| --------- | --------- | --------- | --------- | +| last 3 versions | last 3 versions | last 3 versions | last 2 versions | + +### 支持 Angular 版本 +目前支持 Angular `^14.0.0`版本。 + +### [最新版本](./changelog) \ No newline at end of file diff --git a/src/ng/demo/src/webdoc/joinus-en.md b/src/ng/demo/src/webdoc/joinus-en.md new file mode 100644 index 0000000..e69de29 diff --git a/src/ng/demo/src/webdoc/joinus.md b/src/ng/demo/src/webdoc/joinus.md new file mode 100644 index 0000000..bdbef95 --- /dev/null +++ b/src/ng/demo/src/webdoc/joinus.md @@ -0,0 +1,156 @@ +--- +title: 加入我们 | TinyNG +--- +## 贡献指南 + +### Issue 规范 + +- Issue 仅用于提交 Bug 或 Feature 以及用户体验相关的内容,其它内容可能会被直接关闭。 + +- 在提交 Issue 之前,请搜索相关内容是否已被提出。 + +- 在提 Bug 时请说明清楚使用的 @opentiny/ng 的版本号及相关环境。 + +### Pull Request 规范 + +- 请先 fork 一份到自己的项目下,新建一个分支用于变更。 + + ```bash + git checkout -b my-branch master + ``` + +- commit 信息请遵循 [commit rules](https://github.com/opentiny/ng/blob/main/commit.template)。 + +- 提交 PR 前请先进行 rebase,确保 commit 记录的整洁。 + ```bash + git rebase master -i + git push -f + ``` + +- 如果是修复 `bug` 或者 `issues`,请在 PR 中描述清楚。 + + +### 开发 + +```bash + +# fork && git clone +... +# my-branch +npm install +npm start + +``` + +## 单元测试 + +### 整体测试 +```bash +$ npm test ng-demo +``` +或 +```bash +$ npx ng test ng-demo +``` + +### 指定组件测试 +```bash +$ npm test ${component name for test}-demo +``` +或 +```bash +$ npx ng test ${component name for test}-demo +``` + +例如: +```bash +$ npm test text-demo +``` + +### 添加测试用例 +- 由于库中每个组件都有单独版本,所以对于新加入的组件,需要开发者为其准备单元测试环境。(也可使用自动化脚本为新组件生成环境所需文件,例如: `npm build:test radio`) +- 对于已有组件,直接在组件 `demo/` 目录下新建 `${your component name}.spec.json` 文件后编写测试脚本。 +- 若组件 demo 目录中已经存在 `${your component name}.spec.json` 文件,则直接在此文件中修改用例。 + +TinyNG 使用 [Jasmine 测试框架](https://jasmine.github.io/) 对组件库内容进行单元测试。 +`npm test` 命令在**监视模式**下构建应用,并启动 [karma 测试运行器](https://karma-runner.github.io/)。 +运行结束后,控制台会输出测试结果,内容格式如下: + +``` +✔ Browser application bundle generation complete. +28 11 2022 08:40:17.804:WARN [karma]: No captured browser, open http://localhost:9876/ +28 11 2022 08:40:17.824:INFO [karma-server]: Karma v6.3.20 server started at http://localhost:9876/ +28 11 2022 08:40:17.825:INFO [launcher]: Launching browsers Chrome with concurrency unlimited +28 11 2022 08:40:17.833:INFO [launcher]: Starting browser Chrome +28 11 2022 08:40:19.278:INFO [Chrome 107.0.0.0 (Windows 10)]: Connected on socket swhnqYi_RwdYbu8uAAAB with id 55443332 +Chrome 107.0.0.0 (Windows 10): Executed 5 of 5 SUCCESS (0.562 secs / 0.815 secs) +TOTAL: 5 SUCCESS +``` + +它还会打开 Chrome 浏览器并在 [Jasmine HTML 报告器](https://github.com/dfederm/karma-jasmine-html-reporter)中显示测试输出。可以点击某一行用例,来单独重跑这个用例,或者点击一行描述信息来重跑所选的测试套件中的那些测试。 + +单元测试用例及相关配置在 `/src/test/` 目录下。 + +更多详细内容可以通过查阅 [Angular 官网关于测试的介绍](https://angular.cn/guide/testing)获得。 + +### 代码规范 +遵循 [ESLint](https://github.com/opentiny/ng/blob/main/.eslintrc.js) + +## 求贤纳士 +### Web前端开发工程师 + +#### 岗位职责 + ++ 团队目前前端框架使用 Angular、Vue,使用 ES6、7 进行具体开发工作; + ++ 前端工程化,打造从项目初始化、构建部署、发布、运维的端到端工程体系,打造前端 DevOps; + ++ 可视化搭建技术,基于少码或无码化的搭建方式,提升活动运营的开发效率; + ++ 面向对首屏和 SEO 注重的场景,研发基于 Nodejs+Vue 的同构方案,将页面首屏渲染性能做到极致; + ++ Nodejs 方面,自主研发前端工程体系相关工具,需要大量使用 Nodejs 来更高效率的实现 Web 层研发,并有高并发业务场景使用 Nodejs 做服务端开发; + ++ 基于 Serverless 方案,解决 Nodejs 运维及服务部署问题,提升前端研发效能。 + + +#### 技能要求 + ++ 熟练使用各种前端技术,包括 HTML、CSS、JavaScript 等; + ++ 具备跨终端的前端开发能力,在 Web(PC+Mobile)、Nodejs、Native App 三个方向上至少精通一个方向,具备多个的更佳,鼓励在 Native 和 Web 技术融合上的探索; + ++ 对前端工程化与模块化开发有一定了解,并有实践经验(如 Webpack、Babel、AMD、CMD 等); + ++ 具备优秀的团队协作精神,能利用自身技术能力提升团队整体研发效率,提高团队影响力; + ++ 对前端技术有持续的热情,个性乐观开朗,逻辑性强,沟通流畅; + ++ 乐于分享,善于总结归纳沉淀,并能将好的经验在团队内进行分享; + ++ 可选项:至少熟悉一门非前端的语言(如 Java、PHP、C、C++、Python、Ruby),并有实践经验。 +
    + +### Java后端开发工程师 + +

    岗位职责

    + ++ 参与核心基础框架的架构设计、详细方案设计与开发,为用户提供高可用、高性能的控制台服务; + ++ 负责云原生领域服务方案设计及开发,实现云服务业务大流量、高并发; + ++ 负责公共服务及中间件的孵化、设计及开发工作,构筑业界领先的平台能力。 + +

    技能要求

    + ++ 熟悉 J2EE、Java Web 编程技术,对各种开源的框架如 Spring 等有深入的应用和优化经验; + ++ 熟悉 Netty ,熟悉多线程模型编程和网络IO模型; + ++ 熟悉 Spring Boot、Spring MVC、Redis、MySQL; + ++ 具有很强的学习能力和技术敏感度,有强烈的责任心和进取心,乐于学习和技术分享。 + +
    + +如果您有意愿加入,请加微信咨询:ly5353523 diff --git a/src/ng/demo/src/webdoc/language-en.md b/src/ng/demo/src/webdoc/language-en.md new file mode 100644 index 0000000..e69de29 diff --git a/src/ng/demo/src/webdoc/language.md b/src/ng/demo/src/webdoc/language.md new file mode 100644 index 0000000..2da527f --- /dev/null +++ b/src/ng/demo/src/webdoc/language.md @@ -0,0 +1,209 @@ +--- +title: 国际化 | TinyNG +--- + +# 国际化 + +目前的默认文案是中文,如果需要使用其他语言,可以在初始化时进行配置,也可以在运行中随时修改,可以参考下面的方案。 + +## 设置语言 + +`TinyNG`支持 5 种语言,默认显示中文,在 Angular 项目应用入口文件`app.module.ts`,可设置语言。 + +```typescript +import { TiLocale } from '@opentiny/ng'; +// 导入其他项 + +@NgModule({ + imports: [ + // 导入模块... + ] + + // 配置其他项... +}) +export class AppModule { + constructor() { + // 配置Tiny国际化资源,默认为中文 + /** + * 可用的语言标识 + * TiLocale.EN_US 英文 + * TiLocale.ZH_CN 简体中文 + * TiLocale.ES_US 拉美西语 + * TiLocale.FR_FR 法语 + * TiLocale.PT_BR 葡语 + */ + TiLocale.setLocale(TiLocale.EN_US); + } + ... +} +``` + +## 国际化转化 +#### 使用管道符做国际化转换 +此种方式只支持通过页面刷新切换语言,业务可结合 cookie 切换语言 + +模板文件`locale-basic.html` + +```html +

    {{ 'testStr' | tiTranslate }}

    +

    {{ 'testStrArgs1' | tiTranslate: [1] }}

    +

    {{ 'testStrArgs2' | tiTranslate: [1,2] }}

    +

    {{ 'tiButton.ok' | tiTranslate }}

    + + +``` + +`locale-basic.component.ts`文件 +```typescript +import { Component } from '@angular/core'; +import { + en_US as tinyen_US, + zh_CN as tinyzh_CN, + TiLocale, + TiLocaleFormat, +} from '@opentiny/ng'; + +interface LocaleWords { + tiLocaleKey: string; + testStr: string; + testStrArgs1: string; + testStrArgs2: string; +} + +@Component({ + templateUrl: './locale-basic.html', +}) +export class LocaleBasicComponent { + // 业务自己的词条 + private static myzh_CN: any = { + tiLocaleKey: 'zh-CN', + testStr: '测试', + testStrArgs1: '测试单参数场景 {0}', + testStrArgs2: '测试单多参数场景 {0} 和 {1}', + }; + // 业务自己的词条 + private static myen_US: LocaleWords = { + tiLocaleKey: 'en-US', + testStr: 'test str with args', + testStrArgs1: 'test str with args {0}', + testStrArgs2: 'test str with args {0} and {1}', + }; + + constructor() { + // 添加业务的国际化词条 + TiLocale.setWords({ + 'zh-CN': { ...tinyzh_CN, ...LocaleBasicComponent.myzh_CN }, + 'en-US': { ...tinyen_US, ...LocaleBasicComponent.myen_US }, + }); + TiLocale.setLocale(this.getCookie('localeKey')); + this.setValues(); + } + + changeLocale(localeKey: string): void { + TiLocale.setLocale(localeKey); + } + + // 使用过滤器做国际化转换打开此代码: + setLocaleAndRefresh(localeKey: string): void { + this.changeLocale(localeKey); + document.cookie = `localeKey=${localeKey}`; + location.reload(); + } + + getCookie(key: string): string { + const name: string = key + '='; + const splitedCookie: Array = document.cookie.split(';'); + for (let word of splitedCookie) { + while (word.charAt(0) === ' ') { + word = word.substring(1); + } + if (word.indexOf(name) === 0) { + return word.substring(name.length, word.length); + } + } + + return ''; + } +} +``` + +#### 使用 JavaScript 方法做国际化转换 +此种方式可做到页面无刷新切换语言 + +模板文件`locale-basic.html` + +```html +

    {{testStr}}

    +

    {{testStrArgs1}}

    +

    {{testStrArgs2}}

    +

    {{okBtn}}

    + + +``` + +`locale-basic.component.ts`文件 + +```typescript +import { Component } from '@angular/core'; +import { + en_US as tinyen_US, + zh_CN as tinyzh_CN, + TiLocale, + TiLocaleFormat, +} from '@opentiny/ng'; + +interface LocaleWords { + tiLocaleKey: string; + testStr: string; + testStrArgs1: string; + testStrArgs2: string; +} + +@Component({ + templateUrl: './locale-basic.html', +}) +export class LocaleBasicComponent { + testStr: string; + testStrArgs1: string; + testStrArgs2: string; + okBtn: string; + // 业务自己的词条 + private static myzh_CN: any = { + tiLocaleKey: 'zh-CN', + testStr: '测试', + testStrArgs1: '测试单参数场景 {0}', + testStrArgs2: '测试单多参数场景 {0} 和 {1}', + }; + // 业务自己的词条 + private static myen_US: LocaleWords = { + tiLocaleKey: 'en-US', + testStr: 'test str with args', + testStrArgs1: 'test str with args {0}', + testStrArgs2: 'test str with args {0} and {1}', + }; + constructor() { + // 添加业务的国际化词条 + TiLocale.setWords({ + 'zh-CN': { ...tinyzh_CN, ...LocaleBasicComponent.myzh_CN }, + 'en-US': { ...tinyen_US, ...LocaleBasicComponent.myen_US }, + }); + this.setValues(); + } + + changeLocale(localeKey: string): void { + TiLocale.setLocale(localeKey); + this.setValues(); + } + + setValues(): void { + this.testStr = this.setLocaleValue('testStr'); + this.testStrArgs1 = this.setLocaleValue('testStrArgs1', [1]); + this.testStrArgs2 = this.setLocaleValue('testStrArgs2', [1, 2]); + this.okBtn = this.setLocaleValue('tiButton.ok'); + } + + setLocaleValue(key: string, params?: Array): string { + return TiLocale.translate(key, params); + } +} +``` diff --git a/src/ng/demo/src/webdoc/menus.js b/src/ng/demo/src/webdoc/menus.js new file mode 100644 index 0000000..9e63ded --- /dev/null +++ b/src/ng/demo/src/webdoc/menus.js @@ -0,0 +1,206 @@ +// 注意,删除了useFor属性 + +// title,label增加英文版,以应对将来的国际化功能 +export const docMenus = [ + { + label: '使用指南', + labelEn: 'Guide', // *********** + key: 'doc_use', + children: [ + { + title: '介绍', + titleEn: 'Introduce', + key: 'introduce' + }, + { + title: '快速上手', + titleEn: 'Quick Start', + key: 'getstart' + }, + { + title: '主题配置', + titleEn: 'Theme Customization', + key: 'themedoc' + }, + { + title: '国际化', + titleEn: 'Internationalization', + key: 'language' + }, + { + title: '常见问题', + titleEn: 'FAQ', + key: 'faq' + }, + { + title: '更新日志', + titleEn: 'Change Log', + key: 'changelog' + }, + { + title: '加入我们', + titleEn: 'Join Us', + key: 'joinus' + } + ] + } +]; + +// ------------------------------------------------------------------- +export const cmpMenus = [ + { + label: '表单选择', + labelEn: 'Form Selection', + key: 'cmp_formselect', + children: [ + { name: 'Button', nameCn: '按钮', key: 'button' }, + { name: 'Select', nameCn: '选择器', key: 'select' }, + { name: 'Radio', nameCn: '单选框', key: 'radio' }, + { name: 'Checkbox', nameCn: '复选框', key: 'checkbox' }, + { name: 'Slider', nameCn: '滑块', key: 'slider' }, + { name: 'Switch', nameCn: '开关', key: 'switch' }, + { name: 'Buttongroup', nameCn: '选块组', key: 'buttongroup' }, + { name: 'Spinner', nameCn: '数字微调', key: 'spinner' }, + { name: 'Treeselect', nameCn: '树选择', key: 'treeselect' }, + { name: 'Cascader', nameCn: '级联选择', key: 'cascader' }, + { name: 'Transfer', nameCn: '穿梭框', key: 'transfer' }, + { name: 'Score', nameCn: '评分', key: 'score' }, + { name: 'Linkbutton', nameCn: '按钮链接', key: 'linkbutton' }, + { name: 'Selectgroup', nameCn: '选择组', key: 'selectgroup' } + ] + }, + { + label: '表单文本', + labelEn: 'Form Text', + key: 'cmp_formtext', + children: [ + { name: 'Text', nameCn: '文本框', key: 'text' }, + { name: 'Textarea', nameCn: '多行文本框', key: 'textarea' }, + { name: 'Autocomplete', nameCn: '自动补全', key: 'autocomplete' }, + { name: 'Searchbox', nameCn: '搜索框', key: 'searchbox' }, + { name: 'IP', nameCn: '输入IP', key: 'ip' }, + { name: 'IPsection', nameCn: 'IP分段', key: 'ipsection' }, + { name: 'Tag', nameCn: '标签', key: 'tag' }, + { name: 'TagsInput', nameCn: '标签输入', key: 'tagsinput' }, + { name: 'InputNumber', nameCn: '数字输入框', key: 'inputnumber' }, + { name: 'Labeleditor', nameCn: '可编辑文本', key: 'labeleditor' }, + { name: 'PhoneNumber', nameCn: '电话号码', key: 'phonenumber' }, + { name: 'Path', nameCn: '路径', key: 'path' } + ] + }, + { + label: '表单日期', + labelEn: 'Form Date', + key: 'cmp_formdate', + children: [ + { name: 'Date', nameCn: '日期', key: 'date' }, + { name: 'DateRange', nameCn: '日期范围', key: 'daterange' }, + { name: 'Datetime', nameCn: '日期时间', key: 'datetime' }, + { name: 'DatetimeRange', nameCn: '日期时间范围', key: 'datetimerange' }, + { name: 'Time', nameCn: '时间', key: 'time' } + ] + }, + { + label: '表单辅助', + labelEn: 'Form Assist', + key: 'cmp_formassist', + children: [ + { name: 'Validation', nameCn: '表单校验', key: 'validation' }, + { name: 'Formfield', nameCn: '表单布局', key: 'formfield' }, + { name: 'Upload', nameCn: '文件上传', key: 'upload' }, + { name: 'Uploadimage', nameCn: '图片上传', key: 'uploadimage' }, + { name: 'Buttonselect', nameCn: '选块下拉', key: 'buttonselect' } + ] + }, + { + label: '导航', + labelEn: 'Menu navigation', + key: 'cmp_nav', + children: [ + { name: 'Leftmenu', nameCn: '左侧菜单', key: 'leftmenu' }, + { name: 'Menu', nameCn: '下拉菜单', key: 'menu' }, + { name: 'Nav', nameCn: '顶部导航', key: 'nav' }, + { name: 'Actionmenu', nameCn: '菜单按钮', key: 'actionmenu' }, + { name: 'Accordion', nameCn: '手风琴', key: 'accordion' }, + { name: 'Tabs', nameCn: '页签', key: 'tab' }, + { name: 'Steps', nameCn: '步骤导航', key: 'steps' }, + { name: 'Crumb', nameCn: '面包屑', key: 'crumb' }, + { name: 'Timeline', nameCn: '时间线', key: 'timeline' }, + { name: 'Subtitle', nameCn: '返回标题', key: 'subtitle' }, + { name: 'Anchor', nameCn: '锚点', key: 'anchor' } + ] + }, + { + label: '弹出提示', + labelEn: 'Popup Prompt', + key: 'cmp_popup', + children: [ + { name: 'Tip', nameCn: '气泡提示', key: 'tip' }, + { name: 'Overflow', nameCn: '溢出提示', key: 'overflow' }, + { name: 'Collapse', nameCn: '折叠面板', key: 'collapse' }, + { name: 'Collapsebox', nameCn: '折叠框', key: 'collapsebox' }, + { name: 'Alert', nameCn: '警告', key: 'alert' }, + { name: 'Modal', nameCn: '弹出框', key: 'modal' }, + { name: 'Message', nameCn: '消息弹框', key: 'message' }, + { name: 'Notification', nameCn: '通知弹框', key: 'notification' }, + { name: 'Popconfirm', nameCn: '气泡确认框', key: 'popconfirm' }, + { name: 'Halfmodal', nameCn: '半屏弹窗', key: 'halfmodal' } + ] + }, + { + label: '其他组件', + labelEn: 'Others', + key: 'cmp_others', + children: [ + { name: 'Avatar', nameCn: '头像', key: 'avatar' }, + { name: 'Table', nameCn: '表格', key: 'table' }, + { name: 'Progressbar', nameCn: '进度条', key: 'progressbar' }, + { name: 'Loading', nameCn: '加载', key: 'loading' }, + { name: 'Pagination', nameCn: '分页', key: 'pagination' }, + { name: 'Tree', nameCn: '树', key: 'tree' }, + { name: 'Icon', nameCn: '图标', key: 'icon' }, + { name: 'Iconaction', nameCn: '图标文本链接', key: 'iconaction' }, + { name: 'Intro', nameCn: '新手引导', key: 'intro' }, + { name: 'Swiper', nameCn: '轮播', key: 'swiper' }, + { name: 'Card', nameCn: '卡片', key: 'card' }, + { name: 'Layout', nameCn: '布局', key: 'layout' }, + { name: 'Rate', nameCn: '评分', key: 'rate' }, + { name: 'Rights', nameCn: '权益', key: 'rights' }, + { name: 'Skeleton', nameCn: '骨架屏', key: 'skeleton' }, + { name: 'Rate', nameCn: '评分', key: 'rate' }, + { name: 'Guides', nameCn: '情景引导', key: 'guides' }, + { name: 'Foldtext', nameCn: '折叠文本', key: 'foldtext' }, + { name: 'Productpreview', nameCn: '商品预览', key: 'productpreview' }, + { name: 'Collapsetext', nameCn: '下展文本', key: 'collapsetext' }, + { name: 'Guidesteps', nameCn: '小步骤引导', key: 'guidesteps' }, + { name: 'Collapsebutton', nameCn: '折叠按钮', key: 'collapsebutton' }, + { name: 'Copy', nameCn: '复制', key: 'copy' } + ] + }, + { + label: '工具组件', + labelEn: 'Tools', + key: 'cmp_tools', + children: [ + { name: 'Browser', nameCn: '浏览器信息', key: 'browser' }, + { name: 'Keymap', nameCn: '键值查询', key: 'keymap' }, + { name: 'Log', nameCn: '日志打印', key: 'log' }, + { name: 'Theme', nameCn: '主题配置', key: 'theme' } + ] + }, + { + label: '国际化', + labelEn: 'Locale', + key: 'cmp_locale', + children: [{ name: 'Locale', nameCn: '国际化语言', key: 'locale' }] + }, + { + label: '接口 & 类型', + labelEn: 'Interfaces & Types', + key: 'cmp_interfacesandtypes', + children: [ + { name: 'Interfaces', nameCn: '接口', key: 'interfaces' }, + { name: 'Types', nameCn: '类型', key: 'types' } + ] + } +]; diff --git a/src/ng/demo/src/webdoc/themedoc-en.md b/src/ng/demo/src/webdoc/themedoc-en.md new file mode 100644 index 0000000..e69de29 diff --git a/src/ng/demo/src/webdoc/themedoc.md b/src/ng/demo/src/webdoc/themedoc.md new file mode 100644 index 0000000..7b25224 --- /dev/null +++ b/src/ng/demo/src/webdoc/themedoc.md @@ -0,0 +1,227 @@ +--- +title: 主题配置 | TinyNG +--- + +# 主题配置 + +TinyNG 支持一定程度的样式定制,以满足业务和品牌上多样化的视觉需求,包括但不限于主色、圆角、边框和部分组件的视觉定制。 + +theme + +## 使用预定义主题 + +### 默认主题 + +修改`angular.json`的`styles`字段,全量引入`"node_modules/@opentiny/ng/themes/styles.css"`和`"node_modules/@opentiny/ng/themes/theme-default.css"`。 + +```json +{ + ... + "architect": { + "build": { + ... + "styles": [ + "node_modules/@opentiny/ng/themes/styles.css", + "node_modules/@opentiny/ng/themes/theme-default.css", + "src/styles.css", + ], + ... + } + } + ... +} +``` + +### 官方主题 + +除了默认主题外,我们还提供了 4 种官方主题,欢迎在项目中试用,并且给我们提供反馈。 + +- theme-blue.css +- theme-green.css +- theme-purple.css +- theme-red.css + +修改`angular.json`的`styles`字段,全量引入`"node_modules/@opentiny/ng/themes/styles.css"`和`"node_modules/@opentiny/ng/themes/theme-xxx.css"`。 + +```json +{ + ... + "architect": { + "build": { + ... + "styles": [ + "node_modules/@opentiny/ng/themes/styles.css", + "node_modules/@opentiny/ng/themes/theme-xxx.css", + "src/styles.css", + ], + ... + } + } + ... +} +``` + +## 自定义主题 + +如果需要自定义主题,引入官方主题文件之后,再根据实际需求自定义覆盖主题样式变量的参数。新增 `src/theme-my.css` 文件,修改 `angular.json` 的 `styles` 字段,引入 `src/theme-my.css`。 + +```json +{ + ... + "architect": { + "build": { + ... + "styles": [ + "node_modules/@cloud/tiny3/themes/styles.css", + "node_modules/@cloud/tiny3/themes/theme-default.css", + "src/theme-my.css", + "src/styles.css", + ], + ... + } + } + ... +} +``` + +例如,在以下样例中通过修改 `--ti-base-color-brand` 的数值将预定义默认主题的基础色修改为 `#999999`。 + +```css +:root { + --ti-base-color-brand: #999999; + ... + --ti-common-font-size-base: 16px; + ... + --ti-tag-text-color: #888888; + --ti-tag-icon-color: var(--ti-base-color-icon-graybg-normal); + ... +} +``` + +主题涉及到的变量,请查看 [basic-var.css](https://github.com/opentiny/ng/blob/main/%40opentiny/ng/themes/basic/basic-var.css)。 + +除了手动修改主题涉及到的变量外,通过[主题配置系统](../../designtheme/home)也可以修改主题。 + +## 动态切换主题 + +### 步骤一:将`TinyNG`主题 CSS 文件,复制到`assets`下 + +修改`angular.json`的`assets`字段,参考如下修改,下面`input`,`output`意思是打包时,`"node_modules/@opentiny/ng/themes/"`复制到`"/assets/tiny3/themes/"`。 + +```json +{ + ... + "architect": { + "build": { + ... + "assets": [ + "src/favicon.ico", + "src/assets", + { + "glob": "**/*", + "input": "node_modules/@opentiny/ng/themes/", + "output": "/assets/tiny3/themes/" + } + ], + ... + } + } + ... +} +``` + +### 步骤二:添加基础样式`"node_modules/@opentiny/ng/themes/styles.css"` + +修改`angular.json`的`styles`字段,引入`"node_modules/@opentiny/ng/themes/styles.css"`。 + +```json +{ + ... + "architect": { + "build": { + ... + "styles": [ + "node_modules/@opentiny/ng/themes/styles.css", + "src/styles.css", + ], + ... + } + } + ... +} +``` + +### 步骤三:加载主题 CSS 文件 + +修改Angular项目启动文件`main.ts`,加载主题 CSS 文件后,再启动`AppModule`。 + +```typescript +import { enableProdMode } from '@angular/core'; +import { TiTheme } from '@opentiny/ng'; + +import { AppModule } from './app/AppModule'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +if (environment.production) { + const themename: string = 'default'; + // 加载主题CSS文件。只有生产环境支持在线切换皮肤,所以基础CSS在angular.json中配置,主题CSS在代码中加载,之后再应用。 + // 会从assets/tiny3/themes/theme-${theme}.css 加载CSS文件,放在head link + const link: HTMLLinkElement = TiTheme.loadCss( + `assets/tiny3/themes/theme-${themename}.css`, + 'tiny3theme' + ); + + // 原生支持CSSVars + // 在Chrome下,新加入的CSS载入太迟,CSS样式生效迟,overflow等需要计算宽度的组件有问题,所以要等CSS加载完成后才启动App + link.addEventListener( + 'load', + () => { + TiTheme.bootstrapModule(AppModule); + }, + false + ); + link.addEventListener( + 'error', + () => { + TiTheme.bootstrapModule(AppModule); + }, + false + ); +} else { + TiTheme.bootstrapModule(AppModule); +} +``` + +### 步骤四:调用代码切换主题,详见 [theme](../components/theme) 用例 + +```typescript +import { TiTheme } from '@opentiny/ng'; + +... +TiTheme.loadThemeCss('blue', 'tiny3theme'); +... +``` + + + +## 网页里动态改变组件样式 + +JavaScript 操作 CSS 变量的写法如下:(Chrome、Firefox、Safari 原生支持) + +> IE 的兼容性补丁,暂时不支持 JavaScript 操作 CSS 变量,有可能后期会支持此能力。 + +```javascript +// 设置变量 +document.body.style.setProperty('--primary', '#7F583F'); + +// 读取变量 +document.body.style.getPropertyValue('--primary').trim(); +// '#7F583F' + +// 删除变量 +document.body.style.removeProperty('--primary'); +``` diff --git a/src/ng/demo/src/webdoc/validators.md b/src/ng/demo/src/webdoc/validators.md new file mode 100644 index 0000000..147233b --- /dev/null +++ b/src/ng/demo/src/webdoc/validators.md @@ -0,0 +1,42 @@ +--- +title: 表单校验规则 | TinyNG +--- + +## 说明 +本文介绍 TinyNG 实现的 `TiValidators` 的校验规则及校验方法。 + +## 用法介绍 +支持两种方式声明校验: + +- 1.静态方法:通过调用`TiValidators.${规则名称}`的方式,适用于响应式表单。如`required`声明为`TiValidators.required`。 +- 2.指令方式:通过`ti${规则名称}`指令声明,适用于模板驱动表单。如`required`规则的指令名为`tiRequired`。 + +## 校验规则 + +| 规则名称 | 参数类型 | 参数含义 | 规则说明 | +| :------- | -------: | -------: | -------: | +| required | - | - | 非空校验 | +| maxLength | number | 最大字符长度 | 字符长度最大值校验 | +| minLength | number | 最小字符长度 | 字符长度最小值校验 | +| rangeSize | number | 最小长度限制 | 字符长度大小区间校验 | +| maxValue | number | 最大数值 | 数字最大数值校验 | +| minValue | number | 最小数值 | 数字最小数值校验 | +| rangeValue | number | 最小数值限制 | 数字大小区间校验 | +| regExp | RegExp:string | 正则表达式参数 | 不包括正则表达式头尾标识符'^(?:'、')$' | +| email | - | - | 邮箱校验 | +| contains | string/number | 包含的内容 | 包含校验 | +| notContains | string/number | 不包含的内容 | 不包含校验 | +| equal | string/number/boolean | 相等的内容 | 相等校验。| +| notEqual | string/number | 不相等的内容 | 不相等校验 | +| notScript | - | - | 包含script标签校验 | +| port | - | - | 端口号校验,范围为0~65535 | +| date | - | - | 日期类型校验 | +| url | - | - | url校验 | +| integer | - | - | 整数校验 | +| number | - | - | 数字校验 | +| digits | - | - | 自然数校验 | +| ipv4 | - | - | ipv4校验 | +| ipv6 | - | - | ipv6校验 | +| minCharType | number/RegExp:string | 符合要求的字符种类/字符集对象类型
    默认的字符种类分别为:大写字母、小写字母、数字、特殊字符\`~!@#$%^&*()-_=+\|[{}];:'",<.>/? 和空格 | 符合最小字符种类校验,默认情况为至少包含2种字符类型 | +| notEqualPosRev | () => AbstractControl | 需要比对的表单formControl对象获取函数 | 不能和表单对象的正序或倒序相同 | +| password | TiPasswordValidatorConfig:Object | 密码校验各项规则参数 | 密码校验 | \ No newline at end of file diff --git a/src/ng/demo/test.ts b/src/ng/demo/test.ts new file mode 100644 index 0000000..5a8e0d5 --- /dev/null +++ b/src/ng/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('../../', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/ng/demo/tsconfig.app.json b/src/ng/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/ng/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/ng/demo/tsconfig.spec.json b/src/ng/demo/tsconfig.spec.json new file mode 100644 index 0000000..9c9ff4c --- /dev/null +++ b/src/ng/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "../../**/*.spec.ts", + "../../**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/ng/demolog/DemoLogComponent.ts b/src/ng/demolog/DemoLogComponent.ts new file mode 100644 index 0000000..c524caa --- /dev/null +++ b/src/ng/demolog/DemoLogComponent.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, SimpleChanges, ViewChild } from '@angular/core'; + +@Component({ + selector: 'demo-log', + templateUrl: './log.html', + styleUrls: ['./log.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DemoLogComponent { + @Input() logs: Array; + @ViewChild('log', { static: true }) logRef: ElementRef; + + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + ngOnChanges(changes: SimpleChanges): void { + if (changes['logs']) { + // 手动更新一次视图,使可以滚动到最底部 + this.changeDetectorRef.detectChanges(); + this.logRef.nativeElement.scrollTop = 100000; + } + } +} diff --git a/src/ng/demolog/DemoLogModule.ts b/src/ng/demolog/DemoLogModule.ts new file mode 100644 index 0000000..5299667 --- /dev/null +++ b/src/ng/demolog/DemoLogModule.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DemoLogComponent } from './DemoLogComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [DemoLogComponent], + declarations: [DemoLogComponent] +}) +export class DemoLogModule {} diff --git a/src/ng/demolog/log.html b/src/ng/demolog/log.html new file mode 100644 index 0000000..d66f3ab --- /dev/null +++ b/src/ng/demolog/log.html @@ -0,0 +1,3 @@ +
    +

    {{i + ' ' + log}}

    +
    diff --git a/src/ng/demolog/log.less b/src/ng/demolog/log.less new file mode 100644 index 0000000..e48c15c --- /dev/null +++ b/src/ng/demolog/log.less @@ -0,0 +1,12 @@ +.event-log-container { + max-height: 220px; + font-size: 12px; + overflow-y: auto; + padding: 8px 0 8px 8px; + margin-top: 8px; + background-color: rgba(243, 244, 246); + &:empty { + margin-top: 0px; + padding: 0px; + } +} diff --git a/src/ng/lib/index.ts b/src/ng/lib/index.ts new file mode 100644 index 0000000..ff297b4 --- /dev/null +++ b/src/ng/lib/index.ts @@ -0,0 +1,102 @@ +export * from '@opentiny/ng-accordion'; +export * from '@opentiny/ng-actionmenu'; +export * from '@opentiny/ng-alert'; +export * from '@opentiny/ng-anchor'; +export * from '@opentiny/ng-autocomplete'; +export * from '@opentiny/ng-avatar'; +export * from '@opentiny/ng-base'; +export * from '@opentiny/ng-button'; +export * from '@opentiny/ng-buttongroup'; +export * from '@opentiny/ng-card'; +export * from '@opentiny/ng-cascader'; +export * from '@opentiny/ng-checkbox'; +export * from '@opentiny/ng-collapse'; +export * from '@opentiny/ng-collapsebox'; +export * from '@opentiny/ng-collapsebutton'; +export * from '@opentiny/ng-copy'; +export * from '@opentiny/ng-crumb'; +export * from '@opentiny/ng-date'; +export * from '@opentiny/ng-datebase'; +export * from '@opentiny/ng-datedominator'; +export * from '@opentiny/ng-dateedit'; +export * from '@opentiny/ng-datepanel'; +export * from '@opentiny/ng-daterange'; +export * from '@opentiny/ng-datetime'; +export * from '@opentiny/ng-datetimerange'; +export * from '@opentiny/ng-dominator'; +export * from '@opentiny/ng-drag'; +export * from '@opentiny/ng-drop'; +export * from '@opentiny/ng-droplist'; +export * from '@opentiny/ng-dropsearch'; +export * from '@opentiny/ng-formfield'; +export * from '@opentiny/ng-foldtext'; +export * from '@opentiny/ng-grid'; +export * from '@opentiny/ng-halfmodal'; +export * from '@opentiny/ng-icon'; +export * from '@opentiny/ng-iconaction'; +export * from '@opentiny/ng-imagepreview'; +export * from '@opentiny/ng-include'; +export * from '@opentiny/ng-inputnumber'; +export * from '@opentiny/ng-intro'; +export * from '@opentiny/ng-ip'; +export * from '@opentiny/ng-ipsection'; +export * from '@opentiny/ng-layout'; +export * from '@opentiny/ng-leftmenu'; +export * from '@opentiny/ng-list'; +export * from '@opentiny/ng-loading'; +export * from '@opentiny/ng-locale'; +export * from '@opentiny/ng-linkbutton'; +export * from '@opentiny/ng-menu'; +export * from '@opentiny/ng-message'; +export * from '@opentiny/ng-modal'; +export * from '@opentiny/ng-nav'; +export * from '@opentiny/ng-notification'; +export * from '@opentiny/ng-outline'; +export * from '@opentiny/ng-overflow'; +export * from '@opentiny/ng-pagination'; +export * from '@opentiny/ng-popconfirm'; +export * from '@opentiny/ng-popup'; +export * from '@opentiny/ng-progressbar'; +export * from '@opentiny/ng-progresspie'; +export * from '@opentiny/ng-radio'; +export * from '@opentiny/ng-rate'; +export * from '@opentiny/ng-renderer'; +export * from '@opentiny/ng-rights'; +export * from '@opentiny/ng-score'; +export * from '@opentiny/ng-scroll'; +export * from '@opentiny/ng-searchbox'; +export * from '@opentiny/ng-select'; +export * from '@opentiny/ng-skeleton'; +export * from '@opentiny/ng-slider'; +export * from '@opentiny/ng-spinner'; +export * from '@opentiny/ng-steps'; +export * from '@opentiny/ng-subtitle'; +export * from '@opentiny/ng-swiper'; +export * from '@opentiny/ng-switch'; +export * from '@opentiny/ng-tab'; +export * from '@opentiny/ng-table'; +export * from '@opentiny/ng-tag'; +export * from '@opentiny/ng-tagsinput'; +export * from '@opentiny/ng-text'; +export * from '@opentiny/ng-textarea'; +export * from '@opentiny/ng-time'; +export * from '@opentiny/ng-timeline'; +export * from '@opentiny/ng-tip'; +export * from '@opentiny/ng-transfer'; +export * from '@opentiny/ng-tree'; +export * from '@opentiny/ng-treeselect'; +export * from '@opentiny/ng-upload'; +export * from '@opentiny/ng-utils'; +export * from '@opentiny/ng-validation'; +export * from '@opentiny/ng-zoom'; +export * from '@opentiny/ng-guides'; +export * from '@opentiny/ng-labeleditor'; +export * from '@opentiny/ng-phonenumber'; +export * from '@opentiny/ng-selectgroup'; +export * from '@opentiny/ng-productpreview'; +export * from '@opentiny/ng-buttonselect'; +export * from '@opentiny/ng-collapsetext'; + +export * from '@opentiny/ng-guidesteps'; + +export * from '@opentiny/ng-path'; diff --git a/src/ng/lib/ng-package.json b/src/ng/lib/ng-package.json new file mode 100644 index 0000000..e740ca5 --- /dev/null +++ b/src/ng/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/ng", + "lib": { + "entryFile": "index.ts" + } +} diff --git a/src/ng/lib/package.json b/src/ng/lib/package.json new file mode 100644 index 0000000..5936a90 --- /dev/null +++ b/src/ng/lib/package.json @@ -0,0 +1,110 @@ +{ + "name": "@opentiny/ng", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-accordion": "1.0.0-beta.0", + "@opentiny/ng-actionmenu": "1.0.0-beta.0", + "@opentiny/ng-alert": "1.0.0-beta.0", + "@opentiny/ng-anchor": "1.0.0-beta.0", + "@opentiny/ng-autocomplete": "1.0.0-beta.0", + "@opentiny/ng-avatar": "1.0.0-beta.0", + "@opentiny/ng-base": "1.0.0-beta.0", + "@opentiny/ng-button": "1.0.0-beta.0", + "@opentiny/ng-buttongroup": "1.0.0-beta.0", + "@opentiny/ng-card": "1.0.0-beta.0", + "@opentiny/ng-cascader": "1.0.0-beta.0", + "@opentiny/ng-checkbox": "1.0.0-beta.0", + "@opentiny/ng-collapse": "1.0.0-beta.0", + "@opentiny/ng-collapsebox": "1.0.0-beta.0", + "@opentiny/ng-collapsebutton": "1.0.0-beta.0", + "@opentiny/ng-copy": "1.0.0-beta.0", + "@opentiny/ng-crumb": "1.0.0-beta.0", + "@opentiny/ng-date": "1.0.0-beta.0", + "@opentiny/ng-datebase": "1.0.0-beta.0", + "@opentiny/ng-datedominator": "1.0.0-beta.0", + "@opentiny/ng-dateedit": "1.0.0-beta.0", + "@opentiny/ng-datepanel": "1.0.0-beta.0", + "@opentiny/ng-daterange": "1.0.0-beta.0", + "@opentiny/ng-datetime": "1.0.0-beta.0", + "@opentiny/ng-datetimerange": "1.0.0-beta.0", + "@opentiny/ng-dominator": "1.0.0-beta.0", + "@opentiny/ng-drag": "1.0.0-beta.0", + "@opentiny/ng-drop": "1.0.0-beta.0", + "@opentiny/ng-droplist": "1.0.0-beta.0", + "@opentiny/ng-dropsearch": "1.0.0-beta.0", + "@opentiny/ng-formfield": "1.0.0-beta.0", + "@opentiny/ng-foldtext": "1.0.0-beta.0", + "@opentiny/ng-grid": "1.0.0-beta.0", + "@opentiny/ng-halfmodal": "1.0.0-beta.0", + "@opentiny/ng-icon": "1.0.0-beta.0", + "@opentiny/ng-iconaction": "1.0.0-beta.0", + "@opentiny/ng-imagepreview": "1.0.0-beta.0", + "@opentiny/ng-include": "1.0.0-beta.0", + "@opentiny/ng-inputnumber": "1.0.0-beta.0", + "@opentiny/ng-intro": "1.0.0-beta.0", + "@opentiny/ng-ip": "1.0.0-beta.0", + "@opentiny/ng-ipsection": "1.0.0-beta.0", + "@opentiny/ng-layout": "1.0.0-beta.0", + "@opentiny/ng-leftmenu": "1.0.0-beta.0", + "@opentiny/ng-list": "1.0.0-beta.0", + "@opentiny/ng-loading": "1.0.0-beta.0", + "@opentiny/ng-locale": "1.0.0-beta.0", + "@opentiny/ng-linkbutton": "1.0.0-beta.0", + "@opentiny/ng-menu": "1.0.0-beta.0", + "@opentiny/ng-message": "1.0.0-beta.0", + "@opentiny/ng-modal": "1.0.0-beta.0", + "@opentiny/ng-nav": "1.0.0-beta.0", + "@opentiny/ng-notification": "1.0.0-beta.0", + "@opentiny/ng-outline": "1.0.0-beta.0", + "@opentiny/ng-overflow": "1.0.0-beta.0", + "@opentiny/ng-pagination": "1.0.0-beta.0", + "@opentiny/ng-popconfirm": "1.0.0-beta.0", + "@opentiny/ng-popup": "1.0.0-beta.0", + "@opentiny/ng-progressbar": "1.0.0-beta.0", + "@opentiny/ng-progresspie": "1.0.0-beta.0", + "@opentiny/ng-radio": "1.0.0-beta.0", + "@opentiny/ng-rate": "1.0.0-beta.0", + "@opentiny/ng-renderer": "1.0.0-beta.0", + "@opentiny/ng-rights": "1.0.0-beta.0", + "@opentiny/ng-score": "1.0.0-beta.0", + "@opentiny/ng-scroll": "1.0.0-beta.0", + "@opentiny/ng-searchbox": "1.0.0-beta.0", + "@opentiny/ng-select": "1.0.0-beta.0", + "@opentiny/ng-skeleton": "1.0.0-beta.0", + "@opentiny/ng-slider": "1.0.0-beta.0", + "@opentiny/ng-spinner": "1.0.0-beta.0", + "@opentiny/ng-steps": "1.0.0-beta.0", + "@opentiny/ng-subtitle": "1.0.0-beta.0", + "@opentiny/ng-swiper": "1.0.0-beta.0", + "@opentiny/ng-switch": "1.0.0-beta.0", + "@opentiny/ng-tab": "1.0.0-beta.0", + "@opentiny/ng-table": "1.0.0-beta.0", + "@opentiny/ng-tag": "1.0.0-beta.0", + "@opentiny/ng-tagsinput": "1.0.0-beta.0", + "@opentiny/ng-text": "1.0.0-beta.0", + "@opentiny/ng-textarea": "1.0.0-beta.0", + "@opentiny/ng-time": "1.0.0-beta.0", + "@opentiny/ng-timeline": "1.0.0-beta.0", + "@opentiny/ng-tip": "1.0.0-beta.0", + "@opentiny/ng-transfer": "1.0.0-beta.0", + "@opentiny/ng-tree": "1.0.0-beta.0", + "@opentiny/ng-treeselect": "1.0.0-beta.0", + "@opentiny/ng-upload": "1.0.0-beta.0", + "@opentiny/ng-utils": "1.0.0-beta.0", + "@opentiny/ng-validation": "1.0.0-beta.0", + "@opentiny/ng-zoom": "1.0.0-beta.0", + "@opentiny/ng-themes": "1.0.0-beta.0", + "@opentiny/ng-guides": "1.0.0-beta.0", + "@opentiny/ng-labeleditor": "1.0.0-beta.0", + "@opentiny/ng-phonenumber": "1.0.0-beta.0", + "@opentiny/ng-selectgroup": "1.0.0-beta.0", + "@opentiny/ng-productpreview": "1.0.0-beta.0", + "@opentiny/ng-buttonselect": "1.0.0-beta.0", + "@opentiny/ng-collapsetext": "1.0.0-beta.0", + "@opentiny/ng-guidesteps": "1.0.0-beta.0", + "@opentiny/ng-path": "1.0.0-beta.0" + } +} diff --git a/src/ng/lib/project.json b/src/ng/lib/project.json new file mode 100644 index 0000000..5486061 --- /dev/null +++ b/src/ng/lib/project.json @@ -0,0 +1,49 @@ +{ + "projectType": "library", + "root": "src/ng/lib", + "sourceRoot": "src/ng/lib", + "targets": { + "build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/ng"], + "options": { + "project": "src/ng/lib/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "dependsOn": [ + { + "target": "pack", + "projects": "dependencies" + } + ], + "options": { + "commands": [ + { + "command": "cd dist/libs/ng && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build ng && ng pack ng && node build/publish.js ng --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/notification/demo/karma.conf.js b/src/notification/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/notification/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/notification/demo/project.json b/src/notification/demo/project.json new file mode 100644 index 0000000..f867611 --- /dev/null +++ b/src/notification/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/notification/demo", + "sourceRoot": "src/notification/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/notification", + "index": "src/notification/demo/src/index.html", + "main": "src/notification/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/notification/demo/tsconfig.app.json", + "assets": ["src/notification/demo/src/favicon.ico", "src/notification/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "notification-demo:build:production" + }, + "development": { + "browserTarget": "notification-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js notification" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/notification/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/notification/demo/tsconfig.spec.json", + "karmaConfig": "src/notification/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/notification/demo/src/app/AppComponent.ts b/src/notification/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/notification/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/notification/demo/src/app/AppModule.ts b/src/notification/demo/src/app/AppModule.ts new file mode 100644 index 0000000..c6f3864 --- /dev/null +++ b/src/notification/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { NotificationTestModule } from './notification/NotificationTestModule'; + +@NgModule({ + imports: [ + NotificationTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/notification/demo/src/app/IndexComponent.ts b/src/notification/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..4372fef --- /dev/null +++ b/src/notification/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { NotificationTestModule } from './notification/NotificationTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = NotificationTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/notification/demo/src/app/app.html b/src/notification/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/notification/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/notification/demo/src/app/notification/NotificationAnimationComponent.ts b/src/notification/demo/src/app/notification/NotificationAnimationComponent.ts new file mode 100644 index 0000000..8854d23 --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationAnimationComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-animation.html' +}) +export class NotificationAnimationComponent { + constructor(private tiNotification: TiNotificationService) {} + + onClickAnimation(): void { + this.tiNotification.simple('TinyNG 为 Web 应用提供了丰富的基础 UI 组件'); + } + + onClickWithoutAnimation(): void { + this.tiNotification.simple('这是一个关闭进场、出场动画的通知', { + animation: false + }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationBasicComponent.ts b/src/notification/demo/src/app/notification/NotificationBasicComponent.ts new file mode 100644 index 0000000..fdef96e --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationBasicComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-basic.html' +}) +export class NotificationBasicComponent { + constructor(private tiNotification: TiNotificationService) {} + + onClickOpen(): void { + this.tiNotification.simple('TinyNG 为 Web 应用提供了丰富的基础 UI 组件'); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationCloseComponent.ts b/src/notification/demo/src/app/notification/NotificationCloseComponent.ts new file mode 100644 index 0000000..72c04d5 --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationCloseComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'notification-close.html' +}) +export class NotificationCloseComponent { + constructor(private tiNotification: TiNotificationService) {} + private static noticName: string = 'notification-close-demo'; + target: any; + noticeCount: number = 0; + + onClickOpen(): void { + this.tiNotification.simple(`${this.noticeCount++}: TinyNG 为 Web 应用提供了丰富的基础 UI 组件`); + } + onClickCloseAll(): void { + this.tiNotification.closeAll(); + } + onClickOpenSingle(): void { + let notice: any = this.tiNotification.simple('NotificationService 返回一个通知实例,通过调用实例的 close 方法来关闭它', { + duration: 0, + name: NotificationCloseComponent.noticName + }); + this.target = notice; + } + onClickCloseSingle(): void { + this.target.close(); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationConfigComponent.ts b/src/notification/demo/src/app/notification/NotificationConfigComponent.ts new file mode 100644 index 0000000..3ef891c --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationConfigComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: './notification-config.html' +}) +export class NotificationConfigComponent { + constructor(private tiNotification: TiNotificationService) { + this.tiNotification.config({ top: '100px' }); + } + + onClickOpen(): void { + this.tiNotification.simple('TinyNG 为 Web 应用提供了丰富的基础 UI 组件', { + duration: 0 + }); + this.tiNotification.simple('TinyNG 为 Web 应用提供了丰富的基础 UI 组件', { + position: 'bottom-right', + duration: 0 + }); + } + onClickChangeTop(): void { + this.tiNotification.config({ top: '200px' }); + } + onClickChangeBottom(): void { + this.tiNotification.config({ bottom: '100px' }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationDurationComponent.ts b/src/notification/demo/src/app/notification/NotificationDurationComponent.ts new file mode 100644 index 0000000..ac78176 --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationDurationComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-duration.html' +}) +export class NotificationDurationComponent { + constructor(private tiNotification: TiNotificationService) {} + + onClickDeafult(): void { + this.tiNotification.simple('通知弹窗默认会在 4.5 秒后自动关闭'); + } + + onClickCustomDuring(): void { + this.tiNotification.simple('将 duration 设置为 6000,通知弹窗将在 6 秒后自动关闭', { duration: 6000 }); + } + + onClickWontAutoClose(): void { + this.tiNotification.simple('如果将 duration 设置为 0,通知弹窗将不自动关闭', { duration: 0 }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationEventsComponent.ts b/src/notification/demo/src/app/notification/NotificationEventsComponent.ts new file mode 100644 index 0000000..245d6a4 --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationEventsComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'notification-events.html' +}) +export class NotificationEventsComponent { + constructor(private tiNotification: TiNotificationService) {} + + myLogs: Array = []; + + onClickCloseCallback(): void { + this.tiNotification.simple('通过 onClose 方法定义通知关闭时的回调函数,布尔类型的入参表示是否为用户点击关闭按钮关闭', { + onClose: () => { + this.myLogs = [...this.myLogs, `通知弹窗关闭了`]; + } + }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationHoverPauseComponent.ts b/src/notification/demo/src/app/notification/NotificationHoverPauseComponent.ts new file mode 100644 index 0000000..30035c8 --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationHoverPauseComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-hover-pause.html' +}) +export class NotificationHoverPauseComponent { + constructor(private tiNotification: TiNotificationService) {} + + onClickPause(): void { + this.tiNotification.simple('TinyNG 为 Web 应用提供了丰富的基础 UI 组件'); + } + + onClickContinue(): void { + this.tiNotification.simple('TinyNG 为 Web 应用提供了丰富的基础 UI 组件', { + hoverPause: false + }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationNameComponent.ts b/src/notification/demo/src/app/notification/NotificationNameComponent.ts new file mode 100644 index 0000000..d2c458c --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationNameComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-name.html' +}) +export class NotificationNameComponent { + constructor(private tiNotification: TiNotificationService) {} + + private static noticName: string = 'tiny'; + + onClickOpen(): void { + this.tiNotification.simple('我是原来的通知内容', { + name: NotificationNameComponent.noticName, + duration: 0 + }); + } + onClickChange(): void { + this.tiNotification.success('我是被改变后的内容', { + name: NotificationNameComponent.noticName, + duration: 0 + }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationPositionComponent.ts b/src/notification/demo/src/app/notification/NotificationPositionComponent.ts new file mode 100644 index 0000000..30b600d --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationPositionComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-position.html', + styles: [ + '.demo-notify-position-container {width: 320px; display: grid; grid-template-columns: repeat(3, 1fr); grid-gap: 10px;}', + '.demo-notify-position-container > div {width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;}' + ] +}) +export class NotificationPositionComponent { + constructor(private tiNotification: TiNotificationService) {} + private static content: string = 'TinyNG 为 Web 应用提供了丰富的基础 UI 组件'; + + onClickTopLeft(): void { + this.tiNotification.simple(NotificationPositionComponent.content, { + position: 'top-left' + }); + } + + onClickTop(): void { + this.tiNotification.simple(NotificationPositionComponent.content, { + position: 'top' + }); + } + + onClickTopRight(): void { + this.tiNotification.simple(NotificationPositionComponent.content, { + position: 'top-right' + }); + } + + onClickBottomLeft(): void { + this.tiNotification.simple(NotificationPositionComponent.content, { + position: 'bottom-left' + }); + } + + onClickBottom(): void { + this.tiNotification.simple(NotificationPositionComponent.content, { + position: 'bottom' + }); + } + + onClickBottomRight(): void { + this.tiNotification.simple(NotificationPositionComponent.content, { + position: 'bottom-right' + }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationTemplateComponent.ts b/src/notification/demo/src/app/notification/NotificationTemplateComponent.ts new file mode 100644 index 0000000..27e5b1f --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationTemplateComponent.ts @@ -0,0 +1,14 @@ +import { Component, TemplateRef } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-template.html' +}) +export class NotificationTemplateComponent { + constructor(private tiNotification: TiNotificationService) {} + + value: number = 5; + + onClickOpen(template: TemplateRef): void { + this.tiNotification.template(template, { duration: 0 }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationTestModule.ts b/src/notification/demo/src/app/notification/NotificationTestModule.ts new file mode 100644 index 0000000..4c2f234 --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationTestModule.ts @@ -0,0 +1,99 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; +import { TiButtonModule, TiNotificationModule, TiCardModule, TiRateModule, TiNotificationService } from '@opentiny/ng'; +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { NotificationBasicComponent } from './NotificationBasicComponent'; +import { NotificationCloseComponent } from './NotificationCloseComponent'; +import { NotificationPositionComponent } from './NotificationPositionComponent'; +import { NotificationDurationComponent } from './NotificationDurationComponent'; +import { NotificationNameComponent } from './NotificationNameComponent'; +import { NotificationAnimationComponent } from './NotificationAnimationComponent'; +import { NotificationTemplateComponent } from './NotificationTemplateComponent'; +import { NotificationTypeComponent } from './NotificationTypeComponent'; +import { NotificationEventsComponent } from './NotificationEventsComponent'; +import { NotificationConfigComponent } from './NotificationConfigComponent'; + +@NgModule({ + imports: [ + DemoLogModule, + CommonModule, + FormsModule, + TiNotificationModule, + TiButtonModule, + TiCardModule, + TiRateModule, + RouterModule.forChild(NotificationTestModule.ROUTES) + ], + declarations: [ + NotificationBasicComponent, + NotificationCloseComponent, + NotificationPositionComponent, + NotificationDurationComponent, + NotificationNameComponent, + NotificationAnimationComponent, + NotificationTemplateComponent, + NotificationTypeComponent, + NotificationEventsComponent, + NotificationConfigComponent + ] +}) +export class NotificationTestModule { + constructor(private tiNotification: TiNotificationService) { + this.tiNotification.config({ top: '84px' }); + } + + static readonly ROUTES: Routes = [ + { + path: 'notification/notification-basic', + component: NotificationBasicComponent, + data: { label: 'basic' } + }, + { + path: 'notification/notification-position', + component: NotificationPositionComponent, + data: { label: 'position' } + }, + { + path: 'notification/notification-duration', + component: NotificationDurationComponent, + data: { label: 'duration' } + }, + { + path: 'notification/notification-name', + component: NotificationNameComponent, + data: { label: 'name' } + }, + { + path: 'notification/notification-animation', + component: NotificationAnimationComponent, + data: { label: 'animation' } + }, + { + path: 'notification/notification-template', + component: NotificationTemplateComponent, + data: { label: 'template' } + }, + { + path: 'notification/notification-type', + component: NotificationTypeComponent, + data: { label: 'type' } + }, + { + path: 'notification/notification-events', + component: NotificationEventsComponent, + data: { label: 'events' } + }, + { + path: 'notification/notification-close', + component: NotificationCloseComponent, + data: { label: 'close' } + }, + { + path: 'notification/notification-config', + component: NotificationConfigComponent, + data: { label: 'config' } + } + ]; +} diff --git a/src/notification/demo/src/app/notification/NotificationTypeComponent.ts b/src/notification/demo/src/app/notification/NotificationTypeComponent.ts new file mode 100644 index 0000000..48c5a5c --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationTypeComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-type.html' +}) +export class NotificationTypeComponent { + constructor(private tiNotification: TiNotificationService) {} + private static content: string = 'TinyNG 为 Web 应用提供了丰富的基础 UI 组件'; + + onClickSuccess(): void { + this.tiNotification.success(NotificationTypeComponent.content); + } + onClickPrompt(): void { + this.tiNotification.prompt(NotificationTypeComponent.content); + } + onClickWarn(): void { + this.tiNotification.warn(NotificationTypeComponent.content); + } + onClickError(): void { + this.tiNotification.error(NotificationTypeComponent.content); + } + onClickSimple(): void { + this.tiNotification.simple(NotificationTypeComponent.content); + } +} diff --git a/src/notification/demo/src/app/notification/notification-animation.html b/src/notification/demo/src/app/notification/notification-animation.html new file mode 100644 index 0000000..f39a9a6 --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-animation.html @@ -0,0 +1,2 @@ + + diff --git a/src/notification/demo/src/app/notification/notification-basic.html b/src/notification/demo/src/app/notification/notification-basic.html new file mode 100644 index 0000000..15d5b9b --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-basic.html @@ -0,0 +1 @@ + diff --git a/src/notification/demo/src/app/notification/notification-close.html b/src/notification/demo/src/app/notification/notification-close.html new file mode 100644 index 0000000..dd2a79a --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-close.html @@ -0,0 +1,5 @@ + + +
    + + diff --git a/src/notification/demo/src/app/notification/notification-config.html b/src/notification/demo/src/app/notification/notification-config.html new file mode 100644 index 0000000..d461b0f --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-config.html @@ -0,0 +1,3 @@ + + + diff --git a/src/notification/demo/src/app/notification/notification-duration.html b/src/notification/demo/src/app/notification/notification-duration.html new file mode 100644 index 0000000..553d41a --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-duration.html @@ -0,0 +1,3 @@ + + + diff --git a/src/notification/demo/src/app/notification/notification-events.html b/src/notification/demo/src/app/notification/notification-events.html new file mode 100644 index 0000000..10dd551 --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-events.html @@ -0,0 +1 @@ + diff --git a/src/notification/demo/src/app/notification/notification-hover-pause.html b/src/notification/demo/src/app/notification/notification-hover-pause.html new file mode 100644 index 0000000..184a855 --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-hover-pause.html @@ -0,0 +1,2 @@ + + diff --git a/src/notification/demo/src/app/notification/notification-name.html b/src/notification/demo/src/app/notification/notification-name.html new file mode 100644 index 0000000..481de58 --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-name.html @@ -0,0 +1,2 @@ + + diff --git a/src/notification/demo/src/app/notification/notification-position.html b/src/notification/demo/src/app/notification/notification-position.html new file mode 100644 index 0000000..6853e34 --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-position.html @@ -0,0 +1,8 @@ +
    + + + + + + +
    diff --git a/src/notification/demo/src/app/notification/notification-template.html b/src/notification/demo/src/app/notification/notification-template.html new file mode 100644 index 0000000..73cc364 --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-template.html @@ -0,0 +1,12 @@ + + + + +

    请在下方选择评分

    +
    +
    + +
    + +
    +
    diff --git a/src/notification/demo/src/app/notification/notification-type.html b/src/notification/demo/src/app/notification/notification-type.html new file mode 100644 index 0000000..6d7c0c0 --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-type.html @@ -0,0 +1,5 @@ + + + + + diff --git a/src/notification/demo/src/app/notification/webdoc/notification-demos.js b/src/notification/demo/src/app/notification/webdoc/notification-demos.js new file mode 100644 index 0000000..48e7465 --- /dev/null +++ b/src/notification/demo/src/app/notification/webdoc/notification-demos.js @@ -0,0 +1,146 @@ +export default { + column: '2', + demos: [ + { + demoId: 'notification-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    Notification 组件的最简用法。

    ', + 'en-US': '

    ' + } + }, + { + demoId: 'notification-type', + name: { + 'zh-CN': '通知类型', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': + '

    通过调用TiNotificationService的不同方法来弹出不同类型的通知,包括:successpromptwarnerrorsimple

    ', + 'en-US': '

    ' + }, + apis: [ + 'TiNotificationService.methods.success', + 'TiNotificationService.methods.prompt', + 'TiNotificationService.methods.warn', + 'TiNotificationService.methods.error', + 'TiNotificationService.methods.simple' + ] + }, + { + demoId: 'notification-template', + name: { + 'zh-CN': '自定义内容', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    通过调用TiNotificationServicetemplate方法来自定义通知弹窗内容。

    ', + 'en-US': '

    ' + }, + apis: ['TiNotificationService.methods.template'] + }, + { + demoId: 'notification-duration', + name: { + 'zh-CN': '自动关闭时间', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    通过属性duration配置通知自动关闭的时间,单位为毫秒;注意:当设置为 0 时表示不自动关闭。

    ', + 'en-US': '

    ' + }, + apis: ['TiNoticeConfig.properties.duration'] + }, + { + demoId: 'notification-position', + name: { + 'zh-CN': '弹出位置', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': + '

    通过属性position配置弹出位置,包括top-righttoptop-leftbottom-leftbottombottom-right

    ', + 'en-US': '

    ' + }, + apis: ['TiNoticeConfig.properties.position'] + }, + { + demoId: 'notification-animation', + name: { + 'zh-CN': '禁用动画', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    通过属性animation配置是否使用动画,默认开启动画,当配置为 false 禁用动画。

    ', + 'en-US': '

    ' + }, + apis: ['TiNoticeConfig.properties.animation'] + }, + { + demoId: 'notification-hover-pause', + name: { + 'zh-CN': '鼠标悬停时暂停自动关闭计时', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    通过属性hoverPause配置是否在鼠标悬停在通知弹窗上时暂停自动关闭的计时。

    ', + 'en-US': '

    ' + }, + apis: ['TiNoticeConfig.properties.hoverPause'] + }, + { + demoId: 'notification-name', + name: { + 'zh-CN': '更新消息内容', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    通过属性name配置通知弹窗标识,后续通过唯一的name更新通知弹窗内容。

    ', + 'en-US': '

    ' + }, + apis: ['TiNoticeConfig.properties.name'] + }, + { + demoId: 'notification-events', + name: { + 'zh-CN': '事件', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    通过属性onClose配置通知弹窗关闭时的回调函数。

    ', + 'en-US': '

    ' + }, + apis: ['TiNoticeConfig.properties.onClose'] + }, + { + demoId: 'notification-close', + name: { + 'zh-CN': '关闭通知弹窗', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': + '

    通过属性TiNotificationService的打开通知弹窗方法,会返回一个可关闭对象,调用这个对象的close方法来关闭对应的通知弹窗。同时,你也可以通过TiNotificationService.closeAll()来关闭

    ', + 'en-US': '

    ' + }, + apis: ['TiNotificationService.methods.closeAll'] + }, + { + demoId: 'notification-config', + name: { + 'zh-CN': '配置弹窗距顶底距离', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': + '

    通过调用TiNotificationServiceconfig方法来配置通知弹窗距离顶部或底部的距离,接受配置:topbottom

    ', + 'en-US': '

    ' + }, + apis: ['TiNotificationService.methods.config'] + } + ] +}; diff --git a/src/notification/demo/src/app/notification/webdoc/notification.cn.md b/src/notification/demo/src/app/notification/webdoc/notification.cn.md new file mode 100644 index 0000000..0a66328 --- /dev/null +++ b/src/notification/demo/src/app/notification/webdoc/notification.cn.md @@ -0,0 +1,23 @@ +--- +title: Notification 通知 +--- +# Notification 通知 + +
    + +Notification 是提供通知弹窗的组件。悬浮出现在页面角落,显示全局的通知提醒消息。   + ++ 弹出框组件提供服务方式供业务使用,使用该服务时需要引入模块`TiNotificationModule`,开发者通过调用`TiNotificationService.simple`方法生成弹出框。 + +```typescript +import { TiNotificationModule } from '@opentiny/ng'; +``` + +需要在项目中(建议在根模块)中导入`BrowserAnimationsModule`。 + +```typescript +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +``` +
    + +
    diff --git a/src/notification/demo/src/app/notification/webdoc/notification.en.md b/src/notification/demo/src/app/notification/webdoc/notification.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/notification/demo/src/app/notification/webdoc/notification.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/notification/demo/src/favicon.ico b/src/notification/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/notification/demo/src/index.html b/src/notification/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/notification/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/notification/demo/src/main.ts b/src/notification/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/notification/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/notification/demo/test.ts b/src/notification/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/notification/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/notification/demo/tsconfig.app.json b/src/notification/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/notification/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/notification/demo/tsconfig.spec.json b/src/notification/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/notification/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/notification/lib/index.ts b/src/notification/lib/index.ts new file mode 100644 index 0000000..8efa227 --- /dev/null +++ b/src/notification/lib/index.ts @@ -0,0 +1,5 @@ +export * from './src/TiNotificationComponent'; +export * from './src/TiNotificationModule'; +export * from './src/TiNotificationInterface'; +export * from './src/TiNotificationService'; +export * from './src/TiNotificationContainerComponent'; diff --git a/src/notification/lib/ng-package.json b/src/notification/lib/ng-package.json new file mode 100644 index 0000000..f701fe4 --- /dev/null +++ b/src/notification/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/notification", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/notification/lib/package.json b/src/notification/lib/package.json new file mode 100644 index 0000000..cae90dd --- /dev/null +++ b/src/notification/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "@opentiny/ng-notification", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/animations": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/cdk/overlay": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-alert": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@angular/cdk/portal": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/notification/lib/project.json b/src/notification/lib/project.json new file mode 100644 index 0000000..2498b39 --- /dev/null +++ b/src/notification/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/notification/lib", + "sourceRoot": "src/notification/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/notification"], + "options": { + "project": "src/notification/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/notification"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js notification" + }, + { + "command": "ng default-build notification" + }, + { + "command": "node build/clear-default-theme.js notification" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/notification && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build notification && ng pack notification && node build/publish.js notification --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/notification/lib/src/TiNotificationComponent.html b/src/notification/lib/src/TiNotificationComponent.html new file mode 100644 index 0000000..b27e037 --- /dev/null +++ b/src/notification/lib/src/TiNotificationComponent.html @@ -0,0 +1,19 @@ +
    + {{instance.content}} + + + + + +
    diff --git a/src/notification/lib/src/TiNotificationComponent.ts b/src/notification/lib/src/TiNotificationComponent.ts new file mode 100644 index 0000000..52b81bf --- /dev/null +++ b/src/notification/lib/src/TiNotificationComponent.ts @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { AnimationEvent } from '@angular/animations'; +import { + Component, + ViewEncapsulation, + ChangeDetectorRef, + EventEmitter, + Input, + Output, + OnDestroy, + ChangeDetectionStrategy +} from '@angular/core'; +import { filter, take } from 'rxjs/operators'; + +import { TiNoticeData, TiNoticeConfig, TiTimerType, TiNotificationPosition } from './TiNotificationInterface'; +import { notificationMotion } from './TiNotificationMotion'; + +@Component({ + selector: 'ti-notification', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + styleUrls: ['./notification.less'], + animations: [notificationMotion], + templateUrl: './TiNotificationComponent.html' +}) +export class TiNotificationComponent implements OnDestroy { + config!: Required; + private autoDestroy?: boolean = true; + private timerStamp?: number; + private duration!: number; + private destroyTimer: TiTimerType; + readonly animationEvtEmt: EventEmitter = new EventEmitter(); + + @Input() instance: Required; + @Input() position: TiNotificationPosition; + @Output() readonly destroyed = new EventEmitter<{ id: string }>(); + + constructor(public cdr: ChangeDetectorRef) {} + + private static timerCleaner(targetTimer: TiTimerType): void { + if (targetTimer !== null) { + clearTimeout(targetTimer); + targetTimer = null; + } + } + + ngOnInit(): void { + this.config = this.instance.config as Required; + this.duration = this.config.duration; + if (this.duration === 0) { + this.autoDestroy = false; + } else { + this.timerStamp = Date.now(); + this.startTimer(); + } + if (this.config.animation) { + this.instance.state = 'enter'; + this.animationEvtEmt + .pipe( + filter((evt) => evt.toState === 'leave' && evt.phaseName === 'done'), + take(1) + ) + .subscribe(() => { + this.destroyed.next({ id: this.instance.noticeId }); + }); + } + } + + private startTimer(): void { + if (this.duration > 0) { + TiNotificationComponent.timerCleaner(this.destroyTimer); + this.destroyTimer = setTimeout(() => this.destroy(), this.duration); + this.timerStamp = Date.now(); + } else { + this.destroy(); + } + } + + ngOnDestroy(): void { + if (this.autoDestroy) { + TiNotificationComponent.timerCleaner(this.destroyTimer); + } + this.animationEvtEmt.complete(); + } + + private destroy(): void { + if (this.config.animation) { + this.instance.state = 'leave'; + this.cdr.detectChanges(); + } else { + this.destroyed.next({ id: this.instance.noticeId }); + } + } + + onMouseEnter(): void { + if (this.autoDestroy && this.config.hoverPause) { + TiNotificationComponent.timerCleaner(this.destroyTimer); + this.duration = this.timerStamp + this.duration - Date.now(); + } + } + + onMouseLeave(): void { + if (this.autoDestroy && this.config.hoverPause) { + this.startTimer(); + } + } + + get state(): string | undefined { + if (this.instance.state === 'enter') { + switch (this.position) { + case 'top-right': + case 'bottom-right': + return 'rightEnter'; + case 'top': + return 'topEnter'; + case 'top-left': + case 'bottom-left': + return 'leftEnter'; + case 'bottom': + return 'bottomEnter'; + default: + return 'rightEnter'; + } + } else { + return this.instance.state; + } + } + + alertOpenStateChangeHandle(openState: boolean): void { + if (!openState) { + this.destroy(); + } + } +} diff --git a/src/notification/lib/src/TiNotificationContainerComponent.html b/src/notification/lib/src/TiNotificationContainerComponent.html new file mode 100644 index 0000000..1c493ac --- /dev/null +++ b/src/notification/lib/src/TiNotificationContainerComponent.html @@ -0,0 +1,48 @@ +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    diff --git a/src/notification/lib/src/TiNotificationContainerComponent.ts b/src/notification/lib/src/TiNotificationContainerComponent.ts new file mode 100644 index 0000000..be54ad0 --- /dev/null +++ b/src/notification/lib/src/TiNotificationContainerComponent.ts @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ViewEncapsulation, QueryList, ViewChildren } from '@angular/core'; + +import { TiNoticeBasicConfig, TiNoticeData, TiNotificationRef, TiNotificationConfig } from './TiNotificationInterface'; + +import { TiNotificationComponent } from './TiNotificationComponent'; +import { Util } from '@opentiny/ng-utils'; + +@Component({ + selector: 'ti-notification-container', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + styleUrls: ['./notification.less'], + templateUrl: './TiNotificationContainerComponent.html' +}) +export class TiNotificationContainerComponent { + @ViewChildren(TiNotificationComponent) noticeInstances: QueryList; + + topRightNotices: Array> = []; + topNotices: Array> = []; + topLeftNotices: Array> = []; + bottomLeftNotices: Array> = []; + bottomNotices: Array> = []; + bottomRightNotices: Array> = []; + notices: Array> = []; + notificationConfig: TiNotificationConfig = {}; + consoleHeaderHeight: number = 0; // console头部高度 + + private onHeadChange: (e: CustomEvent) => void; // console头部高度变化时触发 + private consoleDataService: any; + + static TI_NOTIFICATION_DEFAULT_CONFIG: TiNoticeBasicConfig = { + animation: true, + duration: 4500, + hoverPause: true, + position: 'top-right' + }; + + constructor(private cdr: ChangeDetectorRef) {} + + setConsoleHeader(): void { + const consoleContext = (window).getConsoleContext && (window).getConsoleContext(); + this.consoleDataService = consoleContext?.get && consoleContext.get({ name: 'safearea' }); + this.onHeadChange = (e: CustomEvent) => { + this.consoleHeaderHeight = e.detail.top; + this.notificationConfig.top = `calc(var(--ti-common-space-6x) + ${this.consoleHeaderHeight}px)`; + this.reloadNotices(); + }; + // 头部高度变化会触发此事件 + if (this.consoleDataService?.onChange) { + this.consoleDataService.onChange(this.onHeadChange); + } + // console初始化首次进来不会触发consoleDataService.onChange,需要根据getSafeArea()设置一次。 + if (this.consoleDataService?.getSafeArea) { + const safeArea: any = this.consoleDataService.getSafeArea(); + this.consoleHeaderHeight = safeArea.top; + this.notificationConfig.top = `calc(var(--ti-common-space-6x) + ${this.consoleHeaderHeight}px)`; + } + } + + create(userConfig: TiNoticeData): TiNotificationRef { + let targetNotice = this.fixConfig(userConfig); + const existedNotice = this.notices.find((notice) => notice.config.name === targetNotice.config.name); + if (targetNotice.config.name && existedNotice) { + targetNotice = this.updateNotice(targetNotice, existedNotice); + this.markChildChanged(targetNotice.config.name); + } else { + this.bindApis(targetNotice); + this.notices = [...this.notices, targetNotice]; + } + this.reloadNotices(); + const { close } = targetNotice; + return { close } as TiNotificationRef; + } + + updateNotice(targetNotice: TiNoticeData, existedNotice: Required): Required { + existedNotice.content = targetNotice.content; + existedNotice.template = targetNotice.template; + existedNotice.type = targetNotice.type; + existedNotice.config = targetNotice.config; + return existedNotice; + } + + private markChildChanged(name: string): void { + const existedNotice = this.noticeInstances.find((notice) => notice.instance.config.name === name); + if (existedNotice) { + existedNotice.cdr.detectChanges(); + } + } + + private bindApis(targetNotice: Required): void { + targetNotice.close = () => this.close(targetNotice.noticeId); + } + + private reloadNotices(): void { + this.topRightNotices = []; + this.topNotices = []; + this.topLeftNotices = []; + this.bottomLeftNotices = []; + this.bottomNotices = []; + this.bottomRightNotices = []; + this.notices.forEach((notice) => { + const { position } = notice.config; + switch (position) { + case 'top-right': + this.topRightNotices.push(notice); + break; + case 'top': + this.topNotices.push(notice); + break; + case 'top-left': + this.topLeftNotices.push(notice); + break; + case 'bottom-left': + this.bottomLeftNotices.push(notice); + break; + case 'bottom': + this.bottomNotices.push(notice); + break; + case 'bottom-right': + this.bottomRightNotices.push(notice); + break; + default: + this.topRightNotices.push(notice); + } + }); + this.cdr.detectChanges(); + } + + private fixConfig(noticeData: TiNoticeData): Required { + const { animation, duration, hoverPause, position } = TiNotificationContainerComponent.TI_NOTIFICATION_DEFAULT_CONFIG; + noticeData.config = { animation, duration, hoverPause, position, ...noticeData.config }; + return noticeData as Required; + } + + close(id: string): void { + for (let i = 0; i < this.notices.length; i++) { + let target = this.notices[i]; + if (target.noticeId === id) { + this.notices.splice(i, 1); + this.closeHandle(target); + this.reloadNotices(); + break; + } + } + } + + closeAll(): void { + this.notices.forEach((notice) => this.closeHandle(notice)); + this.notices = []; + this.reloadNotices(); + } + + private closeHandle(notice: Required): void { + if (notice.config!.onClose && Util.isFunction(notice.config!.onClose)) { + notice.config.onClose(); + } + } + + config(config: TiNotificationConfig): void { + let confTemp = { ...this.notificationConfig, ...config }; + for (let key in config) { + if (config[key] === 'default') { + delete confTemp[key]; + } + } + this.notificationConfig = confTemp; + this.reloadNotices(); + } +} diff --git a/src/notification/lib/src/TiNotificationInterface.ts b/src/notification/lib/src/TiNotificationInterface.ts new file mode 100644 index 0000000..f644aab --- /dev/null +++ b/src/notification/lib/src/TiNotificationInterface.ts @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TemplateRef } from '@angular/core'; + +/** + * @ignore + */ +export type TiTimerType = ReturnType | number | null; + +/** + * 存储全局唯一容器的 mapper + * @ignore + */ +export interface TiNotificationMapperItem { + target: any; +} + +/** + * 通知弹窗位置 + */ +export type TiNotificationPosition = 'top-right' | 'top' | 'top-left' | 'bottom-left' | 'bottom' | 'bottom-right'; + +/** + * 通知弹窗类型 + */ +export type TiNotificationType = 'success' | 'prompt' | 'warn' | 'error' | 'simple'; + +/** + * 返回的实例对象中提供方法 + */ +export type TiNotificationRef = Pick, 'close'>; + +/** + * @ignore + */ +export type TiNoticeBasicConfig = Omit, 'name' | 'onClose'>; + +/** + * @ignore + * 用户信息配置项 + * 可选传入,暂时不处理左右边距 + */ +export type TiNotificationConfig = Omit; + +/** + * 包装后的 notice 信息 + * @ignore + */ +export interface TiNoticeData { + /** + * @ignore + * 通知弹窗类型,包括 'success' | 'prompt' | 'warn' | 'error' | 'simple' + */ + type?: TiNotificationType; + /** + * 通知弹窗内容,可能为字符串内容或 template + * @ignore + */ + content?: string | TemplateRef; + /** + * @ignore + * 通知弹窗唯一标识 + */ + noticeId?: string; + /** + * 可选配置内容 + */ + config?: TiNoticeConfig; + /** + * @ignore + * 通知弹窗进出场状态 + */ + state?: 'enter' | 'leave'; + /** + * @ignore + * 自定义内容通知弹窗的 TemplateRef + */ + template?: TemplateRef; + /** + * @ignore + * 返回给用户的关闭引用 + */ + close?: () => void; +} + +/** + * @ignore + * 用户输入的配置 + */ +export interface TiNoticeConfig { + /** + * 通知弹窗索引,可通过 name 改变弹窗属性 + */ + name?: string; + /** + * 通知弹窗位置 + * @default 'top-right' + */ + position?: TiNotificationPosition; + /** + * 自动关闭时间,单位毫秒 + * @default 4500 + */ + duration?: number; + /** + * 是否开启动画 + * @default true + */ + animation?: boolean; + /** + * 是否在鼠标悬停时暂停自动关闭的计时 + * @default true + */ + hoverPause?: boolean; + /** + * 通知弹窗关闭后的回调函数 + */ + onClose?: () => void; +} + +/** + * 全局配置 + */ +export interface TiNotificationUserConfig { + top?: string | 0; + left?: string | 0; + bottom?: string | 0; + right?: string | 0; +} diff --git a/src/notification/lib/src/TiNotificationMapper.ts b/src/notification/lib/src/TiNotificationMapper.ts new file mode 100644 index 0000000..3e578a9 --- /dev/null +++ b/src/notification/lib/src/TiNotificationMapper.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Injectable } from '@angular/core'; +import { TiNotificationMapperItem } from './TiNotificationInterface'; + +/** + * 全局容器字典,用来存储外层容器 + */ +@Injectable({ + providedIn: 'root' +}) +export class TiNotificationMapper { + private mapper: Map = new Map(); + + private static readonly KEY: string = 'ti-notification'; + + setItem(target: any): void { + const item: TiNotificationMapperItem = { target }; + this.mapper.set(TiNotificationMapper.KEY, item); + } + + getItem(): T | null { + if (this.mapper.has(TiNotificationMapper.KEY)) { + return this.mapper.get(TiNotificationMapper.KEY)!.target as T; + } + return; + } +} diff --git a/src/notification/lib/src/TiNotificationModule.ts b/src/notification/lib/src/TiNotificationModule.ts new file mode 100644 index 0000000..46ec9a3 --- /dev/null +++ b/src/notification/lib/src/TiNotificationModule.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { OverlayModule } from '@angular/cdk/overlay'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { TiNotificationContainerComponent } from './TiNotificationContainerComponent'; +import { TiNotificationComponent } from './TiNotificationComponent'; + +import { TiAlertModule } from '@opentiny/ng-alert'; +import { TiIconModule } from '@opentiny/ng-icon'; + +@NgModule({ + imports: [OverlayModule, CommonModule, TiAlertModule, TiIconModule], + declarations: [TiNotificationComponent, TiNotificationContainerComponent] +}) +export class TiNotificationModule {} diff --git a/src/notification/lib/src/TiNotificationMotion.ts b/src/notification/lib/src/TiNotificationMotion.ts new file mode 100644 index 0000000..1f150b7 --- /dev/null +++ b/src/notification/lib/src/TiNotificationMotion.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { animate, AnimationTriggerMetadata, state, style, transition, trigger } from '@angular/animations'; + +export const notificationMotion: AnimationTriggerMetadata = trigger('notificationMotion', [ + state('rightEnter', style({ opacity: 1, transform: 'translateX(0)' })), + state('topEnter', style({ opacity: 1, transform: 'translateY(0)' })), + state('leftEnter', style({ opacity: 1, transform: 'translateX(0)' })), + state('bottomEnter', style({ opacity: 1, transform: 'translateY(0)' })), + state('leave', style({ opacity: 0, transform: 'scaleY(0.3)', transformOrigin: '0% 0%' })), + transition('* => rightEnter', [style({ opacity: 0, transform: 'translateX(10%)' }), animate('200ms linear')]), + transition('* => topEnter', [style({ opacity: 0, transform: 'translateY(-10%)' }), animate('200ms linear')]), + transition('* => leftEnter', [style({ opacity: 0, transform: 'translateX(-10%)' }), animate('200ms linear')]), + transition('* => bottomEnter', [style({ opacity: 0, transform: 'translateY(10%)' }), animate('200ms linear')]), + transition('* => leave', [style({ opacity: 1, transform: 'scaleY(1)', transformOrigin: '0% 0%' }), animate('200ms linear')]) +]); diff --git a/src/notification/lib/src/TiNotificationService.ts b/src/notification/lib/src/TiNotificationService.ts new file mode 100644 index 0000000..aa081b5 --- /dev/null +++ b/src/notification/lib/src/TiNotificationService.ts @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Overlay, GlobalPositionStrategy, NoopScrollStrategy, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { ComponentRef, Injectable, Injector, TemplateRef } from '@angular/core'; +import { TiNotificationModule } from './TiNotificationModule'; +import { TiNotificationMapper } from './TiNotificationMapper'; +import { TiNotificationContainerComponent } from './TiNotificationContainerComponent'; +import { TiNoticeConfig, TiNotificationRef, TiNoticeData, TiNotificationType, TiNotificationConfig } from './TiNotificationInterface'; + +@Injectable({ + providedIn: TiNotificationModule +}) +export class TiNotificationService { + private container: TiNotificationContainerComponent; + + private static readonly prefix: string = 'ti-notification-'; + + private static notificationFlag: number = 0; + + constructor(private mapper: TiNotificationMapper, private overlay: Overlay, private injector: Injector) {} + + private static createNoticeId(): string { + return `${this.prefix}${this.notificationFlag++}`; + } + + private createContainer(): TiNotificationContainerComponent { + let container: TiNotificationContainerComponent = this.mapper.getItem(); + if (container) { + return container; + } + + const overlayRef: OverlayRef = this.overlay.create({ + hasBackdrop: false, + disposeOnNavigation: false, + scrollStrategy: new NoopScrollStrategy(), + positionStrategy: new GlobalPositionStrategy() + }); + const componentRef: ComponentRef = overlayRef.attach( + new ComponentPortal(TiNotificationContainerComponent, null, this.injector) + ); + container = componentRef.instance; + this.mapper.setItem(container); + this.container = componentRef.instance; + this.container.setConsoleHeader(); + return container; + } + + /** + * 打开成功通知弹窗 + */ + success(content: string, config?: TiNoticeConfig): TiNotificationRef { + return this.action({ type: 'success', content, config }); + } + + /** + * 打开失败通知弹窗 + */ + error(content: string, config?: TiNoticeConfig): TiNotificationRef { + return this.action({ type: 'error', content, config }); + } + + /** + * 打开提示通知弹窗 + */ + prompt(content: string, config?: TiNoticeConfig): TiNotificationRef { + return this.action({ type: 'prompt', content, config }); + } + + /** + * 打开警告通知弹窗 + */ + warn(content: string, config?: TiNoticeConfig): TiNotificationRef { + return this.action({ type: 'warn', content, config }); + } + + /** + * 打开无图标通知弹窗 + */ + simple(content: string, config?: TiNoticeConfig): TiNotificationRef { + return this.action({ type: 'simple', content, config }); + } + + /** + * 打开通知弹窗方法,已对外提供五个包装方法,此方法暂时对用户隐藏 + * @ignore + * @param type 通知弹窗类型 + * @param content 通知弹窗文案 + * @param config 通知弹窗配置 + * @returns 通知弹窗包装实例,包含一个可关闭对象 + */ + open(type: TiNotificationType, content: string, config?: TiNoticeConfig): TiNotificationRef { + return this.action({ type, content, config }); + } + + /** + * 打开自定义内容通知弹窗 + * @param template 自定义模板 + * @param config 自定义配置 + * @returns 通知弹窗包装实例,包含一个可关闭对象 + */ + template(template: TemplateRef, config?: TiNoticeConfig): TiNotificationRef { + return this.action({ template, config }); + } + + private action(baseData: TiNoticeData): TiNotificationRef { + this.container = this.createContainer(); + + return this.container.create({ + ...baseData, + ...{ + noticeId: TiNotificationService.createNoticeId() + } + }); + } + /** + * 关闭全部已打开的通知弹窗 + */ + closeAll(): void { + this.container.closeAll(); + } + /** + * 设置全局接口 + */ + config(userConfig: TiNotificationConfig): void { + this.container = this.createContainer(); + this.container.config(userConfig); + } +} diff --git a/src/notification/lib/src/notification.less b/src/notification/lib/src/notification.less new file mode 100644 index 0000000..4acdfc7 --- /dev/null +++ b/src/notification/lib/src/notification.less @@ -0,0 +1,34 @@ +@import '../../../themes/basic/base-all.less'; +@import './position.less'; + +:host { + --ti-notification-max-width: 400px; +} + +@ti-notification-prefix: ~'ti3-notification'; + +.@{ti-notification-prefix} { + position: fixed; + z-index: @zindex-notification; + &-notice { + background-color: var(--ti-common-color-bg-white-normal); + margin-bottom: var(--ti-common-space-4x); + max-width: var(--ti-notification-max-width); + position: relative; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + color: var(--ti-common-color-text-primary); + overflow: auto; + &-close { + display: block; + outline: none; + cursor: pointer; + position: absolute; + top: var(--ti-common-space-3x); + right: var(--ti-common-space-4x); + color: var(--ti-common-color-icon-normal); + font-size: var(--ti-common-size-3x); + } + } + .set-position(); +} diff --git a/src/notification/lib/src/position.less b/src/notification/lib/src/position.less new file mode 100644 index 0000000..1563b85 --- /dev/null +++ b/src/notification/lib/src/position.less @@ -0,0 +1,33 @@ +@ti-notification-position-top: ~'&-top-right, &-top, &-top-left'; +@ti-notification-position-bottom: ~'&-bottom-left, &-bottom, &-bottom-right'; +@ti-notification-position-left: ~'&-bottom-left, &-top-left'; +@ti-notification-position-right: ~'&-top-right, &-bottom-right'; +@ti-notification-position-center: ~'&-top, &-bottom'; + +.set-position() { + @{ti-notification-position-top} { + top: var(--ti-common-space-6x); + } + + @{ti-notification-position-bottom} { + bottom: var(--ti-common-space-6x); + } + + @{ti-notification-position-center} { + left: 50%; + transform: translate(-50%); + margin: atuo; + } + + @{ti-notification-position-left} { + left: 0; + margin-right: var(--ti-common-space-0); + margin-left: var(--ti-common-space-6x); + } + + @{ti-notification-position-right} { + right: 0; + margin-left: var(--ti-common-space-0); + margin-right: var(--ti-common-space-6x); + } +} diff --git a/src/outline/lib/index.ts b/src/outline/lib/index.ts new file mode 100644 index 0000000..d7e1022 --- /dev/null +++ b/src/outline/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiOutlineModule'; diff --git a/src/outline/lib/ng-package.json b/src/outline/lib/ng-package.json new file mode 100644 index 0000000..f47b6dd --- /dev/null +++ b/src/outline/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/outline", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/outline/lib/package.json b/src/outline/lib/package.json new file mode 100644 index 0000000..fb63c20 --- /dev/null +++ b/src/outline/lib/package.json @@ -0,0 +1,9 @@ +{ + "name": "@opentiny/ng-outline", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/outline/lib/project.json b/src/outline/lib/project.json new file mode 100644 index 0000000..50de636 --- /dev/null +++ b/src/outline/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/outline/lib", + "sourceRoot": "src/outline/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/outline"], + "options": { + "project": "src/outline/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/outline"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js outline" + }, + { + "command": "ng default-build outline" + }, + { + "command": "node build/clear-default-theme.js outline" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/outline && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build outline && ng pack outline && node build/publish.js outline --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/outline/lib/src/TiOutlineDirective.ts b/src/outline/lib/src/TiOutlineDirective.ts new file mode 100644 index 0000000..ff4c8b3 --- /dev/null +++ b/src/outline/lib/src/TiOutlineDirective.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Renderer2, NgZone, AfterViewInit, OnDestroy, Inject } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +/** + * @ignore + * 点击需要和聚焦区分开。当点击的时候,不需要有outline。 + */ +@Directive({ + selector: '[tiOutline]' +}) +export class TiOutlineDirective implements AfterViewInit { + constructor(private renderer: Renderer2, public hostEle: ElementRef, private zone: NgZone, @Inject(DOCUMENT) private document) {} + private documentVisibilitychangeListener: () => void; + ngAfterViewInit(): void { + this.zone.runOutsideAngular(() => { + let outlineColor: string; + const outlineHostEle: any = this.hostEle.nativeElement; + const transparentColor: string = 'rgba(0, 0, 0, 0)'; // 透明色,跟transparent色值一致 + if (outlineHostEle) { + this.renderer.listen(outlineHostEle, 'mousedown', (): void => { + // 当点击的时候,不需要有outline。 + this.renderer.setStyle(outlineHostEle, 'outlineColor', 'transparent'); + // 注意:仅仅设置outlineColor为透明色,会导致点击后border不可见 + this.renderer.setStyle(outlineHostEle, 'outlineWidth', '0px'); + }); + this.renderer.listen(outlineHostEle, 'blur', (): void => { + // blur时仅能读取到outlineColor,作为窗口切换前的状态标志。 + outlineColor = getComputedStyle(outlineHostEle).outlineColor; + // 恢复outline原生状态 + this.renderer.setStyle(outlineHostEle, 'outline', ''); + }); + } + this.documentVisibilitychangeListener = this.renderer.listen(this.document, 'visibilitychange', (): void => { + if (document.visibilityState === 'visible' && this.document.activeElement === outlineHostEle && outlineColor === transparentColor) { + this.renderer.setStyle(outlineHostEle, 'outlineColor', 'transparent'); + // 解决checkbox组件聚焦状态下页面切换之后,border看不见问题。 + this.renderer.setStyle(outlineHostEle, 'outlineWidth', '0px'); + } + }); + }); + } + + ngOnDestroy(): void { + this.documentVisibilitychangeListener && this.documentVisibilitychangeListener(); + } +} diff --git a/src/outline/lib/src/TiOutlineModule.ts b/src/outline/lib/src/TiOutlineModule.ts new file mode 100644 index 0000000..d9691d5 --- /dev/null +++ b/src/outline/lib/src/TiOutlineModule.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiOutlineDirective } from './TiOutlineDirective'; + +@NgModule({ + imports: [CommonModule], + exports: [TiOutlineDirective], + declarations: [TiOutlineDirective] +}) +export class TiOutlineModule {} +export { TiOutlineDirective } from './TiOutlineDirective'; diff --git a/src/overflow/demo/karma.conf.js b/src/overflow/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/overflow/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/overflow/demo/project.json b/src/overflow/demo/project.json new file mode 100644 index 0000000..d414577 --- /dev/null +++ b/src/overflow/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/overflow/demo", + "sourceRoot": "src/overflow/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/overflow", + "index": "src/overflow/demo/src/index.html", + "main": "src/overflow/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/overflow/demo/tsconfig.app.json", + "assets": ["src/overflow/demo/src/favicon.ico", "src/overflow/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "overflow-demo:build:production" + }, + "development": { + "browserTarget": "overflow-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js overflow" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/overflow/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/overflow/demo/tsconfig.spec.json", + "karmaConfig": "src/overflow/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/overflow/demo/src/app/AppComponent.ts b/src/overflow/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/overflow/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/overflow/demo/src/app/AppModule.ts b/src/overflow/demo/src/app/AppModule.ts new file mode 100644 index 0000000..e303c2e --- /dev/null +++ b/src/overflow/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { OverflowTestModule } from './overflow/OverflowTestModule'; + +@NgModule({ + imports: [ + OverflowTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/overflow/demo/src/app/IndexComponent.ts b/src/overflow/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..3bbe2ec --- /dev/null +++ b/src/overflow/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { OverflowTestModule } from './overflow/OverflowTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = OverflowTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/overflow/demo/src/app/app.html b/src/overflow/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/overflow/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/overflow/demo/src/app/overflow/OverflowDestoryComponent.ts b/src/overflow/demo/src/app/overflow/OverflowDestoryComponent.ts new file mode 100644 index 0000000..10e0b55 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowDestoryComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './overflow-destory.html', + styles: [ + ` + .overflow-cls { + max-width: 200px; + line-height: 30px; + background-color: yellow; + } + ` + ] +}) +export class OverflowDestoryComponent { + textContent: string = '明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。'; + show: boolean = true; + clickFn() { + setTimeout(() => { + this.show = false; + }, 2000); + } +} diff --git a/src/overflow/demo/src/app/overflow/OverflowDirectiveComponent.ts b/src/overflow/demo/src/app/overflow/OverflowDirectiveComponent.ts new file mode 100644 index 0000000..f0710f2 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowDirectiveComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './overflow-directive.html', + styles: [ + ` + .container { + border: 1px solid #000; + width: 70px; + height: 60px; + } + .overflow-cls { + max-width: 100px; + padding: 10px; + background-color: yellow; + } + ` + ] +}) +export class OverflowDirectiveComponent {} diff --git a/src/overflow/demo/src/app/overflow/OverflowMaxlineComponent.ts b/src/overflow/demo/src/app/overflow/OverflowMaxlineComponent.ts new file mode 100644 index 0000000..dc6c716 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowMaxlineComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './overflow-maxline.html' +}) +export class OverflowMaxlineComponent { + textContent: string = '明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。'; +} diff --git a/src/overflow/demo/src/app/overflow/OverflowMaxwidthComponent.ts b/src/overflow/demo/src/app/overflow/OverflowMaxwidthComponent.ts new file mode 100644 index 0000000..c0aa432 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowMaxwidthComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './overflow-maxwidth.html' +}) +export class OverflowMaxwidthComponent { + textContent: string = '明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。'; +} diff --git a/src/overflow/demo/src/app/overflow/OverflowPositionComponent.ts b/src/overflow/demo/src/app/overflow/OverflowPositionComponent.ts new file mode 100644 index 0000000..632bc26 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowPositionComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './overflow-position.html' +}) +export class OverflowPositionComponent { + textContent: string = '明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。'; +} diff --git a/src/overflow/demo/src/app/overflow/OverflowServiceComponent.ts b/src/overflow/demo/src/app/overflow/OverflowServiceComponent.ts new file mode 100644 index 0000000..900b42b --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowServiceComponent.ts @@ -0,0 +1,42 @@ +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { TiOverflowService } from '@opentiny/ng'; + +@Component({ + templateUrl: './overflow-service.html', + styles: [ + ` + .test1 { + max-width: 50px; + } + .container { + border: 1px solid #000; + width: 70px; + height: 60px; + } + .child { + max-width: calc(100% - 20px); + border: 1px solid #000; + box-sizing: border-box; + } + ` + ] +}) +export class OverflowServiceComponent implements OnInit { + @ViewChild('ele1', { static: true }) ele1: ElementRef; + @ViewChild('ele2', { static: true }) ele2: ElementRef; + @ViewChild('ele3', { static: true }) ele3: ElementRef; + @ViewChild('ele4', { static: true }) ele4: ElementRef; + constructor(private overflowService: TiOverflowService) {} + ngOnInit(): void { + this.overflowService.create(this.ele1.nativeElement); + this.overflowService.create(this.ele2.nativeElement, { + tipPosition: 'right', + tipContent: 'my tip define with aa', + tipMaxWidth: '100px' + }); + this.overflowService.create(this.ele3.nativeElement, { + tipElement: this.ele4.nativeElement, + tipPosition: 'right' + }); + } +} diff --git a/src/overflow/demo/src/app/overflow/OverflowTestComponent.ts b/src/overflow/demo/src/app/overflow/OverflowTestComponent.ts new file mode 100644 index 0000000..ea845c7 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowTestComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +@Component({ + templateUrl: './overflow-test.html' +}) +export class OverflowTestComponent { + constructor(private router: Router) {} + goto(): void { + this.router.navigate(['/autocomplete/autocomplete-events']); + } +} diff --git a/src/overflow/demo/src/app/overflow/OverflowTestModule.ts b/src/overflow/demo/src/app/overflow/OverflowTestModule.ts new file mode 100644 index 0000000..dc6bf7f --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowTestModule.ts @@ -0,0 +1,56 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule } from '@angular/common'; + +import { TiIconModule, TiOverflowModule } from '@opentiny/ng'; + +import { OverflowTestComponent } from './OverflowTestComponent'; +import { OverflowMaxlineComponent } from './OverflowMaxlineComponent'; +import { OverflowMaxwidthComponent } from './OverflowMaxwidthComponent'; +import { OverflowTipcontentComponent } from './OverflowTipcontentComponent'; +import { OverflowPositionComponent } from './OverflowPositionComponent'; +import { OverflowDirectiveComponent } from './OverflowDirectiveComponent'; +import { OverflowServiceComponent } from './OverflowServiceComponent'; +import { OverflowDestoryComponent } from './OverflowDestoryComponent'; + +@NgModule({ + imports: [TiIconModule, TiOverflowModule, CommonModule, RouterModule.forChild(OverflowTestModule.ROUTES)], + declarations: [ + OverflowTestComponent, + OverflowServiceComponent, + OverflowMaxlineComponent, + OverflowMaxwidthComponent, + OverflowTipcontentComponent, + OverflowPositionComponent, + OverflowDirectiveComponent, + OverflowDestoryComponent + ] +}) +export class OverflowTestModule { + static readonly LINKS: Array = [{ href: 'directives/TiOverflowDirective.html', label: 'Overflow' }]; + static readonly ROUTES: Routes = [ + { + path: 'overflow/overflow-directive', + component: OverflowDirectiveComponent + }, + { + path: 'overflow/overflow-maxline', + component: OverflowMaxlineComponent + }, + { + path: 'overflow/overflow-maxwidth', + component: OverflowMaxwidthComponent + }, + { + path: 'overflow/overflow-tipcontent', + component: OverflowTipcontentComponent + }, + { + path: 'overflow/overflow-position', + component: OverflowPositionComponent + }, + { path: 'overflow/overflow-service', component: OverflowServiceComponent }, + { path: 'overflow/overflow-test', component: OverflowTestComponent }, + { path: 'overflow/overflow-destory', component: OverflowDestoryComponent } + ]; +} diff --git a/src/overflow/demo/src/app/overflow/OverflowTipcontentComponent.ts b/src/overflow/demo/src/app/overflow/OverflowTipcontentComponent.ts new file mode 100644 index 0000000..3b32174 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowTipcontentComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './overflow-tipcontent.html' +}) +export class OverflowTipcontentComponent { + textContent: string = '明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。'; + textContent1: string = '设置为空,文本超长没有提示信息,仅有溢出样式'; +} diff --git a/src/overflow/demo/src/app/overflow/overflow-destory.html b/src/overflow/demo/src/app/overflow/overflow-destory.html new file mode 100644 index 0000000..dcff85e --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-destory.html @@ -0,0 +1,10 @@ +

    描述

    +

    + 1、overflow指令在labelEdit组件中使用,该示例测试销毁场景: + 点击隐藏按钮,宿主元素延时销毁,鼠标hover到宿主元素,tip呈现,等到宿主元素销毁时查看tip是否销毁 +

    +

    示例

    +
    +
    {{textContent}}
    +
    + diff --git a/src/overflow/demo/src/app/overflow/overflow-directive.html b/src/overflow/demo/src/app/overflow/overflow-directive.html new file mode 100644 index 0000000..ea1d264 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-directive.html @@ -0,0 +1,16 @@ +

    导入

    +

    import {{ '{' }} TiOverflowModule {{ '}' }} from '@opentiny/ng';

    + +
    明月几时有,把酒问青天。
    +
    +

    通过tiTipPosition定义tip位置:

    +
    明月几时有,把酒问青天。
    +
    +

    通过tiTipMaxWidth定义Tip最大宽度:

    +
    明月几时有,把酒问青天。
    +
    +

    通过tiTipContent定义Tip内容:

    +
    明月几时有,把酒问青天。
    +
    +

    当通过tiTipContent定义Tip内容为空字符串时,不出tip:

    +
    明月几时有,把酒问青天。
    diff --git a/src/overflow/demo/src/app/overflow/overflow-maxline.html b/src/overflow/demo/src/app/overflow/overflow-maxline.html new file mode 100644 index 0000000..9d4abf7 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-maxline.html @@ -0,0 +1,2 @@ +
    {{textContent}}
    +
    diff --git a/src/overflow/demo/src/app/overflow/overflow-maxwidth.html b/src/overflow/demo/src/app/overflow/overflow-maxwidth.html new file mode 100644 index 0000000..41e8c06 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-maxwidth.html @@ -0,0 +1,4 @@ +
    {{textContent}}
    +
    + 多行:{{textContent}} +
    diff --git a/src/overflow/demo/src/app/overflow/overflow-position.html b/src/overflow/demo/src/app/overflow/overflow-position.html new file mode 100644 index 0000000..699f7fb --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-position.html @@ -0,0 +1,9 @@ +
    +
    {{textContent}}
    +
    {{textContent}}
    +
    {{textContent}}
    +
    {{textContent}}
    +
    + {{textContent}} +
    +
    diff --git a/src/overflow/demo/src/app/overflow/overflow-service.html b/src/overflow/demo/src/app/overflow/overflow-service.html new file mode 100644 index 0000000..a63c243 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-service.html @@ -0,0 +1,9 @@ +
    aaaaaaa
    +
    +

    自定义tip最大宽宽度(100px)、位置(right)、内容:

    +aaaaaaabbbbbbbbbbb +
    +

    自定义tip占位依赖的host元素:

    +
    +
    aaaaaaaaaaaaaa
    +
    diff --git a/src/overflow/demo/src/app/overflow/overflow-test.html b/src/overflow/demo/src/app/overflow/overflow-test.html new file mode 100644 index 0000000..dde4662 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-test.html @@ -0,0 +1,60 @@ +

    1.tiOverflow默认是inline-block,可以设置max-width,min-width,width。width默认值是auto包裹子内容

    +
    binggo
    +
    +
    binggo
    +
    +
    明月几时有,把酒问青天。
    +
    +
    teadmin-<script>alert(1)</script>
    +
    +

    因为overflow:hidden会改变对齐方式。所以后面的span改改对齐方式即可:

    +
    但愿人长久,千里共婵娟。
    +作者:苏轼 +
    +

    文图混排效果:

    +
    + 但愿人长久 + 千里共婵娟。 +
    +

    2.外层设置宽度(随外层),外层必须是block/inline-block,tiOverflow也改为block

    + +
    月有阴晴圆缺,此事古难全。
    +
    + +
    Bingo
    +
    +
    +
    月有阴晴圆缺,此事古难全。
    +
    + +
    月有阴晴圆缺,此事古难全。(此条未截取,因为父类不是block)
    +
    +
    +

    3.外层设置最大宽度(随内层),外层必须是inline-block,tiOverflow也改为block

    + +
    月有阴晴圆缺,此事古难全。
    +
    + +
    bingo
    +
    +
    +
    月有阴晴圆缺,此事古难全。
    +
    + +
    月有阴晴圆缺,此事古难全。(此条未截取,因为父类不是block)
    +
    +

    4.测试跳转到其他路由地址后,tip是否消失

    + 明月几时有,把酒问青天。 + diff --git a/src/overflow/demo/src/app/overflow/overflow-tipcontent.html b/src/overflow/demo/src/app/overflow/overflow-tipcontent.html new file mode 100644 index 0000000..afc6221 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-tipcontent.html @@ -0,0 +1,7 @@ +
    {{textContent}}
    +
    {{textContent}}
    +
    {{textContent1}}
    +
    + 多行:{{textContent}} +
    +
    多行:设置为空,{{textContent}}
    diff --git a/src/overflow/demo/src/app/overflow/webdoc/overflow-demos.js b/src/overflow/demo/src/app/overflow/webdoc/overflow-demos.js new file mode 100644 index 0000000..9cfabd7 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/webdoc/overflow-demos.js @@ -0,0 +1,71 @@ +export default { + column: '2', + demos: [ + { + demoId: 'overflow-maxline', + name: { + 'zh-CN': '单行、多行溢出', + 'en-US': 'maxline', + }, + desc: { + 'zh-CN': + '

    通过属性maxline配置文本最大行数。注意:多行效果需要设置宿主元素的 line-height

    ', + 'en-US': '

    button color

    ', + }, + apis: [ + 'TiOverflowDirective.properties.tiOverflow', + 'TiOverflowMaxlineDirective.properties.tiOverflow', + 'TiOverflowMaxlineDirective.properties.maxLine', + 'TiOverflowMaxlineDirective.properties.textContent', + ], + }, + { + demoId: 'overflow-maxwidth', + name: { + 'zh-CN': '溢出提示的最大宽度', + 'en-US': 'maxwidth', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipMaxWidth配置文本过长时显示的 tip 的最大宽度。

    ', + 'en-US': '

    maxwidth

    ', + }, + apis: [ + 'TiOverflowDirective.properties.tiTipMaxWidth', + 'TiOverflowMaxlineDirective.properties.tiTipMaxWidth', + ], + }, + { + demoId: 'overflow-tipcontent', + name: { + 'zh-CN': 'tip内容', + 'en-US': 'item', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipContent配置文本过长时显示的 tip 内容,默认为宿主元素文本。

    ', + 'en-US': '

    item

    ', + }, + apis: [ + 'TiOverflowDirective.properties.tiTipContent', + 'TiOverflowMaxlineDirective.properties.tiTipContent', + ], + }, + { + demoId: 'overflow-position', + name: { + 'zh-CN': 'tip方向', + 'en-US': 'item', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipPosition配置文本过长时显示的 tip 位置。

    ', + 'en-US': '

    tipposition

    ', + }, + apis: [ + 'TiOverflowDirective.properties.tiTipPosition', + 'TiOverflowMaxlineDirective.properties.tiTipPosition', + ], + }, + ], +}; diff --git a/src/overflow/demo/src/app/overflow/webdoc/overflow.cn.md b/src/overflow/demo/src/app/overflow/webdoc/overflow.cn.md new file mode 100644 index 0000000..c260225 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/webdoc/overflow.cn.md @@ -0,0 +1,27 @@ +--- +title: Overflow 溢出提示 +--- +# Overflow 溢出提示 + +
    + +内容超长时,超出部分用...代替,并通过 tip 显示全部内容。   + ++ 支持单行、多行。 + +```typescript +import { TiOverflowModule } from '@opentiny/ng'; +``` + +
    + +
    + +内容超长时,超出部分用...代替,并通过 tip 显示全部内容。   + ++ 支持单行 多行。 + +```typescript +import { TiOverflowModule } from '@opentiny/ng'; +``` +
    diff --git a/src/overflow/demo/src/app/overflow/webdoc/overflow.en.md b/src/overflow/demo/src/app/overflow/webdoc/overflow.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/webdoc/overflow.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/overflow/demo/src/favicon.ico b/src/overflow/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/overflow/demo/src/index.html b/src/overflow/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/overflow/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/overflow/demo/src/main.ts b/src/overflow/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/overflow/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/overflow/demo/test.ts b/src/overflow/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/overflow/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/overflow/demo/tsconfig.app.json b/src/overflow/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/overflow/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/overflow/demo/tsconfig.spec.json b/src/overflow/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/overflow/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/overflow/lib/index.ts b/src/overflow/lib/index.ts new file mode 100644 index 0000000..8e37685 --- /dev/null +++ b/src/overflow/lib/index.ts @@ -0,0 +1,3 @@ +export * from './src/TiOverflowModule'; +export * from './src/TiOverflowService'; +export * from './src/TiOverflowServiceModule'; diff --git a/src/overflow/lib/ng-package.json b/src/overflow/lib/ng-package.json new file mode 100644 index 0000000..4117011 --- /dev/null +++ b/src/overflow/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/overflow", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/overflow/lib/package.json b/src/overflow/lib/package.json new file mode 100644 index 0000000..8677ef1 --- /dev/null +++ b/src/overflow/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-overflow", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/overflow/lib/project.json b/src/overflow/lib/project.json new file mode 100644 index 0000000..9a3e6a6 --- /dev/null +++ b/src/overflow/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/overflow/lib", + "sourceRoot": "src/overflow/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/overflow"], + "options": { + "project": "src/overflow/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/overflow"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js overflow" + }, + { + "command": "ng default-build overflow" + }, + { + "command": "node build/clear-default-theme.js overflow" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/overflow && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build overflow && ng pack overflow && node build/publish.js overflow --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/overflow/lib/src/TiOverflowDirective.ts b/src/overflow/lib/src/TiOverflowDirective.ts new file mode 100644 index 0000000..32a62f2 --- /dev/null +++ b/src/overflow/lib/src/TiOverflowDirective.ts @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Input, OnInit } from '@angular/core'; +import { TiOverflowService } from './TiOverflowService'; +import { TiPositionType } from '@opentiny/ng-utils'; + +/** + * 超长情况下文本处理出...并tip提示 + * + */ +@Directive({ + selector: '[tiOverflow]:not([maxLine])' +}) +export class TiOverflowDirective implements OnInit { + /** + * 配置宿主元素支持文本过长提示功能,不需要配置属性值,存在该属性即可 + */ + @Input() tiOverflow: any; + /** + * 配置文本过长时显示的 tip 内容,默认为宿主元素文本 + */ + @Input() tiTipContent: string; + /** + * 配置文本过长时显示的 tip 方向 + */ + @Input() tiTipPosition: TiPositionType; + /** + * 配置文本过长时显示的 tip 的最大宽度 + */ + @Input() tiTipMaxWidth: string = '276px'; + /** + * @ignore + * 决定tip水平方向位置的宿主元素配置 + */ + @Input() tiTipHostEleX: Element; + private overflowRef: any; + constructor(private ele: ElementRef, private overflowService: TiOverflowService) {} + ngOnInit(): void { + this.overflowRef = this.overflowService.create(this.ele.nativeElement, { + tipContent: this.tiTipContent, + tipPosition: this.tiTipPosition, + tipHostEleX: this.tiTipHostEleX, + tipMaxWidth: this.tiTipMaxWidth + }); + } + // 服务方式的overflow无法自销毁,因此此处在组件销毁时销毁服务方式生成的组件实例 + ngOnDestroy(): void { + if (this.overflowRef) { + this.overflowRef.destroy(); + } + } +} diff --git a/src/overflow/lib/src/TiOverflowMaxlineDirective.ts b/src/overflow/lib/src/TiOverflowMaxlineDirective.ts new file mode 100644 index 0000000..6a183f7 --- /dev/null +++ b/src/overflow/lib/src/TiOverflowMaxlineDirective.ts @@ -0,0 +1,290 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + AfterViewInit, + Directive, + ElementRef, + EventEmitter, + Inject, + Input, + NgZone, + OnChanges, + OnDestroy, + Output, + Renderer2, + SimpleChanges, + TemplateRef +} from '@angular/core'; +import { TiTipRef, TiTipService, TiTipShowInfo } from '@opentiny/ng-tip'; +import { Util, TiBrowser, TiPositionType } from '@opentiny/ng-utils'; +import { DOCUMENT } from '@angular/common'; + +/** + * 多行文本超出情况下文本处理出...并tip提示 + * + */ +@Directive({ + selector: '[tiOverflow][maxLine]' +}) +export class TiOverflowMaxlineDirective implements AfterViewInit, OnChanges, OnDestroy { + public nativeElement: any; + constructor( + private hostRef: ElementRef, + private renderer2: Renderer2, + private tipService: TiTipService, + private zone: NgZone, + @Inject(DOCUMENT) private document + ) { + this.nativeElement = this.hostRef.nativeElement; + } + /** + * 文本最大行数 + */ + @Input() maxLine: number = 3; + /** + * 配置文本过长时显示的 tip 的最大宽度 + */ + @Input() tiTipMaxWidth: string = '276px'; + /** + * 配置文本过长时显示的 tip 方向 + */ + @Input() tiTipPosition: TiPositionType; + /** + * 配置文本过长时显示的 tip 内容,默认为宿主元素文本 + */ + @Input() tiTipContent: string; + /** + * @ignore + * 文本被截断之后末尾图标展示 + */ + @Input() iconName: string; + /** + * @ignore + * 图标提示 + */ + @Input() iconTip: string | TemplateRef | any = ''; + /** + * @ignore + * 图标提示内容对应的上下文,tip 内容类型为 templateRef 或 Component 形式时会用到该接口,接口为自定义对象形式 + */ + @Input() iconTipContext: any; + /** + * 宿主文本 + */ + @Input() textContent: string; + /** + * @ignore + * 文本被截断之后的末尾填充符号 + */ + @Input() character: string = '...'; // 暂不开放 + /** + * @ignore + * 图标是否可以聚焦,默认不可聚焦。 + */ + @Input() iconFocusable: boolean = false; + /** + * @ignore + * 图标是否灰化,默认不灰化 + */ + @Input() iconDisabled: boolean = false; + /** + * @ignore + * 文本被截断之后末尾图标点击事件 + */ + @Output() readonly iconClick: EventEmitter = new EventEmitter(); + private text: string; // 宿主元素文本 + private isShave: boolean; // 是否已经截取 + private tipInstance: TiTipRef; // tip实例 + private icontipInstance: TiTipRef; // icontip实例 + + private windowResizeListener: () => void; + private tipContent: string; // 最终展示的tip内容 + private documentVisibilitychangeListener: () => void; + + private shaveTextFn = (): void => { + let fontHtml: string; + if (Util.isEmptyString(this.iconName)) { + fontHtml = ''; + } else { + // 此处添加属性tiOverflowEndicon为了适配在plus3中定义末尾图标的样式 + // 在labelEditor组件中,需要可以聚焦。 + fontHtml = this.iconFocusable + ? `` + : ``; + } + // 修复SSR错误:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle === 'undefined') { + return; + } + const lineHeight: number = parseFloat(getComputedStyle(this.nativeElement).getPropertyValue('line-height')); + const multiLineHeight: number = lineHeight * this.maxLine; + this.nativeElement.textContent = this.text; + // 如果该元素为inline元素时,宽度不生效会导致元素出...样式不生效,因此此处做处理 + if (getComputedStyle(this.nativeElement).display === 'inline') { + this.renderer2.setStyle(this.nativeElement, 'display', 'inline-block'); + } + this.nativeElement.insertAdjacentHTML('beforeend', fontHtml); + if (this.text.length < 2 || this.nativeElement.offsetHeight <= multiLineHeight) { + this.setEvents(); + this.isShave = false; + + return; + } + let charHtml: string = this.character; + charHtml = charHtml.concat(fontHtml); + // 以下使用二分算法计算文本截取位置 + let max: number = this.text.length - 1; + let min: number = 0; + let middle: number; + while (min < max) { + middle = (min + max + 1) / 2; + this.nativeElement.textContent = this.text.slice(0, middle); + this.nativeElement.insertAdjacentHTML('beforeend', charHtml); + if (this.nativeElement.offsetHeight > multiLineHeight + 1) { + max = middle - 1; // 截取的内容少 + } else { + min = middle; // 截取的内容多 + } + } + this.nativeElement.textContent = this.text.slice(0, max); + this.nativeElement.insertAdjacentHTML('beforeend', charHtml); + this.setEvents(); + this.isShave = true; + }; + + ngOnChanges(changes: SimpleChanges): void { + this.text = this.textContent || this.nativeElement.innerHTML; + if ((changes.maxLine && !changes.maxLine.firstChange) || (changes.textContent && !changes.textContent.firstChange)) { + this.shaveTextFn(); + } + + if (this.iconFocusable && changes.iconDisabled && !changes.iconDisabled.firstChange) { + const spanEle: HTMLElement = this.nativeElement.querySelector('span[tiOverflowEndicon]'); + if (spanEle) { + this.renderer2.setAttribute(spanEle, 'tabindex', this.iconDisabled ? '-1' : '0'); + } + } + } + // tip配置 + ngAfterViewInit(): void { + this.text = this.textContent || this.nativeElement.innerHTML; // 视图初始化完成后获取宿主元素文本 + if (TiBrowser.isIE()) { + setTimeout(() => { + this.shaveTextFn(); // IE下需延时处理,否则初始化获取到 offsetHeight 值与谷歌有差异 + }, 0); + } else { + this.shaveTextFn(); + } + this.tipInstance = this.tipService.create(this.nativeElement, { + position: this.tiTipPosition || 'right', + maxWidth: this.tiTipMaxWidth, + trigger: 'mouse', + showFn: (): TiTipShowInfo => { + if (!this.isShave) { + return; + } + this.tipContent = this.tiTipContent !== undefined ? this.tiTipContent : this.text; + + return { content: this.tipContent }; + } + }); + // 修复SSR错误:ERROR ReferenceError: window is not defined + if (typeof window === 'undefined') { + return; + } + this.zone.runOutsideAngular(() => { + this.windowResizeListener = this.renderer2.listen(window, 'resize', this.shaveTextFn); + }); + } + + ngOnDestroy(): void { + if (this.windowResizeListener) { + this.windowResizeListener(); + } + if (this.documentVisibilitychangeListener) { + this.documentVisibilitychangeListener(); + } + // 当组件销毁的时候文本tip也销毁 + // eslint-disable-next-line no-unused-expressions + this.tipInstance?.hide(); + + // 当组件销毁的时候编辑图标tip也销毁 + // eslint-disable-next-line no-unused-expressions + this.icontipInstance?.hide(); + } + + /** + * @ignore + */ + public setEvents(): void { + const clickIconEle: HTMLElement = this.nativeElement.querySelector('span[tiOverflowEndicon]'); + if (!clickIconEle) { + return; + } + + this.renderer2.listen(clickIconEle, 'click', (): void => { + this.zone.run(() => { + if (this.icontipInstance) { + this.icontipInstance.hide(); + } + this.iconClick.emit(); + }); + }); + this.renderer2.listen(clickIconEle, 'mouseenter', (): void => { + if (this.tipInstance) { + this.zone.run(() => { + this.tipInstance.hide(); + }); + } + }); + this.renderer2.listen(clickIconEle, 'mouseleave', (): void => { + if (this.tipInstance && this.isShave) { + this.zone.run(() => { + Util.trigger(this.nativeElement, 'mouseenter'); + }); + } + }); + this.icontipInstance = this.tipService.create(clickIconEle, { + position: 'top', + maxWidth: this.tiTipMaxWidth, + trigger: 'mouse', + showFn: (): TiTipShowInfo => { + return { + content: this.iconTip, + context: this.iconTipContext + }; + } + }); + if (this.iconFocusable) { + this.zone.runOutsideAngular((): void => { + let outlineColor: string; + const transparentColor: string = 'rgba(0, 0, 0, 0)'; // 透明色,跟transparent色值一致 + this.renderer2.listen(clickIconEle, 'mousedown', (): void => { + this.renderer2.setStyle(clickIconEle, 'outlineColor', 'transparent'); + }); + this.renderer2.listen(clickIconEle, 'blur', (): void => { + // blur时仅能读取到outlineColor,作为窗口切换前的状态标志。 + outlineColor = getComputedStyle(clickIconEle).outlineColor; + // 恢复outline原生状态 + this.renderer2.setStyle(clickIconEle, 'outline', ''); + }); + this.documentVisibilitychangeListener = this.renderer2.listen(this.document, 'visibilitychange', (): void => { + if (document.visibilityState === 'visible' && this.document.activeElement === clickIconEle && outlineColor === transparentColor) { + this.renderer2.setStyle(clickIconEle, 'outlineColor', 'transparent'); + } + }); + }); + } + } +} diff --git a/src/overflow/lib/src/TiOverflowModule.ts b/src/overflow/lib/src/TiOverflowModule.ts new file mode 100644 index 0000000..6acd7c6 --- /dev/null +++ b/src/overflow/lib/src/TiOverflowModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiOverflowDirective } from './TiOverflowDirective'; +import { TiOverflowMaxlineDirective } from './TiOverflowMaxlineDirective'; +import { TiOverflowServiceModule } from './TiOverflowServiceModule'; + +@NgModule({ + imports: [TiOverflowServiceModule], + exports: [TiOverflowDirective, TiOverflowMaxlineDirective], + declarations: [TiOverflowDirective, TiOverflowMaxlineDirective] +}) +export class TiOverflowModule {} +export { TiOverflowDirective } from './TiOverflowDirective'; +export { TiOverflowMaxlineDirective } from './TiOverflowMaxlineDirective'; diff --git a/src/overflow/lib/src/TiOverflowService.ts b/src/overflow/lib/src/TiOverflowService.ts new file mode 100644 index 0000000..366f7c6 --- /dev/null +++ b/src/overflow/lib/src/TiOverflowService.ts @@ -0,0 +1,181 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ComponentRef, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiLog, TiPositionType, Util } from '@opentiny/ng-utils'; +import { TiTipRef, TiTipService, TiTipShowInfo } from '@opentiny/ng-tip'; +import { TiOverflowServiceModule } from './TiOverflowServiceModule'; + +/** + * @ignore + * overflow配置项接口,用于[TiOverflowService.create]{@link ../injectables/TiOverflowService.html#create}参数使用 + */ +export interface TiOverflowConfig { + /** + * tip显示时所依据的元素,使用该元素位置显示tip位置,默认为宿主元素,当tip显示所依赖的元素的和宿主元素不一致时才需要定义 + */ + tipElement?: Element | Function; + /** + * tip内容,默认为宿主元素文本 + */ + tipContent?: string; + /** + * tip位置 + */ + tipPosition?: TiPositionType; + /** + * tip最大宽度 + */ + tipMaxWidth?: string; + /** + * @ignore + * 决定定位元素水平方向的元素,用于宿主元素水平方向位置与host元素不一致的场景,暂不对外开放 + */ + tipHostEleX?: Element; +} +/** + * @ignore + * create方法返回值 + */ +export interface TiOverflowRef { + destroy(): void; +} +/** + * @ignore + * 文本过长出...并tip提示配置项,使用该服务时需要引入模块TiOverflowServiceModule,该组件提供了两种使用方式: + * + * 1.服务方式(见如下说明) + * + * 2.指令方式:[TiOverflowDirective]{@link ../directives/TiOverflowDirective.html} + * + */ +@Injectable({ + providedIn: TiOverflowServiceModule +}) +export class TiOverflowService { + private renderer: Renderer2; + constructor(rendererFactory: RendererFactory2, private tiRenderer: TiRenderer, private tipService: TiTipService) { + this.renderer = rendererFactory.createRenderer(null, null); + } + /** + * 生成tip提示方法 + * @param hostElement 文本过长的宿主对象 + * @param config overflow配置项 + * 返回 {destroy(): void} 销毁文本过长的tip提示,使用服务方式时,需要在宿主元素销毁时,通过调用该方法销毁文本过长的tip提示 + */ + create(hostElement: Element, config?: TiOverflowConfig): TiOverflowRef { + this.tiRenderer.setStyles(hostElement, { + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden' + }); + // 非法情况处理 + if (!Util.isElement(hostElement)) { + TiLog.warn('overflow: hostEle type is not element'); + + // 防止外部使用报错,此处做返回值处理 + return { + destroy(): void {} + }; + } + // 如果该元素为inline元素时,宽度不生效会导致元素出...样式不生效,因此此处做处理 + // 修复SSR报错:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle !== 'undefined' && getComputedStyle(hostElement).display === 'inline') { + this.renderer.setStyle(hostElement, 'display', 'inline-block'); + } + const tipInstance: TiTipRef = this.generateTip(hostElement, config || {}); + + return { + destroy(): void { + if (tipInstance) { + tipInstance.hide(); + } + } + }; + } + /** + * @ignore + */ + public isOverflow(element: Element): boolean { + // 复制DOM,并计算元素宽度 + // 此处使用clone方式而不使用scrollWidth方式,是因为目前发现scrollWidth在IE下获取到的值有问题,不可作为文本溢出的判断条件 + const eleStyles: any = getComputedStyle(element); + const cloneEle: any = element.cloneNode(true); + this.tiRenderer.setStyles(cloneEle, { + // 涉及内容字体的相关样式处理 + fontSize: eleStyles.fontSize, + fontWeight: eleStyles.fontWeight, + fontFamily: eleStyles.fontFamily, + padding: eleStyles.padding, + paddingLeft: eleStyles.paddingLeft, // 处理在IE和火狐下获取padding为空问题:在火狐和IE下只能用只能用padding+[方位]的方式来获取元素的padding值 + paddingRight: eleStyles.paddingRight, + border: eleStyles.border, + boxSizing: eleStyles.boxSizing, + height: eleStyles.height, + // 涉及宽度布局的相关样式处理 + maxWidth: 'none', // 清除最大宽度样式,确保内容可显示完全 + width: 'auto', + overflow: 'visible', + display: 'inline-block', // display block的情况下,元素父容器设置margin时,导致导致body变宽,从而与元素本身宽度不匹配,会导致可显示完全但出tip的问题,因此此处改变其display方式 + visibility: 'hidden', // 元素隐藏但做占位处理 + whiteSpace: 'nowrap', + position: 'absolute', // 避免克隆元素影响页面高度,导致出滚动条 + left: '-9999px', + top: '-9999px' + }); + this.renderer.appendChild(document.body, cloneEle); + // 使用getBoundingClientRect而不使用getComputedStyle,是因为getComputedStyle在 + // 各浏览器获取到的宽度不一致(IE下取到的是内容宽度,而在Chrome和FF下取到的是整个元素宽度)。 + // IE下计算精度高(小数点后15位),多数中文和数字或英文混排的文本计算出来的 maxWidth 和 textWidth + // 由于精度高而导致有微小差距,从而影响了 isOverflow 的判断结果,从实际测试得来结论:保留两位小数能够保证判断结果更准确些。 + const maxWidth: number = parseFloat(element.getBoundingClientRect().width.toFixed(2)); + const textWidth: number = parseFloat(cloneEle.getBoundingClientRect().width.toFixed(2)); + // 此处没有使用angular的Renderer2是因为Renderer2.removeChild必须有变化检测才能在dom上生效, + // 此处考虑到性能不触发变化检测,所以选择使用原生的removeChild方法。 + document.body.removeChild(cloneEle); + + return textWidth > maxWidth; + } + private generateTip(element: Element, options: TiOverflowConfig): TiTipRef { + // 文本超长时,显示tip提示: + // tip显示位置元素设置:默认为element + const config: TiOverflowConfig = options || {}; + let hostEleConfig: any = config.tipElement; + if (typeof hostEleConfig === 'function') { + hostEleConfig = hostEleConfig(element); + } + const tipHostEle: Element = hostEleConfig || element; + + return this.tipService.create(tipHostEle, { + hostEleX: config.tipHostEleX, + position: config.tipPosition, + maxWidth: config.tipMaxWidth, + trigger: 'mouse', + showFn: (): TiTipShowInfo => { + // tipContent 为空 或者 未溢出情况下,不显示tip + // tipPosition设置为none是去除Select默认超长tip,改由template内容自定义tip + if (config.tipPosition === 'none' || config.tipContent === '' || !this.isOverflow(element)) { + return; + } + // tip内容设置:默认为元素中的文本内容 + let tipContent: string = config.tipContent; + if (Util.isUndefined(tipContent)) { + // content可能为"",因此此处判断undefined而不使用|| + // 使用element.textContent不能解析html片段 + tipContent = element.innerHTML; + } + + return { content: tipContent }; + } + }); + } +} diff --git a/src/overflow/lib/src/TiOverflowServiceModule.ts b/src/overflow/lib/src/TiOverflowServiceModule.ts new file mode 100644 index 0000000..050cf84 --- /dev/null +++ b/src/overflow/lib/src/TiOverflowServiceModule.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiTipServiceModule } from '@opentiny/ng-tip'; + +@NgModule({ + imports: [TiRendererModule, TiTipServiceModule] +}) +export class TiOverflowServiceModule {} diff --git a/src/pagination/demo/karma.conf.js b/src/pagination/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/pagination/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/pagination/demo/project.json b/src/pagination/demo/project.json new file mode 100644 index 0000000..eaf7808 --- /dev/null +++ b/src/pagination/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/pagination/demo", + "sourceRoot": "src/pagination/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/pagination", + "index": "src/pagination/demo/src/index.html", + "main": "src/pagination/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/pagination/demo/tsconfig.app.json", + "assets": ["src/pagination/demo/src/favicon.ico", "src/pagination/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "pagination-demo:build:production" + }, + "development": { + "browserTarget": "pagination-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js pagination" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/pagination/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/pagination/demo/tsconfig.spec.json", + "karmaConfig": "src/pagination/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/pagination/demo/src/app/AppComponent.ts b/src/pagination/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/pagination/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/pagination/demo/src/app/AppModule.ts b/src/pagination/demo/src/app/AppModule.ts new file mode 100644 index 0000000..25bcec5 --- /dev/null +++ b/src/pagination/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { PaginationTestModule } from './pagination/PaginationTestModule'; + +@NgModule({ + imports: [ + PaginationTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/pagination/demo/src/app/IndexComponent.ts b/src/pagination/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..710c47f --- /dev/null +++ b/src/pagination/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { PaginationTestModule } from './pagination/PaginationTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = PaginationTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/pagination/demo/src/app/app.html b/src/pagination/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/pagination/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/pagination/demo/src/app/pagination/PaginationAutohideComponent.ts b/src/pagination/demo/src/app/pagination/PaginationAutohideComponent.ts new file mode 100644 index 0000000..34fa4b4 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationAutohideComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { TiPageSizeConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './pagination-autohide.html' +}) +export class PaginationAutohideComponent { + currentPage: number = 1; + totalNumber: number = 400; + pageSize: TiPageSizeConfig = { + options: [20, 40, 80], + size: 40 + }; + onClick(): void { + this.totalNumber = this.totalNumber > 20 ? 12 : 400; + } +} diff --git a/src/pagination/demo/src/app/pagination/PaginationDisabledComponent.ts b/src/pagination/demo/src/app/pagination/PaginationDisabledComponent.ts new file mode 100644 index 0000000..dc7060b --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationDisabledComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './pagination-disabled.html' +}) +export class PaginationDisabledComponent { + currentPage: number = 2; + totalNumber: number = 300; + disabled: boolean = true; +} diff --git a/src/pagination/demo/src/app/pagination/PaginationEventComponent.ts b/src/pagination/demo/src/app/pagination/PaginationEventComponent.ts new file mode 100644 index 0000000..626bac4 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationEventComponent.ts @@ -0,0 +1,32 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiPaginationEvent } from '@opentiny/ng'; + +@Component({ + templateUrl: './pagination-event.html', + encapsulation: ViewEncapsulation.None +}) +export class PaginationEventComponent { + myLogs: Array = []; + currentPage: number = 10; + totalNumber: number = 400; + + onPageNumChange(event: TiPaginationEvent): void { + const str: string = `onPageNumChange data=${JSON.stringify(event)}`; + this.myLogs = [...this.myLogs, str]; + } + + onCurrentPageChange(event: number): void { + const str: string = `onCurrentPageChange data=${event}`; + this.myLogs = [...this.myLogs, str]; + } + + onPageUpdate(event: TiPaginationEvent): void { + const str: string = `onPageUpdate data=${JSON.stringify(event)}`; + this.myLogs = [...this.myLogs, str]; + } + + onTotalNumberChange(event: TiPaginationEvent): void { + const str: string = `onTotalNumberChange data=${JSON.stringify(event)}`; + this.myLogs = [...this.myLogs, str]; + } +} diff --git a/src/pagination/demo/src/app/pagination/PaginationFixedComponent.ts b/src/pagination/demo/src/app/pagination/PaginationFixedComponent.ts new file mode 100644 index 0000000..6dd448d --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationFixedComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './pagination-fixed.html' +}) +export class PaginationFixedComponent { + totalNumber: number = 300; + currentPage: number = 10; + isShow: boolean = false; + onClick(): void { + this.isShow = !this.isShow; + } +} diff --git a/src/pagination/demo/src/app/pagination/PaginationLoadingComponent.ts b/src/pagination/demo/src/app/pagination/PaginationLoadingComponent.ts new file mode 100644 index 0000000..38e5d0d --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationLoadingComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { TiPageSizeConfig } from '@opentiny/ng-pagination'; + +@Component({ + templateUrl: './pagination-loading.html', +}) +export class PaginationLoadingComponent { + pageSize: TiPageSizeConfig = { + size: 10, + options: [10, 20, 50], + }; + loading: boolean = false; + + switch(): void { + this.loading = !this.loading; + } +} diff --git a/src/pagination/demo/src/app/pagination/PaginationPageselectwidthComponent.ts b/src/pagination/demo/src/app/pagination/PaginationPageselectwidthComponent.ts new file mode 100644 index 0000000..7bae11f --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationPageselectwidthComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './pagination-pageselectwidth.html' +}) +export class PaginationPageselectwidthComponent { + currentPage: number = 1; + totalNumber: number = 60000; + pageSelectVirtual: boolean = true; +} diff --git a/src/pagination/demo/src/app/pagination/PaginationPagesizeComponent.ts b/src/pagination/demo/src/app/pagination/PaginationPagesizeComponent.ts new file mode 100644 index 0000000..8ca813f --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationPagesizeComponent.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { TiPageSizeConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './pagination-pagesize.html' +}) +export class PaginationPagesizeComponent { + currentPage: number = 2; + totalNumber: number = 300; + currentPage1: number = 3; + totalNumber1: number = 40000; + currentPage2: number = 3; + totalNumber2: number = 400; + currentPage3: number = 2; + totalNumber3: number = 300; + pageSize: TiPageSizeConfig = { + options: [200, 400, 600, 800], + size: 200 + }; + pageSize1: TiPageSizeConfig = { + options: [10, 20, 50], + size: 10, + width: '100px' + }; + pageSize2: TiPageSizeConfig = { + options: [20, 40, 60, 80], + size: 20, + hide: true + }; +} diff --git a/src/pagination/demo/src/app/pagination/PaginationShowgotolinkComponent.ts b/src/pagination/demo/src/app/pagination/PaginationShowgotolinkComponent.ts new file mode 100644 index 0000000..0c20a22 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationShowgotolinkComponent.ts @@ -0,0 +1,10 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './pagination-showgotolink.html', + encapsulation: ViewEncapsulation.None +}) +export class PaginationShowgotolinkComponent { + currentPage: number = 10; + totalNumber: number = 400; +} diff --git a/src/pagination/demo/src/app/pagination/PaginationShowlastpageComponent.ts b/src/pagination/demo/src/app/pagination/PaginationShowlastpageComponent.ts new file mode 100644 index 0000000..82e8ff5 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationShowlastpageComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './pagination-showlastpage.html' +}) +export class PaginationShowlastpageComponent { + currentPage: number = 1; + totalNumber: number = 400; + showLastPage: boolean = false; +} diff --git a/src/pagination/demo/src/app/pagination/PaginationShowtotalnumberComponent.ts b/src/pagination/demo/src/app/pagination/PaginationShowtotalnumberComponent.ts new file mode 100644 index 0000000..aade384 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationShowtotalnumberComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './pagination-showtotalnumber.html' +}) +export class PaginationShowtotalnumberComponent { + currentPage: number = 1; + totalNumber: number = 400; +} diff --git a/src/pagination/demo/src/app/pagination/PaginationTestModule.ts b/src/pagination/demo/src/app/pagination/PaginationTestModule.ts new file mode 100644 index 0000000..f013d81 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationTestModule.ts @@ -0,0 +1,92 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiPaginationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { PaginationDisabledComponent } from './PaginationDisabledComponent'; +import { PaginationShowlastpageComponent } from './PaginationShowlastpageComponent'; +import { PaginationTypeComponent } from './PaginationTypeComponent'; +import { PaginationEventComponent } from './PaginationEventComponent'; +import { PaginationPagesizeComponent } from './PaginationPagesizeComponent'; +import { PaginationShowgotolinkComponent } from './PaginationShowgotolinkComponent'; +import { PaginationPageselectwidthComponent } from './PaginationPageselectwidthComponent'; +import { PaginationShowtotalnumberComponent } from './PaginationShowtotalnumberComponent'; +import { PaginationAutohideComponent } from './PaginationAutohideComponent'; +import { PaginationFixedComponent } from './PaginationFixedComponent'; +import { PaginationLoadingComponent } from './PaginationLoadingComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiPaginationModule, + TiButtonModule, + DemoLogModule, + RouterModule.forChild(PaginationTestModule.ROUTES) + ], + declarations: [ + PaginationDisabledComponent, + PaginationShowlastpageComponent, + PaginationTypeComponent, + PaginationEventComponent, + PaginationPagesizeComponent, + PaginationShowtotalnumberComponent, + PaginationAutohideComponent, + PaginationShowgotolinkComponent, + PaginationPageselectwidthComponent, + PaginationFixedComponent, + PaginationLoadingComponent + ] +}) +export class PaginationTestModule { + static readonly LINKS: Array = [{ href: 'components/TiPaginationComponent.html', label: 'Pagination' }]; + static readonly ROUTES: Routes = [ + { + path: 'pagination/pagination-type', + component: PaginationTypeComponent + }, + { + path: 'pagination/pagination-pagesize', + component: PaginationPagesizeComponent + }, + { + path: 'pagination/pagination-show-lastpage', + component: PaginationShowlastpageComponent + }, + { + path: 'pagination/pagination-disabled', + component: PaginationDisabledComponent + }, + { + path: 'pagination/pagination-event', + component: PaginationEventComponent + }, + { + path: 'pagination/pagination-showtotalnumber', + component: PaginationShowtotalnumberComponent + }, + { + path: 'pagination/pagination-autohide', + component: PaginationAutohideComponent + }, + { + path: 'pagination/pagination-fixed', + component: PaginationFixedComponent + }, + { + path: 'pagination/pagination-showgotolink', + component: PaginationShowgotolinkComponent + }, + { + path: 'pagination/pagination-pageselectwidth', + component: PaginationPageselectwidthComponent + }, + { + path: 'pagination/pagination-loading', + component: PaginationLoadingComponent + } + ]; +} diff --git a/src/pagination/demo/src/app/pagination/PaginationTypeComponent.ts b/src/pagination/demo/src/app/pagination/PaginationTypeComponent.ts new file mode 100644 index 0000000..03c0798 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationTypeComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './pagination-type.html' +}) +export class PaginationTypeComponent { + currentPage: number = 10; + totalNumber: number = 400; + + typeSimple: string = 'simple'; + currentPageSimple: number = 2; + totalNumberSimple: number = 1600; + + typeMini: string = 'mini'; + currentPageMini: number = 2; + totalNumberMini: number = 300; +} diff --git a/src/pagination/demo/src/app/pagination/pagination-autohide.html b/src/pagination/demo/src/app/pagination/pagination-autohide.html new file mode 100644 index 0000000..14be28a --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-autohide.html @@ -0,0 +1,4 @@ + + +
    + diff --git a/src/pagination/demo/src/app/pagination/pagination-disabled.html b/src/pagination/demo/src/app/pagination/pagination-disabled.html new file mode 100644 index 0000000..b926900 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-disabled.html @@ -0,0 +1 @@ + diff --git a/src/pagination/demo/src/app/pagination/pagination-event.html b/src/pagination/demo/src/app/pagination/pagination-event.html new file mode 100644 index 0000000..bf36502 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-event.html @@ -0,0 +1,10 @@ + + diff --git a/src/pagination/demo/src/app/pagination/pagination-fixed.html b/src/pagination/demo/src/app/pagination/pagination-fixed.html new file mode 100644 index 0000000..73a024a --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-fixed.html @@ -0,0 +1,9 @@ + +
    + diff --git a/src/pagination/demo/src/app/pagination/pagination-loading.html b/src/pagination/demo/src/app/pagination/pagination-loading.html new file mode 100644 index 0000000..e94afb9 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-loading.html @@ -0,0 +1,18 @@ + +
    + + +
    + diff --git a/src/pagination/demo/src/app/pagination/pagination-pageselectwidth.html b/src/pagination/demo/src/app/pagination/pagination-pageselectwidth.html new file mode 100644 index 0000000..70cba2b --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-pageselectwidth.html @@ -0,0 +1,8 @@ + diff --git a/src/pagination/demo/src/app/pagination/pagination-pagesize.html b/src/pagination/demo/src/app/pagination/pagination-pagesize.html new file mode 100644 index 0000000..e33b503 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-pagesize.html @@ -0,0 +1,28 @@ +

    1. options

    + +

    2. width

    + +

    3. hide

    + +

    4. largeData

    + diff --git a/src/pagination/demo/src/app/pagination/pagination-showgotolink.html b/src/pagination/demo/src/app/pagination/pagination-showgotolink.html new file mode 100644 index 0000000..302e638 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-showgotolink.html @@ -0,0 +1,6 @@ + diff --git a/src/pagination/demo/src/app/pagination/pagination-showlastpage.html b/src/pagination/demo/src/app/pagination/pagination-showlastpage.html new file mode 100644 index 0000000..1d4ccd6 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-showlastpage.html @@ -0,0 +1,6 @@ + diff --git a/src/pagination/demo/src/app/pagination/pagination-showtotalnumber.html b/src/pagination/demo/src/app/pagination/pagination-showtotalnumber.html new file mode 100644 index 0000000..290199e --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-showtotalnumber.html @@ -0,0 +1,6 @@ + diff --git a/src/pagination/demo/src/app/pagination/pagination-type.html b/src/pagination/demo/src/app/pagination/pagination-type.html new file mode 100644 index 0000000..5893966 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-type.html @@ -0,0 +1,16 @@ +

    1. default

    + +

    2. simple

    + +

    3. mini

    + diff --git a/src/pagination/demo/src/app/pagination/webdoc/pagination-demos.js b/src/pagination/demo/src/app/pagination/webdoc/pagination-demos.js new file mode 100644 index 0000000..18bcbef --- /dev/null +++ b/src/pagination/demo/src/app/pagination/webdoc/pagination-demos.js @@ -0,0 +1,161 @@ +export default { + column: '2', + demos: [ + { + demoId: 'pagination-type', + name: { + 'zh-CN': '分页类型', + 'en-US': 'pagination type', + }, + desc: { + 'zh-CN': + '

    通过属性type配置分页组件的类型,包括default(默认)simplemini。', + 'en-US': '

    pagination type

    ', + }, + apis: [ + 'TiPaginationComponent.properties.type', + 'TiPaginationComponent.properties.currentPage', + 'TiPaginationComponent.properties.totalNumber', + ], + }, + { + demoId: 'pagination-pagesize', + name: { + 'zh-CN': '下拉框选项', + 'en-US': 'pagination pagesize', + }, + desc: { + 'zh-CN': + '通过属性pageSize实现分页组件每页显示条数的相关配置,包括每页显示条数选项列表、每页显示条数选项列表宽度、隐藏每页显示条数选项列表、默认每页显示条数选项列表。', + 'en-US': '

    pagination pagesize

    ', + }, + apis: [ + 'TiPaginationComponent.properties.pageSize', + 'TiPaginationComponent.properties.largeData', + 'TiPageSizeConfig', + ], + }, + { + demoId: 'pagination-pageselectwidth', + name: { + 'zh-CN': 'mini 类型大数据场景', + 'en-US': 'pagination pageselectwidth', + }, + desc: { + 'zh-CN': + '通过属性pageSelectWidth配置 mini 类型下拉框宽度;通过属性pageSelectVirtual配置 mini 类型下拉是否开启虚拟滚动。', + 'en-US': '

    pagination pageselectwidth

    ', + }, + apis: [ + 'TiPaginationComponent.properties.pageSelectWidth', + 'TiPaginationComponent.properties.pageSelectVirtual', + ], + }, + { + demoId: 'pagination-showlastpage', + name: { + 'zh-CN': '分页类型', + 'en-US': 'pagination showlastpage', + }, + desc: { + 'zh-CN': + '

    通过属性showLastPage配置是否展示最后一页页码。', + 'en-US': '

    pagination showlastpage

    ', + }, + apis: ['TiPaginationComponent.properties.showLastPage'], + }, + { + demoId: 'pagination-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'pagination disabled', + }, + desc: { + 'zh-CN': '通过属性disabled配置分页是否为禁用状态。', + 'en-US': '

    pagination disabled

    ', + }, + apis: ['TiPaginationComponent.properties.disabled'], + }, + { + demoId: 'pagination-event', + name: { + 'zh-CN': '事件', + 'en-US': 'pagination event', + }, + desc: { + 'zh-CN': + '当操作选择框改变每页显示条数的时候触发pageNumChange事件;当组件内部改变当前页码的时候触发currentPageChange事件;当操作选择框改变每页显示条数或操作页码改变当前页码的时候触发pageUpdate事件;当组件内部改变总条数的时候触发totalNumberChange事件。', + 'en-US': '

    pagination event

    ', + }, + apis: [ + 'TiPaginationComponent.events.currentPageChange', + 'TiPaginationComponent.events.pageNumChange', + 'TiPaginationComponent.events.pageUpdate', + 'TiPaginationComponent.events.totalNumberChange', + 'TiPaginationEvent', + ], + }, + { + demoId: 'pagination-showtotalnumber', + name: { + 'zh-CN': '展示总条数', + 'en-US': 'pagination showtotalnumber', + }, + desc: { + 'zh-CN': '

    通过属性showTotalNumber配置是否展示总条数。', + 'en-US': '

    pagination showtotalnumber

    ', + }, + apis: ['TiPaginationComponent.properties.showTotalNumber'], + }, + { + demoId: 'pagination-autohide', + name: { + 'zh-CN': '自动隐藏', + 'en-US': 'pagination autohide', + }, + desc: { + 'zh-CN': + '通过属性autoHide配置总条数少于每页显示条数选项列表中的最小值时是否隐藏分页。', + 'en-US': '

    pagination autohide

    ', + }, + apis: ['TiPaginationComponent.properties.autoHide'], + }, + { + demoId: 'pagination-showgotolink', + name: { + 'zh-CN': '跳转按钮', + 'en-US': 'pagination showgotolink', + }, + desc: { + 'zh-CN': + '通过属性showGotoLink配置是否显示跳转按钮,仅在default生效。', + 'en-US': '

    pagination showgotolink

    ', + }, + apis: ['TiPaginationComponent.properties.showGotoLink'], + }, + { + demoId: 'pagination-fixed', + name: { + 'zh-CN': '分页吸底', + 'en-US': 'pagination fixed', + }, + desc: { + 'zh-CN': '

    通过属性fixed配置是否开启分页吸底。', + 'en-US': '

    pagination fixed

    ', + }, + apis: ['TiPaginationComponent.properties.fixed'], + }, + { + demoId: 'pagination-loading', + name: { + 'zh-CN': '加载态', + 'en-US': 'pagination loading', + }, + desc: { + 'zh-CN': '

    通过属性loading配置是否开启加载态。', + 'en-US': '

    pagination loading

    ', + }, + apis: ['TiPaginationComponent.properties.loading'], + }, + ], +}; diff --git a/src/pagination/demo/src/app/pagination/webdoc/pagination.cn.md b/src/pagination/demo/src/app/pagination/webdoc/pagination.cn.md new file mode 100644 index 0000000..358d806 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/webdoc/pagination.cn.md @@ -0,0 +1,24 @@ +--- +title: Pagination 分页 +--- +# Pagination 分页 + +
    + +Pagination 是实现对数据分页显示的组件。   + +```typescript +import { TiPaginationModule } from '@opentiny/ng'; +``` + +
    + +
    + +Pagination 是实现对数据分页显示的组件。   + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/pagination/demo/src/app/pagination/webdoc/pagination.en.md b/src/pagination/demo/src/app/pagination/webdoc/pagination.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/webdoc/pagination.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/pagination/demo/src/favicon.ico b/src/pagination/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/pagination/demo/src/index.html b/src/pagination/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/pagination/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/pagination/demo/src/main.ts b/src/pagination/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/pagination/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/pagination/demo/test.ts b/src/pagination/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/pagination/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/pagination/demo/tsconfig.app.json b/src/pagination/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/pagination/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/pagination/demo/tsconfig.spec.json b/src/pagination/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/pagination/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/pagination/lib/index.ts b/src/pagination/lib/index.ts new file mode 100644 index 0000000..caea4ff --- /dev/null +++ b/src/pagination/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiPaginationModule'; diff --git a/src/pagination/lib/ng-package.json b/src/pagination/lib/ng-package.json new file mode 100644 index 0000000..35c4d3b --- /dev/null +++ b/src/pagination/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/pagination", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/pagination/lib/package.json b/src/pagination/lib/package.json new file mode 100644 index 0000000..ef22cf8 --- /dev/null +++ b/src/pagination/lib/package.json @@ -0,0 +1,18 @@ +{ + "name": "@opentiny/ng-pagination", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-table": "~1.0.0-beta.0", + "@opentiny/ng-select": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/pagination/lib/project.json b/src/pagination/lib/project.json new file mode 100644 index 0000000..0299b31 --- /dev/null +++ b/src/pagination/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/pagination/lib", + "sourceRoot": "src/pagination/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/pagination"], + "options": { + "project": "src/pagination/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/pagination"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js pagination" + }, + { + "command": "ng default-build pagination" + }, + { + "command": "node build/clear-default-theme.js pagination" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/pagination && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build pagination && ng pack pagination && node build/publish.js pagination --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/pagination/lib/src/TiPaginationComponent.ts b/src/pagination/lib/src/TiPaginationComponent.ts new file mode 100644 index 0000000..f6fae05 --- /dev/null +++ b/src/pagination/lib/src/TiPaginationComponent.ts @@ -0,0 +1,853 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ElementRef, + EventEmitter, + Input, + Optional, + Output, + Renderer2, + SimpleChange, + SimpleChanges, + TemplateRef, + ViewChild, + ChangeDetectorRef, + ChangeDetectionStrategy, + NgZone +} from '@angular/core'; +import { Util } from '@opentiny/ng-utils'; +import { TiLocale, TiLocaleFormat } from '@opentiny/ng-locale'; // 获取词条 +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiTableComponent } from '@opentiny/ng-table'; +import { TiSelectComponent } from '@opentiny/ng-select'; +import { Subscription } from 'rxjs'; +import packageInfo from '../package.json'; + +/** + * 实现每页显示条数的相关配置的数据类型 + */ +export interface TiPageSizeConfig { + /** + * 每页显示条数选项列表 + */ + options?: Array; + /** + * 默认每页显示条数 + */ + size?: number; + /** + * 每页显示条数选项列表宽度 + */ + width?: string; + /** + * 是否隐藏每页显示条数选项列表。 + */ + hide?: boolean; +} + +/** + * 当分页更新时触发事件通知出去的参数 + * + * 更新场景包括:1.当前页码改变,2.每页显示条数改变,3.总条数改变。 + */ +export interface TiPaginationEvent { + /** + * 当前页码数 + */ + currentPage: number; + /** + * 每页显示条数 + */ + size: number; + /** + * 总条数 + */ + totalNumber: number; +} + +/** + * @ignore + * 页码的数据类型 + */ +export interface TiPageItem { + id: string; + label: any; +} + +/** + * Pagination分页组件 + * + * 分页组件一般情况下与表格组件配合使用,实现对表格数据的分页显示。 + * + */ +@Component({ + selector: 'ti-pagination', + templateUrl: './pagination.html', + styleUrls: ['./pagination.less', '../../../text/lib/src/text.less'], // 引用text的less文件 + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-page-container]': 'true', + '[class.ti3-pag-container-disable]': 'this.disabled', + '[attr.unselectable]': '"on"', + '[class.ti3-page-container-fixed]': 'fixed' + } +}) +export class TiPaginationComponent extends TiFormComponent { + private static readonly MIN_PAGE: number = 1; + /** + * 分页的类型 + */ + @Input() type: 'default' | 'simple' | 'mini' = 'default'; + /** + * 必选,数据总条数 + */ + @Input() totalNumber: number; + /** + * 组件内部改变总条数时触发的回调,参数:总条数。 + */ + @Output() readonly totalNumberChange: EventEmitter = new EventEmitter(); + /** + * 当前页码数 + */ + @Input() currentPage: number = 1; + /** + * 组件内部改变当前页码时触发的回调,参数:当前页码数。 + */ + @Output() readonly currentPageChange: EventEmitter = new EventEmitter(); + /** + * 必选,每页展示条数的配置 + */ + @Input() pageSize: TiPageSizeConfig; + /** + * 是否显示总条数 + */ + @Input() showTotalNumber: boolean = true; + /** + * 是否显示页码跳转按钮 + */ + @Input() showGotoLink: boolean = false; + /** + * 是否开启自动隐藏 + */ + @Input() autoHide: boolean = false; + /** + * 是否禁用 + */ + @Input() disabled: boolean = false; + /** + * 是否显示最后一页 + */ + @Input() showLastPage: boolean = true; + /** + * 每页展示条数是否使用大数据配置:[20, 50, 100];默认每页显示20条 + */ + @Input() largeData: boolean = false; + /** + * 是否开启分页吸底 + */ + @Input() fixed: boolean = false; + /** + * mini类型分页的下拉框宽度 + */ + @Input() pageSelectWidth: string = '70px'; + /** + * mini类型分页的下拉是否开启虚拟滚动 + */ + @Input() pageSelectVirtual: boolean = false; + /** + * 是否处于加载态 + */ + @Input() loading: boolean = false; + /** + * 当操作下拉框改变每页显示条数时触发的回调,参数:当前页码、每页显示条数和总条数。 + */ + @Output() readonly pageNumChange: EventEmitter = new EventEmitter(); + /** + * 当操作下拉框改变每页显示条数或操作页码改变当前页码时触发的回调,参数:当前页码、每页显示条数和总条数。 + */ + @Output() readonly pageUpdate: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + @ViewChild('mini', { static: true }) mini: TemplateRef = undefined; + /** + * @ignore + */ + @ViewChild('input') inputRef: ElementRef; + /** + * @ignore + * 非mini类型:select下拉组件 + */ + public itemsPerPage: any; + /** + * @ignore + * mini类型:select下拉组件 + */ + public itemsMini: any; + /** + * @ignore + * 用户输入即将跳转的页码 + */ + public gotoPage: { page?: any } = {}; + /** + * @ignore + * 页码列表 + */ + public pages: Array<{ key: any; active: boolean }>; + protected versionInfo: string = super.getVersion(packageInfo); + private hidePage: boolean = false; + private totalPageNum: number; + private oldCurrentPage: number; + private oldTotalNumber: number; + private oldSelected: number; + private oldSize: number; + private subscription: Subscription; + private updateFocusableElems: boolean; + private defaultPageSize: TiPageSizeConfig = { + options: [10, 20, 50], // 下拉选项列表 + size: 10, // 默认每页显示10条 + width: '60px', // select宽度 + hide: false // 设置是否在页面上隐藏每页显示条数选项:下拉列表 + }; + private largePageSize: TiPageSizeConfig = { + options: [20, 50, 100], // 下拉选项列表 + size: 20 // 默认每页显示20条 + }; + private unlistenTiReLayoutX: () => void; + constructor( + protected render: Renderer2, + protected hostRef: ElementRef, + protected changeDetectorRef: ChangeDetectorRef, + @Optional() private table: TiTableComponent, + @Optional() private select: TiSelectComponent, + private zone: NgZone + ) { + super(hostRef, render, changeDetectorRef); + } + + /** + * @description 创建每一个页码对应的数据 + * @params key 页码显示,为数字或者是"···" + * @params isActive 是否为当前激活页 + */ + private static makePage(key: any, isActive: boolean): { key: any; active: boolean } { + return { + key, + active: isActive + }; + } + + ngOnInit(): void { + super.ngOnInit(); + // 组件对外各个接口的初始化处理 + this.initData(); + this.renderPage(); + + if (this.isWithTable()) { + this.subscription = this.table.paginationSubject.subscribe((value: { totalNumber?: number; currentPage?: number }) => { + if (!Util.isUndefined(value.totalNumber) && value.totalNumber !== this.totalNumber) { + this.totalNumber = value.totalNumber; + this.verifyCurrentPage(this.itemsPerPage.selected, true); + this.totalNumberChange.emit(this.totalNumber); + } + + if (!Util.isUndefined(value.currentPage) && value.currentPage !== this.currentPage) { + this.currentPage = value.currentPage; + this.currentPageChange.emit(this.currentPage); + } + }); + // 表格记忆 + this.updateFromStorage(); + } + + // select下拉面板(dropsearch)中使用分页 + if (this.select) { + this.select.dropsearchCom.pagination = this; + } + + // 分页吸底 + if (this.fixed && this.isWithTable()) { + this.zone.runOutsideAngular(() => { + this.unlistenTiReLayoutX = this.renderer.listen(document, 'tiReLayoutX', () => { + this.setFixedStyle(); + }); + }); + } + } + + ngOnChanges(changes: SimpleChanges): void { + const currentPageObj: SimpleChange = changes['currentPage']; + const totalNumberObj: SimpleChange = changes['totalNumber']; + const loadingObj: SimpleChange = changes['loading']; + if (loadingObj && !loadingObj.firstChange) { + this.renderPage(); + } + if ((currentPageObj && !currentPageObj.firstChange) || (totalNumberObj && !totalNumberObj.firstChange)) { + this.verifyCurrentPage(this.itemsPerPage.selected, true); + } + } + + ngAfterViewInit(): void { + // 初始化设置 + if (this.fixed && this.isWithTable()) { + this.setFixedStyle(); + } + } + + ngDoCheck(): void { + let isPageSizeChange: boolean = false; + // 监听开发者设置的 pageSize.options 的改变 + if (this.pageSize?.options && this.pageSize.options !== this.itemsPerPage.options) { + this.itemsPerPage.options = this.pageSize.options; + isPageSizeChange = true; + } + // 监听设置的 pageSize.size 的改变 + if (!Util.isUndefined(this.pageSize) && this.pageSize.size !== this.oldSize) { + this.itemsPerPage.selected = this.pageSize.size; + this.oldSize = this.pageSize.size; + isPageSizeChange = true; + } + + if (isPageSizeChange) { + const index: number = this.itemsPerPage.options.indexOf(this.itemsPerPage.selected); + if (index === -1) { + this.itemsPerPage.selected = this.itemsPerPage.options[0]; + this.syncPageSize(this.itemsPerPage.selected); + this.oldSize = this.pageSize.size; + } + this.verifyCurrentPage(this.itemsPerPage.selected, true); + this.changeDetectorRef.markForCheck(); + } + + if ( + this.oldTotalNumber !== this.totalNumber || + this.itemsPerPage.selected !== this.oldSelected || + this.oldCurrentPage !== this.currentPage + ) { + this.renderPage(); + this.oldTotalNumber = this.totalNumber; + this.oldSelected = this.itemsPerPage.selected; + this.oldCurrentPage = this.currentPage; + this.updateFocusableElems = true; // select使用过这个变量后,会重置为false + } + } + ngAfterViewChecked(): void { + // select中使用的分页为default类型且不带左边select,所以此处限定type === 'default' + if (this.type === 'default' && this.updateFocusableElems) { + // 修复SSR报错:ERROR TypeError: this.nativeElement.querySelectorAll(...) is not a function or its return value is not iterable + this.setFocusableElems(Array.prototype.slice.call(this.nativeElement.querySelectorAll('a'))); + } + } + + ngOnDestroy(): void { + if (this.subscription) { + this.subscription.unsubscribe(); + } + + if (this.unlistenTiReLayoutX) { + this.unlistenTiReLayoutX(); + } + } + + // 数据初始化 + private initData(): void { + // 总条数数字国际化处理,拼接成 ---> 总条数: 条数 + this.setTotalNumberFormat(); + + // 初始化获取itemsPerPage:select组件所需的属性 + this.itemsPerPage = this.getFormatedSize(); + + this.verifyCurrentPage(this.itemsPerPage.selected, true); + + this.oldCurrentPage = this.currentPage; + this.oldTotalNumber = this.totalNumber; + this.oldSize = this.itemsPerPage.selected; + this.oldSelected = this.itemsPerPage.selected; + } + + // 是否有表格当前页记忆 + private isStorageCurrentPage(): boolean { + return ( + this.table.storageId && + (this.table.storageConfig.pagination === true || + (this.table.storageConfig.pagination && this.table.storageConfig.pagination['currentPage'] === true)) + ); + } + + // 是否有表格每页个数记忆 + private isStorageItemsPerPage(): boolean { + return ( + this.table.storageId && + (this.table.storageConfig.pagination === true || + (this.table.storageConfig.pagination && this.table.storageConfig.pagination['itemsPerPage'] === true)) + ); + } + // 处理表格记忆 + private updateFromStorage(): void { + // 修复SSR报错:ERROR TypeError: Cannot read property 'currentPage' of undefined + // 当前页 + if (this.isStorageCurrentPage()) { + const currentPage: number = this.table.sessionStorageState && this.table.sessionStorageState.currentPage; + if (currentPage > 0) { + // 此处异步为处理ExpressionChangedAfterItHasBeenCheckedError报错问题,onpush正常default报错; + Promise.resolve().then(() => { + this.currentPage = currentPage; + this.currentPageChange.emit(currentPage); + }); + } + } + // 修复SSR错误:ERROR TypeError: Cannot read property 'itemsPerPage' of undefined + // 每页条数 + if (this.isStorageItemsPerPage()) { + const itemsPerPage: number = this.table.localStorageState && this.table.localStorageState.itemsPerPage; + if (itemsPerPage > 0) { + this.pageSize.size = itemsPerPage; + const pagination: TiPaginationEvent = { + currentPage: this.currentPage, + size: itemsPerPage, + totalNumber: this.totalNumber + }; + this.pageNumChange.emit(pagination); + } + } + } + + /** + * 根据接口配置来渲染页面 + */ + private renderPage(): void { + if (this.type === 'mini') { + this.configMini(); + } else { + // goto页码与当前页码保持一致 + this.gotoPage = { + page: this.currentPage + }; + + // 页码列表 + this.pages = this.getPages(); + } + + // 用户开启自动隐藏功能且无需显示分页(若总条数 =< 最小的每页显示条数)时,则不需要显示分页时,隐藏分页 + // 初始化totalNumber undefined + this.hidePage = + this.autoHide && (Util.isUndefined(this.totalNumber) || Math.min.apply(null, this.itemsPerPage.options) >= this.totalNumber); + if (this.hidePage) { + this.render.setStyle(this.hostRef.nativeElement, 'display', 'none'); + } else { + this.render.setStyle(this.hostRef.nativeElement, 'display', 'inline-block'); + } + + // 若分页与ti-table配合使用,则当分页更新时,触发ti-table的updatePagination + // 进而触发表格更新回调 + if (this.isWithTable()) { + this.table.updatePagination(this.currentPage, this.itemsPerPage.selected); + } + } + + /** + * @ignore + * 对总页数进行数字国际化处理 + */ + public setTotalNumberFormat(): string { + // 数字国际化格式:整数位保留最小位数.小数位保留最小位数-小数位最大保留位置 + const formatTotalNumber: string = Util.isUndefined(this.totalNumber) ? '0' : TiLocaleFormat.formatNumber(this.totalNumber, '1.0-0'); + + return TiLocale.getLocaleWords().tiPagination['totalLabel'] + formatTotalNumber; + } + + private getFormatedSize(): any { + if (!this.pageSize) { + this.pageSize = this.largeData ? this.largePageSize : this.defaultPageSize; + } + const sizeConfig: TiPageSizeConfig = this.largeData + ? { ...this.defaultPageSize, ...this.largePageSize, ...this.pageSize } + : { ...this.defaultPageSize, ...this.pageSize }; + let index: number = sizeConfig.options.indexOf(sizeConfig.size); + if (index === -1) { + index = 0; + this.syncPageSize(sizeConfig.options[index]); + } + + return { + selected: sizeConfig.options[index], // select选中项 + options: sizeConfig.options, // 每页显示条数选项 + hide: sizeConfig.hide, // 是否显示 + style: { + width: sizeConfig.width // 宽度设置 + } + }; + } + + /** + * 同步变化开发者的pageSize数据 + */ + private syncPageSize(pageSizeNum: number): void { + if (this.pageSize instanceof Object) { + this.pageSize.size = pageSizeNum; + } + } + + /** + * @ignore + * 当前页码是否为最小页码 + */ + public noPrevious(): boolean { + return this.currentPage === TiPaginationComponent.MIN_PAGE; + } + + /** + * @ignore + * 当前页码是否为最大页码 + */ + public noNext(): boolean { + return this.currentPage === this.totalPageNum; + } + + /** + * @ignore + * 当前页码是否为'···' 不可被聚焦 + */ + public isEllipse(page: any): boolean { + return page.key === '···'; + } + + /** + * @ignore + * 操作pageSize选择框,每页显示条数改变时触发 + */ + public onSizeChange(pageSizeNum: number): void { + this.syncPageSize(pageSizeNum); + // pageSize变化时,默认跳转到第1页,并触发currentPageChange事件 + if (this.currentPage !== 1) { + this.currentPage = 1; + this.currentPageChange.emit(this.currentPage); + } + const pagination: TiPaginationEvent = { + currentPage: this.currentPage, + size: pageSizeNum, + totalNumber: this.totalNumber + }; + this.pageNumChange.emit(pagination); + this.pageUpdate.emit(pagination); + } + + // 通过操作选择某一页 + private selectPage(page: any, evt?: Event): void { + // tiny2中此处阻止a标签浏览器默认事件 + if (!Util.isUndefined(evt)) { + evt.preventDefault(); + } + + if (this.disabled) { + return; + } + + const selectedPage: number = parseInt(page, 10); + if (selectedPage !== this.currentPage) { + this.currentPage = selectedPage; + this.currentPageChange.emit(this.currentPage); + this.pageUpdate.emit({ + currentPage: this.currentPage, + size: this.itemsPerPage.selected, + totalNumber: this.totalNumber + }); + } + } + + /** + * 计算总页数 + */ + private calculateTotalPages(pageSizeNum: number): number { + const totalPages: number = Math.ceil(this.totalNumber / pageSizeNum); + + return Math.max(totalPages || 0, 1); // 当totalPages是undefined时,取0 + } + + /** + * 组装mini类型时模板需要的数据 + */ + private configMini(): void { + const options: Array = []; + for (let i: number = 1; i <= this.totalPageNum; i++) { + options.push({ + id: `${i}`, + label: `${i}/${this.totalPageNum}` + }); + } + this.itemsMini = { + options, + selected: options[this.currentPage - 1], + change: (option: TiPageItem): void => { + // 选项改变时,修改选中页 + this.selectPage(option.id); + } + }; + } + + /** + * @description 根据每页显示条数及数据总条数获取页码列表 + */ + private getPages(): Array<{ key: any; active: boolean }> { + if (this.loading) { + return this.getLoadingPages(); + } + const startAndEndPage: Array = this.getInterval(); // 获得显示的起始和结束页 + const startPagNum: number = startAndEndPage[0]; + const endPagNum: number = startAndEndPage[1]; + const pages: Array<{ key: any; active: boolean }> = []; + const restPageNum: number = this.totalPageNum - this.currentPage; // 剩余页数 + + // 只有1页的情况 + if (this.totalPageNum === 1) { + pages.push(TiPaginationComponent.makePage(1, true)); + + return pages; + } + + // 当总页数大于8,并且中间连续按钮的起始位置没有和第1页相连时,创建第1页按钮和省略号 + if (startPagNum > 2 && this.totalPageNum > 8) { + pages.push(TiPaginationComponent.makePage(1, this.currentPage === 1)); + pages.push(TiPaginationComponent.makePage('···', false)); + } + + // 创建中间页按钮 + for (let i: number = startPagNum; i <= endPagNum; i++) { + pages.push(TiPaginationComponent.makePage(i, this.currentPage === i)); + } + + // 当总页数大于8,并且中间连续按钮的结束位置没有和最后一页按钮相连时,创建省略号 + // 当总页数大于8,并且最后一页隐藏且不是后四页时,创建省略号 + if (this.totalPageNum > 8 && ((this.showLastPage && endPagNum < this.totalPageNum - 1) || (!this.showLastPage && restPageNum > 3))) { + pages.push(TiPaginationComponent.makePage('···', false)); + } + + // 最后一页隐藏:如果中间连续按钮的结束位置为总页数的前一页且当前页是后四页时,创建最后一页 + // 最后一页不隐藏:如果中间连续按钮的结束位置,不是最后一页按钮时,创建最后一页按钮 + if ( + (!this.showLastPage && endPagNum === this.totalPageNum - 1 && restPageNum <= 3) || + (this.showLastPage && endPagNum < this.totalPageNum) + ) { + pages.push(TiPaginationComponent.makePage(this.totalPageNum, this.currentPage === this.totalPageNum)); + } + + return pages; + } + + /** + * @description 根据当前页码和要显示的数目,计算分页链接的起始页 + */ + private getInterval(): Array { + let start: number; + let end: number; + // 1.根据Agile规范:页数小于等于8,起、始按钮值分别设为1和最大页数:1 - 总数 + if (this.totalPageNum <= 8) { + start = 1; + end = this.totalPageNum; + + return [start, end]; + } + + // 2.总页数大于8,当前项为前三页:1 - 5 / 1 - 6 + if (this.currentPage <= 3) { + start = 1; + end = 5; + if (!this.showLastPage) { + end = 6; + } + + return [start, end]; + } + + // 3.总页数大于8,当前项是后4页:总数-4 - 总数 + if (this.currentPage > this.totalPageNum - 3) { + start = this.totalPageNum - 4; + end = this.totalPageNum; + + return [start, end]; + } + + // 4.总页数大于8,当前项不在前三页,后三页:显示中间4个按钮 + start = this.currentPage - 1; + end = this.currentPage + 2; + + // 一般数据量比较大的场景隐藏最后一页时,显示中间5个按钮 + if (!this.showLastPage) { + end = this.currentPage + 3; + } + + return [start, end]; + } + + /** + * @description 获取加载态页码列表 + */ + private getLoadingPages(): Array<{ key: string; active: boolean }> { + const pages: Array<{ key: string; active: boolean }> = []; + // 总页数小于8 或 当前页小于等于4的情况 + // 1 2 3 4 5 6 7 ... + if (this.totalPageNum < 8 || this.currentPage <= 4) { + // 结尾页取 总页数 和 7 中间的最小值 + const endPage: number = Math.min(this.totalPageNum, 7); + for (let i: number = 1; i <= endPage; i++) { + // 循环生成中间页 + pages.push(TiPaginationComponent.makePage(i, i === this.currentPage)); + } + // 添加结尾...表示加载态 + pages.push(TiPaginationComponent.makePage('···', false)); + } else { + // 当前页大于4且总页数大于等于8的情况 + // 1 ... 5 6 7 8 9 ... + // 添加1和... + pages.push(TiPaginationComponent.makePage(1, false)); + pages.push(TiPaginationComponent.makePage('···', false)); + // 结尾页取 当前页+2 和 总页数 之间的最小值 + const endPage: number = Math.min(this.currentPage + 2, this.totalPageNum); + // 起始页取 结尾页-4 故共渲染5页 + const startPage: number = endPage - 4; + for (let i: number = startPage; i <= endPage; i++) { + // 循环生成中间页 + pages.push(TiPaginationComponent.makePage(i, i === this.currentPage)); + } + // 添加结尾...表示加载态 + pages.push(TiPaginationComponent.makePage('···', false)); + } + // 返回页码列表 + return pages; + } + + /** + * 设置合法的新页码 + */ + private verifyCurrentPage(pageSizeNum: number, triggerPageUpdate: boolean): void { + this.totalPageNum = this.calculateTotalPages(pageSizeNum); + if (this.currentPage > this.totalPageNum || this.currentPage < TiPaginationComponent.MIN_PAGE) { + this.currentPage = this.currentPage > this.totalPageNum ? this.totalPageNum : TiPaginationComponent.MIN_PAGE; + if (triggerPageUpdate) { + this.pageUpdate.emit({ + currentPage: this.currentPage, + size: pageSizeNum, + totalNumber: this.totalNumber + }); + // 此处异步为处理ExpressionChangedAfterItHasBeenCheckedError报错问题,onpush正常default报错; + Promise.resolve().then(() => { + this.currentPageChange.emit(this.currentPage); + }); + } else { + this.currentPageChange.emit(this.currentPage); + } + } + } + + /** + * @ignore + * 输入跳转页码enter快捷键处理 + */ + public enterPageHandler(e: KeyboardEvent): void { + this.gotoPageHandler(e); + } + + /** + * @ignore + * 根据要跳转的页码,同步修改当前页码 + */ + public gotoPageHandler(event: KeyboardEvent): void { + // 当输入值为不合法时处理 + if (this.gotoPage.page < TiPaginationComponent.MIN_PAGE || this.gotoPage.page > this.totalPageNum) { + this.gotoPage.page = this.currentPage; + + return; + } + + if (this.gotoPage.page !== this.currentPage) { + this.currentPage = this.gotoPage.page; + this.currentPageChange.emit(this.currentPage); + this.pageUpdate.emit({ + currentPage: this.currentPage, + size: this.itemsPerPage.selected, + totalNumber: this.totalNumber + }); + } + } + + /** + * @ignore + * 上一页按钮的点击事件 + */ + public onPrevClick(event: Event): any { + return !this.noPrevious() && this.selectPage(this.currentPage - 1, event); + } + + /** + * @ignore + * 页码点击事件 + */ + public onPageClick(page: any, event: Event): any { + return !this.isEllipse(page) && this.selectPage(page.key, event); + } + + /** + * @ignore + * 下一页按钮的点击事件 + */ + public onNextClick(event: Event): any { + return !this.noNext() && this.selectPage(this.currentPage + 1, event); + } + + /** + * @ignore + * input输入框modelchange事件 + */ + public ngModelChange(value: any): void { + if (value === '') { + this.gotoPage.page = ''; + + return; + } + + const newPage: number = parseInt(value, 10); + if (isNaN(newPage)) { + this.render.setProperty(this.inputRef.nativeElement, 'value', this.gotoPage.page); + + return; + } + + this.gotoPage.page = newPage; + this.render.setProperty(this.inputRef.nativeElement, 'value', this.gotoPage.page); + } + + /** + * @ignore + */ + public trackByFn(index: number, item: any): number { + return item.key; + } + + private isWithTable(): boolean { + return this.table && this.render.parentNode(this.nativeElement).tagName === 'TI-TABLE'; + } + + /** + * 设置分页吸底组件样式 + */ + private setFixedStyle(): void { + if (typeof this.table.nativeElement.getBoundingClientRect !== 'function') { + return; + } + + const left: number = this.table.nativeElement.getBoundingClientRect().left; + // 组件要与tp-layout-section左侧对齐,此处需要减去tp-layout-section左侧间距 24px + this.renderer.setStyle(this.nativeElement, 'left', `calc(${left}px - 24px)`); + this.renderer.setStyle(this.nativeElement, 'width', `calc(100% - ${left}px)`); + } +} diff --git a/src/pagination/lib/src/TiPaginationModule.ts b/src/pagination/lib/src/TiPaginationModule.ts new file mode 100644 index 0000000..d7bc36a --- /dev/null +++ b/src/pagination/lib/src/TiPaginationModule.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiPaginationComponent } from './TiPaginationComponent'; +import { FormsModule } from '@angular/forms'; +import { TiSelectModule } from '@opentiny/ng-select'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiLocaleModule } from '@opentiny/ng-locale'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiIconModule, TiSelectModule, TiTextModule, TiLocaleModule, TiOutlineModule], + exports: [TiPaginationComponent], + declarations: [TiPaginationComponent] +}) +export class TiPaginationModule { + constructor() { + TiLocale.setTiWords(locales); + } +} + +export * from './TiPaginationComponent'; +export { TiPaginationComponent, TiPageSizeConfig, TiPaginationEvent } from './TiPaginationComponent'; diff --git a/src/pagination/lib/src/i18n/TiPaginationWords.ts b/src/pagination/lib/src/i18n/TiPaginationWords.ts new file mode 100644 index 0000000..ecaee8f --- /dev/null +++ b/src/pagination/lib/src/i18n/TiPaginationWords.ts @@ -0,0 +1,8 @@ +export interface TiPaginationWords { + tiPagination: { + gotoLabel: string; + prevTitle: string; + nextTitle: string; + totalLabel: string; + }; +} diff --git a/src/pagination/lib/src/i18n/en_US.ts b/src/pagination/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..e286235 --- /dev/null +++ b/src/pagination/lib/src/i18n/en_US.ts @@ -0,0 +1,10 @@ +import { TiPaginationWords } from './TiPaginationWords'; + +export const en_US: TiPaginationWords = { + tiPagination: { + gotoLabel: 'Go', + prevTitle: 'Previous', + nextTitle: 'Next', + totalLabel: 'Total Records: ' + } +}; diff --git a/src/pagination/lib/src/i18n/es_US.ts b/src/pagination/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..635c529 --- /dev/null +++ b/src/pagination/lib/src/i18n/es_US.ts @@ -0,0 +1,10 @@ +import { TiPaginationWords } from './TiPaginationWords'; + +export const es_US: TiPaginationWords = { + tiPagination: { + gotoLabel: 'Ir', + prevTitle: 'Anterior', + nextTitle: 'Siguiente', + totalLabel: 'Cantidad total de registros: ' + } +}; diff --git a/src/pagination/lib/src/i18n/fr_FR.ts b/src/pagination/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..db599d1 --- /dev/null +++ b/src/pagination/lib/src/i18n/fr_FR.ts @@ -0,0 +1,10 @@ +import { TiPaginationWords } from './TiPaginationWords'; + +export const fr_FR: TiPaginationWords = { + tiPagination: { + gotoLabel: 'Aller', + prevTitle: 'Précédent', + nextTitle: 'Prochain', + totalLabel: 'Total des enregistrements: ' + } +}; diff --git a/src/pagination/lib/src/i18n/index.ts b/src/pagination/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/pagination/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/pagination/lib/src/i18n/pt_BR.ts b/src/pagination/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..85336e9 --- /dev/null +++ b/src/pagination/lib/src/i18n/pt_BR.ts @@ -0,0 +1,10 @@ +import { TiPaginationWords } from './TiPaginationWords'; + +export const pt_BR: TiPaginationWords = { + tiPagination: { + gotoLabel: 'Ir', + prevTitle: 'Anterior', + nextTitle: 'Próximo', + totalLabel: 'Total de registros: ' + } +}; diff --git a/src/pagination/lib/src/i18n/zh_CN.ts b/src/pagination/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..f85a5ff --- /dev/null +++ b/src/pagination/lib/src/i18n/zh_CN.ts @@ -0,0 +1,10 @@ +import { TiPaginationWords } from './TiPaginationWords'; + +export const zh_CN: TiPaginationWords = { + tiPagination: { + gotoLabel: '跳转', + prevTitle: '上一页', + nextTitle: '下一页', + totalLabel: '总条数:' + } +}; diff --git a/src/pagination/lib/src/pagination.html b/src/pagination/lib/src/pagination.html new file mode 100644 index 0000000..d74ec84 --- /dev/null +++ b/src/pagination/lib/src/pagination.html @@ -0,0 +1,134 @@ + + + + {{item}} + + + + + + +
    + {{ 'tiPagination.gotoLabel' | tiTranslate }} + + +
    +
    +
    +
    + +
    + + + + + + + + + + + + + +
    +
    diff --git a/src/pagination/lib/src/pagination.less b/src/pagination/lib/src/pagination.less new file mode 100644 index 0000000..18a60a8 --- /dev/null +++ b/src/pagination/lib/src/pagination.less @@ -0,0 +1,231 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-pagination-box-model: border-box; + --ti-pagination-height: var(--ti-common-size-6x); + --ti-pagination-prev-next-width: 22px; + --ti-pagination-goto-input-width: 45px; +} + +/* -----ti-table with ti-pagination----START----*/ +::ng-deep .ti3-table > .ti3-page-container { + margin-top: var(--ti-common-space-10); +} +/* -----ti-table with ti-pagination----END----*/ + +/* 将所有的类名包含在ti-page-container内部的原因是整体提升组件样式的权重 +防止组件样式被框架样式覆盖 */ +:host.ti3-page-container { + display: inline-block; + .ti3-pag-container { + .user-select(none); + display: inline-block; + line-height: normal; + float: left; + } + + .ti3-pag-total-items { + float: left; + margin: var(--ti-common-space-0) var(--ti-common-space-4x); + vertical-align: middle; + line-height: var(--ti-pagination-height); + height: var(--ti-pagination-height); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + color: var(--ti-common-color-text-secondary); + } + .ti3-pag-pages { + float: left; + height: var(--ti-pagination-height); + & > .ti3-pag-prev { + margin-right: var(--ti-common-space-base); + } + } + + .ti3-pag-page, + .ti3-pag-prev, + .ti3-pag-next { + display: inline-block; + cursor: pointer; + text-decoration: none; + height: var(--ti-pagination-height); + line-height: var(--ti-pagination-height); + vertical-align: middle; + text-align: center; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + .box-sizing(var(--ti-pagination-box-model)); + .border-radius(var(--ti-common-border-radius-normal)); + &:hover { + text-decoration: none; + } + } + + .ti3-pag-page { + color: var(--ti-common-color-text-secondary); + margin-right: var(--ti-common-space-base); + padding: var(--ti-common-space-0) var(--ti-common-space-6); + &.active { + background-color: var(--ti-common-color-bg-emphasize); + color: var(--ti-common-color-text-white); + } + &.ellipse { + outline: none; + border: none; + background: transparent; + cursor: default; + } + &:not(.ellipse):hover { + background-color: var(--ti-common-color-bg-light-normal); + color: var(--ti-common-color-icon-hover); + } + &.active, + &.simple { + &:hover { + background-color: var(--ti-common-color-bg-emphasize); + color: var(--ti-common-color-text-white); + } + } + &:not(.ellipse):active { + background-color: var(--ti-common-color-bg-emphasize); + color: var(--ti-common-color-text-white); + } + } + + .ti3-pag-pages-disable { + .ti3-page-disable { + color: var(--ti-common-color-text-disabled); + background-color: var(--ti-common-color-bg-white-normal); + cursor: not-allowed !important; + outline: none; + &:hover { + color: var(--ti-common-color-text-disabled) !important; + background-color: var(--ti-common-color-bg-white-normal); + } + } + } + + .ti3-pag-prev, + .ti3-pag-next { + width: var(--ti-pagination-prev-next-width); + color: var(--ti-common-color-icon-normal); + height: var(--ti-pagination-prev-next-width); + line-height: var(--ti-pagination-prev-next-width); + &.disabled { + cursor: not-allowed; + background-color: var(--ti-common-color-bg-white-normal); + color: var(--ti-common-color-text-disabled); + outline: none; + & > .ti3-icon-angle-left, + & > .ti3-icon-angle-right { + color: var(--ti-common-color-icon-disabled); + } + } + &:not(.disabled):hover { + color: var(--ti-common-color-icon-hover); + background-color: var(--ti-common-color-bg-light-normal); + } + &:not(.disabled):active { + color: var(--ti-common-color-text-white); + background-color: var(--ti-common-color-bg-emphasize); + } + } + + /* Set up special height for select that shows page size. */ + .ti3-page-size-option, + .ti3-page-mini-option { + float: left; + } + + .ti3-pag-mini-pages { + .ti3-pag-prev, + .ti3-pag-next { + float: left; + } + .ti3-page-mini-option { + float: left; + margin: var(--ti-common-space-0) var(--ti-common-space-2x); + } + .ti3-pag-total-items { + margin-left: var(--ti-common-space-0); + } + } + + /* style for disable condition */ + .ti3-pag-container-disable { + cursor: not-allowed; + } + .ti3-pag-total-disable { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + } + + /* For Page Goto */ + .ti3-pag-goto { + float: left; + display: inline-block; + height: var(--ti-pagination-height); + & .ti3-pag-goto-text { + float: left; + color: var(--ti-common-color-text-secondary); + display: inline-block; + height: var(--ti-pagination-height); + line-height: var(--ti-pagination-height); + margin: var(--ti-common-space-0) 15px; + vertical-align: middle; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + } + & .ti3-pag-goto-input { + float: left; + padding: var(--ti-common-space-0) 5px; + width: var(--ti-pagination-goto-input-width); + height: var(--ti-pagination-height); + } + & .ti3-pag-goto-icon-container { + display: inline-block; + margin-left: 15px; + height: var(--ti-pagination-height); + width: var(--ti-pagination-height); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + .border-radius(3px); + background-color: var(--ti-common-color-bg-white-normal); + cursor: pointer; + text-decoration: none; + vertical-align: middle; + .box-sizing(var(--ti-pagination-box-model)); + & .ti3-pag-goto-icon { + .triangle-up(var(--ti-common-size-3x), calc(var(--ti-common-size-3x) / 1.6), var(--ti-common-color-bg-emphasize)); + transform: translateY(-50%) rotate(90deg); + margin-left: calc((var(--ti-pagination-height) - (var(--ti-common-size-3x) / 1.6)) / 2 - 2px); + margin-top: 50%; + } + &:hover, + &:focus, + &:active { + border-color: var(--ti-common-color-line-active); + } + } + } + + .ti3-pag-goto-disable { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + & .ti3-pag-goto-icon { + border-bottom-color: var(--ti-common-color-icon-disabled) !important; + cursor: not-allowed; + } + } +} + +// 组件吸底展示,主要用于表格数据量过多时,方便切换页码 +:host.ti3-page-container-fixed { + position: fixed; + bottom: 0; + width: 100%; + box-shadow: var(--ti-common-shadow-1-up); + background: var(--ti-common-color-bg-white-normal); + box-sizing: border-box; + padding: var(--ti-common-space-5x) var(--ti-common-space-6x); // 左右间距需要和tp-layout-section间距一致 + z-index: 10; +} diff --git a/src/path/demo/project.json b/src/path/demo/project.json new file mode 100644 index 0000000..d5142ed --- /dev/null +++ b/src/path/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/path/demo", + "sourceRoot": "src/path/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/path", + "index": "src/path/demo/src/index.html", + "main": "src/path/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/path/demo/tsconfig.app.json", + "assets": ["src/path/demo/src/favicon.ico", "src/path/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "path-demo:build:production" + }, + "development": { + "browserTarget": "path-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js path" + } + ] + } + } + } +} diff --git a/src/path/demo/src/app/AppComponent.ts b/src/path/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/path/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/path/demo/src/app/AppModule.ts b/src/path/demo/src/app/AppModule.ts new file mode 100644 index 0000000..c3d6d6d --- /dev/null +++ b/src/path/demo/src/app/AppModule.ts @@ -0,0 +1,25 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { PathTestModule } from './path/PathTestModule'; + +@NgModule({ + imports: [ + PathTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/path/demo/src/app/IndexComponent.ts b/src/path/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..ac83ee4 --- /dev/null +++ b/src/path/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { PathTestModule } from './path/PathTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = PathTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/path/demo/src/app/app.html b/src/path/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/path/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/path/demo/src/app/path/PathListComponent.ts b/src/path/demo/src/app/path/PathListComponent.ts new file mode 100644 index 0000000..3742a67 --- /dev/null +++ b/src/path/demo/src/app/path/PathListComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { TiPathListItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './path-list.html' +}) +export class PathListComponent { + items: Array = [ + { + label: 'type = "file"', + type: 'file' + }, + { + label: 'type = "document"', + type: 'document' + } + ]; + items1: Array = [ + { + label: 'clearable-1', + type: 'file' + }, + { + label: 'clearable-2', + type: 'file' + } + ]; + myLogs: Array = []; + clear(item: TiPathListItem): void { + this.myLogs = [...this.myLogs, `${JSON.stringify(item)}`]; + } +} diff --git a/src/path/demo/src/app/path/PathSelectComponent.ts b/src/path/demo/src/app/path/PathSelectComponent.ts new file mode 100644 index 0000000..929d272 --- /dev/null +++ b/src/path/demo/src/app/path/PathSelectComponent.ts @@ -0,0 +1,277 @@ +import { Component } from '@angular/core'; +import { TiModalService, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './path-select.html' +}) +export class PathSelectComponent { + constructor(tiModal: TiModalService) { + this.tiModal = tiModal; + } + private tiModal: TiModalService; + pathValue: string = 'd/code/TinyUI'; + TiPathListItems = []; + TiPathFieldItems = [ + { + label: 'd' + }, + { + label: 'code' + }, + { + label: 'TinyUI' + } + ]; + + // 表格相关数据 + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + checkedList: Array = []; // 默认选中项 + columns: Array = [ + { + title: 'Name', + width: '40%' + }, + { + title: 'Time', + width: '35%' + }, + { + title: 'Size', + width: '25%' + } + ]; + currentPage: number = 0; + pageSize: { options: Array; size: number } = { + options: [10, 20, 50], + size: 10 + }; + noDadaInfo: string = '暂无表格数据'; + + // 模拟路径数据(为演示方便,此处数据仅为示意) + private pathDataMock = [ + { + label: 'd', + type: 'file', + time: '2019-11-22', + size: '5', + children: [ + { + label: 'code', + type: 'file', + time: '2019-08-22', + size: '10', + children: [ + { + label: 'TinyUI', + type: 'file', + time: '2020-11-11', + size: '15', + children: [ + { + label: 'item_1', + type: 'file', + time: '2020-06-18', + size: '20', + children: [ + { + label: 'item_1', + type: 'document', + time: '2020-02-20', + size: '2' + }, + { + label: 'item_2', + type: 'document', + time: '2020-01-03', + size: '20' + }, + { + label: 'item_3', + type: 'document', + time: '2020-06-18', + size: '200' + }, + { + label: 'empty', + type: 'file', + time: '2019-08-23', + size: '0', + children: [] + } + ] + }, + { + label: 'item_2', + type: 'file', + time: '2020-02-20', + size: '2', + children: [ + { + label: 'item_1', + type: 'document', + time: '2020-02-20', + size: '2' + }, + { + label: 'item_2', + type: 'document', + time: '2020-01-03', + size: '20' + }, + { + label: 'empty', + type: 'file', + time: '2019-08-23', + size: '0', + children: [] + } + ] + }, + { + label: 'item_3', + type: 'document', + time: '2020-03-15', + size: '16' + }, + { + label: 'item_4', + type: 'document', + time: '2020-05-01', + size: '28' + }, + { + label: 'item_5', + type: 'document', + time: '2020-11-11', + size: '35' + }, + { + label: 'empty', + type: 'file', + time: '2019-08-23', + size: '0', + children: [] + } + ] + } + ] + }, + { + label: 'empty', + type: 'file', + time: '2019-08-23', + size: '0', + children: [] + } + ] + } + ]; + + ngOnInit(): void { + // 模拟获取当前页面数据 + this.data = this.queryPageItems(); + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了分页特性,且对源数据未进行分页处理,因此tiny会对数据进行分页处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + // 表格选中项 + this.checkedList = [this.data[0], this.data[2]]; + // 路径列表组件同步表格选中项 + this.setPathList(); + } + + // 模拟获取当前页面数据 + private queryPageItems() { + let temp: any = [...this.pathDataMock]; + for (let i = 0; i < this.TiPathFieldItems.length; i++) { + temp = temp.find((item) => { + return item.label === this.TiPathFieldItems[i].label; + })?.children; + } + return temp; + } + + // pathList组件项目清除事件 + onPathListClear(item) { + // 表格选中项同步路径列表组件 + this.checkedList = this.checkedList.filter((_item) => { + return _item.label !== item.label; + }); + } + + // pathField组件路径变化事件 + onPathfieldChange() { + if (this.TiPathFieldItems.length === 0) { + this.srcData.data = this.pathDataMock; + } else { + this.srcData.data = this.queryPageItems(); + } + // 清空表格选中项 + this.checkedList = []; + } + + // 表格项目双击事件 + onTrDblclick(row) { + if (row.type === 'file') { + this.TiPathFieldItems = [...this.TiPathFieldItems, row]; + this.srcData.data = this.queryPageItems(); + this.checkedList = []; + } + } + + // 路径列表组件同步表格选中项 + private setPathList() { + this.TiPathListItems = [...this.checkedList]; + } + + // 改变路径内容 + private setPathValue(): void { + const pathArray: Array = []; + this.TiPathFieldItems.forEach((item, index) => { + pathArray.push(item.label); + }); + this.pathValue = pathArray.join('/'); + } + + // 打开弹窗 + showModal(content: any): void { + // 备份路径及已选项 + const TiPathFieldItemsOld = [...this.TiPathFieldItems]; + const checkedListOld = [...this.checkedList]; + this.tiModal.open(content, { + id: 'myModal', // 定义id防止同一页面出现多个相同弹框 + animation: false, + // 模板上下文:一般通过context定义的是与弹出动作相关的数据,大部分数据还是建议在外部定义 + // 双向绑定的值,建议放在context对象中,每次打开弹窗都重新就行赋值。 + context: { + name: 'select' + }, + close: (): void => { + console.log('on close event'); + this.TiPathListItems = []; + // 改变路径内容 + this.setPathValue(); + // 路径列表组件同步表格选中项 + this.setPathList(); + }, + dismiss: (): void => { + console.log('on dismiss event'); + // 还原所有路径和已选项 + this.TiPathFieldItems = [...TiPathFieldItemsOld]; + this.onPathfieldChange(); + this.checkedList = [...checkedListOld]; + } + }); + } +} diff --git a/src/path/demo/src/app/path/PathTestModule.ts b/src/path/demo/src/app/path/PathTestModule.ts new file mode 100644 index 0000000..8d8df6f --- /dev/null +++ b/src/path/demo/src/app/path/PathTestModule.ts @@ -0,0 +1,56 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; + +import { + TiButtonModule, + TiCheckboxModule, + TiIconModule, + TiModalModule, + TiPaginationModule, + TiPathModule, + TiTableModule +} from '@opentiny/ng'; +import { PathfieldItemsComponent } from './PathfieldItemsComponent'; +import { PathfieldIspanelComponent } from './PathfieldIspanelComponent'; +import { PathfieldPanelwidthComponent } from './PathfieldPanelwidthComponent'; +import { PathfieldEditableComponent } from './PathfieldEditableComponent'; +import { PathfieldEventComponent } from './PathfieldEventComponent'; +import { PathListComponent } from './PathListComponent'; +import { PathSelectComponent } from './PathSelectComponent'; + +@NgModule({ + imports: [ + CommonModule, + TiButtonModule, + TiModalModule, + TiTableModule, + TiCheckboxModule, + TiPaginationModule, + TiIconModule, + TiPathModule, + DemoLogModule, + RouterModule.forChild(PathTestModule.ROUTES) + ], + declarations: [ + PathfieldItemsComponent, + PathfieldIspanelComponent, + PathfieldPanelwidthComponent, + PathfieldEditableComponent, + PathfieldEventComponent, + PathListComponent, + PathSelectComponent + ] +}) +export class PathTestModule { + static readonly ROUTES: Routes = [ + { path: 'path/pathfield-items', component: PathfieldItemsComponent }, + { path: 'path/pathfield-ispanel', component: PathfieldIspanelComponent }, + { path: 'path/pathfield-panelwidth', component: PathfieldPanelwidthComponent }, + { path: 'path/pathfield-editable', component: PathfieldEditableComponent }, + { path: 'path/pathfield-pathchange', component: PathfieldEventComponent }, + { path: 'path/path-list', component: PathListComponent }, + { path: 'path/path-select', component: PathSelectComponent } + ]; +} diff --git a/src/path/demo/src/app/path/PathfieldEditableComponent.ts b/src/path/demo/src/app/path/PathfieldEditableComponent.ts new file mode 100644 index 0000000..c7add5c --- /dev/null +++ b/src/path/demo/src/app/path/PathfieldEditableComponent.ts @@ -0,0 +1,44 @@ +import { Component } from '@angular/core'; +import { TiPathFieldItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './pathfield-editable.html' +}) +export class PathfieldEditableComponent { + editable: boolean = false; + keepEditState: boolean = true; + items: Array = [ + { + label: 'window' + }, + { + label: 'document' + }, + { + label: 'body' + }, + { + label: 'container' + }, + { + label: 'content' + } + ]; + items1: Array = [ + { + label: 'window' + }, + { + label: 'document' + }, + { + label: 'body' + }, + { + label: 'container' + }, + { + label: 'content' + } + ]; +} diff --git a/src/path/demo/src/app/path/PathfieldEventComponent.ts b/src/path/demo/src/app/path/PathfieldEventComponent.ts new file mode 100644 index 0000000..9549587 --- /dev/null +++ b/src/path/demo/src/app/path/PathfieldEventComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiPathChangeEvent, TiPathFieldItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './pathfield-event.html' +}) +export class PathfieldEventComponent { + items: Array = [ + { + label: 'window' + }, + { + label: 'document' + }, + { + label: 'body' + }, + { + label: 'container' + }, + { + label: 'content' + } + ]; + myLogs: Array = []; + pathChange(event: TiPathChangeEvent | Event): void { + this.myLogs = [...this.myLogs, `pathchage => ${JSON.stringify(event)}`]; + } +} diff --git a/src/path/demo/src/app/path/PathfieldIspanelComponent.ts b/src/path/demo/src/app/path/PathfieldIspanelComponent.ts new file mode 100644 index 0000000..2aa79c5 --- /dev/null +++ b/src/path/demo/src/app/path/PathfieldIspanelComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiPathFieldItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './pathfield-ispanel.html' +}) +export class PathfieldIspanelComponent { + isPanel: boolean = false; + items: Array = [ + { + label: 'window' + }, + { + label: 'document' + }, + { + label: 'body' + }, + { + label: 'container' + }, + { + label: 'content' + } + ]; +} diff --git a/src/path/demo/src/app/path/PathfieldItemsComponent.ts b/src/path/demo/src/app/path/PathfieldItemsComponent.ts new file mode 100644 index 0000000..5baa972 --- /dev/null +++ b/src/path/demo/src/app/path/PathfieldItemsComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { TiPathFieldItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './pathfield-items.html' +}) +export class PathfieldItemsComponent { + items: Array = [ + { + label: 'window' + }, + { + label: 'document' + }, + { + label: 'body' + }, + { + label: 'container' + }, + { + label: 'content' + } + ]; +} diff --git a/src/path/demo/src/app/path/PathfieldPanelwidthComponent.ts b/src/path/demo/src/app/path/PathfieldPanelwidthComponent.ts new file mode 100644 index 0000000..aca4f4a --- /dev/null +++ b/src/path/demo/src/app/path/PathfieldPanelwidthComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiPathFieldItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './pathfield-panelwidth.html' +}) +export class PathfieldPanelwidthComponent { + panelWidth: string = '100px'; + items: Array = [ + { + label: 'window' + }, + { + label: 'document' + }, + { + label: 'body' + }, + { + label: 'container' + }, + { + label: 'content' + } + ]; +} diff --git a/src/path/demo/src/app/path/path-list.html b/src/path/demo/src/app/path/path-list.html new file mode 100644 index 0000000..a220a27 --- /dev/null +++ b/src/path/demo/src/app/path/path-list.html @@ -0,0 +1,9 @@ +

    1. default

    +
    + +
    +

    2. clearable

    +
    + +
    + diff --git a/src/path/demo/src/app/path/path-select.html b/src/path/demo/src/app/path/path-select.html new file mode 100644 index 0000000..98b55fb --- /dev/null +++ b/src/path/demo/src/app/path/path-select.html @@ -0,0 +1,111 @@ +

    描述

    +

    路径选择模板页面示例展示:

    +

    该模板页面为服务展示 pathField 和 pathList 组件如何结合使用(该用例仅做简单参考,实际场景需要各服务根据各自业务进行处理)。

    +

    导入

    +

    import {{ '{' }} TpPathModule {{ '}' }} from '@cloud/path';

    +

    import {{ '{' }} TiButtonModule, TiModalModule, TiTableModule, TiCheckgroupModule, TiPaginationModule {{ '}' }} from '@cloud/tiny3';

    +

    import {{ '{' }} TpIconModule {{ '}' }} from '@cloud/tinyplus3';

    +

    示例

    + +
    +
    {{pathValue}}
    + +
    + + + + + + + {{context.name}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + {{column.title}}
    + + + + {{row.label}} + {{row.time}}{{row.size}}
    {{noDadaInfo}}
    + + + +
    +
    + + + + + +
    + + diff --git a/src/path/demo/src/app/path/pathfield-editable.html b/src/path/demo/src/app/path/pathfield-editable.html new file mode 100644 index 0000000..cea9614 --- /dev/null +++ b/src/path/demo/src/app/path/pathfield-editable.html @@ -0,0 +1,9 @@ +

    1.editable = false

    +
    + +
    + +

    2.keepEditState = true

    +
    + +
    diff --git a/src/path/demo/src/app/path/pathfield-event.html b/src/path/demo/src/app/path/pathfield-event.html new file mode 100644 index 0000000..6cdd62b --- /dev/null +++ b/src/path/demo/src/app/path/pathfield-event.html @@ -0,0 +1,4 @@ +
    + +
    + diff --git a/src/path/demo/src/app/path/pathfield-ispanel.html b/src/path/demo/src/app/path/pathfield-ispanel.html new file mode 100644 index 0000000..be4d4f4 --- /dev/null +++ b/src/path/demo/src/app/path/pathfield-ispanel.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/path/demo/src/app/path/pathfield-items.html b/src/path/demo/src/app/path/pathfield-items.html new file mode 100644 index 0000000..fa57392 --- /dev/null +++ b/src/path/demo/src/app/path/pathfield-items.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/path/demo/src/app/path/pathfield-panelwidth.html b/src/path/demo/src/app/path/pathfield-panelwidth.html new file mode 100644 index 0000000..c5f8b7b --- /dev/null +++ b/src/path/demo/src/app/path/pathfield-panelwidth.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/path/demo/src/app/path/webdoc/path-demos.js b/src/path/demo/src/app/path/webdoc/path-demos.js new file mode 100644 index 0000000..679afb2 --- /dev/null +++ b/src/path/demo/src/app/path/webdoc/path-demos.js @@ -0,0 +1,85 @@ +export default { + column: '2', + demos: [ + { + demoId: 'pathfield-items', + name: { + 'zh-CN': '基本用法', + 'en-US': 'pathfield items' + }, + desc: { + 'zh-CN': '

    Path 组件的最简用法;通过属性items配置具体路径。

    ', + 'en-US': 'pathfield items' + }, + apis: ['TiPathFieldComponent.properties.items', 'TiPathFieldItem.properties.label', 'TiPathFieldItem.properties.label'] + }, + { + demoId: 'pathfield-ispanel', + name: { + 'zh-CN': '下拉面板', + 'en-US': 'pathfield ispanel' + }, + desc: { + 'zh-CN': '

    通过属性isPanel配置点击上级按钮是否展开下拉面板。

    ', + 'en-US': 'pathfield ispanel' + }, + apis: ['TiPathFieldComponent.properties.isPanel'] + }, + { + demoId: 'pathfield-panelwidth', + name: { + 'zh-CN': '面板宽度', + 'en-US': 'pathfield panelwidth' + }, + desc: { + 'zh-CN': '

    通过属性panelWidth配置下拉面板宽度。

    ', + 'en-US': 'pathfield panelwidth' + }, + apis: ['TiPathFieldComponent.properties.panelWidth'] + }, + { + demoId: 'pathfield-editable', + name: { + 'zh-CN': '可编辑', + 'en-US': 'pathfield editable' + }, + desc: { + 'zh-CN': + '

    通过属性editable配置是否支持路径编辑功能;通过属性keepEditState配置按下回车键是否保持编辑状态。

    ', + 'en-US': 'pathfield editable' + }, + apis: ['TiPathFieldComponent.properties.editable', 'TiPathFieldComponent.properties.keepEditState'] + }, + { + demoId: 'pathfield-event', + name: { + 'zh-CN': '事件', + 'en-US': 'pathfield event' + }, + desc: { + 'zh-CN': '

    当激活路径发生变化的时候触发pathChange事件。

    ', + 'en-US': 'pathfield event' + }, + apis: ['TiPathFieldComponent.events.pathChange', 'TiPathChangeEvent.properties.type'] + }, + { + demoId: 'path-list', + name: { + 'zh-CN': '路径列表', + 'en-US': 'path list' + }, + desc: { + 'zh-CN': + '

    PathList 组件的使用方法;通过属性items配置路径列表,通过属性clearable配置路径项是否可删除;当路径项删除的时候触发clear事件。

    ', + 'en-US': 'path list' + }, + apis: [ + 'TiPathListComponent.properties.items', + 'TiPathListComponent.properties.clearable', + 'TiPathListComponent.events.clear', + 'TiPathListItem.properties.label', + 'TiPathListItem.properties.type' + ] + } + ] +}; diff --git a/src/path/demo/src/app/path/webdoc/path.cn.md b/src/path/demo/src/app/path/webdoc/path.cn.md new file mode 100644 index 0000000..e703891 --- /dev/null +++ b/src/path/demo/src/app/path/webdoc/path.cn.md @@ -0,0 +1,15 @@ +--- +title: Path 路径 +--- + +# Path 路径 + +
    + +Path 是设置和显示路径的组件。 + +```typescript +import { TiPathModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/path/demo/src/app/path/webdoc/path.en.md b/src/path/demo/src/app/path/webdoc/path.en.md new file mode 100644 index 0000000..4626ccb --- /dev/null +++ b/src/path/demo/src/app/path/webdoc/path.en.md @@ -0,0 +1,13 @@ +--- +title: Path +--- + +# Path + +
    + +```typescript +import { TiPathModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/path/demo/src/favicon.ico b/src/path/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/path/demo/src/index.html b/src/path/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/path/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/path/demo/src/main.ts b/src/path/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/path/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/path/demo/tsconfig.app.json b/src/path/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/path/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/path/lib/index.ts b/src/path/lib/index.ts new file mode 100644 index 0000000..6c85341 --- /dev/null +++ b/src/path/lib/index.ts @@ -0,0 +1,3 @@ +export * from './src/TiPathModule'; +export * from './src/TiPathFieldComponent'; +export * from './src/TiPathListComponent'; diff --git a/src/path/lib/ng-package.json b/src/path/lib/ng-package.json new file mode 100644 index 0000000..86ac57b --- /dev/null +++ b/src/path/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/path", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/path/lib/package.json b/src/path/lib/package.json new file mode 100644 index 0000000..96b7bde --- /dev/null +++ b/src/path/lib/package.json @@ -0,0 +1,16 @@ +{ + "name": "@opentiny/ng-path", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-droplist": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/path/lib/project.json b/src/path/lib/project.json new file mode 100644 index 0000000..2b5569b --- /dev/null +++ b/src/path/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/path/lib", + "sourceRoot": "src/path/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/path"], + "options": { + "project": "src/path/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/path"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js path" + }, + { + "command": "ng default-build path" + }, + { + "command": "node build/clear-default-theme.js path" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/path && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build path && ng pack path && node build/publish.js path --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/path/lib/src/TiPathFieldComponent.ts b/src/path/lib/src/TiPathFieldComponent.ts new file mode 100644 index 0000000..828fa2c --- /dev/null +++ b/src/path/lib/src/TiPathFieldComponent.ts @@ -0,0 +1,393 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ElementRef, + EventEmitter, + Input, + NgZone, + Output, + Renderer2, + ViewChild, + Inject, + OnChanges, + AfterViewInit, + SimpleChanges, + ChangeDetectionStrategy +} from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiDroplistComponent } from '@opentiny/ng-droplist'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiKeymap } from '@opentiny/ng-utils'; + +/** + * pathField组件路径中单项的数据格式 + */ +export interface TiPathFieldItem { + /** + * 路径内容 + */ + label: string; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} + +/** + * pathField组件路径变化时change事件的参数 + */ +export interface TiPathChangeEvent { + type: 'dropdownSelect' | 'goUpperSelect'; +} + +/** + * pathField组件用于显示路径操作 + */ +@Component({ + selector: 'ti-path-field', + templateUrl: 'path-field.html', + styleUrls: ['path-field.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiPathFieldComponent extends TiBaseComponent implements OnChanges, AfterViewInit { + /** + * 路径数据项 + */ + @Input() items: Array; + /** + * 点击返回按钮时是否展开下拉面板 + */ + @Input() isPanel: boolean = true; + /** + * 是否支持编辑路径 + */ + @Input() editable: boolean = true; + /** + * 编辑状态时敲击回车是否保持编辑状态 + */ + @Input() keepEditState: boolean = false; + /** + * 下拉面板的宽度 + */ + @Input() panelWidth: 'auto' | string = 'auto'; + /** + * 路径内容变化时触发的回调 + */ + @Output() readonly pathChange: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + @ViewChild('items', { static: true }) itemsRef: ElementRef; + /** + * @ignore + */ + @ViewChild('backBtn', { static: true }) backBtnRef: ElementRef; + /** + * @ignore + */ + @ViewChild('droplist', { static: true }) dropListComp: TiDroplistComponent; + /** + * @ignore + */ + @ViewChild('input', { static: false }) inputRef: ElementRef; + + /** + * @ignore + * 显示在路径框中的项目 + */ + public showItems: Array = []; + /** + * @ignore + * 隐藏在下拉菜单中的项目 + */ + public menuItems: Array = []; + /** + * @ignore + * 下拉菜单选中项 + */ + public menuSelected: TiPathFieldItem | null = null; + /** + * @ignore + * 路径框内容是否溢出 + */ + public isOverflow: boolean = false; + /** + * @ignore + * 是否处于编辑路径模式 + */ + public isEditState: boolean = false; + /** + * @ignore + * 编辑模式下路径内容 + */ + public pathValue: string = ''; + /** + * 编辑模式下路径内容 + */ + private pathValueOld: string = ''; + // 页面点击事件 + private onDocumentClick: () => void; + + constructor( + private elementRef: ElementRef, + private renderer2: Renderer2, + private zone: NgZone, + private tiRenderer: TiRenderer, + @Inject(DOCUMENT) private document + ) { + super(elementRef, renderer2); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + // items动态变化时,重新设置显示项 + if (changes['items']) { + this.setShowItems(); + } + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + // 设置显示的项目 + this.setShowItems(); + this.zone.runOutsideAngular(() => { + // 点击页面空白处下拉列表关闭 + this.onDocumentClick = this.renderer2.listen(this.document, 'click', () => { + this.dropListComp.hide(); + }); + }); + } + + /** + * @ignore + * 点击路径框 + */ + public onItemsClick(): void { + // 若不可编辑,直接返回 + if (!this.editable) { + return; + } + // 进入编辑模式 + this.isEditState = true; + setTimeout(() => { + this.inputRef.nativeElement.focus(); + }, 0); + } + + /** + * @ignore + * 点击某项 + */ + public onItemClick(item: TiPathFieldItem, event: Event): void { + const index: number = this.items.indexOf(item); + // 若点击的为最后一项时,不做处理 + if (index === this.items.length - 1) { + return; + } + this.items.splice(index + 1); + // 设置显示的项目 + this.setShowItems(); + this.dropListComp.hide(); + // 触发pathChange事件 + this.pathChange.emit(event); + event.stopPropagation(); + } + + /** + * @ignore + * 点击返回按钮 + */ + public onBackBtnClick(event: Event): void { + if (this.isPanel) { + // 展开面板选择路径模式 + this.dropListComp.show(); + } else { + // 直接返回上级模式 + if (this.items.length) { + this.items.pop(); + this.setShowItems(); + const pathChangeEvent: TiPathChangeEvent = { + type: 'goUpperSelect' + }; + // 触发pathChange事件 + this.pathChange.emit(pathChangeEvent); + } + } + event.stopPropagation(); + } + + /** + * @ignore + * 改变下拉列表选中项 + */ + public onDroplistChange(selected: TiPathFieldItem): void { + const index: number = this.menuItems.findIndex((item: TiPathFieldItem) => { + return item === selected; + }); + // 由于menuItems中项目顺序是倒序,故删除items项目时须用长度减除 + this.items.splice(this.menuItems.length - index); + this.setShowItems(); + const pathChangeEvent: TiPathChangeEvent = { + type: 'dropdownSelect' + }; + // 触发pathChange事件 + this.pathChange.emit(pathChangeEvent); + } + + /** + * @ignore + * 编辑模式下input框按键按下 + */ + public onEditorKeydown(event: KeyboardEvent): void { + if (event.keyCode === TiKeymap.KEY_ENTER || event.keyCode === TiKeymap.KEY_NUMPAD_ENTER) { + // 当路径内容发生变化时 + if (this.pathValue !== this.pathValueOld) { + // 设置路径项目 + this.setItems(); + // 触发pathChange事件 + this.pathChange.emit(event); + } + // 路径编辑模式下敲击回车不保持编辑模式 + if (!this.keepEditState) { + // 退出编辑模式 + this.isEditState = false; + } + } + } + + /** + * @ignore + * 编辑模式下input框失焦 + */ + public onEditorBlur(): void { + // 恢复之前备份的pathValueOld路径内容 + this.pathValue = this.pathValueOld; + // 退出编辑模式 + this.isEditState = false; + } + + /** + * 设置路径项目 + */ + private setItems(): void { + // 通过当前pathValue路径值重新设置路径项目 + const array: Array = this.pathValue.split('/'); + // 重置items路径项目 + this.items.splice(0); + array.forEach((value) => { + const item: TiPathFieldItem = { + label: value + }; + this.items.push(item); + }); + // 设置显示的项目 + this.setShowItems(); + } + + /** + * 设置显示的项目 + */ + private setShowItems(): void { + // 生成路径条父容器的临时DOM,将其放入body中 + const eleStyles: CSSStyleDeclaration = getComputedStyle(this.itemsRef.nativeElement); + const itemsNode: HTMLDivElement = document.createElement('div'); + this.renderer.appendChild(document.body, itemsNode); + // 为临时DOM添加必要的宽度相关的样式,便于宽度计算 + this.tiRenderer.setStyles(itemsNode, { + display: 'inline-flex', + padding: eleStyles.padding, + paddingLeft: eleStyles.paddingLeft, // 处理在IE和火狐下获取padding为空问题:在火狐和IE下只能用只能用padding+[方位]的方式来获取元素的padding值 + paddingRight: eleStyles.paddingRight, + visibility: 'hidden', // 元素隐藏但做占位处理 + position: 'absolute', // 避免克隆元素影响页面高度,导致出滚动条 + left: '-9999px', + top: '-9999px' + }); + + // 获取当前路径条父容器的最大宽度 + const maxWidth: number = this.itemsRef.nativeElement.getBoundingClientRect().width; + // 重置是否溢出标志 + this.isOverflow = false; + // 重置路径框数组和下拉数组 + this.showItems = []; + this.menuItems = []; + for (let i: number = this.items.length - 1; i >= 0; i--) { + // 已经超出最大显示宽度了,直接将当前item放入menuItems中 + if (this.isOverflow) { + this.pushMenuItems(i); + continue; + } + + const itemNode: HTMLDivElement = document.createElement('div'); + this.tiRenderer.setStyles(itemNode, { display: 'flex' }); + this.tiRenderer.prepend(itemsNode, itemNode); + + const itemLabelNode: HTMLDivElement = document.createElement('div'); + itemLabelNode.innerText = this.items[i].label; + + const itemDividNode: HTMLDivElement = document.createElement('div'); + itemDividNode.innerText = '/'; + this.tiRenderer.setStyles(itemDividNode, { margin: '0 4px' }); + + this.renderer.appendChild(itemNode, itemLabelNode); + if (i !== this.items.length - 1) { + this.tiRenderer.insertAfter(itemDividNode, itemLabelNode); + this.tiRenderer.setStyles(itemLabelNode, { maxWidth: '160px' }); + } + + // 放入当前item的DOM后,没有超出最大显示宽度时 + if (itemsNode.getBoundingClientRect().width <= maxWidth) { + // 将当前item放入showItems中 + this.showItems.unshift(this.items[i]); + } else { + // 超出最大显示宽度时 + // 若当前为最后一个item时,直接将其放入showItems中,否则将其放入menuItems中 + if (i === this.items.length - 1) { + this.showItems.unshift(this.items[i]); + } else { + this.pushMenuItems(i); + } + + // 将是否溢出标志置为true + this.isOverflow = true; + } + } + // 移除临时DOM + itemsNode.remove(); + // 设置编辑模式下的路径内容 + this.setPathValue(); + } + + /** + * 向menuItems数组中push项目 + */ + private pushMenuItems(index: number) { + this.menuItems.push({ + label: this.items[index].label + }); + } + + /** + * 设置编辑模式下的路径内容 + */ + private setPathValue(): void { + const pathArray: Array = []; + this.items.forEach((item, index) => { + pathArray.push(item.label); + }); + this.pathValue = pathArray.join('/'); + // 备份当前pathValue路径内容 + this.pathValueOld = this.pathValue; + } +} diff --git a/src/path/lib/src/TiPathListComponent.ts b/src/path/lib/src/TiPathListComponent.ts new file mode 100644 index 0000000..d9a1a99 --- /dev/null +++ b/src/path/lib/src/TiPathListComponent.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; + +export interface TiPathListItem { + /** + * 路径项目内容 + */ + label: string; + /** + * 路径项目类型 + */ + type: 'document' | 'file'; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} + +/** + * pathList组件用于显示路径列表 + */ +@Component({ + selector: 'ti-path-list', + templateUrl: 'path-list.html', + styleUrls: ['path-list.less'] +}) +export class TiPathListComponent extends TiBaseComponent { + /** + * 路径列表数据项 + */ + @Input() items: Array; + /** + * 路径项目是否可删除 + */ + @Input() clearable: boolean = false; + /** + * 当路径项目删除时触发的回调 + */ + @Output() readonly clear: EventEmitter = new EventEmitter(); + + /** + * @ignore + * 清除列表项 + */ + public clearItem(item: TiPathListItem): void { + const index = this.items.indexOf(item); + this.items.splice(index, 1); + // 触发路径项目删除 clear 事件 + this.clear.emit(item); + } +} diff --git a/src/path/lib/src/TiPathModule.ts b/src/path/lib/src/TiPathModule.ts new file mode 100644 index 0000000..4894cfc --- /dev/null +++ b/src/path/lib/src/TiPathModule.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiDroplistModule } from '@opentiny/ng-droplist'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiIconModule } from '@opentiny/ng-icon'; + +import { TiPathFieldComponent } from './TiPathFieldComponent'; +import { TiPathListComponent } from './TiPathListComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiDroplistModule, TiIconModule, TiOverflowModule, TiTextModule], + exports: [TiPathFieldComponent, TiPathListComponent], + declarations: [TiPathListComponent, TiPathFieldComponent] +}) +export class TiPathModule {} + +export { TiPathFieldComponent } from './TiPathFieldComponent'; +export { TiPathListComponent } from './TiPathListComponent'; diff --git a/src/path/lib/src/path-field.html b/src/path/lib/src/path-field.html new file mode 100644 index 0000000..0c94832 --- /dev/null +++ b/src/path/lib/src/path-field.html @@ -0,0 +1,29 @@ +
    +
    + +
    +
    +
    +
    {{item.label}}
    +
    /
    +
    +
    + + +
    + +
    +
    diff --git a/src/path/lib/src/path-field.less b/src/path/lib/src/path-field.less new file mode 100644 index 0000000..98a8fbc --- /dev/null +++ b/src/path/lib/src/path-field.less @@ -0,0 +1,78 @@ +::ng-deep :root { + --ti-pathField-height: 26px; + --ti-pathField-back-width: var(--ti-pathField-height); +} + +:host { + display: block; +} + +.ti3-pathField-container { + display: flex; + height: var(--ti-pathField-height); + line-height: var(--ti-pathField-height); + border: 1px solid var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + align-items: center; + position: relative; +} + +.ti3-pathField-backIcon { + display: block; + width: var(--ti-pathField-back-width); + text-align: center; + font-size: 14px; + cursor: pointer; + border-right: 1px solid var(--ti-common-color-line-normal); + box-sizing: border-box; + &:hover { + color: var(--ti-common-color-bg-emphasize); + } +} +.ti3-pathField-items { + display: flex; + flex: 1; + height: var(--ti-pathField-height); + padding: 0 var(--ti-common-space-10); + box-sizing: border-box; + user-select: none; + overflow: hidden; + .ti3-pathField-item { + &:not(:last-child) { + display: flex; + flex-shrink: 0; + align-items: center; + color: var(--ti-common-color-text-weaken); + cursor: pointer; + &:hover .ti3-pathField-item-label { + color: var(--ti-common-color-text-highlight); + } + } + &:last-child { + width: 100%; + font-weight: bold; + .ti3-pathField-item-label { + max-width: none; + } + .ti3-pathField-item-divid { + display: none; + } + } + } +} + +.ti3-pathField-item-label { + max-width: 160px; +} +.ti3-pathField-item-divid { + margin: 0 var(--ti-common-space-base); +} + +.ti3-pathField-Editor { + width: calc(100% + 2px); + position: absolute; + left: -1px; + input { + width: 100%; + } +} diff --git a/src/path/lib/src/path-list.html b/src/path/lib/src/path-list.html new file mode 100644 index 0000000..ff39b6a --- /dev/null +++ b/src/path/lib/src/path-list.html @@ -0,0 +1,8 @@ +
    + {{i + 1}}. + + {{item.label}} +
    + +
    +
    diff --git a/src/path/lib/src/path-list.less b/src/path/lib/src/path-list.less new file mode 100644 index 0000000..a69651c --- /dev/null +++ b/src/path/lib/src/path-list.less @@ -0,0 +1,42 @@ +.ti3-pathList-item { + height: 28px; + line-height: 18px; + padding: 5px 36px 5px var(--ti-common-space-5x); + margin-bottom: var(--ti-common-space-base); + background-color: var(--ti-common-color-bg-white-emphasize); + box-sizing: border-box; + position: relative; + display: flex; + align-items: center; + &:hover { + .ti3-pathList-close { + display: flex; + } + } +} + +.ti3-pathList-index { + width: 16px; + margin-right: var(--ti-common-space-3x); + flex-shrink: 0; +} + +.ti3-pathList-icon { + display: inline-flex; + margin-right: var(--ti-common-space-3x); + font-size: var(--ti-common-font-size-2); +} + +.ti3-pathList-close { + display: none; + width: 30px; + height: 28px; + position: absolute; + right: 0; + justify-content: center; + align-items: center; + cursor: pointer; + &:hover .ti3-pathList-closeIcon { + color: var(--ti-common-color-bg-emphasize); + } +} diff --git a/src/phonenumber/demo/project.json b/src/phonenumber/demo/project.json new file mode 100644 index 0000000..d1d0290 --- /dev/null +++ b/src/phonenumber/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/phonenumber/demo", + "sourceRoot": "src/phonenumber/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/phonenumber", + "index": "src/phonenumber/demo/src/index.html", + "main": "src/phonenumber/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/phonenumber/demo/tsconfig.app.json", + "assets": ["src/phonenumber/demo/src/favicon.ico", "src/phonenumber/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "phonenumber-demo:build:production" + }, + "development": { + "browserTarget": "phonenumber-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js phonenumber" + } + ] + } + } + } +} diff --git a/src/phonenumber/demo/src/app/AppComponent.ts b/src/phonenumber/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/phonenumber/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/phonenumber/demo/src/app/AppModule.ts b/src/phonenumber/demo/src/app/AppModule.ts new file mode 100644 index 0000000..dd33a22 --- /dev/null +++ b/src/phonenumber/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { PhonenumberTestModule } from './phonenumber/PhonenumberTestModule'; + +@NgModule({ + imports: [ + PhonenumberTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/phonenumber/demo/src/app/IndexComponent.ts b/src/phonenumber/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..6c84a9f --- /dev/null +++ b/src/phonenumber/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { PhonenumberTestModule } from './phonenumber/PhonenumberTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = PhonenumberTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/phonenumber/demo/src/app/app.html b/src/phonenumber/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/phonenumber/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/phonenumber/demo/src/app/phonenumber/PhonenumberBasicComponent.ts b/src/phonenumber/demo/src/app/phonenumber/PhonenumberBasicComponent.ts new file mode 100644 index 0000000..1a11810 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/PhonenumberBasicComponent.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { TiCountryCode } from '@opentiny/ng'; +@Component({ + templateUrl: './phonenumber-basic.html' +}) +export class PhonenumberBasicComponent { + value: string; + countryOptions: Array = [ + { + ISO2Code: 'MO', + Name: '中国澳门特别行政区', + CountryCode: '853' + }, + { + ISO2Code: 'CN', + Name: '中国大陆', + CountryCode: '86' + }, + { + ISO2Code: 'TW', + Name: '中国台湾', + CountryCode: '886' + }, + { + ISO2Code: 'HK', + Name: '中国香港特别行政区', + CountryCode: '852' + } + ]; +} diff --git a/src/phonenumber/demo/src/app/phonenumber/PhonenumberCountryComponent.ts b/src/phonenumber/demo/src/app/phonenumber/PhonenumberCountryComponent.ts new file mode 100644 index 0000000..4721022 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/PhonenumberCountryComponent.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { CountryCode } from 'libphonenumber-js/max'; +import { TiCountryCode } from '@opentiny/ng'; + +@Component({ + templateUrl: './phonenumber-country.html' +}) +export class PhonenumberCountryComponent { + value: string; + country: CountryCode = 'MO'; + countryOptions: Array = [ + { + ISO2Code: 'MO', + Name: '中国澳门特别行政区', + CountryCode: '853' + }, + { + ISO2Code: 'CN', + Name: '中国大陆', + CountryCode: '86' + }, + { + ISO2Code: 'TW', + Name: '中国台湾', + CountryCode: '886' + }, + { + ISO2Code: 'HK', + Name: '中国香港特别行政区', + CountryCode: '852' + } + ]; +} diff --git a/src/phonenumber/demo/src/app/phonenumber/PhonenumberDisabledComponent.ts b/src/phonenumber/demo/src/app/phonenumber/PhonenumberDisabledComponent.ts new file mode 100644 index 0000000..e68c50c --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/PhonenumberDisabledComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { TiCountryCode } from '@opentiny/ng'; + +@Component({ + templateUrl: './phonenumber-disabled.html' +}) +export class PhonenumberDisabledComponent { + value: string; + selectDisabled: boolean = true; + countryOptions: Array = [ + { + ISO2Code: 'MO', + Name: '中国澳门特别行政区', + CountryCode: '853' + }, + { + ISO2Code: 'CN', + Name: '中国大陆', + CountryCode: '86' + }, + { + ISO2Code: 'TW', + Name: '中国台湾', + CountryCode: '886' + }, + { + ISO2Code: 'HK', + Name: '中国香港特别行政区', + CountryCode: '852' + } + ]; +} diff --git a/src/phonenumber/demo/src/app/phonenumber/PhonenumberEventComponent.ts b/src/phonenumber/demo/src/app/phonenumber/PhonenumberEventComponent.ts new file mode 100644 index 0000000..a4ad2a0 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/PhonenumberEventComponent.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; +import { CountryCode } from 'libphonenumber-js/max'; +import { TiCountryCode } from '@opentiny/ng'; + +@Component({ + templateUrl: './phonenumber-event.html' +}) +export class PhonenumberEventComponent { + value: string; + countryOptions: Array = [ + { + ISO2Code: 'MO', + Name: '中国澳门特别行政区', + CountryCode: '853' + }, + { + ISO2Code: 'CN', + Name: '中国大陆', + CountryCode: '86' + }, + { + ISO2Code: 'TW', + Name: '中国台湾', + CountryCode: '886' + }, + { + ISO2Code: 'HK', + Name: '中国香港特别行政区', + CountryCode: '852' + } + ]; + myLogs: Array = []; + countrySelect(event: CountryCode): void { + this.myLogs = [...this.myLogs, `countrySelect: ${JSON.stringify(event)}`]; + } + countryChange(event: string): void { + this.myLogs = [...this.myLogs, `countryChange: ${JSON.stringify(event)}`]; + } +} diff --git a/src/phonenumber/demo/src/app/phonenumber/PhonenumberTestModule.ts b/src/phonenumber/demo/src/app/phonenumber/PhonenumberTestModule.ts new file mode 100644 index 0000000..09446f6 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/PhonenumberTestModule.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; + +import { TiPhonenumberModule } from '@opentiny/ng'; +import { PhonenumberBasicComponent } from './PhonenumberBasicComponent'; +import { PhonenumberDisabledComponent } from './PhonenumberDisabledComponent'; +import { PhonenumberEventComponent } from './PhonenumberEventComponent'; +import { PhonenumberValidComponent } from './PhonenumberValidComponent'; +import { PhonenumberCountryComponent } from './PhonenumberCountryComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiPhonenumberModule, DemoLogModule, RouterModule.forChild(PhonenumberTestModule.ROUTES)], + declarations: [ + PhonenumberBasicComponent, + PhonenumberDisabledComponent, + PhonenumberEventComponent, + PhonenumberValidComponent, + PhonenumberCountryComponent + ] +}) +export class PhonenumberTestModule { + static readonly ROUTES: Routes = [ + { path: 'phonenumber/phonenumber-basic', component: PhonenumberBasicComponent }, + { path: 'phonenumber/phonenumber-disabled', component: PhonenumberDisabledComponent }, + { path: 'phonenumber/phonenumber-event', component: PhonenumberEventComponent }, + { path: 'phonenumber/phonenumber-valid', component: PhonenumberValidComponent }, + { path: 'phonenumber/phonenumber-country', component: PhonenumberCountryComponent } + ]; +} diff --git a/src/phonenumber/demo/src/app/phonenumber/PhonenumberValidComponent.ts b/src/phonenumber/demo/src/app/phonenumber/PhonenumberValidComponent.ts new file mode 100644 index 0000000..e5f7533 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/PhonenumberValidComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { TiCountryCode } from '@opentiny/ng'; + +@Component({ + templateUrl: './phonenumber-valid.html' +}) +export class PhonenumberValidComponent { + value: string; + value1: string; + countryOptions: Array = [ + { + ISO2Code: 'MO', + Name: '中国澳门特别行政区', + CountryCode: '853' + }, + { + ISO2Code: 'CN', + Name: '中国大陆', + CountryCode: '86' + }, + { + ISO2Code: 'TW', + Name: '中国台湾', + CountryCode: '886' + }, + { + ISO2Code: 'HK', + Name: '中国香港特别行政区', + CountryCode: '852' + } + ]; +} diff --git a/src/phonenumber/demo/src/app/phonenumber/phonenumber-basic.html b/src/phonenumber/demo/src/app/phonenumber/phonenumber-basic.html new file mode 100644 index 0000000..b6cc9da --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/phonenumber-basic.html @@ -0,0 +1 @@ + diff --git a/src/phonenumber/demo/src/app/phonenumber/phonenumber-country.html b/src/phonenumber/demo/src/app/phonenumber/phonenumber-country.html new file mode 100644 index 0000000..9a07ae1 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/phonenumber-country.html @@ -0,0 +1 @@ + diff --git a/src/phonenumber/demo/src/app/phonenumber/phonenumber-disabled.html b/src/phonenumber/demo/src/app/phonenumber/phonenumber-disabled.html new file mode 100644 index 0000000..aef73a7 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/phonenumber-disabled.html @@ -0,0 +1 @@ + diff --git a/src/phonenumber/demo/src/app/phonenumber/phonenumber-event.html b/src/phonenumber/demo/src/app/phonenumber/phonenumber-event.html new file mode 100644 index 0000000..17b3b26 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/phonenumber-event.html @@ -0,0 +1,8 @@ + + diff --git a/src/phonenumber/demo/src/app/phonenumber/phonenumber-valid.html b/src/phonenumber/demo/src/app/phonenumber/phonenumber-valid.html new file mode 100644 index 0000000..e847ec9 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/phonenumber-valid.html @@ -0,0 +1,4 @@ +

    1.change

    + +

    2.blur

    + diff --git a/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber-demos.js b/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber-demos.js new file mode 100644 index 0000000..c3ab480 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber-demos.js @@ -0,0 +1,75 @@ +export default { + column: '2', + demos: [ + { + demoId: 'phonenumber-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'phonenumber basic' + }, + desc: { + 'zh-CN': '

    通过属性options配置国家/地区码数据集。

    ', + 'en-US': 'phonenumber basic' + }, + apis: ['TiPhonenumberComponent.properties.options'] + }, + { + demoId: 'phonenumber-country', + name: { + 'zh-CN': '当前国家/地区码', + 'en-US': 'phonenumber country' + }, + desc: { + 'zh-CN': '

    通过属性country配置当前国家/地区码。

    ', + 'en-US': 'phonenumber country' + }, + apis: ['TiPhonenumberComponent.properties.country'] + }, + { + demoId: 'phonenumber-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'phonenumber disabled' + }, + desc: { + 'zh-CN': '

    通过属性selectDisabled配置左侧下拉框禁用状态。

    ', + 'en-US': 'phonenumber disabled' + }, + apis: ['TiPhonenumberComponent.properties.selectDisabled'] + }, + { + demoId: 'phonenumber-event', + name: { + 'zh-CN': '事件', + 'en-US': 'phonenumber event' + }, + desc: { + 'zh-CN': + '

    country发生变化时触发countryChange事件;当选中下拉项时触发countrySelect事件。

    ', + 'en-US': 'phonenumber event' + }, + apis: ['TiPhonenumberComponent.events.countryChange', 'TiPhonenumberComponent.events.countrySelect'] + }, + { + demoId: 'phonenumber-valid', + name: { + 'zh-CN': '', + 'en-US': 'phonenumber valid' + }, + desc: { + 'zh-CN': + '

    通过属性validType配置校验类型,包含change(即时校验)blur(失焦检验)两种类型。

    ', + 'en-US': 'phonenumber valid' + }, + apis: ['TiPhonenumberComponent.properties.validType'] + } + ], + ignoreApis: [ + 'TiPhonenumberComponent.properties.disabled', + 'TiPhonenumberComponent.properties.tabindex', + 'TiPhonenumberComponent.events.blur', + 'TiPhonenumberComponent.events.focus', + 'TiPhonenumberComponent.methods.blur', + 'TiPhonenumberComponent.methods.focus' + ] +}; diff --git a/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.cn.md b/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.cn.md new file mode 100644 index 0000000..60161f9 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.cn.md @@ -0,0 +1,15 @@ +--- +title: Phonenumber 电话号码 +--- + +# Phonenumber 电话号码 + +
    + +电话号码是可以根据国家/地区码显示和校验电话号码的组件。 + +```typescript +import { TiPhonenumberModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.en.md b/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.en.md new file mode 100644 index 0000000..b7a9d88 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.en.md @@ -0,0 +1,13 @@ +--- +title: Phonenumber +--- + +# Phonenumber + +
    + +```typescript +import { TiPhonenumberModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/phonenumber/demo/src/favicon.ico b/src/phonenumber/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d7a7af634ee6d643ac81d81feadff569ac25de3d GIT binary patch literal 300852 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 + + + + + + Tiny + + + + + + + + + diff --git a/src/phonenumber/demo/src/main.ts b/src/phonenumber/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/phonenumber/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/phonenumber/demo/tsconfig.app.json b/src/phonenumber/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/phonenumber/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/phonenumber/lib/index.ts b/src/phonenumber/lib/index.ts new file mode 100644 index 0000000..dd18df6 --- /dev/null +++ b/src/phonenumber/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiPhonenumberModule'; +export * from './src/TiPhonenumberComponent'; diff --git a/src/phonenumber/lib/ng-package.json b/src/phonenumber/lib/ng-package.json new file mode 100644 index 0000000..019c0af --- /dev/null +++ b/src/phonenumber/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/phonenumber", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/phonenumber/lib/package.json b/src/phonenumber/lib/package.json new file mode 100644 index 0000000..20b7c77 --- /dev/null +++ b/src/phonenumber/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "@opentiny/ng-phonenumber", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "libphonenumber-js/max": "1.10.7", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-validation": "~1.0.0-beta.0", + "@opentiny/ng-select": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-text": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/phonenumber/lib/project.json b/src/phonenumber/lib/project.json new file mode 100644 index 0000000..47299b2 --- /dev/null +++ b/src/phonenumber/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/phonenumber/lib", + "sourceRoot": "src/phonenumber/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/phonenumber"], + "options": { + "project": "src/phonenumber/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/phonenumber"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js phonenumber" + }, + { + "command": "ng default-build phonenumber" + }, + { + "command": "node build/clear-default-theme.js phonenumber" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/phonenumber && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build phonenumber && ng pack phonenumber && node build/publish.js phonenumber --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/phonenumber/lib/src/TiPhoneValidatorDirective.ts b/src/phonenumber/lib/src/TiPhoneValidatorDirective.ts new file mode 100644 index 0000000..2f2789c --- /dev/null +++ b/src/phonenumber/lib/src/TiPhoneValidatorDirective.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn, Validators } from '@angular/forms'; +import { TiLocale } from '@opentiny/ng-locale'; +import parsePhoneNumber, { CountryCode, PhoneNumber } from 'libphonenumber-js/max'; // max为严格匹配 TODO:都放在大括号中输入一位的时候报错 +/** + * @ignore + */ +@Directive({ + selector: `[tiPhone][formControlName],[tiPhone][formControl],[tiPhone][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, // 该 Token 用于配置自定义验证器 Provider + useExisting: forwardRef(() => TiPhoneValidatorDirective), + multi: true // 可以使用相同的 Token 去注册多个 Provider + } + ] +}) +export class TiPhoneValidatorDirective implements Validator, OnChanges { + private validator: ValidatorFn = Validators.nullValidator; // 校验规则函数 + private onChange?: () => void; // 承接fn的回调函数 + /** + * @ignore + * 电话号码校验参数 + */ + @Input() tiPhone!: CountryCode; + ngOnChanges(changes: SimpleChanges): void { + if ('tiPhone' in changes) { + this._createValidator(); + if (this.onChange) { + this.onChange(); + } + } + } + + validate(control: AbstractControl): ValidationErrors | null { + return this.validator(control); + } + + registerOnValidatorChange(fn: () => void): void { + this.onChange = fn; + } + + private _createValidator(): void { + this.validator = phone(this.tiPhone); + + function phone(code: CountryCode): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const localeWords: any = TiLocale.getLocaleWords().tiPhonenumber; + + // 输入框为空时 + if (control.value === null || control.value === undefined || control.value.length === 0) { + return { phone: { tiErrorMessage: localeWords.not_blank } }; + } + /** + * 电话号码可以包含空格、逗号、括号、数字、连字符、点号,不能包含大小写字母、汉字、其他特殊字符 + * 正则表达式有^,取反集 + */ + const isInValidPhonenumberReg = /[^0-9-.,\s\(\)]/; + if (control.value.match(isInValidPhonenumberReg)) { + return { phone: { tiErrorMessage: localeWords.invalid } }; + } + // 输入框非空时 + const phoneNum: PhoneNumber = parsePhoneNumber(control.value, code); // 输入字母等phoneNum为undefined + + return phoneNum && phoneNum.isValid() ? null : { phone: { tiErrorMessage: localeWords.invalid } }; + }; + } + } +} diff --git a/src/phonenumber/lib/src/TiPhonenumberComponent.ts b/src/phonenumber/lib/src/TiPhonenumberComponent.ts new file mode 100644 index 0000000..028a505 --- /dev/null +++ b/src/phonenumber/lib/src/TiPhonenumberComponent.ts @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef, Renderer2, ChangeDetectorRef } from '@angular/core'; +import { CountryCode } from 'libphonenumber-js/max'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiValidationConfig } from '@opentiny/ng-validation'; +import { TiSelectComponent } from '@opentiny/ng-select'; +import { TiTextComponent } from '@opentiny/ng-text'; + +export interface TiCountryCode { + /** + * 国家/地区码 + */ + ISO2Code: CountryCode; + /** + * 国家 + */ + Name: string; + /** + * 区号 + */ + CountryCode: string; +} +@Component({ + selector: 'ti-phonenumber', + templateUrl: './phonenumber.html', + styleUrls: ['./phonenumber.less'], + providers: [TiFormComponent.getValueAccessor(TiPhonenumberComponent)] +}) +export class TiPhonenumberComponent extends TiFormComponent implements OnInit { + /* + * 下拉框是否灰化 + */ + @Input() selectDisabled = false; + /** + * 校验类型 + */ + @Input() validType: 'blur' | 'change' = 'change'; + /** + * 当前国家/地区码 + */ + @Input() country: CountryCode = 'CN'; // 默认为中国大陆 + /** + * 国家/地区码数据集 + */ + @Input() options: Array; + /** + * 下拉选中项变化时触发的回调,参数:当前国家/地区码 + */ + @Output() readonly countryChange: EventEmitter = new EventEmitter(); + /** + * 下拉项选中时触发的回调,参数:当前国家/地区码 + */ + @Output() readonly countrySelect: EventEmitter = new EventEmitter(); + /** + * @ignore + * 校验配置 + */ + public validation: TiValidationConfig; + /** + * @ignore + * select组件 + */ + @ViewChild('select', { static: true }) selectComp: TiSelectComponent; + /** + * @ignore + */ + @ViewChild('input', { static: true }) inputComp: TiTextComponent; + + constructor(protected hostRef: ElementRef, protected renderer2: Renderer2, cdRef: ChangeDetectorRef) { + super(hostRef, renderer2, cdRef); + } + + ngOnInit() { + super.ngOnInit(); + // 设置组件可聚焦元素 + this.setFocusableElems([this.selectComp.nativeElement].concat(this.inputComp.nativeElement)); + // 校验配置 + this.validation = { + type: this.validType + }; + } + /** + * @ignore + * 下拉组件select事件 + */ + onSelect(event: TiCountryCode): void { + this.countrySelect.emit(event.ISO2Code); + } + /** + * @ignore + * 下拉组件ngModelChange事件 + */ + onNgModelChange(event: CountryCode): void { + this.country = event; + this.countryChange.emit(event); + } +} diff --git a/src/phonenumber/lib/src/TiPhonenumberModule.ts b/src/phonenumber/lib/src/TiPhonenumberModule.ts new file mode 100644 index 0000000..65288df --- /dev/null +++ b/src/phonenumber/lib/src/TiPhonenumberModule.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiPhonenumberComponent } from './TiPhonenumberComponent'; +import { TiPhoneValidatorDirective } from './TiPhoneValidatorDirective'; +import { TiLocale, TiLocaleModule } from '@opentiny/ng-locale'; +import { locales } from './i18n'; +import { FormsModule } from '@angular/forms'; +import { TiSelectModule } from '@opentiny/ng-select'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiValidationModule } from '@opentiny/ng-validation'; + +@NgModule({ + declarations: [TiPhonenumberComponent, TiPhoneValidatorDirective], + imports: [CommonModule, FormsModule, TiLocaleModule, TiSelectModule, TiTextModule, TiValidationModule], + exports: [TiPhonenumberComponent, TiPhoneValidatorDirective] +}) +export class TiPhonenumberModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiPhonenumberComponent, TiCountryCode } from './TiPhonenumberComponent'; +export { TiPhoneValidatorDirective } from './TiPhoneValidatorDirective'; diff --git a/src/phonenumber/lib/src/i18n/TiPhonenumberWords.ts b/src/phonenumber/lib/src/i18n/TiPhonenumberWords.ts new file mode 100644 index 0000000..ba190b6 --- /dev/null +++ b/src/phonenumber/lib/src/i18n/TiPhonenumberWords.ts @@ -0,0 +1,8 @@ +export interface TiPhonenumberWords { + tiPhonenumber: { + placeholder: string; + not_blank: string; + invalid: string; + not_found: string; + }; +} diff --git a/src/phonenumber/lib/src/i18n/en_US.ts b/src/phonenumber/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..69ca9d5 --- /dev/null +++ b/src/phonenumber/lib/src/i18n/en_US.ts @@ -0,0 +1,10 @@ +import { TiPhonenumberWords } from './TiPhonenumberWords'; + +export const en_US: TiPhonenumberWords = { + tiPhonenumber: { + placeholder: 'Enter a mobile number.', + not_blank: 'This field cannot be left blank.', + invalid: 'Invalid mobile number.', + not_found: 'No records found.' + } +}; diff --git a/src/phonenumber/lib/src/i18n/es_US.ts b/src/phonenumber/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..0d521a3 --- /dev/null +++ b/src/phonenumber/lib/src/i18n/es_US.ts @@ -0,0 +1,10 @@ +import { TiPhonenumberWords } from './TiPhonenumberWords'; + +export const es_US: TiPhonenumberWords = { + tiPhonenumber: { + placeholder: 'Ingrese un número de celular', + not_blank: 'Este campo es obligatorio.', + invalid: 'El número de celular no es válido.', + not_found: 'No se encontraron registros.' + } +}; diff --git a/src/phonenumber/lib/src/i18n/fr_FR.ts b/src/phonenumber/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..a363ef2 --- /dev/null +++ b/src/phonenumber/lib/src/i18n/fr_FR.ts @@ -0,0 +1,10 @@ +import { TiPhonenumberWords } from './TiPhonenumberWords'; + +export const fr_FR: TiPhonenumberWords = { + tiPhonenumber: { + placeholder: 'Ingrese un número de celular', + not_blank: 'Este campo es obligatorio.', + invalid: 'El número de celular no es válido.', + not_found: 'No se encontraron registros.' + } +}; diff --git a/src/phonenumber/lib/src/i18n/index.ts b/src/phonenumber/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/phonenumber/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/phonenumber/lib/src/i18n/pt_BR.ts b/src/phonenumber/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..ccfc42a --- /dev/null +++ b/src/phonenumber/lib/src/i18n/pt_BR.ts @@ -0,0 +1,10 @@ +import { TiPhonenumberWords } from './TiPhonenumberWords'; + +export const pt_BR: TiPhonenumberWords = { + tiPhonenumber: { + placeholder: 'Insira um número de celular.', + not_blank: 'Este campo não pode ser deixado em branco.', + invalid: 'Número de celular inválido.', + not_found: 'Nenhum registro encontrado.' + } +}; diff --git a/src/phonenumber/lib/src/i18n/zh_CN.ts b/src/phonenumber/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..b9095b3 --- /dev/null +++ b/src/phonenumber/lib/src/i18n/zh_CN.ts @@ -0,0 +1,10 @@ +import { TiPhonenumberWords } from './TiPhonenumberWords'; + +export const zh_CN: TiPhonenumberWords = { + tiPhonenumber: { + placeholder: '请输入手机号', + not_blank: '手机号不能为空', + invalid: '手机号格式错误', + not_found: '无匹配结果' + } +}; diff --git a/src/phonenumber/lib/src/phonenumber.html b/src/phonenumber/lib/src/phonenumber.html new file mode 100644 index 0000000..4fa1cf9 --- /dev/null +++ b/src/phonenumber/lib/src/phonenumber.html @@ -0,0 +1,34 @@ + +
    + + + {{item.Name}}(+{{item.CountryCode}}) + +{{select.CountryCode}} + +
    diff --git a/src/phonenumber/lib/src/phonenumber.less b/src/phonenumber/lib/src/phonenumber.less new file mode 100644 index 0000000..3007e0a --- /dev/null +++ b/src/phonenumber/lib/src/phonenumber.less @@ -0,0 +1,102 @@ +@import '../../../themes/basic/base-all.less'; +::ng-deep :root { + --ti-phonenumber-width: 300px; // 组件整体宽度 + --ti-phonenumber-input-width: 220px; // 输入框宽度 + --ti-phonenumber-line-height: 14px; // 短竖线高度 + --ti-phonenumber-select-border-right-height: 26px; // select组件右边框竖线高度 +} + +.ti3-phonenumber-input(@border-color) { + width: calc(var(--ti-phonenumber-input-width) + 1px); // 由于要覆盖中间分隔线,左移了1px,故input框总宽+1 + border: 1px solid @border-color; + margin-left: -1px; // 覆盖select框与input框中间的分隔线 +} + +:host { + display: inline-block; + width: var(--ti-phonenumber-width); + height: var(--ti-input-height); +} + +.ti3-phonenumber-container { + position: relative; + display: flex; + width: 100%; + + // select组件样式 + .ti3-phonenumber-select { + width: calc(var(--ti-phonenumber-width) - var(--ti-phonenumber-input-width)); + border-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + position: relative; + &:before { // 定义中间竖线 + content: ""; + position: absolute; + right: 0; + top: 0; + border-left: 1px solid transparent; + height: var(--ti-phonenumber-select-border-right-height); + z-index: 1; // hover或focus时提高层级,覆盖select框与searchbox框中间的透明分隔线 + } + + &:hover:not([disabled]):not([tiFocused]):before { + border-left-color: var(--ti-common-color-line-hover); + } + + &[tiFocused]:before { + border-left-color: var(--ti-common-color-line-active); + } + + // 正常状态时select组件和输入框组件之间短的竖线 + ::ng-deep & .ti3-select-dominator-dropdown-btn:before { + content: ""; + position: absolute; + right: 0; + height: var(--ti-phonenumber-line-height); + top: calc((var(--ti-phonenumber-select-border-right-height) - var(--ti-phonenumber-line-height)) / 2); + border-left: 1px solid var(--ti-common-color-line-dividing); + } + } + + // 输入框组件样式 + input[tiText].ti3-phonenumber-input { + width: var(--ti-phonenumber-input-width); + z-index: 1; // 左边框被select遮挡,故需要提高权重 + border-left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + order: 1; // 父级为弹性布局,order值越大排列越靠后 + // select灰化时输入框的样式 + &.ti3-phonenumber-select-disable { + .ti3-phonenumber-input(var(--ti-common-color-line-normal)); + } + // 输入框hover时的样式 + &:hover { + .ti3-phonenumber-input(var(--ti-common-color-line-hover)); + } + // 输入框聚焦时的样式 + &:focus { + .ti3-phonenumber-input(var(--ti-common-color-line-active)); + } + } +} +// 失焦校验:错误且已输入失焦时显示错误样式,及时校验:错误且已输入时显示错误样式 +@tiny-invalid-class: +&.ng-invalid.ng-touched[tiBlurCheck]:not([tiFocused]), +&.ng-invalid.ng-dirty[tiBlurCheck]:not([tiFocused]), +&.ng-invalid.ng-touched:not([tiBlurCheck]), +&.ng-invalid.ng-dirty:not([tiBlurCheck]); + +// 校验失败样式 +@{tiny-invalid-class}.ti3-phonenumber-input { // input框校验失败时 + + .ti3-phonenumber-select { + &:not([disabled]) { // 非灰化select组件的边框和背景色 + border-color: var(--ti-common-color-error-border); + background-color: var(--ti-common-color-error-bg); + } + &:hover:before, &[tiFocused]:before { // 中间分割线hover和聚焦边框色 + border-left-color: var(--ti-common-color-error-border); + } + } +} diff --git a/src/polyfills.ts b/src/polyfills.ts new file mode 100644 index 0000000..d05b160 --- /dev/null +++ b/src/polyfills.ts @@ -0,0 +1,60 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags.ts'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + +// SSR未通过,所以去除dragula +// (window as any).global = window; // ng2-dragula库:表格拖拽功能 diff --git a/src/popconfirm/demo/project.json b/src/popconfirm/demo/project.json new file mode 100644 index 0000000..0a96560 --- /dev/null +++ b/src/popconfirm/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/popconfirm/demo", + "sourceRoot": "src/popconfirm/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/popconfirm", + "index": "src/popconfirm/demo/src/index.html", + "main": "src/popconfirm/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/popconfirm/demo/tsconfig.app.json", + "assets": ["src/popconfirm/demo/src/favicon.ico", "src/popconfirm/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "popconfirm-demo:build:production" + }, + "development": { + "browserTarget": "popconfirm-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js popconfirm" + } + ] + } + } + } +} diff --git a/src/popconfirm/demo/src/app/AppComponent.ts b/src/popconfirm/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/popconfirm/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/popconfirm/demo/src/app/AppModule.ts b/src/popconfirm/demo/src/app/AppModule.ts new file mode 100644 index 0000000..6569ff2 --- /dev/null +++ b/src/popconfirm/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { PopconfirmTestModule } from './popconfirm/PopconfirmTestModule'; + +@NgModule({ + imports: [ + PopconfirmTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/popconfirm/demo/src/app/IndexComponent.ts b/src/popconfirm/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..0138a21 --- /dev/null +++ b/src/popconfirm/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { PopconfirmTestModule } from './popconfirm/PopconfirmTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = PopconfirmTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/popconfirm/demo/src/app/app.html b/src/popconfirm/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/popconfirm/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/popconfirm/demo/src/app/popconfirm/PopconfirmBasicComponent.ts b/src/popconfirm/demo/src/app/popconfirm/PopconfirmBasicComponent.ts new file mode 100644 index 0000000..39371d3 --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/PopconfirmBasicComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { TiPopconfirmConfig } from '@opentiny/ng'; +@Component({ + templateUrl: './popconfirm-basic.html' +}) +export class PopconfirmBasicComponent { + popConfig: TiPopconfirmConfig = { + id: 'delete', + content: '确定要删除该安全组规则吗?' + }; +} diff --git a/src/popconfirm/demo/src/app/popconfirm/PopconfirmDefineComponent.ts b/src/popconfirm/demo/src/app/popconfirm/PopconfirmDefineComponent.ts new file mode 100644 index 0000000..29aab4e --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/PopconfirmDefineComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { TiPopconfirmConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './popconfirm-define.html' +}) +export class PopconfirmDefineComponent { + popConfig1: TiPopconfirmConfig = { + id: 'start', + yesPrimary: false, + content: '确定要启用吗?', + yesText: '确认', + noText: '取消', + position: 'right' + }; +} diff --git a/src/popconfirm/demo/src/app/popconfirm/PopconfirmEventComponent.ts b/src/popconfirm/demo/src/app/popconfirm/PopconfirmEventComponent.ts new file mode 100644 index 0000000..7ece9de --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/PopconfirmEventComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { TiPopconfirmConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './popconfirm-event.html' +}) +export class PopconfirmEventComponent { + myLogs: Array = []; + popConfig: TiPopconfirmConfig = { + id: 'delete', + content: '确定要删除该安全组规则吗?', + close: (data: any): void => { + // 可通过data接口传递参数 + this.myLogs = [...this.myLogs, '确认删除']; + }, + dismiss: (data: any): void => { + this.myLogs = [...this.myLogs, '否认删除']; + } + }; +} diff --git a/src/popconfirm/demo/src/app/popconfirm/PopconfirmTableComponent.ts b/src/popconfirm/demo/src/app/popconfirm/PopconfirmTableComponent.ts new file mode 100644 index 0000000..4a3bb95 --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/PopconfirmTableComponent.ts @@ -0,0 +1,123 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './popconfirm-table.html' +}) +export class PopconfirmTableComponent { + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + myLogs: any = []; + columns: Array = [ + { + label: 'first name', + width: '25%' + }, + { + label: 'last name', + width: '20%' + }, + { + label: 'age', + width: '20%' + }, + { + label: 'state', + width: '15%' + }, + { + label: 'options', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 4; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + onSelect(item: any): void { + this.myLogs = [...this.myLogs, item.label]; + } + dataToItemsFn: (data: any) => Array = (data: any) => { + let items: Array = [ + { + label: '删除', + popConfig: { + content: '确定要删除该项吗?', + yesPrimary: true, + close: (): void => { + const index = this.srcData.data.findIndex((current: TiTableRowData): boolean => { + return current.rowId === data.rowId; + }); + this.srcData.data.splice(index, 1); + this.myLogs = [...this.myLogs, '确认删除']; + }, + dismiss: (): void => { + this.myLogs = [...this.myLogs, '否认删除']; + } + } + }, + { + label: '禁用', + popConfig: { + content: '确定要禁用该项吗?', + close: (): void => { + this.myLogs = [...this.myLogs, '确认禁用']; + }, + dismiss: (): void => { + this.myLogs = [...this.myLogs, '否认禁用']; + } + } + }, + { + label: '制作镜像' + }, + { + label: '这是一个很长的选项' + }, + { + label: '制作镜像2', + disabled: true + } + ]; + if (data.state === '已停止') { + items[0].disabled = false; + items[1].disabled = true; + items = [items[0], items[1]]; + } + return items; + }; + + private createRandomItem(id: number): any { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip']; + const firstName: string = nameList[Math.floor(Math.random() * 4)]; + const lastName: string = familyName[Math.floor(Math.random() * 4)]; + const age: number = Math.floor(Math.random() * 100); + const state: string = ['已启动', '已停止'][Math.floor(Math.random() * 2)]; + const rowId: string = 'row_' + id; + + return { + firstName, + lastName, + age, + state, + rowId + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/popconfirm/demo/src/app/popconfirm/PopconfirmTableDefineComponent.ts b/src/popconfirm/demo/src/app/popconfirm/PopconfirmTableDefineComponent.ts new file mode 100644 index 0000000..d8e6585 --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/PopconfirmTableDefineComponent.ts @@ -0,0 +1,87 @@ +import { Component } from '@angular/core'; +import { TiPopconfirmConfig, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './popconfirm-table-define.html' +}) +export class PopconfirmTableDefineComponent { + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + myLogs: any = []; + columns: Array = [ + { + label: 'first name', + width: '25%' + }, + { + label: 'last name', + width: '20%' + }, + { + label: 'age', + width: '20%' + }, + { + label: 'state', + width: '15%' + }, + { + label: 'option', + width: '20%' + } + ]; + + popConfig: TiPopconfirmConfig = { + id: 'delete', + content: '确定要删除吗?', + yesText: '确认', + noText: '取消', + position: 'right', + close: (data: any): void => { + const index = this.srcData.data.findIndex((current: TiTableRowData): boolean => { + return current.rowId === data.rowId; + }); + this.srcData.data.splice(index, 1); + this.myLogs = [...this.myLogs, '确认删除']; + }, + dismiss: (data: any): void => { + this.myLogs = [...this.myLogs, '取消删除']; + } + }; + + ngOnInit(): void { + for (let j: number = 0; j < 4; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + private createRandomItem(id: number): any { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[Math.floor(Math.random() * 4)]; + const lastName: string = familyName[Math.floor(Math.random() * 4)]; + const age: number = Math.floor(Math.random() * 100); + const state: string = ['已启动', '已停止'][Math.floor(Math.random() * 2)]; + const rowId: string = 'row_' + id; + + return { + firstName, + lastName, + age, + state, + rowId + }; + } + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/popconfirm/demo/src/app/popconfirm/PopconfirmTestModule.ts b/src/popconfirm/demo/src/app/popconfirm/PopconfirmTestModule.ts new file mode 100644 index 0000000..13ad81b --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/PopconfirmTestModule.ts @@ -0,0 +1,56 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiActionmenuModule, TiMenuModule, TiPopconfirmModule, TiTableModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { PopconfirmBasicComponent } from './PopconfirmBasicComponent'; +import { PopconfirmDefineComponent } from './PopconfirmDefineComponent'; +import { PopconfirmEventComponent } from './PopconfirmEventComponent'; +import { PopconfirmTableComponent } from './PopconfirmTableComponent'; +import { PopconfirmTableDefineComponent } from './PopconfirmTableDefineComponent'; + +@NgModule({ + imports: [ + CommonModule, + TiActionmenuModule, + TiPopconfirmModule, + TiTableModule, + TiMenuModule, + DemoLogModule, + RouterModule.forChild(PopconfirmTestModule.ROUTES) + ], + declarations: [ + PopconfirmBasicComponent, + PopconfirmDefineComponent, + PopconfirmEventComponent, + PopconfirmTableComponent, + PopconfirmTableDefineComponent + ] +}) +export class PopconfirmTestModule { + static readonly LINKS: Array = [{ href: 'components/TiPopoverComponent.html', label: 'Popconfirm' }]; + static readonly ROUTES: Routes = [ + { + path: 'popconfirm/popconfirm-basic', + component: PopconfirmBasicComponent + }, + { + path: 'popconfirm/popconfirm-define', + component: PopconfirmDefineComponent + }, + { + path: 'popconfirm/popconfirm-event', + component: PopconfirmEventComponent + }, + { + path: 'popconfirm/popconfirm-table', + component: PopconfirmTableComponent + }, + { + path: 'popconfirm/popconfirm-table-define', + component: PopconfirmTableDefineComponent + } + ]; +} diff --git a/src/popconfirm/demo/src/app/popconfirm/popconfirm-basic.html b/src/popconfirm/demo/src/app/popconfirm/popconfirm-basic.html new file mode 100644 index 0000000..e3d3d6b --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/popconfirm-basic.html @@ -0,0 +1 @@ +删除 diff --git a/src/popconfirm/demo/src/app/popconfirm/popconfirm-define.html b/src/popconfirm/demo/src/app/popconfirm/popconfirm-define.html new file mode 100644 index 0000000..ebd5db7 --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/popconfirm-define.html @@ -0,0 +1 @@ +启用 diff --git a/src/popconfirm/demo/src/app/popconfirm/popconfirm-event.html b/src/popconfirm/demo/src/app/popconfirm/popconfirm-event.html new file mode 100644 index 0000000..1aa2a6c --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/popconfirm-event.html @@ -0,0 +1,3 @@ +删除 +
    + diff --git a/src/popconfirm/demo/src/app/popconfirm/popconfirm-table-define.html b/src/popconfirm/demo/src/app/popconfirm/popconfirm-table-define.html new file mode 100644 index 0000000..5ffd60a --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/popconfirm-table-define.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + +
    {{column.label}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.state}} + 删除 +
    +
    + diff --git a/src/popconfirm/demo/src/app/popconfirm/popconfirm-table.html b/src/popconfirm/demo/src/app/popconfirm/popconfirm-table.html new file mode 100644 index 0000000..b5af44e --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/popconfirm-table.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + +
    {{column.label}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.state}} + +
    +
    + diff --git a/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm-demos.js b/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm-demos.js new file mode 100644 index 0000000..f8f937c --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm-demos.js @@ -0,0 +1,68 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'popconfirm-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': '

    Popconfirm 组件的最简用法。

    ', + 'en-US': '

    basic

    ' + } + }, + { + demoId: 'popconfirm-define', + name: { + 'zh-CN': '自定义使用', + 'en-US': 'define' + }, + desc: { + 'zh-CN': + '

    Popconfirm 组件的自定义用法。通过配置TiPopconfirmConfig接口的内容,如contentyesTextnoText等传递数据。

    ', + 'en-US': '

    define

    ' + }, + apis: ['TiPopconfirmDirective.properties.tiPopconfirm'] + }, + { + demoId: 'popconfirm-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event' + }, + desc: { + 'zh-CN': + '

    当点击弹出确认框中“是”的按钮时触发close事件。当点击弹出确认框中的“否”的按钮时触发dismiss事件。

    ', + 'en-US': '

    event

    ' + }, + apis: ['TiPopconfirmConfig.methods.close', 'TiPopconfirmConfig.methods.dismiss'] + }, + { + demoId: 'popconfirm-table', + name: { + 'zh-CN': '在表格中结合 actionmenu 的用法', + 'en-US': 'PopconfirmInTable' + }, + desc: { + 'zh-CN': + '

    通过配置TiActionmenuItem接口中的内容传递数据。使用 Actionmenu 组件,请导入TiActionmenuModule,具体可参考 Actionmenu 组件的使用说明。

    ', + 'en-US': '

    PopconfirmInTable

    ' + }, + apis: ['TiPopconfirmDirective.properties.data', 'TiActionmenuItem.properties.popConfig'] + }, + { + demoId: 'popconfirm-table-define', + name: { + 'zh-CN': '在表格中自定义使用', + 'en-US': 'PopconfirmInTable' + }, + desc: { + 'zh-CN': '

    通过配置TiPopconfirmConfig接口的内容传递数据。

    ', + 'en-US': '

    PopconfirmInTable

    ' + }, + apis: ['TiPopconfirmDirective.properties.tiPopconfirm'] + } + ] +}; diff --git a/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.cn.md b/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.cn.md new file mode 100644 index 0000000..782e055 --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.cn.md @@ -0,0 +1,23 @@ +--- +title: Popconfirm 气泡确认框 +--- +# Popconfirm 气泡确认框 + +
    + +Popconfirm 是一个文字提示气泡框组件。   + +```typescript +import { TiPopconfirmModule } from '@opentiny/ng'; +``` + +
    + +
    + +Popconfirm 是一个文字提示气泡框组件。   + +```typescript +import { TiPopconfirmModule } from '@opentiny/ng'; +``` +
    diff --git a/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.en.md b/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/popconfirm/demo/src/favicon.ico b/src/popconfirm/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/popconfirm/demo/src/index.html b/src/popconfirm/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/popconfirm/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/popconfirm/demo/src/main.ts b/src/popconfirm/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/popconfirm/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/popconfirm/demo/tsconfig.app.json b/src/popconfirm/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/popconfirm/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/popconfirm/lib/index.ts b/src/popconfirm/lib/index.ts new file mode 100644 index 0000000..506e2f7 --- /dev/null +++ b/src/popconfirm/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiPopconfirmModule'; diff --git a/src/popconfirm/lib/ng-package.json b/src/popconfirm/lib/ng-package.json new file mode 100644 index 0000000..8fcc727 --- /dev/null +++ b/src/popconfirm/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/popconfirm", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/popconfirm/lib/package.json b/src/popconfirm/lib/package.json new file mode 100644 index 0000000..2d58f06 --- /dev/null +++ b/src/popconfirm/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "@opentiny/ng-popconfirm", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/popconfirm/lib/project.json b/src/popconfirm/lib/project.json new file mode 100644 index 0000000..f7e0e52 --- /dev/null +++ b/src/popconfirm/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/popconfirm/lib", + "sourceRoot": "src/popconfirm/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/popconfirm"], + "options": { + "project": "src/popconfirm/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/popconfirm"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js popconfirm" + }, + { + "command": "ng default-build popconfirm" + }, + { + "command": "node build/clear-default-theme.js popconfirm" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/popconfirm && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build popconfirm && ng pack popconfirm && node build/publish.js popconfirm --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/popconfirm/lib/src/TiPopconfirmComponent.ts b/src/popconfirm/lib/src/TiPopconfirmComponent.ts new file mode 100644 index 0000000..2157447 --- /dev/null +++ b/src/popconfirm/lib/src/TiPopconfirmComponent.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterViewInit, Component, ElementRef, Input, OnInit, Renderer2, ChangeDetectionStrategy } from '@angular/core'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +@Component({ + selector: 'ti-popconfirm', + templateUrl: 'popconfirm.html', + styleUrls: ['./popconfirm.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-popconfirm-container]': 'true', + '[tabindex]': '0' + } +}) +export class TiPopconfirmComponent extends TiBaseComponent implements OnInit, AfterViewInit { + @Input() config: any; + @Input() destroyPopover: (result?: boolean) => void; + protected versionInfo: string = super.getVersion(packageInfo); + private readonly tiPopconfirm: any = TiLocale.getLocaleWords().tiPopconfirm; + + ngOnInit(): void { + this.config.yesText = this.config.yesText || this.tiPopconfirm.yesLabel; + this.config.noText = this.config.noText || this.tiPopconfirm.noLabel; + } + + ngAfterViewInit(): void { + // 轻量级弹窗阴影定制 + this.renderer.addClass(this.nativeElement.offsetParent, 'ti3-popconfirm-tip'); // 在init中设置会影响button样式 TODO: IE下还存在背景闪烁问题 + } +} diff --git a/src/popconfirm/lib/src/TiPopconfirmDirective.ts b/src/popconfirm/lib/src/TiPopconfirmDirective.ts new file mode 100644 index 0000000..b747e3b --- /dev/null +++ b/src/popconfirm/lib/src/TiPopconfirmDirective.ts @@ -0,0 +1,230 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectorRef, ComponentRef, Directive, ElementRef, Inject, Input, NgZone, OnDestroy, OnInit, Renderer2 } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +import { TiHostLayout, TiPositionType } from '@opentiny/ng-utils'; +import { TiTipRef } from '@opentiny/ng-tip'; +import { TiTipService } from '@opentiny/ng-tip'; +import { TiTipDirective } from '@opentiny/ng-tip'; +import { TiPopconfirmComponent } from './TiPopconfirmComponent'; +import { TiKeymap, Util } from '@opentiny/ng-utils'; + +export interface TiPopconfirmConfig { + /** + * 确认框 id + */ + id?: string; + /** + * 确认框描述信息 + */ + content: string; + /** + * 自定义“是”按钮文本 + */ + yesText?: string; + /** + * 自定义“否”按钮文本 + */ + noText?: string; + /** + * 设置“是”按钮为强调按钮 + */ + yesPrimary?: boolean; + /** + * 确认框弹出方向 + */ + position?: TiPositionType; + /** + * 触发“是”按钮事件 + */ + close?(data?: any): void; + /** + * 触发“否”按钮事件 + */ + dismiss?(data?: any): void; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} + +/** + * TiPopconfirm 气泡确认框指令 + * + * 一般用于操作执行后对用户业务不会有严重影响的轻量级场景。 + * + * + */ +@Directive({ + selector: '[tiPopconfirm]' +}) +export class TiPopconfirmDirective extends TiTipDirective implements OnInit, OnDestroy { + private static readonly Z_INDEX: number = 100; // 默认层级是100,暂不支持可配置 + /** + * 气泡确认框配置对象 + */ + @Input() tiPopconfirm: TiPopconfirmConfig; + /** + * + * 数据接口, 常常绑定表格本行数据 + */ + @Input() data: any; + protected tipInstance: TiTipRef; + private hostElement: any; + private popoverComponentRef: ComponentRef = null; + private tipElement; // popoverComponentRef的原生元素 + private unlistenClick: () => void; + // 可聚焦元素 + private focusableElementsString: string = `a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), + button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), + textarea:not([disabled]):not([tabindex=\'-1\']), + iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]`; + constructor( + private tiTipService: TiTipService, + private hostEleRef: ElementRef, + private zone: NgZone, + private render: Renderer2, + @Inject(DOCUMENT) private document, + private changeDetectorRef: ChangeDetectorRef + ) { + super(tiTipService, hostEleRef); + } + /** + * tip 组件配置 + */ + private tipConfig: any = { + trigger: 'manual', + theme: 'white' + }; + private unListenDocumentKeydown: () => void; + + ngOnInit(): void { + if (!this.tiPopconfirm || !this.tiPopconfirm.content) { + return; + } + this.hostElement = this.hostEleRef.nativeElement; + // 创建实例 + this.tipInstance = this.tiTipService.create(this.hostElement, { + ...this.tipConfig, + position: this.tiPopconfirm.position, + zIndex: TiPopconfirmDirective.Z_INDEX, + registerVisibilityChangeEvent: false + }); + + this.addClickEvent(); + } + + ngOnDestroy(): void { + if (this.tipInstance) { + super.ngOnDestroy(); + } + if (this.unlistenClick) { + this.unlistenClick(); + } + if (this.unListenDocumentKeydown) { + this.unListenDocumentKeydown(); + } + } + + private addClickEvent(): void { + this.zone.runOutsideAngular(() => { + this.unlistenClick = this.render.listen(this.document, 'click', (event: MouseEvent) => { + if (this.hostElement.contains(event.target) && !this.popoverComponentRef) { + this.showPopandFocus(); + + return; + } + if (!(this.tipElement && this.tipElement.contains(event.target))) { + this.zone.run(() => { + this.hide(); + }); + this.popoverComponentRef = null; + } + }); + + this.unListenDocumentKeydown = this.render.listen(document, 'keydown', (event: KeyboardEvent): void => { + switch (event.which) { + case TiKeymap.KEY_TAB: // tab键用于处理在提示框内循环获取焦点 + this.clickTab(event); + break; + case TiKeymap.KEY_ENTER: + if (this.hostElement.contains(event.target) && !this.popoverComponentRef) { + this.showPopandFocus(); + } + break; + default: + break; + } + }); + }); + } + + /** + * @ignore + * + */ + public clickTab(event: KeyboardEvent): void { + const dialogModalEle: HTMLElement = document.querySelector('.ti3-popconfirm-container'); + const focusableElements: NodeList = dialogModalEle?.querySelectorAll(this.focusableElementsString); + Util.focusInDialogOnTabchange(event, focusableElements); + } + /** + * 打开气泡组件,并且把焦点转移到提示框内部,为后续把焦点限制在提示框内做准备。 + * @private + */ + private showPopandFocus(): void { + this.zone.run(() => { + this.popoverComponentRef = this.show(); + const popContainerEle: HTMLElement = document.querySelector('.ti3-popconfirm-container'); + if (popContainerEle) { + popContainerEle.focus(); + } + }); + } + + /** + * 显示气泡确认框 + * @ignore + */ + public show(): ComponentRef { + if (!this.tipInstance) { + return; + } + const destroyPopover: (result?: boolean) => void = (result?: boolean): void => { + this.hide(); + if (result && Util.isFunction(this.tiPopconfirm.close)) { + this.tiPopconfirm.close(this.data); + } else if (!result && Util.isFunction(this.tiPopconfirm.dismiss)) { + this.tiPopconfirm.dismiss(this.data); + } + + // 服务在close和dismiss事件中自行添加逻辑时,视图无法更新需要手动变更刷新 + this.changeDetectorRef.markForCheck(); + }; + const popoverComponentRef: ComponentRef = this.tipInstance.show(TiPopconfirmComponent, { + id: this.tiPopconfirm.id, + config: this.tiPopconfirm, + destroyPopover + }); + this.tipElement = popoverComponentRef.location.nativeElement; + const hostPosition: TiHostLayout = this.tiTipService.positionResult?.hostLayout; + if (typeof getComputedStyle !== 'undefined' && hostPosition?.fixedAncestor) { + const fixedAncestorZindex: number = parseInt(getComputedStyle(hostPosition.fixedAncestor).zIndex, 10); + if (typeof fixedAncestorZindex === 'number' && fixedAncestorZindex > TiPopconfirmDirective.Z_INDEX) { + this.render.setStyle(this.tipElement, 'z-index', fixedAncestorZindex); + } + } + + return popoverComponentRef; + } +} diff --git a/src/popconfirm/lib/src/TiPopconfirmModule.ts b/src/popconfirm/lib/src/TiPopconfirmModule.ts new file mode 100644 index 0000000..0c493c6 --- /dev/null +++ b/src/popconfirm/lib/src/TiPopconfirmModule.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiPopconfirmComponent } from './TiPopconfirmComponent'; +import { TiTipServiceModule } from '@opentiny/ng-tip'; +import { TiLocaleModule, TiLocale } from '@opentiny/ng-locale'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiButtonModule } from '@opentiny/ng-button'; +import { TiPopconfirmDirective } from './TiPopconfirmDirective'; +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, TiTipServiceModule, TiLocaleModule, TiIconModule, TiButtonModule], + exports: [TiPopconfirmDirective], + declarations: [TiPopconfirmComponent, TiPopconfirmDirective], + entryComponents: [TiPopconfirmComponent] +}) +export class TiPopconfirmModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiPopconfirmComponent } from './TiPopconfirmComponent'; +export { TiPopconfirmDirective, TiPopconfirmConfig } from './TiPopconfirmDirective'; diff --git a/src/popconfirm/lib/src/i18n/TiPopconfirmWords.ts b/src/popconfirm/lib/src/i18n/TiPopconfirmWords.ts new file mode 100644 index 0000000..d177154 --- /dev/null +++ b/src/popconfirm/lib/src/i18n/TiPopconfirmWords.ts @@ -0,0 +1,6 @@ +export interface TiPopconfirmWords { + tiPopconfirm: { + yesLabel: string; + noLabel: string; + }; +} diff --git a/src/popconfirm/lib/src/i18n/en_US.ts b/src/popconfirm/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..edc29c3 --- /dev/null +++ b/src/popconfirm/lib/src/i18n/en_US.ts @@ -0,0 +1,8 @@ +import { TiPopconfirmWords } from './TiPopconfirmWords'; + +export const en_US: TiPopconfirmWords = { + tiPopconfirm: { + yesLabel: 'Yes', + noLabel: 'No' + } +}; diff --git a/src/popconfirm/lib/src/i18n/es_US.ts b/src/popconfirm/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..8cec67a --- /dev/null +++ b/src/popconfirm/lib/src/i18n/es_US.ts @@ -0,0 +1,8 @@ +import { TiPopconfirmWords } from './TiPopconfirmWords'; + +export const es_US: TiPopconfirmWords = { + tiPopconfirm: { + yesLabel: 'Sí', + noLabel: 'No' + } +}; diff --git a/src/popconfirm/lib/src/i18n/fr_FR.ts b/src/popconfirm/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..cfc9e90 --- /dev/null +++ b/src/popconfirm/lib/src/i18n/fr_FR.ts @@ -0,0 +1,8 @@ +import { TiPopconfirmWords } from './TiPopconfirmWords'; + +export const fr_FR: TiPopconfirmWords = { + tiPopconfirm: { + yesLabel: 'Oui', + noLabel: 'Non' + } +}; diff --git a/src/popconfirm/lib/src/i18n/index.ts b/src/popconfirm/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/popconfirm/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/popconfirm/lib/src/i18n/pt_BR.ts b/src/popconfirm/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..c65c126 --- /dev/null +++ b/src/popconfirm/lib/src/i18n/pt_BR.ts @@ -0,0 +1,8 @@ +import { TiPopconfirmWords } from './TiPopconfirmWords'; + +export const pt_BR: TiPopconfirmWords = { + tiPopconfirm: { + yesLabel: 'Sim', + noLabel: 'Não' + } +}; diff --git a/src/popconfirm/lib/src/i18n/zh_CN.ts b/src/popconfirm/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..aa26875 --- /dev/null +++ b/src/popconfirm/lib/src/i18n/zh_CN.ts @@ -0,0 +1,8 @@ +import { TiPopconfirmWords } from './TiPopconfirmWords'; + +export const zh_CN: TiPopconfirmWords = { + tiPopconfirm: { + yesLabel: '是', + noLabel: '否' + } +}; diff --git a/src/popconfirm/lib/src/popconfirm.html b/src/popconfirm/lib/src/popconfirm.html new file mode 100644 index 0000000..502243b --- /dev/null +++ b/src/popconfirm/lib/src/popconfirm.html @@ -0,0 +1,20 @@ +
    + + + + +
    + diff --git a/src/popconfirm/lib/src/popconfirm.less b/src/popconfirm/lib/src/popconfirm.less new file mode 100644 index 0000000..0efd193 --- /dev/null +++ b/src/popconfirm/lib/src/popconfirm.less @@ -0,0 +1,49 @@ +:host { + --ti-popconfirm-warn-bg-font-size: var(--ti-common-size-4x); + --ti-popconfirm-warn-bg-line-height: 18px; + --ti-popconfirm-button-margin-left: var(--ti-common-space-2x); + --ti-popconfirm-title-margin-left: var(--ti-common-space-2x); +} + +.ti3-popconfirm-tip { + box-shadow: var(--ti-common-shadow-4-down) !important; +} +:host.ti3-popconfirm-container { + display: inline-block; + outline: none; +} +.ti3-popconfirm-header { + display: flex; + align-content: center; +} + +.ti3-popconfirm-warn-bg { + position: relative; + font-size: var(--ti-popconfirm-warn-bg-font-size); + color: var(--ti-common-color-warn); + line-height: var(--ti-popconfirm-warn-bg-line-height); + align-self: flex-start; // 解决多行文本!图标显示异常问题 +} + +.ti3-popconfirm-warn-icon { + position: absolute; + font-size: calc(var(--ti-popconfirm-warn-bg-font-size) * 0.75); + color: var(--ti-common-color-icon-white); + left: calc((var(--ti-popconfirm-warn-bg-font-size) * 0.25) / 2); + bottom: -1px; +} + +.ti3-popconfirm-title { + color: var(--ti-common-color-text-primary); + font-size: var(--ti-common-font-size-base); + line-height: var(--ti-common-line-height-number); + margin-left: var(--ti-popconfirm-title-margin-left); +} + +.ti3-popconfirm-footer { + margin-top: var(--ti-common-space-3x); + text-align: right; + & button:nth-of-type(2) { + margin-left: var(--ti-popconfirm-button-margin-left); + } +} diff --git a/src/popup/lib/index.ts b/src/popup/lib/index.ts new file mode 100644 index 0000000..73f5082 --- /dev/null +++ b/src/popup/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiPopupService'; +export * from './src/TiPopupModule'; diff --git a/src/popup/lib/ng-package.json b/src/popup/lib/ng-package.json new file mode 100644 index 0000000..731317e --- /dev/null +++ b/src/popup/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/popup", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/popup/lib/package.json b/src/popup/lib/package.json new file mode 100644 index 0000000..78ba411 --- /dev/null +++ b/src/popup/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-popup", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@angular/platform-browser": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/popup/lib/project.json b/src/popup/lib/project.json new file mode 100644 index 0000000..eff9f4c --- /dev/null +++ b/src/popup/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/popup/lib", + "sourceRoot": "src/popup/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/popup"], + "options": { + "project": "src/popup/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/popup"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js popup" + }, + { + "command": "ng default-build popup" + }, + { + "command": "node build/clear-default-theme.js popup" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/popup && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build popup && ng pack popup && node build/publish.js popup --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/popup/lib/src/TiPopupModule.ts b/src/popup/lib/src/TiPopupModule.ts new file mode 100644 index 0000000..9bafcfe --- /dev/null +++ b/src/popup/lib/src/TiPopupModule.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +/** + * @ignore + */ +@NgModule({ + imports: [] +}) +export class TiPopupModule {} diff --git a/src/popup/lib/src/TiPopupService.ts b/src/popup/lib/src/TiPopupService.ts new file mode 100644 index 0000000..1b95196 --- /dev/null +++ b/src/popup/lib/src/TiPopupService.ts @@ -0,0 +1,296 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/* eslint-disable no-param-reassign */ +/** + * 该类提供一个服务,用于管理弹出类组件的创建和销毁 + * 服务中提供三个方法: + * create(componentType) 生成一个popup实例并返回对象, + * componentType:包裹内容的父容器元素类 + * 返回的实例对象中提供方法: + * { + * show({ // 生成元素并在指定容器中显示 + * content:弹出组件内容 + * context:弹出组件上下文 + * container:弹出组件最终放置的容器位置 + * }) : componentRef // 返回生成的组件实例 + * hide():隐藏并销毁元素 + * } + */ +import { + ApplicationRef, + ComponentFactoryResolver, + ComponentRef, + ElementRef, + EmbeddedViewRef, + Injectable, + Injector, + Renderer2, + RendererFactory2, + SecurityContext, + TemplateRef, + ViewRef, + Inject +} from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { DomSanitizer } from '@angular/platform-browser'; + +import { TiPopupModule } from './TiPopupModule'; +import { Util } from '@opentiny/ng-utils'; +/** + * @ignore + * 类型中any代表组件形式 + */ +export type TiContentType = string | TemplateRef | any; +/** + * @ignore + * popup show方法配置 + */ +interface TiPopUpShowConfig { + content?: TiContentType; // 弹出组件内容 + context?: any; // 弹出组件上下文 + contentContext?: any; // 内容部分的组件上下文 + contentParentInjector?: Injector; // 内容部分的组件父级注入器 + container?: any; // 弹出组件最终放置的容器位置 +} +/** + * @ignore + * popup create返回值 + */ +export interface TiPopUpRef { + show(config: TiPopUpShowConfig | {}): ComponentRef; + hide(): void; +} +/** + * @ignore + */ +class ContentRef { + constructor(public nodes: Array, public viewRef?: ViewRef, public componentRef?: ComponentRef) {} +} +/** + * @ignore + */ +@Injectable({ + providedIn: TiPopupModule +}) +export class TiPopupService { + /** + * 由于该服务可能被其他服务使用到,并以服务的形式提供给外部; + * 而Renderer2本身不能脱离于component之外定义或依赖,所以使用RendererFactory2方式实例化进行处理 + * 具体说明见如下: + * https://stackoverflow.com/questions/43070308/using-renderer-in-angular-4 + */ + private renderer: Renderer2; + constructor( + private injector: Injector, + rendererFactory: RendererFactory2, + private componentFactoryResolver: ComponentFactoryResolver, + private applicationRef: ApplicationRef, + private domSanitizer: DomSanitizer, + @Inject(DOCUMENT) private document + ) { + this.renderer = rendererFactory.createRenderer(null, null); + } + + private getParentEle(containerOpt: string | ElementRef): Element { + if (containerOpt instanceof ElementRef) { + // 父元素为正常element元素情况下 + return containerOpt.nativeElement; + } else if (containerOpt === 'body') { + // 父容器为body元素情况下 + return this.document.body; + } + } + + // 销毁组件相关内容 + private static destroyComponentRef(_componentRef: ComponentRef): void { + if (!_componentRef) { + return; + } + // 销毁组件实例 + _componentRef.destroy(); + _componentRef = null; + } + + // 销毁弹出内容相关内容 + private static destroyContentRef(contentRef: ContentRef): void { + // 弹出内容如果有组件实例对象时,销毁组件实例 + if (contentRef.componentRef) { + contentRef.componentRef.destroy(); + } + // 销毁页面视图实例 + if (contentRef.viewRef) { + contentRef.viewRef.destroy(); + } + // 销毁内容实例 + contentRef = null; + } + public create(componentType: any): TiPopUpRef { + let _contentRef: ContentRef; // 弹出内容实例 + let _componentRef: ComponentRef | any; // 生成的组件实例对象 + + return { + show: (options?: TiPopUpShowConfig): ComponentRef => { + // component已生成情况下,不再重复生成 + if (_componentRef) { + return _componentRef; + } + // 获取内容相关信息,包括:组件节点信息、组件实例信息、组件最小视图信息 + _contentRef = this.getContentRef( + options && options.content, + options && options.contentContext, + options && options.contentParentInjector + ); + + // 创建组件实例:为内容本身再包一层父容器组件,用于控制组件(componentType) + _componentRef = this.createCompoentRef({ + componentType, + nodes: _contentRef.nodes, + context: options && options.context + }); + + // 将元素放置在指定容器中 + const parentEle: Element = this.getParentEle(options && options.container); + if (parentEle) { + parentEle.appendChild(_componentRef.location.nativeElement); + } + + // 这时弹出内容已经append,可以对弹出内容进行解析了。 + // 弹出内容中某些元素会在初始化时需要获取自身DOM宽高等,所以要保证弹出内容先append,然后再解析。 + // 解析ng-template形式的弹出内容实例 + if (options.content instanceof TemplateRef) { + // 确保数据变化均可以被检测到 + _contentRef.viewRef.markForCheck(); + // 执行一次变化检测 + _contentRef.viewRef.detectChanges(); + } + + // 使用trycatch是为了组件实例化时产生错误控制台报错)的情况下,后续逻辑能够继续执行(不阻塞)。 + // 主要是为了弹框内容传入的是自定义组件 Component 情况下,当点击按钮打开弹窗,自定义的组件实例化时 + // 产生错误(控制台报错)的情况下,弹窗能正常的打开,且能正常的关闭。 + try { + // 解析component形式的弹出内容实例 + if (typeof options.content === 'function') { + // 在组件的 metadata 中如果设置了OnPush 条件,那么变化检测不会再次执行, + // 但是调用该方法,可以确保数据变化被检测到 + _contentRef.componentRef.changeDetectorRef.markForCheck(); + // 从该组件到其子组件执行一次变化检测 + _contentRef.componentRef.changeDetectorRef.detectChanges(); + } + } catch (error) { + console.error(error); + } + + _componentRef.tiContentRef = _contentRef; + + return _componentRef; + }, + hide: (): void => { + if (!_componentRef) { + return; + } + TiPopupService.destroyComponentRef(_componentRef); + TiPopupService.destroyContentRef(_contentRef); + // 由于该变量在destroyComponentRef函数中赋为null不会改变外部值的改变,导致下次show时判断错误,因此此处需要做处理 + _componentRef = null; + } + }; + } + + private getContentRef = (content: TiContentType, context?: any, contentParentInjector?: Injector): ContentRef => { + // ng-template形式 + if (content instanceof TemplateRef) { + // 将template实例化为内嵌视图,并将其放在可运行环境中进行解析 + const embeddedViewRef: EmbeddedViewRef = content.createEmbeddedView({ + context + }); + this.applicationRef.attachView(embeddedViewRef); // 不做此处处理,ng-template中的标签不会解析 + + return new ContentRef([embeddedViewRef.rootNodes], embeddedViewRef); + } + // 组件形式 + if (typeof content === 'function') { + // 根据传入的component类(即content)创建组件引用 + const componentRef: ComponentRef = this.createCompoentRef({ + componentType: content, + context, + notDetectChanges: true, + parentInjector: contentParentInjector + }); + + return new ContentRef([[componentRef.location.nativeElement]], componentRef.hostView, componentRef); + } + // element Dom形式 + if (content instanceof HTMLElement) { + return new ContentRef([[content]]); + } + + // string形式 或 SafeHtml(object类型) 形式, + if (typeof content === 'string' || typeof content === 'object') { + const wrapEle: Element = this.renderer.createElement('div'); + wrapEle.innerHTML = this.domSanitizer.sanitize(SecurityContext.HTML, content); + + return new ContentRef([wrapEle.childNodes]); + } + + return new ContentRef([]); + }; + /** + * 创建组件实例 + * options { + * componentType: 组件类 + * nodes:组件中的可注入节点 + * context: 组件属性配置,inputs属性均可在此配置 + * } + */ + public createCompoentRef(options: { + componentType?: any; + nodes?: Array; + context?: { outputs?: Object; [propName: string]: any }; + notDetectChanges?: boolean; + parentInjector?: Injector; + }): ComponentRef { + const injector: Injector = Injector.create({ + parent: options.parentInjector || this.injector, + providers: [] + }); + // 1. 根据component类创建组件引用 + const componentRef: ComponentRef = this.componentFactoryResolver + .resolveComponentFactory(options.componentType) + .create(injector, options.nodes); + + // 2. 将组件绑定在ng component树上,不做绑定情况下,内容中的指令无法解析 + this.applicationRef.attachView(componentRef.hostView); + + // 3. 绑定组件上下文定义 + Object.assign(componentRef.instance, options.context); + // outputs事件绑定 + if (options.context && !Util.isUndefined(options.context.outputs)) { + for (const key in options.context.outputs) { + if (Object.prototype.hasOwnProperty.call(options.context.outputs, key)) { + componentRef.instance[key].subscribe(options.context.outputs[key]); + } + } + } + + // 4. 确保组件视图根据数据能实时刷新上。 + // 通过该处理可以确保组件及子组件都完成解析 + if (!options.notDetectChanges) { + // 在组件的 metadata 中如果设置了OnPush 条件,那么变化检测不会再次执行, + // 但是调用该方法,可以确保数据变化被检测到 + componentRef.changeDetectorRef.markForCheck(); + // 从该组件到其子组件执行一次变化检测 + componentRef.changeDetectorRef.detectChanges(); + } + + return componentRef; + } +} diff --git a/src/productpreview/demo/project.json b/src/productpreview/demo/project.json new file mode 100644 index 0000000..6806e8b --- /dev/null +++ b/src/productpreview/demo/project.json @@ -0,0 +1,86 @@ +{ + "projectType": "application", + "root": "src/productpreview/demo", + "sourceRoot": "src/productpreview/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/productpreview", + "index": "src/productpreview/demo/src/index.html", + "main": "src/productpreview/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/productpreview/demo/tsconfig.app.json", + "assets": [ + "src/productpreview/demo/src/favicon.ico", + "src/productpreview/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/image", + "output": "/assets/image/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "productpreview-demo:build:production" + }, + "development": { + "browserTarget": "productpreview-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js productpreview" + } + ] + } + } + } +} diff --git a/src/productpreview/demo/src/app/AppComponent.ts b/src/productpreview/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/productpreview/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/productpreview/demo/src/app/AppModule.ts b/src/productpreview/demo/src/app/AppModule.ts new file mode 100644 index 0000000..ffafda7 --- /dev/null +++ b/src/productpreview/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ProductpreviewTestModule } from './productpreview/ProductpreviewTestModule'; + +@NgModule({ + imports: [ + ProductpreviewTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/productpreview/demo/src/app/IndexComponent.ts b/src/productpreview/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..9e9d368 --- /dev/null +++ b/src/productpreview/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ProductpreviewTestModule } from './productpreview/ProductpreviewTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = ProductpreviewTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/productpreview/demo/src/app/app.html b/src/productpreview/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/productpreview/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/productpreview/demo/src/app/productpreview/ProductpreviewBasicComponent.ts b/src/productpreview/demo/src/app/productpreview/ProductpreviewBasicComponent.ts new file mode 100644 index 0000000..2f03dca --- /dev/null +++ b/src/productpreview/demo/src/app/productpreview/ProductpreviewBasicComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiFilePreviewInfo } from '@opentiny/ng'; + +@Component({ + templateUrl: './productpreview-basic.html' +}) +export class ProductpreviewBasicComponent { + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL']; + fileList: Array = [ + { + previewUrl: `${this.baseUrl}assets/image/1.jpg` + }, + { + previewUrl: `${this.baseUrl}assets/image/2.jpg` + }, + { + previewUrl: `${this.baseUrl}assets/image/3.jpg` + }, + { + previewUrl: `${this.baseUrl}assets/image/1.jpg` + }, + { + previewUrl: `${this.baseUrl}assets/image/2.jpg` + }, + { + previewUrl: `${this.baseUrl}assets/image/3.jpg` + } + ]; +} diff --git a/src/productpreview/demo/src/app/productpreview/ProductpreviewTestModule.ts b/src/productpreview/demo/src/app/productpreview/ProductpreviewTestModule.ts new file mode 100644 index 0000000..2cb4feb --- /dev/null +++ b/src/productpreview/demo/src/app/productpreview/ProductpreviewTestModule.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { TiProductpreviewModule } from '@opentiny/ng'; + +import { ProductpreviewBasicComponent } from './ProductpreviewBasicComponent'; + +@NgModule({ + imports: [CommonModule, TiProductpreviewModule, RouterModule.forChild(ProductpreviewTestModule.ROUTES)], + declarations: [ProductpreviewBasicComponent] +}) +export class ProductpreviewTestModule { + public static readonly ROUTES: Routes = [{ path: 'productpreview/productpreview-basic', component: ProductpreviewBasicComponent }]; +} diff --git a/src/productpreview/demo/src/app/productpreview/productpreview-basic.html b/src/productpreview/demo/src/app/productpreview/productpreview-basic.html new file mode 100644 index 0000000..686e9c5 --- /dev/null +++ b/src/productpreview/demo/src/app/productpreview/productpreview-basic.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/productpreview/demo/src/app/productpreview/webdoc/productpreview-demos.js b/src/productpreview/demo/src/app/productpreview/webdoc/productpreview-demos.js new file mode 100644 index 0000000..1d9c8dc --- /dev/null +++ b/src/productpreview/demo/src/app/productpreview/webdoc/productpreview-demos.js @@ -0,0 +1,17 @@ +export default { + column: '2', + demos: [ + { + demoId: 'productpreview-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    Productproview 组件的最简用法。

    ', + 'en-US': 'Basic usage' + }, + apis: ['TiProductpreviewComponent.properties.files'] + } + ] +}; diff --git a/src/productpreview/demo/src/app/productpreview/webdoc/productpreview.cn.md b/src/productpreview/demo/src/app/productpreview/webdoc/productpreview.cn.md new file mode 100644 index 0000000..b1a40d2 --- /dev/null +++ b/src/productpreview/demo/src/app/productpreview/webdoc/productpreview.cn.md @@ -0,0 +1,32 @@ +--- +title: Productpreview 商品预览 +--- + +# Productpreview 商品预览 + +
    + +Productpreview 用来展示商品的预览内容。   + ++ 商品预览组件,响应式尺寸。 ++ 屏幕分辨率>=1600,尺寸450px。 ++ 1600>屏幕分辨率>=1440,尺寸380px。 ++ 1440>屏幕分辨率>=1280,分辨率>1280,尺寸300px。 + +```typescript +import { TiProductpreviewModule } from '@opentiny/ng'; +``` + +
    + +
    + +Productpreview 商品预览组件。   + +```typescript +import { TiProductpreviewModule } from '@opentiny/ng'; +``` + +
    + + diff --git a/src/productpreview/demo/src/app/productpreview/webdoc/productpreview.en.md b/src/productpreview/demo/src/app/productpreview/webdoc/productpreview.en.md new file mode 100644 index 0000000..5f58dfe --- /dev/null +++ b/src/productpreview/demo/src/app/productpreview/webdoc/productpreview.en.md @@ -0,0 +1,13 @@ +--- +title: Productpreview +--- + +# Productpreview + +
    + +```typescript +import { TiProductpreviewModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/productpreview/demo/src/favicon.ico b/src/productpreview/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/productpreview/demo/src/index.html b/src/productpreview/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/productpreview/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/productpreview/demo/src/main.ts b/src/productpreview/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/productpreview/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/productpreview/demo/tsconfig.app.json b/src/productpreview/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/productpreview/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/productpreview/lib/index.ts b/src/productpreview/lib/index.ts new file mode 100644 index 0000000..d4db45b --- /dev/null +++ b/src/productpreview/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiProductpreviewModule'; +export * from './src/TiProductpreviewComponent'; diff --git a/src/productpreview/lib/ng-package.json b/src/productpreview/lib/ng-package.json new file mode 100644 index 0000000..bd96695 --- /dev/null +++ b/src/productpreview/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/productpreview", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/productpreview/lib/package.json b/src/productpreview/lib/package.json new file mode 100644 index 0000000..bd6771d --- /dev/null +++ b/src/productpreview/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-productpreview", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@opentiny/ng-imagepreview": "~1.0.0-beta.0", + "@opentiny/ng-modal": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-zoom": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/productpreview/lib/project.json b/src/productpreview/lib/project.json new file mode 100644 index 0000000..7d8414b --- /dev/null +++ b/src/productpreview/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/productpreview/lib", + "sourceRoot": "src/productpreview/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/productpreview"], + "options": { + "project": "src/productpreview/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/productpreview"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js productpreview" + }, + { + "command": "ng default-build productpreview" + }, + { + "command": "node build/clear-default-theme.js productpreview" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/productpreview && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build productpreview && ng pack productpreview && node build/publish.js productpreview --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/productpreview/lib/src/TiProductpreviewComponent.ts b/src/productpreview/lib/src/TiProductpreviewComponent.ts new file mode 100644 index 0000000..3917e5a --- /dev/null +++ b/src/productpreview/lib/src/TiProductpreviewComponent.ts @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + OnChanges, + OnInit, + Renderer2, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { TiFilePreviewInfo, TiImagepreviewComponent } from '@opentiny/ng-imagepreview'; +import { TiModalService } from '@opentiny/ng-modal'; +import { TiBaseComponent } from '@opentiny/ng-base'; + +@Component({ + selector: 'ti-productpreview', + templateUrl: './productpreview.html', + styleUrls: ['./productpreview.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiProductpreviewComponent extends TiBaseComponent implements OnInit, OnChanges, AfterViewInit { + /** + * 每页显示的小图个数 + */ + private static readonly ITEM_PER_PAGE: number = 4; + + /** + * 预览文件列表 + */ + @Input() files: Array = []; + + /** + * @ignore + * 缩略图列表,翻页时重新设置 + */ + public thumbList: Array = []; + + /** + * @ignore 记录当前预览的商品索引 + */ + public currentPreviewIndex: number = -1; + + /** + * @ignore 缩略图当前页 + */ + public currentThumbPage: number = 1; + + /** + * @ignore 缩略图总页数 + * + */ + public totalThumbPage: number = 1; + + /** + * @ignore 内部变量 + */ + @ViewChild('productThumb') productThumbEle: ElementRef; + + constructor(private tiModal: TiModalService, private render: Renderer2, private hostEleRef: ElementRef) { + super(hostEleRef, render); + } + + ngAfterViewInit(): void { + this.render.addClass(this.productThumbEle.nativeElement.children[0], 'ti-product-preview-thumb-item-active'); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.files && changes.files.firstChange === false) { + this.files = changes.files.currentValue; + this.initThumb(); + if (this.productThumbEle) { + this.setThumbList(); + } + // 确保files接口变化时,第一个缩略图的选中样式生效 + setTimeout((): void => { + if (this.productThumbEle) { + this.render.addClass(this.productThumbEle.nativeElement.children[0], 'ti-product-preview-thumb-item-active'); + } + }, 0); + } + } + + ngOnInit(): void { + super.ngOnInit(); + this.initThumb(); + } + + /** + * 初始化缩略图相关变量 + */ + private initThumb(): void { + if (this.files && this.files.length > 0) { + this.currentPreviewIndex = 0; + // 计算总页数 + this.totalThumbPage = Math.ceil(this.files.length / TiProductpreviewComponent.ITEM_PER_PAGE); + this.currentThumbPage = 1; + if (this.files.length >= TiProductpreviewComponent.ITEM_PER_PAGE) { + this.thumbList = this.files.slice(0, TiProductpreviewComponent.ITEM_PER_PAGE); + } else { + this.thumbList = this.files; + } + } + } + + /** + * @ignore 主图查看 + * @param event: 事件 + * @param index: 当前选中项在this.thumbList中的索引,this.thumbList中始终只有4项 + */ + onMouseenterThumb(event: any, index: number): void { + this.currentPreviewIndex = (this.currentThumbPage - 1) * TiProductpreviewComponent.ITEM_PER_PAGE + index; + // 计算最后一页的缩略图个数 + const thumbItemNumber: number = this.files.length % TiProductpreviewComponent.ITEM_PER_PAGE; + // 如果是最后一页,且最后一页不满4个 + if (this.totalThumbPage > 1 && this.currentThumbPage === this.totalThumbPage && thumbItemNumber !== 0) { + this.currentPreviewIndex -= TiProductpreviewComponent.ITEM_PER_PAGE - thumbItemNumber; + } + + // 设置选中样式 + const thumbItemEles: any = this.productThumbEle.nativeElement.children; + for (const item of thumbItemEles) { + this.render.removeClass(item, 'ti-product-preview-thumb-item-active'); + } + this.render.addClass(event.target.parentElement, 'ti-product-preview-thumb-item-active'); + } + + /** + * @ignore 弹框中预览大图 + */ + onClickMain(event: any): void { + this.tiModal.open(TiImagepreviewComponent, { + id: this.id + '_productPreviewModal', + modalClass: 'ti-product-preview-modal', + context: { + index: this.currentPreviewIndex, // 当前文件索引 + fileList: this.files, // 预览列表 + id: this.id // 传递id + } + }); + } + + /** + * @ignore 缩略图上一页 + */ + prev(): void { + if (this.currentThumbPage === 1) { + return; + } + this.currentThumbPage--; + this.setThumbList(); + } + + /** + * @ignore 缩略图下一页 + */ + next(): void { + if (this.currentThumbPage === this.totalThumbPage) { + return; + } + this.currentThumbPage++; + this.setThumbList(); + } + + /** + * 设置缩略图列表 + */ + private setThumbList(): void { + // undefined、null、false、string情况下,不处理 + if (!this.files || typeof this.files === 'string' || (this.files && this.files.slice === undefined)) { + return; + } + // tslint:disable-next-line: max-line-length + this.thumbList = this.files.slice( + (this.currentThumbPage - 1) * TiProductpreviewComponent.ITEM_PER_PAGE, + this.currentThumbPage * TiProductpreviewComponent.ITEM_PER_PAGE + ); + + // 计算最后一页的缩略图个数 + const thumbItemNumber: number = this.files.length % TiProductpreviewComponent.ITEM_PER_PAGE; + if (this.totalThumbPage > 1 && this.currentThumbPage === this.totalThumbPage && thumbItemNumber !== 0) { + this.thumbList = this.files.slice(this.files.length - TiProductpreviewComponent.ITEM_PER_PAGE, this.files.length); + } + } +} diff --git a/src/productpreview/lib/src/TiProductpreviewModule.ts b/src/productpreview/lib/src/TiProductpreviewModule.ts new file mode 100644 index 0000000..1726b12 --- /dev/null +++ b/src/productpreview/lib/src/TiProductpreviewModule.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiImagepreviewModule } from '@opentiny/ng-imagepreview'; +import { TiModalModule } from '@opentiny/ng-modal'; +import { TiZoomModule } from '@opentiny/ng-zoom'; +import { TiOutlineModule } from '@opentiny/ng-outline'; + +import { TiProductpreviewComponent } from './TiProductpreviewComponent'; + +@NgModule({ + imports: [CommonModule, TiImagepreviewModule, TiIconModule, TiModalModule, TiZoomModule, TiOutlineModule], + exports: [TiProductpreviewComponent], + declarations: [TiProductpreviewComponent] +}) +export class TiProductpreviewModule {} +export { TiProductpreviewComponent } from './TiProductpreviewComponent'; diff --git a/src/productpreview/lib/src/productpreview.html b/src/productpreview/lib/src/productpreview.html new file mode 100644 index 0000000..9f29f60 --- /dev/null +++ b/src/productpreview/lib/src/productpreview.html @@ -0,0 +1,48 @@ +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    diff --git a/src/productpreview/lib/src/productpreview.less b/src/productpreview/lib/src/productpreview.less new file mode 100644 index 0000000..a6d023d --- /dev/null +++ b/src/productpreview/lib/src/productpreview.less @@ -0,0 +1,157 @@ +@import '../../../themes/basic/base-all.less'; + +::ng-deep :root { + --ti-product-preview-modal-width: 640px; + --ti-product-preview-main-small-size: 300px; + --ti-product-preview-main-middle-size: 380px; + --ti-product-preview-main-large-size: 450px; + --ti-product-preview-thumb-small-size: 49px; + --ti-product-preview-thumb-middle-size: 69px; + --ti-product-preview-thumb-large-size: 86.5px; + --ti-product-preview-thumb-page-size: 24px; + --ti-product-preview-thumb-page-font-size: 24px; + --ti-product-preview-thumb-page-border-radius: 2px; + --ti-product-preview-thumb-page-margin: 16px; + --ti-product-preview-thumb-page-button-size: calc( + var(--ti-product-preview-thumb-page-size) + var(--ti-product-preview-thumb-page-margin) + ); +} + +:host { + display: block; +} +.ti-product-preview-container { + width: 100%; + .ti-product-preview-main { + display: flex; + justify-content: center; + align-items: center; + border: 1px solid var(--ti-common-color-line-dividing); + box-sizing: border-box; + } + .ti-product-preview-thumb-container { + margin-top: var(--ti-common-space-3x); + display: flex; + justify-content: space-between; + align-items: center; + .ti-product-preview-thumb-wapper { + flex: 1; + overflow: hidden; + .ti-product-preview-thumb { + position: relative; + left: 0; + display: flex; + .ti-product-preview-thumb-item { + display: flex; + justify-content: center; + align-items: center; + flex-shrink: 0; + border: 1px solid transparent; + box-sizing: border-box; + img { + width: auto; + height: auto; + } + &:not(:last-child) { + margin-right: var(--ti-common-space-2x); + } + } + .ti-product-preview-thumb-item-active { + border: 1px solid var(--ti-common-color-line-active); + cursor: pointer; + } + } + } + .ti-product-preview-thumb-page { + width: var(--ti-product-preview-thumb-page-size); + height: var(--ti-product-preview-thumb-page-size); + border-radius: var(--ti-product-preview-thumb-page-border-radius); + position: relative; + &:hover:not(.ti-product-preview-thumb-page-disabled) { + cursor: pointer; + color: var(--ti-common-color-icon-hover); + background-color: var(--ti-common-color-bg-light-normal); + } + &:active:not(.ti-product-preview-thumb-page-disabled) { + color: var(--ti-common-bg-minor-active); + background-color: var(--ti-common-color-icon-active); + } + .ti-product-preview-icon { + line-height: var(--ti-product-preview-thumb-page-size); + font-size: var(--ti-product-preview-thumb-page-font-size); + } + } + .ti-product-preview-thumb-page-disabled { + cursor: not-allowed; + outline: none; + color: var(--ti-common-color-icon-disabled); + } + .ti-product-preview-thumb-left { + margin-right: var(--ti-product-preview-thumb-page-margin); + } + .ti-product-preview-thumb-right { + margin-left: var(--ti-product-preview-thumb-page-margin); + } + } +} + +.media(@mainSize, @thumbSize) { + :host { + width: @mainSize; + } + ::ng-deep .ti3-img-zoom-viewer { + width: @mainSize !important; + height: @mainSize !important; + } + .ti-product-preview-container { + .ti-product-preview-main { + height: @mainSize; + img { + max-width: calc(@mainSize - 2px); + max-height: calc(@mainSize - 2px); + } + ::ng-deep .ti3-img-zoom-selector { + width: calc(@mainSize / 2) !important; + height: calc(@mainSize / 2) !important; + } + } + .ti-product-preview-thumb-container { + .ti-product-preview-thumb-wapper { + .ti-product-preview-thumb { + .ti-product-preview-thumb-item { + width: @thumbSize; + height: @thumbSize; + img { + max-width: calc(@thumbSize - 2px); + max-height: calc(@thumbSize - 2px); + } + } + } + } + } + } +} + +// 10.0.4版本新增 +// 分辨率<1440px +@media screen and (max-width: 1440px) { + .media(var(--ti-product-preview-main-small-size), var(--ti-product-preview-thumb-small-size)); +} +// 1440<=分辨率<1600px +@media screen and (min-width: 1440px) and (max-width: 1600px) { + .media(var(--ti-product-preview-main-middle-size), var(--ti-product-preview-thumb-middle-size)); +} +// 分辨率>=1600px +@media screen and (min-width: 1600px) { + .media(var(--ti-product-preview-main-large-size), var(--ti-product-preview-thumb-large-size)); +} + +::ng-deep .ti-product-preview-modal { + width: var(--ti-product-preview-modal-width) !important; + .ti3-modal-close { + top: 0; + } + .ti3-image-preview-container { + margin-top: var(--ti-common-space-2x); + } +} diff --git a/src/progressbar/demo/karma.conf.js b/src/progressbar/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/progressbar/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/progressbar/demo/project.json b/src/progressbar/demo/project.json new file mode 100644 index 0000000..85c627a --- /dev/null +++ b/src/progressbar/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/progressbar/demo", + "sourceRoot": "src/progressbar/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/progressbar", + "index": "src/progressbar/demo/src/index.html", + "main": "src/progressbar/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/progressbar/demo/tsconfig.app.json", + "assets": ["src/progressbar/demo/src/favicon.ico", "src/progressbar/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "progressbar-demo:build:production" + }, + "development": { + "browserTarget": "progressbar-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js progressbar" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/progressbar/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/progressbar/demo/tsconfig.spec.json", + "karmaConfig": "src/progressbar/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/progressbar/demo/src/app/AppComponent.ts b/src/progressbar/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/progressbar/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/progressbar/demo/src/app/AppModule.ts b/src/progressbar/demo/src/app/AppModule.ts new file mode 100644 index 0000000..6c0ba5e --- /dev/null +++ b/src/progressbar/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ProgressbarTestModule } from './progressbar/ProgressbarTestModule'; + +@NgModule({ + imports: [ + ProgressbarTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/progressbar/demo/src/app/IndexComponent.ts b/src/progressbar/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..879ac68 --- /dev/null +++ b/src/progressbar/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ProgressbarTestModule } from './progressbar/ProgressbarTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = ProgressbarTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/progressbar/demo/src/app/app.html b/src/progressbar/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/progressbar/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/progressbar/demo/src/app/progressbar/ProgressbarAnimationComponent.ts b/src/progressbar/demo/src/app/progressbar/ProgressbarAnimationComponent.ts new file mode 100644 index 0000000..0f4fb25 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/ProgressbarAnimationComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +const STEP: number = 10; +@Component({ + templateUrl: 'progressbar-animation.html' +}) +export class ProgressbarAnimationComponent { + value: number = 40; + max: number = 200; + isAnimation: boolean = false; + up(): void { + if (this.value + STEP <= this.max) { + this.value += STEP; + } else { + this.value = this.max; + } + } + down(): void { + if (this.value - STEP > 0) { + this.value -= STEP; + } else { + this.value = 0; + } + } + switchState(): void { + this.isAnimation = !this.isAnimation; + } +} diff --git a/src/progressbar/demo/src/app/progressbar/ProgressbarBasicComponent.ts b/src/progressbar/demo/src/app/progressbar/ProgressbarBasicComponent.ts new file mode 100644 index 0000000..14d8c0d --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/ProgressbarBasicComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: 'progressbar-basic.html' +}) +export class ProgressbarBasicComponent { + value: number = 40; + value1: number = 200; + value2: number = 0; + max: number = 200; +} diff --git a/src/progressbar/demo/src/app/progressbar/ProgressbarClassComponent.ts b/src/progressbar/demo/src/app/progressbar/ProgressbarClassComponent.ts new file mode 100644 index 0000000..fb8c8c3 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/ProgressbarClassComponent.ts @@ -0,0 +1,32 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +const STEP: number = 10; + +@Component({ + templateUrl: './progressbar-class.html', + styleUrls: ['./progressbar-class.less'], + encapsulation: ViewEncapsulation.None +}) +export class ProgressbarClassComponent { + value: number = 80; + value1: number = 50; + max: number = 200; + up(): void { + if (this.value + STEP <= this.max) { + this.value += STEP; + } else { + this.value = this.max; + } + } + down(): void { + if (this.value - STEP > 0) { + this.value -= STEP; + } else { + this.value = 0; + } + } + ngOnInit(): void { + setTimeout(() => { + this.value1 += 100; + }, 500); + } +} diff --git a/src/progressbar/demo/src/app/progressbar/ProgressbarTestModule.ts b/src/progressbar/demo/src/app/progressbar/ProgressbarTestModule.ts new file mode 100644 index 0000000..7a6d19a --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/ProgressbarTestModule.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { TiButtonModule, TiProgressbarModule } from '@opentiny/ng'; +import { ProgressbarBasicComponent } from './ProgressbarBasicComponent'; +import { ProgressbarClassComponent } from './ProgressbarClassComponent'; +import { ProgressbarAnimationComponent } from './ProgressbarAnimationComponent'; + +@NgModule({ + imports: [CommonModule, TiProgressbarModule, TiButtonModule, RouterModule.forChild(ProgressbarTestModule.ROUTES)], + declarations: [ProgressbarBasicComponent, ProgressbarClassComponent, ProgressbarAnimationComponent] +}) +export class ProgressbarTestModule { + static readonly LINKS: Array = [{ href: 'components/TiProgressbarComponent.html', label: 'Progressbar' }]; + static readonly ROUTES: Routes = [ + { + path: 'progressbar/progressbar-basic', + component: ProgressbarBasicComponent, + data: { label: '基本使用' } + }, + { + path: 'progressbar/progressbar-class', + component: ProgressbarClassComponent, + data: { label: '样式设置' } + }, + { + path: 'progressbar/progressbar-animation', + component: ProgressbarAnimationComponent, + data: { label: '动效' } + } + ]; +} diff --git a/src/progressbar/demo/src/app/progressbar/progressbar-animation.html b/src/progressbar/demo/src/app/progressbar/progressbar-animation.html new file mode 100644 index 0000000..a867552 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/progressbar-animation.html @@ -0,0 +1,9 @@ +
    +
    + +
    + {{ 100 * value / max }}% +
    + + + diff --git a/src/progressbar/demo/src/app/progressbar/progressbar-basic.html b/src/progressbar/demo/src/app/progressbar/progressbar-basic.html new file mode 100644 index 0000000..740a101 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/progressbar-basic.html @@ -0,0 +1,18 @@ +
    +
    + +
    + {{ 100 * value / max }}% +
    +
    +
    + +
    + {{ 100 * value2 / max }}% +
    +
    +
    + +
    + {{ 100 * value1 / max }}% +
    diff --git a/src/progressbar/demo/src/app/progressbar/progressbar-class.html b/src/progressbar/demo/src/app/progressbar/progressbar-class.html new file mode 100644 index 0000000..c85d949 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/progressbar-class.html @@ -0,0 +1,16 @@ +
    +
    + + +
    + {{100 * value / max }}% +
    + + +
    +
    + + +
    + {{100 * value1 / max }}% +
    diff --git a/src/progressbar/demo/src/app/progressbar/progressbar-class.less b/src/progressbar/demo/src/app/progressbar/progressbar-class.less new file mode 100644 index 0000000..37302a5 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/progressbar-class.less @@ -0,0 +1,52 @@ +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + + to { + background-position: 0 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + + to { + background-position: 0 0; + } +} + +.container .inline-block .custom-progress-active-warn-bg { + background-image: linear-gradient( + 45deg, + #96adfa 25%, + transparent 25%, + transparent 50%, + #96adfa 50%, + #96adfa 75%, + transparent 75%, + transparent + ); + animation: progress-bar-stripes 1s linear infinite; + background-size: 40px 40px; + background-color: #7693f5; + -webkit-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.container .inline-block .custom-progress-warn-bg { + background-color: #f0ad4e; +} diff --git a/src/progressbar/demo/src/app/progressbar/webdoc/progressbar-demos.js b/src/progressbar/demo/src/app/progressbar/webdoc/progressbar-demos.js new file mode 100644 index 0000000..461a589 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/webdoc/progressbar-demos.js @@ -0,0 +1,48 @@ +export default { + column: '2', + demos: [ + { + demoId: 'progressbar-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    Progressbar 组件的最简用法。通过属性max配置边界值。通过属性value配置当前进度值。

    ', + 'en-US': + '

    Progressbar 组件的最简用法。通过属性max配置边界值。通过属性value配置当前进度值。

    ', + }, + apis: [ + 'TiProgressbarComponent.properties.max', + 'TiProgressbarComponent.properties.value', + ], + }, + { + demoId: 'progressbar-class', + name: { + 'zh-CN': '样式设置', + 'en-US': 'Class', + }, + desc: { + 'zh-CN': '

    通过属性progressClass配置进度条样式。

    ', + 'en-US': '

    通过属性progressClass配置进度条样式。

    ', + }, + apis: ['TiProgressbarComponent.properties.progressClass'], + }, + { + demoId: 'progressbar-animation', + name: { + 'zh-CN': '动画', + 'en-US': 'Animation', + }, + desc: { + 'zh-CN': + '

    通过属性animation配置是否打开动画效果,默认开启。

    ', + 'en-US': + '

    通过属性animation配置是否打开动画效果,默认开启。

    ', + }, + apis: ['TiProgressbarComponent.properties.animation'], + }, + ], +}; diff --git a/src/progressbar/demo/src/app/progressbar/webdoc/progressbar.cn.md b/src/progressbar/demo/src/app/progressbar/webdoc/progressbar.cn.md new file mode 100644 index 0000000..57416b9 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/webdoc/progressbar.cn.md @@ -0,0 +1,24 @@ +--- +title: Progressbar 进度条 +--- +# Progressbar 进度条 + +
    + +Progressbar 是展示当前进度的组件。 + +```typescript +import { TiProgressbarModule } from '@opentiny/ng'; +``` + +
    + +
    + +Progressbar 是展示当前进度的组件。 + +```typescript +import { TiProgressbarModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/progressbar/demo/src/app/progressbar/webdoc/progressbar.en.md b/src/progressbar/demo/src/app/progressbar/webdoc/progressbar.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/webdoc/progressbar.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/progressbar/demo/src/favicon.ico b/src/progressbar/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/progressbar/demo/src/index.html b/src/progressbar/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/progressbar/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/progressbar/demo/src/main.ts b/src/progressbar/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/progressbar/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/progressbar/demo/test.ts b/src/progressbar/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/progressbar/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/progressbar/demo/tsconfig.app.json b/src/progressbar/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/progressbar/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/progressbar/demo/tsconfig.spec.json b/src/progressbar/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/progressbar/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/progressbar/lib/index.ts b/src/progressbar/lib/index.ts new file mode 100644 index 0000000..2c297a9 --- /dev/null +++ b/src/progressbar/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiProgressbarModule'; diff --git a/src/progressbar/lib/ng-package.json b/src/progressbar/lib/ng-package.json new file mode 100644 index 0000000..ccd2053 --- /dev/null +++ b/src/progressbar/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/progressbar", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/progressbar/lib/package.json b/src/progressbar/lib/package.json new file mode 100644 index 0000000..fd256b0 --- /dev/null +++ b/src/progressbar/lib/package.json @@ -0,0 +1,10 @@ +{ + "name": "@opentiny/ng-progressbar", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/progressbar/lib/project.json b/src/progressbar/lib/project.json new file mode 100644 index 0000000..be0dfb4 --- /dev/null +++ b/src/progressbar/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/progressbar/lib", + "sourceRoot": "src/progressbar/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/progressbar"], + "options": { + "project": "src/progressbar/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/progressbar"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js progressbar" + }, + { + "command": "ng default-build progressbar" + }, + { + "command": "node build/clear-default-theme.js progressbar" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/progressbar && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build progressbar && ng pack progressbar && node build/publish.js progressbar --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/progressbar/lib/src/TiProgressbarComponent.ts b/src/progressbar/lib/src/TiProgressbarComponent.ts new file mode 100644 index 0000000..0400cb4 --- /dev/null +++ b/src/progressbar/lib/src/TiProgressbarComponent.ts @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, SimpleChanges, ChangeDetectionStrategy } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * 进度条组件 + * + */ +@Component({ + selector: 'ti-progressbar', + templateUrl: './progressbar.html', + styleUrls: ['./progressbar.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-progress]': 'true' + } +}) +export class TiProgressbarComponent extends TiBaseComponent { + /** + * 当前进度值 + */ + @Input() value: number = 0; + /** + * 进度目标值 + */ + @Input() max: number = 100; + /** + * 进度条样式 + */ + @Input() progressClass: string = ''; + /** + * 进度变化时是否开启动画效果过渡 + */ + @Input() animation: boolean = true; + /** + * @ignore + */ + public percent: number; // 当前进度值 + protected versionInfo: string = super.getVersion(packageInfo); + // 设置合法的数值 + private static verifyNum(newVal: any, defaultValue: any): number { + return isNaN(parseFloat(newVal)) ? defaultValue : newVal; + } + ngOnInit(): void { + super.ngOnInit(); + this.max = TiProgressbarComponent.verifyNum(this.max, this.max); + this.value = TiProgressbarComponent.verifyNum(this.value, this.value); + this.calcPercentage(); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + // 当前值更新,重新计算进度 + this.setChanges(changes, 'value'); + // 当最大值更新,重新计算进度 + this.setChanges(changes, 'max'); + } + + /** + * @description 处理用户输入的改变 + * @param: changes 改变的对象 + * @param: changeKey 那个属性改变 + */ + private setChanges(changes: SimpleChanges, changeKey: string): void { + if (changes[changeKey] && !changes[changeKey].isFirstChange()) { + const oldVal: any = changes[changeKey].previousValue; + const newVal: any = changes[changeKey].currentValue; + const _newValue: number = TiProgressbarComponent.verifyNum(newVal, oldVal); + this[changeKey] = _newValue; + this.calcPercentage(); + } + } + + // 计算当前进度百分比 + private calcPercentage(): void { + // 如果开发者设置数据不合理,则进度置0 + if (this.max === 0) { + this.percent = 0; + + return; + } + this.percent = Number(((this.value * 100) / this.max).toFixed(2)); + if (this.percent > 100) { + this.percent = 100; + } + if (this.percent < 0) { + this.percent = 0; + } + } +} diff --git a/src/progressbar/lib/src/TiProgressbarModule.ts b/src/progressbar/lib/src/TiProgressbarModule.ts new file mode 100644 index 0000000..deabed9 --- /dev/null +++ b/src/progressbar/lib/src/TiProgressbarModule.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiProgressbarComponent } from './TiProgressbarComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [TiProgressbarComponent], + declarations: [TiProgressbarComponent] +}) +export class TiProgressbarModule {} +export { TiProgressbarComponent } from './TiProgressbarComponent'; diff --git a/src/progressbar/lib/src/progressbar.html b/src/progressbar/lib/src/progressbar.html new file mode 100644 index 0000000..76de30a --- /dev/null +++ b/src/progressbar/lib/src/progressbar.html @@ -0,0 +1,9 @@ +
    +
    + +
    diff --git a/src/progressbar/lib/src/progressbar.less b/src/progressbar/lib/src/progressbar.less new file mode 100644 index 0000000..ed6f49b --- /dev/null +++ b/src/progressbar/lib/src/progressbar.less @@ -0,0 +1,54 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-progressbar-height: 5px; +} +:host.ti3-progress { + .box-sizing(border-box); + display: inline-block; + position: relative; + background-color: var(--ti-common-color-line-dividing); + width: 100%; + vertical-align: middle; + overflow: visible; + height: var(--ti-progressbar-height); + margin-bottom: 0; +} + +.ti3-progress-bar { + .box-sizing(border-box); + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 0; + background-color: var(--ti-common-color-bg-emphasize); + .transition(width 0.6s ease); +} + +.ti3-progress-label { + display: none; + .box-sizing(border-box); + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 100%; + background: transparent; + white-space: nowrap; + color: var(--ti-common-color-text-white); + text-align: center; + vertical-align: middle; + &:after { + display: inline-block; + width: 0; + height: 100%; + vertical-align: middle; + content: ''; + } +} + +.ti3-progress-bar.ti3-progress-no-animation { + transition: none !important; + animation: none !important; +} diff --git a/src/progresspie/demo/project.json b/src/progresspie/demo/project.json new file mode 100644 index 0000000..eefe26e --- /dev/null +++ b/src/progresspie/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/progresspie/demo", + "sourceRoot": "src/progresspie/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/progresspie", + "index": "src/progresspie/demo/src/index.html", + "main": "src/progresspie/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/progresspie/demo/tsconfig.app.json", + "assets": ["src/progresspie/demo/src/favicon.ico", "src/progresspie/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "progresspie-demo:build:production" + }, + "development": { + "browserTarget": "progresspie-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js progresspie" + } + ] + } + } + } +} diff --git a/src/progresspie/demo/src/app/AppComponent.ts b/src/progresspie/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/progresspie/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/progresspie/demo/src/app/AppModule.ts b/src/progresspie/demo/src/app/AppModule.ts new file mode 100644 index 0000000..aa14b4e --- /dev/null +++ b/src/progresspie/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ProgresspieTestModule } from './progresspie/ProgresspieTestModule'; + +@NgModule({ + imports: [ + ProgresspieTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/progresspie/demo/src/app/IndexComponent.ts b/src/progresspie/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..5471f85 --- /dev/null +++ b/src/progresspie/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ProgresspieTestModule } from './progresspie/ProgresspieTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = ProgresspieTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/progresspie/demo/src/app/app.html b/src/progresspie/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/progresspie/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/progresspie/demo/src/app/progresspie/ProgresspieTestComponent.ts b/src/progresspie/demo/src/app/progresspie/ProgresspieTestComponent.ts new file mode 100644 index 0000000..8fd3618 --- /dev/null +++ b/src/progresspie/demo/src/app/progresspie/ProgresspieTestComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; + +@Component({ + template: ` +

    1. 不设置value和maxValue,均使用默认值

    + +

    2. 不设置maxValue,maxValue使用默认值;value设置为20

    + +

    3. 同时设置value(10)和maxValue(200)

    + + + + + + ` +}) +export class ProgresspieTestComponent { + value1: number; + value2: number = 20; + value3: number = 10; + maxValue3: number = 200; + valueChanged: string; + changeValue(valueChanged: string, valueIndex: string): void { + this[valueIndex] = parseInt(valueChanged, 10); + } +} diff --git a/src/progresspie/demo/src/app/progresspie/ProgresspieTestModule.ts b/src/progresspie/demo/src/app/progresspie/ProgresspieTestModule.ts new file mode 100644 index 0000000..ea0b9cc --- /dev/null +++ b/src/progresspie/demo/src/app/progresspie/ProgresspieTestModule.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiProgresspieModule } from '@opentiny/ng'; + +import { ProgresspieTestComponent } from './ProgresspieTestComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiProgresspieModule, RouterModule.forChild(ProgresspieTestModule.ROUTES)], + declarations: [ProgresspieTestComponent] +}) +export class ProgresspieTestModule { + static readonly LINKS: Array = [{ href: 'components/TiProgresspieComponent.html', label: 'Progresspie' }]; + static readonly ROUTES: Routes = [ + { + path: 'progresspie/progresspie-basic', + component: ProgresspieTestComponent + } + ]; +} diff --git a/src/progresspie/demo/src/favicon.ico b/src/progresspie/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/progresspie/demo/src/index.html b/src/progresspie/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/progresspie/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/progresspie/demo/src/main.ts b/src/progresspie/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/progresspie/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/progresspie/demo/tsconfig.app.json b/src/progresspie/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/progresspie/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/progresspie/lib/index.ts b/src/progresspie/lib/index.ts new file mode 100644 index 0000000..1c04e93 --- /dev/null +++ b/src/progresspie/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiProgresspieModule'; diff --git a/src/progresspie/lib/ng-package.json b/src/progresspie/lib/ng-package.json new file mode 100644 index 0000000..e9da007 --- /dev/null +++ b/src/progresspie/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/progresspie", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/progresspie/lib/package.json b/src/progresspie/lib/package.json new file mode 100644 index 0000000..06e3288 --- /dev/null +++ b/src/progresspie/lib/package.json @@ -0,0 +1,8 @@ +{ + "name": "@opentiny/ng-progresspie", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/progresspie/lib/project.json b/src/progresspie/lib/project.json new file mode 100644 index 0000000..88e228a --- /dev/null +++ b/src/progresspie/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/progresspie/lib", + "sourceRoot": "src/progresspie/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/progresspie"], + "options": { + "project": "src/progresspie/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/progresspie"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js progresspie" + }, + { + "command": "ng default-build progresspie" + }, + { + "command": "node build/clear-default-theme.js progresspie" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/progresspie && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build progresspie && ng pack progresspie && node build/publish.js progresspie --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/progresspie/lib/src/TiProgresspieComponent.ts b/src/progresspie/lib/src/TiProgresspieComponent.ts new file mode 100644 index 0000000..e84203b --- /dev/null +++ b/src/progresspie/lib/src/TiProgresspieComponent.ts @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterViewInit, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild, ChangeDetectionStrategy } from '@angular/core'; +/** + * @ignore + */ +@Component({ + selector: 'ti-progresspie', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiProgresspieComponent implements OnChanges, AfterViewInit { + private defaultConfig: any = { + value: 0, + maxValue: 100, + color: '#3dcca6', + lineWidth: 2 + }; + @Input() value: number = this.defaultConfig.value; + @Input() maxValue: number = this.defaultConfig.maxValue; + private percent: number; // 计算后的百分比 + @ViewChild('canvas', { static: true }) private canvasEle: ElementRef; // canvas元素对应的ElementRef + private canvasElement: any; // canvas元素对应的nativeElement + nativeElement: Element; // 元素本身 + constructor(private hostEle: ElementRef) { + this.nativeElement = this.hostEle.nativeElement; + } + ngOnChanges(changes: SimpleChanges): void { + // value和maxValue的动态修改均需要重绘进度呈现 + if ((changes.value && !changes.value.isFirstChange()) || (changes.maxValue && !changes.maxValue.isFirstChange())) { + this.draw(); + } + } + ngAfterViewInit(): void { + // 初始化走一次圆饼的绘制,在此处处理是因为canvas对象此处是通过ref方式获取canvas的最早时机 + this.canvasElement = this.canvasEle.nativeElement; + // 修复SSR错误:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle === 'undefined') { + return; + } + this.canvasElement.width = parseFloat(getComputedStyle(this.nativeElement).width); + this.canvasElement.height = parseFloat(getComputedStyle(this.nativeElement).height); + this.draw(); + } + private draw(): void { + this.calcPercent(); + this.drawProgressPie(); + } + // 计算百分比,外部需要保证maxVlue和value均为数字类型 + private calcPercent(): void { + // 计算percent值 + if (this.maxValue === 0) { + this.percent = 0; + + return; + } + + let percent: number = this.value / this.maxValue; + if (isNaN(percent)) { + this.percent = 0; + + return; + } + + if (percent > 1) { + percent = 1; + } + + if (percent < 0) { + percent = 0; + } + this.percent = percent; + } + + private drawProgressPie(): void { + const canvas: any = this.canvasElement; + const ctx: any = canvas.getContext('2d'); + + // 清除先前画布内容 + const width: number = canvas.width; + const height: number = canvas.height; + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // 圆半径及起始点计算 + const d: number = Math.min(width, height); + const cx: number = width / 2; + const cy: number = height / 2; + const lineWidth: number = this.defaultConfig.lineWidth; + const r: number = d / 2 - lineWidth; + const startPoint: number = -Math.PI / 2; + // 笔触样式设置 + const color: string = this.defaultConfig.color; + ctx.fillStyle = color; + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + // 画外圆 + ctx.beginPath(); + ctx.arc(cx, cy, r, startPoint, startPoint + Math.PI * 2); + ctx.stroke(); + + // 画扇形 + const endPoint: number = startPoint + Math.PI * this.percent * 2; + ctx.beginPath(); + ctx.moveTo(cx, cy); // 移动至圆心 + ctx.arc(cx, cy, r, startPoint, endPoint); // 从外边上的点画曲线 + ctx.lineTo(cx, cy); // 从圆心画直线到计算好的圆外上的点 + ctx.fill(); + if (this.percent === 0) { + // 为0的情况下,绘制圆心值边缘竖线 + ctx.stroke(); + } + ctx.closePath(); + } +} diff --git a/src/progresspie/lib/src/TiProgresspieModule.ts b/src/progresspie/lib/src/TiProgresspieModule.ts new file mode 100644 index 0000000..5cccbc4 --- /dev/null +++ b/src/progresspie/lib/src/TiProgresspieModule.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiProgresspieComponent } from './TiProgresspieComponent'; +/** + * @ignore + */ +@NgModule({ + exports: [TiProgresspieComponent], + declarations: [TiProgresspieComponent] +}) +export class TiProgresspieModule {} +export { TiProgresspieComponent } from './TiProgresspieComponent'; diff --git a/src/radio/demo/karma.conf.js b/src/radio/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/radio/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/radio/demo/project.json b/src/radio/demo/project.json new file mode 100644 index 0000000..1b66def --- /dev/null +++ b/src/radio/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/radio/demo", + "sourceRoot": "src/radio/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/radio", + "index": "src/radio/demo/src/index.html", + "main": "src/radio/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/radio/demo/tsconfig.app.json", + "assets": ["src/radio/demo/src/favicon.ico", "src/radio/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "radio-demo:build:production" + }, + "development": { + "browserTarget": "radio-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js radio" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/radio/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/radio/demo/tsconfig.spec.json", + "karmaConfig": "src/radio/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/radio/demo/src/app/AppComponent.ts b/src/radio/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/radio/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/radio/demo/src/app/AppModule.ts b/src/radio/demo/src/app/AppModule.ts new file mode 100644 index 0000000..808183a --- /dev/null +++ b/src/radio/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { RadioTestModule } from './radio/RadioTestModule'; + +@NgModule({ + imports: [ + RadioTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/radio/demo/src/app/IndexComponent.ts b/src/radio/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..4d2e98c --- /dev/null +++ b/src/radio/demo/src/app/IndexComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; +import { RadioTestModule } from './radio/RadioTestModule'; + +@Component({ + template: ` + + + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + moudles: Array = [RadioTestModule.ROUTES]; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/radio/demo/src/app/app.html b/src/radio/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/radio/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/radio/demo/src/app/radio/RadioBasicComponent.ts b/src/radio/demo/src/app/radio/RadioBasicComponent.ts new file mode 100644 index 0000000..50815b8 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './radio-basic.html' +}) +export class RadioBasicComponent { + selected: string = 'BuyNow'; +} diff --git a/src/radio/demo/src/app/radio/RadioDarkComponent.ts b/src/radio/demo/src/app/radio/RadioDarkComponent.ts new file mode 100644 index 0000000..1e73111 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioDarkComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './radio-dark.html' +}) +export class RadioDarkComponent { + selected: string = 'option1'; +} diff --git a/src/radio/demo/src/app/radio/RadioDisabledComponent.ts b/src/radio/demo/src/app/radio/RadioDisabledComponent.ts new file mode 100644 index 0000000..de2f8a1 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioDisabledComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-disabled.html' +}) +export class RadioDisabledComponent { + countries: Array<{ key: string; id: string; disable?: boolean }> = [ + { + key: '中国', + id: 'China' + }, + { + key: '美国', + id: 'America', + disable: true + }, + { + key: '英国', + id: 'England', + disable: false + } + ]; + + radioList: Array = [ + { + id: '1', + label: '中国', + value: 'China', + disabled: true + }, + { + id: '2', + label: '美国', + value: 'America', + disabled: false + }, + { + id: '3', + label: '英国', + value: 'Britain' + } + ]; + + selectedGroup: string = 'China'; + disabled: boolean = true; + selected: string = 'beijing'; + selectedCountry: string; +} diff --git a/src/radio/demo/src/app/radio/RadioEventComponent.ts b/src/radio/demo/src/app/radio/RadioEventComponent.ts new file mode 100644 index 0000000..7e63046 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioEventComponent.ts @@ -0,0 +1,61 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-event.html' +}) +export class RadioEventComponent { + myLogs: Array = []; + + season: string = ''; + seasons: Array = [ + { + value: 'spring', + label: '春天' + }, + { + value: 'summer', + label: '夏天' + }, + { + value: 'autumn', + label: '秋天' + }, + { + value: 'winter', + label: '冬天' + } + ]; + + radioList: Array = [ + { + id: '1', + label: '中国', + value: 'China' + }, + { + id: '2', + label: '美国', + value: 'America' + }, + { + id: '3', + label: '英国', + value: 'Britain' + }, + { + id: '4', + label: '加拿大', + value: 'Canada' + } + ]; + selected: string = 'China'; + + onNgModelChange(model: any): void { + this.myLogs = [...this.myLogs, `ngModelChange:${JSON.stringify(model)}`]; + } + + onNgModelChange1(model: any): void { + this.myLogs = [...this.myLogs, `ngModelChange1:${JSON.stringify(model)}`]; + } +} diff --git a/src/radio/demo/src/app/radio/RadioFocusComponent.ts b/src/radio/demo/src/app/radio/RadioFocusComponent.ts new file mode 100644 index 0000000..e5ded08 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioFocusComponent.ts @@ -0,0 +1,34 @@ +import { Component, ViewChild } from '@angular/core'; + +@Component({ + templateUrl: './radio-focus.html' +}) +export class RadioFocusComponent { + selected1: string = 'option1'; + selected2: string = 'option2'; + + myLogs: Array = []; + radioList: Array = [ + { + value: 'option1', + label: '选项1' + }, + { + value: 'option2', + label: '选项2' + }, + { + value: 'option3', + label: '选项3' + }, + { + value: 'option4', + label: '选项4' + } + ]; + @ViewChild('input') input: Element; + + onFocus(index: number): void { + this.myLogs = [...this.myLogs, `focus:第${index + 1}个单选框`]; + } +} diff --git a/src/radio/demo/src/app/radio/RadioGroupComponent.ts b/src/radio/demo/src/app/radio/RadioGroupComponent.ts new file mode 100644 index 0000000..0599de2 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioGroupComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-group.html' +}) +export class RadioGroupComponent { + radioList: Array = [ + { + id: '1', + label: '中国', + value: 'China' + }, + { + id: '2', + label: '美国', + value: 'America' + }, + { + id: '3', + label: '英国', + value: 'Britain' + } + ]; + selected: string = 'China'; +} diff --git a/src/radio/demo/src/app/radio/RadioGroupDirectionComponent.ts b/src/radio/demo/src/app/radio/RadioGroupDirectionComponent.ts new file mode 100644 index 0000000..22f0b0d --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioGroupDirectionComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-group-direction.html' +}) +export class RadioGroupDirectionComponent { + radioList: Array = [ + { + id: '1', + label: '中国', + value: 'China' + }, + { + id: '2', + label: '美国', + value: 'America' + }, + { + id: '3', + label: '英国', + value: 'Britain' + } + ]; + selected: string = 'America'; +} diff --git a/src/radio/demo/src/app/radio/RadioGroupLabelkeyComponent.ts b/src/radio/demo/src/app/radio/RadioGroupLabelkeyComponent.ts new file mode 100644 index 0000000..4c5ede2 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioGroupLabelkeyComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-group-labelkey.html' +}) +export class RadioGroupLabelkeyComponent { + radioList: Array = [ + { + id: '1', + label: '中国', + value: 'China' + }, + { + id: '2', + label: '美国', + value: 'America' + }, + { + id: '3', + label: '英国', + value: 'Britain' + } + ]; + selected: string = 'China'; +} diff --git a/src/radio/demo/src/app/radio/RadioGroupLinewrapComponent.ts b/src/radio/demo/src/app/radio/RadioGroupLinewrapComponent.ts new file mode 100644 index 0000000..b4570f7 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioGroupLinewrapComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-group-linewrap.html' +}) +export class RadioGroupLinewrapComponent { + manyRadios: Array = [ + { id: '1', label: '小猪佩琪' }, + { id: '2', label: '小红帽' }, + { id: '3', label: '花木兰' }, + { id: '4', label: '雅典娜' }, + { id: '5', label: '舒克' }, + { id: '6', label: '机器猫' }, + { id: '7', label: '灰姑娘' }, + { id: '8', label: '天线宝宝' }, + { id: '9', label: '葫芦娃' }, + { id: '10', label: '孙悟空' } + ]; + selected: string = ''; +} diff --git a/src/radio/demo/src/app/radio/RadioGroupValidationComponent.ts b/src/radio/demo/src/app/radio/RadioGroupValidationComponent.ts new file mode 100644 index 0000000..cf7ea94 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioGroupValidationComponent.ts @@ -0,0 +1,72 @@ +import { Component, ElementRef } from '@angular/core'; +import { TiRadioItem, TiValidationConfig, TiValidators } from '@opentiny/ng'; +import { FormBuilder, FormControl, FormGroup, ValidationErrors } from '@angular/forms'; + +@Component({ + templateUrl: './radio-group-validation.html' +}) +export class RadioGroupValidationComponent { + radioList: Array = [ + { + id: '1', + label: '中国', + value: 'China' + }, + { + id: '2', + label: '美国', + value: 'America' + }, + { + id: '3', + label: '英国', + value: 'Britain' + } + ]; + radioList1: Array = [ + { + id: '1', + label: '中国', + value: 'China' + }, + { + id: '2', + label: '美国', + value: 'America' + }, + { + id: '3', + label: '英国', + value: 'Britain' + } + ]; + + selected: string = ''; + validationConfig: TiValidationConfig = { + errorMessage: { + required: '请至少选择一项' + } + }; + validationConfig1: TiValidationConfig = { + errorMessage: { + required: '请至少选择一项' + } + }; + + myFormGroup: FormGroup; + constructor(fb: FormBuilder, private elementRef: ElementRef) { + this.myFormGroup = fb.group({ + formradiogroup: new FormControl(undefined, [TiValidators.required]) + }); + } + + checkgroup(form: FormGroup): void { + const errors: ValidationErrors | null = TiValidators.check(form); + console.log('errors', errors); + } + + checkgroup1(): void { + const errors: ValidationErrors | null = TiValidators.check(this.myFormGroup); + console.log(errors); + } +} diff --git a/src/radio/demo/src/app/radio/RadioGroupValuekeyComponent.ts b/src/radio/demo/src/app/radio/RadioGroupValuekeyComponent.ts new file mode 100644 index 0000000..3acb893 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioGroupValuekeyComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-group-valuekey.html' +}) +export class RadioGroupValuekeyComponent { + radioList: Array = [ + { + id: '1', + label: '中国', + value: 'China' + }, + { + id: '2', + label: '美国', + value: 'America' + }, + { + id: '3', + label: '英国', + value: 'Britain' + } + ]; + selected: string = '中国'; +} diff --git a/src/radio/demo/src/app/radio/RadioLabelComponent.ts b/src/radio/demo/src/app/radio/RadioLabelComponent.ts new file mode 100644 index 0000000..b5d1578 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioLabelComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-label.html' +}) +export class RadioLabelComponent { + fruit: string = ''; + manyRadios: Array = [ + { id: '1', label: '小猪佩奇' }, + { id: '2', label: '小红帽' }, + { id: '3', label: '花木兰' } + ]; + selected: string = ''; +} diff --git a/src/radio/demo/src/app/radio/RadioTestModule.ts b/src/radio/demo/src/app/radio/RadioTestModule.ts new file mode 100644 index 0000000..fa2631e --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioTestModule.ts @@ -0,0 +1,101 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiButtonModule, TiIconModule, TiRadioModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { RadioBasicComponent } from './RadioBasicComponent'; +import { RadioLabelComponent } from './RadioLabelComponent'; +import { RadioDisabledComponent } from './RadioDisabledComponent'; +import { RadioEventComponent } from './RadioEventComponent'; +import { RadioFocusComponent } from './RadioFocusComponent'; +import { RadioDarkComponent } from './RadioDarkComponent'; +import { RadioGroupComponent } from './RadioGroupComponent'; +import { RadioGroupDirectionComponent } from './RadioGroupDirectionComponent'; +import { RadioGroupLabelkeyComponent } from './RadioGroupLabelkeyComponent'; +import { RadioGroupValuekeyComponent } from './RadioGroupValuekeyComponent'; +import { RadioGroupValidationComponent } from './RadioGroupValidationComponent'; +import { RadioGroupLinewrapComponent } from './RadioGroupLinewrapComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiButtonModule, + TiRadioModule, + TiIconModule, + TiValidationModule, + DemoLogModule, + RouterModule.forChild(RadioTestModule.ROUTES) + ], + declarations: [ + RadioBasicComponent, + RadioDisabledComponent, + RadioEventComponent, + RadioFocusComponent, + RadioDarkComponent, + RadioLabelComponent, + RadioGroupComponent, + RadioGroupDirectionComponent, + RadioGroupLabelkeyComponent, + RadioGroupValuekeyComponent, + RadioGroupValidationComponent, + RadioGroupLinewrapComponent + ] +}) +export class RadioTestModule { + static readonly LINKS: Array = [{ href: 'components/TiRadioComponent.html', label: 'Radio' }]; + static readonly ROUTES: Routes = [ + { + path: 'radio/radio-basic', + component: RadioBasicComponent + }, + { + path: 'radio/radio-disabled', + component: RadioDisabledComponent + }, + { + path: 'radio/radio-label', + component: RadioLabelComponent + }, + { + path: 'radio/radio-event', + component: RadioEventComponent + }, + { + path: 'radio/radio-focus', + component: RadioFocusComponent + }, + { + path: 'radio/radio-dark', + component: RadioDarkComponent + }, + { + path: 'radio/radio-group', + component: RadioGroupComponent + }, + { + path: 'radio/radio-group-direction', + component: RadioGroupDirectionComponent + }, + { + path: 'radio/radio-group-labelkey', + component: RadioGroupLabelkeyComponent + }, + { + path: 'radio/radio-group-valuekey', + component: RadioGroupValuekeyComponent + }, + { + path: 'radio/radio-group-validation', + component: RadioGroupValidationComponent + }, + { + path: 'radio/radio-group-linewrap', + component: RadioGroupLinewrapComponent + } + ]; +} diff --git a/src/radio/demo/src/app/radio/radio-basic.html b/src/radio/demo/src/app/radio/radio-basic.html new file mode 100644 index 0000000..f4097a0 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-basic.html @@ -0,0 +1,10 @@ + + + +
    +
    Current value: {{ selected | json }}
    +
    diff --git a/src/radio/demo/src/app/radio/radio-dark.html b/src/radio/demo/src/app/radio/radio-dark.html new file mode 100644 index 0000000..85bdc65 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-dark.html @@ -0,0 +1,12 @@ +

    1.描述

    +

    深色背景

    +

    2.示例

    +
    +

    +

    +

    + +
    +
    +
    +选中项:{{selected}} diff --git a/src/radio/demo/src/app/radio/radio-disabled.html b/src/radio/demo/src/app/radio/radio-disabled.html new file mode 100644 index 0000000..2821d88 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-disabled.html @@ -0,0 +1,43 @@ +

    1.禁用某个单选框

    + + + + +
    +

    2.在form表单中禁用某个单选框

    +
    + + + +
    +

    3.单选组禁用某个选项

    + diff --git a/src/radio/demo/src/app/radio/radio-event.html b/src/radio/demo/src/app/radio/radio-event.html new file mode 100644 index 0000000..f142e52 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-event.html @@ -0,0 +1,23 @@ + + + +
    + + + diff --git a/src/radio/demo/src/app/radio/radio-focus.html b/src/radio/demo/src/app/radio/radio-focus.html new file mode 100644 index 0000000..7e06577 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-focus.html @@ -0,0 +1,39 @@ +

    1.描述

    +

    单选框焦点功能测试

    +

    2.示例

    +

    2.1 可以通过组件或者原生元素获取焦点

    +

    2.2 tab键可以切换焦点

    +

    单选组1:

    +
    +
    +
    +选中项:{{selected1}}
    +
    +

    单选组2:

    + +tabindex="1" + +tabindex="4" + +tabindex="2" + +tabindex="3"
    +选中项:{{selected2}}
    + +  + +  + +  + +

    事件日志:

    + diff --git a/src/radio/demo/src/app/radio/radio-group-direction.html b/src/radio/demo/src/app/radio/radio-group-direction.html new file mode 100644 index 0000000..7cad6d6 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-group-direction.html @@ -0,0 +1,4 @@ + +
    +
    Current value: {{ selected }}
    +
    diff --git a/src/radio/demo/src/app/radio/radio-group-labelkey.html b/src/radio/demo/src/app/radio/radio-group-labelkey.html new file mode 100644 index 0000000..bc2a2ab --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-group-labelkey.html @@ -0,0 +1,4 @@ + +
    +
    Current value: {{ selected }}
    +
    diff --git a/src/radio/demo/src/app/radio/radio-group-linewrap.html b/src/radio/demo/src/app/radio/radio-group-linewrap.html new file mode 100644 index 0000000..bddb7a8 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-group-linewrap.html @@ -0,0 +1 @@ + diff --git a/src/radio/demo/src/app/radio/radio-group-validation.html b/src/radio/demo/src/app/radio/radio-group-validation.html new file mode 100644 index 0000000..1e4983b --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-group-validation.html @@ -0,0 +1,25 @@ +

    1.模板式表单

    +
    + + +
    + +
    +

    2.响应式表单

    +
    + +
    + +
    diff --git a/src/radio/demo/src/app/radio/radio-group-valuekey.html b/src/radio/demo/src/app/radio/radio-group-valuekey.html new file mode 100644 index 0000000..3c4ab22 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-group-valuekey.html @@ -0,0 +1,4 @@ + +
    +
    Current value: {{ selected }}
    +
    diff --git a/src/radio/demo/src/app/radio/radio-group.html b/src/radio/demo/src/app/radio/radio-group.html new file mode 100644 index 0000000..3972d7e --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-group.html @@ -0,0 +1,4 @@ + +
    +
    Current value: {{ selected }}
    +
    diff --git a/src/radio/demo/src/app/radio/radio-label.html b/src/radio/demo/src/app/radio/radio-label.html new file mode 100644 index 0000000..a9f330a --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-label.html @@ -0,0 +1,40 @@ + + + + + + +
    + + + + + + + diff --git a/src/radio/demo/src/app/radio/webdoc/radio-demos.js b/src/radio/demo/src/app/radio/webdoc/radio-demos.js new file mode 100644 index 0000000..edf2ff0 --- /dev/null +++ b/src/radio/demo/src/app/radio/webdoc/radio-demos.js @@ -0,0 +1,134 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'radio-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': + '

    Radio 组件的最简用法。一组单选框需要使用相同的name属性且 ngModel 绑定同一变量,才能实现单选效果。value属性表示该选项的值,当 ngModel 和某项的value值相等时,表示该项选中。

    ', + 'en-US': '', + }, + apis: ['TiRadioComponent.properties.label'], + }, + { + demoId: 'radio-group', + name: { + 'zh-CN': '单选组', + 'en-US': 'group', + }, + desc: { + 'zh-CN': '

    通过属性items配置radio-group所有选项的数据集合,各选项互斥。

    ', + 'en-US': '', + }, + apis: [ + 'TiRadioGroupComponent.properties.items', + 'TiRadioItem.properties.value' + ], + }, + { + demoId: 'radio-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置禁用状态;通过属性item.disabled配置radio-group选项禁用状态。

    ', + 'en-US': '', + }, + apis: [ + 'TiRadioComponent.properties.disabled', + ], + }, + { + demoId: 'radio-label', + name: { + 'zh-CN': '自定义文本', + 'en-US': '', + }, + desc: { + 'zh-CN': '通过label标签自定义显示文本;通过#item配置radio-group选项区域的模板。', + 'en-US': '', + }, + apis: ['TiRadioGroupComponent.slots.itemTemplate'], + }, + { + demoId: 'radio-group-direction', + name: { + 'zh-CN': '竖向排列', + 'en-US': 'direction', + }, + desc: { + 'zh-CN': '

    通过属性direction配置radio-group排列的方向,包括verticalhorizontal(默认)两种类型。

    ', + 'en-US': '', + }, + apis: ['TiRadioGroupComponent.properties.direction'], + }, + { + demoId: 'radio-group-labelkey', + name: { + 'zh-CN': '显示字段', + 'en-US': 'labelkey', + }, + desc: { + 'zh-CN': '

    通过属性labelKey配置radio-group组件显示数据的键值。

    ', + 'en-US': '', + }, + apis: ['TiRadioGroupComponent.properties.labelKey'], + }, + { + demoId: 'radio-group-valuekey', + name: { + 'zh-CN': '选中值', + 'en-US': 'valuekey', + }, + desc: { + 'zh-CN': + '

    通过属性valueKey配置radio-group组件选中项数据的键值。

    ', + 'en-US': '', + }, + apis: ['TiRadioGroupComponent.properties.valueKey'], + }, + { + demoId: 'radio-group-validation', + name: { + 'zh-CN': '表单校验', + 'en-US': 'validation', + }, + desc: { + 'zh-CN': + '

    通过指令tiValidation实现校验。

    ', + 'en-US': '', + }, + }, + { + demoId: 'radio-group-linewrap', + name: { + 'zh-CN': '自动换行', + 'en-US': 'linewrap', + }, + desc: { + 'zh-CN': '超过固定宽度会自动换行。', + 'en-US': '', + }, + }, + { + demoId: 'radio-event', + name: { + 'zh-CN': '事件', + 'en-US': 'radio event', + }, + desc: { + 'zh-CN': '当元素的值发生变化时触发ngModelChange事件。', + 'en-US': '', + }, + }, + ], + ignoreApis: [ + 'TiRadioGroupComponent.properties.disabled' + ], +}; diff --git a/src/radio/demo/src/app/radio/webdoc/radio.cn.md b/src/radio/demo/src/app/radio/webdoc/radio.cn.md new file mode 100644 index 0000000..b6e9bda --- /dev/null +++ b/src/radio/demo/src/app/radio/webdoc/radio.cn.md @@ -0,0 +1,29 @@ +--- +title: Radio 单选框 +--- +# Radio 单选框 + +
    + +从一组选项中选择一个。 + +```typescript +import { TiRadioModule } from '@opentiny/ng'; +``` + +如需使用校验功能,请导入。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
    + +
    + +从一组选项中选择一个。 + +```typescript +import { TiRadioModule } from '@opentiny/ng'; +``` +
    diff --git a/src/radio/demo/src/app/radio/webdoc/radio.en.md b/src/radio/demo/src/app/radio/webdoc/radio.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/radio/demo/src/app/radio/webdoc/radio.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/radio/demo/src/favicon.ico b/src/radio/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/radio/demo/src/index.html b/src/radio/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/radio/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/radio/demo/src/main.ts b/src/radio/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/radio/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/radio/demo/test.ts b/src/radio/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/radio/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/radio/demo/tsconfig.app.json b/src/radio/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/radio/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/radio/demo/tsconfig.spec.json b/src/radio/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/radio/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/radio/lib/index.ts b/src/radio/lib/index.ts new file mode 100644 index 0000000..bc21e68 --- /dev/null +++ b/src/radio/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiRadioModule'; diff --git a/src/radio/lib/ng-package.json b/src/radio/lib/ng-package.json new file mode 100644 index 0000000..791d024 --- /dev/null +++ b/src/radio/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/radio", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/radio/lib/package.json b/src/radio/lib/package.json new file mode 100644 index 0000000..333d75c --- /dev/null +++ b/src/radio/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-radio", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-checkbox": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/radio/lib/project.json b/src/radio/lib/project.json new file mode 100644 index 0000000..0cdae96 --- /dev/null +++ b/src/radio/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/radio/lib", + "sourceRoot": "src/radio/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/radio"], + "options": { + "project": "src/radio/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/radio"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js radio" + }, + { + "command": "ng default-build radio" + }, + { + "command": "node build/clear-default-theme.js radio" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/radio && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build radio && ng pack radio && node build/publish.js radio --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/radio/lib/src/TiRadioComponent.ts b/src/radio/lib/src/TiRadioComponent.ts new file mode 100644 index 0000000..0e3fe6e --- /dev/null +++ b/src/radio/lib/src/TiRadioComponent.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core'; +import { TiRadioBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * radio单选框组件 + * + * radio组件完全基于原生input实现,但是需要在input中添加tiRadio属性 + * + */ +@Component({ + selector: '[tiRadio]', // 指定组件名称 + templateUrl: './radio.html', // 指定组件模板 + styleUrls: ['./radio.less'], // 样式路径 + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None // 设置组件的试图包装选项:三个值Emulated(默认值),Native,None +}) +export class TiRadioComponent extends TiRadioBaseComponent { + protected versionInfo: string = super.getVersion(packageInfo); + /** + * @ignore + */ + protected canChange(): boolean { + return !this.nativeElement.checked; + } +} diff --git a/src/radio/lib/src/TiRadioGroupComponent.ts b/src/radio/lib/src/TiRadioGroupComponent.ts new file mode 100644 index 0000000..a578349 --- /dev/null +++ b/src/radio/lib/src/TiRadioGroupComponent.ts @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ContentChild, Input, TemplateRef, ViewEncapsulation } from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiCheckboxItem } from '@opentiny/ng-checkbox'; +import packageInfo from '../package.json'; + +/** + * + * ti-radio-group 组件的数据集格式 + */ +export interface TiRadioItem extends TiCheckboxItem { + /** + * 选中值 + */ + value?: string; +} + +/** + * + * 将多个radio聚合在一起,成为一个组,在表单校验时需要使用。 + */ +@Component({ + selector: 'ti-radio-group', + templateUrl: './radio-group.html', + styleUrls: ['./radiogroup.less'], + host: { + '[class.ti-radiogroup-horizon]': 'direction === "horizontal"', + '[class.ti-radiogroup-vertical]': 'direction === "vertical"', + '[class.ti-radiogroup-defalut-item]': '!itemTemplate' + }, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiRadioGroupComponent)] +}) +export class TiRadioGroupComponent extends TiFormComponent { + private static index: number = 0; + /** + * 所有单选项的数据集合 + */ + @Input() items: Array; + /** + * @ignore + * 组内每个 radio 的 name 属性值,多个 radio 需要共同的 name 属性才能聚合为一组 + */ + @Input() name: string; + /** + * 单选项数据要展示的键值 + */ + @Input() labelKey: string = 'label'; + /** + * 指定选中项数据的键值 + */ + @Input() valueKey: string = 'value'; + /** + * 排列方式 + */ + @Input() direction: 'vertical' | 'horizontal' = 'horizontal'; + /** + * 选项区域的插槽 + */ + @ContentChild('item', { static: false }) itemTemplate: TemplateRef; + /** + * @ignore + * ti-radio-group需要给组内的每个radio唯一的name属性 + */ + public uniqueName: string; + protected versionInfo: string = super.getVersion(packageInfo); + ngOnInit(): void { + super.ngOnInit(); + this.uniqueName = `ti_auto_radiogroup_${TiRadioGroupComponent.index++}`; + } +} diff --git a/src/radio/lib/src/TiRadioModule.ts b/src/radio/lib/src/TiRadioModule.ts new file mode 100644 index 0000000..43d4bb9 --- /dev/null +++ b/src/radio/lib/src/TiRadioModule.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiRadioComponent } from './TiRadioComponent'; +import { TiRadioGroupComponent } from './TiRadioGroupComponent'; +import { FormsModule } from '@angular/forms'; +import { TiOutlineModule } from '@opentiny/ng-outline'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiOutlineModule], + exports: [TiRadioComponent, TiRadioGroupComponent], + declarations: [TiRadioComponent, TiRadioGroupComponent] +}) +export class TiRadioModule {} +export { TiRadioComponent } from './TiRadioComponent'; +export { TiRadioGroupComponent, TiRadioItem } from './TiRadioGroupComponent'; diff --git a/src/radio/lib/src/radio-group.html b/src/radio/lib/src/radio-group.html new file mode 100644 index 0000000..8c15e2e --- /dev/null +++ b/src/radio/lib/src/radio-group.html @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/src/radio/lib/src/radio.html b/src/radio/lib/src/radio.html new file mode 100644 index 0000000..0f92a5a --- /dev/null +++ b/src/radio/lib/src/radio.html @@ -0,0 +1,14 @@ + diff --git a/src/radio/lib/src/radio.less b/src/radio/lib/src/radio.less new file mode 100644 index 0000000..09a5055 --- /dev/null +++ b/src/radio/lib/src/radio.less @@ -0,0 +1,175 @@ +@import '../../../themes/basic/base-all.less'; + +@radio-name: tiRadio; + +[@{radio-name}] + label { + --ti-radio-container-size: var(--ti-common-size-4x); + --ti-radio-mark-size: var(--ti-common-size-2x); + --ti-radio-border-weight: var(--ti-common-border-weight-normal); + --ti-radio-mark-offset: calc((var(--ti-radio-container-size) - var(--ti-radio-mark-size) - var(--ti-radio-border-weight) * 2) / 2); + --ti-radio-dark-checked-active-color: var(--ti-common-color-text-link-darkbg); + --ti-radio-timing-function-default: cubic-bezier(0.25, 0.1, 0.25, 1); + --ti-radio-line-height: 1.5em; +} + +input[type='radio'][@{radio-name}] { + // radio的input框不需要显示:radio的样式使用span/label定制,此处只使用input的点选功能 + // 之前设置为left: -9999px,虽然也能让元素不可见,但是当有横向滚动条时,元素聚焦会导致页面移向最左边 + display: none !important; + // basic + & + .ti3-radio { + .box-sizing(border-box); + display: inline-flex; + flex-shrink: 0; + line-height: var(--ti-radio-line-height); + margin-bottom: 0px; + font-weight: normal; + cursor: pointer; + .user-select(); + .ti3-radio-skin { + .box-sizing(border-box); + flex-shrink: 0; + position: relative; + display: inline-block; + margin-bottom: 0px; + margin-top: calc( + (var(--ti-radio-line-height) - var(--ti-radio-container-size)) / 2 + ); // 为保证按钮与文本对齐,margin-top 的值为(文本的行高 (28px) - 按钮宽高 (16px))/ 2 + height: var(--ti-radio-container-size); + width: var(--ti-radio-container-size); + border: var(--ti-radio-border-weight) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + .border-radius(var(--ti-common-border-radius-3)); + font-weight: normal; + cursor: pointer; + .transition (background-color; 0.15s, border-color; 0.15s); + + &::after { + .box-sizing(border-box); + position: absolute; + content: ''; + left: var(--ti-radio-mark-offset); + top: var(--ti-radio-mark-offset); + height: var(--ti-radio-mark-size); + width: var(--ti-radio-mark-size); + opacity: 0; + .border-radius(var(--ti-common-border-radius-3)); + } + } + .ti3-radio-label { + margin-left: var(--ti-common-space-2x); + cursor: pointer; + color: var(--ti-common-color-text-primary); + margin-bottom: 0px; + font-weight: var(--ti-common-font-weight-4); + font-size: var(--ti-common-font-size-base); + } + } + // unchecked hover + &:not(:disabled):not(:checked) + .ti3-radio .ti3-radio-skin { + border-color: var(--ti-common-color-line-normal); + &::after { + background-color: var(--ti-common-color-bg-emphasize); + // 未选中状态: opacity从1到0,从中心向上移动 + opacity: 0; + transform: translate(0px, -7px) scale(0); + transition: opacity 0.15s ease-out, transform 0.15s; + } + &:focus { + border-color: var(--ti-common-color-line-active); + } + &:hover { + border-color: var(--ti-common-color-line-active); + .transition(border-color; 0.2s; var(--ti-radio-timing-function-default)); + } + &:active { + border-color: var(--ti-common-color-line-active); + } + } + &:not(:disabled):checked + .ti3-radio .ti3-radio-skin { + border-color: var(--ti-common-color-line-active); + &::after { + background-color: var(--ti-common-color-bg-emphasize); + // 选中状态:opacity从0到1,原点位置从上边7px移到中心位置 + opacity: 1; + transform: translate(0px, 0px) scale(1); + transition: opacity 0.2s ease-out, transform 0.2s; + } + &:hover, + &:focus { + border-color: var(--ti-common-color-bg-hover); + &::after { + background-color: var(--ti-common-color-bg-hover); + } + } + &:hover { + // 非禁用,选中时hover动画 + .transition (background-color; 0.2s; var(--ti-radio-timing-function-default), + border-color; 0.2s; var(--ti-radio-timing-function-default), + content; 0.2s; var(--ti-radio-timing-function-default));; + } + &:active { + border-color: var(--ti-common-color-bg-hover); + &::after { + background-color: var(--ti-common-color-bg-hover); + } + } + } + // disable + &:disabled + .ti3-radio { + cursor: not-allowed; + .ti3-radio-skin { + background-color: var(--ti-common-color-bg-disabled); + border-color: var(--ti-common-color-line-disabled); + cursor: not-allowed; + } + .ti3-radio-label { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + } + } + &:disabled:checked + .ti3-radio .ti3-radio-skin::after { + background-color: var(--ti-common-color-bg-secondary); + cursor: not-allowed; + opacity: 1; + } + // hide + &.ng-hide + .ti3-radio { + display: none !important; + } +} +// [dark],适配深色背景,目前是内部组件discount使用,没有提供示例 +input[type='radio'][@{radio-name}][dark] { + & + .ti3-radio .ti3-radio-label { + color: var(--ti-common-color-text-darkbg); + } + + &:checked:not(:disabled) + .ti3-radio .ti3-radio-skin { + border-color: var(--ti-common-color-icon-darkbg-active); + &::after { + background-color: var(--ti-common-color-icon-darkbg-active); + } + &:hover, + &:active, + &:focus { + border-color: var(--ti-radio-dark-checked-active-color); + &::after { + background-color: var(--ti-radio-dark-checked-active-color); + } + } + } + &:not(:checked):not(:disabled) + .ti3-radio .ti3-radio-skin { + border-color: var(--ti-common-color-line-normal); + &:hover, + &:active, + &:focus { + border-color: var(--ti-common-color-icon-darkbg-active); + } + } + &:disabled + .ti3-radio .ti3-radio-skin { + background-color: var(--ti-common-color-icon-disabled); + border-color: var(--ti-common-color-line-disabled); + &::after { + background-color: var(--ti-common-color-bg-dark-disabled); + } + } +} diff --git a/src/radio/lib/src/radiogroup.less b/src/radio/lib/src/radiogroup.less new file mode 100644 index 0000000..1e6a6a3 --- /dev/null +++ b/src/radio/lib/src/radiogroup.less @@ -0,0 +1,23 @@ +ti-radio-group { + display: inline-flex; + flex-wrap: wrap; + line-height: 1.5em; + &.ti-radiogroup-vertical { + flex-direction: column; + input[type='radio'][tiRadio] + .ti3-radio, + .radio-group-item { + line-height: var(--ti-common-size-7x); + & .ti3-radio-skin { + // 为保证按钮与文本对齐,margin-top 的值为(文本的行高 (28px) - 按钮宽高 (16px))/ 2 + margin-top: calc((var(--ti-common-size-7x) - var(--ti-radio-container-size)) / 2); + } + } + } +} +.radio-group-item { + display: inline-flex; +} +// ti-radio-group按规范定义了选项之间的默认间距 +ti-radio-group.ti-radiogroup-horizon.ti-radiogroup-defalut-item label.ti3-radio { + margin-right: var(--ti-common-space-5x); +} diff --git a/src/rate/demo/karma.conf.js b/src/rate/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/rate/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/rate/demo/project.json b/src/rate/demo/project.json new file mode 100644 index 0000000..dc03a50 --- /dev/null +++ b/src/rate/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/rate/demo", + "sourceRoot": "src/rate/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/rate", + "index": "src/rate/demo/src/index.html", + "main": "src/rate/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/rate/demo/tsconfig.app.json", + "assets": ["src/rate/demo/src/favicon.ico", "src/rate/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "rate-demo:build:production" + }, + "development": { + "browserTarget": "rate-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js rate" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/rate/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/rate/demo/tsconfig.spec.json", + "karmaConfig": "src/rate/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/rate/demo/src/app/AppComponent.ts b/src/rate/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/rate/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/rate/demo/src/app/AppModule.ts b/src/rate/demo/src/app/AppModule.ts new file mode 100644 index 0000000..cfd1889 --- /dev/null +++ b/src/rate/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { RateTestModule } from './rate/RateTestModule'; + +@NgModule({ + imports: [ + RateTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/rate/demo/src/app/IndexComponent.ts b/src/rate/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..9e43096 --- /dev/null +++ b/src/rate/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { RateTestModule } from './rate/RateTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = RateTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/rate/demo/src/app/app.html b/src/rate/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/rate/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/rate/demo/src/app/rate/RateBasicComponent.ts b/src/rate/demo/src/app/rate/RateBasicComponent.ts new file mode 100644 index 0000000..690a885 --- /dev/null +++ b/src/rate/demo/src/app/rate/RateBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './rate-basic.html' +}) +export class RateBasicComponent { + value: number = 3; +} diff --git a/src/rate/demo/src/app/rate/RateDisabledComponent.ts b/src/rate/demo/src/app/rate/RateDisabledComponent.ts new file mode 100644 index 0000000..051aca8 --- /dev/null +++ b/src/rate/demo/src/app/rate/RateDisabledComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './rate-disabled.html' +}) +export class RateDisabledComponent { + value: number = 1; + disabled: boolean = true; +} diff --git a/src/rate/demo/src/app/rate/RateEventComponent.ts b/src/rate/demo/src/app/rate/RateEventComponent.ts new file mode 100644 index 0000000..0614179 --- /dev/null +++ b/src/rate/demo/src/app/rate/RateEventComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './rate-event.html' +}) +export class RateEventComponent { + myLogs: Array = []; + value: number = 0; + + onNgModelChange(value: Date): void { + this.myLogs = [...this.myLogs, `onNgModelChange() value = ${value}`]; + } +} diff --git a/src/rate/demo/src/app/rate/RateIdComponent.ts b/src/rate/demo/src/app/rate/RateIdComponent.ts new file mode 100644 index 0000000..8cad473 --- /dev/null +++ b/src/rate/demo/src/app/rate/RateIdComponent.ts @@ -0,0 +1,24 @@ +import { Component, DoCheck } from '@angular/core'; + +@Component({ + templateUrl: './rate-id.html' +}) +export class RateIdComponent implements DoCheck { + value = 2; + rateId = 'rate'; + + public idExistMap: Map = new Map(); + public ids: Array = ['rate_icon_0', 'rate_icon_1', 'rate_icon_2', 'rate_icon_3', 'rate_icon_4']; + public allIdExist = false; + + ngDoCheck(): void { + this.allIdExist = true; + this.ids.forEach((id: string) => { + const idExist: boolean = document.getElementById(id) != undefined; + this.idExistMap.set(id, idExist); + if (!idExist) { + this.allIdExist = false; + } + }); + } +} diff --git a/src/rate/demo/src/app/rate/RateLoadComponent.ts b/src/rate/demo/src/app/rate/RateLoadComponent.ts new file mode 100644 index 0000000..bf7def8 --- /dev/null +++ b/src/rate/demo/src/app/rate/RateLoadComponent.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './rate-load.html' +}) +export class RateLoadComponent { + value: any; + + public myLogs: Array = []; + + onChange(event: any): void { + this.myLogs = [...this.myLogs, `onNgModelChange() value=${event}`]; + } + + changeUndefined(): void { + this.value = undefined; + } + changeNull(): void { + this.value = null; + } + + changeText(): void { + this.value = ''; + } + + changeZero(): void { + this.value = 0; + } + + changeNegativeNumber(): void { + this.value = -10; + } + + changeMoreThanFive(): void { + this.value = 10; + } + + changeFour(): void { + this.value = 4; + } + + changeTwo(): void { + this.value = 2; + } +} diff --git a/src/rate/demo/src/app/rate/RateTestModule.ts b/src/rate/demo/src/app/rate/RateTestModule.ts new file mode 100644 index 0000000..9b992b1 --- /dev/null +++ b/src/rate/demo/src/app/rate/RateTestModule.ts @@ -0,0 +1,37 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiRateModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { RateBasicComponent } from './RateBasicComponent'; +import { RateDisabledComponent } from './RateDisabledComponent'; +import { RateEventComponent } from './RateEventComponent'; +import { RateIdComponent } from './RateIdComponent'; +import { RateLoadComponent } from './RateLoadComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiRateModule, DemoLogModule, RouterModule.forChild(RateTestModule.ROUTES)], + declarations: [RateBasicComponent, RateDisabledComponent, RateEventComponent, RateIdComponent, RateLoadComponent] +}) +export class RateTestModule { + static readonly LINKS: Array = [{ label: 'Rate' }]; + static readonly ROUTES: Routes = [ + { + path: 'rate/rate-basic', + component: RateBasicComponent + }, + { + path: 'rate/rate-disabled', + component: RateDisabledComponent + }, + { + path: 'rate/rate-event', + component: RateEventComponent + }, + { path: 'rate/rate-load', component: RateLoadComponent }, + { path: 'rate/rate-id', component: RateIdComponent } + ]; +} diff --git a/src/rate/demo/src/app/rate/rate-basic.html b/src/rate/demo/src/app/rate/rate-basic.html new file mode 100644 index 0000000..49b378a --- /dev/null +++ b/src/rate/demo/src/app/rate/rate-basic.html @@ -0,0 +1 @@ + diff --git a/src/rate/demo/src/app/rate/rate-disabled.html b/src/rate/demo/src/app/rate/rate-disabled.html new file mode 100644 index 0000000..a2e1049 --- /dev/null +++ b/src/rate/demo/src/app/rate/rate-disabled.html @@ -0,0 +1 @@ + diff --git a/src/rate/demo/src/app/rate/rate-event.html b/src/rate/demo/src/app/rate/rate-event.html new file mode 100644 index 0000000..87ae5a1 --- /dev/null +++ b/src/rate/demo/src/app/rate/rate-event.html @@ -0,0 +1,3 @@ + + + diff --git a/src/rate/demo/src/app/rate/rate-id.html b/src/rate/demo/src/app/rate/rate-id.html new file mode 100644 index 0000000..d68354e --- /dev/null +++ b/src/rate/demo/src/app/rate/rate-id.html @@ -0,0 +1,10 @@ +

    描述

    +

    设置组件id,用于自动化测试

    +
    + +

    示例

    + + +

    id是否存在:

    +

    {{id+':'+idExistMap.get(id)}}

    +

    所有id是否存在:{{allIdExist}}

    diff --git a/src/rate/demo/src/app/rate/rate-load.html b/src/rate/demo/src/app/rate/rate-load.html new file mode 100644 index 0000000..8c146b4 --- /dev/null +++ b/src/rate/demo/src/app/rate/rate-load.html @@ -0,0 +1,24 @@ +

    描述

    +

    Rate评分组件,数据加载。输入异常值时,默认会将value设置为0。

    +
    +

    点击异常值,value会被设置为0,同时会触发ngModelChange事件。设置正常0~5之间的正常值时,不会触发ngModelChange事件。

    +
    + +

    示例

    + +
    + +

    异常值

    +
    +
    +
    +
    +
    +
    + +

    正常值

    +
    + + +

    事件日志:

    + diff --git a/src/rate/demo/src/app/rate/webdoc/rate-demos.js b/src/rate/demo/src/app/rate/webdoc/rate-demos.js new file mode 100644 index 0000000..431901b --- /dev/null +++ b/src/rate/demo/src/app/rate/webdoc/rate-demos.js @@ -0,0 +1,39 @@ +export default { + column: '2', + demos: [ + { + demoId: 'rate-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Rate 组件的最简用法。

    ', + 'en-US': '', + } + }, + { + demoId: 'rate-disabled', + name: { + 'zh-CN': '只读', + 'en-US': 'disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态,即只读模式。

    ', + 'en-US': '', + }, + apis: ['TiRateComponent.properties.disabled'] + }, + { + demoId: 'rate-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event', + }, + desc: { + 'zh-CN': '

    当评分值改变的时候触发ngModelChange事件。

    ', + 'en-US': '', + } + } + ], +}; diff --git a/src/rate/demo/src/app/rate/webdoc/rate.cn.md b/src/rate/demo/src/app/rate/webdoc/rate.cn.md new file mode 100644 index 0000000..af3651d --- /dev/null +++ b/src/rate/demo/src/app/rate/webdoc/rate.cn.md @@ -0,0 +1,23 @@ +--- +title: Rate 评分 +--- +# Rate 评分 + +
    + +用于评分的组件。   + +```typescript +import { TiRateModule } from '@opentiny/ng'; +``` + +
    + +
    + +用于评分的组件。   + +```typescript +import { TiRateModule } from '@opentiny/ng'; +``` +
    diff --git a/src/rate/demo/src/app/rate/webdoc/rate.en.md b/src/rate/demo/src/app/rate/webdoc/rate.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/rate/demo/src/app/rate/webdoc/rate.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/rate/demo/src/favicon.ico b/src/rate/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/rate/demo/src/index.html b/src/rate/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/rate/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/rate/demo/src/main.ts b/src/rate/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/rate/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/rate/demo/test.ts b/src/rate/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/rate/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/rate/demo/tsconfig.app.json b/src/rate/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/rate/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/rate/demo/tsconfig.spec.json b/src/rate/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/rate/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/rate/lib/index.ts b/src/rate/lib/index.ts new file mode 100644 index 0000000..e4f3162 --- /dev/null +++ b/src/rate/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiRateModule'; diff --git a/src/rate/lib/ng-package.json b/src/rate/lib/ng-package.json new file mode 100644 index 0000000..235478f --- /dev/null +++ b/src/rate/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/rate", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/rate/lib/package.json b/src/rate/lib/package.json new file mode 100644 index 0000000..1b38fae --- /dev/null +++ b/src/rate/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-rate", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/rate/lib/project.json b/src/rate/lib/project.json new file mode 100644 index 0000000..3b46d04 --- /dev/null +++ b/src/rate/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/rate/lib", + "sourceRoot": "src/rate/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/rate"], + "options": { + "project": "src/rate/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/rate"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js rate" + }, + { + "command": "ng default-build rate" + }, + { + "command": "node build/clear-default-theme.js rate" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/rate && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build rate && ng pack rate && node build/publish.js rate --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/rate/lib/src/TiRateComponent.ts b/src/rate/lib/src/TiRateComponent.ts new file mode 100644 index 0000000..85a3d32 --- /dev/null +++ b/src/rate/lib/src/TiRateComponent.ts @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component } from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +@Component({ + selector: 'ti-rate', + templateUrl: './rate.html', + styleUrls: ['./rate.less'], + providers: [TiFormComponent.getValueAccessor(TiRateComponent)] +}) +export class TiRateComponent extends TiFormComponent { + /** + * @ignore 数组长度为5 + */ + public items: Array = new Array(5); + + /** + * @ignore 记录鼠标移入时的索引 + */ + public hoverIndex: number = -1; + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + this.setDefalutModel(this.model); + } + + /** + * @ignore 当做钩子函数使用 + */ + ngOnModelChange(model: any): void { + this.setDefalutModel(model); + } + + /** + * @ignore 设置model的默认值,在ngOnInit和ngOnModelChange中被调用 + * @param model 模型 + */ + setDefalutModel(model: any): void { + if (typeof this.model !== 'number' || this.model < 0 || model > this.items.length) { + this.model = 0; + } + } + + /** + * @ignore 鼠标进入时,设置hoverIndex + * @param index 图标索引,取值0 ~ this.items.length-1 + */ + onMouseEnter(index: number): void { + if (this.disabled) { + return; + } + + this.hoverIndex = index; + } + + /** + * @ignore 鼠标离开时,设置hoverIndex为-1 + */ + onMouseLeave(): void { + if (this.disabled) { + return; + } + + this.hoverIndex = -1; + } + + /** + * @ignore 点击图标时,设置model + * @param index 图标索引,取值0 ~ this.items.length-1 + */ + onClick(index: number): void { + if (this.disabled) { + return; + } + + this.model = index + 1; + } + + /** + * @ignore 判断当前图标是否需要点亮 + * @param index 图标索引,取值0 ~ this.items.length-1 + */ + isActive(index: number): boolean { + // 当鼠标移入时(this.hoverIndex > -1),hoverIndex及其坐标的图标需要点亮 + // 当鼠标移走后(this.hoverIndex = -1),当前评分值左边的图标需要点亮 + return this.hoverIndex > -1 ? index <= this.hoverIndex : index < this.model; + } + + /** + * @ignore ngFor遍历的trackBy函数,防止数据更新导致所有DOM重新渲染 + * @param index 图标索引,取值0 ~ this.items.length-1 + * @returns 图标索引 + */ + public trackByFn(index: any): any { + return index; + } +} diff --git a/src/rate/lib/src/TiRateModule.ts b/src/rate/lib/src/TiRateModule.ts new file mode 100644 index 0000000..358b75d --- /dev/null +++ b/src/rate/lib/src/TiRateModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { TiRateComponent } from './TiRateComponent'; + +import { TiIconModule } from '@opentiny/ng-icon'; + +@NgModule({ + imports: [CommonModule, TiIconModule], + declarations: [TiRateComponent], + exports: [TiRateComponent] +}) +export class TiRateModule {} +export { TiRateComponent } from './TiRateComponent'; diff --git a/src/rate/lib/src/rate.html b/src/rate/lib/src/rate.html new file mode 100644 index 0000000..7e356a2 --- /dev/null +++ b/src/rate/lib/src/rate.html @@ -0,0 +1,11 @@ + + diff --git a/src/rate/lib/src/rate.less b/src/rate/lib/src/rate.less new file mode 100644 index 0000000..b5385df --- /dev/null +++ b/src/rate/lib/src/rate.less @@ -0,0 +1,30 @@ +:host { + --ti-rate-star-size: var(--ti-common-size-4x); +} + +:host { + cursor: pointer; + display: inline-block; + &[disabled] { + cursor: not-allowed; + } + .ti3-rate-star { + display: inline-block; + font-size: var(--ti-rate-star-size); + height: var(--ti-rate-star-size); + line-height: var(--ti-rate-star-size); + vertical-align: middle; + padding-right: var(--ti-common-space-2x); + &:last-child { + padding-right: 0; + } + } + + .ti3-rate-star-normal { + color: var(--ti-common-color-bg-light-normal); + } + + .ti3-rate-star-active { + color: var(--ti-common-color-warn-secondary); + } +} diff --git a/src/renderer/lib/index.ts b/src/renderer/lib/index.ts new file mode 100644 index 0000000..f99ea2f --- /dev/null +++ b/src/renderer/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiRenderer'; +export * from './src/TiRendererModule'; diff --git a/src/renderer/lib/ng-package.json b/src/renderer/lib/ng-package.json new file mode 100644 index 0000000..2a54afc --- /dev/null +++ b/src/renderer/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/renderer", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/renderer/lib/package.json b/src/renderer/lib/package.json new file mode 100644 index 0000000..afd4225 --- /dev/null +++ b/src/renderer/lib/package.json @@ -0,0 +1,8 @@ +{ + "name": "@opentiny/ng-renderer", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/renderer/lib/project.json b/src/renderer/lib/project.json new file mode 100644 index 0000000..8ea699a --- /dev/null +++ b/src/renderer/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/renderer/lib", + "sourceRoot": "src/renderer/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/renderer"], + "options": { + "project": "src/renderer/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/renderer"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js renderer" + }, + { + "command": "ng default-build renderer" + }, + { + "command": "node build/clear-default-theme.js renderer" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/renderer && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build renderer && ng pack renderer && node build/publish.js renderer --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/renderer/lib/src/TiRenderer.ts b/src/renderer/lib/src/TiRenderer.ts new file mode 100644 index 0000000..566eaee --- /dev/null +++ b/src/renderer/lib/src/TiRenderer.ts @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Renderer2, Injectable, RendererFactory2 } from '@angular/core'; +import { TiRendererModule } from './TiRendererModule'; + +/** + * @ignore + */ +@Injectable({ + providedIn: TiRendererModule +}) +export class TiRenderer { + private renderer: Renderer2; + constructor(rendererFactory: RendererFactory2) { + this.renderer = rendererFactory.createRenderer(null, null); + } + /** + * @description: 将节点插入某节点元素之后 + * @param: sourceEle 被插入节点 + * @param: targetEle 节点插入位置 + */ + public insertAfter(sourceEle, targetEle) { + const parent = targetEle.parentNode; + // 如果最后的节点是目标元素,则直接添加 + if (parent.lastChild === targetEle) { + this.renderer.appendChild(parent, sourceEle); + } else { + // 如果不是,则插在目标元素的下一个兄弟节点之前 + this.renderer.insertBefore(parent, sourceEle, targetEle.nextSibling); + } + } + /** + * @description: 判读一个元素上是否存在某个样式类名 + * @param: ele 被判断的元素 + * @param: className 样式类名 + */ + public hasClass(element, className): boolean { + const classList = element.classList; + return classList.contains(className); + } + /** + * @description: 给指定元素设置属性 + * @param: element 被设置的元素 + * @param: attr Object 属性对象 + */ + public setAttributes(element, attr: Object) { + for (const key in attr) { + if (Object.prototype.hasOwnProperty.call(attr, key)) { + this.renderer.setAttribute(element, key, String(attr[key])); + } + } + } + + /** + * @description: 为元素添加多个样式 + * @param: ele 元素对象 + * @param: styles Object 样式对象,如:{width: 100, height: 200} + */ + public setStyles(ele, styles) { + for (const key in styles) { + if (Object.prototype.hasOwnProperty.call(styles, key)) { + this.renderer.setStyle(ele, key, styles[key]); + } + } + } + + /** + * @description: 将节点插入某父容器,作为第一个元素 + * @param: parentEle 父节点元素 + * @param: sourceEle 需要插入的节点元素 + */ + public prepend(parentEle, sourceEle) { + if (!parentEle) { + return; + } + this.renderer.insertBefore(parentEle, sourceEle, parentEle.firstElementChild); + } + + /** + * @description: 查找一个元素下有某个样式类的子元素 + * @param: element 被查找的元素 + * @param: className 子元素的样式类名 + */ + public findChildrenByClassName(element, className): Array { + let resultChildren = []; + resultChildren = Array.from(element.children).filter((child: any) => { + return this.hasClass(child, className); + }); + + return resultChildren; + } + /** + * @description: 查找一个元素下有某个样式类的子元素 + * @param: element 被查找的元素 + * @param: className 子元素的样式类名 + */ + public findChildrenByTagName(element, tagName: string): Array { + let resultChildren = []; + resultChildren = Array.from(element.children).filter((child: any) => { + return child.tagName === tagName; + }); + + return resultChildren; + } +} diff --git a/src/renderer/lib/src/TiRendererModule.ts b/src/renderer/lib/src/TiRendererModule.ts new file mode 100644 index 0000000..078f216 --- /dev/null +++ b/src/renderer/lib/src/TiRendererModule.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +/** + * @ignore + */ +@NgModule({ + imports: [] +}) +export class TiRendererModule {} diff --git a/src/rights/demo/project.json b/src/rights/demo/project.json new file mode 100644 index 0000000..8be2121 --- /dev/null +++ b/src/rights/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/rights/demo", + "sourceRoot": "src/rights/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/rights", + "index": "src/rights/demo/src/index.html", + "main": "src/rights/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/rights/demo/tsconfig.app.json", + "assets": ["src/rights/demo/src/favicon.ico", "src/rights/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "rights-demo:build:production" + }, + "development": { + "browserTarget": "rights-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js rights" + } + ] + } + } + } +} diff --git a/src/rights/demo/src/app/AppComponent.ts b/src/rights/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/rights/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/rights/demo/src/app/AppModule.ts b/src/rights/demo/src/app/AppModule.ts new file mode 100644 index 0000000..1955211 --- /dev/null +++ b/src/rights/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { RightsTestModule } from './rights/RightsTestModule'; + +@NgModule({ + imports: [ + RightsTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/rights/demo/src/app/IndexComponent.ts b/src/rights/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..b7c6698 --- /dev/null +++ b/src/rights/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { RightsTestModule } from './rights/RightsTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = RightsTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/rights/demo/src/app/app.html b/src/rights/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/rights/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/rights/demo/src/app/rights/RightsBasicComponent.ts b/src/rights/demo/src/app/rights/RightsBasicComponent.ts new file mode 100644 index 0000000..0be0ccf --- /dev/null +++ b/src/rights/demo/src/app/rights/RightsBasicComponent.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; +import { TiRightsItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './rights-basic.html' +}) +export class RightsBasicComponent { + items: Array = [ + { + label: '灵活调整' + }, + { + label: '默认开启基础监控', + tip: '默认开启基础监控的信息描述', + tipPosition: 'bottom' + }, + { + label: '企业级存储', + tip: '企业级村存储信息描述' + }, + { + label: '智能数据底座', + tip: '智能数据底座信息描述' + }, + { + label: '不支持调整', + tip: '负向权益信息描述', + type: 'noSupport' + } + ]; +} diff --git a/src/rights/demo/src/app/rights/RightsTestModule.ts b/src/rights/demo/src/app/rights/RightsTestModule.ts new file mode 100644 index 0000000..3f121a1 --- /dev/null +++ b/src/rights/demo/src/app/rights/RightsTestModule.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiRightsModule } from '@opentiny/ng'; +import { RightsBasicComponent } from './RightsBasicComponent'; +import { RightsTypeComponent } from './RightsTypeComponent'; + +@NgModule({ + imports: [CommonModule, TiRightsModule, RouterModule.forChild(RightsTestModule.ROUTES)], + declarations: [RightsBasicComponent, RightsTypeComponent] +}) +export class RightsTestModule { + static readonly ROUTES: Routes = [ + { + path: 'rights/rights-basic', + component: RightsBasicComponent + }, + { + path: 'rights/rights-type', + component: RightsTypeComponent + } + ]; +} diff --git a/src/rights/demo/src/app/rights/RightsTypeComponent.ts b/src/rights/demo/src/app/rights/RightsTypeComponent.ts new file mode 100644 index 0000000..92741c4 --- /dev/null +++ b/src/rights/demo/src/app/rights/RightsTypeComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { TiRightsItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './rights-type.html' +}) +export class RightsTypeComponent { + items: Array = [ + { + label: '灵活调整', + // tip: this.tips, + tipPosition: 'bottom' + }, + { + label: '默认开启基础监控', + tip: '默认开启基础监控的信息描述' + }, + { + label: '企业级存储', + tip: '企业级村存储信息描述' + }, + { + label: '智能数据底座', + tip: '智能数据底座信息描述' + }, + { + label: '不支持调整', + tip: '负向权益信息描述', + type: 'noSupport' + } + ]; +} diff --git a/src/rights/demo/src/app/rights/rights-basic.html b/src/rights/demo/src/app/rights/rights-basic.html new file mode 100644 index 0000000..e963012 --- /dev/null +++ b/src/rights/demo/src/app/rights/rights-basic.html @@ -0,0 +1 @@ + diff --git a/src/rights/demo/src/app/rights/rights-type.html b/src/rights/demo/src/app/rights/rights-type.html new file mode 100644 index 0000000..5409ccd --- /dev/null +++ b/src/rights/demo/src/app/rights/rights-type.html @@ -0,0 +1 @@ + diff --git a/src/rights/demo/src/app/rights/webdoc/rights-demos.js b/src/rights/demo/src/app/rights/webdoc/rights-demos.js new file mode 100644 index 0000000..aef1e07 --- /dev/null +++ b/src/rights/demo/src/app/rights/webdoc/rights-demos.js @@ -0,0 +1,29 @@ +export default { + column: '2', + demos: [ + { + demoId: 'rights-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': 'Rights 组件的最简用法。', + 'en-US': '' + }, + apis: ['TiRightsComponent.properties.items'] + }, + { + demoId: 'rights-type', + name: { + 'zh-CN': '类型', + 'en-US': '' + }, + desc: { + 'zh-CN': '通过type配置权益类型。', + 'en-US': '' + }, + apis: ['TiRightsComponent.properties.type'] + } + ] +}; diff --git a/src/rights/demo/src/app/rights/webdoc/rights.cn.md b/src/rights/demo/src/app/rights/webdoc/rights.cn.md new file mode 100644 index 0000000..61c2c7c --- /dev/null +++ b/src/rights/demo/src/app/rights/webdoc/rights.cn.md @@ -0,0 +1,19 @@ +--- +title: Rights 权益 +--- + +# Rights 权益 + +
    + +Rights 是介绍产品优势等特性信息的组件。 + +- 分为正向权益和负向权益两种。 + +- 可以是针对全局的权益信息,也可以是针对某个参数的权益信息。 + +```typescript +import { TiRightsModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/rights/demo/src/app/rights/webdoc/rights.en.md b/src/rights/demo/src/app/rights/webdoc/rights.en.md new file mode 100644 index 0000000..8801668 --- /dev/null +++ b/src/rights/demo/src/app/rights/webdoc/rights.en.md @@ -0,0 +1,13 @@ +--- +title: Rights +--- + +# Rights + +
    + +```typescript +import { TiRightsModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/rights/demo/src/favicon.ico b/src/rights/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d7a7af634ee6d643ac81d81feadff569ac25de3d GIT binary patch literal 300852 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 + + + + + + Tiny + + + + + + + + + diff --git a/src/rights/demo/src/main.ts b/src/rights/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/rights/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/rights/demo/tsconfig.app.json b/src/rights/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/rights/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/rights/lib/index.ts b/src/rights/lib/index.ts new file mode 100644 index 0000000..4023eb7 --- /dev/null +++ b/src/rights/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiRightsModule'; +export * from './src/TiRightsComponent'; diff --git a/src/rights/lib/ng-package.json b/src/rights/lib/ng-package.json new file mode 100644 index 0000000..d403c5f --- /dev/null +++ b/src/rights/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/rights", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/rights/lib/package.json b/src/rights/lib/package.json new file mode 100644 index 0000000..18fc3bb --- /dev/null +++ b/src/rights/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-rights", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/rights/lib/project.json b/src/rights/lib/project.json new file mode 100644 index 0000000..71fa8d8 --- /dev/null +++ b/src/rights/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/rights/lib", + "sourceRoot": "src/rights/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/rights"], + "options": { + "project": "src/rights/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/rights"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js rights" + }, + { + "command": "ng default-build rights" + }, + { + "command": "node build/clear-default-theme.js rights" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/rights && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build rights && ng pack rights && node build/publish.js rights --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/rights/lib/src/TiRightsComponent.ts b/src/rights/lib/src/TiRightsComponent.ts new file mode 100644 index 0000000..0ece479 --- /dev/null +++ b/src/rights/lib/src/TiRightsComponent.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { Component, Input, TemplateRef } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiPositionType } from '@opentiny/ng-utils'; + +export interface TiRightsItem { + /** + * 显示权益标题 + */ + label: string; + /** + * tip提示的内容 + */ + tip?: string | TemplateRef; + /** + * tip提示方位 + */ + tipPosition?: TiPositionType; + /** + * 权益类型,默认显示正向权益类型,'noSupport'为负向类型 + */ + type?: 'support' | 'noSupport'; + /** + * 允许多余的属性字段 + */ + [propName: string]: any; +} +/** + * 组件描述 + */ +@Component({ + selector: 'ti-rights', + templateUrl: 'rights.html', + styleUrls: ['rights.less', './icon.less'] +}) +export class TiRightsComponent extends TiBaseComponent { + /** + * 权益总数据 + */ + @Input() items: Array; + /** + * 权益级别,分为页面级别(page)和参数级别(param) + */ + @Input() type: 'page' | 'param' = 'param'; + + ngOnInit(): void { + super.ngOnInit(); + // 权益数量不超过6个 + if (this.items.length >= 6) { + this.items = this.items.slice(0, 5); + } + } +} diff --git a/src/rights/lib/src/TiRightsModule.ts b/src/rights/lib/src/TiRightsModule.ts new file mode 100644 index 0000000..864bb29 --- /dev/null +++ b/src/rights/lib/src/TiRightsModule.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiRightsComponent } from './TiRightsComponent'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiIconModule } from '@opentiny/ng-icon'; + +@NgModule({ + imports: [CommonModule, TiTipModule, TiIconModule], + exports: [TiRightsComponent], + declarations: [TiRightsComponent] +}) +export class TiRightsModule {} +export { TiRightsComponent, TiRightsItem } from './TiRightsComponent'; diff --git a/src/rights/lib/src/fonts/rightsFont.svg b/src/rights/lib/src/fonts/rightsFont.svg new file mode 100644 index 0000000..09cd862 --- /dev/null +++ b/src/rights/lib/src/fonts/rightsFont.svg @@ -0,0 +1,28 @@ + + + + + + +{ + "fontFamily": "rightsFont", + "description": "Font generated by IcoMoon.", + "majorVersion": 1, + "minorVersion": 0, + "version": "Version 1.0", + "fontId": "rightsFont", + "psName": "rightsFont", + "subFamily": "Regular", + "fullName": "rightsFont" +} + + + + + + + + + + + \ No newline at end of file diff --git a/src/rights/lib/src/fonts/rightsFont.woff b/src/rights/lib/src/fonts/rightsFont.woff new file mode 100644 index 0000000000000000000000000000000000000000..2bd6faaa49667d37f262db19209fbc54d5c62628 GIT binary patch literal 1832 zcmcIkPly{;82?@-Gn1Julj>v>H=Ar{lQhX%J4rXmW>acfv;>#B6jxaS3YzXtlZ9+j zl9b)`;!!W6w2J7_lUVId5fLo3AVLpnkJVD}VyguUdhp=J-+ME)=_R*)!}s3r`+fgr z-n{Q^ovzi05r&$j3BD>nyE?~@j?*vZ=80GU>}yB{g5osb}4805jQ1Q1n# zVz6->!_2*64{9c4GU}D7J6%<)q&PP`|ND4EFEER)7mDweCPynv@F>_BN4(%g@Gs>)<2 zG9yf4Da=3ghaZw8R3&tP&9K7!LYXZTIl-iFwS-~ndQ=UBV{yCk{z^8RSxU^ti^l_j zneb%PUR>O%9QMRJXub~rVr|xrXQS+rBZWG)^qmZe9o?C=ThZVGNx&X z-RMU~GH4iI%a7^$KAjbULFl{pd>2iG%?2yg>UOz&=x7BsCMTy$7hjk|)!9VMj>kd) zEsjzsn3|ZFN+pjbGMRcVUzpG5>Y429XzkK7&t70wYE`cUIPENhyB2}JS^ij zZhKe(f62o-MNuC&hhGB|g2lx?+=D-OSOWinhyB36d03_}ZFyJ$|AmKjD$-(aquCnt zYu(O(dbB}JYT?B8sfJsjLj%rRO{e4Z8UttDI +
  • + {{item.label}} +
  • + diff --git a/src/rights/lib/src/rights.less b/src/rights/lib/src/rights.less new file mode 100644 index 0000000..11b4f00 --- /dev/null +++ b/src/rights/lib/src/rights.less @@ -0,0 +1,38 @@ +.tp-rights-container { + display: flex; +} + +.tp-rights-item { + color: #575d6c; + margin-right: var(--ti-common-space-2x); + padding: 3px var(--ti-common-space-2x); + cursor: pointer; + + &:last-child { + margin-right: 0; + } + + .tp-rights-item-icon { + font-size: 16px; + vertical-align: -15%; //TODO:在tiny库设置'bottom'属性,图标可对齐,再该项目中无法对齐,暂使用百分比代替 + } +} + +.tp-rights-item-param { + margin-right: var(--ti-common-space-5x); + padding: 3px 0; +} + +.tp-rights-item-support { + background-color: #edfff9; +} + +.tp-rights-item-nosupport { + background-color: #f5f5f6; +} + +.tp-rights-label { + display: inline-block; + line-height: 18px; + margin-left: var(--ti-common-space-base); +} diff --git a/src/score/demo/karma.conf.js b/src/score/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/score/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/score/demo/project.json b/src/score/demo/project.json new file mode 100644 index 0000000..c15582a --- /dev/null +++ b/src/score/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/score/demo", + "sourceRoot": "src/score/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/score", + "index": "src/score/demo/src/index.html", + "main": "src/score/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/score/demo/tsconfig.app.json", + "assets": ["src/score/demo/src/favicon.ico", "src/score/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "score-demo:build:production" + }, + "development": { + "browserTarget": "score-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js score" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/score/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/score/demo/tsconfig.spec.json", + "karmaConfig": "src/score/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/score/demo/src/app/AppComponent.ts b/src/score/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/score/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/score/demo/src/app/AppModule.ts b/src/score/demo/src/app/AppModule.ts new file mode 100644 index 0000000..05ca7af --- /dev/null +++ b/src/score/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ScoreTestModule } from './score/ScoreTestModule'; + +@NgModule({ + imports: [ + ScoreTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/score/demo/src/app/IndexComponent.ts b/src/score/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..cb6063f --- /dev/null +++ b/src/score/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ScoreTestModule } from './score/ScoreTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = ScoreTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/score/demo/src/app/app.html b/src/score/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/score/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/score/demo/src/app/score/ScoreBasicComponent.ts b/src/score/demo/src/app/score/ScoreBasicComponent.ts new file mode 100644 index 0000000..ec40ad3 --- /dev/null +++ b/src/score/demo/src/app/score/ScoreBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './score-basic.html' +}) +export class ScoreBasicComponent { + value: number = 8; +} diff --git a/src/score/demo/src/app/score/ScoreEventsComponent.ts b/src/score/demo/src/app/score/ScoreEventsComponent.ts new file mode 100644 index 0000000..39adba5 --- /dev/null +++ b/src/score/demo/src/app/score/ScoreEventsComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './score-events.html' +}) +export class ScoreEventsComponent { + value: number = 10; + myLogs: Array = []; + onChange(value: number): void { + this.myLogs = [...this.myLogs, `当前评分值:${value}`]; + } +} diff --git a/src/score/demo/src/app/score/ScoreLimittextComponent.ts b/src/score/demo/src/app/score/ScoreLimittextComponent.ts new file mode 100644 index 0000000..94a659f --- /dev/null +++ b/src/score/demo/src/app/score/ScoreLimittextComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './score-limittext.html' +}) +export class ScoreLimittextComponent { + minText: string = '非常不可能'; + maxText: string = '非常可能'; +} diff --git a/src/score/demo/src/app/score/ScorePaddingComponent.ts b/src/score/demo/src/app/score/ScorePaddingComponent.ts new file mode 100644 index 0000000..b3c1cfd --- /dev/null +++ b/src/score/demo/src/app/score/ScorePaddingComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './score-padding.html' +}) +export class ScorePaddingComponent { + value: number = 8; +} diff --git a/src/score/demo/src/app/score/ScoreTestModule.ts b/src/score/demo/src/app/score/ScoreTestModule.ts new file mode 100644 index 0000000..41b8238 --- /dev/null +++ b/src/score/demo/src/app/score/ScoreTestModule.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Routes, RouterModule } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiScoreModule } from '@opentiny/ng'; +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { ScoreBasicComponent } from './ScoreBasicComponent'; +import { ScoreLimittextComponent } from './ScoreLimittextComponent'; +import { ScoreEventsComponent } from './ScoreEventsComponent'; +import { ScorePaddingComponent } from './ScorePaddingComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, DemoLogModule, TiScoreModule, RouterModule.forChild(ScoreTestModule.ROUTES)], + exports: [RouterModule], + declarations: [ScoreBasicComponent, ScoreLimittextComponent, ScorePaddingComponent, ScoreEventsComponent] +}) +export class ScoreTestModule { + static readonly LINKS: Array = [{ href: 'components/TiScoreComponent.html', label: 'Score' }]; + static readonly ROUTES: Routes = [ + { path: 'score/score-basic', component: ScoreBasicComponent }, + { path: 'score/score-limittext', component: ScoreLimittextComponent }, + { path: 'score/score-padding', component: ScorePaddingComponent }, + { path: 'score/score-events', component: ScoreEventsComponent } + ]; +} diff --git a/src/score/demo/src/app/score/score-basic.html b/src/score/demo/src/app/score/score-basic.html new file mode 100644 index 0000000..29659a4 --- /dev/null +++ b/src/score/demo/src/app/score/score-basic.html @@ -0,0 +1 @@ + diff --git a/src/score/demo/src/app/score/score-events.html b/src/score/demo/src/app/score/score-events.html new file mode 100644 index 0000000..ada405b --- /dev/null +++ b/src/score/demo/src/app/score/score-events.html @@ -0,0 +1 @@ + diff --git a/src/score/demo/src/app/score/score-limittext.html b/src/score/demo/src/app/score/score-limittext.html new file mode 100644 index 0000000..732f82c --- /dev/null +++ b/src/score/demo/src/app/score/score-limittext.html @@ -0,0 +1 @@ + diff --git a/src/score/demo/src/app/score/score-padding.html b/src/score/demo/src/app/score/score-padding.html new file mode 100644 index 0000000..8048e56 --- /dev/null +++ b/src/score/demo/src/app/score/score-padding.html @@ -0,0 +1 @@ + diff --git a/src/score/demo/src/app/score/webdoc/score-demos.js b/src/score/demo/src/app/score/webdoc/score-demos.js new file mode 100644 index 0000000..6a4f860 --- /dev/null +++ b/src/score/demo/src/app/score/webdoc/score-demos.js @@ -0,0 +1,32 @@ +export default { + column: '2', + demos: [ + { + demoId: 'score-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Score 组件的最简用法。

    ', + 'en-US': '

    score basic

    ', + }, + }, + { + demoId: 'score-limittext', + name: { + 'zh-CN': '文本', + 'en-US': 'limittext', + }, + desc: { + 'zh-CN': + '

    通过minTextmaxText配置两侧文本。

    ', + 'en-US': '

    score limittext

    ', + }, + apis: [ + 'TiScoreComponent.properties.minText', + 'TiScoreComponent.properties.maxText', + ], + }, + ], +}; diff --git a/src/score/demo/src/app/score/webdoc/score.cn.md b/src/score/demo/src/app/score/webdoc/score.cn.md new file mode 100644 index 0000000..94227ab --- /dev/null +++ b/src/score/demo/src/app/score/webdoc/score.cn.md @@ -0,0 +1,23 @@ +--- +title: Score 评分 +--- +# Score 评分 + +
    + +Score 用户评分的组件。   + +```typescript +import { TiScoreModule } from '@opentiny/ng'; +``` + +
    + +
    + +Score 用户评分的组件。   + +```typescript +import { TiScoreModule } from '@opentiny/ng'; +``` +
    diff --git a/src/score/demo/src/app/score/webdoc/score.en.md b/src/score/demo/src/app/score/webdoc/score.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/score/demo/src/app/score/webdoc/score.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/score/demo/src/favicon.ico b/src/score/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/score/demo/src/index.html b/src/score/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/score/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/score/demo/src/main.ts b/src/score/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/score/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/score/demo/test.ts b/src/score/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/score/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/score/demo/tsconfig.app.json b/src/score/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/score/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/score/demo/tsconfig.spec.json b/src/score/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/score/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/score/lib/index.ts b/src/score/lib/index.ts new file mode 100644 index 0000000..5229ef0 --- /dev/null +++ b/src/score/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiScoreModule'; diff --git a/src/score/lib/ng-package.json b/src/score/lib/ng-package.json new file mode 100644 index 0000000..da4d0a7 --- /dev/null +++ b/src/score/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/score", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/score/lib/package.json b/src/score/lib/package.json new file mode 100644 index 0000000..02de7cb --- /dev/null +++ b/src/score/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-score", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/score/lib/project.json b/src/score/lib/project.json new file mode 100644 index 0000000..da34bcf --- /dev/null +++ b/src/score/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/score/lib", + "sourceRoot": "src/score/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/score"], + "options": { + "project": "src/score/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/score"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js score" + }, + { + "command": "ng default-build score" + }, + { + "command": "node build/clear-default-theme.js score" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/score && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build score && ng pack score && node build/publish.js score --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/score/lib/src/TiScoreComponent.ts b/src/score/lib/src/TiScoreComponent.ts new file mode 100644 index 0000000..2f95cb8 --- /dev/null +++ b/src/score/lib/src/TiScoreComponent.ts @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { Component, ElementRef, Input, Renderer2, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiLocale } from '@opentiny/ng-locale'; + +/** + * NPS评分组件 + */ +@Component({ + selector: 'ti-score', + templateUrl: './score.html', + styleUrls: ['./score.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiScoreComponent)] +}) +export class TiScoreComponent extends TiFormComponent { + /** + * 最小评分值文本 + */ + @Input() minText: string; + /** + * 最大评分值文本 + */ + @Input() maxText: string; + + /** + * @ignore + * 选块内间距,主要用于调查问卷组件完成页场景 + */ + @Input() padding: string; + + /** + * @ignore + * 定义含有11项成员的数组 + */ + public scoreArray: Array = new Array(11); + + constructor(protected hostRef: ElementRef, protected renderer: Renderer2, protected changeDetectorRef: ChangeDetectorRef) { + super(hostRef, renderer, changeDetectorRef); + this.minText = this.minText || TiLocale.getLocaleWords().tiNpsscore.minText; + this.maxText = this.maxText || TiLocale.getLocaleWords().tiNpsscore.maxText; + } + /** + * @ignore + * 选择评分时,触发事件 + */ + public onClick(value: number): void { + this.model = value; + } +} diff --git a/src/score/lib/src/TiScoreModule.ts b/src/score/lib/src/TiScoreModule.ts new file mode 100644 index 0000000..9077a8f --- /dev/null +++ b/src/score/lib/src/TiScoreModule.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiScoreComponent } from './TiScoreComponent'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, TiOutlineModule], + exports: [TiScoreComponent], + declarations: [TiScoreComponent] +}) +export class TiScoreModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiScoreComponent } from './TiScoreComponent'; diff --git a/src/score/lib/src/i18n/TiScoreWords.ts b/src/score/lib/src/i18n/TiScoreWords.ts new file mode 100644 index 0000000..783a174 --- /dev/null +++ b/src/score/lib/src/i18n/TiScoreWords.ts @@ -0,0 +1,6 @@ +export interface TiScoreWords { + tiNpsscore: { + minText: string; + maxText: string; + }; +} diff --git a/src/score/lib/src/i18n/en_US.ts b/src/score/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..ad83641 --- /dev/null +++ b/src/score/lib/src/i18n/en_US.ts @@ -0,0 +1,8 @@ +import { TiScoreWords } from './TiScoreWords'; + +export const en_US: TiScoreWords = { + tiNpsscore: { + minText: 'Very dissatisfied', + maxText: 'Very satisfied' + } +}; diff --git a/src/score/lib/src/i18n/es_US.ts b/src/score/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..ae45e62 --- /dev/null +++ b/src/score/lib/src/i18n/es_US.ts @@ -0,0 +1,8 @@ +import { TiScoreWords } from './TiScoreWords'; + +export const es_US: TiScoreWords = { + tiNpsscore: { + minText: 'Very dissatisfied', + maxText: 'Very satisfied' + } +}; diff --git a/src/score/lib/src/i18n/fr_FR.ts b/src/score/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..a488d48 --- /dev/null +++ b/src/score/lib/src/i18n/fr_FR.ts @@ -0,0 +1,8 @@ +import { TiScoreWords } from './TiScoreWords'; + +export const fr_FR: TiScoreWords = { + tiNpsscore: { + minText: 'Very dissatisfied', + maxText: 'Very satisfied' + } +}; diff --git a/src/score/lib/src/i18n/index.ts b/src/score/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/score/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/score/lib/src/i18n/pt_BR.ts b/src/score/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..2526ef2 --- /dev/null +++ b/src/score/lib/src/i18n/pt_BR.ts @@ -0,0 +1,8 @@ +import { TiScoreWords } from './TiScoreWords'; + +export const pt_BR: TiScoreWords = { + tiNpsscore: { + minText: 'Very dissatisfied', + maxText: 'Very satisfied' + } +}; diff --git a/src/score/lib/src/i18n/zh_CN.ts b/src/score/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..c3a253d --- /dev/null +++ b/src/score/lib/src/i18n/zh_CN.ts @@ -0,0 +1,8 @@ +import { TiScoreWords } from './TiScoreWords'; + +export const zh_CN: TiScoreWords = { + tiNpsscore: { + minText: '非常不满意', + maxText: '非常满意' + } +}; diff --git a/src/score/lib/src/score.html b/src/score/lib/src/score.html new file mode 100644 index 0000000..a6fafd4 --- /dev/null +++ b/src/score/lib/src/score.html @@ -0,0 +1,18 @@ +
    + {{minText}} + {{maxText}} +
    +
    + {{i}} +
    diff --git a/src/score/lib/src/score.less b/src/score/lib/src/score.less new file mode 100644 index 0000000..aef834d --- /dev/null +++ b/src/score/lib/src/score.less @@ -0,0 +1,48 @@ +::ng-deep :root { + --ti3-score-item-height: 28px; +} + +:host { + display: inline-block; + font-weight: normal; + font-size: var(--ti-common-font-size-base); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-primary); +} + +// 每项评分样式 +.ti3-score-item { + display: inline-block; + text-align: center; + cursor: pointer; + height: var(--ti3-score-item-height); + line-height: var(--ti3-score-item-height); + margin-right: 2px; + background-color: var(--ti-common-color-bg-light-normal); + + &:hover { + background-color: var(--ti-common-color-bg-light-emphasize); + } + + &.ti3-score-item-active { + background-color: var(--ti-common-color-bg-emphasize); + color: var(--ti-common-color-text-white); + } + + &:first-child { + border-radius: var(--ti-common-border-radius-normal) 0 0 var(--ti-common-border-radius-normal); + } + + &:last-child { + margin-right: 0; + border-radius: 0 var(--ti-common-border-radius-normal) var(--ti-common-border-radius-normal) 0; + } +} + +// 文本描述,使用flex布局,文本两端对齐 +.ti3-score-text { + display: flex; + justify-content: space-between; + color: var(--ti-common-color-text-weaken); + margin-bottom: var(--ti-common-space-2x); +} diff --git a/src/scroll/lib/index.ts b/src/scroll/lib/index.ts new file mode 100644 index 0000000..8922fe7 --- /dev/null +++ b/src/scroll/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiScrollModule'; diff --git a/src/scroll/lib/ng-package.json b/src/scroll/lib/ng-package.json new file mode 100644 index 0000000..f1d85f4 --- /dev/null +++ b/src/scroll/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/scroll", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/scroll/lib/package.json b/src/scroll/lib/package.json new file mode 100644 index 0000000..1a24b53 --- /dev/null +++ b/src/scroll/lib/package.json @@ -0,0 +1,9 @@ +{ + "name": "@opentiny/ng-scroll", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/scroll/lib/project.json b/src/scroll/lib/project.json new file mode 100644 index 0000000..5913177 --- /dev/null +++ b/src/scroll/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/scroll/lib", + "sourceRoot": "src/scroll/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/scroll"], + "options": { + "project": "src/scroll/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/scroll"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js scroll" + }, + { + "command": "ng default-build scroll" + }, + { + "command": "node build/clear-default-theme.js scroll" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/scroll && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build scroll && ng pack scroll && node build/publish.js scroll --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/scroll/lib/src/TiScrollDirective.ts b/src/scroll/lib/src/TiScrollDirective.ts new file mode 100644 index 0000000..38cf029 --- /dev/null +++ b/src/scroll/lib/src/TiScrollDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Renderer2 } from '@angular/core'; +import { Util } from '@opentiny/ng-utils'; +/** + * tiScroll指令用于触发document上的tiScroll事件。 + * + * + */ +@Directive({ + selector: '[tiScroll]' +}) +export class TiScrollDirective { + private hostEle: Element; + constructor(private hostEleRef: ElementRef, private renderer2: Renderer2) { + this.hostEle = this.hostEleRef.nativeElement; + this.renderer2.listen(this.hostEle, 'scroll', () => { + Util.trigger(document, 'tiScroll'); + }); + } +} diff --git a/src/scroll/lib/src/TiScrollModule.ts b/src/scroll/lib/src/TiScrollModule.ts new file mode 100644 index 0000000..035edf4 --- /dev/null +++ b/src/scroll/lib/src/TiScrollModule.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiScrollDirective } from './TiScrollDirective'; + +@NgModule({ + imports: [], + exports: [TiScrollDirective], + declarations: [TiScrollDirective] +}) +export class TiScrollModule {} + +export { TiScrollDirective } from './TiScrollDirective'; diff --git a/src/searchbox/demo/karma.conf.js b/src/searchbox/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/searchbox/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/searchbox/demo/project.json b/src/searchbox/demo/project.json new file mode 100644 index 0000000..9c2453b --- /dev/null +++ b/src/searchbox/demo/project.json @@ -0,0 +1,98 @@ +{ + "projectType": "application", + "root": "src/searchbox/demo", + "sourceRoot": "src/searchbox/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/searchbox", + "index": "src/searchbox/demo/src/index.html", + "main": "src/searchbox/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/searchbox/demo/tsconfig.app.json", + "assets": [ + "src/searchbox/demo/src/favicon.ico", + "src/searchbox/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/food", + "output": "/assets/food/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "searchbox-demo:build:production" + }, + "development": { + "browserTarget": "searchbox-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js searchbox" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/searchbox/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/searchbox/demo/tsconfig.spec.json", + "karmaConfig": "src/searchbox/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/searchbox/demo/src/app/AppComponent.ts b/src/searchbox/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/searchbox/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/searchbox/demo/src/app/AppModule.ts b/src/searchbox/demo/src/app/AppModule.ts new file mode 100644 index 0000000..e3fb720 --- /dev/null +++ b/src/searchbox/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SearchboxTestModule } from './searchbox/SearchboxTestModule'; + +@NgModule({ + imports: [ + SearchboxTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/searchbox/demo/src/app/IndexComponent.ts b/src/searchbox/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..4a6e56f --- /dev/null +++ b/src/searchbox/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SearchboxTestModule } from './searchbox/SearchboxTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SearchboxTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/searchbox/demo/src/app/app.html b/src/searchbox/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/searchbox/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxAppendtobodyComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxAppendtobodyComponent.ts new file mode 100644 index 0000000..76300db --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxAppendtobodyComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-appendtobody.html' +}) +export class SearchboxAppendtobodyComponent { + value: string = ''; + options: Array = [{ label: '华北' }, { label: '华南' }, { label: '西北' }, { label: '西南' }]; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxBasicComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxBasicComponent.ts new file mode 100644 index 0000000..8e69ba8 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-basic.html' +}) +export class SearchboxBasicComponent { + value: string = ''; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxDisabledComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxDisabledComponent.ts new file mode 100644 index 0000000..3d66391 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxDisabledComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-disabled.html' +}) +export class SearchboxDisabledComponent { + value: string = ''; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxEventComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxEventComponent.ts new file mode 100644 index 0000000..000eb61 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxEventComponent.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-event.html' +}) +export class SearchboxEventComponent { + myLogs: Array = []; + value: string = ''; + options: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; + onFocus(): void { + this.myLogs = [...this.myLogs, 'on focus']; + } + onBlur(): void { + this.myLogs = [...this.myLogs, 'on blur']; + } + onSearch(value: string): void { + this.myLogs = [...this.myLogs, `search value: ${value}`]; + } + onClear(event: MouseEvent): void { + this.myLogs = [...this.myLogs, 'on clear']; + } + onModelChange(value: string): void { + this.myLogs = [...this.myLogs, `modelChange: ${value}`]; + } + onSelect(option: any): void { + this.myLogs = [...this.myLogs, `select: ${JSON.stringify(option)}`]; + } +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxMaxlengthComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxMaxlengthComponent.ts new file mode 100644 index 0000000..529b18e --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxMaxlengthComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-maxlength.html' +}) +export class SearchboxMaxlengthComponent { + value: string = ''; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxNotsearchComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxNotsearchComponent.ts new file mode 100644 index 0000000..03e0c3f --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxNotsearchComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: 'searchbox-notsearch.html' +}) +export class SearchboxNotsearchComponent { + searchWord: string = ''; + myLogs: Array = []; + + modelChange(value: string): void { + this.myLogs = [...this.myLogs, `changed Value: ${value}`]; + } +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxOptionsComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxOptionsComponent.ts new file mode 100644 index 0000000..fcaa06c --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxOptionsComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiSearchboxComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './searchbox-options.html' +}) +export class SearchboxOptionsComponent { + value: string = ''; + options: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxPanelsizeComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxPanelsizeComponent.ts new file mode 100644 index 0000000..5d7e535 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxPanelsizeComponent.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-panelsize.html' +}) +export class SearchboxPanelsizeComponent { + value: string = ''; + options: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + }, + { + label: '东南' + }, + { + label: '东北' + } + ]; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxReactiveComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxReactiveComponent.ts new file mode 100644 index 0000000..08073e2 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxReactiveComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; + +@Component({ + templateUrl: './searchbox-reactive.html' +}) +export class SearchboxReactiveComponent { + placeholder: string = '请输入内容'; + formControl: FormControl = new FormControl(''); +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxSuggestComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxSuggestComponent.ts new file mode 100644 index 0000000..dbf505a --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxSuggestComponent.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; +import { TiSearchboxComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './searchbox-suggest.html' +}) +export class SearchboxSuggestComponent { + value: string = ''; + + onSuggest(searchbox: TiSearchboxComponent): void { + searchbox.setSuggestions(this.getSuggestion(searchbox.model)); + } + + private getSuggestion(value: string): Array { + const options: Array = value + ? [ + { + label: value + '@example.com' + }, + { + label: value + '@example.com' + }, + { + label: value + '@example.com' + } + ] + : []; + + return options; + } +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxTemplateComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxTemplateComponent.ts new file mode 100644 index 0000000..d83c036 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxTemplateComponent.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; +import { TiSearchboxComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './searchbox-template.html' +}) +export class SearchboxTemplateComponent { + value: string = ''; + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL']; + + onSuggest(searchbox: TiSearchboxComponent): void { + searchbox.setSuggestions(this.getSuggestion(searchbox.model)); + } + + private getSuggestion(value: string): Array { + const options: Array = value + ? [ + { + label: value + '@example.com', + url: `${this.baseUrl}assets/food/cake.png` + }, + { + label: value + '@example.com', + url: `${this.baseUrl}assets/food/coffee.png` + }, + { + label: value + '@example.com', + url: `${this.baseUrl}assets/food/cola.png` + } + ] + : []; + + return options; + } +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxTestComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxTestComponent.ts new file mode 100644 index 0000000..f237f23 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxTestComponent.ts @@ -0,0 +1,58 @@ +import { Component } from '@angular/core'; +import { TiSearchboxComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './searchbox-test.html' +}) +export class SearchboxTestComponent { + value: string = ''; + disabled: boolean = true; + value1: string = '2234244'; + placeholderText: string = '请输入关键字'; + maxlength: number = 10; + myLogs: Array = []; + private static getSuggestion(value: string): Array { + const options: Array = value + ? [ + { + label: value + '1' + }, + { + label: value + value + }, + { + label: value + value + value + } + ] + : [ + { + label: 'a' + }, + { + label: 'b' + }, + { + label: 'c' + } + ]; + + return options; + } + changeSearch(): void { + this.value1 = 'aaa'; + } + onFocus(): any { + this.myLogs = [...this.myLogs, 'on focus']; + } + onBlur(): any { + this.myLogs = [...this.myLogs, 'on blur']; + } + search = (value: string): any => { + this.myLogs = [...this.myLogs, `search value: ${value}`]; + }; + onInputChange(searchbox: TiSearchboxComponent): void { + setTimeout(() => { + searchbox.setSuggestions(SearchboxTestComponent.getSuggestion(searchbox.model)); + }, 500); + } +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxTestModule.ts b/src/searchbox/demo/src/app/searchbox/SearchboxTestModule.ts new file mode 100644 index 0000000..dec4b65 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxTestModule.ts @@ -0,0 +1,115 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiButtonModule, TiSearchboxModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { SearchboxTestComponent } from './SearchboxTestComponent'; +import { SearchboxNotsearchComponent } from './SearchboxNotsearchComponent'; +import { SearchboxBasicComponent } from './SearchboxBasicComponent'; +import { SearchboxEventComponent } from './SearchboxEventComponent'; +import { SearchboxPanelsizeComponent } from './SearchboxPanelsizeComponent'; +import { SearchboxTrimmedComponent } from './SearchboxTrimmedComponent'; +import { SearchboxValidComponent } from './SearchboxValidComponent'; +import { SearchboxDisabledComponent } from './SearchboxDisabledComponent'; +import { SearchboxAppendtobodyComponent } from './SearchboxAppendtobodyComponent'; +import { SearchboxReactiveComponent } from './SearchboxReactiveComponent'; +import { SearchboxOptionsComponent } from './SearchboxOptionsComponent'; +import { SearchboxSuggestComponent } from './SearchboxSuggestComponent'; +import { SearchboxMaxlengthComponent } from './SearchboxMaxlengthComponent'; +import { SearchboxTemplateComponent } from './SearchboxTemplateComponent'; +import { SearchboxVirtualscrollComponent } from './SearchboxVirtualscrollComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiButtonModule, + TiSearchboxModule, + TiValidationModule, + DemoLogModule, + RouterModule.forChild(SearchboxTestModule.ROUTES) + ], + declarations: [ + SearchboxBasicComponent, + SearchboxSuggestComponent, + SearchboxPanelsizeComponent, + SearchboxTrimmedComponent, + SearchboxValidComponent, + SearchboxDisabledComponent, + SearchboxEventComponent, + SearchboxNotsearchComponent, + SearchboxAppendtobodyComponent, + SearchboxTestComponent, + SearchboxReactiveComponent, + SearchboxOptionsComponent, + SearchboxMaxlengthComponent, + SearchboxTemplateComponent, + SearchboxVirtualscrollComponent + ] +}) +export class SearchboxTestModule { + static readonly LINKS: Array = [{ href: 'components/TiSearchboxComponent.html', label: 'Searchbox' }]; + static readonly ROUTES: Routes = [ + { + path: 'searchbox/searchbox-basic', + component: SearchboxBasicComponent + }, + { + path: 'searchbox/searchbox-template', + component: SearchboxTemplateComponent + }, + { + path: 'searchbox/searchbox-reactive', + component: SearchboxReactiveComponent + }, + { + path: 'searchbox/searchbox-options', + component: SearchboxOptionsComponent + }, + { + path: 'searchbox/searchbox-suggest', + component: SearchboxSuggestComponent + }, + { + path: 'searchbox/searchbox-maxlength', + component: SearchboxMaxlengthComponent + }, + { + path: 'searchbox/searchbox-panelsize', + component: SearchboxPanelsizeComponent + }, + { + path: 'searchbox/searchbox-trimmed', + component: SearchboxTrimmedComponent + }, + { + path: 'searchbox/searchbox-valid', + component: SearchboxValidComponent + }, + { + path: 'searchbox/searchbox-virtualscroll', + component: SearchboxVirtualscrollComponent + }, + { + path: 'searchbox/searchbox-disabled', + component: SearchboxDisabledComponent + }, + { + path: 'searchbox/searchbox-event', + component: SearchboxEventComponent + }, + { + path: 'searchbox/searchbox-appendtobody', + component: SearchboxAppendtobodyComponent + }, + { + path: 'searchbox/searchbox-notsearch', + component: SearchboxNotsearchComponent + }, + { path: 'searchbox/searchbox-test', component: SearchboxTestComponent } + ]; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxTrimmedComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxTrimmedComponent.ts new file mode 100644 index 0000000..be5bc88 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxTrimmedComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-trimmed.html' +}) +export class SearchboxTrimmedComponent { + value: string; + searchContent: string = ''; + onSearch(value: string): void { + this.searchContent = value; + } + onModelChange(value: string): void { + this.searchContent = value; + } +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxValidComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxValidComponent.ts new file mode 100644 index 0000000..6abe999 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxValidComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-valid.html' +}) +export class SearchboxValidComponent { + value: string = ''; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxVirtualscrollComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxVirtualscrollComponent.ts new file mode 100644 index 0000000..0bb60c6 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxVirtualscrollComponent.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; +import { TiSearchboxComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './searchbox-virtualscroll.html' +}) +export class SearchboxVirtualscrollComponent { + value: string = ''; + suggestions: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; + + onSuggest(searchbox: TiSearchboxComponent): void { + // 模拟后台异步请求 + setTimeout(() => { + searchbox.setSuggestions(this.getSuggestion(searchbox.model)); + }, 200); + } + + private getSuggestion(value: string): Array { + const options: Array = []; + + for (let i: number = 0; i < 10000; i++) { + const item: any = this.suggestions[i % 4]; + options.push({ label: `${item.label}${i}${value}` }); + } + + return options; + } +} diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-appendtobody.html b/src/searchbox/demo/src/app/searchbox/searchbox-appendtobody.html new file mode 100644 index 0000000..bbcb727 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-appendtobody.html @@ -0,0 +1,7 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-basic.html b/src/searchbox/demo/src/app/searchbox/searchbox-basic.html new file mode 100644 index 0000000..8337200 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-basic.html @@ -0,0 +1 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-disabled.html b/src/searchbox/demo/src/app/searchbox/searchbox-disabled.html new file mode 100644 index 0000000..3a4297d --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-disabled.html @@ -0,0 +1 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-event.html b/src/searchbox/demo/src/app/searchbox/searchbox-event.html new file mode 100644 index 0000000..a207a9d --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-event.html @@ -0,0 +1,14 @@ + + + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-maxlength.html b/src/searchbox/demo/src/app/searchbox/searchbox-maxlength.html new file mode 100644 index 0000000..2204d13 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-maxlength.html @@ -0,0 +1 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-notsearch.html b/src/searchbox/demo/src/app/searchbox/searchbox-notsearch.html new file mode 100644 index 0000000..1e6fa98 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-notsearch.html @@ -0,0 +1,7 @@ +

    描述

    +

    下拉组件中的搜索框组件,设置noBorder属性

    + + +
    +

    事件日志:

    + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-options.html b/src/searchbox/demo/src/app/searchbox/searchbox-options.html new file mode 100644 index 0000000..837cda5 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-options.html @@ -0,0 +1,5 @@ + + +
    +
    Current Value: {{ value }}
    +
    diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-panelsize.html b/src/searchbox/demo/src/app/searchbox/searchbox-panelsize.html new file mode 100644 index 0000000..f13f01a --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-panelsize.html @@ -0,0 +1,8 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-reactive.html b/src/searchbox/demo/src/app/searchbox/searchbox-reactive.html new file mode 100644 index 0000000..c26ff07 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-reactive.html @@ -0,0 +1 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-suggest.html b/src/searchbox/demo/src/app/searchbox/searchbox-suggest.html new file mode 100644 index 0000000..6ff5730 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-suggest.html @@ -0,0 +1,5 @@ + + +
    +
    Current Value: {{ value }}
    +
    diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-template.html b/src/searchbox/demo/src/app/searchbox/searchbox-template.html new file mode 100644 index 0000000..a726933 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-template.html @@ -0,0 +1,8 @@ + + + {{i}}{{item.label}} + + +
    +
    Current Value: {{ value }}
    +
    diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-test.html b/src/searchbox/demo/src/app/searchbox/searchbox-test.html new file mode 100644 index 0000000..e024043 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-test.html @@ -0,0 +1,37 @@ +

    描述

    +

    Searchbox组件内部测试示例

    +

    示例

    +
    +

    用在表单中(第一个form)--模板驱动表单

    +
    +
    +
    0. 1)不展示下拉建议项,仅在回车键及点击事件后触发搜索,弹出框展示搜索内容;2)focus/blur时做日志打印; 3) 最大字符长度为10
    +
    + + {{value}}

    + +
    1.根据输入实时搜索内容:
    +
    + + {{value1}} +
    +

    form's touched: {{form.touched | json}}

    +

    form's untouched: {{form.untouched | json}}

    +
    {{form.value | json}}
    + +
    +

    事件日志:

    + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-trimmed.html b/src/searchbox/demo/src/app/searchbox/searchbox-trimmed.html new file mode 100644 index 0000000..4122e63 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-trimmed.html @@ -0,0 +1,13 @@ + + +
    +
    Current Value: {{ searchContent }}
    +
    Value length: {{ searchContent.length }}
    +
    diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-valid.html b/src/searchbox/demo/src/app/searchbox/searchbox-valid.html new file mode 100644 index 0000000..b0dc476 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-valid.html @@ -0,0 +1 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-virtualscroll.html b/src/searchbox/demo/src/app/searchbox/searchbox-virtualscroll.html new file mode 100644 index 0000000..6ad67bd --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-virtualscroll.html @@ -0,0 +1 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/webdoc/searchbox-demos.js b/src/searchbox/demo/src/app/searchbox/webdoc/searchbox-demos.js new file mode 100644 index 0000000..ae1fc9a --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/webdoc/searchbox-demos.js @@ -0,0 +1,169 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'searchbox-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': '

    Searchbox 组件的最简用法。

    ', + 'en-US': '

    button color

    ' + }, + apis: ['TiSearchboxComponent.properties.placeholder'] + }, + { + demoId: 'searchbox-template', + name: { + 'zh-CN': '下拉建议项模板', + 'en-US': 'template' + }, + desc: { + 'zh-CN': '

    自定义下拉建议项模板。

    ', + 'en-US': '

    button color

    ' + }, + apis: ['TiSearchboxComponent.slots.itemTemplate'] + }, + { + demoId: 'searchbox-trimmed', + name: { + 'zh-CN': '删除搜索内容的首尾空格', + 'en-US': 'trimmed' + }, + desc: { + 'zh-CN': '

    通过trimmed配置是否删除用户搜索内容的首尾空格,不配置时不删除空格。

    ', + 'en-US': '

    trimmed

    ' + }, + apis: ['TiSearchboxComponent.properties.trimmed'] + }, + { + demoId: 'searchbox-options', + name: { + 'zh-CN': '固定下拉建议项', + 'en-US': 'options' + }, + desc: { + 'zh-CN': '

    通过options配置下拉建议项。

    ', + 'en-US': '

    options

    ' + }, + apis: ['TiSearchboxComponent.properties.options'] + }, + { + demoId: 'searchbox-appendtobody', + name: { + 'zh-CN': '附着在body上', + 'en-US': 'appendtobody' + }, + desc: { + 'zh-CN': + '

    通过appendToBody配置下拉面板是否添加在 body 上,在有局部滚动条的场景下,如果配置成 false,下拉面板和选择框不会分离。

    ', + 'en-US': '

    appendtobody

    ' + }, + apis: ['TiSearchboxComponent.properties.appendToBody'] + }, + { + demoId: 'searchbox-suggest', + name: { + 'zh-CN': '动态加载下拉项', + 'en-US': 'item' + }, + desc: { + 'zh-CN': '

    通过suggest事件配置当聚焦或值改变时触发事件,为开发者提供设置建议项的时机。

    ', + 'en-US': '

    item

    ' + }, + apis: ['TiSearchboxComponent.events.suggest', 'TiSearchboxComponent.methods.setSuggestions'] + }, + { + demoId: 'searchbox-maxlength', + name: { + 'zh-CN': '文本最大显示长度', + 'en-US': 'item' + }, + desc: { + 'zh-CN': '

    通过maxlength配置最大文本输入长度。

    ', + 'en-US': '

    item

    ' + }, + apis: ['TiSearchboxComponent.properties.maxlength'] + }, + { + demoId: 'searchbox-panelsize', + name: { + 'zh-CN': '下拉面板大小', + 'en-US': 'item' + }, + desc: { + 'zh-CN': '

    通过panelWidth配置下拉框的宽度。通过panelMaxHeight配置面板最大高度。

    ', + 'en-US': '

    item

    ' + }, + apis: ['TiSearchboxComponent.properties.panelWidth', 'TiSearchboxComponent.properties.panelMaxHeight'] + }, + { + demoId: 'searchbox-reactive', + name: { + 'zh-CN': '响应式表单', + 'en-US': 'item' + }, + desc: { + 'zh-CN': '

    响应式表单的基本用法。

    ', + 'en-US': '

    item

    ' + }, + apis: ['TiSearchboxComponent.properties.placeholder'] + }, + { + demoId: 'searchbox-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'item' + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': '

    item

    ' + }, + apis: ['TiSearchboxComponent.properties.disabled'] + }, + { + demoId: 'searchbox-valid', + name: { + 'zh-CN': '表单校验', + 'en-US': 'item' + }, + desc: { + 'zh-CN': '

    表单校验的用法。

    ', + 'en-US': '

    item

    ' + } + }, + { + demoId: 'searchbox-virtualscroll', + name: { + 'zh-CN': '虚拟滚动', + 'en-US': 'virtualscroll' + }, + desc: { + 'zh-CN': '

    通过属性virtual配置组件是否开启虚拟滚动,一般用于下拉数据量大的场景。

    ', + 'en-US': '' + }, + apis: ['TiSearchboxComponent.properties.virtual'] + }, + { + demoId: 'searchbox-event', + name: { + 'zh-CN': '事件', + 'en-US': 'item' + }, + desc: { + 'zh-CN': + '

    当输入框聚焦按下 enter 键、点击下拉项、点击搜索按钮的时候触发search事件;当点击叉号的时候触发clear事件;当点击下拉项的时候触发select事件;当聚焦的时候触发focus事件;当失焦的时候触发blur事件。

    ', + 'en-US': '

    item

    ' + }, + apis: [ + 'TiSearchboxComponent.events.search', + 'TiSearchboxComponent.events.clear', + 'TiSearchboxComponent.events.select', + 'TiSearchboxComponent.events.blur', + 'TiSearchboxComponent.events.focus' + ] + } + ] +}; diff --git a/src/searchbox/demo/src/app/searchbox/webdoc/searchbox.cn.md b/src/searchbox/demo/src/app/searchbox/webdoc/searchbox.cn.md new file mode 100644 index 0000000..fa15e72 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/webdoc/searchbox.cn.md @@ -0,0 +1,23 @@ +--- +title: Searchbox 搜索框 +--- +# Searchbox 搜索框 + +
    + +Searchbox 是提供搜索数据的组件。   + +```typescript +import { TiSearchboxModule } from '@opentiny/ng'; +``` + +
    + +
    + +Searchbox 是提供搜索数据的组件。   + +```typescript +import { TiSearchboxModule } from '@opentiny/ng'; +``` +
    diff --git a/src/searchbox/demo/src/app/searchbox/webdoc/searchbox.en.md b/src/searchbox/demo/src/app/searchbox/webdoc/searchbox.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/webdoc/searchbox.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/searchbox/demo/src/favicon.ico b/src/searchbox/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/searchbox/demo/src/index.html b/src/searchbox/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/searchbox/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/searchbox/demo/src/main.ts b/src/searchbox/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/searchbox/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/searchbox/demo/test.ts b/src/searchbox/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/searchbox/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/searchbox/demo/tsconfig.app.json b/src/searchbox/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/searchbox/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/searchbox/demo/tsconfig.spec.json b/src/searchbox/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/searchbox/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/searchbox/lib/index.ts b/src/searchbox/lib/index.ts new file mode 100644 index 0000000..3db3e3f --- /dev/null +++ b/src/searchbox/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSearchboxModule'; diff --git a/src/searchbox/lib/ng-package.json b/src/searchbox/lib/ng-package.json new file mode 100644 index 0000000..5f866fa --- /dev/null +++ b/src/searchbox/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/searchbox", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/searchbox/lib/package.json b/src/searchbox/lib/package.json new file mode 100644 index 0000000..69404bb --- /dev/null +++ b/src/searchbox/lib/package.json @@ -0,0 +1,18 @@ +{ + "name": "@opentiny/ng-searchbox", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-autocomplete": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-droplist": "~1.0.0-beta.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/searchbox/lib/project.json b/src/searchbox/lib/project.json new file mode 100644 index 0000000..9f7088d --- /dev/null +++ b/src/searchbox/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/searchbox/lib", + "sourceRoot": "src/searchbox/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/searchbox"], + "options": { + "project": "src/searchbox/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/searchbox"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js searchbox" + }, + { + "command": "ng default-build searchbox" + }, + { + "command": "node build/clear-default-theme.js searchbox" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/searchbox && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build searchbox && ng pack searchbox && node build/publish.js searchbox --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/searchbox/lib/src/TiSearchboxComponent.ts b/src/searchbox/lib/src/TiSearchboxComponent.ts new file mode 100644 index 0000000..ac4f18a --- /dev/null +++ b/src/searchbox/lib/src/TiSearchboxComponent.ts @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from '@angular/core'; +import { TiAutocompleteComponent } from '@opentiny/ng-autocomplete'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * 搜索框组件 + * + */ +@Component({ + selector: 'ti-searchbox', + templateUrl: './searchbox.html', + styleUrls: ['./searchbox.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiSearchboxComponent)], + host: { + '[class.ti3-searchbox-container]': 'true', + '(blur)': 'onBlur()', + '(focus)': 'onFocus()' + } +}) +export class TiSearchboxComponent extends TiAutocompleteComponent { + /** + * 是否删除搜索内容的首尾空格 + */ + @Input() trimmed: boolean = false; + /** + * 是否开启虚拟滚动 + */ + @Input() virtual: boolean = false; + /** + * 当选中下拉选项、按 enter 键或者点击搜索图标时触发的回调,参数:搜索内容 + */ + @Output() readonly search: EventEmitter = new EventEmitter(); + + /** + * @ignore + * input ngModel绑定值 + */ + public inputValue: string; + private isDroplistSearch: boolean = false; // 是否是droplist触发搜索 + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + // 默认有清除图标且不可配置:此处先赋值后继承,是由于父类中初始化需要读取clearable的值,设置清除图标 + this.clearable = true; + super.ngOnInit(); + } + + /** + * @ignore + * 组件model更改时,更新input绑定值 + */ + writeValue(model: string) { + super.writeValue(model); + this.inputValue = model; + } + + // 组件交互方法集合--start + + /** + * @ignore + * enter的触发有两种情况: + * 1.在input框中输入值,然后按下enter,此时正常执行search + * 2.在suggestion面板展开的情况下,通过hover选中一项,然后按下enter, + * 此时的search应该在下拉面板的onDroplistChange回调中触发 + */ + public onInputEnter(): void { + if (this.isDroplistSearch) { + // 排除第2中情况 + this.isDroplistSearch = false; + + return; + } + + this.onSearch(); + } + /** + * @ignore + * 搜索事件触发 + */ + public onSearch(): void { + if (this.disabled) { + return; + } + this.search.emit(this.model); + } + /** + * @ignore + * 两种情况下触发 + * 1.在suggestion面板展开的情况下,通过hover选中一项,然后按下enter + * 2.在suggestion面板展开的情况下,通过鼠标点击选中一项 + */ + onDroplistChange(value: { label: string }): void { + if (value) { + this.model = this.inputValue = value[this.labelKey] || value.label; + this.onSearch(); + this.isDroplistSearch = true; + } + } + // 组件交互方法集合--end + + /** + * @ignore + * 输入框中内容改变事件 + */ + public onInputChange(value: string): void { + if (this.disabled || !this.isFocused) { + return; + } + + const searchValue: string = this.trimValue(value); + this.model = searchValue; + this.inputChangeObserve.next(searchValue); + } + + /** + * 控制是否删除搜索内容的首尾空格 + */ + private trimValue(value: string): string { + let searchValue: string = value; + if (this.trimmed && searchValue) { + searchValue = searchValue.trim(); + } + + return searchValue; + } +} diff --git a/src/searchbox/lib/src/TiSearchboxModule.ts b/src/searchbox/lib/src/TiSearchboxModule.ts new file mode 100644 index 0000000..af6a686 --- /dev/null +++ b/src/searchbox/lib/src/TiSearchboxModule.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiSearchboxComponent } from './TiSearchboxComponent'; +import { FormsModule } from '@angular/forms'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiDroplistModule } from '@opentiny/ng-droplist'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiSearchboxNotsearchComponent } from './TiSearchboxNotsearchComponent'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiOverflowModule, TiDroplistModule, TiIconModule, TiTextModule, TiOutlineModule], + exports: [TiSearchboxComponent, TiSearchboxNotsearchComponent], + declarations: [TiSearchboxComponent, TiSearchboxNotsearchComponent] +}) +export class TiSearchboxModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiSearchboxComponent } from './TiSearchboxComponent'; +export { TiSearchboxNotsearchComponent } from './TiSearchboxNotsearchComponent'; diff --git a/src/searchbox/lib/src/TiSearchboxNotsearchComponent.ts b/src/searchbox/lib/src/TiSearchboxNotsearchComponent.ts new file mode 100644 index 0000000..0ad044a --- /dev/null +++ b/src/searchbox/lib/src/TiSearchboxNotsearchComponent.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { TiSearchboxComponent } from './TiSearchboxComponent'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiLocale } from '@opentiny/ng-locale'; + +/** + * @ignore + */ +@Component({ + selector: 'ti-searchbox-notsearch', + templateUrl: './searchbox.html', + styleUrls: ['./searchbox.less', './searchbox-notsearch.less', '../../../text/lib/src/text.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiSearchboxNotsearchComponent)], + host: { + '[class.ti3-searchbox-container]': 'true', + '(blur)': 'onBlur()', + '(focus)': 'onFocus()' + } +}) +export class TiSearchboxNotsearchComponent extends TiSearchboxComponent { + /** + * @ignore + */ + public placeholder: string = TiLocale.getLocaleWords().tiSearchbox.search; + onInputEnter(): void { + event.preventDefault(); // 不做搜索等处理 + } + onSearch(): void { + event.preventDefault(); // 点击搜索图标时,阻止默认行为 + } +} diff --git a/src/searchbox/lib/src/i18n/TiSearchboxWords.ts b/src/searchbox/lib/src/i18n/TiSearchboxWords.ts new file mode 100644 index 0000000..63a1bf1 --- /dev/null +++ b/src/searchbox/lib/src/i18n/TiSearchboxWords.ts @@ -0,0 +1,5 @@ +export interface TiSearchboxWords { + tiSearchbox: { + search: string; + }; +} diff --git a/src/searchbox/lib/src/i18n/en_US.ts b/src/searchbox/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..9f8e65d --- /dev/null +++ b/src/searchbox/lib/src/i18n/en_US.ts @@ -0,0 +1,7 @@ +import { TiSearchboxWords } from './TiSearchboxWords'; + +export const en_US: TiSearchboxWords = { + tiSearchbox: { + search: 'Search' + } +}; diff --git a/src/searchbox/lib/src/i18n/es_US.ts b/src/searchbox/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..b0bd5f5 --- /dev/null +++ b/src/searchbox/lib/src/i18n/es_US.ts @@ -0,0 +1,7 @@ +import { TiSearchboxWords } from './TiSearchboxWords'; + +export const es_US: TiSearchboxWords = { + tiSearchbox: { + search: 'Buscar' + } +}; diff --git a/src/searchbox/lib/src/i18n/fr_FR.ts b/src/searchbox/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..37af243 --- /dev/null +++ b/src/searchbox/lib/src/i18n/fr_FR.ts @@ -0,0 +1,7 @@ +import { TiSearchboxWords } from './TiSearchboxWords'; + +export const fr_FR: TiSearchboxWords = { + tiSearchbox: { + search: 'Recherchez' + } +}; diff --git a/src/searchbox/lib/src/i18n/index.ts b/src/searchbox/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/searchbox/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/searchbox/lib/src/i18n/pt_BR.ts b/src/searchbox/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..bf58af4 --- /dev/null +++ b/src/searchbox/lib/src/i18n/pt_BR.ts @@ -0,0 +1,7 @@ +import { TiSearchboxWords } from './TiSearchboxWords'; + +export const pt_BR: TiSearchboxWords = { + tiSearchbox: { + search: 'Pesquisar' + } +}; diff --git a/src/searchbox/lib/src/i18n/zh_CN.ts b/src/searchbox/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..011fd08 --- /dev/null +++ b/src/searchbox/lib/src/i18n/zh_CN.ts @@ -0,0 +1,7 @@ +import { TiSearchboxWords } from './TiSearchboxWords'; + +export const zh_CN: TiSearchboxWords = { + tiSearchbox: { + search: '搜索' + } +}; diff --git a/src/searchbox/lib/src/searchbox-notsearch.less b/src/searchbox/lib/src/searchbox-notsearch.less new file mode 100644 index 0000000..0b581db --- /dev/null +++ b/src/searchbox/lib/src/searchbox-notsearch.less @@ -0,0 +1,26 @@ +@import '../../../themes/basic/base-all.less'; +// 定义下拉组件中搜索框的样式 +:host.ti3-searchbox-container:not([disabled])[noBorder] { + background-color: var(--ti-common-color-bg-white-emphasize); + border: none; + &:hover { + background-color: var(--ti-common-color-bg-normal); + } + .ti3-searchbox-input { + background-color: transparent; + } +} +:host.ti3-searchbox-container:not([disabled]) .ti3-searchbox-search:hover { + // 覆盖./searchbox.less中的鼠标悬浮效果 + color: var(--ti-common-color-icon-normal); + cursor: auto; +} + +:host ::ng-deep .ti3-searchbox-search[noBorder] { + color: var(--ti-common-color-icon-normal) !important; + &:hover { + color: var(--ti-common-color-icon-hover) !important; + } +} +// CSS标准中 /deep/ >>>刺穿Shadow DOM, 已废弃。所以,这里暂时用::ng-deep angular关键词,以兼容未来可能其他标准。 +// https://blog.csdn.net/sky_sunshine_x/article/details/80622617 diff --git a/src/searchbox/lib/src/searchbox.html b/src/searchbox/lib/src/searchbox.html new file mode 100644 index 0000000..5781f0f --- /dev/null +++ b/src/searchbox/lib/src/searchbox.html @@ -0,0 +1,47 @@ + +
    + + + + + + + {{item[labelKey]}} + + diff --git a/src/searchbox/lib/src/searchbox.less b/src/searchbox/lib/src/searchbox.less new file mode 100644 index 0000000..fcebf7b --- /dev/null +++ b/src/searchbox/lib/src/searchbox.less @@ -0,0 +1,101 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-searchbox-container-height: var(--ti-common-size-7x); + --ti-searchbox-icon-size: 14px; + --ti-searchbox-icon-area-width: 34px; + --ti-searchbox-divider-width: 1px; + --ti-searchbox-divider-height: calc(var(--ti-searchbox-container-height) / 2); +} + +// 1.先定义组件通用样式,与hover、focused、disabled等状态无关; +:host.ti3-searchbox-container { + position: relative; + border-radius: var(--ti-common-border-radius-normal); + width: var(--ti-common-size-50x); + height: var(--ti-searchbox-container-height); + line-height: normal; + display: inline-block; + vertical-align: middle; + text-align: left; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid); + .box-sizing(border-box); + cursor: pointer; + &:after { + content: ''; + display: inline-block; + height: 100%; + line-height: 100%; + vertical-align: middle; + } + & .ti3-searchbox-input { + // 覆盖ti-text组件默认样式 + height: 100%; + width: calc(100% - var(--ti-searchbox-icon-area-width) - var(--ti-searchbox-divider-width)); + box-sizing: border-box; + border: none !important; // 加important是为了避免被ti-text已有的disable状态下的border样式覆盖 + float: left; + padding: 0 0 0 var(--ti-common-space-10); + text-decoration: none; + outline: none; + box-shadow: none; + background-color: transparent; // 添加透明背景是为了覆盖tiText设置的白色背景,使校验结果样式生效 + &[disabled] { + background-color: transparent; // 添加透明背景是为了覆盖tiText设置的禁用背景,避免配置不一致呈现色块 + } + } + & .ti3-searchbox-divider { + position: absolute; + right: var(--ti-searchbox-icon-area-width); + vertical-align: middle; + height: var(--ti-searchbox-divider-height); + top: calc(50% - var(--ti-searchbox-divider-height) / 2); + width: var(--ti-searchbox-divider-width); + } + & .ti3-searchbox-search { + width: var(--ti-searchbox-icon-area-width); + border-radius: 0 3px 3px 0; + height: 100%; + float: right; + text-align: center; + font-size: var(--ti-searchbox-icon-size); + &:before { + position: relative; + top: calc(50% - var(--ti-searchbox-icon-size) / 2); + } + } +} + +// 2.定义enabled状态下的样式,包括hover、focused等状态 +:host.ti3-searchbox-container:not([disabled]) { + background-color: var(--ti-common-color-bg-white-normal); + border-color: var(--ti-common-color-line-normal); + &:hover { + border-color: var(--ti-common-color-line-hover); + } + &[tiFocused] { + border-color: var(--ti-common-color-line-active); + } + & .ti3-searchbox-divider { + background-color: var(--ti-common-color-line-dividing); + } + & .ti3-searchbox-search { + color: var(--ti-common-color-icon-normal); + &:hover { + color: var(--ti-common-color-icon-hover); + } + } +} + +// 3.定义disabled状态下的样式 +:host.ti3-searchbox-container[disabled] { + background-color: var(--ti-common-color-bg-disabled); + border-color: var(--ti-common-color-line-disabled); + cursor: not-allowed; + & .ti3-searchbox-divider { + background-color: var(--ti-common-color-line-disabled); + } + & .ti3-searchbox-search { + color: var(--ti-common-color-icon-disabled); + } +} diff --git a/src/select/demo/karma.conf.js b/src/select/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/select/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/select/demo/project.json b/src/select/demo/project.json new file mode 100644 index 0000000..34bdc8c --- /dev/null +++ b/src/select/demo/project.json @@ -0,0 +1,98 @@ +{ + "projectType": "application", + "root": "src/select/demo", + "sourceRoot": "src/select/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/select", + "index": "src/select/demo/src/index.html", + "main": "src/select/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/select/demo/tsconfig.app.json", + "assets": [ + "src/select/demo/src/favicon.ico", + "src/select/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/food", + "output": "/assets/food/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "select-demo:build:production" + }, + "development": { + "browserTarget": "select-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js select" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/select/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/select/demo/tsconfig.spec.json", + "karmaConfig": "src/select/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/select/demo/src/app/AppComponent.ts b/src/select/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/select/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/select/demo/src/app/AppModule.ts b/src/select/demo/src/app/AppModule.ts new file mode 100644 index 0000000..273c010 --- /dev/null +++ b/src/select/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SelectTestModule } from './select/SelectTestModule'; + +@NgModule({ + imports: [ + SelectTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/select/demo/src/app/IndexComponent.ts b/src/select/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..f24dbd5 --- /dev/null +++ b/src/select/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SelectTestModule } from './select/SelectTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SelectTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/select/demo/src/app/app.html b/src/select/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/select/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/select/demo/src/app/select/NoEmptyPipe.ts b/src/select/demo/src/app/select/NoEmptyPipe.ts new file mode 100644 index 0000000..e4893e6 --- /dev/null +++ b/src/select/demo/src/app/select/NoEmptyPipe.ts @@ -0,0 +1,9 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { Util } from '@opentiny/ng'; + +@Pipe({ name: 'noempty' }) +export class NoEmptyPipe implements PipeTransform { + transform(value: Array): Array { + return value.filter((item: any) => !Util.isEmptyString(item.label)); + } +} diff --git a/src/select/demo/src/app/select/SelectAppendtobodyComponent.ts b/src/select/demo/src/app/select/SelectAppendtobodyComponent.ts new file mode 100644 index 0000000..af76c42 --- /dev/null +++ b/src/select/demo/src/app/select/SelectAppendtobodyComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './select-appendtobody.html' +}) +export class SelectAppendtobodyComponent { + options: Array = [{ label: '美国' }, { label: '巴西' }, { label: '加拿大' }, { label: '中国' }, { label: '法国' }]; + value: any; +} diff --git a/src/select/demo/src/app/select/SelectBasicComponent.ts b/src/select/demo/src/app/select/SelectBasicComponent.ts new file mode 100644 index 0000000..2f8b7ac --- /dev/null +++ b/src/select/demo/src/app/select/SelectBasicComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-basic.html' +}) +export class SelectBasicComponent { + options: Array = [ + { + label: '中国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + } + ]; + + value: any = this.options[0]; +} diff --git a/src/select/demo/src/app/select/SelectBeforesearchComponent.ts b/src/select/demo/src/app/select/SelectBeforesearchComponent.ts new file mode 100644 index 0000000..6aa13d9 --- /dev/null +++ b/src/select/demo/src/app/select/SelectBeforesearchComponent.ts @@ -0,0 +1,60 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiSelectComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './select-beforesearch.html' +}) +export class SelectBeforesearchComponent { + constructor(private changeDetectorRef: ChangeDetectorRef) {} + options: Array = []; + value: any; + baseOptions: Array = [ + { label: '美国' }, + { label: '巴西' }, + { label: '加拿大' }, + { label: '中国' }, + { label: '法国' }, + { label: '德国' }, + { label: '日本' }, + { label: '韩国' }, + { label: '土耳其' }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + ngOnInit(): void { + this.options = [...this.baseOptions].slice(0, 3); + this.value = this.options[2]; + } + + onBeforeSearch(selectComp: TiSelectComponent): void { + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + this.getData(this.baseOptions, searchWord).then((result: any) => { + // 设置搜索结果 + this.options = result.data; + }); + } + + // 模拟异步请求 + private getData(source: Array, searchWord: string): Promise { + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + if (searchWord) { + const filteredResult: Array = source.filter( + (item: any) => item.label.toLowerCase().indexOf(searchWord.toLowerCase()) !== -1 + ); + resolve({ data: filteredResult }); + } else { + resolve({ + data: [...source.slice(0, 3)] + }); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef.markForCheck() + // 默认模式下不需要 + this.changeDetectorRef.markForCheck(); + }, 500); + }); + } +} diff --git a/src/select/demo/src/app/select/SelectBeforesearchTestComponent.ts b/src/select/demo/src/app/select/SelectBeforesearchTestComponent.ts new file mode 100644 index 0000000..6cef607 --- /dev/null +++ b/src/select/demo/src/app/select/SelectBeforesearchTestComponent.ts @@ -0,0 +1,123 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiSelectComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './select-beforesearch.html' +}) +export class SelectBeforesearchTestComponent { + options: Array = []; + myOptions1: Array = []; + value: any; + mySelecteds1: Array; + + baseOptions: Array = [ + { label: '美国' }, + { label: '巴西' }, + { label: '加拿大' }, + { label: '中国' }, + { label: '法国' }, + { label: '德国' }, + { label: '日本' }, + { label: '韩国' }, + { label: '土耳其' }, + { label: '大不列颠和北爱兰联合王国' } + ]; + + baseOptions1: Array = [ + { label: '美国' }, + { + label: '巴西', + disabled: true + }, + { label: '加拿大' }, + { label: '中国' }, + { + label: '法国', + disabled: true + }, + { + label: '德国', + disabled: true + }, + { label: '日本' }, + { label: '韩国' }, + { + label: '土耳其', + disabled: true + }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + changedOptions: Array = [ + { label: 'America' }, + { label: 'Brazil' }, + { label: 'Canada' }, + { label: 'China' }, + { label: 'France' }, + { label: 'Germany' }, + { label: 'Japan' }, + { label: 'South Korea' }, + { label: 'Turkey' }, + { label: 'United Kingdom' } + ]; + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + ngOnInit(): void { + this.options = [...this.baseOptions].slice(0, 3); + this.myOptions1 = [...this.baseOptions1].slice(0, 3); + this.value = this.options[2]; + this.mySelecteds1 = [this.myOptions1[1], this.myOptions1[2]]; + } + + onBeforeSearch(selectComp: TiSelectComponent): void { + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + this.getData(this.baseOptions, searchWord).then((result: any) => { + // 设置搜索结果 + this.options = result.data; + }); + } + onBeforeSearch1(selectComp: TiSelectComponent): void { + const searchWord: string = selectComp.getSearchWord(); + this.getData(this.baseOptions1, searchWord).then((result: any) => { + // 设置搜索结果 + this.myOptions1 = result.data; + }); + } + + changeOptions(): void { + this.baseOptions = this.changedOptions; + this.options = [...this.baseOptions].slice(0, 3); + this.value = undefined; + } + + changeOptions1(): void { + this.baseOptions1 = this.changedOptions; + this.myOptions1 = [...this.baseOptions1].slice(0, 3); + this.mySelecteds1 = undefined; + } + + // 模拟异步请求 + private getData(source: Array, searchWord: string): Promise { + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + if (searchWord) { + const filteredResult: Array = source.filter( + (item: any) => item.label.toLowerCase().indexOf(searchWord.toLowerCase()) !== -1 + ); + resolve({ data: filteredResult }); + } else { + resolve({ + data: [...source.slice(0, 3)] + }); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef.markForCheck() + // 默认模式下不需要 + // 服务可根据自身变化检测策略决定是否使用该方法 + this.changeDetectorRef.markForCheck(); + }, 500); + }); + } +} diff --git a/src/select/demo/src/app/select/SelectChangeSelectallComponent.ts b/src/select/demo/src/app/select/SelectChangeSelectallComponent.ts new file mode 100644 index 0000000..c5fa83e --- /dev/null +++ b/src/select/demo/src/app/select/SelectChangeSelectallComponent.ts @@ -0,0 +1,126 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-change-selectall.html' +}) +export class SelectChangeSelectallComponent { + selectAll: boolean = true; + + myOptions: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + mySelecteds: any = [this.myOptions[3]]; + + myGroupOptions: Array = [ + { + label: '北美洲', + children: [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + } + ] + }, + { + label: '南美洲', + children: [ + { + label: '巴西', + englishname: 'Brazil', + disabled: false + } + ] + }, + { + label: '亚洲', + children: [ + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + } + ] + }, + { + label: '欧洲', + children: [ + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ] + } + ]; + myGroupSelecteds: any = [this.myGroupOptions[0].children[0], this.myGroupOptions[2].children[1]]; + + changeSelectAll() { + this.selectAll = !this.selectAll; + } +} diff --git a/src/select/demo/src/app/select/SelectClearableComponent.ts b/src/select/demo/src/app/select/SelectClearableComponent.ts new file mode 100644 index 0000000..3328a58 --- /dev/null +++ b/src/select/demo/src/app/select/SelectClearableComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-clearable.html' +}) +export class SelectClearableComponent { + options: Array = [ + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + } + ]; + + value: any = this.options[1]; + multiValue: Array = [this.options[1]]; + myLogs: Array = []; + onClear(): void { + this.myLogs = [...this.myLogs, `onClear()`]; + } +} diff --git a/src/select/demo/src/app/select/SelectDisabledComponent.ts b/src/select/demo/src/app/select/SelectDisabledComponent.ts new file mode 100644 index 0000000..f2fe400 --- /dev/null +++ b/src/select/demo/src/app/select/SelectDisabledComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-disabled.html' +}) +export class SelectDisabledComponent { + options: Array = [ + { + label: '巴西' + }, + { + label: '加拿大', + disabled: true + }, + { + label: '中国' + }, + { + label: '法国' + } + ]; + myOptions: Array = JSON.parse(JSON.stringify(this.options)); + value: any; + myValue: any = this.myOptions[2]; +} diff --git a/src/select/demo/src/app/select/SelectDisabledfocusComponent.ts b/src/select/demo/src/app/select/SelectDisabledfocusComponent.ts new file mode 100644 index 0000000..6bf942e --- /dev/null +++ b/src/select/demo/src/app/select/SelectDisabledfocusComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-disabledfocus.html' +}) +export class SelectDisabledfocusComponent { + options: Array = [ + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + } + ]; + + value: any; + myDisabled: boolean = true; + + changeDisabled(): void { + this.myDisabled = !this.myDisabled; + } +} diff --git a/src/select/demo/src/app/select/SelectEnumComponent.ts b/src/select/demo/src/app/select/SelectEnumComponent.ts new file mode 100644 index 0000000..230326d --- /dev/null +++ b/src/select/demo/src/app/select/SelectEnumComponent.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; + +enum EnglishName { + America, + Brazil, + Canada, + China, + France, + Germany, + Japan, + SouthKorea, + Turkey, + UnitedKingdom +} + +@Component({ + templateUrl: './select-enum.html' +}) +export class SelectEnumComponent { + myOptions: Array = [ + { + label: '美国', + englishname: EnglishName.America + }, + { + label: '巴西', + englishname: EnglishName.Brazil + }, + { + label: '加拿大', + englishname: EnglishName.Canada + }, + { + label: '中国', + englishname: EnglishName.China + }, + { + label: '法国', + englishname: EnglishName.France + }, + { + label: '德国', + englishname: EnglishName.Germany + }, + { + label: '日本', + englishname: EnglishName.Japan + }, + { + label: '韩国', + englishname: EnglishName.SouthKorea + }, + { + label: '土耳其', + englishname: EnglishName.Turkey + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: EnglishName.UnitedKingdom + } + ]; + + mySelected1: EnglishName; + mySelecteds: Array; +} diff --git a/src/select/demo/src/app/select/SelectEventComponent.ts b/src/select/demo/src/app/select/SelectEventComponent.ts new file mode 100644 index 0000000..7ed7329 --- /dev/null +++ b/src/select/demo/src/app/select/SelectEventComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-event.html' +}) +export class SelectEventComponent { + options: Array = [ + { label: '美国' }, + { label: '巴西' }, + { label: '加拿大' }, + { label: '中国' }, + { label: '法国' }, + { label: '德国' }, + { label: '日本' }, + { label: '韩国' }, + { label: '土耳其' }, + { label: '大不列颠和北爱兰联合王国' } + ]; + + value: any; + myLogs: Array = []; + + onSelect(option: any): void { + this.myLogs = [...this.myLogs, `onSelect() event=${JSON.stringify(option)}`]; + } +} diff --git a/src/select/demo/src/app/select/SelectFocusComponent.ts b/src/select/demo/src/app/select/SelectFocusComponent.ts new file mode 100644 index 0000000..573f04c --- /dev/null +++ b/src/select/demo/src/app/select/SelectFocusComponent.ts @@ -0,0 +1,53 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-focus.html' +}) +export class SelectFocusComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + mySelected: any; + + myDisabled: boolean = false; +} diff --git a/src/select/demo/src/app/select/SelectGroupComponent.ts b/src/select/demo/src/app/select/SelectGroupComponent.ts new file mode 100644 index 0000000..2a42457 --- /dev/null +++ b/src/select/demo/src/app/select/SelectGroupComponent.ts @@ -0,0 +1,63 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-group.html' +}) +export class SelectGroupComponent { + options: Array = [ + { + label: '北美洲', + children: [ + { + label: '美国', + disabled: true + }, + { + label: '加拿大' + } + ] + }, + { + label: '南美洲', + children: [ + { + label: '巴西' + } + ] + }, + { + label: '亚洲', + children: [ + { + label: '中国' + }, + { + label: '日本' + }, + { + label: '韩国' + } + ] + }, + { + label: '欧洲', + children: [ + { + label: '法国', + disabled: true + }, + { + label: '德国' + }, + { + label: '土耳其' + }, + { + label: '大不列颠和北爱兰联合王国' + } + ] + } + ]; + + value: any; +} diff --git a/src/select/demo/src/app/select/SelectIdComponent.ts b/src/select/demo/src/app/select/SelectIdComponent.ts new file mode 100644 index 0000000..bb49236 --- /dev/null +++ b/src/select/demo/src/app/select/SelectIdComponent.ts @@ -0,0 +1,161 @@ +import { Component, DoCheck, Inject } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +@Component({ + templateUrl: './select-id.html' +}) +export class SelectIdComponent implements DoCheck { + myOptions: Array = [ + { + label: '蛋糕', + englishname: 'Cake', + url: 'assets/food/cake.png' + }, + { + label: '咖啡', + englishname: 'Coffee', + url: 'assets/food/coffee.png' + }, + { + label: '可乐', + englishname: 'Cola', + url: 'assets/food/cola.png' + }, + { + label: '炸鸡', + englishname: 'Fried Chicken', + url: 'assets/food/fried_chicken.png' + }, + { + label: '薯条', + englishname: 'Fries', + url: 'assets/food/fries.png' + }, + { + label: '汉堡', + englishname: 'Hamburger', + url: 'assets/food/hamburger.png' + }, + { + label: '牛奶', + englishname: 'milk', + url: 'assets/food/milk.png' + }, + { + label: '披萨', + englishname: 'pizza', + url: 'assets/food/pizza.png' + } + ]; + + myGroupOptions: Array = [ + { + label: '北美洲', + children: [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + } + ] + }, + { + label: '南美洲', + children: [ + { + label: '巴西', + englishname: 'Brazil', + disabled: false + } + ] + }, + { + label: '亚洲', + children: [ + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + } + ] + }, + { + label: '欧洲', + children: [ + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ] + } + ]; + + mySelected: any = this.myOptions[0]; + myGroupSelected: any = this.myOptions[0]; + mySelecteds: any = [this.myOptions[2], this.myOptions[3]]; + + idExistMap: Map = new Map(); + ids: Array = [ + 'myselect1', + 'myselect1_dominator', + 'myselect1_dominator_input', + 'myselect1_dominator_btn', + 'myselect1_droplist', + 'myselect1_droplist_list', + 'myselect1_droplist_list_0', + 'myselect2_droplist_list_0', + 'myselect3_dominator_tag0', + 'myselect3_dominator_tag0_closeicon', + 'myselect3_droplist_list_3', + 'myselect4_droplist_searchbox', + 'myselect4_droplist_searchbox_input', + 'myselect5_dominator_input', + 'myselect5_droplist_list_0', + 'myselect5_droplist_list_0_0', + 'myselect5_droplist_list_2', + 'myselect5_droplist_list_2_0', + 'myselect5_droplist_list_2_1' + ]; + allIdExist: boolean = false; + + // 修复SSR报错:ERROR ReferenceError: document is not defined + constructor(@Inject(DOCUMENT) private document) {} + + ngDoCheck(): void { + this.allIdExist = true; + this.ids.forEach((id: string) => { + const idExist: boolean = this.document.getElementById(id) != undefined; + this.idExistMap.set(id, idExist); + if (!idExist) { + this.allIdExist = false; + } + }); + } +} diff --git a/src/select/demo/src/app/select/SelectIdkeyComponent.ts b/src/select/demo/src/app/select/SelectIdkeyComponent.ts new file mode 100644 index 0000000..64f441b --- /dev/null +++ b/src/select/demo/src/app/select/SelectIdkeyComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-idkey.html' +}) +export class SelectIdkeyComponent { + options: Array = [ + { value: 0, label: 'America' }, + { value: 1, label: 'Brazil' }, + { value: 2, label: 'Canada' }, + { value: 3, label: 'China' }, + { value: 30, label: 'China' }, + { value: 4, label: 'France' }, + { value: 5, label: 'Germany' }, + { value: 6, label: 'Japan' }, + { value: 7, label: 'South Korea' }, + { value: 8, label: 'Turkey' }, + { value: 9, label: 'United Kingdom' } + ]; + + value: any; +} diff --git a/src/select/demo/src/app/select/SelectInputComponent.ts b/src/select/demo/src/app/select/SelectInputComponent.ts new file mode 100644 index 0000000..4ab9583 --- /dev/null +++ b/src/select/demo/src/app/select/SelectInputComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-input.html' +}) +export class SelectInputComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + mySelected: any; +} diff --git a/src/select/demo/src/app/select/SelectLabelkeyComponent.ts b/src/select/demo/src/app/select/SelectLabelkeyComponent.ts new file mode 100644 index 0000000..3a3bd48 --- /dev/null +++ b/src/select/demo/src/app/select/SelectLabelkeyComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-labelkey.html' +}) +export class SelectLabelkeyComponent { + options: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + value: any; +} diff --git a/src/select/demo/src/app/select/SelectLazyComponent.ts b/src/select/demo/src/app/select/SelectLazyComponent.ts new file mode 100644 index 0000000..e115bce --- /dev/null +++ b/src/select/demo/src/app/select/SelectLazyComponent.ts @@ -0,0 +1,33 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiSelectComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './select-lazy.html' +}) +export class SelectLazyComponent { + constructor(protected changeDetectorRef: ChangeDetectorRef) {} + options: Array = []; + value: any; + onBeforeOpen(selectComp: TiSelectComponent): void { + // 模拟懒加载场景 + setTimeout(() => { + this.options = [ + { label: '美国' }, + { label: '巴西' }, + { label: '加拿大' }, + { label: '中国' }, + { label: '法国' }, + { label: '德国' }, + { label: '日本' }, + { label: '韩国' }, + { label: '土耳其' }, + { label: '大不列颠和北爱兰联合王国' } + ]; + + this.value = this.options[0]; + selectComp.open(); + // 主动触发脏值检测,更新数据 + this.changeDetectorRef.markForCheck(); + }, 300); + } +} diff --git a/src/select/demo/src/app/select/SelectLeakComponent.ts b/src/select/demo/src/app/select/SelectLeakComponent.ts new file mode 100644 index 0000000..5e89633 --- /dev/null +++ b/src/select/demo/src/app/select/SelectLeakComponent.ts @@ -0,0 +1,87 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +import { Component, Inject } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +declare let global: any; + +@Component({ + templateUrl: './select-leak.html' +}) +export class SelectLeakComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '巴西', + englishname: 'Brazil', + disabled: false + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + }, + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + mySelected: any; + + show: boolean = true; + + dropLen: number = 0; + // 修复SSR报错:ERROR ReferenceError: document is not defined + constructor(@Inject(DOCUMENT) private document) { + // 修复SSR问题:打开页面白屏一直转圈 + if (typeof global !== 'undefined') { + return; + } + } + + dropCount(): void { + setTimeout(() => { + this.dropLen = this.document.body.getElementsByTagName('ti-drop').length; + }, 0); + } + + ngOnInit(): void { + this.dropCount(); + } + + toggle(): void { + this.show = !this.show; + this.dropCount(); + } +} diff --git a/src/select/demo/src/app/select/SelectLoadComponent.ts b/src/select/demo/src/app/select/SelectLoadComponent.ts new file mode 100644 index 0000000..4f8be29 --- /dev/null +++ b/src/select/demo/src/app/select/SelectLoadComponent.ts @@ -0,0 +1,108 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-load.html' +}) +export class SelectLoadComponent { + private dataA: Array = [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '巴西', + englishname: 'Brazil', + disabled: false + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + }, + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + private dataB: Array = [ + { label: '水调歌头' }, + { label: '苏轼' }, + { label: '明月几时有?把酒问青天' }, + { label: '不知天上宫阙,今夕是何年。' }, + { label: '我欲乘风归去,又恐琼楼玉宇,高处不胜寒。' }, + { label: '起舞弄清影,何似在人间?' }, + { label: '转朱阁,低绮户,照无眠。' }, + { label: '不应有恨,何事长向别时圆?' }, + { label: '人有悲欢离合,月有阴晴圆缺,此事古难全。' }, + { label: '但愿人长久,千里共婵娟。' } + ]; + + myOptions: Array; + + mySelected: any; + + mySelecteds: any; + + // 每个组件都用下面六个函数,只改变函数内容 + changeUndefined(): void { + this.myOptions = undefined; + } + changeNull(): void { + this.myOptions = null; + } + changeWrongType(): void { + const temp: any = 5; + this.myOptions = temp; + } + changeZeroData(): void { + this.myOptions = []; + this.mySelected = undefined; + } + changeDataA(): void { + this.myOptions = this.dataA; + this.mySelected = this.myOptions[0]; + this.mySelecteds = [this.myOptions[0], this.myOptions[1]]; + } + changeDataB(): void { + this.myOptions = this.dataB; + this.mySelected = this.myOptions[0]; + this.mySelecteds = [this.myOptions[0], this.myOptions[1]]; + } + + // 改变选中项 + changeSelect(): void { + this.mySelected = this.myOptions[1]; + } + changeSelects(): void { + this.mySelecteds = [this.myOptions[2], this.myOptions[3]]; + } +} diff --git a/src/select/demo/src/app/select/SelectManyComponent.ts b/src/select/demo/src/app/select/SelectManyComponent.ts new file mode 100644 index 0000000..1923fac --- /dev/null +++ b/src/select/demo/src/app/select/SelectManyComponent.ts @@ -0,0 +1,95 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-many.html' +}) +export class SelectManyComponent { + private dataA: Array = [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '巴西', + englishname: 'Brazil', + disabled: false + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + }, + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + private dataB: Array = [ + { label: '水调歌头' }, + { label: '苏轼' }, + { label: '明月几时有?把酒问青天' }, + { label: '不知天上宫阙,今夕是何年。' }, + { label: '我欲乘风归去,又恐琼楼玉宇,高处不胜寒。' }, + { label: '起舞弄清影,何似在人间?' }, + { label: '转朱阁,低绮户,照无眠。' }, + { label: '不应有恨,何事长向别时圆?' }, + { label: '人有悲欢离合,月有阴晴圆缺,此事古难全。' }, + { label: '但愿人长久,千里共婵娟。' } + ]; + + myOptions: Array; + + mySelected: any; + + mySelecteds: any; + + constructor() { + for (let i: number = 0; i < 5; i++) { + // 5次为32倍,320条 + this.dataA = this.dataA.concat(this.dataA); + } + for (let i: number = 0; i < 7; i++) { + // 8次为256倍,2560条 + this.dataB = this.dataB.concat(this.dataB); + } + } + changeDataA(): void { + this.myOptions = this.dataA; + this.mySelected = this.myOptions[0]; + this.mySelecteds = [this.myOptions[0], this.myOptions[1]]; + } + changeDataB(): void { + this.myOptions = this.dataB; + this.mySelected = this.myOptions[0]; + this.mySelecteds = [this.myOptions[0], this.myOptions[1]]; + } +} diff --git a/src/select/demo/src/app/select/SelectMaxlineComponent.ts b/src/select/demo/src/app/select/SelectMaxlineComponent.ts new file mode 100644 index 0000000..7e29347 --- /dev/null +++ b/src/select/demo/src/app/select/SelectMaxlineComponent.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-maxline.html' +}) +export class SelectMaxlineComponent { + options: Array = [ + { + label: '美国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + }, + { + label: '法国' + }, + { + label: '德国' + }, + { + label: '日本' + }, + { + label: '韩国' + }, + { + label: '土耳其' + }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + value: any = [this.options[0]]; +} diff --git a/src/select/demo/src/app/select/SelectMuchComponent.ts b/src/select/demo/src/app/select/SelectMuchComponent.ts new file mode 100644 index 0000000..0be2cbc --- /dev/null +++ b/src/select/demo/src/app/select/SelectMuchComponent.ts @@ -0,0 +1,63 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-much.html' +}) +export class SelectMuchComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '巴西', + englishname: 'Brazil', + disabled: false + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + }, + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + items: Array = []; + + ngOnInit(): void { + for (let i: number = 0; i < 300; i++) { + this.items.push({ value: undefined }); + } + } +} diff --git a/src/select/demo/src/app/select/SelectMultiComponent.ts b/src/select/demo/src/app/select/SelectMultiComponent.ts new file mode 100644 index 0000000..b86dc51 --- /dev/null +++ b/src/select/demo/src/app/select/SelectMultiComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-multi.html' +}) +export class SelectMultiComponent { + public options: Array = [ + { + label: '中国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + } + ]; + + public value: any = [this.options[1]]; +} diff --git a/src/select/demo/src/app/select/SelectNoborderComponent.ts b/src/select/demo/src/app/select/SelectNoborderComponent.ts new file mode 100644 index 0000000..0db1496 --- /dev/null +++ b/src/select/demo/src/app/select/SelectNoborderComponent.ts @@ -0,0 +1,53 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-noborder.html' +}) +export class SelectNoborderComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + mySelected: any; + + mySelecteds: any; +} diff --git a/src/select/demo/src/app/select/SelectNodataComponent.ts b/src/select/demo/src/app/select/SelectNodataComponent.ts new file mode 100644 index 0000000..d91d898 --- /dev/null +++ b/src/select/demo/src/app/select/SelectNodataComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-nodata.html' +}) +export class SelectNodataComponent { + options: Array = []; + value: any; +} diff --git a/src/select/demo/src/app/select/SelectNoemptyComponent.ts b/src/select/demo/src/app/select/SelectNoemptyComponent.ts new file mode 100644 index 0000000..3bd3695 --- /dev/null +++ b/src/select/demo/src/app/select/SelectNoemptyComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-noempty.html' +}) +export class SelectNoemptyComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + mySelected: any; +} diff --git a/src/select/demo/src/app/select/SelectNullComponent.ts b/src/select/demo/src/app/select/SelectNullComponent.ts new file mode 100644 index 0000000..a2551c1 --- /dev/null +++ b/src/select/demo/src/app/select/SelectNullComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-null.html' +}) +export class SelectNullComponent {} diff --git a/src/select/demo/src/app/select/SelectPaginBeforesearchComponent.ts b/src/select/demo/src/app/select/SelectPaginBeforesearchComponent.ts new file mode 100644 index 0000000..16bbc51 --- /dev/null +++ b/src/select/demo/src/app/select/SelectPaginBeforesearchComponent.ts @@ -0,0 +1,188 @@ +import { Component, ChangeDetectorRef } from '@angular/core'; +import { TiPageSizeConfig, TiSelectComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './select-pagin-beforesearch.html' +}) +export class SelectPaginBeforesearchComponent { + constructor(private cdRef: ChangeDetectorRef) {} + private database: Array = [ + { label: 'America', disabled: true }, + { label: 'Brazil' }, + { label: 'Canada', disabled: true }, + { label: 'China' }, + { label: 'France', disabled: true }, + { label: 'Germany' }, + { label: 'Japan' }, + { label: 'South Korea' }, + { label: 'Turkey' }, + { label: 'United Kingdom' } + ]; + private data: Array; + myOptions1: Array; + myOptions2: Array; + myOptions3: Array; + myOptions4: Array; + + mySelected1: any; + mySelected2: any; + mySelected3: Array = [{ label: '3China' }]; + mySelected4: Array = [{ label: '3China' }]; + + totalNumber1: number; + totalNumber2: number; + totalNumber3: number; + totalNumber4: number; + + currentPage1: number = 1; + currentPage2: number = 1; + currentPage3: number = 1; + currentPage4: number = 1; + + pageSize: TiPageSizeConfig = { + hide: true, + // 如果想修改size,需要配置pageSize.option数组,size是option数组的一项,具体参考TiPaginationComponent组件API说明。 + size: 5, + options: [5, 10] + }; + + ngOnInit(): void { + this.getdatas(this.currentPage1).then((result) => { + this.myOptions1 = result.data; + this.totalNumber1 = result.totalNumber; + }); + + this.getdatas(this.currentPage3).then((result) => { + this.myOptions3 = result.data; + this.totalNumber3 = result.totalNumber; + }); + } + + onCurrentPageChange1(currentPage: number, selectComp: TiSelectComponent): void { + // 模拟后台分页 + this.getdatas(currentPage, selectComp.getSearchWord()).then((result) => { + this.myOptions1 = result.data; + }); + } + + onBeforeSearch1(selectComp: TiSelectComponent): void { + console.log('onBeforeSearch1'); // 为了展示onBeforeSearch事件触发了几次 + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + + this.currentPage1 = 1; + // 模拟后台搜索 + this.getdatas(this.currentPage1, searchWord).then((result) => { + this.myOptions1 = result.data; + this.totalNumber1 = result.totalNumber; + }); + } + + onBeforeOpen2(selectComp: TiSelectComponent): void { + // this.currentPage2 = 1; 每次重新打开下拉列表,需要显示第几页数据就请求第几页数据,并更新currentPage + this.getdatas(this.currentPage2).then((result) => { + this.myOptions2 = result.data; + this.cdRef.detectChanges(); + this.totalNumber2 = result.totalNumber; + selectComp.open(); + }); + } + + onCurrentPageChange2(currentPage: number, selectComp: TiSelectComponent): void { + // 模拟后台分页 + this.getdatas(currentPage, selectComp.getSearchWord()).then((result) => { + this.myOptions2 = result.data; + }); + } + + onBeforeSearch2(selectComp: TiSelectComponent): void { + console.log('onBeforeSearch2'); // 为了展示onBeforeSearch事件触发了几次 + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + + this.currentPage2 = 1; + // 模拟后台搜索 + this.getdatas(this.currentPage2, searchWord).then((result) => { + this.myOptions2 = result.data; + this.totalNumber2 = result.totalNumber; + }); + } + + onCurrentPageChange3(currentPage: number, selectComp: TiSelectComponent): void { + // 模拟后台分页 + this.getdatas(currentPage, selectComp.getSearchWord()).then((result) => { + this.myOptions3 = result.data; + }); + } + + onBeforeSearch3(selectComp: TiSelectComponent): void { + console.log('onBeforeSearch3'); // 为了展示onBeforeSearch事件触发了几次 + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + + this.currentPage3 = 1; + // 模拟后台搜索 + this.getdatas(this.currentPage3, searchWord).then((result) => { + this.myOptions3 = result.data; + this.totalNumber3 = result.totalNumber; + }); + } + + onBeforeOpen4(selectComp: TiSelectComponent): void { + // this.currentPage4 = 1; 每次重新打开下拉列表,需要显示第几页数据就请求第几页数据,并更新currentPage + this.getdatas(this.currentPage4).then((result) => { + this.myOptions4 = result.data; + this.cdRef.detectChanges(); + this.totalNumber4 = result.totalNumber; + selectComp.open(); + }); + } + + onCurrentPageChange4(currentPage: number, selectComp: TiSelectComponent): void { + // 模拟后台分页 + this.getdatas(currentPage, selectComp.getSearchWord()).then((result) => { + this.myOptions4 = result.data; + }); + } + + onBeforeSearch4(selectComp: TiSelectComponent): void { + console.log('onBeforeSearch4'); // 为了展示onBeforeSearch事件触发了几次 + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + + this.currentPage4 = 1; + // 模拟后台搜索 + this.getdatas(this.currentPage4, searchWord).then((result) => { + this.myOptions4 = result.data; + this.totalNumber4 = result.totalNumber; + }); + } + + private getdatas(pageNumber?: number, searchWord?: string): Promise { + this.data = []; + for (let i: number = 0; i < 136; i++) { + const item: any = this.database[i % 10]; + this.data.push({ ...item, label: i + item.label }); + } + const promise: Promise = new Promise((resolve: any, rejects: any): void => { + setTimeout(() => { + if (pageNumber === undefined && searchWord === undefined) { + rejects(new Error('没有参数')); + } + if (searchWord) { + const filteredResult: Array = this.data.filter((item) => item.label.toLowerCase().includes(searchWord.toLowerCase())); + const slicedResult: Array = filteredResult.slice(this.pageSize.size * (pageNumber - 1), this.pageSize.size * pageNumber); + const totalNumber: number = filteredResult.length; + resolve({ data: slicedResult, totalNumber }); + } else if (pageNumber) { + resolve({ + data: this.data.slice(this.pageSize.size * (pageNumber - 1), this.pageSize.size * pageNumber), + totalNumber: this.data.length + }); + } + }, 300); + }); + + return promise; + } +} diff --git a/src/select/demo/src/app/select/SelectPaginationComponent.ts b/src/select/demo/src/app/select/SelectPaginationComponent.ts new file mode 100644 index 0000000..56cf8b7 --- /dev/null +++ b/src/select/demo/src/app/select/SelectPaginationComponent.ts @@ -0,0 +1,65 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiPageSizeConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './select-pagination.html' +}) +export class SelectPaginationComponent { + data: Array = [ + { label: '美国', englishname: 'America', disabled: true }, + { label: '巴西', englishname: 'Brazil', disabled: false }, + { label: '加拿大', englishname: 'Canada', disabled: true }, + { label: '中国', englishname: 'China', disabled: false }, + { label: '法国', englishname: 'France', disabled: true }, + { label: '德国', englishname: 'Germany', disabled: false }, + { label: '日本', englishname: 'Japan' }, + { label: '韩国', englishname: 'South Korea' }, + { label: '土耳其', englishname: 'Turkey' }, + { label: '大不列颠和北爱兰联合王国', englishname: 'United Kingdom' } + ]; + + options: Array; + value: any; + totalNumber: number; + pageSize: TiPageSizeConfig = { + hide: true, + size: 20 + }; + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + ngOnInit(): void { + // 初始获取 options 下拉数据 + this.getData(1, this.pageSize.size).then((result: any) => { + this.options = result.data; + this.totalNumber = result.totalNumber; + }); + } + + onCurrentPageChange(currentPage: number): void { + this.getData(currentPage, this.pageSize.size).then((result: any) => { + this.options = result.data; + }); + } + + // 模拟异步请求 + private getData(currentPage: number, size: number): Promise { + const startIndex: number = (currentPage - 1) * size; + const database: Array = []; + for (let i: number = 0; i < 500; i++) { + const item: any = this.data[i % 10]; + database.push({ ...item, label: i + item.label }); + } + + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + resolve({ + data: database.slice(startIndex, startIndex + size), + totalNumber: database.length + }); + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef.markForCheck() + // 默认模式下不需要 + this.changeDetectorRef.markForCheck(); + }, 200); + }); + } +} diff --git a/src/select/demo/src/app/select/SelectPanelComponent.ts b/src/select/demo/src/app/select/SelectPanelComponent.ts new file mode 100644 index 0000000..859e9eb --- /dev/null +++ b/src/select/demo/src/app/select/SelectPanelComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-panel.html' +}) +export class SelectPanelComponent { + options: Array = [ + { label: '水调歌头' }, + { label: '苏轼' }, + { label: '明月几时有?把酒问青天' }, + { label: '不知天上宫阙,今夕是何年。' }, + { label: '我欲乘风归去,又恐琼楼玉宇,高处不胜寒。' }, + { label: '起舞弄清影,何似在人间?' }, + { label: '转朱阁,低绮户,照无眠。' }, + { label: '不应有恨,何事长向别时圆?' }, + { label: '人有悲欢离合,月有阴晴圆缺,此事古难全。' }, + { label: '但愿人长久,千里共婵娟。' } + ]; + + value: any; +} diff --git a/src/select/demo/src/app/select/SelectReservesearchwordComponent.ts b/src/select/demo/src/app/select/SelectReservesearchwordComponent.ts new file mode 100644 index 0000000..5d0a41d --- /dev/null +++ b/src/select/demo/src/app/select/SelectReservesearchwordComponent.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-reservesearchword.html' +}) +export class SelectReservesearchwordComponent { + options: Array = [ + { + label: '美国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + }, + { + label: '法国' + }, + { + label: '德国' + }, + { + label: '日本' + }, + { + label: '韩国' + }, + { + label: '土耳其' + }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + value: any = this.options[0]; +} diff --git a/src/select/demo/src/app/select/SelectScrollLoadComponent.ts b/src/select/demo/src/app/select/SelectScrollLoadComponent.ts new file mode 100644 index 0000000..3075442 --- /dev/null +++ b/src/select/demo/src/app/select/SelectScrollLoadComponent.ts @@ -0,0 +1,116 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiSelectComponent, TiListScrollLoad } from '@opentiny/ng'; + +@Component({ + templateUrl: './select-scroll-load.html' +}) +export class SelectScrollLoadComponent { + constructor(private changeDetectorRef: ChangeDetectorRef) {} + optionsTotalNumber: number; + private size: number = 50; + private data: Array = [ + { + label: '美国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + }, + { + label: '法国' + }, + { + label: '德国' + }, + { + label: '日本' + }, + { + label: '韩国' + }, + { + label: '土耳其' + }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + private database: Array = []; + options: Array = []; + value: any; + + beforeOpen(selectComp: TiSelectComponent) { + if (this.options.length === 0) { + // 第一次打开面板需要从后台获取部分数据 + this.getData(0, this.size).then((result: any) => { + this.options = result.data; // 下拉数据赋值 + this.optionsTotalNumber = result.totalNumber; + setTimeout(() => { + // 有搜索时,在beforeOpen中给 options 赋值后需要延迟再打开面板 + selectComp.open(); // 打开面板 + }, 0); + }); + } else { + selectComp.open(); // 非第一次(已经获取到了下拉数据)时直接打开面板 + } + } + + beforeSearch(selectComp: TiSelectComponent): void { + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + this.getData(0, this.size, searchWord).then((result: any) => { + // 设置搜索结果 + this.options = result.data; + this.optionsTotalNumber = result.totalNumber; + }); + } + + loadMore(scrollLoadInfo: TiListScrollLoad, selectComp: TiSelectComponent): void { + const currentOptions: Array = selectComp.getSearchResult(); + if (currentOptions.length >= this.optionsTotalNumber) { + return; + } + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + scrollLoadInfo.loading = true; + this.getData(currentOptions.length, this.size, searchWord).then((result: any) => { + this.options = [...currentOptions, ...result.data]; + scrollLoadInfo.loading = false; + }); + } + + // 模拟异步请求 + private getData(startIndex: number, size: number, searchWord?: string): Promise { + this.database = []; + for (let i: number = 0; i < 10000; i++) { + const item: any = this.data[i % 10]; + this.database.push({ ...item, label: i + item.label }); + } + + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + if (searchWord) { + const filteredResult: Array = this.database.filter((item: any) => item.label.includes(searchWord)); + const slicedResult: Array = filteredResult.slice(startIndex, startIndex + size); + const totalNumber: number = filteredResult.length; + resolve({ data: slicedResult, totalNumber }); + } else { + resolve({ + data: this.database.slice(startIndex, startIndex + size), + totalNumber: this.database.length + }); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef.markForCheck() + // 默认模式下不需要 + // 服务可根据自身变化检测策略决定是否使用该方法 + this.changeDetectorRef.markForCheck(); + }, 300); + }); + } +} diff --git a/src/select/demo/src/app/select/SelectSearchComponent.ts b/src/select/demo/src/app/select/SelectSearchComponent.ts new file mode 100644 index 0000000..d62ca13 --- /dev/null +++ b/src/select/demo/src/app/select/SelectSearchComponent.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-search.html' +}) +export class SelectSearchComponent { + options: Array = [ + { + label: '美国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + }, + { + label: '法国' + }, + { + label: '德国' + }, + { + label: '日本' + }, + { + label: '韩国' + }, + { + label: '土耳其' + }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + value: any = this.options[0]; +} diff --git a/src/select/demo/src/app/select/SelectSearchkeysComponent.ts b/src/select/demo/src/app/select/SelectSearchkeysComponent.ts new file mode 100644 index 0000000..b8ad4aa --- /dev/null +++ b/src/select/demo/src/app/select/SelectSearchkeysComponent.ts @@ -0,0 +1,63 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-searchkeys.html' +}) +export class SelectSearchkeysComponent { + options: Array = [ + { + label: '美国', + yomi: 'meiguo', + initial: 'mg' + }, + { + label: '巴西', + yomi: 'baxi', + initial: 'bx' + }, + { + label: '加拿大', + yomi: 'jianada', + initial: 'jnd', + disabled: true + }, + { + label: '中国', + yomi: 'zhongguo', + initial: 'zg' + }, + { + label: '法国', + yomi: 'faguo', + initial: 'fg' + }, + { + label: '德国', + yomi: 'deguo', + initial: 'dg', + disabled: true + }, + { + label: '日本', + yomi: 'riben', + initial: 'rb' + }, + { + label: '韩国', + yomi: 'hanguo', + initial: 'hg' + }, + { + label: '土耳其', + yomi: 'tuerqi', + initial: 'teq' + }, + { + label: '大不列颠和北爱兰联合王国', + yomi: 'dabuliedianhebeiaierlanlianhewangguo', + initial: 'dbldhbaellhwg' + } + ]; + + value: any; +} diff --git a/src/select/demo/src/app/select/SelectSelectallComponent.ts b/src/select/demo/src/app/select/SelectSelectallComponent.ts new file mode 100644 index 0000000..e22cbea --- /dev/null +++ b/src/select/demo/src/app/select/SelectSelectallComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-selectall.html' +}) +export class SelectSelectallComponent { + options: Array = [ + { + label: '中国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + } + ]; + + value: any = [this.options[1]]; +} diff --git a/src/select/demo/src/app/select/SelectShowselectednumberComponent.ts b/src/select/demo/src/app/select/SelectShowselectednumberComponent.ts new file mode 100644 index 0000000..dd98109 --- /dev/null +++ b/src/select/demo/src/app/select/SelectShowselectednumberComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-showselectednumber.html' +}) +export class SelectShowselectednumberComponent { + options: Array = [ + { label: '美国' }, + { label: '巴西' }, + { label: '加拿大' }, + { label: '中国' }, + { label: '法国' }, + { label: '德国' }, + { label: '日本' }, + { label: '韩国' }, + { label: '土耳其' }, + { label: '大不列颠和北爱兰联合王国' } + ]; + + value: any = [this.options[3]]; +} diff --git a/src/select/demo/src/app/select/SelectSmallComponent.ts b/src/select/demo/src/app/select/SelectSmallComponent.ts new file mode 100644 index 0000000..8c30439 --- /dev/null +++ b/src/select/demo/src/app/select/SelectSmallComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-small.html' +}) +export class SelectSmallComponent { + options: Array = [ + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + } + ]; + + value: any = this.options[1]; +} diff --git a/src/select/demo/src/app/select/SelectTagComponent.ts b/src/select/demo/src/app/select/SelectTagComponent.ts new file mode 100644 index 0000000..b499bd3 --- /dev/null +++ b/src/select/demo/src/app/select/SelectTagComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-tag.html', + styleUrls: ['./select-tag.less'] +}) +export class SelectTagComponent { + myOptions: Array = [ + { + label: '蛋糕', + englishname: 'Cake', + url: 'assets/food/cake.png', + disabled: true, + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + }, + { + label: '咖啡', + englishname: 'Coffee', + url: 'assets/food/coffee.png', + disabled: false, + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + }, + { + label: '可乐', + englishname: 'Cola', + url: 'assets/food/cola.png', + disabled: true, + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + }, + { + label: '炸鸡', + englishname: 'Fried Chicken', + url: 'assets/food/fried_chicken.png', + disabled: false, + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + }, + { + label: '薯条', + englishname: 'Fries', + url: 'assets/food/fries.png', + disabled: true, + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + }, + { + label: '汉堡', + englishname: 'Hamburger', + url: 'assets/food/hamburger.png', + disabled: false, + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + }, + { + label: '牛奶', + englishname: 'milk', + url: 'assets/food/milk.png', + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + }, + { + label: '披萨', + englishname: 'pizza', + url: 'assets/food/pizza.png', + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + } + ]; + + mySelected: any; + // 也是有初值/有禁用项/带模板的一个测试用例, + mySelecteds: any = []; +} diff --git a/src/select/demo/src/app/select/SelectTemplateComponent.ts b/src/select/demo/src/app/select/SelectTemplateComponent.ts new file mode 100644 index 0000000..11edff4 --- /dev/null +++ b/src/select/demo/src/app/select/SelectTemplateComponent.ts @@ -0,0 +1,50 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './select-template.html' +}) +export class SelectTemplateComponent { + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL']; + options: Array = [ + { + label: '蛋糕', + englishname: 'Cake', + url: `${this.baseUrl}assets/food/cake.png` + }, + { + label: '咖啡', + englishname: 'Coffee', + url: `${this.baseUrl}assets/food/coffee.png` + }, + { + label: '可乐', + englishname: 'Cola', + url: `${this.baseUrl}assets/food/cola.png` + }, + { + label: '炸鸡', + englishname: 'Fried Chicken', + url: `${this.baseUrl}assets/food/fried_chicken.png` + }, + { + label: '薯条', + englishname: 'Fries', + url: `${this.baseUrl}assets/food/fries.png` + }, + { + label: '汉堡', + englishname: 'Hamburger', + url: `${this.baseUrl}assets/food/hamburger.png` + }, + { + label: '牛奶', + englishname: 'milk', + url: `${this.baseUrl}assets/food/milk.png` + }, + { + label: '披萨', + englishname: 'pizza', + url: `${this.baseUrl}assets/food/pizza.png` + } + ]; + value: any; +} diff --git a/src/select/demo/src/app/select/SelectTestModule.ts b/src/select/demo/src/app/select/SelectTestModule.ts new file mode 100644 index 0000000..6c3dd2f --- /dev/null +++ b/src/select/demo/src/app/select/SelectTestModule.ts @@ -0,0 +1,293 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { + TiIconModule, + TiPaginationModule, + TiScrollModule, + TiSelectModule, + TiTextModule, + TiTipModule, + TiValidationModule +} from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { SelectBasicComponent } from './SelectBasicComponent'; +import { SelectGroupComponent } from './SelectGroupComponent'; +import { SelectEventComponent } from './SelectEventComponent'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { SelectLeakComponent } from './SelectLeakComponent'; +import { SelectSmallComponent } from './SelectSmallComponent'; +import { SelectDisabledComponent } from './SelectDisabledComponent'; +import { SelectMultiComponent } from './SelectMultiComponent'; +import { SelectSearchComponent } from './SelectSearchComponent'; +import { SelectNoemptyComponent } from './SelectNoemptyComponent'; +import { NoEmptyPipe } from './NoEmptyPipe'; +import { SelectPanelComponent } from './SelectPanelComponent'; +import { SelectTipComponent } from './SelectTipComponent'; +import { SelectValidComponent } from './SelectValidComponent'; +import { SelectTagComponent } from './SelectTagComponent'; +import { SelectTemplateComponent } from './SelectTemplateComponent'; +import { SelectLoadComponent } from './SelectLoadComponent'; +import { SelectFocusComponent } from './SelectFocusComponent'; +import { SelectNoborderComponent } from './SelectNoborderComponent'; +import { SelectNodataComponent } from './SelectNodataComponent'; +import { SelectInputComponent } from './SelectInputComponent'; +import { SelectManyComponent } from './SelectManyComponent'; +import { SelectIdComponent } from './SelectIdComponent'; +import { SelectValidgroupComponent } from './SelectValidgroupComponent'; +import { SelectTiscrollComponent } from './SelectTiscrollComponent'; +import { SelectAppendtobodyComponent } from './SelectAppendtobodyComponent'; +import { SelectClearableComponent } from './SelectClearableComponent'; +import { SelectMuchComponent } from './SelectMuchComponent'; +import { SelectSelectallComponent } from './SelectSelectallComponent'; +import { SelectMaxlineComponent } from './SelectMaxlineComponent'; +import { SelectSearchkeysComponent } from './SelectSearchkeysComponent'; +import { SelectValuekeyComponent } from './SelectValuekeyComponent'; +import { SelectValuekeyTestComponent } from './SelectValuekeyTestComponent'; +import { SelectTworowComponent } from './SelectTworowComponent'; +import { SelectEnumComponent } from './SelectEnumComponent'; +import { SelectBeforesearchComponent } from './SelectBeforesearchComponent'; +import { SelectBeforesearchTestComponent } from './SelectBeforesearchTestComponent'; +import { SelectPaginationComponent } from './SelectPaginationComponent'; +import { SelectPaginBeforesearchComponent } from './SelectPaginBeforesearchComponent'; +import { SelectNullComponent } from './SelectNullComponent'; +import { SelectVirtualscrollComponent } from './SelectVirtualscrollComponent'; +import { SelectVirtualscrollMultiComponent } from './SelectVirtualscrollMultiComponent'; +import { SelectChangeSelectallComponent } from './SelectChangeSelectallComponent'; +import { SelectIdkeyComponent } from './SelectIdkeyComponent'; +import { SelectScrollLoadComponent } from './SelectScrollLoadComponent'; +import { SelectDisabledfocusComponent } from './SelectDisabledfocusComponent'; +import { SelectShowselectednumberComponent } from './SelectShowselectednumberComponent'; +import { SelectLabelkeyComponent } from './SelectLabelkeyComponent'; +import { SelectReservesearchwordComponent } from './SelectReservesearchwordComponent'; +import { SelectLazyComponent } from './SelectLazyComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiIconModule, + TiPaginationModule, + TiScrollModule, + TiSelectModule, + TiTextModule, + TiTipModule, + TiValidationModule, + DemoLogModule, + RouterModule.forChild(SelectTestModule.ROUTES) + ], + declarations: [ + SelectBasicComponent, + SelectTagComponent, + SelectTemplateComponent, + SelectGroupComponent, + SelectEventComponent, + SelectLazyComponent, + SelectLeakComponent, + SelectLeakComponent, + SelectSmallComponent, + SelectDisabledComponent, + SelectFocusComponent, + SelectDisabledComponent, + SelectSearchComponent, + SelectMultiComponent, + SelectMaxlineComponent, + SelectNoborderComponent, + SelectNoemptyComponent, + SelectNodataComponent, + SelectPanelComponent, + SelectTipComponent, + SelectValidComponent, + SelectLoadComponent, + SelectInputComponent, + SelectManyComponent, + SelectIdComponent, + SelectValidgroupComponent, + SelectTiscrollComponent, + SelectValuekeyComponent, + SelectClearableComponent, + SelectMuchComponent, + SelectSelectallComponent, + SelectSearchkeysComponent, + NoEmptyPipe, + SelectTworowComponent, + SelectEnumComponent, + SelectBeforesearchComponent, + SelectBeforesearchTestComponent, + SelectPaginationComponent, + SelectValuekeyTestComponent, + SelectPaginBeforesearchComponent, + SelectNullComponent, + SelectVirtualscrollComponent, + SelectVirtualscrollMultiComponent, + SelectChangeSelectallComponent, + SelectIdkeyComponent, + SelectScrollLoadComponent, + SelectDisabledfocusComponent, + SelectAppendtobodyComponent, + SelectShowselectednumberComponent, + SelectLabelkeyComponent, + SelectReservesearchwordComponent + ] +}) +export class SelectTestModule { + static readonly LINKS: Array = [{ href: 'components/TiSelectComponent.html', label: 'Select' }]; + static readonly ROUTES: Routes = [ + { + path: 'select/select-basic', + component: SelectBasicComponent + }, + { + path: 'select/select-template', + component: SelectTemplateComponent + }, + { + path: 'select/select-labelkey', + component: SelectLabelkeyComponent + }, + { + path: 'select/select-valuekey', + component: SelectValuekeyComponent + }, + { + path: 'select/select-valuekey-test', + component: SelectValuekeyTestComponent + }, + { + path: 'select/select-group', + component: SelectGroupComponent + }, + { + path: 'select/select-event', + component: SelectEventComponent + }, + { + path: 'select/select-lazy', + component: SelectLazyComponent + }, + { + path: 'select/select-disabled', + component: SelectDisabledComponent + }, + { + path: 'select/select-search', + component: SelectSearchComponent + }, + { + path: 'select/select-searchkeys', + component: SelectSearchkeysComponent + }, + { + path: 'select/select-reservesearchword', + component: SelectReservesearchwordComponent + }, + { + path: 'select/select-beforesearch', + component: SelectBeforesearchComponent + }, + { + path: 'select/select-multi', + component: SelectMultiComponent + }, + { + path: 'select/select-maxline', + component: SelectMaxlineComponent + }, + { + path: 'select/select-multi-maxline', + component: SelectShowselectednumberComponent + }, + { + path: 'select/select-panel', + component: SelectPanelComponent + }, + { + path: 'select/select-tip', + component: SelectTipComponent + }, + { + path: 'select/select-focus', + component: SelectFocusComponent + }, + { + path: 'select/select-nodata', + component: SelectNodataComponent + }, + { + path: 'select/select-tiscroll', + component: SelectTiscrollComponent + }, + { + path: 'select/select-appendtobody', + component: SelectAppendtobodyComponent + }, + { + path: 'select/select-clearable', + component: SelectClearableComponent + }, + { + path: 'select/select-selectall', + component: SelectSelectallComponent + }, + { + path: 'select/select-tag', + component: SelectTagComponent + }, + { + path: 'select/select-tworow', + component: SelectTworowComponent + }, + { + path: 'select/select-pagination', + component: SelectPaginationComponent + }, + { + path: 'select/select-pagin-beforesearch', + component: SelectPaginBeforesearchComponent + }, + { + path: 'select/select-virtualscroll', + component: SelectVirtualscrollComponent + }, + { + path: 'select/select-virtualscroll-multi', + component: SelectVirtualscrollMultiComponent + }, + { + path: 'select/select-scroll-load', + component: SelectScrollLoadComponent + }, + { path: 'select/select-small', component: SelectSmallComponent }, + { path: 'select/select-noborder', component: SelectNoborderComponent }, + { path: 'select/select-noempty', component: SelectNoemptyComponent }, + { path: 'select/select-leak', component: SelectLeakComponent }, + { path: 'select/select-valid', component: SelectValidComponent }, + { path: 'select/select-validgroup', component: SelectValidgroupComponent }, + { path: 'select/select-load', component: SelectLoadComponent }, + { path: 'select/select-many', component: SelectManyComponent }, + { path: 'select/select-input', component: SelectInputComponent }, + { path: 'select/select-id', component: SelectIdComponent }, + { path: 'select/select-much', component: SelectMuchComponent }, + { path: 'select/select-enum', component: SelectEnumComponent }, + { path: 'select/select-null', component: SelectNullComponent }, + { + path: 'select/select-beforesearch-test', + component: SelectBeforesearchTestComponent + }, + { + path: 'select/select-change-selectall', + component: SelectChangeSelectallComponent + }, + { + path: 'select/select-disabledfocus', + component: SelectDisabledfocusComponent + }, + { + path: 'select/select-idkey', + component: SelectIdkeyComponent + } + ]; +} diff --git a/src/select/demo/src/app/select/SelectTipComponent.ts b/src/select/demo/src/app/select/SelectTipComponent.ts new file mode 100644 index 0000000..46dc34e --- /dev/null +++ b/src/select/demo/src/app/select/SelectTipComponent.ts @@ -0,0 +1,29 @@ +import { Component, TemplateRef, ViewChild } from '@angular/core'; + +@Component({ + templateUrl: './select-tip.html' +}) +export class SelectTipComponent { + @ViewChild('tip', { static: true }) tip: TemplateRef; + options: Array; + value: any; + + ngOnInit(): void { + this.options = [ + { label: '水调歌头' }, + { label: '苏轼' }, + { label: '(有tip)明月几时有?把酒问青天。', tip: this.tip }, + { label: '不知天上宫阙,今夕是何年。' }, + { + label: '(有tip)我欲乘风归去,又恐琼楼玉宇,高处不胜寒。', + tip: this.tip + }, + { label: '起舞弄清影,何似在人间?' }, + { label: '转朱阁,低绮户,照无眠。' }, + { label: '不应有恨,何事长向别时圆?' }, + { label: '人有悲欢离合,月有阴晴圆缺,此事古难全。' }, + { label: '但愿人长久,千里共婵娟。' } + ]; + this.value = this.options[1]; + } +} diff --git a/src/select/demo/src/app/select/SelectTiscrollComponent.ts b/src/select/demo/src/app/select/SelectTiscrollComponent.ts new file mode 100644 index 0000000..8f2752d --- /dev/null +++ b/src/select/demo/src/app/select/SelectTiscrollComponent.ts @@ -0,0 +1,61 @@ +import { Component, ElementRef, Renderer2, ViewChild } from '@angular/core'; +import { Util } from '@opentiny/ng'; +@Component({ + templateUrl: './select-tiscroll.html', + styles: ['p {margin-bottom: 10px;}'] +}) +export class SelectTiscrollComponent { + @ViewChild('container') private containerRef: ElementRef; + myOptions: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + mySelected: any; + constructor(private renderer2: Renderer2) {} + + ngAfterViewInit(): void { + // 监听容器滚动事件,触发tiScroll收起下拉 + this.renderer2.listen(this.containerRef.nativeElement, 'scroll', () => { + // tiScroll 是自定义的事件,可以触发面板收起 + Util.trigger(document, 'tiScroll'); + }); + } +} diff --git a/src/select/demo/src/app/select/SelectTworowComponent.ts b/src/select/demo/src/app/select/SelectTworowComponent.ts new file mode 100644 index 0000000..21731eb --- /dev/null +++ b/src/select/demo/src/app/select/SelectTworowComponent.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-tworow.html' +}) +export class SelectTworowComponent { + options: Array = [ + { + primary: '子网1 (192.168.0.1/24)', + secondary: '可用IP数278', + disabled: true + }, + { + primary: '子网2 (192.168.0.1/25)', + secondary: '可用IP数312' + }, + { + primary: '子网3 (192.168.0.1/26)', + secondary: '可用IP数225' + }, + { + primary: '子网4 (192.168.0.1/27)', + secondary: '可用IP数300' + } + ]; + + value: any; +} diff --git a/src/select/demo/src/app/select/SelectValidComponent.ts b/src/select/demo/src/app/select/SelectValidComponent.ts new file mode 100644 index 0000000..ce30630 --- /dev/null +++ b/src/select/demo/src/app/select/SelectValidComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './select-valid.html' +}) +export class SelectValidComponent { + options: Array = [ + { + label: '美国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + }, + { + label: '法国' + }, + { + label: '德国' + }, + { + label: '日本' + }, + { + label: '韩国' + }, + { + label: '土耳其' + }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + value: any = this.options[0]; +} diff --git a/src/select/demo/src/app/select/SelectValidGroupComponent.ts b/src/select/demo/src/app/select/SelectValidGroupComponent.ts new file mode 100644 index 0000000..3581c5e --- /dev/null +++ b/src/select/demo/src/app/select/SelectValidGroupComponent.ts @@ -0,0 +1,75 @@ +import { Component } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; + +@Component({ + templateUrl: './select-validgroup.html' +}) +export class SelectValidgroupComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '巴西', + englishname: 'Brazil', + disabled: false + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + }, + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + mySelected: any; + + mySelecteds: any; + + myinputvalue: string; + + constructor(private fb: FormBuilder) {} + myFormGroup: FormGroup; + ngOnInit(): void { + this.myFormGroup = this.fb.group({ + name: '', + home: '', + age: '' + }); + } + onBlurSelect(): void { + console.log('onBlurSelect'); + } +} diff --git a/src/select/demo/src/app/select/SelectValuekeyComponent.ts b/src/select/demo/src/app/select/SelectValuekeyComponent.ts new file mode 100644 index 0000000..37f91e3 --- /dev/null +++ b/src/select/demo/src/app/select/SelectValuekeyComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-valuekey.html' +}) +export class SelectValuekeyComponent { + options: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + value: any; +} diff --git a/src/select/demo/src/app/select/SelectValuekeyTestComponent.ts b/src/select/demo/src/app/select/SelectValuekeyTestComponent.ts new file mode 100644 index 0000000..5e88c47 --- /dev/null +++ b/src/select/demo/src/app/select/SelectValuekeyTestComponent.ts @@ -0,0 +1,111 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-valuekey-test.html' +}) +export class SelectValuekeyTestComponent { + myOptions: Array = [ + { label: '美国', englishname: 'America' }, + { label: '巴西', englishname: 'Brazil' }, + { label: '加拿大', englishname: 'Canada' }, + { label: '中国', englishname: 'China' }, + { label: '法国', englishname: 'France' }, + { label: '德国', englishname: 'Germany' }, + { label: '日本', englishname: 'Japan' }, + { label: '韩国', englishname: 'South Korea' }, + { label: '土耳其', englishname: 'Turkey' }, + { label: '大不列颠和北爱兰联合王国', englishname: 'United Kingdom' } + ]; + myOptions2: Array = [ + { + label: '北美洲', + children: [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + } + ] + }, + { + label: '南美洲', + children: [ + { + label: '巴西', + englishname: 'Brazil', + disabled: false + } + ] + }, + { + label: '亚洲', + children: [ + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + } + ] + }, + { + label: '欧洲', + children: [ + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ] + } + ]; + myOptions3: Array; + mySelected1: string = 'China'; + mySelecteds: any = ['China', 'Japan']; + mySelecteds2: any = ['China', 'Japan']; + + changeOptions(): void { + this.myOptions = [ + { label: '美国', englishname: 'America' }, + { label: '巴西', englishname: 'Brazil' }, + { label: '加拿大', englishname: 'Canada' }, + { label: '中国', englishname: 'China' }, + { label: '法国', englishname: 'France' }, + { label: '德国', englishname: 'Germany' }, + { label: '日本', englishname: 'Japan' }, + { label: '韩国', englishname: 'South Korea' }, + { label: '土耳其', englishname: 'Turkey' }, + { label: '大不列颠和北爱兰联合王国', englishname: 'United Kingdom' }, + { label: '俄罗斯', englishname: 'Russia' } + ]; + } + + addOptions(): void { + this.myOptions3 = this.myOptions2; + } +} diff --git a/src/select/demo/src/app/select/SelectVirtualscrollComponent.ts b/src/select/demo/src/app/select/SelectVirtualscrollComponent.ts new file mode 100644 index 0000000..a9d9611 --- /dev/null +++ b/src/select/demo/src/app/select/SelectVirtualscrollComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-virtualscroll.html' +}) +export class SelectVirtualscrollComponent { + private data: Array = [ + { + label: '美国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + }, + { + label: '法国' + }, + { + label: '德国' + }, + { + label: '日本' + }, + { + label: '韩国' + }, + { + label: '土耳其' + }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + private dataA: Array = []; + options: Array = []; + value: any; + + constructor() { + for (let i: number = 0; i < 10000; i++) { + const item: any = this.data[i % 10]; + this.dataA.push({ ...item, label: i + item.label }); + } + this.options = [].concat(this.dataA); + } +} diff --git a/src/select/demo/src/app/select/SelectVirtualscrollMultiComponent.ts b/src/select/demo/src/app/select/SelectVirtualscrollMultiComponent.ts new file mode 100644 index 0000000..9f97d61 --- /dev/null +++ b/src/select/demo/src/app/select/SelectVirtualscrollMultiComponent.ts @@ -0,0 +1,201 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-virtualscroll-multi.html' +}) +export class SelectVirtualscrollMultiComponent { + private data: Array = [ + { + label: '美国', + englishname: 'America', + primary: '子网 (192.168.0.1/24)', + secondary: '可用IP数278', + disabled: false + }, + { + label: '巴西', + englishname: 'Brazil', + primary: '子网 (192.168.0.1/25)', + secondary: '可用IP数312', + disabled: false + }, + { + label: '加拿大', + englishname: 'Canada', + primary: '子网 (192.168.0.1/26)', + secondary: '可用IP数225', + disabled: true + }, + { + label: '中国', + englishname: 'China', + primary: '子网 (192.168.0.1/27)', + secondary: '可用IP数300', + disabled: false + }, + { + label: '法国', + englishname: 'France', + primary: '子网 (192.168.0.1/28)', + secondary: '可用IP数278', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + primary: '子网 (192.168.0.1/29)', + secondary: '可用IP数312', + disabled: false + }, + { + label: '日本', + englishname: 'Japan', + primary: '子网 (192.168.0.1/30)', + secondary: '可用IP数225' + }, + { + label: '韩国', + englishname: 'South Korea', + primary: '子网 (192.168.0.1/31)', + secondary: '可用IP数300' + }, + { + label: '土耳其', + englishname: 'Turkey', + primary: '子网 (192.168.0.1/31)', + secondary: '可用IP数300' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom', + primary: '子网 (192.168.0.1/31)', + secondary: '可用IP数300' + } + ]; + + private dataA: Array = []; + private dataB: Array = []; + myOptions1: Array; + myOptions2: Array; + myOptions3: Array; + myOptions4: Array; + myOptions5: Array; + mySelected1: any; + mySelected2: any; + mySelected3: any; + mySelected4: any; + mySelected5: any; + + constructor() { + for (let i: number = 0; i < 10000; i++) { + const item: any = this.data[i % 10]; + this.dataA.push({ ...item, label: i + item.label }); + } + for (let i: number = 0; i < 5; i++) { + const item: any = this.data[i % 10]; + this.dataB.push({ ...item, label: i + item.label }); + } + + this.myOptions1 = []; + this.myOptions2 = [].concat(this.dataA); + this.myOptions3 = this.dataA.slice(0, 1000); + this.myOptions5 = [].concat(this.dataA); + } + + changeEmpty1(): void { + if (this.mySelected1) { + this.mySelected1 = undefined; + } + this.myOptions1 = []; + } + changeMany1(): void { + if (this.mySelected1) { + this.mySelected1 = undefined; + } + this.myOptions1 = [].concat(this.dataA); + } + changeFew1(): void { + if (this.mySelected1) { + this.mySelected1 = undefined; + } + this.myOptions1 = [].concat(this.dataB); + } + changeEmpty2(): void { + if (this.mySelected2) { + this.mySelected2 = undefined; + } + this.myOptions2 = []; + } + changeMany2(): void { + if (this.mySelected2) { + this.mySelected2 = undefined; + } + this.myOptions2 = [].concat(this.dataA); + } + changeFew2(): void { + if (this.mySelected2) { + this.mySelected2 = undefined; + } + this.myOptions2 = [].concat(this.dataB); + } + + changeEmpty3(): void { + if (this.mySelected3) { + this.mySelected3 = undefined; + } + this.myOptions3 = []; + } + changeMany3(): void { + if (this.mySelected3) { + this.mySelected3 = undefined; + } + this.myOptions3 = this.dataA.slice(0, 1000); + } + changeFew3(): void { + if (this.mySelected3) { + this.mySelected3 = undefined; + } + this.myOptions3 = [].concat(this.dataB); + } + + changeEmpty4(): void { + if (this.mySelected4) { + this.mySelected4 = undefined; + } + this.myOptions4 = []; + } + changeMany4(): void { + if (this.mySelected4) { + this.mySelected4 = undefined; + } + this.myOptions4 = this.dataA.slice(0, 1000); + } + changeFew4(): void { + if (this.mySelected4) { + this.mySelected4 = undefined; + } + this.myOptions4 = [].concat(this.dataB); + } + + changeEmpty5(): void { + if (this.mySelected5) { + this.mySelected5 = undefined; + } + this.myOptions5 = []; + } + changeMany5(): void { + if (this.mySelected5) { + this.mySelected5 = undefined; + } + this.myOptions5 = [].concat(this.dataA); + } + changeFew5(): void { + if (this.mySelected5) { + this.mySelected5 = undefined; + } + this.myOptions5 = [].concat(this.dataB); + } + mousedown(): void { + console.log('footer is clicked'); + } +} diff --git a/src/select/demo/src/app/select/select-appendtobody.html b/src/select/demo/src/app/select/select-appendtobody.html new file mode 100644 index 0000000..6ae71bd --- /dev/null +++ b/src/select/demo/src/app/select/select-appendtobody.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/select-basic.html b/src/select/demo/src/app/select/select-basic.html new file mode 100644 index 0000000..b2f6a2a --- /dev/null +++ b/src/select/demo/src/app/select/select-basic.html @@ -0,0 +1,5 @@ + + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/select/demo/src/app/select/select-beforesearch-test.html b/src/select/demo/src/app/select/select-beforesearch-test.html new file mode 100644 index 0000000..e066b63 --- /dev/null +++ b/src/select/demo/src/app/select/select-beforesearch-test.html @@ -0,0 +1,41 @@ +

    描述

    +

    Select组件提供beforeSearch事件接口,监听搜索框的内容变更,可以获取到搜索框的内容

    +

    beforeSearch事件接口

    +

    示例

    +

    示例中,列表初始化时加载三条数据,搜索时加载全部的匹配的数据,比如在示例1搜索“国”,或者在示例2搜索“国”

    +

    1.单选 + 后台搜索:

    +
    +

    +国家: + + +
    +

    选中项:{{mySelecteds | json}}

    +
    + +

    2.多选 + 全选 + 后台搜索,下拉选项中有禁用的:

    +
    +

    +国家: + + +
    +

    选中项:{{mySelecteds1 | json}}

    +
    diff --git a/src/select/demo/src/app/select/select-beforesearch.html b/src/select/demo/src/app/select/select-beforesearch.html new file mode 100644 index 0000000..f814150 --- /dev/null +++ b/src/select/demo/src/app/select/select-beforesearch.html @@ -0,0 +1,9 @@ + + diff --git a/src/select/demo/src/app/select/select-change-selectall.html b/src/select/demo/src/app/select/select-change-selectall.html new file mode 100644 index 0000000..7a48ec4 --- /dev/null +++ b/src/select/demo/src/app/select/select-change-selectall.html @@ -0,0 +1,34 @@ +

    描述

    +

    测试动态改变selectAll接口

    +

    示例

    + +

    1.基本多选:

    +
    +国家: + + + +

    2.分组多选:

    +
    +国家: + + +
    + + diff --git a/src/select/demo/src/app/select/select-clearable.html b/src/select/demo/src/app/select/select-clearable.html new file mode 100644 index 0000000..af06e3c --- /dev/null +++ b/src/select/demo/src/app/select/select-clearable.html @@ -0,0 +1,24 @@ +

    1.单选

    + + + +

    2.多选

    + + + + diff --git a/src/select/demo/src/app/select/select-disabled.html b/src/select/demo/src/app/select/select-disabled.html new file mode 100644 index 0000000..cfee123 --- /dev/null +++ b/src/select/demo/src/app/select/select-disabled.html @@ -0,0 +1,5 @@ +

    1.整体禁用

    + + +

    2.数据项禁用

    + diff --git a/src/select/demo/src/app/select/select-disabledfocus.html b/src/select/demo/src/app/select/select-disabledfocus.html new file mode 100644 index 0000000..e5e02e9 --- /dev/null +++ b/src/select/demo/src/app/select/select-disabledfocus.html @@ -0,0 +1,10 @@ +
    {{myDisabled}} +
    + + + + + +

    diff --git a/src/select/demo/src/app/select/select-enum.html b/src/select/demo/src/app/select/select-enum.html new file mode 100644 index 0000000..01fed39 --- /dev/null +++ b/src/select/demo/src/app/select/select-enum.html @@ -0,0 +1,46 @@ +

    描述

    +

    Select选择下拉组件, 自定义选中值, 设置valueKey时选中值基于valueKey

    +

    valueKey接口

    +

    示例

    + +

    1.单选,设置valueKey:

    +
    +国家: + + +
    +

    选中项:{{mySelected1 | json}}

    +
    + +

    2.多选,设置valueKey:

    +
    +国家: + + +
    +

    选中项:{{mySelecteds | json}}

    +
    + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-event.html b/src/select/demo/src/app/select/select-event.html new file mode 100644 index 0000000..8b4cc9d --- /dev/null +++ b/src/select/demo/src/app/select/select-event.html @@ -0,0 +1,3 @@ + + + diff --git a/src/select/demo/src/app/select/select-focus.html b/src/select/demo/src/app/select/select-focus.html new file mode 100644 index 0000000..67144f7 --- /dev/null +++ b/src/select/demo/src/app/select/select-focus.html @@ -0,0 +1,38 @@ +

    描述

    +

    Select选择下拉组件, 支持聚焦autofocus/focus()/tabindex演示

    +

    示例

    + +

    1.autofocus:

    +

    + +

    2.focus():

    +
    +
    + +  + +  + +  + +

    + +

    3.tabindex:

    +tabindex="1" +

    +tabindex="3" +

    +tabindex="4" +

    +tabindex="2" + + + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-group.html b/src/select/demo/src/app/select/select-group.html new file mode 100644 index 0000000..b165411 --- /dev/null +++ b/src/select/demo/src/app/select/select-group.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/select-id.html b/src/select/demo/src/app/select/select-id.html new file mode 100644 index 0000000..d45d91b --- /dev/null +++ b/src/select/demo/src/app/select/select-id.html @@ -0,0 +1,53 @@ +

    描述

    +

    Select选择下拉组件,带有搜索框,设置[searchable]="true"

    +

    示例

    + +1.单选: + +
    + +2.模板: + + + + {{i}}  +  {{item.label}} {{item.englishname}} + + + +
    + +3.多选: + + +
    + +4.搜索: + + +
    + +5.分组: + +
    + +

    id是否存在:

    +

    {{id+':'+idExistMap.get(id)}}

    +

    所有id是否存在:{{allIdExist}}

    + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-idkey.html b/src/select/demo/src/app/select/select-idkey.html new file mode 100644 index 0000000..6a7312c --- /dev/null +++ b/src/select/demo/src/app/select/select-idkey.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/select-input.html b/src/select/demo/src/app/select/select-input.html new file mode 100644 index 0000000..2417739 --- /dev/null +++ b/src/select/demo/src/app/select/select-input.html @@ -0,0 +1,29 @@ +

    描述

    +

    Select选择下拉组件, 基础使用:不设置labelKey/设置labelKey/自定义内容模板

    +

    示例

    + +

    1.设置options(不设置labelKey,默认显示字段"label"):

    +
    +国家: + + + + + +
    + + + + + +
    + + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-labelkey.html b/src/select/demo/src/app/select/select-labelkey.html new file mode 100644 index 0000000..2005999 --- /dev/null +++ b/src/select/demo/src/app/select/select-labelkey.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/select-lazy.html b/src/select/demo/src/app/select/select-lazy.html new file mode 100644 index 0000000..e013c9a --- /dev/null +++ b/src/select/demo/src/app/select/select-lazy.html @@ -0,0 +1,2 @@ + + diff --git a/src/select/demo/src/app/select/select-leak.html b/src/select/demo/src/app/select/select-leak.html new file mode 100644 index 0000000..0b2cc36 --- /dev/null +++ b/src/select/demo/src/app/select/select-leak.html @@ -0,0 +1,10 @@ + +
    +
    +
    + 国家: + +
    +
    +

    选中项:{{mySelected | json}}

    +

    ti-drop个数:

    diff --git a/src/select/demo/src/app/select/select-load.html b/src/select/demo/src/app/select/select-load.html new file mode 100644 index 0000000..6ada47c --- /dev/null +++ b/src/select/demo/src/app/select/select-load.html @@ -0,0 +1,35 @@ +

    描述

    +

    Select选择下拉组件,数据加载。

    +

    示例

    +

    使用典型场景:空数据->数据A->数据B:

    +
    +单选: + +
    +

    选中项:{{mySelected | json}}

    + +多选: + + +
    +

    选中项:{{mySelecteds | json}}

    + +

    每个组件改变数据,都用下面六个按钮。不变化

    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-many.html b/src/select/demo/src/app/select/select-many.html new file mode 100644 index 0000000..97c0b9b --- /dev/null +++ b/src/select/demo/src/app/select/select-many.html @@ -0,0 +1,29 @@ +

    描述

    +

    Select选择下拉组件,性能测试。

    +

    示例

    +

    使用典型场景:空数据->大数据A->大数据B:

    +
    +单选: + +
    +

    选中项:{{mySelected | json}}

    +

    总条数:{{myOptions&&myOptions.length}}

    + +多选: + + +
    +

    选中项:{{mySelecteds | json}}

    +

    总条数:{{myOptions&&myOptions.length}}

    + +
    +
    + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-maxline.html b/src/select/demo/src/app/select/select-maxline.html new file mode 100644 index 0000000..8cba2a5 --- /dev/null +++ b/src/select/demo/src/app/select/select-maxline.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/select-much.html b/src/select/demo/src/app/select/select-much.html new file mode 100644 index 0000000..3ac05b8 --- /dev/null +++ b/src/select/demo/src/app/select/select-much.html @@ -0,0 +1,9 @@ +

    描述

    +

    Select选择下拉组件性能测试。

    +

    示例

    +

    使用典型场景:页面中使用大量select组件

    +
    + + +


    +
    diff --git a/src/select/demo/src/app/select/select-multi.html b/src/select/demo/src/app/select/select-multi.html new file mode 100644 index 0000000..8c40071 --- /dev/null +++ b/src/select/demo/src/app/select/select-multi.html @@ -0,0 +1,5 @@ + + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/select/demo/src/app/select/select-noborder.html b/src/select/demo/src/app/select/select-noborder.html new file mode 100644 index 0000000..37f36a7 --- /dev/null +++ b/src/select/demo/src/app/select/select-noborder.html @@ -0,0 +1,53 @@ +

    描述

    +

    + Selec选择下拉组件, 设置是否有边框:noborder属性
    设置dominator 到drop之间的距离:dominatorSpace接口
    设置深色主题:dark + 属性(主题只处理了无边框的场景) +

    +

    示例

    + +无边框: + +
    +
    +无边框有间距: + + +
    +
    +深色无边框有间距: +
    + + +
    +
    +
    +默认: + +
    +
    + + diff --git a/src/select/demo/src/app/select/select-nodata.html b/src/select/demo/src/app/select/select-nodata.html new file mode 100644 index 0000000..c337f87 --- /dev/null +++ b/src/select/demo/src/app/select/select-nodata.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/select-noempty.html b/src/select/demo/src/app/select/select-noempty.html new file mode 100644 index 0000000..997e293 --- /dev/null +++ b/src/select/demo/src/app/select/select-noempty.html @@ -0,0 +1,21 @@ +

    描述

    +

    Select选择下拉组件, 只显示非空选项

    +

    示例

    + +

    1.设置options(不设置labelKey,默认显示字段"label"):

    +
    +国家: + + +
    +

    选中项:{{mySelected | json}}

    +
    + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-null.html b/src/select/demo/src/app/select/select-null.html new file mode 100644 index 0000000..ddd24f2 --- /dev/null +++ b/src/select/demo/src/app/select/select-null.html @@ -0,0 +1,42 @@ +

    描述

    +

    Select选择下拉组件,可变@Input接口异常数据。不测ngModel,不测boolean,不测maxline=3等一旦赋值不再改变的接口。

    +

    + 因为Angular + Html模板,ngFor的items可以抵御undefined/null,但不能抵御类型错误(要求array,传入5)。所以,TS代码也保持同样的容错能力,要求能够抵御undefined/null等 +

    +

    + 这里仅测赋值null等是否正常。@Input数据动态变化,null->数据A->数据B,用代码Review来保证,不要在ngOnInit中处理@Input数据(因为Init只跑一次),而应该在ngOnChange里处理。 +

    +

    导入

    +import {{ '{' }} TiSelectModule {{ '}' }} from '@opentiny/ng'; +

    示例

    +以下示例,控制台是否报错,组件显示交互是否正常。 +

    1.undefined:

    +
    +国家: + + + +

    2.null:

    +
    +国家: + + +

    3.空数据([],'',0):

    +
    +国家: + + +

    4.边界数据(不涉及)

    +
    +

    5.超边界数据(不涉及)

    +
    + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-pagin-beforesearch.html b/src/select/demo/src/app/select/select-pagin-beforesearch.html new file mode 100644 index 0000000..f55fb19 --- /dev/null +++ b/src/select/demo/src/app/select/select-pagin-beforesearch.html @@ -0,0 +1,110 @@ +

    描述

    +

    10.0.3提供该功能

    +

    1.Select的下拉选项如果需要分页显示,需要通过footer将分页添加到下拉面板,如果footer功能异常,参考“模板”示例。

    +

    2.分页结合后台搜索的功能,需要开发者控制分页的 currentPage ,并且将对应页码的options数据传入组件。

    +

    导入

    +import {{ '{' }} TiSelectModule, TiPaginationModule {{ '}' }} from '@opentiny/ng'; +

    示例

    +

    设置options和自定义内容模板:

    + +

    1.单选 + 搜索 + 分页:(10.1.16 版本参考该示例)

    + + + + + +
    +

    选中项:{{mySelected1 | json}}

    + +

    2.单选 + 搜索 + 分页 + beforeOpen:(10.1.10~10.1.15 版本参考该示例)

    + + + + + +
    +

    选中项:{{mySelected2 | json}}

    + +

    3.多选 + 搜索 + 分页:(10.1.16 版本参考该示例)

    +
    + + + + + +
    +

    选中项:{{mySelected3 | json}}

    + +

    4.多选 + 搜索 + 分页 + beforeOpen:(10.1.10~10.1.15 版本参考该示例)

    +
    + + + + + +
    +

    选中项:{{mySelected4 | json}}

    diff --git a/src/select/demo/src/app/select/select-pagination.html b/src/select/demo/src/app/select/select-pagination.html new file mode 100644 index 0000000..5465f6e --- /dev/null +++ b/src/select/demo/src/app/select/select-pagination.html @@ -0,0 +1,11 @@ + + + + + +
    diff --git a/src/select/demo/src/app/select/select-panel.html b/src/select/demo/src/app/select/select-panel.html new file mode 100644 index 0000000..360cbf6 --- /dev/null +++ b/src/select/demo/src/app/select/select-panel.html @@ -0,0 +1,2 @@ + + diff --git a/src/select/demo/src/app/select/select-reservesearchword.html b/src/select/demo/src/app/select/select-reservesearchword.html new file mode 100644 index 0000000..70a81cb --- /dev/null +++ b/src/select/demo/src/app/select/select-reservesearchword.html @@ -0,0 +1,9 @@ + + diff --git a/src/select/demo/src/app/select/select-scroll-load.html b/src/select/demo/src/app/select/select-scroll-load.html new file mode 100644 index 0000000..87b32bf --- /dev/null +++ b/src/select/demo/src/app/select/select-scroll-load.html @@ -0,0 +1,12 @@ + + diff --git a/src/select/demo/src/app/select/select-search.html b/src/select/demo/src/app/select/select-search.html new file mode 100644 index 0000000..611e0f3 --- /dev/null +++ b/src/select/demo/src/app/select/select-search.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/select-searchkeys.html b/src/select/demo/src/app/select/select-searchkeys.html new file mode 100644 index 0000000..fd12ebc --- /dev/null +++ b/src/select/demo/src/app/select/select-searchkeys.html @@ -0,0 +1,9 @@ + + diff --git a/src/select/demo/src/app/select/select-selectall.html b/src/select/demo/src/app/select/select-selectall.html new file mode 100644 index 0000000..f389052 --- /dev/null +++ b/src/select/demo/src/app/select/select-selectall.html @@ -0,0 +1,2 @@ + + diff --git a/src/select/demo/src/app/select/select-showselectednumber.html b/src/select/demo/src/app/select/select-showselectednumber.html new file mode 100644 index 0000000..578e8dd --- /dev/null +++ b/src/select/demo/src/app/select/select-showselectednumber.html @@ -0,0 +1,11 @@ + + diff --git a/src/select/demo/src/app/select/select-small.html b/src/select/demo/src/app/select/select-small.html new file mode 100644 index 0000000..7908936 --- /dev/null +++ b/src/select/demo/src/app/select/select-small.html @@ -0,0 +1 @@ +
    diff --git a/src/select/demo/src/app/select/select-tag.html b/src/select/demo/src/app/select/select-tag.html new file mode 100644 index 0000000..6c25de3 --- /dev/null +++ b/src/select/demo/src/app/select/select-tag.html @@ -0,0 +1,70 @@ +

    描述

    +

    Select选择下拉组件选项添加tag标签

    +

    示例

    +

    设置options和自定义内容模板:

    + +

    1.单选

    + + + + {{i}}  +  {{item.label}} {{item.englishname}} + + + {{item.mark}} + {{item.tag}} + + + + + + {{item.label}} + {{item.englishname}} + {{item.tag}} + + + +
    +

    选中项:{{mySelected | json}}

    + +

    2.多选

    + + + + + {{i}}  +  {{item.label}} {{item.englishname}} + + + {{item.mark}} + {{item.tag}} + + + + + + {{item.label}} + {{item.englishname}} + {{item.tag}} + + + +
    +

    选中项:{{mySelecteds | json}}

    + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-tag.less b/src/select/demo/src/app/select/select-tag.less new file mode 100644 index 0000000..50989dc --- /dev/null +++ b/src/select/demo/src/app/select/select-tag.less @@ -0,0 +1,70 @@ +.select-dropdown-list-tag { + white-space: nowrap; + height: 16px; + line-height: 16px; + background-color: #fff3e8; + color: #fa9841; + display: inline-block; + border-radius: 2px; + margin-left: 10px; + padding: 0 4px; +} +.select-dropdown-list-mark { + background-color: #e9edfa; + color: #575d6c; +} +.select-selfTip { + min-width: 50px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.select-tag-container { + display: flex; + flex-grow: 1; + justify-content: space-between; +} + +.tag-in-dominator.select-dropdown-list-tag { + float: right; + transform: translateY(5px); +} +.tag-in-multDominator.select-dropdown-list-tag { + float: right; + transform: translateY(3px); +} + +.footer { + padding: 10px; + position: relative; + font-size: 12px; +} + +.footer ti-icon[local] { + margin-right: 4px; + font-size: 16px; + vertical-align: bottom; +} + +.footer:after { + position: absolute; + display: inline-block; + top: 0; + left: 10px; + content: ''; + width: calc(100% - 20px); + height: 1px; + background-color: var(--ti-common-color-line-dividing); +} + +.select-dropdown-list-mult { + display: inline-flex; + align-items: center; + width: calc(100% - 26px); +} + +.select-dropdown-list { + display: flex; + align-items: center; +} diff --git a/src/select/demo/src/app/select/select-template.html b/src/select/demo/src/app/select/select-template.html new file mode 100644 index 0000000..a75c7ac --- /dev/null +++ b/src/select/demo/src/app/select/select-template.html @@ -0,0 +1,25 @@ + + + + 请选择餐品 + + + {{ item.label }} + + {{ item.englishname }} + + + + {{ item.label }} + {{ item.englishname }} + + + + + + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/select/demo/src/app/select/select-tip.html b/src/select/demo/src/app/select/select-tip.html new file mode 100644 index 0000000..34e0cc1 --- /dev/null +++ b/src/select/demo/src/app/select/select-tip.html @@ -0,0 +1,12 @@ + + + + 当前提示文本:{{item.label}} diff --git a/src/select/demo/src/app/select/select-tiscroll.html b/src/select/demo/src/app/select/select-tiscroll.html new file mode 100644 index 0000000..ee5b4db --- /dev/null +++ b/src/select/demo/src/app/select/select-tiscroll.html @@ -0,0 +1,56 @@ +

    描述

    +

    tiScroll指令。

    +

    + 在有局部滚动条的容器中使用select,为了防止下拉框在容器滚动时与选择框分离,开发者需要在对应的容器上添加tiScroll指令,使下拉框在容器滚动时消失。
    + 也可以自己监听滚动元素的scroll事件,在回调中触发document下的tiScroll事件,使得容器元素滚动时面板收起 +

    +

    导入

    +import {{ '{' }} TiScrollModule {{ '}' }} from '@opentiny/ng'; +

    示例

    +
    +
    +

    1.没有使用tiScroll

    +
    +

    +

    红豆生南国

    +

    春来发几枝

    +

    愿君多采撷

    +

    此物最相思

    +

    红豆生南国

    +

    春来发几枝

    +

    愿君多采撷

    +

    此物最相思

    +
    +
    +
    +

    2.使用tiScroll指令

    +
    + +

    +

    红豆生南国

    +

    春来发几枝

    +

    愿君多采撷

    +

    此物最相思

    +

    红豆生南国

    +

    春来发几枝

    +

    愿君多采撷

    +

    此物最相思

    +
    +
    +
    +

    3.手动触发tiScroll

    +
    + +

    +

    红豆生南国

    +

    春来发几枝

    +

    愿君多采撷

    +

    此物最相思

    +

    红豆生南国

    +

    春来发几枝

    +

    愿君多采撷

    +

    此物最相思

    +
    +
    +
    diff --git a/src/select/demo/src/app/select/select-tworow.html b/src/select/demo/src/app/select/select-tworow.html new file mode 100644 index 0000000..a777589 --- /dev/null +++ b/src/select/demo/src/app/select/select-tworow.html @@ -0,0 +1,13 @@ + + + {{item.primary}} + + +
    {{i}} {{item.primary}}
    +
    {{item.secondary}}
    +
    +
    + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/select/demo/src/app/select/select-valid.html b/src/select/demo/src/app/select/select-valid.html new file mode 100644 index 0000000..f4864a4 --- /dev/null +++ b/src/select/demo/src/app/select/select-valid.html @@ -0,0 +1,10 @@ + + diff --git a/src/select/demo/src/app/select/select-validgroup.html b/src/select/demo/src/app/select/select-validgroup.html new file mode 100644 index 0000000..30a5e64 --- /dev/null +++ b/src/select/demo/src/app/select/select-validgroup.html @@ -0,0 +1,30 @@ +

    描述

    +

    Select选择下拉组件,校验

    +

    示例

    +
    + + + 单选: + + +
    +

    选中项:{{mySelected | json}}

    +
    + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-valuekey-test.html b/src/select/demo/src/app/select/select-valuekey-test.html new file mode 100644 index 0000000..5db3f77 --- /dev/null +++ b/src/select/demo/src/app/select/select-valuekey-test.html @@ -0,0 +1,73 @@ +

    描述

    +

    Select选择下拉组件, 自定义选中值, 设置valueKey时选中值基于valueKey

    +

    valueKey接口

    +

    示例

    + +

    1.单选,设置valueKey:

    +
    +国家: + + +
    +

    选中项:{{mySelected1 | json}}

    +
    + +

    2.多选,设置valueKey:

    +
    +国家: + + +
    +

    选中项:{{mySelecteds | json}}

    +
    + +

    + 3.动态修改 + options 接口数据测试:(select组件的选中值高亮不应发生变化) +

    +
    + +
    +
    +

    单选/多选的options:{{myOptions | json}}

    + +

    4.多选+分组,设置valueKey:

    +
    + + +   + +
    +
    +

    选中项:{{mySelecteds2 | json}}

    +
    + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-valuekey.html b/src/select/demo/src/app/select/select-valuekey.html new file mode 100644 index 0000000..20429bd --- /dev/null +++ b/src/select/demo/src/app/select/select-valuekey.html @@ -0,0 +1,5 @@ + + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/select/demo/src/app/select/select-virtualscroll-multi.html b/src/select/demo/src/app/select/select-virtualscroll-multi.html new file mode 100644 index 0000000..caa55e8 --- /dev/null +++ b/src/select/demo/src/app/select/select-virtualscroll-multi.html @@ -0,0 +1,141 @@ +

    描述

    +

    Select选择下拉组件, 下拉数据量大的时候可以将 virtual 接口设置为 true 来开启虚拟滚动功能。分组下拉不支持虚拟滚动。

    +

    virtual 接口:默认值为 false。

    +

    导入

    +import {{ '{' }} TiSelectModule {{ '}' }} from '@opentiny/ng'; +

    示例

    + +

    1.基础多选:

    +
    +国家: + + +
    +

    选中项:{{mySelected1 | json}}

    +

    总条数:{{myOptions1&&myOptions1.length}}

    + + +
    + +

    2.多选 + 搜索:

    +
    +国家: + + +
    +

    选中项:{{mySelected2 | json}}

    +

    总条数:{{myOptions2&&myOptions2.length}}

    + + +
    + +

    3.多选 + 全选:

    +
    +国家: + + +
    +

    选中项:{{mySelected3 | json}}

    +

    总条数:{{myOptions3&&myOptions3.length}}

    + + +
    + +

    4.多选 + 全选 + 搜索:

    +
    +国家: + + +
    +

    选中项:{{mySelected4 | json}}

    +

    总条数:{{myOptions4&&myOptions4.length}}

    + + +
    + +

    5.多选 + 搜索 + 自定义底部:

    +
    +国家: + + + + + +
    +

    选中项:{{mySelected5 | json}}

    +

    总条数:{{myOptions5&&myOptions5.length}}

    + + +
    + + diff --git a/src/select/demo/src/app/select/select-virtualscroll.html b/src/select/demo/src/app/select/select-virtualscroll.html new file mode 100644 index 0000000..a54fc96 --- /dev/null +++ b/src/select/demo/src/app/select/select-virtualscroll.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/webdoc/select-demos.js b/src/select/demo/src/app/select/webdoc/select-demos.js new file mode 100644 index 0000000..7b8768d --- /dev/null +++ b/src/select/demo/src/app/select/webdoc/select-demos.js @@ -0,0 +1,324 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'select-basic', + name: { + 'zh-CN': '基础', + 'en-US': 'select basic' + }, + desc: { + 'zh-CN': 'select 组件的最简用法。', + 'en-US': '

    select basic

    ' + }, + apis: ['TiSelectComponent.properties.options', 'TiSelectComponent.properties.placeholder'] + }, + { + demoId: 'select-multi', + name: { + 'zh-CN': '多选', + 'en-US': 'select multi' + }, + desc: { + 'zh-CN': '

    通过属性multiple配置多选。', + 'en-US': 'select multi' + }, + apis: ['TiSelectComponent.properties.multiple'] + }, + { + demoId: 'select-selectall', + name: { + 'zh-CN': '全选', + 'en-US': 'select selectall' + }, + desc: { + 'zh-CN': '

    通过属性selectAll配置组件在多选场景下是否显示全选框。', + 'en-US': 'select selectall' + }, + apis: ['TiSelectComponent.properties.selectAll'] + }, + { + demoId: 'select-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'select disabled' + }, + desc: { + 'zh-CN': '

    通过属性disabled配置组件是否禁用。', + 'en-US': 'select disabled' + }, + apis: ['TiSelectComponent.properties.disabled'] + }, + { + demoId: 'select-template', + name: { + 'zh-CN': '模板', + 'en-US': 'select template' + }, + desc: { + 'zh-CN': '自定义模板,组件提供了selecteditemplaceholderfooter四个插槽。', + 'en-US': 'select template' + }, + apis: [ + 'TiSelectComponent.slots.selectedTemplate', + 'TiSelectComponent.slots.itemTemplate', + 'TiSelectComponent.slots.placeholderTemplate', + 'TiSelectComponent.slots.footerTemplate' + ] + }, + { + demoId: 'select-valuekey', + name: { + 'zh-CN': '选中值', + 'en-US': 'select valuekey' + }, + desc: { + 'zh-CN': '

    通过属性valueKey自定义组件选中值。', + 'en-US': 'select valuekey' + }, + apis: ['TiSelectComponent.properties.valueKey'] + }, + { + demoId: 'select-labelkey', + name: { + 'zh-CN': '显示字段', + 'en-US': 'select labelkey' + }, + desc: { + 'zh-CN': '

    通过属性labelKey配置组件下拉面板显示字段。', + 'en-US': 'select labelkey' + }, + apis: ['TiSelectComponent.properties.labelKey'] + }, + { + demoId: 'select-idkey', + name: { + 'zh-CN': '唯一值', + 'en-US': 'select idkey' + }, + desc: { + 'zh-CN': '

    labelkeyvaluekey有重复项时,通过属性idKey设置唯一属性,实现下拉选中。', + 'en-US': 'select idkey' + }, + apis: ['TiSelectComponent.properties.idKey'] + }, + { + demoId: 'select-maxline', + name: { + 'zh-CN': '最大显示行数', + 'en-US': 'select maxline' + }, + desc: { + 'zh-CN': '

    通过属性maxLine配置组件在多选场景下选择框显示的最大行数。', + 'en-US': 'select maxline' + }, + apis: ['TiSelectComponent.properties.maxLine'] + }, + { + demoId: 'select-group', + name: { + 'zh-CN': '分组', + 'en-US': 'select group' + }, + desc: { + 'zh-CN': '

    通过属性options中的children属性设置分组。', + 'en-US': 'select group' + }, + apis: ['TiSelectComponent.properties.options'] + }, + { + demoId: 'select-pagination', + name: { + 'zh-CN': '分页', + 'en-US': 'select pagination' + }, + desc: { + 'zh-CN': '通过footer插槽,实现下拉面板分页场景。', + 'en-US': 'select pagination' + }, + apis: ['TiSelectComponent.slots.footerTemplate'] + }, + { + demoId: 'select-search', + name: { + 'zh-CN': '搜索', + 'en-US': 'select search' + }, + desc: { + 'zh-CN': '

    通过属性searchable配置组件是否显示搜索框。', + 'en-US': 'select search' + }, + apis: ['TiSelectComponent.properties.searchable'] + }, + { + demoId: 'select-searchkeys', + name: { + 'zh-CN': '搜索字段', + 'en-US': 'select searchkeys' + }, + desc: { + 'zh-CN': '

    通过属性searchKeys配置组件搜索字段。', + 'en-US': 'select searchkeys' + }, + apis: ['TiSelectComponent.properties.searchKeys'] + }, + { + demoId: 'select-reservesearchword', + name: { + 'zh-CN': '保留搜索关键词', + 'en-US': 'select reservesearchword' + }, + desc: { + 'zh-CN': '

    搜索场景下,通过属性reserveSearchword配置下拉面板收起后是否保留搜素关键词。', + 'en-US': 'select reservesearchword' + }, + apis: ['TiSelectComponent.properties.reserveSearchword'] + }, + { + demoId: 'select-beforesearch', + name: { + 'zh-CN': '后台搜索', + 'en-US': 'select beforesearch' + }, + desc: { + 'zh-CN': '

    通过事件beforeSearch实现后台搜索。', + 'en-US': 'select beforesearch' + }, + apis: ['TiSelectComponent.events.beforeSearch'] + }, + { + demoId: 'select-lazy', + name: { + 'zh-CN': '懒加载', + 'en-US': 'select lazy' + }, + desc: { + 'zh-CN': '

    通过事件beforeOpen实现懒加载。', + 'en-US': 'select lazy' + }, + apis: ['TiSelectComponent.events.beforeOpen'] + }, + { + demoId: 'select-showselectednumber', + name: { + 'zh-CN': '显示已选项个数', + 'en-US': 'select showSelectedNumber' + }, + desc: { + 'zh-CN': '

    通过属性showSelectedNumber配置组件在多选场景下选择框是否显示已选项的个数。', + 'en-US': 'select showSelectedNumber' + }, + apis: [ + 'TiSelectComponent.properties.showSelectedNumber', + 'TiSelectComponent.properties.showSelectedNumberTip', + 'TiSelectComponent.properties.selectedNumberTipPosition' + ] + }, + { + demoId: 'select-clearable', + name: { + 'zh-CN': '可清除', + 'en-US': 'select clearable' + }, + desc: { + 'zh-CN': '

    通过属性clearable配置组件是否开启清除已选项功能。', + 'en-US': 'select clearable' + }, + apis: ['TiSelectComponent.properties.clearable', 'TiSelectComponent.events.clear'] + }, + { + demoId: 'select-panel', + name: { + 'zh-CN': '面板样式', + 'en-US': 'select panel' + }, + desc: { + 'zh-CN': '

    通过属性panelWidth配置下拉面板宽度,通过属性panelMaxHeight配置下拉面板最大高度。', + 'en-US': 'select panel' + }, + apis: ['TiSelectComponent.properties.panelWidth', 'TiSelectComponent.properties.panelMaxHeight'] + }, + { + demoId: 'select-tip', + name: { + 'zh-CN': '文字提示', + 'en-US': 'select tip' + }, + desc: { + 'zh-CN': '组件的文字提示场景。', + 'en-US': 'select tip' + }, + apis: [ + 'TiSelectComponent.properties.tipMaxWidth', + 'TiSelectComponent.properties.selectedTipPosition', + 'TiSelectComponent.properties.tipPosition' + ] + }, + { + demoId: 'select-nodata', + name: { + 'zh-CN': '空数据', + 'en-US': 'select nodata' + }, + desc: { + 'zh-CN': '

    通过属性noDataText配置组件空数据时显示文本。', + 'en-US': 'select nodata' + }, + apis: ['TiSelectComponent.properties.noDataText'] + }, + { + demoId: 'select-appendtobody', + name: { + 'zh-CN': 'appendToBody', + 'en-US': 'select appendtobody' + }, + desc: { + 'zh-CN': '

    通过属性appendToBody配置下拉面板是否添加在body上。', + 'en-US': 'select appendtobody' + }, + apis: ['TiSelectComponent.properties.appendToBody'] + }, + { + demoId: 'select-virtualscroll', + name: { + 'zh-CN': '虚拟滚动', + 'en-US': 'select virtualscroll' + }, + desc: { + 'zh-CN': '

    通过属性virtual配置组件是否开启虚拟滚动。', + 'en-US': 'select virtualscroll' + }, + apis: ['TiSelectComponent.properties.virtual'] + }, + { + demoId: 'select-scroll-load', + name: { + 'zh-CN': '下拉滚动加载', + 'en-US': 'select scroll-load' + }, + desc: { + 'zh-CN': '

    通过事件scrollToBottom实现下拉滚动加载。', + 'en-US': 'select scroll-load' + }, + apis: [ + 'TiSelectComponent.events.scrollToBottom', + 'TiSelectComponent.methods.getSearchResult', + 'TiSelectComponent.methods.getSearchWord', + 'TiSelectComponent.methods.open' + ] + }, + { + demoId: 'select-event', + name: { + 'zh-CN': '事件', + 'en-US': 'select event' + }, + desc: { + 'zh-CN': '点击当前项时,触发select事件。', + 'en-US': 'select event' + }, + apis: ['TiSelectComponent.events.select'] + } + ] +}; diff --git a/src/select/demo/src/app/select/webdoc/select.cn.md b/src/select/demo/src/app/select/webdoc/select.cn.md new file mode 100644 index 0000000..74891c5 --- /dev/null +++ b/src/select/demo/src/app/select/webdoc/select.cn.md @@ -0,0 +1,28 @@ +--- +title: Select 选择器 +--- +# Select 选择器 + +

    + +Select 组件是提供下拉选项菜单的组件。   + ++ 支持单选、多选、分组、搜索、懒加载等场景。 + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +Select 是提供选项菜单的组件。   + ++ 支持单选、多选、分组、搜索、懒加载等场景。 + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/select/demo/src/app/select/webdoc/select.en.md b/src/select/demo/src/app/select/webdoc/select.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/select/demo/src/app/select/webdoc/select.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git "a/src/select/demo/src/app/select/\346\265\213\350\257\225\347\224\250\344\276\213.txt" "b/src/select/demo/src/app/select/\346\265\213\350\257\225\347\224\250\344\276\213.txt" new file mode 100644 index 0000000..297c5fe --- /dev/null +++ "b/src/select/demo/src/app/select/\346\265\213\350\257\225\347\224\250\344\276\213.txt" @@ -0,0 +1,83 @@ +1select-basic + +selectı򣬿Դ塣 +selectı򣬿Թر塣 +selectǣԴ塣 +selectǣԹر塣 +bodyհ״ߵԪأselectʧʱرա + +ѡеĿѡֵselectı + +2select-disabled + +ѡ +нõѡ޷ѡС +δõѡѡС + +ѡ +нõѡ޷ıѡС +δõѡԸıѡС +ı򣬽õtag޷ȡѡС +ıδõtag޷ȡѡС + +ã +ťúselect޷ + +3select-event +ı䵥ѡ +ѡѡȥѡѡɾѡ + +3select-focus +ı䵥ѡ +ѡѡȥѡѡɾѡ + +4select-focus +ε4ť + +5select-group + +Ч +ѽĿЧ +Ч + +ᱻ +ģ + +6select-id + +7select-lazy +selectһ + +8select-leak +飬selectժûڴй©磺dropȻbodyϣδժ +裺selectdropᱻbodyϡbuttonngIfժselectʱٿdropǷȻbodyϡ +ظһ顣 + +9load +εchangeZeroDatachangeDataAchangeDataBÿεselect鿴 +εchangeSelectchangeSelectsѡݻᷢ仯 + +10multi +ѡѡȥѡѡɾѡ + +11nodata + +12noempty + +13pannel +δ򿪸 + +14search +AnؼʲִСд +֣ + +15small + +16template + +17tip + +18valid + + + diff --git a/src/select/demo/src/favicon.ico b/src/select/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/select/demo/src/index.html b/src/select/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/select/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/select/demo/src/main.ts b/src/select/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/select/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/select/demo/test.ts b/src/select/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/select/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/select/demo/tsconfig.app.json b/src/select/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/select/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/select/demo/tsconfig.spec.json b/src/select/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/select/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/select/lib/index.ts b/src/select/lib/index.ts new file mode 100644 index 0000000..baee9b8 --- /dev/null +++ b/src/select/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSelectModule'; diff --git a/src/select/lib/ng-package.json b/src/select/lib/ng-package.json new file mode 100644 index 0000000..cbc5c84 --- /dev/null +++ b/src/select/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/select", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/select/lib/package.json b/src/select/lib/package.json new file mode 100644 index 0000000..fa4f909 --- /dev/null +++ b/src/select/lib/package.json @@ -0,0 +1,17 @@ +{ + "name": "@opentiny/ng-select", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-dominator": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-dropsearch": "~1.0.0-beta.0", + "@opentiny/ng-list": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/select/lib/project.json b/src/select/lib/project.json new file mode 100644 index 0000000..785d86f --- /dev/null +++ b/src/select/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/select/lib", + "sourceRoot": "src/select/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/select"], + "options": { + "project": "src/select/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/select"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js select" + }, + { + "command": "ng default-build select" + }, + { + "command": "node build/clear-default-theme.js select" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/select && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build select && ng pack select && node build/publish.js select --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/select/lib/src/TiSelectComponent.ts b/src/select/lib/src/TiSelectComponent.ts new file mode 100644 index 0000000..3b03426 --- /dev/null +++ b/src/select/lib/src/TiSelectComponent.ts @@ -0,0 +1,429 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + Component, + ContentChild, + EventEmitter, + HostListener, + Input, + Output, + TemplateRef, + ViewChild +} from '@angular/core'; +import { TiFormComponent, TiWholeComponent } from '@opentiny/ng-base'; +import { TiDominatorComponent } from '@opentiny/ng-dominator'; +import { TiDropComponent } from '@opentiny/ng-drop'; +import { TiDropsearchComponent } from '@opentiny/ng-dropsearch'; +import { TiListComponent, TiListScrollLoad } from '@opentiny/ng-list'; +import { TiKeymap } from '@opentiny/ng-utils'; +import { TiPositionType } from '@opentiny/ng-utils'; +import { TiLocale } from '@opentiny/ng-locale'; +import packageInfo from '../package.json'; + +/** + * Select选择下拉组件 + * + * 支持单选/多选,分组,搜索,懒加载。 + * + * 单选主要功能为从一个数据集合中选择某一条数据,单选与RadioGroup功能相同,只是视觉呈现不同。 + * + * 多选主要功能是从一个数据集合中任意选择多条数据,与checkboxGroup功能相同,只是视觉呈现不同。 + * + */ +@Component({ + selector: 'ti-select', + templateUrl: './select.html', + styleUrls: ['./select.less', './select-small.less'], + providers: [TiFormComponent.getValueAccessor(TiSelectComponent)], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-select-small]': 'size === "small"', + '(blur)': 'onBlur()' + } +}) +export class TiSelectComponent extends TiWholeComponent { + /** + * @ignore + * dominator 到drop的距离 + */ + @Input() dominatorSpace: string = TiDropComponent.DOMINATOR_SPACE + 'px'; + /** + * 下拉面板是否多选 + */ + @Input() multiple: boolean = false; + /** + * 多选场景下,选择框最多显示行数 + */ + @Input() maxLine: number = 2; + /** + * 选择框中选中项的tip提示方向 + */ + @Input() selectedTipPosition: TiPositionType = 'auto'; + /** + * 选择框的占位文本 + */ + @Input() placeholder: string = ''; + // 面板配置 + /** + * 下拉面板的宽度。 + * + * 1."justified": 下拉面板的宽度与选择框宽度保持一致; + * + * 2."auto": 下拉面板的宽度根据下拉项的内容自动撑开; + * + * 3. 固定的下拉框宽度:不小于选择框的宽度,例如:"200px" + */ + @Input() panelWidth: 'justified' | 'auto' | string = 'justified'; // TODO: 确认类型 + /** + * 下拉面板最大高度 + */ + @Input() panelMaxHeight: string; + /** + * 下拉面板无数据时的显示文本 + */ + @Input() noDataText: string = TiLocale.getLocaleWords().tiList.noDataText; + // 列表数据配置 + /** + * 下拉选项的全部数据 + */ + @Input() options: Array; + /** + * 下拉面板要显示的键值 + */ + @Input() labelKey: string = 'label'; + /** + * 下拉面板是否开启搜索功能 + */ + @Input() searchable: boolean = false; + /** + * 指定搜索的字段范围 + */ + @Input() searchKeys: Array; + // 其他配置 + /** + * @ignore + * 选择框大小 + */ + @Input() size: 'default' | 'small' = 'default'; + /** + * 下拉面板选中项的 tip 提示方向 + */ + @Input() tipPosition: TiPositionType = 'right'; + /** + * 选择框是否开启清除已选项功能 + * + * 单选下拉一键清除功能 + * + * 多选下拉一键清除功能 + */ + @Input() clearable: boolean = false; + /** + * 多选场景下是否开启全选功能 + */ + @Input() selectAll: boolean = false; + /** + * 多选场景下选择框是否显示选中项个数 + */ + @Input() showSelectedNumber: boolean = false; + /** + * 是否开启虚拟滚动 + */ + @Input() virtual: boolean = false; + /** + * @ignore + * TODO: 暂不对外开放该接口,后续根据使用场景进行优化 + * 当开启虚拟滚动时,可配置单条选项的高度(单位是px), 默认值30 + */ + @Input() itemSize: number = TiListComponent.OPTION_DEFAULT_HEIGHT; + /** + * tip 提示最大宽度 + */ + @Input() tipMaxWidth: string; + /** + * @ignore + * + * 默认值为 labelKey 的接口值 + * + * idKey指定的属性的值相等时即认为 option 选项是选中的。选中项 ngModel 中的数据跟 options 数据集中的选项(option对象)之间对应相等关系的依据属性。 + * + * 当 modelOption中的 idKey 设置的属性的值 与 option 中的 idKey 设置的属性的值相等时,则认为 modelOption 和 option 是对应的相等关系,即认为 option 选项是选中的。 + * + * 默认当 modelOption === option 或者 modelOption 中的 labelKey 设置的属性的值 与 option 中的 设置的属性的值相等时,则认为 option 选项是选中的。 + * + */ + + /** + * 设置数据唯一标识的键值,默认为 labelKey 的接口值 + */ + @Input() idKey: string; + /** + * 下拉面板是否添加在 body 上 + */ + @Input() appendToBody: boolean = true; + /** + * 多选场景下,选择框是否显示已选个数的tip提示方向 + */ + @Input() showSelectedNumberTip: boolean = false; + /** + * 选择框已选个数的tip提示方向 + */ + @Input() selectedNumberTipPosition: TiPositionType = 'bottom'; + /** + * 搜索场景下,下拉面板收起后是否保留搜索关键词 + */ + @Input() reserveSearchword: boolean = false; + /** + * 打开面板前触发的回调,参数为当前组件实例,一般用于数据懒加载场景 + */ + @Output() readonly beforeOpen: EventEmitter = new EventEmitter(); + /** + * 下拉选项选中或取消选中时触发的回调,参数:当前点击项 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + /** + * 点击清除按钮时触发的回调 + * + * 单选下拉一键清除功能 + * + * 多选下拉一键清除功能 + */ + @Output() readonly clear: EventEmitter = new EventEmitter(); + /** + * 搜索前触发的回调,参数:搜索关键词,一般用于后台搜索 + */ + @Output() readonly beforeSearch: EventEmitter = new EventEmitter(); + /** + * 下拉列表滚动到底部触发的回调,一般用于滚动加载场景 + */ + @Output() readonly scrollToBottom: EventEmitter = new EventEmitter(); + /** + * @ignore + * 用户写的item模板,firstTemplate是第一个或者唯一的template + */ + @ContentChild(TemplateRef, { static: false }) firstTemplate: TemplateRef; + /** + * 下拉选项区域的模板 + */ + @ContentChild('item', { static: false }) itemTemplate: TemplateRef; + /** + * 选择框选中项区域的模板 + */ + @ContentChild('selected', { static: false }) + selectedTemplate: TemplateRef; + /** + * 选择框占位文本区域的模板 + */ + @ContentChild('placeholder', { static: false }) + placeholderTemplate: TemplateRef; + /** + * 下拉面板底部区域的模板 + */ + @ContentChild('footer', { static: false }) footerTemplate: TemplateRef; + /** + * @ignore + * 内部标签donimator组件 + */ + @ViewChild(TiDominatorComponent, { static: true }) + dominatorCom: TiDominatorComponent; + /** + * @ignore + * 内部标签Droplist组件 + */ + @ViewChild(TiDropsearchComponent, { static: true }) + dropsearchCom: TiDropsearchComponent; + protected oldSearchable: boolean = null; + protected versionInfo: string = super.getVersion(packageInfo); + ngOnInit(): void { + super.ngOnInit(); + if (this.beforeSearch.observers.length !== 0) { + this.dropsearchCom.hasBeforeSearchObservers = true; + } + if (this.scrollToBottom.observers.length > 0) { + this.virtual = true; + } + } + + /** + * 兼容旧版: + * 3.1.2版select只能内嵌一个模板,无命名。 + * 新版可以内嵌四个模板,示例书写要求都命名。 + * 但需要兼容旧版无命名测试用例。 + */ + ngAfterContentInit(): void { + super.ngAfterContentInit(); + // 如果item模板为空,那么把第一个出现的无标签(无footer等标签)模板作为item + if (!this.itemTemplate && this.firstTemplate) { + const firstTemplateEle: Element = this.firstTemplate.elementRef.nativeElement; + if ( + firstTemplateEle !== (this.selectedTemplate && this.selectedTemplate.elementRef.nativeElement) && + firstTemplateEle !== (this.placeholderTemplate && this.placeholderTemplate.elementRef.nativeElement) && + firstTemplateEle !== (this.footerTemplate && this.footerTemplate.elementRef.nativeElement) + ) { + this.itemTemplate = this.firstTemplate; + } + } + } + + ngAfterViewChecked(): void { + // 要支持searchable的动态变更,所以ngAfterViewChecked才能获知搜索框节点。 + if (this.searchable !== this.oldSearchable) { + this.oldSearchable = this.searchable; + if (this.searchable) { + this.setFocusableElems(this.dominatorCom.getFocusableElems().concat(this.dropsearchCom.getFocusableElems())); + } else { + this.setFocusableElems(this.dominatorCom.getFocusableElems()); + } + } + if (this.dropsearchCom.isFocusableElemsInPaginationChange) { + this.setFocusableElems(this.dominatorCom.getFocusableElems().concat(this.dropsearchCom.getFocusableElems())); + this.dropsearchCom.isFocusableElemsInPaginationChange = false; + } + // 上面设置好focusElems后,调用父类逻辑afterViewChecked才去设置autofocs + super.ngAfterViewChecked(); + } + + /** + * @ignore + * 切换面板开合状态 + */ + public toggle(): void { + if (!this.dropsearchCom.isShow) { + // 面板关闭时 + this.wantOpen(); + } else { + // 面板开时 + this.close(); + } + } + /** + * 打开面板 + */ + public open(): void { + this.dropsearchCom.show(); + } + /** + * @ignore + * 收起面板 + */ + public close(): void { + this.dropsearchCom.hide(); + } + /** + * @ignore + * 处理点击Dominator事件 + */ + public onClickDominator(): void { + if (this.disabled) { + return; + } + this.toggle(); + } + /** + * @ignore + * ti-select键盘事件处理:回车/空格情况下,展开面板 + * @param event 按键事件 + * @returns void + */ + @HostListener('keydown', ['$event']) public onKeydown(event: KeyboardEvent): void { + if (this.disabled || this.dropsearchCom.isShow) { + return; + } + const enterKeyCodeArr: Array = [TiKeymap.KEY_SPACE, TiKeymap.KEY_ENTER, TiKeymap.KEY_NUMPAD_ENTER]; + if (enterKeyCodeArr.includes(event.keyCode)) { + this.wantOpen(); + // 如果响应了按键,那么不再冒泡 + event.stopPropagation(); + } + } + /** + * @ignore + * 失焦情况下,仅关闭面板,不做聚焦等处理 + */ + public onBlur(): void { + this.dropsearchCom.hideWithoutFocus(); + } + /** + * 点击或者toggle(),尝试打开面板 + */ + protected wantOpen(): void { + if (this.beforeOpen.observers.length === 0) { + // 无beforeOpen懒加载,直接打开 + this.open(); + } else { + // 有beforeOpen懒加载,发出事件 + this.beforeOpen.emit(this); + } + } + /** + * @ignore + * 多选带searchbox场景下,dominator中元素删除时,需要聚焦于searchbox + */ + public onDeleteDominator(): void { + if (this.dropsearchCom.isShow) { + this.dropsearchCom.focus(); + } + } + /** + * @ignore + * 点击下拉选项触发的回调 + */ + public onSelect(option: any): void { + this.select.emit(option); + if (this.multiple) { + return; + } + } + /** + * @ignore + * 单选点击清除按钮时触发clear事件, 如果下拉中有搜索,则需要聚焦于searchbox。 + */ + public onClearDominator(): void { + this.clear.emit(); + if (this.searchable && this.dropsearchCom.isShow) { + this.dropsearchCom.focus(); + } + } + /** + * @ignore + */ + public onBeforeSearch(): void { + this.beforeSearch.emit(this); + } + /** + * 获取搜索关键词 + * @returns 搜索关键词 + */ + public getSearchWord(): string { + return this.dropsearchCom.searchWord; + } + /** + * @ignore + * 设置搜索结果 + * @params 业务设置搜索后的结果,组件不再进行数据处理 + */ + public setSearchResult(searchResult: Array): void { + this.dropsearchCom.setSearchResult(searchResult); + } + /** + * 获取搜索后的下拉列表数据 + */ + public getSearchResult(): Array { + return this.dropsearchCom.searchResult; + } + /** + * @ignore + */ + public onScrollToBottom(scrollLoad: TiListScrollLoad): void { + this.scrollToBottom.emit(scrollLoad); + } +} diff --git a/src/select/lib/src/TiSelectModule.ts b/src/select/lib/src/TiSelectModule.ts new file mode 100644 index 0000000..93a03fa --- /dev/null +++ b/src/select/lib/src/TiSelectModule.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiDropsearchModule } from '@opentiny/ng-dropsearch'; +import { TiDominatorModule } from '@opentiny/ng-dominator'; +import { TiSelectComponent } from './TiSelectComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiDominatorModule, TiDropModule, TiDropsearchModule], + exports: [TiSelectComponent], + declarations: [TiSelectComponent] +}) +export class TiSelectModule {} + +export { TiSelectComponent } from './TiSelectComponent'; diff --git a/src/select/lib/src/select-small.less b/src/select/lib/src/select-small.less new file mode 100644 index 0000000..be47289 --- /dev/null +++ b/src/select/lib/src/select-small.less @@ -0,0 +1,9 @@ +@import '../../../themes/basic/base-all.less'; + +:host.ti3-select-small ti-dominator { + --ti-dominator-container-height: var(--ti-common-size-6x); +} + +// ::ng-deep,表示后面紧跟的样式选择器,不做属性[]包装。 +// CSS标准中 /deep/ >>>刺穿Shadow DOM, 已废弃。所以,这里暂时用::ng-deep angular关键词,以兼容未来可能其他标准。 +// https://blog.csdn.net/sky_sunshine_x/article/details/80622617 diff --git a/src/select/lib/src/select.html b/src/select/lib/src/select.html new file mode 100644 index 0000000..4147532 --- /dev/null +++ b/src/select/lib/src/select.html @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + +{{item[labelKey]}} + + diff --git a/src/select/lib/src/select.less b/src/select/lib/src/select.less new file mode 100644 index 0000000..3992f8f --- /dev/null +++ b/src/select/lib/src/select.less @@ -0,0 +1,37 @@ +@import '../../../themes/basic/base-all.less'; +@import '../../../themes/basic/compnent-container-border.less'; + +:host :extend(.ti3-compnent-container-border all) { + width: var(--ti-common-size-50x); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); +} + +//无边框的样式 +:host[noborder] { + border-color: var(--ti-common-color-bg-white-normal); + &:hover, + &[tiFocused] { + border-color: var(--ti-common-color-bg-white-normal); + } + &[disabled] { + border-color: var(--ti-common-color-bg-disabled); + } +} + +// 深色背景色只处理无边框的场景, disabled /focused 状态没有规范,按透明处理 +:host[dark] { + background-color: transparent; + border-color: transparent; + &:hover, + &[tiFocused], + &[disabled] { + border-color: transparent; + } + ::ng-deep .ti3-select-dominator-text { + color: var(--ti-common-color-text-white); + } + ::ng-deep .ti3-select-dominator-dropdown-btn:after { + border-top-color: var(--ti-common-color-icon-white); + } +} diff --git a/src/selectgroup/demo/project.json b/src/selectgroup/demo/project.json new file mode 100644 index 0000000..538d529 --- /dev/null +++ b/src/selectgroup/demo/project.json @@ -0,0 +1,86 @@ +{ + "projectType": "application", + "root": "src/selectgroup/demo", + "sourceRoot": "src/selectgroup/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/selectgroup", + "index": "src/selectgroup/demo/src/index.html", + "main": "src/selectgroup/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/selectgroup/demo/tsconfig.app.json", + "assets": [ + "src/selectgroup/demo/src/favicon.ico", + "src/selectgroup/demo/src/assets", + { + "glob": "**/*", + "input": "node_modules/ionicons/dist/ionicons/svg", + "output": "/assets/ionicons/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "selectgroup-demo:build:production" + }, + "development": { + "browserTarget": "selectgroup-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js selectgroup" + } + ] + } + } + } +} diff --git a/src/selectgroup/demo/src/app/AppComponent.ts b/src/selectgroup/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/selectgroup/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/selectgroup/demo/src/app/AppModule.ts b/src/selectgroup/demo/src/app/AppModule.ts new file mode 100644 index 0000000..b7baf0b --- /dev/null +++ b/src/selectgroup/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { HttpClientModule } from '@angular/common/http'; +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SelectgroupTestModule } from './selectgroup/SelectgroupTestModule'; + +@NgModule({ + imports: [ + SelectgroupTestModule, + HttpClientModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/selectgroup/demo/src/app/IndexComponent.ts b/src/selectgroup/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..384754d --- /dev/null +++ b/src/selectgroup/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SelectgroupTestModule } from './selectgroup/SelectgroupTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SelectgroupTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/selectgroup/demo/src/app/app.html b/src/selectgroup/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/selectgroup/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/selectgroup/demo/src/app/selectgroup/SelectgroupBasicComponent.ts b/src/selectgroup/demo/src/app/selectgroup/SelectgroupBasicComponent.ts new file mode 100644 index 0000000..3b36299 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/SelectgroupBasicComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { TiSelectgroupItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './selectgroup-basic.html' +}) +export class SelectgroupBasicComponent { + items: Array = [ + { + title: 'Angular', + content: '^15.0.0', + iconName: 'logo-angular' + }, + { + title: 'HTML5', + content: 'html5', + iconName: 'logo-html5', + disabled: true + } + ]; + value: TiSelectgroupItem; +} diff --git a/src/selectgroup/demo/src/app/selectgroup/SelectgroupMultipleComponent.ts b/src/selectgroup/demo/src/app/selectgroup/SelectgroupMultipleComponent.ts new file mode 100644 index 0000000..39c82d8 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/SelectgroupMultipleComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { TiSelectgroupItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './selectgroup-multiple.html' +}) +export class SelectgroupMultipleComponent { + checkPosition: string = 'right-bottom'; + items: Array = [ + { + title: 'Angular', + content: '^15.0.0', + iconName: 'logo-angular' + }, + { + title: 'HTML5', + content: 'html5', + iconName: 'logo-html5' + } + ]; + value: Array; +} diff --git a/src/selectgroup/demo/src/app/selectgroup/SelectgroupSelectComponent.ts b/src/selectgroup/demo/src/app/selectgroup/SelectgroupSelectComponent.ts new file mode 100644 index 0000000..8bf8568 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/SelectgroupSelectComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { TiSelectgroupItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './selectgroup-select.html' +}) +export class SelectgroupSelectComponent { + items: Array = [ + { + title: 'Angular', + options: [{ label: '^13.0.0' }, { label: '^14.0.0' }, { label: '^15.0.0' }], + iconName: 'logo-angular' + }, + { + title: 'HTML5', + options: [{ label: 'html5' }], + iconName: 'logo-html5' + } + ]; + value: TiSelectgroupItem; +} diff --git a/src/selectgroup/demo/src/app/selectgroup/SelectgroupTemplateComponent.ts b/src/selectgroup/demo/src/app/selectgroup/SelectgroupTemplateComponent.ts new file mode 100644 index 0000000..28bf910 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/SelectgroupTemplateComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { TiSelectgroupItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './selectgroup-template.html' +}) +export class SelectgroupTemplateComponent { + items: Array = [ + { + title: 'Angular', + content: '^15.0.0', + iconName: 'logo-angular' + }, + { + title: 'HTML5', + content: 'html5', + iconName: 'logo-html5' + } + ]; + value: TiSelectgroupItem; +} diff --git a/src/selectgroup/demo/src/app/selectgroup/SelectgroupTestModule.ts b/src/selectgroup/demo/src/app/selectgroup/SelectgroupTestModule.ts new file mode 100644 index 0000000..d293f06 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/SelectgroupTestModule.ts @@ -0,0 +1,40 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { SelectgroupBasicComponent } from './SelectgroupBasicComponent'; +import { SelectgroupMultipleComponent } from './SelectgroupMultipleComponent'; +import { SelectgroupValuekeyComponent } from './SelectgroupValuekeyComponent'; +import { SelectgroupTemplateComponent } from './SelectgroupTemplateComponent'; +import { SelectgroupSelectComponent } from './SelectgroupSelectComponent'; +import { TiSelectgroupModule, TiSelectModule, TiIconModule, TiSvgComponent } from '@opentiny/ng'; +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiIconModule, + TiSelectModule, + TiSelectgroupModule, + RouterModule.forChild(SelectgroupTestModule.ROUTES) + ], + declarations: [ + SelectgroupBasicComponent, + SelectgroupMultipleComponent, + SelectgroupValuekeyComponent, + SelectgroupTemplateComponent, + SelectgroupSelectComponent + ] +}) +export class SelectgroupTestModule { + constructor() { + TiSvgComponent.setPath('/assets/ionicons/'); + } + static readonly ROUTES: Routes = [ + { path: 'selectgroup/selectgroup-basic', component: SelectgroupBasicComponent, data: { label: '基础' } }, + { path: 'selectgroup/selectgroup-multiple', component: SelectgroupMultipleComponent, data: { label: '多选' } }, + { path: 'selectgroup/selectgroup-valuekey', component: SelectgroupValuekeyComponent, data: { label: '自定义选中值' } }, + { path: 'selectgroup/selectgroup-template', component: SelectgroupTemplateComponent, data: { label: '自定义模板' } }, + { path: 'selectgroup/selectgroup-select', component: SelectgroupSelectComponent, data: { label: '下拉框' } } + ]; +} diff --git a/src/selectgroup/demo/src/app/selectgroup/SelectgroupValuekeyComponent.ts b/src/selectgroup/demo/src/app/selectgroup/SelectgroupValuekeyComponent.ts new file mode 100644 index 0000000..e71e94a --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/SelectgroupValuekeyComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { TiSelectgroupItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './selectgroup-valuekey.html' +}) +export class SelectgroupValuekeyComponent { + items: Array = [ + { + title: 'Angular', + content: '^15.0.0', + iconName: 'logo-angular' + }, + { + title: 'HTML5', + content: 'html5', + iconName: 'logo-html5' + } + ]; + value: TiSelectgroupItem; +} diff --git a/src/selectgroup/demo/src/app/selectgroup/selectgroup-basic.html b/src/selectgroup/demo/src/app/selectgroup/selectgroup-basic.html new file mode 100644 index 0000000..63a18ff --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/selectgroup-basic.html @@ -0,0 +1,3 @@ + + + diff --git a/src/selectgroup/demo/src/app/selectgroup/selectgroup-multiple.html b/src/selectgroup/demo/src/app/selectgroup/selectgroup-multiple.html new file mode 100644 index 0000000..6174f5a --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/selectgroup-multiple.html @@ -0,0 +1,9 @@ + + + + diff --git a/src/selectgroup/demo/src/app/selectgroup/selectgroup-select.html b/src/selectgroup/demo/src/app/selectgroup/selectgroup-select.html new file mode 100644 index 0000000..e355c75 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/selectgroup-select.html @@ -0,0 +1,10 @@ + + + + + diff --git a/src/selectgroup/demo/src/app/selectgroup/selectgroup-template.html b/src/selectgroup/demo/src/app/selectgroup/selectgroup-template.html new file mode 100644 index 0000000..3fd017b --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/selectgroup-template.html @@ -0,0 +1,28 @@ + + + +
    +
    {{item.title}}
    +
    {{item.content}}
    +
    +
    +
    +
    + + diff --git a/src/selectgroup/demo/src/app/selectgroup/selectgroup-valuekey.html b/src/selectgroup/demo/src/app/selectgroup/selectgroup-valuekey.html new file mode 100644 index 0000000..a51a300 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/selectgroup-valuekey.html @@ -0,0 +1,6 @@ + + + +
    +
    Current Vlaue: {{ value | json }}
    +
    diff --git a/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup-demos.js b/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup-demos.js new file mode 100644 index 0000000..3602b8e --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup-demos.js @@ -0,0 +1,73 @@ +export default { + column: '2', + demos: [ + { + demoId: 'selectgroup-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'selectgroup basic' + }, + desc: { + 'zh-CN': '

    Selectgroup 组件的最简用法。

    ', + 'en-US': 'selectgroup basic' + }, + apis: ['TiSelectitemComponent.properties.item'] + }, + { + demoId: 'selectgroup-multiple', + name: { + 'zh-CN': '多选', + 'en-US': 'selectgroup multiple' + }, + desc: { + 'zh-CN': + '

    通过属性multiple配置是否多选;通过属性checkPosition配置多选角标的位置,包括right-top(默认)、right-bottom两种类型。

    ', + 'en-US': 'selectgroup multiple' + }, + apis: ['TiSelectgroupComponent.properties.multiple', 'TiSelectitemComponent.properties.checkPosition'] + }, + { + demoId: 'selectgroup-valuekey', + name: { + 'zh-CN': '自定义选中值', + 'en-US': 'selectgroup valuekey' + }, + desc: { + 'zh-CN': '

    通过属性valueKey配置选中项数据的键值。

    ', + 'en-US': 'selectgroup valuekey' + }, + apis: ['TiSelectgroupComponent.properties.valueKey'] + }, + { + demoId: 'selectgroup-template', + name: { + 'zh-CN': '模板', + 'en-US': 'selectgroup template' + }, + desc: { + 'zh-CN': '

    通过属性template配置每项卡片展示的模板。

    ', + 'en-US': 'selectgroup template' + }, + apis: ['TiSelectitemComponent.slots.itemTemplate'] + }, + { + demoId: 'selectgroup-select', + name: { + 'zh-CN': '选择下拉', + 'en-US': 'selectgroup select' + }, + desc: { + 'zh-CN': '

    在属性items中配置options可以结合下拉框使用。

    ', + 'en-US': 'selectgroup select' + } + } + ], + ignoreApis: [ + 'TiSelectgroupComponent.properties.disabled', + 'TiSelectgroupComponent.properties.tabindex', + 'TiSelectgroupComponent.events.blur', + 'TiSelectgroupComponent.events.focus', + 'TiSelectgroupComponent.methods.blur', + 'TiSelectgroupComponent.methods.focus' + ] +}; diff --git a/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.cn.md b/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.cn.md new file mode 100644 index 0000000..4a3ef0e --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.cn.md @@ -0,0 +1,15 @@ +--- +title: Selectgroup 选择组 +--- + +# Selectgroup 选择组 + +
    + +选择组是一组选块聚合起来供以选择的组件。 + +```typescript +import { TiSelectgroupModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.en.md b/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.en.md new file mode 100644 index 0000000..5635345 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.en.md @@ -0,0 +1,13 @@ +--- +title: Selectgroup +--- + +# Selectgroup + +
    + +```typescript +import { TiSelectgroupModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/selectgroup/demo/src/favicon.ico b/src/selectgroup/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/selectgroup/demo/src/index.html b/src/selectgroup/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/selectgroup/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/selectgroup/demo/src/main.ts b/src/selectgroup/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/selectgroup/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/selectgroup/demo/tsconfig.app.json b/src/selectgroup/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/selectgroup/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/selectgroup/lib/index.ts b/src/selectgroup/lib/index.ts new file mode 100644 index 0000000..f75fa47 --- /dev/null +++ b/src/selectgroup/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiSelectgroupModule'; +export * from './src/TiSelectgroupComponent'; diff --git a/src/selectgroup/lib/ng-package.json b/src/selectgroup/lib/ng-package.json new file mode 100644 index 0000000..34516a0 --- /dev/null +++ b/src/selectgroup/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/selectgroup", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/selectgroup/lib/package.json b/src/selectgroup/lib/package.json new file mode 100644 index 0000000..26812fd --- /dev/null +++ b/src/selectgroup/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-selectgroup", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/selectgroup/lib/project.json b/src/selectgroup/lib/project.json new file mode 100644 index 0000000..50567fb --- /dev/null +++ b/src/selectgroup/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/selectgroup/lib", + "sourceRoot": "src/selectgroup/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/selectgroup"], + "options": { + "project": "src/selectgroup/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/selectgroup"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js selectgroup" + }, + { + "command": "ng default-build selectgroup" + }, + { + "command": "node build/clear-default-theme.js selectgroup" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/selectgroup && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build selectgroup && ng pack selectgroup && node build/publish.js selectgroup --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/selectgroup/lib/src/TiSelectgroupComponent.ts b/src/selectgroup/lib/src/TiSelectgroupComponent.ts new file mode 100644 index 0000000..0c1e256 --- /dev/null +++ b/src/selectgroup/lib/src/TiSelectgroupComponent.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +/** + * 选择组 组件 + */ +@Component({ + selector: 'ti-selectgroup', + templateUrl: './selectgroup.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiSelectgroupComponent)] +}) +export class TiSelectgroupComponent extends TiFormComponent { + /** + * 是否多选 + */ + @Input() multiple: boolean; + /** + * 指定选中项数据的键值 + */ + @Input() valueKey: string; +} diff --git a/src/selectgroup/lib/src/TiSelectgroupModule.ts b/src/selectgroup/lib/src/TiSelectgroupModule.ts new file mode 100644 index 0000000..1222807 --- /dev/null +++ b/src/selectgroup/lib/src/TiSelectgroupModule.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiIconModule } from '@opentiny/ng-icon'; + +import { TiSelectgroupComponent } from './TiSelectgroupComponent'; +import { TiSelectitemComponent } from './TiSelectitemComponent'; + +@NgModule({ + imports: [CommonModule, TiOverflowModule, TiIconModule], + exports: [TiSelectgroupComponent, TiSelectitemComponent], + declarations: [TiSelectgroupComponent, TiSelectitemComponent] +}) +export class TiSelectgroupModule {} +export { TiSelectgroupComponent } from './TiSelectgroupComponent'; +export { TiSelectitemComponent, TiSelectgroupItem } from './TiSelectitemComponent'; diff --git a/src/selectgroup/lib/src/TiSelectitemComponent.ts b/src/selectgroup/lib/src/TiSelectitemComponent.ts new file mode 100644 index 0000000..fa3d694 --- /dev/null +++ b/src/selectgroup/lib/src/TiSelectitemComponent.ts @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + Input, + Renderer2, + TemplateRef +} from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; + +import { TiSelectgroupComponent } from './TiSelectgroupComponent'; +export interface TiSelectgroupItem { + /** + * 卡片标题 + */ + title: string; + /** + * 卡片内容 + */ + content?: string; + /** + * 卡片是否禁用 + */ + disabled?: boolean; + /** + * 卡片左侧图标名 + */ + iconName: string; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} +@Component({ + selector: 'ti-selectitem', + templateUrl: './selectitem.html', + styleUrls: ['./selectgroup.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-selectitem-box]': 'true', + '[class.ti3-selectitem-disabled]': 'item.disabled', + '[class.ti3-selectitem-checked]': 'isChecked()', + '[class.ti3-selectitem-template]': 'itemTemplate', + '(click)': 'onClick()' + } +}) +export class TiSelectitemComponent extends TiBaseComponent { + /** + * 数据配置 + */ + @Input() item: TiSelectgroupItem; + /** + * 选中标识位置 + */ + @Input() checkPosition: 'right-top' | 'right-bottom' = 'right-top'; + /** + * 每项模板 + */ + @ContentChild('item', /* TODO: add static flag */ { static: true }) itemTemplate: TemplateRef; + + /** + * @ignore + */ + public selectgroup: TiSelectgroupComponent; + + constructor( + selectgroup: TiSelectgroupComponent, + protected hostRef: ElementRef, + protected renderer: Renderer2, + protected changeDetectorRef: ChangeDetectorRef + ) { + super(hostRef, renderer); + this.selectgroup = selectgroup; + } + + private valueFn: (item: any) => any = (item: any) => { + return item[this.selectgroup.valueKey]; + }; + + /** + * @ignore + */ + public onClick(): void { + if (this.item.disabled) { + return; + } + // valuekey是否存在,存在时做处理 + const itemValue: any = this.selectgroup.valueKey ? this.valueFn(this.item) : this.item; + if (this.selectgroup.multiple) { + // 多选 + this.selectgroup.model = this.selectgroup.model || []; + const index: number = this.selectgroup.model.indexOf(itemValue); + if (index > -1) { + this.selectgroup.model.splice(index, 1); + } else { + this.selectgroup.model.push(itemValue); + } + this.selectgroup.model = this.selectgroup.model.concat(); + } else { + // 单选 + this.selectgroup.model = itemValue; + } + } + /** + * @ignore + * 是否选中 + */ + public isChecked(): boolean { + if (this.selectgroup.model) { + this.changeDetectorRef.markForCheck(); + const itemValue: any = this.selectgroup.valueKey ? this.valueFn(this.item) : this.item; + + return this.selectgroup.multiple ? this.selectgroup.model.includes(itemValue) : this.selectgroup.model === itemValue; + } + } +} diff --git a/src/selectgroup/lib/src/selectgroup.html b/src/selectgroup/lib/src/selectgroup.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/selectgroup/lib/src/selectgroup.html @@ -0,0 +1 @@ + diff --git a/src/selectgroup/lib/src/selectgroup.less b/src/selectgroup/lib/src/selectgroup.less new file mode 100644 index 0000000..2bd0248 --- /dev/null +++ b/src/selectgroup/lib/src/selectgroup.less @@ -0,0 +1,180 @@ +@import '../../../themes/basic/base-all.less'; + +::ng-deep :root { + --ti-selectitem-corner-width: 20px; + --ti-selectitem-border-color-hover: var(--ti-common-color-line-active); // 激活/hover 边框色 +} + +// 选中样式角标公共样式 +.ti3-selectitem-corner { + display: inline-block; + width: 0; + height: 0; + position: absolute; + animation: showCorner 200ms; + right: 0; +} + +// 非灰化状态 +:host.ti3-selectitem-box { + .clearfix(); + display: inline-block; + cursor: pointer; + width: 190px; + height: 54px; + padding: var(--ti-common-space-2x) var(--ti-common-space-2x) var(--ti-common-space-2x) var(--ti-common-space-10); + border-radius: var(--ti-common-border-radius-normal); + background-color: var(--ti-common-color-bg-white-normal); + box-sizing: border-box; + margin-right: var(--ti-common-space-3x); + position: relative; + box-shadow: none; + border: 1px solid var(--ti-common-color-line-normal); + // 选中状态 + &.ti3-selectitem-checked { + background-color: var(--ti-common-color-bg-light-normal); + border: 1px solid var(--ti-selectitem-border-color-hover); + .ti3-selectitem-corner-right-bottom { + &:extend(.ti3-selectitem-corner); + border-left: var(--ti-selectitem-corner-width) solid transparent; + border-bottom: var(--ti-selectitem-corner-width) solid var(--ti-selectitem-border-color-hover); + bottom: 0; + & .ti3-selectitem-checkmark { + position: absolute; + color: var(--ti-common-color-icon-white); + top: calc(var(--ti-selectitem-corner-width) - 12px); + right: 0; + } + } + .ti3-selectitem-corner-right-top { + .ti3-selectitem-corner-right-bottom(); + top: 0; + .rotate(-90deg); + & .ti3-selectitem-checkmark { + .rotate(90deg); + } + } + } + // 自定义模板 + &.ti3-selectitem-template { + width: auto; + height: auto; + padding-left: var(--ti-common-space-2x); + vertical-align: middle; + } + &:hover { + border: 1px solid var(--ti-selectitem-border-color-hover); + } +} + +// 灰化状态 +:host.ti3-selectitem-box.ti3-selectitem-disabled { + background: var(--ti-common-color-bg-disabled); + cursor: not-allowed; + border-color: var(--ti-common-color-line-disabled); + box-shadow: none; + :hover { + border-color: var(--ti-common-color-line-disabled); + } + .ti3-selectitem-lefticon, + .ti3-selectitem-title, + .ti3-selectitem-content { + color: var(--ti-common-color-text-disabled); + } + // 选中状态的角标 + &.ti3-selectitem-checked { + .ti3-selectitem-corner-right-bottom, + .ti3-selectitem-corner-right-top { + border-bottom-color: var(--ti-common-color-line-disabled); + } + } +} +:host .ti3-selectitem-lefticon { + float: left; + margin-top: var(--ti-common-space-base); + margin-right: var(--ti-common-space-2x); + font-size: 28px; + line-height: 100%; // FireFox下,该元素float:left或display:block会导致line-height:1.2,高度增加 +} +// 文本溢出的公共样式 +.ti3-selectitem-text { + width: 100%; + display: block; +} +.ti3-selectitem-right { + float: left; + width: calc(100% - 50px); + .ti3-selectitem-title { + &:extend(.ti3-selectitem-text); + .ellipsis(); + font-size: var(--ti-common-font-size-1); + color: var(--ti-common-color-text-primary); + line-height: 20px; + } + .ti3-selectitem-title-top-space { + margin-top: var(--ti-common-space-2x); + } + .ti3-selectitem-content { + &:extend(.ti3-selectitem-text); + .ellipsis(); + font-size: var(--ti-common-font-size-base); + color: var(--ti-common-color-text-secondary); + line-height: var(--ti-common-line-height-number); + } +} + +// 镜像+下拉组件中:定制select组件的样式 +::ng-deep.ti3-selectitem-box { + ti-select { + position: relative; + left: calc(-1 * var(--ti-common-space-10) - 1px); + bottom: calc(-1 * var(--ti-common-space-2x) - 1px); + &:not([disabled]) { + border-color: transparent; + border-top-color: var(--ti-common-color-line-dividing); + background: transparent; + &:hover, + &[tiFocused] { + border-color: transparent; + border-top-color: var(--ti-common-color-line-dividing); + } + } + } + // 悬停 + &:hover ti-select { + &:not([disabled]) { + border-top-color: var(--ti-selectitem-border-color-hover); + &:hover, + &[tiFocused] { + border-top-color: var(--ti-selectitem-border-color-hover); + } + } + } + // 选中 + &.ti3-selectitem-checked ti-select { + &:not([disabled]) { + border-top-color: var(--ti-selectitem-border-color-hover); + &:hover, + &[tiFocused] { + border-top-color: var(--ti-selectitem-border-color-hover); + } + } + } +} + +@keyframes showCorner { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes showCheckmark { + from { + transform: scale(0); + } + to { + transform: scale(1); + } +} diff --git a/src/selectgroup/lib/src/selectitem.html b/src/selectgroup/lib/src/selectitem.html new file mode 100644 index 0000000..38c052c --- /dev/null +++ b/src/selectgroup/lib/src/selectitem.html @@ -0,0 +1,23 @@ +
    + +
    + + + + + +
    + + {{item.title}} + {{item.content}} + +
    +
    + + + {{item.title}} + diff --git a/src/skeleton/demo/karma.conf.js b/src/skeleton/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/skeleton/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/skeleton/demo/project.json b/src/skeleton/demo/project.json new file mode 100644 index 0000000..0162a46 --- /dev/null +++ b/src/skeleton/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/skeleton/demo", + "sourceRoot": "src/skeleton/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/skeleton", + "index": "src/skeleton/demo/src/index.html", + "main": "src/skeleton/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/skeleton/demo/tsconfig.app.json", + "assets": ["src/skeleton/demo/src/favicon.ico", "src/skeleton/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "skeleton-demo:build:production" + }, + "development": { + "browserTarget": "skeleton-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js skeleton" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/skeleton/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/skeleton/demo/tsconfig.spec.json", + "karmaConfig": "src/skeleton/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/skeleton/demo/src/app/AppComponent.ts b/src/skeleton/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/skeleton/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/skeleton/demo/src/app/AppModule.ts b/src/skeleton/demo/src/app/AppModule.ts new file mode 100644 index 0000000..5b39d38 --- /dev/null +++ b/src/skeleton/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SkeletonTestModule } from './skeleton/SkeletonTestModule'; + +@NgModule({ + imports: [ + SkeletonTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/skeleton/demo/src/app/IndexComponent.ts b/src/skeleton/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..01334c4 --- /dev/null +++ b/src/skeleton/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SkeletonTestModule } from './skeleton/SkeletonTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SkeletonTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/skeleton/demo/src/app/app.html b/src/skeleton/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/skeleton/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/skeleton/demo/src/app/skeleton/SkeletonPageComponent.ts b/src/skeleton/demo/src/app/skeleton/SkeletonPageComponent.ts new file mode 100644 index 0000000..7962ae2 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/SkeletonPageComponent.ts @@ -0,0 +1,8 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './skeleton-page.html', + styleUrls: ['./skeleton-page.less'], + encapsulation: ViewEncapsulation.None +}) +export class SkeletonPageComponent {} diff --git a/src/skeleton/demo/src/app/skeleton/SkeletonTestModule.ts b/src/skeleton/demo/src/app/skeleton/SkeletonTestModule.ts new file mode 100644 index 0000000..41a4334 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/SkeletonTestModule.ts @@ -0,0 +1,31 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiSkeletonModule } from '@opentiny/ng'; + +import { SkeletonPageComponent } from './SkeletonPageComponent'; +import { SkeletonTitleComponent } from './SkeletonTitleComponent'; +import { SkeletonTypeComponent } from './SkeletonTypeComponent'; + +@NgModule({ + imports: [CommonModule, TiSkeletonModule, RouterModule.forChild(SkeletonTestModule.ROUTES)], + declarations: [SkeletonPageComponent, SkeletonTitleComponent, SkeletonTypeComponent] +}) +export class SkeletonTestModule { + static readonly LINKS: Array = [{ href: 'components/TiSkeletonComponent.html', label: 'Skeleton' }]; + static readonly ROUTES: Routes = [ + { + path: 'skeleton/skeleton-type', + component: SkeletonPageComponent + }, + { + path: 'skeleton/skeleton-title', + component: SkeletonTitleComponent + }, + { + path: 'skeleton/skeleton-page', + component: SkeletonTypeComponent + } + ]; +} diff --git a/src/skeleton/demo/src/app/skeleton/SkeletonTitleComponent.ts b/src/skeleton/demo/src/app/skeleton/SkeletonTitleComponent.ts new file mode 100644 index 0000000..4c321e3 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/SkeletonTitleComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './skeleton-title.html', + styleUrls: ['./skeleton.less'] +}) +export class SkeletonTitleComponent {} diff --git a/src/skeleton/demo/src/app/skeleton/SkeletonTypeComponent.ts b/src/skeleton/demo/src/app/skeleton/SkeletonTypeComponent.ts new file mode 100644 index 0000000..6fa4312 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/SkeletonTypeComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './skeleton-type.html', + styleUrls: ['./skeleton.less'] +}) +export class SkeletonTypeComponent {} diff --git a/src/skeleton/demo/src/app/skeleton/skeleton-page.html b/src/skeleton/demo/src/app/skeleton/skeleton-page.html new file mode 100644 index 0000000..b3f3b42 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/skeleton-page.html @@ -0,0 +1,24 @@ +
    + + + +
    + +
    + +
    + + + +
    + +
    + +
    + + + + +
    +
    +
    diff --git a/src/skeleton/demo/src/app/skeleton/skeleton-page.less b/src/skeleton/demo/src/app/skeleton/skeleton-page.less new file mode 100644 index 0000000..57c5ab3 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/skeleton-page.less @@ -0,0 +1,43 @@ +body { + background-color: #eef0f5; +} +.leftmenu-skeleton { + width: 192px; + position: absolute; + height: 750px; +} +.content-skeleton { + margin-left: 192px; + padding: 12px 20px 0 20px; + display: flex; +} +.content-left-skeleton { + width: calc(100% - 183px - 12px); +} +.content-left-skeleton-section1 { + height: 200px; +} +.content-left-skeleton-section2 { + display: flex; + margin: 12px 0; +} +.content-left-skeleton-section2 ti-skeleton { + height: 200px; + width: calc((100% - 12px * 2) / 3); +} +.content-left-skeleton-section2 ti-skeleton:not(:last-child) { + margin-right: 12px; +} +.content-right-skeleton { + width: 183px; + margin-left: 12px; +} +.content-right-skeleton-section1 { + height: 200px; +} +.content-right-skeleton-section2 { + height: 95px; +} +.content-right-skeleton ti-skeleton:not(:last-child) { + margin-bottom: 12px; +} diff --git a/src/skeleton/demo/src/app/skeleton/skeleton-title.html b/src/skeleton/demo/src/app/skeleton/skeleton-title.html new file mode 100644 index 0000000..f8b414b --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/skeleton-title.html @@ -0,0 +1,6 @@ +
    +

    1.不设置 title 时,默认为 true

    + +

    2.设置为 false 时,没有标题

    + +
    diff --git a/src/skeleton/demo/src/app/skeleton/skeleton-type.html b/src/skeleton/demo/src/app/skeleton/skeleton-type.html new file mode 100644 index 0000000..5ddd865 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/skeleton-type.html @@ -0,0 +1,6 @@ +
    +

    1.rows 默认类型

    + +

    2.block 类型

    + +
    diff --git a/src/skeleton/demo/src/app/skeleton/skeleton.less b/src/skeleton/demo/src/app/skeleton/skeleton.less new file mode 100644 index 0000000..535ea83 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/skeleton.less @@ -0,0 +1,8 @@ +.content-background { + background-color: #eef0f5; + padding: 20px; +} +.content-skeleton { + width: 400px; + height: 200px; +} diff --git a/src/skeleton/demo/src/app/skeleton/webdoc/skeleton-demos.js b/src/skeleton/demo/src/app/skeleton/webdoc/skeleton-demos.js new file mode 100644 index 0000000..ab1b60b --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/webdoc/skeleton-demos.js @@ -0,0 +1,43 @@ +export default { + column: '1', + demos: [ + { + demoId: 'skeleton-type', + name: { + 'zh-CN': '类型', + 'en-US': 'skeleton type' + }, + desc: { + 'zh-CN': '

    通过属性type配置骨架屏的类型,包含rowsblock两种类型。', + 'en-US': 'skeleton type' + }, + apis: ['TiSkeletonComponent.properties.type'], + codeFiles: ['skeleton-type.html', 'SkeletonTypeComponent.ts', 'skeleton.less'] + }, + { + demoId: 'skeleton-title', + name: { + 'zh-CN': '标题', + 'en-US': 'skeleton title' + }, + desc: { + 'zh-CN': '

    通过属性title配置是否有标题。', + 'en-US': 'skeleton title' + }, + apis: ['TiSkeletonComponent.properties.title'], + codeFiles: ['skeleton-title.html', 'SkeletonTitleComponent.ts', 'skeleton.less'] + }, + { + demoId: 'skeleton-page', + name: { + 'zh-CN': '在典型页面中使用', + 'en-US': 'skeleton page' + }, + desc: { + 'zh-CN': 'skeleton 组件在典型页面中使用。', + 'en-US': '

    skeleton page

    ' + }, + codeFiles: ['skeleton-page.html', 'SkeletonPageComponent.ts', 'skeleton-page.less'] + } + ] +}; diff --git a/src/skeleton/demo/src/app/skeleton/webdoc/skeleton.cn.md b/src/skeleton/demo/src/app/skeleton/webdoc/skeleton.cn.md new file mode 100644 index 0000000..716b993 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/webdoc/skeleton.cn.md @@ -0,0 +1,26 @@ +--- +title: Skeleton 骨架屏 +--- +# Skeleton 骨架屏 + +
    + +Skeleton 是在需要等待加载内容的位置设置一个占位图形组合的组件。   + +```typescript +import { TiSkeletonModule } from '@opentiny/ng'; +``` + +
    + +
    + +Select 是提供选项菜单的组件。   + ++ 支持单选、多选、分组、搜索、懒加载等场景。 + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/skeleton/demo/src/app/skeleton/webdoc/skeleton.en.md b/src/skeleton/demo/src/app/skeleton/webdoc/skeleton.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/webdoc/skeleton.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/skeleton/demo/src/favicon.ico b/src/skeleton/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/skeleton/demo/src/index.html b/src/skeleton/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/skeleton/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/skeleton/demo/src/main.ts b/src/skeleton/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/skeleton/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/skeleton/demo/test.ts b/src/skeleton/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/skeleton/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/skeleton/demo/tsconfig.app.json b/src/skeleton/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/skeleton/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/skeleton/demo/tsconfig.spec.json b/src/skeleton/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/skeleton/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/skeleton/lib/index.ts b/src/skeleton/lib/index.ts new file mode 100644 index 0000000..71cab72 --- /dev/null +++ b/src/skeleton/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSkeletonModule'; diff --git a/src/skeleton/lib/ng-package.json b/src/skeleton/lib/ng-package.json new file mode 100644 index 0000000..0037f39 --- /dev/null +++ b/src/skeleton/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/skeleton", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/skeleton/lib/package.json b/src/skeleton/lib/package.json new file mode 100644 index 0000000..7235b21 --- /dev/null +++ b/src/skeleton/lib/package.json @@ -0,0 +1,9 @@ +{ + "name": "@opentiny/ng-skeleton", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/skeleton/lib/project.json b/src/skeleton/lib/project.json new file mode 100644 index 0000000..fac4d71 --- /dev/null +++ b/src/skeleton/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/skeleton/lib", + "sourceRoot": "src/skeleton/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/skeleton"], + "options": { + "project": "src/skeleton/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/skeleton"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js skeleton" + }, + { + "command": "ng default-build skeleton" + }, + { + "command": "node build/clear-default-theme.js skeleton" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/skeleton && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build skeleton && ng pack skeleton && node build/publish.js skeleton --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/skeleton/lib/src/TiSkeletonComponent.ts b/src/skeleton/lib/src/TiSkeletonComponent.ts new file mode 100644 index 0000000..292d756 --- /dev/null +++ b/src/skeleton/lib/src/TiSkeletonComponent.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input } from '@angular/core'; +/** + * 骨架屏组件 + * + */ +@Component({ + selector: 'ti-skeleton', + templateUrl: 'skeleton.html', + styleUrls: ['skeleton.less'], + host: { + '[class.ti3-skeleton-container]': 'true' + } +}) +export class TiSkeletonComponent { + /** + * 是否显示标题 + */ + @Input() title: boolean = true; + /** + * 类型 + */ + @Input() type: 'rows' | 'block' = 'rows'; +} diff --git a/src/skeleton/lib/src/TiSkeletonModule.ts b/src/skeleton/lib/src/TiSkeletonModule.ts new file mode 100644 index 0000000..7f14ac5 --- /dev/null +++ b/src/skeleton/lib/src/TiSkeletonModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TiSkeletonComponent } from './TiSkeletonComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [TiSkeletonComponent], + declarations: [TiSkeletonComponent] +}) +export class TiSkeletonModule {} + +export { TiSkeletonComponent } from './TiSkeletonComponent'; diff --git a/src/skeleton/lib/src/skeleton.html b/src/skeleton/lib/src/skeleton.html new file mode 100644 index 0000000..c3e6271 --- /dev/null +++ b/src/skeleton/lib/src/skeleton.html @@ -0,0 +1,10 @@ +
    +
      +
    • +
    • +
    +
    diff --git a/src/skeleton/lib/src/skeleton.less b/src/skeleton/lib/src/skeleton.less new file mode 100644 index 0000000..97bf2b3 --- /dev/null +++ b/src/skeleton/lib/src/skeleton.less @@ -0,0 +1,35 @@ +:host { + --ti-skeleton-title-height: var(--ti-common-size-5x); + --ti-skeleton-title-bottom-space: var(--ti-common-space-4x); + --ti-skeleton-row-height: var(--ti-common-size-5x); +} + +:host.ti3-skeleton-container { + display: block; + background-color: var(--ti-common-color-bg-white-normal); + padding: var(--ti-common-space-5x) var(--ti-common-space-6x); + box-shadow: var(--ti-common-shadow-1-down); + box-sizing: border-box; +} +.ti3-skeleton-title { + width: var(--ti-common-size-30x); + height: var(--ti-skeleton-title-height); + background-color: var(--ti-common-color-bg-disabled); + margin-bottom: var(--ti-skeleton-title-bottom-space); +} +.ti3-skeleton-row { + height: var(--ti-skeleton-row-height); + background-color: var(--ti-common-color-bg-disabled); + &:not(:last-child) { + margin-bottom: 12px; + } +} +.ti3-skeleton-block { + background-color: var(--ti-common-color-bg-disabled); +} +.ti3-skeleton-block-with-title { + height: calc(100% - var(--ti-skeleton-title-height) - var(--ti-skeleton-title-bottom-space)); +} +.ti3-skeleton-block-without-title { + height: 100%; +} diff --git a/src/slider/demo/karma.conf.js b/src/slider/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/slider/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/slider/demo/project.json b/src/slider/demo/project.json new file mode 100644 index 0000000..f84bd3d --- /dev/null +++ b/src/slider/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/slider/demo", + "sourceRoot": "src/slider/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/slider", + "index": "src/slider/demo/src/index.html", + "main": "src/slider/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/slider/demo/tsconfig.app.json", + "assets": ["src/slider/demo/src/favicon.ico", "src/slider/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "slider-demo:build:production" + }, + "development": { + "browserTarget": "slider-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js slider" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/slider/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/slider/demo/tsconfig.spec.json", + "karmaConfig": "src/slider/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/slider/demo/src/app/AppComponent.ts b/src/slider/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/slider/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/slider/demo/src/app/AppModule.ts b/src/slider/demo/src/app/AppModule.ts new file mode 100644 index 0000000..a350557 --- /dev/null +++ b/src/slider/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SliderTestModule } from './slider/SliderTestModule'; + +@NgModule({ + imports: [ + SliderTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/slider/demo/src/app/IndexComponent.ts b/src/slider/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..aea6b8b --- /dev/null +++ b/src/slider/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SliderTestModule } from './slider/SliderTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SliderTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/slider/demo/src/app/app.html b/src/slider/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/slider/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/slider/demo/src/app/slider/SliderEventComponent.ts b/src/slider/demo/src/app/slider/SliderEventComponent.ts new file mode 100644 index 0000000..94bd931 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderEventComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './slider-event.html' +}) +export class SliderEventComponent { + myLogs: Array = []; + singleValue: number = 64; + rangeValue: string = '30;85'; + min: number = 0; + max: number = 100; + scales: Array = [0, '', 40, '', 80, 100]; + + changeStop(value: number | string): void { + this.myLogs = [...this.myLogs, `changeStop event = ${value}`]; + } +} diff --git a/src/slider/demo/src/app/slider/SliderFormcontrolComponent.ts b/src/slider/demo/src/app/slider/SliderFormcontrolComponent.ts new file mode 100644 index 0000000..e90bea7 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderFormcontrolComponent.ts @@ -0,0 +1,25 @@ +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms'; + +@Component({ + templateUrl: './slider-formcontrol.html', + encapsulation: ViewEncapsulation.None +}) +export class SliderFormcontrolComponent implements OnInit { + value: number = 6; + value1: number = 2; + value2: number = 10; + singleValue: string = ''; + rangeValue: string = ''; + min: number = 1; + max: number = 12; + scales: Array = ['1个月', '2个月', '3个月', '4个月', '5个月', '6个月', '7个月', '8个月', '9个月', '1年', '2年', '3年']; + sliderForm: FormGroup; + constructor(private fb: FormBuilder) {} + ngOnInit(): void { + this.sliderForm = this.fb.group({ + singleValue: `${this.value}`, + rangeValue: `${this.value1};${this.value2}` + }); + } +} diff --git a/src/slider/demo/src/app/slider/SliderHiddenComponent.ts b/src/slider/demo/src/app/slider/SliderHiddenComponent.ts new file mode 100644 index 0000000..daea4b2 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderHiddenComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './slider-hidden.html' +}) +export class SliderHiddenComponent { + value: number = 64; + min: number = 0; + max: number = 100; + scales: Array = [0, 20, 40, 60, 80, 100]; + hidden: boolean = true; + + number1: number = 300; + number2: number = 650; + value1: string = `${this.number1};${this.number2}`; + min1: number = 100; + max1: number = 2000; + scales1: Array = [100, 300, 650, 1250, 2000]; + ratios1: Array = [0.4, 0.2, 0.2, 0.2]; + tipFormatterFn(value: number): number { + return value; + } +} diff --git a/src/slider/demo/src/app/slider/SliderLimitsComponent.ts b/src/slider/demo/src/app/slider/SliderLimitsComponent.ts new file mode 100644 index 0000000..51af062 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderLimitsComponent.ts @@ -0,0 +1,16 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './slider-limits.html', + encapsulation: ViewEncapsulation.None +}) +export class SliderLimitsComponent { + value: number = 6; + value1: number = 2; + value2: number = 10; + singleValue: string = `${this.value}`; + rangeValue: string = `${this.value1};${this.value2}`; + min: number = 1; + max: number = 12; + scales: Array = ['1个月', '2个月', '3个月', '4个月', '5个月', '6个月', '7个月', '8个月', '9个月', '1年', '2年', '3年']; +} diff --git a/src/slider/demo/src/app/slider/SliderRatiosComponent.ts b/src/slider/demo/src/app/slider/SliderRatiosComponent.ts new file mode 100644 index 0000000..8bcbdd2 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderRatiosComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './slider-ratios.html' +}) +export class SliderRatiosComponent { + singleValue1: number = 300; + singleValue2: number = 650; + singleValue: number = 1250; + rangeValue: string = `${this.singleValue1};${this.singleValue2}`; + min: number = 100; + max: number = 2000; + scales: Array = [100, 300, 650, 1250, 2000]; + ratios: Array = [0.4, 0.2, 0.2, 0.2]; +} diff --git a/src/slider/demo/src/app/slider/SliderScalesComponent.ts b/src/slider/demo/src/app/slider/SliderScalesComponent.ts new file mode 100644 index 0000000..80989d8 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderScalesComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './slider-scales.html' +}) +export class SliderScalesComponent { + singleValue1: number = 200; + max1: number = 1000; + min1: number = 100; + scales1: Array = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]; + + rangeValue: string = '200;600'; + max2: number = 1000; + min2: number = 100; + scales2: Array = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]; + + singleValue: string = '4;8'; + min: number = 1; + max: number = 11; + + scales(value: number, min: number, max: number): string { + if (value === min || value === max) { + return undefined; // 为undefined或null情况下,不显示断点且不显示文本 + } else if ((value - min) % 2 === 0) { + return `${value}个月`; + } else { + return ''; // 为""情况下,显示断点但不显示文本 + } + } +} diff --git a/src/slider/demo/src/app/slider/SliderTemplateComponent.ts b/src/slider/demo/src/app/slider/SliderTemplateComponent.ts new file mode 100644 index 0000000..1238c08 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderTemplateComponent.ts @@ -0,0 +1,54 @@ +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './slider-template.html', + encapsulation: ViewEncapsulation.None +}) +export class SliderTemplateComponent { + value: string = '2;11'; + min: number = 1; + max: number = 12; + scales: Array = [ + { + label: '1个月' + }, + { + label: '2个月' + }, + { + label: '3个月', + iconName: 'discount-sup' + }, + { + label: '4个月' + }, + { + label: '5个月' + }, + { + label: '6个月', + iconName: 'discount-sup' + }, + { + label: '7个月' + }, + { + label: '8个月' + }, + { + label: '9个月' + }, + { + label: '1年', + iconName: 'discount' + }, + { + label: '2年', + iconName: 'discount' + }, + { + label: '3年', + iconName: 'discount' + } + ]; +} diff --git a/src/slider/demo/src/app/slider/SliderTestModule.ts b/src/slider/demo/src/app/slider/SliderTestModule.ts new file mode 100644 index 0000000..28e1164 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderTestModule.ts @@ -0,0 +1,72 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiIconModule, TiSliderModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { SliderLimitsComponent } from './SliderLimitsComponent'; +import { SliderFormcontrolComponent } from './SliderFormcontrolComponent'; +import { SliderScalesComponent } from './SliderScalesComponent'; +import { SliderTemplateComponent } from './SliderTemplateComponent'; +import { SliderRatiosComponent } from './SliderRatiosComponent'; +import { SliderTipComponent } from './SliderTipComponent'; +import { SliderEventComponent } from './SliderEventComponent'; +import { SliderHiddenComponent } from './SliderHiddenComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiSliderModule, + TiIconModule, + DemoLogModule, + RouterModule.forChild(SliderTestModule.ROUTES) + ], + declarations: [ + SliderLimitsComponent, + SliderFormcontrolComponent, + SliderScalesComponent, + SliderTemplateComponent, + SliderRatiosComponent, + SliderTipComponent, + SliderEventComponent, + SliderHiddenComponent + ] +}) +export class SliderTestModule { + static readonly LINKS: Array = [{ href: 'components/TiSliderComponent.html', label: 'Slider' }]; + static readonly ROUTES: Routes = [ + { + path: 'slider/slider-limits', + component: SliderLimitsComponent + }, + { + path: 'slider/slider-fromcontrol', + component: SliderFormcontrolComponent + }, + { + path: 'slider/slider-scales', + component: SliderScalesComponent + }, + { + path: 'slider/slider-template', + component: SliderTemplateComponent + }, + { + path: 'slider/slider-ratios', + component: SliderRatiosComponent + }, + { + path: 'slider/slider-tip', + component: SliderTipComponent + }, + { + path: 'slider/slider-event', + component: SliderEventComponent + }, + { path: 'slider/slider-hidden', component: SliderHiddenComponent } + ]; +} diff --git a/src/slider/demo/src/app/slider/SliderTipComponent.ts b/src/slider/demo/src/app/slider/SliderTipComponent.ts new file mode 100644 index 0000000..df599ba --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderTipComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './slider-tip.html' +}) +export class SliderTipComponent { + value1: number = 8; + value2: number = 12; + value3: number = 2; + value4: number = 12; + rangeValue: string = `${this.value1};${this.value2}`; + rangeValue1: string = `${this.value3};${this.value4}`; + min: number = 1; + max: number = 12; + scales: Array = ['1个月', '2个月', '3个月', '4个月', '5个月', '6个月', '7个月', '8个月', '9个月', '1年', '2年', '3年']; + tipMode: string = 'always'; + tipFormatterFn(value: number): string { + if (value === 10) { + return '付10个月费用,享1年优惠'; + } else if (value === 11) { + return '2年'; + } else if (value === 12) { + return '3年'; + } else { + return `${value}个月`; + } + } +} diff --git a/src/slider/demo/src/app/slider/slider-event.html b/src/slider/demo/src/app/slider/slider-event.html new file mode 100644 index 0000000..d351b83 --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-event.html @@ -0,0 +1,5 @@ + + + + + diff --git a/src/slider/demo/src/app/slider/slider-formcontrol.html b/src/slider/demo/src/app/slider/slider-formcontrol.html new file mode 100644 index 0000000..02d11f7 --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-formcontrol.html @@ -0,0 +1,4 @@ +
    + + +
    diff --git a/src/slider/demo/src/app/slider/slider-hidden.html b/src/slider/demo/src/app/slider/slider-hidden.html new file mode 100644 index 0000000..d2a2bdd --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-hidden.html @@ -0,0 +1,20 @@ +

    1.描述

    +

    Slider组件,组件从隐藏到显示

    +

    2.示例

    +
    +

    单滑块

    + +

    双滑块

    + + +
    + diff --git a/src/slider/demo/src/app/slider/slider-limits.html b/src/slider/demo/src/app/slider/slider-limits.html new file mode 100644 index 0000000..3fa869a --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-limits.html @@ -0,0 +1,2 @@ + + diff --git a/src/slider/demo/src/app/slider/slider-ratios.html b/src/slider/demo/src/app/slider/slider-ratios.html new file mode 100644 index 0000000..2a3fb69 --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-ratios.html @@ -0,0 +1,2 @@ + + diff --git a/src/slider/demo/src/app/slider/slider-scales.html b/src/slider/demo/src/app/slider/slider-scales.html new file mode 100644 index 0000000..d243128 --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-scales.html @@ -0,0 +1,6 @@ +

    1. Array方式双滑块

    + +

    2. Array方式双滑块

    + +

    3. Function方式双滑块

    + diff --git a/src/slider/demo/src/app/slider/slider-template.html b/src/slider/demo/src/app/slider/slider-template.html new file mode 100644 index 0000000..2184c8a --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-template.html @@ -0,0 +1,17 @@ + + + {{scale.label}} + + + + + diff --git a/src/slider/demo/src/app/slider/slider-tip.html b/src/slider/demo/src/app/slider/slider-tip.html new file mode 100644 index 0000000..1487ad2 --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-tip.html @@ -0,0 +1,11 @@ + + + diff --git a/src/slider/demo/src/app/slider/webdoc/slider-demos.js b/src/slider/demo/src/app/slider/webdoc/slider-demos.js new file mode 100644 index 0000000..fbab6b8 --- /dev/null +++ b/src/slider/demo/src/app/slider/webdoc/slider-demos.js @@ -0,0 +1,100 @@ +export default { + column: '1', + + demos: [ + { + demoId: 'slider-limits', + name: { + 'zh-CN': '基本使用', + 'en-US': 'limits', + }, + desc: { + 'zh-CN': + '

    通过属性minmax配置滑动条的范围;通过属性step配置滑动条拖动时的最小间隔。

    ', + 'en-US': '

    limits

    ', + }, + apis: [ + 'TiSliderComponent.properties.max', + 'TiSliderComponent.properties.min', + 'TiSliderComponent.properties.step', + ], + }, + { + demoId: 'slider-formcontrol', + name: { + 'zh-CN': '响应式表单', + 'en-US': 'formcontrol', + }, + desc: { + 'zh-CN': '

    响应式表单的用法。

    ', + 'en-US': '

    formcontrol

    ', + }, + }, + { + demoId: 'slider-scales', + name: { + 'zh-CN': '显示文本', + 'en-US': 'scales', + }, + desc: { + 'zh-CN': + '

    通过属性scales配置滑动条刻度的显示文本;可使用 Array 和 Function 两种方式。

    ', + 'en-US': '

    scales

    ', + }, + apis: ['TiSliderComponent.properties.scales'], + }, + { + demoId: 'slider-template', + name: { + 'zh-CN': '自定义文本', + 'en-US': 'template', + }, + desc: { + 'zh-CN': '

    通过模板scale配置刻度显示文本。

    ', + 'en-US': '

    template

    ', + }, + apis: ['TiSliderComponent.slots.labelTemplate'], + }, + { + demoId: 'slider-ratios', + name: { + 'zh-CN': '不均匀分配的滑动条', + 'en-US': 'ratios', + }, + desc: { + 'zh-CN': '

    通过属性ratios配置滑动条每段所占长度比。

    ', + 'en-US': '

    ratios

    ', + }, + apis: ['TiSliderComponent.properties.ratios'], + }, + { + demoId: 'slider-tip', + name: { + 'zh-CN': '提示', + 'en-US': 'tip', + }, + desc: { + 'zh-CN': + '

    通过属性tipMode配置 tip 的显示方式,包括autoalways两种类型;通过属性tipFormatterFn配置 tip 的显示文本。

    ', + 'en-US': '

    tip

    ', + }, + apis: [ + 'TiSliderComponent.properties.tipMode', + 'TiSliderComponent.properties.tipFormatterFn', + ], + }, + { + demoId: 'slider-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event', + }, + desc: { + 'zh-CN': + '

    当绑定的值变化停止的时候触发changeStop事件,传递出去的参数为:停止时的值。

    ', + 'en-US': '

    event

    ', + }, + apis: ['TiSliderComponent.events.changeStop'], + }, + ], +}; diff --git a/src/slider/demo/src/app/slider/webdoc/slider.cn.md b/src/slider/demo/src/app/slider/webdoc/slider.cn.md new file mode 100644 index 0000000..dc168bf --- /dev/null +++ b/src/slider/demo/src/app/slider/webdoc/slider.cn.md @@ -0,0 +1,24 @@ +--- +title: Slider 滑块 +--- +# Slider 滑块 + +
    + +Slider 滑块组件,显示当前值和选择范围。   + +```typescript +import { TiSliderModule } from '@opentiny/ng'; +``` + +
    + +
    + +Slider 滑动条组件,显示当前值和选择范围。   + +```typescript +import { TiSliderModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/slider/demo/src/app/slider/webdoc/slider.en.md b/src/slider/demo/src/app/slider/webdoc/slider.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/slider/demo/src/app/slider/webdoc/slider.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/slider/demo/src/favicon.ico b/src/slider/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/slider/demo/src/index.html b/src/slider/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/slider/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/slider/demo/src/main.ts b/src/slider/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/slider/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/slider/demo/test.ts b/src/slider/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/slider/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/slider/demo/tsconfig.app.json b/src/slider/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/slider/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/slider/demo/tsconfig.spec.json b/src/slider/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/slider/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/slider/lib/index.ts b/src/slider/lib/index.ts new file mode 100644 index 0000000..0920512 --- /dev/null +++ b/src/slider/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSliderModule'; diff --git a/src/slider/lib/ng-package.json b/src/slider/lib/ng-package.json new file mode 100644 index 0000000..6d1f158 --- /dev/null +++ b/src/slider/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/slider", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/slider/lib/package.json b/src/slider/lib/package.json new file mode 100644 index 0000000..7410874 --- /dev/null +++ b/src/slider/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "@opentiny/ng-slider", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/platform-browser": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-drag": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/slider/lib/project.json b/src/slider/lib/project.json new file mode 100644 index 0000000..f341207 --- /dev/null +++ b/src/slider/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/slider/lib", + "sourceRoot": "src/slider/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/slider"], + "options": { + "project": "src/slider/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/slider"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js slider" + }, + { + "command": "ng default-build slider" + }, + { + "command": "node build/clear-default-theme.js slider" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/slider && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build slider && ng pack slider && node build/publish.js slider --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/slider/lib/src/TiSliderComponent.ts b/src/slider/lib/src/TiSliderComponent.ts new file mode 100644 index 0000000..406f414 --- /dev/null +++ b/src/slider/lib/src/TiSliderComponent.ts @@ -0,0 +1,1027 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + HostListener, + Input, + IterableChanges, + IterableDiffer, + IterableDiffers, + Output, + Renderer2, + SecurityContext, + SimpleChanges, + TemplateRef, + ViewChild, + ChangeDetectionStrategy +} from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { Util } from '@opentiny/ng-utils'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * @ignore + */ +export interface TiDragConfig { + helper: any; + position: { + left: number; + top: number; + }; +} + +/** + * Slider滑块组件 + * + * 滑块组件,通过操作组件选择指示范围 + * + */ +@Component({ + selector: 'ti-slider', + templateUrl: './slider.html', + styleUrls: ['./slider.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-slider-container]': 'true', + '[class.ti3-slider-disable]': 'disabled', + '(mousedown)': 'hostMousedownEvent($event)' + }, + providers: [TiFormComponent.getValueAccessor(TiSliderComponent)] +}) +export class TiSliderComponent extends TiFormComponent { + /** + * 最小范围值 + */ + @Input() min: number = 0; + /** + * 最大范围值 + */ + @Input() max: number = 10; + + /** + * 步长 + */ + @Input() step: number = 1; + /** + * 刻度标记 + * + * 1.当刻度标记为数组时,数组中各元素依次对应各刻度显示值;当刻度标记为 Function 时,返回刻度显示值; + * 参数:value (当前刻度)、max (最大值)、min (最小值)。 + * + * 2.当刻度标记为空字符串,只有刻度没有标记;当刻度标记为 undefined / null,没有刻度标记;其他情况,有刻度且有标记。 + */ + @Input() scales: Array | Function; + /** + * 滑块 tip 提示的显示方式 + */ + @Input() tipMode: 'auto' | 'always' = 'auto'; + /** + * tip 提示内容的函数,返回值是 tip 中显示的文本 + */ + @Input() tipFormatterFn: (value: any) => string; + /** + * 刻度比,依次设置 scales 中两个相邻刻度间的长度占比,总和为 1 + */ + @Input() ratios: Array; + /** + * 滑动停止并且值发生改变时触发的回调 + */ + @Output() readonly changeStop: EventEmitter = new EventEmitter(); + /** + * 获取到用户自定义的刻度 + */ + @ContentChild(TemplateRef, { static: true }) + public labelTemplate: TemplateRef; + /** + * @ignore + * 存放用户传入的刻度值 + */ + public ticks: Array; + /** + * @ignore + * 对于双滑块,左滑块是否处于变化状态 + */ + public isMinPointerActive: boolean; + /** + * @ignore + * 拖动配置参数 + */ + public dragOptions: any; + protected versionInfo: string = super.getVersion(packageInfo); + private ticksArr: Array; // 存放合法的刻度值 + private isDouble: boolean = false; // 判断是否是双滑块,默认是单滑块 false + private isTipAutoShow: boolean = true; // 判断tip提示是否是自动显示(鼠标操作才显示),默认true + private decimalDigit: number; // ratios中最大的小数位数 + private scalesDiffer: IterableDiffer; // scales 变化检查 + private dragStartModel: any; + private pointerMaxEleWidth: number; // 滑块的宽度 + + // 获取模板上DOM变量 + @ViewChild('trackEle', { static: true }) private trackRef: ElementRef; + @ViewChild('selection', { static: true }) private selectionRef: ElementRef; + @ViewChild('pointerMin', { static: true }) private pointerMinRef: ElementRef; + @ViewChild('pointerMax', { static: true }) private pointerMaxRef: ElementRef; + @ViewChild('tipMin', { static: true }) private tipMinRef: ElementRef; + @ViewChild('tipMax', { static: true }) private tipMaxRef: ElementRef; + // 定义DOM变量 + private trackELe: any; + private selectionEle: any; + private pointerMinEle: any; + private pointerMaxEle: any; + private tipMinEle: any; + private tipMaxEle: any; + private isVisibleInit: boolean; // 标识初始化时组件是否可见 + + /** + * @description: 将value值处理成数组 + * @param: value 需要切割的数组 + */ + private static splitValueToArray(value: string): { + valueMin: number; + valueMax: number; + } { + const arr: Array = `${value}`.split(';'); // value为number时强转为字符串 + + return { + valueMin: parseFloat(arr[0]), + valueMax: arr[1] && parseFloat(arr[1]) + }; + } + + /** + * @description: value值是否超限制 + * @param: value: 判断的value值 + * @param: minValue: 最大值 + * @param: maxValue: 最小值 + */ + private static isLimitExceed(value: number, minValue: number, maxValue: number): boolean { + return value < minValue || value > maxValue; + } + + /** + * @description: 当this.scales为函数情况下,转换为ticks数组 + * @param: scaleFormat 被转换的函数 + * @param: minValue 刻度限制的最大值 + * @param: maxValue 刻度限制的最小值 + * @param: step 刻度步长 + */ + private static translateScales(scaleFormat: Function, minValue: number, maxValue: number, step: number): Array { + const valueLen: number = (maxValue - minValue) / step; + const tickArray: Array = []; + for (let i: number = 0; i <= valueLen; i++) { + const stepNValue: number = step * i + minValue; + const formatRet: any = scaleFormat(stepNValue, minValue, maxValue); + if (Util.isUndefined(formatRet) || Util.isNull(formatRet)) { + // 为undefined或null情况下,不打点不显示label + tickArray.push(null); + } else { + // 包含为""(打点不显示label)和非""情况(打点且显示label) + tickArray.push(formatRet); + } + } + + return tickArray; + } + + /** + * @description: 在范围内限制value数值 + * @param: value 校验的value值 + * @param: min 最小值 + * @param: max 最大值 + * @return: 有效value值 + */ + private static limitValue(value: number, min: number, max: number): number { + // 小于min取min,大于max取max,其他不变。 + const res: number = value < min ? min : value > max ? max : value; + + return res; + } + + /** + * @description: 将样式数值转化为calc形式的css样式(设置为百分比形式,确保缩放的支持) + * @param: percent calc百分比 + * @param: subValue calc减去值 + */ + private static parseToCalcStyle(percent: number, subValue?: number): string { + if (Number.isNaN(subValue) || Util.isUndefined(subValue)) { + return `calc(${percent * 100}%)`; + } + + return `calc(${percent * 100}% - ${subValue}px)`; // calc中运算符合2边一定要空格 + } + + constructor( + protected hostRef: ElementRef, + protected renderer2: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + private tiRenderer: TiRenderer, + private iterableDiffers: IterableDiffers, + private domSanitizer: DomSanitizer + ) { + super(hostRef, renderer2, changeDetectorRef); + } + + /** + * @ignore + */ + @HostListener('window:resize') onResize(): void { + this.updateValuePosition(this.model); + } + + // 组件声明周期钩子--start + ngOnInit(): void { + super.ngOnInit(); + this.initDom(); + this.initVars(); + } + /** + * @ignore + */ + public initDom(): void { + this.trackELe = this.trackRef.nativeElement; + this.selectionEle = this.selectionRef.nativeElement; + this.pointerMinEle = this.pointerMinRef.nativeElement; + this.pointerMaxEle = this.pointerMaxRef.nativeElement; + this.tipMinEle = this.tipMinRef.nativeElement; + this.tipMaxEle = this.tipMaxRef.nativeElement; + } + /** + * @ignore + */ + public initVars(): void { + this.step = Number.isNaN(parseFloat(`${this.step}`)) ? 1 : parseFloat(`${this.step}`); + this.setMinMax(); + // ratios中最大的小数位数 + this.decimalDigit = this.getDecimalDigit(); + + // 处理用戶未设置scales或者scales為function情況 + if (!this.scales || !Array.isArray(this.scales)) { + this.setScales(); + } + + this.setTipConfig(); + // 给tip添加事件(tipMode是 'auto'的场景) + this.addTipEvent(); + + this.dragOptions = { + axis: 'x', + start: this.dragStartHandle, + drag: this.dragHandle, + stop: this.dragStopHandle + }; + } + ngOnChanges(changes: SimpleChanges): void { + if (changes['scales'] && !changes['scales'].firstChange && !Array.isArray(this.scales)) { + this.setScales(); + this.updateValuePosition(this.model); + } + + // 监听最大值最小值变化 + if ((changes['min'] && !changes['min'].firstChange) || (changes['max'] && !changes['max'].firstChange)) { + this.setMinMax(); + } + } + ngAfterViewInit(): void { + this.pointerMaxEleWidth = this.pointerMaxEle.offsetWidth; + this.isVisibleInit = this.pointerMaxEleWidth !== 0; + this.changeDetectorRef.detectChanges(); + } + + ngAfterViewChecked(): void { + // 监听滑块的宽度:处理组件从隐藏到显示定位问题 + if (!this.isVisibleInit) { + const pointerMaxWidth: number = this.pointerMaxEle.offsetWidth; + if (pointerMaxWidth !== 0) { + this.isVisibleInit = true; + this.pointerMaxEleWidth = pointerMaxWidth; + this.updateValuePosition(this.model); + this.changeDetectorRef.detectChanges(); + } + } + } + ngDoCheck(): void { + if (!Array.isArray(this.scales)) { + return; + } + if (!this.scalesDiffer) { + this.scalesDiffer = this.iterableDiffers.find(this.scales).create(); + } + + const scalesDiffer: IterableChanges = this.scalesDiffer.diff(this.scales); + if (scalesDiffer) { + this.setScales(); + // 初始化的updateValuePosition在writeValue中执行(writeValue迟于docheck执行) + if (!Util.isUndefined(this.model)) { + this.updateValuePosition(this.model); + } + } + } + // 组件声明周期钩子--end + + // 实现ControlValueAccessor接口 + /** + * @ignore + */ + writeValue(value: any): void { + // TODO: 接口设计重新调整 + // ngModel => value: string | number; // 滑块对应的value值 exp: 单滑块: 15 OR '15' 双滑块 '15;34'; + super.writeValue(value); + if (!Util.isNull(value) && !Util.isUndefined(value)) { + // 1.判断是否是双滑块 + this.isDouble = !Util.isUndefined(TiSliderComponent.splitValueToArray(value).valueMax); // 根据value形式确定单滑块/双滑块 + if (Util.isUndefined(this.onModelChange)) { + // 在reactive-form中使用,初始化赋值调用writeValue时, + // 此时registerOnChange还未被调用,onChangeFn还未被赋值, + // 所以要使用promise(异步)等onChangeFn被赋值后再调用 + Promise.resolve(undefined).then(() => { + this.writeValueHandle(value); + }); + } else { + this.writeValueHandle(value); + } + } + } + private writeValueHandle(value: any): void { + if (this.isDouble) { + this.show(this.pointerMinEle); + } else { + this.hide(this.pointerMinEle); + } + // 赋值操作 && 更新位置 + this.updateValuePosition(value); + } + // 实现ControlValueAccessor接口--end + + // 组件交互方法集合--start + /** + * @ignore + * 不存在下面的问题,用click和mousedown事件都可以(统一使用mousedown事件) + * 使用mousedown替代click,用于防止拖拽过程中触发click事件导致的滑块滑动 + * 问题场景:min滑块拖动与max滑块重合后,鼠标继续向右移动,此时鼠标抬起,触发click事件,导致max发生移动 + */ + public hostMousedownEvent = (event: MouseEvent): void => { + // 灰化状态或点击pointer时不做处理 + if ( + this.disabled || + this.tiRenderer.hasClass(event.target, 'ti3-slider-pointer') || + this.tiRenderer.hasClass(event.target, 'ti3-slider-tip') + ) { + return; + } + this.stepSliderFromClick(event); + }; + + /** + * @description: 添加tip的事件监听 + */ + private addTipEvent(): void { + if (this.isTipAutoShow) { + this.renderer2.listen(this.pointerMaxEle, 'mouseover', this.tipMouseoverHandle); + this.renderer2.listen(this.pointerMinEle, 'mouseover', this.tipMouseoverHandle); + this.renderer2.listen(this.pointerMaxEle, 'mouseleave', this.tipMouseleaveHandle); + this.renderer2.listen(this.pointerMinEle, 'mouseleave', this.tipMouseleaveHandle); + } + } + + /** + * @description: 滑块元素的mouseover事件处理函数 + */ + private tipMouseoverHandle = (event: any): void => { + if (!this.disabled) { + const sliderTip: Element = event.target.querySelector('.ti3-slider-tip'); + if (sliderTip) { + this.show(sliderTip); + } + } + }; + + /** + * @description: 滑块元素的mouseleave事件处理函数 + */ + private tipMouseleaveHandle = (event: any): void => { + if (this.disabled) { + return; + } + this.hide(event.target.querySelector('.ti3-slider-tip')); + }; + + /** + * @description: 宿主元素的mousedown事件处理函数 + */ + private stepSliderFromClick = (event: MouseEvent): void => { + const firstTickX: number = this.trackELe.getBoundingClientRect().left + this.pointerMaxEleWidth / 2; + const oldModel: any = this.model; + let pointerX: number = event.clientX - firstTickX; + pointerX = TiSliderComponent.limitValue(pointerX, 0, this.getBarWidth()); + let value: number = this.positionToValue(pointerX); // 获取value绝对比例值,用于确定滑块移动位置 + // 确定滑动哪个滑块 + let pointer: any = this.pointerMaxEle; + if (this.isDouble) { + const valueMin: number = TiSliderComponent.splitValueToArray(this.model).valueMin; + const valueMax: number = TiSliderComponent.splitValueToArray(this.model).valueMax; + // 以value中间值为界,在最小值及中间值之间=》左滑块移动;否则=》右滑块移动 + const midValue: number = (valueMax + valueMin) / 2; + if (value >= this.min && value < midValue) { + pointer = this.pointerMinEle; + this.isMinPointerActive = true; + } else { + this.isMinPointerActive = false; + } + } + value = this.getStepValue(value); + this.setValue(value); // 向外部通知value值 + this.valueToPosition(value, pointer); // 跳至value对应的坐标位置(与step对应) + if (this.model !== oldModel) { + this.changeStop.emit(this.model); + } + }; + + /** + * @description 拖拽开始执行的事件 + * @param: ui 拖拽助手 {helper:表示被拖拽的助手(helper), position: 助手(helper)的当前 CSS 位置} + */ + private dragStartHandle = (ui: TiDragConfig): void => { + this.dragStartModel = this.model; + if (this.isDouble) { + this.isMinPointerActive = !this.tiRenderer.hasClass(ui.helper, 'ti3-slider-pointer-max'); + } + }; + + /** + * @description 根据拖拽位置更新value值并改变位置呈现 + * @param: ui 拖拽助手 {helper:表示被拖拽的助手(helper), position: 助手(helper)的当前 CSS 位置} + */ + private dragHandle = (ui: TiDragConfig): void => { + // 设置value值 + const value: number = this.dragCommonHandle(ui); + this.setValue(value); // 向外部通知value值 + // 设置tip提示 + if (this.isTipAutoShow) { + // 拖拽过程中显示tip提示 + this.show(ui.helper.querySelector('.ti3-slider-tip')); + } + this.setTip(ui.helper.querySelector('.ti3-slider-tip'), value); // 设置tip提示值及位置 + }; + + /** + * @description: 鼠标弹起之后:根据拖拽位置跳至step对应的值并设置slider + * @param: ui 拖拽助手 {helper:表示被拖拽的助手(helper), position: 助手(helper)的当前 CSS 位置} + */ + private dragStopHandle = (ui: TiDragConfig): void => { + // 设置value值 + const value: number = this.dragCommonHandle(ui); + + if (this.isTipAutoShow) { + // 拖拽停止隐藏tip提示 + this.hide(ui.helper.querySelector('.ti3-slider-tip')); + } + this.valueToPosition(value, ui.helper); // 跳至value对应的坐标位置 + if (this.model !== this.dragStartModel) { + this.changeStop.emit(this.model); + } + }; + + // 组件交互方法集合--end + + // 内部公共方法集合--start + /** + * @description: 拖拽公共处理 + * @param: ui 拖拽助手 {helper:表示被拖拽的助手(helper), position: 助手(helper)的当前 CSS 位置} + */ + private dragCommonHandle(ui: TiDragConfig): number { + const pointerX: number = this.limitDragPosition(ui); // 限制指针在合理的位置范围内 + const value: number = this.positionToStepValue(pointerX); // 获取当前位置对应的value值 + + return value; + } + + /** + * @description: 限制滑块移动位置并设置对应的选择区域宽度 + * @param: pointerX 鼠标点击位置(x方向) + */ + private limitDragPosition(ui: TiDragConfig): number { + let pointerX: number = ui.position.left; // 当前滑块中心距离tick第一个坐标位置 + const barWidth: number = this.getBarWidth(); + let pointerWidthHalf: number = this.pointerMaxEleWidth / 2; + let pointerMinLeft: number; + let pointerMaxLeft: number; + if (this.isDouble) { + if (!this.isMinPointerActive) { + // max对应滑块情况的处理 + pointerMinLeft = parseFloat(getComputedStyle(this.pointerMinEle).left); + pointerX = TiSliderComponent.limitValue(pointerX, pointerMinLeft, barWidth); // 限制滑块在最小和右边界之间 + pointerMaxLeft = pointerX; + } else { + pointerMaxLeft = parseFloat(getComputedStyle(this.pointerMaxEle).left); + pointerX = TiSliderComponent.limitValue(pointerX, 0, pointerMaxLeft); // 限制滑块在左边界和最大之间 + pointerMinLeft = pointerX; + } + const styles: { width: string; left: string } = { + width: `${pointerMaxLeft - pointerMinLeft}px`, + left: `${pointerMinLeft + pointerWidthHalf}px` + }; + this.tiRenderer.setStyles(this.selectionEle, styles); + } else { + pointerX = TiSliderComponent.limitValue(pointerX, 0, barWidth); + this.renderer2.setStyle(this.selectionEle, 'width', `${pointerX + pointerWidthHalf}px`); + } + + ui.position.left = pointerX; // 限制滑块的位置显示 + + return pointerX; + } + + /** + * @description: 根据滑块位置获取value值(和step对应) (拖拽公共函数中用) + * @param: pointerX 鼠标点击位置(x方向) + */ + private positionToStepValue(pointerX: number): number { + const value: number = this.positionToValue(pointerX); // 获取指针对应的value绝对比例值(和step无关) + + return this.getStepValue(value); // 转化value为与step对应的值; + } + + /** + * @description: 获取位置对应的value值 + * @param: pointerX 鼠标点击位置(x方向) + */ + private positionToValue(pointerX: number): number { + const barWidth: number = this.getBarWidth(); + const percent: number = pointerX / barWidth; + if (!Util.isUndefined(this.ratios)) { + return this.unequalPositionToValue(pointerX, barWidth, percent); + } + + return this.min + percent * (this.max - this.min); + } + + /** + * @description: 设置组件的model值,用于向外通知 + * @param: value 当前指针的值 + */ + private setValue(value: number): void { + if (this.isDouble) { + const valueMin: number = TiSliderComponent.splitValueToArray(this.model).valueMin; + const valueMax: number = TiSliderComponent.splitValueToArray(this.model).valueMax; + if (!this.isMinPointerActive && value !== valueMax) { + this.model = `${valueMin};${value}`; + } else if (this.isMinPointerActive && value !== valueMin) { + this.model = `${value};${valueMax}`; + } + } else { + if (value !== this.model) { + this.model = value; + } + } + } + + /** + * @description: 获取对应的step值(根据step修正value值) + * @param: value 当前点击的绝对值 + */ + private getStepValue(value: number): number { + const min: number = this.min; + const step: number = this.step; + let stepValue: number; + const stepN: number = Math.round((value - min) / step); // value值变化几个步长 + if (step.toString().indexOf('.') !== -1) { + const n: number = step.toString().split('.').length; + stepValue = parseFloat((min + stepN * step).toFixed(n)); + } else { + stepValue = min + stepN * step; + } + + return TiSliderComponent.limitValue(stepValue, min, this.max); + } + + /** + * @description: 在不等分条件下获取位置对应的value值 + * @param: pointerX 鼠标点击位置(x方向) + * @param: barWidth 指针宽度 + * @param: percent pointerX占滑动轴百分比 + */ + private unequalPositionToValue(pointerX: number, barWidth: number, percent: number): number { + let limitWidth: number = 0; // 每一段的宽度 + let selectLength: number = 0; // 已被选中区域段的宽度 + let ratiosWidth: number = 0; // 已被选中区域段长度和占总长的比例 + const c: number = this.decimalDigit; + const ticksLen: number = this.ticksArr.length; + for (let i: number = 0; i < ticksLen - 1; i++) { + limitWidth = barWidth * this.ratios[i]; + if (i === 0) { + selectLength = 0; + ratiosWidth = 0; + } else { + ratiosWidth = Number((ratiosWidth + this.ratios[i - 1]).toFixed(c)); + selectLength = barWidth * ratiosWidth; + } + const pointerWidth: number = pointerX - selectLength; + if (percent <= Number((ratiosWidth + this.ratios[i]).toFixed(c)) && percent >= ratiosWidth) { + return this.ticksArr[i] + (pointerWidth / limitWidth) * (this.ticksArr[i + 1] - this.ticksArr[i]); + } + } + } + + /** + * @description: 当前组件写入新值 || min || max || scales发生变化 时更新value对应的位置 + * @param: newValue 需要处理的value的值 + */ + private updateValuePosition(newValue: string): void { + let valueTmp: string; + if (this.isDouble) { + valueTmp = this.restrictDoubleValue(newValue); + const valueMax: number = TiSliderComponent.splitValueToArray(valueTmp).valueMax; + const valueMin: number = TiSliderComponent.splitValueToArray(valueTmp).valueMin; + // 当设置value均为最大值情况下,设置最小滑块层级,确保滑块可被拖动 + // 最小值的情况左右滑块zIndex相等,但是右滑块DOM在左滑块之后,因此根据zIndex规则,右滑块可以覆盖左滑块 + if (valueMin === valueMax && valueMin === this.max) { + this.isMinPointerActive = true; + } + // 设置滑块的位置 + this.valueToPosition(valueMin, this.pointerMinEle); + this.valueToPosition(valueMax, this.pointerMaxEle); + } else { + valueTmp = this.restrictSingleValue(newValue); + this.valueToPosition(valueTmp, this.pointerMaxEle); + } + + if (newValue !== valueTmp) { + this.model = valueTmp; + } + } + + /** + * @description: 限定双滑块value的值 + * @param: value 需要处理的value的值 + */ + private restrictDoubleValue(value: string): string { + const valueMin: number = TiSliderComponent.splitValueToArray(value).valueMin; // 双滑块的小值 + const valueMax: number = TiSliderComponent.splitValueToArray(value).valueMax; // 双滑块的大值 + // 非法情况:非数字,超过限制,value大小顺序不正确情况 + const isInvalidValueMin: boolean = + isNaN(valueMin) || TiSliderComponent.isLimitExceed(valueMin, this.min, this.max) || valueMin > valueMax; + // 非法情况:非数字,超过限制,value大小顺序不正确情况 + const isInvalidValueMax: boolean = + isNaN(valueMax) || TiSliderComponent.isLimitExceed(valueMax, this.min, this.max) || valueMin > valueMax; + + return `${isInvalidValueMin ? this.min : valueMin};${isInvalidValueMax ? this.max : valueMax}`; + } + + /** + * @description: 限定单滑块value的值 + * @param: value 需要处理的value的值 + */ + private restrictSingleValue(value: any): any { + if (isNaN(parseFloat(value)) || value < this.min) { + // value不是数字或者小于最小值point定位到最小值 + return this.min; + } + if (value > this.max) { + // value大于最大值point定位到最大值 + return this.max; + } + + return value; + } + + /** + * @description: 处理tip + */ + private setTipConfig(): void { + const defaultTipShow: 'auto' | 'always' = 'auto'; + const defaultTipFormatter: (value: any) => string = (value: any): any => { + return value; + }; + + this.tipMode = Util.isUndefined(this.tipMode) ? defaultTipShow : this.tipMode; + this.tipFormatterFn = Util.isUndefined(this.tipFormatterFn) ? defaultTipFormatter : this.tipFormatterFn; + this.isTipAutoShow = this.tipMode !== 'always'; + if (this.isTipAutoShow) { + this.hide(this.tipMaxEle); + this.hide(this.tipMinEle); + } else { + this.show(this.tipMaxEle); + this.show(this.tipMinEle); + } + } + + /** + * @description: 处理限制 + */ + private setMinMax(): void { + const defaultMin: number = 0; + const defaultMax: number = 10; + const min: number = parseFloat(`${this.min}`); + const max: number = parseFloat(`${this.max}`); + this.min = isNaN(min) || min >= max ? defaultMin : min; + this.max = isNaN(max) || min >= max ? defaultMax : max; + } + + /** + * @description: 处理刻度 + */ + private setScales(): void { + const step: number = this.step; + const min: number = this.min; + const max: number = this.max; + let ticks: Array = []; + if (typeof this.scales === 'function') { + this.ticks = TiSliderComponent.translateScales(this.scales, min, max, step); + } else if (Array.isArray(this.scales)) { + // 为Array情况下,直接使用数值 + this.ticks = this.scales.concat(); + } else { + // 未传scale(包含非法)的情况下,只显示最小和最大值 + const ticksLen: number = (max - min) / step; + ticks.push(min); + for (let i: number = 0; i < ticksLen - 1; i++) { + // 除最小最大值外,其余只打点不显示label + ticks.push(''); + } + ticks.push(max); + this.ticks = ticks; + } + if (!Util.isUndefined(this.ratios)) { + let ratiosSum: number = 0; // 将ratios中每一项与之前的求和 + const sumArray: Array = []; // 由ratiosSum组成的数组 + const ratiosLen: number = this.ratios.length; + for (let i: number = 0; i < ratiosLen; i++) { + ratiosSum = Number((ratiosSum + Number(this.ratios[i])).toFixed(this.decimalDigit)); + sumArray.push(ratiosSum); + } + ticks = this.getTicks(sumArray, this.decimalDigit); + this.ticks = ticks; + } + } + + /** + * @description: 非均匀情况下确定打点的位置 + * @param: arr 由ratiosSum组成的数组 + * @param: num ratios中最大的小数位数 + */ + private getTicks(arr: Array, num: number): Array { + const minScale: number = Number(Math.pow(0.1, num).toFixed(num)); // 判断是否打点的基数 + let k: number = 1; + let p: number = 0; + const ticks: Array = [this.ticks[0]]; + const length: number = parseInt(`${1 / minScale}`, 10); + for (let i: number = 0; i < length; i++) { + p = Number((p + minScale).toFixed(num)); + if (arr.indexOf(p) === -1) { + // 判断每次相加基数后是否在数组中,不在不打点不显示label + ticks.push(null); + } else { + // 否则打点显示label + ticks.push(this.ticks[k]); + k++; + } + } + + return ticks; + } + + /** + * @description: 显示隐藏的元素 + * @param: ele: 要显示的DOM对象 + */ + private show(ele: any): void { + // 注:此处未使用ng-show方式控制滑块显示是因为有延迟,造成后续方法获取pointer宽度为0,显示错乱 + this.renderer2.setStyle(ele, 'display', 'block'); + } + + /** + * @description: 隐藏显示的元素 + * @param: ele: 要隐藏的DOM对象 + */ + private hide(ele: any): void { + // 注:此处未使用ng-show方式控制滑块显示是因为有延迟,造成后续方法获取pointer宽度为0,显示错乱 + this.renderer2.setStyle(ele, 'display', 'none'); + } + + /** + * @ignore + * @description: 获取ticks的最大显示宽度,一行显示不下情况下,换行显示 + * @param: index 刻度下标 + */ + public calcTickMaxWidth(index: number): string { + const ticksLen: number = this.ticks.length; + const spacePercent: number = 1 / (ticksLen - 1); // 两个刻度点间隔宽度百分百(小数) + if (index === 0 || index === ticksLen - 1) { + // 第一个和最后一个刻度 + return TiSliderComponent.parseToCalcStyle(spacePercent, this.pointerMaxEleWidth / 2); + } + + return TiSliderComponent.parseToCalcStyle(spacePercent); + } + + /** + * @ignore + * @description: 获取 ticks 的left位置 + * @param: index 刻度下标 + */ + public calcTickLeftPosition(index: number): string { + return `${(index / (this.ticks.length - 1)) * 100}%`; + } + + // TODO: 看这个方法在模板上调用添加的样式类没有什么实际作用,是不是可以删除? + /** + * @ignore + * @description: 确定滑动轴打点是否为选中点,根据函数返回值设置选中样式 (是否是selection区域的点) + * @param: index 刻度下标 + */ + public isSelectTick(index: number): boolean { + const ticksLen: number = this.ticks.length; + // 双滑块情况下,位于最大最小值之间为选中状态 + if (this.isDouble) { + const value: Array = this.model.split(';'); + const isLargeThanValMin: boolean = index / ticksLen >= (parseFloat(value[0]) - this.min) / (this.max - this.min); + const isSmallThanValMax: boolean = index / ticksLen <= (parseFloat(value[1]) - this.min) / (this.max - this.min); + + return isLargeThanValMin && isSmallThanValMax; + } + + // 单滑块情况下,小于选中值为选中状态 + return index / ticksLen <= parseFloat(this.model) - this.min; + } + + /** + * @description: 获取滑动轴的宽度(涉及到屏幕缩放,所以需要实时获取) + */ + private getBarWidth(): number { + return this.trackELe.getBoundingClientRect().width - this.pointerMaxEleWidth; + } + + /** + * @description: 获取ratios中最大的小数位数 + */ + private getDecimalDigit(): number { + const decimalArr: Array = []; + if (Array.isArray(this.ratios)) { + this.ratios.forEach((item: number) => { + const decimal: string = item.toString().split('.')[1]; + if (decimal) { + decimalArr.push(decimal.length); + } + }); + } + + return Math.max.apply(null, decimalArr); + } + + /** + * @description: 获取当前value所在的区域以及当前value对应的长度占总长的百分比(小数) + * @param: value 指针对应的value值 + * @param: paragraph 当前打点的段数 + */ + private getValuePercent(value: number, paragraph: number): number { + let ratiosSum: number = 0; // 当前打点的段数下ratios的总和 + for (let i: number = 0; i < paragraph; i++) { + ratiosSum = Number((ratiosSum + this.ratios[i]).toFixed(this.decimalDigit)); + } + + return ( + ratiosSum + ((value - this.ticksArr[paragraph]) / (this.ticksArr[paragraph + 1] - this.ticksArr[paragraph])) * this.ratios[paragraph] + ); + } + + /** + * @description: 设置pointerDOM位置 + * @param: value 指针对应的value值 + * @param: pointer 被设置的DOM对象 + */ + private setPointerPos(value: number, pointer: any): void { + let valuePercent: number; + if (!Util.isUndefined(this.ratios)) { + if (Util.isUndefined(this.ticks)) { + return; + } + this.ticksArr = []; + this.ticks.forEach((tick: string) => { + if (!Util.isNull(tick)) { + this.ticksArr.push(tick); + } + }); + const ticksArrLen: number = this.ticksArr.length; + for (let i: number = 0; i < ticksArrLen - 1; i++) { + if (value >= this.ticksArr[i] && value <= this.ticksArr[i + 1]) { + valuePercent = this.getValuePercent(value, i); + } + } + } else { + valuePercent = (value - this.min) / (this.max - this.min); + } + // 计算value对应的百分比位置,并以tick中心点居中显示 + const pointerLeft: string = TiSliderComponent.parseToCalcStyle(valuePercent); + this.renderer2.setStyle(pointer, 'left', pointerLeft); + } + + /** + * @description: 改变某一指针value值对应的指示位置 + * @param: value 指针对应的value值 + * @param: pointer 被设置的DOM对象 + */ + private valueToPosition(value: any, pointer: any): void { + this.setPointerPos(value, pointer); // 设置pointer位置 + let styles: object; // 滑动轴有效区域背景 样式对象 + // 修复SSR报错:ERROR { Error: Uncaught (in promise): TypeError: this.pointerMaxEle.getBoundingClientRect is not a function + if (typeof this.pointerMaxEle.getBoundingClientRect !== 'function') { + return; + } + // 设置滑动轴有效区域背景 + const pointerMaxLeft: number = this.pointerMaxEle.getBoundingClientRect().left; + const pointerMinLeft: number = this.pointerMinEle.getBoundingClientRect().left; + const barWidth: number = this.getBarWidth(); + const barLeft: number = this.trackELe.getBoundingClientRect().left; + const pointerWidthHalf: number = this.pointerMaxEleWidth / 2; + if (this.isDouble) { + styles = { + width: `${((pointerMaxLeft - pointerMinLeft) * 100) / barWidth}%`, + left: `${((pointerMinLeft - barLeft + pointerWidthHalf) * 100) / barWidth}%` + }; + } else { + styles = { + width: `${((pointerMaxLeft - barLeft + pointerWidthHalf) * 100) / barWidth}%`, + left: 0 + }; + } + this.tiRenderer.setStyles(this.selectionEle, styles); + this.setTip(pointer.querySelector('.ti3-slider-tip'), value); + } + + /** + * @description:设置Tip提示内容及位置 + * @param: curTipEle 要设置tip的元素 + * @param: value 设置tip内容对应的value值 + */ + private setTip(curTipEle: any, value: number): void { + // 设置tip内容 + const tipContent: string = this.tipFormatterFn(value); + curTipEle.innerHTML = this.domSanitizer.sanitize(SecurityContext.HTML, tipContent); + + // 设置当前tip位置 + const tipDisplay: string = getComputedStyle(curTipEle).display; + this.show(curTipEle); // 先把tip显示出来 才能获取宽度 + const tipContentWidth: number = curTipEle.getBoundingClientRect().width; + const styles: { left: string; display: string } = { + left: `${-(tipContentWidth - this.pointerMaxEleWidth) / 2 - 1}px`, // 定位受border的影响,需要减去border的宽度 1px + display: tipDisplay // 设置回原来的display + }; + this.tiRenderer.setStyles(curTipEle, styles); + // tip重合情况处理 (双滑块并且tip是一直显示出来那种场景) + if (this.isDouble && !this.isTipAutoShow) { + /** + * @description: 渲染当前的tip样式 (移除一个class并且添加一个class) + * @param: tipEle 当前tipDOM + * @param: removeClass 要移除的class名字 + * @param: addClass 要添加的class名字 + */ + const renderTipStyle: (tipEle: any, removeClass: string, addClass: string) => void = ( + tipEle: any, + removeClass: string, + addClass: string + ): void => { + this.renderer2.removeClass(tipEle, removeClass); + this.renderer2.addClass(tipEle, addClass); + }; + // 重置tip样式,防止之前样式设置造成的影响 ti3-slider-tip-top + renderTipStyle(this.tipMaxEle, 'ti3-slider-tip-bottom', 'ti3-slider-tip-top'); + renderTipStyle(this.tipMinEle, 'ti3-slider-tip-bottom', 'ti3-slider-tip-top'); + this.show(this.tipMaxEle); + this.show(this.tipMinEle); + + // 最大最小值重合情况下,只显示当前tip + const selectionWidth: number = this.selectionEle.getBoundingClientRect().width; + if (selectionWidth === 0) { + if (this.tiRenderer.hasClass(curTipEle, 'ti3-slider-tip-max')) { + // 当前tip是最大值的 隐藏最小值tip + this.hide(this.tipMinEle); + } else { + // 当前tip是最小值的 隐藏最大值tip + this.hide(this.tipMaxEle); + } + + return; + } + // tip重合情况下,当前tip向下显示 + const tipMaxWidth: number = this.tipMaxEle.getBoundingClientRect().width; + const tipMinWidth: number = this.tipMinEle.getBoundingClientRect().width; + if ((tipMaxWidth + tipMinWidth) / 2 >= selectionWidth) { + renderTipStyle(curTipEle, 'ti3-slider-tip-top', 'ti3-slider-tip-bottom'); + } + } + } +} diff --git a/src/slider/lib/src/TiSliderModule.ts b/src/slider/lib/src/TiSliderModule.ts new file mode 100644 index 0000000..0368fff --- /dev/null +++ b/src/slider/lib/src/TiSliderModule.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiDraggableModule } from '@opentiny/ng-drag'; +import { TiSliderComponent } from './TiSliderComponent'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiIconModule } from '@opentiny/ng-icon'; + +@NgModule({ + imports: [CommonModule, TiDraggableModule, TiIconModule, TiRendererModule], + exports: [TiSliderComponent], + declarations: [TiSliderComponent] +}) +export class TiSliderModule {} +export { TiSliderComponent } from './TiSliderComponent'; diff --git a/src/slider/lib/src/slider.html b/src/slider/lib/src/slider.html new file mode 100644 index 0000000..931611f --- /dev/null +++ b/src/slider/lib/src/slider.html @@ -0,0 +1,43 @@ +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
      +
    • + +
      +
      + +
      + + + + +
      +
    • +
    +
    diff --git a/src/slider/lib/src/slider.less b/src/slider/lib/src/slider.less new file mode 100644 index 0000000..46b2120 --- /dev/null +++ b/src/slider/lib/src/slider.less @@ -0,0 +1,179 @@ +@import "../../../themes/basic/base-all.less"; + +:host { + --ti-slider-track-height: var(--ti-common-size-2x); + --ti-slider-pointer-width: var(--ti-common-size-5x); + --ti-slider-pointer-height: var(--ti-common-size-7x); + --ti-slider-bar-selection-height: var(--ti-common-size-4x); + --ti-slider-tip-triangle-width: 10px; + --ti-slider-tip-triangle-height: 6px; + --ti-slider-border-radius: var(--ti-common-border-radius-normal); + --ti-slider-tick-top: var(--ti-slider-track-height); + --ti-slider-tip-vertical-space: calc(var(--ti-slider-tip-triangle-height) + 5px); + --ti-slider-tick-margin-left: calc(var(--ti-slider-pointer-width)/2); +} + +:host.ti3-slider-container{ + display: block; + margin: var(--ti-common-space-9x) var(--ti-common-space-0); + cursor: pointer; + .clearfix(); + + & .ti3-slider-track-container { + width: 100%; + height: var(--ti-slider-track-height); + position: relative; + background-color: var(--ti-common-color-bg-light-normal); + border-radius: var(--ti-slider-border-radius); + & .ti3-slider-track-content { + width: calc(100% - var(--ti-slider-pointer-width)); + height: var(--ti-slider-track-height); + position: relative; + & .ti3-slider-bar-selection { + height: var(--ti-slider-bar-selection-height); + background-color: var(--ti-common-color-bg-emphasize); + position: absolute; + z-index: 1; + top: calc((var(--ti-slider-track-height) - var(--ti-slider-bar-selection-height)) / 2); + border-radius: var(--ti-slider-border-radius) 0 0 var(--ti-slider-border-radius); + } + & .ti3-slider-pointer-min, + & .ti3-slider-pointer-max { + position: absolute; + width: var(--ti-slider-pointer-width); + height: var(--ti-slider-pointer-height); + line-height: calc(var(--ti-slider-pointer-height) - 2px); + background-color: var(--ti-common-color-bg-white-normal); + top: calc(-1 * (var(--ti-slider-pointer-height) - var(--ti-slider-track-height))/2); + z-index: 1; + .box-sizing(border-box); + border-radius: calc(var(--ti-slider-border-radius) * 5); + &.ti3-icon-slider-point:hover { + color: var(--ti-common-color-bg-hover); + border-color: var(--ti-common-color-bg-hover); + } + &.ti3-icon-slider-point { + position: absolute; + color: var(--ti-common-color-icon-active); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-active); + border-radius: calc(var(--ti-slider-border-radius) * 5); + text-align: center; + .box-shadow(var(--ti-common-shadow-1-down)); + background: linear-gradient(153deg, var(--ti-common-color-bg-white-normal), var(--ti-common-color-bg-light-normal) 99%); + } + & .ti3-slider-tip{ + border-radius: var(--ti-common-border-radius-1); + position: absolute; + background-color: var(--ti-common-color-bg-dark-deep); + color: var(--ti-common-color-text-gray); + padding: var(--ti-common-space-3x) var(--ti-common-space-4x); + word-wrap: break-word; + white-space: pre; + line-height: var(--ti-common-line-height-number); + cursor: default; + font-size: var(--ti-common-font-size-base); + font-family: var(--ti-common-color-text-primary); // 复写字体原因:此处继承父级字体ti3Font + .box-shadow(var(--ti-common-shadow-3-down)); + &.ti3-slider-tip-top{ + bottom: calc(100% + var(--ti-slider-tip-vertical-space)); + &:before{ + .triangle-down(var(--ti-slider-tip-triangle-width); var(--ti-slider-tip-triangle-height); var(--ti-common-color-bg-dark-deep)); + content: " "; + position: absolute; + pointer-events: none; + margin-left: calc(-1 * var(--ti-slider-tip-triangle-width)/2); + z-index: 3; + top: 100%; + left: 50%; + } + } + &.ti3-slider-tip-bottom{ + top: calc(100% + var(--ti-slider-tip-vertical-space)); + &:before{ + .triangle-up(var(--ti-slider-tip-triangle-width); var(--ti-slider-tip-triangle-height); var(--ti-common-color-bg-dark-deep)); + content: " "; + position: absolute; + pointer-events: none; + margin-left: calc(-1 * var(--ti-slider-tip-triangle-width)/2); + z-index: 3; + bottom: 100%; + left: 50%; + } + } + } + } + & .ti3-slider-pointer-min { + display: none; + } + } + + } + + & .ti3-slider-ticks { + position: relative; + width: calc(100% - var(--ti-slider-pointer-width)); + & .ti3-slider-tick { + position: absolute; + height: var(--ti-slider-track-height); + margin-left: var(--ti-slider-tick-margin-left); + top: calc(-1* var(--ti-slider-tick-top)); + & .ti3-slider-tick-dot { + width: 2px; + height: var(--ti-slider-track-height); + background-color: var(--ti-common-color-bg-white-normal); + position: relative; + &.ti3-slider-selection-tick-dot { + background-color: var(--ti-common-color-bg-white-normal); + } + } + &:first-child, &:last-child{ + .ti3-slider-tick-dot { + width: 0; + } + } + &:first-child { + .ti3-slider-tick-value{ + left: calc(-1 * var(--ti-slider-tick-margin-left)); + } + } + &:last-child { + .ti3-slider-tick-value { + white-space: nowrap; + left: calc(-100% + var(--ti-slider-tick-margin-left)); + } + } + & .ti3-slider-tick-value { + position: relative; + top: var(--ti-slider-tick-top); + left: -50%; + color: var(--ti-common-color-text-secondary); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + } + } + } + + &.ti3-slider-disable{ + &.ti3-slider-container { + cursor: not-allowed; + & .ti3-slider-track-container { + background-color: var(--ti-common-color-bg-disabled); + .ti3-slider-bar-selection{ + background-color: var(--ti-common-color-text-disabled); + } + .ti3-slider-pointer-min, .ti3-slider-pointer-max { + &.ti3-icon-slider-point { + color: var(--ti-common-color-icon-disabled); + border-color: var(--ti-common-color-line-disabled); + .box-shadow(none); + background: var(--ti-common-color-bg-disabled); + } + &.ti3-icon-slider-point:hover { + color: var(--ti-common-color-icon-disabled); + border-color: var(--ti-common-color-line-disabled); + } + } + } + } + } +} diff --git a/src/spinner/demo/karma.conf.js b/src/spinner/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/spinner/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/spinner/demo/project.json b/src/spinner/demo/project.json new file mode 100644 index 0000000..5479491 --- /dev/null +++ b/src/spinner/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/spinner/demo", + "sourceRoot": "src/spinner/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/spinner", + "index": "src/spinner/demo/src/index.html", + "main": "src/spinner/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/spinner/demo/tsconfig.app.json", + "assets": ["src/spinner/demo/src/favicon.ico", "src/spinner/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "spinner-demo:build:production" + }, + "development": { + "browserTarget": "spinner-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js spinner" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/spinner/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/spinner/demo/tsconfig.spec.json", + "karmaConfig": "src/spinner/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/spinner/demo/src/app/AppComponent.ts b/src/spinner/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/spinner/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/spinner/demo/src/app/AppModule.ts b/src/spinner/demo/src/app/AppModule.ts new file mode 100644 index 0000000..4f45e4f --- /dev/null +++ b/src/spinner/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SpinnerTestModule } from './spinner/SpinnerTestModule'; + +@NgModule({ + imports: [ + SpinnerTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/spinner/demo/src/app/IndexComponent.ts b/src/spinner/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..00d230d --- /dev/null +++ b/src/spinner/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SpinnerTestModule } from './spinner/SpinnerTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SpinnerTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/spinner/demo/src/app/app.html b/src/spinner/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/spinner/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/spinner/demo/src/app/spinner/SpinnerBasicComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerBasicComponent.ts new file mode 100644 index 0000000..7575e5b --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerBasicComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-basic.html' +}) +export class SpinnerBasicComponent { + max: number = 2000; + min: number = -20; + spinnerValue: number = undefined; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerBasicTestComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerBasicTestComponent.ts new file mode 100644 index 0000000..9a9ecc5 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerBasicTestComponent.ts @@ -0,0 +1,119 @@ +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms'; +export interface SpinnerModel { + min?: number; + max?: number; + spinnerValue?: any; + spinnerValue1?: any; + spinnerValue2?: any; + spinnerValue3?: any; + step?: number; + format?: string; + disable?: any; + placeholder?: any; + blur?: any; + change?: any; +} +@Component({ + templateUrl: './spinner-basic-test.html', + styleUrls: ['./spinnerTest.less'] +}) +export class SpinnerBasicTestComponent implements OnInit { + max: number = 10000; + min: number = -400; + spinnerModel: SpinnerModel = { + max: this.max, + min: this.min, + spinnerValue: 8, + spinnerValue1: '', + spinnerValue2: 10, + spinnerValue3: 10, + step: 1, + format: 'N2', + disable: false, + placeholder: `请输入${this.min}到${this.max}的数值`, + change: (value: number): void => { + console.log('change evt:' + value); + } + }; + @ViewChild('form', { static: true }) form: any; + @ViewChild('spinner1', { static: true }) spinner1: any; + ngForm: any; + spinnerValue: number = 11; + format: string = 'N4'; + spinnerForm: FormGroup; + constructor(private fb: FormBuilder) {} + ngOnInit(): void { + const baseValue: number = 1200; + this.spinnerForm = this.fb.group({ + spinnerValue: baseValue, // 设置初始值 + text1Value: 14, // 设置初始值 + text2Value: baseValue // 设置初始值 + }); + // 订阅响应式表单的值的改变和状态改变 + this.reactiveFormSpinnerValueChange(); + this.reactiveFormValueChanges(); + this.reactiveFormStatusChanges(); + } + // ngModel形式(模板驱动表单)对value的监控 + changeFn(value: number): void { + console.log('change evt:' + value); + } + // 响应式表单 对value的监控 + reactiveFormSpinnerValueChange(): void { + const spinnerValueControl: AbstractControl = this.spinnerForm.get('spinnerValue'); + + spinnerValueControl.valueChanges.subscribe((value: number) => { + console.log(value); + this.spinnerForm.patchValue({ + text2Value: value + }); + }); + } + // 整个响应式表单监控 + reactiveFormValueChanges(): void { + this.spinnerForm.valueChanges.subscribe((data: number) => { + console.log(data); + }); + } + reactiveFormStatusChanges(): void { + this.spinnerForm.statusChanges.subscribe((states: any) => { + console.log(states); + }); + } + focusFn($event: any): void { + console.log('focusEvent'); + } + blurFn($event: any): void { + console.log('blurEvent'); + } + changeValue(): void { + this.spinnerModel.spinnerValue = -401; + console.log(this.spinnerModel.spinnerValue); + } + changeReactiveValue(): void { + this.spinnerForm.patchValue({ + spinnerValue: 1300 // 设置初始值 + }); + } + changeMax(): void { + this.spinnerModel.max = 0; + } + changeMin(): void { + this.spinnerModel.min = 20; + } + changeDisable(): void { + this.spinnerModel.disable = !this.spinnerModel.disable; + } + changePlaceholder(): void { + this.spinnerModel.placeholder = 'Please input the number from -100 to 100.'; + } + focusSpinner1(): void { + const spinner1: any = this.spinner1; + console.log(spinner1); + spinner1.focus(); + setTimeout(() => { + spinner1.blur(); + }, 1000); + } +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerCorrectableComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerCorrectableComponent.ts new file mode 100644 index 0000000..e32683f --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerCorrectableComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-correctable.html' + // styleUrls: ['./spinnerTest.less'] +}) +export class SpinnerCorrectableComponent { + max: number = 200; + min: number = -20; + spinnerValue: number = 23837.4545; + format: string = 'N2'; + correctable: boolean = true; + + changeCorrect(): void { + this.spinnerValue = 23837.4545; + this.correctable = false; + } +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerDisabledComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerDisabledComponent.ts new file mode 100644 index 0000000..1d0251e --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerDisabledComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-disabled.html' +}) +export class SpinnerDisabledComponent { + spinnerValue: number = 1500.3624; + max: number = 2000; + min: number = -20; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerEventComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerEventComponent.ts new file mode 100644 index 0000000..bf5f82e --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerEventComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-event.html' +}) +export class SpinnerEventComponent { + myLogs: Array = []; + max: number = 2000; + min: number = -20; + spinnerValue: number = 1500.3624; + + onModelChange(value: any): void { + this.myLogs = [...this.myLogs, `onModelChange value=${value} type=${typeof value}`]; + } +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerFormatComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerFormatComponent.ts new file mode 100644 index 0000000..050e7ae --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerFormatComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-format.html' +}) +export class SpinnerFormatComponent { + max: number = 2000; + min: number = -20; + spinnerValue: number = 1500.3624; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerIdComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerIdComponent.ts new file mode 100644 index 0000000..9be7f85 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerIdComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + template: ` ` +}) +export class SpinnerIdComponent { + elementId: string = 'spinner'; + pinnerValue: number = 12; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerLoadComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerLoadComponent.ts new file mode 100644 index 0000000..e716f17 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerLoadComponent.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-load.html' +}) +export class SpinnerLoadComponent { + max: number = 10000; + min: number = -400; + spinnerValue: any; + max1: number = 100; + min1: number = 0; + spinnerValue1: number = 40; + max2: number = 100; + min2: number = 0; + spinnerValue2: number = 40; + myLogs: Array = []; + changeLegal(): void { + this.spinnerValue = 1234.66666; + } + changeIllegal(): void { + this.spinnerValue = 'werty3452'; + } + changeMax(): void { + this.spinnerValue1 = 120; + } + changeMin(): void { + this.spinnerValue1 = -100; + } + changeMiddle(): void { + this.spinnerValue1 = 50; + } + onModelChange(value: any): void { + this.myLogs = [...this.myLogs, `onModelChange value=${value}`]; + } + + onModelChange1(value: any): void { + this.myLogs = [...this.myLogs, `onModelChange1 value=${value}`]; + } +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerLocaleableComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerLocaleableComponent.ts new file mode 100644 index 0000000..5f1100d --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerLocaleableComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-localeable.html' +}) +export class SpinnerLocaleableComponent { + max: number = 2000; + min: number = -20; + spinnerValue: number = 1500.3624; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerMaxMinComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerMaxMinComponent.ts new file mode 100644 index 0000000..1c3e501 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerMaxMinComponent.ts @@ -0,0 +1,38 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-max-min.html', + styleUrls: ['./spinnerTest.less'] +}) +export class SpinnerMaxMinComponent { + max: any = 20; + min: number = 0; + spinnerValue: number = 30; + + max1: any = 20; + min1: number = 0; + spinnerValue1: number = 30; + + max2: any = 20; + min2: any = 10; + spinnerValue2: number = 8; + + changeMax(): void { + this.max = 40; + } + changeInvalid(): void { + this.max = 'werq'; + } + changeMax1(): void { + this.max1 = 15; + } + changeInvalid1(): void { + this.max1 = 'wewe'; + } + changeMin2(): void { + this.min2 = 12; + } + changeInvalid2(): void { + this.min2 = 'werq'; + } +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerMaxlengthComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerMaxlengthComponent.ts new file mode 100644 index 0000000..d5e786e --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerMaxlengthComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-maxlength.html' +}) +export class SpinnerMaxlengthComponent { + max: number = 2000; + min: number = -20; + spinnerValue: number = 1500.3624; + maxlength: number = 10; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerStepComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerStepComponent.ts new file mode 100644 index 0000000..69ae5e5 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerStepComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-step.html', + styleUrls: ['./spinnerTest.less'] +}) +export class SpinnerStepComponent { + max: number = 2000; + min: number = -20; + spinnerValue: number = 1500.3624; + step: number = 2; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerStepfnComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerStepfnComponent.ts new file mode 100644 index 0000000..a3e7e5e --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerStepfnComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-stepfn.html' +}) +export class SpinnerStepfnComponent { + myLogs: Array = []; + max: number = 2000; + min: number = -20; + spinnerValue: number = 1500.3624; + + stepFn: (value: number, isAdd: boolean) => number = (value: number, isAdd: boolean) => { + let step: number = 1; + if (value < 10) { + step = isAdd ? 2 : 1; + } else if (value >= 10 && value <= 100) { + step = isAdd ? 10 : 5; + } else { + step = 100; + } + + this.myLogs = [...this.myLogs, `stepFn isAdd=${isAdd} value=${value} step=${step}`]; + + return step; + }; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerTestModule.ts b/src/spinner/demo/src/app/spinner/SpinnerTestModule.ts new file mode 100644 index 0000000..f63d61c --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerTestModule.ts @@ -0,0 +1,118 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiButtonModule, TiSpinnerModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { SpinnerBasicTestComponent } from './SpinnerBasicTestComponent'; +import { SpinnerIdComponent } from './SpinnerIdComponent'; +import { SpinnerValidationTestComponent } from './SpinnerValidationTestComponent'; +import { SpinnerBasicComponent } from './SpinnerBasicComponent'; +import { SpinnerFormatComponent } from './SpinnerFormatComponent'; +import { SpinnerStepComponent } from './SpinnerStepComponent'; +import { SpinnerValidationComponent } from './SpinnerValidationComponent'; +import { SpinnerCorrectableComponent } from './SpinnerCorrectableComponent'; +import { SpinnerLocaleableComponent } from './SpinnerLocaleableComponent'; +import { SpinnerMaxlengthComponent } from './SpinnerMaxlengthComponent'; +import { SpinnerEventComponent } from './SpinnerEventComponent'; +import { SpinnerLoadComponent } from './SpinnerLoadComponent'; +import { SpinnerMaxMinComponent } from './SpinnerMaxMinComponent'; +import { SpinnerDisabledComponent } from './SpinnerDisabledComponent'; +import { SpinnerStepfnComponent } from './SpinnerStepfnComponent'; +import { SpinnerTipComponent } from './SpinnerTipComponent'; +import { SpinnerTipTestComponent } from './SpinnerTipTestComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiValidationModule, + TiButtonModule, + TiSpinnerModule, + DemoLogModule, + RouterModule.forChild(SpinnerTestModule.ROUTES) + ], + declarations: [ + SpinnerBasicTestComponent, + SpinnerIdComponent, + SpinnerBasicComponent, + SpinnerStepComponent, + SpinnerValidationComponent, + SpinnerFormatComponent, + SpinnerValidationTestComponent, + SpinnerCorrectableComponent, + SpinnerLocaleableComponent, + SpinnerMaxlengthComponent, + SpinnerEventComponent, + SpinnerLoadComponent, + SpinnerMaxMinComponent, + SpinnerDisabledComponent, + SpinnerStepfnComponent, + SpinnerTipComponent, + SpinnerTipTestComponent + ] +}) +export class SpinnerTestModule { + static readonly LINKS: Array = [{ href: 'components/TiSpinnerComponent.html', label: 'Spinner' }]; + static readonly ROUTES: Routes = [ + { + path: 'spinner/spinner-basic', + component: SpinnerBasicComponent + }, + { + path: 'spinner/spinner-format', + component: SpinnerFormatComponent + }, + { + path: 'spinner/spinner-step', + component: SpinnerStepComponent + }, + { + path: 'spinner/spinner-validation', + component: SpinnerValidationComponent + }, + { + path: 'spinner/spinner-basic-test', + component: SpinnerBasicTestComponent + }, + { path: 'spinner/id', component: SpinnerIdComponent }, + { + path: 'spinner/spinner-validation-test', + component: SpinnerValidationTestComponent + }, + { + path: 'spinner/spinner-correctable', + component: SpinnerCorrectableComponent + }, + { + path: 'spinner/spinner-localeable', + component: SpinnerLocaleableComponent + }, + { path: 'spinner/spinner-maxlength', component: SpinnerMaxlengthComponent }, + { + path: 'spinner/spinner-disabled', + component: SpinnerDisabledComponent + }, + { + path: 'spinner/spinner-event', + component: SpinnerEventComponent + }, + { + path: 'spinner/spinner-stepfn', + component: SpinnerStepfnComponent + }, + { + path: 'spinner/spinner-tip', + component: SpinnerTipComponent + }, + { + path: 'spinner/spinner-tip-test', + component: SpinnerTipTestComponent + }, + { path: 'spinner/spinner-load', component: SpinnerLoadComponent }, + { path: 'spinner/spinner-max-min', component: SpinnerMaxMinComponent } + ]; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerTipComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerTipComponent.ts new file mode 100644 index 0000000..6a77e73 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerTipComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-tip.html' +}) +export class SpinnerTipComponent { + max: number = 2000; + min: number = -20; + spinnerValue: number = 1500.3624; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerTipTestComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerTipTestComponent.ts new file mode 100644 index 0000000..d6a3d1e --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerTipTestComponent.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-tip-test.html' +}) +export class SpinnerTipTestComponent { + max: number = 10000; + min: number = -400; + spinnerValue: number; + spinnerValue1: number = 180; + spinnerValue2: number = 900; + spinnerValue3: number = 900; + spinnerValue4: number = 900; + spinnerValue5: number = 800; + + changeRange(): void { + this.max = 200; + this.min = 100; + } + + changeMaxValue(): void { + this.max = 800; + } + + changeMinValue(): void { + this.min = 200; + } +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerValidationComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerValidationComponent.ts new file mode 100644 index 0000000..95c1081 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerValidationComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-validation.html' +}) +export class SpinnerValidationComponent { + spinnerValue: number = undefined; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerValidationTestComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerValidationTestComponent.ts new file mode 100644 index 0000000..cba8ee6 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerValidationTestComponent.ts @@ -0,0 +1,32 @@ +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiValidationConfig, TiValidators } from '@opentiny/ng'; +@Component({ + templateUrl: `./spinner-validation-test.html`, + styleUrls: ['./spinnerTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class SpinnerValidationTestComponent { + elementId: string = 'spinner'; + form: FormGroup; + spinnerValue: number = undefined; + placeholder: string = '-90~90'; + placeholder1: string = '-10~100'; + validationObj: TiValidationConfig = { + type: 'blur' + }; + validationObj1: TiValidationConfig = { + type: 'change' + }; + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + mySpinner: new FormControl(1, [TiValidators.required, TiValidators.rangeValue(-10, 100)]) + }); + } + onBlur(): void { + console.log('on blur'); + } + onFocus(): void { + console.log('on focus'); + } +} diff --git a/src/spinner/demo/src/app/spinner/spinner-basic-test.html b/src/spinner/demo/src/app/spinner/spinner-basic-test.html new file mode 100644 index 0000000..03be7fa --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-basic-test.html @@ -0,0 +1,129 @@ +

    Spinner组件默认样式展示

    +
    +
    +

    用在表单中(第一个form)--模板驱动表单

    +
    + + + +
    +

    form's touched: {{form.touched | json}}

    +

    form's untouched: {{form.untouched | json}}

    +
    {{form.value | json}}
    +

    用在表单外

    + + +
    +
    +

    用在表单中(第二个form)--响应式表单

    +
    + + + + +
    + +
    +
    + + + + + + + +
    +
    +
    + +

    表格单元格设置white-space: nowrap;时,spinner布局问题,方案:spinner外框设置white-space: normal;

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    1234
    + + + 456dfdfdfdfdf
    dfdfdhgderjhedtkirkl8756856
    6856895695685632fgjfrkjwtyp;yul
    dujrtikdfgdfhjdfkjfrtkidjft
    +
    +
    diff --git a/src/spinner/demo/src/app/spinner/spinner-basic.html b/src/spinner/demo/src/app/spinner/spinner-basic.html new file mode 100644 index 0000000..8a9a1f6 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-basic.html @@ -0,0 +1 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinner-correctable.html b/src/spinner/demo/src/app/spinner/spinner-correctable.html new file mode 100644 index 0000000..ac3e893 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-correctable.html @@ -0,0 +1,17 @@ +

    描述

    +

    自10.0.1版本起去掉屏蔽该接口,不想强转时不要设置最大最小值就行。

    +

    失去焦点,是否强制转换。默认值为true,支持强制转换。当配置为false时,不支持强制转换!

    +

    示例

    + +

    + diff --git a/src/spinner/demo/src/app/spinner/spinner-disabled.html b/src/spinner/demo/src/app/spinner/spinner-disabled.html new file mode 100644 index 0000000..82e6aa4 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-disabled.html @@ -0,0 +1 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinner-event.html b/src/spinner/demo/src/app/spinner/spinner-event.html new file mode 100644 index 0000000..f328336 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-event.html @@ -0,0 +1,9 @@ + + diff --git a/src/spinner/demo/src/app/spinner/spinner-format.html b/src/spinner/demo/src/app/spinner/spinner-format.html new file mode 100644 index 0000000..c55efed --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-format.html @@ -0,0 +1 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinner-load.html b/src/spinner/demo/src/app/spinner/spinner-load.html new file mode 100644 index 0000000..eb73e0a --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-load.html @@ -0,0 +1,21 @@ +

    描述

    +

    spinner组件,数据加载

    +

    示例

    +

    1. 典型应用场景:非法数据-->数据A--->数据B

    + +

    +     +   

    +

    2. 典型应用场景:数据-->大于最大值--->小于最小值

    + +

    +         +    +

    值改变日志:

    + diff --git a/src/spinner/demo/src/app/spinner/spinner-localeable.html b/src/spinner/demo/src/app/spinner/spinner-localeable.html new file mode 100644 index 0000000..a082e41 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-localeable.html @@ -0,0 +1,8 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinner-max-min.html b/src/spinner/demo/src/app/spinner/spinner-max-min.html new file mode 100644 index 0000000..23c2abc --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-max-min.html @@ -0,0 +1,18 @@ +

    描述

    +

    最大值、最小值设置

    +

    示例

    +

    1.不设置最大最小值时,默认最大值是 2^53, 最小值是 -2^53,当前值为30

    + +

    + +

    2.修改最大值。最小最大值[0,20] 当前值为30

    + +

    +    +   

    + +

    3.修改最小值。最小最大值[10,20] 当前值为8

    + +

    +    +    diff --git a/src/spinner/demo/src/app/spinner/spinner-maxlength.html b/src/spinner/demo/src/app/spinner/spinner-maxlength.html new file mode 100644 index 0000000..e43df09 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-maxlength.html @@ -0,0 +1,8 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinner-step.html b/src/spinner/demo/src/app/spinner/spinner-step.html new file mode 100644 index 0000000..fad6e70 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-step.html @@ -0,0 +1 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinner-stepfn.html b/src/spinner/demo/src/app/spinner/spinner-stepfn.html new file mode 100644 index 0000000..967340c --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-stepfn.html @@ -0,0 +1,2 @@ + + diff --git a/src/spinner/demo/src/app/spinner/spinner-tip-test.html b/src/spinner/demo/src/app/spinner/spinner-tip-test.html new file mode 100644 index 0000000..4444e57 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-tip-test.html @@ -0,0 +1,32 @@ +

    描述

    +

    设置最大最小值时,鼠标移入时显示输入范围提示

    +

    示例

    +

    1.不设置最大最小值

    + +

    +

    2.设置最大最小值默认配置

    + +

    +

    3.只设置最小值

    + +

    +

    4.只设置最大值

    + +

    +

    5.设置最大最小值自定义提示内容

    + +

    +
    6.设置最大最小值时,若不想显示默认提示文本,可自定义提示内容为空字符串''
    + +

    + + + diff --git a/src/spinner/demo/src/app/spinner/spinner-tip.html b/src/spinner/demo/src/app/spinner/spinner-tip.html new file mode 100644 index 0000000..5278d0d --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-tip.html @@ -0,0 +1,9 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinner-validation-test.html b/src/spinner/demo/src/app/spinner/spinner-validation-test.html new file mode 100644 index 0000000..93f8701 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-validation-test.html @@ -0,0 +1,18 @@ +

    Spinner组件 校验展示

    +
    +
    + + +
    +
    +
    + +
    diff --git a/src/spinner/demo/src/app/spinner/spinner-validation.html b/src/spinner/demo/src/app/spinner/spinner-validation.html new file mode 100644 index 0000000..44a5c79 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-validation.html @@ -0,0 +1,9 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinnerTest.less b/src/spinner/demo/src/app/spinner/spinnerTest.less new file mode 100644 index 0000000..f06233c --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinnerTest.less @@ -0,0 +1,22 @@ +.container { + padding-left: 10px; + padding-top: 10px; + a { + text-decoration: none; + background-color: #8bc34a; + color: white; + padding: 5px 10px; + display: inline-block; + border: 1px solid #00bcd4; + margin-right: 10px; + &:last-child { + margin-right: 0; + } + &:hover { + background-color: #ff9800; + } + } + .wrapper { + margin-top: 10px; + } +} diff --git a/src/spinner/demo/src/app/spinner/webdoc/spinner-demos.js b/src/spinner/demo/src/app/spinner/webdoc/spinner-demos.js new file mode 100644 index 0000000..26e0d07 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/webdoc/spinner-demos.js @@ -0,0 +1,137 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'spinner-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Spinner 组件的最简用法。

    ', + 'en-US': '', + }, + apis: [ + 'TiSpinnerComponent.properties.min', + 'TiSpinnerComponent.properties.max', + 'TiSpinnerComponent.properties.placeholder', + ], + }, + { + demoId: 'spinner-format', + name: { + 'zh-CN': '数字精度', + 'en-US': 'format', + }, + desc: { + 'zh-CN': + '

    通过属性format配置小数点位数,使用“n(N)+数字”方式设置,数字代表保留几位小数。

    ', + 'en-US': '', + }, + apis: ['TiSpinnerComponent.properties.format'], + }, + { + demoId: 'spinner-localeable', + name: { + 'zh-CN': '国际化', + 'en-US': 'localeable', + }, + desc: { + 'zh-CN': '

    通过属性localeable配置是否开启国际化。

    ', + 'en-US': '', + }, + apis: ['TiSpinnerComponent.properties.localeable'], + }, + { + demoId: 'spinner-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': '', + }, + apis: ['TiSpinnerComponent.properties.disabled'], + }, + { + demoId: 'spinner-step', + name: { + 'zh-CN': '步长', + 'en-US': 'step', + }, + desc: { + 'zh-CN': '

    通过属性step配置微调器的步长。

    ', + 'en-US': '', + }, + apis: ['TiSpinnerComponent.properties.step'], + }, + { + demoId: 'spinner-stepfn', + name: { + 'zh-CN': '动态步长', + 'en-US': 'stepfn', + }, + desc: { + 'zh-CN': + '

    通过属性stepFn动态配置步长。传递出来的参数为当前值、当前是否点击了加号按钮。

    ', + 'en-US': '', + }, + apis: ['TiSpinnerComponent.properties.stepFn'], + }, + { + demoId: 'spinner-maxlength', + name: { + 'zh-CN': '允许的最大字符数', + 'en-US': 'maxlength', + }, + desc: { + 'zh-CN': + '

    通过属性maxlength配置输入框中允许的最大字符数。

    ', + 'en-US': '', + }, + apis: ['TiSpinnerComponent.properties.maxlength'], + }, + { + demoId: 'spinner-tip', + name: { + 'zh-CN': 'tip提示', + 'en-US': 'tip', + }, + desc: { + 'zh-CN': + '

    通过属性tipContent配置提示内容。通过属性tipPosition配置提示方向。

    ', + 'en-US': '', + }, + apis: [ + 'TiSpinnerComponent.properties.tipContent', + 'TiSpinnerComponent.properties.tipPosition', + ], + }, + { + demoId: 'spinner-validation', + name: { + 'zh-CN': '校验', + 'en-US': 'validation', + }, + desc: { + 'zh-CN': + '

    通过指令tiValidation实现校验。如果要对最大最小值进行校验就不要给 ti-spinner 设置minmax

    ', + 'en-US': '', + }, + }, + { + demoId: 'spinner-event', + name: { + 'zh-CN': '事件', + 'en-US': 'events', + }, + desc: { + 'zh-CN': + '

    当元素内容发生变化的时候触发ngModelChange事件。

    ', + 'en-US': 'spinner event description', + }, + }, + ], +}; diff --git a/src/spinner/demo/src/app/spinner/webdoc/spinner.cn.md b/src/spinner/demo/src/app/spinner/webdoc/spinner.cn.md new file mode 100644 index 0000000..1831536 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/webdoc/spinner.cn.md @@ -0,0 +1,36 @@ +--- +title: Spinner 数字微调 +--- +# Spinner 数字微调 + +
    + +Spinner 是数字微调组件。   + +```typescript +import { TiSpinnerModule } from '@opentiny/ng'; +``` + +如需使用校验功能,请导入。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
    + +
    + +Spinner 是数字微调组件。   + +```typescript +import { TiSpinnerModule } from '@opentiny/ng'; +``` + +如需使用校验功能,请导入。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/spinner/demo/src/app/spinner/webdoc/spinner.en.md b/src/spinner/demo/src/app/spinner/webdoc/spinner.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/webdoc/spinner.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/spinner/demo/src/favicon.ico b/src/spinner/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/spinner/demo/src/index.html b/src/spinner/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/spinner/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/spinner/demo/src/main.ts b/src/spinner/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/spinner/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/spinner/demo/test.ts b/src/spinner/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/spinner/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/spinner/demo/tsconfig.app.json b/src/spinner/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/spinner/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/spinner/demo/tsconfig.spec.json b/src/spinner/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/spinner/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/spinner/lib/index.ts b/src/spinner/lib/index.ts new file mode 100644 index 0000000..4f33553 --- /dev/null +++ b/src/spinner/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSpinnerModule'; diff --git a/src/spinner/lib/ng-package.json b/src/spinner/lib/ng-package.json new file mode 100644 index 0000000..5ce85f5 --- /dev/null +++ b/src/spinner/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/spinner", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/spinner/lib/package.json b/src/spinner/lib/package.json new file mode 100644 index 0000000..e35a8a1 --- /dev/null +++ b/src/spinner/lib/package.json @@ -0,0 +1,17 @@ +{ + "name": "@opentiny/ng-spinner", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-inputnumber": "~1.0.0-beta.0", + "@angular/forms": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-tip": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/spinner/lib/project.json b/src/spinner/lib/project.json new file mode 100644 index 0000000..f49d9c2 --- /dev/null +++ b/src/spinner/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/spinner/lib", + "sourceRoot": "src/spinner/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/spinner"], + "options": { + "project": "src/spinner/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/spinner"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js spinner" + }, + { + "command": "ng default-build spinner" + }, + { + "command": "node build/clear-default-theme.js spinner" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/spinner && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build spinner && ng pack spinner && node build/publish.js spinner --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/spinner/lib/src/TiSpinnerComponent.ts b/src/spinner/lib/src/TiSpinnerComponent.ts new file mode 100644 index 0000000..6db4f00 --- /dev/null +++ b/src/spinner/lib/src/TiSpinnerComponent.ts @@ -0,0 +1,484 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Injector, + Input, + Output, + Renderer2, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { TiLocale, TiLocaleFormat } from '@opentiny/ng-locale'; +import { TiKeymap, TiPositionType, Util } from '@opentiny/ng-utils'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +import { NgControl, NgModel } from '@angular/forms'; +/** + * 数字微调组件 + * + */ +@Component({ + selector: 'ti-spinner', + templateUrl: './spinner.html', + styleUrls: ['./spinner.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiSpinnerComponent)] +}) +export class TiSpinnerComponent extends TiFormComponent { + /** + * @ignore + */ + public method: any = { + METHOD_ADD: 'add', + METHOD_SUB: 'sub' + }; + /** + * 小数保留位数。使用 n + '数字' 形式,例如:'n4',代表保留4位小数。 + * + * 不设置时,10.0.1 版本后小数保留位数最少 0 位,最多 3 位;10.0.0 版本前小数保留位数为 3 位 + */ + @Input() format: string; + /** + * 最大值,支持整数和小数 + */ + @Input() max: number = Math.pow(2, 53); + /** + * 最小值,支持整数和小数 + */ + @Input() min: number = -Math.pow(2, 53); + /** + * 步长,按键盘上、下键或点击 + 、- 按钮增减的数值 + */ + @Input() step: number = 1; + /** + * 输入框的占位文本 + */ + @Input() placeholder: string = ''; + /** + * @ignore + * + * 从 10.0.1 版本开始该接口不再开放。如果不想要组件根据最大最小值进行强制转换,那么不要设置最大最小值即可。 + * + * 失去焦点是否支持根据最大最小值进行强制转换(默认值为 true, 支持强转; 当用户配置为 false 时,不支持强制转换) + */ + @Input() correctable: boolean = true; + /** + * 是否开启国际化 + */ + @Input() localeable: boolean = true; + /** + * 输入框允许的最大字符数 + */ + @Input() maxlength: number = 20; + /** + * @ignore + * 10.1.11版本之前服务使用tiTip指令自行实现提示 + * + * 此处做兼容性处理,添加该接口判断服务是否使用指令实现 + * + */ + @Input() tiTip: string; + /** + * tip 提示内容,当值为空字符串,则不显示 tip。 + * + * 同时设置最大、最小值时,默认提示文本是:输入值必须在 { 0 } 到 { 1 } 之间。; + * 只设置最大值,默认提示文本是:输入值不能超过 { 0 }。 + * 只设置最小值,默认提示文本是:输入值不能小于 { 0 } + */ + @Input() tipContent: string; + /** + * tip 提示方向 + */ + @Input() tipPosition: TiPositionType = 'top'; + /** + * 动态改变步长,参数为当前输入框值、是否点击加号或者键盘上键,返回值:新步长 + */ + @Input() stepFn: (value: number, isAdd: boolean) => number; + /** + * @ignore + * 当数据发生改变时,触发change事件 + */ + @Output() readonly stepChange: EventEmitter = new EventEmitter(); + @ViewChild('input', { static: true }) private inputEle: ElementRef; + + private numberFormat: string = '1.0-3'; // 整数位保留最小位数.小数位保留最小位数-小数位最大保留位置 + /** + * @ignore + */ + public inputValue: number; + /** + * @ignore + */ + public inputTip: string; + protected versionInfo: string = super.getVersion(packageInfo); + private spinnerWords = TiLocale.getLocaleWords().tiSpinner; + /** + * 对最大最小值进行合法性校验 + */ + private static validateMaxAndMin(key: string, value: any): number { + if (!Number.isNaN(parseFloat(value))) { + return value; + } + + if (key === 'max') { + return Math.pow(2, 53); + } + + if (key === 'min') { + return -Math.pow(2, 53); + } + } + + constructor( + protected hostRef: ElementRef, + protected renderer2: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + private injector: Injector + ) { + super(hostRef, renderer2, changeDetectorRef); + } + + // 组件声明周期钩子--start ↓ + ngOnInit(): void { + super.ngOnInit(); + this.setFocusableElems([this.inputEle.nativeElement]); + this.init(); + } + + private init(): void { + if (!TiLocaleFormat.isInvalidFormat(this.format)) { + const precision: number = parseInt(this.format.slice(1), 10); + this.numberFormat = '1.' + precision + '-' + precision; + } + + this.max = TiSpinnerComponent.validateMaxAndMin('max', this.max); + this.min = TiSpinnerComponent.validateMaxAndMin('min', this.min); + this.setInputTip(); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + this.setChanges(changes, 'max'); + this.setChanges(changes, 'min'); + } + + /** + * @ignore + */ + setDisabledState(isDisabled: boolean): void { + super.setDisabledState(isDisabled); + this.setInputTip(); + // 响应式表单场景,onpush策略下需要更新视图 + this.changeDetectorRef.markForCheck(); + } + + // 输入属性发生改变 + private setChanges(changes: SimpleChanges, key: string): void { + if (changes[key] && !changes[key].isFirstChange()) { + this[key] = parseFloat(changes[key].currentValue); + if (Number.isNaN(this[key]) || this.min > this.max) { + this[key] = TiSpinnerComponent.validateMaxAndMin(key, changes[key].previousValue); + } + if (this.correctable && this.model !== undefined) { + // 这里如果去除了setTimeout,在OnPush环境并不会报错。但是在default环境不能去除setTimeout。综合是不去除 + // error: Expression has changed after it was checked. + setTimeout(() => { + const model: number = this.getModelByMinMax(this.inputValue); + if (model !== this.model) { + this.inputValue = model; + this.model = model; + // onpush策略 响应式表单中使用组件值更改后视图不更新,需要手动标记 + this.changeDetectorRef.markForCheck(); + } + }, 0); + } + + this.setInputTip(); + } + } + + // 组件声明周期钩子--end + // 实现ControlValueAccessor接口 + /** + * @ignore + * model --> view + */ + writeValue(value: any): void { + if (value === null) { + return; + } + + if (!Util.isUndefined(value)) { + if (Number.isNaN(parseFloat(value))) { + super.writeValue(value); + this.model = this.inputValue; + + return; + } + + if (this.correctable) { + // 此处添加延时是由于动态修改value的同时修改max或min时,响应式表单场景输入框值显示异常 + // 此处判断是否是模板式表单,决定是否延时处理 + const spinnerControl: any = this.injector.get(NgControl); + const changeModel: () => void = (): void => { + super.writeValue(value); + const val: any = this.getModelByMinMax(value); + this.model = val; + this.inputValue = val; + }; + if (spinnerControl instanceof NgModel) { + changeModel(); + + return; + } + setTimeout(() => { + changeModel(); + this.changeDetectorRef.markForCheck(); + }, 0); + + return; + } + } + + super.writeValue(value); + this.inputValue = value; + } + // 实现ControlValueAccessor接口--end + /** + * @ignore + */ + public blurFn(): void { + let correctValue: number = this.inputValue; + if (this.correctable && this.inputValue !== undefined) { + correctValue = this.getModelByMinMax(this.inputValue); + } + const parseFormatValue: number = correctValue === undefined ? correctValue : parseFloat(this.formatValue(correctValue)); + if (parseFormatValue !== this.model) { + this.model = parseFormatValue; + } + this.inputValue = this.model; + this.onModelTouched(); // 校验 初次聚焦 后续聚焦失焦 + } + /** + * @ignore + * description: Event emitter for producting the `ngModelChange` event after the view model updates. + * ngModelChange 是ngModel指令的@Output. + * 它在viewToModelUpdate函数中触发 + * + */ + public inputChange(value: number): void { + if (value !== undefined && (value < this.min || value > this.max)) { + return; + } + const parseFormatValue: number = value === undefined ? value : parseFloat(this.formatValue(value)); + if (parseFormatValue !== this.model) { + this.model = parseFormatValue; + } + } + /** + * 键盘上下键操作 + * @ignore + */ + public keydownFn(event: any): void { + if (this.disabled) { + return; + } + if (event.keyCode === TiKeymap.KEY_ARROW_UP) { + // 阻止input默认事件,防止按上键时光标移动到内容前面 + event.preventDefault(); + this.stepNumber(this.method.METHOD_ADD); + } else if (event.keyCode === TiKeymap.KEY_ARROW_DOWN) { + this.stepNumber(this.method.METHOD_SUB); + } + } + /** + * @ignore + */ + public stepNumberMousedown(e: any, method: string): void { + if (e.button === TiKeymap.MOUSE_MIDDLE_BUTTON || e.button === TiKeymap.MOUSE_RIGHT_BUTTON || this.disabled) { + return; + } + // 如果是鼠标按下向下btn,输入框需要做获得光标的处理 + this.inputEle.nativeElement.focus(); + e.preventDefault(); // 目的是防止input失去焦点 + this.stepNumber(method); + } + + private stepNumber(method: string): void { + if ( + (this.inputValue >= this.max && method === this.method.METHOD_ADD) || + (this.inputValue <= this.min && method === this.method.METHOD_SUB) + ) { + return; + } + this.stepChange.emit(method); + + if (this.stepFn && Util.isFunction(this.stepFn)) { + const isAdd: boolean = method === 'add' ? true : false; + this.step = this.stepFn(this.inputValue, isAdd); + } + + // 当输入框中的值为空时,点击+,-,显示最小值。 + if (this.inputValue === undefined) { + this.model = this.min; + this.inputValue = this.model; + + return; + } + if (method === this.method.METHOD_ADD) { + if (this.inputValue > this.max - this.step) { + this.model = this.max; + this.inputValue = this.model; + + return; + } + if (this.inputValue < this.min) { + this.model = this.min; + this.inputValue = this.model; + + return; + } + } else if (method === this.method.METHOD_SUB) { + if (this.inputValue < this.min + this.step) { + this.model = this.min; + this.inputValue = this.model; + + return; + } + if (this.inputValue > this.max) { + this.model = this.max; + this.inputValue = this.model; + + return; + } + } + this.model = this.accOperate(this.inputValue, this.step, method); + this.inputValue = this.model; + } + // 组件交互方法集合--end + + // 根据最大最小值得到model值 + private getModelByMinMax(curValue: number): number { + if (curValue < this.min) { + return this.min; + } + + if (curValue > this.max) { + return this.max; + } + + return curValue; + } + + private formatValue(value: number): string { + if (value === undefined) { + return; + } + // https://angular.cn/api/common/DecimalPipe + // digitsInfo: {minIntegerDigits}.{minFractionDigits}-{maxFractionDigits} + // minIntegerDigits: 小数点前最小位数 默认为1 + // minFractionDigits: 小数点后最小位数 默认0 + // maxFractionDigits: 小数点后最大位数 默认3 + const localeValue: string = TiLocaleFormat.formatNumber(value, this.numberFormat); + + return TiLocaleFormat.parseNumber(localeValue).toString(); + } + + // 根据步长、+/-进行数值计算 + private accOperate(value: number, step: number, method: string): number { + let r1: number; + let r2: number; + let c: number; + let m: number; + let _value: number; + let _step: number; + // 计算 val 小数点后数字的位数 + const getLength: (value: string | number) => number = (val: string): number => { + return String(val).split('.')[1] ? String(val).split('.')[1].length : 0; + }; + // 去除 val 中的 ‘.’ + const replacePeriod: (value: string | number) => number = (val: string): number => { + return Number(String(val).replace('.', '')); + }; + r1 = getLength(value); + r2 = getLength(step); + c = Math.abs(r1 - r2); + m = Math.pow(10, Math.max(r1, r2)); + + if (c > 0) { + const cm: number = Math.pow(10, c); + if (r1 > r2) { + _value = replacePeriod(value); + _step = replacePeriod(step) * cm; + } else { + _value = replacePeriod(value) * cm; + _step = replacePeriod(step); + } + } else { + _value = replacePeriod(value); + _step = replacePeriod(step); + } + + if (method === this.method.METHOD_ADD) { + return (_value + _step) / m; + } + + if (method === this.method.METHOD_SUB) { + return (_value - _step) / m; + } + + return undefined; + } + + /** + * 设置tip提示内容 + */ + private setInputTip(): void { + // 兼容使用tiTip指令实现的tip提示 + if (this.disabled || this.tiTip) { + this.inputTip = ''; + + return; + } + + if (Util.isString(this.tipContent)) { + this.inputTip = this.tipContent; + + return; + } + + if (this.max !== Math.pow(2, 53) && this.min !== -Math.pow(2, 53)) { + this.inputTip = Util.formatEntry(this.spinnerWords.rangeValue, [this.min, this.max]); + + return; + } + + if (this.min !== -Math.pow(2, 53)) { + this.inputTip = Util.formatEntry(this.spinnerWords.minValue, [this.min]); + + return; + } + + if (this.max !== Math.pow(2, 53)) { + this.inputTip = Util.formatEntry(this.spinnerWords.maxValue, [this.max]); + + return; + } + } + + // 内部公共方法集合--end +} diff --git a/src/spinner/lib/src/TiSpinnerModule.ts b/src/spinner/lib/src/TiSpinnerModule.ts new file mode 100644 index 0000000..17f0fc6 --- /dev/null +++ b/src/spinner/lib/src/TiSpinnerModule.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiInputNumberModule } from '@opentiny/ng-inputnumber'; +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiSpinnerComponent } from './TiSpinnerComponent'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; +@NgModule({ + imports: [CommonModule, FormsModule, TiIconModule, TiTextModule, TiInputNumberModule, TiTipModule], + exports: [TiSpinnerComponent], + declarations: [TiSpinnerComponent] +}) +export class TiSpinnerModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiSpinnerComponent } from './TiSpinnerComponent'; diff --git a/src/spinner/lib/src/i18n/TiSpinnerWords.ts b/src/spinner/lib/src/i18n/TiSpinnerWords.ts new file mode 100644 index 0000000..5f223d9 --- /dev/null +++ b/src/spinner/lib/src/i18n/TiSpinnerWords.ts @@ -0,0 +1,7 @@ +export interface TiSpinnerWords { + tiSpinner: { + maxValue: string; + minValue: string; + rangeValue: string; + }; +} diff --git a/src/spinner/lib/src/i18n/en_US.ts b/src/spinner/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..194d4b7 --- /dev/null +++ b/src/spinner/lib/src/i18n/en_US.ts @@ -0,0 +1,9 @@ +import { TiSpinnerWords } from './TiSpinnerWords'; + +export const en_US: TiSpinnerWords = { + tiSpinner: { + maxValue: 'Enter a value less than or equal to {0}.', + minValue: 'Enter a value greater than or equal to {0}.', + rangeValue: 'Enter a value from {0} to {1}.' + } +}; diff --git a/src/spinner/lib/src/i18n/es_US.ts b/src/spinner/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..3f325e8 --- /dev/null +++ b/src/spinner/lib/src/i18n/es_US.ts @@ -0,0 +1,9 @@ +import { TiSpinnerWords } from './TiSpinnerWords'; + +export const es_US: TiSpinnerWords = { + tiSpinner: { + maxValue: 'Ingrese un valor inferior o igual a {0}.', + minValue: 'Ingrese un valor superior o igual a {0}.', + rangeValue: 'Ingrese un valor entre {0} y {1}.' + } +}; diff --git a/src/spinner/lib/src/i18n/fr_FR.ts b/src/spinner/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..2cdfa0a --- /dev/null +++ b/src/spinner/lib/src/i18n/fr_FR.ts @@ -0,0 +1,9 @@ +import { TiSpinnerWords } from './TiSpinnerWords'; + +export const fr_FR: TiSpinnerWords = { + tiSpinner: { + maxValue: 'Saisissez une valeur inférieure ou égale à {0}.', + minValue: 'Saisissez une valeur supérieure ou égale à {0}.', + rangeValue: 'Saisissez une valeur comprise entre {0} et {1}.' + } +}; diff --git a/src/spinner/lib/src/i18n/index.ts b/src/spinner/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/spinner/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/spinner/lib/src/i18n/pt_BR.ts b/src/spinner/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..c41d053 --- /dev/null +++ b/src/spinner/lib/src/i18n/pt_BR.ts @@ -0,0 +1,9 @@ +import { TiSpinnerWords } from './TiSpinnerWords'; + +export const pt_BR: TiSpinnerWords = { + tiSpinner: { + maxValue: 'Insira um valor inferior ou igual a {0}.', + minValue: 'Insira um valor superior ou igual a {0}.', + rangeValue: 'Insira um valor de {0} a {1}.' + } +}; diff --git a/src/spinner/lib/src/i18n/zh_CN.ts b/src/spinner/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..a0901d8 --- /dev/null +++ b/src/spinner/lib/src/i18n/zh_CN.ts @@ -0,0 +1,9 @@ +import { TiSpinnerWords } from './TiSpinnerWords'; + +export const zh_CN: TiSpinnerWords = { + tiSpinner: { + maxValue: '输入值不能超过{0}。', + minValue: '输入值不能小于{0}。', + rangeValue: '输入值必须在{0}到{1}之间。' + } +}; diff --git a/src/spinner/lib/src/spinner.html b/src/spinner/lib/src/spinner.html new file mode 100644 index 0000000..efdda9d --- /dev/null +++ b/src/spinner/lib/src/spinner.html @@ -0,0 +1,32 @@ +
    +
    + +
    +
    diff --git a/src/spinner/lib/src/spinner.less b/src/spinner/lib/src/spinner.less new file mode 100644 index 0000000..4f98ec2 --- /dev/null +++ b/src/spinner/lib/src/spinner.less @@ -0,0 +1,109 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-spinner-container-height: var(--ti-common-size-7x); + --ti-spinner-inner-height: calc(var(--ti-spinner-container-height) - var(--ti-common-border-weight-normal) * 2); +} + +:host { + display: inline-block; + position: relative; + vertical-align: middle; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + background-color: var(--ti-common-color-bg-white-normal); + line-height: normal; + width: var(--ti-common-size-30x); + height: var(--ti-spinner-container-height) !important; + white-space: normal; // 解决在表格中的使用问题 + .box-sizing(border-box); + .form-border-animat-init(); + &:active { + border-color: var(--ti-common-color-line-active); + cursor: pointer; + } + &:hover { + border-color: var(--ti-common-color-line-hover); + //默认状态下的hover动画 + .form-border-animat-enter(); + } + &[tiFocused] { + border-color: var(--ti-common-color-line-active); + } + // 错误场景下中间input背景色 + @{tiny-invalid-class} { + .ti3-spinner-upicon, + .ti3-spinner-downicon { + background-color: var(--ti-common-color-error-bg); + } + } +} + +.ti3-spinner-input-box { + position: absolute; + left: var(--ti-spinner-inner-height); + top: 0; + width: calc(100% - var(--ti-spinner-inner-height) * 2); + // 解决浏览器缩放问题:缩放时,输入框底部边框消失 + // 原因:输入框设置固定高度,但其父元素没有高度,随浏览器缩放,输入框固高大于父元素高度,导致底部边框被遮挡 + height: 100%; + .box-sizing(border-box); + + text-align: center; + ime-mode: disabled; + .ti3-spinner-input[tiText] { + text-align: center; + height: 100%; + width: 100%; + line-height: var(--ti-spinner-inner-height); + background-color: var(--ti-common-color-transparent); // 设置text透明色,避免覆盖组件样式 + } +} + +.ti3-spinner-upicon, +.ti3-spinner-downicon { + .box-sizing(border-box); + position: absolute; + text-align: center; + top: 0; + color: var(--ti-common-color-icon-normal); + width: var(--ti-spinner-inner-height); + // 解决浏览器缩放问题:缩放时,按钮禁用时底部边框消失 + height: 100%; + line-height: var(--ti-spinner-inner-height); + &:not(.ti3-spinner-icon-disabled):hover { + color: var(--ti-common-color-icon-hover); + cursor: pointer; + } + &.ti3-spinner-icon-disabled { + color: var(--ti-common-color-icon-disabled); + background-color: var(--ti-common-color-bg-disabled); + cursor: not-allowed; + } +} +.ti3-spinner-upicon { + border-left: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + right: 0; +} + +.ti3-spinner-downicon { + border-right: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + left: 0; +} + +:host[disabled] { + background-color: var(--ti-common-color-bg-disabled); + border-color: var(--ti-common-color-line-disabled); + cursor: not-allowed; + + .ti3-spinner-upicon, + .ti3-spinner-downicon { + // 禁用样式要优先于悬浮样式,增加样式权重 + &:not(.ti3-spinner-icon-disabled) { + color: var(--ti-common-color-icon-disabled); + background-color: var(--ti-common-color-bg-disabled); + cursor: not-allowed; + } + border-color: var(--ti-common-color-line-disabled); + } +} diff --git a/src/steps/demo/karma.conf.js b/src/steps/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/steps/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/steps/demo/project.json b/src/steps/demo/project.json new file mode 100644 index 0000000..6b6cce9 --- /dev/null +++ b/src/steps/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/steps/demo", + "sourceRoot": "src/steps/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/steps", + "index": "src/steps/demo/src/index.html", + "main": "src/steps/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/steps/demo/tsconfig.app.json", + "assets": ["src/steps/demo/src/favicon.ico", "src/steps/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "steps-demo:build:production" + }, + "development": { + "browserTarget": "steps-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js steps" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/steps/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/steps/demo/tsconfig.spec.json", + "karmaConfig": "src/steps/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/steps/demo/src/app/AppComponent.ts b/src/steps/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/steps/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/steps/demo/src/app/AppModule.ts b/src/steps/demo/src/app/AppModule.ts new file mode 100644 index 0000000..23634cf --- /dev/null +++ b/src/steps/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { StepsTestModule } from './steps/StepsTestModule'; + +@NgModule({ + imports: [ + StepsTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/steps/demo/src/app/IndexComponent.ts b/src/steps/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..989a5fb --- /dev/null +++ b/src/steps/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { StepsTestModule } from './steps/StepsTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = StepsTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/steps/demo/src/app/app.html b/src/steps/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/steps/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/steps/demo/src/app/steps/StepsActiveComponent.ts b/src/steps/demo/src/app/steps/StepsActiveComponent.ts new file mode 100644 index 0000000..c177923 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsActiveComponent.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-active.html' // 指定组件模板 +}) +export class StepsActiveComponent { + steps: Array = [ + { + id: 'id1', + label: 'General' + }, + { + id: 'id2', + label: 'Host Group' + }, + { + id: 'id3', + label: 'Policy' + }, + { + id: 'id4', + label: 'Names' + } + ]; + activeStep: TiStepItem = this.steps[2]; + clickable: boolean = true; + labelKey: string = 'id'; + + jump(): void { + this.activeStep = this.steps[1]; + } +} diff --git a/src/steps/demo/src/app/steps/StepsAdaptiveComponent.ts b/src/steps/demo/src/app/steps/StepsAdaptiveComponent.ts new file mode 100644 index 0000000..8e4399a --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsAdaptiveComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-adaptive.html' +}) +export class StepsAdaptiveComponent { + steps: Array = [ + { + label: '购买专业服务' + }, + { + label: '服务受理' + }, + { + label: '查看进展' + }, + { + label: '验收完成' + } + ]; + activeStep: number = 1; +} diff --git a/src/steps/demo/src/app/steps/StepsAdaptiveTestComponent.ts b/src/steps/demo/src/app/steps/StepsAdaptiveTestComponent.ts new file mode 100644 index 0000000..46b03aa --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsAdaptiveTestComponent.ts @@ -0,0 +1,69 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-adaptive-test.html' +}) +export class StepsAdaptiveTestComponent { + steps1: Array = [ + { + label: '基础配置' + }, + { + label: '网络系统配置' + }, + { + label: '其他各种配置' + }, + { + label: '确认配置' + } + ]; + steps2: Array = [ + { + label: 'API设计' + }, + { + label: 'API代理' + }, + { + label: 'API产品' + }, + { + label: '资产中心' + } + ]; + activeStep1: TiStepItem = this.steps1[2]; + activeStep2: TiStepItem = this.steps2[1]; + changeLabel1(): void { + this.steps1[0].label = 'General General General General General General General'; + } + changeSteps1(): void { + this.steps1 = [ + { + label: 'dfhaashdfa dhajdh dhfahdj sd' + }, + { + label: 'fajksdj djfa fjasj fjakd' + }, + { + label: '其他各种系统配置' + }, + { + label: '江山如此多娇,引无数英雄竞折腰' + }, + { + label: '快乐旋转' + } + ]; + } + changeContainerWidht1(container: any): void { + container.style.width = '1200px'; + } + changeLabel2(): void { + this.steps2[2].label = 'General General General General General General General'; + } + changeContainerWidht2(container: any): void { + container.style.marginLeft = '0px'; + } +} diff --git a/src/steps/demo/src/app/steps/StepsBaseComponent.ts b/src/steps/demo/src/app/steps/StepsBaseComponent.ts new file mode 100644 index 0000000..3a24027 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsBaseComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-base.html' +}) +export class StepsBaseComponent { + steps: Array = [ + { + label: '购买专业服务' + }, + { + label: '服务受理' + }, + { + label: '查看进展' + }, + { + label: '验收完成' + } + ]; + activeStep: number = 1; +} diff --git a/src/steps/demo/src/app/steps/StepsBeforeComponent.ts b/src/steps/demo/src/app/steps/StepsBeforeComponent.ts new file mode 100644 index 0000000..d9052f3 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsBeforeComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-before.html' +}) +export class StepsBeforeComponent { + myLogs: Array = []; + steps: Array = [ + { + label: '购买专业服务' + }, + { + label: '服务受理' + }, + { + label: '查看进展(必选项)', + require: true + }, + { + label: '验收完成' + } + ]; + activeStep: number = 1; + beforeStep(step: TiStepItem): void { + let requireIndex: number; + const index: number = this.steps.indexOf(step); + for (let i: number = 0; i < this.steps.length; i++) { + if (this.steps[i].require) { + requireIndex = i; + } + } + + if (index < requireIndex) { + this.activeStep = index; + } else { + this.myLogs = [...this.myLogs, '有必选项未完成,点击项不可直接跳转,需完成未完成项。']; + } + } +} diff --git a/src/steps/demo/src/app/steps/StepsClickableComponent.ts b/src/steps/demo/src/app/steps/StepsClickableComponent.ts new file mode 100644 index 0000000..6fac903 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsClickableComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-clickable.html' +}) +export class StepsClickableComponent { + steps: Array = [ + { + label: '购买专业服务' + }, + { + label: '服务受理' + }, + { + label: '查看进展', + error: true + }, + { + label: '验收完成(禁用)', + disabled: true + } + ]; + + activeStep: number = 1; +} diff --git a/src/steps/demo/src/app/steps/StepsEventsComponent.ts b/src/steps/demo/src/app/steps/StepsEventsComponent.ts new file mode 100644 index 0000000..efed272 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsEventsComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-events.html' +}) +export class StepsEventsComponent { + myLogs: Array = []; + steps: Array = [ + { + label: '购买专业服务' + }, + { + label: '服务受理' + }, + { + label: '查看进展' + }, + { + label: '验收完成' + } + ]; + activeStep: number = 1; + onActiveStepChange(activeStep: number): void { + this.myLogs = [...this.myLogs, `activeStepChange() event => ${JSON.stringify(this.steps[activeStep])}`]; + } +} diff --git a/src/steps/demo/src/app/steps/StepsLabelComponent.ts b/src/steps/demo/src/app/steps/StepsLabelComponent.ts new file mode 100644 index 0000000..d14f363 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsLabelComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-label.html' +}) +export class StepsLabelComponent { + steps: Array = [ + { + label: '购买专业服务', + english: 'Purchase' + }, + { + label: '服务受理', + english: 'Processing' + }, + { + label: '查看进展', + english: 'Progress' + }, + { + label: '验收完成', + english: 'Acceptance' + } + ]; + activeStep: number = 1; +} diff --git a/src/steps/demo/src/app/steps/StepsMaxwidthComponent.ts b/src/steps/demo/src/app/steps/StepsMaxwidthComponent.ts new file mode 100644 index 0000000..debf422 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsMaxwidthComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-maxwidth.html' +}) +export class StepsMaxwidthComponent { + steps: Array = [ + { + label: '咨询购买专业服务,并支付' + }, + { + label: '服务受理' + }, + { + label: '查看进展' + }, + { + label: '验收完成' + } + ]; + activeStep: number = 1; +} diff --git a/src/steps/demo/src/app/steps/StepsTemplateComponent.ts b/src/steps/demo/src/app/steps/StepsTemplateComponent.ts new file mode 100644 index 0000000..8c824a7 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsTemplateComponent.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-template.html' +}) +export class StepsTemplateComponent { + steps: Array = [ + { + label: '购买专业服务' + }, + { + label: '服务受理' + }, + { + label: '查看进展' + }, + { + label: '验收完成' + } + ]; + activeStep: number = 1; + ngOnInit(): void { + this.getIcon(this.activeStep); + } + + activeStepChange(activeStep: number): void { + this.getIcon(activeStep); + } + + getIcon(activeStep: number): void { + this.steps.forEach((step: TiStepItem, index: number) => { + step.icon = index > activeStep ? 'exclamation-circle' : 'add1'; + }); + } +} diff --git a/src/steps/demo/src/app/steps/StepsTestModule.ts b/src/steps/demo/src/app/steps/StepsTestModule.ts new file mode 100644 index 0000000..4f943d1 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsTestModule.ts @@ -0,0 +1,78 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiIconModule, TiStepsModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { StepsActiveComponent } from './StepsActiveComponent'; +import { StepsClickableComponent } from './StepsClickableComponent'; +import { StepsBeforeComponent } from './StepsBeforeComponent'; +import { StepsBaseComponent } from './StepsBaseComponent'; +import { StepsMaxwidthComponent } from './StepsMaxwidthComponent'; +import { StepsLabelComponent } from './StepsLabelComponent'; +import { StepsAdaptiveComponent } from './StepsAdaptiveComponent'; +import { StepsAdaptiveTestComponent } from './StepsAdaptiveTestComponent'; +import { StepsTemplateComponent } from './StepsTemplateComponent'; +import { StepsEventsComponent } from './StepsEventsComponent'; + +@NgModule({ + imports: [CommonModule, TiStepsModule, TiIconModule, DemoLogModule, RouterModule.forChild(StepsTestModule.ROUTES)], + declarations: [ + StepsBaseComponent, + StepsActiveComponent, + StepsClickableComponent, + StepsBeforeComponent, + StepsMaxwidthComponent, + StepsAdaptiveComponent, + StepsLabelComponent, + StepsAdaptiveTestComponent, + StepsTemplateComponent, + StepsEventsComponent + ] +}) +export class StepsTestModule { + static readonly LINKS: Array = [{ href: 'components/TiStepsComponent.html', label: 'Steps' }]; + static readonly ROUTES: Routes = [ + { + path: 'steps/steps-base', + component: StepsBaseComponent + }, + { + path: 'steps/steps-clickable', + component: StepsClickableComponent + }, + { + path: 'steps/steps-active', + component: StepsActiveComponent + }, + { + path: 'steps/steps-before', + component: StepsBeforeComponent + }, + { + path: 'steps/steps-maxwidth', + component: StepsMaxwidthComponent + }, + { + path: 'steps/steps-label', + component: StepsLabelComponent + }, + { + path: 'steps/steps-adaptive', + component: StepsAdaptiveComponent + }, + { + path: 'steps/steps-events', + component: StepsEventsComponent + }, + { + path: 'steps/steps-template', + component: StepsTemplateComponent + }, + { + path: 'steps/steps-adaptive-test', + component: StepsAdaptiveTestComponent + } + ]; +} diff --git a/src/steps/demo/src/app/steps/steps-active.html b/src/steps/demo/src/app/steps/steps-active.html new file mode 100644 index 0000000..61646a1 --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-active.html @@ -0,0 +1,9 @@ +

    1.描述

    +

    activeStep接口测试

    +

    2.示例

    +

    (2.1)activeStep动态变更

    + +

    当前选中项:{{activeStep.label}}

    +
    + + diff --git a/src/steps/demo/src/app/steps/steps-adaptive-test.html b/src/steps/demo/src/app/steps/steps-adaptive-test.html new file mode 100644 index 0000000..482fbab --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-adaptive-test.html @@ -0,0 +1,29 @@ +

    描述

    +

    + adaptive接口场景测试。设置为adaptive为true时,整体宽度会自适应撑满父容器,父容器宽度改变的时候steps也会调整(这个是10.1.2版本才会支持), + label改变时steps也会调整。 +

    +

    示例

    + +

    1.容器固定值宽度,改变容器宽度或改变label,steps都会自适应

    +
    +
    + +
    +
    +

    当前选中项:{{activeStep1.label}}

    +
    + + + + +

    2.容器宽度自适应时,容器宽度变化或改变label,steps都会自适应

    +
    +
    + +
    +
    +

    当前选中项:{{activeStep2.label}}

    +
    + + diff --git a/src/steps/demo/src/app/steps/steps-adaptive.html b/src/steps/demo/src/app/steps/steps-adaptive.html new file mode 100644 index 0000000..b93c24e --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-adaptive.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/steps/demo/src/app/steps/steps-base.html b/src/steps/demo/src/app/steps/steps-base.html new file mode 100644 index 0000000..8640525 --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-base.html @@ -0,0 +1 @@ + diff --git a/src/steps/demo/src/app/steps/steps-before.html b/src/steps/demo/src/app/steps/steps-before.html new file mode 100644 index 0000000..913f6ff --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-before.html @@ -0,0 +1,2 @@ + + diff --git a/src/steps/demo/src/app/steps/steps-clickable.html b/src/steps/demo/src/app/steps/steps-clickable.html new file mode 100644 index 0000000..7a615e5 --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-clickable.html @@ -0,0 +1,4 @@ + +
    +
    Current value: {{ steps[activeStep].label }}
    +
    diff --git a/src/steps/demo/src/app/steps/steps-events.html b/src/steps/demo/src/app/steps/steps-events.html new file mode 100644 index 0000000..731e2f0 --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-events.html @@ -0,0 +1,8 @@ + + diff --git a/src/steps/demo/src/app/steps/steps-label.html b/src/steps/demo/src/app/steps/steps-label.html new file mode 100644 index 0000000..742f141 --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-label.html @@ -0,0 +1 @@ + diff --git a/src/steps/demo/src/app/steps/steps-maxwidth.html b/src/steps/demo/src/app/steps/steps-maxwidth.html new file mode 100644 index 0000000..baff5fd --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-maxwidth.html @@ -0,0 +1 @@ + diff --git a/src/steps/demo/src/app/steps/steps-template.html b/src/steps/demo/src/app/steps/steps-template.html new file mode 100644 index 0000000..2c0c7d3 --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-template.html @@ -0,0 +1,11 @@ + + + + + + + +
    第{{ i }}步
    + {{ step.label }} +
    +
    diff --git a/src/steps/demo/src/app/steps/webdoc/steps-demos.js b/src/steps/demo/src/app/steps/webdoc/steps-demos.js new file mode 100644 index 0000000..ca8f8d6 --- /dev/null +++ b/src/steps/demo/src/app/steps/webdoc/steps-demos.js @@ -0,0 +1,107 @@ +export default { + column: '2', + demos: [ + { + demoId: 'steps-base', + name: { + 'zh-CN': '基本使用', + 'en-US': 'base', + }, + desc: { + 'zh-CN': '

    Steps 组件的最简用法。

    ', + 'en-US': '', + }, + apis: [ + 'TiStepsComponent.properties.steps', + 'TiStepsComponent.properties.activeStep', + 'TiStepsComponent.properties.disabled', + ], + }, + { + demoId: 'steps-clickable', + name: { + 'zh-CN': '是否可点击跳转', + 'en-US': 'clickable', + }, + desc: { + 'zh-CN': + '

    通过属性clickable配置步骤是否可被点击跳转。

    ', + 'en-US': '', + }, + apis: ['TiStepsComponent.properties.clickable'], + }, + { + demoId: 'steps-label', + name: { + 'zh-CN': '显示文本', + 'en-US': 'labelkey', + }, + desc: { + 'zh-CN': '

    通过属性labelKey配置显示文本的键值。

    ', + 'en-US': '', + }, + apis: ['TiStepsComponent.properties.labelKey'], + }, + { + demoId: 'steps-maxwidth', + name: { + 'zh-CN': '文本最大宽度', + 'en-US': 'maxwidth', + }, + desc: { + 'zh-CN': '

    通过属性maxWidth配置文本最大宽度。

    ', + 'en-US': '', + }, + apis: ['TiStepsComponent.properties.maxWidth'], + }, + { + demoId: 'steps-adaptive', + name: { + 'zh-CN': '宽度自适应', + 'en-US': 'adaptive', + }, + desc: { + 'zh-CN': '

    通过属性adaptive配置宽度自适应。

    ', + 'en-US': '', + }, + apis: ['TiStepsComponent.properties.adaptive'], + }, + { + demoId: 'steps-before', + name: { + 'zh-CN': '必选项', + 'en-US': 'require', + }, + desc: { + 'zh-CN': + '

    通过属性require配置某一步骤为必选项。当点击必选步骤的时候触发beforeStep事件。

    ', + 'en-US': '', + }, + apis: ['TiStepsComponent.events.beforeStep'], + }, + { + demoId: 'steps-events', + name: { + 'zh-CN': '事件', + 'en-US': 'events', + }, + desc: { + 'zh-CN': + '

    当步骤改变的时候触发activeStepChange事件。

    ', + 'en-US': '', + }, + apis: ['TiStepsComponent.events.activeStepChange'], + }, + { + demoId: 'steps-template', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'custom template', + }, + desc: { + 'zh-CN': '

    通过#icon配置图标区域的模板,通过#step配置文本区域的模板。

    ', + 'en-US': '', + }, + }, + ], +}; diff --git a/src/steps/demo/src/app/steps/webdoc/steps.cn.md b/src/steps/demo/src/app/steps/webdoc/steps.cn.md new file mode 100644 index 0000000..19a6ef5 --- /dev/null +++ b/src/steps/demo/src/app/steps/webdoc/steps.cn.md @@ -0,0 +1,23 @@ +--- +title: Steps 步骤导航 +--- +# Steps 步骤导航 + +
    + +Steps 是引导用户按照流程完成任务的导航条。   + +```typescript +import { TiStepsModule } from '@opentiny/ng'; +``` + +
    + +
    + +Steps 是引导用户按照流程完成任务的导航条。   + +```typescript +import { TiStepsModule } from '@opentiny/ng'; +``` +
    diff --git a/src/steps/demo/src/app/steps/webdoc/steps.en.md b/src/steps/demo/src/app/steps/webdoc/steps.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/steps/demo/src/app/steps/webdoc/steps.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/steps/demo/src/favicon.ico b/src/steps/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/steps/demo/src/index.html b/src/steps/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/steps/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/steps/demo/src/main.ts b/src/steps/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/steps/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/steps/demo/test.ts b/src/steps/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/steps/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/steps/demo/tsconfig.app.json b/src/steps/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/steps/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/steps/demo/tsconfig.spec.json b/src/steps/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/steps/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/steps/lib/index.ts b/src/steps/lib/index.ts new file mode 100644 index 0000000..5b924e3 --- /dev/null +++ b/src/steps/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiStepsModule'; diff --git a/src/steps/lib/ng-package.json b/src/steps/lib/ng-package.json new file mode 100644 index 0000000..ad914a3 --- /dev/null +++ b/src/steps/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/steps", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/steps/lib/package.json b/src/steps/lib/package.json new file mode 100644 index 0000000..ded6800 --- /dev/null +++ b/src/steps/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-steps", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/steps/lib/project.json b/src/steps/lib/project.json new file mode 100644 index 0000000..065c173 --- /dev/null +++ b/src/steps/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/steps/lib", + "sourceRoot": "src/steps/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/steps"], + "options": { + "project": "src/steps/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/steps"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js steps" + }, + { + "command": "ng default-build steps" + }, + { + "command": "node build/clear-default-theme.js steps" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/steps && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build steps && ng pack steps && node build/publish.js steps --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/steps/lib/src/TiStepsComponent.ts b/src/steps/lib/src/TiStepsComponent.ts new file mode 100644 index 0000000..6cceb02 --- /dev/null +++ b/src/steps/lib/src/TiStepsComponent.ts @@ -0,0 +1,283 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + Input, + IterableDiffers, + NgZone, + Output, + QueryList, + Renderer2, + TemplateRef, + ViewChild, + ViewChildren +} from '@angular/core'; +import { Util } from '@opentiny/ng-utils'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +/** + * 步骤项 + */ +export interface TiStepItem { + /** + * 默认标题,通过配置 labelKey 可以指定其他属性作为标题 + */ + label?: string; + /** + * 是否禁用 + */ + disabled?: boolean; + /** + * 是否错误 + */ + error?: boolean; + /** + * 配置其他自定义属性 + */ + [key: string]: any; +} + +/** + * steps步骤组件 + * + * 点击可跳转和不可跳转两种方式(默认点击不跳转) + * + */ +@Component({ + selector: 'ti-steps', + templateUrl: './steps.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./steps.less'] +}) +export class TiStepsComponent extends TiBaseComponent { + /** + * 必选,步骤导航数据项。 + */ + @Input() steps: Array; + /** + * 是否支持点击跳转功能,当为 false 时,视觉呈现和禁用一致 + */ + @Input() clickable: boolean = false; + /** + * 步骤标题文本最大宽度 + */ + @Input() maxWidth: string; + /** + * 步骤标题要显示的字段。 和 select 保持一致 + */ + @Input() labelKey: string = 'label'; + /** + * 必选,当前激活步骤项,10.1.19 版本支持传入激活项的下标 + */ + @Input() activeStep: any; + /** + * 宽度是否自动撑满父容器。 + * + * 设置为 true 整体宽度会撑满父容器,常用在弹窗场景 + */ + @Input() adaptive: boolean = false; + /** + * 步骤激活项改变时触发的回调 + */ + @Output() readonly activeStepChange: EventEmitter = new EventEmitter(); + /** + * 步骤激活项改变前触发的回调 + */ + @Output() readonly beforeStep: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + @ViewChild('stepRef') stepsRef: ElementRef; + /** + * @ignore + */ + @ViewChildren('line') lineRef: QueryList; + /** + * @ignore + */ + @ViewChildren('explain') explainRef: QueryList; + /** + * @ignore + * 获取到用户自定义的模板 + */ + @ContentChild(TemplateRef, { static: true }) itemTemplate: TemplateRef; + /** + * 文本区域的模板 + */ + @ContentChild('step', { static: true }) stepTemplate: TemplateRef; + /** + * icon 区域的模板 + */ + @ContentChild('icon', { static: true }) iconTemplate: TemplateRef; + protected versionInfo: string = super.getVersion(packageInfo); + private labelChange: boolean; // 标志文本长度是否变化,从而重新计算线长 + private stepsDiffer: any; + private isInitLabelChange: boolean = true; + private explainTotalWidth: number = 0; + private windowResizeListener: () => void; + constructor( + protected elementRef: ElementRef, + protected renderer2: Renderer2, + private iterableDiffers: IterableDiffers, + private zone: NgZone, + private changeDetectorRef: ChangeDetectorRef + ) { + super(elementRef, renderer2); + } + + ngOnInit(): void { + super.ngOnInit(); + if (!this.adaptive) { + return; + } + // this.trackByLabelFn监听对象:可以深度监听 + this.stepsDiffer = this.iterableDiffers.find(this.steps).create(this.trackByLabelFn); + // 修复错误:ERROR ReferenceError: window is not defined + if (typeof window === 'undefined') { + return; + } + this.zone.runOutsideAngular(() => { + this.windowResizeListener = this.renderer2.listen(window, 'resize', this.setLineWidth); + }); + } + + ngDoCheck(): void { + if (this.adaptive) { + // 处理增删步骤 + const stepsDiffer: any = this.stepsDiffer.diff(this.steps); + if (stepsDiffer) { + this.labelChange = true; + } + } + + // 增删步骤,改变属性时指引未发生变化,onpush模式下不会触发变更,故手动触发 + this.changeDetectorRef.markForCheck(); + } + + ngAfterContentInit(): void { + super.ngAfterContentInit(); + /** + * 兼容旧版无命名模板: + * 10.1.16 版本到 10.1.22 之前只能内嵌一个模板,无命名。 + * 10.1.22 版本之后可以内嵌两个模板,#step #icon + */ + if ( + !this.stepTemplate && + this.itemTemplate && + this.itemTemplate.elementRef.nativeElement !== (this.iconTemplate && this.iconTemplate.elementRef.nativeElement) + ) { + this.stepTemplate = this.itemTemplate; + } + } + + ngAfterViewChecked(): void { + // 需要判断当label变化时才计算线长 + if (this.adaptive && this.labelChange) { + this.labelChange = false; + this.setLineWidth(); + // 初始时在前几次的 ngAfterViewChecked 中计算文本宽度,生产环境清缓存场景下计算有点不准确(时机有点早),但在初始时的后几次ngAfterViewChecked中计算的文本宽度是准确的。 + // TODO: 是否有更合适的时机计算(已验证使用MutationObserver也解决不了,在ngAfterViewChecked中加setTimeout(0)也解决不了) + if (this.isInitLabelChange) { + setTimeout(() => { + const explainTotalWidth: number = this.getExplainTotalWidth(); + if (explainTotalWidth !== this.explainTotalWidth) { + this.setLineWidth(explainTotalWidth); + } + }, 50); // 50ms是经验值 + this.isInitLabelChange = false; + } + } + } + + ngOnDestroy(): void { + if (this.windowResizeListener) { + this.windowResizeListener(); + } + } + + /** + * @ignore + * 每步的点击事件处理 + */ + public onClick(index: number): void { + const step: any = this.steps[index]; + // 不支持点击跳转、当前项灰化或者点击当前激活想项时,不处理 + if (!this.clickable || step.disabled || this.activeStep === step || this.activeStep === index) { + return; + } + + // 未定义beforeStep事件,直接跳转,定义beforeStep事件将当前点击项索引传出 + if (this.beforeStep.observers.length === 0) { + this.activeStep = Util.isNumber(this.activeStep) ? index : step; + this.activeStepChange.emit(this.activeStep); + } else { + this.beforeStep.emit(step); + } + } + + // 设置每个步骤之间的横线的宽度 + public setLineWidth = (width?: number): void => { + this.explainTotalWidth = Util.isUndefined(width) ? this.getExplainTotalWidth() : width; + this.lineRef.forEach((stepLine: ElementRef) => { + this.renderer2.setStyle(stepLine.nativeElement, 'width', `calc((100% - ${this.explainTotalWidth}px) / ${this.steps.length - 1})`); + }); + }; + + private getExplainTotalWidth(): number { + let explainTotalWidth: number = 0; + if (!Util.isUndefined(this.explainRef)) { + this.explainRef.forEach((step: ElementRef) => { + // 计算每步选择框和文字的总长度 + explainTotalWidth += step.nativeElement.offsetWidth; + }); + } + + return explainTotalWidth; + } + + /** + * @ignore + * Diff监听steps中label值的改变 + */ + public trackByLabelFn(index: number, item: any): string { + return item.label; + } + + /** + * @ignore + * ngFor 使用 + */ + public trackByIndexFn(index: number): number { + return index; + } + + /** + * @ignore + * 判断当前项的状态 + */ + public getStepState(index: number): string { + const activedIndex: number = Util.isNumber(this.activeStep) ? this.activeStep : this.steps.indexOf(this.activeStep); + if (index < activedIndex) { + return this.steps[index].error ? 'error' : 'complete'; + } else if (index === activedIndex) { + return this.steps[index].error ? 'error' : 'active'; + } else { + return 'uncomplete'; + } + } +} diff --git a/src/steps/lib/src/TiStepsModule.ts b/src/steps/lib/src/TiStepsModule.ts new file mode 100644 index 0000000..812a9ec --- /dev/null +++ b/src/steps/lib/src/TiStepsModule.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiStepsComponent } from './TiStepsComponent'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; + +@NgModule({ + imports: [TiOverflowModule, TiIconModule, CommonModule, TiOutlineModule], + exports: [TiStepsComponent], + declarations: [TiStepsComponent] +}) +export class TiStepsModule {} +export { TiStepsComponent, TiStepItem } from './TiStepsComponent'; diff --git a/src/steps/lib/src/steps.html b/src/steps/lib/src/steps.html new file mode 100644 index 0000000..66ee1fb --- /dev/null +++ b/src/steps/lib/src/steps.html @@ -0,0 +1,51 @@ +
      + +
    • + +
    • +
      +
      + +
      + +
      +
      + + + {{i + 1}} +
      +
      +
      +
      + + + +
      + + + +
      +
    • +
      +
    diff --git a/src/steps/lib/src/steps.less b/src/steps/lib/src/steps.less new file mode 100644 index 0000000..e80b9d5 --- /dev/null +++ b/src/steps/lib/src/steps.less @@ -0,0 +1,227 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-steps-transition-time: 300ms; + --ti-steps-wire-height: 1px; + --ti-steps-max-width: 150px; + --ti-steps-box-size: var(--ti-common-size-5x); +} + +:host { + display: block; +} + +.ti3-steps { + padding: var(--ti-common-space-3x) 0px; + white-space: nowrap; + & .ti3-steps-explain { + vertical-align: middle; + display: inline-block; + .ti3-steps-text-container { + display: flex; + align-items: center; + } + .ti3-steps-icon { + font-size: var(--ti-common-font-size-4); + margin-left: var(--ti-common-space-2x); + } + + .ti3-steps-box { + display: flex; + align-items: center; + justify-content: center; + width: var(--ti-steps-box-size); + height: var(--ti-steps-box-size); + .box-sizing(border-box); + font-size: var(--ti-common-font-size-1); + border: var(--ti-common-border-weight-normal) solid; + margin-left: var(--ti-common-space-2x); + position: relative; + .border-radius(var(--ti-common-border-radius-3)); + &:before { + content: ''; + display: inline-block; + width: var(--ti-steps-box-size); + height: var(--ti-steps-box-size); + .border-radius(var(--ti-common-border-radius-3)); + background-color: var(--ti-common-color-bg-emphasize); + position: absolute; + opacity: 0; + } + & .ti3-steps-box-number { + position: absolute; + width: calc(var(--ti-steps-box-size) * 7 / 10); + height: calc(var(--ti-steps-box-size) * 7 / 10); + line-height: calc(var(--ti-steps-box-size) * 7 / 10); + text-align: center; + ti-icon { + line-height: unset; + } + } + } + + .ti3-steps-text { + display: inline-block; + line-height: var(--ti-steps-box-size); + margin: 0 var(--ti-common-space-2x) 0 var(--ti-common-space-base); + max-width: var(--ti-steps-max-width); + vertical-align: top; + word-break: break-word; + white-space: normal; + } + } + & li { + &:first-child.ti3-steps-explain .ti3-steps-box { + margin-left: 0px; + } + + &:last-child.ti3-steps-explain .ti3-steps-text { + margin-right: 0px; + } + } + + & .ti3-steps-explain-clickable { + cursor: pointer; + } + & .ti3-steps-line { + width: var(--ti-common-size-15x); + height: var(--ti-steps-wire-height); + min-width: var(--ti-common-size-5x); + vertical-align: middle; + display: inline-block; + line-height: 0; // 当父节点有line-height属性并大于3时,子节点继承该属性,此时两条线会错位 + background-color: var(--ti-common-color-line-normal); + font-size: 0; + &:before { + content: ' '; + display: inline-block; + width: 0%; + height: 100%; + background-color: var(--ti-common-color-line-active); + } + } +} + +// 未完成效果 +.ti3-steps .ti3-steps-uncomplete { + &.ti3-steps-line::before { + .steps-line-animation(0%); + } + &.ti3-steps-explain { + .ti3-steps-box { + .steps-box(var(--ti-common-color-text-primary), var(--ti-common-color-text-primary), 0); + } + & .ti3-steps-text { + .steps-text-animation(var(--ti-common-color-text-secondary), 0%); + &:hover { + .steps-text-animation(var(--ti-common-color-prompt), 100%); + } + } + .ti3-steps-icon { + color: var(--ti-common-color-text-secondary); + } + } +} + +// 完成状态 +.ti3-steps .ti3-steps-complete { + &.ti3-steps-line::before { + .steps-line-animation(100%); + } + &.ti3-steps-explain { + .ti3-steps-box { + .steps-box(var(--ti-common-color-icon-active), var(--ti-common-color-icon-active), 0); + } + & .ti3-steps-text { + .steps-text-animation(var(--ti-common-color-text-primary), 0%); + &:hover { + .steps-text-animation(var(--ti-common-color-prompt), 100%); + } + } + .ti3-steps-icon { + color: var(--ti-common-color-text-highlight); + } + } +} + +// 选中状态 +.ti3-steps .ti3-steps-active { + &.ti3-steps-line::before { + .steps-line-animation(100%); + } + &.ti3-steps-explain { + .ti3-steps-box { + .steps-box(var(--ti-common-color-icon-active), var(--ti-common-color-text-white), 1); + } + & .ti3-steps-text { + .steps-text-animation(var(--ti-common-color-text-highlight), 100%); + } + .ti3-steps-icon { + color: var(--ti-common-color-text-highlight); + } + } +} + +// 灰化状态 +.ti3-steps .ti3-steps-disabled { + &.ti3-steps-explain { + .ti3-steps-box { + border-color: var(--ti-common-color-text-disabled); + color: var(--ti-common-color-text-disabled); + &:before { + display: none !important; + } + } + & .ti3-steps-text { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + } + .ti3-steps-icon { + color: var(--ti-common-color-text-disabled); + } + } +} + +// 错误状态 +.ti3-steps .ti3-steps-error { + &.ti3-steps-line::before { + .steps-line-animation(100%); + } + &.ti3-steps-explain { + .ti3-steps-box { + .steps-box(var(--ti-common-color-error), var(--ti-common-color-error), 0); + } + & .ti3-steps-text { + .steps-text-animation(var(--ti-common-color-error), 0%); + } + .ti3-steps-icon { + color: var(--ti-common-color-error); + } + } +} + +.steps-box(@border-color, @color, @number) { + border-color: @border-color; + color: @color; + &:before { + transform: scale(@number); + opacity: @number; + transition: transform var(--ti-steps-transition-time), opacity var(--ti-steps-transition-time); + } +} + +// 不支持IE +.steps-text-animation(@color, @background-size-width) { + color: @color; + background-size: @background-size-width 100%; + background-repeat: no-repeat; + -webkit-background-clip: text; + background-image: linear-gradient(var(--ti-common-color-text-highlight) 0%, var(--ti-common-color-text-highlight) 100%); + background-image: -ms-linear-gradient(top, transparent, transparent); + transition: background-size var(--ti-steps-transition-time), color var(--ti-steps-transition-time); +} + +.steps-line-animation(@width) { + width: @width; + transition: width var(--ti-steps-transition-time); +} diff --git a/src/styles.less b/src/styles.less new file mode 100644 index 0000000..a9e5b09 --- /dev/null +++ b/src/styles.less @@ -0,0 +1,382 @@ +/* You can add global styles to this file, and also import other style files */ +html { + font-size: var(--ti-common-font-size-base); +} +.demo-text-warn { + color: #e37d29; +} + +.demo-code { + font-family: Consolas; + color: #de504e; + background-color: #fbe5e1; + padding: 2px 4px; + border-radius: 4px; +} + +.demo-link { + color: var(--ti-common-color-text-link); + text-decoration: none; + &:hover { + color: var(--ti-common-color-text-link-hover); + text-decoration: underline; + } +} + +.current-value-container { + margin-top: 0.75rem; + padding: 1rem; + border: 1px solid #eee; + border-radius: 0.25rem; + line-height: 1.5rem; +} + +.placehoder { + color: #adb0b8; +} + +.min-w-48 { + min-width: 200px; +} + +.min-h-24 { + min-height: 100px; +} + +.link { + text-decoration: none; + color: var(--ti-common-color-text-link); +} + +.link:hover { + color: var(--ti-common-color-text-link-hover); +} + +/* From Tailwindcss */ +.absolute { + position: absolute; +} +.relative { + position: relative; +} +.fixed { + position: fixed; +} +.top-3 { + top: 0.75rem; +} +.top-10 { + top: 2.5rem; +} +.top-12 { + top: 3rem; +} +.top-40 { + top: 10rem; +} +.right-3 { + right: 0.75rem; +} +.left-10 { + left: 2.5rem; +} +.left-24 { + left: 6rem; +} +.left-28 { + left: 7rem; +} +.float-right { + float: right; +} +.float-left { + float: left; +} +.clear-both { + clear: both; +} +.m-4 { + margin: 1rem; +} +.m-6 { + margin: 1.5rem; +} +.m-20 { + margin: 5rem; +} +.my-2 { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} +.my-3 { + margin-top: 0.75rem; + margin-bottom: 0.75rem; +} +.my-4 { + margin-top: 1rem; + margin-bottom: 1rem; +} +.my-7 { + margin-top: 1.75rem; + margin-bottom: 1.75rem; +} +.mx-1 { + margin-left: 0.25rem; + margin-right: 0.25rem; +} +.mt-3 { + margin-top: 0.75rem; +} +.mt-4 { + margin-top: 1rem; +} +.mt-5 { + margin-top: 1.25rem; +} +.mt-32 { + margin-top: 8rem; +} +.mr-1 { + margin-right: 0.25rem; +} +.mr-2 { + margin-right: 0.5rem; +} +.mr-3 { + margin-right: 0.75rem; +} +.mr-4 { + margin-right: 1rem; +} +.mr-5 { + margin-right: 1.25rem; +} +.mr-7 { + margin-right: 1.75rem; +} +.mb-3 { + margin-bottom: 0.75rem; +} +.mb-4 { + margin-bottom: 1rem; +} +.mb-8 { + margin-bottom: 2rem; +} +.mb-12 { + margin-bottom: 3rem; +} +.ml-3 { + margin-left: 0.75rem; +} +.ml-4 { + margin-left: 1rem; +} +.ml-32 { + margin-left: 8rem; +} +.inline-block { + display: inline-block; +} +.flex { + display: flex; +} +.h-4 { + height: 1rem; +} +.h-5 { + height: 1.25rem; +} +.h-10 { + height: 2.5rem; +} +.h-24 { + height: 6rem; +} +.h-48 { + height: 12rem; +} +.h-80 { + height: 20rem; +} +.h-96 { + height: 24rem; +} +.h-full { + height: 100%; +} +.max-h-48 { + max-height: 12rem; +} +.w-4 { + width: 1rem; +} +.w-5 { + width: 1.25rem; +} +.w-7 { + width: 1.75rem; +} +.w-16 { + width: 4rem; +} +.w-32 { + width: 8rem; +} +.w-40 { + width: 10rem; +} +.w-48 { + width: 12rem; +} +.w-52 { + width: 13rem; +} +.w-60 { + width: 15rem; +} +.w-64 { + width: 16rem; +} +.w-96 { + width: 24rem; +} +.w-auto { + width: auto; +} +.w-2\/5 { + width: 40%; +} +.w-4\/5 { + width: 80%; +} +.w-full { + width: 100%; +} +.max-w-xs { + max-width: 20rem; +} +.max-w-xl { + max-width: 36rem; +} +.max-w-screen-md { + max-width: 768px; +} +.cursor-pointer { + cursor: pointer; +} +.cursor-move { + cursor: move; +} +.justify-between { + justify-content: space-between; +} +.rounded { + border-radius: 0.25rem; +} +.border { + border-width: 1px; +} +.border-solid { + border-style: solid; +} +.border-bottom-solid { + border-bottom-style: solid; +} +.border-gray-300 { + --tw-border-opacity: 1; + border-color: rgba(209, 213, 219, var(--tw-border-opacity)); +} +.bg-gray-300 { + --tw-bg-opacity: 1; + background-color: rgba(209, 213, 219, var(--tw-bg-opacity)); +} +.bg-gray-800 { + --tw-bg-opacity: 1; + background-color: rgba(31, 41, 55, var(--tw-bg-opacity)); +} +.bg-yellow-200 { + --tw-bg-opacity: 1; + background-color: rgba(253, 230, 138, var(--tw-bg-opacity)); +} +.bg-dark { + background-color: #464c59; +} +.bg-opacity-70 { + --tw-bg-opacity: 0.7; +} +.p-1 { + padding: 0.25rem; +} +.p-2 { + padding: 0.5rem; +} +.p-4 { + padding: 1rem; +} +.px-0 { + padding-left: 0px; + padding-right: 0px; +} +.pr-2 { + padding-right: 0.5rem; +} +.pl-2 { + padding-left: 0.5rem; +} +.text-center { + text-align: center; +} +.align-middle { + vertical-align: middle; +} +.align-bottom { + vertical-align: bottom; +} +.leading-6 { + line-height: 1.5rem; +} +.leading-10 { + line-height: 2.5rem; +} +.leading-none { + line-height: 1; +} +.text-red-500 { + --tw-text-opacity: 1; + color: rgba(239, 68, 68, var(--tw-text-opacity)); +} +.text-yellow-400 { + --tw-text-opacity: 1; + color: rgba(251, 191, 36, var(--tw-text-opacity)); +} +.text-yellow-500 { + --tw-text-opacity: 1; + color: rgba(245, 158, 11, var(--tw-text-opacity)); +} +.text-blue-500 { + --tw-text-opacity: 1; + color: rgba(59, 130, 246, var(--tw-text-opacity)); +} +.text-blue-600 { + --tw-text-opacity: 1; + color: rgba(37, 99, 235, var(--tw-text-opacity)); +} +.no-underline { + text-decoration: none; +} +.overflow-auto { + overflow: auto; +} +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} +.font-bold { + font-weight: 700; +} + +pre { + white-space: pre-wrap; + word-wrap: break-word; +} diff --git a/src/subtitle/demo/karma.conf.js b/src/subtitle/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/subtitle/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/subtitle/demo/project.json b/src/subtitle/demo/project.json new file mode 100644 index 0000000..4526b4b --- /dev/null +++ b/src/subtitle/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/subtitle/demo", + "sourceRoot": "src/subtitle/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/subtitle", + "index": "src/subtitle/demo/src/index.html", + "main": "src/subtitle/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/subtitle/demo/tsconfig.app.json", + "assets": ["src/subtitle/demo/src/favicon.ico", "src/subtitle/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "subtitle-demo:build:production" + }, + "development": { + "browserTarget": "subtitle-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js subtitle" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/subtitle/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/subtitle/demo/tsconfig.spec.json", + "karmaConfig": "src/subtitle/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/subtitle/demo/src/app/AppComponent.ts b/src/subtitle/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/subtitle/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/subtitle/demo/src/app/AppModule.ts b/src/subtitle/demo/src/app/AppModule.ts new file mode 100644 index 0000000..99b43bf --- /dev/null +++ b/src/subtitle/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SubtitleTestModule } from './subtitle/SubtitleTestModule'; + +@NgModule({ + imports: [ + SubtitleTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/subtitle/demo/src/app/IndexComponent.ts b/src/subtitle/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..f5aac77 --- /dev/null +++ b/src/subtitle/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SubtitleTestModule } from './subtitle/SubtitleTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SubtitleTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/subtitle/demo/src/app/app.html b/src/subtitle/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/subtitle/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleBasicComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleBasicComponent.ts new file mode 100644 index 0000000..c0f878e --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleBasicComponent.ts @@ -0,0 +1,34 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-basic.html' +}) +export class SubtitleBasicComponent { + items1: Array = [ + { + id: '1', + label: 'item1 test 测试长标题 测试长标题 测试长标题' + } + ]; + + items: Array = [ + { + id: '1', + label: 'item test 测试长标题 测试长标题 测试长标题' + }, + { + id: '2', + label: '弹性云服务器' + }, + { + id: '3', + label: '3' + } + ]; + + clickFn(): void { + this.items[0].label = '变更后的标题'; + } +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleBeforeSearchComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleBeforeSearchComponent.ts new file mode 100644 index 0000000..f5bc131 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleBeforeSearchComponent.ts @@ -0,0 +1,85 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiSelectComponent, TiSubtitleItem, TiSubtitleListScrollLoad } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-before-search.html' +}) +export class SubtitleBeforeSearchComponent { + private size: number = 50; + private data: Array = [ + { label: '购买弹性云服务器购买弹性云服务器购买弹性云服务器' }, + { label: '美国' }, + { label: '巴西' }, + { label: '加拿大' }, + { label: '中国' }, + { label: '法国' }, + { label: '德国' }, + { label: '韩国' }, + { label: '土耳其' }, + { label: '大不列颠和北爱兰联合王国' } + ]; + private database: Array; + totalNumber: number; + items: Array = []; + selected: TiSubtitleItem = { label: '69' + this.data[9].label }; + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + ngOnInit(): void { + this.getData(0, this.size).then((result: any) => { + this.items = result.data; + this.totalNumber = result.totalNumber; + }); + } + + beforeSearch(selectComp: TiSelectComponent): void { + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + this.getData(0, this.size, searchWord).then((result: any) => { + this.items = result.data; + this.totalNumber = result.totalNumber; + }); + } + + loadMore(scrollLoadInfo: TiSubtitleListScrollLoad): void { + const currentOptions: Array = scrollLoadInfo.selectInstance.getSearchResult(); + if (currentOptions.length >= this.totalNumber) { + return; + } + // 获取搜索内容 + const searchWord: string = scrollLoadInfo.selectInstance.getSearchWord(); + scrollLoadInfo.loading = true; + this.getData(currentOptions.length, this.size, searchWord).then((result: any) => { + this.items = [...currentOptions, ...result.data]; + scrollLoadInfo.loading = false; + }); + } + + // 模拟异步请求 + private getData(startIndex: number, size: number, searchWord?: string): Promise { + this.database = []; + for (let i: number = 0; i < 10000; i++) { + const item: any = this.data[i % 10]; + this.database.push({ ...item, label: i + item.label }); + } + + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + if (searchWord) { + const filteredResult: Array = this.database.filter((item: any) => item.label.includes(searchWord)); + const slicedResult: Array = filteredResult.slice(startIndex, startIndex + size); + const totalNumber: number = filteredResult.length; + resolve({ data: slicedResult, totalNumber }); + } else { + resolve({ + data: this.database.slice(startIndex, startIndex + size), + totalNumber: this.database.length + }); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef.markForCheck() + // 默认模式下不需要 + // 服务可根据自身变化检测策略决定是否使用该方法 + this.changeDetectorRef.markForCheck(); + }, 1000); + }); + } +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleDarkComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleDarkComponent.ts new file mode 100644 index 0000000..1a0af36 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleDarkComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-dark.html' +}) +export class SubtitleDarkComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: '弹性云服务器' + }, + { + id: '3', + label: '云服务器' + } + ]; + + toggleItems(): void { + this.items.length = 1; + } +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleEventComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleEventComponent.ts new file mode 100644 index 0000000..1d9e649 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleEventComponent.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-event.html' +}) +export class SubtitleEventComponent { + myLogs: Array = []; + + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: 'ECS-name-1' + }, + { + id: '3', + label: '云服务器' + } + ]; + + selected: TiSubtitleItem = this.items[1]; + + onBack($event: Event): void { + $event.preventDefault(); + this.myLogs = [...this.myLogs, `onBack() event`]; + } + onChange(item: TiSubtitleItem): void { + this.myLogs = [...this.myLogs, `selected change event: ${JSON.stringify(item)}`]; + } +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleIdkeyComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleIdkeyComponent.ts new file mode 100644 index 0000000..2a13942 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleIdkeyComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-idkey.html' +}) +export class SubtitleIdkeyComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: '弹性云服务器' + }, + { + id: '3', + label: '云服务器' + }, + { + id: '4', + label: '弹性云服务器' + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleItemsComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleItemsComponent.ts new file mode 100644 index 0000000..2b99210 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleItemsComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-items.html' +}) +export class SubtitleItemsComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + } + ]; + items1: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: 'ECS-name-1' + }, + { + id: '3', + label: '云服务器' + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleMaxwidthComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleMaxwidthComponent.ts new file mode 100644 index 0000000..d1c3392 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleMaxwidthComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-maxwidth.html' +}) +export class SubtitleMaxwidthComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + } + ]; + items1: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: 'ECS-name-1' + }, + { + id: '3', + label: '云服务器' + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitlePanelwidthComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitlePanelwidthComponent.ts new file mode 100644 index 0000000..00d15c6 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitlePanelwidthComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-panelwidth.html' +}) +export class SubtitlePanelwidthComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: 'ECS-name-1' + }, + { + id: '3', + label: '云服务器' + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleRouteComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleRouteComponent.ts new file mode 100644 index 0000000..a8abdd3 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleRouteComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-route.html' +}) +export class SubtitleRouteComponent { + public items1: Array = [ + { + id: '1', + label: '弹性云服务器' + } + ]; + + routerLink = ['./../subtitle-event']; + // 参数传递 + queryParams = { name: 'route' }; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleScrollLoadComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleScrollLoadComponent.ts new file mode 100644 index 0000000..d5615e8 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleScrollLoadComponent.ts @@ -0,0 +1,74 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiSubtitleItem, TiSubtitleListScrollLoad } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-scroll-load.html' +}) +export class SubtitleScrollLoadComponent { + private size: number = 20; + private data: Array = [ + { label: '购买弹性云服务器购买弹性云服务器购买弹性云服务器' }, + { label: '美国' }, + { label: '巴西' }, + { label: '加拿大' }, + { label: '中国' }, + { label: '法国' }, + { label: '德国' }, + { label: '韩国' }, + { label: '土耳其' }, + { label: '大不列颠和北爱兰联合王国' } + ]; + private database: Array; + totalNumber: number; + items: Array = []; + selected: TiSubtitleItem = { label: '0' + this.data[0].label }; + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + ngOnInit(): void { + this.getData(0, this.size).then((result: any) => { + this.items = result.data; + this.totalNumber = result.totalNumber; + }); + } + + loadMore(scrollLoadInfo: TiSubtitleListScrollLoad): void { + if (this.items.length >= this.totalNumber) { + return; + } + + scrollLoadInfo.loading = true; + this.getData(this.items.length, this.size).then((result: any) => { + this.items = [...this.items, ...result.data]; + scrollLoadInfo.loading = false; + }); + } + + // 模拟异步请求 + private getData(startIndex: number, size: number, searchWord?: string): Promise { + this.database = []; + for (let i: number = 0; i < 10000; i++) { + const item: any = this.data[i % 10]; + this.database.push({ ...item, label: i + item.label }); + } + + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + if (searchWord) { + const filteredResult: Array = this.database.filter((item: any) => item.label.includes(searchWord)); + const slicedResult: Array = filteredResult.slice(startIndex, startIndex + size); + const totalNumber: number = filteredResult.length; + resolve({ data: slicedResult, totalNumber }); + } else { + resolve({ + data: this.database.slice(startIndex, startIndex + size), + totalNumber: this.database.length + }); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef.markForCheck() + // 默认模式下不需要 + // 服务可根据自身变化检测策略决定是否使用该方法 + this.changeDetectorRef.markForCheck(); + }, 1000); + }); + } +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleSearchableComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleSearchableComponent.ts new file mode 100644 index 0000000..b44a91e --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleSearchableComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-searchable.html' +}) +export class SubtitleSearchableComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: 'ECS-name-1' + }, + { + id: '3', + label: '云服务器' + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleTargetComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleTargetComponent.ts new file mode 100644 index 0000000..c327a96 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleTargetComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-target.html' +}) +export class SubtitleTargetComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + } + ]; + items1: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: 'ECS-name-1' + }, + { + id: '3', + label: '云服务器' + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleTestModule.ts b/src/subtitle/demo/src/app/subtitle/SubtitleTestModule.ts new file mode 100644 index 0000000..e666272 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleTestModule.ts @@ -0,0 +1,85 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiSubtitleModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { SubtitleBasicComponent } from './SubtitleBasicComponent'; +import { SubtitleDarkComponent } from './SubtitleDarkComponent'; +import { SubtitleEventComponent } from './SubtitleEventComponent'; +import { SubtitleRouteComponent } from './SubtitleRouteComponent'; +import { SubtitleScrollLoadComponent } from './SubtitleScrollLoadComponent'; +import { SubtitleItemsComponent } from './SubtitleItemsComponent'; +import { SubtitleIdkeyComponent } from './SubtitleIdkeyComponent'; +import { SubtitleTargetComponent } from './SubtitleTargetComponent'; +import { SubtitleMaxwidthComponent } from './SubtitleMaxwidthComponent'; +import { SubtitlePanelwidthComponent } from './SubtitlePanelwidthComponent'; +import { SubtitleSearchableComponent } from './SubtitleSearchableComponent'; +import { SubtitleTipPositionComponent } from './SubtitleTipPositionComponent'; +import { SubtitleBeforeSearchComponent } from './SubtitleBeforeSearchComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiButtonModule, TiSubtitleModule, DemoLogModule, RouterModule.forChild(SubtitleTestModule.ROUTES)], + declarations: [ + SubtitleBasicComponent, + SubtitleDarkComponent, + SubtitleEventComponent, + SubtitleRouteComponent, + SubtitleScrollLoadComponent, + SubtitleItemsComponent, + SubtitleIdkeyComponent, + SubtitleTargetComponent, + SubtitleMaxwidthComponent, + SubtitlePanelwidthComponent, + SubtitleSearchableComponent, + SubtitleTipPositionComponent, + SubtitleBeforeSearchComponent + ] +}) +export class SubtitleTestModule { + static readonly LINKS: Array = [{ href: 'components/TiSubtitleComponent.html', label: 'Subtitle' }]; + public static readonly ROUTES: Routes = [ + { path: 'subtitle/subtitle-basic', component: SubtitleBasicComponent }, + { path: 'subtitle/subtitle-event', component: SubtitleEventComponent }, + { + path: 'subtitle/subtitle-dark', + component: SubtitleDarkComponent + }, + { + path: 'subtitle/subtitle-idkey', + component: SubtitleIdkeyComponent + }, + { + path: 'subtitle/subtitle-route', + component: SubtitleRouteComponent + }, + { + path: 'subtitle/subtitle-scroll-load', + component: SubtitleScrollLoadComponent + }, + { path: 'subtitle/subtitle-target', component: SubtitleTargetComponent }, + { path: 'subtitle/subtitle-items', component: SubtitleItemsComponent }, + { + path: 'subtitle/subtitle-maxwidth', + component: SubtitleMaxwidthComponent + }, + { + path: 'subtitle/subtitle-panelwidth', + component: SubtitlePanelwidthComponent + }, + { + path: 'subtitle/subtitle-searchable', + component: SubtitleSearchableComponent + }, + { + path: 'subtitle/subtitle-tip-position', + component: SubtitleTipPositionComponent + }, + { + path: 'subtitle/subtitle-before-search', + component: SubtitleBeforeSearchComponent + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleTipPositionComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleTipPositionComponent.ts new file mode 100644 index 0000000..17c2568 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleTipPositionComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-tip-position.html' +}) +export class SubtitleTipPositionComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器购买弹性云服务器购买弹性云服务器' + } + ]; + items1: Array = [ + { + id: '1', + label: '购买弹性云服务器购买弹性云服务器购买弹性云服务器' + }, + { + id: '2', + label: 'ECS-name-1' + }, + { + id: '3', + label: '云服务器' + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-basic.html b/src/subtitle/demo/src/app/subtitle/subtitle-basic.html new file mode 100644 index 0000000..a219f03 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-basic.html @@ -0,0 +1,40 @@ +

    1 描述

    +

    基本展示,可配置为文本类型和下拉框类型

    +

    导入

    +

    import {{ '{' }} TiSubtitleModule {{ '}' }} from '@opentiny/ng';

    +

    2 示例

    +
    +

    2.1 基础使用(文本类型的宽度可配置maxWidth接口,设置组件最大宽度自适应显示)

    +

    items 只有1项

    +

    2.1.1未配置maxWidth,默认为250px

    + +
    +

    2.1.2配置maxWidth为300px

    + +
    +
    +

    2.2 可以选择

    +

    items 有多项

    + +
    +
    +

    2.3 searchable 接口

    + +
    +
    +

    2.4 panelWidth 接口(设置下拉面板宽度)

    + +
    +
    +

    2.5 tipPosition 接口(设置标题溢出提示的方向)

    +
    +

    2.5.1 items仅一项

    + +
    +

    2.5.2 items有多项

    + +
    +

    2.6 配置maxWidth为300px

    + + + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-before-search.html b/src/subtitle/demo/src/app/subtitle/subtitle-before-search.html new file mode 100644 index 0000000..620caa7 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-before-search.html @@ -0,0 +1,8 @@ + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-dark.html b/src/subtitle/demo/src/app/subtitle/subtitle-dark.html new file mode 100644 index 0000000..9e73762 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-dark.html @@ -0,0 +1,10 @@ +
    + +
    + + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-event.html b/src/subtitle/demo/src/app/subtitle/subtitle-event.html new file mode 100644 index 0000000..7817e93 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-event.html @@ -0,0 +1,3 @@ + + + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-idkey.html b/src/subtitle/demo/src/app/subtitle/subtitle-idkey.html new file mode 100644 index 0000000..1c16ef6 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-idkey.html @@ -0,0 +1 @@ + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-items.html b/src/subtitle/demo/src/app/subtitle/subtitle-items.html new file mode 100644 index 0000000..b33e1c5 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-items.html @@ -0,0 +1,3 @@ + +
    + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-maxwidth.html b/src/subtitle/demo/src/app/subtitle/subtitle-maxwidth.html new file mode 100644 index 0000000..ad0528f --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-maxwidth.html @@ -0,0 +1,3 @@ + +
    + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-panelwidth.html b/src/subtitle/demo/src/app/subtitle/subtitle-panelwidth.html new file mode 100644 index 0000000..cb396eb --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-panelwidth.html @@ -0,0 +1 @@ + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-route.html b/src/subtitle/demo/src/app/subtitle/subtitle-route.html new file mode 100644 index 0000000..351421f --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-route.html @@ -0,0 +1,14 @@ +

    1 描述

    +

    backRouterLink:设置图标点击后跳转路由,用于应用内跳转。如果配置了该参数,href和targe参数失效。

    +

    queryParams: 设置跳转路由参数。

    +

    10.1.2版本接口routerLink变更为backRouterLink

    + +

    2 示例

    +
    +

    2.1 基础使用

    +

    + +
    +
    +

    2.2 设置routerLink后href是否有效

    + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-scroll-load.html b/src/subtitle/demo/src/app/subtitle/subtitle-scroll-load.html new file mode 100644 index 0000000..fd1ec57 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-scroll-load.html @@ -0,0 +1 @@ + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-searchable.html b/src/subtitle/demo/src/app/subtitle/subtitle-searchable.html new file mode 100644 index 0000000..065c920 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-searchable.html @@ -0,0 +1 @@ + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-target.html b/src/subtitle/demo/src/app/subtitle/subtitle-target.html new file mode 100644 index 0000000..6f17f64 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-target.html @@ -0,0 +1,3 @@ + +
    + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-tip-position.html b/src/subtitle/demo/src/app/subtitle/subtitle-tip-position.html new file mode 100644 index 0000000..34d63fc --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-tip-position.html @@ -0,0 +1,3 @@ + +
    + diff --git a/src/subtitle/demo/src/app/subtitle/webdoc/subtitle-demos.js b/src/subtitle/demo/src/app/subtitle/webdoc/subtitle-demos.js new file mode 100644 index 0000000..64e1cf0 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/webdoc/subtitle-demos.js @@ -0,0 +1,147 @@ +export default { + column: '2', + demos: [ + { + demoId: 'subtitle-items', + name: { + 'zh-CN': '基本使用', + 'en-US': 'base', + }, + desc: { + 'zh-CN': '

    标题数据集,如果只有一个选项则标题以文本形式呈现,如果选项大于1个,标题以下拉的形式呈现。

    ', + 'en-US': '', + }, + apis: [ + 'TiSubtitleComponent.properties.href', + 'TiSubtitleComponent.properties.items' + ] + }, + { + demoId: 'subtitle-target', + name: { + 'zh-CN': '跳转路径', + 'en-US': 'dark', + }, + desc: { + 'zh-CN': '

    通过属性target配置在何处打开链接。

    ', + 'en-US': '', + }, + apis: [ + 'TiSubtitleComponent.properties.target' + ] + }, + { + demoId: 'subtitle-tip-position', + name: { + 'zh-CN': 'tip方向', + 'en-US': 'dark', + }, + desc: { + 'zh-CN': '

    通过属性tipPosition配置标题溢出提示的方向。

    ', + 'en-US': '', + }, + apis: [ + 'TiSubtitleComponent.properties.tipPosition' + ] + }, + { + demoId: 'subtitle-dark', + name: { + 'zh-CN': '暗色背景', + 'en-US': 'dark', + }, + desc: { + 'zh-CN': '

    通过属性dark配置组件在暗色场景呈现。

    ', + 'en-US': '', + } + }, + { + demoId: 'subtitle-maxwidth', + name: { + 'zh-CN': '最大宽度', + 'en-US': 'maxWidth', + }, + desc: { + 'zh-CN': '

    通过属性maxWidth配置组件最大宽度。

    ', + 'en-US': '', + }, + apis: ['TiSubtitleComponent.properties.maxWidth'] + }, + { + demoId: 'subtitle-panelwidth', + name: { + 'zh-CN': '面板最大宽度', + 'en-US': 'panelwidth' + }, + desc: { + 'zh-CN': '

    通过属性panelWidth配置下拉面板宽度。

    ', + 'en-US': '' + }, + apis: ['TiSubtitleComponent.properties.panelWidth'] + }, + { + demoId: 'subtitle-searchable', + name: { + 'zh-CN': '搜索', + 'en-US': 'searchable' + }, + desc: { + 'zh-CN': '

    通过属性searchable配置items大于1项时,展开面板中是否有搜索。

    ', + 'en-US': '' + }, + apis: ['TiSubtitleComponent.properties.searchable'] + }, + { + demoId: 'subtitle-idkey', + name: { + 'zh-CN': '唯一值', + 'en-US': 'idKey' + }, + desc: { + 'zh-CN': '

    通过属性idKey设置唯一属性,实现下拉选中。

    ', + 'en-US': '' + }, + apis: ['TiSubtitleComponent.properties.idKey'] + }, + { + demoId: 'subtitle-scroll-load', + name: { + 'zh-CN': '下拉加载', + 'en-US': 'scrollToBottom' + }, + desc: { + 'zh-CN': '

    当下拉列表数据量大时,先请求部分数据,等滚动条滚动到底部时利用scrollToBottom事件接口提供的时机再一次次去加载后面的数据。

    ', + 'en-US': '' + }, + apis: ['TiSubtitleComponent.events.scrollToBottom'] + }, + { + demoId: 'subtitle-before-search', + name: { + 'zh-CN': '后台搜索', + 'en-US': 'beforeSearch', + }, + desc: { + 'zh-CN': '

    必须搭配scrollToBottom事件接口使用,后台搜索传出下拉搜索框的值,使用了该事件接口,组件内部将不再进行搜索,搜索需由业务在该事件回调中进行(后台搜索),将搜索后的数据传给items接口。

    ', + 'en-US': '', + }, + apis: ['TiSubtitleComponent.events.beforeSearch'] + }, + { + demoId: 'subtitle-event', + name: { + 'zh-CN': '事件', + 'en-US': 'events', + }, + desc: { + 'zh-CN': '

    当点击后退按钮的时候触发back事件,当选中项变化的时候触发selectedChange事件。

    ', + 'en-US': '', + }, + apis: [ + 'TiSubtitleComponent.properties.selected', + 'TiSubtitleComponent.events.back', + 'TiSubtitleComponent.events.selectedChange' + ] + }, + ] +} \ No newline at end of file diff --git a/src/subtitle/demo/src/app/subtitle/webdoc/subtitle.cn.md b/src/subtitle/demo/src/app/subtitle/webdoc/subtitle.cn.md new file mode 100644 index 0000000..b194151 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/webdoc/subtitle.cn.md @@ -0,0 +1,23 @@ +--- +title: Subtitle 返回标题 +--- +# Subtitle 返回标题 + +
    + +Subtitle 用于显示当前页面在系统层级结构中的位置,并能向上返回。   + +```typescript +import { TiSubtitleModule } from '@opentiny/ng'; +``` + +
    + +
    + +Subtitle 用于显示当前页面在系统层级结构中的位置,并能向上返回。   + +```typescript +import { TiSubtitleModule } from '@opentiny/ng'; +``` +
    diff --git a/src/subtitle/demo/src/app/subtitle/webdoc/subtitle.en.md b/src/subtitle/demo/src/app/subtitle/webdoc/subtitle.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/webdoc/subtitle.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/subtitle/demo/src/favicon.ico b/src/subtitle/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/subtitle/demo/src/index.html b/src/subtitle/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/subtitle/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/subtitle/demo/src/main.ts b/src/subtitle/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/subtitle/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/subtitle/demo/test.ts b/src/subtitle/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/subtitle/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/subtitle/demo/tsconfig.app.json b/src/subtitle/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/subtitle/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/subtitle/demo/tsconfig.spec.json b/src/subtitle/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/subtitle/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/subtitle/lib/index.ts b/src/subtitle/lib/index.ts new file mode 100644 index 0000000..be9900a --- /dev/null +++ b/src/subtitle/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSubtitleModule'; diff --git a/src/subtitle/lib/ng-package.json b/src/subtitle/lib/ng-package.json new file mode 100644 index 0000000..ab082c8 --- /dev/null +++ b/src/subtitle/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/subtitle", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/subtitle/lib/package.json b/src/subtitle/lib/package.json new file mode 100644 index 0000000..c526a41 --- /dev/null +++ b/src/subtitle/lib/package.json @@ -0,0 +1,18 @@ +{ + "name": "@opentiny/ng-subtitle", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/router": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-list": "~1.0.0-beta.0", + "@opentiny/ng-select": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-tip": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/subtitle/lib/project.json b/src/subtitle/lib/project.json new file mode 100644 index 0000000..7da3405 --- /dev/null +++ b/src/subtitle/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/subtitle/lib", + "sourceRoot": "src/subtitle/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/subtitle"], + "options": { + "project": "src/subtitle/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/subtitle"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js subtitle" + }, + { + "command": "ng default-build subtitle" + }, + { + "command": "node build/clear-default-theme.js subtitle" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/subtitle && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build subtitle && ng pack subtitle && node build/publish.js subtitle --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/subtitle/lib/src/TiSubtitleComponent.ts b/src/subtitle/lib/src/TiSubtitleComponent.ts new file mode 100644 index 0000000..3f7cdc2 --- /dev/null +++ b/src/subtitle/lib/src/TiSubtitleComponent.ts @@ -0,0 +1,233 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Input, + Output, + Renderer2, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { Params } from '@angular/router'; + +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiListScrollLoad } from '@opentiny/ng-list'; +import { TiSelectComponent } from '@opentiny/ng-select'; +import { TiPositionType } from '@opentiny/ng-utils'; +import { Util } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; +/** + * items接口item项的数据接口 + */ +export interface TiSubtitleItem { + label: string; + [propName: string]: any; +} + +/** + * scrollToBottom 事件回调参数 + * + */ +export interface TiSubtitleListScrollLoad extends TiListScrollLoad { + /** + * subtile 中 下拉选择部分 select 组件实例 + */ + selectInstance?: TiSelectComponent; +} + +@Component({ + selector: 'ti-subtitle', + templateUrl: './subtitle.html', + styleUrls: ['./subtitle.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) + +/** + * 返回标题组件 + * + */ +export class TiSubtitleComponent extends TiBaseComponent { + /** + * 后退按钮链接 + */ + @Input() href: string = ''; + /** + * 标题数据集,如果选项大于 1 个,标题是下拉的形式 + */ + @Input() items: Array = []; + /** + * 下拉面板中是否有搜索框 + */ + @Input() searchable: boolean = false; + /** + * 指定在何处打开链接, 即 a 标签的 target 属性 + */ + @Input() target: '_blank' | '_self' | '_parent' | '_top' = '_blank'; + // select组件选中项模板引用名为#selected, 两个名字冲突,所以使用了别名 + /** + * 选中项,items 的条数大于 1 时才生效 + */ + @Input('selected') selectedItem: TiSubtitleItem; + /** + * 后退按钮点击后的跳转路由, 设置后 href 和 target 失效 + */ + @Input() backRouterLink: string | Array; + /** + * 后退按钮点击后的跳转路由参数 + */ + @Input() queryParams: Params; + /** + * 下拉面板宽度 + */ + @Input() panelWidth: string; + /** + * 最大宽度 + */ + @Input() maxWidth: string = '250px'; + /** + * 标题溢出提示的方向 + */ + @Input() tipPosition: TiPositionType = 'top'; + /** + * @ignore + * + * idKey指定的属性的值相等时即认为select的 option 选项是选中的。选中项 ngModel 中的数据(modelOption对象)跟 options 数据集中的选项(option对象)之间对应相等关系的依据属性。当 + * modelOption中的 idKey 设置的属性的值 与 option 中的 idKey 设置的属性的值相等时,则认为 modelOption 和 option 是对应的相等关系,即认为 option 选项是选中的。 + * + * 默认当 modelOption === option 或者 modelOption 中的 labelKey 设置的属性的值 与 option 中的 labelKey 设置的属性的值相等时,则认为 option 选项是选中的。 + */ + + /** + * 设置数据唯一标识的键值,默认为 labelKey 的接口值 + */ + @Input() idKey: string; + /** + * 选中项变更时触发的回调,items 的条数大于 1 时才生效 + */ + @Output() readonly selectedChange: EventEmitter = new EventEmitter(); + /** + * 点击后退按钮时触发的回调 + */ + @Output() readonly back: EventEmitter = new EventEmitter(); + /** + * + * 必须搭配 scrollToBottom 事件接口使用。后台搜索,传出下拉中搜索框的值。 + * + * 一旦使用该事件接口,组件内部将不再进行搜索,搜索需由业务在该事件时回调中进行(后台搜索),将搜索后的数据传给 items 接口。 + */ + @Output() readonly beforeSearch: EventEmitter = new EventEmitter(); + /** + * 下拉列表滚动到底部的回调 + */ + @Output() readonly scrollToBottom: EventEmitter = new EventEmitter(); + + /** + * @ignore + */ + @ViewChild(TiSelectComponent, { static: false }) + selectComRef: TiSelectComponent; + private hasSelectCom: boolean = false; + /** + * @ignore + */ + public originalItems: Array; // 后台搜索时需要该变量 + /** + * @ignore + * + * items最大宽度 + */ + public itemsMaxWidth: string = ''; + protected versionInfo: string = super.getVersion(packageInfo); + constructor(private element: ElementRef, private renderer2: Renderer2, private changeDetectorRef: ChangeDetectorRef) { + super(element, renderer2); + } + ngOnInit(): void { + super.ngOnInit(); + // 左侧图标的宽度 + const iconWidth: string = getComputedStyle(this.nativeElement).getPropertyValue('--ti3-subtitle-icon-width'); + // 右侧items的最大宽度 + this.itemsMaxWidth = `calc(${this.maxWidth} - ${iconWidth})`; + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + // 如果没有传入选中项,且items 条目数大于1 则默认选中第一项 + if (changes['items'] && Util.isUndefined(this.selectedItem) && this.items?.length > 1) { + this.selectedItem = this.items[0]; + } + + if (this.searchable && this.beforeSearch.observers.length > 0 && !this.originalItems && this.items?.length > 1) { + this.originalItems = this.items; + } + } + + ngDoCheck(): void { + super.ngDoCheck(); + this.changeDetectorRef.markForCheck(); + } + + /** + * @ignore + * 触发back 事件 + */ + public onClick($event: Event): void { + this.back.emit($event); + } + /** + * @ignore + * select选中项改变,触发subtitle selectedChange事件 + */ + public onSelectedOptionChange(option: any): void { + this.selectedChange.emit(option as TiSubtitleItem); + } + /** + * @ignore + */ + public isMoreThanOneItem(): boolean { + let result: boolean = false; + // 后台搜索时,业务会将后台搜索结果赋值给items,如果搜索结果数据条数小于1条,不能认为是实际原有的数据总数就小于1条 + if ((this.searchable && this.beforeSearch.observers.length > 0 && this.originalItems?.length > 1) || this.items?.length > 1) { + result = true; + if (!this.hasSelectCom) { + setTimeout(() => { + // 暗色背景,给ti-select 添加dark属性 + if (this.selectComRef?.nativeElement && !Util.isUndefined(this.hostRef?.nativeElement?.attributes?.['dark'])) { + this.renderer.setAttribute(this.selectComRef.nativeElement, 'dark', ''); + } + }, 0); + } + this.hasSelectCom = true; + } else { + this.hasSelectCom = false; + } + + return result; + } + /** + * @ignore + */ + public onBeforeSearch(event: TiSelectComponent): void { + this.beforeSearch.emit(event); + } + /** + * @ignore + * select下拉列表滚动条滚动到底部时,触发 subtitle 的 scrollToBottom 事件 + */ + public onScrollToBottom(info: TiListScrollLoad, selectCom: TiSelectComponent): void { + info['selectInstance'] = selectCom; + this.scrollToBottom.emit(info); + } +} diff --git a/src/subtitle/lib/src/TiSubtitleModule.ts b/src/subtitle/lib/src/TiSubtitleModule.ts new file mode 100644 index 0000000..46cbe48 --- /dev/null +++ b/src/subtitle/lib/src/TiSubtitleModule.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; + +import { TiSubtitleComponent, TiSubtitleListScrollLoad } from './TiSubtitleComponent'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiSelectModule } from '@opentiny/ng-select'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiTipModule } from '@opentiny/ng-tip'; + +@NgModule({ + imports: [CommonModule, FormsModule, RouterModule, TiOverflowModule, TiSelectModule, TiTipModule, TiIconModule], + exports: [TiSubtitleComponent], + declarations: [TiSubtitleComponent] +}) +export class TiSubtitleModule {} +export { TiSubtitleComponent, TiSubtitleItem, TiSubtitleListScrollLoad } from './TiSubtitleComponent'; diff --git a/src/subtitle/lib/src/subtitle.html b/src/subtitle/lib/src/subtitle.html new file mode 100644 index 0000000..877450c --- /dev/null +++ b/src/subtitle/lib/src/subtitle.html @@ -0,0 +1,73 @@ +
    + + + + + +
    + {{items[0]?.label}} +
    + + + {{item?.label}} + + + {{item?.label}} + + +
    +
    diff --git a/src/subtitle/lib/src/subtitle.less b/src/subtitle/lib/src/subtitle.less new file mode 100644 index 0000000..ed3c706 --- /dev/null +++ b/src/subtitle/lib/src/subtitle.less @@ -0,0 +1,106 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-subtitle-height: var(--ti-common-size-6x); + --ti-subtitle-font-weight: var(--ti-common-font-weight-7); + --ti-subtitle-icon-size: var(--ti-common-size-4x); + --ti-subtitle-icon-padding-left: var(--ti-common-space-6); + --ti-subtitle-icon-padding-right: var(--ti-common-space-2x); + // 左侧按钮区域宽度 = 按钮尺寸 + 左边距 + 右边距。注意:该变量在ts中使用到,用于设置最大宽度 + --ti-subtitle-icon-width: calc( + var(--ti-subtitle-icon-size) + var(--ti-subtitle-icon-padding-left) + var(--ti-subtitle-icon-padding-right) + ); +} + +:host { + display: inline-block; + height: var(--ti-subtitle-height); + line-height: var(--ti-subtitle-height); +} +.ti3-subtitle-container { + display: flex; + align-items: center; +} +//后退按钮 +.ti3-subtitle-icon-back { + position: relative; + display: inline-block; + font-size: var(--ti-subtitle-icon-size); + padding: 0 var(--ti-subtitle-icon-padding-right) 0 var(--ti-subtitle-icon-padding-left); + color: var(--ti-common-color-icon-normal); + font-weight: var(--ti-subtitle-font-weight); + vertical-align: top; + text-decoration: none; + cursor: pointer; + .box-sizing(border-box); + &:hover { + color: var(--ti-common-color-icon-hover); + } + &:after { + content: ''; + position: absolute; + height: var(--ti-subtitle-icon-size); + right: 0; + top: calc((var(--ti-subtitle-height) - var(--ti-subtitle-icon-size)) / 2); + border-right: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + } +} + +// 单文本标题 +.ti3-subtitle-text { + padding-left: var(--ti-common-space-10); + font-size: var(--ti-common-font-size-2); + color: var(--ti-common-color-text-primary); + font-weight: var(--ti-subtitle-font-weight); + .box-sizing(border-box); + cursor: default; +} + +// 覆盖select样式适配subtitle +:host ::ng-deep ti-select { + height: var(--ti-subtitle-height); + line-height: var(--ti-subtitle-height); + width: 100%; + .ti3-overflow-padding { + padding-right: 0; + } + & ::ng-deep ti-dominator.ti3-select-dominator-container { + vertical-align: top; + height: var(--ti-subtitle-height); + line-height: var(--ti-subtitle-height); + --ti-dominator-container-height: var(--ti-subtitle-height); + } +} +:host .ti3-subtitle-selected-text { + width: 100%; + font-size: var(--ti-common-font-size-2); + font-weight: var(--ti-subtitle-font-weight); +} + +// 暗色主题 +:host[dark] { + .ti3-subtitle-icon-back { + color: var(--ti-common-color-icon-white); + &:after { + opacity: 0.2; + } + &:hover { + color: var(--ti-common-color-icon-hover); + } + } + .ti3-subtitle-text { + color: var(--ti-common-color-text-white); + } + + & ::ng-deep ti-select { + background-color: transparent; + border-color: transparent; + &:hover, + &[tiFocused] { + border-color: transparent; + } + } + .ti3-subtitle-selected-text { + color: var(--ti-common-color-text-white); + } +} diff --git a/src/swiper/demo/karma.conf.js b/src/swiper/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/swiper/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/swiper/demo/project.json b/src/swiper/demo/project.json new file mode 100644 index 0000000..d8f29a0 --- /dev/null +++ b/src/swiper/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/swiper/demo", + "sourceRoot": "src/swiper/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/swiper", + "index": "src/swiper/demo/src/index.html", + "main": "src/swiper/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/swiper/demo/tsconfig.app.json", + "assets": ["src/swiper/demo/src/favicon.ico", "src/swiper/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "swiper-demo:build:production" + }, + "development": { + "browserTarget": "swiper-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js swiper" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/swiper/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/swiper/demo/tsconfig.spec.json", + "karmaConfig": "src/swiper/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/swiper/demo/src/app/AppComponent.ts b/src/swiper/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/swiper/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/swiper/demo/src/app/AppModule.ts b/src/swiper/demo/src/app/AppModule.ts new file mode 100644 index 0000000..f8a4314 --- /dev/null +++ b/src/swiper/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SwiperTestModule } from './swiper/SwiperTestModule'; + +@NgModule({ + imports: [ + SwiperTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/swiper/demo/src/app/IndexComponent.ts b/src/swiper/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..f61f319 --- /dev/null +++ b/src/swiper/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SwiperTestModule } from './swiper/SwiperTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SwiperTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/swiper/demo/src/app/app.html b/src/swiper/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/swiper/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/swiper/demo/src/app/swiper/SwiperActiveindexComponent.ts b/src/swiper/demo/src/app/swiper/SwiperActiveindexComponent.ts new file mode 100644 index 0000000..bcb0463 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperActiveindexComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-activeindex.html', + styleUrls: ['./swiper.less'] +}) +export class SwiperActiveindexComponent { + items = ['First', 'Second', 'Third', 'Fourth']; + activeIndex: number = 2; +} diff --git a/src/swiper/demo/src/app/swiper/SwiperAutoplayComponent.ts b/src/swiper/demo/src/app/swiper/SwiperAutoplayComponent.ts new file mode 100644 index 0000000..3a4312d --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperAutoplayComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-autoplay.html', + styleUrls: ['./swiper.less'] +}) +export class SwiperAutoplayComponent { + items = ['First', 'Second', 'Third', 'Fourth']; +} diff --git a/src/swiper/demo/src/app/swiper/SwiperBasicComponent.ts b/src/swiper/demo/src/app/swiper/SwiperBasicComponent.ts new file mode 100644 index 0000000..2a072c0 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperBasicComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-basic.html', + styleUrls: ['./swiper.less'] +}) +export class SwiperBasicComponent { + items = ['First', 'Second', 'Third', 'Fourth']; +} diff --git a/src/swiper/demo/src/app/swiper/SwiperEventsComponent.ts b/src/swiper/demo/src/app/swiper/SwiperEventsComponent.ts new file mode 100644 index 0000000..b921387 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperEventsComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-events.html', + styleUrls: ['./swiper.less'] +}) +export class SwiperEventsComponent { + activeIndex: number = 1; + myLogs: Array = []; + items: Array = ['First', 'Second', 'Third', 'Fourth']; + activeIndexChange = (currentPage: number): void => { + this.myLogs = [...this.myLogs, `activeIndex: ${currentPage}`]; + }; +} diff --git a/src/swiper/demo/src/app/swiper/SwiperIndicatorpositionComponent.ts b/src/swiper/demo/src/app/swiper/SwiperIndicatorpositionComponent.ts new file mode 100644 index 0000000..01f097a --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperIndicatorpositionComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-indicatorposition.html', + styleUrls: ['./swiper.less'] +}) +export class SwiperIndicatorpositionComponent { + items = ['First', 'Second', 'Third', 'Fourth']; +} diff --git a/src/swiper/demo/src/app/swiper/SwiperLoopComponent.ts b/src/swiper/demo/src/app/swiper/SwiperLoopComponent.ts new file mode 100644 index 0000000..9237b52 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperLoopComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-loop.html', + styleUrls: ['./swiper.less'] +}) +export class SwiperLoopComponent { + items = ['First', 'Second', 'Third', 'Fourth']; +} diff --git a/src/swiper/demo/src/app/swiper/SwiperShowcardnumComponent.ts b/src/swiper/demo/src/app/swiper/SwiperShowcardnumComponent.ts new file mode 100644 index 0000000..585443d --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperShowcardnumComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-showcardnum.html', + styleUrls: ['./swiper.less'] +}) +export class SwiperShowcardnumComponent { + cards: Array = [ + { text: 'card-0' }, + { text: 'card-1' }, + { text: 'card-2' }, + { text: 'card-3' }, + { text: 'card-4' }, + { text: 'card-5' }, + { text: 'card-6' }, + { text: 'card-7' } + ]; +} diff --git a/src/swiper/demo/src/app/swiper/SwiperShowcardnumTestComponent.ts b/src/swiper/demo/src/app/swiper/SwiperShowcardnumTestComponent.ts new file mode 100644 index 0000000..321da35 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperShowcardnumTestComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-showcardnum-test.html' +}) +export class SwiperShowcardnumTestComponent { + public newCards: Array = [{ text: 'card-0' }, { text: 'card-1' }, { text: 'card-2' }, { text: 'card-3' }]; + public newCards1: Array = [ + { text: 'card-0' }, + { text: 'card-1' }, + { text: 'card-2' }, + { text: 'card-3' }, + { text: 'card-4' }, + { text: 'card-5' }, + { text: 'card-6' }, + { text: 'card-7' }, + { text: 'card-8' } + ]; + + // 当前激活卡片索引 + public currentIndex: number = 1; + public currentIndex1: number = 2; + + // 激活卡片 + public activeCard(index: number): void { + this.currentIndex = index; + } + + public activeCard1(index: number): void { + this.currentIndex1 = index; + } +} diff --git a/src/swiper/demo/src/app/swiper/SwiperTestModule.ts b/src/swiper/demo/src/app/swiper/SwiperTestModule.ts new file mode 100644 index 0000000..53cbe64 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperTestModule.ts @@ -0,0 +1,76 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiCardModule, TiCheckboxModule, TiModalModule, TiSwiperModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { SwiperBasicComponent } from './SwiperBasicComponent'; +import { SwiperShowcardnumComponent } from './SwiperShowcardnumComponent'; +import { SwiperActiveindexComponent } from './SwiperActiveindexComponent'; +import { SwiperEventsComponent } from './SwiperEventsComponent'; +import { SwiperAutoplayComponent } from './SwiperAutoplayComponent'; +import { SwiperLoopComponent } from './SwiperLoopComponent'; +import { SwiperIndicatorpositionComponent } from './SwiperIndicatorpositionComponent'; +import { SwiperShowcardnumTestComponent } from './SwiperShowcardnumTestComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiSwiperModule, + TiButtonModule, + TiModalModule, + TiCardModule, + TiCheckboxModule, + DemoLogModule, + RouterModule.forChild(SwiperTestModule.ROUTES) + ], + declarations: [ + SwiperBasicComponent, + SwiperShowcardnumComponent, + SwiperActiveindexComponent, + SwiperEventsComponent, + SwiperAutoplayComponent, + SwiperLoopComponent, + SwiperIndicatorpositionComponent, + SwiperShowcardnumTestComponent + ] +}) +export class SwiperTestModule { + public static readonly ROUTES: Routes = [ + { + path: 'swiper/swiper-basic', + component: SwiperBasicComponent + }, + { + path: 'swiper/swiper-showcardnum', + component: SwiperShowcardnumComponent + }, + { + path: 'swiper/swiper-activeindex', + component: SwiperActiveindexComponent + }, + { + path: 'swiper/swiper-events', + component: SwiperEventsComponent + }, + { + path: 'swiper/swiper-autoplay', + component: SwiperAutoplayComponent + }, + { + path: 'swiper/swiper-loop', + component: SwiperLoopComponent + }, + { + path: 'swiper/swiper-showcardnum-test', + component: SwiperShowcardnumTestComponent + }, + { + path: 'swiper/swiper-indicatorposition', + component: SwiperIndicatorpositionComponent + } + ]; +} diff --git a/src/swiper/demo/src/app/swiper/swiper-activeindex.html b/src/swiper/demo/src/app/swiper/swiper-activeindex.html new file mode 100644 index 0000000..95e196f --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-activeindex.html @@ -0,0 +1,10 @@ +
    +
    Current activeIndex: {{ activeIndex | json }}
    +
    + + +
    + {{item}} +
    +
    +
    diff --git a/src/swiper/demo/src/app/swiper/swiper-autoplay.html b/src/swiper/demo/src/app/swiper/swiper-autoplay.html new file mode 100644 index 0000000..fed8fd7 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-autoplay.html @@ -0,0 +1,7 @@ + + +
    + {{item}} +
    +
    +
    diff --git a/src/swiper/demo/src/app/swiper/swiper-basic.html b/src/swiper/demo/src/app/swiper/swiper-basic.html new file mode 100644 index 0000000..eb28418 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-basic.html @@ -0,0 +1,7 @@ + + +
    + {{item}} +
    +
    +
    diff --git a/src/swiper/demo/src/app/swiper/swiper-events.html b/src/swiper/demo/src/app/swiper/swiper-events.html new file mode 100644 index 0000000..e9d9288 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-events.html @@ -0,0 +1,8 @@ + + +
    + {{item}} +
    +
    +
    + diff --git a/src/swiper/demo/src/app/swiper/swiper-indicatorposition.html b/src/swiper/demo/src/app/swiper/swiper-indicatorposition.html new file mode 100644 index 0000000..8ef002e --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-indicatorposition.html @@ -0,0 +1,27 @@ +

    1.默认值为‘below’,指示器在容器外部

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

    2.设置值为‘bottom’,指示器在容器底部

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

    3.设置值为‘none’,不显示指示器

    +
    + + +
    + {{item}} +
    +
    +
    diff --git a/src/swiper/demo/src/app/swiper/swiper-loop.html b/src/swiper/demo/src/app/swiper/swiper-loop.html new file mode 100644 index 0000000..f73e018 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-loop.html @@ -0,0 +1,7 @@ + + +
    + {{item}} +
    +
    +
    diff --git a/src/swiper/demo/src/app/swiper/swiper-showcardnum-test.html b/src/swiper/demo/src/app/swiper/swiper-showcardnum-test.html new file mode 100644 index 0000000..a52f310 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-showcardnum-test.html @@ -0,0 +1,36 @@ +

    1.描述

    +

    showCardNum接口设置每页展示卡片数量

    +

    2.示例,查看最后一页是否存在显示不全,或者两个卡片没有间隙的情况

    +

    1.共4张卡片,每页展示3张卡片

    +
    +
    + + + +

    {{card.text}}

    +
    +
    +
    +
    +

    +

    2.共9张卡片,每页展示5张卡片

    +
    +
    + + + +

    {{card.text}}

    +
    +
    +
    +
    + diff --git a/src/swiper/demo/src/app/swiper/swiper-showcardnum.html b/src/swiper/demo/src/app/swiper/swiper-showcardnum.html new file mode 100644 index 0000000..bc3fabf --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-showcardnum.html @@ -0,0 +1,7 @@ + + +
    +

    {{card.text}}

    +
    +
    +
    diff --git a/src/swiper/demo/src/app/swiper/swiper.less b/src/swiper/demo/src/app/swiper/swiper.less new file mode 100644 index 0000000..5cc4d96 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper.less @@ -0,0 +1,13 @@ +ti-swipercard:nth-child(odd) div { + background-color: #beccfa; +} +ti-swipercard:nth-child(even) div { + background-color: #dfe1e6; +} +ti-swipercard div { + height: 180px; + line-height: 180px; + font-size: 20px; + text-align: center; + border-radius: 2px; +} diff --git a/src/swiper/demo/src/app/swiper/webdoc/swiper-demos.js b/src/swiper/demo/src/app/swiper/webdoc/swiper-demos.js new file mode 100644 index 0000000..c3b13bb --- /dev/null +++ b/src/swiper/demo/src/app/swiper/webdoc/swiper-demos.js @@ -0,0 +1,96 @@ +export default { + column: '2', + demos: [ + { + demoId: 'swiper-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'swiper basic' + }, + desc: { + 'zh-CN': '

    Swiper 组件的最简用法。

    ', + 'en-US': 'swiper basic' + }, + codeFiles: ['swiper-basic.html', 'SwiperBasicComponent.ts', 'swiper.less'] + }, + { + demoId: 'swiper-loop', + name: { + 'zh-CN': '禁止无限循环', + 'en-US': 'swiper loop' + }, + desc: { + 'zh-CN': '

    通过属性loop配置是否无限循环。

    ', + 'en-US': 'swiper loop' + }, + codeFiles: ['swiper-loop.html', 'SwiperLoopComponent.ts', 'swiper.less'], + apis: ['TiSwiperComponent.properties.loop'] + }, + { + demoId: 'swiper-autoplay', + name: { + 'zh-CN': '自动播放', + 'en-US': 'swiper autoplay' + }, + desc: { + 'zh-CN': '<>通过属性autoplay配置是否自动播放。

    ', + 'en-US': 'swiper autoplay' + }, + codeFiles: ['swiper-autoplay.html', 'SwiperAutoplayComponent.ts', 'swiper.less'], + apis: ['TiSwiperComponent.properties.autoplay', 'TiSwiperComponent.properties.autoplaySpeed'] + }, + { + demoId: 'swiper-activeindex', + name: { + 'zh-CN': '指定当前展示项', + 'en-US': 'swiper activeindex' + }, + desc: { + 'zh-CN': '

    通过属性activeindex配置当前展示项。

    ', + 'en-US': 'swiper activeindex' + }, + codeFiles: ['swiper-activeindex.html', 'SwiperActiveindexComponent.ts', 'swiper.less'], + + apis: ['TiSwiperComponent.properties.activeIndex'] + }, + { + demoId: 'swiper-showcardnum', + name: { + 'zh-CN': '每页展示卡片数', + 'en-US': 'swiper showcardnum' + }, + desc: { + 'zh-CN': '

    通过属性showcardnum配置每页展示卡片数。

    ', + 'en-US': 'swiper showcardnum' + }, + codeFiles: ['swiper-showcardnum.html', 'SwiperShowcardnumComponent.ts', 'swiper.less'], + apis: ['TiSwiperComponent.properties.showCardNum', 'TiSwiperComponent.properties.cardGap'] + }, + { + demoId: 'swiper-indicatorposition', + name: { + 'zh-CN': '指示器位置', + 'en-US': 'swiper indicatorposition' + }, + desc: { + 'zh-CN': '

    通过属性indicatorposition配置指示器位置。

    ', + 'en-US': 'swiper indicatorposition' + }, + codeFiles: ['swiper-indicatorposition.html', 'SwiperIndicatorpositionComponent.ts', 'swiper.less'], + apis: ['TiSwiperComponent.properties.indicatorPosition'] + }, + { + demoId: 'swiper-events', + name: { + 'zh-CN': '事件', + 'en-US': 'swiper events' + }, + desc: { + 'zh-CN': '

    activeIndex改变时触发的回调,参数为改变后的activeIndex

    ', + 'en-US': 'swiper events' + }, + codeFiles: ['swiper-events.html', 'SwiperEventsComponent.ts', 'swiper.less'], + apis: ['TiSwiperComponent.properties.'] + } + ] +}; diff --git a/src/swiper/demo/src/app/swiper/webdoc/swiper.cn.md b/src/swiper/demo/src/app/swiper/webdoc/swiper.cn.md new file mode 100644 index 0000000..7229b04 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/webdoc/swiper.cn.md @@ -0,0 +1,31 @@ +--- +title: Swiper 轮播 +--- +# Swiper 轮播 + +
    + +播放一组内容。 + ++ ``是轮播的容器;``是每个轮播内容的容器。 ++ 一组轮播由一个``嵌套多个``组成。 + +```typescript +import { TiSwiperModule } from '@opentiny/ng'; +``` + +
    + +
    +div class="used-tiny"> + +播放一组内容。 + ++ ``是轮播的容器;``是每个轮播内容的容器。 ++ 一组轮播由一个``嵌套多个``组成。 + +```typescript +import { TiSwiperModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/swiper/demo/src/app/swiper/webdoc/swiper.en.md b/src/swiper/demo/src/app/swiper/webdoc/swiper.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/webdoc/swiper.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/swiper/demo/src/favicon.ico b/src/swiper/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/swiper/demo/src/index.html b/src/swiper/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/swiper/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/swiper/demo/src/main.ts b/src/swiper/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/swiper/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/swiper/demo/test.ts b/src/swiper/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/swiper/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/swiper/demo/tsconfig.app.json b/src/swiper/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/swiper/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/swiper/demo/tsconfig.spec.json b/src/swiper/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/swiper/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/swiper/lib/index.ts b/src/swiper/lib/index.ts new file mode 100644 index 0000000..057ff47 --- /dev/null +++ b/src/swiper/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSwiperModule'; diff --git a/src/swiper/lib/ng-package.json b/src/swiper/lib/ng-package.json new file mode 100644 index 0000000..723dd15 --- /dev/null +++ b/src/swiper/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/swiper", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/swiper/lib/package.json b/src/swiper/lib/package.json new file mode 100644 index 0000000..6c8cda2 --- /dev/null +++ b/src/swiper/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-swiper", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/swiper/lib/project.json b/src/swiper/lib/project.json new file mode 100644 index 0000000..4742d17 --- /dev/null +++ b/src/swiper/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/swiper/lib", + "sourceRoot": "src/swiper/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/swiper"], + "options": { + "project": "src/swiper/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/swiper"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js swiper" + }, + { + "command": "ng default-build swiper" + }, + { + "command": "node build/clear-default-theme.js swiper" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/swiper && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build swiper && ng pack swiper && node build/publish.js swiper --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/swiper/lib/src/TiSwiperComponent.ts b/src/swiper/lib/src/TiSwiperComponent.ts new file mode 100644 index 0000000..c894a7f --- /dev/null +++ b/src/swiper/lib/src/TiSwiperComponent.ts @@ -0,0 +1,397 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChildren, + ElementRef, + EventEmitter, + Input, + NgZone, + Output, + QueryList, + Renderer2, + SimpleChange, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiSwipercardComponent } from './TiSwipercardComponent'; +import packageInfo from '../package.json'; +/** + * swiper组件每个展示块为一个card,点击左右按钮可以实现card切换功能 + * + */ +@Component({ + selector: 'ti-swiper', + templateUrl: './swiper.html', + styleUrls: ['./swiper.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-swiper-multiple]': 'showCardNum > 1' + } +}) +export class TiSwiperComponent extends TiBaseComponent { + constructor(private elementRef: ElementRef, private renderer2: Renderer2, private zone: NgZone, private cdRef: ChangeDetectorRef) { + super(elementRef, renderer2); + } + /** + * 指定当前显示项 + */ + @Input() activeIndex: number = 0; + /** + * 是否自动轮播,自动轮播时,鼠标移入元素,轮播暂停;移出元素,轮播继续。 + */ + @Input() autoplay: boolean = false; + /** + * 自动轮播时的播放速度,单位为毫秒 + */ + @Input() autoplaySpeed: number = 3000; + /** + * 每屏展示的项目数量 + */ + @Input() showCardNum: number = 1; + /** + * 多图轮播卡片间距,8-12px,默认为8px + */ + @Input() cardGap: string = '8px'; + /** + * 指示器位置,三个属性值,“below”容器外部,‘bottom’容器内部底部,‘none’不显示 + */ + @Input() indicatorPosition: 'bottom' | 'below' | 'none' = 'below'; + /** + * 是否无限循环 + */ + @Input() loop: boolean = true; + /** + * 切换触发的事件 + */ + @Output() readonly activeIndexChange: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + @ViewChild('cards', { static: true }) cards: ElementRef; + /** + * @ignore + */ + @ContentChildren(TiSwipercardComponent) + swipercardComs: QueryList; + /** + * @ignore + */ + @ViewChild('wrapper', { static: true }) wrapperEle: ElementRef; + /** + * @ignore + * 卡片总页数 + */ + public totalPage: number = 0; + /** + * @ignore + * 当前页, 按照索引值从0开始 + */ + public currentPage: number = 0; + /** + * @ignore + * 面板指示点的集合 + */ + public swiperBullets: Array = []; + protected versionInfo: string = super.getVersion(packageInfo); + // 卡片切换动画间隔,单位ms + private transitionSpeed: number = 600; + // 定时器任务 + private autoplayId: any; + // 卡片总数量 + private cardNum: number = 0; + private pageWidth: number; + private swiperCardWidth: number; + private cardSpace: number; + private totalWidth: number; + private unlistenResize: () => void; + + ngOnInit(): void { + super.ngOnInit(); + // 一屏显示卡片的数目会随窗口大小而变化,所以监听窗口大小变化重新计算分页, + this.zone.runOutsideAngular(() => { + this.unlistenResize = this.renderer2.listen(window, 'resize', () => { + this.setCardsWidth(); + this.setLeftPosition(this.currentPage); + }); + }); + } + + ngAfterContentChecked(): void { + super.ngAfterContentChecked(); + // 当卡片数量变化时,计算总数量 + if (this.cards.nativeElement.children.length !== this.cardNum) { + this.setCardNum(); + this.setCardsWidth(); + this.cdRef.markForCheck(); + } + } + + ngAfterViewInit(): void { + this.setCardsWidth(); + this.setLeftPosition(this.currentPage); + this.renderer.setStyle(this.cards.nativeElement, 'transition', `left ${this.transitionSpeed}ms`); + + // 只有一页时,不自动轮播 + if (this.autoplay && this.autoplaySpeed && this.totalPage > 1) { + this.startAutoplay(); + + this.renderer2.listen(this.wrapperEle.nativeElement, 'mouseenter', () => { + this.stopAutoplay(); + }); + + this.renderer2.listen(this.wrapperEle.nativeElement, 'mouseleave', () => { + this.startAutoplay(); + }); + } + } + ngAfterViewChecked(): void { + // 外层容器宽度有变化时,重新计算宽度及位置 + if (this.wrapperEle && this.wrapperEle.nativeElement.getBoundingClientRect().width !== this.pageWidth) { + this.setCardsWidth(); + this.setLeftPosition(this.currentPage); + } + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + const indexObj: SimpleChange = changes['activeIndex']; + if (indexObj && !indexObj.firstChange) { + this.calculateCurrentPage(indexObj.currentValue); + this.setLeftPosition(this.currentPage); + } + } + + ngOnDestroy(): void { + this.stopAutoplay(); + + if (this.unlistenResize) { + this.unlistenResize(); + } + } + + /** + * @ignore + * 点击左侧按钮触发事件 + */ + public onClickPrev(): void { + this.activeCard(this.currentPage - 1); + } + + /** + * @ignore + * 点击右侧按钮触发事件 + */ + public onClickNext(): void { + this.activeCard(this.currentPage + 1); + } + + /** + * @ignore + * 点击指示器触发事件 + */ + public onClickDot(index: number): void { + this.activeCard(index); + } + /** + * @ignore + * 点击左右侧按钮、面板指示板触发事件 + */ + private activeCard(index: number): void { + if (this.totalPage === 1) { + return; + } + + this.renderer.setStyle(this.cards.nativeElement, 'transition', `left ${this.transitionSpeed}ms`); + if (index === this.totalPage && this.loop) { + this.currentPage = 0; + const first: Element = this.swipercardComs.first.nativeElement; + this.setPosition(first, true); + this.setLeftPosition(this.totalPage); + this.adjustPosition(first); + } else if (index === -1 && this.loop) { + this.currentPage = this.totalPage - 1; + const last: Element = this.swipercardComs.last.nativeElement; + this.setPosition(last, false); + this.setLeftPosition(-1); + this.adjustPosition(last); + } else { + this.currentPage = index; + if (index <= -1) { + this.currentPage = 0; + } + if (index >= this.totalPage) { + this.currentPage = this.totalPage - 1; + } + this.setLeftPosition(this.currentPage); + this.activeIndexChange.emit(this.currentPage); + } + } + + // 计算卡片总数及总页数 + private setCardNum(): void { + this.cardNum = this.cards && this.cards.nativeElement.children && this.cards.nativeElement.children.length; + this.calculatpaging(); + this.swiperBullets.length = this.totalPage; + } + + // 计算卡片容器总宽度 + private setCardsWidth(): void { + this.calculatecardWidth(); + this.renderer.setStyle(this.cards.nativeElement, 'width', `${this.totalWidth}px`); + } + + // 当点击左右按钮时,设置激活卡片相对位置 + private setLeftPosition(index: number): void { + if (this.totalPage === 1) { + return; + } + + // 最后一页未占完 + if (index === this.totalPage - 1 && this.cardNum % this.showCardNum !== 0) { + // 最后一页呈现需要偏移的卡片数 + const swiperCardNum: number = this.cardNum - this.showCardNum; + this.renderer.setStyle( + this.cards.nativeElement, + 'left', + `${-swiperCardNum * this.swiperCardWidth - swiperCardNum * this.cardSpace}px` + ); + + return; + } + + // 无限循环 + if (index === this.totalPage && this.loop) { + this.renderer.setStyle(this.cards.nativeElement, 'left', `${-this.totalWidth}px`); + + return; + } + if (index === -1 && this.loop) { + this.renderer.setStyle( + this.cards.nativeElement, + 'left', + `${this.swiperCardWidth * this.showCardNum + (this.showCardNum - 1) * this.cardSpace}px` + ); + + return; + } + + // left偏移量 + const leftVal: number = -index * this.showCardNum * (this.swiperCardWidth + this.cardSpace); + this.renderer.setStyle(this.cards.nativeElement, 'left', `${leftVal}px`); + } + + // 调整第一页或者最后一页的位置,为动画做准备 + private setPosition(targetEle: Element, lastToFirst: boolean): void { + this.renderer.setStyle(targetEle, 'transform', `translateX(${lastToFirst ? this.totalWidth : this.totalWidth * -1}px)`); + } + + private adjustPosition(targetEle?: Element): void { + setTimeout((): void => { + this.renderer.removeStyle(targetEle, 'transform'); + this.renderer.removeStyle(this.cards.nativeElement, 'transition'); + this.setLeftPosition(this.currentPage); + this.activeIndexChange.emit(this.currentPage); + }, this.transitionSpeed); + } + + // 计算每页显示卡片数,总页数及当前页 + private calculatpaging(): void { + this.calculateTotalPage(); + this.calculateCurrentPage(this.activeIndex); + } + + /** + * 计算卡片宽度 + */ + private calculatecardWidth(): void { + // 初始化时,多图且左右翻页按钮还没有显示,容器元素的可用宽度需要减去左右按钮的宽度之和32+32=64 + if (this.totalPage === 0 && this.showCardNum > 1) { + this.pageWidth = this.wrapperEle.nativeElement.getBoundingClientRect().width - 64; + } else { + this.pageWidth = this.wrapperEle.nativeElement.getBoundingClientRect().width; + } + + // 每页卡片总间距 + this.cardSpace = this.showCardNum > 1 ? parseInt(this.cardGap, 10) : 0; + const cardsSpace: number = (this.showCardNum - 1) * parseInt(this.cardGap, 10); + this.swiperCardWidth = (this.pageWidth - cardsSpace) / this.showCardNum; + this.totalWidth = this.swiperCardWidth * this.cardNum + cardsSpace * this.totalPage; + + this.swipercardComs.toArray().forEach((item: TiSwipercardComponent, index: number): void => { + // 设置卡片宽度 + this.renderer2.setStyle(item.nativeElement, 'width', this.swiperCardWidth + 'px'); + // 设置卡片间距,最后一个卡片不设置 + if (index !== this.cardNum - 1) { + this.renderer2.setStyle(item.nativeElement, 'margin-right', this.cardSpace + 'px'); + } + }); + } + + /** + * 计算总页数 + */ + private calculateTotalPage(): void { + this.totalPage = Math.ceil(this.cardNum / this.showCardNum); + if (this.currentPage > this.totalPage) { + this.activeCard(this.totalPage); + } + } + + /** + * 计算当前页 + */ + private calculateCurrentPage(index: number): void { + // 当前索引值不正确,不处理 + if (index < 0 || index > this.cardNum - 1) { + return; + } + + if (this.showCardNum === 1) { + this.currentPage = index; + + return; + } + + // 当前一页卡片的最大最小索引 + const maxIndex: number = (this.currentPage + 1) * this.showCardNum - 1; + const minIndex: number = this.currentPage > 0 ? this.currentPage * this.showCardNum - 1 : 0; + // 当前索卡片是否在当前页面 + if (index < minIndex || index > maxIndex) { + this.currentPage = Math.floor((index + 1) / this.showCardNum); + } + } + + /** + * 暂停自动播放,清除定时任务 + */ + private stopAutoplay(): void { + if (this.autoplayId) { + clearInterval(this.autoplayId); + this.autoplayId = undefined; + } + } + /** + * 开始自动播放,设置定时任务 + */ + private startAutoplay(): void { + if (!this.autoplayId) { + this.autoplayId = setInterval((): void => { + this.onClickNext(); + this.cdRef.markForCheck(); + }, this.autoplaySpeed); + } + } +} diff --git a/src/swiper/lib/src/TiSwiperModule.ts b/src/swiper/lib/src/TiSwiperModule.ts new file mode 100644 index 0000000..c59ce3a --- /dev/null +++ b/src/swiper/lib/src/TiSwiperModule.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; + +import { TiSwiperComponent } from './TiSwiperComponent'; +import { TiSwipercardComponent } from './TiSwipercardComponent'; + +@NgModule({ + imports: [CommonModule, TiIconModule, TiOutlineModule], + exports: [TiSwiperComponent, TiSwipercardComponent], + declarations: [TiSwiperComponent, TiSwipercardComponent] +}) +export class TiSwiperModule {} +export { TiSwiperComponent } from './TiSwiperComponent'; +export { TiSwipercardComponent } from './TiSwipercardComponent'; diff --git a/src/swiper/lib/src/TiSwipercardComponent.ts b/src/swiper/lib/src/TiSwipercardComponent.ts new file mode 100644 index 0000000..55ec012 --- /dev/null +++ b/src/swiper/lib/src/TiSwipercardComponent.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * TiSwiperCard 是单个卡片组件 + */ +@Component({ + selector: 'ti-swipercard', + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./swipercard.less'] +}) +export class TiSwipercardComponent extends TiBaseComponent { + protected versionInfo: string = super.getVersion(packageInfo); +} diff --git a/src/swiper/lib/src/swiper.html b/src/swiper/lib/src/swiper.html new file mode 100644 index 0000000..9fbbc05 --- /dev/null +++ b/src/swiper/lib/src/swiper.html @@ -0,0 +1,51 @@ +
    + +
    + +
    + + +
    +
    + +
    +
    + + +
    + +
    +
    + + +
      +
    • +
    diff --git a/src/swiper/lib/src/swiper.less b/src/swiper/lib/src/swiper.less new file mode 100644 index 0000000..10dea45 --- /dev/null +++ b/src/swiper/lib/src/swiper.less @@ -0,0 +1,155 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-swiper-icon-height: var(--ti-common-size-12x); + --ti-swiper-icon-color: var(--ti-common-color-icon-white); + --ti-swiper-icon-color-hover: var(--ti-common-color-icon-white); + --ti-swiper-icon-bg-color: rgba(0, 0, 0, 0.2); + --ti-swiper-icon-bg-color-hover: rgba(0, 0, 0, 0.3); + --ti-swiper-finite-icon-color: rgba(255, 255, 255, 0.4); + --ti-swiper-pagination-space: var(--ti-common-space-7x); // 指示器距离容器元素间距 + --ti-swiper-pagination-bullets-space: var(--ti-common-space-6); + --ti-swiper-multiple-icon-bg-color: var(--ti-common-color-transparent); + --ti-swiper-multiple-icon-bg-color-hover: var(--ti-common-color-transparent); + --ti-swiper-bullet-border: 2.5px; +} + +:host { + display: block; + position: relative; // 相对于自己定位,在正常文档流中占位 +} + +.ti3-swiper-container { + .flex-container(row, center, center); + position: relative; +} + +// 左右侧按钮公共样式 +.ti3-swiper-arrow { + flex-shrink: 0; + position: absolute; + display: flex; + align-items: center; + justify-content: center; + top: calc((100% - var(--ti-swiper-icon-height)) / 2); // IE下需要设置top定位,否则会居下显示 + z-index: 1; + font-size: var(--ti-common-size-4x); + width: var(--ti-common-size-6x); + height: var(--ti-swiper-icon-height); + background: var(--ti-swiper-icon-bg-color); + text-align: center; + cursor: pointer; + color: var(--ti-swiper-icon-color); + border-radius: var(--ti-common-border-radius-normal); + + &:not(.ti3-swiper-finite-arrow):hover, + &:not(.ti3-swiper-finite-arrow):focus { + color: var(--ti-swiper-icon-color-hover); + background: var(--ti-swiper-icon-bg-color-hover); + } +} + +.ti3-swiper-finite-arrow { + cursor: not-allowed; + + ti-icon[local] { + color: var(--ti-swiper-finite-icon-color); + } +} + +// 左侧按钮 +.ti3-swiper-prev { + left: var(--ti-common-space-0); +} + +// 右侧按钮 +.ti3-swiper-next { + right: var(--ti-common-space-0); +} + +// 多图轮播左右按钮样式 +:host.ti3-swiper-multiple { + .ti3-swiper-wrapper { + margin: 0 var(--ti-common-space-10); + } + + .ti3-swiper-arrow { + position: static; + background: var(--ti-swiper-multiple-icon-bg-color); + color: var(--ti-common-color-icon-normal); + + &:not(.ti3-swiper-finite-arrow):hover, + &:not(.ti3-swiper-finite-arrow):focus { + background: var(--ti-swiper-multiple-icon-bg-color-hover); + color: var(--ti-common-color-icon-hover); + } + } +} + +// 卡片容器 +.ti3-swiper-wrapper { + position: relative; // 让卡片相对于该元素定位 + overflow-x: hidden; + width: 100%; + + .ti3-swiper-cards-container { + position: relative; + display: flex; + } +} + +// 面板指示点样式 +.ti3-swiper-pagination-bullets { + text-align: center; + margin-top: var(--ti-swiper-pagination-space); + + .ti3-swiper-pagination-bullet { + display: inline-block; + .box-sizing(border-box); + border: var(--ti-swiper-bullet-border) solid var(--ti-common-color-line-normal); + background-color: var(--ti-common-color-line-normal); + border-radius: var(--ti-swiper-bullet-border); + margin: 0 0 0 var(--ti-swiper-pagination-bullets-space); + cursor: pointer; + + &:first-child { + margin: 0; + } + + // 激活后当前bullet样式 + &.ti3-swiper-pagination-bullet-active { + width: var(--ti-common-size-5x); + border-color: var(--ti-common-color-line-hover); + background-color: var(--ti-common-color-line-hover); + } + } +} + +// 指示器在底部样式 +.ti3-swiper-pagination-bullets-bottom { + position: absolute; + width: 100%; + bottom: var(--ti-swiper-pagination-space); + margin-top: 0; +} + +// 指示器在右侧样式,预留样式 +.ti3-swiper-pagination-bullets-right { + position: absolute; + width: 5px; + right: var(--ti-swiper-pagination-space); + top: 50%; + transform: translateY(-50%); + margin-top: 0; + + .ti3-swiper-pagination-bullet { + margin: 0 0 var(--ti-swiper-pagination-bullets-space) 0; + + &.ti3-swiper-pagination-bullet-active { + width: 5px; + height: 20px; + border-color: var(--ti-common-color-line-hover); + background-color: var(--ti-common-color-line-hover); + } + } +} diff --git a/src/swiper/lib/src/swipercard.less b/src/swiper/lib/src/swipercard.less new file mode 100644 index 0000000..5d2e8e3 --- /dev/null +++ b/src/swiper/lib/src/swipercard.less @@ -0,0 +1,4 @@ +:host { + box-sizing: border-box; + flex-shrink: 0; // 宽度不够时不压缩 +} diff --git a/src/switch/demo/karma.conf.js b/src/switch/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/switch/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/switch/demo/project.json b/src/switch/demo/project.json new file mode 100644 index 0000000..2c4b9eb --- /dev/null +++ b/src/switch/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/switch/demo", + "sourceRoot": "src/switch/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/switch", + "index": "src/switch/demo/src/index.html", + "main": "src/switch/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/switch/demo/tsconfig.app.json", + "assets": ["src/switch/demo/src/favicon.ico", "src/switch/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "switch-demo:build:production" + }, + "development": { + "browserTarget": "switch-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js switch" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/switch/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/switch/demo/tsconfig.spec.json", + "karmaConfig": "src/switch/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/switch/demo/src/app/AppComponent.ts b/src/switch/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/switch/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/switch/demo/src/app/AppModule.ts b/src/switch/demo/src/app/AppModule.ts new file mode 100644 index 0000000..c3aa27c --- /dev/null +++ b/src/switch/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SwitchTestModule } from './switch/SwitchTestModule'; + +@NgModule({ + imports: [ + SwitchTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/switch/demo/src/app/IndexComponent.ts b/src/switch/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..9fc0be6 --- /dev/null +++ b/src/switch/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SwitchTestModule } from './switch/SwitchTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SwitchTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/switch/demo/src/app/app.html b/src/switch/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/switch/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/switch/demo/src/app/switch/SwitchBasicComponent.ts b/src/switch/demo/src/app/switch/SwitchBasicComponent.ts new file mode 100644 index 0000000..707414c --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-basic.html' +}) +export class SwitchBasicComponent { + switchState: boolean = true; +} diff --git a/src/switch/demo/src/app/switch/SwitchBeforeComponent.ts b/src/switch/demo/src/app/switch/SwitchBeforeComponent.ts new file mode 100644 index 0000000..35468a3 --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchBeforeComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { TiSwitchComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './switch-before.html' +}) +export class SwitchBeforeComponent { + myLogs: Array = []; + switchState: boolean = true; + + onBeforeChange(component: TiSwitchComponent): void { + if (window.confirm('您确定切换状态吗?')) { + this.switchState = !this.switchState; + this.myLogs = [...this.myLogs, `BeforeChange event = 确认切换状态`]; + } + } +} diff --git a/src/switch/demo/src/app/switch/SwitchDisabledComponent.ts b/src/switch/demo/src/app/switch/SwitchDisabledComponent.ts new file mode 100644 index 0000000..2f7bbcc --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchDisabledComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-disabled.html' +}) +export class SwitchDisabledComponent { + switchState: boolean = true; +} diff --git a/src/switch/demo/src/app/switch/SwitchEventComponent.ts b/src/switch/demo/src/app/switch/SwitchEventComponent.ts new file mode 100644 index 0000000..1612756 --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchEventComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-event.html' +}) +export class SwitchEventComponent { + switchState: boolean = true; + myLogs: Array = []; + onNgModelChange(state: boolean): void { + this.myLogs = [...this.myLogs, `onNgModelChange,state= ${state}`]; + } + onFocus(event: FocusEvent): void { + this.myLogs = [...this.myLogs, 'onFocus']; + } + onBlur(event: FocusEvent): void { + this.myLogs = [...this.myLogs, 'onBlur']; + } + onChange(state: boolean): void { + this.myLogs = [...this.myLogs, `onChange,state= ${state}`]; + } +} diff --git a/src/switch/demo/src/app/switch/SwitchExplanationComponent.ts b/src/switch/demo/src/app/switch/SwitchExplanationComponent.ts new file mode 100644 index 0000000..0575290 --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchExplanationComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-explanation.html' +}) +export class SwitchExplanationComponent { + switchState: boolean = true; + textOn: string = '编辑'; + textOff: string = '查看'; +} diff --git a/src/switch/demo/src/app/switch/SwitchFocusComponent.ts b/src/switch/demo/src/app/switch/SwitchFocusComponent.ts new file mode 100644 index 0000000..04cd013 --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchFocusComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-focus.html' +}) +export class SwitchFocusComponent { + switchState1: boolean = true; + switchState2: boolean = true; + switchState3: boolean = true; + switchState4: boolean = true; + switchState5: boolean = true; + switchState6: boolean = true; + myDisabled: boolean = false; +} diff --git a/src/switch/demo/src/app/switch/SwitchIdComponent.ts b/src/switch/demo/src/app/switch/SwitchIdComponent.ts new file mode 100644 index 0000000..30778f0 --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchIdComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-id.html' +}) +export class SwitchIdComponent { + switchState: boolean = true; +} diff --git a/src/switch/demo/src/app/switch/SwitchLoadComponent.ts b/src/switch/demo/src/app/switch/SwitchLoadComponent.ts new file mode 100644 index 0000000..951772d --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchLoadComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-load.html' +}) +export class SwitchLoadComponent { + switchState: boolean = false; + + // 每个组件都用下面六个函数,只改变函数内容 + changeUndefined(): void { + this.switchState = undefined; + } + changeNull(): void { + this.switchState = null; + } + changeWrongType(): void { + const temp: any = 5; + this.switchState = temp; + } + changeZeroData(): void { + this.switchState = false; + } + changeDataA(): void { + this.switchState = false; + } + changeDataB(): void { + this.switchState = true; + } +} diff --git a/src/switch/demo/src/app/switch/SwitchTemplateComponent.ts b/src/switch/demo/src/app/switch/SwitchTemplateComponent.ts new file mode 100644 index 0000000..2b4d1e2 --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchTemplateComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-template.html' +}) +export class SwitchTemplateComponent { + switchState: boolean = true; +} diff --git a/src/switch/demo/src/app/switch/SwitchTestModule.ts b/src/switch/demo/src/app/switch/SwitchTestModule.ts new file mode 100644 index 0000000..69cdde1 --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchTestModule.ts @@ -0,0 +1,69 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiIconModule, TiSwitchModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { SwitchDisabledComponent } from './SwitchDisabledComponent'; +import { SwitchBasicComponent } from './SwitchBasicComponent'; +import { SwitchExplanationComponent } from './SwitchExplanationComponent'; +import { SwitchBeforeComponent } from './SwitchBeforeComponent'; +import { SwitchEventComponent } from './SwitchEventComponent'; +import { SwitchFocusComponent } from './SwitchFocusComponent'; +import { SwitchIdComponent } from './SwitchIdComponent'; +import { SwitchLoadComponent } from './SwitchLoadComponent'; +import { SwitchTemplateComponent } from './SwitchTemplateComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiButtonModule, + TiIconModule, + TiSwitchModule, + DemoLogModule, + RouterModule.forChild(SwitchTestModule.ROUTES) + ], + declarations: [ + SwitchBasicComponent, + SwitchDisabledComponent, + SwitchExplanationComponent, + SwitchTemplateComponent, + SwitchBeforeComponent, + SwitchEventComponent, + SwitchFocusComponent, + SwitchIdComponent, + SwitchLoadComponent + ] +}) +export class SwitchTestModule { + static readonly LINKS: Array = [{ href: 'components/TiSwitchComponent.html', label: 'Switch' }]; + static readonly ROUTES: Routes = [ + { + path: 'switch/switch-basic', + component: SwitchBasicComponent + }, + { + path: 'switch/switch-disabled', + component: SwitchDisabledComponent + }, + { + path: 'switch/switch-explanation', + component: SwitchExplanationComponent + }, + { + path: 'switch/switch-template', + component: SwitchTemplateComponent + }, + { + path: 'switch/switch-before', + component: SwitchBeforeComponent + }, + { path: 'switch/switch-focus', component: SwitchFocusComponent }, + { path: 'switch/switch-id', component: SwitchIdComponent }, + { path: 'switch/switch-load', component: SwitchLoadComponent }, + { path: 'switch/switch-event', component: SwitchEventComponent } + ]; +} diff --git a/src/switch/demo/src/app/switch/switch-basic.html b/src/switch/demo/src/app/switch/switch-basic.html new file mode 100644 index 0000000..3b2d640 --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-basic.html @@ -0,0 +1 @@ + diff --git a/src/switch/demo/src/app/switch/switch-before.html b/src/switch/demo/src/app/switch/switch-before.html new file mode 100644 index 0000000..c664508 --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-before.html @@ -0,0 +1,2 @@ + + diff --git a/src/switch/demo/src/app/switch/switch-disabled.html b/src/switch/demo/src/app/switch/switch-disabled.html new file mode 100644 index 0000000..ca60a88 --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-disabled.html @@ -0,0 +1 @@ + diff --git a/src/switch/demo/src/app/switch/switch-event.html b/src/switch/demo/src/app/switch/switch-event.html new file mode 100644 index 0000000..213ca84 --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-event.html @@ -0,0 +1,19 @@ +

    描述

    +

    Switch开关组件,事件用法

    +

    注意:(ngModelChange)="onNgModelChange($event)" 监听绑定值的改变

    +

    示例

    +

    1.[(ngModel)]双向绑定开关状态

    +开关: + + +
    +switchState:{{switchState}} + +

    事件日志:

    + diff --git a/src/switch/demo/src/app/switch/switch-explanation.html b/src/switch/demo/src/app/switch/switch-explanation.html new file mode 100644 index 0000000..f7a4cfc --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-explanation.html @@ -0,0 +1 @@ + diff --git a/src/switch/demo/src/app/switch/switch-focus.html b/src/switch/demo/src/app/switch/switch-focus.html new file mode 100644 index 0000000..7cc2698 --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-focus.html @@ -0,0 +1,29 @@ +

    描述

    +

    Switch开关组件,焦点用法

    +

    示例

    + +

    1.autofocus:

    +

    + +

    2.focus():

    +
    +
    + +  + +  + +  + +

    + +

    3.tabindex:

    +tabindex="1" +

    +tabindex="3" +

    +tabindex="4" +

    +tabindex="2" + + diff --git a/src/switch/demo/src/app/switch/switch-id.html b/src/switch/demo/src/app/switch/switch-id.html new file mode 100644 index 0000000..dca01d0 --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-id.html @@ -0,0 +1,8 @@ +

    描述

    +

    Switch开关组件,id

    +

    示例

    +

    1.[(ngModel)]双向绑定开关状态

    +开关: + +
    +switchState:{{switchState}} diff --git a/src/switch/demo/src/app/switch/switch-load.html b/src/switch/demo/src/app/switch/switch-load.html new file mode 100644 index 0000000..9c98732 --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-load.html @@ -0,0 +1,15 @@ +

    描述

    +

    Switch开关组件,加载数据

    +

    示例

    +

    1.[(ngModel)]双向绑定开关状态

    +开关: + +
    +switchState:{{switchState}} +

    每个组件改变数据,都用下面六个按钮。不变化

    +
    +
    +
    +
    +
    +
    diff --git a/src/switch/demo/src/app/switch/switch-template.html b/src/switch/demo/src/app/switch/switch-template.html new file mode 100644 index 0000000..ad3227f --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-template.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/switch/demo/src/app/switch/webdoc/switch-demos.js b/src/switch/demo/src/app/switch/webdoc/switch-demos.js new file mode 100644 index 0000000..825ea5b --- /dev/null +++ b/src/switch/demo/src/app/switch/webdoc/switch-demos.js @@ -0,0 +1,74 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'switch-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Switch 组件的基本用法。

    ', + 'en-US': '

    basic

    ', + }, + }, + { + demoId: 'switch-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': '

    disabled

    ', + }, + apis: ['TiSwitchComponent.properties.disabled'], + }, + { + demoId: 'switch-before', + name: { + 'zh-CN': '开关状态拦截', + 'en-US': 'beforeChange', + }, + desc: { + 'zh-CN': + '

    当开关状态将要发生变化的时候触发beforeChange事件。

    ', + 'en-US': '

    beforeChange

    ', + }, + apis: ['TiSwitchComponent.events.beforeChange'], + }, + { + demoId: 'switch-explanation', + name: { + 'zh-CN': '自定义文本', + 'en-US': 'explanation', + }, + desc: { + 'zh-CN': + '

    通过属性onTextoffText配置开关时显示的文本。

    ', + 'en-US': '

    explanation

    ', + }, + apis: [ + 'TiSwitchComponent.properties.onText', + 'TiSwitchComponent.properties.offText', + ], + }, + { + demoId: 'switch-template', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'template', + }, + desc: { + 'zh-CN': + '

    通过模板onoff配置开关时显示的自定义内容。

    ', + 'en-US': '

    template

    ', + }, + apis: [ + 'TiSwitchComponent.slots.onTemplate', + 'TiSwitchComponent.slots.offTemplate', + ], + }, + ], +}; diff --git a/src/switch/demo/src/app/switch/webdoc/switch.cn.md b/src/switch/demo/src/app/switch/webdoc/switch.cn.md new file mode 100644 index 0000000..c9075d1 --- /dev/null +++ b/src/switch/demo/src/app/switch/webdoc/switch.cn.md @@ -0,0 +1,23 @@ +--- +title: Switch 开关 +--- +# Switch 开关 + +
    + +Switch 是提供开关状态或两种状态切换的组件。   + +```typescript +import { TiSwitchModule } from '@opentiny/ng'; +``` + +
    + +
    + +Switch 是提供开关状态或两种状态切换的组件。   + +```typescript +import { TiSwitchModule } from '@opentiny/ng'; +``` +
    diff --git a/src/switch/demo/src/app/switch/webdoc/switch.en.md b/src/switch/demo/src/app/switch/webdoc/switch.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/switch/demo/src/app/switch/webdoc/switch.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/switch/demo/src/favicon.ico b/src/switch/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/switch/demo/src/index.html b/src/switch/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/switch/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/switch/demo/src/main.ts b/src/switch/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/switch/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/switch/demo/test.ts b/src/switch/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/switch/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/switch/demo/tsconfig.app.json b/src/switch/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/switch/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/switch/demo/tsconfig.spec.json b/src/switch/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/switch/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/switch/lib/index.ts b/src/switch/lib/index.ts new file mode 100644 index 0000000..6053d89 --- /dev/null +++ b/src/switch/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSwitchModule'; diff --git a/src/switch/lib/ng-package.json b/src/switch/lib/ng-package.json new file mode 100644 index 0000000..66797dd --- /dev/null +++ b/src/switch/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/switch", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/switch/lib/package.json b/src/switch/lib/package.json new file mode 100644 index 0000000..ccbe22f --- /dev/null +++ b/src/switch/lib/package.json @@ -0,0 +1,10 @@ +{ + "name": "@opentiny/ng-switch", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/switch/lib/project.json b/src/switch/lib/project.json new file mode 100644 index 0000000..eb9a670 --- /dev/null +++ b/src/switch/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/switch/lib", + "sourceRoot": "src/switch/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/switch"], + "options": { + "project": "src/switch/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/switch"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js switch" + }, + { + "command": "ng default-build switch" + }, + { + "command": "node build/clear-default-theme.js switch" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/switch && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build switch && ng pack switch && node build/publish.js switch --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/switch/lib/src/TiSwitchComponent.ts b/src/switch/lib/src/TiSwitchComponent.ts new file mode 100644 index 0000000..e28340a --- /dev/null +++ b/src/switch/lib/src/TiSwitchComponent.ts @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + Component, + ContentChild, + ElementRef, + EventEmitter, + Input, + Output, + TemplateRef, + ViewChild +} from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * Switch开关组件 + * + * 用于实现页面中的开关操作。 + * + */ +@Component({ + selector: 'ti-switch', + templateUrl: './switch.html', + styleUrls: ['./switch.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-switch-container]': 'true' + }, + providers: [TiFormComponent.getValueAccessor(TiSwitchComponent)] +}) +export class TiSwitchComponent extends TiFormComponent { + /** + * 打开状态的显示文本 + */ + @Input() onText: string = ''; + /** + * 关闭状态的显示文本 + */ + @Input() offText: string = ''; + /** + * 开关切换前触发的回调,参数:组件实例 + */ + @Output() readonly beforeChange: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + @ViewChild('a', { static: true }) aRef: ElementRef; + /** + * 打开状态的自定义模板 + */ + @ContentChild('on', { static: true }) onTemplate: TemplateRef; + /** + * 关闭状态的自定义模板 + */ + @ContentChild('off', { static: true }) offTemplate: TemplateRef; + protected versionInfo: string = super.getVersion(packageInfo); + private initialized: boolean = false; + private hasAnimation: boolean = false; + + ngOnInit(): void { + // 基类中做了设置宿主id的操作 + super.ngOnInit(); + this.setFocusableElems([this.aRef.nativeElement]); + } + /** + * @ignore + */ + ngOnModelChange(value: boolean): void { + super.ngOnModelChange(value); + if (value === null) { + // 以 ngModel 的形式传入值时, writeValue首次传入null + return; + } + + if (this.initialized && !this.hasAnimation) { + // 保证初始时没有动画 + this.renderer.addClass(this.aRef.nativeElement, 'ti3-switch-animation'); + this.hasAnimation = true; + } + this.initialized = true; + } + + /** + * 切换开关状态 + */ + private toggle(): void { + if (this.beforeChange.observers.length === 0) { + // 用户未定义beforeChange + this.model = !this.model; + } else { + this.beforeChange.emit(this); + } + } + /** + * @ignore + * 点击事件 + */ + public onClick(): void { + if (!this.disabled) { + this.toggle(); + } + } +} diff --git a/src/switch/lib/src/TiSwitchModule.ts b/src/switch/lib/src/TiSwitchModule.ts new file mode 100644 index 0000000..c73609c --- /dev/null +++ b/src/switch/lib/src/TiSwitchModule.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiSwitchComponent } from './TiSwitchComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [TiSwitchComponent], + declarations: [TiSwitchComponent] +}) +export class TiSwitchModule {} +export { TiSwitchComponent } from './TiSwitchComponent'; diff --git a/src/switch/lib/src/switch.html b/src/switch/lib/src/switch.html new file mode 100644 index 0000000..cc623ef --- /dev/null +++ b/src/switch/lib/src/switch.html @@ -0,0 +1,30 @@ + +
    + + +
    + {{onText}} +
    +
    + +
    + {{offText}} +
    +
    +
    +
    + +
    diff --git a/src/switch/lib/src/switch.less b/src/switch/lib/src/switch.less new file mode 100644 index 0000000..69e03e0 --- /dev/null +++ b/src/switch/lib/src/switch.less @@ -0,0 +1,125 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-switch-height: var(--ti-common-size-5x); + --ti-switch-width: 38px; + --ti-switch-pointer-width: var(--ti-common-size-4x); + --ti-switch-pointer-space: 2px; + --ti-switch-pointer-explanation-space: var(--ti-common-space-6); +} + +:host.ti3-switch-container { + display: inline-block; + height: var(--ti-switch-height); +} + +// 1.组件通用样式:与hover、focused、disabled等状态无关 +.ti3-switch { + width: var(--ti-switch-width); + height: var(--ti-switch-height); + display: inline-block; + position: relative; + cursor: pointer; + .user-select(none); + + .ti3-switch-track { + position: relative; + width: 100%; + height: 100%; + z-index: 0; + border-radius: var(--ti-switch-height); + .box-sizing (border-box); + } + + .ti3-switch-pointer { + display: inline-block; + position: absolute; + top: calc((var(--ti-switch-height) - var(--ti-switch-pointer-width)) / 2); + z-index: 8; + width: var(--ti-switch-pointer-width); + height: var(--ti-switch-pointer-width); + background: var(--ti-common-color-bg-white-normal); + border-radius: var(--ti-common-border-radius-3); + .box-sizing (border-box); + } + + &.ti3-switch-off { + & .ti3-switch-track { + background: var(--ti-common-color-bg-secondary); + } + + & .ti3-switch-pointer { + left: var(--ti-switch-pointer-space); + } + } + + &.ti3-switch-on { + & .ti3-switch-track { + background: var(--ti-common-color-bg-emphasize); + } + + & .ti3-switch-pointer { + left: calc(100% - var(--ti-switch-pointer-space) - var(--ti-switch-pointer-width)); + } + } + + &.ti3-switch-with-explanation { + width: auto; + + .ti3-switch-track { + width: auto; + } + + .ti3-switch-explanation-container { + display: inline-block; + line-height: var(--ti-switch-height); + color: var(--ti-common-color-text-white); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + } + + &.ti3-switch-off { + .ti3-switch-explanation-container { + padding-left: calc(var(--ti-switch-pointer-explanation-space) + var(--ti-switch-pointer-width) + var(--ti-switch-pointer-space)); + padding-right: var(--ti-common-space-10); + } + } + + &.ti3-switch-on { + .ti3-switch-explanation-container { + padding-left: var(--ti-common-space-10); + padding-right: calc(var(--ti-switch-pointer-explanation-space) + var(--ti-switch-pointer-width) + var(--ti-switch-pointer-space)); + } + } + } +} + +// 2.灰化状态下的样式 +.ti3-switch[disabled] { + cursor: not-allowed; + + & .ti3-switch-pointer { + background: var(--ti-common-color-bg-disabled); + } + + &.ti3-switch-on { + & .ti3-switch-track { + background-color: var(--ti-common-color-bg-light-emphasize); + } + } + + &.ti3-switch-off { + & .ti3-switch-track { + background-color: var(--ti-common-color-bg-dark-disabled); + } + } +} + +/************************************动效************************************************************************/ +.ti3-switch.ti3-switch-animation { + .ti3-switch-track, + .ti3-switch-pointer { + transition: all 0.25s linear; + } +} +/************************************动效************************************************************************/ diff --git a/src/tab/demo/karma.conf.js b/src/tab/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/tab/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/tab/demo/project.json b/src/tab/demo/project.json new file mode 100644 index 0000000..45d2968 --- /dev/null +++ b/src/tab/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/tab/demo", + "sourceRoot": "src/tab/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/tab", + "index": "src/tab/demo/src/index.html", + "main": "src/tab/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tab/demo/tsconfig.app.json", + "assets": ["src/tab/demo/src/favicon.ico", "src/tab/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "tab-demo:build:production" + }, + "development": { + "browserTarget": "tab-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js tab" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/tab/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tab/demo/tsconfig.spec.json", + "karmaConfig": "src/tab/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/tab/demo/src/app/AppComponent.ts b/src/tab/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/tab/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/tab/demo/src/app/AppModule.ts b/src/tab/demo/src/app/AppModule.ts new file mode 100644 index 0000000..b2b2c82 --- /dev/null +++ b/src/tab/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TabTestModule } from './tab/TabTestModule'; + +@NgModule({ + imports: [ + TabTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/tab/demo/src/app/IndexComponent.ts b/src/tab/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..910f9fe --- /dev/null +++ b/src/tab/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TabTestModule } from './tab/TabTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TabTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/tab/demo/src/app/app.html b/src/tab/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/tab/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/tab/demo/src/app/tab/TabBasicComponent.ts b/src/tab/demo/src/app/tab/TabBasicComponent.ts new file mode 100644 index 0000000..ae8f648 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabBasicComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './tab-basic.html' +}) +export class TabBasicComponent { + tabs: any = [ + { + title: 'Tab1', + active: true + }, + { + title: 'Tab2', + disabled: true + }, + { + title: 'Tab3', + active: false + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabBeforeactivechangeComponent.ts b/src/tab/demo/src/app/tab/TabBeforeactivechangeComponent.ts new file mode 100644 index 0000000..964831a --- /dev/null +++ b/src/tab/demo/src/app/tab/TabBeforeactivechangeComponent.ts @@ -0,0 +1,34 @@ +import { Component } from '@angular/core'; +import { TiTabsComponent } from '@opentiny/ng'; +@Component({ + templateUrl: './tab-beforeactivechange.html' +}) +export class TabBeforeactivechangeComponent { + myLogs: Array = []; + tabs: any = [ + { + title: 'Tab1', + active: true, + onActiveChange: (isActive: boolean): void => { + if (isActive) { + this.myLogs = [...this.myLogs, 'tab1: not active => active']; + } else { + this.myLogs = [...this.myLogs, 'tab1: active => not active']; + } + } + }, + { + title: 'Tab2' + }, + { + title: 'Tab3', + beforeActiveChange: (tabs: TiTabsComponent): void => { + this.myLogs = [...this.myLogs, 'tab3: beforeActiveChange trigger']; + setTimeout(() => { + tabs.changeActive(tabs.clickTab); + this.myLogs = [...this.myLogs, 'tab3: changeActive trigger at 1000ms']; + }, 1000); + } + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabContentCompComponent.ts b/src/tab/demo/src/app/tab/TabContentCompComponent.ts new file mode 100644 index 0000000..f82248c --- /dev/null +++ b/src/tab/demo/src/app/tab/TabContentCompComponent.ts @@ -0,0 +1,100 @@ +import { Component, Injector, Input, Injectable } from '@angular/core'; + +@Component({ + templateUrl: './tab-content-comp.html' +}) +export class TabContentCompComponent { + title1: string = 'Libary'; + title2: string = 'Company'; + title3: string = 'Concert'; + active: boolean = true; + type: string = 'pop song'; + + tabs: Array = [ + { + title: 'Libary', + component: LibaryComponent + }, + { + title: 'Company', + active: true, + component: CompanyComponent + }, + { + title: 'Concert1', + component: Concert1Component, + token: Greeter, + data: { suffix: 'hippop', name: 'violet' } // 给组件传参 + } + ]; + + constructor(private injector: Injector) {} + + getInjector(tab: any): any { + if (tab.token && tab.data) { + return Injector.create([{ provide: tab.token, useValue: tab.data }], this.injector); + } + } +} + +@Component({ + selector: 'app-libary', + template: `

    + I'm the content component of {{ name }}. +

    ` +}) +export class LibaryComponent { + name: string = 'Libary'; +} + +@Component({ + selector: 'app-company', + template: `

    + I'm the content component of {{ place }}. +

    ` +}) +export class CompanyComponent { + place: string = 'Company'; +} + +@Component({ + selector: 'app-concert', + template: `

    + I'm the content component of {{ text }}. +

    +

    Params: {{ type }}

    +

    ` +}) +export class ConcertComponent { + text: string = 'Concert'; + @Input() type: string; +} + +@Injectable() +export class Greeter { + suffix: string; + name: string; +} + +@Component({ + selector: 'app-concert1', + template: `

    + I'm the content component of {{ text }}. +

    +

    + Params: {{ concert1.suffix }} +

    +

    +

    + Params: {{ concert1.name }} +

    +

    ` +}) +export class Concert1Component { + text: string = 'Concert1'; + constructor(public concert1: Greeter) {} +} diff --git a/src/tab/demo/src/app/tab/TabCustomHeadComponent.ts b/src/tab/demo/src/app/tab/TabCustomHeadComponent.ts new file mode 100644 index 0000000..d7c49e1 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabCustomHeadComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './tab-custom-head.html', + styles: [ + ` + .sum-color { + color: red; + } + ` + ] +}) +export class TabCustomHeadComponent { + tabs: any = [ + { + title: 'Tab1', + active: false + }, + { + title: 'Tab2', + active: true + }, + { + title: 'Tab3', + active: false + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabDarkComponent.ts b/src/tab/demo/src/app/tab/TabDarkComponent.ts new file mode 100644 index 0000000..9633518 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabDarkComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './tab-dark.html' +}) +export class TabDarkComponent { + tabs: any = [ + { + title: 'Tab1', + active: true + }, + { + title: 'Tab2' + }, + { + title: 'Tab3' + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabDefaultTestComponent.ts b/src/tab/demo/src/app/tab/TabDefaultTestComponent.ts new file mode 100644 index 0000000..7a8a334 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabDefaultTestComponent.ts @@ -0,0 +1,89 @@ +import { Component, OnInit } from '@angular/core'; +@Component({ + templateUrl: './tab-default-test.html', + styles: [ + ` + .sum-color { + color: red; + } + ` + ] +}) +export class TabDefaultTestComponent implements OnInit { + hiden: boolean = false; + tabsId: string = 'tabs_test'; + notShow: boolean = true; + tab1: any = { + id: 'tab1', + sum: 26, + active: false, + title: '大法师', + show: false + }; + otherTabs: Array = [ + { + title: 'Profile', + id: 'tab2', + disabled: true, + active: false, + content: "Raw denim you probably haven't heard of them jean shorts Austin. ", + child: [{ title: 'daxiao' }, { title: 'hello' }] + }, + { + title: 'school', + id: 'tab3', + content: 'show me the school', + active: false, + child: [{ title: 'happy' }, { title: 'marray' }] + }, + { + title: 'About', + id: 'tab4', + content: 'Dynamic content 2', + disabled: false, + active: true, + child: [{ title: 'tinger' }] + } + ]; + + ngOnInit(): void { + setTimeout(() => { + this.tab1.show = true; + this.notShow = false; + }, 3000); + } + + onActiveChange(isActive: boolean, tabId: string): void { + if (isActive) { + console.log(tabId, 'not active => active'); + } else { + console.log(tabId, 'active => not active'); + } + } + + showTabs(): void { + this.hiden = !this.hiden; + } + + activeTab3(): void { + this.otherTabs[1]['active'] = true; + } + + remove(): void { + this.otherTabs.splice(1, 1); + } + + add(): void { + this.otherTabs.push({ + title: '添加', + content: '添加的', + active: true, + child: [{ title: 'freedom' }] + }); + } + + changeHeader(): void { + this.otherTabs[1]['title'] = 'dfasl dajdjfaksj'; + this.tab1.title = 'Our Good and peaceful'; + } +} diff --git a/src/tab/demo/src/app/tab/TabLazyLoadComponent.ts b/src/tab/demo/src/app/tab/TabLazyLoadComponent.ts new file mode 100644 index 0000000..0a1b9f8 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabLazyLoadComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tab-lazy-load.html' +}) +export class TabLazyLoadComponent { + tabs: any = [ + { + title: 'Tab1', + active: true + }, + { + title: 'Tab2' + }, + { + title: 'Tab3' + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabLevel2Component.ts b/src/tab/demo/src/app/tab/TabLevel2Component.ts new file mode 100644 index 0000000..a3d38b9 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabLevel2Component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './tab-level2.html' +}) +export class TabLevel2Component { + tabs: any = [ + { + title: 'Tab1', + active: true + }, + { + title: 'Tab2' + }, + { + title: 'Tab3' + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabLevel2TestComponent.ts b/src/tab/demo/src/app/tab/TabLevel2TestComponent.ts new file mode 100644 index 0000000..97b9287 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabLevel2TestComponent.ts @@ -0,0 +1,50 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tab-level2-test.html' +}) +export class TabLevel2TestComponent { + hiden: boolean = false; + tabsId: string = 'tabs_test'; + tab1: any = { + title: 'Home', + id: 'tab1', + active: false + }; + otherTabs: Array = [ + { + title: 'Profile', + id: 'tab2', + active: true, + disabled: false, + content: `Raw denim you probably haven't heard of them jean shorts Austin. ` + }, + { + title: 'school', + id: 'tab3', + content: 'show me the school' + }, + { + title: 'About', + id: 'tab4', + content: 'Dynamic content 2', + disabled: true + } + ]; + + onActiveChange(isActive: boolean): void { + if (isActive) { + console.log('not active => active'); + } else { + console.log('active => not active'); + } + } + + showTabs = (): void => { + this.hiden = !this.hiden; + }; + + activeTab3 = (): void => { + this.otherTabs[1]['active'] = true; + }; +} diff --git a/src/tab/demo/src/app/tab/TabOverflowComponent.ts b/src/tab/demo/src/app/tab/TabOverflowComponent.ts new file mode 100644 index 0000000..a7bb661 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabOverflowComponent.ts @@ -0,0 +1,46 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tab-overflow.html' +}) +export class TabOverflowComponent { + tabs: Array = [ + { + title: 'Tab1', + active: true + }, + { + title: 'Tab2' + }, + { + title: 'Tab3' + }, + { + title: 'Tab4' + }, + { + title: 'Tab5' + }, + { + title: 'Tab6' + }, + { + title: 'Tab7' + }, + { + title: 'Tab8' + }, + { + title: 'Tab9' + }, + { + title: 'Tab10' + }, + { + title: 'Tab11' + }, + { + title: 'Tab12' + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabRouteComponent.ts b/src/tab/demo/src/app/tab/TabRouteComponent.ts new file mode 100644 index 0000000..9cec9ca --- /dev/null +++ b/src/tab/demo/src/app/tab/TabRouteComponent.ts @@ -0,0 +1,66 @@ +import { Component } from '@angular/core'; +import { ActivatedRoute, NavigationEnd, Router, UrlTree } from '@angular/router'; + +@Component({ + template: `
    Welcome to School!
    ` +}) +export class SchoolComponent {} + +@Component({ + template: `
    Welcome to home!
    ` +}) +export class HomeComponent {} + +@Component({ + templateUrl: './tab-route.html' +}) +export class TabRouteComponent { + tabs: any = [ + { + title: 'home', + active: false, + onActiveChange: (isActive: boolean, title: string): void => { + if (isActive) { + this.router.navigate([title], { relativeTo: this.activeRoute }); + } + } + }, + { + title: 'school', + active: true, + onActiveChange: (isActive: boolean, title: string): void => { + if (isActive) { + this.router.navigate([title], { relativeTo: this.activeRoute }); + } + } + } + ]; + + private urlArray: Array = this.getUrlTree(this.router); + constructor(private router: Router, private activeRoute: ActivatedRoute) { + // 页面回退时,监听当前路由设置激活项 + router.events.forEach((event: any) => { + if (event instanceof NavigationEnd) { + const index: number = this.urlArray.findIndex((url: UrlTree) => { + return this.router.isActive(url, false); + }); + + if (this.tabs[index]) { + this.tabs[index].active = true; + } + } + }); + } + + private getUrlTree(router: Router): Array { + const urlArry: Array = []; + for (const tab of this.tabs) { + const url: UrlTree = router.createUrlTree([tab.title], { + relativeTo: this.activeRoute + }); + urlArry.push(url); + } + + return urlArry; + } +} diff --git a/src/tab/demo/src/app/tab/TabScrollComponent.ts b/src/tab/demo/src/app/tab/TabScrollComponent.ts new file mode 100644 index 0000000..66e535d --- /dev/null +++ b/src/tab/demo/src/app/tab/TabScrollComponent.ts @@ -0,0 +1,31 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + templateUrl: './tab-scroll.html' +}) +export class TabScrollComponent implements OnInit { + items: Array = []; + + tabs: Array = [ + { + title: 'Libary' + }, + { + title: 'Company', + active: true + }, + { + title: 'Concert1' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 15; j++) { + this.items.push((j * 113) % 29); + } + } + + addItems(): void { + this.items.push(333); + } +} diff --git a/src/tab/demo/src/app/tab/TabSmallComponent.ts b/src/tab/demo/src/app/tab/TabSmallComponent.ts new file mode 100644 index 0000000..c1f8205 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabSmallComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './tab-small.html' +}) +export class TabSmallComponent { + tabs: any = [ + { + title: 'Tab1', + active: true + }, + { + title: 'Tab2' + }, + { + title: 'Tab3' + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabTestModule.ts b/src/tab/demo/src/app/tab/TabTestModule.ts new file mode 100644 index 0000000..fcc46a2 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabTestModule.ts @@ -0,0 +1,102 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiFormfieldModule, TiTabModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TabBasicComponent } from './TabBasicComponent'; +import { TabLevel2Component } from './TabLevel2Component'; +import { TabCustomHeadComponent } from './TabCustomHeadComponent'; +import { TabLazyLoadComponent } from './TabLazyLoadComponent'; +import { HomeComponent, SchoolComponent, TabRouteComponent } from './TabRouteComponent'; +import { TabDefaultTestComponent } from './TabDefaultTestComponent'; +import { TabLevel2TestComponent } from './TabLevel2TestComponent'; +import { TabOverflowComponent } from './TabOverflowComponent'; +import { TabSmallComponent } from './TabSmallComponent'; +import { TabBeforeactivechangeComponent } from './TabBeforeactivechangeComponent'; +import { TabDarkComponent } from './TabDarkComponent'; +import { CompanyComponent, Concert1Component, ConcertComponent, LibaryComponent, TabContentCompComponent } from './TabContentCompComponent'; +import { TabScrollComponent } from './TabScrollComponent'; + +@NgModule({ + imports: [CommonModule, RouterModule, TiTabModule, TiFormfieldModule, DemoLogModule, RouterModule.forChild(TabTestModule.ROUTES)], + declarations: [ + TabBasicComponent, + TabBeforeactivechangeComponent, + TabLevel2Component, + TabCustomHeadComponent, + TabLazyLoadComponent, + TabRouteComponent, + TabOverflowComponent, + TabSmallComponent, + HomeComponent, + SchoolComponent, + TabDefaultTestComponent, + TabLevel2TestComponent, + TabContentCompComponent, + CompanyComponent, + ConcertComponent, + Concert1Component, + LibaryComponent, + TabScrollComponent, + TabDarkComponent + ] +}) +export class TabTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiTabsComponent.html', label: 'Tabs' }, + { href: 'components/TiTabComponent.html', label: 'Tab' }, + { href: 'components/TiTabHeaderComponent.html', label: 'TabHeader' } + ]; + static readonly ROUTES: Routes = [ + { + path: 'tab/tab-basic', + component: TabBasicComponent + }, + { + path: 'tab/tab-beforeactivechange', + component: TabBeforeactivechangeComponent + }, + { + path: 'tab/tab-dark', + component: TabDarkComponent + }, + { + path: 'tab/tab-level2', + component: TabLevel2Component + }, + { + path: 'tab/tab-custom-head', + component: TabCustomHeadComponent + }, + { + path: 'tab/tab-lazy-load', + component: TabLazyLoadComponent + }, + { + path: 'tab/tab-overflow', + component: TabOverflowComponent + }, + { + path: 'tab/tab-route', + component: TabRouteComponent, + children: [ + { path: 'home', component: HomeComponent }, + { path: 'school', component: SchoolComponent }, + { path: '', redirectTo: 'school', pathMatch: 'full' } + ] + }, + { + path: 'tab/tab-content-comp', + component: TabContentCompComponent + }, + { + path: 'tab/tab-small', + component: TabSmallComponent + }, + { path: 'tab/tab-default-test', component: TabDefaultTestComponent }, + { path: 'tab/tab-level2-test', component: TabLevel2TestComponent }, + { path: 'tab/tab-scroll', component: TabScrollComponent } + ]; +} diff --git a/src/tab/demo/src/app/tab/tab-basic.html b/src/tab/demo/src/app/tab/tab-basic.html new file mode 100644 index 0000000..4cb5ffb --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-basic.html @@ -0,0 +1,11 @@ + + +
    Content of Tab Pane 1
    +
    + +
    Content of Tab Pane 2
    +
    + +
    Content of Tab Pane 3
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-beforeactivechange.html b/src/tab/demo/src/app/tab/tab-beforeactivechange.html new file mode 100644 index 0000000..3512a74 --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-beforeactivechange.html @@ -0,0 +1,23 @@ + + +
    Content of Tab Pane 1
    +
    + +
    Content of Tab Pane 2
    +
    + +
    Content of Tab Pane 3
    +
    +
    + + diff --git a/src/tab/demo/src/app/tab/tab-content-comp.html b/src/tab/demo/src/app/tab/tab-content-comp.html new file mode 100644 index 0000000..f562e50 --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-content-comp.html @@ -0,0 +1,34 @@ +

    描述

    +

    tab页签组件内容区为组件的示例

    +

    示例

    + +

    1.方式一(在内容区直接以标签的形式使用组件):

    +
    +
    + + + + {{title1}} + + + + + + + + + +
    +

    +

    2.方式二(ngFor循环时,在内容区可借助Angular提供的ngComponentOutlet指令插入组件):

    +
    +
    + + + + + + +
    diff --git a/src/tab/demo/src/app/tab/tab-custom-head.html b/src/tab/demo/src/app/tab/tab-custom-head.html new file mode 100644 index 0000000..8a83a14 --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-custom-head.html @@ -0,0 +1,17 @@ + + + {{tabs[0].title}} +
    Content of Tab Pane 1
    +
    + + + {{tabs[1].title}} + (26) + +
    Content of Tab Pane 2
    +
    + + {{tabs[2].title}} +
    Content of Tab Pane 3
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-dark.html b/src/tab/demo/src/app/tab/tab-dark.html new file mode 100644 index 0000000..b6956cb --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-dark.html @@ -0,0 +1,11 @@ + + +
    Content of Tab Pane 1
    +
    + +
    Content of Tab Pane 2
    +
    + +
    Content of Tab Pane 3
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-default-test.html b/src/tab/demo/src/app/tab/tab-default-test.html new file mode 100644 index 0000000..7419cf8 --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-default-test.html @@ -0,0 +1,51 @@ +

    一级页签测试用例

    + +

    场景1:

    +
    + + + + {{tab1.title}} + ({{tab1.sum}}) + + If the expression evaluates to a string, the string should be one or more space-delimited class names.
    + If the expression evaluates to an object. +
    + +
    + {{tab.title}} +
    {{tab.content}}
    +
    {{item.title}}
    +
    +
    +
    +
    +

    + +

    + +

    + +

    + +

    + +

    + +

    场景2:

    +
    + + +
    +
    {{tab.content}}
    +
    {{item.title}}
    +
    +
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-lazy-load.html b/src/tab/demo/src/app/tab/tab-lazy-load.html new file mode 100644 index 0000000..ff42810 --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-lazy-load.html @@ -0,0 +1,18 @@ + + + +
    Content of Tab Pane 1
    +
    +
    + + +
    Content of Tab Pane 2
    +
    {{ item }}
    +
    +
    + + +
    Content of Tab Pane 3
    +
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-level2-test.html b/src/tab/demo/src/app/tab/tab-level2-test.html new file mode 100644 index 0000000..b74f1c5 --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-level2-test.html @@ -0,0 +1,24 @@ +

    二级页签示例

    +
    +
    + + + If the expression evaluates to a string, the string should be one or more space-delimited class names.
    + If the expression evaluates to an object. +
    + + {{tab.title}} +
    {{tab.content}}
    +
    +
    +
    +

    + +
    + diff --git a/src/tab/demo/src/app/tab/tab-level2.html b/src/tab/demo/src/app/tab/tab-level2.html new file mode 100644 index 0000000..95109ea --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-level2.html @@ -0,0 +1,11 @@ + + +
    Content of Tab Pane 1
    +
    + +
    Content of Tab Pane 2
    +
    + +
    Content of Tab Pane 3
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-overflow.html b/src/tab/demo/src/app/tab/tab-overflow.html new file mode 100644 index 0000000..91cfb57 --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-overflow.html @@ -0,0 +1,7 @@ +
    + + +
    Content of Tab Pane {{ i + 1 }}
    +
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-route.html b/src/tab/demo/src/app/tab/tab-route.html new file mode 100644 index 0000000..58a651d --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-route.html @@ -0,0 +1,8 @@ + + + + + +
    + +
    diff --git a/src/tab/demo/src/app/tab/tab-scroll.html b/src/tab/demo/src/app/tab/tab-scroll.html new file mode 100644 index 0000000..d84939f --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-scroll.html @@ -0,0 +1,40 @@ +

    描述

    +

    本测试用例主要测试tab是否影响页面触发滚动条。

    +

    + 问题 1:在有tab的页面,且tab的祖先级级元素display:none, 页面有滚动条时,将页面滚动条 + 拖至formfiled的头部不在视野范围内时,给formfield中添加数据后页面滚动条会异常跳动到顶部,正常滚动条应该不跳动。该问题在10.1.9版本修复 +

    +

    + 问题 2:在有tab的页面,且使用了 ti-tab-header 标签, 页面有滚动条时,将页面滚动条 + 拖至formfiled的头部不在视野范围内时,给formfield中添加数据后页面滚动条会异常跳动到顶部,正常滚动条应该不跳动。该问题在10.1.9版本修复 +

    +

    示例

    +
    + + +
    {{item}}
    +
    +
    + +
    +
    + +
    + + + +
    + +
    + + + + + title + (59) + + If the expression evaluates to a string, the string should be one or more space-delimited class names.
    + If the expression evaluates to an object. +
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-small.html b/src/tab/demo/src/app/tab/tab-small.html new file mode 100644 index 0000000..083863a --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-small.html @@ -0,0 +1,11 @@ + + +
    Content of Tab Pane 1
    +
    + +
    Content of Tab Pane 2
    +
    + +
    Content of Tab Pane 3
    +
    +
    diff --git a/src/tab/demo/src/app/tab/webdoc/tab-demos.js b/src/tab/demo/src/app/tab/webdoc/tab-demos.js new file mode 100644 index 0000000..3bf344e --- /dev/null +++ b/src/tab/demo/src/app/tab/webdoc/tab-demos.js @@ -0,0 +1,126 @@ +export default { + column: '2', + demos: [ + { + demoId: 'tab-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Tabs 组件的最简用法。

    ', + 'en-US': '', + }, + apis: [ + 'TiTabComponent.properties.disabled', + 'TiTabComponent.properties.active', + ], + }, + { + demoId: 'tab-dark', + name: { + 'zh-CN': '深色背景', + 'en-US': 'dark', + }, + desc: { + 'zh-CN': '

    通过属性dark配置深色背景页签。

    ', + 'en-US': '', + }, + apis: ['TiTabComponent.properties.dark'], + }, + { + demoId: 'tab-small', + name: { + 'zh-CN': '小尺寸页签', + 'en-US': 'small', + }, + desc: { + 'zh-CN': '

    通过属性small配置小尺寸页签。

    ', + 'en-US': '', + }, + apis: ['TiTabComponent.properties.small'], + }, + { + demoId: 'tab-level2', + name: { + 'zh-CN': '二级页签', + 'en-US': 'level2', + }, + desc: { + 'zh-CN': '

    通过属性level2配置二级页签。

    ', + 'en-US': '', + }, + apis: ['TiTabComponent.properties.level2'], + }, + { + demoId: 'tab-custom-head', + name: { + 'zh-CN': '自定义页签头部', + 'en-US': 'custom head', + }, + desc: { + 'zh-CN': + '

    通过ti-tab-header标签自定义当前页签头部。

    ', + 'en-US': '', + }, + apis: ['TiTabComponent.properties.header'], + }, + { + demoId: 'tab-overflow', + name: { + 'zh-CN': '页签超长下拉展示', + 'en-US': 'overflow', + }, + desc: { + 'zh-CN': + '

    使用大量选项卡切换的场景。通过属性panelWidth配置下拉面板的宽度,包含autojustifiedstring三种类型。通过属性panelMaxHeight配置下拉面板的最大高度。通过panelAlign配置面板对齐方式。

    ', + 'en-US': '', + }, + apis: [ + 'TiTabsComponent.properties.panelWidth', + 'TiTabsComponent.properties.panelMaxHeight', + 'TiTabsComponent.properties.panelAlign', + ], + }, + { + demoId: 'tab-lazy-load', + name: { + 'zh-CN': '懒加载', + 'en-US': 'lazyload', + }, + desc: { + 'zh-CN': + '

    通过 ng-template 标签包裹懒加载的内容区,并且加上#tiTabContent标识。当页签初次激活时,才会加载对应的内容。

    ', + 'en-US': '', + }, + }, + { + demoId: 'tab-route', + name: { + 'zh-CN': '路由', + 'en-US': 'route', + }, + desc: { + 'zh-CN': '

    Tabs 组件只用作头部导航展示,内容区由路由控制。

    ', + 'en-US': '', + }, + }, + { + demoId: 'tab-beforeactivechange', + name: { + 'zh-CN': '事件', + 'en-US': 'events', + }, + desc: { + 'zh-CN': + '

    切换页签之前触发beforeActiveChange事件。当切换页签时触发activeChange事件。

    ', + 'en-US': '', + }, + apis: [ + 'TiTabComponent.events.activeChange', + 'TiTabComponent.events.beforeActiveChange', + 'TiTabsComponent.methods.changeActive', + ], + }, + ], +}; diff --git a/src/tab/demo/src/app/tab/webdoc/tab.cn.md b/src/tab/demo/src/app/tab/webdoc/tab.cn.md new file mode 100644 index 0000000..3bd0519 --- /dev/null +++ b/src/tab/demo/src/app/tab/webdoc/tab.cn.md @@ -0,0 +1,23 @@ +--- +title: Tabs 页签 +--- +# Tabs 页签 + +
    + +Tabs 是选项卡切换组件。   + +```typescript +import { TiTabModule } from '@opentiny/ng'; +``` + +
    + +
    + +Tabs 是选项卡切换组件。   + +```typescript +import { TiTabModule } from '@cloud/tiny-config'; +``` +
    diff --git a/src/tab/demo/src/app/tab/webdoc/tab.en.md b/src/tab/demo/src/app/tab/webdoc/tab.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/tab/demo/src/app/tab/webdoc/tab.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/tab/demo/src/favicon.ico b/src/tab/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/tab/demo/src/index.html b/src/tab/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/tab/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/tab/demo/src/main.ts b/src/tab/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/tab/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/tab/demo/test.ts b/src/tab/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/tab/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/tab/demo/tsconfig.app.json b/src/tab/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/tab/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/tab/demo/tsconfig.spec.json b/src/tab/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/tab/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/tab/lib/index.ts b/src/tab/lib/index.ts new file mode 100644 index 0000000..ae118a4 --- /dev/null +++ b/src/tab/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTabModule'; diff --git a/src/tab/lib/ng-package.json b/src/tab/lib/ng-package.json new file mode 100644 index 0000000..5116880 --- /dev/null +++ b/src/tab/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/tab", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/tab/lib/package.json b/src/tab/lib/package.json new file mode 100644 index 0000000..5d8d430 --- /dev/null +++ b/src/tab/lib/package.json @@ -0,0 +1,16 @@ +{ + "name": "@opentiny/ng-tab", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-include": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-droplist": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/tab/lib/project.json b/src/tab/lib/project.json new file mode 100644 index 0000000..fa1fa59 --- /dev/null +++ b/src/tab/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/tab/lib", + "sourceRoot": "src/tab/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/tab"], + "options": { + "project": "src/tab/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/tab"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js tab" + }, + { + "command": "ng default-build tab" + }, + { + "command": "node build/clear-default-theme.js tab" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/tab && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build tab && ng pack tab && node build/publish.js tab --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/tab/lib/src/TiTabComponent.ts b/src/tab/lib/src/TiTabComponent.ts new file mode 100644 index 0000000..4600054 --- /dev/null +++ b/src/tab/lib/src/TiTabComponent.ts @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + HostBinding, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + Renderer2, + SimpleChange, + SimpleChanges, + TemplateRef +} from '@angular/core'; +import { TiTabsComponent } from './TiTabsComponent'; +import { Util } from '@opentiny/ng-utils'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * TiTabComponent 是单个页签组件,包含了页签标题头指令TiTabHeader 和 该页签对应的内容部分 + * + */ +@Component({ + selector: 'ti-tab', + templateUrl: './tab.html', + styleUrls: ['./tab.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-tab-pane]': 'true' + } +}) +export class TiTabComponent extends TiBaseComponent implements OnInit, OnDestroy, OnChanges { + /** + * 页签标题 + */ + @Input() header: string; + /** + * 是否禁用 + */ + @Input() disabled: boolean; + /** + * 当前页签是否激活,该接口是双向绑定的 + */ + @Input() @HostBinding('class.ti3-tab-active') active: boolean; + /** + * 页签激活项发生改变时触发的回调,参数为当前页签是否为激活状态 + */ + @Output() readonly activeChange: EventEmitter = new EventEmitter(); + /** + * 页签激活项发生改变前触发的回调 + */ + @Output() readonly beforeActiveChange: EventEmitter = new EventEmitter(); + + /** + * @ignore + * 获取到用户自定义的模板 + */ + @ContentChild('tiTabContent', { static: true }) + public contentTemplate: TemplateRef; + /** + * @ignore + * 当前页签标题头显示的node节点 + */ + public headNode: any; + /** + * @ignore + * 记录此页签的内容区是否已经加载过 + */ + public loaded: boolean = false; + protected versionInfo: string = super.getVersion(packageInfo); + + constructor( + private elementRef: ElementRef, + private renderer2: Renderer2, + public tabset: TiTabsComponent, + public changeDetectorRef: ChangeDetectorRef + ) { + super(elementRef, renderer2); + } + + ngOnInit(): void { + super.ngOnInit(); + this.createHeadNode(); + this.tabset.addTab(this); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + const activeObj: SimpleChange = changes['active']; + if (activeObj && activeObj.currentValue && this.tabset.selectedTab !== this && !this.disabled) { + this.tabset.activeTab(this, !activeObj.firstChange); + } + + const disabledChange: SimpleChange = changes['disabled']; + if (disabledChange && !disabledChange.firstChange) { + this.tabset.changeDetectorRef.markForCheck(); + } + + const headerChange: SimpleChange = changes['header']; + if (headerChange && !headerChange.firstChange) { + this.createHeadNode(); + this.tabset.changeDetectorRef.markForCheck(); + setTimeout(() => { + this.tabset.setTabStyle(true); + }, 0); + } + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + // 父容器TiTabs的OnDestroy执行晚于TiTab的OnDestroy,所以此处需要setTimeout来延时 + setTimeout(() => { + if (this.tabset.destroyed) { + return; + } + this.tabset.removeTab(this); + // tabs动态增删,onpush模式视图未更新 + this.tabset.changeDetectorRef.markForCheck(); + // 如果没有tab,那么下划线隐藏 + if (this.tabset.tabs.length === 0) { + this.tabset.tiRenderer.setStyles(this.tabset.slider.nativeElement, { + width: 0, + left: 0 + }); + } + }, 0); + } + + private createHeadNode(): void { + if (Util.isString(this.header)) { + this.headNode = this.renderer2.createText(this.header); + } + } +} diff --git a/src/tab/lib/src/TiTabHeaderComponent.ts b/src/tab/lib/src/TiTabHeaderComponent.ts new file mode 100644 index 0000000..e277882 --- /dev/null +++ b/src/tab/lib/src/TiTabHeaderComponent.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterContentInit, AfterViewChecked, ChangeDetectionStrategy, Component, ElementRef } from '@angular/core'; +import { TiTabComponent } from './TiTabComponent'; +/** + * TiTabHeaderComponent 是单个页签的标题头组件,其包裹的元素会作为当前页签的标题头 + * + */ +@Component({ + selector: 'ti-tab-header', + templateUrl: './tab-head.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-tab-head-no-outline]': 'true' + } +}) +export class TiTabHeaderComponent implements AfterContentInit, AfterViewChecked { + private oldHeaderText: string; + constructor(private tab: TiTabComponent, private elementRef: ElementRef) {} + + ngAfterContentInit(): void { + // 由于此处的AfterContentInit会比TiTabComponent组件的OnInit + // 执行得晚,所以ti-tab-header中的内容会覆盖header接口传入的字符串值 + this.tab.headNode = this.elementRef.nativeElement; + if (!this.tab.header) { + // 处理自定义dom没有header接口时对应下拉面板展示 + this.tab.header = this.elementRef.nativeElement.textContent; + } + this.oldHeaderText = this.tab.header; + } + + ngAfterViewChecked(): void { + // 既没有滑动条也没有溢出 + if (!this.tab.tabset.hasSlider && !this.tab.tabset.overflow) { + return; + } + + // 获取当前文本 + const headerText: string = this.elementRef.nativeElement.textContent; + + if (headerText === this.oldHeaderText) { + return; + } + + // 有滑动条slider场景 + if (this.tab.tabset.hasSlider) { + this.tab.tabset.headerWidthChange = true; + } + + // 溢出时:更新header + if (this.tab.tabset.overflow) { + this.tab.header = headerText; + } + + this.oldHeaderText = headerText; + } +} diff --git a/src/tab/lib/src/TiTabModule.ts b/src/tab/lib/src/TiTabModule.ts new file mode 100644 index 0000000..e23454d --- /dev/null +++ b/src/tab/lib/src/TiTabModule.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiTabsComponent } from './TiTabsComponent'; +import { TiTabComponent } from './TiTabComponent'; +import { TiTabHeaderComponent } from './TiTabHeaderComponent'; +import { TiIncludeModule } from '@opentiny/ng-include'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiDroplistModule } from '@opentiny/ng-droplist'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiDroplistModule, TiIconModule, TiIncludeModule, TiRendererModule], + exports: [TiTabsComponent, TiTabComponent, TiTabHeaderComponent], + declarations: [TiTabsComponent, TiTabComponent, TiTabHeaderComponent] +}) +export class TiTabModule {} +export { TiTabComponent } from './TiTabComponent'; +export { TiTabsComponent } from './TiTabsComponent'; +export { TiTabHeaderComponent } from './TiTabHeaderComponent'; diff --git a/src/tab/lib/src/TiTabsComponent.ts b/src/tab/lib/src/TiTabsComponent.ts new file mode 100644 index 0000000..34fe43e --- /dev/null +++ b/src/tab/lib/src/TiTabsComponent.ts @@ -0,0 +1,507 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + AfterViewChecked, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Inject, + Input, + NgZone, + OnDestroy, + OnInit, + Renderer2, + ViewChild +} from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +import { TiTabComponent } from './TiTabComponent'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiDroplistComponent } from '@opentiny/ng-droplist'; +import { TiPositionType, Util } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; +/** + * TiTabs组件属于布局组件,通过该组件产品可以实现不同视图的切换功能。 + * + * 其内部包含2个组件 TiTab 和 TiTabHeader + * + */ +@Component({ + selector: 'ti-tabs', + templateUrl: './tabs.html', + styleUrls: ['./tabs.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-tabs-container]': 'true' // 给host添加类ti-tabs-container替代之前的组件模板内部的最外层div + } +}) +export class TiTabsComponent extends TiBaseComponent implements AfterViewInit, AfterViewChecked, OnInit, OnDestroy { + /** + * 下拉面板的宽度。 + * + * 1."justified": 下拉面板宽度与页签项宽度保持一致; + * + * 2."auto": 下拉面板宽度根据下拉项的内容自动撑开; + * + * 3.固定的下拉面板宽度: 不小于页签项的宽度,例如:"200px"; + * + */ + @Input() panelWidth: string = '100px'; + /** + * 下拉面板对齐方式 + * + */ + @Input() panelAlign: 'left' | 'right' = 'right'; + /** + * 下拉面板最大高度 + * + */ + @Input() panelMaxHeight: string; + /** + * 下拉项超长文本提示方向 + * + * @ignore + */ + @Input() tipPosition: TiPositionType = 'left'; + /** + * @ignore + */ + @ViewChild('slider', { static: true }) slider: ElementRef; + /** + * @ignore + */ + @ViewChild('tabContentContainer', { static: true }) + tabContentContainer: ElementRef; + /** + * @ignore + */ + @ViewChild('more', { static: true }) moreRef: ElementRef; + /** + * @ignore + */ + @ViewChild('moreIcon', { static: true }) moreIconRef: ElementRef; + /** + * @ignore + */ + @ViewChild('tabsList', { static: true }) tabsListRef: ElementRef; + /** + * @ignore + */ + @ViewChild('tabsHeader', { static: true }) tabsHeaderRef: ElementRef; + /** + * @ignore + */ + @ViewChild('tabsContainer', { static: true }) tabsContainerRef: ElementRef; + /** + * @ignore + */ + @ViewChild('droplist', { static: true }) dropListComp: TiDroplistComponent; + /** + * @ignore + */ + public labelKey: string = 'header'; + /** + * @ignore + * 下拉项 + */ + public options: Array = []; + /** + * 被点击tab项 + */ + public clickTab: TiTabComponent; + /** + * @ignore + */ + public onlistenClick: () => void; + /** + * @ignore + */ + public windowResizeListener: () => void; + /** + * @ignore + * TiTabs中包裹的所有的 TiTabComponent 集合 + */ + public tabs: Array = []; + /** + * @ignore + */ + public selectedTab: TiTabComponent; // 当前选中的tab + private isNotVisibleInit: boolean = false; // 标志初始时是否可见 + /** + * @ignore + * TiTabComponent.ts中使用 + */ + public destroyed: boolean = false; + /** + * @ignore + * 是否显示滑动条slider(一级页签显示,二级/暗色页签不显示) + * TiTabHeaderComponent.ts中使用 + */ + public hasSlider: boolean = false; + /** + * @ignore + * 是否溢出 + * TiTabHeaderComponent.ts中使用 + */ + public overflow: boolean = false; + /** + * @ignore + * TiTabHeaderComponent.ts中使用 + */ + public headerWidthChange: boolean = false; + protected versionInfo: string = super.getVersion(packageInfo); + private resolvedPromise: Promise = Promise.resolve(null); + private tabsResizeObserver: any; + /** + * @ignore + * 下拉选中项变更时触发 + */ + public onDroplistChange($event: any): void { + this.tabs.forEach((item: any) => { + if (item.id === this.selectedTab.id) { + if (item.beforeActiveChange.observers.length === 0) { + this.changeActive(item); + } else { + this.clickTab = item; + item.beforeActiveChange.emit(this); + } + } + }); + } + /** + * @ignore + * 点击下拉按钮打开下拉面板 + */ + public showMorePanel(event: MouseEvent): void { + if (this.dropListComp.isShow) { + this.dropListComp.hide(); + + return; + } + this.dropListComp.show(); + event.stopPropagation(); + } + constructor( + private elementRef: ElementRef, + private renderer2: Renderer2, + public tiRenderer: TiRenderer, + private zone: NgZone, + @Inject(DOCUMENT) private document, + public changeDetectorRef: ChangeDetectorRef + ) { + super(elementRef, renderer2); + } + + ngOnInit(): void { + super.ngOnInit(); + // 初始化是否显示滑动条slider(一级页签显示,二级/暗色页签不显示) + this.hasSlider = !this.nativeElement.hasAttribute('level2') && !this.nativeElement.hasAttribute('dark'); + } + + ngAfterViewInit(): void { + super.ngAfterContentInit(); + if (this.hasSlider) { + // 如果祖先元素中display为none,获取到的组件的宽度为0,元素不可见 + this.isNotVisibleInit = this.nativeElement.clientWidth === 0; + } + this.zone.runOutsideAngular(() => { + // 点击documnet优惠信息窗口关闭 + this.onlistenClick = this.renderer2.listen(this.document, 'click', () => this.dropListComp.hide()); + if (typeof window === 'undefined') { + return; + } + this.windowResizeListener = this.renderer2.listen(window, 'resize', () => { + this.setMoreStyle(); + this.setTabStyle(true); + }); + + if (this.isNotVisibleInit && (window as any).ResizeObserver) { + // 利用 ResizeObserver 来监听tabs的尺寸发生改变的时机。IE不支持 ResizeObserver。 + this.tabsResizeObserver = new (window as any).ResizeObserver((entries: any): void => { + // 初始祖先元素中display为none,之后第一次由none变成可见时 + if (entries[0] && entries[0].contentRect.width !== 0) { + this.setTabStyle(false); + this.tabsResizeObserver.disconnect(); + } + }); + this.tabsResizeObserver.observe(this.nativeElement); + } + }); + } + + ngAfterViewChecked(): void { + super.ngAfterViewChecked(); + // 此处执行晚于TiTabHeaderComponent的ngAfterViewChecked + if (this.headerWidthChange) { + this.setTabStyle(false); + this.headerWidthChange = false; + this.setMoreStyle(); + } + } + + ngOnDestroy(): void { + this.destroyed = true; + if (this.windowResizeListener) { + this.windowResizeListener(); + } + if (this.onlistenClick) { + this.onlistenClick(); + } + if (this.tabsResizeObserver) { + this.tabsResizeObserver.disconnect(); + } + } + /** + * @ignore + */ + public setMoreStyle(): void { + if (this.isOverflow()) { + this.renderer2.setStyle(this.moreRef.nativeElement, 'display', 'block'); + } else { + this.renderer2.setStyle(this.moreRef.nativeElement, 'display', 'none'); + } + } + + /** + * @ignore + * TiTabComponent.ts 中调用 + */ + public addTab(tab: TiTabComponent): void { + if (tab.active) { + this.tabs.forEach((item: TiTabComponent) => { + if (item.active) { + this.setActiveValue(item, false); + } + }); + } + this.tabs.push(tab); + const tabElements: Array = Array.from(this.tabContentContainer.nativeElement.children); + this.tabs.sort((a: TiTabComponent, b: TiTabComponent): number => { + return tabElements.indexOf(a.nativeElement) - tabElements.indexOf(b.nativeElement); + }); + if (!tab.active && this.selectedTab && tabElements.indexOf(tab.nativeElement) < tabElements.indexOf(this.selectedTab.nativeElement)) { + setTimeout(() => { + this.setTabStyle(true); + }, 0); + } + this.options = this.tabs; + setTimeout(() => { + this.setMoreStyle(); + }, 0); + // tabs动态增删,onpush模式视图未更新 + this.changeDetectorRef.markForCheck(); + } + /** + * @ignore + * TiTabComponent.ts 中调用 + */ + public removeTab(tab: TiTabComponent): void { + const index: number = this.tabs.indexOf(tab); + const length: number = this.tabs.length; + this.tabs.splice(index, 1); + + setTimeout(() => { + this.setMoreStyle(); + }, 0); + + // 删除当前激活状态的tab + if (tab.active && length > 1) { + const newActiveIndex: number = this.getNewActiveIndex(index, length); + if (newActiveIndex !== -1) { + this.changeActive(this.tabs[newActiveIndex]); + } + + return; + } + + // 删除的是非激活状态的tab,那么就在删除后的tabs数组中查找剩下的哪一个tab是激活状态,再设置激活状态; + if (!tab.active) { + setTimeout(() => { + this.setTabStyle(true); + }, 0); + } + } + /** + * @ignore + * TiTabComponent.ts 中调用 + */ + public activeTab(selectedTab: TiTabComponent, enableAnimate: boolean): void { + this.deActiveOthers(selectedTab); + + selectedTab.loaded = true; + + this.selectedTab = selectedTab; + // onpush模式下,懒加载页签内容不刷新 + this.selectedTab.changeDetectorRef.markForCheck(); + // 只有不是灰化状态时,才触发切换动效 + if (!selectedTab.disabled) { + // 添加定时器,处理ngFor未渲染完毕,获取到得silder的位置不准确 + setTimeout(() => { + // 初始化激活时没有动效 + this.setTabStyle(enableAnimate); + }, 0); + } + } + + private deActiveOthers(selectTab: TiTabComponent): void { + this.tabs.forEach((tab: TiTabComponent) => { + if (tab.active && tab !== selectTab) { + // 处理有路由存在情况下,点击浏览器后退按钮,上一次选中项处于聚焦状态,导致两个页签高亮 #2136 + if (tab.headNode && tab.headNode.parentNode) { + tab.headNode.parentNode.blur(); + } + + this.setActiveValue(tab, false); + } + }); + // 通过其他方式(非直接点击页签)修改tab.active,页面激活效果未更新(onPush问题) + this.changeDetectorRef.markForCheck(); + } + + /** + * @ignore + */ + public click(tab: TiTabComponent): void { + if (tab.disabled || tab.active) { + return; + } + if (tab.beforeActiveChange.observers.length === 0) { + this.changeActive(tab); + } else { + this.clickTab = tab; + tab.beforeActiveChange.emit(this); + } + } + /** + * 页签激活状态变更后执行 + */ + public changeActive(tab: TiTabComponent): void { + this.setActiveValue(tab, true); + this.activeTab(tab, true); + } + + // 删除当前激活状态的tab后,需要激活下一个active tab + private getNewActiveIndex(index: number, length: number): number { + // 如果删除的当前激活状态的tab是最后一个, + // 那么下一个激活的tab就是从右到左第一个不是禁用的tab + if (index === length - 1) { + for (let i: number = index - 1; i >= 0; i--) { + if (!this.tabs[i].disabled) { + return i; + } + } + // 如果删除的当前激活状态的tab不是最后一个, + // 那么下一个激活的tab就是从左到右第一个不是禁用的tab + } else { + for (let i: number = index; i < this.tabs.length; i++) { + if (!this.tabs[i].disabled) { + return i; + } + } + } + + return -1; + } + + /** + * @ignore + * 设置页签样式 + */ + public setTabStyle(enableAnimate: boolean): void { + const targetTab: any = this.selectedTab && this.selectedTab.headNode.parentNode.parentNode; + // 容错处理:没有获取到DOM时直接return; + if (Util.isUndefined(targetTab) || !targetTab.classList.contains('ti3-tab-li')) { + return; + } + // 获取该页签的偏移量 + const tagetLeft: number = targetTab.offsetLeft; + // 需要显示滑动条时设置滑动条的样式及动效 + if (this.hasSlider) { + // 修复SSR错误:ERROR TypeError: targetTab.getBoundingClientRect is not a function + const targetWidth: number = + typeof targetTab.getBoundingClientRect === 'function' ? parseFloat(targetTab.getBoundingClientRect().width.toFixed(2)) : 0; + const transitionSet: string = enableAnimate ? 'left 0.1s ease-in-out' : 'none'; + this.tiRenderer.setStyles(this.slider.nativeElement, { + transition: transitionSet, + width: `${targetWidth}px`, + left: `${tagetLeft}px` + }); + } + this.setTabScroll(targetTab, tagetLeft); + } + // 设置页签超长滑动动效 + private setTabScroll(target: any, distance: number): void { + const moreWidth: number = this.moreRef && this.moreRef.nativeElement.offsetWidth; + const tabsHeaderWidth: number = this.tabsHeaderRef && this.tabsHeaderRef.nativeElement.offsetWidth; + const listWidth: number = this.tabsListRef && this.tabsListRef.nativeElement.offsetWidth; + // 修复SSR错误:ERROR TypeError: this.tabsContainerRef.nativeElement.getBoundingClientRect is not a function + if (typeof this.tabsContainerRef.nativeElement.getBoundingClientRect !== 'function') { + return; + } + const tabsContainerClientRect: any = this.tabsContainerRef && this.tabsContainerRef.nativeElement.getBoundingClientRect(); + const targetClientRect: any = target.getBoundingClientRect(); + // 1、条件1和2:激活项能在当前屏范围内完全显示时 + // 2、尾部页签紧贴右侧时(页签动态删减,导致尾部页签没有紧贴右侧 #15412) + // 同时满足,tabs无需滑动 + if ( + targetClientRect.x >= tabsContainerClientRect.x && + targetClientRect.x <= tabsContainerClientRect.x + tabsContainerClientRect.width - moreWidth - targetClientRect.width && + tabsHeaderWidth <= listWidth + moreWidth + ) { + return; + } + + let marginLeftDistance: number = distance; + const maxDistance: number = this.tabsListRef.nativeElement.scrollWidth - tabsContainerClientRect.width + moreWidth; + if (maxDistance > 0) { + if (distance >= maxDistance) { + marginLeftDistance = maxDistance; + } + } else { + marginLeftDistance = 0; + } + this.renderer2.setStyle(this.tabsHeaderRef.nativeElement, 'transition', 'margin-left 0.1s ease-in-out'); + this.renderer2.setStyle(this.tabsHeaderRef.nativeElement, 'margin-left', -marginLeftDistance + 'px'); + } + + private setActiveValue(tab: TiTabComponent, value: boolean): void { + // 双向数据绑定时,初始传入的值不合法立即修改并传出时会报错。 + // 此处参考ngModel源码setValue的处理,使用promise延后执行时序 + this.resolvedPromise.then(() => { + tab.active = value; + tab.activeChange.emit(value); + // tab快捷键操作时,active状态未及时更新 + this.changeDetectorRef.markForCheck(); + }); + } + + /** + * @ignore + */ + public trackByFn(index: number, item: any): Node { + return item.headNode; + } + + // 空间不足时,出现下拉面板 + private isOverflow(): boolean { + const headerWidth: number = this.tabsContainerRef.nativeElement.offsetWidth; + const listWidth: number = this.tabsListRef.nativeElement.scrollWidth; + const moreWidth: number = this.moreRef.nativeElement.offsetWidth; + + this.overflow = headerWidth - moreWidth < listWidth; + + return this.overflow; + } +} diff --git a/src/tab/lib/src/tab-head.html b/src/tab/lib/src/tab-head.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/tab/lib/src/tab-head.html @@ -0,0 +1 @@ + diff --git a/src/tab/lib/src/tab.html b/src/tab/lib/src/tab.html new file mode 100644 index 0000000..54f71b7 --- /dev/null +++ b/src/tab/lib/src/tab.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/tab/lib/src/tab.less b/src/tab/lib/src/tab.less new file mode 100644 index 0000000..a778f40 --- /dev/null +++ b/src/tab/lib/src/tab.less @@ -0,0 +1,19 @@ +@import '../../../themes/basic/base-all.less'; + +// Hide tabbable panes to start, show them when `.active` +:host.ti3-tab-pane { + display: none; + padding-top: var(--ti-common-space-3x); + animation-duration: 0.3s; + animation-timing-function: ease; + animation-fill-mode: backwards; + &.ti3-tab-active { + display: block; + } + &.active-remove { + display: none !important; + } + &.active-add { + animation-name: tiFadeIn; + } +} diff --git a/src/tab/lib/src/tabs.html b/src/tab/lib/src/tabs.html new file mode 100644 index 0000000..c7fb054 --- /dev/null +++ b/src/tab/lib/src/tabs.html @@ -0,0 +1,44 @@ +
    +
    +
      +
    • + +
    • +
    + + + +
    +
    +
    +
    +
    +
    + +
    + + diff --git a/src/tab/lib/src/tabs.less b/src/tab/lib/src/tabs.less new file mode 100644 index 0000000..a9acc08 --- /dev/null +++ b/src/tab/lib/src/tabs.less @@ -0,0 +1,265 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-tab-second-level-header-height: var(--ti-common-size-7x); + --ti-tab-second-level-header-line-height: calc(var(--ti-tab-second-level-header-height) - 2px); + --ti-tab-dark-container-height: var(--ti-common-size-10x); + --ti-tab-dark-li-height: var(--ti-common-size-9x); + --ti-tab-slider-height: var(--ti-common-border-weight-2); +} + +:host.ti3-tabs-container { + display: block; +} + +.ti3-tab-content { + border-top: 1px solid var(--ti-common-color-line-dividing); +} + +.ti3-tabs { + .box-sizing(); + display: flex; + white-space: nowrap; + flex-shrink: 1; + overflow: hidden; + position: relative; + padding-left: 0; + margin-bottom: 0; + list-style: none; + font-size: 0; + height: var(--ti-common-size-9x); + &::before { + display: table; + content: ' '; + .box-sizing(); + } + &::after { + display: table; + content: ' '; + clear: both; + .box-sizing(); + } + & > li { + .box-sizing(); + margin: 0 var(--ti-common-space-5x); + position: relative; + display: inline-block; + cursor: pointer; + line-height: var(--ti-common-line-height-number); + height: 100%; + font-size: var(--ti-common-font-size-1); + &:first-child { + margin-left: 0; + } + & > a { + .box-sizing(); + position: relative; + display: block; + // 修改a标签的下padding + padding: var(--ti-common-space-6) 0 var(--ti-common-space-2x); + text-decoration: none; + //此处line-height值与height值不一致是为了使文字距蓝色下边线的距离为10px + line-height: var(--ti-common-line-height-number); + height: 100%; + color: var(--ti-common-color-text-secondary); + .transition (color 150ms); + } + &:not(.disabled) > a:hover, + &:not(.disabled) > a:focus { + border-bottom-color: transparent; + color: var(--ti-common-color-text-highlight); + //添加此属性用来兼容ie和火狐下a标签会产生外围的虚线框 + outline: none; + .transition (color 200ms); + } + &.ti3-tab-active:not(.disabled) > a { + cursor: default; + color: var(--ti-common-color-text-highlight); + font-weight: normal; + } + &.disabled > a { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + background-color: transparent; + &:focus { + outline: none !important; + } + } + } +} +.ti3-tab-slider-container { + border-bottom: var(--ti-tab-slider-height) solid transparent; + position: absolute; + left: 0; + right: 0; + bottom: 0; +} +.ti3-tab-slider { + .box-sizing(); + border-bottom: var(--ti-tab-slider-height) solid var(--ti-common-color-bg-emphasize); + width: 0px; + position: absolute; + left: -50px; +} + +:host[level2] .ti3-tabs-container1 { + position: relative; + top: 1px; +} + +:host[level2] .ti3-tabs { + //二级页签的时候,滑块儿不显示 + & + .ti3-tab-more + .ti3-tab-slider-container .ti3-tab-slider { + display: none; + border-bottom: none; + } + height: var(--ti-tab-second-level-header-height); + & > li { + font-size: var(--ti-common-font-size-base); + height: var(--ti-tab-second-level-header-height); + line-height: var(--ti-tab-second-level-header-line-height); + margin: 0; + & > a { + .box-sizing(); + height: var(--ti-tab-second-level-header-height); + line-height: var(--ti-tab-second-level-header-line-height); + color: var(--ti-common-color-text-secondary); + padding: 0 var(--ti-common-space-3x); + border: 1px solid transparent; + } + &:not(.disabled) > a:hover, + &:not(.disabled) > a:focus { + color: var(--ti-common-color-text-highlight); + //添加此属性用来兼容ie和火狐下a标签会产生外围的虚线框 + outline: none; + } + &.ti3-tab-active:not(.disabled) > a { + border-color: var(--ti-common-color-line-dividing); + border-bottom-color: var(--ti-common-color-bg-white-normal); + color: var(--ti-common-color-text-highlight); + padding: 0 var(--ti-common-space-3x); + } + &.ti3-tab-active:not(.disabled), + &:last-child { + margin-right: 0; + } + &.disabled > a { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + background-color: transparent; + } + } +} +:host[dark] > .ti3-tabs-container1 .ti3-tabs-header { + background-color: var(--ti-common-color-bg-dark-normal); +} + +:host[dark] > .ti3-tabs-container1 .ti3-tabs { + height: var(--ti-tab-dark-container-height); + padding-top: calc(var(--ti-tab-dark-container-height) - var(--ti-tab-dark-li-height)); + & + .ti3-tab-more { + padding-right: var(--ti-common-space-5x); + color: var(--ti-common-color-text-darkbg); + &:hover { + color: var(--ti-common-color-bg-emphasize); + } + } + //暗色页签的时候,滑块儿不显示 + & + .ti3-tab-more + .ti3-tab-slider-container .ti3-tab-slider { + display: none; + border-bottom: none; + } + & > li { + margin-left: 0; + margin-right: var(--ti-common-space-base); + & > a { + .box-sizing(); + height: var(--ti-tab-dark-li-height); + line-height: var(--ti-tab-dark-li-height); + color: var(--ti-common-color-text-darkbg); + padding: 0 var(--ti-common-space-6x); + border-radius: var(--ti-common-border-radius-normal) var(--ti-common-border-radius-normal) 0 0; + .transition (background 150ms ease-in); + } + &:not(.disabled) > a:hover, + &:not(.disabled) > a:focus { + background-color: var(--ti-common-color-bg-dark-emphasize); + color: var(--ti-common-color-text-white); + outline: none; + .transition (background 200ms); + } + &.ti3-tab-active:not(.disabled) > a { + background-color: var(--ti-common-color-bg-normal); + color: var(--ti-common-color-text-primary); + .transition (background 250ms); + } + &.disabled > a { + color: var(--ti-common-color-text-gray-disabled); + cursor: not-allowed; + background-color: transparent; + } + &:first-child { + margin-left: var(--ti-common-space-5x); + } + &:last-child { + margin-right: 0; + } + } +} + +// 去掉下边框 +:host[dark].ti3-tabs-container { + & > .ti3-tab-content { + border: none; + } +} + +// 小尺寸页签 +:host[small] .ti3-tab-li { + font-size: var(--ti-common-font-size-base); + margin: 0 var(--ti-common-space-10); +} + +@-webkit-keyframes tiFadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +@keyframes tiFadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.ti3-tab-more { + cursor: pointer; + display: none; + padding-left: var(--ti-common-space-10); + &:hover { + color: var(--ti-common-color-text-highlight); + } + & ti-icon[local] { + display: block; + } +} + +.ti3-tabs-header { + display: flex; + justify-content: space-between; + align-items: center; + position: relative; +} + +.ti3-tabs-container1 { + overflow: hidden; +} + +.ti3-tab-head-no-outline { + outline: none; +} diff --git a/src/table/demo/karma.conf.js b/src/table/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/table/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/table/demo/project.json b/src/table/demo/project.json new file mode 100644 index 0000000..ccf4644 --- /dev/null +++ b/src/table/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/table/demo", + "sourceRoot": "src/table/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/table", + "index": "src/table/demo/src/index.html", + "main": "src/table/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/table/demo/tsconfig.app.json", + "assets": ["src/table/demo/src/favicon.ico", "src/table/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "table-demo:build:production" + }, + "development": { + "browserTarget": "table-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js table" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/table/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/table/demo/tsconfig.spec.json", + "karmaConfig": "src/table/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/table/demo/src/app/AppComponent.ts b/src/table/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/table/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/table/demo/src/app/AppModule.ts b/src/table/demo/src/app/AppModule.ts new file mode 100644 index 0000000..c41135b --- /dev/null +++ b/src/table/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TableTestModule } from './table/TableTestModule'; + +@NgModule({ + imports: [ + TableTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/table/demo/src/app/IndexComponent.ts b/src/table/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..ed5e866 --- /dev/null +++ b/src/table/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TableTestModule } from './table/TableTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TableTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/table/demo/src/app/app.html b/src/table/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/table/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/table/demo/src/app/table/TableActionmenuComponent.ts b/src/table/demo/src/app/table/TableActionmenuComponent.ts new file mode 100644 index 0000000..fdf8575 --- /dev/null +++ b/src/table/demo/src/app/table/TableActionmenuComponent.ts @@ -0,0 +1,81 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-actionmenu.html' +}) +export class TableActionmenuComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + name: 's2.small.2', + createTime: new Date(2003, 2, 6), + operator: 'Pierre Dupont', + state: 'on' + }, + { + name: 's2.xlarge.2', + createTime: new Date(2013, 9, 1), + operator: 'Jacques Germain', + state: 'off' + }, + { + name: 's2.medium.1', + createTime: new Date(2015, 1, 1), + operator: 'Robert Delcourt', + state: 'disabled' + }, + { + name: 's2.large.1', + createTime: new Date(2008, 5, 12), + operator: 'Elisa Menez', + state: 'stop' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'create Time' + }, + { + title: 'Operator' + }, + { + title: 'State' + }, + { + title: 'Action' + } + ]; + + onSelect(item: TiActionmenuItem, row: TiTableRowData): void { + row.state = item.label; + } + + dataToItemsFn: (data: any) => Array = (data: any) => { + let items: Array = [ + { + label: 'on' + }, + { + label: 'stop' + }, + { + label: 'off' + }, + { + label: 'disabled' + } + ]; + if (data.operator === 'Robert Delcourt') { + items = [{ ...items[0], disabled: true }, items[3]]; + } + + return items; + }; +} diff --git a/src/table/demo/src/app/table/TableBasicComponent.ts b/src/table/demo/src/app/table/TableBasicComponent.ts new file mode 100644 index 0000000..084dfd5 --- /dev/null +++ b/src/table/demo/src/app/table/TableBasicComponent.ts @@ -0,0 +1,52 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-basic.html' +}) +export class TableBasicComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], // 源数据 + state: undefined // 源数据分页、排序、搜索状态 + }; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableBasicTestComponent.ts b/src/table/demo/src/app/table/TableBasicTestComponent.ts new file mode 100644 index 0000000..d0ca795 --- /dev/null +++ b/src/table/demo/src/app/table/TableBasicTestComponent.ts @@ -0,0 +1,100 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-basic-test.html' +}) +export class TableBasicTestComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + changeData(): void { + const temp: TiTableRowData = { + firstName: 'slkdfjsadf', + lastName: 'weiorui', + age: 23, + email: 'sdlfk@example.com', + balance: (this.data.length * 761) % 10000, + id: 'weoiriow' + }; + // 推荐 + this.srcData.data = this.srcData.data.concat(temp); + } + changeData2(): void { + // 测试删除 + this.srcData.data.splice(0, 1); + } + + changeData3(): void { + // 测试直接改变一行的数据是否生效 + this.srcData.data[0].firstName = '新的名字'; + this.srcData.data[1].age = 100; + } +} diff --git a/src/table/demo/src/app/table/TableCellTipComponent.ts b/src/table/demo/src/app/table/TableCellTipComponent.ts new file mode 100644 index 0000000..3fd99d7 --- /dev/null +++ b/src/table/demo/src/app/table/TableCellTipComponent.ts @@ -0,0 +1,56 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-cell-tip.html' +}) +export class TableCellTipComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + name: 'Pierre Dupont', + age: 20, + email: 'PierreLong@example.com', + address: 'New York No. 1 Lake Park', + tipContent: 'address: New York No. 1 Lake Park' + }, + { + name: 'Jacques Germain', + age: 42, + email: 'JacquesLong@example.com', + address: 'Sidney No. 1 Lake Park', + tipContent: 'address: Sidney No. 1 Lake Park' + }, + { + name: 'Robert Delcourt', + age: 15, + email: 'RobertLong@example.com', + address: 'London No. 1 Lake Park', + tipContent: 'address: London No. 1 Lake Park' + }, + { + name: 'Elisa Menez', + age: 36, + email: 'ElisaLong@example.com', + address: 'Los Angeles No. 1 Lake Park', + tipContent: 'address: Los Angeles No. 1 Lake Park' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'Age' + }, + { + title: 'Long Email Address' + }, + { + title: 'Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableCelliconsColsresizableComponent.ts b/src/table/demo/src/app/table/TableCelliconsColsresizableComponent.ts new file mode 100644 index 0000000..37ff325 --- /dev/null +++ b/src/table/demo/src/app/table/TableCelliconsColsresizableComponent.ts @@ -0,0 +1,77 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-cellicons-colsresizable.html' +}) +export class TableCelliconsColsresizableComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + sortKey: 'firstName' + }, + { + title: 'last name', + width: '20%', + sortKey: 'lastName' + }, + { + title: 'birth date', + width: '10%', + sortKey: 'age' + }, + { + title: 'balance', + width: '30%', + sortKey: 'balance' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableCheckboxComponent.ts b/src/table/demo/src/app/table/TableCheckboxComponent.ts new file mode 100644 index 0000000..918040d --- /dev/null +++ b/src/table/demo/src/app/table/TableCheckboxComponent.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-checkbox.html' +}) +export class TableCheckboxComponent { + checkedList: Array = []; + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com', + disabled: true + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: '' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; + + ngOnInit(): void { + // this.checkedList.push(this.srcData.data[6]); // 设置初始选中项 + } + + trackByFn(index: number, item: any): number { + return item.firstName; + } +} diff --git a/src/table/demo/src/app/table/TableCheckboxPaginationComponent.ts b/src/table/demo/src/app/table/TableCheckboxPaginationComponent.ts new file mode 100644 index 0000000..5151747 --- /dev/null +++ b/src/table/demo/src/app/table/TableCheckboxPaginationComponent.ts @@ -0,0 +1,146 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-checkbox-pagination.html' +}) +export class TableCheckboxPaginationComponent implements OnInit { + constructor(private ref: ChangeDetectorRef) {} + + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + checkedList: Array = []; // 默认选中项 + currentPage: number = 1; + totalNumber: number = 56; + show: boolean = false; + pageSize: { options: Array; size: number } = { + options: [10, 20, 50, 100], + size: 10 + }; + columns: Array = [ + { + title: '' + }, + { + title: 'last name' + }, + { + title: 'birth date' + }, + { + title: 'balance' + }, + { + title: 'email' + } + ]; + + displayed1: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData1: TiTableSrcData; + private data1: Array = []; + checkedList1: Array = []; // 默认选中项 + currentPage1: number = 1; + totalNumber1: number = 0; + pageSize1: { options: Array; size: number } = { + options: [5, 10, 20, 50], + size: 10 + }; + columns1: Array = [ + { + title: '' + }, + { + title: 'last name' + }, + { + title: 'birth date' + }, + { + title: 'balance' + }, + { + title: 'email' + } + ]; + show1: boolean = false; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + + this.srcData1 = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: true // 源数据已进行分页处理 + } + }; + // 初始时向后台发送请求获取数据 + this.getCurrentPageData(this.currentPage1, this.pageSize1.size).then((data: Array) => { + this.srcData1.data = data; + this.totalNumber1 = 256; + }); + } + + // 模拟后台异步请求返回的数据 + private getCurrentPageData(currentPage: number, itemsPerPage: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + const data: Array = []; + for (let j: number = (currentPage - 1) * itemsPerPage; j < currentPage * itemsPerPage; j++) { + data.push(this.createRandomItem(j)); + } + resolve(data); + // 在onpush模式下,异步场景需要手动触发变更,default模式下不用写该行代码 + this.ref.markForCheck(); + }, 500); + }); + } + + // 表格的数据状态(分页,排序,搜索)发生改变时,此方法会被触发; + // 多用于后台分页、排序和搜索 + stateUpdate(tiTable: TiTableComponent): void { + const dataState: TiTableDataState = tiTable.getDataState(); + this.getCurrentPageData(dataState.pagination.currentPage, dataState.pagination.itemsPerPage).then((data: Array) => { + this.srcData1.data = data; + }); + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const disabled: boolean = id % 8 === 0; + + return { + firstName, + lastName, + age, + email, + balance, + disabled, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableCheckboxPaginationHeadmenuComponent.ts b/src/table/demo/src/app/table/TableCheckboxPaginationHeadmenuComponent.ts new file mode 100644 index 0000000..6c8cf7f --- /dev/null +++ b/src/table/demo/src/app/table/TableCheckboxPaginationHeadmenuComponent.ts @@ -0,0 +1,133 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-checkbox-pagination-headmenu.html' +}) +export class TableCheckboxPaginationHeadmenuComponent implements OnInit { + default: any = { + checkedList: [], + displayedData: [], + srcData: { + data: [], + state: undefined + }, + currentPage: 1, + totalNumber: 56 + }; + + custom: any = { + checkedList: [], + displayedData: [], + srcData: { + data: [], + state: undefined + }, + currentPage: 1, + totalNumber: 68, + options: [ + { + key: 'all', + label: '选择所有数据' + }, + { + key: 'age', + label: '选择所有年龄不小于60岁的' + }, + { + key: 'clear', + label: '清空' + } + ] + }; + columns: Array = [ + { + title: '' + }, + { + title: 'Id' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; + pageSize: { options: Array; size: number } = { + options: [5, 10, 50], + size: 5 + }; + + ngOnInit(): void { + this.default.srcData.data = this.getData(this.default.totalNumber); + this.custom.srcData.data = this.getData(this.custom.totalNumber); + } + + onDefaultHeadmenuSelect(option: any): void { + if (option.key === 'checkAll') { + this.default.checkedList = this.default.srcData.data.filter((row: TiTableRowData) => { + return !row.disabled || this.default.checkedList.includes(row); + }); + } else { + this.default.checkedList = this.default.srcData.data.filter((row: TiTableRowData) => { + return row.disabled && this.default.checkedList.includes(row); + }); + } + } + + onCustomHeadmenuSelect(option: any): void { + if (option.key === 'all') { + this.custom.checkedList = this.custom.srcData.data.filter((row: TiTableRowData) => { + return !row.disabled || this.custom.checkedList.includes(row); + }); + } else if (option.key === 'age') { + this.custom.checkedList = this.custom.srcData.data.filter((row: TiTableRowData, index: number) => { + const isMatched: boolean = row.age >= 60; + return (isMatched && !row.disabled) || (row.disabled && this.custom.checkedList.includes(row)); + }); + } else { + this.custom.checkedList = this.custom.srcData.data.filter((row: TiTableRowData) => { + return row.disabled && this.custom.checkedList.includes(row); + }); + } + } + + trackByFn(index: number, item: TiTableRowData): number { + return item.id; + } + + private getData(totalNumber: number): Array { + const data: Array = []; + for (let j: number = 0; j < totalNumber; j++) { + data.push(this.createRandomItem(j)); + } + + return data; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const disabled: boolean = id % 5 === 1; + + return { + firstName, + lastName, + age, + email, + disabled, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColAlignComponent.ts b/src/table/demo/src/app/table/TableColAlignComponent.ts new file mode 100644 index 0000000..2e258cd --- /dev/null +++ b/src/table/demo/src/app/table/TableColAlignComponent.ts @@ -0,0 +1,52 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-col-align.html' +}) +export class TableColAlignComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableColalignSortResizableTestComponent.ts b/src/table/demo/src/app/table/TableColalignSortResizableTestComponent.ts new file mode 100644 index 0000000..1c5f562 --- /dev/null +++ b/src/table/demo/src/app/table/TableColalignSortResizableTestComponent.ts @@ -0,0 +1,72 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-colalign-sort-resizable-test.html' +}) +export class TableColalignSortResizableTestComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + sortKey: 'firstName' + }, + { + title: 'last name', + sortKey: 'lastName' + }, + { + title: 'birth date', + sortKey: 'age' + }, + { + title: 'balance', + sortKey: 'balance' + }, + { + title: 'email' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColsResizableComponent.ts b/src/table/demo/src/app/table/TableColsResizableComponent.ts new file mode 100644 index 0000000..7eada46 --- /dev/null +++ b/src/table/demo/src/app/table/TableColsResizableComponent.ts @@ -0,0 +1,59 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-cols-resizable.html' +}) +export class TableColsResizableComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com', + address: 'New York No. 1 Lake Park' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com', + address: 'Sidney No. 1 Lake Park' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com', + address: 'London No. 1 Lake Park' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com', + address: 'Los Angeles No. 1 Lake Park' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Address' + }, + { + title: 'Email Address' + }, + { + title: 'Age' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableColsToggleComponent.ts b/src/table/demo/src/app/table/TableColsToggleComponent.ts new file mode 100644 index 0000000..56ab827 --- /dev/null +++ b/src/table/demo/src/app/table/TableColsToggleComponent.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-cols-toggle.html' +}) +export class TableColsToggleComponent { + displayedData: Array = []; + searchable: boolean = true; + selectAll: boolean = true; + srcData: TiTableSrcData = { + data: [ + { + id: '1', + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + id: '2', + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + id: '3', + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + id: '4', + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'Id' + }, + { + title: 'First Name', + show: true + }, + { + title: 'Last Name', + show: false + }, + { + title: 'Age', + show: true + }, + { + title: 'Email Address', + show: true + } + ]; +} diff --git a/src/table/demo/src/app/table/TableColsToggleDetailsComponent.ts b/src/table/demo/src/app/table/TableColsToggleDetailsComponent.ts new file mode 100644 index 0000000..7e0de43 --- /dev/null +++ b/src/table/demo/src/app/table/TableColsToggleDetailsComponent.ts @@ -0,0 +1,81 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-cols-toggle-details.html' +}) +export class TableColsToggleDetailsComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: '' // title为空字符串时,该列信息不在控制列隐藏/显示的下拉中出现,对于操作列适用于改场景 + }, + { + title: 'first name', + width: '20%', + show: true + }, + { + title: 'last name', + width: '20%', + show: false + }, + { + title: 'birth date', + width: '10%', + show: true + }, + { + title: 'balance' + }, + { + title: 'email', + width: '20%', + show: false + } + ]; + + panelWidth: string = '200px'; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColsToggleTestComponent.ts b/src/table/demo/src/app/table/TableColsToggleTestComponent.ts new file mode 100644 index 0000000..71e04d3 --- /dev/null +++ b/src/table/demo/src/app/table/TableColsToggleTestComponent.ts @@ -0,0 +1,103 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-cols-toggle-test.html' +}) +export class TableColsToggleTestComponent implements OnInit { + disabled: boolean = false; + selectAll: boolean = true; + panelWidth: string = '250px'; + searchable: boolean = true; // 可切换测试 + tipContent: string = '控制列隐藏/显示'; // 默认提示文本为'自定义列表项' + tipPosition: string = 'left'; // 默认提示文本方向为'top' + noDataText: string = '无数据'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name 我需要出省略号和tip提示aaaa ', + show: false + }, + { + title: 'last name', + show: true + }, + { + title: 'birth date', + show: true + }, + { + title: 'balance', + show: undefined // undefined表示该列不参与动态显示/隐藏 + }, + { + title: 'email', + show: true + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + // + onClick(): void { + this.disabled = !this.disabled; + this.tipContent = this.disabled ? '禁用状态说明' : '控制列隐藏/显示'; + } + onClick2(): void { + this.selectAll = !this.selectAll; + } + onClick3(): void { + this.searchable = !this.searchable; + } + + onBlur(): void { + console.log('blur', this.columns); + } + + onSelect(item: TiTableColumns): void { + console.log('select', item); + const selectedColumns: Array = this.columns.filter((column: { show?: boolean }) => { + return column.show === true || column.show === undefined; + }); + console.log('显示的列', selectedColumns); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColsresizableBasicComponent.ts b/src/table/demo/src/app/table/TableColsresizableBasicComponent.ts new file mode 100644 index 0000000..b5957ea --- /dev/null +++ b/src/table/demo/src/app/table/TableColsresizableBasicComponent.ts @@ -0,0 +1,72 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-colsresizable-basic.html' +}) +export class TableColsresizableBasicComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '15%' + }, + { + title: 'last name', + width: '25%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColsresizableColstoggleComponent.ts b/src/table/demo/src/app/table/TableColsresizableColstoggleComponent.ts new file mode 100644 index 0000000..e8aca79 --- /dev/null +++ b/src/table/demo/src/app/table/TableColsresizableColstoggleComponent.ts @@ -0,0 +1,79 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-colsresizable-colstoggle.html' +}) +export class TableColsresizableColstoggleComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%', + show: true + }, + { + title: 'birth date', + width: '10%', + show: false + }, + { + title: 'balance', + width: '30%', + show: true + }, + { + title: 'email', + width: '20%', + show: true + } + ]; + panelWidth: number = 200; + searchable: boolean = false; // 可切换测试 + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColsresizableColstoggleFixedheadComponent.ts b/src/table/demo/src/app/table/TableColsresizableColstoggleFixedheadComponent.ts new file mode 100644 index 0000000..9a7014c --- /dev/null +++ b/src/table/demo/src/app/table/TableColsresizableColstoggleFixedheadComponent.ts @@ -0,0 +1,79 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-colsresizable-colstoggle-fixedhead.html' +}) +export class TableColsresizableColstoggleFixedheadComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + show: true + }, + { + title: 'last name', + width: '20%', + show: true + }, + { + title: 'birth date', + width: '10%', + show: true + }, + { + title: 'balance', + width: '30%', + show: undefined + }, + { + title: 'email', + width: '20%', + show: true + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 5; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColsresizableLoadfailComponent.ts b/src/table/demo/src/app/table/TableColsresizableLoadfailComponent.ts new file mode 100644 index 0000000..044c924 --- /dev/null +++ b/src/table/demo/src/app/table/TableColsresizableLoadfailComponent.ts @@ -0,0 +1,56 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-colsresizable-loadfail.html' +}) +export class TableColsresizableLoadfailComponent implements OnInit { + status: number = 404; // 404应该是http请求返回的状态码,此处只是用此变量做模拟 + failLoadInfo: string = '加载失败,请'; + reloadInfo: string = '重新加载'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + reloadFn(): void { + console.log('重新加载'); + } +} diff --git a/src/table/demo/src/app/table/TableColsresizableSortComponent.ts b/src/table/demo/src/app/table/TableColsresizableSortComponent.ts new file mode 100644 index 0000000..e6aea4f --- /dev/null +++ b/src/table/demo/src/app/table/TableColsresizableSortComponent.ts @@ -0,0 +1,77 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-colsresizable-sort.html' +}) +export class TableColsresizableSortComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + sortKey: 'firstName' + }, + { + title: 'last name', + width: '20%', + sortKey: 'lastName' + }, + { + title: 'birth date', + width: '10%', + sortKey: 'age', + asc: false // 默认排序,且为降序 + }, + { + title: 'balance', + sortKey: 'balance' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了排序特性,且对源数据未进行排序处理,因此tiny会对数据进行排序处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColsresizableSortHeadfilterComponent.ts b/src/table/demo/src/app/table/TableColsresizableSortHeadfilterComponent.ts new file mode 100644 index 0000000..ec4d24f --- /dev/null +++ b/src/table/demo/src/app/table/TableColsresizableSortHeadfilterComponent.ts @@ -0,0 +1,120 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-colsresizable-sort-headfilter.html' +}) +export class TableColsresizableSortHeadfilterComponent implements OnInit { + // 初始化时,searchWords 的长度和searchKeys相同,无搜索时设置为空字符串 + searchWords: Array = ['', '']; + searchKeys: Array = ['firstName', 'lastName']; // 设置过滤字段 + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + key: 'firstName', + selected: null, + options: [ + { + label: 'all' + }, + { + label: 'Pierre' + }, + { + label: 'Jacques' + }, + { + label: 'Robert' + } + ] + }, + { + title: 'last name', + width: '10%', + key: 'lastName', + selected: null, + options: [ + { + label: 'all' + }, + { + label: 'Dupont' + }, + { + label: 'Germain' + }, + { + label: 'Delcourt' + }, + { + label: 'bjip' + } + ] + }, + { + title: 'birth date', + width: '10%', + key: 'age', + asc: false // 默认排序,且为降序 + }, + { + title: 'balance', + key: 'balance' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了排序特性,且对源数据未进行排序处理,因此tiny会对数据进行排序处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + // 使 headfilter 和表格搜索联动。根据 headfilter 选中项给表格的搜索接口传入对应的搜索值,进行表格数据搜索。 + // 此示例是利用表格提供的搜索功能来搜索,也可在此事件中来自己实现搜索。 + onSelect(item: any, column: TiTableColumns): void { + const index: number = this.searchKeys.indexOf(column.key); + this.searchWords[index] = item.label === 'all' ? '' : item.label; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColumnFixedComponent.ts b/src/table/demo/src/app/table/TableColumnFixedComponent.ts new file mode 100644 index 0000000..c7b15ac --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnFixedComponent.ts @@ -0,0 +1,80 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-column-fixed.html' +}) +export class TableColumnFixedComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + id: '1', + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com', + address: 'New York No. 1 Lake Park' + }, + { + id: '2', + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com', + address: 'Sidney No. 1 Lake Park' + }, + { + id: '3', + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com', + address: 'London No. 1 Lake Park' + }, + { + id: '4', + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com', + address: 'Los Angeles No. 1 Lake Park' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'Id', + fixed: 'left', + width: '100px' + }, + { + title: 'First Name', + fixed: 'left', + width: '200px' + }, + { + title: 'Last Name', + fixed: 'left', + width: '200px' + }, + { + title: 'Age', + width: '250px' + }, + { + title: 'Email Address', + width: '400px' + }, + { + title: 'Address', + width: '400px' + }, + { + title: 'Action', + fixed: 'right', + width: '150px' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableColumnfixedCheckboxComponent.ts b/src/table/demo/src/app/table/TableColumnfixedCheckboxComponent.ts new file mode 100644 index 0000000..21b2bcd --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedCheckboxComponent.ts @@ -0,0 +1,110 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-checkbox.html' +}) +export class TableColumnfixedCheckboxComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + checkedList: Array = []; // 默认选中项 + columns: Array = [ + { + title: '', + fixed: 'left' + }, + { + title: 'first name', + width: '150px', + fixed: 'left' + }, + { + title: 'last name', + width: '200px', + fixed: 'left' + }, + { + title: 'birth date', + width: '280px', + fixed: 'left' + }, + { + title: 'balance', + width: '340px' + }, + { + title: 'email', + width: '310px' + }, + { + title: 'address', + width: '340px' + }, + { + title: 'phone number', + width: '210px' + }, + { + title: 'parents', + width: '340px' + }, + { + title: 'school', + width: '190px', + fixed: 'right' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了分页特性,且源数据未进行分页处理,因此tiny会对数据进行分页处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + this.checkedList.push(this.data[6]); // 初始选中项 + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const address: string = "Xi'an"; + const phone: string = '123 4567 8900'; + const parents: string = 'father and mother'; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + address, + phone, + parents, + school, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColumnfixedColstoggleComponent.ts b/src/table/demo/src/app/table/TableColumnfixedColstoggleComponent.ts new file mode 100644 index 0000000..01f2b70 --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedColstoggleComponent.ts @@ -0,0 +1,125 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-colstoggle.html' +}) +export class TableColumnfixedColstoggleComponent implements OnInit { + disabled: boolean = false; + selectAll: boolean = true; + panelWidth: string = '250px'; + searchable: boolean = true; // 可切换测试 + tipContent: string = '控制列隐藏/显示'; // 默认提示文本为'自定义列表项' + tipPosition: string = 'left'; // 默认提示文本方向为'top' + noDataText: string = '无数据'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '150px', + fixed: 'left', + show: true + }, + { + title: 'last name', + width: '200px', + fixed: 'left', + show: true + }, + { + title: 'birth date', + width: '180px', + show: true + }, + { + title: 'balance', + width: '340px', + show: undefined + }, + { + title: 'email', + width: '310px', + show: false + }, + { + title: 'address', + width: '340px', + show: true + }, + { + title: 'phone number', + width: '210px', + show: true + }, + { + title: 'parents', + width: '340px', + show: true + }, + { + title: 'school', + width: '190px', + fixed: 'right', + show: true + } + ]; + currentPage: number = 1; + totalNumber: number = 10; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50], + size: 5 + }; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了分页特性,且对源数据未进行分页处理,因此tiny会对数据进行分页处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const address: string = "Xi'an"; + const phone: string = '123 4567 8900'; + const parents: string = 'father and mother'; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + address, + phone, + parents, + school, + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/table/demo/src/app/table/TableColumnfixedEditrowComponent.ts b/src/table/demo/src/app/table/TableColumnfixedEditrowComponent.ts new file mode 100644 index 0000000..f9da46b --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedEditrowComponent.ts @@ -0,0 +1,188 @@ +import { Component, OnInit } from '@angular/core'; +import { TiActionmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-editrow.html' +}) +export class TableColumnfixedEditrowComponent implements OnInit { + // 表格数据 + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + + columns: Array = [ + { + title: '名称/ID', + width: '15%', + fixed: 'left' + }, + { + title: '客户等级', + width: '15%', + fixed: 'left' + }, + { + title: '信用账户金额', + width: '15%' + }, + { + title: '退订次数', + width: '15%' + }, + { + title: '创建时间', + width: '15%' + }, + { + title: '操作员', + width: '15%' + }, + { + title: '操作', + width: '10%', + fixed: 'right' + } + ]; + + // 正在编辑行 + editingRow: TiTableRowData; + // 新添加一行 + newRow: TiTableRowData; + // 展示行actionmenu 选项 + items: Array = [ + { + label: '编辑' + }, + { + label: '删除' + } + ]; + // 编辑行actionmenu 选项 + editingItems: Array = [ + { + label: '保存' + }, + { + label: '取消' + } + ]; + + // 新增行actionmenu + addItems: Array = [ + { + label: '添加' + }, + { + label: '取消' + } + ]; + + // 客户等级备选项 + levelOptions: Array = [ + { + label: '一级' + }, + { + label: '二级' + }, + { + label: '三级' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + // 正常行按钮处理-编辑、删除 + onSelect(item: any, row: TiTableRowData): void { + if (item.label === '编辑') { + this.editingRow = { ...row }; + this.editableRows(false); + } else { + // 删除 + this.srcData.data = this.srcData.data.filter((current: TiTableRowData): boolean => { + return current.id !== row.id; + }); + } + } + + // 正在编辑行按钮处理-保存、取消 + onSelectEditing(item: any, row: TiTableRowData): void { + if (item.label === '保存') { + this.srcData.data = this.srcData.data.map((current: TiTableRowData) => { + if (current.id === row.id) { + current = this.editingRow; + } + + return current; + }); + } + + this.editableRows(true); + this.editingRow = undefined; + } + + // 添加按钮处理 + onSelectAdd(item: any): void { + if (item.label === '添加') { + const newRow: TiTableRowData = { ...this.newRow }; + newRow.id = 'row_' + this.srcData.data.length * 911; + this.srcData.data = [newRow].concat(this.srcData.data); + } + + this.editableRows(true); + + this.newRow = undefined; + } + // 添加行 + addRow(): void { + this.newRow = {}; + this.editingRow = undefined; + this.editableRows(false); + } + + private editableRows(editable: boolean): void { + this.items = this.items.map((item: TiActionmenuItem): TiActionmenuItem => { + return { + ...item, + disabled: !editable, + tip: editable ? '' : '请先保存处于编辑状态的行' + }; + }); + } + + // 模拟数据 + private createRandomItem(id: number): TiTableRowData { + const size: Array = ['small', 'medium', 'medium', 'xlarge']; + const sourceName: string = `s2.${size[(id * 29) % 4]}.2`; + const level: string = '一级'; + const balance: number = ((id + 5) * 1000 * 7) % 10000; + const unsubscribe: number = (id * 17) % 5; + const createTime: Date = new Date(1598872204569 - ((id * 100) % 23) * 3600000 * 24 * 30); + const operator: string = 'Operator001'; + const editable: boolean = true; + + return { + sourceName, + level, + balance, + unsubscribe, + createTime, + operator, + editable, + id: 'row_' + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColumnfixedFixedheadColsresizablePaginationComponent.ts b/src/table/demo/src/app/table/TableColumnfixedFixedheadColsresizablePaginationComponent.ts new file mode 100644 index 0000000..e56ea65 --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedFixedheadColsresizablePaginationComponent.ts @@ -0,0 +1,99 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-fixedhead-colsresizable-pagination.html' +}) +export class TableColumnfixedFixedheadColsresizablePaginationComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + currentPage: number = 1; + totalNumber: number = 112; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50, 100], + size: 10 + }; + columns: Array = [ + { + title: 'first name', + fixed: 'left' + }, + { + title: 'last name', + fixed: 'left' + }, + { + title: 'birth date', + fixed: 'left' + }, + { + title: 'balance' + }, + { + title: 'email' + }, + { + title: 'address' + }, + { + title: 'phone number' + }, + { + title: 'parents' + }, + { + title: 'school', + fixed: 'right' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const address: string = "Xi'an"; + const phone: string = '123 4567 8900'; + const parents: string = 'father and mother'; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + address, + phone, + parents, + school, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColumnfixedHeadfixedComponent.ts b/src/table/demo/src/app/table/TableColumnfixedHeadfixedComponent.ts new file mode 100644 index 0000000..a0b252e --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedHeadfixedComponent.ts @@ -0,0 +1,112 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-columnfixed-headfixed.html' +}) +export class TableColumnfixedHeadfixedComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + id: '1', + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com', + address: 'New York No. 1 Lake Park' + }, + { + id: '2', + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com', + address: 'Sidney No. 1 Lake Park' + }, + { + id: '3', + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com', + address: 'London No. 1 Lake Park' + }, + { + id: '4', + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com', + address: 'Los Angeles No. 1 Lake Park' + }, + { + id: '5', + firstName: 'Pol', + lastName: 'Dupont', + age: 22, + email: 'Pol@example.com', + address: 'Los Angeles No. 1 Lake Park' + }, + { + id: '6', + firstName: 'Adelina', + lastName: 'Germain', + age: 30, + email: 'Adelina@example.com', + address: 'London No. 1 Lake Park' + }, + { + id: '7', + firstName: 'Abner', + lastName: 'Delcourt', + age: 29, + email: 'Abner@example.com', + address: 'Los Angeles No. 1 Lake Park' + }, + { + id: '8', + firstName: 'Emma', + lastName: 'Menez', + age: 48, + email: 'Emma@example.com', + address: 'Sidney No. 1 Lake Park' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'Id', + fixed: 'left', + width: '100px' + }, + { + title: 'First Name', + fixed: 'left', + width: '200px' + }, + { + title: 'Last Name', + fixed: 'left', + width: '200px' + }, + { + title: 'Age', + width: '250px' + }, + { + title: 'Email Address', + width: '400px' + }, + { + title: 'Address', + width: '400px' + }, + { + title: 'Action', + fixed: 'right', + width: '150px' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableColumnfixedLeftmenuComponent.ts b/src/table/demo/src/app/table/TableColumnfixedLeftmenuComponent.ts new file mode 100644 index 0000000..d55b693 --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedLeftmenuComponent.ts @@ -0,0 +1,119 @@ +import { Component, OnInit } from '@angular/core'; +import { TiLeftmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-leftmenu.html' +}) +export class TableColumnfixedLeftmenuComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '200px', + fixed: 'left' + }, + { + title: 'last name', + width: '160px', + fixed: 'left' + }, + { + title: 'birth date', + width: '240px', + fixed: 'left' + }, + { + title: 'balance', + width: '200px' + }, + { + title: 'email', + width: '120px' + }, + { + title: 'address', + width: '280px' + }, + { + title: 'phone number', + width: '100px' + }, + { + title: 'parents', + width: '300px' + }, + { + title: 'school', + width: '200px', + fixed: 'right' + } + ]; + headLabel: string = '头部区域(可定制)'; + marginLeft: string = '192px'; + collapsed: boolean = false; // 默认展开,当设置为true时会收起 + reloadState: boolean = true; // 初始设置为true + items: Array = [ + { + label: '一级菜单' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const address: string = "Xi'an"; + const phone: string = '123 4567 8900'; + const parents: string = 'father and mother'; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + address, + phone, + parents, + school, + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + toggleClick(isHide: boolean): void { + // 需要业务侧在菜单收起\展开时,控制右侧内容的位置 + this.marginLeft = isHide ? '0' : '192px'; + } +} diff --git a/src/table/demo/src/app/table/TableColumnfixedNodataComponent.ts b/src/table/demo/src/app/table/TableColumnfixedNodataComponent.ts new file mode 100644 index 0000000..3116771 --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedNodataComponent.ts @@ -0,0 +1,71 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-nodata.html' +}) +export class TableColumnfixedNodataComponent implements OnInit { + noDadaInfo: string = '暂无表格数据'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '150px', + fixed: 'left' + }, + { + title: 'last name', + width: '200px', + show: true, + fixed: 'left' + }, + { + title: 'birth date', + width: '380px', + fixed: 'left' + }, + { + title: 'balance', + width: '340px' + }, + { + title: 'email', + width: '310px' + }, + { + title: 'address', + width: '340px' + }, + { + title: 'phone number', + width: '210px' + }, + { + title: 'parents', + width: '340px' + }, + { + title: 'school', + width: '190px', + fixed: 'right' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } +} diff --git a/src/table/demo/src/app/table/TableColumnfixedPaginationComponent.ts b/src/table/demo/src/app/table/TableColumnfixedPaginationComponent.ts new file mode 100644 index 0000000..30b7f09 --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedPaginationComponent.ts @@ -0,0 +1,109 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-pagination.html' +}) +export class TableColumnfixedPaginationComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '150px', + fixed: 'left' + }, + { + title: 'last name', + width: '200px', + fixed: 'left' + }, + { + title: 'birth date', + width: '180px' + }, + { + title: 'balance', + width: '340px' + }, + { + title: 'email', + width: '310px' + }, + { + title: 'address', + width: '340px' + }, + { + title: 'phone number', + width: '210px' + }, + { + title: 'parents', + width: '340px' + }, + { + title: 'school', + width: '190px', + fixed: 'right' + } + ]; + currentPage: number = 1; + totalNumber: number = 32; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50], + size: 5 + }; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了分页特性,且对源数据未进行分页处理,因此tiny会对数据进行分页处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const address: string = "Xi'an"; + const phone: string = '123 4567 8900'; + const parents: string = 'father and mother'; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + address, + phone, + parents, + school, + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/table/demo/src/app/table/TableColumnfixedResizableComponent.ts b/src/table/demo/src/app/table/TableColumnfixedResizableComponent.ts new file mode 100644 index 0000000..6c2c391 --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedResizableComponent.ts @@ -0,0 +1,85 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-resizable.html' +}) +export class TableColumnfixedResizableComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '120px', + fixed: 'left' + }, + { + title: 'last name', + width: '120px', + fixed: 'left' + }, + { + title: 'birth date', + width: '80px', + fixed: 'left' + }, + { + title: 'balance' + }, + { + title: 'email' + }, + { + title: 'school', + width: '150px', + fixed: 'right' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + school, + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/table/demo/src/app/table/TableComprehensiveComponent.ts b/src/table/demo/src/app/table/TableComprehensiveComponent.ts new file mode 100644 index 0000000..173c13b --- /dev/null +++ b/src/table/demo/src/app/table/TableComprehensiveComponent.ts @@ -0,0 +1,121 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-comprehensive.html' +}) +export class TableComprehensiveComponent implements OnInit { + disabled: boolean = false; + selectAll: boolean = true; + panelWidth: string = '250px'; + searchable: boolean = true; // 可切换测试 + tipContent: string = '控制列隐藏/显示'; // 默认提示文本为'自定义列表项' + tipPosition: string = 'left'; // 默认提示文本方向为'top' + noDataText: string = '无数据'; + checkedList: Array = []; // 默认选中项 + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + currentPage: number = 1; + totalNumber: number = 112; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50, 100], + size: 10 + }; + columns: Array = [ + { + title: '', + fixed: 'left' + }, + { + title: 'first name', + fixed: 'left', + show: true + }, + { + title: 'last name', + fixed: 'left', + show: true + }, + { + title: 'birth date', + fixed: 'left', + show: true + }, + { + title: 'balance', + show: true + }, + { + title: 'email', + show: undefined + }, + { + title: 'address', + show: true + }, + { + title: 'phone number', + show: true + }, + { + title: 'parents', + show: true + }, + { + title: 'school', + fixed: 'right', + show: true + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const address: string = "Xi'an"; + const phone: string = '123 4567 8900'; + const parents: string = 'father and mother'; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + address, + phone, + parents, + school, + id + }; + } + + onCheckedsChange(event: any): void {} +} diff --git a/src/table/demo/src/app/table/TableDetailsCloseotherdetailsComponent.ts b/src/table/demo/src/app/table/TableDetailsCloseotherdetailsComponent.ts new file mode 100644 index 0000000..054db89 --- /dev/null +++ b/src/table/demo/src/app/table/TableDetailsCloseotherdetailsComponent.ts @@ -0,0 +1,77 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-details-closeotherdetails.html' +}) +export class TableDetailsCloseotherdetailsComponent implements OnInit { + closeOtherDetails: boolean = true; // 设置为true时,则点击展开某一行的详情时,收起其它行的详情;默认不收起其他行 + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: '' + }, + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableDetailsComponent.ts b/src/table/demo/src/app/table/TableDetailsComponent.ts new file mode 100644 index 0000000..89f05a7 --- /dev/null +++ b/src/table/demo/src/app/table/TableDetailsComponent.ts @@ -0,0 +1,55 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-details.html' +}) +export class TableDetailsComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: '' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableDetailsNesttableComponent.ts b/src/table/demo/src/app/table/TableDetailsNesttableComponent.ts new file mode 100644 index 0000000..56c4dac --- /dev/null +++ b/src/table/demo/src/app/table/TableDetailsNesttableComponent.ts @@ -0,0 +1,129 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-details-nesttable.html' +}) +export class TableDetailsNesttableComponent { + displayedData: Array = []; + columns: Array = [ + { + title: '' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com', + data: [ + { + friend: 'Jack', + birthday: new Date(2000, 5, 6), + address: 'London, Park Lane no. 1' + }, + { + friend: 'Edward', + birthday: new Date(2001, 9, 6), + address: 'London, Park Lane no. 0' + } + ] + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com', + data: [ + { + friend: 'Joe Black', + birthday: new Date(1976, 5, 6), + address: 'New York No. 1 Lake Park' + }, + { + friend: 'John Brown', + birthday: new Date(1975, 9, 6), + address: 'Sidney No. 1 Lake Park' + } + ] + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com', + data: [ + { + friend: 'Elisa', + birthday: new Date(2004, 11, 6), + address: 'New York No. 1 Lake Park' + }, + { + friend: 'Jim Red', + birthday: new Date(2005, 6, 16), + address: 'London No. 1 Lake Park' + } + ] + } + ], + state: undefined + }; +} + +@Component({ + selector: 'nested-table', + styleUrls: ['./tableTest.less'], + template: ` + + + + + + + + + + + + + +
    {{ column.title }}
    {{ row.friend }}{{ row.birthday | date }}{{ row.address }}
    +
    ` +}) +export class NestedTableComponent implements OnInit { + @Input() data: Array; + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [], + state: undefined + }; + columns: Array = [ + { + title: 'Friend Name' + }, + { + title: 'Friend Age' + }, + { + title: 'Friend Address' + } + ]; + + ngOnInit(): void { + this.srcData.data = this.data; + } +} diff --git a/src/table/demo/src/app/table/TableDetailsPaginationComponent.ts b/src/table/demo/src/app/table/TableDetailsPaginationComponent.ts new file mode 100644 index 0000000..2941c89 --- /dev/null +++ b/src/table/demo/src/app/table/TableDetailsPaginationComponent.ts @@ -0,0 +1,82 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-details-pagination.html' +}) +export class TableDetailsPaginationComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + currentPage: number = 1; + totalNumber: number = 67; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50], + size: 5 + }; + columns: Array = [ + { + title: '' + }, + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableDynamicDetailsComponent.ts b/src/table/demo/src/app/table/TableDynamicDetailsComponent.ts new file mode 100644 index 0000000..e9d17bb --- /dev/null +++ b/src/table/demo/src/app/table/TableDynamicDetailsComponent.ts @@ -0,0 +1,66 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-dynamic-details.html' +}) +export class TableDynamicDetailsComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: '' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; + + beforeToggle(row: TiTableRowData): void { + if (row.showDetails) { + row.showDetails = false; + } else { + setTimeout(() => { + row.info = `My name is ${row.firstName} ${row.lastName}. I am ${row.age} years old. My email address is ${row.email}.`; + row.showDetails = true; + }, 800); + } + } +} diff --git a/src/table/demo/src/app/table/TableEditallComponent.ts b/src/table/demo/src/app/table/TableEditallComponent.ts new file mode 100644 index 0000000..fc42286 --- /dev/null +++ b/src/table/demo/src/app/table/TableEditallComponent.ts @@ -0,0 +1,80 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-editall.html' +}) +export class TableEditallComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + id: 'row_1', + sourceName: 's2.small.2', + level: '一级', + unsubscribe: '3', + createTime: new Date(2019, 11, 5) + }, + { + id: 'row_2', + sourceName: 's2.medium.2', + level: '三级', + unsubscribe: '1', + createTime: new Date(2022, 1, 15) + }, + { + id: 'row_3', + sourceName: 's2.xlarge.2', + level: '二级', + unsubscribe: '0', + createTime: new Date(2021, 5, 25) + } + ], + state: undefined + }; + columns: Array = [ + { + title: '名称/ID' + }, + { + title: '客户等级' + }, + { + title: '退订次数' + }, + { + title: '创建时间' + }, + { + title: '操作' + } + ]; + + levelOptions: Array = [ + { + label: '一级' + }, + { + label: '二级' + }, + { + label: '三级' + } + ]; + + items: Array = [ + { + label: '删除' + } + ]; + + onDetele(item: any, row: TiTableRowData): void { + this.srcData.data = this.srcData.data.filter((current: TiTableRowData): boolean => { + return current.id !== row.id; + }); + } + + trackByFn(index: number, item: any): string { + return item.id; + } +} diff --git a/src/table/demo/src/app/table/TableEditallTestComponent.ts b/src/table/demo/src/app/table/TableEditallTestComponent.ts new file mode 100644 index 0000000..1b8f91f --- /dev/null +++ b/src/table/demo/src/app/table/TableEditallTestComponent.ts @@ -0,0 +1,168 @@ +import { Component, OnInit } from '@angular/core'; +import { TiActionmenuItem, TiMessageService, TiModalRef, TiTableColumns, TiTableRowData, TiTableSrcData, TiValidators } from '@opentiny/ng'; +import { AbstractControl, FormArray, FormBuilder, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-editall-test.html' +}) +export class TableEditallTestComponent implements OnInit { + // 表格数据 + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: '名称/ID' + }, + { + title: '客户等级' + }, + { + title: '信用账户金额' + }, + { + title: '退订次数' + }, + { + title: '创建时间' + }, + { + title: '操作员' + }, + { + title: '操作' + } + ]; + + // 等级 + levelOptions = [ + { + label: '一级' + }, + { + label: '二级' + }, + { + label: '三级' + } + ]; + + // 表单所有元素 + allFormControl: FormArray = new FormArray([], atLeastOneValidator); + + // 正常行actionmenu选项 + items: Array = [ + { + label: '删除' + } + ]; + + constructor(private fb: FormBuilder, private tiMessage: TiMessageService) {} + + ngOnInit(): void { + for (let j: number = 0; j < 5; j++) { + const item = this.createRandomItem(j); + this.data.push(item); + // 每一行是一个FormGroup, 每一个可输入单元格是一个FormControl + // 所有行组成一个FormArray + this.allFormControl.push(item.formgroupCtrl); + } + this.srcData = { + data: this.data, + + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + // 模拟提交 + onClickSubmit(): void { + this.srcData.data.forEach((item) => { + console.log(item.formgroupCtrl.status); + }); + + this.tiMessage.open({ + content: '提交成功', + close(messageRef: TiModalRef): void { + console.log('on close', messageRef); + }, + dismiss(messageRef: TiModalRef): void { + console.log('on dismiss', messageRef); + } + }); + } + + // 删除 + onSelect(item: any, row: TiTableRowData): void { + if (item.label === '删除') { + const index = this.srcData.data.findIndex((current: TiTableRowData): boolean => { + return current.id === row.id; + }); + this.allFormControl.removeAt(index); + this.srcData.data.splice(index, 1); + } + } + + // 新增一行 + addRow(): void { + const size: Array = ['small', 'medium', 'medium', 'xlargr']; + const sourceName = 's2.' + size[(this.srcData.data.length * 27) % 4] + '.2'; + const id = 'row_' + this.srcData.data.length; + const newRow = { + sourceName, + id, + formgroupCtrl: this.fb.group({ + level: [null, TiValidators.required], + balance: [null, TiValidators.rangeValue(50, 1000)], + unsubscribe: [null, TiValidators.required], + createTime: [null, TiValidators.required], + operator: [null, TiValidators.required] + }) + }; + + this.allFormControl.push(newRow.formgroupCtrl); + + this.srcData.data = [...this.srcData.data, newRow]; + } + + private createRandomItem(id: number): TiTableRowData { + const size: Array = ['small', 'medium', 'medium', 'xlargr']; + const sourceName = 's2.' + size[(id * 27) % 4] + '.2'; + return { + sourceName, + formgroupCtrl: this.fb.group({ + level: [undefined, TiValidators.required], + balance: [null, TiValidators.rangeValue(100, 1000)], + unsubscribe: [null, TiValidators.required], + createTime: [null, TiValidators.required], + operator: [null, TiValidators.required] + }), + id: 'row_' + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} + +// 自定义校验器 +export const atLeastOneValidator: ValidatorFn = (control: FormArray): ValidationErrors | null => { + let validNum = 0; + control.controls.forEach((ctrl: AbstractControl) => { + if (ctrl.status === 'VALID') { + validNum++; + } + console.log(ctrl.status); + }); + + return validNum > 0 + ? null + : { + message: '表格至少填写一行有效数据' + }; +}; diff --git a/src/table/demo/src/app/table/TableEditrowComponent.ts b/src/table/demo/src/app/table/TableEditrowComponent.ts new file mode 100644 index 0000000..5c334e7 --- /dev/null +++ b/src/table/demo/src/app/table/TableEditrowComponent.ts @@ -0,0 +1,123 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-editrow.html' +}) +export class TableEditrowComponent { + editingRow: TiTableRowData; + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + id: 'row_1', + sourceName: 's2.small.2', + level: '一级', + unsubscribe: '3', + createTime: new Date(2019, 11, 5) + }, + { + id: 'row_2', + sourceName: 's2.medium.2', + level: '三级', + unsubscribe: '1', + createTime: new Date(2022, 1, 15) + }, + { + id: 'row_3', + sourceName: 's2.xlarge.2', + level: '二级', + unsubscribe: '0', + createTime: new Date(2021, 5, 25) + } + ], + state: undefined + }; + columns: Array = [ + { + title: '名称/ID' + }, + { + title: '客户等级' + }, + { + title: '退订次数' + }, + { + title: '创建时间' + }, + { + title: '操作' + } + ]; + + items: Array = [ + { + label: '编辑' + }, + { + label: '删除' + } + ]; + + editingItems: Array = [ + { + label: '保存' + }, + { + label: '取消' + } + ]; + + levelOptions: Array = [ + { + label: '一级' + }, + { + label: '二级' + }, + { + label: '三级' + } + ]; + + onSelect(item: any, row: TiTableRowData): void { + if (item.label === '编辑') { + this.editingRow = { ...row }; + this.editableRows(false); + } else { + this.srcData.data = this.srcData.data.filter((current: TiTableRowData): boolean => { + return current.id !== row.id; + }); + } + } + + onSelectEditing(item: any, row: TiTableRowData): void { + if (item.label === '保存') { + this.srcData.data = this.srcData.data.map((current: TiTableRowData) => { + if (current.id === row.id) { + current = this.editingRow; + } + + return current; + }); + } + + this.editingRow = undefined; + this.editableRows(true); + } + + trackByFn(index: number, item: any): string { + return item.id; + } + + private editableRows(editable: boolean): void { + this.items = this.items.map((item: TiActionmenuItem): TiActionmenuItem => { + return { + ...item, + disabled: !editable, + tip: editable ? '' : '请先保存处于编辑状态的行' + }; + }); + } +} diff --git a/src/table/demo/src/app/table/TableEditrowTestComponent.ts b/src/table/demo/src/app/table/TableEditrowTestComponent.ts new file mode 100644 index 0000000..b79ae12 --- /dev/null +++ b/src/table/demo/src/app/table/TableEditrowTestComponent.ts @@ -0,0 +1,184 @@ +import { Component, OnInit } from '@angular/core'; +import { TiActionmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-editrow-test.html' +}) +export class TableEditrowTestComponent implements OnInit { + // 表格数据 + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: '名称/ID', + width: '15%' + }, + { + title: '客户等级', + width: '15%' + }, + { + title: '信用账户金额', + width: '15%' + }, + { + title: '退订次数', + width: '15%' + }, + { + title: '创建时间', + width: '15%' + }, + { + title: '操作员', + width: '15%' + }, + { + title: '操作', + width: '10%' + } + ]; + + // 正在编辑行 + editingRow: TiTableRowData; + // 新添加一行 + newRow: TiTableRowData; + // 展示行actionmenu 选项 + items: Array = [ + { + label: '编辑' + }, + { + label: '删除' + } + ]; + // 编辑行actionmenu 选项 + editingItems: Array = [ + { + label: '保存' + }, + { + label: '取消' + } + ]; + + // 新增行actionmenu + addItems: Array = [ + { + label: '添加' + }, + { + label: '取消' + } + ]; + + // 客户等级备选项 + levelOptions: Array = [ + { + label: '一级' + }, + { + label: '二级' + }, + { + label: '三级' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + // 正常行按钮处理-编辑、删除 + onSelect(item: any, row: TiTableRowData): void { + if (item.label === '编辑') { + this.editingRow = { ...row }; + this.editableRows(false); + } else { + // 删除 + this.srcData.data = this.srcData.data.filter((current: TiTableRowData): boolean => { + return current.id !== row.id; + }); + } + } + + // 正在编辑行按钮处理-保存、取消 + onSelectEditing(item: any, row: TiTableRowData): void { + if (item.label === '保存') { + this.srcData.data = this.srcData.data.map((current: TiTableRowData) => { + if (current.id === row.id) { + current = this.editingRow; + } + + return current; + }); + } + + this.editableRows(true); + this.editingRow = undefined; + } + + // 添加按钮处理 + onSelectAdd(item: any): void { + if (item.label === '添加') { + const newRow: TiTableRowData = { ...this.newRow }; + newRow.id = 'row_' + this.srcData.data.length * 911; + this.srcData.data = [newRow].concat(this.srcData.data); + } + + this.editableRows(true); + + this.newRow = undefined; + } + // 添加行 + addRow(): void { + this.newRow = {}; + this.editingRow = undefined; + this.editableRows(false); + } + + private editableRows(editable: boolean): void { + this.items = this.items.map((item: TiActionmenuItem): TiActionmenuItem => { + return { + ...item, + disabled: !editable, + tip: editable ? '' : '请先保存处于编辑状态的行' + }; + }); + } + + // 模拟数据 + private createRandomItem(id: number): TiTableRowData { + const size: Array = ['small', 'medium', 'medium', 'xlarge']; + const sourceName: string = `s2.${size[(id * 29) % 4]}.2`; + const level: string = '一级'; + const balance: number = ((id + 5) * 1000 * 7) % 10000; + const unsubscribe: number = (id * 17) % 5; + const createTime: Date = new Date(1598872204569 - ((id * 100) % 23) * 3600000 * 24 * 30); + const operator: string = 'Operator001'; + const editable: boolean = true; + + return { + sourceName, + level, + balance, + unsubscribe, + createTime, + operator, + editable, + id: 'row_' + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableFilterComponent.ts b/src/table/demo/src/app/table/TableFilterComponent.ts new file mode 100644 index 0000000..243ca2c --- /dev/null +++ b/src/table/demo/src/app/table/TableFilterComponent.ts @@ -0,0 +1,138 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableRowData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-filter.html' +}) +export class TableFilterComponent implements OnInit { + inputValue1: string = 'p'; + inputValue21: string = ''; + inputValue22: string = ''; + oneWordSearch: any; + moreThanOneWordSearch: any; + data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + searchedData: Array = []; + @ViewChild('table1', { static: true }) table1: TiTableComponent; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.oneWordSearch = { + displayed: [], // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了搜索特性,且对源数据未进行搜索处理,因此tiny会对数据进行搜索处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }, + searchWords: [this.inputValue1], + searchKeys: ['firstName', 'lastName'] + }; + + this.moreThanOneWordSearch = { + displayed: [], // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }, + // 多列多条件搜索时, searchWords,searchKeys 长度相同, searchWords无搜索时设置为空字符串如['', ''] + searchWords: [this.inputValue21, this.inputValue22], + searchKeys: ['firstName', 'age'] + }; + } + + setOneWordSearch(value: string): void { + this.oneWordSearch.searchWords[0] = value; + } + + setMoreThanOneWordSearch(value: string, index: number): void { + this.moreThanOneWordSearch.searchWords[index] = value; + } + + getSearchedResult(): void { + // TiTableComponent提供了getSearchedResult方法,可通过此方法获取到搜索的数据结果。 + this.searchedData = this.table1.getSearchedResult(); + console.log('searchedData', this.searchedData); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id, + test: { + a: 'hello', + b: 'welcome' + } + }; + } + + changeData(): void { + const temp: TiTableRowData = { + firstName: 'slkdfjsadf', + lastName: 'weiorui', + age: 23, + email: 'sdlfk@example.com', + balance: (this.data.length * 761) % 10000, + id: 'weoiriow' + }; + this.oneWordSearch['srcData']['data'].push(temp); + this.moreThanOneWordSearch['srcData']['data'].push(temp); + } + + changeSearchKeys(): void { + this.oneWordSearch['searchKeys'] = []; + } +} diff --git a/src/table/demo/src/app/table/TableFilterStrictComponent.ts b/src/table/demo/src/app/table/TableFilterStrictComponent.ts new file mode 100644 index 0000000..716827e --- /dev/null +++ b/src/table/demo/src/app/table/TableFilterStrictComponent.ts @@ -0,0 +1,140 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableRowData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-filter-strict.html' +}) +export class TableFilterStrictComponent implements OnInit { + inputValue1: string = 'p'; + inputValue21: string = ''; + inputValue22: string = ''; + oneWordSearch: any; + moreThanOneWordSearch: any; + data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + searchedData: Array = []; + @ViewChild('table1', { static: true }) table1: TiTableComponent; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.oneWordSearch = { + displayed: [], // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了搜索特性,且对源数据未进行搜索处理,因此tiny会对数据进行搜索处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }, + searchWords: [this.inputValue1], + searchKeys: ['firstName', 'lastName', 'age'], + searchStrictKeys: ['firstName'] // 指定精确匹配的字段 + }; + + this.moreThanOneWordSearch = { + displayed: [], // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }, + // 多列多条件搜索时, searchWords,searchKeys 长度相同, searchWords无搜索时设置为空字符串如['', ''] + searchWords: [this.inputValue21, this.inputValue22], + searchKeys: ['firstName', 'age'], + searchStrictKeys: ['age'] // 指定精确匹配的字段 + }; + } + + setOneWordSearch(value: string): void { + this.oneWordSearch.searchWords[0] = value; + } + + setMoreThanOneWordSearch(value: string, index: number): void { + this.moreThanOneWordSearch.searchWords[index] = value; + } + + getSearchedResult(): void { + // TiTableComponent提供了getSearchedResult方法,可通过此方法获取到搜索的数据结果。 + this.searchedData = this.table1.getSearchedResult(); + console.log('searchedData', this.searchedData); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'pol', 'Jacques', 'Robert', 'Elisa', 'bjip']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'Bjipuie', 'Polst', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 6]; + const lastName: string = familyName[((id + 3) * 29) % 6]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id, + test: { + a: 'hello', + b: 'welcome' + } + }; + } + + changeData(): void { + const temp: TiTableRowData = { + firstName: 'slkdfjsadf', + lastName: 'weiorui', + age: 23, + email: 'sdlfk@example.com', + balance: (this.data.length * 761) % 10000, + id: 'weoiriow' + }; + this.oneWordSearch['srcData']['data'].push(temp); + this.moreThanOneWordSearch['srcData']['data'].push(temp); + } + + changeSearchKeys(): void { + this.oneWordSearch['searchKeys'] = []; + } +} diff --git a/src/table/demo/src/app/table/TableFixedHeadColsResizableComponent.ts b/src/table/demo/src/app/table/TableFixedHeadColsResizableComponent.ts new file mode 100644 index 0000000..ba71a7a --- /dev/null +++ b/src/table/demo/src/app/table/TableFixedHeadColsResizableComponent.ts @@ -0,0 +1,74 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-fixed-head-cols-resizable.html' +}) +export class TableFixedHeadColsResizableComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 5; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableFixedHeadComponent.ts b/src/table/demo/src/app/table/TableFixedHeadComponent.ts new file mode 100644 index 0000000..09bc42c --- /dev/null +++ b/src/table/demo/src/app/table/TableFixedHeadComponent.ts @@ -0,0 +1,76 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-fixed-head.html' +}) +export class TableFixedHeadComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + }, + { + firstName: 'Pol', + lastName: 'Dupont', + age: 22, + email: 'Pol@example.com' + }, + { + firstName: 'Adelina', + lastName: 'Germain', + age: 30, + email: 'Adelina@example.com' + }, + { + firstName: 'Abner', + lastName: 'Delcourt', + age: 29, + email: 'Abner@example.com' + }, + { + firstName: 'Emma', + lastName: 'Menez', + age: 48, + email: 'Emma@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableFixedHeadInAccordionComponent.ts b/src/table/demo/src/app/table/TableFixedHeadInAccordionComponent.ts new file mode 100644 index 0000000..e3e5b04 --- /dev/null +++ b/src/table/demo/src/app/table/TableFixedHeadInAccordionComponent.ts @@ -0,0 +1,76 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-fixed-head-in-accordion.html' +}) +export class TableFixedHeadInAccordionComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + headClass: string = 'headCls'; + bodyClass: string = 'bodyCls'; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 18; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableFixedHeadNodataComponent.ts b/src/table/demo/src/app/table/TableFixedHeadNodataComponent.ts new file mode 100644 index 0000000..c2a5b77 --- /dev/null +++ b/src/table/demo/src/app/table/TableFixedHeadNodataComponent.ts @@ -0,0 +1,58 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-fixed-head-nodata.html' +}) +export class TableFixedHeadNodataComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + noDadaInfo: string = '暂无表格数据'; + status: number = 404; // 404应该是http请求返回的状态码,此处只是用此变量做模拟 + failLoadInfo: string = '加载失败,请'; + reloadInfo: string = '重新加载'; + noGoodsInfo: string = '您还没有购买任何商品,去'; + goShopInfo: string = '购买商品'; + + columns: TiTableColumns = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } +} diff --git a/src/table/demo/src/app/table/TableFixedHeadPaginationDetailsComponent.ts b/src/table/demo/src/app/table/TableFixedHeadPaginationDetailsComponent.ts new file mode 100644 index 0000000..205f8da --- /dev/null +++ b/src/table/demo/src/app/table/TableFixedHeadPaginationDetailsComponent.ts @@ -0,0 +1,83 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-fixed-head-pagination-details.html' +}) +export class TableFixedHeadPaginationDetailsComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + currentPage: number = 10; + totalNumber: number = 23; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50, 100], + size: 10 + }; + columns: TiTableColumns = [ + { + title: '' + }, + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableFixedheadColsresizablePaginationDetailsComponent.ts b/src/table/demo/src/app/table/TableFixedheadColsresizablePaginationDetailsComponent.ts new file mode 100644 index 0000000..06c70b8 --- /dev/null +++ b/src/table/demo/src/app/table/TableFixedheadColsresizablePaginationDetailsComponent.ts @@ -0,0 +1,83 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-fixedhead-colsresizable-pagination-details.html' +}) +export class TableFixedheadColsresizablePaginationDetailsComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + currentPage: number = 10; + totalNumber: number = 23; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50, 100], + size: 10 + }; + columns: Array = [ + { + title: '' + }, + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableFixheadScrollComponent.ts b/src/table/demo/src/app/table/TableFixheadScrollComponent.ts new file mode 100644 index 0000000..eac0aa1 --- /dev/null +++ b/src/table/demo/src/app/table/TableFixheadScrollComponent.ts @@ -0,0 +1,81 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +declare let global: any; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-fixhead-scroll.html' +}) +export class TableFixheadScrollComponent implements OnInit { + // 列表数据 + items: Array = []; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + noDadaInfo: string = '暂无表格数据'; + + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + this.items.push((j * 113) % 29); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + addItems(): void { + this.items.push(333); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 37) % 4]; + const lastName: string = familyName[((id + 3) * 17) % 4]; + const age: number = Math.floor(((id + 3) * 73) % 100); + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 100) * 181) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableGroupComponent.ts b/src/table/demo/src/app/table/TableGroupComponent.ts new file mode 100644 index 0000000..9587a12 --- /dev/null +++ b/src/table/demo/src/app/table/TableGroupComponent.ts @@ -0,0 +1,151 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-group.html' +}) +export class TableGroupComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'Size' + }, + { + title: 'Type' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [ + { + name: 'Group1: ti-table group test', + type: 'Folder', + size: '--', + showSub: true, + subData: [ + { + name: 'Work', + type: 'Folder', + size: '2.08MB', + showSub: true + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + } + ] + }, + { + name: 'Group2: ti-table group test', + type: 'Folder', + size: '--', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + } + ] + }, + { + name: 'Group3: ti-table group test', + type: 'Folder', + size: '--', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + } + ] + }, + { + name: 'Group4: ti-table group test', + type: 'Folder', + size: '--', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + } + ] + }, + { + name: 'Group5: ti-table group test', + type: 'Folder', + size: '--', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + } + ] + } + ], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } +} diff --git a/src/table/demo/src/app/table/TableGuideComponent.ts b/src/table/demo/src/app/table/TableGuideComponent.ts new file mode 100644 index 0000000..562308f --- /dev/null +++ b/src/table/demo/src/app/table/TableGuideComponent.ts @@ -0,0 +1,55 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-guide.html' +}) +export class TableGuideComponent implements OnInit { + noGoodsInfo: string = '您还没有购买商品,去'; + goShopInfo: string = '购买商品'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + goShopFn(): void { + console.log('去购物'); + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterComponent.ts b/src/table/demo/src/app/table/TableHeadFilterComponent.ts new file mode 100644 index 0000000..9c83b09 --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterComponent.ts @@ -0,0 +1,183 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-head-filter.html' +}) +export class TableHeadFilterComponent { + displayedData: Array = []; + noDadaInfo: string = '暂无数据'; + baseData: Array = [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + gender: 'male', + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + gender: 'female', + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + gender: 'male', + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + gender: 'female', + email: 'Elisa@example.com' + } + ]; + srcData: TiTableSrcData = { + data: this.baseData, + state: undefined + }; + columns: Array = [ + { + title: 'First Name', + key: 'firstName', + selected: null, + labelKey: 'label', // 默认值 + panelAlign: 'left', // 默认值 + panelWidth: 'auto', // 默认值 + options: [ + { + label: 'all' + }, + { + label: 'Pierre' + }, + { + label: 'Jacques' + }, + { + label: 'Robert' + }, + { + label: 'Elisa' + } + ] + }, + { + title: 'Last Name', + key: 'lastName', + selected: null, + multiple: true, + searchable: true, + labelKey: 'label', + panelAlign: 'left', + panelWidth: 'auto', + options: [ + { + label: 'Dupont' + }, + { + label: 'Germain' + }, + { + label: 'Delcourt' + }, + { + label: 'Menez' + } + ] + }, + { + title: 'Age', + key: 'age', + labelKey: 'label', + panelAlign: 'left', + panelWidth: '100px', + virtual: true, + searchable: true, + options: this.getAgeOptions() + }, + { + title: 'Gender', + key: 'gender', + labelKey: 'type', + multiple: true, + selectAll: true, + panelAlign: 'left', + panelWidth: 'auto', + options: [ + { + type: 'female' + }, + { + type: 'male' + } + ] + }, + { + title: 'Email Address', + key: 'email', + panelAlign: 'right', + options: [ + { + label: 'all' + }, + { + label: '@example.com' + }, + { + label: '@example.com' + }, + { + label: '@example.com' + }, + { + label: '@example.com' + } + ] + } + ]; + + onSelect(option: any): void { + this.srcData.data = this.baseData.filter((rowData: TiTableRowData) => { + for (const column of this.columns) { + const labelKey: string = column.labelKey || 'label'; + if (!column.multiple && column.selected) { + if (column.selected[labelKey] === 'all') { + continue; + } + const isMatched: boolean = + column.key === 'email' + ? rowData[column.key].indexOf(column.selected[labelKey]) >= 0 + : column.selected[labelKey] === rowData[column.key]; + if (!isMatched) { + return false; + } + } + if (column.multiple && column.selected && column.selected.length > 0) { + const index: number = column.selected.findIndex((item: any) => { + return item[labelKey] === rowData[column.key]; + }); + if (index < 0) { + return false; + } + } + } + + return true; + }); + } + + private getAgeOptions(): Array<{ label: any }> { + const options: Array<{ label: any }> = [{ label: 'all' }]; + for (let i = 12; i <= 90; i++) { + options.push({ label: i }); + } + + return options; + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterDatetimeComponent.ts b/src/table/demo/src/app/table/TableHeadFilterDatetimeComponent.ts new file mode 100644 index 0000000..1e66da5 --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterDatetimeComponent.ts @@ -0,0 +1,124 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-head-filter-datetime.html' +}) +export class TableHeadFilterDatetimeComponent { + displayedData: Array = []; + noDadaInfo: string = '暂无数据'; + baseData: Array = [ + { + name: 'Pierre Dupont', + birthday: '1993-07-22', + hireDate: '2015-04-28', + start: '2018-07-21 00:00:00', + expired: '2022-07-21 23:59:59' + }, + { + name: 'Jacques Germain', + birthday: '1996-09-02', + hireDate: '2018-12-08', + start: '2019-07-03 08:00:00', + expired: '2028-07-21 22:00:00' + }, + { + name: 'Robert Delcourt', + birthday: '1989-06-20', + hireDate: '2014-05-30', + start: '2015-08-11 06:00:12', + expired: '2019-07-11 21:12:59' + }, + { + name: 'Elisa Menez', + birthday: '2000-06-16', + hireDate: '2022-02-20', + start: '2022-11-01 13:15:00', + expired: '2024-10-01 18:32:59' + } + ]; + srcData: TiTableSrcData = { + data: this.baseData, + state: undefined + }; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'Birthday', + key: 'birthday', + selected: null, + panelAlign: 'left', + isDatetime: true, + datetimeConfig: { + onlyDate: true, + max: new Date(2000, 11, 1), + min: new Date(1989, 5, 1) + } + }, + { + title: 'Hire Date', + key: 'hireDate', + panelAlign: 'left', + isDatetime: true, + datetimeConfig: { + onlyDate: true, + format: 'yyyy-MM-dd' + } + }, + { + title: 'start', + key: 'start', + panelAlign: 'left', + isDatetime: true, + datetimeConfig: { + format: { + date: 'yyyy-MM-dd', + time: 'HH:mm:ss' + } + } + }, + { + title: 'Expired', + key: 'expired', + panelAlign: 'right', + isDatetime: true, + datetimeConfig: { + max: new Date(2026, 12, 25, 21, 59, 59), + min: new Date(2019, 6, 1, 0, 0, 0) + } + } + ]; + + onSelect(selected: { start: Date; end: Date; type: string }): void { + this.srcData.data = this.baseData.filter((rowData: TiTableRowData) => { + for (const column of this.columns) { + if (column.isDatetime && column.selected) { + const startTimestamp: number = column.selected.start && Date.parse(column.selected.start); + const endTimestamp: number = column.selected.end && Date.parse(column.selected.end); + const dataTimestamp: number = rowData[column.key] && Date.parse(rowData[column.key]); + let isMatched: boolean = true; + + if (startTimestamp && endTimestamp) { + isMatched = startTimestamp <= dataTimestamp && dataTimestamp <= endTimestamp; + } + + if (startTimestamp && !endTimestamp) { + isMatched = startTimestamp <= dataTimestamp; + } + + if (!startTimestamp && endTimestamp) { + isMatched = dataTimestamp <= endTimestamp; + } + + if (!isMatched) { + return false; + } + } + } + + return true; + }); + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterDatetimeTestComponent.ts b/src/table/demo/src/app/table/TableHeadFilterDatetimeTestComponent.ts new file mode 100644 index 0000000..5732c99 --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterDatetimeTestComponent.ts @@ -0,0 +1,213 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-head-filter-datetime-test.html' +}) +export class TableHeadFilterDatetimeTestComponent implements OnInit { + public displayed: Array = []; + public srcData: TiTableSrcData; + private data: Array = []; + public columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'birthday', + width: '20%', + key: 'birthday', + selected: undefined, + panelAlign: 'left', + isDatetime: true, + datetimeConfig: { + // 此项为可选项,此处是为了展示如何使用 + format: { + date: 'yyyy-MM-dd', + time: 'HH:mm:ss' + } + } + }, + { + title: 'hireDate', + width: '20%', + key: 'hireDate', + selected: undefined, + panelAlign: 'left', + isDatetime: true, + datetimeConfig: { + // 此项为可选项,此处为了展示如何使用 + max: new Date(2024, 9, 13, 21, 6, 59), + min: new Date(2020, 8, 1, 8, 30, 0) + } + }, + { + title: 'start', + width: '20%', + key: 'start', + selected: undefined, + panelAlign: 'left', + isDatetime: true, + datetimeConfig: { + onlyDate: true, + format: 'yyyy-MM-dd' // 此项为可选项,此处为了展示如何使用 + } + }, + { + title: 'expired', + width: '20%', + key: 'expired', + selected: undefined, + isDatetime: true, + panelAlign: 'right', + datetimeConfig: { + onlyDate: true, + max: new Date(2024, 10, 31), // 此项为可选项,此处为了展示如何使用 + min: new Date(2020, 9, 1) // 此项为可选项,此处为了展示如何使用 + } + } + ]; + + ngOnInit(): void { + // 随机生成10条数据 + for (let j: number = 0; j < 10; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + // 设置初始化第二列的 headfilter 的选中项,start表示开始时间,end表示结束时间,start,end需传入Date类型 + this.columns[1].selected = { + start: new Date(2021, 6, 21, 40, 23, 45), + end: new Date(2021, 6, 22, 40, 23, 45) + }; + // 根据初始化第二列的 headfilter 的选中项对表格筛选 + this.onSelect(this.columns[1].selected, this.columns[1]); + + // 设置初始化第四列的 headfilter 的选中项,start表示开始时间,end表示结束时间,start,end需传入Date类型 + this.columns[3].selected = { + start: new Date(2021, 6, 21), + end: new Date(2021, 6, 22, 23, 59, 59) + }; + // 根据初始化第四列的 headfilter 的选中项对表格筛选 + this.onSelect(this.columns[3].selected, this.columns[3]); + } + + /* 表格自带搜索功能不满足此种场景,此示例是在 onselect 事件中通过对表格的 srcData 进行处理实现过滤。 + * 通过select事件,获取时间日期下拉中的选中项,时间日期的选中项包括: + { + start:开始时间 + end: 结束时间 + type: 'datetime' 当前选中项的面板类型,方便用户进行选中面板的辨别 + } + **/ + + /** + * @param items 当前选中项 + * @param column 表格中配置的每个列的表头信息 + */ + public onSelect(items: any, column: TiTableColumns): void { + // 从每一行进行过滤筛选 + this.srcData.data = this.data.filter((rowData: TiTableRowData) => { + // 遍历所有列 + for (const columnData of this.columns) { + // 只有筛选列有选中项时进行筛选,如果某一筛选列选中项不包含当前行数据,则跳出循环 + if (columnData.isDatetime && columnData.selected) { + // 将时间日期转为时间戳,通过时间戳进行日期大小的比较 + const start: number = this.isValidDate(columnData.selected.start) ? Date.parse(columnData.selected.start) : undefined; + const currentTime: number = this.isValidDate(new Date(rowData[columnData.key])) ? Date.parse(rowData[columnData.key]) : undefined; + const end: number = this.isValidDate(columnData.selected.end) ? Date.parse(columnData.selected.end) : undefined; + let exit: boolean = true; + + // 如果当时值不存在或者不是有效的时间日期格式,则怕排查此列,跳出循环 + if (!currentTime) { + return false; + } + + // 开始结束时间都存在 + if (start && end) { + exit = start <= currentTime && currentTime <= end; + } + + // 开始时间存在,结束时间不存在, + if (start && !end) { + exit = start <= currentTime; + } + + // 结束时间存在,开始时间不存在 + if (!start && end) { + exit = currentTime <= end; + } + + if (exit === false) { + return false; + } + } + } + + return true; + }); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const firstName: string = nameList[((id + 3) * 19) % 5]; + // 时间日期格式列 + const birthday: string = this.timestampToTime(new Date(2021, 6, 21, 40, 23, 45).getTime() + id * 1000 * 60 * 60 * 24); + // 日期格式列,测试时间日期面板是否可以过滤日期格式 + const hireDate: string = this.timestampToTime(new Date(2021, 6, 21).getTime() + id * 1000 * 60 * 60 * 24 * 100); + const expired: string = this.timestampToTime(new Date(2021, 6, 21).getTime() + id * 1000 * 60 * 60 * 24 * 100, true); + const start: string = this.timestampToTime(new Date(2021, 6, 21).getTime() + id * 1000 * 60 * 60 * 24 * 50, true); + + return { + start, + birthday, + hireDate, + expired, + firstName, + id + }; + } + + // 将时间戳转换为时间格式,此处的函数是为了创建表格中的随机时间,服务可自行设置自己的时间 + public timestampToTime(timestamp: number, isDateFormat?: boolean): string { + const date: Date = new Date(timestamp); // 时间戳为10位需*1000,时间戳为13位的话不需乘1000 + + const Y: string = date.getFullYear() + '-'; + + const M: string = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'; + + const D: string = (date.getDate() < 10 ? '0' + date.getDate() : date.getDate()) + ' '; + + const h: string = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours()) + ':'; + + const m: string = (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()) + ':'; + + const s: string | number = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds(); + + // 返回日期格式 + if (isDateFormat) { + return Y + M + D; + } + + // 返回时间日期格式 + return Y + M + D + h + m + s; + } + + // 判断当前值是否日期格式 + public isValidDate(dateTemp: Date | string): boolean { + let date: Date | string = dateTemp; + if (Object.prototype.toString.call(date) === '[object String]') { + // 转为时间格式 + date = new Date(dateTemp); + } + + return Object.prototype.toString.call(date) === '[object Date]' && String(date) !== 'Invalid Date'; + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterMultiComponent.ts b/src/table/demo/src/app/table/TableHeadFilterMultiComponent.ts new file mode 100644 index 0000000..15398dd --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterMultiComponent.ts @@ -0,0 +1,152 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-head-filter-multi.html' +}) +export class TableHeadFilterMultiComponent implements OnInit { + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + key: 'firstName', // 该列的 headfilter 要过滤的字段 + selected: null, + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'Pierre' + }, + { + label: 'Jacques' + }, + { + label: 'Robert' + }, + { + label: 'Pol' + }, + { + label: 'Elisa' + } + ], + multiple: true + }, + { + title: 'last name', + width: '20%', + key: 'lastName', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + searchable: true, // 该列的 headfilter 的下拉中是否开启搜索功能 + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'Dupont' + }, + { + label: 'Germain' + }, + { + label: 'Delcourt' + }, + { + label: 'bjip' + }, + { + label: 'Menez' + }, + { + label: 'Henry' + }, + { + label: 'Jeff' + }, + { + label: 'John' + }, + { + label: 'Elizabeth' + } + ], + multiple: true, + selectAll: true + }, + { + title: 'birth date', + width: '20%' + }, + { + title: 'balance', + width: '20%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + // 随机生成10条数据 + for (let j: number = 0; j < 10; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + // 设置初始化第一列 headfilter 的选中项 + this.columns[0].selected = [this.columns[0].options[0], this.columns[0].options[1]]; + // 根据初始化第一列 headfilter 的选中项对表格筛选 + this.onSelect(this.columns[0].selected, this.columns[0]); + } + + // 表格自带搜索功能不满足此种场景,此示例是在 onselect 事件中通过对表格的 srcData 进行处理实现过滤。 + onSelect(items: any, column: TiTableColumns): void { + console.log('select', items); + console.log('column', column); + + // 从每一行进行过滤筛选 + this.srcData.data = this.data.filter((rowData: TiTableRowData) => { + // 遍历所有列 + for (const columnData of this.columns) { + // 只有筛选列有选中项时进行筛选,如果某一筛选列选中项不包含当前行数据,则跳出循环 + if (columnData.selected && columnData.selected.length) { + const index: number = columnData.selected.findIndex((item: any) => { + return item.label === rowData[columnData.key]; + }); + if (index < 0) { + return false; + } + } + } + + return true; + }); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez', 'Henry', 'Jeff', 'John', 'Elizabeth']; + const firstName: string = nameList[((id + 3) * 19) % 5]; + const lastName: string = familyName[((id + 3) * 19) % 9]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterMultiValuekeyComponent.ts b/src/table/demo/src/app/table/TableHeadFilterMultiValuekeyComponent.ts new file mode 100644 index 0000000..0854236 --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterMultiValuekeyComponent.ts @@ -0,0 +1,222 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-head-filter-multi-valuekey.html' +}) +export class TableHeadFilterMultiValuekeyComponent implements OnInit { + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + key: 'firstName', // 该列的 headfilter 要过滤的字段 + selected: null, + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'Pol', + value: '玫瑰' + }, + { + label: 'Pierre', + value: '皮特' + }, + { + label: 'Jacques', + value: '杰克' + }, + { + label: 'Robert', + value: '罗伯特' + } + ], + multiple: true + }, + { + title: 'last name', + width: '20%', + key: 'lastName', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + searchable: true, // 该列的 headfilter 的下拉中是否开启搜索功能 + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'Dupont', + value: '李白' + }, + { + label: 'Germain', + value: '杜甫' + }, + { + label: 'Delcourt', + value: '杜牧' + }, + { + label: 'bjip', + value: '王维' + }, + { + label: 'Menez', + value: '李清照' + }, + { + label: 'Henry', + value: '白居易' + }, + { + label: 'Jeff', + value: '范仲淹' + }, + { + label: 'John', + value: '陆游' + }, + { + label: 'Elizabeth', + value: '孟浩然' + } + ], + multiple: true, + selectAll: true + }, + { + title: 'birth date', + width: '20%' + }, + { + title: 'balance', + width: '20%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + // 随机生成10条数据 + for (let j: number = 0; j < 10; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + // 设置初始化第一列 headfilter 的选中项,此处的选中项根基于valueKey进行设置 + this.columns[0].selected = [this.columns[0].options[0].value, this.columns[0].options[1].value]; + const items: Array = [this.columns[0].options[0], this.columns[0].options[1]]; + // 根据初始化第一列 headfilter 的选中项对表格筛选 + this.onSelect(items, this.columns[0]); + } + + // 表格自带搜索功能不满足此种场景,此示例是在 onselect 事件中通过对表格的 srcData 进行处理实现过滤。 + /** + * @param items 当前的选中项 + * @param column 当前列的数据 column.selected的值是通过keyValue获得的当前选中项 + */ + onSelect(items: any, column: TiTableColumns): void { + // 从每一行进行过滤筛选 + this.srcData.data = this.data.filter((rowData: TiTableRowData) => { + // 遍历所有列 + for (const columnData of this.columns) { + // 只有筛选列有选中项时进行筛选,如果某一筛选列选中项不包含当前行数据,则跳出循环 + if (columnData.selected && columnData.selected.length) { + const index: number = columnData.selected.findIndex((selectedItem: any) => { + return selectedItem === rowData[columnData.key].value; + }); + if (index < 0) { + return false; + } + } + } + + return true; + }); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = [ + { + label: 'Pierre', + value: '皮特' + }, + { + label: 'Jacques', + value: '杰克' + }, + { + label: 'Robert', + value: '罗伯特' + }, + { + label: 'Pol', + value: '玫瑰' + }, + { + label: 'Elisa', + value: '安蕾斯' + } + ]; + const familyName: Array = [ + { + label: 'Dupont', + value: '李白' + }, + { + label: 'Germain', + value: '杜甫' + }, + { + label: 'Delcourt', + value: '杜牧' + }, + { + label: 'bjip', + value: '王维' + }, + { + label: 'Menez', + value: '李清照' + }, + { + label: 'Henry', + value: '白居易' + }, + { + label: 'Jeff', + value: '范仲淹' + }, + { + label: 'John', + value: '陆游' + }, + { + label: 'Elizabeth', + value: '孟浩然' + } + ]; + const firstName: object = nameList[((id + 3) * 19) % 5]; + const lastName: object = familyName[((id + 3) * 19) % 9]; + const age: number = ((id + 3) * 13) % 100; + const balance: number = ((id + 3) * 761) % 10000; + const email: string = `${age}${balance}@whatever.com`; + + return { + firstName, + lastName, + age, + balance, + email, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterTestComponent.ts b/src/table/demo/src/app/table/TableHeadFilterTestComponent.ts new file mode 100644 index 0000000..802a65d --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterTestComponent.ts @@ -0,0 +1,184 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-head-filter-test.html' +}) +export class TableHeadFilterTestComponent implements OnInit { + searchWords: Array = ['', '', '', '']; + searchKeys: Array = ['firstName', 'lastName', 'gender', 'email']; // 设置过滤字段 + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + key: 'firstName', // 该列的 headfilter 要过滤的字段 + selected: null, + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'all' + }, + { + label: 'Pierre' + }, + { + label: 'Jacques' + }, + { + label: 'Robert' + } + ] + }, + { + title: 'last name', + width: '8%', + key: 'lastName', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + searchable: true, // 该列的 headfilter 的下拉中是否开启搜索功能 + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'all' + }, + { + label: 'Dupont' + }, + { + label: 'Germain' + }, + { + label: 'Delcourt' + }, + { + label: 'bjip' + }, + { + label: 'Menez' + }, + { + label: 'Henry' + }, + { + label: 'Jeff' + }, + { + label: 'John' + }, + { + label: 'Elizabeth' + } + ] + }, + { + title: 'birth date', + width: '20%' + }, + { + title: 'balance', + width: '15%' + }, + { + title: 'email', + searchable: true, + key: 'email', // 该列的 headfilter 要过滤的字段 + labelKey: 'label', // 该列的 headfilter 下拉中显示的字段 + panelWidth: '120px', // 该列的 headfilter 的下拉框宽度 + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'all' + }, + { + label: 'Pierre' + }, + { + label: 'Pol' + }, + { + label: 'Jacques' + }, + { + label: 'Robert' + }, + { + label: 'Elisa' + } + ] + }, + { + title: 'classification', + key: 'gender', // 该列的 headfilter 要过滤的字段 + selected: null, + searchable: false, + labelKey: 'type', // 该列的 headfilter 下拉中显示的字段 + panelWidth: '120px', // 该列的 headfilter 的下拉框宽度 + panelAlign: 'right', // 该列的 headfilter 下拉面板对齐方式,默认左对齐 + options: [ + { + // 该列的 headfilter 下拉选择项 + type: 'all' + }, + { + type: 'girl' + }, + { + type: 'boy' + } + ] + } + ]; + + ngOnInit(): void { + // 设置初始化第一列 headfilter 的选中项 + this.columns[0]['selected'] = this.columns[0].options[1]; + // 根据 headfilter 选中项给表格的搜索接口传入对应的搜索值,进行表格搜索 + const index: number = this.searchKeys.indexOf(this.columns[0].key); + this.searchWords[index] = this.columns[0].options[1].label; + + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + // 使 headfilter 和表格搜索联动。根据 headfilter 选中项给表格的搜索接口传入对应的搜索值,进行表格数据搜索。 + // 此示例是利用表格提供的搜索功能来搜索,也可在此事件中来自己实现搜索。 + onSelect(item: any, column: TiTableColumns): void { + const index: number = this.searchKeys.indexOf(column.key); + const labelKey: string = column.labelKey || 'label'; + this.searchWords[index] = item[labelKey] === 'all' ? '' : item[labelKey]; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez', 'Henry', 'Jeff', 'John', 'Elizabeth']; + const firstName: string = nameList[((id + 3) * 19) % 5]; + const lastName: string = familyName[((id + 3) * 19) % 9]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const gender: string = ['girl', 'boy'][((id + 3) * 19) % 2]; + + return { + firstName, + lastName, + age, + email, + balance, + gender, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterValuekeyComponent.ts b/src/table/demo/src/app/table/TableHeadFilterValuekeyComponent.ts new file mode 100644 index 0000000..9ffb044 --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterValuekeyComponent.ts @@ -0,0 +1,226 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-head-filter-valuekey.html' +}) +export class TableHeadFilterValuekeyComponent implements OnInit { + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + key: 'firstName', // 该列的 headfilter 要过滤的字段 + selected: null, + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'all', + value: '' + }, + { + label: 'Pierre', + value: '皮特' + }, + { + label: 'Jacques', + value: '杰克' + }, + { + label: 'Robert', + value: '罗伯特' + } + ] + }, + { + title: 'last name', + width: '20%', + key: 'lastName', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + searchable: true, // 该列的 headfilter 的下拉中是否开启搜索功能 + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'all', + value: '' + }, + { + label: 'Dupont', + value: '李白' + }, + { + label: 'Germain', + value: '杜甫' + }, + { + label: 'Delcourt', + value: '杜牧' + }, + { + label: 'bjip', + value: '王维' + }, + { + label: 'Menez', + value: '李清照' + }, + { + label: 'Henry', + value: '白居易' + }, + { + label: 'Jeff', + value: '范仲淹' + }, + { + label: 'John', + value: '陆游' + }, + { + label: 'Elizabeth', + value: '孟浩然' + } + ] + }, + { + title: 'birth date', + width: '20%' + }, + { + title: 'balance', + width: '20%' + }, + { + title: 'email', + width: '20%' + }, + { + title: 'classification', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + // 设置初始化第一列 headfilter 的选中项,此处的选中项根基于valueKey进行设置 + this.columns[0].selected = this.columns[0].options[1].value; + const items: Array = this.columns[0].options[0]; + // 根据初始化第一列 headfilter 的选中项对表格筛选 + this.onSelect(items, this.columns[0]); + } + + // 表格自带搜索功能不满足此种场景,此示例是在 onselect 事件中通过对表格的 srcData 进行处理实现过滤。 + /** + * @param items 当前的选中项 + * @param column 当前列的数据 column.selected的值是通过keyValue获得的当前选中项 + */ + onSelect(item: any, column: TiTableColumns): void { + this.srcData.data = this.data.filter((rowData: TiTableRowData) => { + // 遍历所有列 + for (const columnData of this.columns) { + // 只有筛选列有选中项时进行筛选,如果某一筛选列选中项不包含当前行数据,则跳出循环 + if (columnData.selected) { + const isExit: boolean = columnData.selected === rowData[columnData.key].value; + if (!isExit) { + return false; + } + } + } + + return true; + }); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = [ + { + label: 'Pierre', + value: '皮特' + }, + { + label: 'Jacques', + value: '杰克' + }, + { + label: 'Robert', + value: '罗伯特' + }, + { + label: 'Pol', + value: '玫瑰' + }, + { + label: 'Elisa', + value: '安蕾斯' + } + ]; + const familyName: Array = [ + { + label: 'Dupont', + value: '李白' + }, + { + label: 'Germain', + value: '杜甫' + }, + { + label: 'Delcourt', + value: '杜牧' + }, + { + label: 'bjip', + value: '王维' + }, + { + label: 'Menez', + value: '李清照' + }, + { + label: 'Henry', + value: '白居易' + }, + { + label: 'Jeff', + value: '范仲淹' + }, + { + label: 'John', + value: '陆游' + }, + { + label: 'Elizabeth', + value: '孟浩然' + } + ]; + const firstName: string = nameList[((id + 3) * 19) % 5]; + const lastName: string = familyName[((id + 3) * 19) % 9]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const gender: string = ['girl', 'boy'][((id + 3) * 19) % 2]; + + return { + firstName, + lastName, + age, + email, + balance, + gender, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterVirtualscrollComponent.ts b/src/table/demo/src/app/table/TableHeadFilterVirtualscrollComponent.ts new file mode 100644 index 0000000..63cb6e0 --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterVirtualscrollComponent.ts @@ -0,0 +1,178 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-head-filter-virtualscroll.html' +}) +export class TableHeadFilterVirtualscrollComponent implements OnInit { + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + currentPage: number = 1; + totalNumber: number = 30000; + columns: Array = [ + { + title: 'first name', + width: '20%', + key: 'firstName', // 该列的 headfilter 要过滤的字段 + selected: null, + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'all' + }, + { + label: 'Pierre' + }, + { + label: 'Jacques' + }, + { + label: 'Robert' + } + ] + }, + { + title: 'last name', + width: '8%', + key: 'lastName', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + multiple: true, + options: [ + { + label: 'Dupont' + }, + { + label: 'Germain' + }, + { + label: 'Delcourt' + }, + { + label: 'bjip' + }, + { + label: 'Menez' + }, + { + label: 'Henry' + }, + { + label: 'Jeff' + }, + { + label: 'John' + }, + { + label: 'Elizabeth' + } + ] + }, + { + title: 'total', + width: '20%', + key: 'total', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + searchable: true, + virtual: true, // 该列的 headfilter 的下拉中开启虚拟滚动功能 + options: [] + }, + { + title: 'balance', + width: '30%', + multiple: true, + key: 'balance', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + searchable: true, + virtual: true, // 该列的 headfilter 的下拉中开启虚拟滚动功能 + options: [] + }, + { + title: 'email', + multiple: true, + key: 'email', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + searchable: true, + selectAll: true, + virtual: true, // 该列的 headfilter 的下拉中开启虚拟滚动功能 + options: [] + } + ]; + + ngOnInit(): void { + this.columns[2].options.push({ label: 'all' }); + for (let j: number = 0; j < 10000; j++) { + this.columns[2].options.push({ label: j + ' total' }); + } + + for (let j: number = 0; j < 10000; j++) { + this.columns[3].options.push({ label: j + ' balance' }); + } + + for (let j: number = 0; j < 10000; j++) { + this.columns[4].options.push({ label: j + '@whatever.com' }); + } + + for (let j: number = 0; j < 30000; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + // 使 headfilter 和表格搜索联动。根据 headfilter 选中项给表格的搜索接口传入对应的搜索值,进行表格数据搜索。 + // 此示例是利用表格提供的搜索功能来搜索,也可在此事件中来自己实现搜索。 + onSelect(item: any, column: TiTableColumns): void { + // 从每一行进行过滤筛选 + this.srcData.data = this.data.filter((rowData: TiTableRowData) => { + // 遍历所有列 + for (const columnData of this.columns) { + // 只有筛选列有选中项时进行筛选,如果某一筛选列选中项不包含当前行数据,则跳出循环 + if (columnData.selected) { + // 单选过滤 + if (!Array.isArray(columnData.selected) && columnData.selected.label !== 'all') { + if (columnData.selected.label !== rowData[columnData.key]) { + return false; + } + // 多选过滤 + } else if (columnData.selected.length > 0) { + const index: number = columnData.selected.findIndex((item: any) => { + return item.label === rowData[columnData.key]; + }); + if (index < 0) { + return false; + } + } + } + } + + return true; + }); + this.totalNumber = this.srcData.data.length; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez', 'Henry', 'Jeff', 'John', 'Elizabeth']; + const firstName: string = nameList[((id + 3) * 19) % 5]; + const lastName: string = familyName[((id + 3) * 19) % 9]; + const total: string = (((id + 3) * 13) % 10000) + ' total'; + const email: string = (((id + 2) * 19) % 10000) + '@whatever.com'; + const balance: string = (((id + 4) * 761) % 10000) + ' balance'; + return { + firstName, + lastName, + total, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableLoadFailComponent.ts b/src/table/demo/src/app/table/TableLoadFailComponent.ts new file mode 100644 index 0000000..23148d4 --- /dev/null +++ b/src/table/demo/src/app/table/TableLoadFailComponent.ts @@ -0,0 +1,56 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-load-fail.html' +}) +export class TableLoadFailComponent implements OnInit { + status: number = 404; // 404应该是http请求返回的状态码,此处只是用此变量做模拟 + failLoadInfo: string = '加载失败,请'; + reloadInfo: string = '重新加载'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + reloadFn(): void { + console.log('重新加载'); + } +} diff --git a/src/table/demo/src/app/table/TableNodataComponent.ts b/src/table/demo/src/app/table/TableNodataComponent.ts new file mode 100644 index 0000000..4bcbd9f --- /dev/null +++ b/src/table/demo/src/app/table/TableNodataComponent.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-nodata.html' +}) +export class TableNodataComponent { + noDataInfo: string = '暂无表格数据'; + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [], + state: undefined + }; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableNodataSimpleComponent.ts b/src/table/demo/src/app/table/TableNodataSimpleComponent.ts new file mode 100644 index 0000000..1c79ea5 --- /dev/null +++ b/src/table/demo/src/app/table/TableNodataSimpleComponent.ts @@ -0,0 +1,50 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-nodata-simple.html' +}) +export class TableNodataSimpleComponent implements OnInit { + noDadaInfo: string = '暂无表格数据'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } +} diff --git a/src/table/demo/src/app/table/TableNodataTestComponent.ts b/src/table/demo/src/app/table/TableNodataTestComponent.ts new file mode 100644 index 0000000..0ccb72d --- /dev/null +++ b/src/table/demo/src/app/table/TableNodataTestComponent.ts @@ -0,0 +1,50 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-nodata-test.html' +}) +export class TableNodataTestComponent implements OnInit { + noDadaInfo: string = '暂无表格数据'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } +} diff --git a/src/table/demo/src/app/table/TableOverflowLinkComponent.ts b/src/table/demo/src/app/table/TableOverflowLinkComponent.ts new file mode 100644 index 0000000..8238cc7 --- /dev/null +++ b/src/table/demo/src/app/table/TableOverflowLinkComponent.ts @@ -0,0 +1,90 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-overflow-link.html' +}) +export class TableOverflowLinkComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name(默认设置)', + width: '20%' + }, + { + title: 'last name(解决方案一)', + width: '20%' + }, + { + title: 'email(解决方案二)', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = [ + '明月几时有,把酒问青天,不知天上宫阙,今夕是何年', + '我欲乘风归去,又恐琼楼玉宇,高处不胜寒。', + '起舞弄清影,何似在人间。', + '转朱阁,低绮户,照无眠。不应有恨,何事长向别时圆,', + '人有悲欢离合,月有阴晴圆缺,此事古难全。但愿人长久,千里共婵娟。' + ]; + + const familyName: Array = [ + '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪。', + '人生得意须尽欢,莫使金樽空对月。天生我材必有用,千金散尽还复来。', + '烹羊宰牛且为乐,会须一饮三百杯。岑夫子,丹丘生,将进酒,杯莫停。', + '与君歌一曲,请君为我倾耳听。钟鼓馔玉不足贵⑮,但愿长醉不复醒。', + '古来圣贤皆寂寞,惟有饮者留其名。陈王昔时宴平乐,斗酒十千恣欢谑。' + ]; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/table/demo/src/app/table/TablePagiWithFilterComponent.ts b/src/table/demo/src/app/table/TablePagiWithFilterComponent.ts new file mode 100644 index 0000000..1613b9b --- /dev/null +++ b/src/table/demo/src/app/table/TablePagiWithFilterComponent.ts @@ -0,0 +1,95 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-pagi-with-filter.html' +}) +export class TablePagiWithFilterComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + inputValue: string = 'p'; + searchWords: Array = [this.inputValue]; + searchKeys: Array = ['firstName', 'lastName', 'age', 'balance', 'email']; + currentPage: number = 1; + totalNumber: number = 62; + pageSize: { options: Array; size: number } = { + options: [10, 20, 50, 100], + size: 10 + }; + columns: TiTableColumns = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + searchedData: Array = []; + @ViewChild('table', { static: true }) table: TiTableComponent; + + ngOnInit(): void { + for (let j: number = 0; j < 62; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + setSearchWords(value: string): void { + this.searchWords[0] = value; + } + + getSearchedResult(): void { + // TiTableComponent提供了getSearchedResult方法,可通过此方法获取到搜索的数据结果。 + this.searchedData = this.table.getSearchedResult(); + console.log('searchedData', this.searchedData); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TablePaginationComponent.ts b/src/table/demo/src/app/table/TablePaginationComponent.ts new file mode 100644 index 0000000..d8f5b29 --- /dev/null +++ b/src/table/demo/src/app/table/TablePaginationComponent.ts @@ -0,0 +1,70 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-pagination.html' +}) +export class TablePaginationComponent implements OnInit { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [], + state: undefined + }; + columns: Array = [ + { + title: 'Id', + width: '10%' + }, + { + title: 'First Name', + width: '20%' + }, + { + title: 'Last Name', + width: '20%' + }, + { + title: 'Age', + width: '10%' + }, + { + title: 'Email Address', + width: '40%' + } + ]; + currentPage: number = 2; + totalNumber: number = 312; + pageSize: { options: Array; size: number } = { + options: [10, 20, 50, 100], + size: 10 + }; + + ngOnInit(): void { + const data: Array = []; + for (let i: number = 0; i < this.totalNumber; i++) { + data.push(this.createRandomItem(i)); + } + this.srcData.data = data; + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'Bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + + return { + firstName, + lastName, + age, + email, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableRadioComponent.ts b/src/table/demo/src/app/table/TableRadioComponent.ts new file mode 100644 index 0000000..c9c0256 --- /dev/null +++ b/src/table/demo/src/app/table/TableRadioComponent.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-radio.html' +}) +export class TableRadioComponent { + displayedData: Array = []; + selectedValue: string; + srcData: TiTableSrcData = { + data: [ + { + id: '1', + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + id: '2', + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com', + disabled: true + }, + { + id: '3', + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + id: '4', + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: '' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/table/demo/src/app/table/TableRadioTestComponent.ts b/src/table/demo/src/app/table/TableRadioTestComponent.ts new file mode 100644 index 0000000..391ae97 --- /dev/null +++ b/src/table/demo/src/app/table/TableRadioTestComponent.ts @@ -0,0 +1,108 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-radio-test.html' +}) +export class TableRadioTestComponent implements OnInit { + currentPage: number = 1; + totalNumber: number = 256; + pageSize: { options: Array; size: number } = { + options: [10, 20, 50, 100], + size: 10 + }; + selectedValue: number; // 选中项 + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: '' + }, + { + title: 'last name' + }, + { + title: 'birth date' + }, + { + title: 'balance' + }, + { + title: 'email' + } + ]; + constructor(private ref: ChangeDetectorRef) {} + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,模拟了后台分页场景 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: true // 源数据已进行分页处理 + } + }; + + // 初始时向后台发送请求获取数据 + this.getCurrentPageData(this.currentPage, this.pageSize.size).then((data: Array) => { + this.srcData.data = data; + this.totalNumber = 256; + this.selectedValue = data[2].id; + }); + } + + // 表格的数据状态(分页,排序,搜索)发生改变时,此方法会被触发; + // 多用于后台分页、排序和搜索 + stateUpdate(tiTable: TiTableComponent): void { + const dataState: TiTableDataState = tiTable.getDataState(); + this.getCurrentPageData(dataState.pagination.currentPage, dataState.pagination.itemsPerPage).then((data: Array) => { + this.srcData.data = data; + }); + } + + // 模拟后台异步请求返回的数据 + private getCurrentPageData(currentPage: number, itemsPerPage: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + const data: Array = []; + for (let j: number = (currentPage - 1) * itemsPerPage; j < currentPage * itemsPerPage; j++) { + data.push(this.createRandomItem(j)); + } + resolve(data); + // 在onpush模式下,异步场景需要手动触发变更,default模式下不用写该行代码 + this.ref.markForCheck(); + }, 1000); + }); + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const disabled: boolean = id % 4 === 0; + + return { + firstName, + lastName, + age, + email, + balance, + disabled, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableRowDrag2Component.ts b/src/table/demo/src/app/table/TableRowDrag2Component.ts new file mode 100644 index 0000000..b64927a --- /dev/null +++ b/src/table/demo/src/app/table/TableRowDrag2Component.ts @@ -0,0 +1,77 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-row-drag2.html' +}) +export class TableRowDrag2Component { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + id: '1', + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + id: '2', + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + id: '3', + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + id: '4', + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'Id' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; + private startIndex: number = 0; + + trackByFn(index: number, item: any): string { + return item.id; + } + + dragstart(index: number): void { + this.startIndex = index; + } + + dragover(event: any): void { + event.preventDefault(); + } + + drop(endIndex: number): void { + const current: TiTableRowData = this.displayedData.splice(this.startIndex, 1); + this.displayedData.splice(endIndex, 0, current[0]); + } +} diff --git a/src/table/demo/src/app/table/TableRowspanComponent.ts b/src/table/demo/src/app/table/TableRowspanComponent.ts new file mode 100644 index 0000000..6269274 --- /dev/null +++ b/src/table/demo/src/app/table/TableRowspanComponent.ts @@ -0,0 +1,58 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-rowspan.html' +}) +export class TableRowspanComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com', + rowSpan: 3, + colSpan: 1 + }, + { + firstName: 'Jacques', + age: 42, + email: 'Jacques@example.com', + rowHide: true, + colSpan: 1 + }, + { + firstName: 'Robert', + age: 15, + email: 'Robert@example.com', + rowHide: true, + colSpan: 1 + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 'no data', + rowSpan: 1, + colSpan: 2, + colHide: true + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableSearchComponent.ts b/src/table/demo/src/app/table/TableSearchComponent.ts new file mode 100644 index 0000000..9cb99e6 --- /dev/null +++ b/src/table/demo/src/app/table/TableSearchComponent.ts @@ -0,0 +1,103 @@ +import { Component } from '@angular/core'; +import { TiTableColumns } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-search.html' +}) +export class TableSearchComponent { + oneWordSearchValue: string = 'p'; + moreThanOneWordSearchValue1: string = ''; + moreThanOneWordSearchValue2: string = ''; + noDadaInfo: string = '暂无数据'; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; + oneWordSearch: any = { + displayedData: [], + srcData: { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }, + searchWords: [this.oneWordSearchValue], + searchKeys: ['firstName', 'lastName', 'age'], + searchStrictKeys: ['age'] + }; + moreThanOneWordSearch: any = { + displayedData: [], + srcData: { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }, + searchWords: [this.moreThanOneWordSearchValue1, this.moreThanOneWordSearchValue2], + searchKeys: ['firstName', 'age'] + }; + + setOneWordSearch(value: string): void { + this.oneWordSearch.searchWords[0] = value; + } + + setMoreThanOneWordSearch(value: string, index: number): void { + this.moreThanOneWordSearch.searchWords[index] = value; + } +} diff --git a/src/table/demo/src/app/table/TableServerPagiComponent.ts b/src/table/demo/src/app/table/TableServerPagiComponent.ts new file mode 100644 index 0000000..efbf93d --- /dev/null +++ b/src/table/demo/src/app/table/TableServerPagiComponent.ts @@ -0,0 +1,111 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-server-pagi.html' +}) +export class TableServerPagiComponent implements OnInit { + myLogs: Array = []; + + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + currentPage: number = 2; + totalNumber: number; + pageSize: { options: Array; size: number } = { + options: [10, 20, 50, 100], + size: 10 + }; + constructor(private ref: ChangeDetectorRef) {} + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了分页特性,且对源数据已进行了分页处理,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: true // 后台分页,源数据已进行了分页处理 + } + }; + + // 初始时向后台发送请求获取数据 + this.getCurrentPageData(this.currentPage, this.pageSize.size).then((data: Array) => { + this.myLogs = [...this.myLogs, '获取数据成功!']; + this.srcData.data = data; + this.totalNumber = 256; + }); + } + + // 表格的数据状态(分页,排序,搜索)发生改变时,此方法会被触发; + // 多用于后台分页、排序和搜索 + stateUpdate(tiTable: TiTableComponent): void { + this.myLogs = [...this.myLogs, `dataState=> ${JSON.stringify(tiTable.getDataState())}`]; + this.myLogs = [...this.myLogs, `triggerEvent=> ${tiTable.getTriggerEvent()}`]; + const dataState: TiTableDataState = tiTable.getDataState(); + this.getCurrentPageData(dataState.pagination.currentPage, dataState.pagination.itemsPerPage).then((data: Array) => { + this.myLogs = [...this.myLogs, '获取数据成功!']; + this.srcData.data = data; + }); + } + + // 模拟后台异步请求返回的数据 + private getCurrentPageData(currentPage: number, itemsPerPage: number): Promise { + this.myLogs = [...this.myLogs, '模拟向后台发送请求']; + return new Promise((resolve, reject) => { + setTimeout(() => { + const data: Array = []; + for (let j: number = (currentPage - 1) * itemsPerPage; j < currentPage * itemsPerPage; j++) { + data.push(this.createRandomItem(j)); + } + resolve(data); + // 在onpush模式下,异步场景需要手动触发变更,default模式下不用写该行代码 + this.ref.markForCheck(); + }, 1000); + }); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableServerPagiSearchSortComponent.ts b/src/table/demo/src/app/table/TableServerPagiSearchSortComponent.ts new file mode 100644 index 0000000..6941148 --- /dev/null +++ b/src/table/demo/src/app/table/TableServerPagiSearchSortComponent.ts @@ -0,0 +1,152 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-server-pagi-search-sort.html' +}) +export class TableServerPagiSearchSortComponent implements OnInit { + value: string = ''; + loading: boolean = true; + displayedData: Array = []; + searchWords: Array = [this.value]; + searchKeys: Array = ['firstName', 'lastName', 'email']; + currentPage: number = 1; + totalNumber: number = 0; + pageSize: { options: Array; size: number } = { + options: [10, 20, 50, 100], + size: 10 + }; + srcData: TiTableSrcData = { + data: [], + state: { + searched: true, // 源数据已进行过搜索处理 + sorted: true, // 源数据已进行过排序处理 + paginated: true // 源数据已进行过分页处理 + } + }; + columns: TiTableColumns = [ + { + title: 'Id' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age', + sortKey: 'age' + }, + { + title: 'Email Address' + } + ]; + private baseData: Array = []; + + ngOnInit(): void { + for (let j: number = 0; j < 226; j++) { + this.baseData.push(this.createRandomItem(j)); + } + + this.getCurrentPageData(this.currentPage, this.pageSize.size, this.searchWords, this.searchKeys, this.columns[2].sortKey, null).then( + (response: any) => { + this.loading = false; + this.srcData.data = response.data; + this.totalNumber = response.totalNumber; + } + ); + } + + // 点击搜索时,将搜索的值通过 tiTable 提供的 searchWords 接口传进表格, + // 然后表格的搜索数据状态就会改变,从而触发 stateUpdate的 事件的执行。 + onSearch(value: string): void { + this.searchWords[0] = value; + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + // 表格的数据状态(分页,排序,搜索)发生改变时,此方法会被触发 + onStateUpdate(tiTable: TiTableComponent): void { + const dataState: TiTableDataState = tiTable.getDataState(); + this.loading = true; + this.getCurrentPageData( + dataState.pagination.currentPage, + dataState.pagination.itemsPerPage, + dataState.search.searchWords, + dataState.search.searchKeys, + dataState.sort.sortKey, + dataState.sort.asc + ).then((response: any) => { + this.loading = false; + this.srcData.data = response.data; + this.totalNumber = response.totalNumber; + }); + } + + // 模拟异步远程请求返回的数据 + private getCurrentPageData( + currentPage: number, + itemsPerPage: number, + searchWords: Array, + searchKeys: Array, + sortKey: string, + asc: boolean + ): Promise { + let output: Array = [].concat(this.baseData); + + output = this.search(output, searchWords, searchKeys); + const totalNumber: number = output.length; + + if (sortKey && asc !== null) { + output.sort((a: any, b: any): number => { + return (a[sortKey] - b[sortKey]) * (asc ? 1 : -1); + }); + } + + const start: number = (currentPage - 1) * itemsPerPage; + output = output.slice(start, start + parseInt(itemsPerPage.toString(), 10)); + + return new Promise((resolve, reject) => { + setTimeout(() => { + resolve({ data: output, totalNumber }); + }, 1000); + }); + } + + private search(src: Array, searchWords: Array, searchKeys: Array): Array { + const output: Array = []; + src.forEach((item: TiTableRowData) => { + let isMatched: boolean = false; + for (let i: number = 0; i < searchKeys.length; i++) { + if (item[searchKeys[i]].toLowerCase().indexOf(searchWords[0].toLowerCase()) > -1) { + isMatched = true; + break; + } + } + if (isMatched) { + output.push(item); + } + }); + + return output; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + return { + firstName, + lastName, + age, + email, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableSmallComponent.ts b/src/table/demo/src/app/table/TableSmallComponent.ts new file mode 100644 index 0000000..ca93eb0 --- /dev/null +++ b/src/table/demo/src/app/table/TableSmallComponent.ts @@ -0,0 +1,52 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-small.html' +}) +export class TableSmallComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableSoldoutComponent.ts b/src/table/demo/src/app/table/TableSoldoutComponent.ts new file mode 100644 index 0000000..766ed44 --- /dev/null +++ b/src/table/demo/src/app/table/TableSoldoutComponent.ts @@ -0,0 +1,91 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-soldout.html' +}) +export class TableSoldoutComponent implements OnInit { + checkedList: Array = []; // 默认选中项 + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 21; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + this.checkedList.push(this.data[2]); + this.checkedList.push(this.data[6]); // 初始选中项 + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const messageArr: Array = [ + { text: '', class: 'ti3-table-row-message-soldout', iconName: '' }, + { text: 'New', class: 'ti3-table-row-message-new', iconName: '' }, + { + text: '', + class: 'ti3-table-row-message-recommended', + iconName: 'discount-sup' + } + ]; + const message: any = messageArr[((id + 3) * 19) % 3]; + + return { + firstName, + lastName, + age, + email, + balance, + message, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableSortBasicComponent.ts b/src/table/demo/src/app/table/TableSortBasicComponent.ts new file mode 100644 index 0000000..61b75a2 --- /dev/null +++ b/src/table/demo/src/app/table/TableSortBasicComponent.ts @@ -0,0 +1,93 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-sort-basic.html' +}) +export class TableSortBasicComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + sortKey: 'firstName', // 设置排序时按照源数据中的哪一个属性进行排序, + width: '10%' + }, + { + title: 'last name', + sortKey: 'lastName', + width: '25%' + }, + { + title: 'birthday', + sortKey: 'birthday', + asc: false, // 默认排序,且为降序 + width: '20%' + }, + { + title: 'balance', + sortKey: 'balance', + width: '30%' + }, + { + title: 'email' + } + ]; + @ViewChild(TiTableComponent, { static: true }) tableCom: TiTableComponent; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了排序特性,且对源数据未进行排序处理,因此tiny会对数据进行排序处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 1) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const birthday: string = new Date(1600657756626 - age * 3600 * 24 * 365 * 1000).toISOString().substring(0, 10); + + return { + firstName, + lastName, + age, + birthday, + email, + balance, + id + }; + } + + clearSort(): void { + const dataState: TiTableDataState = this.tableCom.getDataState(); + dataState.sort.sortKey = undefined; + dataState.sort.asc = null; + } + + // 10.1.13 版本支持手动修改表格排序状态 + changeSort(): void { + const dataState: TiTableDataState = this.tableCom.getDataState(); + dataState.sort.sortKey = 'firstName'; + dataState.sort.asc = false; + } +} diff --git a/src/table/demo/src/app/table/TableSortComparefnComponent.ts b/src/table/demo/src/app/table/TableSortComparefnComponent.ts new file mode 100644 index 0000000..351f2ef --- /dev/null +++ b/src/table/demo/src/app/table/TableSortComparefnComponent.ts @@ -0,0 +1,85 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-sort-comparefn.html' +}) +export class TableSortComparefnComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + sortKey: 'firstName', // 设置排序时按照源数据中的哪一个属性进行排序, + compareFn: (a: object, b: object, predicate: string): number => { + console.log('firstName compareFn', this); + console.log('Params', a, b, predicate); + + return a[predicate].length - b[predicate].length; + } + }, + { + title: 'last name', + sortKey: 'lastName' + }, + { + title: 'birth date', + sortKey: 'age', + asc: true, // 默认排序,且为升序 + compareFn: (a: Object, b: Object, predicate: string): number => { + console.log('age compareFn', this); + console.log('Params', a, b, predicate); + + return a[predicate] - b[predicate]; + } + }, + { + title: 'balance', + sortKey: 'balance' + }, + { + title: 'email' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了排序特性,且对源数据未进行排序处理,因此tiny会对数据进行排序处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableSortComparefnLocaleComponent.ts b/src/table/demo/src/app/table/TableSortComparefnLocaleComponent.ts new file mode 100644 index 0000000..6f162a5 --- /dev/null +++ b/src/table/demo/src/app/table/TableSortComparefnLocaleComponent.ts @@ -0,0 +1,82 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-sort-comparefn-locale.html' +}) +export class TableSortComparefnLocaleComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + sortKey: 'firstName', // 设置排序时按照源数据中的哪一个属性进行排序, + asc: true // 默认排序,且为升序 + }, + { + title: 'last name', + sortKey: 'lastName' + }, + { + title: 'birth date', + sortKey: 'age' + }, + { + title: 'balance', + sortKey: 'balance' + }, + { + title: 'email' + } + ]; + + // 表格组件提供的排序中字符串比较是使用基于标准字典的 Unicode 值来进行比较的。如果开发者需要本地化的排序, + // 可使用tiHeadSort组件的compareFn接口来自定义所在列的本地化排序规则。本地化排序规则可利用 localeCompare 方法。 + compareFn = (a: TiTableRowData, b: TiTableRowData, sortKey: string): number => { + console.log('Params', a, b, sortKey); + const language: string = 'zh-CN'; // 根据实际情况设置当前语言种类 + + return a[sortKey].localeCompare(b[sortKey], language); // localeCompare方法还有更多配置,可在网上查阅。 + }; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了排序特性,且对源数据未进行排序处理,因此tiny会对数据进行排序处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['内存状态', '网口状态', '文件状态', '磁盘寿命', 'CPU状态', 'I/O状态']; + const familyName: Array = ['主机信息统计', '网络统计', '文件系统统计', 'CPU统计', 'I/O统计']; + const firstName: string = nameList[((id + 3) * 19) % 6]; + const lastName: string = familyName[((id + 3) * 29) % 5]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableSortComponent.ts b/src/table/demo/src/app/table/TableSortComponent.ts new file mode 100644 index 0000000..38e622a --- /dev/null +++ b/src/table/demo/src/app/table/TableSortComponent.ts @@ -0,0 +1,72 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-sort.html' +}) +export class TableSortComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com', + poetry: '但愿人长久,千里共婵娟。' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com', + poetry: '天生我材必有用,千金散尽还复来。' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com', + poetry: '人生得意须尽欢,莫使金樽空对月。' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com', + poetry: '明月几时有,把酒问青天,不知天上宫阙,今夕是何年。' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'First Name', + sortKey: 'firstName', + asc: true + }, + { + title: 'Last Name', + sortKey: 'lastName', + compareFn: (a: TiTableRowData, b: TiTableRowData, sortKey: string): number => { + return a[sortKey].length - b[sortKey].length; + } + }, + { + title: 'Age', + sortKey: 'age' + }, + { + title: 'Favorite Chinese Poetry', + sortKey: 'poetry', + compareFn: (a: TiTableRowData, b: TiTableRowData, sortKey: string): number => { + const language: string = 'zh-CN'; + + return a[sortKey].localeCompare(b[sortKey], language); + } + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableSortDetailsComponent.ts b/src/table/demo/src/app/table/TableSortDetailsComponent.ts new file mode 100644 index 0000000..6bd74bb --- /dev/null +++ b/src/table/demo/src/app/table/TableSortDetailsComponent.ts @@ -0,0 +1,89 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-sort-details.html' +}) +export class TableSortDetailsComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + columns: TiTableColumns = [ + { + title: '' + }, + { + title: 'first name', + sortKey: 'firstName' // 设置排序时按照源数据中的哪一个属性进行排序, + }, + { + title: 'last name', + sortKey: 'lastName' + }, + { + title: 'birth date', + sortKey: 'age', + asc: true // 默认排序,且为升序 + }, + { + title: 'balance', + sortKey: 'balance' + }, + { + title: 'email' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } + + changeData(): void { + const temp: TiTableRowData = { + firstName: 'Rose', + lastName: 'Jack', + age: 23, + email: 'roseJack@example.com', + balance: (this.data.length * 761) % 10000, + id: this.data.length + }; + this.srcData['data'].push(temp); + } +} diff --git a/src/table/demo/src/app/table/TableSortResetComponent.ts b/src/table/demo/src/app/table/TableSortResetComponent.ts new file mode 100644 index 0000000..a39e48f --- /dev/null +++ b/src/table/demo/src/app/table/TableSortResetComponent.ts @@ -0,0 +1,71 @@ +import { Component, ViewChild } from '@angular/core'; +import { TiTableComponent, TiTableColumns, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-sort-reset.html' +}) +export class TableSortResetComponent { + @ViewChild(TiTableComponent, { static: true }) tableInstance: TiTableComponent; + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'First Name', + sortKey: 'firstName' + }, + { + title: 'Last Name', + sortKey: 'lastName', + compareFn: (a: TiTableRowData, b: TiTableRowData, sortKey: string): number => { + return a[sortKey].length - b[sortKey].length; + } + }, + { + title: 'Age', + sortKey: 'age' + }, + { + title: 'Email Address' + } + ]; + + sortByAge(): void { + const dataState: TiTableDataState = this.tableInstance.getDataState(); + dataState.sort.sortKey = 'age'; + dataState.sort.asc = false; + } + + clearSorters(): void { + const dataState: TiTableDataState = this.tableInstance.getDataState(); + dataState.sort.sortKey = undefined; + dataState.sort.asc = null; + } +} diff --git a/src/table/demo/src/app/table/TableSortTestComponent.ts b/src/table/demo/src/app/table/TableSortTestComponent.ts new file mode 100644 index 0000000..f39d238 --- /dev/null +++ b/src/table/demo/src/app/table/TableSortTestComponent.ts @@ -0,0 +1,93 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-sort-test.html' +}) +export class TableSortTestComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + sortKey: 'firstName', // 设置排序时按照源数据中的哪一个属性进行排序, + width: '10%' + }, + { + title: 'last name', + sortKey: 'lastName', + width: '25%' + }, + { + title: 'birthday', + sortKey: 'birthday', + asc: false, // 默认排序,且为降序 + width: '20%' + }, + { + title: 'balance', + sortKey: 'balance', + width: '30%' + }, + { + title: 'email' + } + ]; + @ViewChild(TiTableComponent, { static: true }) tableCom: TiTableComponent; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了排序特性,且对源数据未进行排序处理,因此tiny会对数据进行排序处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 1) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const birthday: string = new Date(1600657756626 - age * 3600 * 24 * 365 * 1000).toISOString().substring(0, 10); + + return { + firstName, + lastName, + age, + birthday, + email, + balance, + id + }; + } + + clearSort(): void { + const dataState: TiTableDataState = this.tableCom.getDataState(); + dataState.sort.sortKey = undefined; + dataState.sort.asc = null; + } + + // 10.1.13 版本支持手动修改表格排序状态 + changeSort(): void { + const dataState: TiTableDataState = this.tableCom.getDataState(); + dataState.sort.sortKey = 'firstName'; + dataState.sort.asc = false; + } +} diff --git a/src/table/demo/src/app/table/TableStorageComponent.ts b/src/table/demo/src/app/table/TableStorageComponent.ts new file mode 100644 index 0000000..afc10f2 --- /dev/null +++ b/src/table/demo/src/app/table/TableStorageComponent.ts @@ -0,0 +1,69 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-storage.html' +}) +export class TableStorageComponent implements OnInit { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [], + state: undefined + }; + currentPage: number = 1; + totalNumber: number = 126; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 40, 60], + size: 10 + }; + columns: Array = [ + { + title: 'Id', + width: '10%' + }, + { + title: 'First Name', + width: '25%' + }, + { + title: 'Last Name', + field: 'lastName', + sortKey: 'lastName', + width: '25%' + }, + { + title: 'Age', + sortKey: 'age', + width: '10%' + }, + { + title: 'email', + width: '30%' + } + ]; + + ngOnInit(): void { + const data: Array = []; + for (let j: number = 0; j < this.totalNumber; j++) { + data.push(this.createRandomItem(j)); + } + this.srcData.data = data; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 1) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + + return { + firstName, + lastName, + age, + email, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableStorageConfigComponent.ts b/src/table/demo/src/app/table/TableStorageConfigComponent.ts new file mode 100644 index 0000000..a3600d4 --- /dev/null +++ b/src/table/demo/src/app/table/TableStorageConfigComponent.ts @@ -0,0 +1,101 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData, TiTableStorageConfig } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-storage-config.html' +}) +export class TableStorageConfigComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + field: 'firstName', + width: '10%' + }, + { + title: 'last name', + field: 'lastName', + show: true, + width: '25%' + }, + { + title: 'birthday', + sortKey: 'birthday', + field: 'birthday', + show: true, + asc: false, + width: '20%' + }, + { + title: 'balance', + field: 'balance', + show: true, + sortKey: 'balance', + width: '30%' + }, + { + title: 'email', + field: 'email', + show: true + } + ]; + @ViewChild(TiTableComponent, { static: true }) tableCom: TiTableComponent; + + currentPage: number = 1; + totalNumber: number = 100; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 40, 60], + size: 5 + }; + + storageConfig: TiTableStorageConfig = { + sort: false, + // pagination设置为false,表示当前页和每页个数都不记忆 + // pagination设置为true,表示当前页和每页个数都要记忆 + // 10.1.2版本支持只记忆当前页或只记忆每页个数 + pagination: { + currentPage: false, + itemsPerPage: true + }, + colsWidth: false + }; + + ngOnInit(): void { + for (let j: number = 0; j < 100; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 1) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const birthday: string = new Date(1600657756626 - age * 3600 * 24 * 365 * 1000).toISOString().substring(0, 10); + + return { + firstName, + lastName, + age, + birthday, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableStorageFilterComponent.ts b/src/table/demo/src/app/table/TableStorageFilterComponent.ts new file mode 100644 index 0000000..8f4f41c --- /dev/null +++ b/src/table/demo/src/app/table/TableStorageFilterComponent.ts @@ -0,0 +1,123 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-storage-filter.html' +}) +export class TableStorageFilterComponent implements OnInit { + storageId: string = 'storage-filter'; + searchWords: Array = ['', '', '']; + searchKeys: Array = ['firstName', 'lastName', 'age']; // 设置过滤字段 + displayed: Array = []; + srcData: TiTableSrcData; + data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%', + key: 'lastName', + options: [ + { + label: 'all' + }, + { + label: 'Dupont' + }, + { + label: 'Germain' + }, + { + label: 'Delcourt' + }, + { + label: 'bjip' + }, + { + label: 'Menez' + }, + { + label: 'Henry' + }, + { + label: 'Jeff' + }, + { + label: 'John' + }, + { + label: 'Elizabeth' + } + ] + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + // 修复SSR报错:ERROR ReferenceError: window is not defined + if (typeof window === 'undefined') { + return; + } + // 检查缓存 + let storageSearchWords = window.sessionStorage.getItem(this.storageId); + if (storageSearchWords) { + let { searchWords } = JSON.parse(storageSearchWords); + this.searchWords = [...searchWords]; + // 表格过滤选中项 + this.columns[1].selected = this.columns[1].options.find((option) => { + return option.label === searchWords[1]; + }); + } + } + + onSelect(item: any, column: TiTableColumns): void { + const index: number = this.searchKeys.indexOf(column.key); + this.searchWords[index] = item['label'] === 'all' ? '' : item['label']; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableStorageServeComponent.ts b/src/table/demo/src/app/table/TableStorageServeComponent.ts new file mode 100644 index 0000000..6d2b125 --- /dev/null +++ b/src/table/demo/src/app/table/TableStorageServeComponent.ts @@ -0,0 +1,115 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-storage-serve.html' +}) +export class TableStorageServeComponent implements OnInit { + myLogs: Array = []; + storageId: string = 'table-storage-pagation-serve'; + storageConfig = { + sort: false, + pagination: true, + colsWidth: false + }; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + currentPage: number = 1; + totalNumber: number = 222; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50, 100], + size: 5 + }; + constructor(private ref: ChangeDetectorRef) {} + ngOnInit(): void { + this.srcData = { + data: [], + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: true // 后台分页,源数据已进行了分页处理 + } + }; + // 修复SSR报错:ERROR ReferenceError: window is not defined + // 处理用户第一次访问页面,没有记忆状态,不会触发stateUpdate + let storageState = typeof window !== 'undefined' ? window.localStorage.getItem('this.storageId') : undefined; + if (!storageState) { + this.getCurrentPageData(this.currentPage, this.pageSize.size).then((data: Array) => { + this.myLogs = [...this.myLogs, '初始化化成功']; + this.srcData.data = data; + this.totalNumber = 256; + }); + } + } + + // 表格的数据状态(分页,排序,搜索)发生改变时,此方法会被触发; + // 多用于后台分页、排序和搜索 + stateUpdate(tiTable: TiTableComponent): void { + this.myLogs = [...this.myLogs, `dataState=> ${JSON.stringify(tiTable.getDataState())}`]; + this.myLogs = [...this.myLogs, `triggerEvent=> ${tiTable.getTriggerEvent()}`]; + const dataState: TiTableDataState = tiTable.getDataState(); + this.getCurrentPageData(dataState.pagination.currentPage, dataState.pagination.itemsPerPage).then((data: Array) => { + this.myLogs = [...this.myLogs, '获取数据成功!']; + this.srcData.data = data; + this.totalNumber = 256; + }); + } + + // 模拟后台异步请求返回的数据 + private getCurrentPageData(currentPage: number, itemsPerPage: number): Promise { + this.myLogs = [...this.myLogs, '模拟向后台发送请求']; + return new Promise((resolve, reject) => { + setTimeout(() => { + const data: Array = []; + for (let j: number = (currentPage - 1) * itemsPerPage; j < currentPage * itemsPerPage; j++) { + data.push(this.createRandomItem(j)); + } + resolve(data); + // 在onpush模式下,异步场景需要手动触发变更,default模式下不用写该行代码 + this.ref.markForCheck(); + }, 1000); + }); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableTestModule.ts b/src/table/demo/src/app/table/TableTestModule.ts new file mode 100644 index 0000000..c3fa926 --- /dev/null +++ b/src/table/demo/src/app/table/TableTestModule.ts @@ -0,0 +1,642 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ScrollingModule } from '@angular/cdk/scrolling'; + +import { + TiAccordionModule, + TiActionmenuModule, + TiButtonModule, + TiCheckboxModule, + TiDateModule, + TiFormfieldModule, + TiIconModule, + TiLeftmenuModule, + TiLoadingModule, + TiMessageModule, + TiOverflowModule, + TiPaginationModule, + TiRadioModule, + TiSearchboxModule, + TiSelectModule, + TiTableModule, + TiTextModule, + TiTipModule, + TiValidationModule +} from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TableBasicComponent } from './TableBasicComponent'; +import { TableBasicTestComponent } from './TableBasicTestComponent'; +import { TableFilterStrictComponent } from './TableFilterStrictComponent'; +import { TableFilterComponent } from './TableFilterComponent'; +import { TablePaginationComponent } from './TablePaginationComponent'; +import { TableServerPagiComponent } from './TableServerPagiComponent'; +import { TablePagiWithFilterComponent } from './TablePagiWithFilterComponent'; +import { TableDetailsComponent } from './TableDetailsComponent'; +import { TableDetailsCloseotherdetailsComponent } from './TableDetailsCloseotherdetailsComponent'; +import { TableDynamicDetailsComponent } from './TableDynamicDetailsComponent'; +import { TableSortBasicComponent } from './TableSortBasicComponent'; +import { TableSortComparefnComponent } from './TableSortComparefnComponent'; +import { TableSortDetailsComponent } from './TableSortDetailsComponent'; +import { TableSortComparefnLocaleComponent } from './TableSortComparefnLocaleComponent'; +import { TableSortTestComponent } from './TableSortTestComponent'; +import { TableColsToggleComponent } from './TableColsToggleComponent'; +import { TableColsToggleDetailsComponent } from './TableColsToggleDetailsComponent'; +import { NestedTableComponent, TableDetailsNesttableComponent } from './TableDetailsNesttableComponent'; +import { TableDetailsPaginationComponent } from './TableDetailsPaginationComponent'; +import { TableColsResizableComponent } from './TableColsResizableComponent'; +import { TableColsresizableSortComponent } from './TableColsresizableSortComponent'; +import { TableColsresizableColstoggleComponent } from './TableColsresizableColstoggleComponent'; +import { TableColsresizableLoadfailComponent } from './TableColsresizableLoadfailComponent'; +import { TableCellTipComponent } from './TableCellTipComponent'; +import { TableCelliconsColsresizableComponent } from './TableCelliconsColsresizableComponent'; +import { TableNodataComponent } from './TableNodataComponent'; +import { TableNodataSimpleComponent } from './TableNodataSimpleComponent'; +import { TableLoadFailComponent } from './TableLoadFailComponent'; +import { TableGuideComponent } from './TableGuideComponent'; +import { TableColAlignComponent } from './TableColAlignComponent'; +import { TableColalignSortResizableTestComponent } from './TableColalignSortResizableTestComponent'; +import { TableGroupComponent } from './TableGroupComponent'; +import { TableCheckboxComponent } from './TableCheckboxComponent'; +import { TableRadioComponent } from './TableRadioComponent'; +import { TableRowspanComponent } from './TableRowspanComponent'; +import { TableSoldoutComponent } from './TableSoldoutComponent'; +import { TableTreeComponent } from './TableTreeComponent'; +import { TableTreeUnknowdeepthComponent } from './TableTreeUnknowdeepthComponent'; +import { TableTreeMulitiselectComponent } from './TableTreeMulitiselectComponent'; +import { TableSmallComponent } from './TableSmallComponent'; +import { TableFixedHeadComponent } from './TableFixedHeadComponent'; +import { TableFixedHeadPaginationDetailsComponent } from './TableFixedHeadPaginationDetailsComponent'; +import { TableFixedHeadInAccordionComponent } from './TableFixedHeadInAccordionComponent'; +import { TableFixedHeadNodataComponent } from './TableFixedHeadNodataComponent'; +import { TableFixedHeadColsResizableComponent } from './TableFixedHeadColsResizableComponent'; +import { TableFixedheadColsresizablePaginationDetailsComponent } from './TableFixedheadColsresizablePaginationDetailsComponent'; +import { TableActionmenuComponent } from './TableActionmenuComponent'; +import { TableHeadFilterTestComponent } from './TableHeadFilterTestComponent'; +import { TableHeadFilterValuekeyComponent } from './TableHeadFilterValuekeyComponent'; +import { TableHeadFilterMultiValuekeyComponent } from './TableHeadFilterMultiValuekeyComponent'; +import { TableHeadFilterMultiComponent } from './TableHeadFilterMultiComponent'; +import { TableHeadFilterDatetimeComponent } from './TableHeadFilterDatetimeComponent'; +import { TableHeadFilterVirtualscrollComponent } from './TableHeadFilterVirtualscrollComponent'; +import { TableColsresizableSortHeadfilterComponent } from './TableColsresizableSortHeadfilterComponent'; +import { TableServerPagiSearchSortComponent } from './TableServerPagiSearchSortComponent'; +import { TableColsresizableColstoggleFixedheadComponent } from './TableColsresizableColstoggleFixedheadComponent'; +import { TableColumnFixedComponent } from './TableColumnFixedComponent'; +import { TableColumnfixedResizableComponent } from './TableColumnfixedResizableComponent'; +import { TableColumnfixedHeadfixedComponent } from './TableColumnfixedHeadfixedComponent'; +import { TableColumnfixedEditrowComponent } from './TableColumnfixedEditrowComponent'; +import { TableColumnfixedNodataComponent } from './TableColumnfixedNodataComponent'; +import { TableColumnfixedCheckboxComponent } from './TableColumnfixedCheckboxComponent'; +import { TableColumnfixedColstoggleComponent } from './TableColumnfixedColstoggleComponent'; +import { TableColumnfixedPaginationComponent } from './TableColumnfixedPaginationComponent'; +import { TableColumnfixedFixedheadColsresizablePaginationComponent } from './TableColumnfixedFixedheadColsresizablePaginationComponent'; +import { TableColumnfixedLeftmenuComponent } from './TableColumnfixedLeftmenuComponent'; +import { TableEditrowComponent } from './TableEditrowComponent'; +import { TableEditallComponent } from './TableEditallComponent'; +import { TableOverflowLinkComponent } from './TableOverflowLinkComponent'; +import { TableFixheadScrollComponent } from './TableFixheadScrollComponent'; +import { TableRowDrag2Component } from './TableRowDrag2Component'; +import { TableStorageFilterComponent } from './TableStorageFilterComponent'; +import { TableStorageConfigComponent } from './TableStorageConfigComponent'; +import { TableStorageServeComponent } from './TableStorageServeComponent'; +import { TableStorageComponent } from './TableStorageComponent'; +import { TableCheckboxPaginationComponent } from './TableCheckboxPaginationComponent'; +import { TableComprehensiveComponent } from './TableComprehensiveComponent'; +import { TableVirtualscrollBasicComponent } from './TableVirtualscrollBasicComponent'; +import { TableVirtualscrollSizesComponent } from './TableVirtualscrollSizesComponent'; +import { TableVirtualscrollComprehensiveComponent } from './TableVirtualscrollComprehensiveComponent'; +import { TableVirtualscrollTreeComponent } from './TableVirtualscrollTreeComponent'; +import { TableCheckboxPaginationHeadmenuComponent } from './TableCheckboxPaginationHeadmenuComponent'; +import { TableNodataTestComponent } from './TableNodataTestComponent'; +import { TableSortComponent } from './TableSortComponent'; +import { TableSortResetComponent } from './TableSortResetComponent'; +import { TableSearchComponent } from './TableSearchComponent'; +import { TableHeadFilterComponent } from './TableHeadFilterComponent'; +import { TableHeadFilterDatetimeTestComponent } from './TableHeadFilterDatetimeTestComponent'; +import { TableRadioTestComponent } from './TableRadioTestComponent'; +import { TableColsToggleTestComponent } from './TableColsToggleTestComponent'; +import { TableColsresizableBasicComponent } from './TableColsresizableBasicComponent'; +import { TableEditrowTestComponent } from './TableEditrowTestComponent'; +import { TableEditallTestComponent } from './TableEditallTestComponent'; +import { TableVirtualscrollComponent } from './TableVirtualscrollComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiActionmenuModule, + TiPaginationModule, + TiTipModule, + TiOverflowModule, + TiCheckboxModule, + TiRadioModule, + TiAccordionModule, + TiTextModule, + TiSearchboxModule, + TiTableModule, + TiIconModule, + TiSelectModule, + TiDateModule, + TiValidationModule, + TiLeftmenuModule, + TiMessageModule, + ScrollingModule, + TiFormfieldModule, + TiButtonModule, + TiLoadingModule, + DemoLogModule, + RouterModule.forChild(TableTestModule.ROUTES) + ], + declarations: [ + TableBasicComponent, + TableFilterComponent, + TableFilterStrictComponent, + TablePaginationComponent, + TableServerPagiComponent, + TablePagiWithFilterComponent, + TableSortBasicComponent, + TableDetailsComponent, + TableDetailsCloseotherdetailsComponent, + TableDynamicDetailsComponent, + TableSortComparefnComponent, + TableSortComparefnLocaleComponent, + TableSortTestComponent, + TableSortDetailsComponent, + TableColsToggleComponent, + TableColsToggleDetailsComponent, + TableDetailsNesttableComponent, + TableDetailsPaginationComponent, + NestedTableComponent, + TableColsResizableComponent, + TableColsresizableSortComponent, + TableColsresizableColstoggleComponent, + TableColsresizableLoadfailComponent, + TableCellTipComponent, + TableCelliconsColsresizableComponent, + TableNodataComponent, + TableNodataSimpleComponent, + TableLoadFailComponent, + TableGuideComponent, + TableColAlignComponent, + TableColalignSortResizableTestComponent, + TableGroupComponent, + TableCheckboxComponent, + TableRadioComponent, + TableRowspanComponent, + TableSoldoutComponent, + TableTreeComponent, + TableTreeUnknowdeepthComponent, + TableTreeMulitiselectComponent, + TableSmallComponent, + TableFixedHeadComponent, + TableFixedHeadPaginationDetailsComponent, + TableFixedHeadInAccordionComponent, + TableFixedHeadNodataComponent, + TableFixedHeadColsResizableComponent, + TableFixedheadColsresizablePaginationDetailsComponent, + TableActionmenuComponent, + TableHeadFilterTestComponent, + TableHeadFilterValuekeyComponent, + TableHeadFilterMultiValuekeyComponent, + TableHeadFilterMultiComponent, + TableHeadFilterDatetimeComponent, + TableHeadFilterVirtualscrollComponent, + TableColsresizableSortHeadfilterComponent, + TableServerPagiSearchSortComponent, + TableColsresizableColstoggleFixedheadComponent, + TableColumnFixedComponent, + TableColumnfixedResizableComponent, + TableColumnfixedHeadfixedComponent, + TableColumnfixedNodataComponent, + TableColumnfixedCheckboxComponent, + TableColumnfixedColstoggleComponent, + TableColumnfixedPaginationComponent, + TableColumnfixedFixedheadColsresizablePaginationComponent, + TableEditrowComponent, + TableEditallComponent, + TableFixheadScrollComponent, + TableColumnfixedLeftmenuComponent, + TableColumnfixedEditrowComponent, + TableStorageComponent, + TableStorageFilterComponent, + TableStorageConfigComponent, + TableStorageServeComponent, + TableCheckboxPaginationComponent, + TableCheckboxPaginationHeadmenuComponent, + TableRowDrag2Component, + TableComprehensiveComponent, + TableVirtualscrollBasicComponent, + TableVirtualscrollSizesComponent, + TableVirtualscrollComprehensiveComponent, + TableVirtualscrollTreeComponent, + TableOverflowLinkComponent, + TableBasicTestComponent, + TableNodataTestComponent, + TableSortComponent, + TableSortResetComponent, + TableSearchComponent, + TableHeadFilterComponent, + TableHeadFilterDatetimeTestComponent, + TableRadioTestComponent, + TableColsToggleTestComponent, + TableColsresizableBasicComponent, + TableEditrowTestComponent, + TableEditallTestComponent, + TableVirtualscrollComponent + ] +}) +export class TableTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiTableComponent.html', label: 'TiTable' }, + { href: 'components/TiColsToggleComponent.html', label: 'ColsToggle' }, + { href: 'components/TiCellTextComponent.html', label: 'CellText' }, + { href: 'components/TiCellIconsComponent.html', label: 'CellIcons' }, + { href: 'components/TiHeadSortComponent.html', label: 'HeadSort' }, + { + href: 'directives/TiColsResizableDirective.html', + label: 'ColsResizable' + }, + { href: 'components/TiDetailsIconComponent.html', label: 'DetailsIcon' }, + { href: 'directives/TiDetailsTrDirective.html', label: 'DetailsTr' }, + { href: 'directives/TiColspanDirective.html', label: 'Colspan' } + ]; + static readonly ROUTES: Routes = [ + { + path: 'table/table-basic', + component: TableBasicComponent + }, + { + path: 'table/table-small', + component: TableSmallComponent + }, + { + path: 'table/table-pagination', + component: TablePaginationComponent + }, + { + path: 'table/table-server-pagi', + component: TableServerPagiComponent + }, + { + path: 'table/table-checkbox', + component: TableCheckboxComponent + }, + { + path: 'table/table-checkbox-pagination', + component: TableCheckboxPaginationComponent + }, + { + path: 'table/table-checkbox-pagination-headmenu', + component: TableCheckboxPaginationHeadmenuComponent + }, + { + path: 'table/table-radio-test', + component: TableRadioTestComponent + }, + { + path: 'table/table-filter', + component: TableFilterComponent + }, + { + path: 'table/table-filter-strict', + component: TableFilterStrictComponent + }, + { + path: 'table/table-sort-basic', + component: TableSortBasicComponent + }, + { + path: 'table/table-sort-comparefn', + component: TableSortComparefnComponent + }, + { + path: 'table/table-sort-comparefn-locale', + component: TableSortComparefnLocaleComponent + }, + { + path: 'table/table-details', + component: TableDetailsComponent + }, + { + path: 'table/table-details-closeotherdetails', + component: TableDetailsCloseotherdetailsComponent + }, + { + path: 'table/table-details-nesttable', + component: TableDetailsNesttableComponent + }, + { + path: 'table/table-dynamic-details', + component: TableDynamicDetailsComponent + }, + { + path: 'table/table-cols-toggle-test', + component: TableColsToggleTestComponent + }, + { + path: 'table/table-colsresizable-basic', + component: TableColsresizableBasicComponent + }, + { + path: 'table/table-colsresizable-sort', + component: TableColsresizableSortComponent + }, + { + path: 'table/table-cellicons-colsresizable', + component: TableCelliconsColsresizableComponent + }, + { + path: 'table/table-nodata', + component: TableNodataComponent + }, + { + path: 'table/table-nodata-simple', + component: TableNodataSimpleComponent + }, + { + path: 'table/table-load-fail', + component: TableLoadFailComponent + }, + { + path: 'table/table-guide', + component: TableGuideComponent + }, + { + path: 'table/table-soldout', + component: TableSoldoutComponent + }, + { + path: 'table/table-cell-tip', + component: TableCellTipComponent + }, + { + path: 'table/table-col-align', + component: TableColAlignComponent + }, + { + path: 'table/table-group', + component: TableGroupComponent + }, + { + path: 'table/table-rowspan', + component: TableRowspanComponent + }, + { + path: 'table/table-tree', + component: TableTreeComponent + }, + { + path: 'table/table-tree-unknowdeepth', + component: TableTreeUnknowdeepthComponent + }, + { + path: 'table/table-tree-mulitiselect', + component: TableTreeMulitiselectComponent + }, + { + path: 'table/table-fixed-head', + component: TableFixedHeadComponent + }, + { + path: 'table/table-actionmenu', + component: TableActionmenuComponent + }, + { + path: 'table/table-head-filter-test', + component: TableHeadFilterTestComponent + }, + { + path: 'table/table-head-filter-valuekey', + component: TableHeadFilterValuekeyComponent + }, + { + path: 'table/table-head-filter-multi-valuekey', + component: TableHeadFilterMultiValuekeyComponent + }, + { + path: 'table/table-head-filter-multi', + component: TableHeadFilterMultiComponent + }, + { + path: 'table/table-head-filter-datetime-test', + component: TableHeadFilterDatetimeTestComponent + }, + { + path: 'table/table-head-filter-virtualscroll', + component: TableHeadFilterVirtualscrollComponent + }, + { + path: 'table/table-server-pagi-search-sort', + component: TableServerPagiSearchSortComponent + }, + { + path: 'table/table-colsresizable-colstoggle-fixedhead', + component: TableColsresizableColstoggleFixedheadComponent + }, + { + path: 'table/table-column-fixed', + component: TableColumnFixedComponent + }, + { + path: 'table/table-columnfixed-resizable', + component: TableColumnfixedResizableComponent + }, + { + path: 'table/table-columnfixed-headfixed', + component: TableColumnfixedHeadfixedComponent + }, + { + path: 'table/table-columnfixed-checkbox', + component: TableColumnfixedCheckboxComponent + }, + { + path: 'table/table-columnfixed-colstoggle', + component: TableColumnfixedColstoggleComponent + }, + { + path: 'table/table-editrow-test', + component: TableEditrowTestComponent + }, + { + path: 'table/table-editall-test', + component: TableEditallTestComponent + }, + { + path: 'table/table-row-drag2', + component: TableRowDrag2Component + }, + { + path: 'table/table-storage', + component: TableStorageComponent + }, + { + path: 'table/table-storage-filter', + component: TableStorageFilterComponent + }, + { + path: 'table/table-storage-config', + component: TableStorageConfigComponent + }, + { + path: 'table/table-storage-serve', + component: TableStorageServeComponent + }, + { + path: 'table/table-virtualscroll-basic', + component: TableVirtualscrollBasicComponent + }, + { + path: 'table/table-virtualscroll-comprehensive', + component: TableVirtualscrollComprehensiveComponent + }, + { + path: 'table/table-virtualscroll-tree', + component: TableVirtualscrollTreeComponent + }, + // 用作内部测试用例 + { + path: 'table/table-pagi-with-filter', + component: TablePagiWithFilterComponent + }, + { + path: 'table/table-details-pagination', + component: TableDetailsPaginationComponent + }, + { path: 'table/table-sort-details', component: TableSortDetailsComponent }, + { + path: 'table/table-cols-toggle-details', + component: TableColsToggleDetailsComponent + }, + { + path: 'table/table-colsresizable-toggle', + component: TableColsresizableColstoggleComponent + }, + { + path: 'table/table-colsresizable-loadfail', + component: TableColsresizableLoadfailComponent + }, + { + path: 'table/table-colsresizable-sort-headfilter', + component: TableColsresizableSortHeadfilterComponent + }, + { + path: 'table/table-col-align-sort-resizable', + component: TableColalignSortResizableTestComponent + }, + { + path: 'table/table-fixed-head-pagination-details', + component: TableFixedHeadPaginationDetailsComponent + }, + { + path: 'table/table-fixed-head-accordion', + component: TableFixedHeadInAccordionComponent + }, + { + path: 'table/table-fixed-head-nodata', + component: TableFixedHeadNodataComponent + }, + { + path: 'table/table-fixed-head-cols-resizable', + component: TableFixedHeadColsResizableComponent + }, + { + path: 'table/table-fixed-head/cols-resizable-pagination-details', + component: TableFixedheadColsresizablePaginationDetailsComponent + }, + { + path: 'table/table-columnfixed-nodata', + component: TableColumnfixedNodataComponent + }, + { + path: 'table/table-columnfixed-pagination', + component: TableColumnfixedPaginationComponent + }, + { + path: 'table/table-columnfixed-fixedhead-colsresizable-pagination', + component: TableColumnfixedFixedheadColsresizablePaginationComponent + }, + { + path: 'table/table-columnfixed-leftmenu', + component: TableColumnfixedLeftmenuComponent + }, + { + path: 'table/table-columnfixed-editrow', + component: TableColumnfixedEditrowComponent + }, + { + path: 'table/table-fixhead-scroll', + component: TableFixheadScrollComponent + }, + { + path: 'table/table-comprehensive', + component: TableComprehensiveComponent + }, + { + path: 'table/table-virtualscroll-sizes', + component: TableVirtualscrollSizesComponent + }, + { + path: 'table/table-overflow-link', + component: TableOverflowLinkComponent + }, + { + path: 'table/table-sort-test', + component: TableSortTestComponent + }, + { + path: 'table/table-basic-test', + component: TableBasicTestComponent + }, + { + path: 'table/table-nodata-test', + component: TableNodataTestComponent + }, + { + path: 'table/table-sort', + component: TableSortComponent + }, + { + path: 'table/table-sort-reset', + component: TableSortResetComponent + }, + { + path: 'table/table-search', + component: TableSearchComponent + }, + { + path: 'table/table-head-filter', + component: TableHeadFilterComponent + }, + { + path: 'table/table-head-filter-datetime', + component: TableHeadFilterDatetimeComponent + }, + { + path: 'table/table-radio', + component: TableRadioComponent + }, + { + path: 'table/table-cols-toggle', + component: TableColsToggleComponent + }, + { + path: 'table/table-cols-resizable', + component: TableColsResizableComponent + }, + { + path: 'table/table-editrow', + component: TableEditrowComponent + }, + { + path: 'table/table-editall', + component: TableEditallComponent + }, + { + path: 'table/table-virtualscroll', + component: TableVirtualscrollComponent + } + ]; +} diff --git a/src/table/demo/src/app/table/TableTreeComponent.ts b/src/table/demo/src/app/table/TableTreeComponent.ts new file mode 100644 index 0000000..8452b6b --- /dev/null +++ b/src/table/demo/src/app/table/TableTreeComponent.ts @@ -0,0 +1,310 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-tree.html' +}) +export class TableTreeComponent { + displayedData: Array = []; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'Size' + }, + { + title: 'Type' + } + ]; + srcData: TiTableSrcData = { + data: [ + { + name: 'Documents', + type: 'Folder', + size: '-', + showSub: true, + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + showSub: true, + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB', + show: false + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB', + show: false + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB', + show: false + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-' + } + ] + }, + { + name: 'Documents', + type: 'Folder', + size: '-' + }, + { + name: 'Documents', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + } + ] + }, + { + name: 'Documents', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + } + ] + }, + { + name: 'Documents', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + } + ] + } + ], + state: undefined + }; +} diff --git a/src/table/demo/src/app/table/TableTreeMulitiselectComponent.ts b/src/table/demo/src/app/table/TableTreeMulitiselectComponent.ts new file mode 100644 index 0000000..e68c8bc --- /dev/null +++ b/src/table/demo/src/app/table/TableTreeMulitiselectComponent.ts @@ -0,0 +1,356 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData, Util } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-tree-mulitiselect.html' +}) +export class TableTreeMulitiselectComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + checkedList: Array = []; // 默认选中项 + youCheckedList: Array = []; // 默认选中项 + sonCheckedList: Array = []; // 默认选中项 + + data: Array = [ + { + name: 'Documents', + type: 'Folder', + size: '-', + showSub: true, + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + showSub: true, + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB', + show: false + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB', + show: false + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB', + show: false + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-' + } + ] + }, + { + name: 'Documents', + type: 'Folder', + size: '-' + }, + { + name: 'Documents', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + } + ] + }, + { + name: 'Documents', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + } + ] + }, + { + name: 'Documents', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + } + ] + } + ]; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'Size' + }, + { + title: 'Type' + } + ]; + + // 设置各层级复选组绑定items数据 + private static getCheckgroupItems(treeData: Array, items: Array, parentData?: Array): any { + if (treeData) { + for (const data of treeData) { + if (data.subData) { + this.getCheckgroupItems(data.subData, items); + } else { + items.push(data); + } + } + } else { + items.push(parentData); + } + + return items; + } + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + onMyChange(checkedList: Array): void { + console.log(checkedList); + } + + // 获取数据 + getItems(data: Array, parentData?: Array): Array { + const items: Array = []; + + return TableTreeMulitiselectComponent.getCheckgroupItems(data, items, parentData); + } +} diff --git a/src/table/demo/src/app/table/TableTreeUnknowdeepthComponent.ts b/src/table/demo/src/app/table/TableTreeUnknowdeepthComponent.ts new file mode 100644 index 0000000..4259f94 --- /dev/null +++ b/src/table/demo/src/app/table/TableTreeUnknowdeepthComponent.ts @@ -0,0 +1,151 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-tree-unknowdeepth.html' +}) +export class TableTreeUnknowdeepthComponent implements OnInit { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [], + state: undefined + }; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'Size' + }, + { + title: 'Type' + } + ]; + treeData: Array = [ + { + name: 'tree_0', + size: 'tree_0_size', + type: 'tree_0_type', + expand: true, + children: [ + { + name: 'tree_0_0', + size: 'tree_0_0_size', + type: 'tree_0_0_type', + expand: true, + children: [ + { + name: 'tree_0_0_0', + size: 'tree_0_0_0_size', + type: 'tree_0_0_0_type' + }, + { + name: 'tree_0_0_1', + size: 'tree_0_0_1_size', + type: 'tree_0_0_1_type', + children: [ + { + name: 'tree_0_0_1_0', + size: 'tree_0_0_1_0_size', + type: 'tree_0_0_1_0_type' + }, + { + name: 'tree_0_0_1_1', + size: 'tree_0_0_1_1_size', + type: 'tree_0_0_1_1_type' + } + ] + } + ] + }, + { + name: 'tree_0_1', + size: 'tree_0_1_size', + type: 'tree_0_1_type', + expand: true, + children: [ + { + name: 'tree_0_1_0', + size: 'tree_0_1_0_size', + type: 'tree_0_1_0_type' + } + ] + }, + { + name: 'tree_0_2', + size: 'tree_0_2_size', + type: 'tree_0_2_type' + } + ] + }, + { + name: 'tree_1', + size: 'tree_1_size', + type: 'tree_1_type' + }, + { + name: 'tree_2', + size: 'tree_2_size', + type: 'tree_2_type' + }, + { + name: 'tree_3', + size: 'tree_3_size', + type: 'tree_3_type', + children: [ + { + name: 'tree_3_0', + size: 'tree_3_0_size', + type: 'tree_3_0_type' + }, + { + name: 'tree_3_1', + size: 'tree_3_1_size', + type: 'tree_3_1_type' + } + ] + }, + { + name: 'tree_4', + size: 'tree_4_size', + type: 'tree_4_type' + } + ]; + + ngOnInit(): void { + this.srcData.data = this.getFlatData(this.treeData); + } + + toggle(node: TiTableRowData): void { + node.expand = !node.expand; + this.srcData.data = this.getFlatData(this.treeData); + } + + getLevelStyle(node: TiTableRowData): { 'padding-left': string } { + return { + 'padding-left': `${node.level * 24 + 10}px` // 图标16px + 间距8px = 24px + }; + } + + /** + * 对树形结构数据做扁平化处理 + * @param nodes 同层级且隶属同一父节点的节点集合 + * @param level 节点层级,根节点层级为0,往下依次类推 + */ + private getFlatData(nodes: Array, level?: number): Array { + let result: Array = []; + if (!nodes) { + return result; + } + nodes.forEach((item: TiTableRowData, index: number): void => { + item.level = level ? level : 0; + item.hasChildren = item.children && item.children.length > 0; + result.push(item); + if (item.expand && item.hasChildren) { + result = result.concat(this.getFlatData(item.children, item.level + 1)); + } + }); + + return result; + } +} diff --git a/src/table/demo/src/app/table/TableVirtualscrollBasicComponent.ts b/src/table/demo/src/app/table/TableVirtualscrollBasicComponent.ts new file mode 100644 index 0000000..2816b74 --- /dev/null +++ b/src/table/demo/src/app/table/TableVirtualscrollBasicComponent.ts @@ -0,0 +1,88 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-virtualscroll-basic.html' +}) +export class TableVirtualscrollBasicComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + noDadaInfo: string = '暂无表格数据'; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.getData(20000), // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + changeEmpty(): void { + this.srcData.data = []; + } + changeMany(): void { + this.srcData.data = this.getData(20000); + } + changeFew(): void { + this.srcData.data = this.getData(5); + } + + private getData(total: number): Array { + const data: Array = []; + for (let j: number = 0; j < total; j++) { + data.push(this.createRandomItem(j)); + } + + return data; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 5]; + const lastName: string = familyName[((id + 3) * 29) % 5]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableVirtualscrollComponent.ts b/src/table/demo/src/app/table/TableVirtualscrollComponent.ts new file mode 100644 index 0000000..6891d06 --- /dev/null +++ b/src/table/demo/src/app/table/TableVirtualscrollComponent.ts @@ -0,0 +1,64 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-virtualscroll.html' +}) +export class TableVirtualscrollComponent implements OnInit { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [], + state: undefined + }; + columns: Array = [ + { + title: 'Id', + width: '10%' + }, + { + title: 'First Name', + width: '20%' + }, + { + title: 'Last Name', + width: '20%' + }, + { + title: 'Age', + width: '10%' + }, + { + title: 'Email Address', + width: '40%' + } + ]; + + ngOnInit(): void { + const data: Array = []; + for (let i: number = 0; i < 1000; i++) { + data.push(this.createRandomItem(i)); + } + this.srcData.data = data; + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'Bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + + return { + firstName, + lastName, + age, + email, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableVirtualscrollComprehensiveComponent.ts b/src/table/demo/src/app/table/TableVirtualscrollComprehensiveComponent.ts new file mode 100644 index 0000000..6682d0d --- /dev/null +++ b/src/table/demo/src/app/table/TableVirtualscrollComprehensiveComponent.ts @@ -0,0 +1,139 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-virtualscroll-comprehensive.html' +}) +export class TableVirtualscrollComprehensiveComponent implements OnInit { + selectAll: boolean = true; + panelWidth: string = '250px'; + searchable: boolean = true; // 可切换测试 + noDataText: string = '无数据'; + checkedList: Array = []; // 默认选中项 + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + currentPage: number = 1; + totalNumber: number = 16112; + pageSize: { options: Array; size: number } = { + options: [50, 120, 260, 500, 1000], + size: 120 + }; + searchWords: Array = ['']; + searchKeys: Array = ['lastName']; // 设置过滤字段 + columns: Array = [ + { + title: 'first name', + fixed: 'left', + sortKey: 'firstName' + }, + { + title: 'last name', + fixed: 'left', + key: 'lastName', + selected: null, + options: [ + { + label: 'all' + }, + { + label: 'Dupont' + }, + { + label: 'Germain' + }, + { + label: 'Delcourt' + }, + { + label: 'bjip' + }, + { + label: 'Menez' + } + ] + }, + { + title: 'birth date', + show: true + }, + { + title: 'balance', + show: true + }, + { + title: 'email', + show: false + }, + { + title: 'address', + show: true + }, + { + title: 'phone number', + show: true + }, + { + title: 'parents', + show: true + }, + { + title: 'school', + fixed: 'right' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + onSelect(item: any, column: TiTableColumns): void { + const index: number = this.searchKeys.indexOf(column.key); + const labelKey: string = column.labelKey || 'label'; + this.searchWords[index] = item[labelKey] === 'all' ? '' : item[labelKey]; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 5]; + const lastName: string = familyName[((id + 3) * 29) % 5]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const address: string = "Xi'an"; + const phone: string = '123 4567 8900'; + const parents: string = 'father and mother'; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + address, + phone, + parents, + school, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableVirtualscrollSizesComponent.ts b/src/table/demo/src/app/table/TableVirtualscrollSizesComponent.ts new file mode 100644 index 0000000..2cad7c3 --- /dev/null +++ b/src/table/demo/src/app/table/TableVirtualscrollSizesComponent.ts @@ -0,0 +1,88 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-virtualscroll-sizes.html' +}) +export class TableVirtualscrollSizesComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + noDadaInfo: string = '暂无表格数据'; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据,此处测试初始化无数据+虚拟滚动 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + changeEmpty(): void { + this.srcData.data = []; + } + changeMany(): void { + this.srcData.data = this.getData(20000); + } + changeFew(): void { + this.srcData.data = this.getData(3); + } + + private getData(total: number): Array { + const data: Array = []; + for (let j: number = 0; j < total; j++) { + data.push(this.createRandomItem(j)); + } + + return data; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableVirtualscrollTreeComponent.ts b/src/table/demo/src/app/table/TableVirtualscrollTreeComponent.ts new file mode 100644 index 0000000..70dcffe --- /dev/null +++ b/src/table/demo/src/app/table/TableVirtualscrollTreeComponent.ts @@ -0,0 +1,308 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-virtualscroll-tree.html' +}) +export class TableVirtualscrollTreeComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + noDadaInfo: string = '暂无表格数据'; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'Size' + }, + { + title: 'Type' + } + ]; + treeData: Array = [ + { + name: 'tree_0', + size: 'tree_0_size', + type: 'tree_0_type', + expand: true, + children: [ + { + name: 'tree_0_0', + size: 'tree_0_0_size', + type: 'tree_0_0_type', + expand: true, + children: [ + { + name: 'tree_0_0_0', + size: 'tree_0_0_0_size', + type: 'tree_0_0_0_type' + }, + { + name: 'tree_0_0_1', + size: 'tree_0_0_1_size', + type: 'tree_0_0_1_type' + } + ] + }, + { + name: 'tree_0_1', + size: 'tree_0_1_size', + type: 'tree_0_1_type', + expand: true, + children: [ + { + name: 'tree_0_1_0', + size: 'tree_0_1_0_size', + type: 'tree_0_1_0_type' + } + ] + }, + { + name: 'tree_0_2', + size: 'tree_0_2_size', + type: 'tree_0_2_type' + } + ] + }, + { + name: 'tree_1', + size: 'tree_1_size', + type: 'tree_1_type' + }, + { + name: 'tree_2', + size: 'tree_2_size', + type: 'tree_2_type' + }, + { + name: 'tree_3', + size: 'tree_3_size', + type: 'tree_3_type', + expand: true, + children: [ + { + name: 'tree_3_0', + size: 'tree_3_0_size', + type: 'tree_3_0_type' + }, + { + name: 'tree_3_1', + size: 'tree_3_1_size', + type: 'tree_3_1_type' + } + ] + }, + { + name: 'tree_4', + size: 'tree_4_size', + type: 'tree_4_type' + }, + { + name: 'tree_5', + size: 'tree_5_size', + type: 'tree_5_type', + expand: true, + children: [ + { + name: 'tree_5_0', + size: 'tree_5_0_size', + type: 'tree_5_0_type', + expand: true, + children: [ + { + name: 'tree_5_0_0', + size: 'tree_5_0_0_size', + type: 'tree_5_0_0_type' + }, + { + name: 'tree_5_0_1', + size: 'tree_5_0_1_size', + type: 'tree_5_0_1_type' + } + ] + }, + { + name: 'tree_5_1', + size: 'tree_5_1_size', + type: 'tree_5_1_type', + expand: true, + children: [ + { + name: 'tree_5_1_0', + size: 'tree_5_1_0_size', + type: 'tree_5_1_0_type' + } + ] + }, + { + name: 'tree_5_2', + size: 'tree_5_2_size', + type: 'tree_5_2_type' + } + ] + }, + { + name: 'tree_6', + size: 'tree_6_size', + type: 'tree_6_type' + }, + { + name: 'tree_7', + size: 'tree_7_size', + type: 'tree_7_type' + }, + { + name: 'tree_8', + size: 'tree_8_size', + type: 'tree_8_type', + expand: true, + children: [ + { + name: 'tree_8_0', + size: 'tree_8_0_size', + type: 'tree_8_0_type' + }, + { + name: 'tree_8_1', + size: 'tree_8_1_size', + type: 'tree_8_1_type' + } + ] + }, + { + name: 'tree_9', + size: 'tree_9_size', + type: 'tree_9_type' + }, + { + name: 'tree_10', + size: 'tree_10_size', + type: 'tree_10_type', + expand: true, + children: [ + { + name: 'tree_10_0', + size: 'tree_10_0_size', + type: 'tree_10_0_type', + expand: true, + children: [ + { + name: 'tree_10_0_0', + size: 'tree_10_0_0_size', + type: 'tree_10_0_0_type' + }, + { + name: 'tree_10_0_1', + size: 'tree_10_0_1_size', + type: 'tree_10_0_1_type' + } + ] + }, + { + name: 'tree_10_1', + size: 'tree_10_1_size', + type: 'tree_10_1_type', + expand: true, + children: [ + { + name: 'tree_10_1_0', + size: 'tree_10_1_0_size', + type: 'tree_10_1_0_type' + } + ] + }, + { + name: 'tree_10_2', + size: 'tree_10_2_size', + type: 'tree_10_2_type' + } + ] + }, + { + name: 'tree_11', + size: 'tree_11_size', + type: 'tree_11_type' + }, + { + name: 'tree_12', + size: 'tree_12_size', + type: 'tree_12_type' + }, + { + name: 'tree_13', + size: 'tree_13_size', + type: 'tree_13_type', + expand: true, + children: [ + { + name: 'tree_13_0', + size: 'tree_13_0_size', + type: 'tree_13_0_type' + }, + { + name: 'tree_13_1', + size: 'tree_13_1_size', + type: 'tree_13_1_type' + } + ] + }, + { + name: 'tree_14', + size: 'tree_14_size', + type: 'tree_14_type' + } + ]; + + ngOnInit(): void { + this.srcData = { + data: this.getFlatData(this.treeData), // 源数据 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + toggle(node: TiTableRowData): void { + node.expand = !node.expand; + this.srcData.data = this.getFlatData(this.treeData); + } + + getLevelStyle(node: TiTableRowData): { 'padding-left': string } { + return { + 'padding-left': `${node.level * 24 + 10}px` // 图标16px + 间距8px = 24px + }; + } + + changeEmpty(): void { + this.srcData.data = []; + } + + changeMany(): void { + this.srcData.data = this.getFlatData(this.treeData); + } + + /** + * 对树形结构数据做扁平化处理 + * @param nodes 同层级且隶属同一父节点的节点集合 + * @param level 节点层级,根节点层级为0,往下依次类推 + */ + private getFlatData(nodes: Array, level?: number): Array { + let result: Array = []; + if (!nodes) { + return result; + } + nodes.forEach((item: TiTableRowData, index: number): void => { + item.level = level ? level : 0; + item.hasChildren = item.children && item.children.length > 0; + result.push(item); + if (item.expand && item.hasChildren) { + result = result.concat(this.getFlatData(item.children, item.level + 1)); + } + }); + + return result; + } +} diff --git a/src/table/demo/src/app/table/table-actionmenu.html b/src/table/demo/src/app/table/table-actionmenu.html new file mode 100644 index 0000000..5a3b840 --- /dev/null +++ b/src/table/demo/src/app/table/table-actionmenu.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.name}}{{row.createTime | date: 'yyyy-MM-dd'}}{{row.operator}}{{row.state}} + +
    +
    diff --git a/src/table/demo/src/app/table/table-basic-test.html b/src/table/demo/src/app/table/table-basic-test.html new file mode 100644 index 0000000..cac0ebd --- /dev/null +++ b/src/table/demo/src/app/table/table-basic-test.html @@ -0,0 +1,43 @@ +
    +

    描述

    +

    + 表格的基本使用
    + 使用前先了解一下表格的主要功能和数据流。表格组件的功能是把传入的srcData 通过一系列的处理,
    + 输出到displayedData中,再用displayedData的数据显示出来。
    + 表格的数据流是 srcData =>(根据srcData.state状态转化)=>displayedData,
    + 一系列的转化包括分页、排序、搜索。所以srcData.data 中的数据变化, 以及srcData.state 中的分页、排序、搜索状态变化,都会触发一次 + srcData.data =>(srcData.state)=>displayedData的转化。
    + 用户只需输入源数据srcData.data 以及初始化状态srcData.state, 使用输出数据displayedData 展示成表格。 +

    + +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    导入

    +

    import {{ '{' }} TiTableModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-basic.html b/src/table/demo/src/app/table/table-basic.html new file mode 100644 index 0000000..25343d2 --- /dev/null +++ b/src/table/demo/src/app/table/table-basic.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-cell-tip.html b/src/table/demo/src/app/table/table-cell-tip.html new file mode 100644 index 0000000..d66501a --- /dev/null +++ b/src/table/demo/src/app/table/table-cell-tip.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}
    {{row.name}}{{row.age}}{{row.email}}{{row.address}}
    +
    diff --git a/src/table/demo/src/app/table/table-cellicons-colsresizable.html b/src/table/demo/src/app/table/table-cellicons-colsresizable.html new file mode 100644 index 0000000..2a761d2 --- /dev/null +++ b/src/table/demo/src/app/table/table-cellicons-colsresizable.html @@ -0,0 +1,62 @@ +
    +

    描述

    +

    当表格有列拖动功能且单元格文本后有可操作图标时,在拖动列宽变小内容显示不下时,需要使文本出'...', 而图标显示完全。

    +

    需要使用tiCellText组件包裹单元格中的文本,使用tiCellIcons组件包裹图标

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    +

    表格单元格中有图标 + 列拖动

    + +
    + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} + + + + + + {{columns[1].title}} + + + + + + {{columns[2].title}} + + + {{columns[3].title}} + + {{columns[4].title}}
    + {{row.firstName}} + + + + {{row.lastName}}{{row.age}} + {{row.balance}} + + + + {{row.email}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-checkbox-pagination-headmenu.html b/src/table/demo/src/app/table/table-checkbox-pagination-headmenu.html new file mode 100644 index 0000000..17d5e3e --- /dev/null +++ b/src/table/demo/src/app/table/table-checkbox-pagination-headmenu.html @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + +
    + + + {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}{{columns[5].title}}
    + + {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + + + {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}{{columns[5].title}}
    + + {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    + +
    diff --git a/src/table/demo/src/app/table/table-checkbox-pagination.html b/src/table/demo/src/app/table/table-checkbox-pagination.html new file mode 100644 index 0000000..0d3c25e --- /dev/null +++ b/src/table/demo/src/app/table/table-checkbox-pagination.html @@ -0,0 +1,87 @@ +

    描述

    +

    表格复选 + 分页

    +

    1. 表格复选功能需要配合tiCheckgroup组件来实现,具体可参考tiCheckgroup组件的使用说明。

    +

    2. 按UI规范,全选复选框控制选中/取消选中当前页。

    +

    3. 某页选中一些行,跳转到其他页,再次返回到之前页,之前选中的行仍然是选中的。

    +

    导入

    +import {{ '{' }} TiTableModule, TiCheckboxModule, TiOverflowModule {{ '}' }} from '@opentiny/ng'; +

    示例

    + +

    1.复选 + 前台分页

    +
    + +
    checkedList:{{checkedList|json}}
    + + + + + + + + + + + + + + + + + + + + + + + +
    + + + {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + {{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    + +
    +
    +

    2.复选 + 后台分页

    +
    +

    注意 tiCheckgroup 上 valueKey 接口的使用

    + +
    checkedList1:{{checkedList1 | json}}
    +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + {{columns1[1].title}}{{columns1[2].title}}{{columns1[3].title}}{{columns1[4].title}}
    + + {{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    + +
    diff --git a/src/table/demo/src/app/table/table-checkbox.html b/src/table/demo/src/app/table/table-checkbox.html new file mode 100644 index 0000000..2771977 --- /dev/null +++ b/src/table/demo/src/app/table/table-checkbox.html @@ -0,0 +1,27 @@ +

    Selected {{ checkedList.length }} rows.

    + + + + + + + + + + + + + + + + + + + + +
    + + {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-col-align.html b/src/table/demo/src/app/table/table-col-align.html new file mode 100644 index 0000000..5aadf10 --- /dev/null +++ b/src/table/demo/src/app/table/table-col-align.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-colalign-sort-resizable-test.html b/src/table/demo/src/app/table/table-colalign-sort-resizable-test.html new file mode 100644 index 0000000..4b9d2d7 --- /dev/null +++ b/src/table/demo/src/app/table/table-colalign-sort-resizable-test.html @@ -0,0 +1,46 @@ +
    +

    描述

    +

    表格列对齐方式、排序、列拖动

    +

    示例

    + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} + + + {{columns[1].title}} + + + {{columns[2].title}} + + + {{columns[3].title}} + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-cols-resizable.html b/src/table/demo/src/app/table/table-cols-resizable.html new file mode 100644 index 0000000..ad7f57d --- /dev/null +++ b/src/table/demo/src/app/table/table-cols-resizable.html @@ -0,0 +1,34 @@ + +
    + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}} + {{columns[3].title}} + + + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.address}} + {{row.email}} + + + + {{row.age}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-cols-toggle-details.html b/src/table/demo/src/app/table/table-cols-toggle-details.html new file mode 100644 index 0000000..58f257f --- /dev/null +++ b/src/table/demo/src/app/table/table-cols-toggle-details.html @@ -0,0 +1,52 @@ +
    +

    描述

    +

    表格详情/列隐藏显示 + 详情展开

    +

    示例

    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}{{columns[5].title}}
    + + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    +
    +
    详情展示:{{row.firstName}}
    +
    详情展示:{{row.balance}}
    +
    详情展示:{{row.lastName}}
    +
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-cols-toggle-test.html b/src/table/demo/src/app/table/table-cols-toggle-test.html new file mode 100644 index 0000000..61c3a95 --- /dev/null +++ b/src/table/demo/src/app/table/table-cols-toggle-test.html @@ -0,0 +1,55 @@ +

    描述

    +

    表格列动态显示/隐藏功能需要配合tiColsToggle组件来实现,具体可参考tiColsToggle组件的使用说明。

    +

    + tiColsToggle组件需要放在tiTable标签内部,具体布局位置由开发者来控制;此功能与columns的中的show属性 + 设置密切相关,需要使用ngIf利用show的值来控制需要动态显示/隐藏的列;columns的相关配置可参考tiTable组 件的columns接口说明。 +

    +

    + 另外:selectAll- 设置是否有全选项,默认false
    + disabled- 设置禁用
    + searchable-设置是否有搜索,默认true +

    +

    列隐藏/显示按钮悬浮时有默认提示文本和方向,服务也可自行配置。

    +

    注意:使用 tiTip 组件,请导入 TiTipModule;使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + + + +
    + + + +
    + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-cols-toggle.html b/src/table/demo/src/app/table/table-cols-toggle.html new file mode 100644 index 0000000..31bd3af --- /dev/null +++ b/src/table/demo/src/app/table/table-cols-toggle.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-colsresizable-basic.html b/src/table/demo/src/app/table/table-colsresizable-basic.html new file mode 100644 index 0000000..6cfdda0 --- /dev/null +++ b/src/table/demo/src/app/table/table-colsresizable-basic.html @@ -0,0 +1,36 @@ +

    描述

    +

    + 要开启列拖动功能时,需要在ti-table标签上使用tiColsResizable指令,并且需要给table一个带有 + ti3-resize-wrapper样式类的父容器。如果要禁止某一列的拖动,则可以给当前列表头的th标签上添加 not-resizable 属性 +

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-colsresizable-colstoggle-fixedhead.html b/src/table/demo/src/app/table/table-colsresizable-colstoggle-fixedhead.html new file mode 100644 index 0000000..e38f5e5 --- /dev/null +++ b/src/table/demo/src/app/table/table-colsresizable-colstoggle-fixedhead.html @@ -0,0 +1,52 @@ +
    +

    描述

    +

    列拖动特性 + 列动态显示/隐藏特性 + 表头锁定特性

    +

    示例

    + +
    + + +
    + + + +
    + + + + + + + + + +
    {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-colsresizable-colstoggle.html b/src/table/demo/src/app/table/table-colsresizable-colstoggle.html new file mode 100644 index 0000000..ca33e7c --- /dev/null +++ b/src/table/demo/src/app/table/table-colsresizable-colstoggle.html @@ -0,0 +1,36 @@ +
    +

    描述

    +

    表格列拖动、列隐藏/显示

    +

    示例

    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-colsresizable-loadfail.html b/src/table/demo/src/app/table/table-colsresizable-loadfail.html new file mode 100644 index 0000000..b577e3d --- /dev/null +++ b/src/table/demo/src/app/table/table-colsresizable-loadfail.html @@ -0,0 +1,34 @@ +
    +

    描述

    +

    表格列拖动特性 + 数据加载失败特性

    +

    示例

    + +
    + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + {{failLoadInfo}} + {{reloadInfo}} +
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-colsresizable-sort-headfilter.html b/src/table/demo/src/app/table/table-colsresizable-sort-headfilter.html new file mode 100644 index 0000000..1ac5972 --- /dev/null +++ b/src/table/demo/src/app/table/table-colsresizable-sort-headfilter.html @@ -0,0 +1,67 @@ +
    +

    描述

    +

    表格列拖动特性 + 排序特性 + 表头过滤

    +

    用户可通过panelAlign接口控制表头过滤下拉面板展开方向,详见用例

    +

    示例

    + + +
    + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} + + + + + {{columns[1].title}} + + + + + {{columns[2].title}} + + + {{columns[3].title}} + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-colsresizable-sort.html b/src/table/demo/src/app/table/table-colsresizable-sort.html new file mode 100644 index 0000000..2ec6978 --- /dev/null +++ b/src/table/demo/src/app/table/table-colsresizable-sort.html @@ -0,0 +1,43 @@ +

    描述

    +

    表格列拖动特性 + 排序特性

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + +
    + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} + + + {{columns[1].title}} + + + {{columns[2].title}} + + + {{columns[3].title}} + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-column-fixed.html b/src/table/demo/src/app/table/table-column-fixed.html new file mode 100644 index 0000000..35d6d34 --- /dev/null +++ b/src/table/demo/src/app/table/table-column-fixed.html @@ -0,0 +1,22 @@ + +
    + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}{{row.address}}action
    +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-checkbox.html b/src/table/demo/src/app/table/table-columnfixed-checkbox.html new file mode 100644 index 0000000..f4a32f3 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-checkbox.html @@ -0,0 +1,41 @@ +
    +

    描述

    +

    表格复选列固定

    +

    示例

    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + {{column.title}}
    + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.address}}{{row.phone}}{{row.parents}}{{row.school}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-colstoggle.html b/src/table/demo/src/app/table/table-columnfixed-colstoggle.html new file mode 100644 index 0000000..8dee616 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-colstoggle.html @@ -0,0 +1,49 @@ +
    +

    描述

    +

    列固定 + 列动态显示/隐藏

    + +
    + + + +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    + {{column.title}} +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.address}}{{row.phone}}{{row.parents}}{{row.school}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-editrow.html b/src/table/demo/src/app/table/table-columnfixed-editrow.html new file mode 100644 index 0000000..9c40b71 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-editrow.html @@ -0,0 +1,95 @@ +

    描述

    +

    表格列固定+行编辑示例场景

    + +

    示例

    + + +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    + + + + + + + + + + + + + + +
    {{row.sourceName}}{{row.level}}{{row.balance}}{{row.unsubscribe}}{{row.createTime| date: "yyyy-MM-dd"}}{{row.operator}} + +
    {{row.sourceName}} + + + + + + + + + + + + +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-fixedhead-colsresizable-pagination.html b/src/table/demo/src/app/table/table-columnfixed-fixedhead-colsresizable-pagination.html new file mode 100644 index 0000000..e682af9 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-fixedhead-colsresizable-pagination.html @@ -0,0 +1,44 @@ +
    +

    描述

    +

    表格列固定 + 表头锁定特性 + 列拖动特性 + 分页特性

    +

    注意:使用 pagination 组件,请导入 TiPaginationModule

    + + + +
    + + + + + + +
    {{column.title}}
    +
    + + + +
    + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.address}}{{row.phone}}{{row.parents}}{{row.school}}
    +
    + +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-headfixed.html b/src/table/demo/src/app/table/table-columnfixed-headfixed.html new file mode 100644 index 0000000..62ddf13 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-headfixed.html @@ -0,0 +1,31 @@ + +
    + + + + + + +
    {{column.title}}
    +
    +
    + + + + + + + + + + + + + + + + + +
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}{{row.address}}action
    +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-leftmenu.html b/src/table/demo/src/app/table/table-columnfixed-leftmenu.html new file mode 100644 index 0000000..f57f9d1 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-leftmenu.html @@ -0,0 +1,54 @@ +

    描述

    +

    测试固定列和leftmenu

    +

    示例

    + + + +
    {{headLabel}}
    +
    + + + +
    {{m1.label}}
    +
    + + {{m2.label}} + +
    +
    + + +
    +

    描述

    +

    表格的列固定 + leftmenu, 主要测试leftmenu向左展开/收起时, 表格列固定是否正常

    +

    示例

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.address}}{{row.phone}}{{row.parents}}{{row.school}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-nodata.html b/src/table/demo/src/app/table/table-columnfixed-nodata.html new file mode 100644 index 0000000..9ad668a --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-nodata.html @@ -0,0 +1,35 @@ +
    +

    描述

    +

    表格无数据 + 列固定

    +

    示例

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.address}}{{row.phone}}{{row.parents}}{{row.email}}{{row.school}}
    {{noDadaInfo}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-pagination.html b/src/table/demo/src/app/table/table-columnfixed-pagination.html new file mode 100644 index 0000000..9de0480 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-pagination.html @@ -0,0 +1,33 @@ +
    +

    描述

    +

    表格的列固定 + 分页, 主要测试操作分页表格数据变动时, 表格列固定是否正常

    +

    示例

    + + +
    + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.address}}{{row.phone}}{{row.parents}}{{row.school}}
    +
    + + + +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-resizable.html b/src/table/demo/src/app/table/table-columnfixed-resizable.html new file mode 100644 index 0000000..d51d189 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-resizable.html @@ -0,0 +1,37 @@ +
    +

    描述

    +

    表格列固定 + 列拖动特性

    +

    表格的列固定功能。使用 tiColumnFixed 指令和带有ti3-table-container 这个样式类的容器配合实现。

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + + +
    + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.school}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-comprehensive.html b/src/table/demo/src/app/table/table-comprehensive.html new file mode 100644 index 0000000..01c0080 --- /dev/null +++ b/src/table/demo/src/app/table/table-comprehensive.html @@ -0,0 +1,82 @@ +
    +

    描述

    +

    综合示例,表格复选 + 列动态显示/隐藏 + 列固定 + 表头锁定特性 + 列拖动特性 + 分页特性

    +

    注意:使用 pagination 组件,请导入 TiPaginationModule

    + + +
    + + + +
    + + +
    + + + + + + + + + +
    + + + + + {{column.title}} +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.address}}{{row.phone}}{{row.parents}}{{row.school}}
    +
    + +
    +
    diff --git a/src/table/demo/src/app/table/table-details-closeotherdetails.html b/src/table/demo/src/app/table/table-details-closeotherdetails.html new file mode 100644 index 0000000..a9ad345 --- /dev/null +++ b/src/table/demo/src/app/table/table-details-closeotherdetails.html @@ -0,0 +1,51 @@ +
    +

    描述

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    表格点击展开某一行的详情,需要收起其它行的详情时,需要将closeOtherDetails接口设为true,该接口默认为false。

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}{{columns[5].title}}
    + + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    +
    +
    详情展示:{{row.firstName}}
    +
    详情展示:{{row.balance}}
    +
    详情展示:{{row.lastName}}
    +
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-details-nesttable.html b/src/table/demo/src/app/table/table-details-nesttable.html new file mode 100644 index 0000000..3875fea --- /dev/null +++ b/src/table/demo/src/app/table/table-details-nesttable.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    + +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-details-pagination.html b/src/table/demo/src/app/table/table-details-pagination.html new file mode 100644 index 0000000..c6ade71 --- /dev/null +++ b/src/table/demo/src/app/table/table-details-pagination.html @@ -0,0 +1,52 @@ +
    +

    描述

    +

    表格详情展开特性 + 前台分页特性

    +

    示例

    +

    注意:使用 pagination 组件,请导入 TiPaginationModule

    +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}{{columns[5].title}}
    + + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    + + +
    +
    详情展示:{{row.firstName}}
    +
    详情展示:{{row.balance}}
    +
    详情展示:{{row.lastName}}
    +
    +
    + +
    +
    diff --git a/src/table/demo/src/app/table/table-details.html b/src/table/demo/src/app/table/table-details.html new file mode 100644 index 0000000..21dc5c4 --- /dev/null +++ b/src/table/demo/src/app/table/table-details.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    +
    Name: {{row.firstName}} {{row.lastName}}
    +
    Email: {{row.email}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-dynamic-details.html b/src/table/demo/src/app/table/table-dynamic-details.html new file mode 100644 index 0000000..bced8a3 --- /dev/null +++ b/src/table/demo/src/app/table/table-dynamic-details.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    +

    {{row.info}}

    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-editall-test.html b/src/table/demo/src/app/table/table-editall-test.html new file mode 100644 index 0000000..8bf36ac --- /dev/null +++ b/src/table/demo/src/app/table/table-editall-test.html @@ -0,0 +1,60 @@ +

    描述

    +

    + 表格整体编辑场景示例 +
    +

    +
    +

    示例

    +
    + +
    +
    + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.sourceName}} + + + + + + + + + + + + +
    +
    +
    + + + + +

    {{allFormControl.errors.message}}

    + +
    + +
    + + diff --git a/src/table/demo/src/app/table/table-editall.html b/src/table/demo/src/app/table/table-editall.html new file mode 100644 index 0000000..cc9edee --- /dev/null +++ b/src/table/demo/src/app/table/table-editall.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    + + + + + + + + + +
    +
    + +
    +
    Current Table Data: {{ srcData.data | json }}
    +
    diff --git a/src/table/demo/src/app/table/table-editrow-test.html b/src/table/demo/src/app/table/table-editrow-test.html new file mode 100644 index 0000000..ca5d16c --- /dev/null +++ b/src/table/demo/src/app/table/table-editrow-test.html @@ -0,0 +1,87 @@ +

    描述

    +

    表格行编辑场景示例

    + +

    示例

    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    + + + + + + + + + + + + + + +
    {{row.sourceName}}{{row.level}}{{row.balance}}{{row.unsubscribe}}{{row.createTime| date: "yyyy-MM-dd"}}{{row.operator}} + +
    {{row.sourceName}} + + + + + + + + + + + + +
    +
    diff --git a/src/table/demo/src/app/table/table-editrow.html b/src/table/demo/src/app/table/table-editrow.html new file mode 100644 index 0000000..c67e217 --- /dev/null +++ b/src/table/demo/src/app/table/table-editrow.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.sourceName}}{{row.level}}{{row.unsubscribe}}{{row.createTime| date: "yyyy-MM-dd"}} + +
    {{row.sourceName}} + + + + + + + +
    +
    diff --git a/src/table/demo/src/app/table/table-filter-strict.html b/src/table/demo/src/app/table/table-filter-strict.html new file mode 100644 index 0000000..7d15ae2 --- /dev/null +++ b/src/table/demo/src/app/table/table-filter-strict.html @@ -0,0 +1,93 @@ +
    +

    描述

    +

    表格搜索利用tiTable组件的searchKeys和searchWords接口来实现。

    +

    默认为模糊匹配,可设置 searchStrictKeys 接口 来指定其中哪些字段是精确匹配的。

    +

    表格组件提供了简单的搜索功能,如果搜索条件复杂可自行处理搜索逻辑,将搜索后的数据直接传给 srcData.data 源数据接口。

    +

    示例

    +

    + 1.针对源数据在searchKeys指定的字段下搜索searchWords[0]指定的字符串, + 在指定字段中的任一字段搜索到时即满足条件(并集)。默认为模糊匹配,哪些字段是精确匹配取决于searchStrictKeys。 如:searchWords: ['po'], + searchKeys: ['firstName', 'lastName', 'age'], searchStrictKeys: ['firstName'], + 则或在firstName字段中等于(精确匹配)‘po’,或在lastName字段包含(模糊匹配)‘po’, 或在age字段包含(模糊匹配)‘po’时本行数据即满足搜索条件。 +

    + + + +
    +

    searchWords: {{oneWordSearch.searchWords[0]}}

    +

    searchKeys: {{oneWordSearch.searchKeys}}

    +

    searchStrictKeys: {{oneWordSearch.searchStrictKeys}}

    +
    + +

    搜索结果个数: {{ searchedData.length }}

    +
    + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    + +

    + 2.针对源数据在searchKeys指定的字段下搜索searchWords对应(按顺序)元素指定的字符串, + 在指定字段中的所有字段搜索到对应值时才满足条件(交集)。默认为模糊匹配,哪些字段是精确匹配取决于searchStrictKeys。 如:searchWords: ['b', + '39'], searchKeys: ['firstName', 'age'], searchStrictKeys: ['age'], 则在firstName字段包含(模糊匹配)‘b’且 + 在age字段等于(精确匹配)‘39’时本行数据才满足搜索条件。 +

    + + +
    +

    searchWords: {{moreThanOneWordSearch.searchWords[0]}},{{moreThanOneWordSearch.searchWords[1]}}

    +

    searchKeys: {{moreThanOneWordSearch.searchKeys}}

    +

    searchKeys: {{moreThanOneWordSearch.searchStrictKeys}}

    +
    + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-filter.html b/src/table/demo/src/app/table/table-filter.html new file mode 100644 index 0000000..aa56e34 --- /dev/null +++ b/src/table/demo/src/app/table/table-filter.html @@ -0,0 +1,85 @@ +
    +

    描述

    +

    表格搜索利用tiTable组件的searchKeys和searchWords接口来实现。

    +

    表格组件提供了简单的搜索功能,如果搜索条件复杂可自行处理搜索逻辑,将搜索后的数据直接传给srcData.data源数据接口。

    +

    示例

    +

    + 1.针对源数据在searchKeys指定的字段下搜索searchWords[0]指定的字符串, 在指定字段中的任一字段搜索到时即满足条件(并集)。如:searchWords: + ['b'], searchKeys: ['firstName', 'lastName'],则在firstName字段包含‘b’, 或在lastName字段包含‘b’时本行数据即满足搜索条件 +

    + + + +
    +

    searchWords: {{oneWordSearch.searchWords[0]}}

    +

    searchKeys: {{oneWordSearch.searchKeys}}

    +
    + +

    搜索结果个数: {{ searchedData.length }}

    +
    + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    + +

    + 2.针对源数据在searchKeys指定的字段下搜索searchWords对应(按顺序)元素指定的字符串, + 在指定字段中的所有字段搜索到对应值时才满足条件(交集)。如:searchWords: ['b', '18'], searchKeys: ['firstName', + 'age'],则在firstName字段包含‘b’且 在age字段包含‘18’时本行数据才满足搜索条件 +

    + + +
    +

    searchWords: {{moreThanOneWordSearch.searchWords[0]}},{{moreThanOneWordSearch.searchWords[1]}}

    +

    searchKeys: {{moreThanOneWordSearch.searchKeys}}

    +
    + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-fixed-head-cols-resizable.html b/src/table/demo/src/app/table/table-fixed-head-cols-resizable.html new file mode 100644 index 0000000..8bd1dac --- /dev/null +++ b/src/table/demo/src/app/table/table-fixed-head-cols-resizable.html @@ -0,0 +1,41 @@ +
    +

    描述

    +

    表格表头锁定特性 + 列拖动特性

    +

    示例

    + + + +
    + + + + + + + +
    {{column.title}}
    +
    +
    dasdf
    + + + +
    + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-fixed-head-in-accordion.html b/src/table/demo/src/app/table/table-fixed-head-in-accordion.html new file mode 100644 index 0000000..7565c3b --- /dev/null +++ b/src/table/demo/src/app/table/table-fixed-head-in-accordion.html @@ -0,0 +1,49 @@ +
    +

    描述

    +

    放在手风琴中的表格表头锁定特性

    +

    注意:使用 accordion 组件,请导入 TiAccordionModule

    +

    +

    示例

    + + + + Fixed head table in accordion + + + +
    + + + + + + + +
    {{column.title}}
    +
    +
    dasdf
    + + + +
    + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-fixed-head-nodata.html b/src/table/demo/src/app/table/table-fixed-head-nodata.html new file mode 100644 index 0000000..540a158 --- /dev/null +++ b/src/table/demo/src/app/table/table-fixed-head-nodata.html @@ -0,0 +1,187 @@ +
    +

    描述

    +

    表头固定和无数各种场景测试

    +

    示例

    +

    无数据(ti3-table-nodata)

    +

    + + + +
    + + + + + + + +
    {{column.title}}
    +
    +
    dasdf
    + + + +
    + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    +
    +
    +

    无数据("ti3-table-nodata-guide)

    +

    + + + +
    + + + + + + + +
    {{column.title}}
    +
    +
    dasdf
    + + + +
    + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + {{noGoodsInfo}} + {{goShopInfo}} +
    +
    +
    + +

    加载失败(ti3-table-loadfail)

    +

    + + + +
    + + + + + + + +
    {{column.title}}
    +
    +
    dasdf
    + + + +
    + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + {{failLoadInfo}} + {{reloadInfo}} +
    +
    +
    +
    +
    +

    无数据简单类型(ti3-table-nodata-simple)

    +

    + + + +
    + + + + + + + +
    {{column.title}}
    +
    +
    dasdf
    + + + +
    + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-fixed-head-pagination-details.html b/src/table/demo/src/app/table/table-fixed-head-pagination-details.html new file mode 100644 index 0000000..d3910e6 --- /dev/null +++ b/src/table/demo/src/app/table/table-fixed-head-pagination-details.html @@ -0,0 +1,72 @@ +
    +

    描述

    +

    表格表头锁定特性

    +

    注意:使用 pagination 组件,请导入 TiPaginationModule

    +

    示例

    + + + +
    + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}{{columns[5].title}}
    +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    + + +
    +
    详情展示:{{row.firstName}}
    +
    详情展示:{{row.balance}}
    +
    详情展示:{{row.lastName}}
    +
    +
    +
    + +
    +
    diff --git a/src/table/demo/src/app/table/table-fixed-head.html b/src/table/demo/src/app/table/table-fixed-head.html new file mode 100644 index 0000000..98bf043 --- /dev/null +++ b/src/table/demo/src/app/table/table-fixed-head.html @@ -0,0 +1,28 @@ + +
    + + + + + + +
    {{column.title}}
    +
    +
    + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-fixedhead-colsresizable-pagination-details.html b/src/table/demo/src/app/table/table-fixedhead-colsresizable-pagination-details.html new file mode 100644 index 0000000..8a2e4ec --- /dev/null +++ b/src/table/demo/src/app/table/table-fixedhead-colsresizable-pagination-details.html @@ -0,0 +1,72 @@ +
    +

    描述

    +

    表格表头锁定特性 + 列拖动特性 + 分页特性 + 详情展开特性

    +

    注意:使用 pagination 组件,请导入 TiPaginationModule

    +

    示例

    + + + +
    + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}{{columns[5].title}}
    +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    + + +
    +
    详情展示:{{row.firstName}}
    +
    详情展示:{{row.balance}}
    +
    详情展示:{{row.lastName}}
    +
    +
    +
    + +
    +
    diff --git a/src/table/demo/src/app/table/table-fixhead-scroll.html b/src/table/demo/src/app/table/table-fixhead-scroll.html new file mode 100644 index 0000000..30d586e --- /dev/null +++ b/src/table/demo/src/app/table/table-fixhead-scroll.html @@ -0,0 +1,51 @@ +

    描述

    +

    本测试用例主要测试表头固定是否影响页面触发滚动条。

    +

    + 在有表格表头固定的页面,且页面有滚动条,将页面滚动条 + 拖至formfiled的头部不在视野范围内时,给formfield中添加数据后页面滚动条会异常跳动到顶部,正常滚动条应该不跳动。该问题在10.1.8版本修复 +

    +

    示例

    +
    + + +
    {{item}}
    +
    +
    + +
    +
    + +
    + + + + + + +
    {{column.title}}
    +
    + +
    + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-group.html b/src/table/demo/src/app/table/table-group.html new file mode 100644 index 0000000..5f7a8b8 --- /dev/null +++ b/src/table/demo/src/app/table/table-group.html @@ -0,0 +1,42 @@ +
    +

    描述

    +

    开发者可结合数据利用表格dom结构来实现简单的表格分组

    +

    示例

    + + +
    + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    + + + + {{you.name}} +
    {{son.name}}{{son.size}}{{son.type}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-guide.html b/src/table/demo/src/app/table/table-guide.html new file mode 100644 index 0000000..67227e4 --- /dev/null +++ b/src/table/demo/src/app/table/table-guide.html @@ -0,0 +1,65 @@ +
    +

    描述

    +

    表格无数据时导引,结合tiColspan指令和ti3-table-nodata-guide样式类实现。

    +

    示例

    + +

    大表格(默认表格)

    +

    + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + {{noGoodsInfo}} + {{goShopInfo}} +
    +
    +



    +

    小表格

    +

    + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + {{noGoodsInfo}} + {{goShopInfo}} +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter-datetime-test.html b/src/table/demo/src/app/table/table-head-filter-datetime-test.html new file mode 100644 index 0000000..9033b4d --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter-datetime-test.html @@ -0,0 +1,65 @@ +
    +

    描述

    +

    + 表格表头过滤要结合 tiCellText 组件和 tiHeadFilter 组件来实现。ti-cell-text包裹的是表头单元格 + 文本,ti-head-filter为表头过滤的漏斗图标,tiHeadFilter的接口说明请查询 TiHeadFilterComponent。 +

    +

    表头过滤-时间日期通过配置 isDatetimedatetimeConfig 等接口来配置表头过滤下拉面板。

    +

    isDatetime 接口控制表头过滤是否设置为时间日期面板。

    +

    datetimeConfig 接口设置表头过滤时间日期面板中的配置信息:

    +

    datetimeConfig.format 可选,设置日期时间显示格式。

    +

    datetimeConfig.min 可选,设置最小值。

    +

    datetimeConfig.max 可选,设置最大值。

    +

    datetimeConfig.onlyDate 可选,设置是否仅是日期的选择(没有时间的选择),不设置时默认是日期时间的选择。

    +

    panelAlign 接口控制表头过滤下拉面板展开方向,默认左对齐

    +

    panelWidth 接口控制表头过滤下拉框的宽度,默认为'auto',搜索场景下默认值修改为“180px”

    +

    备注:

    +

    + searchable,labelKey + 两个接口,因时间日期和下拉是两个相互独立的面板,且面板中不包含搜索框和下拉options的匹配,此处不涉及这两个接口的使用。 + 所以在这两个接口在时间日期面板中使用无效 +

    +

    + 因时间日期面板涉及的场景较多,需求不一,表格只提供接口及过滤时机,具体搜索实现方案,用户根据实际场景进行自定义,此处提供简单示例供参考 +

    +

    示例

    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    {{column.title}} + + {{column.title}} + + + +
    {{row.firstName}}{{row.birthday}}{{row.hireDate}}{{row.start}}{{row.expired}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter-datetime.html b/src/table/demo/src/app/table/table-head-filter-datetime.html new file mode 100644 index 0000000..dacf551 --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter-datetime.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}} + {{column.title}} + + +
    {{row.name}}{{row.birthday}}{{row.hireDate}}{{row.start}}{{row.expired}}
    {{noDadaInfo}}
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter-multi-valuekey.html b/src/table/demo/src/app/table/table-head-filter-multi-valuekey.html new file mode 100644 index 0000000..83580f8 --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter-multi-valuekey.html @@ -0,0 +1,59 @@ +
    +

    描述

    +

    + 表格表头过滤要结合 tiCellText 组件和 tiHeadFilter 组件来实现。ti-cell-text包裹的是表头单元格 + 文本,ti-head-filter为表头过滤的漏斗图标,tiHeadFilter的接口说明请查询 TiHeadFilterComponent。 +

    +

    + 表头过滤-多选通过配置 multipleselectAll + 等接口来配置表头过滤下拉面板。 +

    +

    multiple 接口控制表头过滤是否为多选,默认为单选。

    +

    selectAll 接口控制表头过滤下拉多选是否开启全选功能,默认不开启增。

    +

    panelAlign 接口控制表头过滤下拉面板展开方向,默认左对齐

    +

    searchable 接口控制表头过滤下拉是否开启搜索功能,默认不开启

    +

    labelKey 接口控制表头过滤下拉中显示的字段,默认为'label'

    +

    panelWidth 接口控制表头过滤下拉框的宽度,默认为'auto',搜索场景下默认值修改为“180px”

    +

    valuekey 自定义选中值, 设置valueKey时选中值基于valueKey,单选或者多选的选中值结构都和select组件保持一致

    +

    示例

    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    + + {{column.title}} + + + {{column.title}}
    {{row.firstName.label}}{{row.lastName.label}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter-multi.html b/src/table/demo/src/app/table/table-head-filter-multi.html new file mode 100644 index 0000000..42840f3 --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter-multi.html @@ -0,0 +1,57 @@ +
    +

    描述

    +

    + 表格表头过滤要结合 tiCellText 组件和 tiHeadFilter 组件来实现。ti-cell-text包裹的是表头单元格 + 文本,ti-head-filter为表头过滤的漏斗图标,tiHeadFilter的接口说明请查询 TiHeadFilterComponent。 +

    +

    + 表头过滤-多选通过配置 multipleselectAll + 等接口来配置表头过滤下拉面板。 +

    +

    multiple 接口控制表头过滤是否为多选,默认为单选。

    +

    selectAll 接口控制表头过滤下拉多选是否开启全选功能,默认不开启增。

    +

    panelAlign 接口控制表头过滤下拉面板展开方向,默认左对齐

    +

    searchable 接口控制表头过滤下拉是否开启搜索功能,默认不开启

    +

    labelKey 接口控制表头过滤下拉中显示的字段,默认为'label'

    +

    panelWidth 接口控制表头过滤下拉框的宽度,默认为'auto',搜索场景下默认值修改为“180px”

    +

    示例

    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    + + {{column.title}} + + + {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter-test.html b/src/table/demo/src/app/table/table-head-filter-test.html new file mode 100644 index 0000000..b55b929 --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter-test.html @@ -0,0 +1,84 @@ +
    +

    描述

    +

    + 表格表头过滤要结合 tiCellText 组件和 tiHeadFilter 组件来实现。ti-cell-text包裹的是表头单元格 + 文本,ti-head-filter为表头过滤的漏斗图标,tiHeadFilter的接口说明请查询 TiHeadFilterComponent。 +

    +

    panelAlign 接口控制表头过滤下拉面板展开方向,默认左对齐

    +

    searchable 接口控制表头过滤下拉是否开启搜索功能,默认不开启

    +

    labelKey 接口控制表头过滤下拉中显示的字段,默认为'label'

    +

    panelWidth 接口控制表头过滤下拉框的宽度,默认为'auto',搜索场景下默认值修改为“180px”

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    +

    + + + + + + + + + + + + + + + + + + + + + + + +
    + + {{column.title}} + + + {{column.title}} + + {{column.title}} + + + + + {{column.title}} + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.gender}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter-valuekey.html b/src/table/demo/src/app/table/table-head-filter-valuekey.html new file mode 100644 index 0000000..04c3bc6 --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter-valuekey.html @@ -0,0 +1,52 @@ +
    +

    描述

    +

    + 表格表头过滤要结合 tiCellText 组件和 tiHeadFilter 组件来实现。ti-cell-text包裹的是表头单元格 + 文本,ti-head-filter为表头过滤的漏斗图标,tiHeadFilter的接口说明请查询 TiHeadFilterComponent。 +

    +

    panelAlign 接口控制表头过滤下拉面板展开方向,默认左对齐

    +

    searchable 接口控制表头过滤下拉是否开启搜索功能,默认不开启

    +

    labelKey 接口控制表头过滤下拉中显示的字段,默认为'label'

    +

    panelWidth 接口控制表头过滤下拉框的宽度,默认为'auto',搜索场景下默认值修改为“180px”

    +

    valuekey 自定义选中值, 设置valueKey时选中值基于valueKey,单选或者多选的选中值结构都和select组件保持一致

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    +

    + + + + + + + + + + + + + + + + + + + + + +
    + + {{column.title}} + + + {{column.title}}
    {{row.firstName.label}}{{row.lastName.label}}{{row.age}}{{row.balance}}{{row.email}}{{row.gender}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter-virtualscroll.html b/src/table/demo/src/app/table/table-head-filter-virtualscroll.html new file mode 100644 index 0000000..5ed475b --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter-virtualscroll.html @@ -0,0 +1,55 @@ +
    +

    描述

    +

    表格表头过滤下拉面板如果数据量很大,可通过 virtual 接口开启下拉面板虚拟滚动功能,默认不开启

    +

    + 表格表头过滤要结合 tiCellText 组件和 tiHeadFilter 组件来实现。ti-cell-text包裹的是表头单元格 + 文本,ti-head-filter为表头过滤的漏斗图标,tiHeadFilter的接口说明请查询 TiHeadFilterComponent。 +

    +

    multiple 接口控制表头过滤是否为多选,默认为单选。

    +

    selectAll 接口控制表头过滤下拉多选是否开启全选功能,默认不开启增。

    +

    searchable 接口控制表头过滤下拉是否开启搜索功能,默认不开启

    +

    virtual 接口控制表头过滤下拉是否开启下拉面板虚拟滚动功能,默认不开启

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    +

    + + + + + + + + + + + + + + + + + + + +
    + + {{column.title}} + + +
    {{row.firstName}}{{row.lastName}}{{row.total}}{{row.balance}}{{row.email}}
    + + + +
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter.html b/src/table/demo/src/app/table/table-head-filter.html new file mode 100644 index 0000000..59104fc --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
    + {{column.title}} + + + + {{column.title}} + + + 邮箱后缀:{{item.label}} + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.gender}}{{row.email}}
    {{noDadaInfo}}
    +
    diff --git a/src/table/demo/src/app/table/table-load-fail.html b/src/table/demo/src/app/table/table-load-fail.html new file mode 100644 index 0000000..65fa73b --- /dev/null +++ b/src/table/demo/src/app/table/table-load-fail.html @@ -0,0 +1,65 @@ +
    +

    描述

    +

    表格加载数据失败,结合tiColspan指令和ti3-table-loadfail样式类实现。

    +

    示例

    + +

    大表格(默认表格)

    +

    + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + {{failLoadInfo}} + {{reloadInfo}} +
    +
    +



    +

    小表格

    +

    + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + {{failLoadInfo}} + {{reloadInfo}} +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-nodata-simple.html b/src/table/demo/src/app/table/table-nodata-simple.html new file mode 100644 index 0000000..c5d2ff0 --- /dev/null +++ b/src/table/demo/src/app/table/table-nodata-simple.html @@ -0,0 +1,28 @@ +

    描述

    +

    表格无数据简单类型,结合tiColspan指令和ti3-table-nodata-simple样式类实现。

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    diff --git a/src/table/demo/src/app/table/table-nodata-test.html b/src/table/demo/src/app/table/table-nodata-test.html new file mode 100644 index 0000000..355e900 --- /dev/null +++ b/src/table/demo/src/app/table/table-nodata-test.html @@ -0,0 +1,57 @@ +

    描述

    +

    表格无数据,结合tiColspan指令和ti3-table-nodata样式类实现。

    +

    示例

    + +

    大表格(默认表格)

    +

    + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    +



    +

    小表格

    +

    + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    diff --git a/src/table/demo/src/app/table/table-nodata.html b/src/table/demo/src/app/table/table-nodata.html new file mode 100644 index 0000000..b717136 --- /dev/null +++ b/src/table/demo/src/app/table/table-nodata.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    {{noDataInfo}}
    +
    diff --git a/src/table/demo/src/app/table/table-overflow-link.html b/src/table/demo/src/app/table/table-overflow-link.html new file mode 100644 index 0000000..160671b --- /dev/null +++ b/src/table/demo/src/app/table/table-overflow-link.html @@ -0,0 +1,49 @@ +
    +

    描述

    +

    + 表格中有链接,使用tiOverflow的场景
    + 问题:表格中使用A标签,使用tiOverflow后,tip显示的内容,会有A标签的样式
    + 原因:tiOverflow获取的是元素的innerHTML,将tiOverflow写在td上面,会获取到td中的所有内容,从而显示在tip里面。
    + 原本tip中获取的是元素的textContent,因为textContent只能获取文本,如果想要显示图片,则无法获取到。
    + 针对以上原因,目前提供的方案有两种:
    + 解决方案一:将tiOverflow写在A标签上,设置A标签的宽度
    + 解决方案二:通过使用tiTipContent接口,将tip中的内容重新设置即可 +

    + +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    导入

    +

    import {{ '{' }} TiTableModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    + {{row.firstName}} + + {{row.lastName}} + + {{row.email}} + {{row.age}}{{row.balance}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-pagi-with-filter.html b/src/table/demo/src/app/table/table-pagi-with-filter.html new file mode 100644 index 0000000..b07b2e2 --- /dev/null +++ b/src/table/demo/src/app/table/table-pagi-with-filter.html @@ -0,0 +1,36 @@ +

    描述

    +

    表格前台分页 + 前台过滤特性

    +

    注意:使用 pagination 组件,请导入 TiPaginationModule。

    +

    示例

    + + +

    搜索结果个数: {{ searchedData.length }}

    + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + +
    diff --git a/src/table/demo/src/app/table/table-pagination.html b/src/table/demo/src/app/table/table-pagination.html new file mode 100644 index 0000000..c3fae36 --- /dev/null +++ b/src/table/demo/src/app/table/table-pagination.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    + +
    diff --git a/src/table/demo/src/app/table/table-radio-test.html b/src/table/demo/src/app/table/table-radio-test.html new file mode 100644 index 0000000..c0f37dc --- /dev/null +++ b/src/table/demo/src/app/table/table-radio-test.html @@ -0,0 +1,46 @@ +

    描述

    +

    表格单选功能需要配合tiRadio组件来实现,具体可参考tiRadio组件的使用说明。使用单选时, 需要给单选框对应列(th,td)加 radio-column 属性。

    +

    注意:使用 tiRadio 组件,请导入 TiRadioModule;使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + + + {{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    + +
    diff --git a/src/table/demo/src/app/table/table-radio.html b/src/table/demo/src/app/table/table-radio.html new file mode 100644 index 0000000..40647bd --- /dev/null +++ b/src/table/demo/src/app/table/table-radio.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + +
    {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-row-drag2.html b/src/table/demo/src/app/table/table-row-drag2.html new file mode 100644 index 0000000..f1baadf --- /dev/null +++ b/src/table/demo/src/app/table/table-row-drag2.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-rowspan.html b/src/table/demo/src/app/table/table-rowspan.html new file mode 100644 index 0000000..9c7bd9b --- /dev/null +++ b/src/table/demo/src/app/table/table-rowspan.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-search.html b/src/table/demo/src/app/table/table-search.html new file mode 100644 index 0000000..149e9d6 --- /dev/null +++ b/src/table/demo/src/app/table/table-search.html @@ -0,0 +1,70 @@ +firstName,lastName,or age: + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    {{noDadaInfo}}
    +
    +


    + +firstName: + +age: + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    {{noDadaInfo}}
    +
    diff --git a/src/table/demo/src/app/table/table-server-pagi-search-sort.html b/src/table/demo/src/app/table/table-server-pagi-search-sort.html new file mode 100644 index 0000000..c6e39ad --- /dev/null +++ b/src/table/demo/src/app/table/table-server-pagi-search-sort.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}} + {{columns[3].title}} + + {{columns[4].title}}
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    + +
    暂无数据
    + +
    diff --git a/src/table/demo/src/app/table/table-server-pagi.html b/src/table/demo/src/app/table/table-server-pagi.html new file mode 100644 index 0000000..4c7564e --- /dev/null +++ b/src/table/demo/src/app/table/table-server-pagi.html @@ -0,0 +1,32 @@ +
    +

    描述

    +

    表格分页要结合tiPagination分页组件使用,后台分页时需要将srcData的state.paginated 设置为true(表示传入组件的数据已经进行过分页处理)。

    +

    注意:使用 tiPagination 组件,请导入 TiPaginationModule;使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + +

    事件日志:

    + +
    + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + + +
    +
    diff --git a/src/table/demo/src/app/table/table-small.html b/src/table/demo/src/app/table/table-small.html new file mode 100644 index 0000000..da9c802 --- /dev/null +++ b/src/table/demo/src/app/table/table-small.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-soldout.html b/src/table/demo/src/app/table/table-soldout.html new file mode 100644 index 0000000..2864da9 --- /dev/null +++ b/src/table/demo/src/app/table/table-soldout.html @@ -0,0 +1,46 @@ +

    描述

    +

    开发者可参考该示例实现表格售罄场景

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + + + {{row.lastName}} + {{row.message.text}} + {{row.age}}{{row.balance | currency}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-sort-basic.html b/src/table/demo/src/app/table/table-sort-basic.html new file mode 100644 index 0000000..a5235cb --- /dev/null +++ b/src/table/demo/src/app/table/table-sort-basic.html @@ -0,0 +1,51 @@ +
    +

    描述

    +

    表格排序要结合tiCellText组件和tiHeadSort组件来实现。ti-cell-text包裹的是表头单元格 文本,ti-head-sort为点击的排序图标。

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + + +

    + + + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} + + + + {{columns[1].title}} + + + {{columns[2].title}} + + + + {{columns[3].title}} + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.birthday}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-sort-comparefn-locale.html b/src/table/demo/src/app/table/table-sort-comparefn-locale.html new file mode 100644 index 0000000..afd214c --- /dev/null +++ b/src/table/demo/src/app/table/table-sort-comparefn-locale.html @@ -0,0 +1,50 @@ +
    +

    描述

    +

    + 表格组件提供的排序中字符串比较是使用基于标准字典的 Unicode 值来进行比较的。如果开发真需要本地化的排序, + 可使用tiHeadSort组件的compareFn接口来自定义所在列的本地化排序规则。本地化排序规则可利用 localeCompare 方法。 +

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} (此列用了自定义排序规则) + + + + + + {{columns[1].title}} (此列用了自定义排序规则) + + + {{columns[2].title}} + + + {{columns[3].title}} + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-sort-comparefn.html b/src/table/demo/src/app/table/table-sort-comparefn.html new file mode 100644 index 0000000..1bcec9d --- /dev/null +++ b/src/table/demo/src/app/table/table-sort-comparefn.html @@ -0,0 +1,47 @@ +
    +

    描述

    +

    可使用tiHeadSort组件的compareFn接口来自定义所在列的排序规则。

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} (此列用了自定义排序规则) + + + + + {{columns[1].title}} + + + {{columns[2].title}} (此列用了自定义排序规则) + + + + {{columns[3].title}} + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-sort-details.html b/src/table/demo/src/app/table/table-sort-details.html new file mode 100644 index 0000000..58ce830 --- /dev/null +++ b/src/table/demo/src/app/table/table-sort-details.html @@ -0,0 +1,67 @@ +
    +

    描述

    +

    表格详情

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}} + {{columns[1].title}} + + + + {{columns[2].title}} + + + {{columns[3].title}} + + + + {{columns[4].title}} + + {{columns[5].title}}
    + + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    + + +
    +
    详情展示:{{row.firstName}}
    +
    详情展示:{{row.balance}}
    +
    详情展示:{{row.lastName}}
    +
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-sort-reset.html b/src/table/demo/src/app/table/table-sort-reset.html new file mode 100644 index 0000000..e3446bc --- /dev/null +++ b/src/table/demo/src/app/table/table-sort-reset.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} + + + {{columns[1].title}} + + + {{columns[2].title}} + + {{columns[3].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-sort-test.html b/src/table/demo/src/app/table/table-sort-test.html new file mode 100644 index 0000000..73fce59 --- /dev/null +++ b/src/table/demo/src/app/table/table-sort-test.html @@ -0,0 +1,37 @@ +
    +

    描述

    +

    表格排序要结合tiCellText组件和tiHeadSort组件来实现。ti-cell-text包裹的是表头单元格 文本,ti-head-sort为点击的排序图标。

    +

    10.1.14 版本 ti-head-sort 组件初始无序时(默认为null)兼容传入 undefined。

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + + +

    + + + + + + + + + + + + + + + + + + +
    + {{column.title}} + + +
    {{row.firstName}}{{row.lastName}}{{row.birthday}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-sort.html b/src/table/demo/src/app/table/table-sort.html new file mode 100644 index 0000000..ba41fc3 --- /dev/null +++ b/src/table/demo/src/app/table/table-sort.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} + + + {{columns[1].title}} + + + {{columns[2].title}} + + + {{columns[3].title}} + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.poetry}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-storage-config.html b/src/table/demo/src/app/table/table-storage-config.html new file mode 100644 index 0000000..21e8761 --- /dev/null +++ b/src/table/demo/src/app/table/table-storage-config.html @@ -0,0 +1,49 @@ +

    描述

    +

    + storageConfig 接口设置表格记忆项排序、分页、列宽 的开关 +
    + 测试:排序、列宽和分页都是设置了false,如果没有记忆功能,测试用过。
    + 只记忆当前页或只记忆每页个数 +

    +

    示例

    +

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + {{column.title}} + + {{column.title}}
    {{row[column.field]}}
    +
    + +
    diff --git a/src/table/demo/src/app/table/table-storage-filter.html b/src/table/demo/src/app/table/table-storage-filter.html new file mode 100644 index 0000000..4e349e1 --- /dev/null +++ b/src/table/demo/src/app/table/table-storage-filter.html @@ -0,0 +1,49 @@ +

    描述

    +

    表格记忆--过滤搜索

    +

    示例

    + +

    + first name: + + age : +

    +
    +

    searchWords: {{searchWords|json}}

    +

    searchKeys: {{searchKeys|json}}

    +
    + + + + + + + + + + + + + + + + + + + +
    + {{column.title}} + + {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-storage-serve.html b/src/table/demo/src/app/table/table-storage-serve.html new file mode 100644 index 0000000..d2d33a5 --- /dev/null +++ b/src/table/demo/src/app/table/table-storage-serve.html @@ -0,0 +1,34 @@ +

    描述

    +

    表格记忆--后台分页

    +

    示例

    + +

    事件日志:

    + +
    + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + +
    diff --git a/src/table/demo/src/app/table/table-storage.html b/src/table/demo/src/app/table/table-storage.html new file mode 100644 index 0000000..29bf157 --- /dev/null +++ b/src/table/demo/src/app/table/table-storage.html @@ -0,0 +1,29 @@ + +
    + + + + + + + + + + + + + + + + + + + + +
    + {{column.title}} + + {{column.title}}
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    + +
    diff --git a/src/table/demo/src/app/table/table-tree-mulitiselect.html b/src/table/demo/src/app/table/table-tree-mulitiselect.html new file mode 100644 index 0000000..55dddd2 --- /dev/null +++ b/src/table/demo/src/app/table/table-tree-mulitiselect.html @@ -0,0 +1,76 @@ +

    描述

    +

    开发者可结合数据利用表格dom结构和ticheckgroup组件来实现的复选树表,具体可参考tiCheckgroup组件的使用说明。

    +

    不建议分页,父子关系会有影响

    +

    注意:使用 TiIcon 组件,请导入 TiIconModule;使用 tiCheckgroup 组件,请导入 TiCheckboxModule

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + {{column.title}}
    + + + + + + {{you.name}} + {{you.size}}{{you.type}}
    + + + + + + {{son.name}} + {{son.size}}{{son.type}}
    + + + {{grandson.name}} + {{grandson.size}}{{grandson.type}}
    +
    diff --git a/src/table/demo/src/app/table/table-tree-unknowdeepth.html b/src/table/demo/src/app/table/table-tree-unknowdeepth.html new file mode 100644 index 0000000..b47128c --- /dev/null +++ b/src/table/demo/src/app/table/table-tree-unknowdeepth.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + +
    {{column.title}}
    + + + + {{node.name}} + {{node.size}}{{node.type}}
    +
    diff --git a/src/table/demo/src/app/table/table-tree.html b/src/table/demo/src/app/table/table-tree.html new file mode 100644 index 0000000..9b69297 --- /dev/null +++ b/src/table/demo/src/app/table/table-tree.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    + + + + {{you.name}} + {{you.size}}{{you.type}}
    + + + + {{son.name}} + {{son.size}}{{son.type}}
    + {{grandson.name}} + {{grandson.size}}{{grandson.type}}
    +
    diff --git a/src/table/demo/src/app/table/table-virtualscroll-basic.html b/src/table/demo/src/app/table/table-virtualscroll-basic.html new file mode 100644 index 0000000..0e5ae0a --- /dev/null +++ b/src/table/demo/src/app/table/table-virtualscroll-basic.html @@ -0,0 +1,65 @@ +
    +

    描述

    +

    表格虚拟滚动。

    +

    虚拟滚动是和表头锁定功能搭配使用的。

    +

    虚拟滚动不支持详情展开、数据分组。

    +

    + 表格表头锁定功能:使用双表来实现的,样式类ti3-table-fixed-head包裹的是表头(第一个表格),样式类ti3-table-container包裹的是表体(第二个表格)。 +

    +

    + 表格虚拟滚动功能:在有样式类 ti3-table-container 的元素(第二个表格的父容器)上使用 itemSize + 接口设置每条数据占据的高度,并且给其设置最大高度或高度,然后在 div.ti3-table-container > table > tbody > tr 上用 cdkVirtualFor + 替换原有的 ngFor。 +

    +

    导入

    +

    import {{ '{' }} TiTableModule, TiOverflowModule {{ '}' }} from '@opentiny/ng';

    +

    使用虚拟滚动需要引入 ScrollingModule 模块, 前提是安装了 @angular/cdk 三方库。

    +

    import {{ '{' }} ScrollingModule {{ '}' }} from '@angular/cdk/scrolling';

    +

    示例

    + + +
    + + + + +
    + + + + + + + +
    {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-virtualscroll-comprehensive.html b/src/table/demo/src/app/table/table-virtualscroll-comprehensive.html new file mode 100644 index 0000000..ff72808 --- /dev/null +++ b/src/table/demo/src/app/table/table-virtualscroll-comprehensive.html @@ -0,0 +1,106 @@ +
    +

    描述

    +

    虚拟滚动是和表头锁定功能搭配使用的。

    +

    虚拟滚动不支持详情展开、数据分组。

    +

    虚拟滚动综合示例,虚拟滚动 + 表头锁定特性 + 列动态显示/隐藏 + 列固定 + 列拖动特性 + 分页特性 + 排序特性 + 表头搜索特性

    +

    + 表格虚拟滚动功能:在有样式类 ti3-table-container 的元素(第二个表格的父容器)上使用 itemSize + 接口设置每条数据占据的高度,并且给其设置最大高度或高度,然后在 div.ti3-table-container > table > tbody > tr 上用 cdkVirtualFor + 替换原有的 ngFor。 +

    +

    导入

    +

    import {{ '{' }} TiTableModule, TiOverflowModule, TiPaginationModule {{ '}' }} from '@opentiny/ng';

    +

    使用虚拟滚动需要引入 ScrollingModule 模块, 前提是安装了 @angular/cdk 三方库。

    +

    import {{ '{' }} ScrollingModule {{ '}' }} from '@angular/cdk/scrolling;

    +

    示例

    + + + +
    + + + +
    + + +
    + + + + + + + + + + + + + +
    + {{column.title}} + + + + {{column.title}} + + + {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + {{row.id + '-' + row.firstName}} + {{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.address}}{{row.phone}}{{row.parents}}{{row.id + '-' + row.school}}
    +
    + + +
    +
    diff --git a/src/table/demo/src/app/table/table-virtualscroll-sizes.html b/src/table/demo/src/app/table/table-virtualscroll-sizes.html new file mode 100644 index 0000000..b7a0203 --- /dev/null +++ b/src/table/demo/src/app/table/table-virtualscroll-sizes.html @@ -0,0 +1,204 @@ +
    +

    描述

    +

    虚拟滚动是和表头锁定功能搭配使用的。

    +

    虚拟滚动不支持详情展开、数据分组。

    +

    + 表格表头锁定功能:使用双表来实现的,样式类ti3-table-fixed-head包裹的是表头(第一个表格),样式类ti3-table-container包裹的是表体(第二个表格)。 +

    +

    + 表格虚拟滚动功能:在有样式类 ti3-table-container 的元素(第二个表格的父容器)上使用 itemSize + 接口设置每条数据占据的高度,并且给其设置最大高度或高度,然后在 div.ti3-table-container > table > tbody > tr 上用 cdkVirtualFor + 替换原有的 ngFor。 +

    +

    在使用虚拟滚动时可根据具体表格数据每行的高度来设置 itemSize 接口,详见下面个示例。

    +

    导入

    +

    import {{ '{' }} TiTableModule, TiOverflowModule {{ '}' }} from '@opentiny/ng';

    +

    使用虚拟滚动需要引入 ScrollingModule 模块, 前提是安装了 @angular/cdk 三方库。

    +

    import {{ '{' }} ScrollingModule {{ '}' }} from '@angular/cdk/scrolling;

    +

    示例

    + + +
    + +

    1.表格-单行:

    +
    + + + +
    + + + + + + + +
    {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +


    + +

    2.表格-多行:

    +
    + + + +
    + + + + + + + +
    {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    {{row.firstName}}
    +
    {{row.id + row.lastName}}
    +
    {{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +


    + +

    3.小表格-单行:

    +
    + + + +
    + + + + + + + +
    {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +


    + +

    4.小表格-多行:

    +
    + + + +
    + + + + + + + +
    {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    {{row.firstName}}
    +
    {{row.id + row.lastName}}
    +
    {{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-virtualscroll-tree.html b/src/table/demo/src/app/table/table-virtualscroll-tree.html new file mode 100644 index 0000000..ec9c0bd --- /dev/null +++ b/src/table/demo/src/app/table/table-virtualscroll-tree.html @@ -0,0 +1,67 @@ +
    +

    描述

    +

    表格虚拟滚动

    +

    虚拟滚动是和表头锁定功能搭配使用的。

    +

    虚拟滚动不支持详情展开、数据分组。

    +

    + 表格表头锁定功能:使用双表来实现的,样式类ti3-table-fixed-head包裹的是表头(第一个表格),样式类ti3-table-container包裹的是表体(第二个表格)。 +

    +

    + 表格虚拟滚动功能:在有样式类 ti3-table-container 的元素(第二个表格的父容器)上使用 itemSize + 接口设置每条数据占据的高度,并且给其设置最大高度或高度,然后在 div.ti3-table-container > table > tbody > tr 上用 cdkVirtualFor + 替换原有的 ngFor。 +

    +

    导入

    +

    import {{ '{' }} TiTableModule {{ '}' }} from '@opentiny/ng';

    +

    使用虚拟滚动需要引入 ScrollingModule 模块, 前提是安装了 @angular/cdk 三方库。

    +

    import {{ '{' }} ScrollingModule {{ '}' }} from '@angular/cdk/scrolling';

    +

    示例

    + + + + + + +
    + + + + + + + +
    {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + +
    + + + + {{node.name}} + {{node.size}}{{node.type}}
    {{noDadaInfo}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-virtualscroll.html b/src/table/demo/src/app/table/table-virtualscroll.html new file mode 100644 index 0000000..18504cb --- /dev/null +++ b/src/table/demo/src/app/table/table-virtualscroll.html @@ -0,0 +1,29 @@ + +
    + + + + + + +
    {{column.title}}
    +
    +
    + + + + + + + + + + + + + + + +
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/tableTest.less b/src/table/demo/src/app/table/tableTest.less new file mode 100644 index 0000000..09b9530 --- /dev/null +++ b/src/table/demo/src/app/table/tableTest.less @@ -0,0 +1,19 @@ +.containerStyle { + margin-left: auto; + margin-right: auto; + margin-top: 20px; +} +.containerTipStyle { + width: 35%; + margin-left: auto; + margin-right: auto; + margin-top: 30px; +} +.x { + max-width: calc(100% - 8px - 15px); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + display: inline-block; + vertical-align: top; +} diff --git a/src/table/demo/src/app/table/webdoc/table-demos.js b/src/table/demo/src/app/table/webdoc/table-demos.js new file mode 100644 index 0000000..163c01b --- /dev/null +++ b/src/table/demo/src/app/table/webdoc/table-demos.js @@ -0,0 +1,504 @@ +export default { + column: '1', + demos: [ + { + demoId: 'table-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': '

    Table 组件的最简用法。

    ', + 'en-US': '' + }, + apis: ['TiTableComponent.properties.srcData', 'TiTableComponent.properties.displayedData'] + }, + { + demoId: 'table-nodata', + name: { + 'zh-CN': '空表格', + 'en-US': 'nodata' + }, + desc: { + 'zh-CN': + '

    通过给tr标签上添加ti3-table-nodata样式类,在td标签上使用tiColspan属性指令配置表格数据为空时的状态。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-small', + name: { + 'zh-CN': '紧凑型', + 'en-US': 'samll' + }, + desc: { + 'zh-CN': '

    通过给ti-table标签上添加ti3-table-small样式类配置为紧凑型表格。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-cell-tip', + name: { + 'zh-CN': '带提示的省略', + 'en-US': 'cell ellipsis' + }, + desc: { + 'zh-CN': `

    通过使用tiOverflow属性指令使文本内容根据单元格宽度自动省略显示,并在鼠标悬浮时弹出完整内容提示,具体可查看 Overflow 属性指令示例及 + API。
    需导入:import { TiOverflowModule } from '@opentiny/ng';

    `, + 'en-US': '' + } + }, + { + demoId: 'table-pagination', + name: { + 'zh-CN': '分页', + 'en-US': 'pagination' + }, + desc: { + 'zh-CN': `

    通过ti-pagination组件实现分页功能,具体可查看 Pagination 组件示例及 API。
    需导入: + import { TiPaginationModule } from '@opentiny/ng';

    `, + 'en-US': '' + } + }, + { + demoId: 'table-sort', + name: { + 'zh-CN': '排序', + 'en-US': 'sort' + }, + desc: { + 'zh-CN': `

    通过ti-head-sort组件(可点击的排序图标)和ti-cell-text组件(表头单元格文本)实现对某一列数据的排序功能。通过ti-head-sort组件的 + 属性sortKey配置该列排序时依据的数据属性,属性asc配置初始默认是否为升序,属性compareFn配置自定义排序规则函数。

    `, + 'en-US': '' + }, + apis: ['TiHeadSortComponent.properties.sortKey', 'TiHeadSortComponent.properties.asc', 'TiHeadSortComponent.properties.compareFn'] + }, + { + demoId: 'table-sort-reset', + name: { + 'zh-CN': '可控的排序', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    通过ti-table组件实例上getDataState方法的返回值对象中的sort属性控制当前的排序状态,其中sort.sortKey配置当前排序 + 的列,sort.asc配置是否为升序。

    `, + 'en-US': '' + }, + apis: ['TiHeadSortComponent.properties.sortKey', 'TiHeadSortComponent.properties.asc', 'TiHeadSortComponent.properties.compareFn'] + }, + { + demoId: 'table-search', + name: { + 'zh-CN': '搜索', + 'en-US': 'search' + }, + desc: { + 'zh-CN': `

    通过属性searchWords配置需被检索的字符串的集合;通过属性searchKeys配置搜索的字段范围;通过属性searchStrictKeys配置其 + 中哪些字段是精确匹配(等于)的。

    `, + 'en-US': '' + }, + apis: [ + 'TiTableComponent.properties.searchWords', + 'TiTableComponent.properties.searchKeys', + 'TiTableComponent.properties.searchStrictKeys' + ] + }, + { + demoId: 'table-server-pagi-search-sort', + name: { + 'zh-CN': '远程加载数据', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    使用Promise简单模拟从服务端请求异步数据,具有分页、排序和搜索功能以及数据加载 loading 效果,开发者可接入实际从服务端获取数据的方式。设置属性 + srcData.state.paginated为 true 来指定由服务端分页;设置属性srcData.state.sorted为 true 来指定由服务端排序;设置属性 + srcData.state.searched为 true 来指定由服务端搜索。分页、排序和搜索状态改变时会触发stateUpdate回调,在该时机通过其参数的 + getDataState方法获取服务端需要的参数后,向服务端发送请求获取数据。
    使用分页功能需导入:import { TiPaginationModule } from '@opentiny/ng'; +

    `, + 'en-US': '' + }, + apis: ['TiTableComponent.properties.srcData', 'TiTableComponent.events.stateUpdate', 'TiTableComponent.methods.getDataState'] + }, + { + demoId: 'table-head-filter', + name: { + 'zh-CN': '筛选-单选/多选菜单', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    通过ti-head-filter组件(筛选的漏斗图标)和ti-cell-text组件(表头单元格文本)来实现表头筛选功能。ti-head-filter组件提供筛选 + 条件的选择能力和筛选的时机,筛选的数据处理逻辑需自行控制。

    `, + 'en-US': '' + }, + apis: [ + 'TiHeadFilterComponent.properties.multiple', + 'TiHeadFilterComponent.properties.selectAll', + 'TiHeadFilterComponent.properties.labelKey', + 'TiHeadFilterComponent.properties.panelWidth', + 'TiHeadFilterComponent.properties.options', + 'TiHeadFilterComponent.properties.searchable', + 'TiHeadFilterComponent.properties.panelAlign', + 'TiHeadFilterComponent.properties.virtual', + 'TiHeadFilterComponent.events.select', + 'TiHeadFilterComponent.slots.itemTemplate' + ] + }, + { + demoId: 'table-head-filter-datetime', + name: { + 'zh-CN': '筛选-时间日期菜单', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    ti-head-filter组件提供筛选条件的选择能力和筛选的时机,筛选的数据处理逻辑需自行控制。通过属性isDatetime配置为时间日期菜单;通过属性 + datetimeConfig设置时间日期菜单中的配置信息。

    `, + 'en-US': '' + }, + apis: [ + 'TiHeadFilterComponent.properties.isDatetime', + 'TiHeadFilterComponent.properties.datetimeConfig', + 'TiHeadFilterComponent.properties.panelAlign', + 'TiHeadFilterComponent.events.select' + ] + }, + { + demoId: 'table-radio', + name: { + 'zh-CN': '单行选择', + 'en-US': 'radio' + }, + desc: { + 'zh-CN': `

    通过tiRadio实现单行选择的功能,具体可查看 Radio 组件示例及 API。另外,需要在选择列的thtd + 上添加radio-column属性,在tr上使用ti3-selected-tr样式类,选择列的td上使用ti3-disabled-cell样式类。 +
    需导入:import { TiRadioModule } from '@opentiny/ng';

    `, + 'en-US': '' + } + }, + { + demoId: 'table-checkbox', + name: { + 'zh-CN': '多行选择', + 'en-US': 'checkbox' + }, + desc: { + 'zh-CN': `

    通过tiCheckgroup和tiCheckitem实现多行选择的功能,具体可查看 Checkgroup 组件示例及 API。另外,需要在选择列的 + thtd上添加checkbox-column属性,在tr上使用ti3-selected-tr样式类,选择列的td上使用 + ti3-disabled-cell样式类。
    需导入:import { TiCheckboxModule } from '@opentiny/ng';

    `, + 'en-US': '' + } + }, + { + demoId: 'table-checkbox-pagination-headmenu', + name: { + 'zh-CN': '扩展多行选择全选菜单', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    表头的全选复选框默认选中/清空当前页数据,可通过ti-head-menu组件扩展多行选择全选菜单,添加选择所有数据和清空所有数据的菜单项。ti-head-menu + 组件默认有 '选择所有' 和 '清空所有' 这两个菜单项,可以通过属性options自定义菜单项。
    使用分页功能需导入:import { TiPaginationModule } from '@opentiny/ng'; +
    使用多行选择功能需导入:import { TiCheckboxModule } from '@opentiny/ng';

    `, + 'en-US': '' + }, + apis: ['TiHeadMenuComponent.properties.options', 'TiHeadMenuComponent.events.select'] + }, + { + demoId: 'table-details', + name: { + 'zh-CN': '可展开', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    通过ti-details-icon组件、tiDetailsTr结构指令和tiColspan属性指令实现展开功能。另外,需要在展开图标列的thtd上 + 添加details-icon-column属性,在展开的行里的元素使用ti3-table-detail-container样式类。

    `, + 'en-US': '' + }, + apis: [ + 'TiDetailsIconComponent.properties.row', + 'TiDetailsIconComponent.properties.index', + 'TiDetailsTrDirective.properties.tiDetailsTr' + ] + }, + { + demoId: 'table-dynamic-details', + name: { + 'zh-CN': '可控的展开', + 'en-US': '' + }, + desc: { + 'zh-CN': + '

    当展开或收起时会触发 ti-details-icon 组件的beforeToggle事件,开发者可在该时机使用行数据的showDetails属性来控制是否展开及展开的时间点。

    ', + 'en-US': '' + }, + apis: [ + 'TiDetailsIconComponent.properties.row', + 'TiDetailsIconComponent.properties.index', + 'TiDetailsTrDirective.properties.tiDetailsTr', + 'TiDetailsIconComponent.events.beforeToggle' + ] + }, + { + demoId: 'table-details-nesttable', + name: { + 'zh-CN': '嵌套子表格', + 'en-US': '' + }, + desc: { + 'zh-CN': + '

    在展开的行里嵌入表格,注意嵌入的表格组件需自己定义,可以给嵌入的表格组件里的ti-table标签上添加ti3-table-nest样式类。

    ', + 'en-US': '' + }, + apis: [ + 'TiTableComponent.properties.closeOtherDetails', + 'TiDetailsIconComponent.properties.row', + 'TiDetailsIconComponent.properties.index', + 'TiDetailsTrDirective.properties.tiDetailsTr' + ] + }, + { + demoId: 'table-cols-toggle', + name: { + 'zh-CN': '列设置', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    通过ti-cols-toggle组件和ti-table组件上的属性columns实现列设置功能。通过属性columns配置所有列数据, + 在thtd上利用每列数据中的show属性值设置ngIf来控制列的显示/隐藏,ti-cols-toggle组件需 + 要放在ti-table元素内部,具体呈现位置布局由开发者自行控制。

    `, + 'en-US': '' + }, + apis: [ + 'TiTableComponent.properties.columns', + 'TiColsToggleComponent.properties.searchable', + 'TiColsToggleComponent.properties.selectAll' + ] + }, + { + demoId: 'table-cols-resizable', + name: { + 'zh-CN': '拖拽调整列宽', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    通过在ti-table组件上使用tiColsResizable属性指令和属性columns实现拖拽调整列宽的功能。另外,table元素需要一个 + 带有ti3-resize-wrapper样式类的父容器。如果要使某一列不能拖拽调整列宽,可以给该列表头的th标签上添加not-resizable属性。

    `, + 'en-US': '' + } + }, + { + demoId: 'table-fixed-head', + name: { + 'zh-CN': '固定表头', + 'en-US': '' + }, + desc: { + 'zh-CN': + '

    通过双表实现固定表头功能,实际显示的表头(第一个表格)在带有样式类ti3-table-fixed-head的容器里,表体(第二个表格)在带有样式类ti3-table-container的容器里。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-column-fixed', + name: { + 'zh-CN': '固定列', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    通过在th标签上使用tiColumnFixed属性指令实现固定前后列的功能。通过属性tiColumnFixed配置是固定的左右位置。另外,table + 元素需要一个带有ti3-table-container的父容器。

    `, + 'en-US': '' + }, + apis: ['TiColumnFixedDirective.properties.tiColumnFixed'] + }, + { + demoId: 'table-columnfixed-headfixed', + name: { + 'zh-CN': '固定头和列', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    同时固定头和列,适合同时有大量列和数据的展示。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-col-align', + name: { + 'zh-CN': '对齐方式', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    单元格内容默认左对齐,可直接在单元格中使用text-align样式设置居中或右对齐。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-rowspan', + name: { + 'zh-CN': '行/列合并', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    W3C标准table一样,使用colspanrowspan合并行/列。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-virtualscroll', + name: { + 'zh-CN': '虚拟滚动', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    结合@angular/cdk/scrolling实现虚拟滚动。虚拟滚动是和固定表头功能搭配使用的,在有ti3-table-container样式类的元素标签(第二个表格的父容器) + 上使用属性itemSize设置每行数据占据的高度,并且给其设置最大高度或高度,另外在div.ti3-table-container > table > tbody > tr上用 + cdkVirtualFor结构指令替换原有的ngFor
    需导入:import { ScrollingModule } from '@angular/cdk/scrolling';

    `, + 'en-US': '' + } + }, + { + demoId: 'table-actionmenu', + name: { + 'zh-CN': '操作列', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    通过ti-actionmenu组件实现最后一列操作列,具体可查看 Actionmenu 组件示例及 API。
    需导入: + import { TiActionmenuModule } from '@opentiny/ng';

    `, + 'en-US': '' + } + }, + { + demoId: 'table-editrow', + name: { + 'zh-CN': '可编辑行', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    自由编辑行内容,开发者可以参照该示例根据自己需求自由定制表格的行编辑功能。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-editall', + name: { + 'zh-CN': '可编辑表格', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    自由编辑表格全部内容,开发者可以参照该示例根据自己需求自由定制表格的编辑功能。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-row-drag2', + name: { + 'zh-CN': '行拖拽排序', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    使用H5的拖拽能力。在dragstart事件中获取当前拖拽元素起始下标,dragover事件中获取拖拽终止下标,drop + 事件中根据拖拽起始和终止下标操作移动数据。

    `, + 'en-US': '' + } + }, + { + demoId: 'table-tree', + name: { + 'zh-CN': '深度确定的树形数据展示', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    深度确定的树形数据可利用树形结构的HTML展示,开发者可以参照该示例根据自己需求自由展示树形数据。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-tree-unknowdeepth', + name: { + 'zh-CN': '深度不确定的树形数据展示', + 'en-US': '' + }, + desc: { + 'zh-CN': + '

    深度不确定的树形数据,将其转换成扁平化数据之后再展示,本示例中提供了树形结构与扁平化数据之间的转换函数,实际业务中请根据需求修改。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-storage', + name: { + 'zh-CN': '排序、分页和列宽状态缓存', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    排序和分页的当前页码是SessionStorage缓存,分页每页显示几条和拖拽调整后的列宽是LocalStorage缓存。通过属性storageId配置 + 缓存表格状态的唯一标志值,一旦配置了storageId,那么表格各状态就会缓存,如果想使某一状态不缓存,可使用属性storageConfig进行配置。 +
    使用分页功能需导入:import { TiPaginationModule } from '@opentiny/ng';

    `, + 'en-US': '' + }, + apis: ['TiTableComponent.properties.storageId', 'TiTableComponent.properties.storageConfig'] + } + ], + ignoreApis: [ + 'TiHeadFilterComponent.properties.appendToBody', + 'TiHeadFilterComponent.properties.clearable', + 'TiHeadFilterComponent.properties.maxLine', + 'TiHeadFilterComponent.properties.placeholder', + 'TiHeadFilterComponent.properties.reserveSearchword', + 'TiHeadFilterComponent.properties.searchKeys', + 'TiHeadFilterComponent.properties.selectedNumberTipPosition', + 'TiHeadFilterComponent.properties.selectedTipPosition', + 'TiHeadFilterComponent.properties.showSelectedNumber', + 'TiHeadFilterComponent.properties.showSelectedNumberTip', + 'TiHeadFilterComponent.properties.tipMaxWidth', + 'TiHeadFilterComponent.properties.tipPosition', + 'TiHeadFilterComponent.properties.disabled', + 'TiHeadFilterComponent.properties.tabindex', + 'TiHeadFilterComponent.events.beforeOpen', + 'TiHeadFilterComponent.events.beforeSearch', + 'TiHeadFilterComponent.events.clear', + 'TiHeadFilterComponent.events.scrollToBottom', + 'TiHeadFilterComponent.events.blur', + 'TiHeadFilterComponent.events.focus', + 'TiHeadFilterComponent.slots.footerTemplate', + 'TiHeadFilterComponent.slots.placeholderTemplate', + 'TiHeadFilterComponent.slots.selectedTemplate', + 'TiHeadFilterComponent.methods.getSearchResult', + 'TiHeadFilterComponent.methods.getSearchWord', + 'TiHeadFilterComponent.methods.open', + 'TiHeadFilterComponent.methods.blur', + 'TiHeadFilterComponent.methods.focus', + 'TiColsToggleComponent.properties.appendToBody', + 'TiColsToggleComponent.properties.clearable', + 'TiColsToggleComponent.properties.labelKey', + 'TiColsToggleComponent.properties.maxLine', + 'TiColsToggleComponent.properties.multiple', + 'TiColsToggleComponent.properties.options', + 'TiColsToggleComponent.properties.panelMaxHeight', + 'TiColsToggleComponent.properties.placeholder', + 'TiColsToggleComponent.properties.reserveSearchword', + 'TiColsToggleComponent.properties.searchKeys', + 'TiColsToggleComponent.properties.selectedNumberTipPosition', + 'TiColsToggleComponent.properties.selectedTipPosition', + 'TiColsToggleComponent.properties.showSelectedNumber', + 'TiColsToggleComponent.properties.showSelectedNumberTip', + 'TiColsToggleComponent.properties.tipMaxWidth', + 'TiColsToggleComponent.properties.virtual', + 'TiColsToggleComponent.properties.valueKey', + 'TiColsToggleComponent.properties.tabindex', + 'TiColsToggleComponent.properties.idKey', + 'TiColsToggleComponent.events.beforeOpen', + 'TiColsToggleComponent.events.beforeSearch', + 'TiColsToggleComponent.events.clear', + 'TiColsToggleComponent.events.scrollToBottom', + 'TiColsToggleComponent.slots.footerTemplate', + 'TiColsToggleComponent.slots.placeholderTemplate', + 'TiColsToggleComponent.slots.selectedTemplate', + 'TiColsToggleComponent.slots.itemTemplate', + 'TiColsToggleComponent.methods.getSearchResult', + 'TiColsToggleComponent.methods.getSearchWord', + 'TiColsToggleComponent.methods.open', + 'TiColsToggleComponent.methods.blur', + 'TiColsToggleComponent.methods.focus' + ] +}; diff --git a/src/table/demo/src/app/table/webdoc/table.cn.md b/src/table/demo/src/app/table/webdoc/table.cn.md new file mode 100644 index 0000000..3d7927c --- /dev/null +++ b/src/table/demo/src/app/table/webdoc/table.cn.md @@ -0,0 +1,44 @@ +--- +title: Table 表格 +--- +# Table 表格 + +
    + +展示行列数据。   + ++ 当有大量结构化的数据需要展现时; + ++ 当需要对数据进行排序、过滤、分页、自定义操作等复杂行为时。 + +```typescript +import { TiTableModule } from '@opentiny/ng'; +``` + +如需使用单元格带提示的省略功能,请导入: + +```typescript +import { TiOverflowModule } from '@opentiny/ng'; +``` + +
    + +
    + +展示行列数据。   + ++ 当有大量结构化的数据需要展现时; + ++ 当需要对数据进行排序、过滤、分页、自定义操作等复杂行为时。 + +```typescript +import { TiTableModule } from '@opentiny/ng'; +``` + +如需使用单元格带提示的省略功能,请导入: + +```typescript +import { TiOverflowModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/table/demo/src/app/table/webdoc/table.en.md b/src/table/demo/src/app/table/webdoc/table.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/table/demo/src/app/table/webdoc/table.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/table/demo/src/favicon.ico b/src/table/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/table/demo/src/index.html b/src/table/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/table/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/table/demo/src/main.ts b/src/table/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/table/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/table/demo/test.ts b/src/table/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/table/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/table/demo/tsconfig.app.json b/src/table/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/table/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/table/demo/tsconfig.spec.json b/src/table/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/table/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/table/lib/index.ts b/src/table/lib/index.ts new file mode 100644 index 0000000..ee258f5 --- /dev/null +++ b/src/table/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTableModule'; diff --git a/src/table/lib/ng-package.json b/src/table/lib/ng-package.json new file mode 100644 index 0000000..973474d --- /dev/null +++ b/src/table/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/table", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/table/lib/package.json b/src/table/lib/package.json new file mode 100644 index 0000000..007c783 --- /dev/null +++ b/src/table/lib/package.json @@ -0,0 +1,28 @@ +{ + "name": "@opentiny/ng-table", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-select": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-dropsearch": "~1.0.0-beta.0", + "@opentiny/ng-list": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-datebase": "~1.0.0-beta.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@angular/cdk/scrolling": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-date": "~1.0.0-beta.0", + "@opentiny/ng-datetime": "~1.0.0-beta.0", + "@opentiny/ng-searchbox": "~1.0.0-beta.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/table/lib/project.json b/src/table/lib/project.json new file mode 100644 index 0000000..76cc3d8 --- /dev/null +++ b/src/table/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/table/lib", + "sourceRoot": "src/table/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/table"], + "options": { + "project": "src/table/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/table"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js table" + }, + { + "command": "ng default-build table" + }, + { + "command": "node build/clear-default-theme.js table" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/table && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build table && ng pack table && node build/publish.js table --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/table/lib/src/TiCellIconsComponent.ts b/src/table/lib/src/TiCellIconsComponent.ts new file mode 100644 index 0000000..0800c61 --- /dev/null +++ b/src/table/lib/src/TiCellIconsComponent.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * TiCellIcons 表格单元格中放置图标的容器组件 + * + * 配合TiCellText组件使用可使表格单元格中内容显示不下时,文本出省略号..., + * 而文本后的放在TiCellIcons图标始终可以显示出来 + * + */ +@Component({ + selector: 'ti-cell-icons', + templateUrl: './cell-icons.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-cell-icons-container]': 'true' + } +}) +export class TiCellIconsComponent {} diff --git a/src/table/lib/src/TiCellTextComponent.ts b/src/table/lib/src/TiCellTextComponent.ts new file mode 100644 index 0000000..db44514 --- /dev/null +++ b/src/table/lib/src/TiCellTextComponent.ts @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterViewInit, Component, ElementRef, Input, OnInit, Renderer2, ViewChild, ChangeDetectionStrategy } from '@angular/core'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiPositionType } from '@opentiny/ng-utils'; + +/** + * TiCellText 表格单元格中放置文本的容器组件 + * + * 提供文本溢出时出省略号...,且hover时出tip的功能。 + * + * 配合TiCellText组件使用可使表格单元格中内容显示不下时,文本出省略号..., + * 而文本后的放在TiCellIcons图标始终可以显示出来 + * + */ +@Component({ + selector: 'ti-cell-text', + templateUrl: './cell-text.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-cell-text-container]': 'true' + } +}) +export class TiCellTextComponent implements AfterViewInit { + /** + * 文本超长显示时 tip 内容 + */ + @Input() tipContent: string; + /** + * 文本超长显示时 tip 位置 + */ + @Input() tipPosition: TiPositionType; + @ViewChild('text', { static: true }) private cellTextEle: ElementRef; + constructor(private renderer: Renderer2, private elementRef: ElementRef, private tiRenderer: TiRenderer) {} + + ngAfterViewInit(): void { + // 保证有图标时,图标渲染完再计算宽度:可看该测试用例table-cellicons-colsresizable + setTimeout(() => { + this.setMaxWidth(); + }, 0); + } + + private setMaxWidth(): void { + let otherWidth: number = 0; + const parent: Element = this.renderer.parentNode(this.elementRef.nativeElement); + const iconsContainer: Element = this.tiRenderer.findChildrenByClassName(parent, 'ti3-cell-icons-container')[0]; + const sortContainer: Element = this.tiRenderer.findChildrenByClassName(parent, 'ti3-sort-container')[0]; + const headFilterContainer: Element = this.tiRenderer.findChildrenByClassName(parent, 'ti3-head-filter-container')[0]; + const renameContainer: Element = this.tiRenderer.findChildrenByTagName(parent, 'TP-RENAME')[0]; + + // 修复SSR报错:ERROR TypeError: sortContainer.getBoundingClientRect is not a function + if (typeof parent.getBoundingClientRect !== 'function') { + return; + } + // 修复SSR报错:ERROR TypeError: Cannot read property 'getBoundingClientRect' of undefined + if (iconsContainer) { + const iconsContainerWidth: number = parseFloat(iconsContainer.getBoundingClientRect().width.toFixed(1)); + otherWidth += iconsContainerWidth; + } + + if (sortContainer) { + const sortContainerWidth: number = parseFloat(sortContainer.getBoundingClientRect().width.toFixed(1)); + otherWidth += sortContainerWidth; + } + + if (headFilterContainer) { + const headFilterContainerWidth: number = parseFloat(headFilterContainer.getBoundingClientRect().width.toFixed(1)); + otherWidth += headFilterContainerWidth; + } + + if (renameContainer) { + const renameContainerWidth: number = parseFloat(renameContainer.getBoundingClientRect().width.toFixed(1)); + otherWidth += renameContainerWidth; + } + + this.renderer.setStyle(this.cellTextEle.nativeElement, 'max-width', `calc(100% - ${otherWidth}px)`); + } +} diff --git a/src/table/lib/src/TiColClickDirective.ts b/src/table/lib/src/TiColClickDirective.ts new file mode 100644 index 0000000..405a2f1 --- /dev/null +++ b/src/table/lib/src/TiColClickDirective.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterViewInit, Directive, ElementRef, Renderer2 } from '@angular/core'; +/** + * 根据规范:“单/复选框”关联表格使用时,为了提升选中效率,点击热区为“单/复选框”按钮所在单元格,点击即可选中。 + */ +@Directive({ + selector: 'td[checkbox-column], th[checkbox-column], td[radio-column]' +}) +export class TiColClickDirective implements AfterViewInit { + constructor(private renderer: Renderer2, private elementRef: ElementRef) {} + ngAfterViewInit(): void { + this.renderer.listen(this.elementRef.nativeElement, 'click', (event: MouseEvent): void => { + // 全选复选框右侧有下拉操作图标时,不能做热区放大 + if (this.elementRef.nativeElement.tagName === 'TH' && this.elementRef.nativeElement.querySelector('ti-head-menu')) { + return; + } + const inputEle: any = this.elementRef.nativeElement.querySelector('input'); + const checkboxLabelEle: any = this.elementRef.nativeElement.querySelector('.ti3-checkbox'); + const radioLabelEle: any = this.elementRef.nativeElement.querySelector('.ti3-radio'); + // 点击单/复选空白区域时,触发input点击事件 + // 表格和单/复选联用时,checkboxLabelEle和radioLabelEle不会同时出现,所以需要分别进行判断 + // inputEle和checkboxLabelEle/radioLabelEle是兄弟元素 + // event.target !== inputEle阻止在firefox下因为inputEle.click()产生的冒泡循环 + if ( + event.target !== inputEle && + ((checkboxLabelEle && !(event.target === checkboxLabelEle || checkboxLabelEle.contains(event.target))) || + (radioLabelEle && !(event.target === radioLabelEle || radioLabelEle.contains(event.target)))) + ) { + inputEle.click(); + } + }); + } +} diff --git a/src/table/lib/src/TiColsResizableDirective.ts b/src/table/lib/src/TiColsResizableDirective.ts new file mode 100644 index 0000000..f90f588 --- /dev/null +++ b/src/table/lib/src/TiColsResizableDirective.ts @@ -0,0 +1,604 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Directive, + DoCheck, + ElementRef, + IterableChanges, + IterableDiffer, + IterableDiffers, + NgZone, + OnDestroy, + OnInit, + Renderer2, + Inject, + Input +} from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Subscription } from 'rxjs'; + +import { TiTableColumns, TiTableComponent } from './TiTableComponent'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { Util } from '@opentiny/ng-utils'; + +import { TiTableFixedHeadService } from './TiTableFixedHeadService'; + +/** + * @ignore + */ +export interface ResizableOpts { + table: any; + ths: Array; + wrap: any; // 包裹表格的父容器,拖动表格超过父容器时出滚动条 + mouseXPosition: number; // 光标位置,列拖动时使用 + target: any; // 拖动的当前列 + storedSizes: Array; // 用来实时保存各列宽度 + storedSortEleSizes: Array; // 用来保存各列sort元素的宽度, + storeTableWidthChange: number; + secondTable?: any; + secondThs?: Array; + xScrollState?: boolean; + yScrollState?: boolean; + isYOverflowedWithX?: boolean; // 表头锁定时,列拖动过程中横向滚动条的出现导致纵向滚动条出现的标志位 +} +/** + * TiColsResizable 表格列拖动指令 + * + * 支持拖动表格列来调整各列宽度。 + * + * 在ti-table标签上加了tiColsResizable属性,则表格自动具有列拖动功能; + * 同时还需要对table元素包裹父容器div,并对父容器设置 ti3-resize-wrapper样式类。 + */ +@Directive({ + selector: 'ti-table[tiColsResizable]' +}) +export class TiColsResizableDirective implements OnInit, DoCheck, OnDestroy { + /** + * @ignore + */ + public static readonly UNSELECTABLE_CLASS: string = 'ti3-unselectable'; + /** + * @ignore + * 移到表头能够显示拖动线的感应范围 + */ + public static readonly COL_BORDER_RANGE: number = 10; + /** + * @ignore + */ + public readonly notResizableAttr: string = 'not-resizable'; + /** + * 是否开启列拖动功能 + */ + @Input('tiColsResizable') isColsResizable: boolean | ''; + /** + * @ignore + */ + public resizableOpts: ResizableOpts = { + table: null, + ths: null, + wrap: null, + mouseXPosition: 0, + target: null, + storedSizes: [], + storedSortEleSizes: [], + storeTableWidthChange: 0, + isYOverflowedWithX: false + }; // 初始化列拖动属性的配置项 + private hostEle: Element; + private documentMouseMoveListener: () => void; + private documentMouseUpListener: () => void; + private windowResizeListener: () => void; + private isDragStart: boolean = false; + private columsDiffer: IterableDiffer; + private subscription: Subscription; + private isColumnsInit: boolean = true; + /** + * @ignore + */ + public isfirstYScrollStateChange: boolean = true; + /** + * @ignore + */ + public tableCom: TiTableComponent; + constructor( + tableCom: TiTableComponent, + private renderer: Renderer2, + private elementRef: ElementRef, + private zone: NgZone, + private tiRenderer: TiRenderer, + private iterableDiffers: IterableDiffers, + private fixedHeadService: TiTableFixedHeadService, + @Inject(DOCUMENT) private document + ) { + this.hostEle = this.elementRef.nativeElement; + this.tableCom = tableCom; + } + + private static getThs(table: Element): Array { + return Array.from(table.children[0].children[0].children); + } + + private static getWidth(element: any): number { + return parseFloat(element.getBoundingClientRect().width.toFixed(1)); + } + + private static trackByFn(index: number, item: { show?: boolean }): boolean { + return item.show; + } + + ngOnInit(): void { + if (this.isColsResizable === false) { + return; + } + this.subscription = this.tableCom.viewInitSubject.subscribe(() => { + this.processColsResizable(); + this.isColumnsInit = false; // 主要处理 columns 初始为空,异步的场景 + // 表格记忆 + // 版本更新后用户第一次访问,localStorageState['colsWidth']不存在 + // 修复SSR错误:ERROR TypeError: Cannot read property 'colsWidth' of undefined + if ( + this.tableCom.storageId && + this.tableCom.storageConfig && + this.tableCom.storageConfig.colsWidth && + this.tableCom.localStorageState && + this.tableCom.localStorageState['colsWidth'] + ) { + this.initStorageColsWidth(); + } + }); + + this.columsDiffer = this.iterableDiffers.find(this.tableCom.columns).create<{ show?: boolean }>(TiColsResizableDirective.trackByFn); + } + + ngDoCheck(): void { + if (this.isColsResizable === false) { + return; + } + const columsDiffer: IterableChanges = this.columsDiffer.diff(this.tableCom.columns); + if (columsDiffer) { + if (this.isColumnsInit) { + this.isColumnsInit = false; + + return; + } + this.zone.runOutsideAngular(() => { + setTimeout(() => { + this.setDefaultWidth(); + }, 0); + }); + } + } + + ngOnDestroy(): void { + if (this.subscription) { + this.subscription.unsubscribe(); + } + if (this.documentMouseMoveListener) { + this.documentMouseMoveListener(); + } + if (this.documentMouseUpListener) { + this.documentMouseUpListener(); + } + if (this.windowResizeListener) { + this.windowResizeListener(); + } + } + + private processColsResizable(): void { + this.setTableWidthChange(); + const table: any = (this.resizableOpts.table = this.getTable()); + const thead: any = table.children[0]; + if (this.tableCom.isFixedHead) { + this.resizableOpts.secondTable = this.getSecondTable(); + this.resizableOpts.xScrollState = this.overflowedX(); + this.resizableOpts.yScrollState = this.overflowedY(); + this.resizableOpts.ths = TiColsResizableDirective.getThs(table); + this.resizableOpts.secondThs = TiColsResizableDirective.getThs(this.resizableOpts.secondTable); + this.tableCom.processYScrollStateChangeWithColsResizable = (): void => { + this.fixedHeadService.processYScrollStateChange(this); + }; + } + this.zone.runOutsideAngular(() => { + // 避免不停触发变化检测 + this.renderer.listen(thead, 'mousemove', (event: MouseEvent) => { + const currentTh: any = event.target; + + if (currentTh['nodeName'] !== 'TH') { + return; + } + + // 获取当前th距页面文档的left值 + const left: number = currentTh['getBoundingClientRect']().left + this.document.documentElement.scrollLeft; + const thWidth: number = currentTh['getBoundingClientRect']().width; + // 判断光标是否落在th的右边缘 + const isOnColBorder: boolean = Math.abs(event.pageX - Math.round(left + thWidth)) <= TiColsResizableDirective.COL_BORDER_RANGE; + + if (isOnColBorder) { + if (event.target['hasAttribute'](this.notResizableAttr) || this.isLastColumn(currentTh)) { + return; + } + this.createDividingLine(currentTh); + } else { + this.removeDividingLine(); + } + }); + this.renderer.listen(thead, 'mouseleave', () => { + this.removeDividingLine(); + }); + this.renderer.listen(thead, 'mousedown', (event: MouseEvent) => { + const currentTh: any = event.target; + if (this.tiRenderer.hasClass(currentTh, 'col-resize-active')) { + this.isDragStart = true; + // 鼠标点击的当前列 + this.resizableOpts.target = currentTh; + // 更新光标位置 + this.resizableOpts.mouseXPosition = event.pageX; + // 在拖动前获取当前表格最新的列信息、宽度,很关键,使得设置宽度与样式实际宽度一直 + this.updateTableInfo(); + // 给页面设置不可选样式,避免拖动时页面或表格内部出现被选中的蓝色区域 + this.toggleTextSelection(true); + } + + if (this.tableCom.isFixedHead) { + this.resizableOpts.xScrollState = this.overflowedX(); + this.resizableOpts.yScrollState = this.overflowedY(); + } + }); + + const tableContainer: any = this.tiRenderer.findChildrenByClassName(this.hostEle, 'ti3-resize-wrapper')[0]; + if (!this.tiRenderer.hasClass(tableContainer, 'ti3-table-container')) { + this.renderer.listen(tableContainer, 'scroll', () => { + Util.trigger(this.document, 'tiScroll'); + }); + } + + this.documentMouseMoveListener = this.renderer.listen(this.document, 'mousemove', (event: MouseEvent) => { + // 列拖动的动作应该是先mousedown,然后mousemove,因此先判断是否已经触发了mousedown + if (!this.isDragStart || this.resizableOpts.mouseXPosition === 0 || !this.resizableOpts.target) { + return; + } + + this.mouseMove(event); + }); + this.documentMouseUpListener = this.renderer.listen(this.document, 'mouseup', (event: MouseEvent) => { + if (!this.isDragStart) { + return; + } + + this.toggleTextSelection(false); // 恢复页面可选样式 + this.stopResize(); // 保存最新宽度到浏览器中 + this.isDragStart = false; + }); + + // 修复SSR报错:ERROR ReferenceError: window is not defined + if (typeof window === 'undefined') { + return; + } + this.windowResizeListener = this.renderer.listen(window, 'resize', () => { + this.setTableWidthChange(); + }); + }); + } + + private mouseMove(event: MouseEvent): void { + const options: ResizableOpts = this.resizableOpts; + const lastColIndex: number = options.ths.length - 1; + const curColIndex: number = parseInt(options.target.getAttribute('ti-visible-index'), 10); + const colWidth: number = options.storedSizes[curColIndex]; + const leftEdge: number = parseFloat((event.pageX - options.mouseXPosition).toFixed(1)); + const minWidth: number = this.getColMinWidth(options.target); + + // 当拖拽方向为列宽度减小的方向且列宽已达到文本区最小宽度时阻止拖动 + if (leftEdge >= 0 || colWidth + leftEdge > Math.ceil(minWidth)) { + // 更新拖动列宽度 + options.storedSizes[curColIndex] += leftEdge; + this.setWidth(options.target, options.storedSizes[curColIndex]); + + // 对最后一列的列宽的整体处理方案: + // 如果更新拖动列宽度后的表格的总宽度小于表格初始宽度,则将宽度差补偿到最后一列; + // 如果更新拖动列宽度后的表格的总宽度大于表格初始宽度,且之前最后一列列宽有补偿时, + // 将最后一列的补偿先抵消掉,然后有超出时再出滚动条 + + // s: storeTableWidthChange(此次拖动前各列拖动值的累计和) + // b: 此次拖动前给末列的补偿值 + // l: leftEdge(此次拖动的距离) + // s l s + l b 此次拖动后对末列宽的设置 + // -5 -1 -6 < 0 +5 +1(-leftEdge) + // -5 +3 -2 < 0 +5 -3(-leftEdge) + // +5 -8 -3 < 0 0 +3(-(storeColsWidthChange+leftEdge)) + // -5 +8 3 > 0 +5 -5(+storeColsWidthChange) + // +5 +3 8 > 0 0 +0(不做处理) + // +5 -3 2 > 0 0 +0(不做处理) + if (leftEdge + options.storeTableWidthChange < 0) { + if (options.storeTableWidthChange < 0) { + options.storedSizes[lastColIndex] -= leftEdge; + } else { + options.storedSizes[lastColIndex] -= leftEdge + options.storeTableWidthChange; + } + } else { + if (options.storeTableWidthChange < 0) { + options.storedSizes[lastColIndex] += options.storeTableWidthChange; + } + } + + this.setWidth(options.ths[lastColIndex], options.storedSizes[lastColIndex]); + + // 更新各列拖动相对值的累计和 + options.storeTableWidthChange += leftEdge; + + // 表头锁定 + if (this.tableCom.isFixedHead) { + // 更新第二个表格(bodyTable)中的当前列和最后一列的宽度 + this.setWidth(options.secondThs[curColIndex], options.storedSizes[curColIndex]); + this.setWidth(options.secondThs[options.secondThs.length - 1], options.storedSizes[lastColIndex]); + + this.fixedHeadService.handleYNotOverflowedWithX(this.resizableOpts, this.tableCom.tbodyContainer); + + // 当横向滚动条出现导致纵向滚动条出现时对账本做处理 + this.fixedHeadService.handleYOverflowedWithX(this.resizableOpts, this.tableCom.tbodyContainer); + + // 纵向滚动条状态变化时对固定的表头右边填充块的处理 + this.fixedHeadService.processOverflowY(this.tableCom.theadContainer, this.tableCom.tbodyContainer, this.tableCom); + } + + if (this.tableCom.fixedColumnInfo.hasFixedColumn) { + this.tableCom.thResizeSubject.next({ + th: options.target, + leftEdge, + resizableOpts: this.resizableOpts + }); + const container: any = this.tableCom.fixedColumnInfo.container; + if (container) { + const scrollLeft: number = container.scrollLeft; + const isRightColumnFloat: boolean = scrollLeft + container.clientWidth < container.scrollWidth; + this.tableCom.containerScrollXChangeSubject.next({ + scrollLeft, + isRightColumnFloat + }); + } + } + } + + // 更新光标位置 + options.mouseXPosition = event.pageX; + } + + private getColMinWidth(thElement: any): number { + // 此处的值是文本区所占最小宽度;当容器宽度小于此值时,文本会完全显示,但超出部分会直接被截断。 + // Chrome 和 IE下省略号'...'所占宽度大概为10px; + // Firefox 下出现省略号'...'所占宽度大概为49px(中文字符); + // 此处为保证三个浏览器都表现正常,并根据规范4.0取值50(49 + 1, 1为矫正偏差值)。 + let minWidth: number = 50; + + minWidth += parseFloat(window.getComputedStyle(thElement).paddingLeft) + parseFloat(window.getComputedStyle(thElement).paddingRight); + const sortEle: any = this.tiRenderer.findChildrenByClassName(thElement, 'ti3-sort-container')[0]; + const iconsEle: any = this.tiRenderer.findChildrenByClassName(thElement, 'ti3-cell-icons-container')[0]; + const headFilterEle: any = this.tiRenderer.findChildrenByClassName(thElement, 'ti3-head-filter-container')[0]; + + if (sortEle) { + const sortWidth: number = parseFloat(sortEle.getBoundingClientRect().width.toFixed(1)); + minWidth += sortWidth; + } + + if (iconsEle) { + const filterWidth: number = parseFloat(iconsEle.getBoundingClientRect().width.toFixed(1)); + minWidth += filterWidth; + } + + if (headFilterEle) { + const headFilterWidth: number = parseFloat(headFilterEle.getBoundingClientRect().width.toFixed(1)); + minWidth += headFilterWidth; + } + + return minWidth; + } + + private setTableWidthChange(): void { + const tableContainer: any = this.tiRenderer.findChildrenByClassName(this.hostEle, 'ti3-resize-wrapper')[0]; + // 表格容器自身宽度(不包括Y方向滚动条的宽度) + const clientWidth: number = tableContainer ? tableContainer['clientWidth'] : 0; + // 表格容器实际内容区域宽度 + const scrollWidth: number = tableContainer ? tableContainer['scrollWidth'] : 0; + + // resizableOpts.storeColsWidthChange为各列拖动相对值的累计和 + // 将表格内容溢出部分的宽度赋给各列拖动相对值的累计和 + this.resizableOpts.storeTableWidthChange = scrollWidth - clientWidth; + } + + private stopResize(): void { + this.updateStoredSizes(); + this.resizableOpts.mouseXPosition = 0; + this.resizableOpts.target = null; + if (this.tableCom.isFixedHead) { + this.resizableOpts.xScrollState = this.overflowedX(); + this.resizableOpts.yScrollState = this.overflowedY(); + } + } + + private getTable(): any { + // 用户给table元素加父容器,通过父容器设置overflow属性 + const tableContainerClassName: string = this.tableCom.isFixedHead ? 'ti3-table-fixed-head' : 'ti3-resize-wrapper'; + const tableContainer: any = this.tiRenderer.findChildrenByClassName(this.hostEle, tableContainerClassName)[0]; + const table: any = tableContainer.children[0].nodeName === 'TABLE' ? tableContainer.children[0] : null; + + return table; + } + + /** + * @ignore + */ + public isLastColumn(th: any): boolean { + const parentElement: any = th.parentElement; + if (!parentElement) { + return true; + } + const ths: any = th.parentElement.children; + const index: number = Array.from(ths).indexOf(th); + + return index === ths.length - 1; + } + + // 当光标移到感应区时,出现拖动分割线 + private createDividingLine(th: any): void { + this.renderer.addClass(th, 'col-resize-active'); + + const index: number = Array.from(th.parentElement.children).indexOf(th); + const bodyTable: any = this.tableCom.isFixedHead ? this.resizableOpts.secondTable : this.resizableOpts.table; + // 防止不存在tbody的场景报错 + if (!bodyTable.children[1]) { + return; + } + const trs: Array = Array.from(bodyTable.children[1].children).filter((tr: any) => { + return this.needDividingLine(tr); + }); + trs.forEach((tr: any) => { + // 分组场景index 大于tr.children.length + if (!tr.children[index]) { + return; + } + this.renderer.addClass(tr.children[index], 'col-resize-active'); + }); + } + + // 当光标离开感应区时,拖动分割线消失 + private removeDividingLine(): void { + const bodyTable: any = this.tableCom.isFixedHead ? this.resizableOpts.secondTable : this.resizableOpts.table; + const ths: Array = Array.from(this.resizableOpts.table.children[0].children[0].children); + + ths.forEach((th: Element) => { + this.renderer.removeClass(th, 'col-resize-active'); + }); + + // 防止不存在tbody的场景报错 + if (!bodyTable.children[1]) { + return; + } + const trs: Array = Array.from(bodyTable.children[1].children); + trs.forEach((tr: Element) => { + if (!this.needDividingLine(tr)) { + return; + } + Array.from(tr.children).forEach((td: any) => { + this.renderer.removeClass(td, 'col-resize-active'); + }); + }); + } + + private updateTableInfo(): void { + this.resizableOpts.ths = TiColsResizableDirective.getThs(this.resizableOpts.table); + this.updateStoredSizes(); // 保存最新宽度 + this.setComputedWidth(); // 设置最新宽度 + } + + private initStorageColsWidth(): void { + this.resizableOpts.ths = TiColsResizableDirective.getThs(this.resizableOpts.table); + this.resizableOpts.storedSizes = this.tableCom.localStorageState['colsWidth']; + this.setComputedWidth(); + // 各列宽度设置后,可能出现滚动条,所以要计算一次 storeTableWidthChange的值 + this.setTableWidthChange(); + } + + /** + * @ignore + * 将当前表格各列的宽度更新到到resizableOpts.storedSizes中 + */ + public updateStoredSizes = (): void => { + this.resizableOpts.storedSizes = []; + this.resizableOpts.ths.forEach((th: any, index: number) => { + this.tiRenderer.setAttributes(th, { 'ti-visible-index': index }); + this.resizableOpts.storedSizes[index] = TiColsResizableDirective.getWidth(th); + }); + // 更新表格记忆 + if (this.tableCom.storageId && this.tableCom.storageConfig.colsWidth) { + this.tableCom.localStorageState['colsWidth'] = this.resizableOpts.storedSizes; + } + }; + + private setComputedWidth(): void { + if (this.resizableOpts.table && this.resizableOpts.ths && this.resizableOpts.ths.length) { + this.resizableOpts.ths.forEach((th: any, index: number) => { + this.setWidth(th, this.resizableOpts.storedSizes[index]); + // 表头锁定 + if (this.tableCom.isFixedHead) { + this.resizableOpts.secondThs = TiColsResizableDirective.getThs(this.resizableOpts.secondTable); + this.setWidth(this.resizableOpts.secondThs[index], this.resizableOpts.storedSizes[index]); + } + }); + } + } + + /** + * @ignore + */ + public setWidth = (element: any, width: number): void => { + this.renderer.setStyle(element, 'width', width + 'px'); + }; + + // 将表格宽度设置为默认宽度 + private setDefaultWidth(): void { + const ths: Array = (this.resizableOpts.ths = TiColsResizableDirective.getThs(this.resizableOpts.table)); + // 列隐藏或显示后,将各列宽设置为初始用户设置的列宽 + ths.forEach((th: any) => { + const width: string = th.getAttribute('width') ? th.getAttribute('width') : ''; + // 注意:设置列宽的方式有多种,此处统一默认以width属性设置的宽为准 + this.renderer.setStyle(th, 'width', width); + }); + + // 表头锁定 + if (this.tableCom.isFixedHead) { + const secondTable: any = this.getSecondTable(); + const secondThs: Array = TiColsResizableDirective.getThs(secondTable); + this.resizableOpts.secondThs = secondThs; + this.resizableOpts.xScrollState = this.overflowedX(); + this.resizableOpts.yScrollState = this.overflowedY(); + + secondThs.forEach((th: any) => { + const width: string = th.getAttribute('width') ? th.getAttribute('width') : ''; + // 注意:设置列宽的方式有多种,此处统一默认以width属性设置的宽为准 + this.renderer.setStyle(th, 'width', width); + }); + this.fixedHeadService.processYScrollStateChange(this); + } + this.setTableWidthChange(); + } + + // 当列拖动进行时去掉文字可选样式(user-select: none) + private toggleTextSelection(toggle: boolean): void { + const body: any = this.document.body; + if (toggle) { + this.renderer.addClass(body, 'ti3-unselectable'); + this.renderer.setAttribute(body, 'unselectable', 'on'); + } else { + this.renderer.removeClass(body, 'ti3-unselectable'); + body.removeAttribute('unselectable'); + } + } + + private needDividingLine(tr: any): boolean { + return this.tableCom.needTr(tr); + } + + // 供表头锁定使用 + private getSecondTable(): Element { + const secondTableContainer: any = this.tiRenderer.findChildrenByClassName(this.hostEle, 'ti3-resize-wrapper')[0]; + + return secondTableContainer ? secondTableContainer.children[0] : null; + } + + private overflowedX(isNum?: boolean): any { + return this.fixedHeadService.overflowedResult(this.tableCom.tbodyContainer, 'X', isNum); + } + + private overflowedY(isNum?: boolean): any { + return this.fixedHeadService.overflowedResult(this.tableCom.tbodyContainer, 'Y', isNum); + } +} diff --git a/src/table/lib/src/TiColsToggleComponent.ts b/src/table/lib/src/TiColsToggleComponent.ts new file mode 100644 index 0000000..27b8e79 --- /dev/null +++ b/src/table/lib/src/TiColsToggleComponent.ts @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ElementRef, + Input, + Renderer2, + SimpleChanges, + ViewChild, + ChangeDetectorRef, + ChangeDetectionStrategy +} from '@angular/core'; +import { TiTableComponent } from './TiTableComponent'; +import { TiColsToggleDropComponent } from './TiColsToggleDropComponent'; +import { TiSelectComponent } from '@opentiny/ng-select'; +import { TiKeymap, TiPositionType } from '@opentiny/ng-utils'; +import { TiLocale } from '@opentiny/ng-locale'; +import packageInfo from '../package.json'; +// 下面注释,可以避免编译lib时正则报错。原理未知,副作用未知。 +// @dynamic +/** + * TiColsToggle 控制列动态隐藏/显示的组件 + * + * ti-cols-toggle 用来显示列操作按钮,点击该按钮可在打开的下拉中设置各列的隐藏/显示; + * 在 ti-table 标签内,开发者可灵活设置其位置。 + * + * TiTable 上的 columns 接口传入值各项(列)的 show 和 title 属性值影响着各列的隐藏/显示状 + * 态,具体可参考 TiTableComponent columns 输入接口说明。 + * + * 开发者必须给需要动态显示/隐藏的列(th,td)使用 ngIf, 利用 ngIf 和 tiTable 上的 columns + * 接口值中每列的show的值来控制当前列的显示或隐藏。 + * + * ### 接口说明 + * **Inputs:** + * + * | 名称 | 类型 | 默认值 | 功能描述 | + * | -------- | :----- | :---- | :---- | + * | disabled | boolean | false | 设置列操作按钮是否禁用 | + * | searchable | boolean | false | 是否显示搜索框 | + * | selectAll | boolean | false | 是否开启全选功能| + * | panelWidth | string | 'justified' | 下拉面板的宽度,可选值为'justified'(默认), 'auto'或自定义宽度,但宽度不能小于select面板的宽度 | + * | noDataText | string | '暂无数据' | 无数据时的显示文本。默认值改为国际化词条 | + * | select | EventEmitter | 无 | 选中/取消选中事件,向外通知当前项数据,需要自行判断当前项是否选中| + * | focus | HTML事件 | 无 | 聚焦事件 | + * | blur | HTML事件 | 无 | 失焦事件 | + * + *

    以下说明不可用,不做参考。该组件只有以上disabled、searchable、panelWidth、selectAll 这四个Input接口 + * 和foucs、blur、select 这三个事件接口可用,其余方法、Input、输出等(继承于TiSelectComponent,是供select的内部使用的)都不可用

    + */ +@Component({ + selector: 'ti-cols-toggle', + templateUrl: './cols-toggle.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiColsToggleComponent extends TiSelectComponent { + private static readonly DEFAULT_PANEL_WIDTH: number = 250; + private static readonly MAX_PANEL_WIDTH: number = 300; + private static readonly MIN_PANEL_WIDTH: number = 200; + /** + * @ignore + * 10.0.3 版本之前服务使用 tiTip 指令自行实现提示,为做兼容性处理,添加该接口判断服务是否使用指令实现 + */ + @Input() tiTip: string; + /** + * 设置按钮 tip 提示内容 + */ + @Input() tipContent: string = TiLocale.getLocaleWords().tiTable.colsToggleTip; + /** + * 设置按钮 tip 提示方向 + */ + @Input() tipPosition: TiPositionType = 'top'; + /** + * @ignore + */ + @ViewChild('toggleMenu', { static: true }) toggleMenuEleRef: ElementRef; + /** + * @ignore + */ + @ViewChild(TiColsToggleDropComponent, { static: true }) dropsearchCom: TiColsToggleDropComponent; + /** + * @ignore + */ + public table: TiTableComponent; + protected versionInfo: string = super.getVersion(packageInfo); + // 标记是否需要更新聚焦元素 + private shouldUpdateFocusableElems: boolean = false; + constructor( + protected elementRef: ElementRef, + protected renderer2: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + table: TiTableComponent + ) { + super(elementRef, renderer2, changeDetectorRef); + this.table = table; + } + + ngOnInit(): void { + // 兼容使用tiTip指令实现的tip提示 + this.tipContent = this.tiTip ? '' : this.tipContent; + this.tipPosition = this.tiTip ? undefined : this.tipPosition; + this.setDropPanelWidth(); + super.ngOnInit(); + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + this.updateFocusableElems(); + // TODO:严格来说,不应该继承自Select,Select中有对dominator的处理。看有没有更好实现方式。 + } + + ngAfterViewChecked(): void { + // 这里不能调用父类方法,因为Select父类对dominator处理,空指针报错。 + // TODO: 看是否能够不继承select + + if (this.shouldUpdateFocusableElems) { + this.shouldUpdateFocusableElems = false; + this.updateFocusableElems(); + } + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['searchable'] && !changes['searchable'].firstChange) { + // searchable 参数变更后,dropSearch 组件中会重新获取搜索输入框元素,this.dropsearchCom.getFocusableElems() 方法获取的搜索框元素失效。 + // 由于dropSearch在ngAfterViewChecked中变更聚焦元素,时机比ngOnChanges晚,但比组件的ngAfterViewChecked早, + // 所以用shouldUpdateFocusableElems变量标记是否需要更新聚焦元素,之后在ngAfterViewChecked中处理 + this.shouldUpdateFocusableElems = true; + } + } + /** + * @ignore + * 在失焦时,通知更新了表格的列数据,用于表格记忆上传列显示数据 + * + * 继承了TiSelectComponent,TiSelectComponent 中在 @Component 元数据 host 配置中调用了该方法 + */ + public onBlur(): void { + super.onBlur(); + // 表格记忆通知列切换变化 + if (this.table.storageId) { + this.table.updateColumnsSubject.next(null); + } + } + /** + * @ignore + * 阻止 button 聚焦时点击空格回车触发 click 事件导致面板展开关闭异常 + */ + public preventKeydownDefault(event: KeyboardEvent): void { + const enterKeyCodeArr: Array = [TiKeymap.KEY_SPACE, TiKeymap.KEY_ENTER, TiKeymap.KEY_NUMPAD_ENTER]; + if (enterKeyCodeArr.includes(event.keyCode)) { + event.preventDefault(); + } + } + // 更新可聚焦元素 + private updateFocusableElems(): void { + if (this.searchable) { + // 推荐在onInit()时调用setFocusableElems(), 但是ngFor/ngIf中的元素在ngAfterViewInit()才能获取到 + this.setFocusableElems([this.toggleMenuEleRef.nativeElement].concat(this.dropsearchCom.getFocusableElems())); + } else { + this.setFocusableElems([this.toggleMenuEleRef.nativeElement]); + } + } + + private setDropPanelWidth(): void { + // TODO: + const panelWidthNum: number = parseInt(this.panelWidth, 10); + + if (panelWidthNum > TiColsToggleComponent.MAX_PANEL_WIDTH) { + this.panelWidth = `${TiColsToggleComponent.MAX_PANEL_WIDTH}px`; + + return; + } + + if (panelWidthNum < TiColsToggleComponent.MIN_PANEL_WIDTH) { + this.panelWidth = `${TiColsToggleComponent.MIN_PANEL_WIDTH}px`; + } + } +} diff --git a/src/table/lib/src/TiColsToggleDropComponent.ts b/src/table/lib/src/TiColsToggleDropComponent.ts new file mode 100644 index 0000000..f70ec2d --- /dev/null +++ b/src/table/lib/src/TiColsToggleDropComponent.ts @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectorRef, + ChangeDetectionStrategy, + Component, + ElementRef, + IterableChanges, + IterableDiffer, + IterableDiffers, + NgZone, + Renderer2, + ViewChild +} from '@angular/core'; +import { TiDropsearchComponent } from '@opentiny/ng-dropsearch'; +import { TiListComponent } from '@opentiny/ng-list'; +import { TiTableColumns, TiTableComponent } from './TiTableComponent'; +import packageInfo from '../package.json'; +/** + * @ignore + * TiColsToggleDrop 控制列动态隐藏/显示的下拉组件 + * + */ +@Component({ + selector: 'ti-cols-toggle-drop', + templateUrl: './cols-toggle-drop.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-cols-toggle-drop-container]': 'true' + } +}) +export class TiColsToggleDropComponent extends TiDropsearchComponent { + public static readonly DOMINATOR_SPACE: number = 4; + public static readonly DEFAULT_LIST_MAX_HEIGHT: number = 30 * 8 + 8; + public static readonly LIST_WITH_SEARCHBOX_MAX_HEIGHT: number = 30 * 7; + public static readonly SEARCHBOX_AREA_HEIGHT: number = 28 + 6 + 4; // 搜索框所占区域高度 + public static readonly DROP_VERTICAL_PADDING: number = 4 + 4; + public dominatorSpace: string = TiColsToggleDropComponent.DOMINATOR_SPACE + 'px'; + public columns: Array = []; + public selectedColumns: Array; // TODO: 可以不定义此变量,用this.model来代替 + public readonly listLabelKey: string = 'title'; + protected versionInfo: string = super.getVersion(packageInfo); + private columnsDiffer: IterableDiffer; + private optionsChangeInner: boolean = false; + /** + * @ignore + * 覆写 TiDroplistComponent 中的 listInited 值 + */ + public listInited: boolean = true; + @ViewChild(TiListComponent, { static: true }) listCom: TiListComponent; + constructor( + protected elementRef: ElementRef, + protected renderer2: Renderer2, + public changeDetectorRef: ChangeDetectorRef, + protected zone: NgZone, + private iterableDiffers: IterableDiffers, + private table: TiTableComponent + ) { + super(elementRef, renderer2, changeDetectorRef, zone); + } + + private static trackByFn(index: number, item: any): string { + // 表格记忆show属性也需要跟踪 + return item.title + item.show; + } + + ngOnInit(): void { + super.ngOnInit(); + this.multiple = true; // 设置为多选。 + this.heightExcludeList = this.searchable + ? TiColsToggleDropComponent.DROP_VERTICAL_PADDING + TiColsToggleDropComponent.SEARCHBOX_AREA_HEIGHT + : TiColsToggleDropComponent.DROP_VERTICAL_PADDING; + this.defaultListMaxHeight = this.searchable + ? TiColsToggleDropComponent.LIST_WITH_SEARCHBOX_MAX_HEIGHT + : TiColsToggleDropComponent.DEFAULT_LIST_MAX_HEIGHT; + this.searchKeys = ['title']; + this.columnsDiffer = this.iterableDiffers.find(this.options).create(TiColsToggleDropComponent.trackByFn); + } + + ngDoCheck(): void { + // 动态监听columns的变化(主要是引用不变,内容变化colums.push等), 从而更新经过 tiColumns 管道的数据(数据引用变了才会进pipe管道) + super.ngDoCheck(); + const columnsDiffer: IterableChanges = this.columnsDiffer.diff(this.options); + if (columnsDiffer) { + if (this.optionsChangeInner) { + this.optionsChangeInner = false; + } else { + this.selectedColumns = this.options.filter((column: { show?: boolean }) => { + return column.show === true || column.show === undefined; + }); + this.columns = this.options.concat(); // 强制变化,不然colums.push的列不能出现在列表中,与在html中使用的 PIPE 有关 + if (this.searchable && this.searchResult === this.options) { + // 如果 options 由于push等方法导致的改变,这里将 searchResult 引用更新一下,这样在 dropsearch 中打开面板时 + // 把 options 的值赋给 searchResult(searchResult = options)时,searchResult新数据(引用改变了)才能进 tiColumns 管道, 从而渲染出来。 + this.searchResult = this.searchResult.concat(); + } + } + + // columns变化需要处理列固定 + if (this.table.fixedColumnInfo.hasFixedColumn) { + // 需要延迟处理,columns变化时,组件渲染还未完成。 + // 但如果在ngAfterViewChecked处理,太频繁,影响性能。 + setTimeout((): void => { + // 处理左侧列固定 + this.table.updateFixedThLeftSubject.next(null); + this.table.updateFixedTdLeftSubject.next(null); + // 处理右侧列固定 + const scrollLeft: number = this.table.fixedColumnInfo.container.scrollLeft; + const isRightColumnFloat: boolean = + scrollLeft + this.table.fixedColumnInfo.container.clientWidth < this.table.fixedColumnInfo.container.scrollWidth; + this.table.containerScrollXChangeSubject.next({ + scrollLeft, + isRightColumnFloat + }); + }, 0); + } + } + } + + public onSelect(option: any): void { + if (option === this.listCom.optionSelectAll) { + this.options.forEach((item: any) => { + if (!this.isDisabledFn(item)) { + item.show = this.selectedColumns.includes(item); + } + }); + } else { + option.show = this.selectedColumns.includes(option); + } + this.optionsChangeInner = true; + // 需要在父类select.emit之前,更改option内容 + super.onSelect(option); + // TODO: 没有处理,用户主动改变绑定变量selectedColumns。应该在docheck里监听selectedColumns(this.model) + } + + public isDisabledFn = (item: { disabled?: boolean; show?: boolean }): boolean => { + return item.disabled === true || (item.show === undefined && item !== this.listCom.optionSelectAll); + }; + + // 鼠标点击到搜索框外围的空白,会失焦导致面板关闭 + // 此处做特殊处理,当点击空白时通过阻止默认事件的方式处理 + public onMousedownSearchBoxOuter(event: MouseEvent): void { + if ((event.target as any).tagName === 'INPUT') { + return; + } + event.preventDefault(); + } +} diff --git a/src/table/lib/src/TiColspanDirective.ts b/src/table/lib/src/TiColspanDirective.ts new file mode 100644 index 0000000..4da0916 --- /dev/null +++ b/src/table/lib/src/TiColspanDirective.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterViewInit, Directive, ElementRef, OnDestroy, Renderer2 } from '@angular/core'; +import { TiTableComponent } from './TiTableComponent'; +/** + * TiColspan 表格跨列数colspan 计算, + * + * 根据表头第一行tr中th的数目,同步colspan数。 + * + * 主要配合表格详情展开功能使用,用于表格详情展示行的的列合并。 + * + */ +@Directive({ + selector: 'td[tiColspan]' +}) +export class TiColspanDirective implements AfterViewInit, OnDestroy { + private firstTrObserver: MutationObserver; + constructor(private table: TiTableComponent, private renderer: Renderer2, private elementRef: ElementRef) {} + ngAfterViewInit(): void { + // 获取thead中第一行tr + const tableFirstTr = (this.table.nativeElement as HTMLElement).querySelector('table>thead>tr'); + if (!tableFirstTr) { + return; + } + // 初始化设置一次colspan + this.renderer.setAttribute(this.elementRef.nativeElement, 'colspan', String(tableFirstTr.children.length)); + // 修复SSR报错:ERROR ReferenceError: MutationObserver is not defined + if (typeof MutationObserver === 'undefined') { + return; + } + this.firstTrObserver = new MutationObserver((mutationsList) => { + for (let mutation of mutationsList) { + if (mutation.type === 'childList') { + this.renderer.setAttribute(this.elementRef.nativeElement, 'colspan', String(tableFirstTr.children.length)); + } + } + }); + // 只需要处理tr中th的增删,所以只配置 childList为true, + this.firstTrObserver.observe(tableFirstTr, { childList: true }); + } + ngOnDestroy(): void { + if (this.firstTrObserver) { + this.firstTrObserver.disconnect(); + } + } +} diff --git a/src/table/lib/src/TiColumnFixedDirective.ts b/src/table/lib/src/TiColumnFixedDirective.ts new file mode 100644 index 0000000..f5427f0 --- /dev/null +++ b/src/table/lib/src/TiColumnFixedDirective.ts @@ -0,0 +1,266 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterViewChecked, Directive, ElementRef, NgZone, OnInit, OnDestroy, Renderer2, Input } from '@angular/core'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiTableComponent } from './TiTableComponent'; +import { TiBrowser } from '@opentiny/ng-utils'; +import { Subscription } from 'rxjs'; +/** + * tiColumnFixed 表格列固定(IE和Edge浏览器不支持) + * + * 适用场景:对于列数很多的表格,可以固定前后列,横向滚动查看其它数据。 + * + * 规范:左侧支持最多3列固定(3列不包含单选/复选框所在列),右侧支持最多1列固定。横向滚动条贯穿左右,与表格最左和最右对齐。 + * 表格下展场景不需要支持列固定。 + * + */ +@Directive({ + selector: 'th[tiColumnFixed], td[tiColumnFixed]', + host: { + '[class.ti3-table-column-fixed-right]': "!notSupportBrowser && type === 'right'", + '[class.ti3-table-column-fixed-left]': "!notSupportBrowser && type === 'left'" + } +}) +export class TiColumnFixedDirective implements OnInit, AfterViewChecked, OnDestroy { + /** + * 固定的左右位置,左侧可连续多列固定,且必须从第一列开始固定,右侧只能固定一列,且必须是最后一列 + */ + @Input('tiColumnFixed') type: 'right' | 'left'; + private tagName: 'TH' | 'TD'; + private tdLeft: number; + private element: any; + private columnFixedLeftLast: boolean = false; + private floatingFixedColumn: boolean = false; + private firstAfterViewChecked: boolean = true; + private containerScrollXChangeSubscription: Subscription; + private thResizeSubscription: Subscription; + private updateFixedThLeftSubscription: Subscription; + private updateFixedTdLeftSubscription: Subscription; + /** + * @ignore + */ + public notSupportBrowser: boolean; + constructor( + private renderer: Renderer2, + elementRef: ElementRef, + private tiRenderer: TiRenderer, + private tableCom: TiTableComponent, + private zone: NgZone + ) { + this.element = elementRef.nativeElement; + } + + ngOnInit(): void { + // IE 不支持粘性定位position: sticky,使用其他定位方式有闪动,目前无法解决; EDGE 固定列边框不显示, 表头固定时拖动横向滚动条表头横向平移不及时 + this.notSupportBrowser = TiBrowser.isIE() || TiBrowser.isEdge(); + if (this.notSupportBrowser || !this.type) { + return; + } + + if (!this.tableCom.fixedColumnInfo.container) { + const container: any = this.tiRenderer.findChildrenByClassName(this.tableCom.hostEle, 'ti3-table-container')[0]; + if (!container) { + return; + } + this.tableCom.fixedColumnInfo.container = container; + } + + this.tableCom.fixedColumnInfo.hasFixedColumn = true; + + this.tagName = this.element.tagName; + + if (this.type === 'right' && this.tagName === 'TH') { + // 表头固定需要此参数 + this.tableCom.fixedColumnInfo.thFixedRight = this.element; + } + + this.addBehavior(); + } + + ngAfterViewChecked(): void { + if (this.notSupportBrowser || !this.type || !this.firstAfterViewChecked) { + return; + } + + this.firstAfterViewChecked = false; + this.init(); + } + + ngOnDestroy(): void { + if (this.containerScrollXChangeSubscription) { + this.containerScrollXChangeSubscription.unsubscribe(); + } + if (this.thResizeSubscription) { + this.thResizeSubscription.unsubscribe(); + } + if (this.updateFixedThLeftSubscription) { + this.updateFixedThLeftSubscription.unsubscribe(); + } + if (this.updateFixedTdLeftSubscription) { + this.updateFixedTdLeftSubscription.unsubscribe(); + } + } + + private init(): void { + if (this.type === 'left') { + if (this.tagName === 'TH') { + this.processTh(); + } else { + this.processTd(); + } + } + + if (this.tagName === 'TH') { + this.zone.runOutsideAngular(() => { + // 表头初始时容器的滚动状态还未初始完成 + setTimeout(() => { + const scrollLeft: number = this.tableCom.fixedColumnInfo.container.scrollLeft; + const isRightColumnFloat: boolean = + scrollLeft + this.tableCom.fixedColumnInfo.container.clientWidth < this.tableCom.fixedColumnInfo.container.scrollWidth; + this.tableCom.containerScrollXChangeSubject.next({ + scrollLeft, + isRightColumnFloat + }); + }, 0); + }); + } else { + const scrollLeft: number = this.tableCom.fixedColumnInfo.container.scrollLeft; + const isRightColumnFloat: boolean = + scrollLeft + this.tableCom.fixedColumnInfo.container.clientWidth < this.tableCom.fixedColumnInfo.container.scrollWidth; + this.tableCom.containerScrollXChangeSubject.next({ + scrollLeft, + isRightColumnFloat + }); + } + } + + private addBehavior(): void { + // 处理左右固定的最后一列是否有阴影(看起来有浮动效果) + this.containerScrollXChangeSubscription = this.tableCom.containerScrollXChangeSubject.subscribe((scrollInfo: any) => { + if (this.type === 'right' && scrollInfo.isRightColumnFloat !== this.floatingFixedColumn) { + this.processLastFixedColumn(this.element, scrollInfo.isRightColumnFloat); + } + if (this.type === 'left' && this.columnFixedLeftLast && scrollInfo.scrollLeft > 0 !== this.floatingFixedColumn) { + this.processLastFixedColumn(this.element, scrollInfo.scrollLeft > 0); + } + }); + + // 处理左侧固定的列的left值(左侧可多列固定) + if (this.type === 'left' && this.tagName === 'TH') { + this.thResizeSubscription = this.tableCom.thResizeSubject.subscribe((thResizeInfo: any) => { + const th: any = thResizeInfo.th; + if (th !== this.element) { + return; + } + + const nextSibling: any = th.nextElementSibling; + if (nextSibling && this.tiRenderer.hasClass(nextSibling, 'ti3-table-column-fixed-left')) { + const siblings: any = th.parentElement.children; + const index: number = Array.from(siblings).indexOf(th); + const changeColumnsIndex: Array = []; + for (let i: number = index + 1; i < siblings.length; i++) { + if (!siblings[i] || !this.tiRenderer.hasClass(siblings[i], 'ti3-table-column-fixed-left')) { + break; + } + changeColumnsIndex.push(i); + this.tableCom.fixedColumnInfo.fixedColumnLeftValues[i] += thResizeInfo.leftEdge; + this.renderer.setStyle(siblings[i], 'left', `${this.tableCom.fixedColumnInfo.fixedColumnLeftValues[i]}px`); + } + const bodyTable: any = this.tableCom.isFixedHead ? thResizeInfo.resizableOpts.secondTable : thResizeInfo.resizableOpts.table; + const trs: Array = Array.from(bodyTable.children[1].children).filter((tr: any) => { + return this.needFixedColumnTr(tr); + }); + trs.forEach((tr: any) => { + changeColumnsIndex.forEach((columnIndex: number) => { + this.renderer.setStyle( + tr.children[columnIndex], + 'left', + `${this.tableCom.fixedColumnInfo.fixedColumnLeftValues[columnIndex]}px` + ); + }); + }); + } + }); + + this.updateFixedThLeftSubscription = this.tableCom.updateFixedThLeftSubject.subscribe(() => { + this.processTh(); + }); + } + + if (this.type === 'left' && this.tagName === 'TD') { + this.updateFixedTdLeftSubscription = this.tableCom.updateFixedTdLeftSubject.subscribe(() => { + this.processTd(); + }); + } + } + + private processTd(): void { + const siblings: any = this.element.parentElement.children; + const index: number = Array.from(siblings).indexOf(this.element); + // 进行判空处理,因为有可能是td元素先出现。 + // TODO: 考虑下processTd和processTd是否可以使用相同的处理方式。 + if ( + this.tableCom.fixedColumnInfo.fixedColumnLeftValues && + this.tableCom.fixedColumnInfo.fixedColumnLeftValues.length && + this.tableCom.fixedColumnInfo.fixedColumnLeftValues[index] !== this.tdLeft + ) { + this.tdLeft = this.tableCom.fixedColumnInfo.fixedColumnLeftValues[index]; + this.renderer.setStyle(this.element, 'left', `${this.tdLeft}px`); + } + + if (index === this.tableCom.fixedColumnInfo.columnFixedLeftLastIndex) { + this.columnFixedLeftLast = true; + } + } + + private processTh(): void { + const siblings: any = this.element.parentElement.children; + let left: number = 0; + this.tableCom.fixedColumnInfo.fixedColumnLeftValues = []; + + for (let i: number = 0; i < siblings.length; i++) { + const current: any = siblings[i]; + if (this.tableCom.fixedColumnInfo.fixedColumnLeftValues[i] !== left) { + this.tableCom.fixedColumnInfo.fixedColumnLeftValues[i] = left; + this.renderer.setStyle(current, 'left', `${left}px`); + } + // offsetWidth 包括边框 + left += current.offsetWidth; + const nextSibling: any = siblings[i + 1]; + if (nextSibling && !this.tiRenderer.hasClass(nextSibling, 'ti3-table-column-fixed-left')) { + this.tableCom.fixedColumnInfo.columnFixedLeftLastIndex = i; + if (current === this.element) { + this.columnFixedLeftLast = true; + } + break; + } else { + if (current === this.element) { + this.columnFixedLeftLast = false; + } + } + } + } + + private processLastFixedColumn(ele: any, add: boolean): void { + if (add) { + this.floatingFixedColumn = true; + this.renderer.addClass(ele, 'ti3-table-floating-fixed-column'); + } else { + this.floatingFixedColumn = false; + this.renderer.removeClass(ele, 'ti3-table-floating-fixed-column'); + } + } + + private needFixedColumnTr(tr: any): boolean { + return this.tableCom.needTr(tr); + } +} diff --git a/src/table/lib/src/TiColumnsPipe.ts b/src/table/lib/src/TiColumnsPipe.ts new file mode 100644 index 0000000..d6ab387 --- /dev/null +++ b/src/table/lib/src/TiColumnsPipe.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Pipe, PipeTransform } from '@angular/core'; +import { Util } from '@opentiny/ng-utils'; +/** + * @ignore + * TiColumnsPipe 过滤掉columns中的title为空的数据项 + * + */ +@Pipe({ name: 'tiColumns' }) +export class TiColumnsPipe implements PipeTransform { + transform(value: Array): Array { + return value.filter((item: any) => !Util.isEmptyString(item.title)); + } +} diff --git a/src/table/lib/src/TiDetailsIconComponent.ts b/src/table/lib/src/TiDetailsIconComponent.ts new file mode 100644 index 0000000..b4aa81f --- /dev/null +++ b/src/table/lib/src/TiDetailsIconComponent.ts @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + DoCheck, + ElementRef, + EventEmitter, + Input, + OnInit, + Output, + Renderer2, + ChangeDetectionStrategy, + ChangeDetectorRef +} from '@angular/core'; +import { TiBrowser, Util } from '@opentiny/ng-utils'; +import { TiTableComponent, TiTableRowData } from './TiTableComponent'; +/** + * TiDetailsIcon 详情展开图标组件 + * + * ti-details-icon 用来显示详情展开图标,其嵌在详情展开列的 td 标签中; + * 点击该图标,对应详情行在展开/收起两个状态之间切换。 + */ +@Component({ + selector: 'ti-details-icon', + templateUrl: './details-icon.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '(click)': 'onClick()' + } +}) +export class TiDetailsIconComponent implements OnInit, DoCheck { + /** + * @ignore + * 表格详情中,非详情行标记样式类 + */ + public static TABLE_ClASS_DETAIL_BASE: string = 'ti3-table-detail-icon-tr'; + /** + * 当前行数据 + */ + @Input() row: TiTableRowData; + /** + * 当前行索引值,即 ngFor 中对应的 index + */ + @Input() index: number; + /** + * 点击详情展开图标时触发的回调,参数为当前行数据,一旦使用此接口,就由开发者决定是否要展开/收起, + * 可通过行数据中的 showDetails 属性控制是否及何时展开/收起 + */ + @Output() readonly beforeToggle: EventEmitter = new EventEmitter(); + private oldShowDetails: boolean = false; + private element: any; + constructor( + private table: TiTableComponent, + private elementRef: ElementRef, + private renderer: Renderer2, + private changeRef: ChangeDetectorRef + ) { + this.element = this.elementRef.nativeElement; + } + + ngOnInit(): void { + this.row = Util.isUndefined(this.row) ? {} : this.row; + this.row.showDetails = Util.isUndefined(this.row.showDetails) ? false : this.row.showDetails; + // 给当前行添加样式类标记 + const parentTr: any = this.renderer.parentNode(this.renderer.parentNode(this.element)); + this.renderer.addClass(parentTr, TiDetailsIconComponent.TABLE_ClASS_DETAIL_BASE); + } + + ngDoCheck(): void { + // 如果需要关闭其它行的详情(即只能展开一行的详情)时,需要监听showDetails + if (this.table.closeOtherDetails && this.row.showDetails !== this.oldShowDetails) { + this.oldShowDetails = this.row.showDetails; + if (this.row.showDetails) { + // 当打开当前行的详情时,关闭其他行的详情 + this.table.closeOtherDetailsFn(this.row); + } + } + // 使用closeOtherDetails和beforeToggle接口时,保证图标收起 + this.changeRef.markForCheck(); + } + + /** + * @ignore + */ + public onClick(): void { + if (this.beforeToggle.observers.length > 0) { + this.beforeToggle.emit(this.row); + } else { + this.row.showDetails = !this.row.showDetails; + } + + // 为了解决 codeclub #1720 + // 在chrome浏览器下,滚动条滚到底部后,这时页面内容高度变小,滚动条保留在底部(正常的现象), + // 之后页面内容高度再变大时,滚动条依然保留在底部。 + // 这导致了当页面滚动条拖动到底部后,将表格的pagesize由大变小(如20条修改为10条),再去点击某一行详情展开时, + // 视觉上详情行是向上展开的,其原因是滚动条一直停留在底部;这时,只要稍微手动触发滚动条滚动,详情正常展开 + // 注意:此问题只修复了在body上出滚动条的表格的详情展开问题。 + if (TiBrowser.isChrome()) { + const scrollTop: number = Math.ceil(document.documentElement.scrollTop); + // 窗口可视区域高度 + const height: number = document.documentElement.clientHeight; + const scrollHeight: number = document.body.scrollHeight; + if (scrollTop > 0 && scrollTop + height >= scrollHeight) { + document.body.scrollTop = scrollTop - 0.5; + document.body.scrollTop = scrollTop + 0.5; + } + } + } +} diff --git a/src/table/lib/src/TiDetailsTrDirective.ts b/src/table/lib/src/TiDetailsTrDirective.ts new file mode 100644 index 0000000..fbdc7a4 --- /dev/null +++ b/src/table/lib/src/TiDetailsTrDirective.ts @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, DoCheck, Input, Renderer2, TemplateRef, ViewContainerRef } from '@angular/core'; +import { TiTableComponent, TiTableRowData } from './TiTableComponent'; +import { TiDetailsIconComponent } from './TiDetailsIconComponent'; +/** + * TiDetailsTr 详情行结构指令 + * + * 使用时需要在其前面加 * 语法糖,传入当前行数据; + * 其内部根据row.showDetails的值来控制详情行是否显示,功能类似于ngIf。 + */ +@Directive({ + selector: '[tiDetailsTr]' +}) +export class TiDetailsTrDirective implements DoCheck { + /** + * 当前行数据 + */ + @Input('tiDetailsTr') row: TiTableRowData; + private oldShowDetails: boolean; + constructor( + private templateRef: TemplateRef, + private viewContainerRef: ViewContainerRef, + private renderer: Renderer2, + private table: TiTableComponent + ) {} + + ngDoCheck(): void { + if (this.row.showDetails !== this.oldShowDetails) { + this.updateView(); + this.oldShowDetails = this.row.showDetails; + } + } + private updateView(): void { + if (this.row.showDetails) { + // 上下文参数是否需要传递 + this.viewContainerRef.createEmbeddedView(this.templateRef); + // 结构指令的宿主元素是一个 comment(注释) 的Node节点, + // 生成节点元素插入DOM中的位置跟angularCompilerOptions.enableIvy 配置有关,true时在宿主元素之前,false时在宿主元素之后, + // 而 ng9开始angularCompilerOptions.enableIvy 默认为true,但一些项目为兼容性,强制设置为了false,所以要做一下兼容处理 + let detailTr: HTMLElement; + const nativeElement = this.templateRef.elementRef.nativeElement; + if ( + nativeElement.previousSibling && + nativeElement.previousSibling.classList && + nativeElement.previousSibling.classList.contains(TiDetailsIconComponent.TABLE_ClASS_DETAIL_BASE) + ) { + detailTr = nativeElement.nextSibling; + } else { + detailTr = nativeElement.previousSibling; + } + + this.renderer.addClass(detailTr, 'ti3-details-tr'); + } else { + this.viewContainerRef.clear(); + // TODO: 方案是否可优化 + // 表头锁定时,展开时可在table的AfterViewChecked中获取到表格变化后的高度, + // 但是收起时,在table的AfterViewChecked中获取不到表格变化后的高度,不能及时处理滚动条的变化 + // 所以此处使用setTimeout使其延时触发table的AfterViewChecked,使其能够处理由高度变化引起的滚动条状态的改变 + // 注:使用ngIf指令也是如此效果 + if (this.table.isFixedHead) { + setTimeout(() => {}, 0); + } + } + } +} diff --git a/src/table/lib/src/TiHeadFilterComponent.ts b/src/table/lib/src/TiHeadFilterComponent.ts new file mode 100644 index 0000000..f2f5443 --- /dev/null +++ b/src/table/lib/src/TiHeadFilterComponent.ts @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ElementRef, Input, ViewChild } from '@angular/core'; +import { TiSelectComponent } from '@opentiny/ng-select'; +import { TiHeadFilterDatetimeConfig, TiHeadFilterDropComponent } from './TiHeadFilterDropComponent'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +// 下面注释,可以避免编译lib时正则报错。原理未知,副作用未知。 +// @dynamic +/** + * TiHeadFilter 表头过滤(漏斗)组件, 嵌在表头的 th 中使用。 + * + * ti-head-filter 用来显示过滤操作漏斗图标,点击其可打开下拉框,选中下拉框中的选项时, + * 可在提供的 select 事件中做对应的表格数据过滤处理。 + * + * ### 接口说明 + * **Inputs:** + * + * | 名称 | 类型 | 默认值 | 功能描述 | + * | -------- | :----- | :---- | :---- | + * | options | array | [] | 所有过滤项 | + * | ngModel | object | false | 可设置和获取 headfilter下拉选中值 | + * | labelKey | string | 'label' | 下拉面板中要显示的字段| + * | valueKey | string | '' | 当存在valueKey时,选中值基于valueKey| + * | idKey | string | '' | idKey指定的属性的值相等时即认为 option 选项是选中的 | + * | panelMaxHeight | string | 最大显示8条数据 | 下下拉面板的最大显示高度,溢出时则出滚动条。 | + * | id | string | undefined | HTML属性id,自动化测试要求必须给id赋值 | + * | select | function | 无 | 下拉面板选中事件,向外通知选中数据 | + * | multiple | boolean | false | 下拉面板是否为多选 | + * | selectAll | boolean | false | 下拉面板是否显示全选框 | + * | panelAlign | string | 'left' | 下拉面板展开对齐方式| + * | searchable | boolean | false | 是否开启搜索功能| + * | panelWidth | 'auto' 或 string | 'auto' | 设置下拉面板的宽度,"auto"表示下拉框的宽度根据下拉选项的内容自动撑开,也可设置固定的下拉框宽度(不小于过滤图标的宽度),例如:"200px"| + * | noDataText | string | '暂无数据' | 无数据时的显示文本。默认值改为国际化词条 | + * | isDatetime | boolean | false | 下拉面板是否为时间日期格式 | + * | datetimeConfig | object | {} | 时间日期中的基本信息配置 | + * + *

    以下说明不可用,不做参考。该组件只有以上15个Input接口和1个Output接口 + * 可用,其余方法、Input、输出等(继承于TiSelectComponent,是供select的内部使用的)都不可用

    + */ +@Component({ + selector: 'ti-head-filter', + templateUrl: './head-filter.html', + styleUrls: ['./head-filter.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiHeadFilterComponent)], + host: { + '[class.ti3-head-filter-container]': 'true', + '(blur)': 'onBlur()' + } +}) +export class TiHeadFilterComponent extends TiSelectComponent { + /** + * 面板对齐方式 + */ + @Input() panelAlign: 'left' | 'right' = 'left'; + // 由于headfilter与select中的panelWidth的默认值和类型不一样,所以这里重定义panelWidth接口 + /** + * 下拉面板的最大宽度,'auto' 表示下拉框的宽度根据下拉选项的内容自动撑开,string 是可以设置具体的宽度值,比如 '200px' + */ + @Input() panelWidth: 'auto' | string = undefined; // 设置为undefined是为了覆盖父类的panelWidth值 + + /** + * 是否为时间日期菜单 + */ + @Input() isDatetime: boolean = false; + /** + * 时间日期的相关配置 + */ + @Input() datetimeConfig: TiHeadFilterDatetimeConfig = {}; + /** + * @ignore + */ + @ViewChild('filterIcon', { static: true }) filterIconEleRef: ElementRef; + /** + * @ignore + */ + @ViewChild(TiHeadFilterDropComponent, { static: true }) + dropsearchCom: TiHeadFilterDropComponent; + protected versionInfo: string = super.getVersion(packageInfo); + /** + * @ignore + * 在初始化DOM渲染完成之时,将下拉面板中的所有选中项放在一起,使可聚焦元素都放在组件内部,从而在点击面板进行相关操作时, + * 组件不会触发blur事件,保证面板内操作的正常执行,在点击组件外的空白处时,保证blur事件的正常执行 + */ + ngAfterViewInit(): void { + if (this.isDatetime) { + const elemsNodeList: Array = [this.filterIconEleRef.nativeElement].concat( + this.dropsearchCom.dropCom.nativeElement, + this.dropsearchCom.datetimeStartCom.getFocusableElems(), + this.dropsearchCom.datetimeEndCom.getFocusableElems(), + this.dropsearchCom.datetimeOkCom.nativeElement, + this.dropsearchCom.datetimeCancelCom.nativeElement + ); + this.setFocusableElems(elemsNodeList); + } + } + + ngAfterViewChecked(): void { + // 这里不能调用父类方法,因为Select父类对dominator处理,空指针报错。 + // TODO: 看是否能够不继承select + if (!this.isDatetime && this.searchable !== this.oldSearchable) { + this.oldSearchable = this.searchable; + if (this.searchable) { + this.setFocusableElems([this.filterIconEleRef.nativeElement].concat(this.dropsearchCom.getFocusableElems())); + } else { + this.setFocusableElems([this.filterIconEleRef.nativeElement]); + } + } + } + + /** + * @ignore + * 失焦情况下,仅关闭面板,不做聚焦等处理 + */ + public onBlur(): void { + super.onBlur(); + if (this.searchable) { + if (getComputedStyle(this.filterIconEleRef.nativeElement).outlineStyle === 'none') { + this.renderer.setStyle(this.filterIconEleRef.nativeElement, 'outlineStyle', ''); + } + } + } + + /** + * @ignore + * seachbox:180 + * datetime:310 面板的宽度需要固定值 + * 其他场景如果不设置,则自动撑开 + */ + public getDropWidth(): string { + let panelWidthTemp: string = 'auto'; + if (!this.isDatetime && this.searchable) { + panelWidthTemp = '180px'; + } + if (this.isDatetime) { + panelWidthTemp = this.datetimeConfig?.onlyDate ? '270px' : '310px'; + } + + panelWidthTemp = this.panelWidth ? this.panelWidth : panelWidthTemp; + + return panelWidthTemp; + } +} diff --git a/src/table/lib/src/TiHeadFilterDropComponent.ts b/src/table/lib/src/TiHeadFilterDropComponent.ts new file mode 100644 index 0000000..75beca2 --- /dev/null +++ b/src/table/lib/src/TiHeadFilterDropComponent.ts @@ -0,0 +1,270 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input, SimpleChanges, ViewChild } from '@angular/core'; +import { TiDropsearchComponent } from '@opentiny/ng-dropsearch'; +import { TiDateBaseComponent, TiDatetimeFormat } from '@opentiny/ng-datebase'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiBrowser } from '@opentiny/ng-utils'; +import { TiButtonComponent } from '@opentiny/ng-button'; +import packageInfo from '../package.json'; + +/** + * datetime 接口相应的匹配值 + */ +export interface TiHeadFilterDatetimeConfig { + /** + * datetime 类型日期显示格式 + */ + format?: string | TiDatetimeFormat; + /** + * datetime可选最小值 + */ + min?: Date; + /** + * datetime可选最大值 + */ + max?: Date; + /** + * 是否仅是日期的选择(没有时间的选择)。不设置时默认是日期时间的选择 + */ + onlyDate?: boolean; + + [propName: string]: any; +} + +/** + * @ignore + * TiHeadFilterDrop 表头过滤漏斗组件的下拉框选择组件 + */ +@Component({ + selector: 'ti-head-filter-drop', + templateUrl: './head-filter-drop.html', + styleUrls: ['./head-filter-drop.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiHeadFilterDropComponent)] +}) +export class TiHeadFilterDropComponent extends TiDropsearchComponent { + public static readonly DOMINATOR_SPACE: number = 6; + public dominatorSpace: string = TiHeadFilterDropComponent.DOMINATOR_SPACE + 'px'; + // 选中项 + public selected: Array; + // 时间日期面板选中项 + public datetimeSelected: TiHeadFilterDatetimeConfig = {}; + // 时间日期值范围设置 + public datetimeLimit: TiHeadFilterDatetimeConfig = {}; + protected heightExcludeContent: number = 10; // 上下边框为2,上下padding为8 + protected buttonHeight: number = 45; // 确定、取消按钮高度 + protected versionInfo: string = super.getVersion(packageInfo); + /** + * 是否开启时间日期下拉 + */ + @Input() isDatetime: boolean = false; + /** + * 时间日期相关的配置 + */ + @Input() datetimeConfig: TiHeadFilterDatetimeConfig = {}; + /** + * @ignore + * 时间日期范围开始 + */ + @ViewChild('datetimeStart', { static: false }) + datetimeStartCom: TiDateBaseComponent; + /** + * @ignore + * 时间日期范围结束 + */ + @ViewChild('datetimeEnd', { static: false }) + datetimeEndCom: TiDateBaseComponent; + @ViewChild('datetimeOk', { static: false }) datetimeOkCom: TiButtonComponent; + @ViewChild('datetimeCancel', { static: false }) + datetimeCancelCom: TiButtonComponent; + + ngOnInit(): void { + super.ngOnInit(); + this.initDatetime(); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + + // 重新设置headfilterdrop的heightExcludeList + if (changes['searchable']) { + if (changes['searchable'].currentValue) { + this.heightExcludeList = this.multiple + ? this.heightExcludeContent + this.buttonHeight + TiDropsearchComponent.SEARCHBOX_EXCLUDE_HEIGHT + : this.heightExcludeContent + TiDropsearchComponent.SEARCHBOX_EXCLUDE_HEIGHT; + } else { + this.heightExcludeList = this.multiple ? this.heightExcludeContent + this.buttonHeight : this.heightExcludeContent; + } + } + } + + // 初始化时间日期面板信息 + public initDatetime(): void { + if (this.isDatetime) { + // 初始化 this.datetimeConfig如果用户传入undefined,组件会报错,此处做下初始化容错处理。 + this.datetimeConfig = this.datetimeConfig ? this.datetimeConfig : {}; + // 开始日期的max是结束日期的选中值,如果结束日期没有选择,使用item中设置的max + this.datetimeLimit.startMax = this.datetimeSelected?.end ? this.datetimeSelected.end : this.datetimeConfig.max; + // 结束日期的min是开始日期的选中值,如果开始日期没有选择,使用中设置的min + this.datetimeLimit.endMin = this.datetimeSelected?.start ? this.datetimeSelected.start : this.datetimeConfig.min; + } + } + + writeValue(model: any): void { + super.writeValue(model); + // 初始化selected选中项 + if (this.multiple) { + this.selected = [...(model ? model : [])]; + } else if (this.isDatetime) { + this.datetimeSelected = { + start: this.isValidDate(model?.start) ? model.start : undefined, + end: this.isValidDate(model?.end) ? model.end : undefined + }; + this.dateStartChange(this.datetimeSelected.start); + this.dateEndChange(this.datetimeSelected.end); + } else { + this.selected = model ? model : ''; + } + } + + onSelect(option: any): void { + // 多选时不处理onSelect事件(在点击确定按钮时触发) + if (this.multiple) { + return; + } + this.model = option; + super.onSelect(option); + } + + private restoreSelected(): void { + // 还原selected选中项 + if (this.multiple) { + this.selected = [...(this.model ? this.model : [])]; + } + + // 还原时间日期的选中项 + if (this.isDatetime) { + this.datetimeSelected = { + start: this.isValidDate(this.model?.start) ? this.model.start : undefined, + end: this.isValidDate(this.model?.end) ? this.model.end : undefined + }; + } + } + + public hide(): void { + // 处理问题:搜索框时,在下拉中选择完收起后漏斗图标由于聚焦而有了虚线框 + if (this.searchable) { + this.renderer.setStyle(this.dominatorElem, 'outlineStyle', 'none'); + super.hide(); + } else { + super.hide(); + } + this.restoreSelected(); + } + + hideWithoutFocus(): void { + super.hideWithoutFocus(); + this.restoreSelected(); + } + + // 按钮面板鼠标按下事件(解决因点击按钮面板导致整个下拉面板隐藏的问题) + btnContainerMousedown(event: any): void { + event.preventDefault(); + } + + // 确定按钮 + okClick(): void { + // 选中值发生改变时对model值做处理 + if (JSON.stringify(this.selected) !== JSON.stringify(this.model ? this.model : [])) { + this.model = [...this.selected]; + } + // 触发onSelect事件 + this.select.emit(this.model); + this.hide(); + } + + // 取消按钮 + cancelClick(): void { + // 还原selected选中项 + this.selected = [...(this.model ? this.model : [])]; + this.hide(); + } + + // 时间面板确定按钮 + onClickDatetimeOK(): void { + // 进行时间戳对比,在选中项发生改变时再进行model的赋值。直接给model赋值,在开始和结束时间都没有改变的情况下,也会触发ngmodelchange事件 + if ( + Date.parse(this.datetimeSelected?.start) !== Date.parse(this.model?.start) || + Date.parse(this.datetimeSelected?.end) !== Date.parse(this.model?.end) + ) { + this.model = { + start: this.datetimeSelected.start, + end: + this.datetimeSelected.end && this.datetimeConfig?.onlyDate + ? new Date( + this.datetimeSelected.end.getFullYear(), + this.datetimeSelected.end.getMonth(), + this.datetimeSelected.end.getDate(), + 23, + 59, + 59 + ) + : this.datetimeSelected.end, // 如果只是日期的选择,那么结束日期的时间应该是 23:59:59 + type: 'datetime' + }; + } + // 触发onSelect事件 + this.select.emit(this.model); + this.hide(); + } + + // 时间面板取消按钮 + onClickDatetimeCancel(): void { + this.hide(); + } + + // Invalid Date判断 + private isValidDate(dateTemp: Date | string): boolean { + let date: Date | string = dateTemp; + if (Object.prototype.toString.call(date) === '[object String]') { + // 转为时间格式 + date = new Date(dateTemp); + } + + return Object.prototype.toString.call(date) === '[object Date]' && String(date) !== 'Invalid Date'; + } + + // 监听开始时间值的变化,开始时间的最大值不大于结束选中时间 + dateStartChange(value: Date): void { + if (this.isValidDate(value)) { + this.datetimeLimit.endMin = value; + } else if (this.isValidDate(this.datetimeConfig?.min as Date)) { + this.datetimeLimit.endMin = this.datetimeConfig?.min; + } else { + // 没有设置min,开始日期选中又清除后,需要设置一个min的缺省值 + this.datetimeLimit.endMin = undefined; + } + } + + // 监听结束时间值的变化,结束时间的最最小值不小于开始选中时间 + dateEndChange(value: any): void { + if (this.isValidDate(value)) { + this.datetimeLimit.startMax = value; + } else if (this.isValidDate(this.datetimeConfig?.max as Date)) { + this.datetimeLimit.startMax = this.datetimeConfig?.max; + } else { + // 没有设置 max, 结束日期选中又清除后,需要设置一个max的缺省值 + this.datetimeLimit.startMax = undefined; + } + } +} diff --git a/src/table/lib/src/TiHeadMenuComponent.ts b/src/table/lib/src/TiHeadMenuComponent.ts new file mode 100644 index 0000000..501b912 --- /dev/null +++ b/src/table/lib/src/TiHeadMenuComponent.ts @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewChild, ViewEncapsulation } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiDropComponent } from '@opentiny/ng-drop'; +import { TiLocale } from '@opentiny/ng-locale'; +import packageInfo from '../package.json'; +/** + * TiHeadMenu 表头下拉菜单组件,搭配表头复选 tiCheckGroup 组件使用 + * + * + * 嵌在表头的th中使用,点击该组件时打开下拉操作菜单项面板 + * + * 组件有默认下拉菜单项,也可支持自定义下拉菜单项 + */ +@Component({ + selector: 'ti-head-menu', + templateUrl: './head-menu.html', + styleUrls: ['./head-menu.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None +}) +export class TiHeadMenuComponent extends TiBaseComponent { + /** + * 下拉菜单项的数据集合 + */ + @Input() options: Array; + /** + * 选中菜单项时触发的回调,参数为该菜单项数据 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + @ViewChild(TiDropComponent, { static: true }) dropCom: TiDropComponent; + /** + * @ignore + */ + public dominatorSpace: string = '8px'; + /** + * @ignore + */ + public selected: any; + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + super.ngOnInit(); + if (!this.options) { + // 默认菜单项 + this.options = [ + { + key: 'checkAll', + label: TiLocale.getLocaleWords().tiTable.headMenuSelectAll + }, + { + key: 'uncheckAll', + label: TiLocale.getLocaleWords().tiTable.headMenuClearAll + } + ]; + } + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + const parent: any = this.nativeElement.parentElement; // 表头单元格th + if (parent?.hasAttribute('checkbox-column')) { + this.renderer.addClass(parent, 'ti3-head-menu-cell'); + } + } + + /** + * @ignore + * 触发select事件 + */ + public onSelect(option: any): void { + this.dropCom.hide(); + this.select.emit(option); + setTimeout(() => { + this.selected = undefined; + }, 0); + } + + /** + * @ignore + */ + onBlur(): void { + this.dropCom.hide(); + } +} diff --git a/src/table/lib/src/TiHeadSortComponent.ts b/src/table/lib/src/TiHeadSortComponent.ts new file mode 100644 index 0000000..1f2b483 --- /dev/null +++ b/src/table/lib/src/TiHeadSortComponent.ts @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, DoCheck, ElementRef, Input, OnInit, Renderer2, ChangeDetectionStrategy } from '@angular/core'; +import { TiTableComponent } from './TiTableComponent'; +import { Util } from '@opentiny/ng-utils'; +/** + * TiHeadSort 表头排序(上下箭头)组件 + * + * 嵌在表头的th中使用,点击该组件时表格数据按该列的排序规则进行升序/降序排序 + * + * 组件有默认的排序规则,也可支持自定义排序规则 + */ +@Component({ + selector: 'ti-head-sort', + templateUrl: './head-sort.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-sort-container]': 'true', + '(click)': 'onClick()' + } +}) +export class TiHeadSortComponent implements OnInit, DoCheck { + /** + * 必选,排序时依据的数据属性 + * + */ + @Input() sortKey: string; + /** + * 初始默认是否为升序,默认值 null 表示不排序 + */ + @Input() asc: boolean = null; + /** + * 排序函数(参考 Array.sort 的 compareFunction) + */ + @Input() compareFn: (a: any, b: any, sortKey?: string) => number; + /** + * @ignore + * 排序状态,也是图标名 + */ + public sortState: string = 'sort'; + + private hostElement: HTMLElement; + /** + * @ignore + */ + public table: TiTableComponent; + constructor(private elementRef: ElementRef, table: TiTableComponent) { + this.hostElement = this.elementRef.nativeElement; + this.table = table; + } + ngOnInit(): void { + // 更新TiTableComponent的enableSort,表示启用了排序。 + this.table.enableSort = true; + // 修复SSR报错:ERROR TypeError: Cannot read property 'sort' of undefined + // 表格记忆 + if (this.table?.storageId && this.table?.storageConfig?.sort && this.table?.sessionStorageState?.sort) { + this.asc = this.sortKey === this.table.sessionStorageState.sort.sortKey ? this.table.sessionStorageState.sort.asc : null; + } + if (Util.isUndefined(this.asc)) { + // 初始无序时(默认为null)兼容传入 undefined + this.asc = null; + } + if (this.asc !== null) { + // 如果有默认排序,即排序的初始化,将排序初始化的各个值赋给 tiTable 的 tableState 对应的 sort 属性 + this.sort(true); + } + } + + ngDoCheck(): void { + this.asc = this.table.dataState.sort.sortKey === this.sortKey ? this.table.dataState.sort.asc : null; + if (this.sortKey === this.table.dataState.sort.sortKey && this.asc === true) { + this.sortState = 'sort-ascent'; + } else if (this.sortKey === this.table.dataState.sort.sortKey && this.asc === false) { + this.sortState = 'sort-descent'; + } else { + this.sortState = 'sort'; + } + } + + /** + * @ignore + */ + public onClick(): void { + if (!this.sortKey) { + return; + } + // =》 无序 =》 升序 =》 降序 =》 无序 + if (this.asc === null) { + this.asc = true; + } else if (this.asc) { + this.asc = false; + } else { + this.asc = null; + } + this.sort(false); + } + + private sort(isDefaultSort: boolean): void { + this.table.updateSort(this.sortKey, this.asc, isDefaultSort, this.compareFn); + } +} diff --git a/src/table/lib/src/TiTableComponent.ts b/src/table/lib/src/TiTableComponent.ts new file mode 100644 index 0000000..e72ff3c --- /dev/null +++ b/src/table/lib/src/TiTableComponent.ts @@ -0,0 +1,1088 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + AfterViewChecked, + AfterViewInit, + Component, + ElementRef, + EventEmitter, + Input, + IterableChanges, + IterableDiffer, + IterableDiffers, + NgZone, + OnDestroy, + OnInit, + Output, + Renderer2, + ViewEncapsulation, + ChangeDetectionStrategy, + ChangeDetectorRef +} from '@angular/core'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { Util } from '@opentiny/ng-utils'; +import { Subject } from 'rxjs'; +import { TiTableFixedHeadService } from './TiTableFixedHeadService'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +/** + * TiTable 组件上 srcData 输入接口中 state 属性值的类型接口 + */ +export interface TiTableSrcState { + /** + * 传给组件的表格源数据是否已经过搜索处理 + */ + searched: boolean; + /** + * 传给组件的表格源数据是否已经过排序处理 + */ + sorted: boolean; + /** + * 传给组件的表格源数据是否已经过分页处理 + */ + paginated: boolean; +} +/** + * 表格当前数据的搜索、排序、分页状态值接口 + */ +export interface TiTableDataState { + /** + * 搜索状态,其包含两个属性: + * + * searchWords:类型为Array,搜索字符串集合 + * + * searchKeys:类型为Array,搜索指定的字段范围 + * + * searchStrictKeys: 类型为Array,搜索指定进行严格搜索的字段范围。 + * + */ + search: { + searchWords: Array; + searchKeys?: Array; + searchStrictKeys?: Array; + }; + /** + * 排序状态,其包含两个属性: + * + * sortKey:类型为 String,进行排序的数据属性 + * + * asc:类型为 boolean,是否为升序 + * + */ + sort: { sortKey: string; asc: boolean }; + /** + * 分页状态,其包含两个属性: + * + * currentPage:类型Number,表示当前页 + * + * itemsPerPage:类型为Number,每页显示条数 + * + */ + pagination: { currentPage: number; itemsPerPage: number }; +} + +/** + * 表格行数据类型接口 + */ +export interface TiTableRowData { + /** + * 控制当前详情行是否展开 + * + */ + showDetails?: boolean; + /** + * 允许有多余的属性字段 + * + */ + [propName: string]: any; +} +/** + * TiTable 组件上 srcData 输入接口的数据类型接口 + */ +export interface TiTableSrcData { + /** + * 源数据 + */ + data: Array; + /** + * 源数据状态(是否已经过排序 sorted、过滤 searched、分页 paginated 处理)。比如,远程加载数据的分页场景下,从远程获取传入给 data 的数据为当前页数据, + * 即源数据已经进行了分页,因此应将 state.paginated 设置为true + * + */ + state: TiTableSrcState; +} + +/** + * 表格的各列信息 + * + */ +export interface TiTableColumns { + /** + * 表头文本内容,当某一列的 title 为空字符串时,当前列一直在表格中显示,但是不出现在控制列动态 + * 隐藏/显示 设置面板中,例如单选列、多选列、详情展开图标列 + * + */ + title?: string; + /** + * 设置列宽,支持百分比和px值; + */ + width?: string; + /** + * 此属性只有在表格具有列设置功能时才需要设置,show 可以分别设置如下值: + * + * true: 表示该列默认显示,用户可以通过列设置下拉面板操作切换其显示/隐藏状态。 + * + * false : 表示该列默认隐藏,用户可以通过列设置下拉面板操作切换其显示/隐藏状态。 + * + * undefined : 表示该列不具备动态显示/隐藏功能,固定显示。 + * + * 注意:设置show属性值时,title为空字符串时,该列固定显示,但不出现在列操作面板中。 + */ + show?: boolean; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} + +/** + * 表格当前页、每页个数记忆开关 + */ +export interface TiPaginationStorageConfig { + currentPage?: boolean; + itemsPerPage?: boolean; +} +/** + * 表格记忆各项开关 + */ +export interface TiTableStorageConfig { + /** + * 排序 + */ + sort: boolean; + /** + * 分页 + */ + pagination: boolean | TiPaginationStorageConfig; + /** + * 列宽 + */ + colsWidth: boolean; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} +/** + * TiTable 表格组件 + * + * 支持以表格的形式展示多条数据 + * + * 支持前后台分页、搜索、排序、详情展开、列拖动(调整列宽)、控制列动态隐藏/显示、行复选、行单选、 + * 表格单元格(th, td)智能tip、树表、列文本对齐方式、表头固定等功能 + * + */ +@Component({ + selector: 'ti-table', + templateUrl: './table.html', + styleUrls: ['./table.less', './table-nodata-small-nest-resize.less', './table-toggle-sort-details.less', './table-tree-fix.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + host: { + '[class.ti3-table]': 'true' + } +}) +export class TiTableComponent extends TiBaseComponent implements OnInit, AfterViewChecked, AfterViewInit, OnDestroy { + private static DEFAULT_SRC_DATA: TiTableSrcData = { + data: [], + state: { + searched: false, + sorted: false, + paginated: false + } + }; + /** + * 必选,源数据及其状态,srcData.data 是源数据,srcData.state 是数据状态(是否已经过排序 sorted、过滤 searched、分页 paginated 处理) + */ + @Input() srcData: TiTableSrcData = TiTableComponent.DEFAULT_SRC_DATA; + /** + * 必选,表格呈现的数据, 只需在初始化时将其设置为空数组即可,组件内部会对其赋值和更新 + * + */ + @Input() displayedData: Array; // 表格中实时展示的数据集 + /** + * 缓存表格状态的唯一标志值 + */ + @Input() storageId: string; + /** + * 配置表格各状态是否进行缓存 + */ + @Input() storageConfig: TiTableStorageConfig = { + sort: true, + pagination: true, + colsWidth: true + }; + /** + * 表格呈现的数据改变时触发的回调, 参数为呈现的数据集合 + */ + @Output() readonly displayedDataChange: EventEmitter> = new EventEmitter(); + /** + * 表格各列信息 + */ + @Input() columns: Array = []; + /** + * 被搜索的字符串的集合 + * + * 当 searchWords 长度为 1 时,在 searchKeys 指定的字段下搜索 searchWords[0] 指定的字符串, 在指定字段中的任一字段中搜索到时即满足条件(并集)。如:searchWords: ['b'], + * searchKeys: ['firstName', 'lastName'], 则在 firstName 字段包含 'b',或在 lastName 字段包含 'b' 时本行数据即满足搜索条件。 + * + * 当 searchWords 长度大于 1 且 searchKeys 与 searchWords 长度相等时,在 searchKeys 指定的字段下搜索 searchWords 对应(按索引顺序)指定的字符串,在指定字段中的所有字段 + * 搜索到对应字符串时才满足条件(交集)。如:searchWords: ['b', '18'], searchKeys: ['firstName', 'age'], 则在 firstName 字段包含 'b' 且在 age 字段包含 '18' 时本行数据才满足搜索条件。 + */ + @Input() searchWords: Array = null; + /** + * 搜索的字段范围 + */ + @Input() searchKeys: Array = null; // 是否使用null + /** + * 搜索时精确匹配(等于)的字段范围, 其值必须是属性 searchKeys 指定字段的范围的子集,不设置时默认按 searchKeys 指定字段范围进行模糊搜索 + * + */ + @Input() searchStrictKeys: Array = null; + /** + * 是否在展开当前行时关闭其他行 + */ + @Input() closeOtherDetails: boolean = false; + /** + * 分页、排序和搜索状态改变时触发的回调,参数为 ti-table 组件实例,一般用于远程加载数据 + * + */ + @Output() readonly stateUpdate: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + public hostEle: Element; + protected versionInfo: string = super.getVersion(packageInfo); + private srcDataDiffer: IterableDiffer; + private data: Array; + private promise: Promise = Promise.resolve(null); + /** + * @ignore 判断是否使用了ti-head-sort组件 + */ + public enableSort: boolean = false; + /** + * @ignore + */ + public paginationSubject: Subject = new Subject(); + /** + * @ignore + */ + public viewInitSubject: Subject = new Subject(); + /** + * @ignore + */ + public containerScrollXChangeSubject: Subject = new Subject(); + /** + * @ignore + */ + public thResizeSubject: Subject = new Subject(); + /** + * @ignore + */ + public updateFixedThLeftSubject: Subject = new Subject(); + /** + * @ignore + */ + public updateFixedTdLeftSubject: Subject = new Subject(); + /** + * @ignore + */ + public updateColumnsSubject: Subject> = new Subject>(); + /** + * @ignore + * 其中包含了sort、search、pagination三个属性,每个属性都描述了当前表格数据的参数状态: + * + * sort:object类型,包含了sortKey(类型为string,进行排序的数据属性)、asc(类型为boolean,是否为升序)属性; + * + * search: object类型,包含了searchWords(类型为array,搜索字符串集合)、searchKeys(类型为Array,搜索指定的字段范围)属性; + * + * pagination: object类型,包含了currentPage(类型number,表示当前页)、itemsPerPage(类型为Number,每页显示条数)属性。 + */ + public dataState: TiTableDataState = { + search: { searchWords: null, searchKeys: null, searchStrictKeys: null }, + sort: { sortKey: '', asc: null }, + pagination: { currentPage: -1, itemsPerPage: -1 } + }; + /** + * @ignore 子元素需要访问,所以public + */ + public localStorageState: { [propName: string]: any }; + /** + * @ignore 子元素需要访问,所以public + */ + public sessionStorageState: { [propName: string]: any }; + private isDataStateChange: boolean = false; + private oldSearchWords: Array = null; + private oldSearchKeys: Array = null; + private oldPagination: { currentPage: number; itemsPerPage: number } = { + currentPage: -1, + itemsPerPage: -1 + }; + private oldSort: { sortKey: string; asc: boolean } = { + sortKey: '', + asc: null + }; + /** + * 触发 stateUpdate 的事件(即引起表格数据状态改变的起因事件),其有"search", "sort", "pagination"这三个值 + */ + private triggerEvent: 'search' | 'sort' | 'pagination'; + private searchedResult: Array = []; + private customCompareFn: (a: TiTableRowData, b: TiTableRowData, sortKey?: string) => number; + /** + * @ignore + * 表格是否为表头锁定 + */ + public isFixedHead: boolean = false; + /** + * @ignore + */ + public theadContainer: Element; + /** + * @ignore + */ + public tbodyContainer: Element; + private oldTbodyHeight: number = 0; + private tbodyResizeObserver: any; + private unlistenFixedHeadWindowResize: () => void; + private unlistenFixedColumnWindowResize: () => void; + private unlistenWindowBeforeunload: () => void; + /** + * @ignore + */ + public processYScrollStateChangeWithColsResizable: () => void; + /** + * @ignore + */ + public fixedColumnInfo: { + hasFixedColumn?: boolean; + container?: any; + thFixedRight?: any; + containerWidth?: number; + fixedColumnLeftValues?: Array; + columnFixedLeftLastIndex?: number; + } = {}; + + constructor( + private iterableDiffers: IterableDiffers, + elementRef: ElementRef, + private fixedHeadService: TiTableFixedHeadService, + renderer: Renderer2, + private zone: NgZone, + private tiRenderer: TiRenderer, + private changeRef: ChangeDetectorRef + ) { + super(elementRef, renderer); + this.hostEle = elementRef.nativeElement; + } + + /** + * 测试某一字串(testStr)中是否存在子串(key) + * @param testStr 待检索的字符串或数值 + * @param key 检索的子串 + */ + private static isMatched(testStr: any, keywords: string, isStrict: boolean): boolean { + if (!Util.isString(testStr) && !Util.isNumber(testStr)) { + return false; + } + + if (isStrict) { + return keywords === '' || String(testStr) === keywords; + } else { + return String(testStr).toLowerCase().indexOf(keywords.toLowerCase()) >= 0; + } + } + + private static searchByOneWord( + src: Array, + searchWords: Array, + searchKeys: Array, + searchStrictKeys: Array + ): Array { + const output: Array = []; + src.forEach((item: TiTableRowData) => { + if (searchKeys && searchKeys.length > 0) { + for (const key of searchKeys) { + if (TiTableComponent.isMatched(item[key], searchWords[0], searchStrictKeys?.includes(key))) { + output.push(item); + break; + } + } + } else { + for (const key in item) { + if (TiTableComponent.isMatched(item[key], searchWords[0], searchStrictKeys?.includes(key))) { + output.push(item); + break; + } + } + } + }); + + return output; + } + + private static searchByMoreThanOneWord( + src: Array, + searchWords: Array, + searchKeys: Array, + searchStrictKeys: Array + ): Array { + const output: Array = []; + src.forEach((item: TiTableRowData) => { + let isMatched: boolean = true; + for (let i: number = 0; i < searchKeys.length; i++) { + if (!TiTableComponent.isMatched(item[searchKeys[i]], searchWords[i], searchStrictKeys?.includes(searchKeys[i]))) { + isMatched = false; + break; + } + } + if (isMatched) { + output.push(item); + } + }); + + return output; + } + + private static getSearchedData( + inputData: Array, + searchWords: Array, + searchKeys: Array, + searchStrictKeys: Array + ): Array { + let outData: Array = []; + + if (searchWords && searchWords.length === 1) { + outData = TiTableComponent.searchByOneWord(inputData, searchWords, searchKeys, searchStrictKeys); + } else if (searchWords && searchWords.length > 1 && searchKeys && searchKeys.length === searchWords.length) { + outData = TiTableComponent.searchByMoreThanOneWord(inputData, searchWords, searchKeys, searchStrictKeys); + } + + return outData; + } + + private static doCompare(a: any, b: any): number { + let result: number = 0; + if (a.type === b.type) { + if (a.value !== b.value) { + result = a.value < b.value ? -1 : 1; + } + } else { + result = a.type < b.type ? -1 : 1; + } + + return result; + } + + private static getPredicateValue(value: TiTableRowData, predicate: string): any { + if (!Util.isEmptyString(predicate) && Object.prototype.hasOwnProperty.call(value, predicate)) { + return value[predicate]; + } + + return value; + } + + private static getPredicateValueObj(value: any, index: number): { value: any; type: string } { + let resultVal: any = value; + if (value === null) { + resultVal = 'null'; + } else if (Util.isString(value)) { + resultVal = value.toLowerCase(); + } else if (Util.isArray(value)) { + resultVal = value.toString(); + } else if (typeof value === 'object') { + resultVal = index; + } else if (Util.isFunction(value)) { + const resultValue: any = value(); + + return TiTableComponent.getPredicateValueObj(resultValue, index); + } + const type: string = typeof resultVal; + + return { value: resultVal, type }; + } + + private static safeCopy(data: Array): Array { + return data ? [].concat(data) : []; + } + + private static isValueEqual(n: any, o: any): boolean { + if (!(n instanceof Object) || !(o instanceof Object)) { + return n === o; + } + + if (Util.isArray(n) && Util.isArray(o)) { + if (n.length !== o.length) { + return false; + } else { + for (let i: number = 0; i < o.length; i++) { + if (n[i] !== o[i]) { + return false; + } + } + } + + return true; + } + + if (Object.keys(n).length !== Object.keys(o).length) { + return false; + } else { + for (const key in n) { + if (!Object.prototype.hasOwnProperty.call(o, key) || n[key] !== o[key]) { + return false; + } + } + } + + return true; + } + + ngOnInit(): void { + super.ngOnInit(); + if (!this.srcData) { + this.srcData = TiTableComponent.DEFAULT_SRC_DATA; + } + if (!this.srcData.data) { + this.srcData.data = []; + } + this.srcDataDiffer = this.iterableDiffers.find(this.srcData.data).create(); + + if (this.searchWords !== null) { + this.oldSearchWords = TiTableComponent.safeCopy(this.searchWords); + this.dataState.search.searchWords = this.searchWords; + } + + if (this.searchKeys !== null) { + this.oldSearchKeys = TiTableComponent.safeCopy(this.searchKeys); + this.dataState.search.searchKeys = this.searchKeys; + } + if (this.searchStrictKeys !== null) { + this.dataState.search.searchStrictKeys = this.searchStrictKeys; + } + // 表格记忆 + if (this.storageId) { + this.initStorageDataState(); + } + } + + ngAfterViewInit(): void { + this.initFixedHead(); + if (this.fixedColumnInfo.hasFixedColumn) { + this.addFixedColumnBehavior(); + } + this.viewInitSubject.next(null); + if (this.storageId) { + // 在ngOnDestroy 无法保存,所有使用beforeunload + // 为什么不用unload? beforeunload 兼容比unload效果好 + // 参考:https://sinaad.github.io/xfe/2016/06/29/beforeunlod-vs-unload/ + // 修复SSR报错:ERROR ReferenceError: window is not defined + if (typeof window === 'undefined') { + return; + } + this.unlistenWindowBeforeunload = this.renderer.listen(window, 'beforeunload', () => { + this.updateStorageDataState(); + }); + } + } + + // 为了确保表格内嵌组件(比如tiHeadSort,tiPagination等)的数据变化在表格组件中能监听到,排序、搜索、分页 + // 的相关数据监听需要放在表格组件的AfterViewChecked周期中。因为表格组件的AfterViewChecked生命周期晚于 + // 内嵌组件(比如tiHeadSort,tiPagination等)的各个checked生命周期。 + // eslint-disable-next-line complexity + ngAfterViewChecked(): void { + // 监听srcData.data的变化 + const dataArr: Array = this.srcData && this.srcData.data ? this.srcData.data : []; + const srcDataDiffer: IterableChanges = this.srcDataDiffer.diff(dataArr); + if (srcDataDiffer) { + this.updateSafeCopy(); + this.data2displayed(); + } + + // 监听search相关接口值变化 + if (this.searchWords !== null && !TiTableComponent.isValueEqual(this.searchWords, this.oldSearchWords)) { + this.oldSearchWords = TiTableComponent.safeCopy(this.searchWords); + this.isDataStateChange = true; + this.triggerEvent = 'search'; + this.dataState.search.searchWords = this.searchWords; + // 搜索时,如果使用分页时,把用于表格内部计算分页的当前页码置为1 + if (this.dataState.pagination.itemsPerPage !== -1 && this.dataState.pagination.currentPage !== 1) { + this.dataState.pagination.currentPage = 1; + // 使用Promise是为了解决使用stateUpdate接口做后台分页+后台搜索时, + // 分页组件ExpressionChangedAfterItHasBeenCheckedError报错问题。 + // 此处不能使用setTimeout来解决,使用setTimeout会导致stateUpdate多次执行。 + this.promise.then(() => { + this.paginationSubject.next({ currentPage: 1 }); + }); + } + } + + if (this.searchKeys !== null && this.searchWords.length > 0 && !TiTableComponent.isValueEqual(this.searchKeys, this.oldSearchKeys)) { + this.oldSearchKeys = TiTableComponent.safeCopy(this.searchKeys); + this.isDataStateChange = true; + this.triggerEvent = 'search'; + this.dataState.search.searchKeys = this.searchKeys; + // 搜索时,如果使用分页时,把用于表格内部计算分页的当前页码置为1 + if (this.dataState.pagination.itemsPerPage !== -1 && this.dataState.pagination.currentPage !== 1) { + this.dataState.pagination.currentPage = 1; + this.promise.then(() => { + this.paginationSubject.next({ currentPage: 1 }); + }); + } + } + + if (!TiTableComponent.isValueEqual(this.dataState.sort, this.oldSort)) { + this.oldSort = { ...this.dataState.sort }; + this.isDataStateChange = true; + this.triggerEvent = 'sort'; + } + + if ( + this.dataState.pagination.currentPage !== -1 && + this.dataState.pagination.itemsPerPage !== -1 && + !TiTableComponent.isValueEqual(this.dataState.pagination, this.oldPagination) + ) { + this.oldPagination = { ...this.dataState.pagination }; + this.isDataStateChange = true; + this.triggerEvent = 'pagination'; + } + + if (this.isDataStateChange) { + this.updateTable(); + this.isDataStateChange = false; + } + + // 表格容器宽度改变时,列宽和横向滚动条状态都可能发生改变,这时需要处理表格列固定 + if (this.fixedColumnInfo.hasFixedColumn) { + const containerWidth: number = this.fixedColumnInfo.container.clientWidth; + if (containerWidth !== this.fixedColumnInfo.containerWidth) { + this.fixedColumnInfo.containerWidth = containerWidth; + this.processFixedColumn(); + } + } + } + + ngOnDestroy(): void { + if (this.isFixedHead && this.unlistenFixedHeadWindowResize) { + this.unlistenFixedHeadWindowResize(); + } + + if (this.unlistenFixedColumnWindowResize) { + this.unlistenFixedColumnWindowResize(); + } + + if (this.tbodyResizeObserver) { + this.tbodyResizeObserver.disconnect(); + } + + if (this.storageId) { + // 路由跳转时,页面没有刷新,beforeunload监听不生效,所以要更新一次记忆数据。 + this.updateStorageDataState(); + // 修复SSR错误:TypeError: this.unlistenWindowBeforeunload is not a function + typeof this.unlistenWindowBeforeunload === 'function' && this.unlistenWindowBeforeunload(); + } + } + // 初始化表格记忆 + private initStorageDataState(): void { + // 修复SSR报错:ERROR ReferenceError: window is not defined + if (typeof window === 'undefined') { + return; + } + // 缓存在sessionStorage中的数据 + let sessionStorageState = window.sessionStorage.getItem(this.storageId); + // 缓存在localStorage 中的数据 + let localStorageState = window.localStorage.getItem(this.storageId); + + // 版本发布后,用户第一次访问时,sessionStorageState、localStorageState是null + this.sessionStorageState = sessionStorageState ? JSON.parse(sessionStorageState) : {}; + this.localStorageState = localStorageState ? JSON.parse(localStorageState) : {}; + } + // 更新本地存储中的 + private updateStorageDataState() { + const localStorageState = { ...this.localStorageState }; + const sessionStorageState = { ...this.sessionStorageState }; + + // 排序 + if (this.storageConfig.sort && this.dataState.sort.sortKey) { + sessionStorageState['sort'] = this.dataState.sort; + } + + // 表格分页 + if ( + (this.storageConfig.pagination === true || + (this.storageConfig.pagination && this.storageConfig.pagination['itemsPerPage'] === true)) && + this.dataState.pagination.itemsPerPage !== -1 + ) { + // 每页条数 + localStorageState['itemsPerPage'] = this.dataState.pagination.itemsPerPage; + } + // 表格分页 + if ( + (this.storageConfig.pagination === true || + (this.storageConfig.pagination && this.storageConfig.pagination['currentPage'] === true)) && + this.dataState.pagination.currentPage !== -1 + ) { + // 当前页 + sessionStorageState['currentPage'] = this.dataState.pagination.currentPage; + } + + // 表格搜索 + if (this.dataState.search.searchKeys) { + sessionStorageState['searchKeys'] = this.dataState.search.searchKeys; + } + if (this.dataState.search.searchWords) { + sessionStorageState['searchWords'] = this.dataState.search.searchWords; + } + if (this.dataState.search.searchStrictKeys) { + sessionStorageState['searchStrictKeys'] = this.dataState.search.searchStrictKeys; + } + + if (Object.keys(sessionStorageState).length > 0) { + // 修复:SSR报错:ReferenceError: window is not defined + typeof window !== 'undefined' && window.sessionStorage.setItem(this.storageId, JSON.stringify(sessionStorageState)); + } + if (Object.keys(localStorageState).length > 0) { + typeof window !== 'undefined' && window.localStorage.setItem(this.storageId, JSON.stringify(localStorageState)); + } + } + + /** + * @ignore + */ + public updatePagination(currentPage: number, itemsPerPage: number): void { + if (this.oldPagination.currentPage === -1 && this.oldPagination.itemsPerPage === -1) { + this.oldPagination = { + currentPage, + itemsPerPage + }; + } + this.dataState.pagination.currentPage = currentPage; + this.dataState.pagination.itemsPerPage = itemsPerPage; + } + + /** + * @ignore + * 排序状态更新及处理 + * + * @param sortKey - 进行排序的数据属性(如对数据的name属性值进行排序,则传入"name") + * @param asc - 是否为升序,true表示升序,false表示降序,null表示无序 + * @param isDefaultSort - 是否为默认排序 + */ + public updateSort( + sortKey: string, + asc: boolean, + isDefaultSort: boolean, + compareFn: (a: TiTableRowData, b: TiTableRowData, sortKey?: string) => number + ): void { + if (isDefaultSort) { + this.oldSort = { + sortKey, + asc + }; + } + this.dataState.sort.sortKey = sortKey; + this.dataState.sort.asc = asc; + this.customCompareFn = compareFn; + } + /** + * 获取表格当前的数据状态(sort、search、pagination) + * @return TiTableDataState + */ + public getDataState(): TiTableDataState { + return this.dataState; + } + + /** + * 获取当前触发 stateUpdate 回调的操作(即引起表格数据状态改变的起因操作),有 "search", "sort", "pagination" 这三个值 + * @return 有"search", "sort", "pagination"这三个值 + */ + public getTriggerEvent(): string { + return this.triggerEvent; + } + + /** + * 使用搜索功能时,获取搜索的结果数据 + * @return 搜索到的数据集合 + * + */ + public getSearchedResult(): Array { + return this.searchedResult; + } + + private updateTable(): void { + if (this.isFrontPagination() || this.isFrontSort() || this.isFrontSearch()) { + this.data2displayed(); + } + // 延时的目的是防止stateUpdate 回调中同步修改数据时报错。 + this.promise.then(() => { + this.stateUpdate.emit(this); + }); + } + + /** + * 根据数据状态(search,sort,pagination)处理数据 + */ + private dataProcessor(data: Array): Array { + let output = data; + // 如果存在过滤条件且开发者传递的源数据尚未进行过滤处理,则进行前台过滤处理 + if (this.isFrontSearch()) { + output = TiTableComponent.getSearchedData(output, this.searchWords, this.searchKeys, this.searchStrictKeys); + this.searchedResult = [].concat(output); + const totalNumber: number = output.length; + // 如果存在分页且开发者传递的源数据尚未进行分页处理,即进行前台分页, + if (this.isFrontPagination()) { + this.promise.then(() => { + this.paginationSubject.next({ totalNumber }); + }); + } + } + // 如果存在排序且开发者传递的源数据尚未进行排序处理,则进行前台排序处理(字典排序) + if (this.isFrontSort()) { + output = this.getSortedData(output, this.dataState.sort.sortKey, this.dataState.sort.asc); + } + + // 存在分页且开发者传递的源数据尚未进行分页处理,则进行前台分页 + if (this.isFrontPagination()) { + const pagination: { currentPage: number; itemsPerPage: number } = this.dataState.pagination; + const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage; + output = output.slice(start, start + parseInt(pagination.itemsPerPage.toString(), 10)); + } + + return output; + } + /** + * data => displayedData,处理分3步: + * 1. 复制data数据 + * 2. 根据 分页、过滤、排序 处理数据 + * 3. 通过displayedDataChange 事件更新到displayedData + */ + private data2displayed() { + // 复制源数据 + let output: Array = TiTableComponent.safeCopy(this.data); + + // 处理数据 + output = this.dataProcessor(output); + + // 更新到displayedData + this.promise.then(() => { + // 同步修改内部数据 + this.displayedData = output; + this.displayedDataChange.emit(output); + }); + } + private isFrontSearch(): boolean { + const srcState: TiTableSrcState = this.getSrcState(); + return this.searchWords && this.searchWords.length > 0 && (!srcState || !srcState.searched); + } + private isFrontSort(): boolean { + const srcState: TiTableSrcState = this.getSrcState(); + return this.enableSort && (!srcState || !srcState.sorted); + } + private isFrontPagination(): boolean { + const srcState: TiTableSrcState = this.getSrcState(); + const pagination: { currentPage: number; itemsPerPage: number } = this.dataState.pagination; + return pagination.currentPage !== -1 && pagination.itemsPerPage !== -1 && (!srcState || !srcState.paginated); + } + + private getSortedData(data: Array, predicate: string, asc: boolean): Array { + let resultArr: Array = []; + if (!Util.isArray(data)) { + return resultArr; + } + + // 无序状态 + if (!predicate || asc === null) { + return data; + } + + const compareArr: Array<{ + value: TiTableRowData; + index: number; + predicateValueObj: { value: any; type: string }; + }> = data.map((value: TiTableRowData, index: number) => { + const predicateValue: any = TiTableComponent.getPredicateValue(value, predicate); + + return { + value, + index, + predicateValueObj: TiTableComponent.getPredicateValueObj(predicateValue, index) + }; + }); + compareArr.sort(this.compareTo); + resultArr = compareArr.map((item: { value: TiTableRowData; index: number; predicateValueObj: { value: any; type: string } }) => { + return item.value; + }); + if (!asc) { + resultArr.reverse(); + } + + return resultArr; + } + + private compareTo = ( + v1: { + value: TiTableRowData; + index: number; + predicateValueObj: { value: any; type: string }; + }, + v2: { + value: TiTableRowData; + index: number; + predicateValueObj: { value: any; type: string }; + } + ): number => { + let result: number = 0; + if (Util.isFunction(this.customCompareFn)) { + result = this.customCompareFn(v1.value, v2.value, this.dataState.sort.sortKey); + } else { + result = TiTableComponent.doCompare(v1.predicateValueObj, v2.predicateValueObj); + if (result === 0) { + result = v1.index - v2.index; + } + } + + return result; + }; + + private updateSafeCopy(): void { + this.data = TiTableComponent.safeCopy(this.srcData && this.srcData.data ? this.srcData.data : []); + } + + private getSrcState(): TiTableSrcState { + return this.srcData && this.srcData.state ? this.srcData.state : undefined; + } + + /** + * @ignore + */ + public closeOtherDetailsFn(currentRow: TiTableRowData): void { + this.displayedData.forEach((row: TiTableRowData) => { + if (row !== currentRow && row['showDetails']) { + // 保证其他详情展开能收起 + this.promise.then(() => { + row['showDetails'] = false; + this.changeRef.markForCheck(); // onpush模式下手动触发,详情收起 + }); + } + }); + } + + private initFixedHead(): void { + this.theadContainer = this.fixedHeadService.getTheadContainer(this.hostEle); + if (!this.theadContainer) { + return; + } + this.tbodyContainer = this.fixedHeadService.getTbodyContainer(this.theadContainer); + this.isFixedHead = !Util.isUndefined(this.theadContainer) && !Util.isUndefined(this.tbodyContainer); + + if (this.isFixedHead) { + this.renderer.listen(this.tbodyContainer, 'scroll', () => { + const scrollLeft: number = this.tbodyContainer.scrollLeft; + this.renderer.setStyle(this.theadContainer.children[0], 'marginLeft', -scrollLeft + 'px'); + Util.trigger(document, 'tiScroll'); + }); + + this.unlistenFixedHeadWindowResize = this.renderer.listen('window', 'resize', () => { + this.fixedHeadService.processOverflowY(this.theadContainer, this.tbodyContainer, this); + }); + + if ((window as any).ResizeObserver && this.tbodyContainer.children[0]) { + // 利用 ResizeObserver 来监听表体的尺寸发生改变的时机。IE不支持 ResizeObserver。 + this.tbodyResizeObserver = new (window as any).ResizeObserver((entries: any): void => { + if (entries[0] && entries[0].contentRect.height !== this.oldTbodyHeight) { + // 如果表头锁定+列拖动;在 TiColsResizableDirective 的 oninit 时会对该方法赋值;否则为 undefined + if (this.processYScrollStateChangeWithColsResizable) { + this.processYScrollStateChangeWithColsResizable(); + } else { + this.fixedHeadService.processOverflowY(this.theadContainer, this.tbodyContainer, this); + } + this.fixedHeadService.removeTbodyContainerBorderBottom(this.tbodyContainer, this.displayedData); + this.oldTbodyHeight = entries[0].contentRect.height; + } + }); + this.tbodyResizeObserver.observe(this.tbodyContainer.children[0]); + } + } + } + private addFixedColumnBehavior(): void { + const container: any = this.tiRenderer.findChildrenByClassName(this.hostEle, 'ti3-table-container')[0]; + if (!container) { + return; + } + this.zone.runOutsideAngular(() => { + this.renderer.listen(container, 'scroll', () => { + const scrollLeft: number = container.scrollLeft; + const isRightColumnFloat: boolean = scrollLeft + container.clientWidth < container.scrollWidth; + this.containerScrollXChangeSubject.next({ + scrollLeft, + isRightColumnFloat + }); + }); + + this.unlistenFixedColumnWindowResize = this.renderer.listen('window', 'resize', () => { + const containerWidth: number = this.fixedColumnInfo.container.clientWidth; + if (containerWidth !== this.fixedColumnInfo.containerWidth) { + this.fixedColumnInfo.containerWidth = containerWidth; + this.processFixedColumn(); + } + }); + }); + } + + /** + * @ignore + */ + public processFixedColumn(): void { + const scrollLeft: number = this.fixedColumnInfo.container.scrollLeft; + const isRightColumnFloat: boolean = + scrollLeft + this.fixedColumnInfo.container.clientWidth < this.fixedColumnInfo.container.scrollWidth; + this.containerScrollXChangeSubject.next({ + scrollLeft, + isRightColumnFloat + }); + this.updateFixedThLeftSubject.next(null); + this.updateFixedTdLeftSubject.next(null); + } + + /** + * @ignore + */ + public needTr(tr: any): boolean { + if (!tr) { + return false; + } + const classes: Array = [ + 'ti3-details-tr', + 'ti3-table-nodata', + 'ti3-table-loadfail', + 'ti3-table-nodata-guide', + 'ti3-table-nodata-simple' + ]; + for (const className of classes) { + if (this.tiRenderer.hasClass(tr, className)) { + return false; + } + } + + return true; + } +} diff --git a/src/table/lib/src/TiTableFixedHeadService.ts b/src/table/lib/src/TiTableFixedHeadService.ts new file mode 100644 index 0000000..c787192 --- /dev/null +++ b/src/table/lib/src/TiTableFixedHeadService.ts @@ -0,0 +1,290 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiTableFixedHeadServiceModule } from './TiTableFixedHeadServiceModule'; +import { ResizableOpts, TiColsResizableDirective } from './TiColsResizableDirective'; +import { TiTableComponent } from './TiTableComponent'; +/** + * @ignore + * 提供表头锁定功能 + */ +@Injectable({ + providedIn: TiTableFixedHeadServiceModule +}) +export class TiTableFixedHeadService { + private renderer: Renderer2; + constructor(rendererFactory: RendererFactory2, private tiRenderer: TiRenderer) { + this.renderer = rendererFactory.createRenderer(null, null); + } + + public getTheadContainer(hostEle: any): any { + return this.tiRenderer.findChildrenByClassName(hostEle, 'ti3-table-fixed-head')[0]; + } + + public getTbodyContainer(theadContainer: any): any { + const tbodyContainer: any = theadContainer.nextElementSibling; + if (tbodyContainer && this.tiRenderer.hasClass(tbodyContainer, 'ti3-table-container')) { + return tbodyContainer; + } + + return; + } + + public processOverflowY(theadContainer: any, tbodyContainer: any, tableCom: TiTableComponent): void { + const headTable: any = theadContainer.children[0]; + const scrollBarWidth: number = this.getScrollBarWidth('Y'); + const isOverflowedY: boolean = this.overflowedResult(tbodyContainer, 'Y'); + const isOverflowedX: boolean = this.overflowedResult(tbodyContainer, 'X'); + // 根据表体的宽度来设置表头的宽度 + if (isOverflowedY && !isOverflowedX) { + this.renderer.setStyle(headTable, 'width', `calc(100% - ${scrollBarWidth}px)`); + } else { + this.renderer.setStyle(headTable, 'width', '100%'); + } + + const fixheadFiller: any = this.tiRenderer.findChildrenByClassName(theadContainer, 'ti3-table-fixed-head-filler')[0]; + // 1px为经验值,避免计算的误差导致填充块和表头之间有一点空白间隔 + const fillerWidth: number = scrollBarWidth + 1; + // 根据Y轴是否有滚动条来控制表头右侧的填充块的有无 + if (isOverflowedY) { + if (tableCom.fixedColumnInfo.thFixedRight) { + this.renderer.setStyle(tableCom.fixedColumnInfo.thFixedRight, 'right', `${scrollBarWidth}px`); + } + + if (fixheadFiller) { + this.renderer.setStyle(fixheadFiller, 'width', fillerWidth + 'px'); + + return; + } + + const headerFiller: any = document.createElement('div'); + this.renderer.addClass(headerFiller, 'ti3-table-fixed-head-filler'); + this.renderer.setStyle(headerFiller, 'width', fillerWidth + 'px'); + theadContainer.appendChild(headerFiller); + } else if (fixheadFiller) { + this.renderer.removeChild(theadContainer, fixheadFiller); + if (tableCom.fixedColumnInfo.thFixedRight) { + this.renderer.setStyle(tableCom.fixedColumnInfo.thFixedRight, 'right', 0); + } + } + } + + public removeTbodyContainerBorderBottom(tbodyContainer: any, displayedData: Array): void { + const tbody: any = tbodyContainer.children[0].children[1]; + const hasNodataTbody: any = this.tiRenderer.findChildrenByClassName(tbody, 'ti3-table-nodata').length > 0; + const hasNodataGuideTbody: any = this.tiRenderer.findChildrenByClassName(tbody, 'ti3-table-nodata-guide').length > 0; + const hasLoadfailTbody: any = this.tiRenderer.findChildrenByClassName(tbody, 'ti3-table-loadfail').length > 0; + const hasNodataSimpleTbody: any = this.tiRenderer.findChildrenByClassName(tbody, 'ti3-table-nodata-simple').length > 0; + // 判断是否包含表格无数据,有图片是大间距,其他场景全部为单行文字样式,包括以下四种情况; + // 1、表格无数据:ti-table-nodata(有图片); + // 2、查询内容为空,提示用户'购买商品':ti-table-nodata-guide; + // 3、加载不成功,提示'重新加载':ti-table-loadfail。 + // 4. 表格无数据:ti-table-nodata-simple,只有文字提示(无图片) + const noBorderTerm: any = hasNodataTbody || hasNodataGuideTbody || hasLoadfailTbody || hasNodataSimpleTbody; + + if (displayedData.length === 0 && noBorderTerm) { + this.renderer.setStyle(tbodyContainer, 'border-bottom', 'none'); + } else { + this.renderer.setStyle(tbodyContainer, 'border-bottom', ''); + } + } + + // 由于用scrollHeight计算在IE和火狐下偶尔有1px的误差, + // 所以此处直接获取表格更准确的高度和容器的clientHeight作对比 + public overflowedResult(tbodyContainer: any, direction: string, isNum?: boolean): any { + let containerSize: number = 0; + let tbodySize: number = 0; + const tbodyTable: any = tbodyContainer.children[0]; + if (direction === 'X') { + containerSize = tbodyContainer.clientWidth; + // 修复SSR错误:ERROR TypeError: tbodyTable.getBoundingClientRect is not a function + if (typeof tbodyTable.getBoundingClientRect !== 'function') { + return; + } + tbodySize = this.numRound(tbodyTable.getBoundingClientRect().width, 0); + } else { + containerSize = tbodyContainer.clientHeight; + // 修复SSR错误:ERROR TypeError: tbodyTable.getBoundingClientRect is not a function + if (typeof tbodyTable.getBoundingClientRect !== 'function') { + return; + } + tbodySize = this.numRound(tbodyTable.getBoundingClientRect().height, 0); + } + + // 若isNum为true,返回差值,否则返回布尔值 + return isNum ? tbodySize - containerSize : tbodySize > containerSize; + } + + public processYScrollStateChange(colsResizableDire: TiColsResizableDirective): void { + const options: ResizableOpts = colsResizableDire.resizableOpts; + // 主要处理 columns 初始异步的场景 + if (!options.ths || options.ths.length === 0) { + return; + } + const tbodyContainer: any = colsResizableDire.tableCom.tbodyContainer; + const theadContainer: any = colsResizableDire.tableCom.theadContainer; + const newYScrollState: boolean = this.overflowedResult(tbodyContainer, 'Y'); + if (newYScrollState === options.yScrollState) { + this.processOverflowY(theadContainer, tbodyContainer, colsResizableDire.tableCom); + + return; + } + + const newXScrollState: boolean = this.overflowedResult(tbodyContainer, 'X'); + let lastColIndex: number = options.ths.length - 1; + let lastTh: any = options.ths[lastColIndex]; + let lastThWidth: number = options.storedSizes[lastColIndex]; + const scrollBarWidth: number = this.getScrollBarWidth('Y'); + // 纵向滚动条由无到有 + if (newYScrollState) { + if (newXScrollState && newXScrollState === options.xScrollState) { + options.storeTableWidthChange += scrollBarWidth; + } else { + // 第一次纵向滚动条状态发生变化要减小表格宽度时,去减小非特殊列中宽度最大的一列的宽度 + // 这样处理是为了避免最后一列的宽度变小 + if (colsResizableDire.isfirstYScrollStateChange) { + lastTh = this.getMaxWidthTh(colsResizableDire); + if (lastTh) { + lastColIndex = parseInt(lastTh.getAttribute('ti-visible-index'), 10); + lastThWidth = options.storedSizes[lastColIndex]; + } + } + options.storedSizes[lastColIndex] = lastThWidth - scrollBarWidth; + } + + // 纵向滚动条由有到无 + } else { + if (newXScrollState === options.xScrollState) { + if (newXScrollState) { + options.storeTableWidthChange -= scrollBarWidth; + } else { + options.storedSizes[lastColIndex] = lastThWidth + scrollBarWidth; + } + } else if (!newXScrollState) { + options.storedSizes[lastColIndex] = lastThWidth + (scrollBarWidth - options.storeTableWidthChange); + options.storeTableWidthChange = 0; + } + } + if (lastTh) { + colsResizableDire.setWidth(lastTh, options.storedSizes[lastColIndex]); + colsResizableDire.setWidth(options.secondThs[lastColIndex], options.storedSizes[lastColIndex]); + } + + this.processOverflowY(theadContainer, tbodyContainer, colsResizableDire.tableCom); + colsResizableDire.isfirstYScrollStateChange = false; + options.xScrollState = this.overflowedResult(tbodyContainer, 'X'); + options.yScrollState = this.overflowedResult(tbodyContainer, 'Y'); + + // 纵向滚动条状态改变时可能会引起列宽改变和横向滚动条状态改变,所以要处理列固定 + if (colsResizableDire.tableCom.fixedColumnInfo.hasFixedColumn) { + colsResizableDire.tableCom.processFixedColumn(); + } + } + + private getMaxWidthTh(colsResizableDire: TiColsResizableDirective): any { + const options: ResizableOpts = colsResizableDire.resizableOpts; + colsResizableDire.updateStoredSizes(); + + const ths: Array = options.ths.filter((th: any) => { + // 排除最后一列和不可拖动的列 + return !th.hasAttribute(colsResizableDire.notResizableAttr) && !colsResizableDire.isLastColumn(th); + }); + + // 按列宽从小到大排序 + const sortedThs: Array = ths.sort((a: any, b: any) => { + const indexA: number = parseInt(a.getAttribute('ti-visible-index'), 10); + const indexB: number = parseInt(b.getAttribute('ti-visible-index'), 10); + + return options.storedSizes[indexA] - options.storedSizes[indexB]; + }); + + // 返回宽度最大的列 + return sortedThs[sortedThs.length - 1]; + } + + // 处理在列拖动过程中横向滚动条的出现引起纵向滚动条出现时的账本(storeColsWidthChange)的变化 + public handleYOverflowedWithX(resizableOpts: ResizableOpts, tbodyContainer: any): void { + const newYScrollState: boolean = this.overflowedResult(tbodyContainer, 'Y'); + + if (newYScrollState === resizableOpts.yScrollState) { + return; + } + + const scrollBarWidth: number = this.getScrollBarWidth('Y'); + const newXScrollState: boolean = this.overflowedResult(tbodyContainer, 'X'); + if (newYScrollState) { + resizableOpts.storeTableWidthChange += scrollBarWidth; + resizableOpts.isYOverflowedWithX = true; + } + + resizableOpts.xScrollState = newXScrollState; + resizableOpts.yScrollState = newYScrollState; + } + + public handleYNotOverflowedWithX(resizableOpts: ResizableOpts, tbodyContainer: any): void { + const scrollBarWidthX: number = this.getScrollBarWidth('X'); + const scrollBarWidthY: number = this.getScrollBarWidth('Y'); + const overflowedHeight: number = this.overflowedResult(tbodyContainer, 'Y', true); + const overflowedWidth: number = this.overflowedResult(tbodyContainer, 'X', true); + // 当列拖动横向滚动条出现导致纵向滚动条出现后,再缩小列宽时,由于无法准确获得横向滚动条消失的时刻去对表格 + // 宽度做补偿,所以当表格列宽到达容器边界附近时,用临时将overflow-x设为hidden的方式使横向滚动条消失。 + if ( + resizableOpts.isYOverflowedWithX && + overflowedHeight > 0 && + overflowedHeight <= scrollBarWidthX && + overflowedWidth > 0 && + overflowedWidth <= scrollBarWidthY + ) { + this.renderer.setStyle(tbodyContainer, 'overflow-x', 'hidden'); + // 对账本(storeTableWidthChange)清零 + resizableOpts.storeTableWidthChange = 0; + resizableOpts.isYOverflowedWithX = false; + + setTimeout(() => { + this.renderer.setStyle(tbodyContainer, 'overflow-x', 'auto'); + }, 0); + } + } + + // 获取滚动条宽度 + public getScrollBarWidth(direction: string): number { + const temporaryDiv: any = document.createElement('div'); + this.tiRenderer.setStyles(temporaryDiv, { + position: 'absolute', + top: '-9999px', + left: '-9999px', + width: '100px', + height: '100px', + overflow: 'hidden' + }); + const nestDiv: any = document.createElement('div'); + this.tiRenderer.setStyles(nestDiv, { width: '100%', height: '100%' }); + temporaryDiv.appendChild(nestDiv); + document.body.appendChild(temporaryDiv); + const noScrollWidth: number = nestDiv.getBoundingClientRect().width; + const noScrollHeight: number = nestDiv.getBoundingClientRect().height; + this.renderer.setStyle(temporaryDiv, 'overflow', 'scroll'); + const scrollWidth: number = nestDiv.getBoundingClientRect().width; + const scrollHeight: number = nestDiv.getBoundingClientRect().height; + this.renderer.removeChild(document.body, temporaryDiv); + const scrollBarWidthX: number = this.numRound(noScrollHeight - scrollHeight, 1); + const scrollBarWidthY: number = this.numRound(noScrollWidth - scrollWidth, 1); + + return direction === 'X' ? scrollBarWidthX : scrollBarWidthY; + } + + // 对浮点数四舍五入保留指定小数位数的数字 + public numRound(num: number, decimal: number): number { + return parseFloat(num.toFixed(decimal)); + } +} diff --git a/src/table/lib/src/TiTableFixedHeadServiceModule.ts b/src/table/lib/src/TiTableFixedHeadServiceModule.ts new file mode 100644 index 0000000..55180e5 --- /dev/null +++ b/src/table/lib/src/TiTableFixedHeadServiceModule.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +/** + * @ignore + */ +@NgModule({ + imports: [] +}) +export class TiTableFixedHeadServiceModule {} diff --git a/src/table/lib/src/TiTableFixedSizeVirtualScrollDirective.ts b/src/table/lib/src/TiTableFixedSizeVirtualScrollDirective.ts new file mode 100644 index 0000000..72f6bd9 --- /dev/null +++ b/src/table/lib/src/TiTableFixedSizeVirtualScrollDirective.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, forwardRef } from '@angular/core'; +import { CdkFixedSizeVirtualScroll, FixedSizeVirtualScrollStrategy, VIRTUAL_SCROLL_STRATEGY } from '@angular/cdk/scrolling'; + +/** + * @ignore + */ +export function fixedSizeVirtualScrollStrategyFactory( + fixedSizeDir: TiTableFixedSizeVirtualScrollDirective +): FixedSizeVirtualScrollStrategy { + return fixedSizeDir._scrollStrategy; +} + +/** + * @ignore + * 配合表格的表体虚拟滚动容器组件 TiTableVirtualScrollViewportComponent 来使用的指令。 + * + * 主要是重新定义了指令选择器selector。 + * + * + * 相关代码参考 @angular/cdk/scrolling 提供的 CdkFixedSizeVirtualScroll 指令。 + * + */ +@Directive({ + selector: '.ti3-table-container[itemSize]', + providers: [ + { + provide: VIRTUAL_SCROLL_STRATEGY, + useFactory: fixedSizeVirtualScrollStrategyFactory, + deps: [forwardRef(() => TiTableFixedSizeVirtualScrollDirective)] + } + ] +}) +export class TiTableFixedSizeVirtualScrollDirective extends CdkFixedSizeVirtualScroll {} diff --git a/src/table/lib/src/TiTableModule.ts b/src/table/lib/src/TiTableModule.ts new file mode 100644 index 0000000..d5050f8 --- /dev/null +++ b/src/table/lib/src/TiTableModule.ts @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ScrollingModule } from '@angular/cdk/scrolling'; +import { TiDateModule } from '@opentiny/ng-date'; +import { TiDatetimeModule } from '@opentiny/ng-datetime'; +import { TiListModule } from '@opentiny/ng-list'; +import { TiSearchboxModule } from '@opentiny/ng-searchbox'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiTableFixedHeadServiceModule } from './TiTableFixedHeadServiceModule'; +import { TiTableComponent } from './TiTableComponent'; +import { TiHeadFilterComponent } from './TiHeadFilterComponent'; +import { TiHeadFilterDropComponent } from './TiHeadFilterDropComponent'; +import { TiHeadSortComponent } from './TiHeadSortComponent'; +import { TiHeadMenuComponent } from './TiHeadMenuComponent'; +import { TiDetailsIconComponent } from './TiDetailsIconComponent'; +import { TiDetailsTrDirective } from './TiDetailsTrDirective'; +import { TiColspanDirective } from './TiColspanDirective'; +import { TiColsToggleComponent } from './TiColsToggleComponent'; +import { TiColsToggleDropComponent } from './TiColsToggleDropComponent'; +import { TiColsResizableDirective } from './TiColsResizableDirective'; +import { TiCellTextComponent } from './TiCellTextComponent'; +import { TiCellIconsComponent } from './TiCellIconsComponent'; +import { TiColumnsPipe } from './TiColumnsPipe'; +import { TiColumnFixedDirective } from './TiColumnFixedDirective'; +import { TiColClickDirective } from './TiColClickDirective'; +import { TiTableVirtualScrollViewportComponent } from './TiTableVirtualScrollViewportComponent'; +import { TiTableFixedSizeVirtualScrollDirective } from './TiTableFixedSizeVirtualScrollDirective'; +import { TiLocaleModule, TiLocale } from '@opentiny/ng-locale'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiButtonModule } from '@opentiny/ng-button'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { locales } from './i18n'; +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiButtonModule, + TiIconModule, + TiListModule, + TiSearchboxModule, + TiDropModule, + TiOverflowModule, + TiTableFixedHeadServiceModule, + TiLocaleModule, + TiRendererModule, + TiTipModule, + TiOutlineModule, + ScrollingModule, + TiDateModule, + TiDatetimeModule + ], + exports: [ + TiTableComponent, + TiHeadFilterComponent, + TiHeadSortComponent, + TiHeadMenuComponent, + TiDetailsIconComponent, + TiDetailsTrDirective, + TiColspanDirective, + TiColsToggleComponent, + TiColsResizableDirective, + TiCellTextComponent, + TiCellIconsComponent, + TiColumnFixedDirective, + TiColClickDirective, + TiTableVirtualScrollViewportComponent, + TiTableFixedSizeVirtualScrollDirective + ], + declarations: [ + TiTableComponent, + TiHeadFilterComponent, + TiHeadFilterDropComponent, + TiHeadSortComponent, + TiHeadMenuComponent, + TiDetailsIconComponent, + TiDetailsTrDirective, + TiColspanDirective, + TiColsToggleComponent, + TiColsToggleDropComponent, + TiColsResizableDirective, + TiCellTextComponent, + TiCellIconsComponent, + TiColumnsPipe, + TiColumnFixedDirective, + TiColClickDirective, + TiTableVirtualScrollViewportComponent, + TiTableFixedSizeVirtualScrollDirective + ] +}) +export class TiTableModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { + TiTableComponent, + TiTableSrcState, + TiTableDataState, + TiTableRowData, + TiTableSrcData, + TiTableColumns, + TiTableStorageConfig, + TiPaginationStorageConfig +} from './TiTableComponent'; +export { TiHeadFilterComponent } from './TiHeadFilterComponent'; +export { TiHeadSortComponent } from './TiHeadSortComponent'; +export { TiHeadMenuComponent } from './TiHeadMenuComponent'; +export { TiDetailsIconComponent } from './TiDetailsIconComponent'; +export { TiDetailsTrDirective } from './TiDetailsTrDirective'; +export { TiColspanDirective } from './TiColspanDirective'; +export { TiColsToggleComponent } from './TiColsToggleComponent'; +export { TiColsResizableDirective } from './TiColsResizableDirective'; +export { TiCellTextComponent } from './TiCellTextComponent'; +export { TiCellIconsComponent } from './TiCellIconsComponent'; +export { TiColumnFixedDirective } from './TiColumnFixedDirective'; +export { TiColClickDirective } from './TiColClickDirective'; +export { TiTableVirtualScrollViewportComponent } from './TiTableVirtualScrollViewportComponent'; +export { fixedSizeVirtualScrollStrategyFactory, TiTableFixedSizeVirtualScrollDirective } from './TiTableFixedSizeVirtualScrollDirective'; diff --git a/src/table/lib/src/TiTableVirtualScrollViewportComponent.ts b/src/table/lib/src/TiTableVirtualScrollViewportComponent.ts new file mode 100644 index 0000000..7c67b11 --- /dev/null +++ b/src/table/lib/src/TiTableVirtualScrollViewportComponent.ts @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewEncapsulation } from '@angular/core'; +import { CdkVirtualForOf, CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { Subject, Subscription } from 'rxjs'; + +/** + * 表格的表体虚拟滚动容器组件 + * + * + * 为了在原有表格表头锁定的基础上简化使用虚拟滚动且不增加dom层级,这里并没有让业务直接使用 @angular/cdk/scrolling 提供的 CdkVirtualScrollViewport() 组件, + * 而是继承 CdkVirtualScrollViewport 来适配 TiTable。 + * + * 相关代码参考 @angular/cdk/scrolling 提供的 CdkVirtualScrollViewport 组件。 + */ +@Component({ + selector: '.ti3-table-container[itemSize]', + templateUrl: './table-virtual-scroll-viewport.html', + styleUrls: ['./table-virtual-scroll-viewport.less'], + host: { + '[class.ti3-table-virtual-scroll-viewport]': 'true', + // _totalContentHeight 该变量虽然以下划线开头,但在 CdkVirtualScrollViewport 类中是 public 的,且在 CdkVirtualScrollViewport 组件的模板中也有使用 + '[style.height]': 'getTotalContentHeight()', + // 初始化无数据时_totalContentHeight为空字符串,需要兼容这种情况 + '[class.ti3-table-virtual-scroll-viewport-nodata]': `getTotalContentHeight() === '0px'` + }, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + // 参考 CdkVirtualScrollViewport 组件的元数据配置 + provide: CdkVirtualScrollViewport, + useExisting: TiTableVirtualScrollViewportComponent + } + ] +}) +export class TiTableVirtualScrollViewportComponent extends CdkVirtualScrollViewport implements OnInit { + private dataStreamSubscription: Subscription; + /** + * + * 虚拟滚动容器中是否有 cdkVirtualFor 指令 + * + * 使用表格时经常会在无数据或数据加载中时将 cdkVirtualFor 指令那块dom销毁隐藏掉(ngIf=false), 而在有数据时才会使用 cdkVirtualFor 指令渲染数据。 + * + * hasCdkVirtualFor 为 true 时表示有 cdkVirtualFor 指令存在, 为 false 时表示此时 cdkVirtualFor 指令不存在。 + */ + private hasCdkVirtualFor: boolean = false; + + ngOnInit(): void { + // 获取 .ti3-table-container 容器的子元素: table 元素 + const tableEle: any = this.elementRef.nativeElement.children[0]; + // 将子元素 table 作为 virtual-scroll-content-wrapper,类比 CdkVirtualScrollViewport 组件中的 .cdk-virtual-scroll-content-wrapper + // 元素,从而做到不增加dom层级 + if (tableEle) { + tableEle.className += 'ti3-table-virtual-scroll-content-wrapper'; + // 覆写 CdkVirtualScrollViewport 类中的 _contentWrapper 变量(该变量在CdkVirtualScrollViewport中是public) + this._contentWrapper = new ElementRef(tableEle); + super.ngOnInit(); + } + } + + /** + * @ignore + * + * cdkVirtualFor 指令在初始化时会调用 CdkVirtualScrollViewport 虚拟滚动容器中的 attach 方法 + * + * 覆写 CdkVirtualScrollViewport 类中的 attach 方法。 + * + * @param forOf cdkVirtualFor结构指令实例 + */ + public attach(forOf: CdkVirtualForOf): void { + super.attach(forOf); + + // 此时 cdkVirtualFor 指令初始化,即存在 dkVirtualFor 指令。 + this.hasCdkVirtualFor = true; + + // 这里可以获取到forOf: CdkVirtualForOf,在forOf.dataStream中可以获取数据改变(数据搜索过滤,排序,切换分页等)的时机, + // 需要在这个时机将虚拟滚动容器滚动条置回顶部。 + this.ngZone.runOutsideAngular(() => { + this.dataStreamSubscription = forOf.dataStream.subscribe((data: any): void => { + this.elementRef.nativeElement.scrollTop = 0; + }); + }); + + // 使用表格时经常会是 CdkVirtualScrollViewport 虚拟滚动容器(即该组件)一直存在,而在无数据或数据加载中时将 cdkVirtualFor 指令 + // 那块dom销毁隐藏掉(ngIf=false), 有数据时才会使用 cdkVirtualFor 指令渲染数据。在 cdkVirtualFor 指令部分动态切换(生成或销毁)过程中, + // 销毁后再生成时,需要手动触发渲染一次数据,不然出现空白数据渲染不出来。 + setTimeout(() => { + // 需延时等 cdkVirtualFor 指令拿到数据集 + // 研究源码发现此方法可触发使 cdkVirtualFor 指令进行计算渲染数据 + (this.renderedRangeStream as Subject).next(this.getRenderedRange()); + }, 0); + } + + /** + * @ignore + * + * cdkVirtualFor 指令在 OnDestroy 时会调用 CdkVirtualScrollViewport 虚拟滚动容器中的 detach 方法。 + * + * 覆写 CdkVirtualScrollViewport 类中的 detach 方法。 + */ + public detach(): void { + super.detach(); + + // 此时 cdkVirtualFor 指令销毁,即不存在 dkVirtualFor 指令。 + this.hasCdkVirtualFor = false; + + // 取消在 attach 方法中的订阅。 + if (this.dataStreamSubscription) { + this.dataStreamSubscription.unsubscribe(); + } + } + + /** + * @ignore + * + * 由于数据量可能有多有少,所以业务不能直接给表体容器设置高度而是要设置最大高度。 + * + * CdkVirtualScrollViewport 组件中通过 getViewportSize 方法获取虚拟容器高度来计算实际应该渲染的数据,而这个方法底层是直接是读取滚动容器的 clientHeight, + * 那么给容器设置最大高度时这个 clientHeight 获取的就是不准确的, 所以这里要 覆写 CdkVirtualScrollViewport 类中的 getViewportSize 方法。 + */ + public getViewportSize(): number { + const maxHeight: number = parseInt(getComputedStyle(this.elementRef.nativeElement).maxHeight, 10); + if (maxHeight) { + return maxHeight; + } + const height: number = parseInt(getComputedStyle(this.elementRef.nativeElement).height, 10); + if (height) { + return height; + } + + return 0; + } + + /** + * @ignore + * + * 元数据 host 中使用 + */ + public getTotalContentHeight(): string { + if (!this.hasCdkVirtualFor || !this._totalContentHeight) { + return '0px'; + } + // _totalContentHeight 该变量虽然以下划线开头,但在 CdkVirtualScrollViewport 类中是 public 的,且在 CdkVirtualScrollViewport 组件的模板中也有使用 + const totalContentHeight: number = parseInt(this._totalContentHeight, 10); + + // 减去 1px 是因为表格最后一条行是没有border-bottom的 + return totalContentHeight > 0 ? `${totalContentHeight - 1}px` : this._totalContentHeight; + } +} diff --git a/src/table/lib/src/cell-icons.html b/src/table/lib/src/cell-icons.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/table/lib/src/cell-icons.html @@ -0,0 +1 @@ + diff --git a/src/table/lib/src/cell-text.html b/src/table/lib/src/cell-text.html new file mode 100644 index 0000000..c319eb7 --- /dev/null +++ b/src/table/lib/src/cell-text.html @@ -0,0 +1,3 @@ + + + diff --git a/src/table/lib/src/cols-toggle-drop.html b/src/table/lib/src/cols-toggle-drop.html new file mode 100644 index 0000000..647ec52 --- /dev/null +++ b/src/table/lib/src/cols-toggle-drop.html @@ -0,0 +1,31 @@ + +
    + +
    + + +
    diff --git a/src/table/lib/src/cols-toggle.html b/src/table/lib/src/cols-toggle.html new file mode 100644 index 0000000..80d9e7a --- /dev/null +++ b/src/table/lib/src/cols-toggle.html @@ -0,0 +1,22 @@ + + diff --git a/src/table/lib/src/details-icon.html b/src/table/lib/src/details-icon.html new file mode 100644 index 0000000..d26f703 --- /dev/null +++ b/src/table/lib/src/details-icon.html @@ -0,0 +1,7 @@ + + + diff --git a/src/table/lib/src/head-filter-drop.html b/src/table/lib/src/head-filter-drop.html new file mode 100644 index 0000000..4324719 --- /dev/null +++ b/src/table/lib/src/head-filter-drop.html @@ -0,0 +1,138 @@ + + + +
    + + +
    + + + + + + {{item[labelKey]}} + + +
    +
    + + +
    +
    +
    + + + +
    + + + +
    +
    {{ 'tiTable.headFilterDatetimeTitle' | tiTranslate }}
    +
    + + + + + + + +
    +
    + + + + + + + +
    +
    + + +
    +
    +
    diff --git a/src/table/lib/src/head-filter-drop.less b/src/table/lib/src/head-filter-drop.less new file mode 100644 index 0000000..5b654f1 --- /dev/null +++ b/src/table/lib/src/head-filter-drop.less @@ -0,0 +1,40 @@ +.ti3-head-filter-drop ti-searchbox-notsearch { + width: 100%; +} +.ti3-head-filter-drop-button-container { + padding: var(--ti-common-space-10) var(--ti-common-space-0); + margin: var(--ti-common-space-0) var(--ti-common-space-10); + border-top: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solids) var(--ti-common-color-line-dividing); +} + +.ti3-head-filter-drop-button-ok { + margin-right: var(--ti-common-space-2x); +} + +.ti3-head-filter-drop-datetime { + padding: var(--ti-common-space-10); + .ti3-head-filter-drop-datetime-tip { + line-height: var(--ti-common-line-height-number); + padding-bottom: var(--ti-common-space-10); + color: var(--ti-common-color-text-weaken); + } + + .ti3-head-filter-drop-datetime-field { + height: var(--ti-common-size-7x); + min-width: 250px; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--ti-common-space-10); + } + + .ti3-head-filter-drop-datetime-buttons { + border-top: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + padding-top: var(--ti-common-space-10); + display: flex; + justify-content: flex-end; + button:first-child { + margin-right: var(--ti-common-space-10); + } + } +} diff --git a/src/table/lib/src/head-filter.html b/src/table/lib/src/head-filter.html new file mode 100644 index 0000000..5bf35e5 --- /dev/null +++ b/src/table/lib/src/head-filter.html @@ -0,0 +1,27 @@ + + + + + + +{{item[labelKey]}} diff --git a/src/table/lib/src/head-filter.less b/src/table/lib/src/head-filter.less new file mode 100644 index 0000000..cf74bb0 --- /dev/null +++ b/src/table/lib/src/head-filter.less @@ -0,0 +1,20 @@ +:host.ti3-head-filter-container { + display: inline-block; + vertical-align: middle; + padding-left: var(--ti-common-space-2x); + line-height: var(--ti-common-size-4x); + width: var(--ti-common-size-4x); + .ti3-head-filter-icon { + font-size: var(--ti-common-size-4x); + display: block; + cursor: pointer; + color: var(--ti-common-color-icon-normal); + &:hover { + color: var(--ti-common-color-icon-hover); + } + } + + &.ti3-head-filtered .ti3-head-filter-icon { + color: var(--ti-common-color-icon-active); + } +} diff --git a/src/table/lib/src/head-menu.html b/src/table/lib/src/head-menu.html new file mode 100644 index 0000000..d56b9dd --- /dev/null +++ b/src/table/lib/src/head-menu.html @@ -0,0 +1,15 @@ + + + + + diff --git a/src/table/lib/src/head-menu.less b/src/table/lib/src/head-menu.less new file mode 100644 index 0000000..32eed50 --- /dev/null +++ b/src/table/lib/src/head-menu.less @@ -0,0 +1,26 @@ +ti-head-menu { + --ti-head-menu-angle-size: var(--ti-common-size-3x); +} + +.ti3-head-menu-angle { + font-size: var(--ti-head-menu-angle-size); + width: var(--ti-head-menu-angle-size); + line-height: var(--ti-head-menu-angle-size); + display: inline-block; + vertical-align: middle; + margin-left: -4px; + cursor: pointer; + &:hover { + color: var(--ti-common-color-icon-hover); + } +} +.ti3-head-menu-angle-down { + transform: rotate(90deg); +} +.ti3-head-menu-angle-up { + transform: rotate(-90deg); +} +.ti3-table > table > thead > tr > th:first-child[checkbox-column].ti3-head-menu-cell { + width: 52px !important; + cursor: auto; +} diff --git a/src/table/lib/src/head-sort.html b/src/table/lib/src/head-sort.html new file mode 100644 index 0000000..5f254bc --- /dev/null +++ b/src/table/lib/src/head-sort.html @@ -0,0 +1,3 @@ + + + diff --git a/src/table/lib/src/i18n/TiTableWords.ts b/src/table/lib/src/i18n/TiTableWords.ts new file mode 100644 index 0000000..a806cbc --- /dev/null +++ b/src/table/lib/src/i18n/TiTableWords.ts @@ -0,0 +1,8 @@ +export interface TiTableWords { + tiTable: { + colsToggleTip: string; + headFilterDatetimeTitle: string; + headMenuSelectAll: string; + headMenuClearAll: string; + }; +} diff --git a/src/table/lib/src/i18n/en_US.ts b/src/table/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..70ea7d8 --- /dev/null +++ b/src/table/lib/src/i18n/en_US.ts @@ -0,0 +1,10 @@ +import { TiTableWords } from './TiTableWords'; + +export const en_US: TiTableWords = { + tiTable: { + colsToggleTip: 'Customize Column', + headFilterDatetimeTitle: 'Enter at least one date.', + headMenuSelectAll: 'Select All', + headMenuClearAll: 'Clear All' + } +}; diff --git a/src/table/lib/src/i18n/es_US.ts b/src/table/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..86219bd --- /dev/null +++ b/src/table/lib/src/i18n/es_US.ts @@ -0,0 +1,10 @@ +import { TiTableWords } from './TiTableWords'; + +export const es_US: TiTableWords = { + tiTable: { + colsToggleTip: 'Personalizar columna', + headFilterDatetimeTitle: 'Ingrese al menos una fecha.', + headMenuSelectAll: 'Seleccionar todo', + headMenuClearAll: 'Deseleccionar todo' + } +}; diff --git a/src/table/lib/src/i18n/fr_FR.ts b/src/table/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..17190b0 --- /dev/null +++ b/src/table/lib/src/i18n/fr_FR.ts @@ -0,0 +1,10 @@ +import { TiTableWords } from './TiTableWords'; + +export const fr_FR: TiTableWords = { + tiTable: { + colsToggleTip: 'Personnaliser la colonne', + headFilterDatetimeTitle: 'Saisissez au moins une date.', + headMenuSelectAll: 'Sélectionner tout', + headMenuClearAll: 'Effacer tout' + } +}; diff --git a/src/table/lib/src/i18n/index.ts b/src/table/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/table/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/table/lib/src/i18n/pt_BR.ts b/src/table/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..27f0ae0 --- /dev/null +++ b/src/table/lib/src/i18n/pt_BR.ts @@ -0,0 +1,10 @@ +import { TiTableWords } from './TiTableWords'; + +export const pt_BR: TiTableWords = { + tiTable: { + colsToggleTip: 'Personalizar coluna', + headFilterDatetimeTitle: 'Insira pelo menos uma data.', + headMenuSelectAll: 'Selecionar tudo', + headMenuClearAll: 'Limpar tudo' + } +}; diff --git a/src/table/lib/src/i18n/zh_CN.ts b/src/table/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..85415bd --- /dev/null +++ b/src/table/lib/src/i18n/zh_CN.ts @@ -0,0 +1,10 @@ +import { TiTableWords } from './TiTableWords'; + +export const zh_CN: TiTableWords = { + tiTable: { + colsToggleTip: '自定义列表项', + headFilterDatetimeTitle: '请至少输入一个日期', + headMenuSelectAll: '选择所有', + headMenuClearAll: '清空所有' + } +}; diff --git a/src/table/lib/src/table-nodata-small-nest-resize.less b/src/table/lib/src/table-nodata-small-nest-resize.less new file mode 100644 index 0000000..16b6684 --- /dev/null +++ b/src/table/lib/src/table-nodata-small-nest-resize.less @@ -0,0 +1,198 @@ +@import '../../../themes/basic/base-all.less'; + +/* ---------------table无数据基础样式----START-----------------------------------------*/ +.table-nodata (@bg) { + background-color: var(--ti-common-color-bg-white-normal); + & > td { + position: relative; + overflow: visible !important; + background: @bg; + font-size: var(--ti-common-font-size-1); + color: var(--ti-common-color-text-secondary); + text-align: center; + line-height: 20px; + vertical-align: top; + border-bottom: none; + } +} +.ti3-table > table, +.ti3-table > .ti3-resize-wrapper > table, +.ti3-table > .ti3-table-fixed-head > table, +.ti3-table > .ti3-table-container > table { + & > tbody > tr { + &.ti3-table-nodata { + .table-nodata(var(--ti-table-nodata-td-bg-img-url) 50% var(--ti-table-nodata-lead-icon-padding) no-repeat); + & > td { + height: var(--ti-table-nodata-height); + padding: calc(var(--ti-table-nodata-lead-icon-height) + var(--ti-table-nodata-lead-icon-padding)) 0 0 0; + } + } + + &.ti3-table-loadfail, + &.ti3-table-nodata-guide { + .table-nodata(none); + & > td { + padding: var(--ti-common-space-5x) 0; + .table-nodata-a; + } + } + &.ti3-table-nodata-simple { + .table-nodata(none); + & > td { + padding: var(--ti-common-space-5x) 0; + } + } + } +} +/* ---------------table无数据基础样式----END-----------------------------------------*/ + +/* ---------------table窄行基础样式----START-----------------------------------------*/ +.ti3-table-small > table, +.ti3-table-small > .ti3-resize-wrapper > table, +.ti3-table-small > .ti3-table-fixed-head > table, +.ti3-table-small > .ti3-table-container > table { + & > thead > tr > th { + height: var(--ti-table-small-th-height); + line-height: var(--ti-table-small-th-height); + } + + & > tbody > tr > td { + padding: var(--ti-common-space-2x) var(--ti-table-small-td-horizontal-padding); + line-height: var(--ti-common-line-height-number); + } + + & > tbody > tr.ti3-details-tr > td { + padding: var(--ti-common-space-5x); + } + + .small-table-nodata (@bg) { + & > td { + vertical-align: top; + background: @bg; + } + } + + & > tbody > tr { + &.ti3-table-nodata { + .small-table-nodata(var(--ti-table-nodata-td-bg-img-url) 50% var(--ti-table-nodata-lead-icon-padding) no-repeat); + & > td { + height: var(--ti-table-small-nodata-height); + padding: calc(var(--ti-table-nodata-lead-icon-height) + var(--ti-table-nodata-lead-icon-padding)) 0 0 0; + } + } + + &.ti3-table-loadfail, + &.ti3-table-nodata-guide { + .small-table-nodata(none); + & > td { + .table-nodata-a; + } + } + } +} +/* ---------------table窄行基础样式----END-----------------------------------------*/ + +/* --------------- table嵌套样式 ----start------------------*/ +.table-nodata-a { + a { + color: var(--ti-common-color-text-link); + text-decoration: none; + &:hover { + text-decoration: underline; + color: var(--ti-common-color-text-link-hover); + cursor: pointer; + } + } +} +.ti3-table-nest > table, +.ti3-table-nest > .ti3-resize-wrapper > table, +.ti3-table-nest > .ti3-table-fixed-head > table, +.ti3-table-nest > .ti3-table-container > table { + & > thead > tr > th { + height: var(--ti-table-nest-th-height); + line-height: var(--ti-table-nest-th-height); + } + + .nest-table-nodata (@bg) { + & > td { + vertical-align: top; + background: @bg; + } + } + + & > tbody > tr { + &.ti3-table-nodata { + .nest-table-nodata(var(--ti-table-nodata-td-bg-img-url) 50% var(--ti-table-nodata-lead-icon-padding) no-repeat); + & > td { + height: var(--ti-table-small-nodata-height); + padding: calc(var(--ti-table-nodata-lead-icon-height) + var(--ti-table-nodata-lead-icon-padding)) 0 0 0; + } + } + + &.ti3-table-loadfail &.ti3-table-nodata-guide { + .nest-table-nodata(none); + & > td { + .table-nodata-a; + } + } + } + + & > tbody > tr > td { + padding: var(--ti-common-space-2x) var(--ti-table-small-td-horizontal-padding); + line-height: var(--ti-common-line-height-number); + border-bottom: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + } + + & > tbody > tr.ti3-details-tr > td { + padding: var(--ti-common-space-5x); + } +} + +/* --------------- table嵌套样式 ----end--------------------*/ + +/* ---------------table列拖动样式(colsResizable)----START----------------------------------------------*/ +// 列拖动父容器区域 +.ti3-resize-wrapper { + overflow-x: auto; + min-height: ~'0%'; // 解决:IE浏览器下,将表格列拖动出现横向滚动条时,分页会与表体出现空白,且hover到表格时,空白范围在不断改变; #7091 +} +// 页面文字不可选,避免出现蓝色选择区域(对于列拖动) +.ti3-unselectable { + .user-select(none); +} +.ti3-table > table, +.ti3-table > .ti3-resize-wrapper > table, +.ti3-table > .ti3-table-fixed-head > table, +.ti3-table > .ti3-table-container > table { + & > thead > tr > th.col-resize-active { + // 因为IE下给伪元素设置cursor是不生效的,所以cursor没有加在th.col-resize-active:after上 + cursor: col-resize !important; // 加important是为了避免被排序的手型鼠标样式覆盖掉 + } + & > thead > tr > th.col-resize-active:after { + // 列拖动激活状态时th样式 + content: ''; + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 2px; + height: 100%; + border-right: var(--ti-common-border-weight-normal) var(--ti-common-border-style-dashed) var(--ti-common-color-line-dividing); + background: transparent; + } + & > tbody > tr > td.col-resize-active { + position: relative; + } + & > tbody > tr:not(.ti3-table-nodata) > td.col-resize-active:after { + // 列拖动激活状态时td样式 + content: ''; + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 2px; + height: 100%; + border-right: var(--ti-common-border-weight-normal) var(--ti-common-border-style-dashed) var(--ti-common-color-line-dividing); + } +} +/* ---------------table列拖动样式(colsResizable)----END----------------------------------------------*/ diff --git a/src/table/lib/src/table-toggle-sort-details.less b/src/table/lib/src/table-toggle-sort-details.less new file mode 100644 index 0000000..185e3d9 --- /dev/null +++ b/src/table/lib/src/table-toggle-sort-details.less @@ -0,0 +1,124 @@ +@import '../../../themes/basic/base-all.less'; +ti-drop.ti3-cols-toggle-drop { + --ti-table-cols-toggle-horizontal-padding: var(--ti-common-space-10); +} +/* ---------------table列动态隐藏和显示(colsToggle)----START----------------------------------------------*/ +.ti3-cols-toggle-drop { + .border-radius(var(--ti-common-border-radius-normal)); + .box-shadow(var(--ti-common-shadow-2-down)); + min-width: 200px !important; + max-width: 300px !important; + & > .ti3-cols-toggle-searchbox-wrap { + padding: var(--ti-common-space-10) var(--ti-table-cols-toggle-horizontal-padding) var(--ti-common-space-base) !important; + .box-sizing(border-box); + .ti3-cols-toggle-searchbox { + width: 100% !important; + } + } + .ti3-overflow-padding { + padding: 0 var(--ti-table-cols-toggle-horizontal-padding) !important; + } + .ti3-dropdown-no-data { + padding-left: var(--ti-table-cols-toggle-horizontal-padding) !important; + } + .ti3-dropdown-option-hover { + color: var(--ti-common-color-text-primary); + } +} + +/* ---------------table列动态隐藏和显示(colsToggle)----END----------------------------------------------*/ + +/* ---------------table列排序(sort)----START----------------------------------------------*/ +.ti3-table > table, +.ti3-table > .ti3-resize-wrapper > table, +.ti3-table > .ti3-table-fixed-head > table, +.ti3-table > .ti3-table-container > table { + & > thead > tr > th { + & > .ti3-sort-container { + display: inline-block; + cursor: pointer; // 鼠标的形态与是否触发排序功能保持一致,避免在整个th区域表现为手型 + outline-offset: -1px; // 防止tab聚焦的时候,上下outline被遮挡。 + // padding-right由原来的30px改为0.6em是为了解决 #7611 + // 原因:当父元素设置内容溢出时出省略号的功能时将内部元素的padding和margin也算作内容 + & .ti3-sort { + cursor: pointer; + width: var(--ti-common-size-4x); + height: var(--ti-common-size-4x); + font-size: var(--ti-common-size-4x); + display: inline-block; + margin-left: var(--ti-common-space-2x); + vertical-align: middle; + line-height: var(--ti-common-size-4x); + &:hover { + color: var(--ti-common-color-icon-hover); + } + & .ti3-headsort-icon-active { + color: var(--ti-common-color-icon-active); + } + } + } + } +} + +/* ---------------table列排序(sort)----END----------------------------------------------*/ + +/* ---------------table表头有排序图标或漏斗图标或其他图标时文本样式和图标容器样式----START----------------------------------------------*/ +.ti3-table > table, +.ti3-table > .ti3-resize-wrapper > table, +.ti3-table > .ti3-table-fixed-head > table, +.ti3-table > .ti3-table-container > table { + & > thead > tr > th, + & > tbody > tr > td { + & > .ti3-cell-text-container { + & > .ti3-cell-text { + display: inline-block; + vertical-align: top; + } + } + & > .ti3-cell-icons-container { + display: inline-block; + } + } +} +/* ---------------table列排序(sort)----END----------------------------------------------*/ + +/* ---------------table详情展开(details)----START----------------------------------------------*/ + +// 由于每次详情展开时,需要给当前行的下展详情底部与下一行表格数据之间增加下边框,因此增加border-bottom属性 +.ti3-table > table > tbody, +.ti3-table > .ti3-resize-wrapper > table > tbody, +.ti3-table > .ti3-table-container > table > tbody { + & > .ti3-details-tr { + background: var(--ti-common-color-bg-white-emphasize); + & > td { + padding: var(--ti-common-space-0); + border-bottom: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + // 隔离作用,防止td的white-space:nowrap 属性继承下去,影响详情行样式 + white-space: normal; + & > .ti3-table-detail-container { + padding: var(--ti-common-space-5x); + background-color: var(--ti-common-color-bg-white-normal); + } + } + } + & > tr > td .ti3-toggle-details { + cursor: pointer; + color: var(--ti-common-color-icon-normal); + & .ti3-icon { + vertical-align: middle; + font-size: 15px; + &:hover { + color: var(--ti-common-color-icon-hover); + } + } + } +} +.ti3-icon-angle-transform-up-thin { + display: inline-block; + .rotate(-90deg); +} +.ti3-icon-angle-transform-down-thin { + display: inline-block; + .rotate(90deg); +} +/* ---------------table详情展开(details)----END----------------------------------------------*/ diff --git a/src/table/lib/src/table-tree-fix.less b/src/table/lib/src/table-tree-fix.less new file mode 100644 index 0000000..4d640bb --- /dev/null +++ b/src/table/lib/src/table-tree-fix.less @@ -0,0 +1,145 @@ +@import '../../../themes/basic/base-all.less'; +/* ---------------table树表特性(tree)----START----------------------------------------------*/ +.ti3-table > table > tbody, +.ti3-table > .ti3-resize-wrapper > table > tbody, +.ti3-table > .ti3-table-container > table > tbody { + & > tr > td .ti3-table-tree { + cursor: pointer; + .ti3-icon-minus-square, + .ti3-icon-plus-square { + font-size: var(--ti-table-tree-square-icon-font-size); + margin-right: var(--ti-common-space-2x); + line-height: 18px; + vertical-align: bottom; + } + .ti3-icon-minus-square { + color: var(--ti-common-color-icon-active); + } + .ti3-icon-plus-square { + color: var(--ti-common-color-icon-normal); + } + .ti3-icon-minus-square:hover, + .ti3-icon-plus-square:hover { + color: var(--ti-common-color-icon-hover); + } + .ti3-icon-minus-square:active, + .ti3-icon-plus-square:active { + color: var(--ti-common-color-icon-active); + background: var(--ti-common-color-bg-white-normal); + } + } + // 各层级内容对齐 + & > tr > td .ti3-table-tree-no-leaf { + margin-left: calc(var(--ti-table-tree-square-icon-font-size) + var(--ti-common-space-2x)); + } +} + +/* ---------------table树表特性(tree)----END----------------------------------------------*/ + +/* ---------------table设置tip时克隆DOM的样式----START----------------------------------------------*/ +.ti3-table-cell-clone { + overflow: visible !important; + position: absolute !important; + visibility: hidden !important; + .box-sizing(var(--ti-table-box-model)); + width: auto !important; +} +/* ---------------table设置tip时克隆DOM的样式----END----------------------------------------------*/ + +/* ---------------table表头锁定(双表)----START----------------------------------------------*/ +.ti3-table { + & > .ti3-table-fixed-head { + overflow: hidden; + position: relative; + & > table { + margin-left: 0; + } + & > .ti3-table-fixed-head-filler { + position: absolute; + height: 100%; + right: 0; + background-color: var(--ti-common-color-bg-white-emphasize); + top: 0; + bottom: 0; + } + & + .ti3-table-container { + // 表头锁定时,表格容器底部有边框 + border-bottom: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + & > table > thead > tr > th { + height: 0; + border-bottom: none; + } + & > table > tbody > tr { + &:last-child > td { + border-bottom: none !important; + } + } + } + } + & > .ti3-table-container { + overflow: auto; + } +} +/* ---------------table表头锁定(双表头)----END----------------------------------------------*/ + +/* ---------------table列固定----START----------------------------------------------*/ +.ti3-table-column-fixed-left { + position: sticky !important; + left: 0; + z-index: 1; + background-color: inherit; + &.ti3-table-floating-fixed-column { + box-shadow: 5px 0 5px 0 rgba(0, 0, 0, 0.1); + } +} +.ti3-table-column-fixed-right { + position: sticky !important; + right: 0; + z-index: 1; + background-color: inherit; + &.ti3-table-floating-fixed-column { + box-shadow: -5px 0 5px 0 rgba(0, 0, 0, 0.1); + } +} +/* ---------------table列固定----END----------------------------------------------*/ +.ti3-table-row-disable { + color: #999; +} + +.ti3-table-soldout { + &:extend(.ti3-icon all); + &:extend(.ti3-icon-sold-out all); + width: 16px; + display: inline-block; + height: 16px; + background: #999; + text-align: center; + line-height: 16px; + color: #fff; + font-size: 12px; +} + +.ti3-table-row-message { + display: inline-block; + padding: 0 4px; + height: 16px; + line-height: 16px; + font-size: 12px; + color: #fff; + text-align: center; + margin-left: 5px; +} +.ti3-table-row-message-soldout { + &:extend(.ti3-table-row-message all); + background: #999; +} + +.ti3-table-row-message-new { + &:extend(.ti3-table-row-message all); + background: #e41f2b; +} + +.ti3-table-row-message-recommended { + &:extend(.ti3-table-row-message all); + background: #f57f06; +} diff --git a/src/table/lib/src/table-virtual-scroll-viewport.html b/src/table/lib/src/table-virtual-scroll-viewport.html new file mode 100644 index 0000000..4efb2fd --- /dev/null +++ b/src/table/lib/src/table-virtual-scroll-viewport.html @@ -0,0 +1,3 @@ + + +
    diff --git a/src/table/lib/src/table-virtual-scroll-viewport.less b/src/table/lib/src/table-virtual-scroll-viewport.less new file mode 100644 index 0000000..6aef48a --- /dev/null +++ b/src/table/lib/src/table-virtual-scroll-viewport.less @@ -0,0 +1,34 @@ +.ti3-table-virtual-scroll-viewport { + display: block; + position: relative; + overflow: auto; + contain: strict; + transform: translateZ(0); + will-change: scroll-position; + -webkit-overflow-scrolling: touch; + + &.ti3-table-virtual-scroll-viewport-nodata { + // 以下样式都是为了让在无数据时无数据的dom能够显示出来(撑起 ti3-table-virtual-scroll-viewport 的高度) + height: auto !important; // 覆盖 style.height: 0 的设置; + contain: none; + & > .ti3-table-virtual-scroll-content-wrapper { + position: static; + } + } +} + +.ti3-table-virtual-scroll-content-wrapper { + position: absolute; + top: 0; + left: 0; + contain: content; +} + +.ti3-table-virtual-scroll-spacer { + position: absolute; + top: 0; + left: 0; + height: inherit; + width: 1px; + transform-origin: 0 0; +} diff --git a/src/table/lib/src/table.html b/src/table/lib/src/table.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/table/lib/src/table.html @@ -0,0 +1 @@ + diff --git a/src/table/lib/src/table.less b/src/table/lib/src/table.less new file mode 100644 index 0000000..e7a2d51 --- /dev/null +++ b/src/table/lib/src/table.less @@ -0,0 +1,187 @@ +@import '../../../themes/basic/base-all.less'; + +ti-table { + --ti-table-box-model: border-box; + --ti-table-th-horizontal-padding: var(--ti-common-space-10); + --ti-table-nodata-lead-icon-padding: var(--ti-common-space-5x); + --ti-table-nodata-td-bg-img-url: data-uri('../../../themes/basic/img/table-nodata-bg.png'); + --ti-table-small-td-horizontal-padding: var(--ti-common-space-10); + --ti-table-nodata-height: 210px; + --ti-table-small-nodata-height: 190px; + --ti-table-nodata-lead-icon-height: var(--ti-common-size-20x); + --ti-table-cols-toggle-menu-size: var(--ti-common-size-7x); + --ti-table-tree-square-icon-font-size: var(--ti-common-font-size-2); + --ti-table-th-height: var(--ti-common-size-7x); + --ti-table-small-th-height: var(--ti-common-size-7x); + --ti-table-nest-th-height: var(--ti-common-size-7x); + --ti-table-column-select-icon-width: var(--ti-common-size-8x); + --ti-table-column-icon-width: 42px; + --ti-table-timing-function-default: cubic-bezier(0.25, 0.1, 0.25, 1); +} + +/* ---------------table基础样式----START----------------------------------------------*/ +.ti3-table { + display: block; +} +.table-nodata-a { + a { + color: var(--ti-common-color-text-link); + text-decoration: none; + &:hover { + text-decoration: underline; + color: var(--ti-common-color-text-link-hover); + cursor: pointer; + } + } +} + +.ti3-table > table, +.ti3-table > .ti3-resize-wrapper > table, +.ti3-table > .ti3-table-fixed-head > table, +.ti3-table > .ti3-table-container > table { + .box-sizing(var(--ti-table-box-model)); + border-width: 0; + border-collapse: separate; + table-layout: fixed; + border-spacing: 0; + empty-cells: show; + margin-bottom: 0; + width: 100%; + & > thead > tr > th { + .box-sizing(var(--ti-table-box-model)); + position: relative; + cursor: default; + background-color: var(--ti-common-color-bg-white-emphasize); + outline: 0; + height: var(--ti-table-th-height); + line-height: calc(var(--ti-table-th-height) - 1px); + color: var(--ti-common-color-text-secondary); + font-weight: var(--ti-common-font-weight-6); + border-left: none; + padding: 0; + padding-left: var(--ti-table-th-horizontal-padding); + padding-right: var(--ti-table-th-horizontal-padding); + text-align: left; + vertical-align: middle; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border-bottom: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + &:not(:last-child)::after { + width: 1px; + background: var(--ti-common-color-text-white); + color: transparent; + height: 100%; + position: absolute; + right: 0; + top: 0; + content: ''; + } + &[checkbox-column], + &[radio-column] { + width: var(--ti-common-size-8x) !important; + overflow: visible; + padding-right: var(--ti-common-space-6); + font-size: 0; // 解决图标溢出问题 + &::after { + width: 0; + } + } + // 单选时,th不需要radio按钮 + &[checkbox-column] { + cursor: pointer; + } + &.ti3-disabled-cell { + cursor: not-allowed; + } + &[details-icon-column] { + width: var(--ti-table-column-icon-width) !important; + overflow: visible; + padding-right: var(--ti-common-space-6); + &::after { + width: 0; + } + } + &:first-child { + &[checkbox-column], + &[radio-column] { + width: var(--ti-table-column-icon-width) !important; + padding-left: var(--ti-common-space-5x) !important; + } + } + } + & > tbody > tr { + border-spacing: 0; + background-color: var(--ti-common-color-bg-white-normal); + .transition (background-color 150ms var(--ti-table-timing-function-default));; + //行hover时显示当前行下的cti-rename组件图标 + &:hover { + .cti-rename-edit { + visibility: visible; + } + } + &.ti3-selected-tr { + background-color: var(--ti-common-color-bg-white-normal); + &:hover { + background-color: var(--ti-common-color-bg-white-normal); + } + } + + &.ti3-disabled-tr { + background-color: var(--ti-common-color-bg-disabled); + & > td { + color: var(--ti-common-color-text-disabled); + &[checkbox-column], + &[radio-column] { + cursor: not-allowed; + } + } + } + + &:not(.ti3-details-tr):not(.ti3-disabled-tr):not(.ti3-table-nodata):not(.ti3-table-nodata-simple):not(.ti3-table-loadfail):not( + .ti3-table-nodata-guide + ):not(.ti3-selected-tr) { + &:hover { + background-color: var(--ti-common-color-bg-white-emphasize); + .transition (background-color 200ms var(--ti-table-timing-function-default));; + } + } + + & > td { + .box-sizing(var(--ti-table-box-model)); + border-left: none; + border-top: none; + padding: var(--ti-common-space-3x) var(--ti-common-space-10); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: left; + vertical-align: middle; + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-primary); + border-bottom: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + &[checkbox-column], + &[radio-column] { + overflow: visible; + padding-right: var(--ti-common-space-6); + font-size: 0; // 解决图标溢出问题 + cursor: pointer; + } + &.ti3-disabled-cell { + cursor: not-allowed; + } + &[details-icon-column] { + overflow: visible; + padding-left: var(--ti-common-space-5x); + padding-right: var(--ti-common-space-6); + } + &:first-child { + &[checkbox-column], + &[radio-column] { + padding-left: var(--ti-common-space-5x); + } + } + } + } +} +/* ---------------table基础样式----End----------------------------------------------*/ diff --git a/src/tag/demo/karma.conf.js b/src/tag/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/tag/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/tag/demo/project.json b/src/tag/demo/project.json new file mode 100644 index 0000000..42bfae3 --- /dev/null +++ b/src/tag/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/tag/demo", + "sourceRoot": "src/tag/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/tag", + "index": "src/tag/demo/src/index.html", + "main": "src/tag/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tag/demo/tsconfig.app.json", + "assets": ["src/tag/demo/src/favicon.ico", "src/tag/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "tag-demo:build:production" + }, + "development": { + "browserTarget": "tag-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js tag" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/tag/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tag/demo/tsconfig.spec.json", + "karmaConfig": "src/tag/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/tag/demo/src/app/AppComponent.ts b/src/tag/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/tag/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/tag/demo/src/app/AppModule.ts b/src/tag/demo/src/app/AppModule.ts new file mode 100644 index 0000000..caee982 --- /dev/null +++ b/src/tag/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TagTestModule } from './tag/TagTestModule'; + +@NgModule({ + imports: [ + TagTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/tag/demo/src/app/IndexComponent.ts b/src/tag/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..aa79cac --- /dev/null +++ b/src/tag/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TagTestModule } from './tag/TagTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TagTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/tag/demo/src/app/app.html b/src/tag/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/tag/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/tag/demo/src/app/tag/TagBasicComponent.ts b/src/tag/demo/src/app/tag/TagBasicComponent.ts new file mode 100644 index 0000000..db76caa --- /dev/null +++ b/src/tag/demo/src/app/tag/TagBasicComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tag-basic.html' +}) +export class TagBasicComponent { + myLogs: Array = []; + + onDelete(): void { + this.myLogs = [...this.myLogs, `onDelete() delete`]; + } +} diff --git a/src/tag/demo/src/app/tag/TagDefaultComponent.ts b/src/tag/demo/src/app/tag/TagDefaultComponent.ts new file mode 100644 index 0000000..9732c98 --- /dev/null +++ b/src/tag/demo/src/app/tag/TagDefaultComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tag-default.html' +}) +export class TagDefaultComponent { + disabled: boolean = false; + student1: any = { + id: 1, + name: '苏轼', + age: 36 + }; + onDelete(data: any): void { + console.log('onDelete:' + data.name); + } + onClick(event: MouseEvent): void { + console.log('onClick:原生事件'); + console.log(event); + } + changeDisabled(): void { + this.disabled = !this.disabled; + } +} diff --git a/src/tag/demo/src/app/tag/TagDisabledComponent.ts b/src/tag/demo/src/app/tag/TagDisabledComponent.ts new file mode 100644 index 0000000..c68ed38 --- /dev/null +++ b/src/tag/demo/src/app/tag/TagDisabledComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tag-disabled.html' +}) +export class TagDisabledComponent {} diff --git a/src/tag/demo/src/app/tag/TagEditComponent.ts b/src/tag/demo/src/app/tag/TagEditComponent.ts new file mode 100644 index 0000000..db10048 --- /dev/null +++ b/src/tag/demo/src/app/tag/TagEditComponent.ts @@ -0,0 +1,62 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tag-edit.html' +}) +export class TagEditComponent { + myLogs: Array = []; + inputValue: string = ''; + showInput: boolean = false; + + items: any = [ + { + label: 'Tag1' + }, + { + label: 'Tag2', + disabled: true + }, + { + label: 'Tag3' + } + ]; + onDelete(item: any): void { + if (item.disabled === true) { + return; + } + // 删除选中 + const index: number = this.items.indexOf(item); + if (index !== -1) { + this.items.splice(index, 1); + } + + this.myLogs = [...this.myLogs, `delete ${item.label}`]; + } + + onClick(): void { + this.showInput = true; + } + + onInputKeyup(event: KeyboardEvent): void { + // 获取输入框的值 + const value: string = this.inputValue; + + this.inputValue = ''; + this.showInput = false; + + // 输入框为空时或者已经生成当前标签时 + if (value.trim() === '' || this.findFirstIndex(this.items, 'label', value) !== -1) { + return; + } + + this.items.push({ label: value }); + } + + private findFirstIndex(arr: any, key: string, value: string): number { + if (!(arr instanceof Array)) { + return -1; + } + + return arr.findIndex((i: any) => i[key] === value); + } +} diff --git a/src/tag/demo/src/app/tag/TagTestModule.ts b/src/tag/demo/src/app/tag/TagTestModule.ts new file mode 100644 index 0000000..68bde13 --- /dev/null +++ b/src/tag/demo/src/app/tag/TagTestModule.ts @@ -0,0 +1,48 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiIconModule, TiTagModule, TiTextModule } from '@opentiny/ng'; +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; + +import { TagBasicComponent } from './TagBasicComponent'; +import { TagDisabledComponent } from './TagDisabledComponent'; +import { TagEditComponent } from './TagEditComponent'; +import { TagDefaultComponent } from './TagDefaultComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiIconModule, + TiTagModule, + TiButtonModule, + TiIconModule, + TiTextModule, + DemoLogModule, + RouterModule.forChild(TagTestModule.ROUTES) + ], + declarations: [TagBasicComponent, TagDisabledComponent, TagEditComponent, TagDefaultComponent] +}) +export class TagTestModule { + static readonly LINKS: Array = [{ href: 'components/TiTagComponent.html', label: 'Tag' }]; + static readonly ROUTES: Routes = [ + { + path: 'tag/tag-basic', + component: TagBasicComponent + }, + { + path: 'tag/tag-disabled', + component: TagDisabledComponent + }, + { + path: 'tag/tag-edit', + component: TagEditComponent + }, + { + path: 'tag/tag-default', + component: TagDefaultComponent + } + ]; +} diff --git a/src/tag/demo/src/app/tag/tag-basic.html b/src/tag/demo/src/app/tag/tag-basic.html new file mode 100644 index 0000000..6647258 --- /dev/null +++ b/src/tag/demo/src/app/tag/tag-basic.html @@ -0,0 +1 @@ +Default diff --git a/src/tag/demo/src/app/tag/tag-default.html b/src/tag/demo/src/app/tag/tag-default.html new file mode 100644 index 0000000..2df70b8 --- /dev/null +++ b/src/tag/demo/src/app/tag/tag-default.html @@ -0,0 +1,25 @@ +

    描述

    +

    Tag标签组件,基础用法

    +

    导入

    +

    import {{ '{' }} TiTagModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    +

    1.ti-tag默认是inline-block,max-width: 100%;

    +
    + go + binggo + 明月几时有,把酒问青天。不知天上宫阙,今夕是何年 + + 但愿人长久,千里共婵娟。 + + 但愿人长久,千里共婵娟。 +
    +

    2.设置可以设置max-width,min-width,width。width默认值是

    +明月几时有,把酒问青天。不知天上宫阙,今夕是何年 +明月几时有,把酒问青天。不知天上宫阙,今夕是何年 +

    +

    3.事件和方法

    +{{student1.name}} +明月几时有,把酒问青天。不知天上宫阙,今夕是何年 +
    +
    + diff --git a/src/tag/demo/src/app/tag/tag-disabled.html b/src/tag/demo/src/app/tag/tag-disabled.html new file mode 100644 index 0000000..17d3017 --- /dev/null +++ b/src/tag/demo/src/app/tag/tag-disabled.html @@ -0,0 +1 @@ +Disabled diff --git a/src/tag/demo/src/app/tag/tag-edit.html b/src/tag/demo/src/app/tag/tag-edit.html new file mode 100644 index 0000000..8098603 --- /dev/null +++ b/src/tag/demo/src/app/tag/tag-edit.html @@ -0,0 +1,9 @@ +{{item.label}} + + + + + + + diff --git a/src/tag/demo/src/app/tag/webdoc/tag-demos.js b/src/tag/demo/src/app/tag/webdoc/tag-demos.js new file mode 100644 index 0000000..5406d94 --- /dev/null +++ b/src/tag/demo/src/app/tag/webdoc/tag-demos.js @@ -0,0 +1,40 @@ +export default { + column: '2', + demos: [ + { + demoId: 'tag-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Tag 组件的最简用法。

    ', + 'en-US': '', + } + }, + { + demoId: 'tag-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': '', + }, + apis: ['TiTagComponent.properties.disabled'], + }, + { + demoId: 'tag-events', + name: { + 'zh-CN': '事件', + 'en-US': 'events', + }, + desc: { + 'zh-CN':'

    当点击删除按钮的时候触发delete事件。

    ', + 'en-US': '', + }, + apis: ['TiTagComponent.events.delete'], + } + ] +}; diff --git a/src/tag/demo/src/app/tag/webdoc/tag.cn.md b/src/tag/demo/src/app/tag/webdoc/tag.cn.md new file mode 100644 index 0000000..1edda3d --- /dev/null +++ b/src/tag/demo/src/app/tag/webdoc/tag.cn.md @@ -0,0 +1,23 @@ +--- +title: Tag 标签 +--- +# Tag 标签 + +
    + +Tag 是标签组件。   + +```typescript +import { TiTagModule } from '@opentiny/ng'; +``` + +
    + +
    + +Tag 是标签组件。   + +```typescript +import { TiTagModule } from '@cloud/tiny-config'; +``` +
    diff --git a/src/tag/demo/src/app/tag/webdoc/tag.en.md b/src/tag/demo/src/app/tag/webdoc/tag.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/tag/demo/src/app/tag/webdoc/tag.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/tag/demo/src/favicon.ico b/src/tag/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/tag/demo/src/index.html b/src/tag/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/tag/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/tag/demo/src/main.ts b/src/tag/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/tag/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/tag/demo/test.ts b/src/tag/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/tag/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/tag/demo/tsconfig.app.json b/src/tag/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/tag/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/tag/demo/tsconfig.spec.json b/src/tag/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/tag/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/tag/lib/index.ts b/src/tag/lib/index.ts new file mode 100644 index 0000000..6172381 --- /dev/null +++ b/src/tag/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTagModule'; diff --git a/src/tag/lib/ng-package.json b/src/tag/lib/ng-package.json new file mode 100644 index 0000000..6c45b6c --- /dev/null +++ b/src/tag/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/tag", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/tag/lib/package.json b/src/tag/lib/package.json new file mode 100644 index 0000000..5b31f1a --- /dev/null +++ b/src/tag/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-tag", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/tag/lib/project.json b/src/tag/lib/project.json new file mode 100644 index 0000000..cc0dd91 --- /dev/null +++ b/src/tag/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/tag/lib", + "sourceRoot": "src/tag/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/tag"], + "options": { + "project": "src/tag/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/tag"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js tag" + }, + { + "command": "ng default-build tag" + }, + { + "command": "node build/clear-default-theme.js tag" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/tag && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build tag && ng pack tag && node build/publish.js tag --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/tag/lib/src/TiTagComponent.ts b/src/tag/lib/src/TiTagComponent.ts new file mode 100644 index 0000000..660cdda --- /dev/null +++ b/src/tag/lib/src/TiTagComponent.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiPositionType } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; +// TODO: 等待规范统一,可能只保留方形tag +// TODO: hover时是否需要高亮,选中是否高亮。增加selectable接口,表示是否可以选中。 +// TODO: 禁用时是否显示叉号?规范和tiny2不一致。 +// TODO: 禁用时,不发出原生click事件。 +/** + * Tag标签组件,支持显示自定义内容,和删除事件通知。 + * + */ +@Component({ + selector: 'ti-tag', + templateUrl: './tag.html', + styleUrls: ['./tag-rect.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-multiselect-box-cell]': 'true', + '[class.ti3-multiselect-option-disabled]': 'disabled', + '[style.maxWidth]': 'maxWidth' + } +}) +export class TiTagComponent extends TiBaseComponent { + /** + * 是否禁用 + */ + @Input() disabled: boolean = false; + /** + * @ignore + * 选中项文本超出时tip展开方向 + */ + @Input() selectedTipPosition: TiPositionType = 'auto'; + /** + * @ignore + * 最大宽度,TpSearchbox 使用 + */ + @Input() maxWidth: string; + /** + * 点击删除按钮时触发的回调 + */ + @Output() readonly delete: EventEmitter = new EventEmitter(); + protected versionInfo: string = super.getVersion(packageInfo); + /** + * @ignore + * @param $event + */ + public onClickDelete($event: MouseEvent): void { + if (this.disabled) { + return; + } + $event.stopPropagation(); // 阻止冒泡,阻止了Tag整体的onClick事件,也阻止Tag之外接收到Click。 + this.delete.emit(); + } + /** + * 删除 Tag 标签元素 + */ + public remove(): void { + this.renderer.removeChild(this.renderer.parentNode(this.nativeElement), this.nativeElement); + } +} diff --git a/src/tag/lib/src/TiTagModule.ts b/src/tag/lib/src/TiTagModule.ts new file mode 100644 index 0000000..ec03c3a --- /dev/null +++ b/src/tag/lib/src/TiTagModule.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiTagComponent } from './TiTagComponent'; +import { CommonModule } from '@angular/common'; +import { TiIconModule } from '@opentiny/ng-icon'; + +@NgModule({ + imports: [CommonModule, TiIconModule], + exports: [TiTagComponent], + declarations: [TiTagComponent] +}) +export class TiTagModule {} +export { TiTagComponent } from './TiTagComponent'; diff --git a/src/tag/lib/src/tag-arrow.less b/src/tag/lib/src/tag-arrow.less new file mode 100644 index 0000000..b528508 --- /dev/null +++ b/src/tag/lib/src/tag-arrow.less @@ -0,0 +1,44 @@ +@import '../../../themes/basic/base-all.less'; +:host.ti3-tag-container { + display: inline-block; + max-width: 221px; + height: 28px; + line-height: 28px; + margin-left: 24px; + padding-right: 28px; + font-size: 14px; + background-color: var(--ti-common-color-bg-normal); + color: var(--ti-common-color-text-primary); + cursor: pointer; + box-sizing: border-box; + position: relative; + + &:hover { + .ti3-tag-clear { + color: var(--ti-common-color-icon-hover); + } + } + + .ti3-tag-selected-triangle { + position: absolute; + top: 0; + left: -14px; + width: 0; + height: 0; + border-top: 14px solid transparent; + border-right: 14px solid var(--ti-common-color-bg-normal); + border-bottom: 14px solid transparent; + } + + .ti3-tag-clear { + position: absolute; + right: 7px; + top: 0; + background-color: transparent; + color: #999; + font-size: 14px; + text-align: center; + vertical-align: middle; + cursor: pointer; + } +} diff --git a/src/tag/lib/src/tag-rect.less b/src/tag/lib/src/tag-rect.less new file mode 100644 index 0000000..54fafb0 --- /dev/null +++ b/src/tag/lib/src/tag-rect.less @@ -0,0 +1,66 @@ +//新插入的盒子的样式.ti3-multiselect-box-cell 默认样式 +:host { + --ti-tag-container-height: var(--ti-common-size-5x); + --ti-tag-clear-icon-size: var(--ti-common-size-3x); + --ti-tag-border-weight: var(--ti-common-border-weight-normal); + --ti-tag-icon-area-width: calc(var(--ti-tag-clear-icon-size) + var(--ti-common-space-10) * 2); // 图标大小12px + 左右间距各10px +} +:host.ti3-multiselect-box-cell { + display: inline-flex; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + + &::after { + content: ''; + overflow: hidden; + display: block; + } + + width: auto; + max-width: calc(100% - var(--ti-tag-border-weight) * 2); + border-radius: var(--ti-common-border-radius-normal); + height: var(--ti-tag-container-height); + line-height: var(--ti-tag-container-height); + border: var(--ti-tag-border-weight) var(--ti-common-border-style-solid) var(--ti-common-color-transparent); + overflow: hidden; + color: var(--ti-common-color-text-secondary); + background-color: var(--ti-common-color-bg-normal); + box-sizing: content-box; + + .ti3-box-cell-key { + box-sizing: border-box; + margin-left: var(--ti-common-space-10); + margin-right: var(--ti-common-space-6); + width: calc(100% - var(--ti-tag-icon-area-width) - var(--ti-common-space-6) - var(--ti-common-space-10)); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + &:hover { + color: var(--ti-common-color-text-highlight); + } + } + & .ti3-icon-close-staic { + box-sizing: border-box; + color: var(--ti-common-color-icon-graybg-normal); + width: var(--ti-tag-icon-area-width); + height: 100%; + text-align: center; + font-size: var(--ti-tag-clear-icon-size); + &:hover { + color: var(--ti-common-color-icon-graybg-hover); + } + } + + &.ti3-multiselect-option-disabled { + color: var(--ti-common-color-text-disabled); + background-color: var(--ti-common-color-bg-disabled); + border-color: var(--ti-common-color-bg-disabled); + cursor: not-allowed; + & .ti3-box-cell-key { + color: var(--ti-common-color-text-disabled); + } + & .ti3-icon-close-staic { + color: var(--ti-common-color-icon-graybg-disabled); + } + } +} diff --git a/src/tag/lib/src/tag.html b/src/tag/lib/src/tag.html new file mode 100644 index 0000000..4cb351b --- /dev/null +++ b/src/tag/lib/src/tag.html @@ -0,0 +1,2 @@ +
    +
    diff --git a/src/tagsinput/demo/karma.conf.js b/src/tagsinput/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/tagsinput/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/tagsinput/demo/project.json b/src/tagsinput/demo/project.json new file mode 100644 index 0000000..d37143f --- /dev/null +++ b/src/tagsinput/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/tagsinput/demo", + "sourceRoot": "src/tagsinput/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/tagsinput", + "index": "src/tagsinput/demo/src/index.html", + "main": "src/tagsinput/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tagsinput/demo/tsconfig.app.json", + "assets": ["src/tagsinput/demo/src/favicon.ico", "src/tagsinput/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "tagsinput-demo:build:production" + }, + "development": { + "browserTarget": "tagsinput-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js tagsinput" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/tagsinput/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tagsinput/demo/tsconfig.spec.json", + "karmaConfig": "src/tagsinput/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/tagsinput/demo/src/app/AppComponent.ts b/src/tagsinput/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/tagsinput/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/tagsinput/demo/src/app/AppModule.ts b/src/tagsinput/demo/src/app/AppModule.ts new file mode 100644 index 0000000..ff6ffb1 --- /dev/null +++ b/src/tagsinput/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TagsInputTestModule } from './tagsinput/TagsInputTestModule'; + +@NgModule({ + imports: [ + TagsInputTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/tagsinput/demo/src/app/IndexComponent.ts b/src/tagsinput/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..2a3852c --- /dev/null +++ b/src/tagsinput/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TagsInputTestModule } from './tagsinput/TagsInputTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TagsInputTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/tagsinput/demo/src/app/app.html b/src/tagsinput/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/tagsinput/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsInputTestModule.ts b/src/tagsinput/demo/src/app/tagsinput/TagsInputTestModule.ts new file mode 100644 index 0000000..445f4c0 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsInputTestModule.ts @@ -0,0 +1,105 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiTagsInputModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TagsinputDisabledComponent } from './TagsinputDisabledComponent'; +import { TagsinputSuggestionComponent } from './TagsinputSuggestionComponent'; +import { TagsinputBasicComponent } from './TagsinputBasicComponent'; +import { TagsinputPanelwidthComponent } from './TagsinputPanelwidthComponent'; +import { TagsinputValidComponent } from './TagsinputValidComponent'; +import { TagsinputLabelkeyComponent } from './TagsinputLabelkeyComponent'; +import { TagsinputEventsComponent } from './TagsinputEventsComponent'; +import { TagsinputValuekeyComponent } from './TagsinputValuekeyComponent'; +import { TagsinputNullComponent } from './TagsinputNullComponent'; +import { TagsinputReactiveComponent } from './TagsinputReactiveComponent'; +import { TagsinputTemplateComponent } from './TagsinputTemplateComponent'; +import { TagsinputMaxlengthComponent } from './TagsinputMaxlengthComponent'; +import { TagsinputSeparatorsComponent } from './TagsinputSeparatorsComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiTagsInputModule, + TiValidationModule, + ReactiveFormsModule, + DemoLogModule, + RouterModule.forChild(TagsInputTestModule.ROUTES) + ], + declarations: [ + TagsinputDisabledComponent, + TagsinputSuggestionComponent, + TagsinputBasicComponent, + TagsinputPanelwidthComponent, + TagsinputValidComponent, + TagsinputLabelkeyComponent, + TagsinputEventsComponent, + TagsinputValuekeyComponent, + TagsinputNullComponent, + TagsinputReactiveComponent, + TagsinputTemplateComponent, + TagsinputMaxlengthComponent, + TagsinputSeparatorsComponent + ] +}) +export class TagsInputTestModule { + static readonly LINKS: Array = [{ href: 'components/TiTagsInputComponent.html', label: 'TagsInput' }]; + static readonly ROUTES: Routes = [ + { + path: 'tagsinput/tagsinput-basic', + component: TagsinputBasicComponent + }, + { + path: 'tagsinput/tagsinput-suggestion', + component: TagsinputSuggestionComponent + }, + { + path: 'tagsinput/tagsinput-panelwidth', + component: TagsinputPanelwidthComponent + }, + { + path: 'tagsinput/tagsinput-labelkey', + component: TagsinputLabelkeyComponent + }, + { + path: 'tagsinput/tagsinput-valid', + component: TagsinputValidComponent + }, + { + path: 'tagsinput/tagsinput-disabled', + component: TagsinputDisabledComponent + }, + { + path: 'tagsinput/tagsinput-events', + component: TagsinputEventsComponent + }, + { + path: 'tagsinput/tagsinput-valuekey', + component: TagsinputValuekeyComponent + }, + { + path: 'tagsinput/tagsinput-reactive', + component: TagsinputReactiveComponent + }, + { + path: 'tagsinput/tagsinput-template', + component: TagsinputTemplateComponent + }, + { + path: 'tagsinput/tagsinput-null', + component: TagsinputNullComponent + }, + { + path: 'tagsinput/tagsinput-maxlength', + component: TagsinputMaxlengthComponent + }, + { + path: 'tagsinput/tagsinput-separators', + component: TagsinputSeparatorsComponent + } + ]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputBasicComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputBasicComponent.ts new file mode 100644 index 0000000..d32eaab --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputBasicComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsinputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-basic.html' +}) +export class TagsinputBasicComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + placeholder: string = '当前无选中项'; + selected: Array = []; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputDisabledComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputDisabledComponent.ts new file mode 100644 index 0000000..619aa57 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputDisabledComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsinputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-disabled.html' +}) +export class TagsinputDisabledComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + selected: Array = [this.suggestions[0]]; + disabled: boolean = true; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputEventsComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputEventsComponent.ts new file mode 100644 index 0000000..ea9a34c --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputEventsComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsinputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-events.html' +}) +export class TagsinputEventsComponent { + myLogs: Array = []; + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + selected: Array = [this.suggestions[0]]; + + onModelchange(selected: Array): void { + this.selected = selected; + this.myLogs = [...this.myLogs, `onModelchange() event=${JSON.stringify(selected)}`]; + } +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputLabelkeyComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputLabelkeyComponent.ts new file mode 100644 index 0000000..1c784ed --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputLabelkeyComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tagsinput-labelkey.html' +}) +export class TagsinputLabelkeyComponent { + suggestions: Array = [ + { id: '1', label: '中文', english: 'Chinese' }, + { id: '2', label: '英文', english: 'English' }, + { id: '3', label: '拉美西语', english: 'Latin American' }, + { id: '4', label: '欧洲西语', english: 'European Spanish' }, + { id: '5', label: '法语', english: 'French' }, + { id: '6', label: '葡萄牙语', english: 'Portuguese' } + ]; + selected: Array = [this.suggestions[0]]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputMaxlengthComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputMaxlengthComponent.ts new file mode 100644 index 0000000..789ee54 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputMaxlengthComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tagsinput-maxlength.html' +}) +export class TagsinputMaxlengthComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + selected: Array = []; + placeholder: string = '当前无选中项'; + maxlength: number = 10; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputNullComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputNullComponent.ts new file mode 100644 index 0000000..89f8649 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputNullComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsinputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-null.html' +}) +export class TagsinputNullComponent { + id: string = 'tag'; + selected: Array = []; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputPanelwidthComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputPanelwidthComponent.ts new file mode 100644 index 0000000..325a17f --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputPanelwidthComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +export interface SuggestionItem { + id?: string; + label?: string; +} + +@Component({ + templateUrl: './tagsinput-panelwidth.html' +}) +export class TagsinputPanelwidthComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + selected: Array = [this.suggestions[0]]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputReactiveComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputReactiveComponent.ts new file mode 100644 index 0000000..1483594 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputReactiveComponent.ts @@ -0,0 +1,25 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { SuggestionItem } from './TagsInputPanelwidthComponent'; +@Component({ + templateUrl: './tagsinput-reactive.html' +}) +export class TagsinputReactiveComponent implements OnInit { + form: FormGroup; + constructor(private fb: FormBuilder) {} + + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + + ngOnInit(): void { + this.form = this.fb.group({ + mytagsInput: [[this.suggestions[0]]] // 设置初始值 + }); + } +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputSeparatorsComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputSeparatorsComponent.ts new file mode 100644 index 0000000..bcdc450 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputSeparatorsComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsInputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-separators.html' +}) +export class TagsinputSeparatorsComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + + selected: Array = [this.suggestions[2]]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputSuggestionComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputSuggestionComponent.ts new file mode 100644 index 0000000..dfdcd25 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputSuggestionComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsinputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-suggestion.html' +}) +export class TagsinputSuggestionComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + + selected: Array = [this.suggestions[0]]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputTemplateComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputTemplateComponent.ts new file mode 100644 index 0000000..91824b6 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputTemplateComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsinputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-template.html' +}) +export class TagsinputTemplateComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + + selected: Array = [this.suggestions[0]]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputValidComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputValidComponent.ts new file mode 100644 index 0000000..5c456e0 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputValidComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsinputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-valid.html' +}) +export class TagsinputValidComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + + selected: Array = [this.suggestions[0]]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputValuekeyComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputValuekeyComponent.ts new file mode 100644 index 0000000..e878e54 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputValuekeyComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tagsinput-valuekey.html' +}) +export class TagsinputValuekeyComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + selected: Array = [this.suggestions[0].id]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-basic.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-basic.html new file mode 100644 index 0000000..83f7e27 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-basic.html @@ -0,0 +1,10 @@ + +
    +
    Current value: {{ selected | json }}
    +
    diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-disabled.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-disabled.html new file mode 100644 index 0000000..155fa78 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-disabled.html @@ -0,0 +1,7 @@ + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-events.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-events.html new file mode 100644 index 0000000..70c4c63 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-events.html @@ -0,0 +1,8 @@ + + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-labelkey.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-labelkey.html new file mode 100644 index 0000000..fbd6c10 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-labelkey.html @@ -0,0 +1 @@ + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-maxlength.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-maxlength.html new file mode 100644 index 0000000..ca47254 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-maxlength.html @@ -0,0 +1,11 @@ + +
    +
    Current value: {{ selected | json }}
    +
    diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-null.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-null.html new file mode 100644 index 0000000..6a6c88d --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-null.html @@ -0,0 +1,17 @@ +

    描述

    +

    测试suggestion接口为null、undefined、空数据时,组件功能是否正常

    +

    示例

    +

    1.undefined:

    +
    +

    选中项: {{selected | json}}

    + +

    +

    2.null:

    +
    +

    选中项: {{selected | json}}

    + +

    +

    3.空数据[]:

    +
    +

    选中项: {{selected | json}}

    + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-panelwidth.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-panelwidth.html new file mode 100644 index 0000000..2dc84bf --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-panelwidth.html @@ -0,0 +1 @@ + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-reactive.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-reactive.html new file mode 100644 index 0000000..6982540 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-reactive.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-separators.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-separators.html new file mode 100644 index 0000000..7dba87e --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-separators.html @@ -0,0 +1,10 @@ + +
    +
    Current value: {{ selected | json }}
    +
    diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-suggestion.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-suggestion.html new file mode 100644 index 0000000..7e06bb5 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-suggestion.html @@ -0,0 +1 @@ + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-template.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-template.html new file mode 100644 index 0000000..c4dda27 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-template.html @@ -0,0 +1,5 @@ + + + {{i}} {{item.label}} + + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-valid.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-valid.html new file mode 100644 index 0000000..8e10751 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-valid.html @@ -0,0 +1 @@ + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-valuekey.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-valuekey.html new file mode 100644 index 0000000..ba95bf2 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-valuekey.html @@ -0,0 +1,4 @@ + +
    +
    Current value: {{ selected | json }}
    +
    diff --git a/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput-demos.js b/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput-demos.js new file mode 100644 index 0000000..1acfd27 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput-demos.js @@ -0,0 +1,135 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'tagsinput-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': '

    TagsInput 组件的最简用法。

    ', + 'en-US': '' + }, + apis: ['TiTagsInputComponent.properties.suggestions', 'TiTagsInputComponent.properties.placeholder'] + }, + { + demoId: 'tagsinput-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled' + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': '' + }, + apis: ['TiTagsInputComponent.properties.disabled'] + }, + { + demoId: 'tagsinput-events', + name: { + 'zh-CN': '事件', + 'en-US': 'events' + }, + desc: { + 'zh-CN': '

    当元素内容发生变化的时候触发ngModelChange事件。

    ', + 'en-US': 'tagsinput events description' + } + }, + { + demoId: 'tagsinput-labelkey', + name: { + 'zh-CN': '显示文本', + 'en-US': 'labelkey' + }, + desc: { + 'zh-CN': '

    通过属性labelKey配置显示文本的键值。

    ', + 'en-US': '' + }, + apis: ['TiTagsInputComponent.properties.labelKey'] + }, + { + demoId: 'tagsinput-panelwidth', + name: { + 'zh-CN': '下拉面板宽度', + 'en-US': 'panelwidth' + }, + desc: { + 'zh-CN': + '

    通过属性panelWidth配置下拉面板宽度,包含autojustifiedstring三种类型。

    ', + 'en-US': '' + }, + apis: ['TiTagsInputComponent.properties.panelWidth'] + }, + { + demoId: 'tagsinput-valid', + name: { + 'zh-CN': '校验', + 'en-US': 'tagsinput validation' + }, + desc: { + 'zh-CN': '

    通过指令tiValidation实现校验。

    ', + 'en-US': '' + } + }, + { + demoId: 'tagsinput-valuekey', + name: { + 'zh-CN': '绑定值为基础类型', + 'en-US': 'valuekey' + }, + desc: { + 'zh-CN': '

    通过属性valueKey配置绑定值为基础类型的值。

    ', + 'en-US': '' + }, + apis: ['TiTagsInputComponent.properties.valueKey'] + }, + { + demoId: 'tagsinput-separators', + name: { + 'zh-CN': '自动分词', + 'en-US': 'separators' + }, + desc: { + 'zh-CN': '

    通过属性separators配置自动分词的分隔符,试着粘贴下中文 Chinese,法语 French|泰文。', + 'en-US': '' + }, + apis: ['TiTagsInputComponent.properties.separators'] + }, + { + demoId: 'tagsinput-reactive', + name: { + 'zh-CN': '响应式表单', + 'en-US': 'reactive-form' + }, + desc: { + 'zh-CN': '

    响应式表单的基本用法。

    ', + 'en-US': '' + } + }, + { + demoId: 'tagsinput-template', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'template' + }, + desc: { + 'zh-CN': '

    通过#item配置下拉面板中选项的模板。

    ', + 'en-US': '' + } + }, + { + demoId: 'tagsinput-maxlength', + name: { + 'zh-CN': '允许的最大字符数', + 'en-US': 'maxlength' + }, + desc: { + 'zh-CN': '

    通过属性maxlength配置输入框中允许的最大字符数。

    ', + 'en-US': '' + }, + apis: ['TiTagsInputComponent.properties.maxlength'] + } + ] +}; diff --git a/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.cn.md b/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.cn.md new file mode 100644 index 0000000..b49358e --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.cn.md @@ -0,0 +1,40 @@ +--- +title: TagsInput 标签输入 +--- +# TagsInput 标签输入 + +
    + +TagsInput 是标签输入框组件。   + ++ 支持输入框输入标签、可以联想选择标签等场景。 + +```typescript +import { TiTagsInputModule } from '@opentiny/ng'; +``` + ++ 如需使用校验功能,请导入。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
    + +
    + +TagsInput 是标签输入框组件。   + ++ 支持输入框输入标签、可以联想选择标签等场景。 + +```typescript +import { TiTagsInputModule } from '@cloud/tiny-config'; +``` + ++ 如需使用校验功能,请导入。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.en.md b/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/tagsinput/demo/src/favicon.ico b/src/tagsinput/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/tagsinput/demo/src/index.html b/src/tagsinput/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/tagsinput/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/tagsinput/demo/src/main.ts b/src/tagsinput/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/tagsinput/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/tagsinput/demo/test.ts b/src/tagsinput/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/tagsinput/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/tagsinput/demo/tsconfig.app.json b/src/tagsinput/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/tagsinput/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/tagsinput/demo/tsconfig.spec.json b/src/tagsinput/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/tagsinput/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/tagsinput/lib/index.ts b/src/tagsinput/lib/index.ts new file mode 100644 index 0000000..64bf38f --- /dev/null +++ b/src/tagsinput/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTagsInputModule'; diff --git a/src/tagsinput/lib/ng-package.json b/src/tagsinput/lib/ng-package.json new file mode 100644 index 0000000..8fed06b --- /dev/null +++ b/src/tagsinput/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/tagsinput", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/tagsinput/lib/package.json b/src/tagsinput/lib/package.json new file mode 100644 index 0000000..aa2b6c7 --- /dev/null +++ b/src/tagsinput/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "@opentiny/ng-tagsinput", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-dominator": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-dropsearch": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/tagsinput/lib/project.json b/src/tagsinput/lib/project.json new file mode 100644 index 0000000..0947c6e --- /dev/null +++ b/src/tagsinput/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/tagsinput/lib", + "sourceRoot": "src/tagsinput/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/tagsinput"], + "options": { + "project": "src/tagsinput/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/tagsinput"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js tagsinput" + }, + { + "command": "ng default-build tagsinput" + }, + { + "command": "node build/clear-default-theme.js tagsinput" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/tagsinput && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build tagsinput && ng pack tagsinput && node build/publish.js tagsinput --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/tagsinput/lib/src/TiTagsInputComponent.ts b/src/tagsinput/lib/src/TiTagsInputComponent.ts new file mode 100644 index 0000000..2ceaa31 --- /dev/null +++ b/src/tagsinput/lib/src/TiTagsInputComponent.ts @@ -0,0 +1,479 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ContentChild, ElementRef, Input, TemplateRef, ViewChild } from '@angular/core'; +import { TiDominatorComponent } from '@opentiny/ng-dominator'; +import { TiFormComponent, TiWholeComponent } from '@opentiny/ng-base'; +import { TiDropsearchComponent } from '@opentiny/ng-dropsearch'; +import { TiKeymap } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; +/** + * TagsInput标签输入组件 + * + * TagsInput组件主要实现了一个可以输入标签、可以联想选择标签的功能组件。 + * + */ +@Component({ + selector: 'ti-tags-input', + templateUrl: './tagsinput.html', + styleUrls: ['./tagsinput.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiTagsInputComponent)], + host: { + '(blur)': 'onBlur()' + } +}) +export class TiTagsInputComponent extends TiWholeComponent { + /** + * 必选,下拉建议项 + */ + @Input() suggestions: Array = []; + /** + * 输入框的占位文本 + */ + @Input() placeholder: string = ''; + /** + * 下拉面板的宽度。 + * + * 1."justified": 下拉面板宽度与输入框宽度保持一致; + * + * 2."auto": 下拉面板宽度根据下拉选项的内容自动撑开; + * + * 3.固定的下拉面板宽度: 不小于输入框组件的宽度,例如:"200px" + */ + @Input() panelWidth: string = 'justified'; + /** + * 下拉建议项要展示的键值 + */ + @Input() labelKey: string = 'label'; + /** + * 输入框允许的最大字符数 + */ + @Input() maxlength: number; + /** + * 自动分词的分隔符,12.1.28 新增 + */ + @Input() separators: Array = []; + /** + * @ignore + * 内部的input标签 + */ + @ViewChild('input', { static: true }) public inputRef: ElementRef; + /** + * @ignore + * 用户写的item模板 + */ + @ContentChild(TemplateRef, { static: true }) itemTemplate: TemplateRef; + /** + * @ignore + * 内部标签donimator组件 + */ + @ViewChild(TiDominatorComponent, { static: true }) + dominatorCom: TiDominatorComponent; + /** + * @ignore + * 内部标签selectDrop组件 + */ + @ViewChild(TiDropsearchComponent, { static: true }) + selectDrop: TiDropsearchComponent; + /** + * @ignore + * input输入框的值 + */ + public inputValue: string = ''; // 输入框内容 + /** + * @ignore + * 兼容TiWholeComponent,该组件仅多选 + */ + public multiple: boolean = true; + /** + * 自动分词的分隔符正则 + * + * @private + */ + private separatorsReg: RegExp; + /** + * 记录粘贴的文本 + * + * @private + */ + private pasteValue: string; + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + super.ngOnInit(); + if (this.separators.length !== 0) { + const separatorsStr: string = this.separators.join(''); + this.separatorsReg = new RegExp(`[${separatorsStr}]`); + } + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + // 推荐在onInit()时调用setFocusableElems(), 但是ngFor/ngIf中的元素在ngAfterViewInit()才能获取到 + this.setFocusableElems([this.dominatorCom.nativeElement, this.inputRef.nativeElement]); + } + + /** + * @ignore + * 处理select事件 + */ + onSelect(event: any): void { + // 当前内容对应的tag不在选中列表中或者不是输入生成的tag + + if (this.findFirstIndex(this.modelWhole, this.labelKey, event[this.labelKey]) === -1 || !event.isInput) { + this.modelWhole.push(event); + this.modelWhole = this.modelWhole.concat(); + } + + this.selectDrop.searchResult = this.getSuggestions(this.suggestions, this.modelWhole); + // 去除选中项样式 + this.selectDrop.listCom.model = undefined; + + // 清空输入框 + this.inputValue = ''; + // 选中项由一行变为两行时,需要重定位 + setTimeout(() => { + this.selectDrop.rePosition(); + }, 0); + } + + /** + * @ignore + * 处理点击Dominator事件 + */ + onClickDominator(): void { + if (this.disabled) { + return; + } + + this.inputRef.nativeElement.focus(); + } + + /** + * @ignore + * 点击选中项的叉号:从选中项中移除当前选中项 + */ + onDelete(event: { item: any; model: any }): void { + this.inputRef.nativeElement.focus(); + // 选中项由两行变为一行时,需要重定位 + // 在app onpush和app default两种模式测试,确实需要settimeout,但不需要markfor + setTimeout(() => { + this.selectDrop.rePosition(); + }, 0); + this.selectDrop.searchResult = this.getSuggestions(this.suggestions, event.model); + } + + /** + * @ignore + * 组件整体失焦时,面板收起 + */ + onBlur(): void { + this.selectDrop.hide(); + } + + /** + * @ignore + * 输入框失焦时,面板隐藏,根据文本框内容处理是否添加到选中项 + */ + onInputBlur(): void { + const value: string = this.inputValue; + + // 清空输入框 + this.inputValue = ''; + + // 输入框为空时或者当前输入框内容对应的标签已经在选中列表中了 + if (value.trim() === '' || this.findFirstIndex(this.modelWhole, this.labelKey, value) !== -1) { + return; + } + // 将输入框内容添加到选中标签中 + this.addTagToSelected(value); + } + + /** + * @ignore + * 处理input框聚焦事件 + */ + onInputFocus(event: FocusEvent): void { + this.selectDrop.searchResult = this.getSuggestions(this.suggestions, this.modelWhole); + // 视图绘制早于变量更新,变量更新后下拉定位不正确, 需要延时处理 + setTimeout(() => { + this.showSelectDrop(); + }, 0); + } + + /** + * @ignore + * 处理input输入框keyup快捷键功能 + */ + onInputKeyup(event: KeyboardEvent): void { + if (event.keyCode === TiKeymap.KEY_ENTER || event.keyCode === TiKeymap.KEY_NUMPAD_ENTER) { + this.responseEnter(event); + } + } + + /** + * @ignore + * 处理input输入框keydown快捷键功能 + * 回删键只能在keydown中处理:因为回删时,在keydown时输入框为当前值,keyup时为删除后的值 + * 比如输入一个字符,回删,获取的应该是回删前的一个字符,而不是没有删除后的 + */ + onInputKeydown(event: KeyboardEvent): void { + if (event.keyCode === TiKeymap.KEY_BACKSPACE) { + this.responseBackspace(event); + } + } + + /** + * @ignore + * 获取粘贴时的文本 + * @param event + */ + onInputPaste(event: any): void { + this.pasteValue = event.clipboardData.getData('Text'); + } + + /** + * @ignore + * 输入框内容变化时的处理 + * @param inputValue + */ + onInputChange(inputValue: string): void { + let value: string = inputValue; + + // 此处在粘贴事件中获取值的原因:当粘贴值包含换行符时,输入框会将换行符处理为空格 + if (this.pasteValue) { + value = this.pasteValue; + this.pasteValue = ''; + } + + // 输入值匹配到分隔符时 + if (this.separatorsReg?.test(value)) { + this.tokenSeparate(value); + + return; + } + + // 记录上次匹配项长度 + const oldSearchResultLength: number = this.selectDrop.searchResult?.length; + // 过滤出匹配项 + this.selectDrop.searchWordChange(inputValue); + + // 由于dropsearch中searchWordChange调用setSearchResult方法,当匹配项长度变化时才会去重定位 + // tagsinput组件 匹配项长度相同选中项行数变化时,也需要重定位 + if (this.selectDrop.searchResult?.length === oldSearchResultLength) { + setTimeout(() => { + this.selectDrop.rePosition(); + }, 0); + } + + // 从匹配项中移除已选中项 + this.selectDrop.searchResult = this.getSuggestions(this.selectDrop.searchResult, this.modelWhole); + + // 输入框为空时或者当前输入框内容对应的标签已经在选中列表中了,无需将输入框值添加至建议项中 + if (inputValue.trim() === '' || this.findFirstIndex(this.selectDrop.searchResult, this.labelKey, inputValue) !== -1) { + return; + } + + // 有labelkey和valueKey时,需要设置对应键值 + if (this.valueKey) { + this.selectDrop.searchResult.unshift({ + id: inputValue, + [this.labelKey]: inputValue, + [this.valueKey]: inputValue, + isInput: true + }); + } else { + this.selectDrop.searchResult.unshift({ + id: inputValue, + [this.labelKey]: inputValue, + isInput: true + }); + } + } + + /** + * 处理input输入框enter键功能 + * @param event + */ + private responseEnter(event: KeyboardEvent): void { + // 获取输入框的值 + const value: string = this.inputValue; + + // 当有建议项存在时,按回车键会执行下拉建议项select事件逻辑,此处无需再处理 + if (this.suggestions && this.suggestions.length !== 0) { + return; + } + + // 清空input框的内容 + this.inputValue = ''; + + this.addTag(value); + } + + /** + * 处理input输入框回删键功能 + * @param event + */ + private responseBackspace(event: KeyboardEvent): void { + this.selectDrop.hide(); + // 面板关闭时 + if (!this.selectDrop.isShow) { + this.showSelectDrop(); + // 选中项由两行变为一行时,需要重定位 + setTimeout(() => { + this.selectDrop.rePosition(); + }, 0); + } + const value: string = this.inputRef.nativeElement.value; + // 输入框值不为空字符串时 + if (value !== '') { + return; + } + + // 获取当前选中项的长度 + const modelLength: number = this.modelWhole.length; + + // 当前已经没有选中项时,不做处理 + if (modelLength === 0) { + return; + } + + const lastSelected: any = this.modelWhole[modelLength - 1]; + const index: number = this.suggestions?.indexOf(lastSelected); + // 从选中项中删除 + this.modelWhole.pop(); + this.modelWhole = this.modelWhole.concat(); + + // 将要从选中项删除的最后一项是下拉选项中的一项时,将其添加到现有的下拉选项中; + // 如果是用户自己创建的,则不放入下拉项中 + if (index !== -1) { + this.selectDrop.searchResult = this.getSuggestions(this.suggestions, this.modelWhole); + } + } + + /** + * 根据数组中属性找是否有匹配项 + * @param arr 需要匹配的数组 + * @param key 需要查找的属性 + * @param value 属性值 + * @returns + */ + private findFirstIndex(arr: any, key: string, value: string): number { + if (!(arr instanceof Array)) { + return -1; + } + + return arr.findIndex((i: any) => i[key] === value); + } + + /** + * 将输入框内容添加到选中标签中 + * @param inputValue 输入框的内容 + */ + private addTagToSelected(inputValue: string): void { + const index: number = this.findFirstIndex(this.suggestions, this.labelKey, inputValue); + // 有labelkey和valueKey时,需要设置对应键值 + const newTag: any = + index === -1 + ? this.valueKey + ? { + id: inputValue, + [this.labelKey]: inputValue, + [this.valueKey]: inputValue + } + : { id: inputValue, [this.labelKey]: inputValue } + : this.suggestions[index]; + this.modelWhole.push(newTag); + this.modelWhole = this.modelWhole.concat(); + } + + /** + * 从suggestions中删除选中项 + */ + private getSuggestions(suggestions: any, selected: any): Array { + const newSuggestions: any = !(suggestions instanceof Array) ? [] : suggestions.concat(); // 使用数组的concat方法,实现深拷贝 + + let index: number; + for (const select of selected) { + index = newSuggestions.indexOf(select); + + if (index !== -1) { + // 选中项如果在下拉列表中存在,则删除 + newSuggestions.splice(index, 1); + } + } + + // 下拉建议项数据更新时,需要手动触发变化检测 + if (newSuggestions !== this.selectDrop.searchResult) { + this.selectDrop.changeDetectorRef.markForCheck(); + } + + return newSuggestions; + } + + /** + * @ignore + * 展开面板时设置下拉选中项和hoverOption + */ + public showSelectDrop(): void { + if (!this.suggestions || (this.suggestions && this.suggestions.length === 0)) { + return; + } + // 显示下拉面板 + this.selectDrop.show(); + + if (!this.selectDrop.listCom) { + return; + } + // 设置selectDrop的选中项 + this.selectDrop.listCom.model = undefined; + } + + /** + * 自动分词时,将输入框的值分隔生成 tag 并处理建议项数据 + * + * @private + * @param value + */ + private tokenSeparate(value: string): void { + const valueArr: Array = value.split(this.separatorsReg); + + // 清空输入框的值 + this.inputRef.nativeElement.value = ''; + this.inputValue = ''; + + valueArr.forEach((item: string) => { + // 过滤出匹配项 + this.selectDrop.searchWordChange(item); + this.addTag(item); + }); + + // 从匹配项中移除已选中项 + this.selectDrop.searchResult = this.getSuggestions(this.suggestions, this.modelWhole); + } + + /** + * 生成 tag 标签 + * + * @private + * @param value + * @returns + */ + private addTag(value: string): void { + // 输入框为空时或者当前输入框内容对应的标签已经在选中列表中了 + if (value.trim() === '' || this.findFirstIndex(this.modelWhole, this.labelKey, value) !== -1) { + return; + } + + this.addTagToSelected(value); + } +} diff --git a/src/tagsinput/lib/src/TiTagsInputModule.ts b/src/tagsinput/lib/src/TiTagsInputModule.ts new file mode 100644 index 0000000..30830b5 --- /dev/null +++ b/src/tagsinput/lib/src/TiTagsInputModule.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiTagsInputComponent } from './TiTagsInputComponent'; +import { TiDropsearchModule } from '@opentiny/ng-dropsearch'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiDominatorModule } from '@opentiny/ng-dominator'; +/** + * + */ +@NgModule({ + imports: [CommonModule, FormsModule, TiDominatorModule, TiDropsearchModule, TiTextModule], + exports: [TiTagsInputComponent], + declarations: [TiTagsInputComponent] +}) +export class TiTagsInputModule {} +export { TiTagsInputComponent } from './TiTagsInputComponent'; diff --git a/src/tagsinput/lib/src/tagsinput.html b/src/tagsinput/lib/src/tagsinput.html new file mode 100644 index 0000000..8a3c759 --- /dev/null +++ b/src/tagsinput/lib/src/tagsinput.html @@ -0,0 +1,63 @@ + + + + +
    + + + {{inputValue}} +
    +
    + + + + + + + + + + + {{item[labelKey]}} + diff --git a/src/tagsinput/lib/src/tagsinput.less b/src/tagsinput/lib/src/tagsinput.less new file mode 100644 index 0000000..1118ac1 --- /dev/null +++ b/src/tagsinput/lib/src/tagsinput.less @@ -0,0 +1,37 @@ +@import '../../../themes/basic/base-all.less'; +@import '../../../themes/basic/compnent-container-border.less'; + +:host { + &:extend(.ti3-compnent-container-border all); +} + +.ti3-dominator-tagsinput { + input[tiText] { + position: absolute; + left: 0; + padding: var(--ti-common-space-0) var(--ti-common-space-10); + width: 100%; + height: 100%; + .box-sizing(border-box); + background-color: transparent; // 添加透明背景是为了覆盖tiText设置的白色背景,使校验结果样式生效 + } + .ti3-dominator-input-container { + position: relative; + display: inline-block; + vertical-align: middle; + overflow: hidden; + max-width: 100%; + height: calc( + var(--ti-common-size-7x) - var(--ti-common-border-weight-normal) * 2 - var(--ti-common-space-1) * 2 + ); // tagsinput上下边框2px + dominator内边距2px = 4px + padding: var(--ti-common-space-0) var(--ti-common-space-10); + .box-sizing(border-box); + & > span { + position: relative; + display: block; + visibility: hidden; + max-width: 100%; + height: 100%; + } + } +} diff --git a/src/text/demo/karma.conf.js b/src/text/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/text/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/text/demo/project.json b/src/text/demo/project.json new file mode 100644 index 0000000..b5edca5 --- /dev/null +++ b/src/text/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/text/demo", + "sourceRoot": "src/text/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/text", + "index": "src/text/demo/src/index.html", + "main": "src/text/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/text/demo/tsconfig.app.json", + "assets": ["src/text/demo/src/favicon.ico", "src/text/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "text-demo:build:production" + }, + "development": { + "browserTarget": "text-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js text" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/text/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/text/demo/tsconfig.spec.json", + "karmaConfig": "src/text/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/text/demo/src/app/AppComponent.ts b/src/text/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/text/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/text/demo/src/app/AppModule.ts b/src/text/demo/src/app/AppModule.ts new file mode 100644 index 0000000..491ba9d --- /dev/null +++ b/src/text/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TextTestModule } from './text/TextTestModule'; + +@NgModule({ + imports: [ + TextTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/text/demo/src/app/IndexComponent.ts b/src/text/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..73138ad --- /dev/null +++ b/src/text/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TextTestModule } from './text/TextTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TextTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/text/demo/src/app/app.html b/src/text/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/text/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/text/demo/src/app/text/TextBasicComponent.ts b/src/text/demo/src/app/text/TextBasicComponent.ts new file mode 100644 index 0000000..d23424f --- /dev/null +++ b/src/text/demo/src/app/text/TextBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + templateUrl: './text-basic.html' +}) +export class TextBasicComponent { + value: string = '长期艰苦奋斗'; +} diff --git a/src/text/demo/src/app/text/TextClearComponent.ts b/src/text/demo/src/app/text/TextClearComponent.ts new file mode 100644 index 0000000..fe3e56b --- /dev/null +++ b/src/text/demo/src/app/text/TextClearComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './text-clear.html' +}) +export class TextClearComponent { + value: string = '长期艰苦奋斗'; +} diff --git a/src/text/demo/src/app/text/TextDisabledComponent.ts b/src/text/demo/src/app/text/TextDisabledComponent.ts new file mode 100644 index 0000000..6768cd9 --- /dev/null +++ b/src/text/demo/src/app/text/TextDisabledComponent.ts @@ -0,0 +1,9 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + templateUrl: './text-disabled.html' +}) +export class TextDisabledComponent { + value: string = '长期艰苦奋斗'; + disabled: boolean = true; +} diff --git a/src/text/demo/src/app/text/TextEventsComponent.ts b/src/text/demo/src/app/text/TextEventsComponent.ts new file mode 100644 index 0000000..005b72f --- /dev/null +++ b/src/text/demo/src/app/text/TextEventsComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './text-events.html' +}) +export class TextEventsComponent { + myLogs: Array = []; + value: string = '长期艰苦奋斗'; + + onClear(value: string): void { + this.myLogs = [...this.myLogs, `clear:${value}`]; + } + + onFocus(value: string): void { + this.myLogs = [...this.myLogs, `focus:${value}`]; + } + + onBlur(value: string): void { + this.myLogs = [...this.myLogs, `blur:${value}`]; + } + + onNgModelChange(value: string): void { + this.myLogs = [...this.myLogs, `modelChange:${value}`]; + } +} diff --git a/src/text/demo/src/app/text/TextFocusComponent.ts b/src/text/demo/src/app/text/TextFocusComponent.ts new file mode 100644 index 0000000..f589d7d --- /dev/null +++ b/src/text/demo/src/app/text/TextFocusComponent.ts @@ -0,0 +1,8 @@ +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; + +@Component({ + templateUrl: './text-focus.html' +}) +export class TextFocusComponent { + value: string = '长期艰苦奋斗'; +} diff --git a/src/text/demo/src/app/text/TextMaskinputComponent.ts b/src/text/demo/src/app/text/TextMaskinputComponent.ts new file mode 100644 index 0000000..0881902 --- /dev/null +++ b/src/text/demo/src/app/text/TextMaskinputComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './text-maskinput.html' +}) +export class TextMaskinputComponent { + mask: string = '000-0000-0000'; + value: string = 'dfasd13af464sd4fasdfas7d8asdf4as5df4'; +} diff --git a/src/text/demo/src/app/text/TextNoborderTestComponent.ts b/src/text/demo/src/app/text/TextNoborderTestComponent.ts new file mode 100644 index 0000000..8d04c49 --- /dev/null +++ b/src/text/demo/src/app/text/TextNoborderTestComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './text-noborder-test.html' +}) +export class TextNoborderTestComponent { + noborder: boolean = true; + value: string = '长期艰苦奋斗'; +} diff --git a/src/text/demo/src/app/text/TextPasswordComponent.ts b/src/text/demo/src/app/text/TextPasswordComponent.ts new file mode 100644 index 0000000..776828c --- /dev/null +++ b/src/text/demo/src/app/text/TextPasswordComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './text-password.html' +}) +export class TextPasswordComponent { + passwordValue: string = '123456'; +} diff --git a/src/text/demo/src/app/text/TextPasswordVisibleComponent.ts b/src/text/demo/src/app/text/TextPasswordVisibleComponent.ts new file mode 100644 index 0000000..b2908e7 --- /dev/null +++ b/src/text/demo/src/app/text/TextPasswordVisibleComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './text-password-visible.html' +}) +export class TextPasswordVisibleComponent { + myLogs: Array = []; + + passwordValue: string = '123456'; + isVisible: boolean = false; + + visibleChange(passwordVisible: boolean): void { + this.isVisible = passwordVisible; + this.myLogs = [...this.myLogs, `passwordVisibleState:${passwordVisible}`]; + } + + changeVisible(): void { + this.isVisible = !this.isVisible; + this.myLogs = [...this.myLogs, `passwordVisibleState:${this.isVisible}`]; + } +} diff --git a/src/text/demo/src/app/text/TextReactiveComponent.ts b/src/text/demo/src/app/text/TextReactiveComponent.ts new file mode 100644 index 0000000..bb1fa8f --- /dev/null +++ b/src/text/demo/src/app/text/TextReactiveComponent.ts @@ -0,0 +1,16 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; + +@Component({ + templateUrl: './text-reactive.html' +}) +export class TextReactiveComponent implements OnInit { + form: FormGroup; + constructor(private fb: FormBuilder) {} + + ngOnInit(): void { + this.form = this.fb.group({ + myInput: ['长期艰苦奋斗'] + }); + } +} diff --git a/src/text/demo/src/app/text/TextReadonlyComponent.ts b/src/text/demo/src/app/text/TextReadonlyComponent.ts new file mode 100644 index 0000000..c2ad8de --- /dev/null +++ b/src/text/demo/src/app/text/TextReadonlyComponent.ts @@ -0,0 +1,8 @@ +import { Component, ElementRef, Renderer2, ViewChild } from '@angular/core'; + +@Component({ + templateUrl: './text-readonly.html' +}) +export class TextReadonlyComponent { + value: string = '长期艰苦奋斗'; +} diff --git a/src/text/demo/src/app/text/TextTestModule.ts b/src/text/demo/src/app/text/TextTestModule.ts new file mode 100644 index 0000000..91af0c1 --- /dev/null +++ b/src/text/demo/src/app/text/TextTestModule.ts @@ -0,0 +1,90 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiTextModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TextBasicComponent } from './TextBasicComponent'; +import { TextClearComponent } from './TextClearComponent'; +import { TextPasswordComponent } from './TextPasswordComponent'; +import { TextReadonlyComponent } from './TextReadonlyComponent'; +import { TextFocusComponent } from './TextFocusComponent'; +import { TextDisabledComponent } from './TextDisabledComponent'; +import { TextEventsComponent } from './TextEventsComponent'; +import { TextNoborderTestComponent } from './TextNoborderTestComponent'; +import { TextReactiveComponent } from './TextReactiveComponent'; +import { TextPasswordVisibleComponent } from './TextPasswordVisibleComponent'; +import { TextMaskinputComponent } from './TextMaskinputComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiButtonModule, + TiTextModule, + DemoLogModule, + RouterModule.forChild(TextTestModule.ROUTES) + ], + declarations: [ + TextBasicComponent, + TextClearComponent, + TextPasswordComponent, + TextReadonlyComponent, + TextFocusComponent, + TextDisabledComponent, + TextEventsComponent, + TextNoborderTestComponent, + TextReactiveComponent, + TextPasswordVisibleComponent, + TextMaskinputComponent + ] +}) +export class TextTestModule { + static readonly LINKS: Array = [{ href: 'components/TiTextComponent.html', label: 'Text' }]; + static readonly ROUTES: Routes = [ + { + path: 'text/text-basic', + component: TextBasicComponent + }, + { + path: 'text/text-clear', + component: TextClearComponent + }, + { + path: 'text/text-password', + component: TextPasswordComponent + }, + { + path: 'text/text-password-visible', + component: TextPasswordVisibleComponent + }, + { + path: 'text/text-readonly', + component: TextReadonlyComponent + }, + { + path: 'text/text-focus', + component: TextFocusComponent + }, + { + path: 'text/text-disabled', + component: TextDisabledComponent + }, + { + path: 'text/text-events', + component: TextEventsComponent + }, + { + path: 'text/text-reactive', + component: TextReactiveComponent + }, + { path: 'text/text-noborder-test', component: TextNoborderTestComponent }, + { + path: 'text/text-maskinput', + component: TextMaskinputComponent + } + ]; +} diff --git a/src/text/demo/src/app/text/text-basic.html b/src/text/demo/src/app/text/text-basic.html new file mode 100644 index 0000000..2a70c3f --- /dev/null +++ b/src/text/demo/src/app/text/text-basic.html @@ -0,0 +1,4 @@ + +
    +
    Current value: {{ value }}
    +
    diff --git a/src/text/demo/src/app/text/text-clear.html b/src/text/demo/src/app/text/text-clear.html new file mode 100644 index 0000000..40073f0 --- /dev/null +++ b/src/text/demo/src/app/text/text-clear.html @@ -0,0 +1 @@ + diff --git a/src/text/demo/src/app/text/text-disabled.html b/src/text/demo/src/app/text/text-disabled.html new file mode 100644 index 0000000..a1dc978 --- /dev/null +++ b/src/text/demo/src/app/text/text-disabled.html @@ -0,0 +1 @@ + diff --git a/src/text/demo/src/app/text/text-events.html b/src/text/demo/src/app/text/text-events.html new file mode 100644 index 0000000..83c0dea --- /dev/null +++ b/src/text/demo/src/app/text/text-events.html @@ -0,0 +1,13 @@ + +
    + diff --git a/src/text/demo/src/app/text/text-focus.html b/src/text/demo/src/app/text/text-focus.html new file mode 100644 index 0000000..10266c9 --- /dev/null +++ b/src/text/demo/src/app/text/text-focus.html @@ -0,0 +1 @@ + diff --git a/src/text/demo/src/app/text/text-maskinput.html b/src/text/demo/src/app/text/text-maskinput.html new file mode 100644 index 0000000..7059dfb --- /dev/null +++ b/src/text/demo/src/app/text/text-maskinput.html @@ -0,0 +1,4 @@ + +
    +
    Current Value: {{ value }}
    +
    diff --git a/src/text/demo/src/app/text/text-noborder-test.html b/src/text/demo/src/app/text/text-noborder-test.html new file mode 100644 index 0000000..0039ff1 --- /dev/null +++ b/src/text/demo/src/app/text/text-noborder-test.html @@ -0,0 +1,2 @@ +

    1.noborder接口测试:

    + diff --git a/src/text/demo/src/app/text/text-password-visible.html b/src/text/demo/src/app/text/text-password-visible.html new file mode 100644 index 0000000..0af42ac --- /dev/null +++ b/src/text/demo/src/app/text/text-password-visible.html @@ -0,0 +1,12 @@ + +
    +
    + + diff --git a/src/text/demo/src/app/text/text-password.html b/src/text/demo/src/app/text/text-password.html new file mode 100644 index 0000000..e5d3e7d --- /dev/null +++ b/src/text/demo/src/app/text/text-password.html @@ -0,0 +1,5 @@ + + +
    +
    + diff --git a/src/text/demo/src/app/text/text-reactive.html b/src/text/demo/src/app/text/text-reactive.html new file mode 100644 index 0000000..0280fa1 --- /dev/null +++ b/src/text/demo/src/app/text/text-reactive.html @@ -0,0 +1,6 @@ +
    + +
    +
    +
    Current value: {{ form.value.myInput }}
    +
    diff --git a/src/text/demo/src/app/text/text-readonly.html b/src/text/demo/src/app/text/text-readonly.html new file mode 100644 index 0000000..3705577 --- /dev/null +++ b/src/text/demo/src/app/text/text-readonly.html @@ -0,0 +1 @@ + diff --git a/src/text/demo/src/app/text/text.spec.ts b/src/text/demo/src/app/text/text.spec.ts new file mode 100644 index 0000000..61d0583 --- /dev/null +++ b/src/text/demo/src/app/text/text.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; + +import { TiTextModule, TiTextComponent } from '../../../../lib/src/TiTextModule'; +import { TextBasicComponent } from './TextBasicComponent'; +NamedNodeMap; +describe('text', () => { + let testComponent: TextBasicComponent; + let fixture: ComponentFixture; + let testDebugEl: DebugElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TiTextModule, FormsModule], + declarations: [TiTextComponent, TextBasicComponent] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TextBasicComponent); + testComponent = fixture.componentInstance; + testDebugEl = fixture.debugElement.query(By.directive(TiTextComponent)); + }); + + describe('text basic', () => { + it('should component defined', () => { + fixture.detectChanges(); + expect(testComponent).toBeDefined(); + }); + + it('should id correct', () => { + fixture.detectChanges(); + expect(testDebugEl.nativeElement!.getAttribute('id')).toEqual('text-basic'); + }); + + it('should ngModel work', fakeAsync(() => { + const testNativeEl = testDebugEl.nativeElement; + fixture.detectChanges(); + tick(); + expect(testNativeEl.value).toEqual('长期艰苦奋斗'); + testNativeEl.value = 'My string'; + var event = new Event('input', { + bubbles: true, + cancelable: true + }); + testNativeEl.dispatchEvent(event); + fixture.detectChanges(); + tick(); + expect(testNativeEl.value).toEqual('My string'); + })); + }); +}); diff --git a/src/text/demo/src/app/text/webdoc/text-demos.js b/src/text/demo/src/app/text/webdoc/text-demos.js new file mode 100644 index 0000000..f3d46cc --- /dev/null +++ b/src/text/demo/src/app/text/webdoc/text-demos.js @@ -0,0 +1,131 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'text-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Text 组件的最简用法。

    ', + 'en-US': '

    basic

    ', + }, + }, + { + demoId: 'text-clear', + name: { + 'zh-CN': '清除功能', + 'en-US': 'clear', + }, + desc: { + 'zh-CN': '

    通过属性clearable配置一键清除功能。

    ', + 'en-US': '

    clear

    ', + }, + apis: ['TiTextComponent.properties.clearable'], + }, + { + demoId: 'text-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'text disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': '

    disabled

    ', + }, + }, + { + demoId: 'text-focus', + name: { + 'zh-CN': '聚焦', + 'en-US': 'focus', + }, + desc: { + 'zh-CN': + '

    通过属性autofocus配置页面重新加载时是否自动聚焦。

    ', + 'en-US': '

    focus

    ', + }, + }, + { + demoId: 'text-password', + name: { + 'zh-CN': '密码框', + 'en-US': 'password', + }, + desc: { + 'zh-CN': + '

    通过type = "password"配置为密码框。通过noeye属性配置为密码不显示。

    ', + 'en-US': '

    password

    ', + }, + apis: [ + 'TiTextComponent.properties.type', + 'TiTextComponent.properties.noeye', + ], + }, + { + demoId: 'text-password-visible', + name: { + 'zh-CN': '密码是否可见', + 'en-US': '', + }, + desc: { + 'zh-CN': + '

    通过passwordVisible属性配置密码是否可见,默认不可见。当密码可见状态改变的时候触发passwordVisibleChange事件。

    ', + 'en-US': '

    reactive

    ', + }, + apis: [ + 'TiTextComponent.properties.passwordVisible', + 'TiTextComponent.events.passwordVisibleChange', + ], + }, + { + demoId: 'text-maskinput', + name: { + 'zh-CN': '格式化输入', + 'en-US': 'maskinput', + }, + desc: { + 'zh-CN': '

    通过属性tiMask配置其输入框数字的格式;输入框中呈现的是格式化后的数字,但是通过ngModel取得的值为纯数字。', + 'en-US': '', + }, + apis: ['TiMaskDirective.properties.tiMask'], + }, + { + demoId: 'text-readonly', + name: { + 'zh-CN': '只读状态', + 'en-US': 'readonly', + }, + desc: { + 'zh-CN': '

    通过readonly属性配置只读状态。

    ', + 'en-US': '

    readonly

    ', + }, + }, + { + demoId: 'text-events', + name: { + 'zh-CN': '事件', + 'en-US': 'events', + }, + desc: { + 'zh-CN': + '

    当元素聚焦的时候触发focus事件。当元素失焦的时候触发blur事件。当元素内容发生变化的时候的时候触发ngModelChange事件。当点击清除图标的时候触发clear事件。

    ', + 'en-US': '

    events', + }, + apis: ['TiTextComponent.events.clear'], + }, + { + demoId: 'text-reactive', + name: { + 'zh-CN': '响应式表单', + 'en-US': 'reactive-form', + }, + desc: { + 'zh-CN': '

    响应式表单的基本用法

    ', + 'en-US': '

    reactive

    ', + }, + }, + ], +}; diff --git a/src/text/demo/src/app/text/webdoc/text.cn.md b/src/text/demo/src/app/text/webdoc/text.cn.md new file mode 100644 index 0000000..9035273 --- /dev/null +++ b/src/text/demo/src/app/text/webdoc/text.cn.md @@ -0,0 +1,29 @@ +--- +title: Text 文本框 +--- +# Text 文本框 + +
    + +文本输入框。   + ++ 支持禁用、密码显示、完全清除等场景。 + ++ 支持通过`tiMask`属性限制用户只能输入数字及设置数字格式,常用于限定身份证、手机号码等格式的输入。 + +```typescript +import { TiTextModule } from '@opentiny/ng'; +``` + +
    + +
    + +文本输入框。   + ++ 支持禁用、密码显示、完全清除等场景。 + +```typescript +import { TiTextModule } from '@opentiny/ng'; +``` +
    diff --git a/src/text/demo/src/app/text/webdoc/text.en.md b/src/text/demo/src/app/text/webdoc/text.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/text/demo/src/app/text/webdoc/text.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/text/demo/src/favicon.ico b/src/text/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/text/demo/src/index.html b/src/text/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/text/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/text/demo/src/main.ts b/src/text/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/text/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/text/demo/test.ts b/src/text/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/text/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/text/demo/tsconfig.app.json b/src/text/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/text/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/text/demo/tsconfig.spec.json b/src/text/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/text/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/text/lib/index.ts b/src/text/lib/index.ts new file mode 100644 index 0000000..9c46f7b --- /dev/null +++ b/src/text/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTextModule'; diff --git a/src/text/lib/ng-package.json b/src/text/lib/ng-package.json new file mode 100644 index 0000000..3b60e57 --- /dev/null +++ b/src/text/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/text", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/text/lib/package.json b/src/text/lib/package.json new file mode 100644 index 0000000..0dc62ce --- /dev/null +++ b/src/text/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-text", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/text/lib/project.json b/src/text/lib/project.json new file mode 100644 index 0000000..cdf1fff --- /dev/null +++ b/src/text/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/text/lib", + "sourceRoot": "src/text/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/text"], + "options": { + "project": "src/text/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/text"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js text" + }, + { + "command": "ng default-build text" + }, + { + "command": "node build/clear-default-theme.js text" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/text && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build text && ng pack text && node build/publish.js text --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/text/lib/src/TiMaskDirective.ts b/src/text/lib/src/TiMaskDirective.ts new file mode 100644 index 0000000..083bac3 --- /dev/null +++ b/src/text/lib/src/TiMaskDirective.ts @@ -0,0 +1,283 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, forwardRef, Input, Renderer2 } from '@angular/core'; +import { DefaultValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { TiBrowser, Util } from '@opentiny/ng-utils'; +/** + * TiMask 格式化数字指令 + * + * 该指令主要用于输入框中,限制用户只能输入数字,可以通过设置tiMask属性接口设置其数字的格式:身份证,手机号码等形式输入。 + * + * 输入框中呈现的是格式化后的数字,但是通过ngModel取得的值为纯数字的值,如输入框中呈现的值为'123 456 789',通过ngModel取得的值为'123456789' + * + */ +@Directive({ + selector: '[tiMask]', + providers: [ + { + // MASK_INPUT_VALUE_ACCESSOR, 原本是外部变量或者static变量,lib编译不通过 + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TiMaskDirective), + multi: true + } + ], + host: { + '(input)': 'handleInput($event.target.value)', + '(compositionstart)': 'handleCompositionStart()', + '(compositionend)': 'handleCompositionEnd($event.target.value)', + '(blur)': 'blur()' + } +}) +export class TiMaskDirective extends DefaultValueAccessor { + private static readonly NUM_SIGN: string = '0'; + /** + * 设置数字格式 + */ + @Input('tiMask') format: string = '000 0000 0000'; + private element: HTMLInputElement; + private ctxPos: any; + private valueCharPosOffset: any; + private composing: boolean = false; // 是否正在拼写拼音 + private onChangeFn: Function; // 需要在registerOnChange中注册,用于改变模型值时调用 + private modelValue: any; + constructor(private renderer: Renderer2, private elementRef: ElementRef) { + // super中第三个参数是DefaultValueAccessor中对文本段落(例如中文)输入的处理的一个标志位, + // 表示是否为非安卓系统,在PC端此参数为true。后续如果要处理移动端,则可能需要动态根据此参数对 + // 文本段落(例如中文)输入做处理。 + super(renderer, elementRef, true); + this.element = this.elementRef.nativeElement; + } + + // 获取在format范围内的有效字符串 + private static getClearValue(value: any, format: string): void { + const formatNumLen: number = format.replace(/[^0]/g, '').length; + let clearValue: any = value.replace(/\D/g, ''); + clearValue = clearValue.slice(0, formatNumLen); + + return clearValue; + } + + private static formatValue(value: any, format: string): any { + if (!value) { + return ''; + } + const clearValue: any = TiMaskDirective.getClearValue(value, format); + const clearValueLen: number = clearValue.length; // value长度 + let valueCharPosOffset: number = 0; // value字符位置增量 + let valueCharPos: number; // 循环中的value字符位置 + let newValue: any = ''; + let formatChar: string; + for (let i: number = 0, len: number = format.length; i < len; i++) { + valueCharPos = i - valueCharPosOffset; + if (valueCharPos >= clearValueLen) { + // value内容循环完成情况下,不进行处理 + break; + } + formatChar = format[i]; + if (formatChar === TiMaskDirective.NUM_SIGN) { + // format字符为数字情况下的处理 + newValue += clearValue[valueCharPos]; + } else { + // format字符非数字情况下的处理,此时value字段中增加format字符 + valueCharPosOffset++; + newValue += formatChar; + } + } + + return newValue; + } + + // model => view + /** + * @ignore + */ + writeValue(value: any): void { + // 使用ngModel时,初始赋值第一次传入的value为null + if (value === null) { + return; + } + // format modelValue + const formatValue: any = TiMaskDirective.formatValue(value, this.format); + // Write formatted modelValue to view + super.writeValue(formatValue); + // 格式化后也需要通知修改模型值 + this.modelValue = this.getAntiFormatValue(formatValue); + if (this.modelValue !== value) { + if (Util.isUndefined(this.onChangeFn)) { + // 在reactive-form中使用,初始化赋值调用writeValue时, + // 此时registerOnChange还未被调用,onChangeFn还未被赋值, + // 所以要使用setTimeout等onChangeFn被赋值后再调用 + setTimeout(() => { + this.onChangeFn(this.modelValue); + }, 0); + } else { + this.onChangeFn(this.modelValue); + } + } + } + /** + * @ignore + */ + registerOnChange(fn: (value: any) => void): void { + this.onChangeFn = fn; + } + + /** + * @ignore + * view => model + */ + public handleInput(value: any): void { + if (!this.composing) { + // 在parser()中对view值进行格式化处理,再重写view的值; + this.parser(value); + } + } + + /** + * @ignore + * 中文输入之前(在输入拼音前) + */ + public handleCompositionStart(): void { + // 在IE和FF下中文输入法下,输入拼音时不会触发input事件, + // 但是在Chrome下,输入拼音时会触发input事件,所以针对Chrome要做特殊处理, + // 使其在输入拼音时不做格式化处理 + if (TiBrowser.isChrome()) { + this.composing = true; + } + } + + /** + * @ignore + * 文本段完成输入或取消输入 + */ + public handleCompositionEnd(value: any): void { + if (TiBrowser.isChrome()) { + this.composing = false; + // 在Chrome下compositionend比input执行滞后, + this.parser(value); + } + } + + /** + * @ignore + */ + public blur(): void { + // 此处的onTouched继承于DefaultValueAccessor + this.onTouched(); + } + + private parser(value: any): void { + const formattedValue: any = TiMaskDirective.formatValue(value, this.format); + this.setCtxPos(value, this.format); + const ctxPos: number = this.ctxPos; + + // 设置viewValue及光标位置 + if (value !== formattedValue) { + this.renderer.setProperty(this.element, 'value', formattedValue); + // 设置光标位置:value非法及输入数字后需要变换位置情况下,需要设置光标位置 + if (this.element === document.activeElement) { + this.element.setSelectionRange(ctxPos, ctxPos); + } + } + const modelValue: any = this.getAntiFormatValue(formattedValue); + if (modelValue !== this.modelValue) { + this.onChangeFn(modelValue); + this.modelValue = modelValue; + } + } + + // 获取反格式化后的value + private getAntiFormatValue(formattedValue: any): any { + return formattedValue.replace(/\D/g, ''); + } + + private setCtxPos(value: any, format: string): void { + // 元素有光标情况下,设置元素光标位置 + this.initCtxPos(); + + if (!value) { + return; + } + + // value非空情况下的处理 + this.valueCharPosOffset = 0; // value字符位置增量 + const ctxPos: number = this.ctxPos; // 初始化ctxPos,后续循环中会以此为光标位置作为对比值 + for (let i: number = 0, len: number = format.length; i < len; i++) { + if (i - this.valueCharPosOffset >= value.length) { + // value字符循环完成情况下,不进行后续处理 + break; + } + this.setCharPos(i, value, format, ctxPos); + } + } + + private setCharPos(pos: number, value: any, format: string, ctxPos: number): void { + const valueCharPos: number = pos - this.valueCharPosOffset; // 循环中的value字符位置 + const valueChar: any = value[valueCharPos]; // 当前value字符 + const formatChar: string = format[pos]; // 当前format字符 + if (formatChar === TiMaskDirective.NUM_SIGN) { + // format为数字情况下的处理 + this.setPosWithNum(value, valueChar, valueCharPos, ctxPos); + } else { + // format非数字情况处理 + this.setPosWithoutNum(valueChar, valueCharPos, formatChar, ctxPos); + } + } + + private setPosWithNum(value: any, valueChar: any, valueCharPos: number, ctxPos: number): void { + if (valueChar.match(/\d/)) { + // value字符相匹配情况下,不做处理 + return; + } + + // value字符不匹配情况下,光标前移,直到找到下一个数字为止 + let valueCharNew: any; + for (let j: number = valueCharPos, valueLen: number = value.length; j < valueLen; j++) { + valueCharNew = value[j]; + if (valueCharNew.match(/\d/)) { + // 找到value字符为数字的情况下,结束循环 + return; + } + + // 找到value字符非数字情况下,过滤掉该字符,光标前移,value偏移量后移,下次循环跳过该字符的检索 + if (ctxPos && ctxPos >= j + 1) { + this.ctxPos--; + } + this.valueCharPosOffset--; + } + } + + private setPosWithoutNum(valueChar: any, valueCharPos: number, formatChar: string, ctxPos: number): void { + // value为数字情况下,value偏移量前移,确保下次循环依然为该value字符 + if (valueChar.match(/\d/)) { + this.valueCharPosOffset++; + if (ctxPos && ctxPos >= valueCharPos + 1) { + this.ctxPos++; + } + + return; + } + + // value非数字且不匹配format字符情况下,过滤掉该字符,下次继续循环下个value字符 + if (valueChar !== formatChar && ctxPos && ctxPos >= valueCharPos + 1) { + this.ctxPos--; + } + } + + private initCtxPos(): void { + let ctxPositon: number; + if (this.element === document.activeElement) { + ctxPositon = this.element.selectionStart; + } + this.ctxPos = ctxPositon; + } +} diff --git a/src/text/lib/src/TiTextComponent.ts b/src/text/lib/src/TiTextComponent.ts new file mode 100644 index 0000000..e175358 --- /dev/null +++ b/src/text/lib/src/TiTextComponent.ts @@ -0,0 +1,389 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DoCheck, + ElementRef, + EventEmitter, + NgZone, + OnDestroy, + OnInit, + Output, + Optional, + Renderer2, + Input, + SimpleChanges +} from '@angular/core'; +import { NgControl } from '@angular/forms'; +import { TiBrowser, Util } from '@opentiny/ng-utils'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiAutofocusComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * 本组件基于原生input标签进行扩展,原生input加 tiText 属性即为text组件, + * 原生input的所有属性以及 Angular 的各种属性指令都可以使用。 + * + */ +@Component({ + selector: '[tiText]', + template: '', + styleUrls: ['text.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-text-input-show-icon]': 'isShowClear || showEye', + '[class.ti3-text-input-show-clear]': 'isShowClear', + '[class.ti3-text-input-show-password]': 'showEye', + '[class.ti3-text-input-noborder]': 'isNoBorder', + '(focus)': 'handleFocus()' + } +}) +export class TiTextComponent extends TiAutofocusComponent implements OnInit, DoCheck, OnDestroy { + private static readonly CLEAR_WIDTH: number = 26; // 常量,清除按钮区域宽度 + private static readonly PASSWORD_WIDTH: number = 36; // 常量,眼睛按钮区域宽度 + /** + * + * 密码是否可见 + * + */ + @Input() passwordVisible: boolean = false; + /** + * 密码可见/不可见状态改变时触发的回调 + * + */ + @Output() readonly passwordVisibleChange: EventEmitter = new EventEmitter(); + /** + * 点击清除按钮触发的回调,一般用于文本清除场景 + */ + @Output() readonly clear: EventEmitter = new EventEmitter(); + /** + * @ignore + * + * autoComplete 也使用了该变量 + */ + public isClearActive: boolean; + /** + * @ignore + * + * autoComplete 也使用了该变量 + */ + public isShowClear: boolean; + /** + * @ignore + */ + public isNoBorder: boolean; + /** + * @ignore + * + * 密码类型是否显示眼睛 + */ + public showEye: boolean = false; + protected versionInfo: string = super.getVersion(packageInfo); + private isFirstFocus: boolean = true; // 是否为第一次聚焦 + private hasClear: boolean = false; // 是否有清除功能 + private pwdHackEle: HTMLInputElement; // 当类型为password时,动态生成放在password文本框前的input元素 + private lastIsHover: boolean = undefined; // 最后一次绘制叉号的状态。 + private lastDisabled: boolean = undefined; // 最后一次叉号是否禁用。 + constructor( + elementRef: ElementRef, + renderer: Renderer2, + private tiRenderer: TiRenderer, + @Optional() private formControl: NgControl, + private zone: NgZone, + private changeDetectorRef: ChangeDetectorRef + ) { + super(elementRef, renderer); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + if (changes['passwordVisible'] && !changes['passwordVisible'].firstChange) { + if (this.showEye && !this.isClearActive) { + const isHover: boolean = this.nativeElement.disabled ? null : false; + this.lastIsHover = undefined; + this.controlEyeShow(isHover); + } + } + } + + ngOnInit(): void { + super.ngOnInit(); + this.showEye = this.nativeElement.type === 'password' && !this.nativeElement.hasAttribute('noeye'); + this.hasClear = this.nativeElement.hasAttribute('clearable') && !this.showEye; + this.isNoBorder = this.nativeElement.hasAttribute('noborder'); + + // 初始化注册事件 + this.initFn(); + this.handlePassword(); + + // Chrome下输入框有时出现自动联想(该问题出现无规律可寻,比较奇怪), + // 将其autocomplete置为off可规避此问题 + // 之后firefox及IE11也出现类似问题,所以不区分浏览器差异,统一添加这层处理 + if (this.nativeElement.type === 'text' && this.nativeElement.autocomplete !== 'on') { + this.renderer.setAttribute(this.nativeElement, 'autocomplete', 'off'); + } + + // 由于在valueChanges中监听不到动态表单中文本框的初始值 + // 所以此处对初始值要做单独处理 + this.controlClearShow(this.nativeElement.value); + + // 文本框的值发生变化时触发 + if (this.formControl) { + this.formControl.valueChanges.subscribe((value: any): void => { + this.controlClearShow(value); + // onpush策略 需要更新视图 + this.changeDetectorRef.markForCheck(); + }); + } + + // 初始设置eye图标 + this.controlEyeShow(); + } + ngDoCheck(): void { + const disabled: boolean = this.nativeElement.disabled; + // 与上次disabled状态相同,则返回 + if (this.lastDisabled === disabled) { + return; + } + // 叉号特殊处理,2)禁用 + if (this.isShowClear || this.showEye) { + if (disabled) { + this.setClearBackgroud(null); + this.lastDisabled = true; + } else { + // 从禁用改为非禁用 + this.setClearBackgroud(false); + this.lastDisabled = false; + } + } + } + + ngOnDestroy(): void { + // 销毁组件时需要将内部生成dom也移除 + if (this.pwdHackEle) { + this.renderer.removeChild(this.renderer.parentNode(this.pwdHackEle), this.pwdHackEle); + } + } + + /** + * @ignore + */ + public handleFocus(): void { + // IE和火狐浏览器在首次autofoucus聚焦时会将光标移到文字起始位置,用户体验不好 + // 需要手动设置光标位置到行尾 + if (this.isFirstFocus && this.nativeElement.autofocus && (TiBrowser.isFirefox() || TiBrowser.isIE())) { + this.nativeElement.setSelectionRange(this.nativeElement.value.length, this.nativeElement.value.length); + } + this.isFirstFocus = false; + } + + private initFn(): void { + this.zone.runOutsideAngular(() => { + // 避免不停触发变化检测 + this.renderer.listen(this.nativeElement, 'mousemove', this.handleMousemove); + this.renderer.listen(this.nativeElement, 'mouseout', this.handleMouseout); + this.renderer.listen(this.nativeElement, 'mousedown', this.handleMousedown); + this.renderer.listen(this.nativeElement, 'click', this.handleClick); + }); + } + + /** + * @ignore + * 鼠标移入清除按钮区域时,给宿主元素添加ti-text-clear-active样式 + */ + public handleMousemove = (event: MouseEvent): void => { + if (this.nativeElement.disabled || (!this.isShowClear && !this.showEye)) { + return; + } + + // 鼠标移入清除按钮区域时,给宿主元素添加ti-text-clear-active样式; + // 否则去掉ti-text-clear-active样式 + this.isClearActive = this.isIconField(event); + + this.isClearActive + ? this.renderer.addClass(this.nativeElement, 'ti3-text-clear-active') + : this.renderer.removeClass(this.nativeElement, 'ti3-text-clear-active'); + + // 密码类型只需要设置悬浮时手型样式 + if (this.showEye) { + return; + } + + // 叉号特殊处理,3)hover + this.isClearActive ? this.setClearBackgroud(true) : this.setClearBackgroud(false); + }; + /** + * @ignore + */ + public handleMouseout = (event: MouseEvent) => { + if (this.nativeElement.disabled || !this.isShowClear) { + this.isClearActive = false; + + return; + } + // 叉号特殊处理,3)hover + this.setClearBackgroud(false); + }; + + /** + * @ignore + * 切换密码可见不可见时,输入框需处于失焦状态 + */ + public handleMousedown = (event: MouseEvent) => { + if (this.nativeElement.disabled || !this.showEye || !this.isIconField(event)) { + return; + } + + event.preventDefault(); + + // 聚焦到可输入区域,再点击眼睛图标时,输入框应失焦 + if (document.activeElement === this.nativeElement) { + this.nativeElement.blur(); + } + }; + /** + * @ignore + * @param isHover true标示悬浮,false标示非悬浮,null标示禁用,undefinded标示不画叉号。 + * @returns + */ + setClearBackgroud(isHover: boolean): void { + // 如果跟上次一样,则返回。 + if (this.lastIsHover === isHover) { + return; + } else { + this.lastIsHover = isHover; + } + + let colorStr: string = ''; + if (Util.supportsCssVars()) { + // Chrome等支持CSSVar的浏览器,正常操作, 从CSSVar变量中取出颜色。 + let varName: string = ''; + if (isHover === false) { + varName = '--ti-common-color-icon-normal'; + } else if (isHover === true) { + varName = '--ti-common-color-icon-hover'; + } else if (isHover === null) { + varName = '--ti-common-color-icon-disabled'; + } + // 修复SSR报错:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle !== 'undefined') { + colorStr = getComputedStyle(document.documentElement).getPropertyValue(varName); + } + } else { + // IE和低版Edge不支持CSSVar的浏览器,从一个CSS样式中取出颜色。 + let styleName: string = ''; + if (isHover === false) { + styleName = 'flood-color'; + } else if (isHover === true) { + styleName = 'lighting-color'; + } else if (isHover === null) { + styleName = 'stop-color'; + } + // 修复SSR报错:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle !== 'undefined') { + colorStr = getComputedStyle(this.nativeElement).getPropertyValue(styleName); + } + } + colorStr = colorStr.trim().replace('#', '%23'); // URL编码 + let urlStr: string; + if (this.showEye) { + urlStr = this.passwordVisible + ? `url(data:image/svg+xml,%3Csvg%20version=%221.1%22%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%2232%22%20height=%2232%22%20viewBox=%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22${colorStr}%22%20d=%22M16%206.4c9.4%200%2016%206.4%2016%209.6%200%203-6.6%209.6-16%209.6-9.2%200-16-6.4-16-9.6s6.8-9.6%2016-9.6zM16%208.2c-4.2%200-7.8%203.6-7.8%207.8%200%204.4%203.4%207.8%207.8%207.8%204.2%200%207.8-3.6%207.8-7.8%200-4.4-3.6-7.8-7.8-7.8zM16%2011c2.8%200%205%202.2%205%205s-2.2%205-5%205-5-2.2-5-5c0-2.8%202.2-5%205-5z%22%3E%3C/path%3E%3C/svg%3E%0A)` + : `url(data:image/svg+xml,%3Csvg%20version=%221.1%22%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%2232%22%20height=%2232%22%3E%3Cpath%20fill%3D%22${colorStr}%22%20d=%22M1.6%202.925c0.6-0.6%201.4-0.6%201.8%200v0l25.6%2024.2c0.2%200.2%200.4%200.6%200.4%201s-0.2%200.6-0.4%201c-0.2%200.2-0.6%200.4-1%200.4s-0.8-0.2-1-0.4v0l-5-5.2c-2%200.6-4%201-6%201-9.2%200-16-6.6-16-9.8%200-1.8%202-4.6%205.4-6.6v0l-3.6-3.8c-0.6-0.4-0.6-1.2-0.2-1.8zM9%2011.725c-0.4%201-0.6%202.2-0.6%203.4%200%204.4%203.4%208%207.8%208%201.4%200%202.8-0.4%203.8-1v0l-2.4-2.4c-0.4%200.2-1%200.4-1.6%200.4-2.8%200-5-2.2-5-5%200-0.4%200-0.8%200.2-1.2v0l-2.2-2.2zM16%205.325c9.4%200%2016%206.6%2016%209.8%200%201.8-2%204.6-5.6%206.8v0l-3.4-3.2c0.6-1%200.8-2.2%200.8-3.4%200-4.4-3.4-8-7.8-8-1.4%200-2.8%200.4-4%201.2v0%200l-2-2c2-1%204-1.2%206-1.2zM16%209.925c2.8%200%205%202.2%205%205%200%200.4%200%201-0.2%201.4v0l-6.6-6c0.6-0.2%201.2-0.4%201.8-0.4z%22%3E%3C/path%3E%3C/svg%3E)`; + } else { + urlStr = `url(data:image/svg+xml,%3Csvg%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216px%22%20height%3D%2216px%22%3E%3Cpath%20fill%3D%22${colorStr}%22%20d%3D%22M12.529%2C11.469L9.061%2C8l3.469-3.469c0.294-0.294%2C0.295-0.768%2C0.001-1.062c-0.293-0.293-0.769-0.293-1.061%2C0L8%2C6.938L4.53%2C3.469c-0.292-0.293-0.767-0.293-1.061%2C0C3.177%2C3.762%2C3.177%2C4.238%2C3.471%2C4.532L6.939%2C8l-3.469%2C3.468c-0.294%2C0.293-0.294%2C0.768-0.001%2C1.062C3.616%2C12.678%2C3.81%2C12.75%2C4%2C12.75s0.384-0.072%2C0.53-0.219L8%2C9.062l3.47%2C3.469c0.147%2C0.146%2C0.339%2C0.219%2C0.53%2C0.219c0.192%2C0%2C0.384-0.072%2C0.53-0.219C12.823%2C12.236%2C12.824%2C11.762%2C12.529%2C11.469z%22%2F%3E%3C%2Fsvg%3E)`; + } + this.renderer.setStyle(this.nativeElement, 'backgroundImage', urlStr); + } + + /** + * @ignore + * 当在清除按钮区域点击时,清空输入框内容 + */ + public handleClick = (event: MouseEvent) => { + if (this.nativeElement.disabled || (!this.isShowClear && !this.showEye) || !this.isIconField(event)) { + return; + } + + if (this.isShowClear) { + this.renderer.setProperty(this.nativeElement, 'value', ''); + Util.trigger(this.nativeElement, 'input'); + this.zone.run(() => { + this.clear.emit(event); + }); + + this.nativeElement.focus(); + } + + if (this.showEye) { + this.passwordVisible = !this.passwordVisible; + this.lastIsHover = undefined; + this.controlEyeShow(); + this.zone.run(() => { + this.passwordVisibleChange.emit(this.passwordVisible); + }); + } + }; + + private handlePassword(): void { + // 密码框情况下,默认关闭autocomplete,避免浏览器弹出记住密码提示框引起的安全问题 + if (TiBrowser.isChrome() && this.nativeElement.type === 'password' && this.nativeElement.autocomplete !== 'on') { + this.pwdHackEle = this.renderer.createElement('input'); + this.tiRenderer.setAttributes(this.pwdHackEle, { + type: 'text', + tabindex: -1, + autocomplete: 'off' + }); + this.renderer.addClass(this.pwdHackEle, 'ti3-password-hack-input'); + // 插入一个input元素,阻止密码框前面的input联想用户名 + this.renderer.insertBefore(this.nativeElement.parentElement, this.pwdHackEle, this.nativeElement); + } + } + + // 控制clear是否显示,文本框为空时不显示,有内容时才显示 + private controlClearShow(value: any): void { + if (!this.hasClear) { + return; + } + const lastIsShowClear: boolean = this.isShowClear; + this.isShowClear = Util.isString(value) && value !== ''; + // 与上次状态相同,则返回 + if (this.isShowClear === lastIsShowClear) { + return; + } + + // 叉号特殊处理,1)初始化和监听是否显示 + if (this.isShowClear) { + this.setClearBackgroud(false); + } else { + this.renderer.removeStyle(this.nativeElement, 'background'); + this.lastIsHover = undefined; + } + } + + // 控制eye是否显示, + private controlEyeShow(isHover: boolean = false): void { + if (!this.showEye) { + return; + } + this.setClearBackgroud(isHover); + this.nativeElement.type = this.passwordVisible ? 'text' : 'password'; + } + + // 判断事件是否在清除按钮区域 + private isIconField(event: MouseEvent): boolean { + const iconWidth: number = this.showEye ? TiTextComponent.PASSWORD_WIDTH : TiTextComponent.CLEAR_WIDTH; + + return event.clientX - this.nativeElement.getBoundingClientRect().left > this.nativeElement.getBoundingClientRect().width - iconWidth; + } +} diff --git a/src/text/lib/src/TiTextModule.ts b/src/text/lib/src/TiTextModule.ts new file mode 100644 index 0000000..cf11ca2 --- /dev/null +++ b/src/text/lib/src/TiTextModule.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiTextComponent } from './TiTextComponent'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiMaskDirective } from './TiMaskDirective'; + +@NgModule({ + imports: [CommonModule, TiRendererModule], + exports: [TiTextComponent, TiMaskDirective], + declarations: [TiTextComponent, TiMaskDirective] +}) +export class TiTextModule {} +export { TiTextComponent } from './TiTextComponent'; +export { TiMaskDirective } from './TiMaskDirective'; diff --git a/src/text/lib/src/clear.svg b/src/text/lib/src/clear.svg new file mode 100644 index 0000000..d74f9da --- /dev/null +++ b/src/text/lib/src/clear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/text/lib/src/invisible.svg b/src/text/lib/src/invisible.svg new file mode 100644 index 0000000..5fe8764 --- /dev/null +++ b/src/text/lib/src/invisible.svg @@ -0,0 +1 @@ + diff --git a/src/text/lib/src/text.less b/src/text/lib/src/text.less new file mode 100644 index 0000000..fe78873 --- /dev/null +++ b/src/text/lib/src/text.less @@ -0,0 +1,101 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-text-clear-width: 26px; // 在ts中判断是否在清除按钮区域时需要此参数 + --ti-text-password-width: 36px; // 在ts中判断是否在眼睛按钮区域时需要此参数 +} + +// 注意:backgroud url里的svg fill填充颜色使用CSS var不生效。所以改用js监听hover和禁用,设置backgroud样式 +:host[tiText] { + border-radius: var(--ti-common-border-radius-normal); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + height: var(--ti-common-size-7x); + line-height: normal; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + color: var(--ti-common-color-text-primary); + background-color: var(--ti-common-color-bg-white-normal); + padding: 0 var(--ti-common-space-10); + .box-sizing(border-box); + outline: none; + vertical-align: middle; + /*IE下js暂时无法读取css var变量的值。所以,把这三个var存放在不用的css style里。需要这3个变量ie、edge下可读*/ + flood-color: var(--ti-common-color-icon-normal); + lighting-color: var(--ti-common-color-icon-hover); + stop-color: var(--ti-common-color-icon-disabled); + + &.ti3-text-input-show-icon { + background-repeat: no-repeat; + background-size: var(--ti-common-size-4x); + } + + &.ti3-text-input-show-clear { + padding-right: var(--ti-text-clear-width) !important; + background-position: center right 5px; + } + + &.ti3-text-input-show-password { + padding-right: var(--ti-text-password-width) !important; + background-position: center right 10px; + } + + &.ti3-text-input-noborder { + outline: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} + +:host[tiText]:not([disabled]):not([noborder]) { + &.ti3-text-input-show-icon { + &.ti3-text-clear-active:hover { + cursor: pointer; + } + } + + &:hover { + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-hover); + } + + &:focus { + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-active); + } +} + +:host[tiText][disabled] { + &:disabled { + background-color: var(--ti-common-color-bg-disabled); + color: var(--ti-common-color-text-disabled); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-disabled); //FireFox浏览器,初始设置focused为true时,disable状态有蓝框 + cursor: not-allowed !important; + &.ti3-text-input-noborder { + border: 0; + } + } +} + +.ti3-password-hack-input { + width: 0; + height: 0; + position: absolute; + top: -9999px; + left: -9999px; +} + +/***********************************动效************************************/ +:host[tiText] { + .form-border-animat-init(); + &.ti3-text-input-show-icon { + &:focus { + // 添加focus状态下的动画 + transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1), font-size 0.15s cubic-bezier(0.4, 0, 0.2, 1); + } + } + + //添加hover状态下的动画效果 + &:hover:not(:focus) { + //默认状态下的hover动画 + .form-border-animat-enter(); + } +} diff --git a/src/text/lib/src/visible.svg b/src/text/lib/src/visible.svg new file mode 100644 index 0000000..a7af23c --- /dev/null +++ b/src/text/lib/src/visible.svg @@ -0,0 +1 @@ + diff --git a/src/textarea/demo/karma.conf.js b/src/textarea/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/textarea/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/textarea/demo/project.json b/src/textarea/demo/project.json new file mode 100644 index 0000000..001c1ce --- /dev/null +++ b/src/textarea/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/textarea/demo", + "sourceRoot": "src/textarea/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/textarea", + "index": "src/textarea/demo/src/index.html", + "main": "src/textarea/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/textarea/demo/tsconfig.app.json", + "assets": ["src/textarea/demo/src/favicon.ico", "src/textarea/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "textarea-demo:build:production" + }, + "development": { + "browserTarget": "textarea-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js textarea" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/textarea/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/textarea/demo/tsconfig.spec.json", + "karmaConfig": "src/textarea/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/textarea/demo/src/app/AppComponent.ts b/src/textarea/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/textarea/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/textarea/demo/src/app/AppModule.ts b/src/textarea/demo/src/app/AppModule.ts new file mode 100644 index 0000000..50549f7 --- /dev/null +++ b/src/textarea/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TextareaTestModule } from './textarea/TextareaTestModule'; + +@NgModule({ + imports: [ + TextareaTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/textarea/demo/src/app/IndexComponent.ts b/src/textarea/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..738bd16 --- /dev/null +++ b/src/textarea/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TextareaTestModule } from './textarea/TextareaTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TextareaTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/textarea/demo/src/app/app.html b/src/textarea/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/textarea/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/textarea/demo/src/app/textarea/TextareaAutofocusComponent.ts b/src/textarea/demo/src/app/textarea/TextareaAutofocusComponent.ts new file mode 100644 index 0000000..c2964c9 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaAutofocusComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './textarea-autofocus.html' +}) +export class TextareaAutofocusComponent { + value: string = '解落三秋叶,能开二月花。'; + placeholder: string = '欢迎使用Tiny UI'; +} diff --git a/src/textarea/demo/src/app/textarea/TextareaDisabledComponent.ts b/src/textarea/demo/src/app/textarea/TextareaDisabledComponent.ts new file mode 100644 index 0000000..389dd8d --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaDisabledComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './textarea-disabled.html' +}) +export class TextareaDisabledComponent { + value: string = '解落三秋叶,能开二月花。'; + disabled: boolean = true; +} diff --git a/src/textarea/demo/src/app/textarea/TextareaMaxlengthComponent.ts b/src/textarea/demo/src/app/textarea/TextareaMaxlengthComponent.ts new file mode 100644 index 0000000..4637788 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaMaxlengthComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; +@Component({ + templateUrl: './textarea-maxlength.html' +}) +export class TextareaMaxlengthComponent { + value: string = '解落三秋叶'; + placeholder: string = '欢迎使用Tiny UI'; + aaControl: any = new FormControl(''); +} diff --git a/src/textarea/demo/src/app/textarea/TextareaNoneComponent.ts b/src/textarea/demo/src/app/textarea/TextareaNoneComponent.ts new file mode 100644 index 0000000..550f08e --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaNoneComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './textarea-none.html' +}) +export class TextareaNoneComponent { + value: string = '123'; + show: boolean = false; + changeShow = (): void => { + this.show = !this.show; + }; +} diff --git a/src/textarea/demo/src/app/textarea/TextareaResizeComponent.ts b/src/textarea/demo/src/app/textarea/TextareaResizeComponent.ts new file mode 100644 index 0000000..3b55491 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaResizeComponent.ts @@ -0,0 +1,11 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './textarea-resize.html', // 指定组件模板 + encapsulation: ViewEncapsulation.None +}) +export class TextareaResizeComponent { + value: string = ''; + value1: string = ''; + value2: string = ''; +} diff --git a/src/textarea/demo/src/app/textarea/TextareaScrollComponent.ts b/src/textarea/demo/src/app/textarea/TextareaScrollComponent.ts new file mode 100644 index 0000000..f127cfa --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaScrollComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './textarea-scroll.html' +}) +export class TextareaScrollComponent { + items: Array = new Array(20); + enableWhiteList: boolean = false; + item1: any = { + show: true, + label: '姓名:', + required: true, + value: '' + }; + item2: any = { + label: '输入框:', + required: true, + value: '' + }; +} diff --git a/src/textarea/demo/src/app/textarea/TextareaTestModule.ts b/src/textarea/demo/src/app/textarea/TextareaTestModule.ts new file mode 100644 index 0000000..d451011 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaTestModule.ts @@ -0,0 +1,63 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiFormfieldModule, TiSwitchModule, TiTextareaModule, TiValidationModule } from '@opentiny/ng'; + +import { TextareaResizeComponent } from './TextareaResizeComponent'; +import { TextareaMaxlengthComponent } from './TextareaMaxlengthComponent'; +import { TextareaAutofocusComponent } from './TextareaAutofocusComponent'; +import { TextareaDisabledComponent } from './TextareaDisabledComponent'; +import { TextareaValidComponent } from './TextareaValidComponent'; +import { TextareaNoneComponent } from './TextareaNoneComponent'; +import { TextareaWidthComponent } from './TextareaWidthComponent'; +import { TextareaScrollComponent } from './TextareaScrollComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiTextareaModule, + TiValidationModule, + TiFormfieldModule, + TiSwitchModule, + RouterModule.forChild(TextareaTestModule.ROUTES) + ], + declarations: [ + TextareaResizeComponent, + TextareaMaxlengthComponent, + TextareaAutofocusComponent, + TextareaDisabledComponent, + TextareaValidComponent, + TextareaWidthComponent, + TextareaNoneComponent, + TextareaScrollComponent + ] +}) +export class TextareaTestModule { + static readonly LINKS: Array = [{ href: 'components/TiTextareaComponent.html', label: 'Textarea' }]; + static readonly ROUTES: Routes = [ + { + path: 'textarea/textarea-resize', + component: TextareaResizeComponent + }, + { + path: 'textarea/textarea-maxlength', + component: TextareaMaxlengthComponent + }, + { + path: 'textarea/textarea-autofocus', + component: TextareaAutofocusComponent + }, + { + path: 'textarea/textarea-disabled', + component: TextareaDisabledComponent + }, + { path: 'textarea/textarea-width', component: TextareaWidthComponent }, + { path: 'textarea/textarea-valid', component: TextareaValidComponent }, + { path: 'textarea/textarea-none', component: TextareaNoneComponent }, + { path: 'textarea/textarea-scroll', component: TextareaScrollComponent } + ]; +} diff --git a/src/textarea/demo/src/app/textarea/TextareaValidComponent.ts b/src/textarea/demo/src/app/textarea/TextareaValidComponent.ts new file mode 100644 index 0000000..ae67eaa --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaValidComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './textarea-valid.html' +}) +export class TextareaValidComponent { + value: string = ''; + value1: string = '123'; + placeholder: string = '欢迎使用Tiny UI'; + // 使用TiValidation定义接口类型 + validation: TiValidationConfig = { + type: 'blur' + }; +} diff --git a/src/textarea/demo/src/app/textarea/TextareaWidthComponent.ts b/src/textarea/demo/src/app/textarea/TextareaWidthComponent.ts new file mode 100644 index 0000000..1f1306f --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaWidthComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './textarea-width.html' +}) +export class TextareaWidthComponent { + value: string = ''; + placeholder: string = '欢迎使用Tiny UI'; +} diff --git a/src/textarea/demo/src/app/textarea/textarea-autofocus.html b/src/textarea/demo/src/app/textarea/textarea-autofocus.html new file mode 100644 index 0000000..daea912 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-autofocus.html @@ -0,0 +1,11 @@ + +
    +
    Current value: {{ value }}
    +
    diff --git a/src/textarea/demo/src/app/textarea/textarea-disabled.html b/src/textarea/demo/src/app/textarea/textarea-disabled.html new file mode 100644 index 0000000..847b301 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-disabled.html @@ -0,0 +1,7 @@ + diff --git a/src/textarea/demo/src/app/textarea/textarea-maxlength.html b/src/textarea/demo/src/app/textarea/textarea-maxlength.html new file mode 100644 index 0000000..fa91014 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-maxlength.html @@ -0,0 +1,8 @@ + diff --git a/src/textarea/demo/src/app/textarea/textarea-none.html b/src/textarea/demo/src/app/textarea/textarea-none.html new file mode 100644 index 0000000..4d4198e --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-none.html @@ -0,0 +1,18 @@ +

    1.描述

    +

    场景测试:祖先元素初始display为none时,组件设置class样式类不生效问题

    +

    2.示例

    +

    1.通过class设置样式

    +
    + +
    +

    2.通过style设置样式

    +
    + +
    + + diff --git a/src/textarea/demo/src/app/textarea/textarea-resize.html b/src/textarea/demo/src/app/textarea/textarea-resize.html new file mode 100644 index 0000000..ed7cc57 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-resize.html @@ -0,0 +1,5 @@ + +
    + +
    + diff --git a/src/textarea/demo/src/app/textarea/textarea-scroll.html b/src/textarea/demo/src/app/textarea/textarea-scroll.html new file mode 100644 index 0000000..f438f3e --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-scroll.html @@ -0,0 +1,19 @@ +

    1.描述

    +

    + 场景测试:在有textarea的页面,且textarea的宿主元素初始display:none, + 页面有滚动条时,将页面滚动条拖至formfiled的头部不在视野范围内时,textarea动态显示时页面滚动条会异常跳动到顶部。 +

    +

    2.示例

    + + + item1 + + + ewdfwew + + + + wqwqw + + + diff --git a/src/textarea/demo/src/app/textarea/textarea-valid.html b/src/textarea/demo/src/app/textarea/textarea-valid.html new file mode 100644 index 0000000..ba2ee64 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-valid.html @@ -0,0 +1,8 @@ +

    1.描述

    +

    校验

    +

    2.示例

    +

    (2.1)blur校验

    + + +

    (2.2)及时校验

    + diff --git a/src/textarea/demo/src/app/textarea/textarea-width.html b/src/textarea/demo/src/app/textarea/textarea-width.html new file mode 100644 index 0000000..255e13e --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-width.html @@ -0,0 +1,7 @@ +

    设置宽度自适应

    + +

    + + +

    + diff --git a/src/textarea/demo/src/app/textarea/webdoc/textarea-demos.js b/src/textarea/demo/src/app/textarea/webdoc/textarea-demos.js new file mode 100644 index 0000000..e333a11 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/webdoc/textarea-demos.js @@ -0,0 +1,54 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'textarea-autofocus', + name: { + 'zh-CN': '自动聚焦', + 'en-US': 'autofocus', + }, + desc: { + 'zh-CN': + '

    通过属性autofocus配置页面重新加载时是否自动聚焦。

    ', + 'en-US': '

    autofocus

    ', + }, + }, + { + demoId: 'textarea-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'textarea disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': '

    disabled

    ', + }, + }, + { + demoId: 'textarea-maxlength', + name: { + 'zh-CN': '限制可输入字数', + 'en-US': 'maxlength', + }, + desc: { + 'zh-CN': + '

    通过属性maxlength配置文本框可输入字符的最大长度。

    ', + 'en-US': '

    maxlength

    ', + }, + }, + { + demoId: 'textarea-resize', + name: { + 'zh-CN': '调整文本框大小', + 'en-US': 'resize', + }, + desc: { + 'zh-CN': + '

    通过属性resize配置文本框调整的方向,包含none、vertical、horizontal三种类型。

    ', + 'en-US': '

    resize

    ', + }, + apis: ['TiTextareaComponent.properties.resize'], + }, + ], +}; diff --git a/src/textarea/demo/src/app/textarea/webdoc/textarea.cn.md b/src/textarea/demo/src/app/textarea/webdoc/textarea.cn.md new file mode 100644 index 0000000..83db92c --- /dev/null +++ b/src/textarea/demo/src/app/textarea/webdoc/textarea.cn.md @@ -0,0 +1,27 @@ +--- +title: Textarea 多行文本框 +--- +# Textarea 多行文本框 + +
    + +文本框组件基于原生 textarea 标签进行扩展,原生 textarea 加 tiTextarea 属性指令即为 textarea 组件。   + ++ 支持调整大小、限制输入字数、自动聚焦等场景。 + +```typescript +import { TiTextareaModule } from '@opentiny/ng'; +``` + +
    + +
    + +文本框组件基于原生 textarea 标签进行扩展,原生 textarea 加 tiTextarea 属性指令即为 textarea 组件。   + ++ 支持调整大小、限制输入字数、自动聚焦等场景。 + +```typescript +import { TiTextareaModule } from '@opentiny/ng'; +``` +
    diff --git a/src/textarea/demo/src/app/textarea/webdoc/textarea.en.md b/src/textarea/demo/src/app/textarea/webdoc/textarea.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/webdoc/textarea.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/textarea/demo/src/favicon.ico b/src/textarea/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/textarea/demo/src/index.html b/src/textarea/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/textarea/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/textarea/demo/src/main.ts b/src/textarea/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/textarea/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/textarea/demo/test.ts b/src/textarea/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/textarea/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/textarea/demo/tsconfig.app.json b/src/textarea/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/textarea/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/textarea/demo/tsconfig.spec.json b/src/textarea/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/textarea/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/textarea/lib/index.ts b/src/textarea/lib/index.ts new file mode 100644 index 0000000..1b3814a --- /dev/null +++ b/src/textarea/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTextareaModule'; diff --git a/src/textarea/lib/ng-package.json b/src/textarea/lib/ng-package.json new file mode 100644 index 0000000..4aad4e8 --- /dev/null +++ b/src/textarea/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/textarea", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/textarea/lib/package.json b/src/textarea/lib/package.json new file mode 100644 index 0000000..3e89886 --- /dev/null +++ b/src/textarea/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-textarea", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/textarea/lib/project.json b/src/textarea/lib/project.json new file mode 100644 index 0000000..78e16a6 --- /dev/null +++ b/src/textarea/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/textarea/lib", + "sourceRoot": "src/textarea/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/textarea"], + "options": { + "project": "src/textarea/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/textarea"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js textarea" + }, + { + "command": "ng default-build textarea" + }, + { + "command": "node build/clear-default-theme.js textarea" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/textarea && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build textarea && ng pack textarea && node build/publish.js textarea --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/textarea/lib/src/TiFormatNumPipe.ts b/src/textarea/lib/src/TiFormatNumPipe.ts new file mode 100644 index 0000000..78fa2ed --- /dev/null +++ b/src/textarea/lib/src/TiFormatNumPipe.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Pipe, PipeTransform } from '@angular/core'; +import { TiLocaleFormat } from '@opentiny/ng-locale'; + +/** + * @ignore + * TiFormatNumPipe 对textarea字数统计的数字进行国际化处理 + * + */ +@Pipe({ name: 'tiFormatNum' }) +export class TiFormatNumPipe implements PipeTransform { + private numberFormat: string = '1.0-0'; // 整数位保留最小位数.小数位保留最小位数-小数位最大保留位置 + transform(value: number): string { + return TiLocaleFormat.formatNumber(value, this.numberFormat); + } +} diff --git a/src/textarea/lib/src/TiTextareaComponent.ts b/src/textarea/lib/src/TiTextareaComponent.ts new file mode 100644 index 0000000..9393fc9 --- /dev/null +++ b/src/textarea/lib/src/TiTextareaComponent.ts @@ -0,0 +1,456 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + AfterViewChecked, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DoCheck, + ElementRef, + Inject, + Input, + NgZone, + OnDestroy, + OnInit, + Optional, + Renderer2, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { TiBrowser, Util } from '@opentiny/ng-utils'; +import { TiAutofocusComponent } from '@opentiny/ng-base'; +import { NgControl } from '@angular/forms'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import packageInfo from '../package.json'; +/** + * Textarea多行文本框组件 + * + * 文本框组件基于原生textarea标签进行扩展,原生textarea加tiTextarea属性指令即为textarea组件。 + * + */ +@Component({ + selector: '[tiTextarea]', // 指定组件名称 + templateUrl: './textarea.html', // 指定组件模板 + styleUrls: ['./textarea.less'], // 样式路径 + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '(focus)': 'onFocus()' + }, + encapsulation: ViewEncapsulation.None +}) +export class TiTextareaComponent extends TiAutofocusComponent implements OnInit, AfterViewChecked, AfterViewInit, DoCheck, OnDestroy { + /** + * 文本框大小,该属性提供了四个属性值: vertical(仅可调整垂直方向的大小,即调整组件的高度)、 + * horizontal(仅可调节水平方向的大小,即调整组件的宽度)、 + * both(水平和垂直方向均可调节,宽高都可调节)、 + * none(不可调整组件大小) + */ + @Input() resize: 'none' | 'vertical' | 'horizontal' | 'both' = 'both'; + /** + * @ignore + */ + @ViewChild('resize') private resizeIconRef: ElementRef; + /** + * @ignore + */ + @ViewChild('maxlength') private eleCharacterRef: ElementRef; + /** + * @ignore + */ + @ViewChild('charactersCount') private charactersCountRef: ElementRef; + /** + * @ignore + * 是否设置maxlength属性 + */ + public hasMaxlength: boolean; + /** + * @ignore + * 用户设置的最大值 + */ + public maxLength: number; + /** + * @ignore + * 当前输入内容的长度 + */ + public countLength: number; + protected versionInfo: string = super.getVersion(packageInfo); + // 用作拖动文本框大小 + private options: any = { + $target: undefined, + storeWidth: 0, + storeHeight: 0, + mouseXPosition: 0, + mouseYPosition: 0 + }; + // 默认最大最小宽高 + private defaultStyle: any = { + minWidth: 60, + minHeight: 64, + maxWidth: 1280, + maxHeight: 9999 + }; + private element: HTMLTextAreaElement; // 宿主元素 + private attrs: NamedNodeMap; // 宿主元素的属性 + private container: HTMLDivElement; // 创建div标签 + private textAreaMinWidth: number; // 最小宽度 + private textAreaMinHeight: number; // 最小高度 + private textAreaMaxWidth: number; // 最大宽度 + private textAreaMaxHeight: number; // 最大高度 + private isVisibleInit: boolean; // 标志初始时是否可见 + private isFirstFocus: boolean = true; // 是否为第一次聚焦 + private documentMouseMoveListener: () => void; + private documentMouseUpListener: () => void; + private textareaResizeObserver: any; + + constructor( + elementRef: ElementRef, + renderer: Renderer2, + private tiRenderer: TiRenderer, + private zone: NgZone, + @Optional() private formControl: NgControl, + @Inject(DOCUMENT) private document, + private changeDetectorRef: ChangeDetectorRef + ) { + super(elementRef, renderer); + this.element = elementRef.nativeElement; // 获取宿主元素 + this.attrs = this.element.attributes; // 可以根据属性名获取各个属性 + this.container = this.renderer.createElement('div'); // 创建div标签 + this.renderer.addClass(this.container, 'ti3-textarea-container'); + } + + ngOnInit(): void { + super.ngOnInit(); + // 初始化maxlength属性 + this.hasMaxlength = Util.isUndefined(this.attrs['maxlength']) ? false : true; + // 原生不设置maxlength属性时该值为-1 + this.maxLength = this.hasMaxlength ? this.element.maxLength : -1; + // 文本框的值发生变化时触发 + // 使用ngModel初始赋值时,触发valueChanges。 + // 使用formControl初始赋值时,不会触发valueChanges。 + if (this.formControl) { + this.formControl.valueChanges.subscribe((value: any) => { + this.setCountLength(); + // onpush策略,需要手动触发变更检测 + this.changeDetectorRef.markForCheck(); + }); + } + + // 使用formControl初始赋值时,初始化不会触发valueChanges,所以此处额外执行一次。 + // 使用ngModel初始赋值时,此处通过this.element.value拿到的值为空字符串,无法正确显示当前输入内容的长度 + this.setCountLength(); + } + + ngAfterViewInit(): void { + // 如果祖先元素中display为none,获取到的组件的宽度为0,元素不可见 + this.isVisibleInit = this.element.offsetWidth !== 0; + if (this.isVisibleInit) { + this.setContainerstyle(); + } + + // maxLength接口添加样式 + this.setMaxLengthFn(); + + // 处理resize功能 + this.resizeFn(); + // 上面执行完初始化dom后,调用父类的ngAfterViewInit去设置autofocus + super.ngAfterViewInit(); + if (!this.isVisibleInit && (window as any).ResizeObserver) { + // 利用 ResizeObserver 来监听宿主元素的尺寸发生改变的时机。IE不支持 ResizeObserver。 + this.textareaResizeObserver = new (window as any).ResizeObserver((entries: any): void => { + // 初始祖先元素中display为none,之后第一次由none变成可见时 + if (entries[0] && entries[0].contentRect.width !== 0) { + this.setContainerstyle(); + this.textareaResizeObserver.disconnect(); + } + }); + this.textareaResizeObserver.observe(this.element); + } + } + + ngAfterViewChecked(): void { + // 修复SSR报错:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle === 'undefined') { + return; + } + // 处理通过设置display的方式隐藏组件:内部生成的dom也需要隐藏 + if (getComputedStyle(this.element).display === 'none') { + this.renderer.setStyle(this.container, 'display', 'none'); + } else { + this.renderer.setStyle(this.container, 'display', 'block'); + } + } + // maxlength通过接口传值需触发重新判断,因为init函数中执行较早且只执行一次。 + ngDoCheck(): void { + this.hasMaxlength = Util.isUndefined(this.attrs['maxlength']) ? false : true; + this.maxLength = this.hasMaxlength ? this.element.maxLength : -1; + } + + // 销毁组件时需要将内部生成dom也移除 + ngOnDestroy(): void { + this.renderer.removeChild(this.renderer.parentNode(this.container), this.container); + if (this.documentMouseMoveListener) { + this.documentMouseMoveListener(); + } + if (this.documentMouseUpListener) { + this.documentMouseUpListener(); + } + + if (this.textareaResizeObserver) { + this.textareaResizeObserver.disconnect(); + } + } + + /** + * @ignore + */ + public onFocus(): void { + // IE和火狐浏览器在首次autofoucus聚焦时会将光标移到文字起始位置,用户体验不好 + // 需要手动设置光标位置到行尾 + if (this.isFirstFocus && (this.element as any).autofocus && (TiBrowser.isFirefox() || TiBrowser.isIE())) { + this.element.setSelectionRange(this.element.value.length, this.element.value.length); + } + this.isFirstFocus = false; + } + + private setContainerstyle(): void { + // 设置container的样式 + this.setContainerSize(); + // 将宿主元素的样式类移到container上 + this.setContainerClass(); + // 初始化创建dom + this.initFn(); + } + + private setContainerSize(): void { + // 修正SSR错误:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle === 'undefined') { + return; + } + + // 判断用户设置的width是否为百分比 + const PERCENT_REGEXP: RegExp = /^(100|[1-9]?\d(.\d\d?\d?)?)%$|0$/; + const width: string = this.element.style.width; + const isPercent: boolean = PERCENT_REGEXP.test(width); + + this.tiRenderer.setStyles(this.container, { + height: getComputedStyle(this.element).height, + width: isPercent ? width : getComputedStyle(this.element).width, + // 若服务在textarea上面设置margin外边距,会导致textarea所占的页面空间大于外框的宽高,所以要将textarea上设置的margin转移到外边框 + margin: this.element.style.margin + }); + // 取掉宿主元素的margin属性: + this.element.style.margin = ''; + } + + private setContainerClass(): void { + // 获取未经过angular解析后的属性需要在onint中处理:若服务在textarea添加class,则把其加到父容器上 + const cNames: string = this.element.className; + if (cNames) { + const cNameArr: Array = this.element.className.split(' '); // 存在多个class + for (const cName of cNameArr) { + // 跟校验有关的样式是以"ng-"开头,如果转移到外部容器上会影响校验样式 + if (!/^ng-/.test(cName)) { + this.renderer.removeClass(this.element, cName); + this.renderer.addClass(this.container, cName); + } + } + } + } + + // 初始化创建dom + private initFn(): void { + const pNode: any = this.renderer.parentNode(this.element); // 找到宿主元素父元素 + const nextNode: any = this.renderer.nextSibling(this.element); // 找到宿主元素的兄弟元素 + + // 将宿主元素包裹在this.container里边: + this.renderer.removeChild(pNode, this.element); // 从父元素中移除宿主元素 + this.renderer.appendChild(this.container, this.element); // 将宿主元素放入创建的div标签中 + this.renderer.insertBefore(pNode, this.container, nextNode); + + if (this.resize !== 'none') { + this.moveNode(this.resizeIconRef.nativeElement); + } + + if (this.hasMaxlength) { + this.moveNode(this.eleCharacterRef.nativeElement); + } + } + + private moveNode(node: Element): void { + this.renderer.removeChild(this.element, node); + this.renderer.appendChild(this.container, node); + } + + // maxLength接口 + private setMaxLengthFn(): void { + // 添加字数限制与当前输入字数显示功能 + if (this.attrs['maxlength'] && !isNaN(this.maxLength)) { + // 有maxlength限制时给外框div加上标志类 ti3-textarea-container-counter + this.renderer.addClass(this.container, 'ti3-textarea-container-counter'); + } else { + this.renderer.removeClass(this.container, 'ti3-textarea-container-counter'); + } + } + + // resize接口 + private resizeFn(): void { + if (this.resize === 'none') { + return; + } + + this.textAreaMinWidth = this.getSizeNumber('minWidth'); + this.textAreaMinHeight = this.getSizeNumber('minHeight'); + this.textAreaMaxWidth = this.getSizeNumber('maxWidth'); + this.textAreaMaxHeight = this.getSizeNumber('maxHeight'); + + // 清除textarea上的样式 + this.tiRenderer.setStyles(this.element, { + minWidth: '', + minHeight: '', + maxWidth: '', + maxHeight: '' + }); + + this.zone.runOutsideAngular(() => { + // 避免不停触发变化检测 + // resizeIconRef模板的mousedown事件 + this.renderer.listen(this.resizeIconRef.nativeElement, 'mousedown', ($event: MouseEvent) => { + this.renderer.addClass(this.element, 'ti3-textarea-resize-border'); // 拖拽时边框颜色为聚焦时 + this.options.$target = $event.target; + this.options.mouseXPosition = $event.pageX; + this.options.mouseYPosition = $event.pageY; + // 在拖动前获取当前文本框最新的高度、宽度 + this.updateTextAreaSize(); + // 给页面设置不可选样式,避免拖动时页面出现被选中的蓝色区域 + this.toggleTextSelection(true); + }); + + // resizeIconRef模板的mouseup事件 + this.renderer.listen(this.resizeIconRef.nativeElement, 'mouseup', ($event: MouseEvent) => { + this.renderer.removeClass(this.element, 'ti3-textarea-resize-border'); + }); + + this.documentMouseMoveListener = this.renderer.listen(this.document, 'mousemove', this.mouseMoveHandlerFn); + this.documentMouseUpListener = this.renderer.listen(this.document, 'mouseup', this.mouseUpHandlerFn); + }); + } + + private setCountLength(): void { + if (this.hasMaxlength) { + this.countLength = this.element.value.length; + // safari浏览器回车键是按两个字计算 + if (TiBrowser.isSafari()) { + this.countLength = this.countLength + this.element.value.split('\n').length - 1; + } + } + } + + // 拖动函数 + private mouseMove(event: MouseEvent): void { + // 阻止调整文本框大小时文本框聚焦 + event.preventDefault(); + const width: number = this.options.storeWidth; + const height: number = this.options.storeHeight; + const horizonWidth: number = event.pageX - this.options.mouseXPosition; + const verticalHeight: number = event.pageY - this.options.mouseYPosition; + + if (this.resize !== 'vertical' && width + horizonWidth >= this.textAreaMinWidth && width + horizonWidth < this.textAreaMaxWidth) { + // 更新textarea宽度 + this.options.storeWidth += horizonWidth; + // 调整width + this.renderer.setStyle(this.container, 'width', `${this.options.storeWidth}px`); + } + + if ( + this.resize !== 'horizontal' && + height + verticalHeight >= this.textAreaMinHeight && + height + verticalHeight < this.textAreaMaxHeight + ) { + // 更新textarea高度 + this.options.storeHeight += verticalHeight; + this.renderer.setStyle(this.container, 'height', `${this.options.storeHeight}px`); + } + + // 更新光标位置 + this.options.mouseXPosition = event.pageX; + this.options.mouseYPosition = event.pageY; + } + + private stopResize(): void { + this.options.mouseXPosition = 0; + this.options.mouseYPosition = 0; + this.updateTextAreaSize(); + this.options.$target = null; + } + + // 获取min-width、min-height等的样式值 + private getSizeNumber(value: string): number { + const val: string = this.element.style[value]; // 可实现获取样式 + + if (val !== 'none' && val !== '') { + return parseFloat(val.replace(/px/, '')); + } + + return this.defaultStyle[value]; + } + + // 在拖动前获取当前文本框最新的高度、宽度 + private updateTextAreaSize(): void { + this.options.storeWidth = this.container.clientWidth; + this.options.storeHeight = this.container.clientHeight; + } + + // 给页面设置不可选样式,避免拖动时页面出现被选中的蓝色区域 + private toggleTextSelection(toggle: boolean): void { + const body: any = this.document.getElementsByTagName('body'); + if (toggle) { + this.renderer.addClass(body[0], 'ti3-unselectable'); + this.renderer.setAttribute(body[0], 'unselectable', 'on'); + } else { + this.renderer.removeClass(body[0], 'ti3-unselectable'); + this.renderer.removeAttribute(body[0], 'unselectable'); + } + } + + /** + * @ignore + * 绑定在document上的mouseMove事件 + */ + public mouseMoveHandlerFn = ($event: MouseEvent): void => { + // 拖动的动作应该是先mousedown,然后mousemove,因此先判断是否已经触发了mousedown + if ((this.options.mouseXPosition === 0 && this.options.mouseYPosition === 0) || !this.options.$target) { + return; + } + this.mouseMove($event); + }; + /** + * @ignore + * 绑定在document上的mouseUp事件 + */ + public mouseUpHandlerFn = (): void => { + // 解决问题:当拖动鼠标时,鼠标不在小三角上时,鼠标放开不会移除样式 + // 为确保鼠标弹起时, textarea的边框样式恢复默认: + if (this.isResizing()) { + this.renderer.removeClass(this.element, 'ti3-textarea-resize-border'); + } + try { + this.stopResize(); + this.toggleTextSelection(false); + } catch (e) {} + }; + + private isResizing(): boolean { + return this.element.className.indexOf('ti3-textarea-resize-border') > -1; + } +} diff --git a/src/textarea/lib/src/TiTextareaModule.ts b/src/textarea/lib/src/TiTextareaModule.ts new file mode 100644 index 0000000..e31949a --- /dev/null +++ b/src/textarea/lib/src/TiTextareaModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiTextareaComponent } from './TiTextareaComponent'; +import { TiFormatNumPipe } from './TiFormatNumPipe'; +import { TiRendererModule } from '@opentiny/ng-renderer'; + +@NgModule({ + imports: [CommonModule, TiRendererModule], + exports: [TiTextareaComponent], + declarations: [TiTextareaComponent, TiFormatNumPipe] +}) +export class TiTextareaModule {} +export { TiTextareaComponent } from './TiTextareaComponent'; diff --git a/src/textarea/lib/src/textarea.html b/src/textarea/lib/src/textarea.html new file mode 100644 index 0000000..698ad92 --- /dev/null +++ b/src/textarea/lib/src/textarea.html @@ -0,0 +1,5 @@ + + + {{countLength | tiFormatNum}}/{{maxLength | + tiFormatNum}} diff --git a/src/textarea/lib/src/textarea.less b/src/textarea/lib/src/textarea.less new file mode 100644 index 0000000..0c4f6ea --- /dev/null +++ b/src/textarea/lib/src/textarea.less @@ -0,0 +1,144 @@ +@import "../../../themes/basic/base-all.less"; + +.ti3-textarea-container { + --ti-textarea-counter-height: var(--ti-common-size-4x); + --ti-textarea-counter-margin-top: 2px; +} + +@textarea-name: tiTextarea; + +.textarea-resize-slash(@color, @size, @position) { + content: ""; + width: @size; + height: @size; + display: block; + border-bottom: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) @color; + transform: rotate(135deg); + bottom: @position; + right: @position; + position: absolute; +} +.textarea-resize-color(@color) { + &:before{ + border-bottom-color: @color; + } + &:after { + border-bottom-color: @color; + } +} +.textarea-color(@color) { + border-color: @color; + & ~ .ti3-textarea-resize-icon { + .textarea-resize-color(@color); + } +} + +//处理校验失败时textarea右下角三角颜色问题 +textarea{ + @{tiny-invalid-class}{ + &~ .ti3-textarea-resize-icon{ + &:before{ + border-bottom-color: var(--ti-common-color-error-border) !important; + } + &:after { + border-bottom-color: var(--ti-common-color-error-border) !important; + } + } + } +} + +.ti3-textarea-container { + min-height: var(--ti-common-size-16x); + position: relative; + display: inline-block; + overflow: hidden; + vertical-align: middle; + line-height: normal; + + & > textarea[@{textarea-name}] { + .form-border-animat-init(); + overflow-x: hidden; + width: 100% !important; + height: 100% !important; + resize: none !important; + border-radius: var(--ti-common-border-radius-normal); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + color: var(--ti-common-color-text-primary); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + background-color: var(--ti-common-color-bg-white-normal); + padding: var(--ti-common-space-6) var(--ti-common-space-10); + .box-sizing(border-box); + outline: none; + vertical-align: middle; + &:disabled { + background-color: var(--ti-common-color-bg-disabled); + color: var(--ti-common-color-text-disabled); + border-color: var(--ti-common-color-line-disabled); + cursor: not-allowed !important; + } + + &:not(:disabled) { + &:hover { + .textarea-color(var(--ti-common-color-line-hover)); + .form-border-animat-enter(); + } + &:focus{ + .textarea-color(var(--ti-common-color-line-active)); + .form-border-animat-enter(); + } + &.ti3-textarea-resize-border {// textareaa拉伸时,边框和小三角颜色的设置 + .textarea-color(var(--ti-common-color-line-active)); + .form-border-animat-enter(); + } + } + } + + & > .ti3-textarea-resize-icon { + position: absolute; + bottom: 1px; + right: 1px; + width: 10px; + height: 10px; + cursor: nw-resize; + &:before { + .textarea-resize-slash(var(--ti-common-color-line-normal), 8.4px, -2px); + } + &:after { + .textarea-resize-slash(var(--ti-common-color-line-normal), 4.2px, 0px); + } + } + + & > .ti3-textarea-counter { + height: var(--ti-textarea-counter-height); + line-height: var(--ti-textarea-counter-height); + display: block; + .box-sizing(border-box); + position: absolute; + right: 0; + margin-top: var(--ti-textarea-counter-margin-top); + text-align: right; + color: var(--ti-common-color-text-weaken); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + background-color: var(--ti-common-color-bg-white-normal); + & > .ti3-textarea-counter-count { + color:var(--ti-common-color-text-primary); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + } + } + + &.ti3-textarea-container-counter { + & > textarea[@{textarea-name}] { + height: calc(100% - var(--ti-textarea-counter-height) - var(--ti-textarea-counter-margin-top)) !important; + } + & > .ti3-textarea-resize-icon { + bottom: calc(var(--ti-textarea-counter-height) + var(--ti-textarea-counter-margin-top)); + } + } +} + +.ti3-unselectable { + .user-select(none); +} \ No newline at end of file diff --git a/src/themes/README.md b/src/themes/README.md new file mode 100644 index 0000000..a019c0b --- /dev/null +++ b/src/themes/README.md @@ -0,0 +1,30 @@ +### 样式开发规范: + +1. 变量开发部分 + +【使用变量目的】 + +为了方便组件整体换肤和扩展,使用CSS变量进行了公共变量提取 + +【开发说明】 + +**基础变量开发部分:** + +基础变量开发包含以下两个文件 + +* `themes/basic/basic-var.less`:文件中定义了所有可变的视觉变量 + + **注意点:** + + 1)颜色变量定义部分,区分了基础色和场景色 + + 基础色:用于描述用到的所有颜色变量,以 ti-base- 开头命名,该变量仅在场景色中使用,组件中不可使用 + + 场景色:用于描述组件使用场景中使用到的颜色变量,以 ti-common- 开头命名,具体组件中使用 + + 2)除非组件特殊宽高、组件交互样式属性外,组件中使用到的所有变量均需引用该文件中的变量 + +**样式开发部分:** + +1. 字体图标中的图标样式类应该只包含原始的图标编码,不要加其他样式属性 + \ No newline at end of file diff --git a/src/themes/basic/base-all.less b/src/themes/basic/base-all.less new file mode 100644 index 0000000..14d614f --- /dev/null +++ b/src/themes/basic/base-all.less @@ -0,0 +1,6 @@ +// 供组件引用的基础less变量和mixin函数。经过less编译后,零冗余。 + +// 公共样式 +@import 'common.less'; +// 混合 +@import 'mixins.less'; diff --git a/src/themes/basic/basic-var.css b/src/themes/basic/basic-var.css new file mode 100644 index 0000000..44b9f7c --- /dev/null +++ b/src/themes/basic/basic-var.css @@ -0,0 +1,346 @@ +/*基础变量定义*/ +:root { + /* 一.颜色*/ + /* 1.基础色:此处的颜色变量为所有组件应用到的颜色,此处颜色仅在公共色中使用,具体组件不可使用*/ + --ti-base-color-white: #FFFFFF; /* 纯白*/ + + /* 1.1品牌色*/ + /* 品牌主色*/ + --ti-base-color-brand-6: #5E7CE0; /* 主色蓝*/ + /* 主色衍生色*/ + --ti-base-color-brand-8: #344899; /* 品牌色-8*/ + --ti-base-color-brand-7: #526ECC; /* 品牌色-7*/ + --ti-base-color-brand-5: #7693F5; /* 品牌色-5*/ + --ti-base-color-brand-4: #96ADFA; /* 品牌色-4*/ + --ti-base-color-brand-3: #BECCFA; /* 品牌色-3*/ + --ti-base-color-brand-2: #E9EDFA; /* 品牌色-2*/ + --ti-base-color-brand-1: #F2F5FC; /* 品牌色-1*/ + + /* 1.2中立色*/ + /* 公用灰色系,用于文本、图标、线条、背景色*/ + --ti-base-color-common-9: #181818; /* 中立色-9*/ + --ti-base-color-common-8: #282B33; /* 中立色-8*/ + --ti-base-color-common-7: #252B3A; /* 中立色-7*/ + --ti-base-color-common-6: #464C59; /* 中立色-6*/ + --ti-base-color-common-5: #575D6C; /* 中立色-5*/ + --ti-base-color-common-4: #5C6173; /* 中立色-4*/ + --ti-base-color-common-3: #8A8E99; /* 中立色-3*/ + --ti-base-color-common-2: #ADB0B8; /* 中立色-2*/ + --ti-base-color-common-1: #DFE1E6; /* 中立色-1*/ + + /* 1.3背景色*/ + --ti-base-color-bg-9: #B12220; /* 背景-9*/ + --ti-base-color-bg-8: #C7000B; /* 背景-8*/ + --ti-base-color-bg-7: #D64A52; /* 背景-7*/ + --ti-base-color-bg-6: #EEF0F5; /* 背景-6*/ + --ti-base-color-bg-5: #F5F5F6; /* 背景-5*/ + --ti-base-color-bg-4: #FAFAFA; /* 背景-4*/ + --ti-base-color-bg-3: #FFFFFF; /* 背景-3*/ + --ti-base-color-bg-2: #FFFFFF; /* 背景-2*/ + --ti-base-color-bg-1: #FFFFFF; /* 背景-1*/ + + /* 1.4功能色*/ + --ti-base-color-error-4: #DE504E; /* 错误-4*/ + --ti-base-color-error-3: #F66F6A; /* 错误-3*/ + --ti-base-color-error-2: #FFBCBA; /* 错误-2*/ + --ti-base-color-error-1:#FFEEED; /* 错误-1*/ + + --ti-base-color-success-4: #3AC295; /* 成功-4*/ + --ti-base-color-success-3: #50D4AB; /* 成功-3*/ + --ti-base-color-success-2:#ACF2DC; /* 成功-2*/ + --ti-base-color-success-1:#EDFFF9; /* 成功-1*/ + + --ti-base-color-warn-5: #E37D29; /* 告警-5*/ + --ti-base-color-warn-4: #FA9841; /* 告警-4*/ + --ti-base-color-warn-3:#FAC20A; /* 告警-3*/ + --ti-base-color-warn-2: #FFD0A6; /* 告警-2*/ + --ti-base-color-warn-1:#FFF3E8; /* 告警-1*/ + + --ti-base-color-prompt-4: var(--ti-base-color-brand-7); /* 提示-4*/ + --ti-base-color-prompt-3: var(--ti-base-color-brand-6); /* 提示-3*/ + --ti-base-color-prompt-2: var(--ti-base-color-brand-3); /* 提示-2*/ + --ti-base-color-prompt-1: #EBF6FF; /* 提示-1*/ + + --ti-base-color-prompt-icon-from: #7769E8; /* 渐变图标-提示-起始色*/ + --ti-base-color-prompt-icon-to: #58BBFF; /* 渐变图标-提示-终止色*/ + + /* 状态图标色*/ + --ti-base-color-icon-info:#6CBFFF; /* 状态图标-常规*/ + + /* 图表色*/ + --ti-base-color-data-3:#A6DD82; /* 图表数据色-3*/ + --ti-base-color-data-4:#F3689A; /* 图表数据色-4*/ + --ti-base-color-data-5:#A97AF8; /* 图表数据色-5*/ + + /* 透明色*/ + --ti-base-color-transparent: transparent; + + /* 2.公共色:此处颜色为组件场景色,根据使用场景分为以下几大类,具体组件引用以下颜色,如在使用过程中有问题,请自行按类别添加*/ + /* 2.1提示类型颜色,用于alert组件、涉及功能提示的背景、文字、图标等的颜色使用*/ + --ti-common-color-success: var(--ti-base-color-success-3); /* 成功-图标色/状态图标-成功*/ + --ti-common-color-text-success: var(--ti-base-color-success-4); /* 成功-文字色*/ + --ti-common-color-success-bg: var(--ti-base-color-success-1); /* 成功-背景色*/ + --ti-common-color-success-border: var(--ti-base-color-success-2); /* 成功-边框色*/ + + --ti-common-color-error: var(--ti-base-color-error-3); /* 错误-图标色/状态图标-危险、错误、失败/深色Tip中的价格文本*/ + --ti-common-color-error-text: var(--ti-base-color-error-4); /* 错误-文字色*/ + --ti-common-color-error-bg: var(--ti-base-color-error-1); /* 错误-背景色/校验背景色*/ + --ti-common-color-error-border: var(--ti-base-color-error-3); /* 错误-校验边框色*/ + --ti-common-color-error-border-secondary: var(--ti-base-color-error-2); /* 错误-alert边框色*/ + + --ti-common-color-warn: var(--ti-base-color-warn-4); /* 告警-图标色/状态图标-警告*/ + --ti-common-color-warn-text: var(--ti-base-color-warn-5); /* 告警-文字色*/ + --ti-common-color-warn-bg: var(--ti-base-color-warn-1); /* 告警-背景色*/ + --ti-common-color-warn-border: var(--ti-base-color-warn-2); /* 告警-边框色*/ + --ti-common-color-warn-secondary: var(--ti-base-color-warn-3); /* 次要告警-图标色/状态图标-异常*/ + + --ti-common-color-prompt: var(--ti-base-color-prompt-3); /* 提示-图标色*/ + --ti-common-color-prompt-text: var(--ti-base-color-prompt-4); /* 提示-图标色*/ + --ti-common-color-prompt-bg: var(--ti-base-color-prompt-1); /* 提示-背景色*/ + --ti-common-color-prompt-border: var(--ti-base-color-prompt-2); /* 提示-边框色*/ + --ti-common-color-prompt-icon-from:var(--ti-base-color-prompt-icon-from); /* 渐变图标-提示-起始色*/ + --ti-common-color-prompt-icon-to:var(--ti-base-color-prompt-icon-to); /* 渐变图标-提示-终止色*/ + + /* 2.2文本色*/ + --ti-common-color-text-primary: var(--ti-base-color-common-7); /* 一级文本色-重要信息/标题颜色/输入类文本颜色*/ + --ti-common-color-text-secondary: var(--ti-base-color-common-5); /* 二级文本色-次要信息*/ + --ti-common-color-text-weaken: var(--ti-base-color-common-3); /* 三级文本色-弱化信息/说明文字*/ + --ti-common-color-text-disabled: var(--ti-base-color-common-2); /* 文本禁用信息*/ + --ti-common-color-text-darkbg: var(--ti-base-color-common-2); /* 深色背景下文本信息*/ + --ti-common-color-text-darkbg-disabled: var(--ti-base-color-common-5); /* 深色背景下文本信息禁用色*/ + --ti-common-color-text-link: var(--ti-base-color-brand-7); /* 链接色*/ + --ti-common-color-text-link-hover: var(--ti-base-color-brand-8); /* 链接悬浮色*/ + --ti-common-color-text-link-darkbg: var(--ti-base-color-brand-4); /* 深色背景链接色*/ + --ti-common-color-text-link-darkbg-hover: var(--ti-base-color-brand-3); /* 深色背景链接悬浮色*/ + --ti-common-color-text-highlight: var(--ti-base-color-brand-7); /* 文本高亮色*/ + --ti-common-color-text-white: var(--ti-base-color-white); /* 深色背景或图标上文字色*/ + --ti-common-color-text-gray: var(--ti-base-color-white); /* 深色背景下的文本色,用于tip*/ + --ti-common-color-text-gray-disabled: var(--ti-base-color-common-4); /* 深色背景下的灰色文本禁用色,用于tab页签中*/ + --ti-common-color-text-important: var(--ti-base-color-error-4); /* 文本_金额*/ + + /* 2.3图标色*/ + /* 浅底背景图标色*/ + --ti-common-color-icon-normal: var(--ti-base-color-common-5); + --ti-common-color-icon-hover: var(--ti-base-color-brand-6); + --ti-common-color-icon-active: var(--ti-base-color-brand-6); + --ti-common-color-icon-disabled: var(--ti-base-color-common-2); /* 图标禁用色/状态图标-禁用、停止*/ + --ti-common-color-icon-white: var(--ti-base-color-white); + + /* 灰色背景下图标色*/ + --ti-common-color-icon-graybg-normal: var(--ti-base-color-common-2); + --ti-common-color-icon-graybg-hover: var(--ti-base-color-brand-6); + --ti-common-color-icon-graybg-active: var(--ti-base-color-brand-6); + --ti-common-color-icon-graybg-disabled: var(--ti-base-color-common-1); + + /* 深底背景图标色*/ + --ti-common-color-icon-darkbg-normal: var(--ti-base-color-common-2); + --ti-common-color-icon-darkbg-hover: var(--ti-base-color-brand-5); + --ti-common-color-icon-darkbg-active: var(--ti-base-color-brand-5); + --ti-common-color-icon-darkbg-disabled: var(--ti-base-color-common-5); + + /* 状态图标背景色*/ + --ti-common-color-icon-info: var(--ti-base-color-icon-info); /* 状态图标-常规、信息提示*/ + + /* 2.4线颜色,用于边框,线条等的颜色使用*/ + --ti-common-color-line-normal: var(--ti-base-color-common-2); + --ti-common-color-line-hover: var(--ti-base-color-common-5); + --ti-common-color-line-active: var(--ti-base-color-brand-6); + --ti-common-color-line-disabled: var(--ti-base-color-common-1); + /* 分割线颜色*/ + --ti-common-color-line-dividing: var(--ti-base-color-common-1); + /* 虚线*/ + --ti-common-color-dash-line-normal: var(--ti-base-color-common-5); + --ti-common-color-dash-line-hover: var(--ti-base-color-brand-7); + + /* 2.5背景色*/ + /* 背景基础色各状态色*/ + --ti-common-color-bg-normal: var(--ti-base-color-bg-6); /* 通用背景-页面背景色/下拉搜索框背景色/标签背景色*/ + --ti-common-color-bg-emphasize: var(--ti-base-color-brand-6); /* 背景高亮色*/ + --ti-common-color-bg-disabled: var(--ti-base-color-bg-5); /* 禁用背景色*/ + --ti-common-color-bg-hover: var(--ti-base-color-brand-8); /* 主色背景悬浮色*/ + --ti-common-color-bg-gray: var(--ti-base-color-bg-4); /* 新区域组件-悬浮背景色*/ + --ti-common-color-bg-secondary: var(--ti-base-color-common-2); /* 开关组件-关闭状态-背景色*/ + + /* 重要背景色,主要用于重要按钮场景*/ + --ti-common-bg-primary: var(--ti-base-color-bg-8); /* 重要按钮背景色*/ + --ti-common-bg-primary-hover: var(--ti-base-color-bg-7); /* 重要按钮背景悬浮、focus色*/ + --ti-common-bg-primary-active: var(--ti-base-color-bg-9); /* 重要按钮背景色按下色*/ + + /* 次要背景色,主要用于次要按钮场景*/ + --ti-common-bg-minor: var(--ti-base-color-bg-2); /* 次要按钮背景色*/ + --ti-common-bg-minor-hover: var(--ti-base-color-bg-1); /* 次要按钮背景悬浮、focus色*/ + --ti-common-bg-minor-active: var(--ti-base-color-bg-3); /* 次要按钮背景色按下色*/ + + /* 白底背景状态色*/ + --ti-common-color-bg-white-normal: var(--ti-base-color-white); /* 白色背景,用于输入框背景*/ + --ti-common-color-bg-white-emphasize: var(--ti-base-color-brand-1); /* 白色hover或强调色,如表头背景、表格悬浮、下拉选项悬浮背景*/ + + /* 浅底背景状态色*/ + --ti-common-color-bg-light-normal: var(--ti-base-color-brand-2); /* 滑块slider-背景色/多选快buttongroup-默认背景色/树组件选中背景色*/ + --ti-common-color-bg-light-emphasize: var(--ti-base-color-brand-3); /* 浅背景hover或强调色,开关组件“开”禁用背景色*/ + + /* 深色底背景状态色*/ + --ti-common-color-bg-dark-normal: var(--ti-base-color-common-6); /* 一级tab页签背景色*/ + --ti-common-color-bg-dark-emphasize: var(--ti-base-color-common-4); /* 一级tab页签背景-悬浮色*/ + --ti-common-color-bg-dark-active: var(--ti-common-color-bg-normal); /* 一级tab页签背景-激活/focus状态背景色*/ + --ti-common-color-bg-dark-deep: var(--ti-base-color-common-6); /* tip、alert提示背景色*/ + --ti-common-color-bg-dark-disabled: var(--ti-base-color-common-1); /* 深色背景禁用色,开关组件“关”禁用背景色*/ + + /* 顶部导航*/ + --ti-common-color-bg-navigation: var(--ti-base-color-common-8); /* 顶部导航背景色/顶部导航下拉悬浮背景色*/ + --ti-common-color-bg-dark-select: var(--ti-base-color-common-9); /* 顶部导航下拉背景色 */ + + /* 2.6 图表色*/ + --ti-common-color-data-1: var(--ti-base-color-success-3); /* 图表数据色-1*/ + --ti-common-color-data-2: var(--ti-base-color-icon-info); /* 图表数据色-2*/ + --ti-common-color-data-3: var(--ti-base-color-data-3); /* 图表数据色-3*/ + --ti-common-color-data-4: var(--ti-base-color-data-4); /* 图表数据色-4*/ + --ti-common-color-data-5: var(--ti-base-color-data-5); /* 图表数据色-5*/ + --ti-common-color-data-6: var(--ti-base-color-warn-3); /* 图表数据色-6*/ + --ti-common-color-data-7: var(--ti-base-color-warn-4); /* 图表数据色-7*/ + --ti-common-color-data-8: var(--ti-base-color-error-3); /* 图表数据色-8*/ + + /* 2.7 透明色*/ + --ti-common-color-transparent: var(--ti-base-color-transparent); + + /* 二.其他变量*/ + /* 边框圆角*/ + --ti-common-border-radius-normal: 2px; /* 常规*/ + --ti-common-border-radius-0: 0px; /* 直角*/ + --ti-common-border-radius-1: 4px; /* 圆角-1*/ + --ti-common-border-radius-2: 8px; /* 圆角-2*/ + --ti-common-border-radius-3: 50%; /* 圆形*/ + + /* 字号*/ + --ti-common-font-size-base: 12px; /* 正文-常规*/ + --ti-common-font-size-1: 14px; /* 标题-小*/ + --ti-common-font-size-2: 16px; /* 标题-中*/ + --ti-common-font-size-3: 18px; /* 标题-大*/ + --ti-common-font-size-4: 20px; /* 字号-4*/ + --ti-common-font-size-5: 24px; /* 字号-5*/ + --ti-common-font-size-6: 32px; /* 字号-6*/ + --ti-common-font-size-7: 36px; /* 字号-7*/ + + /* 行高*/ + --ti-common-line-height-number: 1.5; /* 文字行高倍数,建议组件中设置行高使用该变量,如有特殊情况,请自行定义*/ + + /* 间距:适用于组件中的margin、padding*/ + --ti-common-space-base: 4px; /* 基础间距*/ + --ti-common-space-2x: calc(var(--ti-common-space-base) * 2); /* 间距-2*/ + --ti-common-space-3x: calc(var(--ti-common-space-base) * 3); /* 间距-3*/ + --ti-common-space-4x: calc(var(--ti-common-space-base) * 4); /* 间距-4*/ + --ti-common-space-5x: calc(var(--ti-common-space-base) * 5); /* 间距-5*/ + --ti-common-space-6x: calc(var(--ti-common-space-base) * 6); /* 间距-6*/ + --ti-common-space-7x: calc(var(--ti-common-space-base) * 7); /* 间距-7*/ + --ti-common-space-8x: calc(var(--ti-common-space-base) * 8); /* 间距-8*/ + --ti-common-space-9x: calc(var(--ti-common-space-base) * 9); /* 间距-9*/ + --ti-common-space-10x: calc(var(--ti-common-space-base) * 10); /* 间距-10*/ + /* 其他间距*/ + --ti-common-space-0: 0px; /* 其他间距-1*/ + --ti-common-space-1: 1px; /* 其他间距-2*/ + --ti-common-space-6: 6px; /* 其他间距-3*/ + --ti-common-space-10: 10px; /* 其他间距-4*/ + + /* 阴影*/ + --ti-common-shadow-1-up: 0 -1px 4px 0 rgba(0,0,0,0.1); /* 阴影-1 上*/ + --ti-common-shadow-1-down: 0 1px 4px 0 rgba(0,0,0,0.1); /* 阴影-1 下*/ + --ti-common-shadow-1-left: -1px 0px 4px 0 rgba(0,0,0,0.1); /* 阴影-1 左*/ + --ti-common-shadow-1-right: 1px 0px 4px 0 rgba(0,0,0,0.1); /* 阴影-1 右*/ + --ti-common-shadow-2-up: 0 -2px 8px 0 rgba(0,0,0,0.2); /* 阴影-2 上*/ + --ti-common-shadow-2-down: 0 2px 8px 0 rgba(0,0,0,0.2); /* 阴影-2 下*/ + --ti-common-shadow-2-left: -2px 0 8px 0 rgba(238, 10, 10, 0.2); /* 阴影-2 左*/ + --ti-common-shadow-2-right: 2px 0 8px 0 rgba(252, 5, 5, 0.2); /* 阴影-2 右*/ + --ti-common-shadow-3-up: 0 -4px 16px 0 rgba(0,0,0,0.2); /* 阴影-3 上*/ + --ti-common-shadow-3-down: 0 4px 16px 0 rgba(0,0,0,0.2); /* 阴影-3 下*/ + --ti-common-shadow-3-left: -4px 0 16px 0 rgba(0,0,0,0.2); /* 阴影-3 左*/ + --ti-common-shadow-3-right: 4px 0 16px 0 rgba(0,0,0,0.2); /* 阴影-3 右*/ + --ti-common-shadow-4-up: 0 -8px 40px 0 rgba(0,0,0,0.2); /* 阴影-4 上*/ + --ti-common-shadow-4-down: 0 8px 40px 0 rgba(0,0,0,0.2); /* 阴影-4 下*/ + --ti-common-shadow-4-left: -8px 0 40px 0 rgba(0,0,0,0.2); /* 阴影-4 左*/ + --ti-common-shadow-4-right: 8px 0 40px 0 rgba(0,0,0,0.2); /* 阴影-4 右*/ + + /* 提示类阴影*/ + --ti-common-shadow-error: 0 1px 3px 0 rgba(199,54,54,0.25); /* 错误*/ + --ti-common-shadow-warn: 0 1px 3px 0 rgba(204,100,20,0.25); /* 告警*/ + --ti-common-shadow-prompt: 0 1px 3px 0 rgba(70,94,184,0.25); /* 提示*/ + --ti-common-shadow-success: 0 1px 3px 0 rgba(39,176,128,0.25); /* 成功*/ + + /* 字体*/ + --ti-common-font-family: "Helvetica", "Arial", "PingFangSC-Regular", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", "Microsoft JhengHei"; + + /* 字重*/ + --ti-common-font-weight-1: 100; /* 极细*/ + --ti-common-font-weight-2: 200; /* 纤细*/ + --ti-common-font-weight-3: 300; /* 细体*/ + --ti-common-font-weight-4: normal; /* 常规*/ + --ti-common-font-weight-5: 500; /* 中等*/ + --ti-common-font-weight-6: 600; /* 半粗*/ + --ti-common-font-weight-7: bold; /* 粗体*/ + --ti-common-font-weight-8: 800; /* 中黑*/ + --ti-common-font-weight-9: 900; /* 黑体*/ + + /* 边框粗细*/ + --ti-common-border-weight-normal: 1px; /* 常规*/ + --ti-common-border-weight-1: 2px; /* 较粗*/ + --ti-common-border-weight-2: 3px; /* 粗*/ + + /* 边框样式*/ + --ti-common-border-style-dashed: dashed; /* 虚线*/ + --ti-common-border-style-dotted: dotted; /* 点线*/ + --ti-common-border-style-solid: solid; /* 实线*/ + + /* 尺寸*/ + --ti-common-size-base: 4px; /* 基础尺寸*/ + --ti-common-size-2x: calc(var(--ti-common-size-base) * 2); /* 尺寸-2*/ + --ti-common-size-3x: calc(var(--ti-common-size-base) * 3); /* 尺寸-3*/ + --ti-common-size-4x: calc(var(--ti-common-size-base) * 4); /* 尺寸-4*/ + --ti-common-size-5x: calc(var(--ti-common-size-base) * 5); /* 尺寸-5*/ + --ti-common-size-6x: calc(var(--ti-common-size-base) * 6); /* 尺寸-6*/ + --ti-common-size-7x: calc(var(--ti-common-size-base) * 7); /* 尺寸-7*/ + --ti-common-size-8x: calc(var(--ti-common-size-base) * 8); /* 尺寸-8*/ + --ti-common-size-9x: calc(var(--ti-common-size-base) * 9); /* 尺寸-9*/ + --ti-common-size-10x: calc(var(--ti-common-size-base) * 10); /* 尺寸-10*/ + --ti-common-size-11x: calc(var(--ti-common-size-base) * 11); /* 尺寸-11*/ + --ti-common-size-12x: calc(var(--ti-common-size-base) * 12); /* 尺寸-12*/ + --ti-common-size-13x: calc(var(--ti-common-size-base) * 13); /* 尺寸-13*/ + --ti-common-size-14x: calc(var(--ti-common-size-base) * 14); /* 尺寸-14*/ + --ti-common-size-15x: calc(var(--ti-common-size-base) * 15); /* 尺寸-15*/ + --ti-common-size-16x: calc(var(--ti-common-size-base) * 16); /* 尺寸-16*/ + --ti-common-size-17x: calc(var(--ti-common-size-base) * 17); /* 尺寸-17*/ + --ti-common-size-18x: calc(var(--ti-common-size-base) * 18); /* 尺寸-18*/ + --ti-common-size-19x: calc(var(--ti-common-size-base) * 19); /* 尺寸-19*/ + --ti-common-size-20x: calc(var(--ti-common-size-base) * 20); /* 尺寸-20*/ + --ti-common-size-21x: calc(var(--ti-common-size-base) * 21); /* 尺寸-21*/ + --ti-common-size-22x: calc(var(--ti-common-size-base) * 22); /* 尺寸-22*/ + --ti-common-size-23x: calc(var(--ti-common-size-base) * 23); /* 尺寸-23*/ + --ti-common-size-24x: calc(var(--ti-common-size-base) * 24); /* 尺寸-24*/ + --ti-common-size-25x: calc(var(--ti-common-size-base) * 25); /* 尺寸-25*/ + --ti-common-size-26x: calc(var(--ti-common-size-base) * 26); /* 尺寸-26*/ + --ti-common-size-27x: calc(var(--ti-common-size-base) * 27); /* 尺寸-27*/ + --ti-common-size-28x: calc(var(--ti-common-size-base) * 28); /* 尺寸-28*/ + --ti-common-size-29x: calc(var(--ti-common-size-base) * 29); /* 尺寸-29*/ + --ti-common-size-30x: calc(var(--ti-common-size-base) * 30); /* 尺寸-30*/ + --ti-common-size-31x: calc(var(--ti-common-size-base) * 31); /* 尺寸-31*/ + --ti-common-size-32x: calc(var(--ti-common-size-base) * 32); /* 尺寸-32*/ + --ti-common-size-33x: calc(var(--ti-common-size-base) * 33); /* 尺寸-33*/ + --ti-common-size-34x: calc(var(--ti-common-size-base) * 34); /* 尺寸-34*/ + --ti-common-size-35x: calc(var(--ti-common-size-base) * 35); /* 尺寸-35*/ + --ti-common-size-36x: calc(var(--ti-common-size-base) * 36); /* 尺寸-36*/ + --ti-common-size-37x: calc(var(--ti-common-size-base) * 37); /* 尺寸-37*/ + --ti-common-size-38x: calc(var(--ti-common-size-base) * 38); /* 尺寸-38*/ + --ti-common-size-39x: calc(var(--ti-common-size-base) * 39); /* 尺寸-39*/ + --ti-common-size-40x: calc(var(--ti-common-size-base) * 40); /* 尺寸-40*/ + --ti-common-size-41x: calc(var(--ti-common-size-base) * 41); /* 尺寸-41*/ + --ti-common-size-42x: calc(var(--ti-common-size-base) * 42); /* 尺寸-42*/ + --ti-common-size-43x: calc(var(--ti-common-size-base) * 43); /* 尺寸-43*/ + --ti-common-size-44x: calc(var(--ti-common-size-base) * 44); /* 尺寸-44*/ + --ti-common-size-45x: calc(var(--ti-common-size-base) * 45); /* 尺寸-45*/ + --ti-common-size-46x: calc(var(--ti-common-size-base) * 46); /* 尺寸-46*/ + --ti-common-size-47x: calc(var(--ti-common-size-base) * 47); /* 尺寸-47*/ + --ti-common-size-48x: calc(var(--ti-common-size-base) * 48); /* 尺寸-48*/ + --ti-common-size-49x: calc(var(--ti-common-size-base) * 49); /* 尺寸-49*/ + --ti-common-size-50x: calc(var(--ti-common-size-base) * 50); /* 尺寸-50*/ + /* 其他尺寸*/ + --ti-common-size-0: 0px; /* 其他尺寸-1*/ + --ti-common-size-auto: auto; /* 其他尺寸-2*/ +} \ No newline at end of file diff --git a/src/themes/basic/build.less b/src/themes/basic/build.less new file mode 100644 index 0000000..bda0bfd --- /dev/null +++ b/src/themes/basic/build.less @@ -0,0 +1,4 @@ +// 编译后是给用户提供的style.css,所有组件公用部分。目前,仅normalize.less + +// 规范化 +@import 'normalize.less'; diff --git a/src/themes/basic/common.less b/src/themes/basic/common.less new file mode 100644 index 0000000..71d78a7 --- /dev/null +++ b/src/themes/basic/common.less @@ -0,0 +1,21 @@ +/** + * @description + * 该文件下定义组件less变量 + */ + +//-- Z-index master list +// +// Warning: Avoid customizing these values. They're used for a bird's eye view +// of components dependent on the z-axis and are designed to all work together. +// @zindex-navbar: 1000; +// @zindex-dropdown: 1000; +// @zindex-popover: 1060; +// @zindex-tooltip: 1070; +// @zindex-navbar-fixed: 1030; +// 如果确认其他index都不用了,那么以下这两个变量,移入modal +@zindex-modal-background: 1200; +@zindex-modal: 1300; +@zindex-notification: 6000; +// 失焦校验:错误且已输入失焦时显示错误样式,及时校验:错误且已输入时显示错误样式 +@tiny-invalid-class: &.ng-invalid.ng-touched[tiBlurCheck]:not([tiFocused]), &.ng-invalid.ng-dirty[tiBlurCheck]:not([tiFocused]), + &.ng-invalid.ng-touched:not([tiBlurCheck]):not([tiRadiobaseCheck]), &.ng-invalid.ng-dirty:not([tiBlurCheck]):not([tiRadiobaseCheck]); diff --git a/src/themes/basic/compnent-container-border.less b/src/themes/basic/compnent-container-border.less new file mode 100644 index 0000000..1cc7602 --- /dev/null +++ b/src/themes/basic/compnent-container-border.less @@ -0,0 +1,33 @@ +// @import "mixins.less"; // 一般组件,都会先引入mixins.less,所以这里就不必引入 + +// 组件边框样式:ip,select,spinner,tagsinput,date,datetime,daterange,datetimerange等组件 +// 组件皮儿已定义background-color,所以内部元素可以不定义background-color +// 组件皮儿已定义border,所以dominator边框如果和皮儿边框重合,那么dominator边框为透明。 +// 因为dominator和组件皮儿一样大,所以是dominator定义的cursor: not-allowed !important; 生效 +.ti3-compnent-container-border { + // 1.组件边框,通用尺寸样式,与disabled、hover、focused等状态无关; + display: inline-block; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid); + border-radius: var(--ti-common-border-radius-normal); + .box-sizing(border-box); + + // 2.组件边框,非禁用颜色样式,包括hover和focused等状态 + &:not([disabled]) { + background-color: var(--ti-common-color-bg-white-normal); + border-color: var(--ti-common-color-line-normal); + &:hover { + border-color: var(--ti-common-color-line-hover); + } + &[tiFocused] { + border-color: var(--ti-common-color-line-active); + } + // 是否存在hover focused同时出现的样式? + } + + // 3.组件边框,禁用颜色样式,包括hover和focused等状态 + &[disabled] { + background-color: var(--ti-common-color-bg-disabled); + border-color: var(--ti-common-color-line-disabled); + cursor: not-allowed !important; + } +} diff --git a/src/themes/basic/img/table-loadfail-bg.png b/src/themes/basic/img/table-loadfail-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..894d3daeb7fa5e14444b60035fe3de4944172baa GIT binary patch literal 3437 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 P00000NkvXXu0mjfD~)mj literal 0 HcmV?d00001 diff --git a/src/themes/basic/img/table-nodata-bg.png b/src/themes/basic/img/table-nodata-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..2180606f605ba69ed3d0d114ee012495e514c2a4 GIT binary patch literal 3430 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 P00000NkvXXu0mjfsB8wT literal 0 HcmV?d00001 diff --git a/src/themes/basic/img/upload-image-preview.png b/src/themes/basic/img/upload-image-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..e7cc4a76fed4d75d2759fc15f03f62889fa65e4b GIT binary patch literal 230 zcmVouE~7={4)_Fk4EZw0c_ua zNdz{M_BHSn;3#M%Eo#K!%_pD*=D=Cfx(1R0cEPso6R-k~lC~vCHbmP8U?yq#6QCqY z@Bdfhx2d)-z%7?am4lecrfb(haNF1TPZ$gcwt*(X9+IYm g9pu&=v_cN>1Q_Z$5!!^i0000007*qoM6N<$f?j1`r~m)} literal 0 HcmV?d00001 diff --git a/src/themes/basic/link-no-decoration.less b/src/themes/basic/link-no-decoration.less new file mode 100644 index 0000000..e06c42c --- /dev/null +++ b/src/themes/basic/link-no-decoration.less @@ -0,0 +1,10 @@ +.ti-link-no-decoration { + color: var(--ti-common-color-text-link); + text-decoration: none; + cursor: pointer; + + &:hover { + color: var(--ti-common-color-text-link-hover); + text-decoration: underline; + } +} diff --git a/src/themes/basic/mixins.less b/src/themes/basic/mixins.less new file mode 100644 index 0000000..9853564 --- /dev/null +++ b/src/themes/basic/mixins.less @@ -0,0 +1,242 @@ +// 阴影 +// @x 横轴偏移 +// @y 纵轴偏移 +// @blur 阴影偏移 +// @color 阴影颜色 +.box-shadow (...) { + -moz-box-shadow: @arguments; + -webkit-box-shadow: @arguments; + box-shadow: @arguments; +} + +// 盒模型计算模式 +// @type border-box | content-box +.box-sizing (@type:border-box) { + -webkit-box-sizing: @type; + -moz-box-sizing: @type; + -ms-box-sizing: @type; + box-sizing: @type; +} + +// 变换 +.transition (...) { + -moz-transition: @arguments; + -webkit-transition: @arguments; + transition: @arguments; +} + +.transition-transform(@transition) { + -webkit-transition: -webkit-transform @transition; + -moz-transition: -moz-transform @transition; + -o-transition: -o-transform @transition; + transition: transform @transition; +} +// 圆角 +.border-radius (...) { + -moz-border-radius: @arguments; + -webkit-border-radius: @arguments; + border-radius: @arguments; +} + +// 文字选择 +.user-select (@type:none) { + // 火狐 + -moz-user-select: @type; + // webkit浏览器 + -webkit-user-select: @type; + // IE10 + -ms-user-select: @type; + // 早期浏览器 + -khtml-user-select: @type; + user-select: @type; +} + +.triangle-up(@triangle-width; @triangle-height; @triangle-color) { + width: 0; + height: 0; + border-left: calc(@triangle-width / 2) solid transparent; + border-right: calc(@triangle-width / 2) solid transparent; + border-bottom: @triangle-height solid @triangle-color; +} + +.triangle-down(@triangle-width; @triangle-height; @triangle-color) { + width: 0; + height: 0; + border-left: calc(@triangle-width / 2) solid transparent; + border-right: calc(@triangle-width / 2) solid transparent; + border-top: @triangle-height solid @triangle-color; +} + +.translate(@x; @y) { + -webkit-transform: translate(@x, @y); + -ms-transform: translate(@x, @y); // IE9 only + -o-transform: translate(@x, @y); + transform: translate(@x, @y); +} + +// 旋转 +.rotate(@x) { + -webkit-transform: rotate(@x); + -ms-transform: rotate(@x); // IE9 only + -moz-transform: rotate(@x); + -o-transform: rotate(@x); + transform: rotate(@x); +} + +.clearfix() { + &:after { + display: table; + content: ''; + clear: both; + } + &:before { + display: table; + content: ''; + } +} +.background-linear-vertical(@start-color, @end-color...) { + & when (length(@arguments) > 1) { + background: mix(extract(@arguments, 1), extract(@arguments, 2), 50%); + background: -webkit-linear-gradient(top, @arguments); + background: -moz-linear-gradient(top, @arguments); + background: -o-linear-gradient(top, @arguments); + background: linear-gradient(top, @arguments); + } + + & when(length(@arguments) = 1) { + background: @arguments; + } +} + +.ellipsis() { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.flex-container(@direction: row, @justify-content: flex-start, @align-items: stretch) { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: @direction; + -ms-flex-direction: @direction; + -moz-flex-direction: @direction; + flex-direction: @direction; + -webkit-justify-content: @justify-content; + -moz-justify-content: @justify-content; + -ms-justify-content: @justify-content; + justify-content: @justify-content; + -webkit-align-items: @align-items; + -moz-align-items: @align-items; + -ms-align-items: @align-items; + align-items: @align-items; +} + +.flex-item(@grow: 0) { + -webkit-flex-grow: @grow; + -ms-flex-grow: @grow; + -moz-flex-grow: @grow; + flex-grow: @grow; +} + +/*------------------------------------------------动效------------------------------------------------*/ +//动效公共的less方法 +.animation(@animate-name, @time, @timing-function: cubic-bezier(0.4, 0, 0.2, 1), @delay:0s, @count:1,@wards:forwards) { + animation: @animate-name @time @timing-function @delay @count @wards; +} + +//.common-animation用来定义动画的函数,如果不想使用该函数中的某个属性,直接使用默认值:false +.common-animation(@common-name, + @from-border-color:false,@to-border-color:false, + @from-background-color:false,@to-background-color:false, + @from-color:false,@to-color:false) { + @keyframes @common-name { + from { + border: 1px solid @from-border-color; + background-color: @from-background-color; + color: @from-color; + } + to { + border: 1px solid @to-border-color; + background-color: @to-background-color; + color: @to-color; + } + } +} + +//定义一个移动动画函数 +.translate-animation(@name,@from-y:0px,@to-y:0px,@from-x:0px,@to-x:0px) { + @keyframes @name { + from { + transform: translate(@from-x, @from-y); + } + to { + transform: translate(@to-x, @to-y); + } + } +} + +//定义一个缩放函数 +.scale-animation(@scale-name,@from-scale,@to-scale) { + @keyframes @scale-name { + from { + transform: scale(@from-scale); + } + to { + transform: scale(@to-scale); + } + } +} + +//定义一个淡入淡出函数:使用的透明度参数必须是0--100,不能是0-1 +.fade-animation(@fade-name,@from-opacity,@to-opacity) { + @keyframes @fade-name { + from { + opacity: @from-opacity; + } + to { + opacity: @to-opacity; + } + } +} + +// 定义一个Y轴方向上缩放的动画函数 +.scaleY-animation(@scaleY-name,@scaleY-origin,@from-scaleY,@to-scaleY) { + @keyframes @scaleY-name { + from { + transform-origin: @scaleY-origin; + transform: scaleY(@from-scaleY); + } + to { + transform-origin: @scaleY-origin; + transform: scaleY(@to-scaleY); + } + } +} + +//定义一个旋转函数 +.rotate-aniamtion(@rotate-name,@from-rotate,@to-rotate) { + @keyframes @rotate-name { + from { + transform: rotate(@from-rotate); + -moz-transform: rotate(@from-rotate); + -ms-transform: rotate(@from-rotate); + -webkit-transform: rotate(@from-rotate); + } + to { + transform: rotate(@to-rotate); + -moz-transform: rotate(@to-rotate); + -ms-transform: rotate(@to-rotate); + -webkit-transform: rotate(@to-rotate); + } + } +} + +// 表单边框变化移入动画,在边框颜色变化过程中处理 +.form-border-animat-enter() { + .transition (border-color 0.2s); +} + +// 表单边框变化移出动画,在边框颜色初始定义时处理 +.form-border-animat-init() { + .transition (border-color 0.15s); +} diff --git a/src/themes/basic/normalize.less b/src/themes/basic/normalize.less new file mode 100644 index 0000000..a9be017 --- /dev/null +++ b/src/themes/basic/normalize.less @@ -0,0 +1,201 @@ +@import 'common.less'; // 此文件用到了@tiny-invalid-class +/* 基础样式 */ +body { + margin: 0; + padding: 0; + font-size: var(--ti-common-font-size-base); + font-family: var(--ti-common-font-family); + color: var(--ti-common-color-text-primary); + background-color: var(--ti-common-color-bg-white-normal); +} + +button, +blockquote, /* 结构类 */ +div, +section, +h1,h2,h3,h4,h5,h6, /* 标题类 */ +ul,ol,dl,dt,dd,li, /* 列表类 */ +pre, /* 文本格式元素 */ +code, +form,fieldset,legend,input,textarea, /* 表单类 */ +th,td, /* 表格类 */ +p, +select { + margin: 0; + padding: 0; +} + +/* 表单部分 */ +input { + vertical-align: middle; +} + +fieldset, +img { + border: 0; +} +// 校正不同浏览器下字体,字号及边距差异化 +button, +input, +textarea { + font-family: inherit; + font-size: 100%; + margin: 0; // 为了兼容Firefox 4+、Safari 5、Chrome +} +// IE下line-height属性默认是inherit,因此需要显示设置 +button { + line-height: normal; +} + +textarea { + overflow: auto; // 移除默认的纵向滚动条(IE 8/9) + vertical-align: top; // 统一各浏览器的对齐格式。 + resize: vertical; +} + +// 移除内部的padding、border值。为了兼容 Firefox 4+ +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} +// 特殊标签差异化处理 +address, +caption, +cite, +code, +dfn, +em, +var { + font-style: normal; + font-weight: 400; +} + +// 去除列表标记 +ol, +ul { + list-style: none; +} +// 标记引用 +q:before { + content: ''; +} +q:after { + content: ''; +} + +// 缩写类标签 +abbr, +acronym { + border: 0; + font-variant: normal; +} + +// 为了兼容Firefox 4+, Safari 5, and Chrome +b, +strong { + font-weight: 700; +} + +// 防止在所有浏览器中的“ sub”和“ sup”影响行高。 +sub { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; + bottom: -0.25em; +} +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; + top: -0.5em; +} +// 在IE10下,移除链接中的灰色背景色。 +a { + background-color: transparent; +} + +// 设置默认样式,避免受到bootstrap等第三方样式库的影响 +label { + margin-bottom: 0; + font-weight: 400; +} + +/* 消除表格单元格直接空隙 */ + +table { + border-spacing: 0 0; + border-collapse: collapse; + margin-bottom: 0; +} + +/* H5新标签 */ +figcaption, // 定义figure元素的标题 +figure,hgroup, // 用于对元素进行组合 +article,footer,header,nav,section,aside, // 结构布局类标签 +menu, // 定义菜单列表 +main, // 规定文档的主要内容 +details,summary { + // 定义标题详情 + display: block; +} + +button::-moz-focus-inner { + padding: 0; + border: none; +} + +audio, +canvas, +progress, +video { + display: inline-block; // 兼容 IE 8/9 + vertical-align: baseline; // 兼容Chrome, Firefox, and Opera +} + +// IE10+下隐藏的clear和password按钮 +input::-ms-clear { + width: 0; + height: 0; +} +input[type='password']::-ms-reveal { + width: 0; + height: 0; +} + +// placeholder 相关样式 适用于 Chrome/Opera/Safari +::-webkit-input-placeholder { + color: var(--ti-common-color-text-disabled); + text-overflow: initial !important; +} + +// placeholder 相关样式 适用于Firefox 19+ +::-moz-placeholder { + color: var(--ti-common-color-text-disabled); + opacity: 1; + text-overflow: initial; +} +// placeholder 相关样式 适用于Firefox 18- +:-moz-placeholder { + color: var(--ti-common-color-text-disabled); + opacity: 1; +} + +// 处理IE下placeholder文本颜色显示问题 +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + color: var(--ti-common-color-text-disabled) !important; +} + +// 一些Angular和组件相关的样式 +// 表单元素校验样式,归属于表单的样式定义,校验中没有合适的文件定义和加载,因此需要在公共处定义样式 +input, +textarea, +[tiForm] { + @{tiny-invalid-class} { + border-color: var(--ti-common-color-error-border) !important; + background-color: var(--ti-common-color-error-bg); + } +} diff --git a/src/themes/package.json b/src/themes/package.json new file mode 100644 index 0000000..9c815b1 --- /dev/null +++ b/src/themes/package.json @@ -0,0 +1,5 @@ +{ + "name": "@opentiny/ng-themes", + "version": "1.0.0-beta.0", + "license": "MIT" +} \ No newline at end of file diff --git a/src/themes/project.json b/src/themes/project.json new file mode 100644 index 0000000..f1b81dd --- /dev/null +++ b/src/themes/project.json @@ -0,0 +1,38 @@ +{ + "projectType": "library", + "root": "src/themes", + "sourceRoot": "src/themes", + "targets": { + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/themes"], + "options": { + "commands": [ + { + "command": "node build/buildThemes.js" + } + ] + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/themes && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build themes && ng pack themes && node build/publish.js themes --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/themes/theme-blue/basic-var.less b/src/themes/theme-blue/basic-var.less new file mode 100644 index 0000000..d0c81bc --- /dev/null +++ b/src/themes/theme-blue/basic-var.less @@ -0,0 +1,16 @@ +/*基础变量定义*/ +:root { + /* 一.颜色*/ + /* 1.基础色:此处的颜色变量为所有组件应用到的颜色,此处颜色仅在公共色中使用,具体组件不可使用*/ + /* 1.1品牌色*/ + /* 品牌主色*/ + --ti-base-color-brand-6: #6cbfff; /* 主色蓝*/ + /* 主色衍生色*/ + --ti-base-color-brand-8: #0f6999; /* 品牌色-8*/ + --ti-base-color-brand-7: #4ea6e6; /* 品牌色-7*/ + --ti-base-color-brand-5: #85caff; /* 品牌色-5*/ + --ti-base-color-brand-4: #9ed5ff; /* 品牌色-4*/ + --ti-base-color-brand-3: #b8e0ff; /* 品牌色-3*/ + --ti-base-color-brand-2: #d1ebff; /* 品牌色-2*/ + --ti-base-color-brand-1: #ebf6ff; /* 品牌色-1*/ +} diff --git a/src/themes/theme-blue/build.less b/src/themes/theme-blue/build.less new file mode 100644 index 0000000..c343975 --- /dev/null +++ b/src/themes/theme-blue/build.less @@ -0,0 +1,5 @@ +/*注意:css var变量,以最后一次出现的生效。*/ +// 基础变量 +@import (less) '../basic/basic-var.css'; +// 基础变量 +@import 'basic-var.less'; diff --git a/src/themes/theme-default/build.less b/src/themes/theme-default/build.less new file mode 100644 index 0000000..7bad1b0 --- /dev/null +++ b/src/themes/theme-default/build.less @@ -0,0 +1,3 @@ +/*注意:css var变量,以最后一次出现的生效。*/ +// 基础变量 +@import (less) '../basic/basic-var.css'; diff --git a/src/themes/theme-green/basic-var.less b/src/themes/theme-green/basic-var.less new file mode 100644 index 0000000..6902b99 --- /dev/null +++ b/src/themes/theme-green/basic-var.less @@ -0,0 +1,16 @@ +/*基础变量定义*/ +:root { + /* 一.颜色*/ + /* 1.基础色:此处的颜色变量为所有组件应用到的颜色,此处颜色仅在公共色中使用,具体组件不可使用*/ + /* 1.1品牌色*/ + /* 品牌主色*/ + --ti-base-color-brand-6: #a6dd82; /* 主色绿*/ + /* 主色衍生色*/ + --ti-base-color-brand-8: #5e9629; /* 品牌色-8*/ + --ti-base-color-brand-7: #92cc68; /* 品牌色-7*/ + --ti-base-color-brand-5: #b3e890; /* 品牌色-5*/ + --ti-base-color-brand-4: #c5f2a7; /* 品牌色-4*/ + --ti-base-color-brand-3: #d8fcc0; /* 品牌色-3*/ + --ti-base-color-brand-2: #e5ffd4; /* 品牌色-2*/ + --ti-base-color-brand-1: #f0ffe6; /* 品牌色-1*/ +} diff --git a/src/themes/theme-green/build.less b/src/themes/theme-green/build.less new file mode 100644 index 0000000..c343975 --- /dev/null +++ b/src/themes/theme-green/build.less @@ -0,0 +1,5 @@ +/*注意:css var变量,以最后一次出现的生效。*/ +// 基础变量 +@import (less) '../basic/basic-var.css'; +// 基础变量 +@import 'basic-var.less'; diff --git a/src/themes/theme-purple/basic-var.less b/src/themes/theme-purple/basic-var.less new file mode 100644 index 0000000..454e8ed --- /dev/null +++ b/src/themes/theme-purple/basic-var.less @@ -0,0 +1,16 @@ +/*基础变量定义*/ +:root { + /* 一.颜色*/ + /* 1.基础色:此处的颜色变量为所有组件应用到的颜色,此处颜色仅在公共色中使用,具体组件不可使用*/ + /* 1.1品牌色*/ + /* 品牌主色*/ + --ti-base-color-brand-6: #a97af8; /* 主色紫*/ + /* 主色衍生色*/ + --ti-base-color-brand-8: #3f1a9c; /* 品牌色-8*/ + --ti-base-color-brand-7: #8a5ce0; /* 品牌色-7*/ + --ti-base-color-brand-5: #bc94ff; /* 品牌色-5*/ + --ti-base-color-brand-4: #caabff; /* 品牌色-4*/ + --ti-base-color-brand-3: #d8c2ff; /* 品牌色-3*/ + --ti-base-color-brand-2: #e7d9ff; /* 品牌色-2*/ + --ti-base-color-brand-1: #f5f0ff; /* 品牌色-1*/ +} diff --git a/src/themes/theme-purple/build.less b/src/themes/theme-purple/build.less new file mode 100644 index 0000000..c343975 --- /dev/null +++ b/src/themes/theme-purple/build.less @@ -0,0 +1,5 @@ +/*注意:css var变量,以最后一次出现的生效。*/ +// 基础变量 +@import (less) '../basic/basic-var.css'; +// 基础变量 +@import 'basic-var.less'; diff --git a/src/themes/theme-red/basic-var.less b/src/themes/theme-red/basic-var.less new file mode 100644 index 0000000..9dc0087 --- /dev/null +++ b/src/themes/theme-red/basic-var.less @@ -0,0 +1,16 @@ +/*基础变量定义*/ +:root { + /* 一.颜色*/ + /* 1.基础色:此处的颜色变量为所有组件应用到的颜色,此处颜色仅在公共色中使用,具体组件不可使用*/ + /* 1.1品牌色*/ + /* 品牌主色*/ + --ti-base-color-brand-6: #f3689a; /* 主色红*/ + /* 主色衍生色*/ + --ti-base-color-brand-8: #96114d; /* 品牌色-8*/ + --ti-base-color-brand-7: #db4d83; /* 品牌色-7*/ + --ti-base-color-brand-5: #fc86b0; /* 品牌色-5*/ + --ti-base-color-brand-4: #ffa1c2; /* 品牌色-4*/ + --ti-base-color-brand-3: #ffbad2; /* 品牌色-3*/ + --ti-base-color-brand-2: #ffd4e3; /* 品牌色-2*/ + --ti-base-color-brand-1: #ffedf3; /* 品牌色-1*/ +} diff --git a/src/themes/theme-red/build.less b/src/themes/theme-red/build.less new file mode 100644 index 0000000..c343975 --- /dev/null +++ b/src/themes/theme-red/build.less @@ -0,0 +1,5 @@ +/*注意:css var变量,以最后一次出现的生效。*/ +// 基础变量 +@import (less) '../basic/basic-var.css'; +// 基础变量 +@import 'basic-var.less'; diff --git a/src/time/demo/karma.conf.js b/src/time/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/time/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/time/demo/project.json b/src/time/demo/project.json new file mode 100644 index 0000000..2ea0648 --- /dev/null +++ b/src/time/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/time/demo", + "sourceRoot": "src/time/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/time", + "index": "src/time/demo/src/index.html", + "main": "src/time/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/time/demo/tsconfig.app.json", + "assets": ["src/time/demo/src/favicon.ico", "src/time/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "time-demo:build:production" + }, + "development": { + "browserTarget": "time-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js time" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/time/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/time/demo/tsconfig.spec.json", + "karmaConfig": "src/time/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/time/demo/src/app/AppComponent.ts b/src/time/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/time/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/time/demo/src/app/AppModule.ts b/src/time/demo/src/app/AppModule.ts new file mode 100644 index 0000000..1771188 --- /dev/null +++ b/src/time/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TimeTestModule } from './time/TimeTestModule'; + +@NgModule({ + imports: [ + TimeTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/time/demo/src/app/IndexComponent.ts b/src/time/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..7c186a4 --- /dev/null +++ b/src/time/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TimeTestModule } from './time/TimeTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TimeTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/time/demo/src/app/app.html b/src/time/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/time/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/time/demo/src/app/time/TimeCleariconComponent.ts b/src/time/demo/src/app/time/TimeCleariconComponent.ts new file mode 100644 index 0000000..08f04ac --- /dev/null +++ b/src/time/demo/src/app/time/TimeCleariconComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './time-clearicon.html' +}) +export class TimeCleariconComponent { + timeValue: string = '10:10:10'; +} diff --git a/src/time/demo/src/app/time/TimeDisabledComponent.ts b/src/time/demo/src/app/time/TimeDisabledComponent.ts new file mode 100644 index 0000000..02a3e3e --- /dev/null +++ b/src/time/demo/src/app/time/TimeDisabledComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './time-disabled.html' +}) +export class TimeDisabledComponent { + timeValue: string = '8:23:27'; +} diff --git a/src/time/demo/src/app/time/TimeEventComponent.ts b/src/time/demo/src/app/time/TimeEventComponent.ts new file mode 100644 index 0000000..5feb8b1 --- /dev/null +++ b/src/time/demo/src/app/time/TimeEventComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './time-event.html' +}) +export class TimeEventComponent { + myLogs: Array = []; + timeValue: string = ''; + + ngModelChangeFn($event: any): void { + this.myLogs = [...this.myLogs, `ngModelChange model=${$event}`]; + } +} diff --git a/src/time/demo/src/app/time/TimeFormatComponent.ts b/src/time/demo/src/app/time/TimeFormatComponent.ts new file mode 100644 index 0000000..5ffd6e7 --- /dev/null +++ b/src/time/demo/src/app/time/TimeFormatComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './time-format.html' +}) +export class TimeFormatComponent { + timeValue: string = '8:23:27'; + timeValue1: string = '8:23:27'; + timeValue2: string = '8:23:27'; + timeValue3: string = '8:23:27'; + format: string = 'HH:mm:ss'; + format1: string = 'HH:mm'; + format2: string = 'HH'; + format3: string = 'mm:ss'; +} diff --git a/src/time/demo/src/app/time/TimeMaxComponent.ts b/src/time/demo/src/app/time/TimeMaxComponent.ts new file mode 100644 index 0000000..3195290 --- /dev/null +++ b/src/time/demo/src/app/time/TimeMaxComponent.ts @@ -0,0 +1,40 @@ +import { Component, OnInit } from '@angular/core'; +import { AbstractControl, FormBuilder, FormControl, FormGroup } from '@angular/forms'; +@Component({ + templateUrl: './time-max.html' +}) +export class TimeMaxComponent implements OnInit { + timeValue: string = 'sss'; + disabled: boolean = false; + timeId: string = 'timeId'; + min: string = '8:23:27'; + max: string = '21:45:47'; + format: string = 'HH:mm:ss'; + timeForm: FormGroup; + timeBaseValue: string = '18:22:00'; + constructor(private fb: FormBuilder) {} + ngOnInit(): void { + this.timeForm = this.fb.group({ + timeValue: { value: this.timeBaseValue, disabled: this.disabled }, + text1Value: new FormControl(this.timeBaseValue), + text2Value: '1111' + }); + this.reactiveFormTimeValueChange(); + } + changeMaxValue(): void { + this.max = '9:10:21'; + } + changeDisable(): void { + this.disabled = !this.disabled; + } + // 响应式表单 对value的监控 + reactiveFormTimeValueChange(): void { + const spinnerValueControl: AbstractControl = this.timeForm.get('timeValue'); + spinnerValueControl.valueChanges.subscribe((value: string) => { + console.log(value); + this.timeForm.patchValue({ + text1Value: value + }); + }); + } +} diff --git a/src/time/demo/src/app/time/TimeMaxminComponent.ts b/src/time/demo/src/app/time/TimeMaxminComponent.ts new file mode 100644 index 0000000..cae6832 --- /dev/null +++ b/src/time/demo/src/app/time/TimeMaxminComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './time-maxmin.html' +}) +export class TimeMaxminComponent { + timeValue: string; + timeValue1: string; + max: string = '21:45:00'; + min: string = '21:45:00'; + format: string = 'HH:mm:ss'; +} diff --git a/src/time/demo/src/app/time/TimeMinComponent.ts b/src/time/demo/src/app/time/TimeMinComponent.ts new file mode 100644 index 0000000..ab55a77 --- /dev/null +++ b/src/time/demo/src/app/time/TimeMinComponent.ts @@ -0,0 +1,43 @@ +import { Component, OnInit } from '@angular/core'; +import { AbstractControl, FormBuilder, FormControl, FormGroup } from '@angular/forms'; +@Component({ + templateUrl: './time-min.html' +}) +export class TimeMinComponent implements OnInit { + timeValue: string = '9:12:45'; + min: string = '8:23:27'; + max: string = '21:45:47'; + format: string = 'HH:mm:ss'; + disabled: boolean = false; + timeForm: FormGroup; + timeBaseValue: string = '18:22:00'; + constructor(private fb: FormBuilder) {} + ngOnInit(): void { + this.timeForm = this.fb.group({ + timeValue: { value: this.timeBaseValue, disabled: this.disabled }, + text1Value: new FormControl(this.timeBaseValue), + text2Value: '1111' + }); + this.reactiveFormTimeValueChange(); + } + changeMinValue(): void { + this.min = '6:12:13'; + } + // 响应式表单 对value的监控 + reactiveFormTimeValueChange(): void { + const spinnerValueControl: AbstractControl = this.timeForm.get('timeValue'); + spinnerValueControl.valueChanges.subscribe((value: string) => { + console.log(value); + this.timeForm.patchValue({ + text1Value: value + }); + }); + } + + // 改变time禁用装态(动态表单形式) + changeFormGroupDisable(): void { + this.disabled = !this.disabled; + this.timeForm.controls['timeValue'].disable({ onlySelf: this.disabled }); + this.timeForm.controls['text1Value'].disable(); + } +} diff --git a/src/time/demo/src/app/time/TimeOptionDisabledComponent.ts b/src/time/demo/src/app/time/TimeOptionDisabledComponent.ts new file mode 100644 index 0000000..2d2c8d6 --- /dev/null +++ b/src/time/demo/src/app/time/TimeOptionDisabledComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './time-option-disabled.html' +}) +export class TimeOptionDisabledComponent { + timeValue: string = ''; + timeValue1: string = ''; + timeValue2: string = ''; + timeValue3: string = ''; + timeValue4: string = ''; + timeValue5: string = ''; + min: string = '8:23:27'; + max: string = '21:56:47'; + min1: string = '8:23:27'; + max1: string = '8:56:47'; + min2: string = '8:23:27'; + max2: string = '8:23:47'; + + changeMaxMinValue(): void { + this.max2 = '23:59:59'; + this.min2 = '00:00:00'; + } +} diff --git a/src/time/demo/src/app/time/TimePanelalignComponent.ts b/src/time/demo/src/app/time/TimePanelalignComponent.ts new file mode 100644 index 0000000..b5a83e9 --- /dev/null +++ b/src/time/demo/src/app/time/TimePanelalignComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './time-panelalign.html' +}) +export class TimePanelalignComponent { + timeValue: string = '10:10:10'; +} diff --git a/src/time/demo/src/app/time/TimeReactiveComponent.ts b/src/time/demo/src/app/time/TimeReactiveComponent.ts new file mode 100644 index 0000000..67dcf23 --- /dev/null +++ b/src/time/demo/src/app/time/TimeReactiveComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; + +@Component({ + templateUrl: './time-reactive.html' +}) +export class TimeReactiveComponent { + timeValue: string = ''; + timeValue1: string = ''; + format: string = 'HH:mm:ss'; + format1: string = 'HH:mm'; + timeGroup = new FormGroup({ + timeForm: new FormControl('timeValue'), + timeForm1: new FormControl('timeValue1') + }); +} diff --git a/src/time/demo/src/app/time/TimeTestModule.ts b/src/time/demo/src/app/time/TimeTestModule.ts new file mode 100644 index 0000000..2164380 --- /dev/null +++ b/src/time/demo/src/app/time/TimeTestModule.ts @@ -0,0 +1,94 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiButtonModule, TiTimeModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TimeValidationComponent } from './TimeValidationComponent'; +import { TimeFormatComponent } from './TimeFormatComponent'; +import { TimeMaxminComponent } from './TimeMaxminComponent'; +import { TimeEventComponent } from './TimeEventComponent'; +import { TimeCleariconComponent } from './TimeCleariconComponent'; +import { TimeOptionDisabledComponent } from './TimeOptionDisabledComponent'; +import { TimePanelalignComponent } from './TimePanelalignComponent'; +import { TimeReactiveComponent } from './TimeReactiveComponent'; +import { TimeDisabledComponent } from './TimeDisabledComponent'; +import { TimeMaxComponent } from './TimeMaxComponent'; +import { TimeMinComponent } from './TimeMinComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiValidationModule, + TiButtonModule, + TiTimeModule, + DemoLogModule, + RouterModule.forChild(TimeTestModule.ROUTES) + ], + declarations: [ + TimeFormatComponent, + TimeMaxminComponent, + TimeValidationComponent, + TimeEventComponent, + TimeCleariconComponent, + TimeOptionDisabledComponent, + TimePanelalignComponent, + TimeReactiveComponent, + TimeDisabledComponent, + TimeMaxComponent, + TimeMinComponent + ] +}) +export class TimeTestModule { + static readonly LINKS: Array = [{ href: 'components/TiTimeComponent.html', label: 'Time' }]; + static readonly ROUTES: Routes = [ + { + path: 'time/time-format', + component: TimeFormatComponent + }, + { + path: 'time/time-maxmin', + component: TimeMaxminComponent + }, + { + path: 'time/time-validation', + component: TimeValidationComponent + }, + { + path: 'time/time-event', + component: TimeEventComponent + }, + { + path: 'time/time-disabled', + component: TimeDisabledComponent + }, + { + path: 'time/time-clearicon', + component: TimeCleariconComponent + }, + { + path: 'time/time-panelalign', + component: TimePanelalignComponent + }, + { + path: 'time/time-option-disabled', + component: TimeOptionDisabledComponent + }, + { + path: 'time/time-reactive', + component: TimeReactiveComponent + }, + { + path: 'time/time-max', + component: TimeMaxComponent + }, + { + path: 'time/time-min', + component: TimeMinComponent + } + ]; +} diff --git a/src/time/demo/src/app/time/TimeValidationComponent.ts b/src/time/demo/src/app/time/TimeValidationComponent.ts new file mode 100644 index 0000000..a2c8661 --- /dev/null +++ b/src/time/demo/src/app/time/TimeValidationComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './time-validation.html' +}) +export class TimeValidationComponent { + timeValue: string = ''; +} diff --git a/src/time/demo/src/app/time/time-clearicon.html b/src/time/demo/src/app/time/time-clearicon.html new file mode 100644 index 0000000..d604fb1 --- /dev/null +++ b/src/time/demo/src/app/time/time-clearicon.html @@ -0,0 +1 @@ + diff --git a/src/time/demo/src/app/time/time-disabled.html b/src/time/demo/src/app/time/time-disabled.html new file mode 100644 index 0000000..0f6963a --- /dev/null +++ b/src/time/demo/src/app/time/time-disabled.html @@ -0,0 +1 @@ + diff --git a/src/time/demo/src/app/time/time-event.html b/src/time/demo/src/app/time/time-event.html new file mode 100644 index 0000000..ae4e380 --- /dev/null +++ b/src/time/demo/src/app/time/time-event.html @@ -0,0 +1,3 @@ + +
    + diff --git a/src/time/demo/src/app/time/time-format.html b/src/time/demo/src/app/time/time-format.html new file mode 100644 index 0000000..eda7cd3 --- /dev/null +++ b/src/time/demo/src/app/time/time-format.html @@ -0,0 +1,10 @@ + +
    +
    + +
    +
    + +
    +
    + diff --git a/src/time/demo/src/app/time/time-max.html b/src/time/demo/src/app/time/time-max.html new file mode 100644 index 0000000..34015aa --- /dev/null +++ b/src/time/demo/src/app/time/time-max.html @@ -0,0 +1,16 @@ +

    1.描述

    +

    max接口测试

    +

    2.示例

    +

    (2.1)max设置为合法时间时,以该值为最大值

    +
    +
    + +
    + +

    (2.2)动态变更

    +
    + +
    +
    + + diff --git a/src/time/demo/src/app/time/time-maxmin.html b/src/time/demo/src/app/time/time-maxmin.html new file mode 100644 index 0000000..fe6c860 --- /dev/null +++ b/src/time/demo/src/app/time/time-maxmin.html @@ -0,0 +1,4 @@ + +
    +
    + diff --git a/src/time/demo/src/app/time/time-min.html b/src/time/demo/src/app/time/time-min.html new file mode 100644 index 0000000..8821088 --- /dev/null +++ b/src/time/demo/src/app/time/time-min.html @@ -0,0 +1,17 @@ +

    1.描述

    +

    min接口测试

    +

    2.示例

    +

    (2.1)min设置为合法时间时,以该值为最小值

    +
    + +
    +
    +
    +
    +

    (2.2)动态变更

    +
    + +
    +
    + + diff --git a/src/time/demo/src/app/time/time-option-disabled.html b/src/time/demo/src/app/time/time-option-disabled.html new file mode 100644 index 0000000..946fcd4 --- /dev/null +++ b/src/time/demo/src/app/time/time-option-disabled.html @@ -0,0 +1,21 @@ +

    描述

    +

    时间面板时分秒禁用场景测试

    +

    示例

    +

    1.不设置最大最小值:

    + +

    +

    2.只设置最大值:

    + +

    +

    3.只设置最小值:

    + +

    +

    4.最大最小值都设置:

    + +

    +

    5.最大时与最小时相同:

    + +

    +

    6.最大时分与最小时分相同:

    + + diff --git a/src/time/demo/src/app/time/time-panelalign.html b/src/time/demo/src/app/time/time-panelalign.html new file mode 100644 index 0000000..f8bf9c0 --- /dev/null +++ b/src/time/demo/src/app/time/time-panelalign.html @@ -0,0 +1 @@ + diff --git a/src/time/demo/src/app/time/time-reactive.html b/src/time/demo/src/app/time/time-reactive.html new file mode 100644 index 0000000..dc2873e --- /dev/null +++ b/src/time/demo/src/app/time/time-reactive.html @@ -0,0 +1,6 @@ +
    + +
    +
    + +
    diff --git a/src/time/demo/src/app/time/time-validation.html b/src/time/demo/src/app/time/time-validation.html new file mode 100644 index 0000000..8447182 --- /dev/null +++ b/src/time/demo/src/app/time/time-validation.html @@ -0,0 +1 @@ + diff --git a/src/time/demo/src/app/time/webdoc/time-demos.js b/src/time/demo/src/app/time/webdoc/time-demos.js new file mode 100644 index 0000000..21bf4f3 --- /dev/null +++ b/src/time/demo/src/app/time/webdoc/time-demos.js @@ -0,0 +1,100 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'time-format', + name: { + 'zh-CN': '显示格式', + 'en-US': 'format' + }, + desc: { + 'zh-CN': '

    通过属性format配置时间显示的格式。包括 HH:mm:ss、HH:mm、HH、mm:ss 四种格式。

    ', + 'en-US': '

    format

    ' + }, + apis: ['TiTimeComponent.properties.format'] + }, + { + demoId: 'time-maxmin', + name: { + 'zh-CN': '预设范围', + 'en-US': 'maxmin' + }, + desc: { + 'zh-CN': + '

    通过属性max配置可以选择或输入的合法的最大时间。通过属性min配置可以选择或输入的合法的最小时间。

    ', + 'en-US': '

    maxmin

    ' + }, + apis: ['TiTimeComponent.properties.max', 'TiTimeComponent.properties.min'] + }, + { + demoId: 'time-clearicon', + name: { + 'zh-CN': '清除功能', + 'en-US': 'clear' + }, + desc: { + 'zh-CN': '

    通过属性clearIcon配置是否存在清除功能。

    ', + 'en-US': '

    clear

    ' + }, + apis: ['TiTimeComponent.properties.clearIcon'] + }, + { + demoId: 'time-panelalign', + name: { + 'zh-CN': '面板对齐方式', + 'en-US': 'panelalign' + }, + desc: { + 'zh-CN': + '

    通过属性panelAlign配置时间下拉面板与时间框的对齐方式,包括left(默认)、right

    ', + 'en-US': '

    panelalign

    ' + }, + apis: ['TiTimeComponent.properties.panelAlign'] + }, + { + demoId: 'time-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event' + }, + desc: { + 'zh-CN': '

    当时间框内值改变的时候触发ngModelChange事件。

    ', + 'en-US': '

    event

    ' + } + }, + { + demoId: 'time-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled' + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': 'disabled' + } + }, + { + demoId: 'time-reactive', + name: { + 'zh-CN': '响应式表单', + 'en-US': 'reactive' + }, + desc: { + 'zh-CN': '

    响应式表单中时间选择器的最简用法。

    ', + 'en-US': '

    time-reactive

    ' + } + }, + { + demoId: 'time-validation', + name: { + 'zh-CN': '校验', + 'en-US': 'validation' + }, + desc: { + 'zh-CN': '

    通过指令tiValidation实现校验。

    ', + 'en-US': '

    validation

    ' + } + } + ] +}; diff --git a/src/time/demo/src/app/time/webdoc/time.cn.md b/src/time/demo/src/app/time/webdoc/time.cn.md new file mode 100644 index 0000000..5013e73 --- /dev/null +++ b/src/time/demo/src/app/time/webdoc/time.cn.md @@ -0,0 +1,23 @@ +--- +title: Time 时间 +--- +# Time 时间 + +
    + +Time 是输入或者选择时间的控件。   + +```typescript +import { TiTimeModule } from '@opentiny/ng'; +``` + +
    + +
    + +Time 是输入或者选择时间的控件。   + +```typescript +import { TiTimeModule } from '@opentiny/ng'; +``` +
    \ No newline at end of file diff --git a/src/time/demo/src/app/time/webdoc/time.en.md b/src/time/demo/src/app/time/webdoc/time.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/time/demo/src/app/time/webdoc/time.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/time/demo/src/favicon.ico b/src/time/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/time/demo/src/index.html b/src/time/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/time/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/time/demo/src/main.ts b/src/time/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/time/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/time/demo/test.ts b/src/time/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/time/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/time/demo/tsconfig.app.json b/src/time/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/time/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/time/demo/tsconfig.spec.json b/src/time/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/time/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/time/lib/index.ts b/src/time/lib/index.ts new file mode 100644 index 0000000..7f54dcb --- /dev/null +++ b/src/time/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTimeModule'; diff --git a/src/time/lib/ng-package.json b/src/time/lib/ng-package.json new file mode 100644 index 0000000..5802f0c --- /dev/null +++ b/src/time/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/time", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/time/lib/package.json b/src/time/lib/package.json new file mode 100644 index 0000000..4bc7df6 --- /dev/null +++ b/src/time/lib/package.json @@ -0,0 +1,18 @@ +{ + "name": "@opentiny/ng-time", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-datedominator": "~1.0.0-beta.0", + "@opentiny/ng-list": "~1.0.0-beta.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-button": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/time/lib/project.json b/src/time/lib/project.json new file mode 100644 index 0000000..7c99349 --- /dev/null +++ b/src/time/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/time/lib", + "sourceRoot": "src/time/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/time"], + "options": { + "project": "src/time/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/time"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js time" + }, + { + "command": "ng default-build time" + }, + { + "command": "node build/clear-default-theme.js time" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/time && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build time && ng pack time && node build/publish.js time --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/time/lib/src/TiTimeComponent.ts b/src/time/lib/src/TiTimeComponent.ts new file mode 100644 index 0000000..9051d82 --- /dev/null +++ b/src/time/lib/src/TiTimeComponent.ts @@ -0,0 +1,943 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + HostListener, + Inject, + Input, + NgZone, + Renderer2, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { TiDateUtil, TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiLocaleFormat } from '@opentiny/ng-locale'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiDropComponent } from '@opentiny/ng-drop'; +import { DOCUMENT } from '@angular/common'; +import { TiListComponent } from '@opentiny/ng-list'; +import packageInfo from '../package.json'; +/** + * @ignore + */ +export interface TimeField { + hour: string; + minute: string; + second: string; +} + +/** + * @ignore + */ +export interface TiComputingParams { + max: number; + min: number; + needAddZero: boolean; +} + +/** + * Time时间组件 + * + * Time组件提供了一种方便的显示和设置时间的方式。 + * + */ +@Component({ + selector: 'ti-time', + templateUrl: './time.html', + styleUrls: ['./time.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiTimeComponent)], + host: { + '[class.ti3-time-container]': 'true', + '(blur)': 'onBlur()' + } +}) +export class TiTimeComponent extends TiFormComponent { + /** + * 时间显示格式。各个时间段格式规则; 默认值:'HH:mm:ss' + * + * 1.小时可以设置为: + * + * HH —— 24 小时制,两位数字表示小时(00-23) + * + * H —— 24 小时制,开头不补零数字表示小时(0-23) + * + * 2.分钟可以设置为: + * + * mm —— 两位数字表示分钟值(00-59) + * + * m —— 开头不补零数字表示分钟值(0-59) + * + * 3.秒可以设置为: + * + * ss —— 两位数字表示秒值(00-59) + * + * s —— 开头不补零数字表示秒值(0-59) + * + * 说明:开头补零是指当前时间是个位数字时,前边补零,始终保持两位数字 + */ + @Input() format: string; + /** + * 时间最大值,默认值:'23:59:59' + */ + @Input() max: string; + /** + * 时间最小值,默认值:'00:00:00' + */ + @Input() min: string; + /** + * 是否展示清除时间图标(默认显示) + */ + @Input() clearIcon: boolean = true; + /** + * 面板对齐方式 + */ + @Input() panelAlign: 'left' | 'right' = 'left'; + /** + * @ignore + */ + @ViewChild('dominator', { static: true }) private containerRef: ElementRef; + /** + * @ignore + */ + @ViewChild(TiDropComponent, { static: true }) dropCom: TiDropComponent; + /** + * @ignore + */ + @ViewChild('text', { static: true }) textCom: ElementRef; + /** + * @ignore + */ + @ViewChild('input', { static: true }) inputEle: ElementRef; + /** + * @ignore + */ + @ViewChild('button', { static: true }) buttonEle: ElementRef; + /** + * @ignore + * time编辑框内部value值 + */ + public inputValue: string = ''; + /** + * @ignore + * dominator最终显示时间值 + */ + public timeValue: string = ''; + /** + * @ignore + */ + public dominatorCom: ElementRef; + /** + * @ignore + * 时间下拉面板宽度,根据时间格式宽度各异 + */ + public timePanelWidth: number = 284; + /** + * @ignore + * 底部确认按钮是否禁用 + */ + public buttonDisabled: boolean = true; + /** + * @ignore + * placeholder提示文本 + */ + public placeholder: string; + /** + * @ignore + */ + public hourOptions: Array; + /** + * @ignore + */ + public minuteOptions: Array; + /** + * @ignore + */ + public secondOptions: Array; + /** + * @ignore + */ + public selectedHour: any; + /** + * @ignore + */ + public selectedMinute: any; + /** + * @ignore + */ + public selectedSecond: any; + /** + * @ignore + */ + public hourScroll: number; + /** + * @ignore + */ + public minuteScroll: number; + /** + * @ignore + */ + public secondScroll: number; + /** + * @ignore + * 只展示小时段 + */ + public onlyHour: boolean; + /** + * @ignore + * 只展示时分段 + */ + public onlyHourMinute: boolean; + /** + * @ignore + * 只展示分秒段 + */ + public onlyMinuteSecond: boolean; + /** + * @ignore + */ + public oldInputValue: string = ''; + /** + * @ignore + * 面板与dominator的距离 + */ + public dominatorSpace: string = '-30px'; + /** + * @ignore + * 是否清除 + */ + public isClearClick: boolean; + protected versionInfo: string = super.getVersion(packageInfo); + // 默认最大最小值 + private config: { min: string; max: string } = { + min: '00:00:00', + max: '23:59:59' + }; + private documentKeydownListener: () => void; + + constructor( + protected hostRef: ElementRef, + protected renderer2: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + private zone: NgZone, + @Inject(DOCUMENT) private document + ) { + super(hostRef, renderer2, changeDetectorRef); + } + + /** + * @description: 若时间字符串没有一个冒号时处理成合法的事件字符串(加冒号) + * @param value 时间字符串 + */ + private static addColon(value: any): string { + // new Date()时,时间中没有一个冒号得到的是非法时间 + if (value.match(/:/)) { + return value; + } + let ampm: string = value.match(/am|AM|pm|PM/); + ampm = ampm || ''; + + return `${parseInt(value, 10)}: ${ampm}`; + } + + /** + * @description 判读字符串是否是合法的时间 + * @param: time: 校验的时间 + */ + private static isValidTime(time: string): boolean { + // value非字符串或者为空字符串时,为非法时间 + if (!Util.isString(time) || time.trim() === '') { + return false; + } + + const date: any = new Date(`2018/5/15 ${TiTimeComponent.addColon(time)}`); + + // any类型是为了防止编译报错 Date类型不能和string类型比较 + return !(String(date) === 'Invalid Date'); + } + + /** + * @description: hh/HH/mm/ss 时间格式时,显示的值处理成2位,不足2位补0 + * @param: num : 各个时间显示框的数值 + * @param: length : 需要处理之后的长度 + */ + private static addZero(num: number, length: number): string { + const zeroNum: string = `00${num}`; + + return zeroNum.substr(zeroNum.length - length, length); + } + /** + * @description newVal参数为合法值时返回newVal,否则返回defaultValue参数 + * @param: newVal:新值 + * @param: defaultValue:默认值 + */ + private static verifyTime(newVal: string, defaultValue: string): string { + return TiTimeComponent.isValidTime(newVal) ? newVal : defaultValue; + } + /** + * @description 比较value1和value2两个时间值大小.value1 < value2时,返回true;否则返回false ; + * @param: value1:比较的前一个值 + * @param: value2:比较的后一个值 + */ + private static isSmaller(value1: string, value2: string): boolean { + return Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value1)}`) < Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value2)}`); + } + + /** + * @description 比较value1和value2两个时间值大小.value1 > value2时,返回true;否则返回false ; + * @param: value1:比较的前一个值 + * @param: value2:比较的后一个值 + */ + private static isBigger(value1: string, value2: string): boolean { + return Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value1)}`) > Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value2)}`); + } + + /** + * @description 比较value1和value2两个时间值大小.value1 <= value2时,返回true;否则返回false ; + * @param: value1:比较的前一个值 + * @param: value2:比较的后一个值 + */ + private static isSmallerOrEqual(value1: string, value2: string): boolean { + return Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value1)}`) <= Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value2)}`); + } + + /** + * @description 比较value1和value2两个时间值大小.value1 >= value2时,返回true;否则返回false ; + * @param: value1:比较的前一个值 + * @param: value2:比较的后一个值 + */ + private static isBiggerOrEqual(value1: string, value2: string): boolean { + return Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value1)}`) >= Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value2)}`); + } + + // 组件声明周期钩子--start + ngOnInit(): void { + super.ngOnInit(); + // 初始化变量 + this.initVariable(); + + this.showTimeWithFormat(); + + // 最大最小值校验 + this.validateMaxAndMin(); + + this.setTimeOptions(); + + // 时间框提示文本按照小写显示 + this.placeholder = this.format.toLowerCase(); + + this.zone.runOutsideAngular(() => { + // 避免不停触发变化检测 + // document上的Ecs快捷键功能 + this.documentKeydownListener = this.renderer2.listen(this.document, 'keydown', this.keydownHandlerFn); + }); + } + + private setTimeOptions(): void { + this.hourOptions = this.setOptions(24, 'hour'); + this.minuteOptions = this.setOptions(60, 'minute'); + this.secondOptions = this.setOptions(60, 'second'); + } + // 根据格式format判断面板展示情况 + private showTimeWithFormat(): void { + // 纯小时 + if (this.format.indexOf('m') === -1) { + this.onlyHour = true; + } else if (this.format.indexOf('s') === -1) { + // 仅时分 + this.onlyHourMinute = true; + } else if (this.format.indexOf('H') === -1) { + // 仅分秒 + this.onlyMinuteSecond = true; + } + } + + private initVariable(): void { + // 1. 时间格式校验 + this.format = Util.isString(this.format) ? this.format : TiDateUtil.DEFAULT_TIME_FORMAT; + } + + private validateMaxAndMin(): void { + this.config.max = this.formatTime(this.config.max); + this.config.min = this.formatTime(this.config.min); + // 最大值合法性校验 + this.max = TiTimeComponent.verifyTime(this.formatTime(this.max), this.config.max); + // 最小值合法性校验 + this.min = TiTimeComponent.verifyTime(this.formatTime(this.min), this.config.min); + // 最大最小值矛盾时,设置为默认值 + if (TiTimeComponent.isSmaller(this.max, this.min)) { + this.min = this.config.min; + this.max = this.config.max; + } + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + // 1.0 minValue监控 + if (changes['min'] && !changes['min'].isFirstChange()) { + this.min = this.formatTime(changes['min'].currentValue); + // 新minValue值非法时,恢复到之前值 + if (!this.isValidMinValue(this.min)) { + this.min = changes['min'].previousValue; + + return; + } + // 对value值进行最小值校验 + // setTimeout不能去掉,在onpush环境没问题,但是在default环境会报错 + // (ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked) + setTimeout(() => { + const model: string = this.addHour(this.model); + if (TiTimeComponent.isValidTime(model) && TiTimeComponent.isSmaller(model, this.min)) { + this.model = this.min; + this.formatValue(); + } + this.setDisableData(true); + this.changeDetectorRef.markForCheck(); + }, 0); + } + // 2.0 maxValue监控 + if (changes['max'] && !changes['max'].isFirstChange()) { + this.max = this.formatTime(changes['max'].currentValue); + // 新maxValue值非法时,恢复到之前值 + if (!this.isValidMaxValue(this.max)) { + this.max = changes['max'].previousValue; + + return; + } + // 对value值进行最小值校验 + // setTimeout不能去掉,在onpush环境没问题,但是在default环境会报错 + // (ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked) + setTimeout(() => { + const model: string = this.addHour(this.model); + if (TiTimeComponent.isValidTime(model) && TiTimeComponent.isBigger(model, this.max)) { + this.model = this.max; + this.formatValue(); + } + this.setDisableData(true); + this.changeDetectorRef.markForCheck(); + }, 0); + } + // 3.0 format监控 + if (changes['format'] && !changes['format'].isFirstChange()) { + // 新format值非法时,恢复到之前值 + if (!Util.isString(changes['format'].currentValue)) { + this.format = changes['format'].previousValue; + + return; + } + this.showTimeWithFormat(); + if (TiTimeComponent.isValidTime(this.addHour(this.model))) { + this.formatValue(); + } + } + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + this.dominatorCom = this.containerRef.nativeElement; + this.setFocusableElems( + [this.dominatorCom] + .concat(this.dropCom.nativeElement) + .concat(this.textCom.nativeElement) + .concat(this.inputEle.nativeElement) + .concat(this.buttonEle.nativeElement) + ); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + if (this.documentKeydownListener) { + this.documentKeydownListener(); + } + } + // 组件声明周期钩子--end + + // 实现ControlValueAccessor接口 + /** + * @ignore + */ + writeValue(value: any): void { + super.writeValue(value); + if (value === '' || !this.isValidValue(value)) { + this.model = ''; + this.buttonDisabled = true; + if (this.timeValue) { + this.timeValue = ''; + this.inputValue = ''; + this.oldInputValue = ''; + this.setSelectVal(); + } + + this.setDisableData(); + + return; + } + this.buttonDisabled = false; + this.model = value; + this.formatValue(); + this.setDisableData(); + } + // 实现ControlValueAccessor接口--end + /** + * @ignore + * @description: 组件整体失焦之后处理函数 + */ + public onBlur(): void { + // 失焦后下拉关闭 + this.hidePanel(); + this.inputValue = this.model; + this.setDisableData(); + this.setSelectVal(); + this.getScrollData(); + this.oldInputValue = this.inputValue; + } + // 面板延迟200ms关闭,用户输入新值之后,直接点击确认按钮关闭面板时, + // 面板内部选中项先刷新 再关闭 200ms的延迟 用户可以看到这样一个视觉的过程 + // 200ms通过本地用例,跟规范侧共同决定的一个延时数值 + private hidePanel(): void { + setTimeout(() => { + this.dropCom.hide(); + // 初始化数据,保证有时间值时,初次打开数据定位在顶部 + this.hourScroll = -1; + this.minuteScroll = -1; + this.secondScroll = -1; + }, 200); + } + /** + * @ignore + * 点击dominator打开面板 + */ + public onShowClick(): void { + if (this.disabled) { + return; + } + if (this.isClearClick) { + this.isClearClick = false; + return; + } + this.getScrollData(); + this.dropCom.show(); + this.textCom.nativeElement.focus(); + this.buttonDisabled = !this.model; + } + /** + * @ignore + * 点击确认按钮关闭面板 + */ + public okClickFn(): void { + this.timeValue = this.inputValue; + this.model = this.inputValue; + // 记录旧值,当输入非法时,返回之前合法值 + this.oldInputValue = this.inputValue; + this.containerRef.nativeElement.focus(); + this.hidePanel(); + } + /** + * @ignore + * 时间面板点击选择事件 + * @param val + * @param title + */ + public onSelect(val: any, title: string): void { + // 禁用状态 + if (val.disabled) { + return; + } + let arr: Array = []; + if (this.onlyHour) { + arr = ['00']; + } else if (this.onlyHourMinute) { + arr = ['00', '00']; + } else { + arr = ['00', '00', '00']; + } + const timeArr: Array = this.inputValue === '' ? arr : this.addHour(this.inputValue).split(':'); + switch (title) { + case 'hour': + timeArr[0] = val.label; + timeArr[1] = timeArr[1] ? timeArr[1] : '00'; + timeArr[2] = timeArr[2] ? timeArr[2] : '00'; + this.hourScroll = val.label * 30; + break; + case 'minute': + timeArr[1] = val.label; + timeArr[2] = timeArr[2] ? timeArr[2] : '00'; + this.minuteScroll = val.label * 30; + break; + case 'second': + timeArr[2] = val.label; + this.secondScroll = val.label * 30; + break; + default: + break; + } + + // 仅有分秒时,去除时 + if (this.onlyMinuteSecond) { + timeArr.shift(); + } + + this.inputValue = timeArr.join(':'); + this.setDisableData(); + this.setSelectVal(); + this.oldInputValue = this.inputValue; + this.buttonDisabled = false; + } + + private setSelectVal(): void { + let value: string = this.addHour(this.inputValue); + + if (!TiTimeComponent.isValidTime(value)) { + this.selectedHour = null; + this.selectedMinute = null; + this.selectedSecond = null; + return; + } + const newtimeArr: Array = this.validateValue(value).split(':'); + const hourVal: string = TiTimeComponent.addZero(Number(newtimeArr[0]), 2); + const minuteVal: string = TiTimeComponent.addZero(Number(newtimeArr[1]), 2); + const secondVal: string = TiTimeComponent.addZero(Number(newtimeArr[2]), 2); + this.selectedHour = this.getSelectedVal(this.hourOptions, hourVal); + this.selectedMinute = this.getSelectedVal(this.minuteOptions, minuteVal); + this.selectedSecond = this.getSelectedVal(this.secondOptions, secondVal); + // 失焦之后,个位的数字需补零 + if (this.onlyHour) { + this.inputValue = hourVal; + } else if (this.onlyHourMinute) { + this.inputValue = hourVal + ':' + minuteVal; + } else if (this.onlyMinuteSecond) { + this.inputValue = minuteVal + ':' + secondVal; + } else { + this.inputValue = hourVal + ':' + minuteVal + ':' + secondVal; + } + this.getScrollData(); + } + + private keydownHandlerFn = (event: KeyboardEvent): void => { + if (event.keyCode === TiKeymap.KEY_ESCAPE) { + this.hidePanel(); + } + }; + /** + * @ignore + * 组件快捷键处理tab键 enter键 + */ + @HostListener('keydown', ['$event']) public onKeydown(event: KeyboardEvent): void { + switch (event.keyCode) { + case TiKeymap.KEY_ENTER: // ENTER键(大键盘) + case TiKeymap.KEY_NUMPAD_ENTER: // ENTER键(数字小键盘) + this.responseEnter(); + break; + default: + break; + } + } + /** + * @ignore + * enter键的功能:如果面板展开不处理,面板收起则展开,设置datePanel指令的接口值 + */ + public responseEnter(): void { + if (this.dropCom.isShow) { + return; + } + this.getScrollData(); + // 时间面板展开 + this.dropCom.show(); + this.textCom.nativeElement.focus(); + } + /** + * @ignore + * 输入为中文冒号时,自动转换为英文冒号 + * @param val + */ + public onInputChangeFn(val: any): void { + if (!Util.isEmptyString(val)) { + let value: string = val; + value = value.replace(':', ':'); + this.inputValue = value; + } else { + this.buttonDisabled = true; + } + } + + /** + * @ignore + * 时间框enter事件 + */ + public timeKeydownFn(event: KeyboardEvent, val: string): void { + if (event.keyCode === TiKeymap.KEY_ENTER || event.keyCode === TiKeymap.KEY_NUMPAD_ENTER) { + this.timeBlur(val); + } + } + + /** + * @ignore + * + */ + public timeBlur(val: string): void { + if (val === '') { + this.buttonDisabled = true; + this.inputValue = ''; + this.oldInputValue = this.inputValue; + this.getScrollData(); + + return; + } + + const timeArr: Array = val.split(TiDateUtil.COLON_REGEXP); + const formatArr: Array = this.format.split(':'); + const datetime: Date = new Date(`2022/01/28 ${TiDateUtil.addColon(this.addHour(val))}`); + // 用户输入与format不匹配或当前时间是非法值时,都按照非法字符处理 + if (timeArr.length !== formatArr.length || String(datetime) === 'Invalid Date') { + this.inputValue = this.oldInputValue; + this.buttonDisabled = Util.isEmptyString(this.inputValue); + + return; + } + + // 时分秒是负数或不是数字时 + timeArr.forEach((item: any) => { + if (item < 0 || isNaN(Number(item))) { + this.inputValue = this.oldInputValue; + this.buttonDisabled = Util.isEmptyString(this.inputValue); + + return; + } + }); + this.setDisableData(); + this.setSelectVal(); + this.getScrollData(); + this.oldInputValue = this.inputValue; + this.buttonDisabled = false; + } + // 获取滚动值 + private getScrollData(): void { + if (Util.isEmptyString(this.inputValue)) { + this.hourScroll = 0; + this.minuteScroll = 0; + this.secondScroll = 0; + } else { + const timeArr: Array = this.addHour(this.inputValue).split(TiDateUtil.COLON_REGEXP); + this.hourScroll = timeArr[0] * 30; + this.minuteScroll = timeArr[1] * 30; + this.secondScroll = timeArr[2] * 30; + } + } + /** + * @ignore + * 清除 + */ + public onIconClearClick(): void { + if (this.disabled || !this.clearIcon) { + return; + } + this.inputValue = ''; + this.oldInputValue = ''; + this.timeValue = ''; + this.model = ''; + this.selectedHour = null; + this.selectedMinute = null; + this.selectedSecond = null; + this.buttonDisabled = true; + this.isClearClick = true; + this.setDisableData(); + } + + private getSelectedVal(options: Array, val: any): Function { + return options.find((item: any) => { + if (!item.disabled && item.label === val) { + return item; + } else { + return null; + } + }); + } + private setOptions(num: number, labelKey?: string): Array { + const options: Array = []; + for (let i: number = 0; i < num; i++) { + options[i] = { + label: TiTimeComponent.addZero(i, 2), + disabled: false + }; + } + + return options; + } + + private setDisableData(isChange?: boolean): boolean { + if (this.max === this.config.max && this.min === this.config.min && !isChange) { + return false; + } + const maxArr: Array = this.max.split(':'); + const minArr: Array = this.min.split(':'); + const value: string = this.addHour(this.inputValue); + const timeArr: Array = this.validateValue(value).split(':'); + // 根据最大最小值判定小时禁用项 + this.hourOptions.forEach((item: any) => { + item.disabled = Number(item.label) < Number(minArr[0]) || Number(item.label) > Number(maxArr[0]); + }); + + if (Util.isNumber(Number(timeArr[0])) && Number(minArr[0]) === Number(maxArr[0])) { + // 最大小时最小小时相等情况,需考虑分钟禁用 + this.minuteOptions.forEach((minuteItem: any) => { + minuteItem.disabled = Number(minuteItem.label) < Number(minArr[1]) || Number(minuteItem.label) > Number(maxArr[1]); + }); + } else if (Number(timeArr[0]) === Number(minArr[0])) { + // 最小小时情况,需考虑分钟禁用 + this.minuteOptions.forEach((minuteItem: any) => { + minuteItem.disabled = Number(minuteItem.label) < Number(minArr[1]); + }); + } else if (Number(timeArr[0]) === Number(maxArr[0])) { + // 最大小时情况,需考虑分钟禁用 + this.minuteOptions.forEach((minuteItem: any) => { + minuteItem.disabled = Number(minuteItem.label) > Number(maxArr[1]); + }); + } else { + this.minuteOptions.forEach((minuteItem: any) => { + minuteItem.disabled = false; + }); + } + + if (Util.isNumber(Number(timeArr[1])) && Number(minArr[0]) === Number(maxArr[0]) && Number(minArr[1]) === Number(maxArr[1])) { + // 最大小时分最小小时分相等情况,需考虑分钟禁用 + this.secondOptions.forEach((secondItem: any) => { + secondItem.disabled = Number(secondItem.label) < Number(minArr[2]) || Number(secondItem.label) > Number(maxArr[2]); + }); + } else if (Number(timeArr[0]) === Number(minArr[0]) && Number(timeArr[1]) === Number(minArr[1])) { + // 最小时分情况,需考虑秒数禁用 + this.secondOptions.forEach((secondItem: any) => { + secondItem.disabled = Number(secondItem.label) < Number(minArr[2]); + }); + } else if (Number(timeArr[0]) === Number(maxArr[0]) && Number(timeArr[1]) === Number(maxArr[1])) { + // 最大时分情况,需考虑秒数禁用 + this.secondOptions.forEach((secondItem: any) => { + secondItem.disabled = Number(secondItem.label) > Number(maxArr[2]); + }); + } else { + this.secondOptions.forEach((secondItem: any) => { + secondItem.disabled = false; + }); + } + + // 需要修改配置对象的引用,实现下拉禁用状态及时刷新 + this.hourOptions = [...this.hourOptions]; + this.minuteOptions = [...this.minuteOptions]; + this.secondOptions = [...this.secondOptions]; + } + + /** + * @ignore + * 时间选择框部分鼠标移出时去除hover样式 + */ + public onMouseleave(listCom: TiListComponent): void { + listCom.hoverOption = null; + } + // 组件交互方法集合--end + + // 内部公共方法集合--start + /** + * 校正当前时间为最大值或最小值 + * @param value 时间值 + * @returns + */ + private validateValue(value: string): string { + let timeValue: string = value; + + if (TiTimeComponent.isSmaller(timeValue, this.min)) { + timeValue = this.min; + } + + if (TiTimeComponent.isBigger(timeValue, this.max)) { + timeValue = this.max; + } + + return timeValue; + } + + /** + * @description: 时间格式化及时间各个框显示值设置 + */ + private formatValue(): void { + // 格式化value + const date: Date = new Date(`2018/5/15 ${TiTimeComponent.addColon(this.model)}`); + const formatStr: string = TiLocaleFormat.formatTime(date, this.format); + if (this.model !== formatStr) { + this.model = formatStr; + } + this.timeValue = this.model; + this.inputValue = this.model; + this.oldInputValue = this.inputValue; + const timeArr: Array = this.addHour(this.inputValue).split(':'); + this.selectedHour = this.getSelectedVal(this.hourOptions, TiTimeComponent.addZero(Number(timeArr[0]), 2)); + this.selectedMinute = this.getSelectedVal(this.minuteOptions, TiTimeComponent.addZero(Number(timeArr[1]), 2)); + this.selectedSecond = this.getSelectedVal(this.secondOptions, TiTimeComponent.addZero(Number(timeArr[2]), 2)); + } + /** + * @description: 校验动态更新的minValue是否是合法值 + * @param: minValue:动态更新的传入的minValue + */ + private isValidMinValue(minValue: string): boolean { + return TiTimeComponent.isValidTime(minValue) && TiTimeComponent.isSmallerOrEqual(minValue, this.max); + } + + /** + * @description: 校验动态更新的maxValue是否是合法值 + * @param: maxValue:动态更新的传入的maxValue + */ + private isValidMaxValue(maxValue: string): boolean { + return TiTimeComponent.isValidTime(maxValue) && TiTimeComponent.isBiggerOrEqual(maxValue, this.min); + } + + /** + * @description: 判断动态更新写入组件的value值是否是一个合法值 + * @param: value 动态更新写入组件的value值 + */ + private isValidValue(value: string): boolean { + const time: string = this.formatTime(value); + + return ( + TiTimeComponent.isValidTime(time) && + TiTimeComponent.isBiggerOrEqual(time, this.min) && + TiTimeComponent.isSmallerOrEqual(time, this.max) + ); + } + + /** + * @description: 格式化时间值 + * @param: value 值 + */ + private formatTime(value: string): string { + if (!value) { + return value; + } + const date: Date = new Date(`2018/5/15 ${TiTimeComponent.addColon(value)}`); + + if (String(date) === 'Invalid Date') { + return value; + } + + return this.addHour(TiLocaleFormat.formatTime(date, this.format)); + } + + /** + * 若当前格式是 mm:ss,补充小时位 + * @param value 时间值 + * @returns + */ + private addHour(value: string): string { + return this.onlyMinuteSecond && value ? `00:${value}` : value; + } + // 内部公共方法集合--end +} diff --git a/src/time/lib/src/TiTimeModule.ts b/src/time/lib/src/TiTimeModule.ts new file mode 100644 index 0000000..d2a31c6 --- /dev/null +++ b/src/time/lib/src/TiTimeModule.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiTimeComponent } from './TiTimeComponent'; +import { TiDateDominatorModule } from '@opentiny/ng-datedominator'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiListModule } from '@opentiny/ng-list'; +import { TiLocaleModule, TiLocale } from '@opentiny/ng-locale'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiButtonModule } from '@opentiny/ng-button'; +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiButtonModule, TiDropModule, TiListModule, TiDateDominatorModule, TiLocaleModule, TiTextModule], + exports: [TiTimeComponent], + declarations: [TiTimeComponent] +}) +export class TiTimeModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiTimeComponent } from './TiTimeComponent'; diff --git a/src/time/lib/src/i18n/TiTimeWords.ts b/src/time/lib/src/i18n/TiTimeWords.ts new file mode 100644 index 0000000..17984e1 --- /dev/null +++ b/src/time/lib/src/i18n/TiTimeWords.ts @@ -0,0 +1,7 @@ +export interface TiTimeWords { + tiTime: { + hour: string; + minute: string; + second: string; + }; +} diff --git a/src/time/lib/src/i18n/en_US.ts b/src/time/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..dfa9417 --- /dev/null +++ b/src/time/lib/src/i18n/en_US.ts @@ -0,0 +1,9 @@ +import { TiTimeWords } from './TiTimeWords'; + +export const en_US: TiTimeWords = { + tiTime: { + hour: 'Hour', + minute: 'Minute', + second: 'Second' + } +}; diff --git a/src/time/lib/src/i18n/es_US.ts b/src/time/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..a1e0eec --- /dev/null +++ b/src/time/lib/src/i18n/es_US.ts @@ -0,0 +1,9 @@ +import { TiTimeWords } from './TiTimeWords'; + +export const es_US: TiTimeWords = { + tiTime: { + hour: 'Hora', + minute: 'Minuto', + second: 'Segundo' + } +}; diff --git a/src/time/lib/src/i18n/fr_FR.ts b/src/time/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..095a6ac --- /dev/null +++ b/src/time/lib/src/i18n/fr_FR.ts @@ -0,0 +1,9 @@ +import { TiTimeWords } from './TiTimeWords'; + +export const fr_FR: TiTimeWords = { + tiTime: { + hour: 'Heure', + minute: 'Minute', + second: 'Second' + } +}; diff --git a/src/time/lib/src/i18n/index.ts b/src/time/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/time/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/time/lib/src/i18n/pt_BR.ts b/src/time/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..79f83cb --- /dev/null +++ b/src/time/lib/src/i18n/pt_BR.ts @@ -0,0 +1,9 @@ +import { TiTimeWords } from './TiTimeWords'; + +export const pt_BR: TiTimeWords = { + tiTime: { + hour: 'Hora', + minute: 'Minuto', + second: 'Segundo' + } +}; diff --git a/src/time/lib/src/i18n/zh_CN.ts b/src/time/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..52372bf --- /dev/null +++ b/src/time/lib/src/i18n/zh_CN.ts @@ -0,0 +1,9 @@ +import { TiTimeWords } from './TiTimeWords'; + +export const zh_CN: TiTimeWords = { + tiTime: { + hour: '时', + minute: '分', + second: '秒' + } +}; diff --git a/src/time/lib/src/time.html b/src/time/lib/src/time.html new file mode 100644 index 0000000..f007346 --- /dev/null +++ b/src/time/lib/src/time.html @@ -0,0 +1,93 @@ +{{ placeholder }} + + + + + +
    + +
    + {{ 'tiTime.hour' | tiTranslate }} + +
    + +
    + {{ 'tiTime.minute' | tiTranslate }} + +
    + +
    + {{ 'tiTime.second' | tiTranslate }} + +
    +
    +
    + +
    +
    diff --git a/src/time/lib/src/time.less b/src/time/lib/src/time.less new file mode 100644 index 0000000..57840b2 --- /dev/null +++ b/src/time/lib/src/time.less @@ -0,0 +1,89 @@ +@import '../../../themes/basic/base-all.less'; +@import '../../../themes/basic/compnent-container-border.less'; + +ti-drop { + --ti-time-select-title-height: 30px; +} + +:host.ti3-time-container :extend(.ti3-compnent-container-border all) { + width: var(--ti-common-size-30x); +} + +::ng-deep :root .ti3-time-drop-container { + &:focus { + outline: 0px; + } +} +:host.ti3-time-container { + display: inline-flex; + align-items: center; + font-size: var(--ti-common-font-size-base); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + height: var(--ti-common-size-7x); + line-height: normal; + background-color: var(--ti-common-color-bg-white-normal); + .box-sizing(border-box); + .user-select(); +} +.ti3-time-select-container { + display: flex; + justify-content: space-around; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); +} +.ti3-time-select-item-container { + width: 100%; + border-right: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); +} +.ti3-time-select-item-container:last-child { + border-right: none; +} +::ng-deep :root .ti3-time-select-list { + height: 230px; + width: 100%; + overflow-y: auto; + & li { + text-align: center; + margin-top: 0; + } +} +.ti3-dropdown-container.ti3-time-drop-container { + height: 365px; + padding: var(--ti-common-space-3x) var(--ti-common-space-4x); + z-index: 10002; +} +.ti3-time-edit { + display: block; + margin-bottom: var(--ti-common-space-3x); + width: 100%; +} + +::ng-deep .ti3-time-edit { + border-color: var(--ti-common-color-line-active) !important; +} + +.ti3-time-select-title { + width: 100%; + display: block; + margin-left: -8px; // ���������ȵ�һ�� + height: var(--ti-time-select-title-height); + line-height: var(--ti-time-select-title-height); + text-align: center; +} +.ti3-time-button-confirm { + margin-top: var(--ti-common-space-3x); + float: right; +} +::ng-deep .ti3-time-edit-with-button-disabled { + opacity: 0; +} +::ng-deep .ti3-time-edit-with-button-undisabled { + display: none; +} +.ti3-time-tab-input { + width: 0; + height: 0; + position: absolute; + top: -9999px; + left: -9999px; +} diff --git a/src/timeline/demo/karma.conf.js b/src/timeline/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/timeline/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/timeline/demo/project.json b/src/timeline/demo/project.json new file mode 100644 index 0000000..6ad9698 --- /dev/null +++ b/src/timeline/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/timeline/demo", + "sourceRoot": "src/timeline/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/timeline", + "index": "src/timeline/demo/src/index.html", + "main": "src/timeline/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/timeline/demo/tsconfig.app.json", + "assets": ["src/timeline/demo/src/favicon.ico", "src/timeline/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "timeline-demo:build:production" + }, + "development": { + "browserTarget": "timeline-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js timeline" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/timeline/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/timeline/demo/tsconfig.spec.json", + "karmaConfig": "src/timeline/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/timeline/demo/src/app/AppComponent.ts b/src/timeline/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/timeline/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/timeline/demo/src/app/AppModule.ts b/src/timeline/demo/src/app/AppModule.ts new file mode 100644 index 0000000..bb67fa8 --- /dev/null +++ b/src/timeline/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TimelineTestModule } from './timeline/TimelineTestModule'; + +@NgModule({ + imports: [ + TimelineTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/timeline/demo/src/app/IndexComponent.ts b/src/timeline/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..ad93cee --- /dev/null +++ b/src/timeline/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TimelineTestModule } from './timeline/TimelineTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TimelineTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/timeline/demo/src/app/app.html b/src/timeline/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/timeline/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/timeline/demo/src/app/timeline/TimelineBasicComponent.ts b/src/timeline/demo/src/app/timeline/TimelineBasicComponent.ts new file mode 100644 index 0000000..8cdff37 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineBasicComponent.ts @@ -0,0 +1,25 @@ +import { TiTimelineOption } from '@opentiny/ng'; +import { Component } from '@angular/core'; +@Component({ + templateUrl: './timeline-basic.html' +}) +export class TimelineBasicComponent { + options: Array = [ + { + label: '部署准备', + time: '2021年3月19日 11:30:26' + }, + { + label: '服务器部署', + time: '2021年3月19日 11:35:26' + }, + { + label: '网络配置', + time: '2021年3月19日 11:48:26' + }, + { + label: '实例发放', + time: '2021年3月19日 11:50:26' + } + ]; +} diff --git a/src/timeline/demo/src/app/timeline/TimelineDarkComponent.ts b/src/timeline/demo/src/app/timeline/TimelineDarkComponent.ts new file mode 100644 index 0000000..c945761 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineDarkComponent.ts @@ -0,0 +1,56 @@ +import { TiTimelineOption } from '@opentiny/ng'; +import { Component } from '@angular/core'; +@Component({ + templateUrl: './timeline-dark.html' +}) +export class TimelineDarkComponent { + activeIndex: number = 7; + options: Array = [ + { + label: '部署准备', + isTitle: true + }, + { + label: '下发部署任务', + time: '2021年3月19日 11:30:26' + }, + { + label: '服务器部署', + isTitle: true + }, + { + label: '分配服务器', + time: '2021年3月19日 11:35:26' + }, + { + label: '部署服务器', + time: '2021年3月19日 11:36:26' + }, + { + label: '网络配置', + isTitle: true + }, + { + label: '绑定EIP', + time: '2021年3月19日 11:40:26' + }, + { + label: '绑定安全组', + time: '2021年3月19日 11:45:26', + errorMessage: `失败原因:链接超时查看解决方案`, + type: 'danger' + }, + { + label: '绑定设备IP', + time: '2021年3月19日 11:48:26' + }, + { + label: '实例发放', + isTitle: true + }, + { + label: '开始发放实例', + time: '2021年3月19日 11:50:26' + } + ]; +} diff --git a/src/timeline/demo/src/app/timeline/TimelineHelptipComponent.ts b/src/timeline/demo/src/app/timeline/TimelineHelptipComponent.ts new file mode 100644 index 0000000..a549fd7 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineHelptipComponent.ts @@ -0,0 +1,58 @@ +import { TiTimelineOption } from '@opentiny/ng'; +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './timeline-helptip.html' +}) +export class TimelineHelptipComponent { + options: Array = [ + { + label: '部署准备', + isTitle: true + }, + { + label: '下发部署任务', + time: '2021年3月19日 11:30:26' + }, + { + label: '服务器部署', + iconTip: '服务器部署', + isTitle: true + }, + { + label: '分配服务器', + time: '2021年3月19日 11:35:26' + }, + { + label: '部署服务器', + time: '2021年3月19日 11:36:26' + }, + { + label: '网络配置', + isTitle: true + }, + { + label: '绑定EIP', + time: '2021年3月19日 11:40:26', + iconTip: 'EIP指的是可以独立购买和持有的公网IP地址资源。', + iconTipPosition: 'bottom-left', + iconTipMaxWidth: '200px' + }, + { + label: '绑定安全组', + time: '2021年3月19日 11:45:26' + }, + { + label: '绑定设备IP', + time: '2021年3月19日 11:48:26' + }, + { + label: '实例发放', + isTitle: true + }, + { + label: '开始发放实例', + time: '2021年3月19日 11:50:26' + } + ]; +} diff --git a/src/timeline/demo/src/app/timeline/TimelineMultiComponent.ts b/src/timeline/demo/src/app/timeline/TimelineMultiComponent.ts new file mode 100644 index 0000000..ea1d6cc --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineMultiComponent.ts @@ -0,0 +1,56 @@ +import { TiTimelineOption } from '@opentiny/ng'; +import { Component } from '@angular/core'; +@Component({ + templateUrl: './timeline-multi.html' +}) +export class TimelineMultiComponent { + activeIndex: number = 7; + options: Array = [ + { + label: '部署准备', + isTitle: true + }, + { + label: '下发部署任务', + time: '2021年3月19日 11:30:26' + }, + { + label: '服务器部署', + isTitle: true + }, + { + label: '分配服务器', + time: '2021年3月19日 11:35:26' + }, + { + label: '部署服务器', + time: '2021年3月19日 11:36:26' + }, + { + label: '网络配置', + isTitle: true + }, + { + label: '绑定EIP', + time: '2021年3月19日 11:40:26' + }, + { + label: '绑定安全组', + time: '2021年3月19日 11:45:26', + errorMessage: `失败原因:链接超时查看解决方案`, + type: 'danger' + }, + { + label: '绑定设备IP', + time: '2021年3月19日 11:48:26' + }, + { + label: '实例发放', + isTitle: true + }, + { + label: '开始发放实例', + time: '2021年3月19日 11:50:26' + } + ]; +} diff --git a/src/timeline/demo/src/app/timeline/TimelineTempleteComponent.ts b/src/timeline/demo/src/app/timeline/TimelineTempleteComponent.ts new file mode 100644 index 0000000..e35a01a --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineTempleteComponent.ts @@ -0,0 +1,34 @@ +import { TiTimelineOption } from '@opentiny/ng'; +import { Component } from '@angular/core'; +@Component({ + templateUrl: './timeline-templete.html' +}) +export class TimelineTempleteComponent { + options: Array = [ + { + label: 'primary', + time: '2015年4月28日 11:30:26', + type: 'primary' + }, + { + label: 'info', + time: '2015年4月28日 11:30:26', + type: 'info' + }, + { + label: 'success', + time: '2015年4月28日 11:30:26', + type: 'success' + }, + { + label: 'warning', + time: '2015年4月28日 11:30:26', + type: 'warning' + }, + { + label: 'danger', + time: '2015年4月28日 11:30:26', + type: 'danger' + } + ]; +} diff --git a/src/timeline/demo/src/app/timeline/TimelineTestComponent.ts b/src/timeline/demo/src/app/timeline/TimelineTestComponent.ts new file mode 100644 index 0000000..d2ba583 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineTestComponent.ts @@ -0,0 +1,113 @@ +import { TiTimelineOption } from '@opentiny/ng'; +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './timeline-test.html' +}) +export class TimelineTestComponent { + activeIndex: number = 4; + activeIndex1: number = 0; + options: Array = [ + { + label: '部署准备', + isTitle: true + }, + { + label: '下发部署任务', + time: '2021年3月19日 11:30:26' + }, + { + label: '服务器部署', + iconTip: 'tip', + isTitle: true + }, + { + label: '分配服务器', + time: '2021年3月19日 11:35:26' + }, + { + label: '部署服务器', + time: '2021年3月19日 11:36:26' + }, + { + label: '网络配置', + isTitle: true + }, + { + label: '绑定EIP', + time: '2021年3月19日 11:40:26' + }, + { + label: '绑定安全组', + time: '2021年3月19日 11:45:26', + errorMessage: `失败原因:链接超时查看解决方案` + }, + { + label: '绑定设备IP', + time: '2021年3月19日 11:48:26' + }, + { + label: '实例发放', + isTitle: true + }, + { + label: '开始发放实例', + time: '2021年3月19日 11:50:26' + } + ]; + options1: Array = [ + { + label: '部署准备', + isTitle: true + }, + { + label: '下发部署任务', + time: '2021年3月19日 11:30:26' + }, + { + label: '服务器部署', + iconTip: 'tip', + isTitle: true + }, + { + label: '分配服务器', + time: '2021年3月19日 11:35:26' + }, + { + label: '部署服务器', + time: '2021年3月19日 11:36:26' + }, + { + label: '网络配置', + isTitle: true + }, + { + label: '绑定EIP', + time: '2021年3月19日 11:40:26' + }, + { + label: '绑定安全组', + time: '2021年3月19日 11:45:26', + errorMessage: `失败原因:链接超时查看解决方案` + }, + { + label: '绑定设备IP', + time: '2021年3月19日 11:48:26' + }, + { + label: '实例发放', + isTitle: true + }, + { + label: '开始发放实例', + time: '2021年3月19日 11:50:26' + } + ]; + changeActiveStep(): void { + this.activeIndex = 7; + this.activeIndex1 = 7; + } + changeDanger(): void { + this.options[7].type = 'danger'; + } +} diff --git a/src/timeline/demo/src/app/timeline/TimelineTestModule.ts b/src/timeline/demo/src/app/timeline/TimelineTestModule.ts new file mode 100644 index 0000000..3344b68 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineTestModule.ts @@ -0,0 +1,63 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiTimelineModule, TiTipModule } from '@opentiny/ng'; + +import { TimelineMultiComponent } from './TimelineMultiComponent'; +import { TimelineDarkComponent } from './TimelineDarkComponent'; +import { TimelineBasicComponent } from './TimelineBasicComponent'; +import { TimelineTempleteComponent } from './TimelineTempleteComponent'; +import { TimelineTypeComponent } from './TimelineTypeComponent'; +import { TimelineHelptipComponent } from './TimelineHelptipComponent'; +import { TimelineTestComponent } from './TimelineTestComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiTimelineModule, TiTipModule, RouterModule.forChild(TimelineTestModule.ROUTES)], + declarations: [ + TimelineBasicComponent, + TimelineDarkComponent, + TimelineMultiComponent, + TimelineTempleteComponent, + TimelineTypeComponent, + TimelineHelptipComponent, + TimelineTestComponent + ] +}) +export class TimelineTestModule { + static readonly LINKS: Array = [{ label: 'Timeline' }]; + static readonly ROUTES: Routes = [ + { + path: 'timeline/timeline-basic', + component: TimelineBasicComponent, + data: { label: '基础' } + }, + { + path: 'timeline/timeline-multi', + component: TimelineMultiComponent, + data: { label: '多级展示' } + }, + { + path: 'timeline/timeline-dark', + component: TimelineDarkComponent, + data: { label: '深色背景展示' } + }, + { + path: 'timeline/timeline-templete', + component: TimelineTempleteComponent + }, + { + path: 'timeline/timeline-type', + component: TimelineTypeComponent + }, + { + path: 'timeline/timeline-helptip', + component: TimelineHelptipComponent + }, + { + path: 'timeline/timeline-test', + component: TimelineTestComponent + } + ]; +} diff --git a/src/timeline/demo/src/app/timeline/TimelineTypeComponent.ts b/src/timeline/demo/src/app/timeline/TimelineTypeComponent.ts new file mode 100644 index 0000000..7af4eb2 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineTypeComponent.ts @@ -0,0 +1,34 @@ +import { TiTimelineOption } from '@opentiny/ng'; +import { Component } from '@angular/core'; +@Component({ + templateUrl: './timeline-type.html' +}) +export class TimelineTypeComponent { + options: Array = [ + { + label: 'primary', + time: '2015年4月28日 11:30:26', + type: 'primary' + }, + { + label: 'info', + time: '2015年4月28日 11:30:26', + type: 'info' + }, + { + label: 'success', + time: '2015年4月28日 11:30:26', + type: 'success' + }, + { + label: 'warning', + time: '2015年4月28日 11:30:26', + type: 'warning' + }, + { + label: 'danger', + time: '2015年4月28日 11:30:26', + type: 'danger' + } + ]; +} diff --git a/src/timeline/demo/src/app/timeline/timeline-basic.html b/src/timeline/demo/src/app/timeline/timeline-basic.html new file mode 100644 index 0000000..a66c621 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/timeline-basic.html @@ -0,0 +1 @@ + diff --git a/src/timeline/demo/src/app/timeline/timeline-dark.html b/src/timeline/demo/src/app/timeline/timeline-dark.html new file mode 100644 index 0000000..054a6f7 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/timeline-dark.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/timeline/demo/src/app/timeline/timeline-helptip.html b/src/timeline/demo/src/app/timeline/timeline-helptip.html new file mode 100644 index 0000000..864aae8 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/timeline-helptip.html @@ -0,0 +1 @@ + diff --git a/src/timeline/demo/src/app/timeline/timeline-multi.html b/src/timeline/demo/src/app/timeline/timeline-multi.html new file mode 100644 index 0000000..74d25fa --- /dev/null +++ b/src/timeline/demo/src/app/timeline/timeline-multi.html @@ -0,0 +1 @@ + diff --git a/src/timeline/demo/src/app/timeline/timeline-templete.html b/src/timeline/demo/src/app/timeline/timeline-templete.html new file mode 100644 index 0000000..202f76d --- /dev/null +++ b/src/timeline/demo/src/app/timeline/timeline-templete.html @@ -0,0 +1,6 @@ + + + {{item.label}} + {{item.time}} + + diff --git a/src/timeline/demo/src/app/timeline/timeline-test.html b/src/timeline/demo/src/app/timeline/timeline-test.html new file mode 100644 index 0000000..144707c --- /dev/null +++ b/src/timeline/demo/src/app/timeline/timeline-test.html @@ -0,0 +1,23 @@ +

    1、描述

    +

    时间线组件,呈现为一组时间序列的文本展示

    +

    导入

    +

    import {{ '{' }} TpTimelineModule {{ '}' }} from '@opentiny/ng';

    +

    基本展示

    +
    + +

    + + +
    +

    tip场景

    +鼠标移入出tip使用场景 + + + + diff --git a/src/timeline/demo/src/app/timeline/timeline-type.html b/src/timeline/demo/src/app/timeline/timeline-type.html new file mode 100644 index 0000000..3ad10ab --- /dev/null +++ b/src/timeline/demo/src/app/timeline/timeline-type.html @@ -0,0 +1 @@ + diff --git a/src/timeline/demo/src/app/timeline/webdoc/timeline-demos.js b/src/timeline/demo/src/app/timeline/webdoc/timeline-demos.js new file mode 100644 index 0000000..27348e7 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/webdoc/timeline-demos.js @@ -0,0 +1,89 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'timeline-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Timeline 组件的最简用法。

    ', + 'en-US': '', + }, + apis: [ + 'TiTimelineComponent.properties.options', + 'TiTimelineOption.properties.label', + 'TiTimelineOption.properties.time' + ], + }, + { + demoId: 'timeline-type', + name: { + 'zh-CN': '类型', + 'en-US': 'type', + }, + desc: { + 'zh-CN': '

    通过属性option.type配置 timeline 节点类型,包括infosuccessdangerprimarywarning

    ', + 'en-US': '', + }, + apis: ['TiTimelineOption.properties.type'], + }, + { + demoId: 'timeline-multi', + name: { + 'zh-CN': '多级', + 'en-US': 'multi', + }, + desc: { + 'zh-CN': '

    通过属性activeIndex配置当前激活项;通过属性option.isTitle配置是否为一级节点标题;通过属性option.errorMessage配置节点执行失败的错误信息。

    ', + 'en-US': '', + }, + apis: [ + 'TiTimelineComponent.properties.activeIndex', + 'TiTimelineOption.properties.isTitle', + 'TiTimelineOption.properties.errorMessage' + ], + }, + { + demoId: 'timeline-dark', + name: { + 'zh-CN': '深色背景', + 'en-US': 'dark', + }, + desc: { + 'zh-CN': '

    通过属性dark配置深色背景时间线。

    ', + 'en-US': '', + } + }, + { + demoId: 'timeline-helptip', + name: { + 'zh-CN': '提示信息', + 'en-US': 'helptip', + }, + desc: { + 'zh-CN': '

    通过属性option.iconTip配置帮助图标提示内容;通过属性option.iconTipPosition配置提示方向;通过属性option.iconTipMaxWidth配置提示信息最大宽度。

    ', + 'en-US': '', + }, + apis: [ + 'TiTimelineOption.properties.iconTip', + 'TiTimelineOption.properties.iconTipPosition', + 'TiTimelineOption.properties.iconTipMaxWidth' + ], + }, + { + demoId: 'timeline-templete', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'templete', + }, + desc: { + 'zh-CN': '

    通过属性#item配置时间线节点内容的模板。

    ', + 'en-US': '', + }, + apis: ['TiTimelineComponent.slots.itemTemplate'], + } + ] +} diff --git a/src/timeline/demo/src/app/timeline/webdoc/timeline.cn.md b/src/timeline/demo/src/app/timeline/webdoc/timeline.cn.md new file mode 100644 index 0000000..c805522 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/webdoc/timeline.cn.md @@ -0,0 +1,23 @@ +--- +title: Timeline 时间线 +--- +# Timeline 时间线 + +
    + +垂直展示时间流信息的组件。    + +```typescript +import { TiTimelineModule } from '@opentiny/ng'; +``` + +
    + +
    + +垂直展示时间流信息的组件。   + +```typescript +import { TiTimelineModule } from '@opentiny/ng'; +``` +
    \ No newline at end of file diff --git a/src/timeline/demo/src/app/timeline/webdoc/timeline.en.md b/src/timeline/demo/src/app/timeline/webdoc/timeline.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/webdoc/timeline.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/timeline/demo/src/favicon.ico b/src/timeline/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/timeline/demo/src/index.html b/src/timeline/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/timeline/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/timeline/demo/src/main.ts b/src/timeline/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/timeline/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/timeline/demo/test.ts b/src/timeline/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/timeline/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/timeline/demo/tsconfig.app.json b/src/timeline/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/timeline/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/timeline/demo/tsconfig.spec.json b/src/timeline/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/timeline/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/timeline/lib/index.ts b/src/timeline/lib/index.ts new file mode 100644 index 0000000..8a52840 --- /dev/null +++ b/src/timeline/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTimelineModule'; diff --git a/src/timeline/lib/ng-package.json b/src/timeline/lib/ng-package.json new file mode 100644 index 0000000..bfd1328 --- /dev/null +++ b/src/timeline/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/timeline", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/timeline/lib/package.json b/src/timeline/lib/package.json new file mode 100644 index 0000000..046eab5 --- /dev/null +++ b/src/timeline/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-timeline", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/timeline/lib/project.json b/src/timeline/lib/project.json new file mode 100644 index 0000000..8f6d71c --- /dev/null +++ b/src/timeline/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/timeline/lib", + "sourceRoot": "src/timeline/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/timeline"], + "options": { + "project": "src/timeline/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/timeline"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js timeline" + }, + { + "command": "ng default-build timeline" + }, + { + "command": "node build/clear-default-theme.js timeline" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/timeline && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build timeline && ng pack timeline && node build/publish.js timeline --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/timeline/lib/src/TiTimelineComponent.ts b/src/timeline/lib/src/TiTimelineComponent.ts new file mode 100644 index 0000000..a109b8e --- /dev/null +++ b/src/timeline/lib/src/TiTimelineComponent.ts @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + Input, + Renderer2, + SimpleChanges, + TemplateRef +} from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { Util } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; +export interface TiTimelineOption { + /** + * 节点类型 + */ + type?: 'info' | 'success' | 'danger' | 'primary' | 'warning'; + /** + * 必选,节点文本 + */ + label: string; + /** + * 节点时间 + */ + time?: string; + /** + * 节点是否为一级标题 + */ + isTitle?: boolean; + /** + * 节点执行失败错误信息 + */ + errorMessage?: string; + /** + * 帮助提示图标 tip 提示内容 + */ + iconTip?: string | TemplateRef | any; + /** + * 帮助提示图标 tip 提示方向 + */ + iconTipPosition?: string; + /** + * 帮助提示图标 tip 最大宽度 + */ + iconTipMaxWidth?: string; +} +/** + * 时间线timeline组件 + */ +@Component({ + selector: 'ti-timeline', + templateUrl: './timeline.html', + styleUrls: ['./timeline.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiTimelineComponent extends TiBaseComponent { + /** + * 必选,时间线节点数据集 + */ + @Input() options: Array; + /** + * 当前激活项 + */ + @Input() activeIndex: number = -1; + /** + * 时间线节点内容区域的模板 + */ + @ContentChild('item', { static: true }) itemTemplate: TemplateRef; + /** + * @ignore + * 时间轴一级信息集合 + */ + public titleOptions: Array = []; + protected versionInfo: string = super.getVersion(packageInfo); + + constructor(protected hostRef: ElementRef, protected renderer: Renderer2, protected changeDetectorRef: ChangeDetectorRef) { + super(hostRef, renderer); + } + + ngOnInit(): void { + super.ngOnInit(); + this.titleOptions = this.getTitleOptions(); + this.changeToSuccess(); + } + ngOnChanges(changes: SimpleChanges): void { + if (changes['activeIndex'] && !changes['activeIndex'].firstChange) { + this.changeToSuccess(); + } + if (changes['options'] && !changes['options'].firstChange) { + this.titleOptions = this.getTitleOptions(); + } + } + + ngDoCheck(): void { + super.ngDoCheck(); + // 改为onPush模式后,复杂数据类型接口的变更必须要修改引用地址,才能出发变更检测。此处添加标记向前兼容。 + this.changeDetectorRef.markForCheck(); + } + + /** + * @ignore + */ + public findItemIndex(item: TiTimelineOption) { + return this.titleOptions.findIndex((ele: TiTimelineOption) => ele === item); + } + /** + * @ignore + * 获取时间轴一级(标题)信息集合 + */ + public getTitleOptions(): Array { + return this.options.filter((option: TiTimelineOption) => option.isTitle); + } + /** + * @ignore + * activeIndex之前的项全部变更为success状态 + */ + public changeToSuccess(): void { + if (this.activeIndex === -1 || Util.isUndefined(this.activeIndex) || !Util.isNumber(this.activeIndex)) { + return; + } + this.options.forEach((option: TiTimelineOption, index: number) => { + option.type = index < this.activeIndex ? 'success' : option.type || 'primary'; + }); + } + /** + * @ignore + * 判断当前一级时间轴状态 + */ + public isSuccess(option: TiTimelineOption): boolean { + if (!option.isTitle) { + return; + } + let level2Options: Array = []; + const index: number = this.options.findIndex((item: TiTimelineOption) => item === option); + let isSucess: boolean = true; + for (let i = index + 1; i <= this.activeIndex; i++) { + if (this.options[i] && this.options[i].isTitle) { + break; + } + level2Options.push(this.options[i]); + } + if (level2Options.length > 0) { + level2Options.forEach((option: TiTimelineOption) => { + if (option.type && option.type !== 'success') { + isSucess = false; + } + }); + } + + return isSucess; + } +} diff --git a/src/timeline/lib/src/TiTimelineModule.ts b/src/timeline/lib/src/TiTimelineModule.ts new file mode 100644 index 0000000..ea0b3b3 --- /dev/null +++ b/src/timeline/lib/src/TiTimelineModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiTimelineComponent } from './TiTimelineComponent'; + +@NgModule({ + imports: [CommonModule, TiIconModule, TiTipModule], + exports: [TiTimelineComponent], + declarations: [TiTimelineComponent] +}) +export class TiTimelineModule {} +export { TiTimelineComponent, TiTimelineOption } from './TiTimelineComponent'; diff --git a/src/timeline/lib/src/timeline.html b/src/timeline/lib/src/timeline.html new file mode 100644 index 0000000..8b99f3d --- /dev/null +++ b/src/timeline/lib/src/timeline.html @@ -0,0 +1,60 @@ +
      +
    • + + + {{findItemIndex(item) + 1}} + + + + +
    • +
    + + +
    + {{item.label}} + + + {{item.time}} + +
    + {{item.time}} +
    + +
    +
    + + + + diff --git a/src/timeline/lib/src/timeline.less b/src/timeline/lib/src/timeline.less new file mode 100644 index 0000000..9589c8c --- /dev/null +++ b/src/timeline/lib/src/timeline.less @@ -0,0 +1,220 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-timeline-badge-width: var(--ti-common-size-2x); + --ti-timeline-line-width: 1px; + --ti-timeline-label-line-height: calc(var(--ti-common-font-size-base) * var(--ti-common-line-height-number)); + --ti-timeline-level1-width: 14px; + --ti-timeline-level1-border-weight: var(--ti-common-border-weight-normal); + --ti-timeline-level1-line-height: calc(var(--ti-common-font-size-1) * var(--ti-common-line-height-number)); + --ti-timeline-current-border-weight: var(--ti-common-border-weight-1); +} + +.ti3-timeline-container { + position: relative; + padding-bottom: calc(var(--ti-common-space-5x)); + list-style: none; + font-weight: var(--ti-common-font-weight-4); + &:after { + content: ''; + left: calc(-1 * var(--ti-timeline-line-width) / 2); + position: absolute; + top: calc((var(--ti-timeline-label-line-height) + var(--ti-timeline-badge-width)) / 2); + width: var(--ti-timeline-line-width); + height: calc(100% - var(--ti-timeline-badge-width)); + background-color: var(--ti-common-color-line-dividing); + } + &:last-child { + padding-bottom: 0; + &:after { + display: none; + } + } +} +// 只有多级信息场景下,时间轴线条才会变色 +.ti3-timeline-multistage .ti3-timeline-container.ti3-timeline-completed { + &:after { + background-color: var(--ti-common-color-success); + } +} +.ti3-timeline-label { + font-size: var(--ti-common-font-size-base); + display: block; + padding-left: var(--ti-common-space-5x); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-primary); + position: relative; + word-wrap: break-word; + &:before { + content: ''; + position: absolute; + left: calc(-1 * var(--ti-timeline-badge-width) / 2); + top: calc((var(--ti-timeline-label-line-height) - var(--ti-timeline-badge-width)) / 2); + width: var(--ti-timeline-badge-width); + height: var(--ti-timeline-badge-width); + background-color: var(--ti-common-color-line-dividing); + border-radius: var(--ti-common-border-radius-3); + z-index: 1; + } +} +.ti3-timeline-multistage { + padding-left: var(--ti-common-space-2x); + .ti3-timeline-label { + color: var(--ti-common-color-text-weaken); + } + .ti3-timeline-success { + color: var(--ti-common-color-text-primary); + } + .ti3-timeline-time-level2 { + display: inline-block; + padding-left: var(--ti-common-space-3x); + } +} +// 当前进行中的步骤,水波纹样式 +.ti3-timeline-currentStep { + &:before { + left: calc(-1 * var(--ti-timeline-badge-width) / 2 - var(--ti-timeline-current-border-weight)); + top: calc((var(--ti-timeline-label-line-height) - var(--ti-timeline-badge-width)) / 2 - var(--ti-timeline-current-border-weight)); + background-color: var(--ti-common-color-success); + background-clip: content-box; + border: var(--ti-timeline-current-border-weight) var(--ti-common-border-style-solid) rgba(80, 212, 171, 0.2); + } +} +.ti3-timeline-multistage .ti3-timeline-danger { + &:before { + background-color: var(--ti-common-color-error-text); + border: var(--ti-timeline-current-border-weight) var(--ti-common-border-style-solid) rgba(222, 80, 78, 0.2); + } +} +.changecolor(@color) { + background-color: @color; + border-color: @color; +} +.ti3-timeline-info:before { + .changecolor(var(--ti-common-color-prompt)); +} +.ti3-timeline-success:before { + .changecolor(var(--ti-common-color-success)); +} +.ti3-timeline-warning:before { + .changecolor(var(--ti-common-color-warn)); +} +.ti3-timeline-danger:before { + .changecolor(var(--ti-common-color-error)); +} +.ti3-timeline-time { + font-size: var(--ti-common-font-size-base); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-weaken); + display: block; + white-space: pre; +} + +.ti3-timeline-active { + color: var(--ti-common-color-text-success); +} +.ti3-timeline-active-danger { + color: var(--ti-common-color-error-text); +} + +.ti3-timeline-level1 { + width: var(--ti-timeline-level1-width); + height: var(--ti-timeline-level1-width); + text-align: center; + line-height: calc(var(--ti-timeline-level1-width) + var(--ti-timeline-level1-border-weight)); + display: inline-block; + border: var(--ti-timeline-level1-border-weight) var(--ti-common-border-style-solid) var(--ti-common-color-text-weaken); + border-radius: var(--ti-common-border-radius-3); + color: var(--ti-common-color-text-weaken); + position: absolute; + left: calc(-1 * calc(var(--ti-timeline-level1-width) + var(--ti-timeline-level1-border-weight) * 2) / 2); + top: calc((var(--ti-timeline-level1-line-height) - var(--ti-timeline-level1-width) - var(--ti-timeline-level1-border-weight) * 2) / 2); + background-color: var(--ti-common-color-bg-white-normal); + z-index: 1; + .box-sizing(content-box); +} + +.ti3-timeline-level1-success { + color: var(--ti-common-color-success); + border-color: var(--ti-common-color-success); +} + +.ti3-timeline-processing { + background: var(--ti-common-color-success); + color: var(--ti-common-color-text-white); +} + +.ti3-timeline-errorMessagg { + color: var(--ti-common-color-error-text); + padding-top: var(--ti-common-space-base); +} + +.ti3-timeline-label-level1 { + font-size: var(--ti-common-font-size-1); +} + +.ti3-timeline-label-container { + display: flex; + align-items: center; +} +.ti3-timeline-icontip { + font-size: var(--ti-common-font-size-2); + padding-left: var(--ti-common-space-2x); + color: var(--ti-common-color-icon-normal); + vertical-align: bottom; +} + +:host[dark] { + .ti3-timeline-container { + &:after { + background: var(--ti-common-color-line-normal); + } + } + .ti3-timeline-label { + color: var(--ti-common-color-text-darkbg); + &:before { + background: var(--ti-common-color-line-normal); + } + } + .ti3-timeline-level1 { + color: var(--ti-common-color-text-darkbg); + border-color: var(--ti-common-color-line-normal); + background-color: var(--ti-common-color-bg-dark-normal); + } + .ti3-timeline-level1.ti3-timeline-level1-success { + color: var(--ti-common-color-success); + border-color: var(--ti-common-color-success); + } + .ti3-timeline-success, + .ti3-timeline-currentStep { + &:before { + background: var(--ti-common-color-success); + background-clip: content-box; + } + } + .ti3-timeline-currentStep.ti3-timeline-danger { + &:before { + background: var(--ti-common-color-error); + background-clip: content-box; + border: var(--ti-timeline-current-border-weight) var(--ti-common-border-style-solid) rgba(246, 111, 106, 0.2); + } + } + .ti3-timeline-active-danger, + .ti3-timeline-errorMessagg { + color: var(--ti-common-color-error); + } + .ti3-timeline-time { + color: var(--ti-common-color-text-darkbg); + } + .ti3-timeline-success { + color: var(--ti-common-color-text-gray); + } + .ti3-timeline-level1.ti3-timeline-processing { + background-color: var(--ti-common-color-success); + color: var(--ti-common-color-text-white); + } + // 目前深色背景只有tip场景,故深色背景下不会再有问号tip + .ti3-timeline-icontip { + display: none; + } +} diff --git a/src/tip/demo/karma.conf.js b/src/tip/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/tip/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/tip/demo/project.json b/src/tip/demo/project.json new file mode 100644 index 0000000..90d845a --- /dev/null +++ b/src/tip/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/tip/demo", + "sourceRoot": "src/tip/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/tip", + "index": "src/tip/demo/src/index.html", + "main": "src/tip/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tip/demo/tsconfig.app.json", + "assets": ["src/tip/demo/src/favicon.ico", "src/tip/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "tip-demo:build:production" + }, + "development": { + "browserTarget": "tip-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js tip" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/tip/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tip/demo/tsconfig.spec.json", + "karmaConfig": "src/tip/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/tip/demo/src/app/AppComponent.ts b/src/tip/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/tip/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/tip/demo/src/app/AppModule.ts b/src/tip/demo/src/app/AppModule.ts new file mode 100644 index 0000000..fe87613 --- /dev/null +++ b/src/tip/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TipTestModule } from './tip/TipTestModule'; + +@NgModule({ + imports: [ + TipTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/tip/demo/src/app/IndexComponent.ts b/src/tip/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..43a4c0a --- /dev/null +++ b/src/tip/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TipTestModule } from './tip/TipTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TipTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/tip/demo/src/app/app.html b/src/tip/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/tip/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/tip/demo/src/app/tip/TipBasicComponent.ts b/src/tip/demo/src/app/tip/TipBasicComponent.ts new file mode 100644 index 0000000..f1c3820 --- /dev/null +++ b/src/tip/demo/src/app/tip/TipBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-basic.html' +}) +export class TipBasicComponent { + content: string = 'mouseover to show tip'; +} diff --git a/src/tip/demo/src/app/tip/TipContentCompComponent.ts b/src/tip/demo/src/app/tip/TipContentCompComponent.ts new file mode 100644 index 0000000..a364bde --- /dev/null +++ b/src/tip/demo/src/app/tip/TipContentCompComponent.ts @@ -0,0 +1,51 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + templateUrl: './tip-content-comp.html' +}) +export class TipContentCompComponent { + myLogs: Array = []; + // 定义组件类型 + // eslint-disable-next-line no-use-before-define + component: any = TipDemoComponent; + + // 定义tip显示内容的上下文 + tipContext: any = { + label: 'Are you sure delete this task?', // 该属性与TipDemoComponent组件中的@Input属性定义对应 + outputs: { + // 定义在outputs对象中的属性(例如 ok cancel)与TipDemoComponent组件中的@Output属性定义对应 + ok: ($event: string): void => { + this.myLogs = [...this.myLogs, `ok() event = ${$event}`]; + }, + cancel: ($event: string): void => { + this.myLogs = [...this.myLogs, `cancel() event = ${$event}`]; + } + } + }; +} + +// 自定义组件,此处为了方便demo展示与tip生成组件写在同一文件,项目中请将组件单独写在一个文件中 +@Component({ + template: ` + + {{ label }} +
    + + +
    + ` +}) +export class TipDemoComponent { + // 定义元素属性,该属性可作为Tip上下文使用 + @Input() label: string; + @Output() readonly ok: EventEmitter = new EventEmitter(); + @Output() readonly cancel: EventEmitter = new EventEmitter(); + + onOk(): void { + this.ok.emit('ok event'); + } + + onCancel(): void { + this.cancel.emit('cancel event'); + } +} diff --git a/src/tip/demo/src/app/tip/TipContentTemplateComponent.ts b/src/tip/demo/src/app/tip/TipContentTemplateComponent.ts new file mode 100644 index 0000000..784213f --- /dev/null +++ b/src/tip/demo/src/app/tip/TipContentTemplateComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-content-template.html' +}) +export class TipContentTemplateComponent { + tip1Context: string = 'this is a tip'; + tip2Context: string = 'this is another tip'; +} diff --git a/src/tip/demo/src/app/tip/TipEmptyComponent.ts b/src/tip/demo/src/app/tip/TipEmptyComponent.ts new file mode 100644 index 0000000..2ffc45a --- /dev/null +++ b/src/tip/demo/src/app/tip/TipEmptyComponent.ts @@ -0,0 +1,59 @@ +import { AfterViewInit, ChangeDetectorRef, Component, TemplateRef, ViewChild } from '@angular/core'; +import { TipDemoComponent } from './TipContentCompComponent'; + +@Component({ + templateUrl: './tip-empty.html', + styleUrls: ['./tipTest.less'] +}) +export class TipEmptyComponent implements AfterViewInit { + // tip内容为string, tip内容为null + tipStr: string = null; + + // tip内容为组件 + component: any = TipDemoComponent; + + // 获取tip内容模板,实际上获取不到模板时,tipTemplate为undefined + @ViewChild('tipTempContent') tipTemplate: TemplateRef; + + // 当tip内容是模板时,控制tip是否显示 + showTipTemplate: boolean = true; + + tipTempContext: any = { + label: 'this is a tip of template.' + }; + + tipCompContext: any = { + label: 'name:', // 该属性与TipDemoComponent组件中的@Input属性定义对应 + value: 'this is a tip of component.', // 该属性与TipDemoComponent组件中的@Input属性定义对应 + outputs: { + // 定义在outputs对象中的属性(例如 ok cancel)与TipDemoComponent组件中的@Output属性定义对应 + ok: ($event: string): void => {}, + cancel: ($event: string): void => {} + } + }; + + constructor(private cd: ChangeDetectorRef) {} + + ngAfterViewInit(): void { + // 处理ExpressionChangedAfterItHasBeenCheckedError报错 + this.cd.detectChanges(); + console.log(this.tipTemplate); + } + + onClickTemp(event: any): void { + this.showTipTemplate = !this.showTipTemplate; + // 处理ExpressionChangedAfterItHasBeenCheckedError报错 + this.cd.detectChanges(); + console.log(this.tipTemplate); + } + + onClickComp(event: any): void { + this.component ? (this.component = null) : (this.component = TipDemoComponent); + console.log(this.component); + } + + onClickStr(event: any): void { + this.tipStr ? (this.tipStr = null) : (this.tipStr = 'tip内容'); + console.log(this.tipStr); + } +} diff --git a/src/tip/demo/src/app/tip/TipHasArrowComponent.ts b/src/tip/demo/src/app/tip/TipHasArrowComponent.ts new file mode 100644 index 0000000..f71db1b --- /dev/null +++ b/src/tip/demo/src/app/tip/TipHasArrowComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-has-arrow.html' +}) +export class TipHasArrowComponent {} diff --git a/src/tip/demo/src/app/tip/TipLongTextPositionComponent.ts b/src/tip/demo/src/app/tip/TipLongTextPositionComponent.ts new file mode 100644 index 0000000..8b013b9 --- /dev/null +++ b/src/tip/demo/src/app/tip/TipLongTextPositionComponent.ts @@ -0,0 +1,63 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-long-text-position.html', + styleUrls: ['./tipTest.less'] +}) +export class TipLongTextPositionComponent { + tipStr: string = `苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。`; +} diff --git a/src/tip/demo/src/app/tip/TipMaxWidthComponent.ts b/src/tip/demo/src/app/tip/TipMaxWidthComponent.ts new file mode 100644 index 0000000..fac3621 --- /dev/null +++ b/src/tip/demo/src/app/tip/TipMaxWidthComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-max-width.html' +}) +export class TipMaxWidthComponent { + tipStr: string = 'this is my tip, it is a long tip, its maxWidth is defined'; +} diff --git a/src/tip/demo/src/app/tip/TipPositionComponent.ts b/src/tip/demo/src/app/tip/TipPositionComponent.ts new file mode 100644 index 0000000..fd8d7cd --- /dev/null +++ b/src/tip/demo/src/app/tip/TipPositionComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-position.html', + styles: [ + ` + .center { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + margin: 0; + } + ` + ] +}) +export class TipPositionComponent { + tipStr: string = 'myTip'; +} diff --git a/src/tip/demo/src/app/tip/TipPositionTestComponent.ts b/src/tip/demo/src/app/tip/TipPositionTestComponent.ts new file mode 100644 index 0000000..c9a6dab --- /dev/null +++ b/src/tip/demo/src/app/tip/TipPositionTestComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-position-test.html' +}) +export class TipPositionTestComponent { + tipStr: string = 'myTip'; +} diff --git a/src/tip/demo/src/app/tip/TipServiceComponent.ts b/src/tip/demo/src/app/tip/TipServiceComponent.ts new file mode 100644 index 0000000..f7fdc13 --- /dev/null +++ b/src/tip/demo/src/app/tip/TipServiceComponent.ts @@ -0,0 +1,42 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { TiTipRef, TiTipService, TiTipShowInfo } from '@opentiny/ng'; + +@Component({ + templateUrl: './tip-service.html' +}) +export class TipServiceComponent { + private tipInstance: TiTipRef; // tip组件实例 + private tipShowState: boolean = false; // tip显示状态标志位 + @ViewChild('button') buttonRef: ElementRef; + constructor(private tipService: TiTipService) {} + ngAfterViewInit(): void { + this.tipService.create(this.buttonRef.nativeElement, { + position: 'right', + trigger: 'mouse', // 需要与 showFn 结合使用;指定触发 tip 显示的方式 + showFn: (): TiTipShowInfo => { + // 设置为 'mouse' 时的显示函数,返回值类型为 TiTipShowInfo + return { content: 'tip content', context: {} }; + } + }); + } + onClick($event: any): void { + if (!this.tipInstance) { + // 生成tip实例:通过调用tipService的create方法生成 + this.tipInstance = this.tipService.create($event.target, { + position: 'right' + }); + } + + // tip实例已生成情况下,切换tip提示的显示状态 + this.toggleTip(); + } + toggleTip(): void { + if (!this.tipShowState) { + this.tipInstance.show('tip content'); // 显示tip提示,并定义其内容为'tip content' + this.tipShowState = true; // 设置tip显示状态标志位 + } else { + this.tipInstance.hide(); // 隐藏tip提示 + this.tipShowState = false; // 重置tip显示状态标志位 + } + } +} diff --git a/src/tip/demo/src/app/tip/TipServiceDestroyComponent.ts b/src/tip/demo/src/app/tip/TipServiceDestroyComponent.ts new file mode 100644 index 0000000..a81597f --- /dev/null +++ b/src/tip/demo/src/app/tip/TipServiceDestroyComponent.ts @@ -0,0 +1,32 @@ +import { Component, ElementRef } from '@angular/core'; +import { TiTipRef, TiTipService, TiTipShowInfo } from '@opentiny/ng'; + +@Component({ + templateUrl: './tip-service-destroy.html' +}) +export class TipServiceDestroyComponent { + private tipInstance: TiTipRef; // tip组件实例 + constructor(private tipService: TiTipService) {} + + addTip($event: any, target: ElementRef): void { + if (this.tipInstance) { + return; + } + // 生成tip实例:通过调用tipService的create方法生成 + this.tipInstance = this.tipService.create(target.nativeElement, { + position: 'right', + trigger: 'mouse', + showFn: (): TiTipShowInfo => { + return { + content: '自定义tip内容' + }; + } + }); + } + removeTip($event: any): void { + if (this.tipInstance) { + this.tipInstance.destroy(); + this.tipInstance = null; + } + } +} diff --git a/src/tip/demo/src/app/tip/TipTestModule.ts b/src/tip/demo/src/app/tip/TipTestModule.ts new file mode 100644 index 0000000..272aeb2 --- /dev/null +++ b/src/tip/demo/src/app/tip/TipTestModule.ts @@ -0,0 +1,111 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiIconModule, TiTipModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TipBasicComponent } from './TipBasicComponent'; +import { TipPositionComponent } from './TipPositionComponent'; +import { TipContentTemplateComponent } from './TipContentTemplateComponent'; +import { TipContentCompComponent, TipDemoComponent } from './TipContentCompComponent'; +import { TipHasArrowComponent } from './TipHasArrowComponent'; +import { TipMaxWidthComponent } from './TipMaxWidthComponent'; +import { TipTriggerComponent } from './TipTriggerComponent'; +import { TipServiceComponent } from './TipServiceComponent'; +import { TipZindexComponent } from './TipZindexComponent'; +import { TipPositionTestComponent } from './TipPositionTestComponent'; +import { TipServiceDestroyComponent } from './TipServiceDestroyComponent'; +import { TipValidPositionTestComponent } from './TipValidPositionTestComponent'; +import { TipEmptyComponent } from './TipEmptyComponent'; +import { TipLongTextPositionComponent } from './TipLongTextPositionComponent'; + +@NgModule({ + imports: [ + CommonModule, + TiTipModule, + TiButtonModule, + TiIconModule, + FormsModule, + ReactiveFormsModule, + DemoLogModule, + RouterModule.forChild(TipTestModule.ROUTES) + ], + declarations: [ + TipBasicComponent, + TipContentTemplateComponent, + TipContentCompComponent, + TipPositionComponent, + TipHasArrowComponent, + TipMaxWidthComponent, + TipServiceComponent, + TipServiceDestroyComponent, + TipTriggerComponent, + TipPositionTestComponent, + TipValidPositionTestComponent, + TipDemoComponent, + TipEmptyComponent, + TipLongTextPositionComponent, + TipZindexComponent + ], + entryComponents: [TipDemoComponent] +}) +export class TipTestModule { + static readonly LINKS: Array = [ + { href: 'directives/TiTipDirective.html', label: 'Tip' }, + { href: 'injectables/TiTipService.html', label: 'TiTipService' } + ]; + static readonly ROUTES: Routes = [ + { + path: 'tip/tip-basic', + component: TipBasicComponent + }, + { + path: 'tip/tip-trigger', + component: TipTriggerComponent + }, + { + path: 'tip/tip-content-template', + component: TipContentTemplateComponent + }, + { + path: 'tip/tip-content-comp', + component: TipContentCompComponent + }, + { + path: 'tip/tip-position', + component: TipPositionComponent + }, + { + path: 'tip/tip-has-arrow', + component: TipHasArrowComponent + }, + { + path: 'tip/tip-max-width', + component: TipMaxWidthComponent + }, + { + path: 'tip/tip-service', + component: TipServiceComponent + }, + { + path: 'tip/tip-zindex', + component: TipZindexComponent + }, + { path: 'tip/tip-service-destroy', component: TipServiceDestroyComponent }, + { path: 'tip/tip-position-test', component: TipPositionTestComponent }, + { + path: 'tip/tip-valid-position-test', + component: TipValidPositionTestComponent + }, + { + path: 'tip/tip-empty', + component: TipEmptyComponent + }, + { + path: 'tip/tip-long-text-position', + component: TipLongTextPositionComponent + } + ]; +} diff --git a/src/tip/demo/src/app/tip/TipTriggerComponent.ts b/src/tip/demo/src/app/tip/TipTriggerComponent.ts new file mode 100644 index 0000000..483f1b8 --- /dev/null +++ b/src/tip/demo/src/app/tip/TipTriggerComponent.ts @@ -0,0 +1,15 @@ +import { Component, ViewChild } from '@angular/core'; +import { TiTipDirective } from '@opentiny/ng'; + +@Component({ + templateUrl: './tip-trigger.html' +}) +export class TipTriggerComponent { + @ViewChild('tip', { static: true }) tipDirective: TiTipDirective; + tipContent: string = 'click to show tip'; + show: boolean = false; + onClick(): void { + this.show = !this.show; + this.show ? this.tipDirective.hide() : this.tipDirective.show(); + } +} diff --git a/src/tip/demo/src/app/tip/TipValidPositionTestComponent.ts b/src/tip/demo/src/app/tip/TipValidPositionTestComponent.ts new file mode 100644 index 0000000..ede4479 --- /dev/null +++ b/src/tip/demo/src/app/tip/TipValidPositionTestComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-valid-position-test.html', + styleUrls: ['./tipTest.less'] +}) +export class TipValidPositionTestComponent { + tipStr11: string = '寻寻觅觅,冷冷清清,凄凄惨惨戚戚。乍'; + tipStr12: string = + '苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。' + + '园中菊花堆积满地,都已经憔悴不堪,如今还有谁来采摘?冷清清地守着窗子,独自一个人怎么熬到天黑?梧桐叶上细雨淋漓,到黄昏时分,还是点点滴滴。这般情景,怎么能用一个愁字了结!'; + tipStr13: string = + '苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。'; + tipStr21: string = + '苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。'; + tipStr22: string = + '苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。'; + tipStr31: string = '寻寻觅觅,冷冷清清,凄凄惨惨戚戚。乍'; + tipStr32: string = + '苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。'; + tipStr33: string = '寻寻觅觅,冷冷清清,凄凄惨惨戚戚。乍'; +} diff --git a/src/tip/demo/src/app/tip/TipZindexComponent.ts b/src/tip/demo/src/app/tip/TipZindexComponent.ts new file mode 100644 index 0000000..714bb7a --- /dev/null +++ b/src/tip/demo/src/app/tip/TipZindexComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-zindex.html', + styles: [ + ` + .zIndex { + z-index: 1000; + } + ` + ] +}) +export class TipZindexComponent { + tipStr: string = 'this is my tip dfa'; +} diff --git a/src/tip/demo/src/app/tip/tip-basic.html b/src/tip/demo/src/app/tip/tip-basic.html new file mode 100644 index 0000000..86ae6a2 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-basic.html @@ -0,0 +1 @@ + diff --git a/src/tip/demo/src/app/tip/tip-content-comp.html b/src/tip/demo/src/app/tip/tip-content-comp.html new file mode 100644 index 0000000..3f56a1f --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-content-comp.html @@ -0,0 +1,2 @@ + + diff --git a/src/tip/demo/src/app/tip/tip-content-template.html b/src/tip/demo/src/app/tip/tip-content-template.html new file mode 100644 index 0000000..b169b94 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-content-template.html @@ -0,0 +1,8 @@ + + + + + + + {{context}} + diff --git a/src/tip/demo/src/app/tip/tip-empty.html b/src/tip/demo/src/app/tip/tip-empty.html new file mode 100644 index 0000000..5ca6579 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-empty.html @@ -0,0 +1,21 @@ +

    描述

    +

    当组件内容为空(null,undefined、空字符串)时,tip自动隐藏。

    +

    示例

    +

    1、提示内容的数据类型为字符串,将空字符串赋值给内容变量,不显示tip

    +
    mouseover to show tip
    + +

    tipStr:{{tipStr}}

    + +

    2、提示内容为模板,模板为undefined时,不显示tip

    +
    mouseover to show tip
    + +

    showTipTemplate:{{showTipTemplate}}

    + + + {{context.label}} + + +

    3、提示内容为组件,设置组件为null时,不显示tip

    +
    mouseover to show tip
    + +

    组件:{{component}}

    diff --git a/src/tip/demo/src/app/tip/tip-has-arrow.html b/src/tip/demo/src/app/tip/tip-has-arrow.html new file mode 100644 index 0000000..cef1a7e --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-has-arrow.html @@ -0,0 +1 @@ + diff --git a/src/tip/demo/src/app/tip/tip-long-text-position.html b/src/tip/demo/src/app/tip/tip-long-text-position.html new file mode 100644 index 0000000..719ee87 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-long-text-position.html @@ -0,0 +1,17 @@ +

    本测试用例主要测试tip中的文字超长,在屏幕显示不下时,出滚动条,在各个方向的展示情况

    +
    +
    +
    top
    +
    top
    +
    top-left
    +
    +
    +
    left
    +
    right
    +
    +
    +
    bottom-right
    +
    bottom
    +
    bottom-left
    +
    +
    diff --git a/src/tip/demo/src/app/tip/tip-max-width.html b/src/tip/demo/src/app/tip/tip-max-width.html new file mode 100644 index 0000000..c5bf0fc --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-max-width.html @@ -0,0 +1 @@ + diff --git a/src/tip/demo/src/app/tip/tip-position-test.html b/src/tip/demo/src/app/tip/tip-position-test.html new file mode 100644 index 0000000..718766c --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-position-test.html @@ -0,0 +1,18 @@ +

    本测试用例除了测位置以外,也测试了有滚动条情况下的tip行为

    +
    +
    top
    +
    top-left
    +
    top-right
    +
    bottom
    +
    bottom-left
    +
    bottom-right
    +
    left
    +
    left-top
    +
    left-bottom
    +
    right
    +
    right-top
    +
    right-bottom
    +
    center
    + +
    auto
    +
    diff --git a/src/tip/demo/src/app/tip/tip-position.html b/src/tip/demo/src/app/tip/tip-position.html new file mode 100644 index 0000000..efd7a5d --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-position.html @@ -0,0 +1,23 @@ +
    +
    + + + +
    +
    + + + +
    + +
    + + + +
    +
    + + + +
    +
    diff --git a/src/tip/demo/src/app/tip/tip-service-destroy.html b/src/tip/demo/src/app/tip/tip-service-destroy.html new file mode 100644 index 0000000..bcf4af4 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-service-destroy.html @@ -0,0 +1,9 @@ +

    1 描述

    +

    服务生成方式生成的实例tipInstance 可以通过tipInstance.destroy()销毁

    +

    2 示例

    +

    2.1 tip实例 destroy()方法

    +
    +
    + + + diff --git a/src/tip/demo/src/app/tip/tip-service.html b/src/tip/demo/src/app/tip/tip-service.html new file mode 100644 index 0000000..13c7182 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-service.html @@ -0,0 +1,2 @@ +
    + diff --git a/src/tip/demo/src/app/tip/tip-trigger.html b/src/tip/demo/src/app/tip/tip-trigger.html new file mode 100644 index 0000000..0f0d092 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-trigger.html @@ -0,0 +1 @@ + diff --git a/src/tip/demo/src/app/tip/tip-valid-position-test.html b/src/tip/demo/src/app/tip/tip-valid-position-test.html new file mode 100644 index 0000000..1ae74f8 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-valid-position-test.html @@ -0,0 +1,17 @@ +

    本测试用例主要测试tip在开发者指定的位置显示不下时,能够自适应在其能显示的位置显示

    +
    +
    +
    top
    +
    top
    +
    top-left
    +
    +
    +
    left
    +
    right
    +
    +
    +
    bottom-right
    +
    bottom
    +
    bottom-left
    +
    +
    diff --git a/src/tip/demo/src/app/tip/tip-zindex.html b/src/tip/demo/src/app/tip/tip-zindex.html new file mode 100644 index 0000000..1ea9dbc --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-zindex.html @@ -0,0 +1,6 @@ +
    +
    z-index: 1000
    + +
    + +
    diff --git a/src/tip/demo/src/app/tip/tipTest.less b/src/tip/demo/src/app/tip/tipTest.less new file mode 100644 index 0000000..845d19e --- /dev/null +++ b/src/tip/demo/src/app/tip/tipTest.less @@ -0,0 +1,38 @@ +.blockCls { + border: 1px solid #ccc; + width: 100px; + height: 100px; + margin: 10px 0; +} +.demo { + border: 1px solid #ccc; + width: 80px; + margin: 10px; + padding: 10px; + display: inline-block; + text-align: center; +} +.demo-tiny { + border: 1px solid #ccc; + margin: 5px 10px; + padding: 2px; + display: inline-block; + cursor: default; +} +body { + margin: 100px; + position: relative; +} +.test-tip { + border: 1px solid #ccc; + width: 200px; + height: 100px; + display: inline-block; +} +.center { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + margin: 0; +} diff --git a/src/tip/demo/src/app/tip/webdoc/tip-demos.js b/src/tip/demo/src/app/tip/webdoc/tip-demos.js new file mode 100644 index 0000000..eb95d75 --- /dev/null +++ b/src/tip/demo/src/app/tip/webdoc/tip-demos.js @@ -0,0 +1,123 @@ +export default { + column: '2', + demos: [ + { + demoId: 'tip-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'tiTip', + }, + desc: { + 'zh-CN': '

    通过属性tiTip配置气泡提示内容。

    ', + 'en-US': '

    tiTip

    ', + }, + apis: ['TiTipDirective.properties.tiTip'], + }, + { + demoId: 'tip-content-template', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'tiTipContext', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipContext配置显示内容对应的上下文。

    ', + 'en-US': '

    button color

    ', + }, + apis: ['TiTipDirective.properties.tiTipContext'], + }, + { + demoId: 'tip-content-comp', + name: { + 'zh-CN': '内容为组件', + 'en-US': 'component', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipContext配置显示内容对应的上下文,适用于 tip 内容较复杂且多个页面中共用的场景。

    ', + 'en-US': '

    item

    ', + }, + apis: ['TiTipDirective.properties.tiTipContext'], + }, + { + demoId: 'tip-has-arrow', + name: { + 'zh-CN': '气泡提示不带箭头', + 'en-US': 'hasArrow', + }, + desc: { + 'zh-CN': '

    通过属性tiTipHasArrow配置是否带箭头。

    ', + 'en-US': '

    item

    ', + }, + apis: ['TiTipDirective.properties.tiTipHasArrow'], + }, + { + demoId: 'tip-position', + name: { + 'zh-CN': '位置', + 'en-US': 'position', + }, + desc: { + 'zh-CN': '

    通过属性tiTipPosition配置 tip 显示位置。

    ', + 'en-US': '

    item

    ', + }, + apis: ['TiTipDirective.properties.tiTipPosition', 'TiPositionType'], + }, + { + demoId: 'tip-max-width', + name: { + 'zh-CN': '最大宽度', + 'en-US': 'position', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipMaxWidth配置 tip 的最大宽度。

    ', + 'en-US': '

    item

    ', + }, + apis: ['TiTipDirective.properties.tiTipMaxWidth'], + }, + { + demoId: 'tip-service', + name: { + 'zh-CN': '服务生成', + 'en-US': 'position', + }, + desc: { + 'zh-CN': '

    通过服务的方式生成 tip。

    ', + 'en-US': '

    item

    ', + }, + apis: [ + 'TiTipService.methods.create', + 'TiTipDirective.methods.hide', + 'TiTipDirective.methods.show', + 'TiTipShowInfo', + ], + }, + { + demoId: 'tip-zindex', + name: { + 'zh-CN': '层级', + 'en-US': 'position', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipZIndex配置 tip 的 z-index 属性值。

    ', + 'en-US': '

    item

    ', + }, + apis: ['TiTipDirective.properties.tiTipZIndex'], + }, + { + demoId: 'tip-trigger', + name: { + 'zh-CN': '点击触发', + 'en-US': 'trigger', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipTrigger配置 tip 的生成方式。

    ', + 'en-US': '

    item

    ', + }, + apis: ['TiTipDirective.properties.tiTipTrigger'], + }, + ], +}; diff --git a/src/tip/demo/src/app/tip/webdoc/tip.cn.md b/src/tip/demo/src/app/tip/webdoc/tip.cn.md new file mode 100644 index 0000000..985a460 --- /dev/null +++ b/src/tip/demo/src/app/tip/webdoc/tip.cn.md @@ -0,0 +1,23 @@ +--- +title: Tip 气泡提示 +--- +# Tip 气泡提示 + +
    + +Tip 是提供文本气泡提示的组件,可以通过指令方式、服务方式生成。 + +```typescript +import { TiTipModule } from '@opentiny/ng'; +``` + +
    + +
    + +Tip 是提供文本气泡提示的组件,可以通过指令方式、服务方式生成。 + +```typescript +import { TiTipModule } from '@opentiny/ng'; +``` +
    diff --git a/src/tip/demo/src/app/tip/webdoc/tip.en.md b/src/tip/demo/src/app/tip/webdoc/tip.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/tip/demo/src/app/tip/webdoc/tip.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/tip/demo/src/favicon.ico b/src/tip/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/tip/demo/src/index.html b/src/tip/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/tip/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/tip/demo/src/main.ts b/src/tip/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/tip/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/tip/demo/test.ts b/src/tip/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/tip/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/tip/demo/tsconfig.app.json b/src/tip/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/tip/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/tip/demo/tsconfig.spec.json b/src/tip/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/tip/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/tip/lib/index.ts b/src/tip/lib/index.ts new file mode 100644 index 0000000..b76064a --- /dev/null +++ b/src/tip/lib/index.ts @@ -0,0 +1,4 @@ +export * from './src/TiTipModule'; +export * from './src/TiTipServiceModule'; +export * from './src/TiTipService'; +export * from './src/TiTipInterface'; diff --git a/src/tip/lib/ng-package.json b/src/tip/lib/ng-package.json new file mode 100644 index 0000000..371691b --- /dev/null +++ b/src/tip/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/tip", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/tip/lib/package.json b/src/tip/lib/package.json new file mode 100644 index 0000000..9baf2b3 --- /dev/null +++ b/src/tip/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-tip", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/router": ">=13.0.0", + "@opentiny/ng-popup": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/tip/lib/project.json b/src/tip/lib/project.json new file mode 100644 index 0000000..18e5ef4 --- /dev/null +++ b/src/tip/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/tip/lib", + "sourceRoot": "src/tip/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/tip"], + "options": { + "project": "src/tip/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/tip"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js tip" + }, + { + "command": "ng default-build tip" + }, + { + "command": "node build/clear-default-theme.js tip" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/tip && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build tip && ng pack tip && node build/publish.js tip --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/tip/lib/src/TiTipContainerComponent.ts b/src/tip/lib/src/TiTipContainerComponent.ts new file mode 100644 index 0000000..2763f80 --- /dev/null +++ b/src/tip/lib/src/TiTipContainerComponent.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +/** + * @ignore + */ +@Component({ + selector: 'ti-tip-container', + template: `
    +
    `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-tooltip]': 'true', + '[style.z-index]': 'zIndex' + }, + styleUrls: ['./tip.less'] +}) +export class TiTipContainerComponent { + @Input() zIndex: number; +} diff --git a/src/tip/lib/src/TiTipDirective.ts b/src/tip/lib/src/TiTipDirective.ts new file mode 100644 index 0000000..1cdad46 --- /dev/null +++ b/src/tip/lib/src/TiTipDirective.ts @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ComponentRef, Directive, ElementRef, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; +import { TiPositionType, Util } from '@opentiny/ng-utils'; +import { TiTipRef, TiTipShowInfo } from './TiTipInterface'; +import { TiTipService } from './TiTipService'; + +// TODO:exportAs: 'tiTip' 可去除? +/** + * tip提供了两种使用方式: + * 1.服务方式:[TiTipService]{@link ../injectables/TiTipService.html} + * + * 2.指令方式(见如下说明) + * + */ +@Directive({ + selector: '[tiTip]', + exportAs: 'tiTip' +}) +export class TiTipDirective implements OnInit, OnDestroy { + /** + * tip 提示方向 + */ + @Input() tiTipPosition?: TiPositionType = 'auto'; + /** + * tip 最大宽度 + */ + @Input() tiTipMaxWidth: string = '276px'; + /** + * 是否带箭头 + */ + @Input() tiTipHasArrow?: boolean = true; + /** + * tip 显示内容对应的上下文,tip内容类型为 templateRef 或 Component 形式时会用到该参数,参数为自定义对象形式 + * + * 注意:指令形式时才会使用到该参数 + */ + @Input() tiTipContext?: any; + /** + * tip 触发行为,默认支持鼠标移入时显示 + * + * 注意:指令形式时才会使用到该参数 + */ + @Input() tiTipTrigger?: 'mouse' | 'manual' = 'mouse'; + /** + * @ignore + * 决定tip水平方向位置的宿主元素配置 + */ + @Input() tiTipHostEleX: Element; + /** + * z-index 属性值 + */ + @Input() tiTipZIndex: number; + protected tipInstance: TiTipRef; + private hostEle: Element; + private _tiTip: string | TemplateRef | any; + constructor(private tipService: TiTipService, hostEleRef: ElementRef) { + this.hostEle = hostEleRef.nativeElement; + } + + private static isInValidValue(value: string | TemplateRef | any): boolean { + return Util.isUndefined(value) || Util.isNull(value) || value === ''; + } + ngOnInit(): void { + // 初始创建tip实例 + this.tipInstance = this.tipService.create(this.hostEle, { + position: this.tiTipPosition, + maxWidth: this.tiTipMaxWidth, + hasArrow: this.tiTipHasArrow, + hostEleX: this.tiTipHostEleX, + zIndex: this.tiTipZIndex, + trigger: this.tiTipTrigger || 'mouse', // 指令方式,默认为'mouse' + showFn: (): TiTipShowInfo => { + if (!this.tipInstance || TiTipDirective.isInValidValue(this._tiTip)) { + return; + } + + return { content: this._tiTip, context: this.tiTipContext }; + } + }); + } + /** + * tip显示内容配置 + * + * 类型:string | TemplateRef | any + * + * 传入string类型时,在v3.0.6及之前的版本存在XSS攻击风险;v3.0.7做了安全处理后不存在XSS风险了,把传入的字符串当做纯文本解析,传入什么显示什么; + * v10.1.4版本支持html字符串片段,也已做安全处理。 + */ + @Input() + // 监听tip值的变化,当tip为''或undefined情况下,直接隐藏tip组件 + /** + * @ignore + */ + set tiTip(value: string | TemplateRef | any) { + this._tiTip = value; + if (TiTipDirective.isInValidValue(value)) { + this.hide(); + } + } + /** + * @ignore + */ + get tiTip(): string | TemplateRef | any { + return this._tiTip; + } + /** + * 显示 tip 的方法 + */ + public show(): ComponentRef { + if (!this.tipInstance || TiTipDirective.isInValidValue(this._tiTip)) { + return; + } + + return this.tipInstance.show(this._tiTip, this.tiTipContext); + } + /** + * 隐藏 tip 的方法 + */ + public hide(): void { + if (this.tipInstance) { + this.tipInstance.hide(); + } + } + // 宿主销毁,tip连带销毁 + ngOnDestroy(): void { + if (this.tipInstance) { + this.tipInstance.hide(); + this.tipInstance = null; + } + } +} diff --git a/src/tip/lib/src/TiTipInterface.ts b/src/tip/lib/src/TiTipInterface.ts new file mode 100644 index 0000000..49c1a8e --- /dev/null +++ b/src/tip/lib/src/TiTipInterface.ts @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ComponentRef, TemplateRef } from '@angular/core'; +import { TiPositionEventType, TiPositionType } from '@opentiny/ng-utils'; +/** + * showFn 函数返回值类型 + * + */ +export interface TiTipShowInfo { + /** + * tip 显示内容,可为字符串/ng-template/component + */ + content: string | TemplateRef | any; + /** + * tip 显示内容对应的上下文,content类型为templateRef或Component形式时会用到该参数 + */ + context?: any; +} +/** + * tip配置项, + * 作为[TiTipService.create]{@link ../injectables/TiTipService.html#create}方法中的参数类型使用 + */ +export interface TiTipConfig { + /** + * 位置 + * + * @default 'auto' + */ + position?: TiPositionType; + /** + * 最大宽度 + * + * @default '276px' + */ + maxWidth?: string; + /** + * 是否带箭头 + * + * @default true + */ + hasArrow?: boolean; + /** + * tip色系 'white'(tip背景为浅色)/'dark'(tip背景为深色), 不设置时默认为 'dark' + */ + theme?: 'dark' | 'white'; + /** + * tip 的触发方式,设置为 'manual' 时手动触发,设置为 'mouse' 时应和 showFn 结合使用鼠标在宿主悬浮时触发 + */ + trigger?: 'mouse' | 'manual'; + /** + * @ignore + * + * 页面不可见时,是否让tip消失,默认消失,intro组件不消失 + */ + registerVisibilityChangeEvent?: boolean; + /** + * + * tip的z-index属性值 + */ + zIndex?: number; + /** + * @ignore + */ + positionEventTypes?: Array; + /** + * trigger 设置为 'mouse' 时的显示函数,返回值类型为 TiTipShowInfo + */ + showFn?(): TiTipShowInfo; + /** + * @ignore + * 决定定位元素水平方向的元素,用于宿主元素水平方向位置与host元素不一致的场景,暂不对外开放 + */ + hostEleX?: Element; +} +/** + * tip生成实例对象 + * 作为[TiTipService.create]{@link ../injectables/TiTipService.html#create}方法的返回值使用 + */ +export interface TiTipRef { + /** + * @ignore + * 内部变量,用于标识mouse事件触发情况下,鼠标是否在tip元素上 + */ + isInsideTip?: boolean; + /** + * 显示tip方法 + * + * **函数类型:**(content: string | TemplateRef | any, context?: any) => void; + * + * **参数类型:** + * + * 1. content tip显示内容,可为字符串/ng-template/component,具体用法见示例 + * + * 2. context tip显示内容对应的上下文,content类型为templateRef或Component形式时会用到该参数 + * + */ + show(content: string | TemplateRef | any, context?: any): ComponentRef; + /** + * 隐藏tip方法 + * + * **函数类型:** () => void; + */ + hide(): void; + /** + * @ignore + * 销毁tip实例方法,如下场景需要此方法: + * 一定条件下,希望通过TiTpService给目标元素添加tip,并使用mouseenter/mouseleave 触发tip, 当条件改变后需要去除目标元素上的tip。 + * 目前 TpFrozen 在使用此接口 + * **函数类型:** () => void; + */ + destroy(): void; +} diff --git a/src/tip/lib/src/TiTipModule.ts b/src/tip/lib/src/TiTipModule.ts new file mode 100644 index 0000000..5bf0ca9 --- /dev/null +++ b/src/tip/lib/src/TiTipModule.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiTipDirective } from './TiTipDirective'; +import { TiTipServiceModule } from './TiTipServiceModule'; +// 引用TiTipModule,可以使用Tip指令和Tip服务 +@NgModule({ + imports: [CommonModule, TiTipServiceModule], + exports: [TiTipDirective], + declarations: [TiTipDirective] +}) +export class TiTipModule {} +export { TiTipDirective } from './TiTipDirective'; diff --git a/src/tip/lib/src/TiTipService.ts b/src/tip/lib/src/TiTipService.ts new file mode 100644 index 0000000..b8b4b19 --- /dev/null +++ b/src/tip/lib/src/TiTipService.ts @@ -0,0 +1,343 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/* eslint-disable no-eq-null */ +/* eslint-disable eqeqeq */ +/** + * 该类提供服务,用于管理Tip组件的创建和销毁 + * 服务中提供三个方法: + * create(hostEle, config) 生成一个tip实例并返回对象, + * hostEle:宿主元素 + * config:{ + * position:tip元素位置 + * maxWidth:最大宽度 + * hasArrow:是否带箭头 + * theme:tip色系 'white'/'dark' + * } + * 返回的实例对象中提供方法: + * { + * show({ // 显示Tip组件 + * content:弹出组件内容 + * context:弹出组件上下文 + * }) + * hide():隐藏并销毁Tip + * } + */ +import { ComponentRef, Inject, Injectable, NgZone, Optional, Renderer2, RendererFactory2 } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { NavigationEnd, Router, RouterEvent } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; + +import { TiContentType, TiPopUpRef, TiPopupService } from '@opentiny/ng-popup'; +import { Position, TiPositionResult, TiPositionType, Util } from '@opentiny/ng-utils'; +import { TiRenderer } from '@opentiny/ng-renderer'; + +import { TiTipContainerComponent } from './TiTipContainerComponent'; +import { TiTipServiceModule } from './TiTipServiceModule'; +import { TiTipConfig, TiTipRef, TiTipShowInfo } from './TiTipInterface'; +/** + * @ignore + */ +export interface TiTipShowConfig { + popInstance: TiPopUpRef; + hostEle: Element; + content: TiContentType; + context: any; + config: TiTipConfig; +} + +/** + * @ignore + */ +interface ShareValues { + mouseenterTimer: any; +} +/** + * tip提供了两种使用方式: + * + * 1.服务方式(见如下说明),使用该服务时需要引入模块TiTipServiceModule + * + * 2.指令方式:[TiTipDirective]{@link ../directives/TiTipDirective.html} + * + */ +@Injectable({ + providedIn: TiTipServiceModule +}) +export class TiTipService { + private static readonly DEFAULT_WIDTH: number = 276; // tip换行宽度 + private static readonly SPACE: number = 6.5 + 5; // tip框与元素本身的距离 = 三角宽高 + tip三角到触发tip的内容区域的距离 + private static readonly MOUSE_ENTER_DELAY: number = 500; // 防止每次鼠标不小心经过目标元素就会显示出tip的内容,所以增加适当的延迟 + private render: Renderer2; + /** + * @ignore + */ + public positionResult: TiPositionResult; // Position.setPosition()的返回值; + constructor( + private popService: TiPopupService, + rendererFactory: RendererFactory2, + private tiRenderer: TiRenderer, + private zone: NgZone, + @Inject(DOCUMENT) private document, + @Optional() private router: Router + ) { + this.render = rendererFactory.createRenderer(null, null); + } + /** + * 页面激活窗口改变事件处理,此处获取事件名称,该事件后续会在tip显示时注册,隐藏时销毁 + * 添加该事件用于解决的问题现象是:tip带链接,并且点击链接跳转至新开页面,因此,当返回先前页面时,tip不消失(因为未触发任何tip消失的事件), + * 且移入其他出tip的元素,会出现页面有多个tip的现象 + */ + private getVisibleChangeEventName(): string { + const hiddenProperty: string = + 'hidden' in this.document + ? 'hidden' + : 'webkitHidden' in this.document + ? 'webkitHidden' + : 'mozHidden' in this.document + ? 'mozHidden' + : ''; + + return hiddenProperty.replace(/hidden/i, 'visibilitychange'); + } + /** + * 创建 tip 实例 + */ + public create(hostEle: Element, config?: TiTipConfig): TiTipRef { + // 宿主元素不存在情况下,不做处理 + if (!Util.isElement(hostEle)) { + return; + } + const shareValues: ShareValues = { mouseenterTimer: undefined }; + const tipConfig: TiTipConfig = config ? config : {}; + const tipInstance: TiTipRef = this.createTip(hostEle, shareValues, tipConfig); + this.addTriggerEvent(hostEle, tipConfig, tipInstance, shareValues); + + return tipInstance; + } + /** + * 创建Tip实例 + * + * @param hostEle tip生成所依附的宿主元素 + * + * @param config tip属性配置 + * + * @returns 生成的Tip实例对象如下: + * + * ``` + * { + * // 显示tip方法 + * // @param content {string | TemplateRef | any} tip显示内容,可为字符串/ng-template/component,具体用法见示例 + * // @param context {any} tip显示内容对应的上下文,content类型为templateRef或Component形式时会用到该参数 + * show: (content: string | TemplateRef | any, context?: any) => void; + * + * // 隐藏tip方法 + * hide: () => void; + * } + * ``` + */ + private createTip(hostEle: Element, shareValues: ShareValues, config?: TiTipConfig): TiTipRef { + const popInstance: TiPopUpRef = this.popService.create(TiTipContainerComponent); // tip弹出服务实例,可通过调用show/hide方法切换组件的显示状态 + let tipComponentRef: any; // 生成好的 tip 组件实例对象componentRef + const visibilityChangeEvent: string = this.getVisibleChangeEventName(); + let visibleChangeEvtHandle: () => void; + let routerChangeSub: Subscription; + + // 组件添加全局事件销毁相关处理 + let eventHandles: Array<() => void> = []; // 用于存储事件句柄,事件句柄在事件取消时需要用到 + + // 隐藏处理函数 + const hideFn: () => void = (): void => { + if (shareValues?.mouseenterTimer !== undefined) { + clearTimeout(shareValues.mouseenterTimer); + } + if (tipComponentRef != null) { + // 销毁弹出元素 + popInstance.hide(); + // 通过执行事件返回句柄方法解绑事件 + Position.removePosChangeEvts(eventHandles); + if (config.registerVisibilityChangeEvent !== false) { + visibleChangeEvtHandle(); + } + routerChangeSub?.unsubscribe(); + tipComponentRef = null; + } + }; + + return { + show: (content: string, context?: any): ComponentRef | undefined => { + if (Util.isUndefined(content) || content === '') { + return; + } + // 显示Tip元素 + tipComponentRef = this.showTip({ + popInstance, + hostEle, + content, + context, + config + }); + // 添加全局事件,用于控制特殊情况下宿主位置改变时Tip的隐藏 + eventHandles = Position.addPosChangeEvts(hideFn, this.render, config.positionEventTypes); + if (config.registerVisibilityChangeEvent !== false) { + visibleChangeEvtHandle = this.render.listen(this.document, visibilityChangeEvent, hideFn); + } + // 部分服务使用了路由复用策略重写了 angular/router 的 RouteReuseStrategy 接口 + // 使用 hashchange 监听不到通过 routerLink/navigate 路由跳转的场景 + // 此时宿主元素的 ngOnDestroy 不会触发不会销毁 tip,因此监听路由变化的方式 + routerChangeSub = this.router?.events.pipe(filter((event: RouterEvent) => event instanceof NavigationEnd)).subscribe(() => { + hideFn(); + }); + + return tipComponentRef; + }, + hide: (): void => { + hideFn(); + }, + // 销毁tip + destroy: (): void => { + // 销毁tip前先隐藏 + hideFn(); + } + }; + } + + // 根据trigger配置为宿主元素添加事件,该事件用于控制tip的显示/隐藏 + private addTriggerEvent(hostEle: Element, config: TiTipConfig, tipInstance: TiTipRef, shareValues: ShareValues): void { + // 非mouse情况下,不用做事件处理 + if (config.trigger !== 'mouse') { + return; + } + let tipComponentRef: ComponentRef = null; + // 默认情况下,使用mouse进行tip显示和隐藏控制(只对指令形式有效) + this.zone.runOutsideAngular(() => { + const unlistenMouseenterFn: () => void = this.render.listen(hostEle, 'mouseenter', () => { + if (typeof config.showFn !== 'function') { + return; + } + shareValues.mouseenterTimer = setTimeout(() => { + this.zone.run(() => { + const showInfo: TiTipShowInfo = config.showFn(); + if (!showInfo) { + return; + } + tipComponentRef = tipInstance.show(showInfo.content, showInfo.context); + if (!tipComponentRef) { + return; + } + // 根据trigger配置添加tip元素本身事件,此处事件用于支持移出tip元素时tip消失 + const targetEle: Element = tipComponentRef.location.nativeElement; + // eslint-disable-next-line max-nested-callbacks + this.render.listen(targetEle, 'mouseleave', (event: MouseEvent) => { + /** + * 此处处理是为了解决Chrome高版本下,连续点击tip区域情况下,导致tip消失的问题 + * 【问题原因】chrome高版本(chrome60以上版本)下,连续的click事件会触发tipEle的mouseleave事件, + * 从而导致tip消失 + * 【解决方案】如mouseleve事件是由tip元素本身点击触发的,则event.relatedTarget为null,则通过该 + * 方式进行特殊情况排除 + */ + if (event.relatedTarget === null) { + return; + } + tipInstance.hide(); + tipComponentRef = null; + }); + }); + }, TiTipService.MOUSE_ENTER_DELAY); + }); + const unlistenMouseleaveFn: () => void = this.render.listen(hostEle, 'mouseleave', (event: MouseEvent) => { + if (shareValues?.mouseenterTimer !== undefined) { + clearTimeout(shareValues?.mouseenterTimer); + } + // 鼠标移入tip时,tip不消失 + if (tipComponentRef && !tipComponentRef.location.nativeElement.contains(event.relatedTarget)) { + this.zone.run(() => { + tipInstance.hide(); + tipComponentRef = null; + }); + } + }); + // 给实例添加销毁方法 + tipInstance.destroy = (): void => { + // 先隐藏tip示例再取消监听事件 + tipInstance.hide(); + unlistenMouseenterFn(); + unlistenMouseleaveFn(); + }; + }); + } + + private showTip(options: TiTipShowConfig): ComponentRef { + const tipComponentRef: ComponentRef = options.popInstance.show({ + content: options.content, + contentContext: options.context, + context: { zIndex: options.config.zIndex }, + container: 'body' + }); + const targetEle: Element = tipComponentRef.location.nativeElement; + this.tiRenderer.setStyles(targetEle, { + left: '-9999px', + top: '-9999px' + }); + this.setTipTheme(targetEle, options.config); + // 计算元素宽高时,需要确保元素已生成 + this.setTipWidth(targetEle, options.config); + this.setPosition(options.hostEle, targetEle, options.config); + + return tipComponentRef; + } + + private setTipTheme(ele: Element, config: TiTipConfig): void { + if (!config || !config.theme) { + return; + } + if (config.theme === 'white') { + this.render.addClass(ele, 'ti3-tip-white-theme'); + } + } + + private setTipWidth(ele: Element, config: TiTipConfig): void { + const maxWidth: number = parseInt((config && config.maxWidth) || (TiTipService.DEFAULT_WIDTH as any), 10); + // 修复SSR错误:ERROR TypeError: ele.getBoundingClientRect is not a function + if (typeof ele.getBoundingClientRect !== 'function') { + return; + } + const targetWidth: number = ele.getBoundingClientRect().width; + if (targetWidth > maxWidth) { + // 如果宽度超过最大值,重新设置当前tip的内容宽度 + this.render.setStyle(ele, 'width', `${maxWidth}px`); + } + } + + private setPosition(hostEle: Element, targetEle: Element, config: TiTipConfig): void { + this.positionResult = Position.setPosition({ + targetEle, + hostEle, + hostEleX: config.hostEleX, + position: (config && config.position) || 'auto', + hostSpace: TiTipService.SPACE, + fixMaxHeight: true, + hasOffsetFix: true + }); + const position: TiPositionType = this.positionResult.position; + if (!config || config.hasArrow !== false) { + // 未定义或定义为false情况下都需要加该样式类控制三角样式 + this.render.addClass(targetEle, `ti3-tooltip-${position}`); + } + // 设置该样式类是为了支持鼠标移入Tip不消失:使用该样式类用于确保tip和宿主元素连接处的DOM在tip范围内 + this.render.addClass(targetEle.querySelector('.ti3-tooltip-sqr'), `ti3-tooltip-${position}-sqr`); + // 设置当前tip的内容最大高度,超出显示滚动条 + this.tiRenderer.setStyles(targetEle.querySelector('.ti3-tooltip-content'), { + // targetEle的最大高度 = 可视区域高度avilableHeight - 11 * 2 (tip的横向padding) - 1 * 2(border) + maxHeight: `${this.positionResult.avilableHeight - 22 - 2}px` + }); + } +} diff --git a/src/tip/lib/src/TiTipServiceModule.ts b/src/tip/lib/src/TiTipServiceModule.ts new file mode 100644 index 0000000..69ad2d7 --- /dev/null +++ b/src/tip/lib/src/TiTipServiceModule.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiTipContainerComponent } from './TiTipContainerComponent'; +import { TiPopupModule } from '@opentiny/ng-popup'; +import { TiRendererModule } from '@opentiny/ng-renderer'; + +/** + * @ignore + */ +@NgModule({ + imports: [TiPopupModule, TiRendererModule], + exports: [], + declarations: [TiTipContainerComponent], + entryComponents: [TiTipContainerComponent] +}) +export class TiTipServiceModule {} diff --git a/src/tip/lib/src/tip.less b/src/tip/lib/src/tip.less new file mode 100644 index 0000000..bcf1f01 --- /dev/null +++ b/src/tip/lib/src/tip.less @@ -0,0 +1,343 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-tip-before-square-size: 10px; + --ti-tip-square-distance: 5px; // tip小三角的尖 到 宿主元素的距离 + --ti-tip-before-square-height: 9px; // (10*cos45=7)+1px边框 + 1px调整保证移动之后无缝隙 + --ti-tip-sqr-height: calc(var(--ti-tip-square-distance) + var(--ti-tip-before-square-height)); // sqr元素的宽、高 + // 4.0规范 tip都是深色,根据intro组件需求,添加white主题 + --ti-tip-white-theme-bg: var(--ti-common-color-bg-white-normal); + --ti-tip-z-index: 5000; + --ti-tip-offset: 9px; // 非居中对齐时,小箭头的偏移量 + --ti-tip-box-shadow-color: rgba(0, 0, 0, 0.2); +} + +:host.ti3-tooltip { + display: block; + position: absolute; + background-color: var(--ti-common-color-bg-dark-deep); + color: var(--ti-common-color-text-gray); + padding: calc(var(--ti-common-space-3x) - 1px) calc(var(--ti-common-space-4x) - 1px); //tip的内容区域的padding:1px,此处减去1px与规范保持一致 + border: 1px solid var(--ti-common-color-bg-dark-deep); + box-shadow: var(--ti-common-shadow-3-down); + border-radius: var(--ti-common-border-radius-1); + .box-sizing(border-box); + z-index: var(--ti-tip-z-index); + white-space: normal; // 自动换行属性,normal为默认值,此处处理为了防止外部样式影响 + word-wrap: break-word; // 换行方式:单词整体换行 + line-height: var(--ti-common-line-height-number); + &.ti3-tip-white-theme { + background-color: var(--ti-tip-white-theme-bg); + color: var(--ti-common-color-text-secondary); + border-color: var(--ti-common-color-bg-white-normal); + &::after, + &::before { + background: var(--ti-tip-white-theme-bg); + } + &.ti3-tooltip-top, + &.ti3-tooltip-top-left, + &.ti3-tooltip-top-right, + &.ti3-tooltip-bottom, + &.ti3-tooltip-bottom-right, + &.ti3-tooltip-bottom-left, + &.ti3-tooltip-left, + &.ti3-tooltip-left-top, + &.ti3-tooltip-left-bottom, + &.ti3-tooltip-right, + &.ti3-tooltip-right-top, + &.ti3-tooltip-right-bottom { + &::before { + border-left-color: var(--ti-common-color-bg-white-normal); + border-top-color: var(--ti-common-color-bg-white-normal); + } + } + & .ti3-tooltip-content { + .box-sizing(border-box); + overflow-y: auto; + padding: 1px; + } + } +} + +/** +* 此处设置padding:1px解决的问题是: +* 【问题现象】:ie浏览器下,汉字且字数较少时,未达到最大高度出现滚动条,主要是字体为汉字时会出现这个现象 +* 【出现该问题的原因】:属浏览器对字体解析的问题,具体原因不明 +* 【解决办法】:将内容区域的padding增加1px,修正因ie浏览器字体解析产生的偏差 +*/ + +.ti3-tooltip-content { + .box-sizing(border-box); + overflow-y: auto; + padding: 1px; + font-weight: var(--ti-common-font-weight-4); + font-size: var(--ti-common-font-size-base); + ::ng-deep a { + color: var(--ti-common-color-text-link-darkbg); + text-decoration: none; + &:hover { + color: var(--ti-common-color-text-link-darkbg-hover); + text-decoration: underline; + } + } +} + +//**-sqr元素为触发tooltip的内容区域与tooltip之间的连接的矩形过度区域 +// 提取-sqr的公共样式变量 +.ti3-tooltip-position-sqr(@width, @height) { + position: absolute; + background-color: transparent; + box-sizing: border-box; + width: @width; + height: @height; +} + +// 提取before的公共样式 +.ti3-tooltip-position-before { + display: block; + position: absolute; + border-left: 1px solid var(--ti-common-color-bg-dark-deep); + border-top: v1px solid var(--ti-common-color-bg-dark-deep); + content: ''; + width: var(--ti-tip-before-square-size); + height: var(--ti-tip-before-square-size); + overflow: hidden; + .rotate(45deg); + background: var(--ti-common-color-bg-dark-deep); +} +// 提取after的公共样式 +.ti3-tooltip-position-after(@width, @height) { + display: block; + position: absolute; + content: ''; + overflow: hidden; + background: var(--ti-common-color-bg-dark-deep); + width: @width; + height: @height; +} +.ti3-tooltip-bottom-sqr, +.ti3-tooltip-bottom-left-sqr, +.ti3-tooltip-bottom-right-sqr { + .ti3-tooltip-position-sqr(100%, var(--ti-tip-sqr-height)); + left: 0; + bottom: 100%; +} + +//before元素为旋转形成三角形的正方形,after元素为遮挡before元素另一半的三角的长方形 + +:host.ti3-tooltip-bottom::after { + .ti3-tooltip-position-after(20px, 10px); + left: 50%; + margin-left: -10px; + top: 0; +} +:host.ti3-tooltip-bottom::before { + .ti3-tooltip-position-before(); + left: 50%; + margin-left: calc(-1 * var(--ti-tip-before-square-size) / 2); + top: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px -1px 5px var(--ti-tip-box-shadow-color); +} +// position: bottom-left +:host.ti3-tooltip-bottom-left::after { + .ti3-tooltip-position-after(20px, 10px); + left: calc(var(--ti-tip-offset) - 3px); + top: 0; +} + +:host.ti3-tooltip-bottom-left::before { + .ti3-tooltip-position-before(); + left: var(--ti-tip-offset); + top: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: bottom-right +:host.ti3-tooltip-bottom-right::after { + .ti3-tooltip-position-after(20px, 10px); + right: calc(var(--ti-tip-offset) - 3px); + top: 0; +} + +:host.ti3-tooltip-bottom-right::before { + .ti3-tooltip-position-before(); + right: var(--ti-tip-offset); + top: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: top +.ti3-tooltip-top-sqr, +.ti3-tooltip-top-left-sqr, +.ti3-tooltip-top-right-sqr { + .ti3-tooltip-position-sqr(100%, var(--ti-tip-sqr-height)); + left: 0; + top: 100%; +} + +:host.ti3-tooltip-top::after { + .ti3-tooltip-position-after(20px, 10px); + left: 50%; + margin-left: -10px; + bottom: 0; +} + +:host.ti3-tooltip-top::before { + .ti3-tooltip-position-before(); + transform: rotate(-135deg); + left: 50%; + margin-left: calc(-1 * var(--ti-tip-before-square-size) / 2); + bottom: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: top-left +:host.ti3-tooltip-top-left::after { + .ti3-tooltip-position-after(20px, 10px); + left: calc(var(--ti-tip-offset) - 3px); + bottom: 0; +} + +:host.ti3-tooltip-top-left::before { + .ti3-tooltip-position-before(); + transform: rotate(-135deg); + left: var(--ti-tip-offset); + bottom: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: top-right +:host.ti3-tooltip-top-right::after { + .ti3-tooltip-position-after(20px, 10px); + right: calc(var(--ti-tip-offset) - 3px); + bottom: 0; +} + +:host.ti3-tooltip-top-right::before { + .ti3-tooltip-position-before(); + transform: rotate(-135deg); + right: var(--ti-tip-offset); + bottom: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: left +.ti3-tooltip-left-sqr, +.ti3-tooltip-left-top-sqr, +.ti3-tooltip-left-bottom-sqr { + .ti3-tooltip-position-sqr(var(--ti-tip-sqr-height), 100%); + top: 0; + left: 100%; +} + +:host.ti3-tooltip-left::after { + .ti3-tooltip-position-after(10px, 20px); + top: 50%; + margin-top: -10px; + right: 0; +} + +:host.ti3-tooltip-left::before { + .ti3-tooltip-position-before(); + transform: rotate(135deg); + top: 50%; + margin-top: calc(-1 * var(--ti-tip-before-square-size) / 2); + right: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: 1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: left-top +:host.ti3-tooltip-left-top::after { + .ti3-tooltip-position-after(10px, 20px); + top: calc(var(--ti-tip-offset) - 3px); + right: 0; +} + +:host.ti3-tooltip-left-top::before { + .ti3-tooltip-position-before(); + transform: rotate(135deg); + content: ''; + top: var(--ti-tip-offset); + right: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: 1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: left-bottom +:host.ti3-tooltip-left-bottom::after { + .ti3-tooltip-position-after(10px, 20px); + bottom: calc(var(--ti-tip-offset) - 3px); + right: 0; +} + +:host.ti3-tooltip-left-bottom::before { + .ti3-tooltip-position-before(); + transform: rotate(135deg); + bottom: var(--ti-tip-offset); + right: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: 1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: right +.ti3-tooltip-right-sqr, +.ti3-tooltip-right-top-sqr, +.ti3-tooltip-right-bottom-sqr { + .ti3-tooltip-position-sqr(var(--ti-tip-sqr-height), 100%); + top: 0; + right: 100%; +} + +:host.ti3-tooltip-right::after { + .ti3-tooltip-position-after(10px, 20px); + top: 50%; + margin-top: -10px; + left: 0; +} + +:host.ti3-tooltip-right::before { + .ti3-tooltip-position-before(); + transform: rotate(-45deg); + top: 50%; + margin-top: calc(-1 * var(--ti-tip-before-square-size) / 2); + left: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px 1px 5px var(--ti-tip-box-shadow-color); +} + +// position: right-top +:host.ti3-tooltip-right-top::after { + .ti3-tooltip-position-after(10px, 20px); + top: calc(var(--ti-tip-offset) - 3px); + left: 0; +} + +:host.ti3-tooltip-right-top::before { + .ti3-tooltip-position-before(); + transform: rotate(-45deg); + top: var(--ti-tip-offset); + left: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px 1px 5px var(--ti-tip-box-shadow-color); +} + +// position: right-bottom +:host.ti3-tooltip-right-bottom::after { + .ti3-tooltip-position-after(10px, 20px); + bottom: calc(var(--ti-tip-offset) - 3px); + left: 0; +} + +:host.ti3-tooltip-right-bottom::before { + .ti3-tooltip-position-before(); + transform: rotate(-45deg); + bottom: var(--ti-tip-offset); + left: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px 1px 5px var(--ti-tip-box-shadow-color); +} + +/* ---------------设置tip克隆DOM的样式----START----------------------------------------------*/ +.ti3-tooltip-clone { + overflow: visible !important; + position: absolute !important; + visibility: hidden !important; + .box-sizing(border-box); + width: auto !important; +} +/* ---------------设置tip克隆DOM的样式----END----------------------------------------------*/ diff --git a/src/transfer/demo/karma.conf.js b/src/transfer/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/transfer/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/transfer/demo/project.json b/src/transfer/demo/project.json new file mode 100644 index 0000000..6bba26c --- /dev/null +++ b/src/transfer/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/transfer/demo", + "sourceRoot": "src/transfer/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/transfer", + "index": "src/transfer/demo/src/index.html", + "main": "src/transfer/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/transfer/demo/tsconfig.app.json", + "assets": ["src/transfer/demo/src/favicon.ico", "src/transfer/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "transfer-demo:build:production" + }, + "development": { + "browserTarget": "transfer-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js transfer" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/transfer/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/transfer/demo/tsconfig.spec.json", + "karmaConfig": "src/transfer/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/transfer/demo/src/app/AppComponent.ts b/src/transfer/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/transfer/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/transfer/demo/src/app/AppModule.ts b/src/transfer/demo/src/app/AppModule.ts new file mode 100644 index 0000000..3d8b709 --- /dev/null +++ b/src/transfer/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TransferTestModule } from './transfer/TransferTestModule'; + +@NgModule({ + imports: [ + TransferTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/transfer/demo/src/app/IndexComponent.ts b/src/transfer/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..fb2707c --- /dev/null +++ b/src/transfer/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TransferTestModule } from './transfer/TransferTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TransferTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/transfer/demo/src/app/app.html b/src/transfer/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/transfer/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/transfer/demo/src/app/transfer/TransferBasicComponent.ts b/src/transfer/demo/src/app/transfer/TransferBasicComponent.ts new file mode 100644 index 0000000..8b896a2 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferBasicComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { myOptions, mySelecteds } from './data.js'; +@Component({ + templateUrl: './transfer-basic.html' +}) +export class TransferBasicComponent { + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + mySelecteds: Array = JSON.parse(JSON.stringify(mySelecteds)); +} diff --git a/src/transfer/demo/src/app/transfer/TransferDisabledComponent.ts b/src/transfer/demo/src/app/transfer/TransferDisabledComponent.ts new file mode 100644 index 0000000..cce71ae --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferDisabledComponent.ts @@ -0,0 +1,44 @@ +import { Component, ChangeDetectorRef } from '@angular/core'; +import { mySelecteds } from './data.js'; +@Component({ + templateUrl: './transfer-disabled.html' +}) +export class TransferDisabledComponent { + constructor(protected changeDetectorRef: ChangeDetectorRef) {} + + myOptions: Array = [ + { + label: '一帆风顺' + }, + { + label: '二龙戏珠', + disabled: true + }, + { + label: '三阳开泰', + disabled: false + }, + { + label: '四季发财' + }, + { + label: '五福临门' + }, + { + label: '六六大顺' + }, + { + label: '七星高照' + }, + { + label: '八面来风' + }, + { + label: '九九归一' + }, + { + label: '十全十美' + } + ]; + mySelecteds: Array = JSON.parse(JSON.stringify(mySelecteds)); +} diff --git a/src/transfer/demo/src/app/transfer/TransferEventComponent.ts b/src/transfer/demo/src/app/transfer/TransferEventComponent.ts new file mode 100644 index 0000000..a97ce82 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferEventComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { myOptions } from './data.js'; +@Component({ + templateUrl: './transfer-event.html' +}) +export class TransferEventComponent { + myLogs: Array = []; + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + mySelecteds: Array = []; + + transferToLeft(event: object): void { + this.myLogs = [...this.myLogs, `transferToLeft event = ${JSON.stringify(event)}}`]; + } + + transferToRight(event: object): void { + this.myLogs = [...this.myLogs, `transferToRight event = ${JSON.stringify(event)}}`]; + } + + onNgModelChange(event: object): void { + this.myLogs = [...this.myLogs, `onNgModelChange event = ${JSON.stringify(event)}}`]; + } +} diff --git a/src/transfer/demo/src/app/transfer/TransferIdComponent.ts b/src/transfer/demo/src/app/transfer/TransferIdComponent.ts new file mode 100644 index 0000000..bbd06a7 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferIdComponent.ts @@ -0,0 +1,38 @@ +import { Component, Inject } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { myOptions, mySelecteds } from './data.js'; +@Component({ + templateUrl: './transfer-id.html' +}) +export class TransferIdComponent { + // 设置穿梭框源数据 + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + // 设置穿梭框初始选中项,选中项会移动到右侧面板 + mySelecteds: Array = JSON.parse(JSON.stringify(mySelecteds)); + ids: Array = [ + 'myTransfer', + 'myTransfer_left_list_list_list', + 'myTransfer_left_list_list_0', + 'myTransfer_left_list_select_all', + 'myTransfer_to_left_button', + 'myTransfer_right_list_list_list', + 'myTransfer_right_list_list_0', + 'myTransfer_right_list_select_all', + 'myTransfer_to_right_button' + ]; + + idExistMap: Map = new Map(); + allIdExist: boolean = false; + // 修复SSR报错:ERROR ReferenceError: document is not defined + constructor(@Inject(DOCUMENT) private document) {} + ngDoCheck(): void { + this.allIdExist = true; + this.ids.forEach((id: string) => { + const idExist: boolean = this.document.getElementById(id) !== undefined; + this.idExistMap.set(id, idExist); + if (!idExist) { + this.allIdExist = false; + } + }); + } +} diff --git a/src/transfer/demo/src/app/transfer/TransferIdkeyComponent.ts b/src/transfer/demo/src/app/transfer/TransferIdkeyComponent.ts new file mode 100644 index 0000000..f0b8757 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferIdkeyComponent.ts @@ -0,0 +1,55 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './transfer-idkey.html' +}) +export class TransferIdkeyComponent { + // 设置穿梭框源数据 + myOptions: Array = [ + { + id: 1, + label: '一帆风顺' + }, + { + id: 2, + label: '一帆风顺' + }, + { + id: 3, + label: '一帆风顺' + }, + { + id: 4, + label: '四季发财' + }, + { + id: 5, + label: '五福临门' + }, + { + id: 6, + label: '六六大顺' + }, + { + id: 7, + label: '七星高照' + }, + { + id: 8, + label: '八面来风' + }, + { + id: 9, + label: '九九归一' + }, + { + id: 10, + label: '十全十美' + } + ]; + mySelecteds: Array = [ + { + id: 4, + label: '四季发财' + } + ]; +} diff --git a/src/transfer/demo/src/app/transfer/TransferLabelkeyComponent.ts b/src/transfer/demo/src/app/transfer/TransferLabelkeyComponent.ts new file mode 100644 index 0000000..e8f2000 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferLabelkeyComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { valueOptions } from './data.js'; +@Component({ + templateUrl: './transfer-labelkey.html' +}) +export class TransferLabelkeyComponent { + myOptions: Array = JSON.parse(JSON.stringify(valueOptions)); + mySelecteds: Array = []; + labelKey: string = 'label'; +} diff --git a/src/transfer/demo/src/app/transfer/TransferLazyComponent.ts b/src/transfer/demo/src/app/transfer/TransferLazyComponent.ts new file mode 100644 index 0000000..5078ec7 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferLazyComponent.ts @@ -0,0 +1,21 @@ +import { Component, ChangeDetectorRef } from '@angular/core'; +import { myOptions, mySelecteds } from './data.js'; +@Component({ + templateUrl: './transfer-lazy.html' +}) +export class TransferLazyComponent { + constructor(protected changeDetectorRef: ChangeDetectorRef) {} + // 设置穿梭框源数据 + myOptions: Array = []; + // 设置穿梭框初始选中项,选中项会移动到右侧面板 + mySelecteds: Array = []; + + ngOnInit(): void { + setTimeout(() => { + this.myOptions = JSON.parse(JSON.stringify(myOptions)); + this.mySelecteds = JSON.parse(JSON.stringify(mySelecteds)); + // OnPush模式下,需要手动更新数据 + this.changeDetectorRef.markForCheck(); + }, 1000); + } +} diff --git a/src/transfer/demo/src/app/transfer/TransferLoadComponent.ts b/src/transfer/demo/src/app/transfer/TransferLoadComponent.ts new file mode 100644 index 0000000..5303fcb --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferLoadComponent.ts @@ -0,0 +1,85 @@ +import { Component } from '@angular/core'; +import { myOptions } from './data.js'; +@Component({ + templateUrl: './transfer-load.html', + styleUrls: ['./transfer.less'] +}) +export class TransferLoadComponent { + private dataA: Array = JSON.parse(JSON.stringify(myOptions)); + + private dataB: Array = [ + { label: '水调歌头' }, + { label: '苏轼' }, + { label: '明月几时有?把酒问青天' }, + { label: '不知天上宫阙,今夕是何年。' }, + { label: '我欲乘风归去,又恐琼楼玉宇,高处不胜寒。' }, + { label: '起舞弄清影,何似在人间?' }, + { label: '转朱阁,低绮户,照无眠。' }, + { label: '不应有恨,何事长向别时圆?' }, + { label: '人有悲欢离合,月有阴晴圆缺,此事古难全。' }, + { label: '但愿人长久,千里共婵娟。' } + ]; + + private dataC: Array = [ + { + label: '三阳开泰' + }, + { + label: '四季发财' + }, + { + label: '五福临门' + }, + { + label: '六六大顺' + }, + { + label: '七星高照' + }, + { + label: '八面来风' + }, + { + label: '九九归一' + }, + { + label: '十全十美' + } + ]; + + // 设置穿梭框源数据 + myOptions: Array; + // 设置穿梭框初始选中项,选中项会移动到右侧面板 + mySelecteds: Array; + + // 每个组件都用下面六个函数,只改变函数内容 + changeUndefined(): void { + this.myOptions = undefined; + } + changeNull(): void { + this.myOptions = null; + } + changeWrongType(): void { + const temp: any = 5; + this.myOptions = temp; + } + changeZeroData(): void { + this.myOptions = []; + } + changeDataA(): void { + this.myOptions = this.dataA; + this.mySelecteds = [this.myOptions[0], this.myOptions[1]]; + } + changeDataB(): void { + this.myOptions = this.dataB; + this.mySelecteds = [this.myOptions[0], this.myOptions[1]]; + } + changeDataC(): void { + this.myOptions = this.dataC; + } + + // 改变选中项 + changeSelects(): void { + this.mySelecteds = [this.myOptions[2], this.myOptions[3]]; + } +} diff --git a/src/transfer/demo/src/app/transfer/TransferNodatatextComponent.ts b/src/transfer/demo/src/app/transfer/TransferNodatatextComponent.ts new file mode 100644 index 0000000..7cc6dcd --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferNodatatextComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { myOptions } from './data.js'; +@Component({ + templateUrl: './transfer-nodatatext.html' +}) +export class TransferNodatatextComponent { + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + mySelecteds: Array = []; + noDataText: string = '暂无列表数据'; +} diff --git a/src/transfer/demo/src/app/transfer/TransferPaginationComponent.ts b/src/transfer/demo/src/app/transfer/TransferPaginationComponent.ts new file mode 100644 index 0000000..f8e251c --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferPaginationComponent.ts @@ -0,0 +1,34 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './transfer-pagination.html' +}) +export class TransferPaginationComponent { + myOptions: Array; + mySelecteds: Array; + myOptionsSearchable: Array; + mySelectedsSearchable: Array; + searchable: boolean = true; + // 设置搜索字段, 该数组中设置多个字段,就会根据该数组中的任意一个字段进行搜索匹配 + searchKeys: Array = ['label']; + private database: Array = [ + { label: 'America', disabled: true }, + { label: 'Brazil' }, + { label: 'Canada', disabled: true }, + { label: 'China' }, + { label: 'France', disabled: true }, + { label: 'Germany' }, + { label: 'Japan' }, + { label: 'South Korea' }, + { label: 'Turkey' }, + { label: 'United Kingdom' } + ]; + ngOnInit(): void { + const data: Array = []; + for (let i: number = 0; i < 136; i++) { + const item: any = this.database[i % 10]; + data.push({ ...item, label: i + item.label }); + } + this.myOptions = data; + this.myOptionsSearchable = [...data]; + } +} diff --git a/src/transfer/demo/src/app/transfer/TransferPlaceholderComponent.ts b/src/transfer/demo/src/app/transfer/TransferPlaceholderComponent.ts new file mode 100644 index 0000000..a2229b7 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferPlaceholderComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { myOptions, mySelecteds } from './data.js'; +@Component({ + templateUrl: './transfer-placeholder.html' +}) +export class TransferPlaceholderComponent { + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + mySelecteds: Array = JSON.parse(JSON.stringify(mySelecteds)); + searchable: boolean = true; + placeholder: string = '请搜索'; + + // 改变placeholder的内容 + changePlaceholder(): void { + this.placeholder = '请搜索关键字'; + } +} diff --git a/src/transfer/demo/src/app/transfer/TransferSearchableComponent.ts b/src/transfer/demo/src/app/transfer/TransferSearchableComponent.ts new file mode 100644 index 0000000..0d95667 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferSearchableComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { myOptions, mySelecteds } from './data.js'; +@Component({ + templateUrl: './transfer-searchable.html' +}) +export class TransferSearchableComponent { + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + mySelecteds: Array = JSON.parse(JSON.stringify(mySelecteds)); + searchable: boolean = true; +} diff --git a/src/transfer/demo/src/app/transfer/TransferSearchkeysComponent.ts b/src/transfer/demo/src/app/transfer/TransferSearchkeysComponent.ts new file mode 100644 index 0000000..2d8369f --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferSearchkeysComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { valueOptions } from './data.js'; +@Component({ + templateUrl: './transfer-searchkeys.html' +}) +export class TransferSearchkeysComponent { + // 设置穿梭框源数据 + myOptions: Array = JSON.parse(JSON.stringify(valueOptions)); + // 设置穿梭框初始选中项,选中项会移动到右侧面板 + mySelecteds: Array = [ + { + label: '四季发财', + name: '心远地自偏' + } + ]; + searchable: boolean = true; + // 设置搜索字段, 该数组中设置多个字段,就会根据该数组中的任意一个字段进行搜索匹配 + searchKeys: Array = ['label', 'name']; +} diff --git a/src/transfer/demo/src/app/transfer/TransferSizeComponent.ts b/src/transfer/demo/src/app/transfer/TransferSizeComponent.ts new file mode 100644 index 0000000..48091ce --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferSizeComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { myOptions, mySelecteds } from './data.js'; +@Component({ + templateUrl: './transfer-size.html' +}) +export class TransferSizeComponent { + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + mySelecteds: Array = JSON.parse(JSON.stringify(mySelecteds)); +} diff --git a/src/transfer/demo/src/app/transfer/TransferTableComponent.ts b/src/transfer/demo/src/app/transfer/TransferTableComponent.ts new file mode 100644 index 0000000..a1b7e11 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferTableComponent.ts @@ -0,0 +1,56 @@ +import { Component } from '@angular/core'; +import { TiTableRowData, TiTransferColumn } from '@opentiny/ng'; +@Component({ + templateUrl: './transfer-table.html' +}) +export class TransferTableComponent { + myOptions: Array; + mySelecteds: Array; + myOptions1: Array; + mySelecteds1: Array; + myOptions2: Array; + mySelecteds2: Array; + searchable: boolean = true; + searchKeys: Array = ['firstName', 'lastName']; + columns: Array = [ + { + title: 'First Name', // 表头列文本内容 + field: 'firstName', // 不自定义行展示模板时,表格每列按照源数据哪一项属性展示数据的标识值 + width: '40%' + }, + { + title: 'Last Name', + field: 'lastName', + width: '30%' + }, + { + title: 'Age', + field: 'age', + width: '30%' + } + ]; + ngOnInit(): void { + const data: Array = []; + for (let j: number = 0; j < 100; j++) { + data.push(this.createRandomItem(j)); + } + this.myOptions = data; + this.myOptions1 = JSON.parse(JSON.stringify(this.myOptions)); + this.myOptions2 = JSON.parse(JSON.stringify(this.myOptions)); + } + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[Math.floor(Math.random() * 5)]; + const lastName: string = familyName[Math.floor(Math.random() * 5)]; + const age: number = ((id + 3) * 13) % 100; + const disabled: boolean = Math.random() > 0.7 ? true : false; + + return { + firstName, + lastName, + age, + disabled + }; + } +} diff --git a/src/transfer/demo/src/app/transfer/TransferTestModule.ts b/src/transfer/demo/src/app/transfer/TransferTestModule.ts new file mode 100644 index 0000000..b8d72b6 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferTestModule.ts @@ -0,0 +1,122 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiIconModule, TiOverflowModule, TiPaginationModule, TiTransferModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TransferBasicComponent } from './TransferBasicComponent'; +import { TransferLazyComponent } from './TransferLazyComponent'; +import { TransferSizeComponent } from './TransferSizeComponent'; +import { TransferLabelkeyComponent } from './TransferLabelkeyComponent'; +import { TransferNodatatextComponent } from './TransferNodatatextComponent'; +import { TransferEventComponent } from './TransferEventComponent'; +import { TransferTitlesComponent } from './TransferTitlesComponent'; +import { TransferDisabledComponent } from './TransferDisabledComponent'; +import { TransferLoadComponent } from './TransferLoadComponent'; +import { TransferSearchableComponent } from './TransferSearchableComponent'; +import { TransferSearchkeysComponent } from './TransferSearchkeysComponent'; +import { TransferPlaceholderComponent } from './TransferPlaceholderComponent'; +import { TransferIdkeyComponent } from './TransferIdkeyComponent'; +import { TransferPaginationComponent } from './TransferPaginationComponent'; +import { TransferIdComponent } from './TransferIdComponent'; +import { TransferTableComponent } from './TransferTableComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiButtonModule, + TiPaginationModule, + TiIconModule, + TiOverflowModule, + TiTransferModule, + DemoLogModule, + RouterModule.forChild(TransferTestModule.ROUTES) + ], + declarations: [ + TransferBasicComponent, + TransferLazyComponent, + TransferSizeComponent, + TransferLabelkeyComponent, + TransferNodatatextComponent, + TransferEventComponent, + TransferTitlesComponent, + TransferDisabledComponent, + TransferLoadComponent, + TransferSearchableComponent, + TransferSearchkeysComponent, + TransferPlaceholderComponent, + TransferIdkeyComponent, + TransferPaginationComponent, + TransferIdComponent, + TransferTableComponent + ] +}) +export class TransferTestModule { + static readonly LINKS: Array = [{ href: 'components/TiTransferComponent.html', label: 'Transfer' }]; + static readonly ROUTES: Routes = [ + { + path: 'transfer/transfer-basic', + component: TransferBasicComponent + }, + { + path: 'transfer/transfer-lazy', + component: TransferLazyComponent + }, + { + path: 'transfer/transfer-size', + component: TransferSizeComponent + }, + { + path: 'transfer/transfer-labelkey', + component: TransferLabelkeyComponent + }, + { + path: 'transfer/transfer-nodatatext', + component: TransferNodatatextComponent + }, + { + path: 'transfer/transfer-titles', + component: TransferTitlesComponent + }, + { + path: 'transfer/transfer-event', + component: TransferEventComponent + }, + { + path: 'transfer/transfer-disabled', + component: TransferDisabledComponent + }, + { + path: 'transfer/transfer-load', + component: TransferLoadComponent + }, + { + path: 'transfer/transfer-searchable', + component: TransferSearchableComponent + }, + { + path: 'transfer/transfer-searchkeys', + component: TransferSearchkeysComponent + }, + { + path: 'transfer/transfer-placeholder', + component: TransferPlaceholderComponent + }, + { + path: 'transfer/transfer-idkey', + component: TransferIdkeyComponent + }, + { + path: 'transfer/transfer-pagination', + component: TransferPaginationComponent + }, + { + path: 'transfer/transfer-table', + component: TransferTableComponent + }, + { path: 'transfer/transfer-id', component: TransferIdComponent } + ]; +} diff --git a/src/transfer/demo/src/app/transfer/TransferTitlesComponent.ts b/src/transfer/demo/src/app/transfer/TransferTitlesComponent.ts new file mode 100644 index 0000000..82c02a4 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferTitlesComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { myOptions } from './data.js'; +@Component({ + templateUrl: './transfer-titles.html' +}) +export class TransferTitlesComponent { + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + mySelecteds: Array = []; +} diff --git a/src/transfer/demo/src/app/transfer/data.js b/src/transfer/demo/src/app/transfer/data.js new file mode 100644 index 0000000..60f7a7d --- /dev/null +++ b/src/transfer/demo/src/app/transfer/data.js @@ -0,0 +1,81 @@ +export const myOptions = [ + { + label: '一帆风顺', + }, + { + label: '二龙戏珠', + }, + { + label: '三阳开泰', + }, + { + label: '四季发财', + }, + { + label: '五福临门', + }, + { + label: '六六大顺', + }, + { + label: '七星高照', + }, + { + label: '八面来风', + }, + { + label: '九九归一', + }, + { + label: '十全十美', + }, +]; + +export const valueOptions = [ + { + label: '一帆风顺', + name: '结庐在人境', + }, + { + label: '二龙戏珠', + name: '而无车马喧', + }, + { + label: '三阳开泰', + name: '问君何能尔', + }, + { + label: '四季发财', + name: '心远地自偏', + }, + { + label: '五福临门', + name: '采菊东篱下', + }, + { + label: '六六大顺', + name: '悠然见南山', + }, + { + label: '七星高照', + name: '山气日夕佳', + }, + { + label: '八面来风', + name: '飞鸟相与还', + }, + { + label: '九九归一', + name: '此中有真意', + }, + { + label: '十全十美', + name: '欲辨已忘言', + }, +]; + +export const mySelecteds = [ + { + label: '四季发财', + }, +]; diff --git a/src/transfer/demo/src/app/transfer/transfer-basic.html b/src/transfer/demo/src/app/transfer/transfer-basic.html new file mode 100644 index 0000000..dd8fa4b --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-basic.html @@ -0,0 +1,4 @@ + +
    +
    Current Select: {{ mySelecteds | json }}
    +
    diff --git a/src/transfer/demo/src/app/transfer/transfer-disabled.html b/src/transfer/demo/src/app/transfer/transfer-disabled.html new file mode 100644 index 0000000..df40058 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-disabled.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-event.html b/src/transfer/demo/src/app/transfer/transfer-event.html new file mode 100644 index 0000000..5d2acd2 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-event.html @@ -0,0 +1,9 @@ + + diff --git a/src/transfer/demo/src/app/transfer/transfer-id.html b/src/transfer/demo/src/app/transfer/transfer-id.html new file mode 100644 index 0000000..ac40e72 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-id.html @@ -0,0 +1,15 @@ +

    1.描述

    +

    transfer组件是否给所有可点击元素添加上了id

    +

    绑定id后,每一个可点击元素(或可以hover的交互元素),都自动有了id,哪些节点有了id,在本页面就可以查看,不必打开调试控制台。

    +

    导入

    +

    import {{ '{' }} TiTransferModule {{ '}' }} from '@opentiny/ng';

    +

    2.示例

    +
    + +
    +

    已经选中的项:{{ mySelecteds | json }}

    + +

    id是否存在:

    +

    {{id+':'+idExistMap.get(id)}}

    +

    所有id是否存在:{{allIdExist}}

    +
    diff --git a/src/transfer/demo/src/app/transfer/transfer-idkey.html b/src/transfer/demo/src/app/transfer/transfer-idkey.html new file mode 100644 index 0000000..9131afb --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-idkey.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-labelkey.html b/src/transfer/demo/src/app/transfer/transfer-labelkey.html new file mode 100644 index 0000000..80806cc --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-labelkey.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-lazy.html b/src/transfer/demo/src/app/transfer/transfer-lazy.html new file mode 100644 index 0000000..e7fc4d3 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-lazy.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-load.html b/src/transfer/demo/src/app/transfer/transfer-load.html new file mode 100644 index 0000000..520f600 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-load.html @@ -0,0 +1,14 @@ +

    1.描述

    +

    此处测试异常数据 1.changeWrongType 点击该按钮会报错 2.点击 changeUndefined changeNull changeWrongType 再点击 changeSelects 会报错

    + +
    +
    Current Select: {{ mySelecteds | json }}
    +
    + +
    + +
    + +
    + + diff --git a/src/transfer/demo/src/app/transfer/transfer-nodatatext.html b/src/transfer/demo/src/app/transfer/transfer-nodatatext.html new file mode 100644 index 0000000..6d66eba --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-nodatatext.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-pagination.html b/src/transfer/demo/src/app/transfer/transfer-pagination.html new file mode 100644 index 0000000..a42ebb8 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-pagination.html @@ -0,0 +1,11 @@ +

    1.分页场景

    + +

    2.分页+搜索场景

    + diff --git a/src/transfer/demo/src/app/transfer/transfer-placeholder.html b/src/transfer/demo/src/app/transfer/transfer-placeholder.html new file mode 100644 index 0000000..5385e70 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-placeholder.html @@ -0,0 +1,7 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-searchable.html b/src/transfer/demo/src/app/transfer/transfer-searchable.html new file mode 100644 index 0000000..a4ac1ff --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-searchable.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-searchkeys.html b/src/transfer/demo/src/app/transfer/transfer-searchkeys.html new file mode 100644 index 0000000..41f31ce --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-searchkeys.html @@ -0,0 +1,8 @@ + +
    diff --git a/src/transfer/demo/src/app/transfer/transfer-size.html b/src/transfer/demo/src/app/transfer/transfer-size.html new file mode 100644 index 0000000..fb57165 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-size.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-table.html b/src/transfer/demo/src/app/transfer/transfer-table.html new file mode 100644 index 0000000..c37cb56 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-table.html @@ -0,0 +1,37 @@ +

    1.分页场景+配置每页显示条数为20

    + +

    2.搜索场景+自定义模板

    + + + {{row.firstName}} + {{row.lastName}} + {{row.age}} + + +

    3.分页+搜索场景

    + diff --git a/src/transfer/demo/src/app/transfer/transfer-titles.html b/src/transfer/demo/src/app/transfer/transfer-titles.html new file mode 100644 index 0000000..6721c22 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-titles.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer.less b/src/transfer/demo/src/app/transfer/transfer.less new file mode 100644 index 0000000..39d5cb6 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer.less @@ -0,0 +1,4 @@ +.transfer-button { + width: 200px; + margin: 10px; +} diff --git a/src/transfer/demo/src/app/transfer/webdoc/transfer-demos.js b/src/transfer/demo/src/app/transfer/webdoc/transfer-demos.js new file mode 100644 index 0000000..75c89a0 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/webdoc/transfer-demos.js @@ -0,0 +1,248 @@ +export default { + column: '1', + demos: [ + { + demoId: 'transfer-basic', + name: { 'zh-CN': '基本用法', 'en-US': 'transfer basic' }, + desc: { + 'zh-CN': '

    Transfer 的最简用法。

    ', + 'en-US': 'transfer basic' + }, + apis: [ 'TiTransferComponent.properties.options' ], + codeFiles: [ + 'transfer-basic.html', + 'TransferBasicComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'transfer disabled' + }, + desc: { + 'zh-CN': '

    通过属性options配置哪些数据被禁用

    ', + 'en-US': 'transfer disabled' + }, + codeFiles: [ + 'transfer-disabled.html', + 'TransferDisabledComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-lazy', + name: { + 'zh-CN': '懒加载', + 'en-US': 'transfer lazy' + }, + desc: { + 'zh-CN': '

    懒加载的场景。

    ', + 'en-US': 'transfer lazy' + }, + codeFiles: [ + 'transfer-lazy.html', + 'TransferLazyComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-size', + name: { + 'zh-CN': '自定义大小', + 'en-US': 'transfer size' + }, + desc: { + 'zh-CN': '

    通过属性widthheight配置宽高。

    ', + 'en-US': 'transfer size' + }, + apis: [ + 'TiTransferComponent.properties.height', + 'TiTransferComponent.properties.width' + ], + codeFiles: [ + 'transfer-size.html', + 'TransferSizeComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-labelkey', + name: { + 'zh-CN': '显示字段', + 'en-US': 'transfer labelkey' + }, + desc: { + 'zh-CN': '

    通过属性labelKey配置显示字段。

    ', + 'en-US': 'transfer labelkey' + }, + apis: [ + 'TiTransferComponent.properties.labelKey' + ], + codeFiles: [ + 'transfer-disabled.html', + 'TransferLabelkeyComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-nodatatext', + name: { + 'zh-CN': '无数据显示文本', + 'en-US': 'transfer nodatatext' + }, + desc: { + 'zh-CN': '

    通过属性noDataText配置无数据时显示的文本。

    ', + 'en-US': 'transfer nodatatext' + }, + apis: [ + 'TiTransferComponent.properties.noDataText' + ], + codeFiles: [ + 'transfer-nodatatext.html', + 'TransferNodatatextComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-titles', + name: { + 'zh-CN': '自定义面板头部标题', + 'en-US': 'transfer titles' + }, + desc: { + 'zh-CN': '

    通过属性leftTitlerightTitle配置左右面板的头部标题。

    ', + 'en-US': 'transfer titles' + }, + apis: [ + 'TiTransferComponent.properties.leftTitle', + 'TiTransferComponent.properties.rightTitle' + ], + codeFiles: [ + 'transfer-titles.html', + 'TransferTitlesComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-event', + name: { + 'zh-CN': '事件', + 'en-US': 'transfer event' + }, + desc: { + 'zh-CN': '

    当点击向右按钮的时侯触发transferToRight事件;当点击向左按钮的时侯触发transferToLeft事件;传递出去的参数为:此次穿梭的数据。

    ', + 'en-US': 'transfer event' + }, + apis: [ + 'TiTransferComponent.events.transferToLeft', + 'TiTransferComponent.events.transferToRight' + ], + codeFiles: [ + 'transfer-event.html', + 'TransferEventComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-searchable', + name: { + 'zh-CN': '搜索', + 'en-US': 'transfer searchable' + }, + desc: { + 'zh-CN': '

    通过属性searchable配置是否开启搜索。

    ', + 'en-US': 'transfer searchable' + }, + apis: [ 'TiTransferComponent.properties.searchable' ], + codeFiles: [ + 'transfer-searchable.html', + 'TransferSearchableComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-searchkeys', + name: { + 'zh-CN': '设置搜索字段', + 'en-US': 'transfer searchkeys' + }, + desc: { + 'zh-CN': '

    通过属性searchKeys配置搜索字段。

    ', + 'en-US': 'transfer searchkeys' + }, + apis: [ 'TiTransferComponent.properties.searchKeys' ], + codeFiles: [ + 'transfer-searchkeys.html', + 'TransferSearchkeysComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-placeholder', + name: { + 'zh-CN': '搜索框文字提示', + 'en-US': 'transfer placeholder' + }, + desc: { + 'zh-CN': '

    通过属性placeholder配置搜索框的文字提示。

    ', + 'en-US': 'transfer placeholder' + }, + apis: [ 'TiTransferComponent.properties.placeholder' ], + codeFiles: [ + 'transfer-placeholder.html', + 'TransferPlaceholderComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-idkey', + name: { + 'zh-CN': '唯一标识', + 'en-US': 'transfer idkey' + }, + desc: { + 'zh-CN': '

    通过属性idKey配置数据唯一标识的键值。

    ', + 'en-US': 'transfer idkey' + }, + apis: [ + 'TiTransferComponent.properties.idKey' + ], + codeFiles: [ + 'transfer-idkey.html', + 'TransferIdkeyComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-pagination', + name: { + 'zh-CN': '分页', + 'en-US': 'transfer pagination' + }, + desc: { + 'zh-CN': '

    通过属性pageable配置是否开启分页;通过属性pageSize配置每页展示的条数。

    ', + 'en-US': 'transfer pagination' + }, + apis: [ + 'TiTransferComponent.properties.pageable', + 'TiTransferComponent.properties.pageSize' + ] + }, + { + demoId: 'transfer-table', + name: { 'zh-CN': '表格', 'en-US': 'transfer table' }, + desc: { + 'zh-CN': '

    通过属性type配置穿梭框的类型,为table时使用表格类型;通过属性columns配置表格的表头列属性。

    ', + 'en-US': 'transfer table' + }, + apis: [ + 'TiTransferComponent.properties.type', + 'TiTransferComponent.properties.columns', + 'TiTransferComponent.slots.rowTemplate', + 'TiTransferColumn' + ] + } + ] +} diff --git a/src/transfer/demo/src/app/transfer/webdoc/transfer.cn.md b/src/transfer/demo/src/app/transfer/webdoc/transfer.cn.md new file mode 100644 index 0000000..aad514e --- /dev/null +++ b/src/transfer/demo/src/app/transfer/webdoc/transfer.cn.md @@ -0,0 +1,23 @@ +--- +title: Transfer 穿梭框 +--- +# Transfer 穿梭框 + +
    + +Transfer 穿梭框即双向选择器,选择一个或以上的选项后,点击对应的方向键,可以把选中的选项移动到另一栏。 + +```typescript +import { TiTransferModule } from '@opentiny/ng'; +``` + +
    + +
    + +Tip 是提供文本气泡提示的组件,可以通过指令方式、服务方式生成。 + +```typescript +import { TiTipModule } from '@opentiny/ng'; +``` +
    diff --git a/src/transfer/demo/src/app/transfer/webdoc/transfer.en.md b/src/transfer/demo/src/app/transfer/webdoc/transfer.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/webdoc/transfer.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/transfer/demo/src/favicon.ico b/src/transfer/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/transfer/demo/src/index.html b/src/transfer/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/transfer/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/transfer/demo/src/main.ts b/src/transfer/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/transfer/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/transfer/demo/test.ts b/src/transfer/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/transfer/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/transfer/demo/tsconfig.app.json b/src/transfer/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/transfer/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/transfer/demo/tsconfig.spec.json b/src/transfer/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/transfer/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/transfer/lib/index.ts b/src/transfer/lib/index.ts new file mode 100644 index 0000000..e54e046 --- /dev/null +++ b/src/transfer/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTransferModule'; diff --git a/src/transfer/lib/ng-package.json b/src/transfer/lib/ng-package.json new file mode 100644 index 0000000..b1d24dd --- /dev/null +++ b/src/transfer/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/transfer", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/transfer/lib/package.json b/src/transfer/lib/package.json new file mode 100644 index 0000000..20ba4a3 --- /dev/null +++ b/src/transfer/lib/package.json @@ -0,0 +1,22 @@ +{ + "name": "@opentiny/ng-transfer", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-dropsearch": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-list": "~1.0.0-beta.0", + "@opentiny/ng-searchbox": "~1.0.0-beta.0", + "@opentiny/ng-table": "~1.0.0-beta.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-checkbox": "~1.0.0-beta.0", + "@opentiny/ng-pagination": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/transfer/lib/project.json b/src/transfer/lib/project.json new file mode 100644 index 0000000..264feaf --- /dev/null +++ b/src/transfer/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/transfer/lib", + "sourceRoot": "src/transfer/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/transfer"], + "options": { + "project": "src/transfer/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/transfer"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js transfer" + }, + { + "command": "ng default-build transfer" + }, + { + "command": "node build/clear-default-theme.js transfer" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/transfer && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build transfer && ng pack transfer && node build/publish.js transfer --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/transfer/lib/src/TiTransferColumn.ts b/src/transfer/lib/src/TiTransferColumn.ts new file mode 100644 index 0000000..4850f5c --- /dev/null +++ b/src/transfer/lib/src/TiTransferColumn.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiTableColumns } from '@opentiny/ng-table'; + +export interface TiTransferColumn extends TiTableColumns { + /** + * 表格每列按照源数据哪一项属性展示数据的标识值;使用自定义模板时无效 + */ + field?: string; +} diff --git a/src/transfer/lib/src/TiTransferComponent.ts b/src/transfer/lib/src/TiTransferComponent.ts new file mode 100644 index 0000000..c748422 --- /dev/null +++ b/src/transfer/lib/src/TiTransferComponent.ts @@ -0,0 +1,325 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, SimpleChanges, Output, EventEmitter, ChangeDetectionStrategy, ContentChild, TemplateRef } from '@angular/core'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiTransferUtil } from './transferUtil'; +import { TiTransferColumn } from './TiTransferColumn'; +import packageInfo from '../package.json'; + +@Component({ + selector: 'ti-transfer', + templateUrl: './transfer.html', + providers: [TiFormComponent.getValueAccessor(TiTransferComponent)], + styleUrls: ['./transfer.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiTransferComponent extends TiFormComponent { + /** + * 源数据 + */ + @Input() options: Array = []; + /** + * 列表数据要展示的键值 + */ + @Input() labelKey: string = 'label'; + /** + * 设置数据唯一标识的键值 + */ + @Input() idKey: string; + /** + * 左侧面板标题文本 + * + * 默认值 : 可选项(国际化) + */ + @Input() leftTitle: string = TiLocale.getLocaleWords().tiTransfer.available; + /** + * 右侧面板标题文本 + * + * 默认值 : 已选项(国际化) + */ + @Input() rightTitle: string = TiLocale.getLocaleWords().tiTransfer.selected; + /** + * 无数据时的显示文本 + * + * 默认值 : 暂无数据(国际化) + */ + @Input() noDataText: string = TiLocale.getLocaleWords().tiList.noDataText; + /** + * 穿梭框的高度 + * + * 1.type 为 list 默认值为 300px + * + * 2.type 为 table 默认值为 443px + * + */ + @Input() height: string; + /** + * 穿梭框的宽度 + * + * 1.type 为 list 默认值为 200px + * + * 2.type 为 table 默认值为 340px + * + */ + @Input() width: string; + /** + * 是否开启搜索 + */ + @Input() searchable: boolean = false; + /** + * 输入框的提示文本 + * + * 默认值 : 请输入关键字搜索(国际化) + */ + @Input() placeholder: string = TiLocale.getLocaleWords().tiTransfer.placeholder; + /** + * 搜索字段 + */ + @Input() searchKeys: Array; + /** + * 是否开启分页 + */ + @Input() pageable: boolean = false; + /** + * 每页显示数据条数 + */ + @Input() pageSize: number = 10; + /** + * 点击左向右的按钮点时触发的回调 + */ + @Output() readonly transferToRight: EventEmitter = new EventEmitter(); + /** + * 点击右向左的按钮点时触发的回调 + */ + @Output() readonly transferToLeft: EventEmitter = new EventEmitter(); + /** + * 穿梭框类型 + */ + @Input() type: 'list' | 'table' = 'list'; + /** + * 表格类型时表头显示配置 + */ + @Input() columns: Array = []; + /** + * @ignore + * 选中事件,向外通知option数据,因不了解业务使用场景,暂不对外开发该接口 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + /** + * 表格类型每行展示的模板 + */ + @ContentChild('row', { static: false }) rowTemplate: TemplateRef; + /** + * @ignore + * 左侧面板选中项 + */ + public leftSelectedOptions: Array = []; + /** + * @ignore + * 右侧面板选中项 + */ + public rightSelectedOptions: Array = []; + /** + * @ignore + * 设置向右button的disabled状态 + */ + public toRightButtonDisabled: boolean = true; + /** + * @ignore + * 设置向左button的disabled状态 + */ + public toLeftButtonDisabled: boolean = true; + /** + * @ignore + * 穿梭框的左侧面板的列表数据 + * + * 默认值 : [] + */ + public leftOptions: Array = []; // options的数据 + /** + * @ignore + * 穿梭框的右侧面板的列表数据 + * + * 默认值 : [] + */ + public rightOptions: Array = []; // options的数据 + protected versionInfo: string = super.getVersion(packageInfo); + + // 初始化右侧面板的数据 + writeValue(model: any): void { + super.writeValue(model); + this.rightOptions = this.getEqualOptions(model, this.options); + this.leftOptions = this.getLeftOptions(); + + // 根据左右面板最新数据来获取最新的左右面板选中项 + this.leftSelectedOptions = this.getEqualOptions(this.leftSelectedOptions, this.leftOptions); + this.rightSelectedOptions = this.getEqualOptions(this.rightSelectedOptions, this.rightOptions); + + this.toRightButtonDisabled = this.getButtondisabled(this.leftSelectedOptions); + this.toLeftButtonDisabled = this.getButtondisabled(this.rightSelectedOptions); + } + + private filterModel(option: any): boolean { + // 从右侧面板列表中查找当前的option的index + const index: number = this.rightOptions.findIndex((rightOption) => { + return TiTransferUtil.isEqualOption(this.idKey, this.labelKey, rightOption, option); + }); + // 结果大于-1,说明当前的option存在于右侧面板中 + return index > -1; + } + + // 获取left面板的数据 + public getLeftOptions(): Array { + const leftOptions: Array = this.options + ? this.options.filter((option) => { + // 过滤掉右侧面板数据 + return !this.filterModel(option); + }) + : []; + + return leftOptions; + } + + /** + * 点击向右button事件 + */ + public onClickToRight(): void { + if (this.toRightButtonDisabled) { + return; + } + // 1.将左侧的选中项同步到右侧 + this.rightOptions = [...this.leftSelectedOptions, ...this.rightOptions]; + // 2.将左侧的选中项从左侧的options中进行清除 + this.leftOptions = this.leftOptions.filter((option) => { + return this.leftSelectedOptions.indexOf(option) < 0; + }); + // 将右侧列表的值传给model,实现双向绑定 + this.model = this.rightOptions; + const selectParms: object = { + rightOptions: this.rightOptions, + leftOptions: this.leftOptions, + toRightOptions: this.leftSelectedOptions + }; + // 将相关数据传到外面 + this.transferToRight.emit(selectParms); + // 清除已选中项,清除选中项 + this.leftSelectedOptions = []; + // 设置向右箭头的disabled状态 + this.toRightButtonDisabled = true; + } + + /** + * 点击向左button事件 + */ + public onClickToLeft(): void { + if (this.toLeftButtonDisabled) { + return; + } + // 1.将右侧的选中项移动到左边,并进行恢复初始数据 + this.leftOptions = this.recoveryOptions([...this.leftOptions, ...this.rightSelectedOptions]); + // 2.将右侧的选中项从右侧的options中进行清除 + this.rightOptions = this.rightOptions.filter((option) => { + return this.rightSelectedOptions.indexOf(option) < 0; + }); + + // 将右侧列表的值传给model,实现双向绑定 + this.model = this.rightOptions; + const selectParms: object = { + rightOptions: this.rightOptions, + leftOptions: this.leftOptions, + toLeftOptions: this.rightSelectedOptions + }; + + // 将相关数据传到外面 + this.transferToLeft.emit(selectParms); + // 清除已选中项,清除选中状态; + this.rightSelectedOptions = []; + // 设置向左箭头的disabled状态 + this.toLeftButtonDisabled = true; + } + + // 为左右两侧面板数据赋值 + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + if (changes?.options) { + this.rightOptions = this.getEqualOptions(this.model, this.options); + this.leftOptions = this.getLeftOptions(); + + // 根据左右面板最新数据来获取最新的左右面板选中项 + this.leftSelectedOptions = this.getEqualOptions(this.leftSelectedOptions, this.leftOptions); + this.rightSelectedOptions = this.getEqualOptions(this.rightSelectedOptions, this.rightOptions); + + this.toRightButtonDisabled = this.getButtondisabled(this.leftSelectedOptions); + this.toLeftButtonDisabled = this.getButtondisabled(this.rightSelectedOptions); + } + } + + // 获取相同的option部分 + private getEqualOptions(modelOptions: Array = [], options: Array = []): Array { + const optionsTemp: Array = []; + modelOptions?.forEach((modelOption) => { + const equalOption: object = options?.find((option) => { + return TiTransferUtil.isEqualOption(this.idKey, this.labelKey, modelOption, option); + }); + + if (equalOption) { + optionsTemp.push(equalOption); + } + }); + + return optionsTemp; + } + public leftModelChange(options: Array = []): void { + // 判断左侧面板是否有选中项,从而控制向右button的禁用状态 + this.toRightButtonDisabled = this.getButtondisabled(options); + } + public rightModelChange(options: Array = []): void { + // 判断右侧面板是否有选中项,从而控制向左button的禁用状态 + this.toLeftButtonDisabled = this.getButtondisabled(options); + } + + // 获取左右箭头的disabled状态 + private getButtondisabled(options: Array = []): boolean { + return !options || options.length === 0; + } + + // 当右侧数据到左侧时,恢复左侧options的顺序 + private recoveryOptions(leftOptions: Array = []): Array { + const optionsTemp: Array = []; + this.options?.forEach((oldOption) => { + leftOptions?.forEach((leftOption) => { + if (TiTransferUtil.isEqualOption(this.idKey, this.labelKey, leftOption, oldOption)) { + optionsTemp.push(leftOption); + } + }); + }); + + return optionsTemp; + } + + /** + * @ignore + * 左侧面板选中/取消事件回调,暂不对外开放 + */ + public onLeftSelect(option: any): void { + this.select.emit(option); + } + + /** + * @ignore + * 右侧面板取消/选中事件回调,暂不对外开放 + */ + public onRightSelect(option: any): void { + this.select.emit(option); + } +} diff --git a/src/transfer/lib/src/TiTransferModule.ts b/src/transfer/lib/src/TiTransferModule.ts new file mode 100644 index 0000000..2e5df73 --- /dev/null +++ b/src/transfer/lib/src/TiTransferModule.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiTransferComponent } from './TiTransferComponent'; +import { TiTransferListComponent } from './transferlist/TiTransferListComponent'; +import { FormsModule } from '@angular/forms'; +import { TiDropsearchModule } from '@opentiny/ng-dropsearch'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiLocaleModule, TiLocale } from '@opentiny/ng-locale'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiListModule } from '@opentiny/ng-list'; +import { TiSearchboxModule } from '@opentiny/ng-searchbox'; +import { TiTableModule } from '@opentiny/ng-table'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiCheckboxModule } from '@opentiny/ng-checkbox'; +import { TiPaginationModule } from '@opentiny/ng-pagination'; +import { locales } from './i18n'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiLocaleModule, + TiIconModule, + TiDropsearchModule, + TiDropModule, + TiListModule, + TiSearchboxModule, + TiPaginationModule, + TiTableModule, + TiOverflowModule, + TiCheckboxModule + ], + exports: [TiTransferComponent, TiTransferListComponent], + declarations: [TiTransferComponent, TiTransferListComponent] +}) +export class TiTransferModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiTransferComponent } from './TiTransferComponent'; +export { TiTransferListComponent } from './transferlist/TiTransferListComponent'; +export { TiTransferColumn } from './TiTransferColumn'; diff --git a/src/transfer/lib/src/i18n/TiTransferWords.ts b/src/transfer/lib/src/i18n/TiTransferWords.ts new file mode 100644 index 0000000..cd0b91e --- /dev/null +++ b/src/transfer/lib/src/i18n/TiTransferWords.ts @@ -0,0 +1,7 @@ +export interface TiTransferWords { + tiTransfer: { + available: string; + selected: string; + placeholder: string; + }; +} diff --git a/src/transfer/lib/src/i18n/en_US.ts b/src/transfer/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..408bc8c --- /dev/null +++ b/src/transfer/lib/src/i18n/en_US.ts @@ -0,0 +1,9 @@ +import { TiTransferWords } from './TiTransferWords'; + +export const en_US: TiTransferWords = { + tiTransfer: { + available: 'Available', + selected: 'Selected', + placeholder: 'Enter a keyword.' + } +}; diff --git a/src/transfer/lib/src/i18n/es_US.ts b/src/transfer/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..0035e94 --- /dev/null +++ b/src/transfer/lib/src/i18n/es_US.ts @@ -0,0 +1,9 @@ +import { TiTransferWords } from './TiTransferWords'; + +export const es_US: TiTransferWords = { + tiTransfer: { + available: 'Disponible', + selected: 'Seleccionado/s', + placeholder: 'Ingrese una palabra clave.' + } +}; diff --git a/src/transfer/lib/src/i18n/fr_FR.ts b/src/transfer/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..e071788 --- /dev/null +++ b/src/transfer/lib/src/i18n/fr_FR.ts @@ -0,0 +1,9 @@ +import { TiTransferWords } from './TiTransferWords'; + +export const fr_FR: TiTransferWords = { + tiTransfer: { + available: 'Disponible', + selected: 'Sélectionné', + placeholder: 'Entrez un mot-clé.' + } +}; diff --git a/src/transfer/lib/src/i18n/index.ts b/src/transfer/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/transfer/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/transfer/lib/src/i18n/pt_BR.ts b/src/transfer/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..dc96629 --- /dev/null +++ b/src/transfer/lib/src/i18n/pt_BR.ts @@ -0,0 +1,9 @@ +import { TiTransferWords } from './TiTransferWords'; + +export const pt_BR: TiTransferWords = { + tiTransfer: { + available: 'Disponíveis', + selected: 'Selecionados', + placeholder: 'Insira uma palavra-chave.' + } +}; diff --git a/src/transfer/lib/src/i18n/zh_CN.ts b/src/transfer/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..8cab162 --- /dev/null +++ b/src/transfer/lib/src/i18n/zh_CN.ts @@ -0,0 +1,9 @@ +import { TiTransferWords } from './TiTransferWords'; + +export const zh_CN: TiTransferWords = { + tiTransfer: { + available: '可选项', + selected: '已选项', + placeholder: '请输入关键字搜索' + } +}; diff --git a/src/transfer/lib/src/transfer.html b/src/transfer/lib/src/transfer.html new file mode 100644 index 0000000..d268357 --- /dev/null +++ b/src/transfer/lib/src/transfer.html @@ -0,0 +1,73 @@ +
    + + + + + + + +
    +
    +
    +
    + + + + + + +
    + + + + {{row[key.field]}} + + diff --git a/src/transfer/lib/src/transfer.less b/src/transfer/lib/src/transfer.less new file mode 100644 index 0000000..39edb59 --- /dev/null +++ b/src/transfer/lib/src/transfer.less @@ -0,0 +1,75 @@ +:host { + // 按钮容器宽度 + --ti-transfer-buttons-container-width: 42px; + // 按钮尺寸 + --ti-transfer-button-size: var(--ti-common-size-5x); + // 按钮边框 + --ti-transfer-button-border-weight: var(--ti-common-border-weight-normal); + --ti-transfer-button-border-style: var(--ti-common-border-style-solid); + --ti-transfer-button-border-radius: var(--ti-common-border-radius-normal); + // 按钮边框色和背景色 + --ti-transfer-button-color: var(--ti-common-color-bg-emphasize); + // 按钮图标色 + --ti-transfer-button-icon-color: var(--ti-common-color-icon-white); + // 按钮边框禁用色 + --ti-transfer-button-border-color-disabled: var(--ti-common-color-line-disabled); + // 按钮背景禁用色 + --ti-transfer-button-bg-color-disabled: var(--ti-common-color-bg-disabled); + // 按钮下边距 + --ti-transfer-button-margin-bottom: var(--ti-common-space-base); + // 水平内边距 = (容器宽度 - 按钮尺寸 - 按钮边框 * 2) / 2 + --ti-transfer-buttons-padding-horizontal: calc( + (var(--ti-transfer-buttons-container-width) - var(--ti-transfer-button-size) - var(--ti-transfer-button-border-weight) * 2) / 2 + ); +} + +.ti3-transfer-container { + display: flex; +} + +.ti3-transfer-buttons-container { + display: flex; + flex-direction: column; + justify-content: center; + width: var(--ti-transfer-buttons-container-width); + padding: 0 var(--ti-transfer-buttons-padding-horizontal); + box-sizing: border-box; +} + +.ti3-transfer-button { + width: var(--ti-transfer-button-size); + height: var(--ti-transfer-button-size); + border: var(--ti-transfer-button-border-weight) var(--ti-transfer-button-border-style) var(--ti-transfer-button-color); + border-radius: var(--ti-transfer-button-border-radius); + line-height: var(--ti-transfer-button-size); + text-align: center; + background-color: var(--ti-transfer-button-color); + font-size: calc(var(--ti-transfer-button-size) * 3 / 5); + color: var(--ti-transfer-button-icon-color); + cursor: pointer; + box-sizing: content-box; + + &:hover, + &:focus, + &:active { + background-color: var(--ti-common-color-bg-hover); + } +} + +// 穿梭框左右button被禁用的样式 +.ti3-transfer-button-disabled { + border: var(--ti-transfer-button-border-weight) var(--ti-transfer-button-border-style) var(--ti-transfer-button-border-color-disabled); + color: var(--ti-common-color-icon-graybg-disabled); + background-color: var(--ti-transfer-button-bg-color-disabled); + cursor: not-allowed; + + &:hover, + &:focus, + &:active { + background-color: var(--ti-transfer-button-bg-color-disabled); + } +} + +.ti3-transfer-right-button { + margin-bottom: var(--ti-transfer-button-margin-bottom); +} diff --git a/src/transfer/lib/src/transferUtil.ts b/src/transfer/lib/src/transferUtil.ts new file mode 100644 index 0000000..e9c037a --- /dev/null +++ b/src/transfer/lib/src/transferUtil.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +export class TiTransferUtil { + /** + * @ignore + * 对比选中项与option数据的对应关系 + */ + public static isEqualOption(idKey: any, labelKey: any, modelOption: any, option: any): boolean { + if (idKey) { + return modelOption[idKey] === option[idKey]; + } + + return ( + modelOption === option || + (modelOption[labelKey] !== undefined && option[labelKey] !== undefined && modelOption[labelKey] === option[labelKey]) + ); + } +} diff --git a/src/transfer/lib/src/transferlist/TiTransferListComponent.ts b/src/transfer/lib/src/transferlist/TiTransferListComponent.ts new file mode 100644 index 0000000..e1f84ab --- /dev/null +++ b/src/transfer/lib/src/transferlist/TiTransferListComponent.ts @@ -0,0 +1,404 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + Component, + Input, + SimpleChanges, + Output, + EventEmitter, + ElementRef, + Renderer2, + ChangeDetectorRef, + TemplateRef, + ContentChild +} from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiTableRowData, TiTableSrcData } from '@opentiny/ng-table'; +import { Util } from '@opentiny/ng-utils'; +import { TiTransferColumn } from '../TiTransferColumn'; +import { TiTransferUtil } from '../transferUtil'; +import packageInfo from '../../package.json'; + +@Component({ + selector: 'ti-transfer-list', + templateUrl: './transfer-list.html', + styleUrls: ['./transfer-list.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiTransferListComponent)] +}) +export class TiTransferListComponent extends TiFormComponent { + /** + * 穿梭框的源数据 + * + * 默认值 : [] + */ + @Input() options: Array = []; + /** + * 穿梭框列表要显示的字段 + * + * 默认值 : label + */ + @Input() labelKey: string = 'label'; + /** + * @ignore + * 列表数据中的唯一标识属性,该接口暂不对外开放,后续如果业务场景labelKey对应的值确实有重复时,再对外开放该接口。 + * + * 默认值 : label + */ + @Input() idKey: string; + /** + * 设置面板头部标题 + */ + @Input() title: string; + /** + * 无数据时的显示文本 + */ + @Input() noDataText: string; + /** + * 设置面板的高度 + * + * 缺省值 : 300px + */ + @Input() height: string; + /** + * 设置面板的宽度 + * + * 缺省值 : 200px + */ + @Input() width: string; + /** + * 设置是否添加搜索框 + * + * 缺省值 : false + */ + @Input() searchable: boolean = false; + /** + * 输入框的placeholder + */ + @Input() placeholder: string; + /** + * 要搜索的字段数组 + * + * 缺省值 : labelKey + */ + @Input() searchKeys: Array; + /** + * 设置是否开启分页 + * + * 缺省值 : false + */ + @Input() pageable: boolean = false; + /** + * 分页数据大小 + */ + @Input() pageSize: number = 10; + /** + * 表格类型 默认为list + */ + @Input() type: 'list' | 'table' = 'list'; + /** + * table类型表格表头 + */ + @Input() columns: Array = []; + /** + * @ignore + * 选中事件,向外通知option数据 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + @ContentChild(TemplateRef, { static: false }) itemTemplate: TemplateRef; + /** + * @ignore + * 面板左上角options总数显示 + */ + public selectedNumber: number = 0; + /** + * @ignore + * 面板左上角选中项个数显示 + */ + public totalNumber: number = 0; + /** + * @ignore + * 全选按钮的选中状态 + */ + public selectAllState: boolean; + /** + * @ignore + * 搜索框内单词 + */ + public searchWord: string; + /** + * @ignore + * 搜索结果数组 + */ + public searchResult: Array = new Array(); + /** + * @ignore + * list展示数据数组 + */ + public dispalyData: Array = new Array(); + /** + * @ignore + * 分页当前页数 + */ + public currentPage: number = 1; + /** + * @ignore + * 分页数据总数 + */ + public total: number; + /** + * @ignore + * table类型下数据总数 + */ + public tableTotalNumber: number; + /** + * @ignore + * table类型下选中项列表 + */ + public checkedList: Array = []; + /** + * @ignore + * table类型实际展示的数据 + */ + public displayed: Array = []; + /** + * @ignore + * table类型表个配置项 + */ + public srcData: TiTableSrcData; + + protected versionInfo: string = super.getVersion(packageInfo); + + constructor(hostRef: ElementRef, renderer: Renderer2, changeDetectorRef: ChangeDetectorRef) { + super(hostRef, renderer, changeDetectorRef); + } + /** + * 监听选中项的变化 + * @param model 选中项 + */ + protected ngOnModelChange(model: any): void { + // 更新选中项的总数 + this.selectedNumber = model ? model.length : 0; + // 点击option,设置全选按钮的选中状态 + this.setSelectedAllState(); + } + + // 监听options的变化,在options改变时,改变右上角数字总数的值 + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + /** + * 如果有搜索框,options改变时,在页面上显示的searchResult, + * 此处需要将改变的options重新赋给searchResult + */ + if (changes?.options) { + if (this.searchable) { + this.searchResult = this.options; + this.searchWord = ''; + } + // 更新options总数和选中项总数 + this.totalNumber = changes.options.currentValue ? changes.options.currentValue.length : 0; + this.selectedNumber = this.model ? this.model.length : 0; + if (this.type === 'list') { + // options改变时,设置全选按钮的选中状态 + this.setSelectedAllState(); + this.changePagination(this.currentPage); + } + if (this.type === 'table') { + this.srcData = { + data: this.options, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + this.tableTotalNumber = this.options.length; + } + } + } + + // 触发分页修改总数的展示数据 + public changePagination(event: number): void { + if (this.pageable) { + this.total = this.searchable ? this.searchResult.length : this.options.length; + this.dispalyData = this.searchable + ? this.searchResult.slice(this.pageSize * (event - 1), this.pageSize * event) + : this.options.slice(this.pageSize * (event - 1), this.pageSize * event); + } else { + this.dispalyData = this.searchable ? this.searchResult : this.options; + } + } + + // list列表选中/取消选项事件 + public onSelect(option: any): void { + this.select.emit(option); + } + + public onClickSelectAll(): void { + this.changeModel(this.selectAllState === false || this.selectAllState === null); + } + + private changeModel(state: boolean): void { + this.model = state + ? this.options.filter((option) => { + return !option.disabled; + }) + : this.model.filter((option: any) => { + return option.disabled; + }); + this.selectAllState = state; + } + + // 设置全选按钮的选中状态 + public setSelectedAllState(): void { + if (this.isValidData()) { + this.selectAllState = false; + return; + } + + const selectedOptions: Array = this.getSelectedOptions(); + + if (selectedOptions.length === 0) { + this.selectAllState = false; + return; + } + + const selectableOptions: Array = this.getSelectableOptions(); + + this.selectAllState = selectedOptions.length === selectableOptions.length ? true : null; + } + + // 判断数据有效性 + private isValidData(): boolean { + return !this.model || this.model.length === 0 || !this.options || this.options.length === 0; + } + + private getSelectedOptions(): Array { + return this.options.filter((item: any) => { + return this.isSelectable(item) && this.isSelected(item); + }); + } + + private getSelectableOptions(): Array { + return this.options.filter((item: any) => { + return this.isSelectable(item); + }); + } + + // 是否为可选数据项 + protected isSelectable(item: any): boolean { + return !this.isDisabled(item); + } + + // 当前元素是否为禁用状态 + private isDisabled(item: any): boolean { + return item && item.disabled; + } + + // 是已选中数据项 + public isSelected(item: any): boolean { + if (!this.model) { + return false; + } + + return this.getIndex(item, this.model) !== -1; + } + + private getIndex(item: any, arr: Array): number { + if (!arr || !item) { + return -1; + } + for (let i: number = 0; i < arr.length; i++) { + if (TiTransferUtil.isEqualOption(this.idKey, this.labelKey, arr[i], item)) { + return i; + } + } + + return -1; + } + // 在searchword进行变化时,进行前台搜索相关处理 + public searchWordChange(searchWord: string): void { + if (this.type === 'table') { + return; + } + this.filterSearchReslut(searchWord); + } + + /** + * 前台搜索时使用,查找搜索结果 + */ + private filterSearchReslut(searchWord: string): void { + if (this.options?.length >= 0) { + // 搜索结果临时值。结果默认值,是原数据 + let searchResult: Array = this.options; + // 如果搜索词存在 + if (!Util.isEmptyString(searchWord)) { + // 在集合中搜索 + searchResult = searchResult.filter((option: any) => { + if (!this.searchKeys) { + // 没有定义searchKeys时,取labelKey + return this.isMatchbWithSWord(option, this.labelKey, searchWord); + } else { + // 已定义searchKeys,任一条目匹配即可 + return this.isMatchBySearchkeys(option, searchWord); + } + }); + } + this.searchResult = searchResult; + this.changePagination(this.currentPage); + } + } + + // 已定义searchKeys,任一条目匹配即可 + private isMatchBySearchkeys(option: any, searchWord: string): boolean { + for (const searchKey of this.searchKeys) { + if (this.isMatchbWithSWord(option, searchKey, searchWord)) { + return true; + } + } + + return false; + } + + // 下拉项中的searchKey字段是否和搜索字段相匹配 + private isMatchbWithSWord(option: any, searchKey: string, searchWord: string): boolean { + return Util.isString(option[searchKey]) && option[searchKey].toLowerCase().indexOf(searchWord.toLowerCase()) >= 0; + } + + /** + * 问题:搜索框无法聚焦 + * 原因:在list组件中为了避免domintor失焦,在mousedown中,阻止了默认行为。 + * 方案: + * 在transfer中不涉及domintor失焦场景,不需要阻止默认行为, + * 给list组件绑定mousedown事件 + * 使用event.stopImmediatePropagation() 阻止同一元素绑定相同事件,后续的执行的相同事件不会被触发 + * 此处mousedown的执行顺序:先执行transfer中的mousedown,再执行list中的mousedown。 + * 通过stopImmediatePropagation方法,从而阻止了list组件中ousedown事件的触发 + */ + public onListMousedown(event: MouseEvent): void { + event.stopImmediatePropagation(); + } + + // 将表格选中项通知给model + public onCheckedsChange(checkeds: Array): void { + this.model = [...checkeds]; + } + + // 将model更改后值的通知给表格选中项 + writeValue(model: any): void { + super.writeValue(model); + if (this.type === 'table' && model !== null) { + this.checkedList = model; + } + } +} diff --git a/src/transfer/lib/src/transferlist/transfer-list.html b/src/transfer/lib/src/transferlist/transfer-list.html new file mode 100644 index 0000000..6d23e3f --- /dev/null +++ b/src/transfer/lib/src/transferlist/transfer-list.html @@ -0,0 +1,153 @@ +
    +
    + + {{selectedNumber}} / {{totalNumber}} +
    +
    + + +
    + +
    + + + +
    +
    + +
    + +
    + + +
    + +
    + + + + + + + + + +
    + + {{column.title}}
    +
    +
    + + + + + + + + + + + + + + + + + + +
    + +
    {{noDataText}}
    +
    + +
    +
    +
    + + + + + {{ title }} + diff --git a/src/transfer/lib/src/transferlist/transfer-list.less b/src/transfer/lib/src/transferlist/transfer-list.less new file mode 100644 index 0000000..add9f54 --- /dev/null +++ b/src/transfer/lib/src/transferlist/transfer-list.less @@ -0,0 +1,156 @@ +:host { + // 容器边框 + --ti-transfer-list-container-border-weight: var(--ti-common-border-weight-normal); + --ti-transfer-list-container-border-style: var(--ti-common-border-style-solid); + --ti-transfer-list-container-border-radius: var(--ti-common-border-radius-normal); + --ti-transfer-list-container-border-color: var(--ti-common-color-line-normal); + --ti-transfer-list-header-height: var(--ti-common-size-10x); + --ti-transfer-list-header-bg-color: var(--ti-common-color-bg-white-emphasize); + --ti-transfer-list-header-padding-left: var(--ti-common-space-10); + --ti-transfer-list-selected-number-space-right: var(--ti-common-space-10); + --ti-transfer-list-checkbox-size: var(--ti-common-size-4x); + --ti-transfer-list-checkbox-margin-right: var(--ti-common-space-10); + --ti-transfer-list-checkbox-border-weight: var(--ti-common-border-weight-normal); + --ti-transfer-list-checkbox-border-style: var(--ti-common-border-style-solid); + --ti-transfer-list-checkbox-border-radius: var(--ti-common-border-radius-normal); + --ti-transfer-list-checkbox-bg-color: var(--ti-common-color-bg-white-normal); + --ti-transfer-list-checkbox-icon-color: var(--ti-common-color-icon-white); + --ti-transfer-list-checkbox-color-selected: var(--ti-common-color-bg-emphasize); + --ti-transfer-list-no-data-padding-top: var(--ti-common-size-25x); + --ti-transfer-list-no-data-image-size: var(--ti-common-size-20x); + --ti-transfer-list-no-data-text-color: var(--ti-common-color-text-secondary); +} + +.ti3-transfer-list-container { + border: var(--ti-transfer-list-container-border-weight) var(--ti-transfer-list-container-border-style) + var(--ti-transfer-list-container-border-color); + border-radius: var(--ti-transfer-list-container-border-radius); + box-sizing: border-box; +} + +.ti3-transfer-list-header { + position: relative; + height: var(--ti-transfer-list-header-height); + padding: 0 var(--ti-transfer-list-header-padding-left); + border-bottom: var(--ti-transfer-list-container-border-weight) var(--ti-transfer-list-container-border-style) + var(--ti-transfer-list-container-border-color); + line-height: var(--ti-transfer-list-header-height); + background: var(--ti-transfer-list-header-bg-color); + box-sizing: border-box; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); +} + +.ti3-transfer-list-selected-number { + position: absolute; + right: var(--ti-transfer-list-selected-number-space-right); +} + +.ti3-transfer-list-body { + height: calc(100% - var(--ti-transfer-list-header-height)); // 列表的高度是用户自定义高度减去面板头部高度 + overflow-y: auto; + box-sizing: border-box; +} + +.ti3-transfer-table-height { + height: calc(100% - var(--ti-transfer-list-header-height)); // 表格的高度是用户自定义高度减去面板头部高度 +} + +.ti3-transfer-table-body-height { + height: calc(100% - 29px); // 表格出现滚动条容器的最大高度是 ti-table 的高度减去面板头部高度 + border: none !important; +} + +.ti3-transfer-table-border { + border-bottom: 1px solid var(--ti-common-color-line-dividing) !important; + border-radius: 0px; +} + +.ti3-transfer-searchbox-drop { + height: calc(100% - var(--ti-transfer-list-header-height) - 48px); // 有搜索框无分页时,列表和表格的高度需要再减去搜索框占用的高度 +} + +.ti3-transfer-pagination-drop { + height: calc(100% - var(--ti-transfer-list-header-height) - 32px); // 有分页无搜索框时,列表和表格的高度需要再减去分页占用的高度 +} + +.ti3-transfer-searchbox-pagination-drop { + height: calc(100% - var(--ti-transfer-list-header-height) - 48px - 32px); // 表格容器的高度需要再减去搜索框和分页占用的高度 +} + +.ti3-transfer-searchbox-container { + padding: 10px; +} + +.ti3-transfer-table-pagination-hidden { + opacity: 0; + pointer-events: none; +} + +.ti3-transfer-table-shadow { + position: relative; + box-sizing: border-box; + height: 32px; + padding-top: 4px; + box-shadow: var(--ti-common-shadow-1-up); +} + +.ti3-transfer-pagination { + text-align: right; + .ti3-transfer-table-shadow(); +} + +.ti3-table > .ti3-page-container { + margin-top: 0px; + width: 100%; + .ti3-transfer-table-shadow(); +} + +::ng-deep .ti3-pag-mini-pages { + float: right; + padding-right: 9px; +} + +.ti3-transfer-list-checkbox { + display: inline-block; + width: var(--ti-transfer-list-checkbox-size); + height: var(--ti-transfer-list-checkbox-size); + margin-right: var(--ti-transfer-list-checkbox-margin-right); + border: var(--ti-transfer-list-checkbox-border-weight) var(--ti-transfer-list-checkbox-border-style) + var(--ti-transfer-list-container-border-color); + border-radius: var(--ti-transfer-list-checkbox-border-radius); + line-height: calc(var(--ti-transfer-list-checkbox-size) - var(--ti-transfer-list-checkbox-border-weight) * 2); + text-align: center; + background-color: var(--ti-transfer-list-checkbox-bg-color); + color: var(--ti-transfer-list-checkbox-icon-color); + box-sizing: border-box; + cursor: pointer; +} + +// 正常选中 +.ti3-transfer-list-checkbox-selected { + border-color: var(--ti-transfer-list-checkbox-color-selected); + background: var(--ti-transfer-list-checkbox-color-selected); +} + +// 半选 +.ti3-transfer-list-checkbox-indeterminate { + border-width: 5px; + border-color: var(--ti-transfer-list-checkbox-color-selected); + vertical-align: sub; + + &::before { + content: ''; + } +} + +.ti3-transfer-list-no-data { + text-align: center; + padding-top: var(--ti-transfer-list-no-data-padding-top); + box-sizing: border-box; + color: var(--ti-transfer-list-no-data-text-color); + background: no-repeat data-uri('../../../../themes/basic/img/table-nodata-bg.png'); + background-position-x: 50%; + background-position-y: 20px; + background-size: var(--ti-transfer-list-no-data-image-size) var(--ti-transfer-list-no-data-image-size); +} diff --git a/src/tree/demo/karma.conf.js b/src/tree/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/tree/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/tree/demo/project.json b/src/tree/demo/project.json new file mode 100644 index 0000000..316caef --- /dev/null +++ b/src/tree/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/tree/demo", + "sourceRoot": "src/tree/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/tree", + "index": "src/tree/demo/src/index.html", + "main": "src/tree/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tree/demo/tsconfig.app.json", + "assets": ["src/tree/demo/src/favicon.ico", "src/tree/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "tree-demo:build:production" + }, + "development": { + "browserTarget": "tree-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js tree" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/tree/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tree/demo/tsconfig.spec.json", + "karmaConfig": "src/tree/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/tree/demo/src/app/AppComponent.ts b/src/tree/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/tree/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/tree/demo/src/app/AppModule.ts b/src/tree/demo/src/app/AppModule.ts new file mode 100644 index 0000000..521f1ba --- /dev/null +++ b/src/tree/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TreeTestModule } from './tree/TreeTestModule'; + +@NgModule({ + imports: [ + TreeTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/tree/demo/src/app/IndexComponent.ts b/src/tree/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..e67d0e7 --- /dev/null +++ b/src/tree/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TreeTestModule } from './tree/TreeTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TreeTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/tree/demo/src/app/app.html b/src/tree/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/tree/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/tree/demo/src/app/tree/TreeBeforeExpandComponent.ts b/src/tree/demo/src/app/tree/TreeBeforeExpandComponent.ts new file mode 100644 index 0000000..b539c60 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeBeforeExpandComponent.ts @@ -0,0 +1,70 @@ +import { Component, ViewEncapsulation, ChangeDetectorRef } from '@angular/core'; +import { TiTreeComponent, TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-before-expand.html', + encapsulation: ViewEncapsulation.None +}) +export class TreeBeforeExpandComponent { + constructor(private changeDetectorRef: ChangeDetectorRef) {} + tree: Array = [ + { + label: '家用电器', + addable: true, + children: [] + }, + { + label: '视频', + addable: true, + children: [] + } + ]; + treeID: number = 5; + beforeExpand(TreeCom: TiTreeComponent): void { + const item: TiTreeNode = TreeCom.getBeforeExpandNode(); + item.loadStatus = 'loading'; + const getDataPromise: any = this.getNodeData(item); + getDataPromise + .then((data: Array) => { + item.children = data; + item.expanded = true; + item.loadStatus = 'success'; + }) + .catch(() => { + item.expanded = true; + item.loadStatus = 'error'; + }); + } + createData(item: TiTreeNode): Array { + const data: Array = []; + if (item.label === '家用电器') { + for (let i: number = 0; i < 3; i++) { + const dataList: TiTreeNode = { + type: 'FILE', + label: `item${this.treeID}` + }; + this.treeID++; + data.push(dataList); + } + + return data; + } + + return data; + } + getNodeData(item: TiTreeNode): any { + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + const data: Array = this.createData(item); + if (data && data.length > 0) { + resolve(data); + } else { + const errMsg: string = '获取数据失败'; + reject(errMsg); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef + this.changeDetectorRef.markForCheck(); + }, 2000); + }); + } +} diff --git a/src/tree/demo/src/app/tree/TreeBeforeMoreComponent.ts b/src/tree/demo/src/app/tree/TreeBeforeMoreComponent.ts new file mode 100644 index 0000000..8f35c2a --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeBeforeMoreComponent.ts @@ -0,0 +1,100 @@ +import { Component, ViewEncapsulation, ChangeDetectorRef } from '@angular/core'; +import { TiTreeComponent, TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-before-more.html', + encapsulation: ViewEncapsulation.None +}) +export class TreeBeforeMoreComponent { + constructor(private changeDetectorRef: ChangeDetectorRef) {} + innerData: Array = [ + { + label: '家用电器', + children: [] + }, + { + label: '视频', + children: [] + } + ]; + treeID: number = 0; + multiple: boolean = true; + offset: number; // 当前已展开的节点数量 + nodeNum: number = 2; // 每次点击展开节点的数量 + beforeExpand(TreeCom: TiTreeComponent): void { + const node: TiTreeNode = TreeCom.getBeforeExpandNode(); + node.loadStatus = 'loading'; + this.offset = 0; + const getDataPromise: any = this.getNodeData(node, this.offset, this.nodeNum, 'before'); + getDataPromise + .then((data: Array) => { + node.children = data; + node.showMore = true; + node.expanded = true; + node.loadStatus = 'success'; + }) + .catch(() => { + node.showMore = false; + node.expanded = true; + node.loadStatus = 'error'; + }); + } + beforeMore(node: TiTreeNode): void { + node.moreStatus = 'loading'; + this.offset = node.children?.length; + const getDataPromise: any = this.getNodeData(node, this.offset, this.nodeNum, 'more'); + getDataPromise + .then((data: Array) => { + node.children = node.children.concat(data); + node.moreStatus = 'success'; + }) + .catch(() => { + node.moreStatus = 'error'; + }); + } + getNodeData(item: TiTreeNode, offset: number, num: number, style: string): any { + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + const data: Array = this.createData(item, offset, num, style); + if (data && data.length > 0) { + resolve(data); + } else { + const errMsg: string = '获取数据失败'; + reject(errMsg); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef + this.changeDetectorRef.markForCheck(); + }, 2000); + }); + } + + createData(item: TiTreeNode, offset: number, num: number, style: string): Array { + const data: Array = []; + let result: Array; + this.treeID = 0; + if (style === 'before' && (item.label === '家用电器' || item.label === '视频')) { + for (let i: number = 0; i < 100; i++) { + const dataList: TiTreeNode = { + label: item.label + this.treeID, + children: [] + }; + this.treeID++; + data.push(dataList); + } + } + + if (style === 'more' && item.label === '家用电器') { + for (let i: number = 0; i < 100; i++) { + const dataList: TiTreeNode = { + label: item.label + this.treeID, + children: [] + }; + this.treeID++; + data.push(dataList); + } + } + result = data.slice(offset, offset + num); + + return result; + } +} diff --git a/src/tree/demo/src/app/tree/TreeChangedbycheckboxComponent.ts b/src/tree/demo/src/app/tree/TreeChangedbycheckboxComponent.ts new file mode 100644 index 0000000..fd0e44a --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeChangedbycheckboxComponent.ts @@ -0,0 +1,46 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-changedbycheckbox.html', + encapsulation: ViewEncapsulation.None +}) +export class TreeChangedbycheckboxComponent { + innerData: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeCheckRelationComponent.ts b/src/tree/demo/src/app/tree/TreeCheckRelationComponent.ts new file mode 100644 index 0000000..e652578 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeCheckRelationComponent.ts @@ -0,0 +1,55 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; +@Component({ + templateUrl: './tree-check-relation.html' +}) +export class TreeCheckRelationComponent { + multiple: boolean = true; + checkRelation: boolean = false; + innerData: Array = [ + { + label: 'LIC', + expanded: true, + children: [ + { + label: 'CBU安全', + expanded: true, + children: [ + { + label: 'SRE领域', + expanded: true, + children: [ + { + label: '操作系统' + }, + { + label: '网络' + } + ] + }, + { + label: '近6个小时' + }, + { + label: '近12个小时' + }, + { + label: '近1天' + } + ] + }, + { + label: '近3天', + children: [ + { + label: 'ALM' + }, + { + label: 'MIC' + } + ] + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeDisabledComponent.ts b/src/tree/demo/src/app/tree/TreeDisabledComponent.ts new file mode 100644 index 0000000..c969de2 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeDisabledComponent.ts @@ -0,0 +1,48 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-disabled.html' +}) +export class TreeDisabledComponent { + data: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: true, + children: [ + { + label: '海尔空调', + disabled: true + }, + { + label: '美的空调' + } + ] + } + ] + }, + { + label: '生活电器', + disabled: true, + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeDragBeforedropComponent.ts b/src/tree/demo/src/app/tree/TreeDragBeforedropComponent.ts new file mode 100644 index 0000000..f6dff6f --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeDragBeforedropComponent.ts @@ -0,0 +1,95 @@ +import { Component } from '@angular/core'; +import { TiTreeDragNode, TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-drag-beforedrop.html' +}) +export class TreeDragBeforedropComponent { + myLogs: Array = []; + innerData: Array = [ + { + label: '家用电器', + key: '0-1', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调', + disabled: true + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + expanded: true, + children: [ + { + label: '加湿器', + draggable: false + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + children: [ + { + label: '男装', + key: '0-2-1' + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + draggable: false, + children: [ + { + label: '面部护理', + draggable: false + }, + { + label: '口腔护理' + } + ] + } + ]; + + onbeforeDrop(event: TiTreeDragNode): boolean { + this.myLogs = [...this.myLogs, `beforeDrop nodeName = ${event.dragNode.label} =>${event.targetNode.label}`]; + const parentNode: TiTreeNode = TiTreeUtil.getParentNode(this.innerData, event.targetNode); + if (parentNode?.label === '大家电') { + return; + } else { + TiTreeUtil.dropApply(event, this.innerData); + } + } +} diff --git a/src/tree/demo/src/app/tree/TreeDragComponent.ts b/src/tree/demo/src/app/tree/TreeDragComponent.ts new file mode 100644 index 0000000..2958501 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeDragComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeDragNode, TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-drag.html' +}) +export class TreeDragComponent { + myLogs: Array = []; + innerData: Array = [ + { + label: '家用电器', + key: '0-1', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调', + disabled: true + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + children: [ + { + label: '男装', + key: '0-2-1' + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + onDrop(event: TiTreeDragNode): void { + this.myLogs = [...this.myLogs, `nodeDrop nodeName = ${event.dragNode.label} =>${event.targetNode.label}`]; + } +} diff --git a/src/tree/demo/src/app/tree/TreeEventComponent.ts b/src/tree/demo/src/app/tree/TreeEventComponent.ts new file mode 100644 index 0000000..8fd54ce --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeEventComponent.ts @@ -0,0 +1,74 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; +@Component({ + templateUrl: './tree-event.html' +}) +export class TreeEventComponent { + myLogs: Array = []; + innerData: Array = [ + { + label: 'LIC', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: 'CBU安全', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: 'SRE领域', + expanded: true, + checked: 'indeterminate', + custom: true, + children: [ + { + label: '操作系统', + checked: true + }, + { + label: '网络' + } + ] + }, + { + label: '近6个小时' + }, + { + label: '近12个小时' + }, + { + label: '近1天' + } + ] + }, + { + label: '近3天', + children: [ + { + label: 'ALM' + }, + { + label: 'MIC' + } + ] + } + ] + } + ]; + + selectFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `select nodeName= ${node.label}`]; + } + + changeFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `change nodeName= ${node.label}`]; + } + + expandFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `expand nodeName= ${node.label}`]; + } + collapseFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `collapse nodeName= ${node.label}`]; + } +} diff --git a/src/tree/demo/src/app/tree/TreeIconComponent.ts b/src/tree/demo/src/app/tree/TreeIconComponent.ts new file mode 100644 index 0000000..5d70a6e --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeIconComponent.ts @@ -0,0 +1,106 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-icon.html', // 指定组件模板 + encapsulation: ViewEncapsulation.None +}) +export class TreeIconComponent { + data: Array = [ + { + label: 'asdasdAAA', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '大家电', + expanded: true, + expandIcon: 'ti3-icon ti3-icon-document', + collapseIcon: 'ti3-icon ti3-icon-file', + checked: 'indeterminate', + children: [ + { + expandIcon: 'ti3-icon ti3-icon-document', + collapseIcon: 'ti3-icon ti3-icon-file', + label: '空调', + expanded: true, + checked: 'indeterminate', + children: [ + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '海尔空调', + checked: true + }, + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '美的空调' + } + ] + }, + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '冰箱' + }, + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '洗衣机' + }, + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '热水器' + } + ] + }, + { + label: '生活电器', + expandIcon: 'ti3-icon ti3-icon-document', + collapseIcon: 'ti3-icon ti3-icon-file', + children: [ + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '加湿器' + }, + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expandIcon: 'ti3-icon ti3-icon-document', + collapseIcon: 'ti3-icon ti3-icon-file', + children: [ + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '男装' + }, + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '女装' + } + ] + }, + { + label: '化妆', + expandIcon: 'ti3-icon ti3-icon-document', + collapseIcon: 'ti3-icon ti3-icon-file', + expanded: true, + checked: true, + children: [ + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '面部护理面部护理', + checked: true + }, + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '口腔护理', + checked: true + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeLoadComponent.ts b/src/tree/demo/src/app/tree/TreeLoadComponent.ts new file mode 100644 index 0000000..2a1ce51 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeLoadComponent.ts @@ -0,0 +1,132 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-load.html' +}) +export class TreeLoadComponent { + /** + * 初始值采用空数据。经测试,初始值为undefined或null,会报错。 + */ + myData: Array = []; + + private dataA: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + private dataB: Array = [ + { + label: '家用电器', + children: [] + }, + { + label: '服饰', + children: [] + }, + { + label: '视频', + children: [] + }, + { + label: '食品', + children: [] + } + ]; + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + selectedData: Array = TiTreeUtil.getSelectedData(this.myData, false, false); + onChange(event: TiTreeNode): void { + console.log(event); + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData = TiTreeUtil.getSelectedData(this.myData, false, false); + } + // 每个组件都用下面六个函数,只改变函数内容 + changeUndefined(): void { + this.myData = undefined; + } + changeNull(): void { + this.myData = null; + } + changeWrongType(): void { + const temp: any = 5; + this.myData = temp; + } + changeNullData(): void { + this.myData = []; + this.selectedData = TiTreeUtil.getSelectedData(this.myData, false, false); + } + changeDataA(): void { + this.myData = this.dataA; + this.selectedData = TiTreeUtil.getSelectedData(this.myData, false, false); + } + changeDataB(): void { + this.myData = this.dataB; + this.selectedData = TiTreeUtil.getSelectedData(this.myData, false, false); + } +} diff --git a/src/tree/demo/src/app/tree/TreeManyComponent.ts b/src/tree/demo/src/app/tree/TreeManyComponent.ts new file mode 100644 index 0000000..a4ff946 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeManyComponent.ts @@ -0,0 +1,169 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-many.html' +}) +export class TreeManyComponent { + innerData1: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + selectedData1: Array = TiTreeUtil.getSelectedData(this.innerData1, false, false); + + innerData2: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + selectedData2: Array = TiTreeUtil.getSelectedData(this.innerData2, false, false); + + onSelect1(event: TiTreeNode): void { + console.log(event); + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData1 = TiTreeUtil.getSelectedData(this.innerData1, false, false); + } + + onSelect2(event: TiTreeNode): void { + console.log(event); + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData2 = TiTreeUtil.getSelectedData(this.innerData2, false, false); + } +} diff --git a/src/tree/demo/src/app/tree/TreeMultiselectComponent.ts b/src/tree/demo/src/app/tree/TreeMultiselectComponent.ts new file mode 100644 index 0000000..585fe63 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeMultiselectComponent.ts @@ -0,0 +1,57 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; +@Component({ + templateUrl: './tree-multiselect.html' +}) +export class TreeMultiselectComponent { + innerData: Array = [ + { + label: 'LIC', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: 'CBU安全', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: 'SRE领域', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '操作系统', + checked: true + }, + { + label: '网络' + } + ] + }, + { + label: '近6个小时' + }, + { + label: '近12个小时' + }, + { + label: '近1天' + } + ] + }, + { + label: '近3天', + children: [ + { + label: 'ALM' + }, + { + label: 'MIC' + } + ] + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeOperateComponent.ts b/src/tree/demo/src/app/tree/TreeOperateComponent.ts new file mode 100644 index 0000000..b99b771 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeOperateComponent.ts @@ -0,0 +1,110 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; +@Component({ + templateUrl: './tree-operate.html' +}) +export class TreeOperateComponent { + myLogs: Array = []; + innerData: Array = [ + { + label: 'LIC', + expanded: true, + checked: 'indeterminate', + addable: true, + editable: true, + deleteable: true, + children: [ + { + label: 'CBU安全', + expanded: true, + checked: 'indeterminate', + addable: true, + editable: true, + deleteable: true, + children: [ + { + label: 'SRE领域', + expanded: true, + checked: 'indeterminate', + custom: true, + addable: true, + editable: true, + deleteable: true, + children: [ + { + label: '操作系统', + checked: true, + addable: true + }, + { + label: '网络', + deleteable: true, + addable: true + } + ] + }, + { + label: '近6个小时', + disabled: true + }, + { + label: '近12个小时', + addable: true, + editable: true, + deleteable: true + }, + { + label: '近1天', + disabled: true, + addable: true, + editable: true, + deleteable: true + } + ] + }, + { + label: '近3天', + addable: true, + editable: true, + deleteable: true, + children: [ + { + label: 'ALM', + editable: true, + deleteable: true + }, + { + label: 'MIC', + editable: true, + deleteable: true + } + ] + } + ] + } + ]; + nodeAddFn(node: TiTreeNode): void { + node.addable = true; + node.editable = true; + node.deleteable = true; + this.myLogs = [...this.myLogs, `add nodeName = ${node.label}`]; + } + + nodeEditFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `edit nodeName = ${node.label}`]; + } + + nodeDeleteFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `delete nodeName = ${node.label}`]; + } + + // 编辑节点之后的回调 + afterNodeEditFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `afterNodeEdit nodeName = ${node.label}`]; + } + + // 添加节点之后的回调 + afterNodeAddFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `afterNodeAdd nodeName = ${node.label}`]; + } +} diff --git a/src/tree/demo/src/app/tree/TreeParentcheckableComponent.ts b/src/tree/demo/src/app/tree/TreeParentcheckableComponent.ts new file mode 100644 index 0000000..59f493d --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeParentcheckableComponent.ts @@ -0,0 +1,48 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-parentcheckable.html' +}) +export class TreeParentcheckableComponent { + multiple: boolean = true; + parentCheckable: boolean = false; + + innerData: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeRadioselectComponent.ts b/src/tree/demo/src/app/tree/TreeRadioselectComponent.ts new file mode 100644 index 0000000..a010486 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeRadioselectComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-radioselect.html' +}) +export class TreeRadioselectComponent { + innerData: Array = [ + { + label: '家用电器', + children: [ + { + label: '大家电', + children: [ + { + label: '空调', + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + checked: 'indeterminate', + expanded: true, + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + selectedData: Array = TiTreeUtil.getSelectedData(this.innerData, false, false); + + onSelect(event: TiTreeNode): void { + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData = TiTreeUtil.getSelectedData(this.innerData, false, false); + } +} diff --git a/src/tree/demo/src/app/tree/TreeSearchComponent.ts b/src/tree/demo/src/app/tree/TreeSearchComponent.ts new file mode 100644 index 0000000..f996c93 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeSearchComponent.ts @@ -0,0 +1,238 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-search.html', + encapsulation: ViewEncapsulation.None +}) +export class TreeSearchComponent { + data: Array = [ + { + label: '家用电器', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '大家电', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '空调', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '海尔空调', + checked: true + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + children: [ + { + label: '男装' + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + expanded: true, + checked: true, + children: [ + { + label: '面部护理面部护理', + checked: true + }, + { + label: '口腔护理', + checked: true + } + ] + } + ]; + datadynamic: Array = [ + { + label: '家用电器', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '大家电', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '空调', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '海尔空调', + checked: true + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + children: [ + { + label: '男装' + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + expanded: true, + checked: true, + children: [ + { + label: '面部护理面部护理', + checked: true + }, + { + label: '口腔护理', + checked: true + } + ] + } + ]; + + showData: Array = this.data; + showDatadynamic: Array = this.datadynamic; + + searchWord: string = ''; + searchWorddynamic: string = ''; + noData: boolean = false; + noDatadynamic: boolean = false; + highlightWords: string; + + onSearch(value: string): void { + // 树数据拷贝,服务也可自行实现 + const searchResult: Array = TiTreeUtil.copy(this.data); + // 根据用户传入的方法筛选 + TiTreeUtil.search(searchResult, (cnode: TiTreeNode): boolean => { + return cnode.label.indexOf(value) >= 0; + }); + // 展开整个树 + TiTreeUtil.traverse(searchResult, (node: TiTreeNode): void => { + node.expanded = true; + }); + // 将用户输入的值传入tree组件中并过滤显示高亮 + this.highlightWords = value; + this.showData = searchResult; + if (this.showData.length < 1) { + this.noData = true; + } + } + // 监听搜索字符串的改变,搭配search事件 + onChange(event: string): void { + if (event === '') { + // 如果搜索字符串清空,那么tree设置为原始数据。 + this.showData = this.data; + this.highlightWords = ''; // 取消高亮显示 + this.noData = false; + } + } + /** + * 监听输入搜索字符串的改变,返回搜索到的内容(高亮显示) + * @param event 搜索字符串 + */ + onChangedynamic(event: string): void { + const searchResult: Array = TiTreeUtil.copy(this.datadynamic); + if (event === '') { + // 如果搜索字符串清空,那么tree设置为原始数据。 + this.showDatadynamic = this.datadynamic; + this.noDatadynamic = false; + + return; + } + TiTreeUtil.search(searchResult, (cnode: TiTreeNode): boolean => { + return cnode.label.indexOf(event) >= 0; + }); + TiTreeUtil.traverse(searchResult, (node: TiTreeNode): void => { + node.expanded = true; + }); + + this.showDatadynamic = searchResult; + this.noDatadynamic = false; + if (this.showDatadynamic.length < 1) { + this.noDatadynamic = true; + } + } + // 在搜索结果中的每一次勾选,需要同步到原始数据也勾选 + onSelect(node: TiTreeNode): void { + // this.showData !== this.data表示正在显示搜索结果 + if (this.showData !== this.data && TiTreeUtil.isLeaf(node)) { + TiTreeUtil.checkedLeafNode(node) ? TiTreeUtil.selectNode(this.data, node, true) : TiTreeUtil.deSelectNode(this.data, node, true); + } + } + onSelectdynamic(node: TiTreeNode): void { + if (this.showDatadynamic !== this.datadynamic && TiTreeUtil.isLeaf(node)) { + TiTreeUtil.checkedLeafNode(node) + ? TiTreeUtil.selectNode(this.datadynamic, node, true) + : TiTreeUtil.deSelectNode(this.datadynamic, node, true); + } + } +} diff --git a/src/tree/demo/src/app/tree/TreeShortcutkeyComponent.ts b/src/tree/demo/src/app/tree/TreeShortcutkeyComponent.ts new file mode 100644 index 0000000..a866219 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeShortcutkeyComponent.ts @@ -0,0 +1,88 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-shortcutkey.html' +}) +export class TreeShortcutkeyComponent { + innerData: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + disabled: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机', + disabled: true + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + }, + { + label: '冰箱' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + }, + { + label: '儿童装', + disabled: true + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeSmallComponent.ts b/src/tree/demo/src/app/tree/TreeSmallComponent.ts new file mode 100644 index 0000000..2606bac --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeSmallComponent.ts @@ -0,0 +1,58 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-small.html' +}) +export class TreeSmallComponent { + innerData: Array = [ + { + label: 'LIC', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: 'CBU安全', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: 'SRE领域', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '操作系统', + checked: true + }, + { + label: '网络' + } + ] + }, + { + label: '近6个小时' + }, + { + label: '近12个小时' + }, + { + label: '近1天' + } + ] + }, + { + label: '近3天', + children: [ + { + label: 'ALM' + }, + { + label: 'MIC' + } + ] + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeTemplateComponent.ts b/src/tree/demo/src/app/tree/TreeTemplateComponent.ts new file mode 100644 index 0000000..7a1df36 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeTemplateComponent.ts @@ -0,0 +1,72 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-template.html' +}) +export class TreeTemplateComponent { + innerData: Array = [ + { + label: 'LIC', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: 'CBU安全', + expanded: true, + expandIcon: 'document', + collapseIcon: 'file', + checked: 'indeterminate', + children: [ + { + expandIcon: 'document', + collapseIcon: 'file', + label: 'SRE领域', + expanded: true, + checked: 'indeterminate', + custom: true, + children: [ + { + expandIcon: 'calendar', + label: '操作系统', + checked: true + }, + { + expandIcon: 'calendar', + label: '网络' + } + ] + }, + { + expandIcon: 'calendar', + label: '近6个小时' + }, + { + expandIcon: 'calendar', + label: '近12个小时' + }, + { + expandIcon: 'calendar', + label: '近1天' + } + ] + }, + { + label: '近3天', + expandIcon: 'document', + collapseIcon: 'file', + children: [ + { + expandIcon: 'calendar', + label: 'ALM' + }, + { + expandIcon: 'calendar', + label: 'MIC' + } + ] + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeTestModule.ts b/src/tree/demo/src/app/tree/TreeTestModule.ts new file mode 100644 index 0000000..75027ad --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeTestModule.ts @@ -0,0 +1,160 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiIconModule, TiOverflowModule, TiSearchboxModule, TiTreeModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TreeRadioselectComponent } from './TreeRadioselectComponent'; +import { TreeMultiselectComponent } from './TreeMultiselectComponent'; +import { TreeUtilComponent } from './TreeUtilComponent'; +import { TreeBeforeExpandComponent } from './TreeBeforeExpandComponent'; +import { TreeParentcheckableComponent } from './TreeParentcheckableComponent'; +import { TreeSearchComponent } from './TreeSearchComponent'; +import { TreeLoadComponent } from './TreeLoadComponent'; +import { TreeChangedbycheckboxComponent } from './TreeChangedbycheckboxComponent'; +import { TreeManyComponent } from './TreeManyComponent'; +import { TreeDisabledComponent } from './TreeDisabledComponent'; +import { TreeSmallComponent } from './TreeSmallComponent'; +import { TreeTemplateComponent } from './TreeTemplateComponent'; +import { TreeIconComponent } from './TreeIconComponent'; +import { TreeShortcutkeyComponent } from './TreeShortcutkeyComponent'; +import { TreeDragComponent } from './TreeDragComponent'; +import { TreeOperateComponent } from './TreeOperateComponent'; +import { TreeBeforeMoreComponent } from './TreeBeforeMoreComponent'; +import { TreeDragBeforedropComponent } from './TreeDragBeforedropComponent'; +import { TreeEventComponent } from './TreeEventComponent'; +import { TreeCheckRelationComponent } from './TreeCheckRelationComponent'; +import { TreeVirtualscrollComponent } from './TreeVirtualscrollComponent'; +import { TreeVirtualscrollSmallComponent } from './TreeVirtualscrollSmallComponent'; +import { TreeVirtualscrollDragComponent } from './TreeVirtualscrollDragComponent'; +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiTreeModule, + TiOverflowModule, + TiSearchboxModule, + TiIconModule, + TiButtonModule, + DemoLogModule, + RouterModule.forChild(TreeTestModule.ROUTES) + ], + declarations: [ + TreeRadioselectComponent, + TreeMultiselectComponent, + TreeBeforeExpandComponent, + TreeParentcheckableComponent, + TreeUtilComponent, + TreeSearchComponent, + TreeLoadComponent, + TreeChangedbycheckboxComponent, + TreeManyComponent, + TreeDisabledComponent, + TreeSmallComponent, + TreeTemplateComponent, + TreeIconComponent, + TreeShortcutkeyComponent, + TreeDragComponent, + TreeDragBeforedropComponent, + TreeOperateComponent, + TreeEventComponent, + TreeBeforeMoreComponent, + TreeCheckRelationComponent, + TreeVirtualscrollComponent, + TreeVirtualscrollSmallComponent, + TreeVirtualscrollDragComponent + ] +}) +export class TreeTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiTreeComponent.html', label: 'Tree' }, + { href: 'classes/TiTreeUtil.html', label: 'TiTreeUtil' } + ]; + static readonly ROUTES: Routes = [ + { + path: 'tree/tree-radioselect', + component: TreeRadioselectComponent + }, + { + path: 'tree/tree-multiselect', + component: TreeMultiselectComponent + }, + { + path: 'tree/tree-before-expand', + component: TreeBeforeExpandComponent + }, + { + path: 'tree/tree-before-more', + component: TreeBeforeMoreComponent + }, + { + path: 'tree/tree-search', + component: TreeSearchComponent + }, + { + path: 'tree/tree-parentcheckable', + component: TreeParentcheckableComponent + }, + { + path: 'tree/tree-check-relation', + component: TreeCheckRelationComponent + }, + { + path: 'tree/tree-util', + component: TreeUtilComponent + }, + { + path: 'tree/tree-changedbycheckbox', + component: TreeChangedbycheckboxComponent + }, + { + path: 'tree/tree-disabled', + component: TreeDisabledComponent + }, + { + path: 'tree/tree-small', + component: TreeSmallComponent + }, + { + path: 'tree/tree-template', + component: TreeTemplateComponent + }, + { + path: 'tree/tree-shortcutkey', + component: TreeShortcutkeyComponent + }, + { + path: 'tree/tree-drag', + component: TreeDragComponent + }, + { + path: 'tree/tree-drag-beforedrop', + component: TreeDragBeforedropComponent + }, + { + path: 'tree/tree-operate', + component: TreeOperateComponent + }, + { + path: 'tree/tree-event', + component: TreeEventComponent + }, + { + path: 'tree/tree-virtualscroll', + component: TreeVirtualscrollComponent + }, + { path: 'tree/tree-load', component: TreeLoadComponent }, + { path: 'tree/tree-many', component: TreeManyComponent }, + { path: 'tree/tree-icon', component: TreeIconComponent }, + { + path: 'tree/tree-virtualscroll-small', + component: TreeVirtualscrollSmallComponent + }, + { + path: 'tree/tree-virtualscroll-drag', + component: TreeVirtualscrollDragComponent + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeUtilComponent.ts b/src/tree/demo/src/app/tree/TreeUtilComponent.ts new file mode 100644 index 0000000..4317d63 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeUtilComponent.ts @@ -0,0 +1,278 @@ +import { AfterViewInit, Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; +@Component({ + templateUrl: './tree-util.html', // 指定组件模板 + styleUrls: ['./treeTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class TreeUtilComponent implements OnInit, AfterViewInit { + newId: number; + arr: Array; + innerData: Array = [ + { + label: '家用电器', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '大家电', + expanded: true, + expandIcon: 'document', // 10.1.2版本之后可通过自定义模板设置图标及样式 + collapseIcon: 'file', + checked: 'indeterminate', + children: [ + { + expandIcon: 'document', + collapseIcon: 'file', + label: '空调', + expanded: true, + checked: 'indeterminate', + children: [ + { + expandIcon: 'calendar', + label: '海尔空调', + checked: true + }, + { + expandIcon: 'calendar', + label: '美的空调' + } + ] + }, + { + expandIcon: 'calendar', + label: '冰箱' + }, + { + expandIcon: 'calendar', + label: '洗衣机' + }, + { + expandIcon: 'calendar', + label: '热水器' + } + ] + }, + { + label: '生活电器', + expandIcon: 'document', + collapseIcon: 'file', + children: [ + { + expandIcon: 'calendar', + label: '加湿器' + }, + { + expandIcon: 'calendar', + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + expandIcon: 'document', + collapseIcon: 'file', + children: [ + { + expandIcon: 'calendar', + label: '男装' + }, + { + expandIcon: 'calendar', + label: '女装' + } + ] + }, + { + label: '化妆', + expandIcon: 'document', + collapseIcon: 'file', + expanded: false, + checked: true, + children: [ + { + expandIcon: 'calendar', + label: '面部护理', + checked: true + }, + { + expandIcon: 'calendar', + label: '口腔护理', + checked: true + } + ] + } + ]; + multiple: boolean = true; + selectedData: Array = []; + ngOnInit(): void { + [...this.arr] = this.innerData; + this.newId = 40; + this.selectedData = TiTreeUtil.getSelectedData(this.innerData, false, true); + } + + ngAfterViewInit(): void { + console.log(TiTreeUtil.getParentNode(this.innerData, this.innerData[0].children[0].children[0])); + } + selectFn(event: TiTreeNode): void { + this.selectedData = TiTreeUtil.getSelectedData(this.innerData, false, true); + } + + changeFn(event: TiTreeNode): void { + console.log(event, 'change'); + } + + /** + * @description 生成一个节点/多个节点: + * @param nodeNum 生成节点的个数:1,生成一个节点 + */ + createData(id: number, nodeNum: number): Array { + const data: Array = []; + if (nodeNum === 1) { + const dataList: TiTreeNode = { + label: `新增节点${id}` + }; + data.push(dataList); + } else { + for (let i: number = 0; i < nodeNum; i++) { + this.newId = id + i; + const dataLists: TiTreeNode = { + label: `新增节点${this.newId}` + }; + data.push(dataLists); + } + } + + return data; + } + + // 展开整个树 + expandNode(): void { + const data: Array = this.arr.concat(); + TiTreeUtil.traverse(data, traverseFn); + + function traverseFn(node: TiTreeNode): void { + node.expanded = true; + } + this.innerData = data; + } + + // 折叠整个树 + deExpandNode(): void { + const data: Array = this.arr.concat(); + TiTreeUtil.traverse(data, traverseFn); + + function traverseFn(node: TiTreeNode): void { + node.expanded = false; + } + this.innerData = data; + } + + // 根节点指定位置添加一个节点 + addNode1(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 1); + console.log(node); + TiTreeUtil.addNode(data, node, 1); + this.innerData = data; + } + + // 根节点指定位置添加多个节点 + addNode2(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 2); + TiTreeUtil.addNode(data, node, 1); + this.innerData = data; + } + + // 根节点追加一个节点 + addNode3(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 1); + TiTreeUtil.addNode(data, node, -1); + this.innerData = data; + } + + // 根节点追加多个节点 + addNode4(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 2); + TiTreeUtil.addNode(data, node, -1); + this.innerData = data; + } + + // 父节点指定位置添加一个节点 + addNode5(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 1); + TiTreeUtil.addNode(data, node, 0, data[1]); + // 更新数据的选中状态。 + // 多选树且叶子节点的父节点可选时,如果父节点是选中的,添加一个非选中的叶子节点,父节点的选中状态应该改变, + // 叶子节点的选中状态决定父节点的选中状态,要保证数据节点的选中状态一致,使用此方法更新数据选中项。 + TiTreeUtil.updateChecked(data); + this.innerData = data; + } + + // 父节点指定位置添加多个节点 + addNode6(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 2); + TiTreeUtil.addNode(data, node, 0, data[1]); + // 更新数据的选中状态。原因参考addNode5方法内注释 + TiTreeUtil.updateChecked(data); + + this.innerData = data; + } + + // 父节点追加一个节点 + addNode7(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 1); + TiTreeUtil.addNode(data, node, -1, data[1]); + // 更新数据的选中状态。原因参考addNode5方法内注释 + TiTreeUtil.updateChecked(data); + + this.innerData = data; + } + + // 父节点追加多个节点 + addNode8(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 2); + TiTreeUtil.addNode(data, node, -1, data[1]); + // 更新数据的选中状态。原因参考addNode5方法内注释 + TiTreeUtil.updateChecked(data); + this.innerData = data; + } + + // 删除一个节点:美的空调 + deleteNode(): void { + const data: Array = this.arr.concat(); + TiTreeUtil.removeNode(data, data[0].children[0].children[0].children[1]); + // 更新数据的选中状态。原因参考addNode5方法内注释 + TiTreeUtil.updateChecked(data); + this.innerData = data; + } + // 选中并展开面部护理 + selectNode(): void { + const data: Array = this.arr.concat(); + TiTreeUtil.expandNode(data, data[2].children[0]); + TiTreeUtil.selectNode(data, data[2].children[0], true); + this.innerData = data; + } + + // 取消选中并展开海尔空调 + deSelectNode(): void { + const data: Array = this.arr.concat(); + TiTreeUtil.deSelectNode(data, data[0].children[0].children[0].children[0], true); + TiTreeUtil.expandNode(data, data[0].children[0].children[0].children[0]); + this.innerData = data; + } + + remove(): void { + this.innerData.pop(); + } +} diff --git a/src/tree/demo/src/app/tree/TreeVirtualscrollComponent.ts b/src/tree/demo/src/app/tree/TreeVirtualscrollComponent.ts new file mode 100644 index 0000000..32ac1c3 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeVirtualscrollComponent.ts @@ -0,0 +1,48 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTreeComponent, TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-virtualscroll.html', + styleUrls: ['./treeTest.less'] +}) +export class TreeVirtualscrollComponent implements OnInit { + @ViewChild('tree', { static: true }) treeCom: TiTreeComponent; + data: Array = []; + ngOnInit(): void { + this.data = this.createData('node-radio', 3, 60); + } + createData(parentLabel: string, level: number, num: number): Array { + const result: Array = []; + for (let i: number = 0; i < num; i++) { + const item: TiTreeNode = { + label: `${parentLabel}-${i}` + }; + if (level > 1) { + item.children = this.createData(item.label, level - 1, num); + } + result.push(item); + } + + return result; + } + + scrollStart(): void { + this.treeCom.virtualScrollViewport.scrollToIndex(0, 'smooth'); + } + + scrollMiddle(): void { + TiTreeUtil.traverse(this.data, (node: TiTreeNode) => { + return (node.expanded = false); + }); + + this.treeCom.virtualScrollViewport.scrollToIndex(this.data.length / 2, 'smooth'); + } + + scrollEnd(): void { + TiTreeUtil.traverse(this.data, (node: TiTreeNode) => { + return (node.expanded = false); + }); + + this.treeCom.virtualScrollViewport.scrollToIndex(this.data.length, 'smooth'); + } +} diff --git a/src/tree/demo/src/app/tree/TreeVirtualscrollDragComponent.ts b/src/tree/demo/src/app/tree/TreeVirtualscrollDragComponent.ts new file mode 100644 index 0000000..000c371 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeVirtualscrollDragComponent.ts @@ -0,0 +1,43 @@ +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-virtualscroll-drag.html', + encapsulation: ViewEncapsulation.None +}) +export class TreeVirtualscrollDragComponent implements OnInit { + data1: Array = []; + data2: Array = []; + selectedData1: Array; + selectedData2: Array; + + ngOnInit(): void { + this.data1 = this.createData('node-radio', 3, 60); + this.data2 = this.createData('node-multiple', 3, 50); + } + + onSelect1(event: TiTreeNode): void { + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData1 = TiTreeUtil.getSelectedData(this.data1, false, false); + } + + onSelect2(event: TiTreeNode): void { + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData2 = TiTreeUtil.getSelectedData(this.data2, false, true); + } + + createData(parentLabel: string, level: number, num: number): Array { + const result: Array = []; + for (let i: number = 0; i < num; i++) { + const item: TiTreeNode = { + label: `${parentLabel}-${i}` + }; + if (level > 1) { + item.children = this.createData(item.label, level - 1, num); + } + result.push(item); + } + + return result; + } +} diff --git a/src/tree/demo/src/app/tree/TreeVirtualscrollSmallComponent.ts b/src/tree/demo/src/app/tree/TreeVirtualscrollSmallComponent.ts new file mode 100644 index 0000000..aba1614 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeVirtualscrollSmallComponent.ts @@ -0,0 +1,42 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-virtualscroll-small.html' +}) +export class TreeVirtualscrollSmallComponent implements OnInit { + data1: Array = []; + data2: Array = []; + selectedData1: Array; + selectedData2: Array; + + ngOnInit(): void { + this.data1 = this.createData('node-radio', 3, 60); + this.data2 = this.createData('node-multiple', 3, 50); + } + + onSelect1(event: TiTreeNode): void { + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData1 = TiTreeUtil.getSelectedData(this.data1, false, false); + } + + onSelect2(event: TiTreeNode): void { + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData2 = TiTreeUtil.getSelectedData(this.data2, false, true); + } + + createData(parentLabel: string, level: number, num: number): Array { + const result: Array = []; + for (let i: number = 0; i < num; i++) { + const item: TiTreeNode = { + label: `${parentLabel}-${i}` + }; + if (level > 1) { + item.children = this.createData(item.label, level - 1, num); + } + result.push(item); + } + + return result; + } +} diff --git a/src/tree/demo/src/app/tree/tree-before-expand.html b/src/tree/demo/src/app/tree/tree-before-expand.html new file mode 100644 index 0000000..cfd7c33 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-before-expand.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-before-more.html b/src/tree/demo/src/app/tree/tree-before-more.html new file mode 100644 index 0000000..b05facf --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-before-more.html @@ -0,0 +1,7 @@ + diff --git a/src/tree/demo/src/app/tree/tree-changedbycheckbox.html b/src/tree/demo/src/app/tree/tree-changedbycheckbox.html new file mode 100644 index 0000000..532c18e --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-changedbycheckbox.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-check-relation.html b/src/tree/demo/src/app/tree/tree-check-relation.html new file mode 100644 index 0000000..aa060e1 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-check-relation.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-disabled.html b/src/tree/demo/src/app/tree/tree-disabled.html new file mode 100644 index 0000000..6be040b --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-disabled.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-drag-beforedrop.html b/src/tree/demo/src/app/tree/tree-drag-beforedrop.html new file mode 100644 index 0000000..9123704 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-drag-beforedrop.html @@ -0,0 +1,7 @@ + + + {{item.label}} + 不可拖拽 + + + diff --git a/src/tree/demo/src/app/tree/tree-drag.html b/src/tree/demo/src/app/tree/tree-drag.html new file mode 100644 index 0000000..4e67809 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-drag.html @@ -0,0 +1,2 @@ + + diff --git a/src/tree/demo/src/app/tree/tree-event.html b/src/tree/demo/src/app/tree/tree-event.html new file mode 100644 index 0000000..f0fac0e --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-event.html @@ -0,0 +1,10 @@ + + diff --git a/src/tree/demo/src/app/tree/tree-icon.html b/src/tree/demo/src/app/tree/tree-icon.html new file mode 100644 index 0000000..a67eae7 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-icon.html @@ -0,0 +1,6 @@ +

    1.描述

    +

    树组件通过接口配置class类名设置图标(供于内部测试、追溯,不对外暴露该实例)

    +

    2.示例

    +
    + +
    diff --git a/src/tree/demo/src/app/tree/tree-load.html b/src/tree/demo/src/app/tree/tree-load.html new file mode 100644 index 0000000..aae4b16 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-load.html @@ -0,0 +1,35 @@ +

    1.描述

    +

    单选树, 数据加载和数据改变

    +

    2.示例

    + + + + + + + + + +
    +
    + +
    +
    +
      +
    • +

      {{i+1}}.选中项:

      + {{item.label}}: +
      +

      {{i+1}}.父节点:

      + {{child.label}}/ +


      +
    • +
    +
    +

    每个组件改变数据,都用下面六个按钮。不变化

    +
    +
    +
    +
    +
    +
    diff --git a/src/tree/demo/src/app/tree/tree-many.html b/src/tree/demo/src/app/tree/tree-many.html new file mode 100644 index 0000000..0a138d7 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-many.html @@ -0,0 +1,60 @@ +

    1.描述

    +

    主要是为了测试同一个页面中存在两个及以上的树组件,在各自的select接口中调用TiTreeUtil.getSelectedData返回的数据是否相互影响。

    +

    2.示例

    +

    第一棵树;

    + + + + + + + + + +
    +
    + +
    +
    +
    +
      +
    • +

      {{i+1}}.选中项:

      + {{item.label}}: +
      +

      {{i+1}}.父节点:

      + {{child.label}}/ +


      +
    • +
    +
    +
    + +

    第二棵树;

    + + + + + + + + + +
    +
    + +
    +
    +
    +
      +
    • +

      {{i+1}}.选中项:

      + {{item.label}}: +
      +

      {{i+1}}.父节点:

      + {{child.label}}/ +


      +
    • +
    +
    +
    diff --git a/src/tree/demo/src/app/tree/tree-multiselect.html b/src/tree/demo/src/app/tree/tree-multiselect.html new file mode 100644 index 0000000..8173a42 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-multiselect.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-operate.html b/src/tree/demo/src/app/tree/tree-operate.html new file mode 100644 index 0000000..6a3f767 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-operate.html @@ -0,0 +1,11 @@ + + diff --git a/src/tree/demo/src/app/tree/tree-parentcheckable.html b/src/tree/demo/src/app/tree/tree-parentcheckable.html new file mode 100644 index 0000000..303e9fa --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-parentcheckable.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-radioselect.html b/src/tree/demo/src/app/tree/tree-radioselect.html new file mode 100644 index 0000000..b87931d --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-radioselect.html @@ -0,0 +1,5 @@ + +
    +
    选中项: {{ selectedData[0].label }}
    +
    父节点:{{child.label}}/
    +
    diff --git a/src/tree/demo/src/app/tree/tree-search.html b/src/tree/demo/src/app/tree/tree-search.html new file mode 100644 index 0000000..21f11d2 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-search.html @@ -0,0 +1,30 @@ +
    +
    +

    1. 点击搜索图标搜索

    + +
    暂无数据
    + +
    +
    +

    2. 动态输入实时搜索

    + + +
    +
    diff --git a/src/tree/demo/src/app/tree/tree-shortcutkey.html b/src/tree/demo/src/app/tree/tree-shortcutkey.html new file mode 100644 index 0000000..f3f6fd8 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-shortcutkey.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-small.html b/src/tree/demo/src/app/tree/tree-small.html new file mode 100644 index 0000000..3f03809 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-small.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-template.html b/src/tree/demo/src/app/tree/tree-template.html new file mode 100644 index 0000000..d46b13e --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-template.html @@ -0,0 +1,14 @@ + + + + + + {{item.label}} + + + diff --git a/src/tree/demo/src/app/tree/tree-util.html b/src/tree/demo/src/app/tree/tree-util.html new file mode 100644 index 0000000..c4609a6 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-util.html @@ -0,0 +1,18 @@ + +

    1. 展开、折叠整个树

    + + +

    2. 添加节点

    + +
    + +
    + +
    + + +

    3. 选中、展开、删除

    + +
    + + diff --git a/src/tree/demo/src/app/tree/tree-virtualscroll-drag.html b/src/tree/demo/src/app/tree/tree-virtualscroll-drag.html new file mode 100644 index 0000000..f68d3d3 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-virtualscroll-drag.html @@ -0,0 +1,41 @@ +

    1.描述

    +

    树虚拟滚动 + 支持拖拽

    +

    数据量大时可使用 virtual 接口开启虚拟滚动,默认值为 false。使用虚拟滚动时,需要给 ti-tree 设置高度。

    +

    配置 nodeDraggable 接口使节点支持拖放, 默认值为 false。

    + +

    导入

    +

    import {{ '{' }} TiTreeModule {{ '}' }} from '@opentiny/ng';

    + +

    2.示例

    +
    +
    +

    2.1 单选

    + + +

    + 选中项label:{{item.label}} +
    + +
    +

    2.2 多选

    + + +

    + 选中项label:{{item.label}}, +
    +
    diff --git a/src/tree/demo/src/app/tree/tree-virtualscroll-small.html b/src/tree/demo/src/app/tree/tree-virtualscroll-small.html new file mode 100644 index 0000000..69e3f88 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-virtualscroll-small.html @@ -0,0 +1,35 @@ +

    1.描述

    +

    虚拟滚动 + small

    +

    数据量大时可使用 virtual 接口开启虚拟滚动,默认值为 false。

    +

    使用虚拟滚动时,需要给 ti-tree 设置高度。

    + +

    导入

    +

    import {{ '{' }} TiTreeModule {{ '}' }} from '@opentiny/ng';

    + +

    2.示例

    + +
    +
    +

    2.1 small型单选

    + + +

    + 选中项label:{{item.label}} +
    + +
    +

    2.2 small型多选

    + + +

    + 选中项label:{{item.label}}, +
    +
    diff --git a/src/tree/demo/src/app/tree/tree-virtualscroll.html b/src/tree/demo/src/app/tree/tree-virtualscroll.html new file mode 100644 index 0000000..560317a --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-virtualscroll.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/tree/demo/src/app/tree/treeTest.less b/src/tree/demo/src/app/tree/treeTest.less new file mode 100644 index 0000000..04106cd --- /dev/null +++ b/src/tree/demo/src/app/tree/treeTest.less @@ -0,0 +1,4 @@ +.tree-button { + width: 200px; + margin: 10px; +} diff --git a/src/tree/demo/src/app/tree/webdoc/tree-demos.js b/src/tree/demo/src/app/tree/webdoc/tree-demos.js new file mode 100644 index 0000000..d31d91b --- /dev/null +++ b/src/tree/demo/src/app/tree/webdoc/tree-demos.js @@ -0,0 +1,249 @@ +export default { + column: '2', + demos: [ + { + demoId: 'tree-radioselect', + name: { + 'zh-CN': '单选树', + 'en-US': 'tree radioselect' + }, + desc: { + 'zh-CN': '

    单选树的基本用法。

    ', + 'en-US': 'tree radioselect' + }, + apis: ['TiTreeComponent.properties.data'] + }, + { + demoId: 'tree-multiselect', + name: { + 'zh-CN': '多选树', + 'en-US': 'tree multiselect' + }, + desc: { + 'zh-CN': '

    通过属性multiple配置是否为多选树。

    ', + 'en-US': 'tree multiselect' + }, + apis: ['TiTreeComponent.properties.multiple'] + }, + { + demoId: 'tree-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'tree disabled' + }, + desc: { + 'zh-CN': + '

    通过数据项的disabled属性配置树节点是否为禁用状态;10.1.3 版本到 10.1.14 版本父节点禁用,展开收起图标也禁用;10.1.15 版本及之后:父节点禁用,展开收起图标不禁用。

    ', + 'en-US': 'tree disabled' + } + }, + { + demoId: 'tree-parentcheckable', + name: { + 'zh-CN': '父节点是否支持选中', + 'en-US': 'tree parentcheckable' + }, + desc: { + 'zh-CN': '

    通过属性parentCheckable配置多选树时父节点是否支持选中。

    ', + 'en-US': 'tree parentcheckable' + }, + apis: ['TiTreeComponent.properties.parentCheckable'] + }, + { + demoId: 'tree-check-relation', + name: { + 'zh-CN': '不关联父子节点', + 'en-US': 'tree check-relation' + }, + desc: { + 'zh-CN': '

    通过属性checkRelation配置父子节点选中时是否具有关联关系。

    ', + 'en-US': 'tree check relation' + }, + apis: ['TiTreeComponent.properties.checkRelation'] + }, + { + demoId: 'tree-changedbycheckbox', + name: { + 'zh-CN': '只有点击checkbox能选中', + 'en-US': 'tree changedbycheckbox' + }, + desc: { + 'zh-CN': '

    通过属性changedByCheckbox配置是否只有点击 checkbox 能选中树节点,仅为多选树时生效。

    ', + 'en-US': 'tree changedbycheckbox' + }, + apis: ['TiTreeComponent.properties.changedByCheckbox'] + }, + { + demoId: 'tree-template', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'tree template' + }, + desc: { + 'zh-CN': '

    通过模板item配置树节点的内容。

    ', + 'en-US': 'tree template' + }, + apis: ['TiTreeComponent.slots.itemTemplate'] + }, + { + demoId: 'tree-shortcutkey', + name: { + 'zh-CN': '支持快捷键操作', + 'en-US': 'tree shortcutkey' + }, + desc: { + 'zh-CN': + '

    支持快捷键操作;上下键:在同级的树节点移动,左右键:展开收起树节点及跨层级移动焦点,Enter 键或 Space 键:选中或取消当前焦点的树节点。

    ', + 'en-US': 'tree shortcutkey' + } + }, + { + demoId: 'tree-drag', + name: { + 'zh-CN': '支持拖拽', + 'en-US': 'tree drag' + }, + desc: { + 'zh-CN': '

    通过属性nodeDraggable配置是否支持拖拽;当拖拽入目标树节点的时候触发nodeDrop事件。

    ', + 'en-US': 'tree drag' + }, + apis: ['TiTreeComponent.properties.nodeDraggable', 'TiTreeComponent.events.nodeDrop'] + }, + { + demoId: 'tree-drag-beforedrop', + name: { + 'zh-CN': '部分拖拽', + 'en-US': 'tree drag beforedrop' + }, + desc: { + 'zh-CN': + '

    通过数据项的draggable属性配置当前树节点是否支持拖拽;当当前树节点将要拖拽入目标树节点的时候触发beforeDrop事件。

    ', + 'en-US': 'tree drag beforedrop' + }, + apis: ['TiTreeComponent.events.beforeDrop', 'TiTreeDragNode'] + }, + { + demoId: 'tree-operate', + name: { + 'zh-CN': '支持操作按钮', + 'en-US': 'tree operate' + }, + desc: { + 'zh-CN': + '

    组件提供增加、编辑、删除三种悬浮操作按钮,分别通过数据项的addableeditabledeleteable属性控制;当点击增加按钮的时候触发nodeAdded事件,传递出去的树节点 parent属性;当点击编辑按钮的时候触发nodeEdited事件;当点击删除按钮的时候触发nodeDeleted事件。

    ', + 'en-US': 'tree operate' + }, + apis: [ + 'TiTreeComponent.events.nodeAdded', + 'TiTreeComponent.events.nodeEdited', + 'TiTreeComponent.events.nodeDeleted', + 'TiTreeComponent.events.afterNodeEdit', + 'TiTreeComponent.events.afterNodeAdd' + ] + }, + { + demoId: 'tree-event', + name: { + 'zh-CN': '事件', + 'en-US': 'tree event' + }, + desc: { + 'zh-CN': + '

    当选中树节点的时候触发select事件;当选中项发生改变的时候触发change事件;expandcollapse事件,当父节点展开时候触发expand事件;当父节点收起触发collapse事件。

    ', + 'en-US': 'tree event' + }, + apis: [ + 'TiTreeComponent.events.select', + 'TiTreeComponent.events.change', + 'TiTreeComponent.events.expand', + 'TiTreeComponent.events.collapse', + 'TiTreeNode' + ] + }, + { + demoId: 'tree-before-expand', + name: { + 'zh-CN': '异步请求', + 'en-US': 'tree before expand' + }, + desc: { + 'zh-CN': + '

    当树节点将要展开的时候触发beforeExpand事件,一般用于异步数据获取,异步加载状态分为 loading(正在加载)、success(加载成功)、error(加载失败)三种状态。

    ', + 'en-US': 'tree before expand' + }, + apis: ['TiTreeComponent.events.beforeExpand', 'TiTreeComponent.methods.getBeforeExpandNode', 'TiTreeComponent'] + }, + { + demoId: 'tree-before-more', + name: { + 'zh-CN': '分段加载数据', + 'en-US': 'tree before-more' + }, + desc: { + 'zh-CN': '

    当点击更多按钮的时候触发beforeExpand事件,一般用于大数据需要分段加载的场景。

    ', + 'en-US': 'tree before more' + }, + apis: ['TiTreeComponent.events.beforeMore'] + }, + { + demoId: 'tree-search', + name: { + 'zh-CN': '树搜索', + 'en-US': 'tree search' + }, + desc: { + 'zh-CN': '

    使用 Searchbox 组件配合实现树搜索的场景。

    ', + 'en-US': 'tree search' + }, + apis: ['TiTreeComponent.properties.highlightWords'] + }, + { + demoId: 'tree-small', + name: { + 'zh-CN': '紧凑型', + 'en-US': 'tree small' + }, + desc: { + 'zh-CN': '

    通过属性small配置树组件是否为紧凑型。

    ', + 'en-US': 'tree small' + }, + apis: ['TiTreeComponent.properties.small'] + }, + { + demoId: 'tree-util', + name: { + 'zh-CN': '公共方法', + 'en-US': 'tree util' + }, + desc: { + 'zh-CN': '

    树组件实现对树节点的增删查等操作的公共方法。

    ', + 'en-US': 'tree util' + }, + apis: [ + 'TiTreeUtil.methods.copy', + 'TiTreeUtil.methods.addNode', + 'TiTreeUtil.methods.deSelectNode', + 'TiTreeUtil.methods.expandNode', + 'TiTreeUtil.methods.getParentNode', + 'TiTreeUtil.methods.getSelectedData', + 'TiTreeUtil.methods.removeNode', + 'TiTreeUtil.methods.search', + 'TiTreeUtil.methods.selectNode', + 'TiTreeUtil.methods.traverse', + 'TiTreeUtil.methods.updateChecked' + ] + }, + { + demoId: 'tree-virtualscroll', + name: { + 'zh-CN': '虚拟滚动', + 'en-US': 'tree virtualscroll' + }, + desc: { + 'zh-CN': '

    通过属性virtual配置是否开启虚拟滚动,虚拟滚动时需要设置高度。

    ', + 'en-US': 'tree virtualscroll' + }, + apis: ['TiTreeComponent.properties.virtual'] + } + ] +}; diff --git a/src/tree/demo/src/app/tree/webdoc/tree.cn.md b/src/tree/demo/src/app/tree/webdoc/tree.cn.md new file mode 100644 index 0000000..6b96d6a --- /dev/null +++ b/src/tree/demo/src/app/tree/webdoc/tree.cn.md @@ -0,0 +1,31 @@ +--- +title: Tree 树 +--- +# Tree 树 + +
    + +Tree 是可以完整展现数据的层级关系,并具有展开收起选择等交互功能的组件。   + ++ 支持单选、多选两种场景。 + ++ 提供 TiTreeUtil 公共方法,包括增、删、改、查、选中、取消选中、遍历、筛选、获取选中项等操作。 + +```typescript +import { TiTreeModule } from '@opentiny/ng'; +``` + +
    + +
    + +Tree 是可以完整展现数据的层级关系,并具有展开收起选择等交互功能的组件。   + ++ 支持单选、多选两种场景。 + ++ 提供 TiTreeUtil 公共方法,包括增、删、改、查、选中、取消选中、遍历、筛选、获取选中项等操作。 + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    diff --git a/src/tree/demo/src/app/tree/webdoc/tree.en.md b/src/tree/demo/src/app/tree/webdoc/tree.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/tree/demo/src/app/tree/webdoc/tree.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/tree/demo/src/favicon.ico b/src/tree/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/tree/demo/src/index.html b/src/tree/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/tree/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/tree/demo/src/main.ts b/src/tree/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/tree/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/tree/demo/test.ts b/src/tree/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/tree/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/tree/demo/tsconfig.app.json b/src/tree/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/tree/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/tree/demo/tsconfig.spec.json b/src/tree/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/tree/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/tree/lib/index.ts b/src/tree/lib/index.ts new file mode 100644 index 0000000..7d1d7cb --- /dev/null +++ b/src/tree/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTreeModule'; diff --git a/src/tree/lib/ng-package.json b/src/tree/lib/ng-package.json new file mode 100644 index 0000000..550b1e2 --- /dev/null +++ b/src/tree/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/tree", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/tree/lib/package.json b/src/tree/lib/package.json new file mode 100644 index 0000000..132c20a --- /dev/null +++ b/src/tree/lib/package.json @@ -0,0 +1,20 @@ +{ + "name": "@opentiny/ng-tree", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/cdk": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-checkbox": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-loading": "~1.0.0-beta.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-validation": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/tree/lib/project.json b/src/tree/lib/project.json new file mode 100644 index 0000000..0bfa362 --- /dev/null +++ b/src/tree/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/tree/lib", + "sourceRoot": "src/tree/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/tree"], + "options": { + "project": "src/tree/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/tree"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js tree" + }, + { + "command": "ng default-build tree" + }, + { + "command": "node build/clear-default-theme.js tree" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/tree && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build tree && ng pack tree && node build/publish.js tree --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/tree/lib/src/TiAutoSelectDirective.ts b/src/tree/lib/src/TiAutoSelectDirective.ts new file mode 100644 index 0000000..8e0c711 --- /dev/null +++ b/src/tree/lib/src/TiAutoSelectDirective.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef } from '@angular/core'; + +/** + * @ignore + * 指令功能: 悬浮操作按钮场景,提供input框内容选中功能 + */ +@Directive({ + selector: '[tiAutoSelect]' +}) +export class TiAutoSelectDirective { + constructor(private elementRef: ElementRef) {} + ngOnInit(): void { + setTimeout(() => { + this.elementRef.nativeElement.select(); + }, 0); + } +} diff --git a/src/tree/lib/src/TiHighlightPipe.ts b/src/tree/lib/src/TiHighlightPipe.ts new file mode 100644 index 0000000..626fa63 --- /dev/null +++ b/src/tree/lib/src/TiHighlightPipe.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Pipe, PipeTransform } from '@angular/core'; +/** + * @ignore + * TiHighlightPipe 替换搜索的值并高亮显示 + */ +@Pipe({ + name: 'tiHighlight', + pure: true +}) +export class TiHighlightPipe implements PipeTransform { + transform(value: string, keyword: string) { + if (!keyword) { + return value; + } + const regx: RegExp = /[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘',。、]/gi; + const keywords: string = keyword.replace(regx, (item) => { + return `\\${item}`; + }); + const reg: RegExp = new RegExp(keywords, 'ig'); + const result = value.replace(reg, (word) => { + return `${word}`; + }); + return result; + } +} diff --git a/src/tree/lib/src/TiTreeComponent.ts b/src/tree/lib/src/TiTreeComponent.ts new file mode 100644 index 0000000..d4747e5 --- /dev/null +++ b/src/tree/lib/src/TiTreeComponent.ts @@ -0,0 +1,1111 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + Input, + IterableChanges, + IterableDiffer, + IterableDiffers, + Output, + QueryList, + Renderer2, + SimpleChanges, + TemplateRef, + ViewChild, + ViewChildren +} from '@angular/core'; +import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiTreeUtil } from './TiTreeUtil'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import packageInfo from '../package.json'; + +// TODO: 采用TiCheckgroup重构后,不需要那么多TreeUtil方法,不需要选中节点/取消选中节点方法。 +/** + * 树组件中每个节点的配置 + */ +export interface TiTreeNode { + /** + * 节点文本 + */ + label?: string; + /** + * 节点是否展开 + */ + expanded?: boolean; + /** + * 多选场景下,节点选中状态 + */ + checked?: boolean | string; + /** + * 节点展开时的字体图标,10.1.2 支持自定义模板添加图标 + */ + expandIcon?: string; + /** + * 节点收起时的字体图标,10.1.2 支持自定义模板添加图标 + */ + collapseIcon?: string; + /** + * 是否禁用 + */ + disabled?: boolean; + /** + * 子节点数据集 + */ + children?: Array; + /** + * 异步加载状态 + */ + loadStatus?: 'loading' | 'error' | 'success'; + /** + * 是否支持悬浮编辑功能 + */ + editable?: boolean; + /** + * 是否支持悬浮添加功能 + */ + addable?: boolean; + /** + * 是否支持悬浮删除功能 + */ + deleteable?: boolean; + /** + * 是否显示'更多'按钮 + */ + showMore?: boolean; + /** + * '更多'加载状态 + */ + moreStatus?: 'loading' | 'error' | 'success'; + /** + * 是否支持拖拽 + * 1.禁用状态不可拖拽; + * 2.非禁用状态: + * true: 节点可拖拽; + * false: 节点不可拖拽; + * undefined: 如果 nodeDraggable 为 true,则可拖拽;为 false 或者 undefined,则不可拖拽 + */ + draggable?: boolean; + /** + * @ignore + * 父节点 + */ + parent?: TiTreeNode; + /** + * @ignore + * 开启虚拟滚动时会给节点添加该内部属性 + * 节点层级,根节点层级为0,往下依次类推 + */ + level?: number; + /** + * @ignore + * 开启虚拟滚动时会给节点添加该内部属性 + * 是否为同层级且隶属同一父节点的节点集合中的最后一个 + */ + isLast?: boolean; + /** + * @ignore + * 开启虚拟滚动时会给节点添加该内部属性 + * 是否为同层级且隶属同一父节点的节点集合中的第一个 + */ + isFirst?: boolean; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} +/** + * drag info + */ +export interface TiTreeDragNode { + /** + * 事件对象 + */ + event?: DragEvent; + /** + * 目标节点 + */ + targetNode?: TiTreeNode; + /** + * 被拖拽的节点 + */ + dragNode?: TiTreeNode; + /** + * 放置位置,-1 代表当前节点前,0 代表当前节点里面,1 代表当前节点后 + */ + dropPosition?: number; +} +/** + * Tree树组件 + * + * 分类:支持单选、多选两种类型 + * + * 公共方法:树组件提供 [TiTreeUtil]{@link ../classes/TiTreeUtil.html} 公共方法,包括增、删、改、查、选中、取消选中、遍历、筛选、获取选中项等操作 + * + */ +@Component({ + selector: 'ti-tree', + templateUrl: './tree.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./tree.less'], + host: { + '[class.ti3-tree-virtual-scroll]': 'virtual' + } +}) +export class TiTreeComponent extends TiBaseComponent { + private static readonly DEFAULT_ITEM_SIZE: number = 30; + private static readonly DEFAULT_ITEM_LEFT_SPACE: number = 24; // .ti3-tree-leaf-node 元素左边距 + private static readonly SMALL_ITEM_LEFT_SPACE: number = 16; // .ti3-tree-leaf-node 元素左边距 + /** + * 组件全量数据 + */ + @Input() data: Array; + /** + * 多选场景下,父节点是否可被点击选中 + */ + @Input() parentCheckable: boolean = true; + /** + * 是否多选 + */ + @Input() multiple: boolean = false; + /** + * 多选场景下,当 changedByCheckbox 为 true 时: + * + * 1.点击文本,复选框状态不会改变,只会触发 select 回调; + * + * 2.点击复选框只会触发 change 回调; + * + * 3.复选框 disabled 时,点击文本会触发 select 事件 + */ + @Input() changedByCheckbox: boolean = false; + /** + * 是否高亮匹配树节点中搜索到的文本 + */ + @Input() highlightWords: string; + /** + * @ignore + * 支持拖放 + * + * 设置为true:所有节点可拖拽 + * + * 设置为false/undefined:所有节点不可拖拽 + * + * 如果节点设置了draggable属性,节点是否可拖拽就需要看draggable的值 + */ + /** + * 是否支持拖放 + */ + @Input() nodeDraggable: boolean = false; + /** + * 多选场景下,是否关联父子节点选中状态 + */ + @Input() checkRelation: boolean = true; + /** + * 是否开启虚拟滚动 + */ + @Input() virtual: boolean = false; + /** + * 节点展开前触发的回调,一般用于异步数据获取 + */ + @Output() readonly beforeExpand: EventEmitter = new EventEmitter(); + /** + * 点击节点时触发的回调 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + /** + * 当前选中项改变时触发的回调,参数:改变的选中项 + */ + @Output() readonly change: EventEmitter = new EventEmitter(); + /** + * 拖拽节点放入目标节点前触发的回调,参数:当前拖拽节点 + */ + @Output() readonly beforeDrop: EventEmitter = new EventEmitter(); + /** + * 鼠标在拖放目标上释放时触发的回调,参数:当前拖拽节点 + */ + @Output() readonly nodeDrop: EventEmitter = new EventEmitter(); + /** + * 点击悬浮添加节点按钮时触发的回调,参数:当前新增的节点 + */ + @Output() readonly nodeAdded: EventEmitter = new EventEmitter(); + /** + * 点击悬浮编辑节点按钮时触发的回调,参数:当前编辑的节点 + */ + @Output() readonly nodeEdited: EventEmitter = new EventEmitter(); + /** + * 点击悬浮删除节点按钮时触发的回调,参数:当前删除的节点 + */ + @Output() readonly nodeDeleted: EventEmitter = new EventEmitter(); + /** + * 点击 '更多' 按钮时触发的回调,参数:当前点击的 '更多' 按钮节点 + */ + @Output() readonly beforeMore: EventEmitter = new EventEmitter(); + /** + * 节点展开时触发的回调,参数:当前点击的节点 + */ + @Output() readonly expand: EventEmitter = new EventEmitter(); + /** + * 节点折叠时触发的回调,参数:当前点击的节点 + */ + @Output() readonly collapse: EventEmitter = new EventEmitter(); + /** + * 新增节点后的回调,参数:当前新增的节点 + */ + @Output() readonly afterNodeAdd: EventEmitter = new EventEmitter(); + /** + * 编辑节点后的回调,参数:当前编辑的节点 + */ + @Output() readonly afterNodeEdit: EventEmitter = new EventEmitter(); + /** + * 用于异步场景:当前点击需要展开的父节点 + */ + private beforeExpandNode: TiTreeNode; + /** + * 监听data改变 + */ + private dataDiffer: IterableDiffer; + /** + * @ignore + * 获取到用户自定义的模板 + */ + @ContentChild(TemplateRef, { static: true }) itemTemplate: TemplateRef; + /** + * @ignore + * 获取文本区域dom集合 + */ + @ViewChildren('nodeList') elems: QueryList; + /** + * CdkVirtualScrollViewport 实例 + */ + @ViewChild(CdkVirtualScrollViewport, { static: false }) + virtualScrollViewport: CdkVirtualScrollViewport; + /** + * @ignore + * 模板中使用,高亮的选中项 + */ + public actived: TiTreeNode; + /** + * @ignore + * 词条 + */ + public treeLan = TiLocale.getLocaleWords().tiTree; + /** + * @ignore + * 模板中使用,虚拟滚动时每个节点占据的高度 + */ + public itemSize: number = TiTreeComponent.DEFAULT_ITEM_SIZE; + /** + * 被拖拽节点 + */ + private dragNode: TiTreeNode; + /** + * 拖拽position + */ + private dropPosition: number; + /** + * 拖放经过的节点 + */ + private overNode: TiTreeNode; + /** + * 当前操作的节点是否为新增的节点 + */ + private addingNode: boolean = false; + /** + * 是否为 small(紧凑型) 的树 + */ + private isSmall: boolean = false; + private oldData: Array; + protected versionInfo: string = super.getVersion(packageInfo); + constructor( + protected elementRef: ElementRef, + protected renderer2: Renderer2, + protected iterableDiffers: IterableDiffers, + private tiRenderer: TiRenderer, + private cdRef: ChangeDetectorRef + ) { + super(elementRef, renderer2); + } + ngOnInit(): void { + super.ngOnInit(); + this.dataDiffer = this.iterableDiffers.find(this.data).create((index: number, item: TiTreeNode) => { + return item.checked; + }); + // 内部使用的数据,用于记录用户的操作改变 + // TODO: 仅在初始化时挡非法数据,是不够的。建议去除。但因为要兼容已发出的版本,所以不去除。 + this.data = !Util.isArray(this.data) ? [] : this.data; + this.oldData = this.data; + this.isSmall = this.nativeElement.hasAttribute('small'); + } + + ngOnChanges(changes: SimpleChanges): void { + // 动态修改选中项时需要修改索引才能使选中项高亮 + if (changes['data'] && !changes['data'].firstChange) { + // 选中项背景高亮 + this.actived = this.initActived(this.data); + } + } + + ngDoCheck(): void { + super.ngDoCheck(); + if (this.data === this.oldData) { + const dataChanges: IterableChanges = this.dataDiffer.diff(this.data); + if (dataChanges) { + // 重新初始化选中项背景高亮 + this.actived = this.initActived(this.data); + } + } else { + this.oldData = this.data; + } + + // 异步挂载数据,改变节点属性时指引未发生变化,onpush模式下不会触发变更,故手动触发 + this.cdRef.markForCheck(); + } + /** + * @ignore + * @description 判断是否显示复选框 + * @param node 节点数据 + */ + public showCheckboxFn(node: TiTreeNode): boolean { + if (this.multiple !== true) { + return false; + } + + if (this.parentCheckable === true) { + return true; + } + + return TiTreeUtil.isLeaf(node); + } + + /** + * @ignore + * @description 点击父节点图标执行的逻辑 + * @param node 当前节点数据 + */ + public onClickPnodeIcon(node: TiTreeNode, event: MouseEvent): void { + // 阻止事件冒泡:点击父节点图标无高亮样式 + event.stopPropagation(); + this.beforeExpandNode = node; + // 1.当前节点是展开状态 + if (node.expanded) { + node.expanded = false; + this.collapse.emit(node); + } else if (this.beforeExpand.observers.length === 0) { + // 2.如果未定义beforeExpand事件(非异步),点击时让节点展开 + node.expanded = true; + this.expand.emit(node); + } else { + // 3.异步获取数据:将组件实例通知出去 + this.beforeExpand.emit(this); + } + } + + /** + * 获取当前需要展开的父节点,适用于异步场景 + */ + public getBeforeExpandNode(): TiTreeNode { + return this.beforeExpandNode; + } + /** + * @ignore + * @param node 节点 + */ + public onClickReload(event: MouseEvent, node): void { + node.expanded = false; + this.onClickPnodeIcon(node, event); + } + + /** + * @ignore + * @description 根据item的isExpanded属性获取item图标 + * @param node 当前节点数据 + */ + public getItemIcon(node: TiTreeNode): string { + if (TiTreeUtil.isLeaf(node)) { + return `${node.expandIcon} ti3-tree-node-icon`; + } + + return `${node.expanded ? node.expandIcon : node.collapseIcon} ti3-tree-node-icon`; + } + + /** + * @ignore + * @description 点击复选框触发select、change事件 + * 需要注意:click事件中拿到的是操作前的选中状态,而change事件中拿到的是操作后的选中状态 + * @param node 当前节点数据 + */ + public onInputChange(node: TiTreeNode, event: Event): void { + if (node.disabled === true) { + return; + } + // 点击当前项高亮 + this.actived = node; + if (this.checkRelation) { + this.setSeletedState(node, this.data, node.checked); + } + if (!this.changedByCheckbox) { + this.select.emit(node); + } + this.change.emit(node); + } + + /** + * @ignore + * 点击文本区域 + * @param node 当前节点数据 + * @param event 鼠标事件 + * @returns void + */ + public onItemWrapperClick(node: TiTreeNode): void { + if (node.disabled === true || node.editing) { + return; + } + // 悬浮功能: 点击生成节点时,显示操作按钮 + if (!node.editing) { + node.isHover = true; + } + // 点击当前项高亮 + this.actived = node; + // 1.处理多选情况 + if (this.multiple === true) { + if (this.changedByCheckbox) { + this.select.emit(node); + + return; + } + + // 1.1 处理父节点不支持多选 + if (!this.showCheckboxFn(node)) { + return; + } + + // 1.2 处理父节点支持多选 + node.checked = node.checked !== true; + // 处理当前节点选中状态变化后,对父子节点的影响 + if (this.checkRelation) { + this.setSeletedState(node, this.data, node.checked); + } + this.select.emit(node); + this.change.emit(node); + + return; + } + + // 2.处理单选场景 + // 2.1(单选且已选中)或者(单选且非叶子节点)的情况下,点击只会触发select事件,因为当前选中项不会发生变化 + if (node.checked === true || !TiTreeUtil.isLeaf(node)) { + // 触发select事件 + this.select.emit(node); + + return; + } + + // 2.2单选叶子节点未选中 + this.deSelectAllNode(this.data); + node.checked = true; + TiTreeUtil.selectParents(node, this.data, 'indeterminate'); // 设置祖先节点的选中状态 + + // 触发select和change事件 + this.select.emit(node); + + this.change.emit(node); + } + + // 处理当前节点选中状态变化后,对父子节点的影响 + private setSeletedState = (node: TiTreeNode, allData: Array, checked: boolean | string): void => { + if (checked === true) { + TiTreeUtil.selectAllChildren(node); + TiTreeUtil.selectParents(node, allData, true); + } else { + TiTreeUtil.deSelectAllChildren(node); + TiTreeUtil.deSelectParents(node, allData); + } + }; + + /** + * 单选时,取消所有节点的选中状态 + * @param allData 所有节点数据 + */ + private deSelectAllNode = (allData: Array): void => { + allData.forEach((node: TiTreeNode) => { + TiTreeUtil.deSelectAllChildren(node); + }); + }; + + /** + * @ignore + */ + public trackByFn(index: number, node: any): any { + return index; + } + /** + * @ignore + * 判断是否为叶子节点 + */ + public isLeaf(node: TiTreeNode): boolean { + return !Util.isArray(node.children); + } + + // 初始化选中项高亮 + private initActived = (data: Array): any => { + let result: TiTreeNode; + for (const node of data) { + if (node.checked === true) { + return node; + } + + if (!TiTreeUtil.isLeaf(node)) { + result = this.initActived(node.children); + } + + if (!Util.isUndefined(result)) { + return result; + } + } + + return result; + }; + /** + * @ignore + * @param node 节点数据 + */ + public onBlur(node: TiTreeNode): void { + if (node.focused) { + node.focused = false; // 失焦后节点删除获焦标志类 + } + } + /** + * @ignore + * @param node 节点数据 + */ + public onFocus(node: TiTreeNode): void { + if (!node.focused && !node.disabled) { + node.focused = true; // 设置获焦标志类 + } + } + /** + * @ignore + * @param event 键盘事件 + * @param node 当前节点数据 + */ + public onKeydown(event: KeyboardEvent, node: TiTreeNode): void { + /** + * 快捷键交互定义 + * 1,tab键切换焦点至树组件的文本区域,复选框无需提供焦点,文本样式为文本悬浮态 + * 2,树组件内部焦点切换支持上下左右键: + * -上下键:按上下键可以在并列的节点移动,如无并列节点无法移动。 + * -左右键:展开收起节点及跨层级移动焦点。当焦点再子节点时,按左键可收起改节点,焦点移动至父节点; + * 当焦点在父节点上时,按右键可将焦点移动到启子节点上,如节点未展开则同时展开该子节点 + * 3,Enter键或Space键:当焦点在节点名称上时,按下键则选中或取消该节点,选中后节点变为选中态 + */ + // 阻止默认和冒泡 + if ( + [ + TiKeymap.KEY_ENTER, + TiKeymap.KEY_NUMPAD_ENTER, + TiKeymap.KEY_SPACE, + TiKeymap.KEY_ARROW_UP, + TiKeymap.KEY_ARROW_DOWN, + TiKeymap.KEY_ARROW_LEFT, + TiKeymap.KEY_ARROW_RIGHT + ].indexOf(event.keyCode) > -1 + ) { + event.preventDefault(); + event.stopPropagation(); + } + switch (event.keyCode) { + case TiKeymap.KEY_ENTER: // ENTER键 + case TiKeymap.KEY_NUMPAD_ENTER: // ENTER键(苹果数字小键盘) + case TiKeymap.KEY_SPACE: + this.onItemWrapperClick(node); + break; + case TiKeymap.KEY_ARROW_UP: // 向上箭头,同级 + this.setKeyDownUp(node, 'up'); + break; + case TiKeymap.KEY_ARROW_DOWN: // 向下箭头,同级 + this.setKeyDownUp(node, 'down'); + break; + case TiKeymap.KEY_ARROW_LEFT: // 向左箭头,跨节点层级 + this.setKeyleft(node); + break; + case TiKeymap.KEY_ARROW_RIGHT: // 向右箭头,跨节点层级 + this.setKeyright(node); + break; + default: + break; + } + } + + // 上下键 + private setKeyDownUp(node: TiTreeNode, type: string): void { + let parArr: Array; + const pareNode: TiTreeNode = TiTreeUtil.getParentNode(this.data, node); // 获取父节点 + if (pareNode) { + parArr = pareNode.children; + } else { + parArr = this.data; + } + const curIndex: number = parArr.findIndex((item: TiTreeNode) => item === node); // 获取当前节点在其父节点集合的index + this.setFocusNode(node, parArr, curIndex, type); + } + // 左键 + private setKeyleft(node: TiTreeNode): void { + // 当正在编辑时,关闭其父节点时,节点恢复文本状态 + if (node.editing && node.label !== '') { + delete node.editing; + } + const pareNode: TiTreeNode = TiTreeUtil.getParentNode(this.data, node); + if (!pareNode) { + // 当前节点时一级节点时 return + return; + } + pareNode.expanded = false; + this.collapse.emit(node); + if (pareNode.disabled) { + this.setKeyleft(pareNode); + + return; + } + pareNode.focused = true; + this.setFocusElem(node); + } + // 右键 + private setKeyright(node: TiTreeNode): void { + if (TiTreeUtil.isLeaf(node)) { + return; + } + const childNodeArr: Array = node.children; + const childnoDisabledArr: Array = childNodeArr.filter((childNode: TiTreeNode) => !childNode.disabled); // 获取子节点未禁用的节点集合 + if (childnoDisabledArr.length < 1) { + return; + } + const childFirstNode: TiTreeNode = childnoDisabledArr[0]; // 未禁用的节点集合的first获取焦点 + childFirstNode.focused = true; + if (!node.expanded) { + node.expanded = true; + this.expand.emit(node); + } + this.setFocusElem(node); + } + // 上下键焦点处理 + private setFocusNode(node: TiTreeNode, parList: Array, curIndex: number, type: string): void { + let targetNode: TiTreeNode; + let targetIndex: number; + let _curIndex: number = curIndex; + if (type === 'up') { + // 同级获取上一个节点 + targetIndex = _curIndex - 1; + if (targetIndex < 0) { + targetIndex = parList.length - 1; + } + targetNode = parList[targetIndex]; + } else { + // 同级获取下一个节点 + targetIndex = _curIndex + 1; + if (targetIndex > parList.length - 1) { + targetIndex = 0; + } + targetNode = parList[targetIndex]; + } + // 上下移动时,first或last节点禁用时,return + if ((targetNode === parList[0] || targetNode === parList[parList.length - 1]) && targetNode.disabled) { + return; + } + // 当中间节点有禁用时,移动到禁用的节点上一个或下一个节点 + if (targetNode.disabled) { + _curIndex = targetIndex; + this.setFocusNode(node, parList, _curIndex, type); + + return; + } + targetNode.focused = true; + this.setFocusElem(node); + } + + // 目标元素获取焦点 + private setFocusElem(node: TiTreeNode): void { + if (node.focused) { + node.focused = false; + } + // 当按键操作会改变文本元素是否添加class`ti3-tree-text-focus`, 加延时是为了class变更后,获取正确的含有class的文本区域元素 + // 类ti3-tree-text-focus作为获焦元素的标志 + setTimeout(() => { + // 找到含有类ti3-tree-text-focus的文本元素 + const focusNode: ElementRef = this.elems.toArray().find((elem: ElementRef) => { + return this.tiRenderer.hasClass(elem.nativeElement, 'ti3-tree-text-focus'); + }); + if (focusNode) { + focusNode.nativeElement.focus(); + } + }, 0); + } + + /** + * @ignore + * @param node 当前节点数据 + * @description 节点是否可拖拽 + */ + public isDraggable(node: TiTreeNode): boolean { + return !node.disabled && (node.draggable || (this.nodeDraggable && node.draggable !== false)); + } + + /** + * @ignore + * @param startEvent 鼠标事件 + * @param node 当前节点数据 + * @description 拖拽开始时在被拖拽元素上触发此事件 + */ + public onDragstart(event: DragEvent, node: TiTreeNode): void { + // 当节点的draggable属性设置为false表示不可拖拽,但是选中文本时会触发dragstart事件,故阻止默认行为 + if (!this.isDraggable(node)) { + event.preventDefault(); + + return; + } + + event.stopPropagation(); + this.dragNode = node; // 保存被拖拽的节点 + } + + /** + * @ignore + * @param enterEvent 鼠标事件 + * @param node 当前节点数据 + * @description 拖拽鼠标进入元素时在该元素上触发 + */ + public onDragenter(event: DragEvent, node: TiTreeNode): void { + event.preventDefault(); + this.overNode = null; + + // 拖拽节点和目标节点是同一节点,retuen + if (node === this.dragNode) { + return; + } + + // 保存移动到的节点 + this.overNode = node; + + // 拖拽的节点拖拽到其子节点时,关闭其节点 + if (this.dragNode === TiTreeUtil.getParentNode(this.data, node)) { + this.dragNode.expanded = false; + this.collapse.emit(node); + } + } + + /** + * @ignore + * @param overEvent 鼠标事件 + * @param node 当前节点数据 + * @description 拖拽时鼠标在目标元素上移动时触发 + */ + public onDragover(event: DragEvent, node: TiTreeNode): void { + event.preventDefault(); + event.dataTransfer.effectAllowed = 'move'; + if (node === this.overNode) { + this.clearDragClass(event); // 清除节点之前的样式 + this.dropPosition = this.calcDropPosition(event); // 获取移动到节点的位置 + // 拖拽经过父节点,放置内部位置时,且该节点未展开时,展开其节点 + if (this.dropPosition === 0 && !node.expanded && node.children && node.children.length > 1) { + node.expanded = true; + this.expand.emit(node); + } + // 设置拖动过程中的状态样式 + this.renderer2.addClass(event.currentTarget, this.setDragOverClass(node)); + } + } + + /** + * @ignore + * @param leaveEvent 鼠标事件 + * @param node 当前节点数据 + * @description 拖拽时鼠标在离开目标元素时触发 + */ + public onDragleave(event: DragEvent, node: TiTreeNode): void { + event.preventDefault(); + this.clearDragClass(event); + } + + /** + * @ignore + * @param dropEvent 鼠标事件 + * @param node 当前节点数据 + * @description 鼠标在拖放目标上释放时,在拖放目标上触发 + */ + public onDrop(event: DragEvent, node: TiTreeNode): void { + event.preventDefault(); + this.dragNode.focused = false; + this.clearDragClass(event); + // 异步加载节点时禁止放置到节点内部 + if (this.dropPosition === 0 && node.loadStatus === 'loading') { + return; + } + // 拖拽节点和目标节点是同一节点,retuen + if (node === this.dragNode) { + return; + } + const params: TiTreeDragNode = { + event, + targetNode: node, + dragNode: this.dragNode, + dropPosition: this.dropPosition + }; + + if (this.beforeDrop.observers.length > 0) { + this.beforeDrop.emit(params); + + return; + } + + TiTreeUtil.dropApply(params, this.data); + + // 拖放节点是父节点且是收起状态时,展开该节点 + if (node.children && node.children.length > 0 && !node.expanded) { + node.expanded = true; + this.expand.emit(node); + } + this.nodeDrop.emit(params); + } + + /** + * @ignore + * @param endEvent 鼠标事件 + * @param node 当前节点数据 + * @description 鼠标在拖放目标上释放时,在拖拽元素上触发 + */ + public onDragend(event: DragEvent, node: TiTreeNode): void { + event.preventDefault(); + this.dragNode = null; + this.overNode = null; + } + + // 计算拖拽节点的放置方式0(作为目标节点的子节点),-1(放置在目标节点的前面),1(放置在目标节点的后面) + private calcDropPosition(event: DragEvent): number { + const clientY: number = event.clientY; + const { top, height } = (event.target as HTMLElement).getBoundingClientRect(); + const gapHeight: number = height / 3; + if (clientY > top + height - gapHeight) { + return 1; + } + if (clientY < top + gapHeight) { + return -1; + } + + return 0; + } + + // 清除拖拽样式 + private clearDragClass(event: DragEvent): void { + // 拖拽过程中的三种状态 + const dragClasses: Array = ['ti3-tree-drag-over-inner', 'ti3-tree-drag-over-top', 'ti3-tree-drag-over-bottom']; + dragClasses.forEach((item: string) => { + this.renderer2.removeClass(event.currentTarget, item); + }); + } + + // 根据拖动的位置获取相应的样式 + private setDragOverClass(node: TiTreeNode): string { + if (node !== this.overNode) { + return; + } + if (this.dropPosition === 0) { + return 'ti3-tree-drag-over-inner'; + } else if (this.dropPosition === -1) { + return 'ti3-tree-drag-over-top'; + } else if (this.dropPosition === 1) { + return 'ti3-tree-drag-over-bottom'; + } + + return ''; + } + + /** + * @ignore + * @param node 节点 + * @param type hover标志 + */ + public onMousenode(node: TiTreeNode, type: string): void { + // 鼠标进入节点且不是编辑状态时,显示操作按钮 + if (type === 'enter' && !node.editing) { + node.isHover = true; + } else { + delete node.isHover; + } + } + + /** + * @ignore + * @param node 节点 + */ + public onBlurEdit(node: TiTreeNode): void { + if (node.label === '') { + return; + } + + delete node.editing; + + if (this.addingNode) { + this.addingNode = false; + this.afterNodeAdd.emit(node); + + return; + } + + this.afterNodeEdit.emit(node); + } + + /** + * @ignore + * @param node 节点 + */ + public onKeydownEdit(event: KeyboardEvent): void { + // 当中文输入法成对输入符号(中括号 大括号),默认会触发左键将光标移动到前后符号中间,会触发父节点的左键事件,导致父节点收起 + event.stopPropagation(); + } + + /** + * @ignore + * @param deleteEvent 删除事件 + * @param node 节点 + */ + public deleteNode(event: Event, node: TiTreeNode): void { + // 阻止事件冒泡 防止触发选中 + event.stopPropagation(); + const parentNode: TiTreeNode = TiTreeUtil.getParentNode(this.data, node); + TiTreeUtil.removeNode(this.data, node); + // 节点的父节点无子节点时, 删除children属性,解决依旧会显示展开图标问题 + if (parentNode && parentNode.children.length < 1) { + delete parentNode.children; + } + // 更新多选状态 + if (this.multiple) { + TiTreeUtil.updateChecked(this.data); + } + this.nodeDeleted.emit(node); + } + + /** + * @ignore + * @param editEvent 编辑事件 + * @param node 节点 + */ + public editNode(event: Event, node: TiTreeNode): void { + // 阻止事件冒泡 防止触发选中 + event.stopPropagation(); + node.editing = true; + delete node.isHover; + this.nodeEdited.emit(node); + // autofocus视图变化时 会报错 视图变化时 强制变检一次,消除报错(Expression has changed after it was checked) + this.cdRef.detectChanges(); + } + + /** + * @ignore + * @param addEvent 增加事件 + * @param node 节点 + */ + public addNode(event: Event, node: TiTreeNode): void { + // 阻止事件冒泡 防止触发选中 + event.stopPropagation(); + this.addingNode = true; + const newNode: TiTreeNode = { + label: this.treeLan.newNode, + editing: true, + parent: node + }; + node.expanded = true; + this.expand.emit(node); + this.nodeAdded.emit(newNode); + TiTreeUtil.addNode(this.data, [newNode], 0, node); + // 更新多选状态 + if (this.multiple) { + TiTreeUtil.updateChecked(this.data); + } + // autofocus视图变化时 会报错 强制变检一次,消除报错(Expression has changed after it was checked) + this.cdRef.detectChanges(); + } + /** + * @ignore + * @param node 节点 + */ + public onClickMore(node: TiTreeNode): void { + this.beforeMore.emit(node); + } + + /** + * @ignore + * 开启虚拟滚动时需要对节点数据做扁平化处理 + * @param nodes 同层级且隶属同一父节点的节点集合 + * @param level 节点层级,根节点层级为0,往下依次类推 + */ + public getFlatData(nodes: Array, level?: number): Array { + let result: Array = []; + if (!nodes) { + return result; + } + nodes.forEach((item: TiTreeNode, index: number): void => { + item.level = level ? level : 0; + item.isLast = index === nodes.length - 1; + item.isFirst = index === 0; + result.push(item); + if (item.expanded && item.children && item.children.length > 0) { + result = result.concat(this.getFlatData(item.children, item.level + 1)); + } + }); + + return result; + } + + /** + * @ignore + * 虚拟滚动扁平化结构时,获取不同层级节点左侧的缩进 + * @param node 节点 + */ + public getNodeLeftSpace(node: TiTreeNode): string { + const space: number = this.isSmall ? TiTreeComponent.SMALL_ITEM_LEFT_SPACE : TiTreeComponent.DEFAULT_ITEM_LEFT_SPACE; + + return `${node.level * space}px`; + } + /** + * @ignore + * 是否为根节点 + * @param node 节点 + */ + public isRootNode(node: TiTreeNode): boolean { + return this.data.includes(node); + } + /** + * @ignore + * @param level 节点层级 + */ + public getVerticalGuideLines(level: number): Array { + return new Array(level); + } + + /** + * @ignore + * @param level 节点层级 + */ + public getVerticalGuideLineLeft(level: number): string { + const size: number = this.isSmall ? TiTreeComponent.SMALL_ITEM_LEFT_SPACE : TiTreeComponent.DEFAULT_ITEM_LEFT_SPACE; + // 其中 6 和 8 是 展开/收起图标宽度的一半 + const offset: number = this.isSmall ? 6 : 8; + + return `${(level * size + offset) * -1}px`; + } +} diff --git a/src/tree/lib/src/TiTreeModule.ts b/src/tree/lib/src/TiTreeModule.ts new file mode 100644 index 0000000..0beb93e --- /dev/null +++ b/src/tree/lib/src/TiTreeModule.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ScrollingModule } from '@angular/cdk/scrolling'; +import { TiTreeComponent } from './TiTreeComponent'; +import { TiCheckboxModule } from '@opentiny/ng-checkbox'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiLoadingModule } from '@opentiny/ng-loading'; +import { TiHighlightPipe } from './TiHighlightPipe'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiValidationModule } from '@opentiny/ng-validation'; +import { TiAutoSelectDirective } from './TiAutoSelectDirective'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ScrollingModule, + TiCheckboxModule, + TiIconModule, + FormsModule, + TiLoadingModule, + TiRendererModule, + TiTextModule, + TiValidationModule + ], + exports: [TiTreeComponent], + declarations: [TiTreeComponent, TiHighlightPipe, TiAutoSelectDirective] +}) +export class TiTreeModule { + constructor() { + TiLocale.setTiWords(locales); + } +} + +export * from './TiTreeComponent'; +export { TiTreeComponent, TiTreeNode, TiTreeDragNode } from './TiTreeComponent'; +export { TiTreeUtil } from './TiTreeUtil'; diff --git a/src/tree/lib/src/TiTreeUtil.ts b/src/tree/lib/src/TiTreeUtil.ts new file mode 100644 index 0000000..a6c5df6 --- /dev/null +++ b/src/tree/lib/src/TiTreeUtil.ts @@ -0,0 +1,620 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Util } from '@opentiny/ng-utils'; +import { TiTreeNode, TiTreeDragNode } from './TiTreeComponent'; +/** + * + * 用于封装给树组件 [TiTreeComponent]{@link ../components/TiTreeComponent.html} + * 提供公共方法,包括增、删、改、查、选中、取消选中、遍历、筛选、获取选中项等操作 + */ +export class TiTreeUtil { + private static selectedData: Array = []; + private static pnode: Array = []; + /** + * 查找指定节点的父节点 + * @param data 所有节点数据 + * @param node 节点 + */ + public static getParentNode(data: Array, node: TiTreeNode): TiTreeNode { + if (!Array.isArray(data)) { + return; + } + + for (const item of data) { + if (!TiTreeUtil.isLeaf(item)) { + if (item.children.indexOf(node) !== -1) { + return item; + } else { + const result: TiTreeNode = TiTreeUtil.getParentNode(item.children, node); + if (result) { + return result; + } + } + } + } + } + + /** + * 对整个树的每个节点执行一次给定的函数 + * @param data 所有节点数据 + * @param traverseFn 对各个节点执行的操作 + */ + public static traverse(data: Array, traverseFn: Function): void { + if (!Util.isFunction(traverseFn) || !Array.isArray(data)) { + return; + } + + for (const item of data) { + traverseFn(item); + if (!TiTreeUtil.isLeaf(item)) { + TiTreeUtil.traverse(item.children, traverseFn); + } + } + } + + /** + * 更新多选树各节点的选中状态;主要用于手动添加或删除节点时,当前节点的兄弟节点和父节点的选中状态不一致。 + * @param data 所有节点数据 + */ + public static updateChecked(data: Array): void { + TiTreeUtil.check(data); + } + + /** + * 检查当前数据的多选状态 + * @param data 当前节点集合 + * @param allData 全部数据集合 + */ + private static check(data: Array): void { + for (const item of data) { + // 非叶子节点深度优先递归,再回溯当前节点 + if (!TiTreeUtil.isLeaf(item)) { + TiTreeUtil.check(item.children); + // 根据子节点计算当前节点选中状态 + item.checked = TiTreeUtil.computeChecked(item); + } + } + } + + /** + * 当前节点是否是叶子节点的父节点, + * 因为更新节点的选中状态时,根据叶子节点的选中状态计算出父节点的选中状态,再更新父节点的选中状态。 + * @param node 当前节点 + */ + private static isLeafsParentNode(node: TiTreeNode): boolean { + if (!Array.isArray(node.children)) { + return false; + } else { + for (const child of node.children) { + if (Array.isArray(child.children)) { + return false; + } + } + + return true; + } + } + + /** + * 根据子节点的多选状态,计算出当前节点的多选状态 + * @param node 当前节点 + */ + private static computeChecked(node: TiTreeNode): boolean | string { + const childrens: Array = node.children; + if (!childrens?.length) { + return node?.checked; + } + let selectedNum: number = 0; + let unSelectedNum: number = 0; + for (const child of childrens) { + if (child.checked === true) { + selectedNum++; + } else if (!child.checked) { + // false和undefined 两种 + unSelectedNum++; + } + } + if (selectedNum === childrens.length) { + return true; + } else if (unSelectedNum === childrens.length) { + return false; + } else { + return 'indeterminate'; + } + } + + /** + * 添加节点 + * @param data 所有节点数据 + * @param node 添加的一个或多个节点 + * @param index 添加的位置: -1表示从尾部追加 + * @param pNode 指定要添加的节点的父节点,没有指定父节点时默认添加到根节点 + */ + public static addNode(data: Array, node: Array, index: any, pNode?: TiTreeNode): void { + const k: number = parseInt(index, 10); + if (isNaN(k) || !Array.isArray(data)) { + return; + } + + const nodes: Array = Array.isArray(node) ? node : [node]; + // 当没有指定父节点时,默认将节点添加到根节点 + if (!pNode) { + const resuleIndex: any = index === -1 ? data.length : index; + for (let i: number = 0; i < nodes.length; i++) { + data.splice(resuleIndex + i, 0, nodes[i]); + } + + return; + } + + // 如果父节点是叶子节点时,给叶子节点添加children数组 + if (TiTreeUtil.isLeaf(pNode)) { + pNode.children = []; + } + + // 根据Index值,将node插入指定位置 + if (index === -1) { + pNode.children = pNode.children.concat(nodes); // 返回值为新数组 + } else { + for (let j: number = 0; j < nodes.length; j++) { + pNode.children.splice(index + j, 0, nodes[j]); + } + } + } + + /** + * 删除指定节点 + * @param data 所有节点数据 + * @param node 节点 + */ + public static removeNode(data: Array, node: TiTreeNode): void { + if (!Array.isArray(data)) { + return; + } + + // 找到不匹配搜索内容的叶子节点并删除。为了避免要删除的元素在数组中的索引改变,从后向前循环 + for (let i: number = data.length - 1; i >= 0; i--) { + if (data[i] === node) { + TiTreeUtil.deleteArr(data, i); + + return; + } + + if (!TiTreeUtil.isLeaf(data[i])) { + TiTreeUtil.removeNode(data[i].children, node); + } + } + } + // 3.0.3 selectNode/unSelectNode第三个参数:是否单选, 改为是否多选。已确认无人使用。 + // TODO:这两个函数的重复代码,可以合并 + /** + * 选中指定节点 + * @param data 所有节点数据 + * @param node 节点 + * @param multiple 是否是多选树 + */ + public static selectNode(data: Array, node: TiTreeNode, multiple: boolean): void { + if (!Array.isArray(data)) { + return; + } + if (!multiple) { + // 单选模式 + TiTreeUtil.traverse(data, (travNode: TiTreeNode): void => { + travNode.checked = false; // 清空所有的选中项 + }); + node.checked = true; // 将当前项置为选中 + } else { + // 多选 + TiTreeUtil.selectAllChildren(node); // 先置当前节点选中,如果当前节点是非叶子节点 递归让其子节点全部选中 + } + + TiTreeUtil.selectParents(node, data, true); // 设置父节点选中情况 + } + // TODO:Tiny4 deselect改为unselect + /** + * 取消选中指定节点 + * @param data 所有节点数据 + * @param node 节点 + * @param multiple 是否是多选树 + */ + public static deSelectNode(data: Array, node: TiTreeNode, multiple: boolean): void { + if (!Array.isArray(data)) { + return; + } + if (!multiple) { + TiTreeUtil.traverse(data, (travNode: TiTreeNode): void => { + travNode.checked = false; // 清空所有的选中项 + }); + } else { + TiTreeUtil.deSelectAllChildren(node); + TiTreeUtil.deSelectParents(node, data); + } + } + + /** + * 展开指定节点的祖先节点 + * @param data 所有节点数据 + * @param node 节点 + */ + public static expandNode(data: Array, node: TiTreeNode): boolean { + if (!Array.isArray(data)) { + return; + } + + let num: number = 0; + let result: boolean = false; + for (const item of data) { + if (item === node) { + num++; + } else if (!TiTreeUtil.isLeaf(item)) { + result = TiTreeUtil.expandNode(item.children, node); + if (result) { + item.expanded = true; + num++; + } + } + } + + return num > 0; + } + + /** + * 筛选匹配的节点 + * @param data 所有节点数据 + * @param matchFn 用户传入匹配节点的回调 + * @returns 是否找到 + */ + public static search(data: Array, matchFn: (node: TiTreeNode) => boolean): boolean { + if (!Util.isFunction(matchFn) || !Array.isArray(data)) { + return false; + } + + let findNum: number = 0; + let isFind: boolean = false; + // 找到不匹配搜索内容的叶子节点并删除。为了避免要删除的元素在数组中的索引改变,从后向前循环 + for (let i: number = data.length - 1; i >= 0; i--) { + if (!matchFn(data[i]) && !TiTreeUtil.isLeaf(data[i])) { + // 未匹配到且为父节点 + isFind = TiTreeUtil.search(data[i].children, matchFn); + if (isFind) { + findNum++; + } else { + TiTreeUtil.deleteArr(data, i); + } + } else if (matchFn(data[i])) { + // 匹配到 + findNum++; + } else { + // 未匹配到且为叶子节点 + TiTreeUtil.deleteArr(data, i); + } + } + + return findNum > 0; + } + + /** + * 获取当前选中节点 + * @param data 所有节点数据 + * @param onlySelectLeaf 选中项中是否只包含叶子节点 + * @param multiple 是否是多选树 + * @param checkRelation 父子节点选中状态是否关联 + */ + public static getSelectedData( + data: Array, + onlySelectLeaf: boolean, + multiple: boolean, + checkRelation?: boolean + ): Array { + // 每次遍历之前需要清空当前选中项列表 + TiTreeUtil.selectedData = []; + TiTreeUtil.querySelectedNode(data, onlySelectLeaf, multiple, checkRelation); + + return TiTreeUtil.selectedData; + } + + /** + * @ignore 获取当前选中节点 + * @description 作为treeselect 组件单独使用内部方法,规避回显时和选中不一致问题。 + * @param data 所有节点数据 + * @param onlySelectLeaf 选中项中是否只包含叶子节点 + * @param multiple 是否是多选树 + * @param checkRelation 父子节点选中状态是否关联 + */ + public static getTreeSelectedData( + data: Array, + onlySelectLeaf: boolean, + multiple: boolean, + checkRelation?: boolean + ): Array { + // 每次遍历之前需要清空当前选中项列表 + TiTreeUtil.selectedData = []; + TiTreeUtil.querySelectedNode(data, onlySelectLeaf, multiple, checkRelation, true); + + return TiTreeUtil.selectedData; + } + + /** + * @ignore + * @description 根据整棵树的节点数据,查询所有选中项,并更新selectedData + * @param data 全部节点数据 + * @param onlySelectLeaf 选中项中是否只包含叶子节点 + * @param multiple 是否多选模式 + * @param checkRelation 父子节点选中状态是否关联 + * @param isTreeselectEcho 是否是下拉树回显 多选情况下 + */ + public static querySelectedNode( + data: Array, + onlySelectLeaf: boolean, + multiple: boolean, + checkRelation?: boolean, + isTreeselectEcho?: boolean + ): void { + let tempNode: any; + const relation: boolean = checkRelation !== false; + for (let i: number = 0; i < data.length; i++) { + tempNode = data[i]; + + //多选下拉 父节点为选中状态时 + if (multiple && !TiTreeUtil.isLeaf(tempNode) && isTreeselectEcho) { + if (tempNode.checked === true) { + TiTreeUtil.selectedData.push(tempNode); + continue; + } + } + + // 多选树,父节点且父子选中关系不关联 + if (multiple && !relation && !TiTreeUtil.isLeaf(tempNode)) { + if (tempNode.checked === true) { + TiTreeUtil.selectedData.push(tempNode); + } + + TiTreeUtil.querySelectedNode(tempNode.children, onlySelectLeaf, multiple, relation, isTreeselectEcho); + } else if (TiTreeUtil.checkedParentNode(tempNode)) { + TiTreeUtil.pnode.push(tempNode); + TiTreeUtil.querySelectedNode(tempNode.children, onlySelectLeaf, multiple, relation, isTreeselectEcho); + + // 单选情况下仅查找到第一个选中的叶子节点即可 + if (!multiple) { + break; + } + } + + // 叶子节点选中 + if (TiTreeUtil.checkedLeafNode(tempNode)) { + TiTreeUtil.selectedData.push(tempNode); + + if (!onlySelectLeaf && (!multiple || (multiple && relation))) { + tempNode.parent = TiTreeUtil.pnode.concat(); + // 单选情况下仅查找到第一个选中的叶子节点即可 + if (!multiple) { + TiTreeUtil.pnode.splice(0, TiTreeUtil.pnode.length); + break; + } + } + } + + // 遍历到最后一项时将父节点从pnode中移除 + if (i === data.length - 1) { + TiTreeUtil.pnode.pop(); + } + } + } + + /** + * @ignore + * @description 判断节点node是否是一个处于选中或半选状态的父节点 + * @param node 节点数据 + */ + public static checkedParentNode(node: TiTreeNode): boolean { + return (node.checked === true || node.checked === 'indeterminate') && !TiTreeUtil.isLeaf(node); + } + + /** + * @ignore + * @description 判断节点node是否是一个处于选中状态的叶子节点 + * @param node 节点数据 + */ + public static checkedLeafNode(node: TiTreeNode): boolean { + return node.checked === true && TiTreeUtil.isLeaf(node); + } + + /** + * @ignore + * 判断是否为叶子节点 + */ + public static isLeaf(item: TiTreeNode): boolean { + return !Util.isArray(item.children); + } + + /** + * @ignore + * 从数组arr中删除下标为index的节点 + */ + public static deleteArr(arr: Array, index: number): void { + arr.splice(index, 1); + } + // TODO: checked传入false时,与deSelectParents有什么区别?可以合并么? + /** + * @ignore + * 当子节点选中时,设置祖先元素的选中状态 + * @param item 子节点的数据 + * @param allData 全部节点数据 + * @param checked 取值为:true/false/'indeterminate' + */ + public static selectParents(item: TiTreeNode, allData: Array, checked: boolean | string): void { + const pNode: TiTreeNode = TiTreeUtil.getParentNode(allData, item); + if (Util.isUndefined(pNode)) { + return; + } + + // 当子元素为半选时,祖先元素一律设置为半选状态 + if (checked === 'indeterminate') { + pNode.checked = 'indeterminate'; + TiTreeUtil.selectParents(pNode, allData, 'indeterminate'); + + return; + } + + const childrens: Array = pNode.children; + let selectedNum: number = 0; + for (const child of childrens) { + if (child.checked === true) { + selectedNum++; + } + } + + if (selectedNum === childrens.length) { + pNode.checked = true; + TiTreeUtil.selectParents(pNode, allData, true); + } else { + pNode.checked = 'indeterminate'; + TiTreeUtil.selectParents(pNode, allData, 'indeterminate'); + } + } + + /** + * @ignore + * 根据父节点选择子节点 + */ + public static selectAllChildren(item: TiTreeNode): void { + // 如果子节点是禁用状态不做处理; + if (item.disabled !== true) { + item.checked = true; + } + + if (!TiTreeUtil.isLeaf(item)) { + item.children.forEach((child: TiTreeNode) => { + TiTreeUtil.selectAllChildren(child); + }); + } + } + + // TODO:Tiny4 deselect改为unselect + /** + * @ignore + * 父节点取消选中,置子节点都为取消选中状态 + * @param item 子节点的数据 + */ + public static deSelectAllChildren(item: TiTreeNode): void { + item.checked = false; + + if (Util.isArray(item.children) && item.children.length > 0) { + item.children.forEach((child: TiTreeNode) => { + TiTreeUtil.deSelectAllChildren(child); + }); + } + } + + // TODO:Tiny4 deselect改为unselect + /** + * @ignore + * 当前节点取消选中时,设置祖先元素的选中状态 + * @param item 当前节点的数据 + * @param allData 全部的节点数据 + */ + public static deSelectParents(item: TiTreeNode, allData: Array): void { + const pNode: TiTreeNode = TiTreeUtil.getParentNode(allData, item); + if (Util.isUndefined(pNode)) { + return; + } + + const childrens: Array = pNode.children; + let selectedNum: number = 0; + for (const child of childrens) { + if (child.checked === true || child.checked === 'indeterminate') { + selectedNum++; + } + } + + if (selectedNum === 0) { + pNode.checked = false; + TiTreeUtil.deSelectParents(pNode, allData); + } else { + pNode.checked = 'indeterminate'; + TiTreeUtil.selectParents(pNode, allData, 'indeterminate'); + } + } + /** + * @ignore + * @param event 拖拽数据 + * @param data 所用节点数据 + */ + public static dropApply(event: TiTreeDragNode, data: Array): void { + if (!Array.isArray(data)) { + return; + } + const dragNode: TiTreeNode = event.dragNode; // 拖拽节点 + const dragParentNode: TiTreeNode = TiTreeUtil.getParentNode(data, dragNode); // 拖拽节点的父节点 + const dropNode: TiTreeNode = event.targetNode; // 拖放节点 + const dropParentNode: TiTreeNode = TiTreeUtil.getParentNode(data, dropNode); // 拖放节点的父节点 + const dropPosition: number = event.dropPosition; // 拖拽位置 + TiTreeUtil.removeNode(data, dragNode); + // 拖拽父节点无子节点时,删除children属性,此处是为解决当children为空时,展开收起图标还会存在的问题 + if (dragParentNode?.children.length < 1) { + delete dragParentNode.children; + } + if (dropPosition === 0) { + TiTreeUtil.addNode(data, [dragNode], -1, dropNode); + } else { + let index: number; + if (!dropParentNode) { + // 拖放节点为顶级级节点时 + index = data.indexOf(dropNode); + if (dropPosition === -1) { + data.splice(index, 0, dragNode); + } else { + data.splice(index + 1, 0, dragNode); + } + } else { + index = dropParentNode.children.indexOf(dropNode); + if (dropPosition === -1) { + TiTreeUtil.addNode(data, [dragNode], index, dropParentNode); + } else { + TiTreeUtil.addNode(data, [dragNode], index + 1, dropParentNode); + } + } + } + // 更新选中状态 + TiTreeUtil.updateChecked(data); + } + + /** + * 主干深拷贝,叶子浅拷贝 + * 因为搜索结果里,叶子节点的勾选效果,需要保留在原始数据。 + * @param data 节点数据 + * @returns 复制后的数据 + */ + /** + * 拷贝节点数据,对非叶子节点深拷贝,叶子节点浅拷贝 + * @param data 节点数据 + * @returns 拷贝后的数据 + */ + public static copy(data: any): Array { + // 叶子节点 或 children 为 [] 也需要进行浅拷贝 + if ( + typeof data !== 'object' || + data === null || + (data.label !== undefined && (TiTreeUtil.isLeaf(data) || (data.children && data.children.length === 0))) + ) { + return data; + } + + const clone: any = Array.isArray(data) ? data.slice() : { ...data }; + const keys: Array = Object.keys(clone); + for (const key of keys) { + clone[key] = TiTreeUtil.copy(clone[key]); + } + + return clone; + } +} diff --git a/src/tree/lib/src/i18n/TiTreeWords.ts b/src/tree/lib/src/i18n/TiTreeWords.ts new file mode 100644 index 0000000..0788815 --- /dev/null +++ b/src/tree/lib/src/i18n/TiTreeWords.ts @@ -0,0 +1,9 @@ +export interface TiTreeWords { + tiTree: { + newNode: string; + create: string; + edit: string; + delete: string; + more: string; + }; +} diff --git a/src/tree/lib/src/i18n/en_US.ts b/src/tree/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..e7b9d5f --- /dev/null +++ b/src/tree/lib/src/i18n/en_US.ts @@ -0,0 +1,11 @@ +import { TiTreeWords } from './TiTreeWords'; + +export const en_US: TiTreeWords = { + tiTree: { + newNode: 'New node ', + create: 'Create', + edit: 'Edit', + delete: 'Delete', + more: 'More' + } +}; diff --git a/src/tree/lib/src/i18n/es_US.ts b/src/tree/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..2f487e7 --- /dev/null +++ b/src/tree/lib/src/i18n/es_US.ts @@ -0,0 +1,11 @@ +import { TiTreeWords } from './TiTreeWords'; + +export const es_US: TiTreeWords = { + tiTree: { + newNode: 'Nodo nuevo ', + create: 'Crear', + edit: 'Editar', + delete: 'Eliminar', + more: 'Más' + } +}; diff --git a/src/tree/lib/src/i18n/fr_FR.ts b/src/tree/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..11382bc --- /dev/null +++ b/src/tree/lib/src/i18n/fr_FR.ts @@ -0,0 +1,11 @@ +import { TiTreeWords } from './TiTreeWords'; + +export const fr_FR: TiTreeWords = { + tiTree: { + newNode: 'Nouveau nœud ', + create: 'Créer', + edit: 'Editer', + delete: 'Supprimer', + more: 'Plus' + } +}; diff --git a/src/tree/lib/src/i18n/index.ts b/src/tree/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/tree/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/tree/lib/src/i18n/pt_BR.ts b/src/tree/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..f1ef7d3 --- /dev/null +++ b/src/tree/lib/src/i18n/pt_BR.ts @@ -0,0 +1,11 @@ +import { TiTreeWords } from './TiTreeWords'; + +export const pt_BR: TiTreeWords = { + tiTree: { + newNode: 'Novo nó ', + create: 'Criar', + edit: 'Editar', + delete: 'Excluir', + more: 'Mais' + } +}; diff --git a/src/tree/lib/src/i18n/zh_CN.ts b/src/tree/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..aa763dd --- /dev/null +++ b/src/tree/lib/src/i18n/zh_CN.ts @@ -0,0 +1,11 @@ +import { TiTreeWords } from './TiTreeWords'; + +export const zh_CN: TiTreeWords = { + tiTree: { + newNode: '新节点', + create: '新增', + edit: '编辑', + delete: '删除', + more: '更多' + } +}; diff --git a/src/tree/lib/src/tree.html b/src/tree/lib/src/tree.html new file mode 100644 index 0000000..07c77f2 --- /dev/null +++ b/src/tree/lib/src/tree.html @@ -0,0 +1,293 @@ + + +
      + +
    • + +
    • +
      +
    +
    +
    + +
      +
    • + +
    • +
    +
    + + +
    + + + + + +
    + + + + + + + + + + + + + + + +
    +
    +
      +
    • + +
    • + + + + + +
    • +
      + + +
      +
    • +
      +
    +
    + +
  • + +
    + + +
    {{treeLan.more}}
    +
    + + + +
    +
  • +
    + + +
    + + + + + + +
    + + + + + + + + + + + + + + + +
    +
    +
    diff --git a/src/tree/lib/src/tree.less b/src/tree/lib/src/tree.less new file mode 100644 index 0000000..298d0b2 --- /dev/null +++ b/src/tree/lib/src/tree.less @@ -0,0 +1,360 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-tree-node-icon-size: var(--ti-common-size-4x); + --ti-tree-item-height: 30px; // 节点的高度 + --ti-tree-square-icon-size: var(--ti-common-size-4x); // 展开收起图标大小 + --ti-tree-icon-right-space: var(--ti-common-space-2x); // 展开收起图标、复选框右侧间距 + --ti-tree-node-icon-space: calc( + var(--ti-tree-square-icon-size) + var(--ti-tree-node-icon-size) + var(--ti-common-space-base) * 2 + var(--ti-tree-icon-right-space) * 2 + ); // 展开收起图标大小 + 复选图标大小 + 展开收起图标右侧间距 + 文本内容距离右侧间距 + 复选图标右侧间距 + --ti-tree-node-text-nomultiple-space: calc( + var(--ti-tree-square-icon-size) + var(--ti-tree-icon-right-space) * 2 + ); // 用于计算文本区域宽度:没有多选的场景 + --ti-tree-node-text-multiple-leaf-space: calc( + var(--ti-tree-node-icon-size) + var(--ti-common-space-base) + var(--ti-tree-icon-right-space) * 2 + ); // 用于计算文本区域宽度:多选且为叶子节点的场景,复选图标大小 + 复选图标左侧间距 + 复选图标右侧距离文本内容间距 * 文本内容距离右侧间距 + --ti-tree-node-text-padding-space: calc( + var(--ti-common-space-2x) + var(--ti-common-space-base) + ); // 用于计算文本区域宽度: 没有多选且为叶子节点场景,文本区域的左右padding值 + --ti-tree-item-guide-line-first-child-space: var(--ti-common-space-2x); + --ti-tree-item-guide-line-width: var(--ti-common-size-2x); // 导航线宽度 + --ti-tree-small-checkbox-size: var(--ti-common-size-3x); + --ti-tree-small-icon-font-size: var(--ti-common-size-3x); + --ti-tree-small-icon-space: calc(var(--ti-common-space-base) / 2); + --ti-tree-drag-border-color-default: var( + --ti-common-color-line-active + ); // 拖动放置时显示的边框颜色,单独提出避免与checkbox的边框颜色互相影响 + --ti-tree-square-bg-color-active: var( + --ti-common-color-bg-white-normal + ); // 展开收起图标背景激活色,单独提出避免与checkbox的背景色互相影响 +} +:host.ti3-tree-virtual-scroll { + display: block; + cdk-virtual-scroll-viewport { + height: 100%; + } +} +:host ::ng-deep .cdk-virtual-scroll-content-wrapper { + width: 100%; +} + +.ti3-tree-node-text { + max-width: 100%; + display: inline-block; + .ellipsis(); +} + +.ti3-tree-parent-node { + font-size: var(--ti-common-font-size-base); + position: relative; +} + +.ti3-tree-node-icon { + text-align: center; + font-size: var(--ti-tree-node-icon-size); + margin-right: var(--ti-tree-icon-right-space); +} + +.ti3-tree-leaf-node { + margin-left: var(--ti-common-space-6x); + position: relative; +} + +.ti3-tree-node-text-wrapper { + display: flex; + align-items: center; + width: ~'calc(100% - var(--ti-tree-node-icon-space))'; + white-space: nowrap; + padding: var(--ti-common-space-0) var(--ti-common-space-2x) var(--ti-common-space-0) var(--ti-common-space-base); + &:hover { + background: var(--ti-common-color-bg-white-emphasize); + &.ti3-tree-text-disabled-wrapper { + background: none; + } + } + &:focus { + outline: none; + } + &.ti3-tree-text-focus { + background: var(--ti-common-color-bg-white-emphasize); + outline: none; + } + .ti3-tree-operate { + font-size: var(--ti-common-font-size-2); + color: var(--ti-common-color-icon-normal); + margin-left: var(--ti-common-space-2x); + &:hover, + &:active { + color: var(--ti-common-color-icon-hover); + cursor: pointer; + } + &:first-of-type { + margin-left: var(--ti-common-space-3x); + } + } + .ti3-tree-edit-input { + height: var(--ti-tree-item-height); + } +} +.ti3-tree-text-nomultiple-wrapper { + width: ~'calc(100% - var(--ti-tree-node-text-nomultiple-space))'; +} + +.ti3-tree-text-multiple-leaf-wrapper { + width: ~'calc(100% - var(--ti-tree-node-text-multiple-leaf-space))'; +} + +.ti3-tree-text-leaf-wrapper { + width: ~'calc(100% - var(--ti-tree-node-text-padding-space))'; +} + +.ti3-tree-content-box { + display: flex; + align-items: center; + cursor: pointer; + height: var(--ti-tree-item-height); + line-height: var(--ti-tree-item-height); + padding-left: var(--ti-common-space-2x); +} + +.ti3-tree-content-box-disabled { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; +} + +.ti3-tree-item-active { + background: var(--ti-common-color-bg-light-normal); + &:hover, + &:focus { + background: var(--ti-common-color-bg-light-normal); + } +} + +.ti3-tree-item-leaf { + margin-left: calc(var(--ti-tree-square-icon-size) + var(--ti-common-space-base)); +} + +.ti3-tree-checkbox-wrapper { + display: inline-block; + font-size: 0; // 解决图标溢出问题 +} + +:host { + .ti3-tree-content-box .ti3-tree-checkbox-wrapper { + ::ng-deep input[type='checkbox'][tiCheckbox] { + & + .ti3-checkbox { + .ti3-checkbox-skin { + margin: 0 var(--ti-common-space-base) 0 var(--ti-common-space-base); // 规范调整,复选框距离左右侧间距4px + } + } + } + } +} + +.ti3-tree-item-loading { + padding: 0 2px 0 2px; + margin-right: var(--ti-common-space-base); +} + +.ti3-tree-container { + .ti3-icon-minus-square { + color: var(--ti-common-color-icon-active); + } + + .ti3-icon-plus-square { + color: var(--ti-common-color-icon-normal); + } + + .ti3-icon-minus-square, + .ti3-icon-plus-square { + margin-right: var(--ti-common-space-base); + font-size: var(--ti-tree-square-icon-size); + // i标签元素如果不设置具体宽度,在浏览器缩放小于100%的时候,宽度会变大,导致后面的文本区域换行。 + display: inline-block; + width: var(--ti-tree-square-icon-size); + // 由于line-height继承性,图标按下时,整个图标节点背景色会变化 + line-height: var(--ti-tree-square-icon-size); + + &:hover { + cursor: pointer; + color: var(--ti-common-color-icon-active); + } + + &:active { + color: var(--ti-common-color-icon-active); + background: var(--ti-tree-square-bg-color-active); + } + } +} + +.ti3-tree-content-drag-box { + height: var(--ti-tree-item-height); + border-bottom: 2px solid transparent; + border-top: 2px solid transparent; + box-sizing: border-box; +} + +.ti3-tree-drag-over-inner { + border: var(--ti-common-border-weight-1) var(--ti-common-border-style-solid) var(--ti-tree-drag-border-color-default); + padding: 0 var(--ti-common-space-6) 0 2px; +} + +.ti3-tree-drag-over-top { + border-top: var(--ti-common-border-weight-1) var(--ti-common-border-style-solid) var(--ti-tree-drag-border-color-default); +} + +.ti3-tree-drag-over-bottom { + border-bottom: var(--ti-common-border-weight-1) var(--ti-common-border-style-solid) var(--ti-tree-drag-border-color-default); +} +/* -----------------------------------------------左侧导航线样式--------------------------------------------*/ +.ti3-tree-leaf-node { + // 导航线基础样式 + & > .ti3-tree-content-box > .ti3-tree-item-guide-line { + position: absolute; + width: var(--ti-tree-item-guide-line-width); + height: var(--ti-tree-item-height); + .box-sizing(border-box); + border-left: 1px solid var(--ti-common-color-line-dividing); + border-bottom: 1px solid var(--ti-common-color-line-dividing); + top: calc(-1 * var(--ti-tree-item-height) / 2); + left: calc(-1 * var(--ti-tree-square-icon-size) / 2); + } + + // 第一个子节点的导航线样式:正常场景和虚拟滚动场景 + &:not(.ti3-tree-virtual-scroll-node):first-child, + &.ti3-tree-virtual-scroll-first-child-node { + & > .ti3-tree-content-box > .ti3-tree-item-guide-line { + height: calc( + var(--ti-tree-item-height) - var(--ti-tree-square-icon-size) / 2 - var(--ti-tree-item-guide-line-first-child-space) + ); // 第一个节点的导航线与图标的间距为8px + top: calc(var(--ti-tree-square-icon-size) / 2 + var(--ti-tree-item-guide-line-first-child-space) - var(--ti-tree-item-height) / 2); + } + } + + &:not(.ti3-tree-virtual-scroll-node):not(:last-child) > .ti3-tree-parent-node:after { + content: ''; + position: absolute; + height: 100%; + .box-sizing(border-box); + width: var(--ti-tree-item-guide-line-width); + border-left: 1px solid var(--ti-common-color-line-dividing); + left: calc(-1 * var(--ti-tree-square-icon-size) / 2); + top: calc(-1 * var(--ti-tree-item-height) / 2); + } + + &.ti3-tree-virtual-scroll-node { + > .ti3-tree-content-box > .ti3-tree-item-guide-line { + border-left: 0px; + } + > .ti3-tree-content-box > .ti3-tree-item-vertical-guide-line { + position: absolute; + height: 100%; + top: 0px; + width: 1px; + background-color: var(--ti-common-color-line-dividing); + &.ti3-tree-last-item-vertical-guide-line { + height: 50%; + } + } + } +} + +/* -----------------------------------------------紧凑型(运维场景)--------------------------------------------*/ +:host[small] { + .ti3-icon-minus-square, + .ti3-icon-plus-square { + margin-right: var(--ti-tree-small-icon-space); + font-size: var(--ti-tree-small-icon-font-size); + width: var(--ti-tree-small-icon-font-size); + line-height: var(--ti-tree-small-icon-font-size); + } + + .ti3-tree-node-icon { + font-size: var(--ti-tree-small-icon-font-size); + margin-right: var(--ti-common-space-base); + } + + .ti3-tree-content-box .ti3-tree-checkbox-wrapper { + ::ng-deep input[type='checkbox'][tiCheckbox] { + & + .ti3-checkbox { + .ti3-checkbox-skin { + margin: 0 var(--ti-tree-small-icon-space) 0 var(--ti-tree-small-icon-space); + width: var(--ti-tree-small-checkbox-size); + height: var(--ti-tree-small-checkbox-size); + line-height: var(--ti-tree-small-checkbox-size); + } + } + + &:indeterminate + .ti3-checkbox { + .ti3-checkbox-skin:before { + top: calc((var(--ti-tree-small-checkbox-size) - 8px) / 2); + left: calc((var(--ti-tree-small-checkbox-size) - 8px) / 2); + } + } + + &:checked + .ti3-checkbox { + .ti3-checkbox-skin:before { + width: calc(var(--ti-tree-small-checkbox-size) / 12 * 5); + height: calc(var(--ti-tree-small-checkbox-size) / 12 * 7); + } + } + } + } + + .ti3-tree-item-leaf { + margin-left: calc(var(--ti-tree-small-icon-font-size) + var(--ti-tree-small-icon-space)); + } + + .ti3-tree-content-box { + padding-left: var(--ti-common-space-base); + } + + .ti3-tree-leaf-node { + margin-left: calc(var(--ti-common-space-base) + var(--ti-tree-small-icon-font-size)); + + .ti3-tree-content-box { + padding-left: var(--ti-common-space-base); + } + + & > .ti3-tree-content-box > .ti3-tree-item-guide-line { + width: calc(var(--ti-tree-small-icon-font-size) / 2); + left: calc(-1 * var(--ti-tree-small-icon-font-size) / 2); + } + + &:not(.ti3-tree-virtual-scroll-node):not(:last-child) > .ti3-tree-parent-node:after { + left: calc(-1 * var(--ti-tree-small-icon-font-size) / 2); + } + } + + .ti3-tree-node-text-wrapper { + padding-left: var(--ti-tree-small-icon-space); + } +} + +// 搜索高亮色 +::ng-deep .ti3-font-highlight { + color: var(--ti-common-color-text-highlight); +} + +.ti3-tree-more-content { + display: inline-block; +} + +.ti3-tree-more-box { + margin-left: 0px; + padding: 0px; +} + +.ti3-tree-more-loading { + margin: 0 var(--ti-common-space-6) 0 var(--ti-common-space-10); +} + +.ti3-tree-node-more-text { + margin-left: var(--ti-common-space-base); + color: var(--ti-common-color-prompt-text); +} + +.ti3-tree-more-error { + padding-left: var(--ti-common-space-3x); +} diff --git a/src/treeselect/demo/karma.conf.js b/src/treeselect/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/treeselect/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/treeselect/demo/project.json b/src/treeselect/demo/project.json new file mode 100644 index 0000000..8989fb1 --- /dev/null +++ b/src/treeselect/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/treeselect/demo", + "sourceRoot": "src/treeselect/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/treeselect", + "index": "src/treeselect/demo/src/index.html", + "main": "src/treeselect/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/treeselect/demo/tsconfig.app.json", + "assets": ["src/treeselect/demo/src/favicon.ico", "src/treeselect/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "treeselect-demo:build:production" + }, + "development": { + "browserTarget": "treeselect-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js treeselect" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/treeselect/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/treeselect/demo/tsconfig.spec.json", + "karmaConfig": "src/treeselect/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/treeselect/demo/src/app/AppComponent.ts b/src/treeselect/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/treeselect/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/treeselect/demo/src/app/AppModule.ts b/src/treeselect/demo/src/app/AppModule.ts new file mode 100644 index 0000000..1a6fa8c --- /dev/null +++ b/src/treeselect/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TreeselectTestModule } from './treeselect/TreeselectTestModule'; + +@NgModule({ + imports: [ + TreeselectTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/treeselect/demo/src/app/IndexComponent.ts b/src/treeselect/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..04d3e90 --- /dev/null +++ b/src/treeselect/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TreeselectTestModule } from './treeselect/TreeselectTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TreeselectTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/treeselect/demo/src/app/app.html b/src/treeselect/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/treeselect/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectBasicComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectBasicComponent.ts new file mode 100644 index 0000000..6df660d --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectBasicComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-basic.html' +}) +export class TreeselectBasicComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 当前选中项 + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectBeforeExpandComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectBeforeExpandComponent.ts new file mode 100644 index 0000000..8fe8bd4 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectBeforeExpandComponent.ts @@ -0,0 +1,98 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-before-expand.html' +}) +export class TreeselectBeforeExpandComponent { + constructor(private changeDetectorRef: ChangeDetectorRef) {} + private treeID: number = 5; + // 单选 + options: Array = [ + { + label: '家用电器', + children: [] + }, + { + label: '服饰', + children: [] + }, + { + label: '视频', + children: [] + }, + { + label: '食品', + children: [] + } + ]; + + value: any = TiTreeUtil.getTreeSelectedData(this.options, true, false); + + // 异步请求数据后插入当前节点并展开 + onBeforeExpand(item: TiTreeNode, multile?: boolean): void { + if (item.children.length > 0) { + // 已经加载过数据就不再重复加载,业务可根据自身场景来决定是否需要进行该处理 + item.expanded = true; + + return; + } + item.loadStatus = 'loading'; // 标志正在加载 + const getDataPromise: any = this.getNodeData(item); + getDataPromise + .then((data: Array) => { + // 1.将请求到的数据挂到该节点上 + item.children = data; + // 2. 多选情况下,如果该节点是选中状态,则需要将所有子节点也选中 + if (multile && item.checked === true) { + item.children.forEach((child: TiTreeNode) => { + child.checked = true; + }); + } + // 3.将该节点展开 + item.expanded = true; + item.loadStatus = 'success'; // 标志加载成功 + }) + .catch(() => { + // 异步获取数据失败 + item.expanded = true; // 打开当前节点 + item.loadStatus = 'error'; // 标志加载失败 + }); + } + + private getNodeData(item: TiTreeNode): any { + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + const data: Array = this.createData(item); + if (data && data.length > 0) { + resolve(data); + } else { + const errMsg: string = '获取数据失败'; + reject(errMsg); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef + // 默认模式下不需要 + // 服务可根据自身接口增加该方法 + this.changeDetectorRef.markForCheck(); + }, 2000); + }); + } + + private createData(item: TiTreeNode): Array { + const data: Array = []; + if (item.label === '服饰' || item.label === '家用电器') { + for (let i: number = 0; i < 3; i++) { + const dataList: TiTreeNode = { + type: 'FILE', + label: `item${this.treeID}` + }; + this.treeID++; + data.push(dataList); + } + + return data; + } + + return data; + } +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectBeforeMoreComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectBeforeMoreComponent.ts new file mode 100644 index 0000000..5285f7e --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectBeforeMoreComponent.ts @@ -0,0 +1,143 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-before-more.html' +}) +export class TreeselectBeforeMoreComponent { + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + options: Array = [ + { + label: '家用电器', + children: [] + }, + { + label: '服饰', + children: [] + }, + { + label: '视频', + children: [] + }, + { + label: '食品', + children: [] + } + ]; + + value: any = TiTreeUtil.getTreeSelectedData(this.options, true, true); + treeID: number = 0; + offset: number; // 当前已展开的节点数量 + nodeNum: number = 3; // 每次点击展开节点的数量 + + // 异步请求数据后插入当前节点并展开 + beforeExpand(node: TiTreeNode, multile?: boolean): void { + if (node.children.length > 0) { + // 已经加载过数据就不再重复加载,业务可根据自身场景来决定是否需要进行该处理 + node.expanded = true; + + return; + } + + node.loadStatus = 'loading'; // 标志正在加载 + this.offset = 0; + const getDataPromise: Promise = this.getNodeData(node, this.offset, this.nodeNum, 'before'); + getDataPromise + .then((data: Array) => { + // 1.将请求到的数据挂到该节点上 + node.children = data; + // 2. 多选情况下,如果该节点是选中状态,则需要将所有子节点也选中 + if (multile && node.checked === true) { + node.children.forEach((child: TiTreeNode) => { + child.checked = true; + }); + } + node.showMore = true; // 当前节点下显示更多按钮 + // 3.将该节点展开 + node.expanded = true; + node.loadStatus = 'success'; // 标志加载成功 + }) + .catch((error: Error) => { + // 异步获取数据失败 + node.expanded = true; // 打开当前节点 + node.loadStatus = 'error'; // 标志加载失败 + }); + } + + // 点击更多异步请求其余节点 + beforeMore(node: TiTreeNode, multile?: boolean): void { + node.moreStatus = 'loading'; + this.offset = node.children?.length; + const getDataPromise: Promise = this.getNodeData(node, this.offset, this.nodeNum, 'more'); + getDataPromise + .then((data: Array) => { + // 多选情况下,如果该节点是选中状态,则需要将新添加的子节点也选中 + if (multile && node.checked === true) { + data.forEach((child: TiTreeNode) => { + child.checked = true; + }); + } + // 将请求的数据合并到该节点上 + node.children = node.children.concat(data); + node.moreStatus = 'success'; // 标志加载成功 + // 此处根据业务实际返回,数据加载完后隐藏更多按钮 + if (node.children.length > 6) { + node.showMore = false; + } + }) + .catch((error: Error) => { + node.moreStatus = 'error'; // 异步获取数据失败 + }); + } + + // 模拟异步请求 + private getNodeData(item: TiTreeNode, offset: number, num: number, style: string): any { + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + const data: Array = this.createData(item, offset, num, style); + if (data && data.length > 0) { + resolve(data); + } else { + const errMsg: string = '获取数据失败'; + reject(errMsg); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef + // 默认模式下不需要 + // 服务可根据自身接口增加该方法 + this.changeDetectorRef.markForCheck(); + }, 2000); + }); + } + + private createData(item: TiTreeNode, offset: number, num: number, style: string): Array { + const data: Array = []; + let result: Array; + this.treeID = 0; + if (style === 'before' && (item.label === '服饰' || item.label === '家用电器' || item.label === '视频' || item.label === '食品')) { + for (let i: number = 0; i < 100; i++) { + const dataList: TiTreeNode = { + label: item.label + this.treeID, + children: [] + }; + this.treeID++; + data.push(dataList); + } + } + + if (style === 'more' && (item.label === '服饰' || item.label === '家用电器')) { + for (let i: number = 0; i < 100; i++) { + const dataList: TiTreeNode = { + label: item.label + this.treeID, + children: [] + }; + this.treeID++; + data.push(dataList); + } + } + + result = data.slice(offset, offset + num); + + return result; + } +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectClearableComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectClearableComponent.ts new file mode 100644 index 0000000..e4fdd7c --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectClearableComponent.ts @@ -0,0 +1,90 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-clearable.html' +}) +export class TreeselectClearableComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 当前选中项 + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); + + myLogs: Array = []; + + onClear(): void { + this.myLogs = [...this.myLogs, `onClear()`]; + } +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectDisabledComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectDisabledComponent.ts new file mode 100644 index 0000000..f017ec5 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectDisabledComponent.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-disabled.html' +}) +export class TreeselectDisabledComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调', + disabled: true + } + ] + } + ] + } + ] + } + ]; + + value: TiTreeNode = []; + myOptions: Array = JSON.parse(JSON.stringify(this.options)); + myValue: TiTreeNode = JSON.parse(JSON.stringify(this.value)); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectDropmaxheightComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectDropmaxheightComponent.ts new file mode 100644 index 0000000..623ea8c --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectDropmaxheightComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-dropmaxheight.html' +}) +export class TreeselectDropmaxheightComponent { + dropMaxHeight: number = 300; + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectEventComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectEventComponent.ts new file mode 100644 index 0000000..40644f4 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectEventComponent.ts @@ -0,0 +1,89 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-event.html' +}) +export class TreeselectEventComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + value: Array = TiTreeUtil.getTreeSelectedData(this.options, true, false); + + myLogs: Array = []; + + onSelect(event: any): void { + this.myLogs = [...this.myLogs, `onSelect() event=${JSON.stringify(event)}`]; + } +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectFocusComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectFocusComponent.ts new file mode 100644 index 0000000..67e3f1f --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectFocusComponent.ts @@ -0,0 +1,82 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-focus.html' +}) +export class TreeselectFocusComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + mySelected: any = TiTreeUtil.getTreeSelectedData(this.options, true, false); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectLabelkeyComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectLabelkeyComponent.ts new file mode 100644 index 0000000..51ca7eb --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectLabelkeyComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-labelkey.html' +}) +export class TreeselectLabelkeyComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 当前选中项 + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectLazyloadComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectLazyloadComponent.ts new file mode 100644 index 0000000..452231c --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectLazyloadComponent.ts @@ -0,0 +1,97 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiTreeNode, TiTreeselectComponent, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-lazyload.html' +}) +export class TreeselectLazyloadComponent { + options: Array = []; + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); + + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + onBeforeOpen(treeselectComp: TiTreeselectComponent): void { + setTimeout(() => { + this.options = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 此处设置options的值,无法触发ngOnInit,需要设置下oldOptions + treeselectComp.oldOptions = this.options; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, false); + // OnPush模式下,异步刷新都需要手动触发。 + this.changeDetectorRef.markForCheck(); + treeselectComp.open(); + }, 1000); + } +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectLoadComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectLoadComponent.ts new file mode 100644 index 0000000..85a1ec8 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectLoadComponent.ts @@ -0,0 +1,130 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-load.html' +}) +export class TreeselectLoadComponent { + options: Array; + value: any; + + private dataA: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + private dataB: Array = [ + { + label: '家用电器', + children: [] + }, + { + label: '服饰', + children: [] + }, + { + label: '视频', + children: [] + }, + { + label: '食品', + children: [] + } + ]; + + // 每个组件都用下面六个函数,只改变函数内容 + changeUndefined(): void { + this.options = undefined; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, false); + } + changeNull(): void { + this.options = null; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, false); + } + changeWrongType(): void { + const temp: any = 5; + this.options = temp; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, false); + } + changeNullData(): void { + this.options = []; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, false); + } + changeDataA(): void { + this.options = this.dataA; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, false); + } + changeDataB(): void { + this.options = this.dataB; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, false); + } +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectMaxlineComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectMaxlineComponent.ts new file mode 100644 index 0000000..cf26518 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectMaxlineComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-maxline.html' +}) +export class TreeselectMaxlineComponent { + public options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 当前选中项 + public value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectModalComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectModalComponent.ts new file mode 100644 index 0000000..72664c3 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectModalComponent.ts @@ -0,0 +1,112 @@ +import { Component } from '@angular/core'; +import { TiModalService, TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-modal.html' +}) +export class TreeselectModalComponent { + constructor(private tiModal: TiModalService) {} + + openModal(): void { + this.tiModal.open(ModalTestComponent, {}); + } +} +@Component({ + template: ` + 在弹窗中使用树下拉选择组件 + + + + + + + + ` +}) +export class ModalTestComponent { + options: Array = []; + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, true); + data: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + ngOnInit(): void { + this.options = this.data; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, true); + } + + // 模板中实际调用的是Modal服务提供的close和dismiss方法,并非此处定义的方法; + // 在此处定义close和dismiss方法只是为了避免生产环境打包时报错 + close(): void {} + dismiss(): void {} +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectMultiComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectMultiComponent.ts new file mode 100644 index 0000000..19cae0a --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectMultiComponent.ts @@ -0,0 +1,83 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-multi.html' +}) +export class TreeselectMultiComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + // 当前选中项 + value: Array = TiTreeUtil.getTreeSelectedData(this.options, true, true); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectNodataComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectNodataComponent.ts new file mode 100644 index 0000000..afc65c4 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectNodataComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-nodata.html' +}) +export class TreeselectNodataComponent { + options: Array = []; + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); + noDataText: string = '空数据'; +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectOptionsChangeComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectOptionsChangeComponent.ts new file mode 100644 index 0000000..d6243ec --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectOptionsChangeComponent.ts @@ -0,0 +1,69 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-options-change.html' +}) +export class TreeselectOptionsChangeComponent implements OnInit { + options: Array = []; + value: TiTreeNode = []; + myOptions: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + } + ]; + + constructor(private cdRef: ChangeDetectorRef) {} + + ngOnInit(): void { + // 模拟请求后台数据 + setTimeout((): void => { + this.options = this.myOptions; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, true); + // OnPush模式下,异步刷新都需要手动触发。 + this.cdRef.markForCheck(); + }, 2000); + } +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectPanelwidthComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectPanelwidthComponent.ts new file mode 100644 index 0000000..8bcfe21 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectPanelwidthComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-panelwidth.html' +}) +export class TreeselectPanelwidthComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调 (1.5匹 静悦 新一级 变频省电 冷暖 卧室挂式空调挂机 大风口)' + }, + { + label: '美的空调 (新一级 极光先锋 大1.5匹 智能家电 变频冷暖 壁挂式空调挂机 一键智控温 K 1.5)' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 当前选中项 + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectSearchComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectSearchComponent.ts new file mode 100644 index 0000000..1ad7a5c --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectSearchComponent.ts @@ -0,0 +1,82 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-search.html' +}) +export class TreeselectSearchComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + value: Array = TiTreeUtil.getTreeSelectedData(this.options, true, true); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectSelectallComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectSelectallComponent.ts new file mode 100644 index 0000000..6c98d22 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectSelectallComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-selectall.html' +}) +export class TreeselectSelectallComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 当前选中项 + value: Array = TiTreeUtil.getTreeSelectedData(this.options, true, true); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectTestModule.ts b/src/treeselect/demo/src/app/treeselect/TreeselectTestModule.ts new file mode 100644 index 0000000..de128fc --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectTestModule.ts @@ -0,0 +1,153 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiModalModule, TiSelectModule, TiTreeselectModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TreeselectBasicComponent } from './TreeselectBasicComponent'; +import { TreeselectMultiComponent } from './TreeselectMultiComponent'; +import { TreeselectEventComponent } from './TreeselectEventComponent'; +import { TreeselectSearchComponent } from './TreeselectSearchComponent'; +import { TreeselectDisabledComponent } from './TreeselectDisabledComponent'; +import { TreeselectSelectallComponent } from './TreeselectSelectallComponent'; +import { TreeselectNodataComponent } from './TreeselectNodataComponent'; +import { TreeselectValidationComponent } from './TreeselectValidationComponent'; +import { TreeselectLazyloadComponent } from './TreeselectLazyloadComponent'; +import { TreeselectLoadComponent } from './TreeselectLoadComponent'; +import { TreeselectFocusComponent } from './TreeselectFocusComponent'; +import { TreeselectDropmaxheightComponent } from './TreeselectDropmaxheightComponent'; +import { TreeselectOptionsChangeComponent } from './TreeselectOptionsChangeComponent'; +import { ModalTestComponent, TreeselectModalComponent } from './TreeselectModalComponent'; +import { TreeselectBeforeExpandComponent } from './TreeselectBeforeExpandComponent'; +import { TreeselectBeforeMoreComponent } from './TreeselectBeforeMoreComponent'; +import { TreeselectClearableComponent } from './TreeselectClearableComponent'; +import { TreeselectMaxlineComponent } from './TreeselectMaxlineComponent'; +import { TreeselectPanelwidthComponent } from './TreeselectPanelwidthComponent'; +import { TreeselectLabelkeyComponent } from './TreeselectLabelkeyComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiTreeselectModule, + TiValidationModule, + TiModalModule, + TiButtonModule, + TiSelectModule, + DemoLogModule, + RouterModule.forChild(TreeselectTestModule.ROUTES) + ], + declarations: [ + TreeselectBasicComponent, + TreeselectMultiComponent, + TreeselectEventComponent, + TreeselectSearchComponent, + TreeselectDisabledComponent, + TreeselectSelectallComponent, + TreeselectNodataComponent, + TreeselectValidationComponent, + TreeselectLazyloadComponent, + TreeselectLoadComponent, + TreeselectFocusComponent, + TreeselectDropmaxheightComponent, + TreeselectOptionsChangeComponent, + TreeselectModalComponent, + TreeselectBeforeExpandComponent, + TreeselectBeforeMoreComponent, + TreeselectClearableComponent, + TreeselectMaxlineComponent, + TreeselectPanelwidthComponent, + TreeselectLabelkeyComponent, + ModalTestComponent + ], + entryComponents: [ModalTestComponent] +}) +export class TreeselectTestModule { + static readonly LINKS: Array = [{ href: 'components/TiTreeselectComponent.html', label: 'Treeselect' }]; + static readonly ROUTES: Routes = [ + { + path: 'treeselect/treeselect-basic', + component: TreeselectBasicComponent + }, + { + path: 'treeselect/treeselect-multi', + component: TreeselectMultiComponent + }, + { + path: 'treeselect/treeselect-selectall', + component: TreeselectSelectallComponent + }, + { + path: 'treeselect/treeselect-clearable', + component: TreeselectClearableComponent + }, + { + path: 'treeselect/treeselect-event', + component: TreeselectEventComponent + }, + { + path: 'treeselect/treeselect-search', + component: TreeselectSearchComponent + }, + { + path: 'treeselect/treeselect-labelkey', + component: TreeselectLabelkeyComponent + }, + { + path: 'treeselect/treeselect-disabled', + component: TreeselectDisabledComponent + }, + { + path: 'treeselect/treeselect-nodata', + component: TreeselectNodataComponent + }, + { + path: 'treeselect/treeselect-validation', + component: TreeselectValidationComponent + }, + { + path: 'treeselect/treeselect-lazyload', + component: TreeselectLazyloadComponent + }, + + { + path: 'treeselect/treeselect-dropmaxheight', + component: TreeselectDropmaxheightComponent + }, + + { + path: 'treeselect/treeselect-before-expand', + component: TreeselectBeforeExpandComponent + }, + { + path: 'treeselect/treeselect-before-more', + component: TreeselectBeforeMoreComponent + }, + { + path: 'treeselect/treeselect-modal', + component: TreeselectModalComponent + }, + { + path: 'treeselect/treeselect-options-change', + component: TreeselectOptionsChangeComponent + }, + { + path: 'treeselect/treeselect-load', + component: TreeselectLoadComponent + }, + { + path: 'treeselect/treeselect-focus', + component: TreeselectFocusComponent + }, + { + path: 'treeselect/treeselect-maxline', + component: TreeselectMaxlineComponent + }, + { + path: 'treeselect/treeselect-panelwidth', + component: TreeselectPanelwidthComponent + } + ]; +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectValidationComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectValidationComponent.ts new file mode 100644 index 0000000..b25a3a0 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectValidationComponent.ts @@ -0,0 +1,83 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-validation.html' +}) +export class TreeselectValidationComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); +} diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-basic.html b/src/treeselect/demo/src/app/treeselect/treeselect-basic.html new file mode 100644 index 0000000..c878167 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-basic.html @@ -0,0 +1,5 @@ + + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-before-expand.html b/src/treeselect/demo/src/app/treeselect/treeselect-before-expand.html new file mode 100644 index 0000000..e4f2a2b --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-before-expand.html @@ -0,0 +1,8 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-before-more.html b/src/treeselect/demo/src/app/treeselect/treeselect-before-more.html new file mode 100644 index 0000000..3450a2b --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-before-more.html @@ -0,0 +1,10 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-clearable.html b/src/treeselect/demo/src/app/treeselect/treeselect-clearable.html new file mode 100644 index 0000000..4780be8 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-clearable.html @@ -0,0 +1,11 @@ + + + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-disabled.html b/src/treeselect/demo/src/app/treeselect/treeselect-disabled.html new file mode 100644 index 0000000..fcfebdc --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-disabled.html @@ -0,0 +1,6 @@ +

    1. 整体禁用

    + + + +

    2. 数据项禁用

    + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-dropmaxheight.html b/src/treeselect/demo/src/app/treeselect/treeselect-dropmaxheight.html new file mode 100644 index 0000000..083ad9a --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-dropmaxheight.html @@ -0,0 +1,8 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-event.html b/src/treeselect/demo/src/app/treeselect/treeselect-event.html new file mode 100644 index 0000000..320d983 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-event.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-focus.html b/src/treeselect/demo/src/app/treeselect/treeselect-focus.html new file mode 100644 index 0000000..b4a8aec --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-focus.html @@ -0,0 +1,33 @@ +

    1、autofocus

    +

    设置[(ngModel)]为空数组[]

    + tabindex为0 + +

    2、手动设置焦点

    +

    [(ngModel)]不为空数组

    + +tabindex为0

    +  +   +  + +

    + +

    3、tabindex

    +

    tabindex="1"

    + +

    +

    tabindex="4"

    + +

    +

    tabindex="2"

    + +

    +

    tabindex="3"

    + +

    diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-labelkey.html b/src/treeselect/demo/src/app/treeselect/treeselect-labelkey.html new file mode 100644 index 0000000..42ba731 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-labelkey.html @@ -0,0 +1 @@ + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-lazyload.html b/src/treeselect/demo/src/app/treeselect/treeselect-lazyload.html new file mode 100644 index 0000000..f4470c8 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-lazyload.html @@ -0,0 +1,8 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-load.html b/src/treeselect/demo/src/app/treeselect/treeselect-load.html new file mode 100644 index 0000000..638430b --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-load.html @@ -0,0 +1,19 @@ +

    1. 默认值声明options和ngModel绑定的变量,不进行赋值

    +

    设置[(ngModel)]为空数组[]

    + + +
    +
    Current Select: {{ value | json }}
    +
    + +

    每个组件改变数据,都用下面六个按钮。不变化

    + +使用TiTreeUtil.getSelectedData获取选中数据时会报错,需要手动将[(ngModel)]置为空数组
    + + +使用TiTreeUtil.getSelectedData获取选中数据时会报错,需要手动将[(ngModel)]置为空数组
    + +
    +
    +
    +
    diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-maxline.html b/src/treeselect/demo/src/app/treeselect/treeselect-maxline.html new file mode 100644 index 0000000..d444f45 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-maxline.html @@ -0,0 +1,2 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-modal.html b/src/treeselect/demo/src/app/treeselect/treeselect-modal.html new file mode 100644 index 0000000..d62de46 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-modal.html @@ -0,0 +1 @@ + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-multi.html b/src/treeselect/demo/src/app/treeselect/treeselect-multi.html new file mode 100644 index 0000000..2bb937a --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-multi.html @@ -0,0 +1,5 @@ + + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-nodata.html b/src/treeselect/demo/src/app/treeselect/treeselect-nodata.html new file mode 100644 index 0000000..7406063 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-nodata.html @@ -0,0 +1,2 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-options-change.html b/src/treeselect/demo/src/app/treeselect/treeselect-options-change.html new file mode 100644 index 0000000..eeecc14 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-options-change.html @@ -0,0 +1,5 @@ + + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-panelwidth.html b/src/treeselect/demo/src/app/treeselect/treeselect-panelwidth.html new file mode 100644 index 0000000..a2d254a --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-panelwidth.html @@ -0,0 +1,2 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-search.html b/src/treeselect/demo/src/app/treeselect/treeselect-search.html new file mode 100644 index 0000000..7ee1a4e --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-search.html @@ -0,0 +1,10 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-selectall.html b/src/treeselect/demo/src/app/treeselect/treeselect-selectall.html new file mode 100644 index 0000000..c0e66c4 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-selectall.html @@ -0,0 +1,9 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-validation.html b/src/treeselect/demo/src/app/treeselect/treeselect-validation.html new file mode 100644 index 0000000..6ce417c --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-validation.html @@ -0,0 +1,10 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/webdoc/treeselect-demos.js b/src/treeselect/demo/src/app/treeselect/webdoc/treeselect-demos.js new file mode 100644 index 0000000..22cc97a --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/webdoc/treeselect-demos.js @@ -0,0 +1,202 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'treeselect-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'treeselect basic', + }, + desc: { + 'zh-CN': 'TreeSelect 组件的最简用法。', + 'en-US': '

    treeselect basic

    ', + }, + apis: ['TiTreeselectComponent.properties.options'], + }, + { + demoId: 'treeselect-multi', + name: { + 'zh-CN': '多选', + 'en-US': 'multiple', + }, + desc: { + 'zh-CN': '

    通过属性multiple配置组件是否为多选。', + 'en-US': '

    multiple

    ', + }, + apis: ['TiTreeselectComponent.properties.multiple'], + }, + { + demoId: 'treeselect-selectall', + name: { + 'zh-CN': '全选', + 'en-US': 'selectAll', + }, + desc: { + 'zh-CN': + '

    通过属性selectAll配置组件在多选场景下是否显示全选框。', + 'en-US': '

    selectAll

    ', + }, + apis: ['TiTreeselectComponent.properties.selectAll'], + }, + { + demoId: 'treeselect-labelkey', + name: { + 'zh-CN': '自定义选中项', + 'en-US': 'labelkey', + }, + desc: { + 'zh-CN': '

    通过属性labelKey配置组件下拉显示的字段。', + 'en-US': '

    labelkey

    ', + }, + apis: ['TiTreeselectComponent.properties.labelKey'], + }, + { + demoId: 'treeselect-clearable', + name: { + 'zh-CN': '可清除', + 'en-US': 'clearable', + }, + desc: { + 'zh-CN': + '通过属性clearable配置组件是否开启清除已选项功能。', + 'en-US': '

    clearable

    ', + }, + apis: ['TiTreeselectComponent.properties.clearable'], + }, + { + demoId: 'treeselect-search', + name: { + 'zh-CN': '可搜索', + 'en-US': 'searchable', + }, + desc: { + 'zh-CN': '通过属性searchable配置组件是否显示搜索框。', + 'en-US': '

    searchable

    ', + }, + apis: ['TiTreeselectComponent.properties.searchable'], + }, + { + demoId: 'treeselect-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled', + }, + desc: { + 'zh-CN': '通过属性disabled配置组件是否禁用。', + 'en-US': '

    disabled

    ', + }, + apis: ['TiTreeselectComponent.properties.disabled'], + }, + { + demoId: 'treeselect-dropmaxheight', + name: { + 'zh-CN': '下拉面板最大高度', + 'en-US': 'dropMaxHeight', + }, + desc: { + 'zh-CN': '通过属性dropMaxHeight配置组件下拉面板最大高度。', + 'en-US': '

    dropMaxHeight

    ', + }, + apis: ['TiTreeselectComponent.properties.dropMaxHeight'], + }, + { + demoId: 'treeselect-nodata', + name: { + 'zh-CN': '空数据', + 'en-US': 'nodata', + }, + desc: { + 'zh-CN': '通过属性noDataText配置组件空数据显示文本。', + 'en-US': '

    nodata

    ', + }, + apis: ['TiTreeselectComponent.properties.noDataText'], + }, + { + demoId: 'treeselect-maxline', + name: { + 'zh-CN': '显示行数', + 'en-US': 'maxline', + }, + desc: { + 'zh-CN': + '通过属性maxLine配置组件在多选场景下选择框显示的最大行数。', + 'en-US': '

    maxLine

    ', + }, + apis: ['TiTreeselectComponent.properties.maxLine'], + }, + { + demoId: 'treeselect-panelwidth', + name: { + 'zh-CN': '下拉宽度', + 'en-US': 'panelwidth', + }, + desc: { + 'zh-CN': + '

    通过属性panelWidth配置下拉宽度,包含justifiedautostring三种类型。', + 'en-US': '

    panelWidth

    ', + }, + apis: ['TiTreeselectComponent.properties.panelWidth'], + }, + { + demoId: 'treeselect-lazyload', + name: { + 'zh-CN': '懒加载', + 'en-US': 'lazyload', + }, + desc: { + 'zh-CN': '通过事件beforeOpen实现懒加载。', + 'en-US': '

    beforeOpen

    ', + }, + apis: ['TiTreeselectComponent.events.beforeOpen'], + }, + { + demoId: 'treeselect-before-expand', + name: { + 'zh-CN': '异步加载', + 'en-US': 'before-expand', + }, + desc: { + 'zh-CN': '通过事件beforeExpand实现异步加载。', + 'en-US': '

    disabled

    ', + }, + apis: ['TiTreeselectComponent.events.beforeExpand'], + }, + { + demoId: 'treeselect-before-more', + name: { + 'zh-CN': '分段加载', + 'en-US': 'before-more', + }, + desc: { + 'zh-CN': + '点击 “更多” 按钮,通过事件beforeMore实现分段异步加载。', + 'en-US': '

    before-more

    ', + }, + apis: ['TiTreeselectComponent.events.beforeMore'], + }, + { + demoId: 'treeselect-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event', + }, + desc: { + 'zh-CN': '点击当前项时,触发select事件。', + 'en-US': '

    before-event

    ', + }, + apis: ['TiTreeselectComponent.events.select'], + }, + { + demoId: 'treeselect-validation', + name: { + 'zh-CN': '校验', + 'en-US': 'validation', + }, + desc: { + 'zh-CN': '

    通过指令tiValidation配置组件的校验规则。', + 'en-US': '

    validation

    ', + }, + }, + ], +}; diff --git a/src/treeselect/demo/src/app/treeselect/webdoc/treeselect.cn.md b/src/treeselect/demo/src/app/treeselect/webdoc/treeselect.cn.md new file mode 100644 index 0000000..f8e524e --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/webdoc/treeselect.cn.md @@ -0,0 +1,25 @@ +--- +title: Treeselect 树选择 +--- + +# Treeselect 树选择 + +
    + +Treeselect 组件是树选择组件。支持单选、多选等,默认为单选。多选选中后以标签形式呈现,标签可删除。 + +```typescript +import { TiTreeselectModule } from "@opentiny/ng"; +``` + +
    + +
    + +Treeselect 组件是树选择组件。支持单选、多选等,默认为单选。多选选中后以标签形式呈现,标签可删除。 + +```typescript +import { TiSelectModule } from "@opentiny/ng"; +``` + +
    diff --git a/src/treeselect/demo/src/app/treeselect/webdoc/treeselect.en.md b/src/treeselect/demo/src/app/treeselect/webdoc/treeselect.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/webdoc/treeselect.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/treeselect/demo/src/favicon.ico b/src/treeselect/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/treeselect/demo/src/index.html b/src/treeselect/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/treeselect/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/treeselect/demo/src/main.ts b/src/treeselect/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/treeselect/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/treeselect/demo/test.ts b/src/treeselect/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/treeselect/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/treeselect/demo/tsconfig.app.json b/src/treeselect/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/treeselect/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/treeselect/demo/tsconfig.spec.json b/src/treeselect/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/treeselect/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/treeselect/lib/index.ts b/src/treeselect/lib/index.ts new file mode 100644 index 0000000..7efbc76 --- /dev/null +++ b/src/treeselect/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTreeselectModule'; diff --git a/src/treeselect/lib/ng-package.json b/src/treeselect/lib/ng-package.json new file mode 100644 index 0000000..74219ad --- /dev/null +++ b/src/treeselect/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/treeselect", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/treeselect/lib/package.json b/src/treeselect/lib/package.json new file mode 100644 index 0000000..6d32b67 --- /dev/null +++ b/src/treeselect/lib/package.json @@ -0,0 +1,20 @@ +{ + "name": "@opentiny/ng-treeselect", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-tree": "~1.0.0-beta.0", + "@opentiny/ng-dominator": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-searchbox": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-select": "~1.0.0-beta.0", + "@angular/forms": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-dropsearch": "~1.0.0-beta.0", + "@opentiny/ng-checkbox": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/treeselect/lib/project.json b/src/treeselect/lib/project.json new file mode 100644 index 0000000..9cfb9ca --- /dev/null +++ b/src/treeselect/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/treeselect/lib", + "sourceRoot": "src/treeselect/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/treeselect"], + "options": { + "project": "src/treeselect/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/treeselect"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js treeselect" + }, + { + "command": "ng default-build treeselect" + }, + { + "command": "node build/clear-default-theme.js treeselect" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/treeselect && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build treeselect && ng pack treeselect && node build/publish.js treeselect --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/treeselect/lib/src/TiTreeselectComponent.ts b/src/treeselect/lib/src/TiTreeselectComponent.ts new file mode 100644 index 0000000..72f1a1b --- /dev/null +++ b/src/treeselect/lib/src/TiTreeselectComponent.ts @@ -0,0 +1,625 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, SimpleChanges, ViewChild } from '@angular/core'; + +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiTreeComponent, TiTreeNode, TiTreeUtil } from '@opentiny/ng-tree'; +import { TiDominatorComponent } from '@opentiny/ng-dominator'; +import { TiDropComponent } from '@opentiny/ng-drop'; +import { TiSearchboxNotsearchComponent } from '@opentiny/ng-searchbox'; +import { TiLocale } from '@opentiny/ng-locale'; +import { Util } from '@opentiny/ng-utils'; +import { TiSelectComponent } from '@opentiny/ng-select'; +import { TiDropsearchComponent } from '@opentiny/ng-dropsearch'; +import packageInfo from '../package.json'; +/** + * Treeselect树选择下拉组件 + * + * 支持单选/多选,全选,搜索,懒加载。 + * + * 单选主要功能为从Tree组件数据中选择某一条数据,单选与Tree功能相同,只是视觉呈现不同。 + * + * 多选主要功能是从Tree组件数据中任意选择多条数据,多选与Tree功能相同,只是视觉呈现不同。 + * + * 该组件继承自TiSelectComponent,其中 + * + * 输入属性:labelKey、searchKeys、tipPosition、panelMaxHeight和valueKey暂不支持; + * + */ +@Component({ + selector: 'ti-treeselect', + templateUrl: 'treeselect.html', + styleUrls: ['./treeselect.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiTreeselectComponent)], + host: { + '(blur)': 'onBlur()' + } +}) +export class TiTreeselectComponent extends TiSelectComponent { + // 搜索框的高度 + private static readonly SEARCHBOX_HEIGHT: number = 30; + /** + * 下拉面板最大高度 + */ + @Input() dropMaxHeight: number = (30 + 1) * 8; + /** + * 树节点展开前触发的回调,一般用于异步数据获取 + */ + @Output() readonly beforeExpand: EventEmitter = new EventEmitter(); + /** + * 点击 “更多” 按钮时触发的回调,一般用于分段加载场景 + */ + @Output() readonly beforeMore: EventEmitter = new EventEmitter(); + + /** + * @ignore 搜索结果 + */ + public oldOptions: Array; + + /** + * @ignore 内部标签 + */ + @ViewChild(TiDropComponent, { static: true }) dropCom: TiDropComponent; + /** + * @ignore 内部标签 + */ + @ViewChild(TiDominatorComponent, { static: true }) + dominatorCom: TiDominatorComponent; + /** + * @ignore 内部标签 + */ + @ViewChild('searchboxCom') searchboxCom: TiSearchboxNotsearchComponent; + /** + * @ignore 内部标签 + */ + @ViewChild('datatemplate') dataTemplate: ElementRef; + + /** + * @ignore 全选框的半选中状态 + */ + public indeterminate: boolean = false; + + /** + * @ignore 是否全选中 + */ + public isAllSelected: boolean = false; + + /** + * @ignore 搜索内容 + */ + public searchText: string = ''; + + /** + * @ignore 是否处于搜索状态 + */ + private isInSearch: boolean = false; + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + super.ngOnInit(); + // 强制装换,方便调用super.xxx + this.dropsearchCom = this.dropCom as unknown as TiDropsearchComponent; + + // 记录传入的数据,浅拷贝 + this.oldOptions = this.options; + } + + ngOnChanges(changes: SimpleChanges): void { + // 解决延迟或者动态设置options,树组件未渲染的问题 + if (changes['options'] && !changes['options'].firstChange) { + // 重新记录传入的数据 + this.oldOptions = changes['options'].currentValue; + } + } + + ngDoCheck(): void { + super.ngDoCheck(); + // 异步挂载数据,改变节点属性时指引未发生变化,onpush模式下不会触发变更,故手动触发 + this.changeDetectorRef.markForCheck(); + } + + ngAfterViewChecked(): void { + if (this.searchable !== this.oldSearchable) { + this.oldSearchable = this.searchable; + if (this.searchable) { + this.setFocusableElems( + this.dominatorCom.getFocusableElems().concat(this.searchboxCom.getFocusableElems()).concat(this.dropCom.nativeElement) + ); + } else { + this.setFocusableElems(this.dominatorCom.getFocusableElems().concat(this.dropCom.nativeElement)); + } + } + // 调用父类逻辑afterViewChecked才去设置autofocs + super.ngAfterViewChecked(); + } + + /** + * @ignore drop高度被压缩时,重新设置下拉面板的高度 + */ + private restyleDropMaxHeight(): void { + let dropCurMaxHeight: number = parseInt(this.dropCom.nativeElement.style.maxHeight, 10); + + let dropMaxHeightAdapted: number = this.dropMaxHeight; + if (!isNaN(dropCurMaxHeight)) { + // 减去搜索框高度和间距的和 + if (this.searchable) { + dropCurMaxHeight -= TiTreeselectComponent.SEARCHBOX_HEIGHT; + } + if (dropCurMaxHeight < this.dropMaxHeight) { + dropMaxHeightAdapted = dropCurMaxHeight; + } + } + + this.renderer.setStyle(this.dataTemplate.nativeElement, 'max-height', dropMaxHeightAdapted + 'px'); + } + + ngOnModelChange(): void { + if (this.multiple) { + // 多选时,重新设置下拉面板定位 + this.setPosition(); + if (this.selectAll) { + // 允许全选时,设置全选框状态 + this.setAllSelectCheckboxState(); + } + } + } + + /** + * @ignore 设置下拉面板定位 + */ + setPosition(optionsChange?: boolean): void { + setTimeout(() => { + if (optionsChange) { + this.dropCom.resetPosition(); + } else { + this.dropCom.setPosition(); + } + this.restyleDropMaxHeight(); + }, 0); + } + + /** + * @ignore 关闭下拉面板并转移焦点 + */ + public close(): void { + this.dropCom.hide(); + // 焦点转移至dominator + if (this.searchable) { + this.dominatorCom.focus(); + } + } + + /** + * @ignore 仅仅关闭下拉面板 + */ + private closeWithoutFocus(): void { + this.dropCom.hide(); + } + + /** + * @ignore 打开下拉面板 + */ + public open(): void { + // 初始化最大高度 + this.renderer.setStyle(this.dataTemplate.nativeElement, 'max-height', this.dropMaxHeight + 'px'); + this.dropCom.show(); + this.restyleDropMaxHeight(); + // 焦点转移至搜索框 + if (this.searchable) { + this.searchboxCom.focus(); + } + } + + /** + * 尝试打开下拉面板 + */ + public wantOpen(): void { + super.wantOpen(); + } + + /** + * @ignore 点击ti-dominator触发的事件 + * 点击下拉面板,展开或者关闭面板 + */ + public onClickDominator(): void { + if (this.disabled) { + return; + } + if (!this.dropCom.isShow) { + // 清空搜索内容 + this.searchText = ''; + this.searchTextChange(this.searchText); + this.wantOpen(); + } else { + this.close(); + } + } + + /** + * @ignore + * 单选点击清除按钮时触发 clear 事件, 如果下拉中有搜索,则需要聚焦于 searchbox。 + */ + public onClearDominator(): void { + this.clear.emit(); + + if (this.dropsearchCom.isShow && this.searchable) { + this.searchboxCom.focus(); + } + } + + /** + * @ignore 删除ti-dominator选中项时触发的事件 + * 需要取消树节点的选中 + */ + public onDeleteDominatorTag(item: any): void { + const deletedItem: TiTreeNode = item.item; + // 1.取消选中当前节点、设置子节点及祖先节点的选中情况。从model中删除时,item.item.checked还是为true,要设置为false + this.setSelectState(deletedItem, !deletedItem.checked); + + // 2.设置为非全选 + this.isAllSelected = false; + // 3.设置全选框状态 + if (this.multiple && this.selectAll) { + item.model.length === 0 ? (this.indeterminate = false) : (this.indeterminate = true); + } + } + + /** + * @ignore ti-searchbox-notsearch搜索框内容变化时触发的事件 + */ + public searchTextChange(searchText: string): void { + if (Util.isEmptyString(searchText)) { + this.options = this.oldOptions; + this.isInSearch = false; + } else { + // this.oldOptions 叶子浅拷贝 + const searchResult: Array = TiTreeUtil.copy(this.oldOptions); + TiTreeUtil.search(searchResult, (cnode: TiTreeNode): boolean => { + return cnode.label.toLowerCase().indexOf(this.searchText.toLowerCase()) >= 0; + }); + // 展开整个树 + TiTreeUtil.traverse(searchResult, (node: TiTreeNode): void => { + if (node.children && node.children.length > 0) { + node.expanded = true; + } + }); + this.options = searchResult; + this.isInSearch = true; + } + if (this.multiple && this.selectAll) { + this.setAllSelectCheckboxState(); + } + + // 重新设置面板定位 + this.setPosition(true); + } + + /** + * @ignore + */ + public onBeforeExpand(treeInst: TiTreeComponent): void { + const node: TiTreeNode = treeInst.getBeforeExpandNode(); + if (this.beforeExpand.observers.length === 0) { + node.expanded = true; + } else { + this.beforeExpand.emit(node); + } + } + + /** + * @ignore + */ + public onBeforeMore(node: TiTreeNode): void { + this.beforeMore.emit(node); + } + + /** + * @ignore 从model中获取option所在的索引 + */ + private getItemIndexFromModel(option: TiTreeNode): number { + let index: number = -1; + index = this.model.findIndex((item: any) => { + // 判断item和option的引用是否相同即可 + return item === option; + }); + + return index; + } + + /** + * @ignore 获取需要放到model中的节点,根据规范,如果子节点全部选中了,需要在model中放入父节点或者祖先节点。找到第一个checked为true的节点即可 + * node: 树组件select事件中的参数 + */ + private getRealSelectedNode(node: TiTreeNode): TiTreeNode { + let selectedNode: TiTreeNode = null; + const parentNode: TiTreeNode = TiTreeUtil.getParentNode(this.oldOptions, node); + parentNode && parentNode.checked === true ? (selectedNode = this.getRealSelectedNode(parentNode)) : (selectedNode = node); + + return selectedNode; + } + + /** + * @ignore 获取需要从model移除的节点,根据规范,如果某个子节点取消选中了,那么可能是它的父节点或祖先节点或所有子节点从model中被移除 + * node:树组件select事件的参数 + */ + private getRealUnselectedNode(node: TiTreeNode): TiTreeNode { + let unselectedNode: TiTreeNode = null; + let parentNode: TiTreeNode = TiTreeUtil.getParentNode(this.oldOptions, node); + + if (this.getItemIndexFromModel(node) !== -1) { + // 需要从model中移除的节点是node本身 + unselectedNode = node; + } else { + // 需要从model中移除的节点是node父节点或祖先节点 + // 当节点处于半选状态时,点击节点的复选框,节点状态为选中,否则为非选中,所以要先判断parentNode是否存在 + while (parentNode && this.getItemIndexFromModel(parentNode) === -1) { + parentNode = TiTreeUtil.getParentNode(this.oldOptions, parentNode); + } + // parentNode可能为undefined + unselectedNode = parentNode; + } + + return unselectedNode; + } + + /** + * @ignore 取消选中时,获取被选中的子节点,根据规范,父节点或祖先节点被选中之后,如果取消了某个子节点的选中,那么需要把它的所有选中的子节点加入model中 + * unselectedNode:需要从model中移除的节点 + */ + private getSelectedChildren(unselectedNode: TiTreeNode): Array { + let selectedChildren: Array = []; + if (unselectedNode.children) { + unselectedNode.children.forEach((child: TiTreeNode) => { + if (child.checked === true) { + selectedChildren.push(child); + } else { + selectedChildren = selectedChildren.concat(this.getSelectedChildren(child)); + } + }); + } + + return selectedChildren; + } + + /** + * @ignore 根据checked的值,设置option节点及其子节点和祖先节点的选中状态 + * option: 选中的节点;checked:option.checked属性,true/false + */ + private setSelectState(option: TiTreeNode, checked: boolean): void { + // 对oldOptions进行操作,将搜索时的结果映射回来 + TiTreeUtil.traverse(this.oldOptions, (node: TiTreeNode) => { + if (node === option) { + node.checked = checked; + if (checked === true) { + TiTreeUtil.selectAllChildren(node); + TiTreeUtil.selectParents(node, this.oldOptions, true); + } else { + TiTreeUtil.deSelectAllChildren(node); + TiTreeUtil.deSelectParents(node, this.oldOptions); + } + } + }); + } + + /** + * @ignore 选中某个节点后,更新this.model + * option: 选中的节点 + */ + private setModelWhenSelected(option: TiTreeNode): void { + // 1.获取option最后一个被选中的祖先节点 + const selectedNode: TiTreeNode = this.getRealSelectedNode(option); + // 2.从model中删除selectNode的所有子节点,一直到叶子节点 + TiTreeUtil.traverse([selectedNode], (node: TiTreeNode) => { + let deleteIndex: number = this.getItemIndexFromModel(node); + while (deleteIndex !== -1) { + this.model.splice(deleteIndex, 1); + deleteIndex = this.getItemIndexFromModel(node); + } + }); + // 3.将selectedNode加入到this.model中 + if (this.getItemIndexFromModel(selectedNode) === -1) { + this.model.push(selectedNode); + } + } + + /** + * @ignore 取消选中某个节点后,更新this.model + * option: 被取消选中的节点 + */ + private setModelWhenUnselected(option: TiTreeNode): void { + // 1.获取需要从model中删除的节点 + const unselectedNode: TiTreeNode = this.getRealUnselectedNode(option); + + // 2.更新model + if (unselectedNode) { + // 2.1unselectedNode为option或option的祖先节点 + const deleteIndex: number = this.getItemIndexFromModel(unselectedNode); + this.model.splice(deleteIndex, 1); + // 将option所有选中的子节点存放到this.model中 + if (unselectedNode !== option) { + const selectedChildren: Array = this.getSelectedChildren(unselectedNode); + this.model.push(...selectedChildren); + } + } else { + // 2.2unselectedNode为undefined,这种情况下,只需从model中删除option所有选中的子节点 + TiTreeUtil.traverse([option], (node: TiTreeNode) => { + const deleteIndex: number = this.getItemIndexFromModel(node); + if (deleteIndex !== -1) { + this.model.splice(deleteIndex, 1); + } + }); + } + } + + /** + * @ignore 处于搜索状态下,更新model + */ + private setModelWhenSearch(data: Array, checked: boolean): void { + // 或者所有处于checked状态的叶子节点 + let selectedLeafNodes: Array = []; + selectedLeafNodes = this.getLeafNodes(data, checked); + // 根据每个叶子节点更新model + for (const leafNode of selectedLeafNodes) { + // 设置oldOptions各节点状态 + this.setSelectState(leafNode, checked); + checked ? this.setModelWhenSelected(leafNode) : this.setModelWhenUnselected(leafNode); + } + } + + /** + * @ignore 获取data里面选中或者未选中的全部叶子节点 + * data:搜索范围,checked:true/false + */ + private getLeafNodes(data: Array, checked: boolean): Array { + const leafNodes: Array = []; + TiTreeUtil.traverse(data, (node: TiTreeNode) => { + if ((TiTreeUtil.isLeaf(node) || (node.children && node.children.length === 0)) && node.checked === checked) { + leafNodes.push(node); + } + }); + + return leafNodes; + } + + /** + * @ignore 设置全选checkbox的状态 + */ + private setAllSelectCheckboxState(): void { + // 1.假设节点都被选中或都没被选中 + let isAllSelected: boolean = true; + let isAllUnSelected: boolean = true; + + // 2.判断是否所有节点都被选中或都没被选中,disabled状态下的节点不用统计 + TiTreeUtil.traverse(this.options, (node: TiTreeNode) => { + // 2.1 有节点没被选中 + if (!node.disabled && node.checked !== true) { + isAllSelected = false; + } + // 2.2 有节点被选中或半选中 + if (!node.disabled && (node.checked === true || node.checked === 'indeterminate')) { + isAllUnSelected = false; + } + }); + + // 3.设置checkbox状态 + this.isAllSelected = isAllSelected; + this.indeterminate = !isAllSelected && !isAllUnSelected; + } + + /** + * @ignore ti-tree的select事件 + */ + public onTreeSelect(option: any): void { + // 节点被禁用,不做操作,直接返回 + if (option.disabled === true) { + return; + } + + // 1.触发select事件 + this.select.emit(option); + if (!this.multiple) { + // 2.1单选,直接替换model + TiTreeUtil.traverse(this.options, (node: TiTreeNode) => { + if (node === option) { + node.checked = true; + this.model = [option]; + } else { + node.checked = false; + } + }); + // 关闭下拉面板 + this.close(); + } else { + // 2.2多选 + if (!this.isInSearch) { + // 2.2.1非搜索状态 + // 设置oldOptions各节点状态 + this.setSelectState(option, option.checked); + if (option.checked === true) { + this.setModelWhenSelected(option); + } else { + this.setModelWhenUnselected(option); + } + } else { + // 2.2.2搜索状态 + this.setModelWhenSearch([option], option.checked); + } + + // 2.3手动触发ngModelChange事件,splice和push无法触发 + this.model = this.model.concat(); + // 2.4设置全选状态 + if (this.selectAll) { + this.setAllSelectCheckboxState(); + } + } + + // 3.重新设置ti-dominator高度 + this.setPosition(); + } + + /** + * @ignore 全选复选框的按钮事件 + */ + public onSelectAllChange(isAllSelected: any): void { + // 1.将当前tree节点全部选中 或 不选中 + TiTreeUtil.traverse(this.options, (node: TiTreeNode) => { + if (!node.disabled) { + node.checked = isAllSelected; + } + }); + + // 2.设置model + if (!this.isInSearch) { + // 2.1 未处于搜索状态下,使用concat进行单层深拷贝 + !this.isAllSelected ? (this.model = this.options.concat()) : (this.model = []); + } else { + // 2.2 处于搜索状态下 + this.setModelWhenSearch(this.options, isAllSelected); + // 手动触发ngModelChange事件 + this.model = this.model.concat(); + } + } + + /** + * @ignore 全选复选框的点击事件 + */ + public onClickSelectAll(event: any): void { + // 点击时,this.isAllSelected为false,所以要取反 + this.onSelectAllChange(!this.isAllSelected); + event.preventDefault(); + } + + /** + * @ignore + * 失焦情况下,仅关闭面板,不做聚焦等处理 + */ + public onBlur(): void { + this.closeWithoutFocus(); + } + + /** + * @ignore + * 鼠标点击到空白,原本会失焦,此处通过阻止默认事件的方式进行了处理 + */ + public onMouseDownDropOuter(event: any): void { + event.preventDefault(); + } + + /** + * @ignore + * #datatemplate内部滚动条会引起外部滚动条事件触发,引起弹框内的树下拉组件无法使用鼠标拖动滚动条,因此此处阻止事件冒泡 + */ + public onMouseWheel(event: any): void { + event.stopPropagation(); + } +} diff --git a/src/treeselect/lib/src/TiTreeselectModule.ts b/src/treeselect/lib/src/TiTreeselectModule.ts new file mode 100644 index 0000000..5908ddb --- /dev/null +++ b/src/treeselect/lib/src/TiTreeselectModule.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; + +import { TiTreeselectComponent } from './TiTreeselectComponent'; +import { TiDominatorModule } from '@opentiny/ng-dominator'; +import { TiDropsearchModule } from '@opentiny/ng-dropsearch'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiTreeModule } from '@opentiny/ng-tree'; +import { TiSearchboxModule } from '@opentiny/ng-searchbox'; +import { TiCheckboxModule } from '@opentiny/ng-checkbox'; + +@NgModule({ + imports: [ + TiDominatorModule, + TiDropsearchModule, + TiDropModule, + TiTreeModule, + TiSearchboxModule, + TiCheckboxModule, + FormsModule, + CommonModule + ], + exports: [TiTreeselectComponent], + declarations: [TiTreeselectComponent] +}) +export class TiTreeselectModule {} + +export { TiTreeselectComponent } from './TiTreeselectComponent'; diff --git a/src/treeselect/lib/src/treeselect.html b/src/treeselect/lib/src/treeselect.html new file mode 100644 index 0000000..bd21ea3 --- /dev/null +++ b/src/treeselect/lib/src/treeselect.html @@ -0,0 +1,73 @@ + + + + + + + + +
    + + +
  • + +
  • + + +
    +
    +
    + + + +
    +
    diff --git a/src/treeselect/lib/src/treeselect.less b/src/treeselect/lib/src/treeselect.less new file mode 100644 index 0000000..f6d938d --- /dev/null +++ b/src/treeselect/lib/src/treeselect.less @@ -0,0 +1,49 @@ +@import '../../../themes/basic/base-all.less'; +@import '../../../themes/basic/compnent-container-border.less'; + +:host { + --ti-treeselect-container-width: 280px; +} + +:host :extend(.ti3-compnent-container-border all) { + width: var(--ti-treeselect-container-width); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); +} + +ti-drop { + outline: none; +} + +.ti3-treeselect-selectall-option { + list-style: none; + cursor: pointer; + color: inherit; + text-align: left; + line-height: var(--ti-common-line-height-number); + background-color: var(--ti-common-color-bg-white-normal); + padding: var(--ti-common-space-6) var(--ti-common-space-2x); + &:hover { + background-color: var(--ti-common-color-bg-white-emphasize); + color: var(--ti-common-color-text-highlight); + } +} + +.ti3-treeselect-searchbox { + width: 100% !important; +} + +.ti3-treeselect-datacontainer { + overflow-y: auto; + overflow-x: hidden; + padding: var(--ti-common-space-base) var(--ti-common-space-0); + max-height: inherit; + box-sizing: border-box; +} + +.ti3-treeselect-dropdown-no-data { + padding: var(--ti-common-space-6) var(--ti-common-space-10); + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + line-height: var(--ti-common-line-height-number); +} diff --git a/src/tsconfig.lib.json b/src/tsconfig.lib.json new file mode 100644 index 0000000..726be2b --- /dev/null +++ b/src/tsconfig.lib.json @@ -0,0 +1,21 @@ +{ + "extends": "../tsconfig.base.json", + "files": [], + "references": [ + { + "path": "./tsconfig.lib.prod.json" + } + ], + "compilerOptions": { + "outDir": "../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true + } +} \ No newline at end of file diff --git a/src/tsconfig.lib.prod.json b/src/tsconfig.lib.prod.json new file mode 100644 index 0000000..2a2faa8 --- /dev/null +++ b/src/tsconfig.lib.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/src/upload/demo/karma.conf.js b/src/upload/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/upload/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/upload/demo/project.json b/src/upload/demo/project.json new file mode 100644 index 0000000..60324c2 --- /dev/null +++ b/src/upload/demo/project.json @@ -0,0 +1,98 @@ +{ + "projectType": "application", + "root": "src/upload/demo", + "sourceRoot": "src/upload/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/upload", + "index": "src/upload/demo/src/index.html", + "main": "src/upload/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/upload/demo/tsconfig.app.json", + "assets": [ + "src/upload/demo/src/favicon.ico", + "src/upload/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/image", + "output": "/assets/image/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "upload-demo:build:production" + }, + "development": { + "browserTarget": "upload-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js upload" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/upload/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/upload/demo/tsconfig.spec.json", + "karmaConfig": "src/upload/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/upload/demo/src/app/AppComponent.ts b/src/upload/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/upload/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/upload/demo/src/app/AppModule.ts b/src/upload/demo/src/app/AppModule.ts new file mode 100644 index 0000000..f807c0d --- /dev/null +++ b/src/upload/demo/src/app/AppModule.ts @@ -0,0 +1,28 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { UploadTestModule } from './upload/UploadTestModule'; +import { UploadimageTestModule } from './uploadimage/UploadimageTestModule'; + +@NgModule({ + imports: [ + UploadTestModule, + UploadimageTestModule, + BrowserAnimationsModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/upload/demo/src/app/IndexComponent.ts b/src/upload/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..677850c --- /dev/null +++ b/src/upload/demo/src/app/IndexComponent.ts @@ -0,0 +1,52 @@ +import { Component } from '@angular/core'; +import { UploadTestModule } from './upload/UploadTestModule'; +import { UploadimageTestModule } from './uploadimage/UploadimageTestModule'; + +@Component({ + template: ` + + + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + moudles: Array = [UploadTestModule.ROUTES, UploadimageTestModule.ROUTES]; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/upload/demo/src/app/app.html b/src/upload/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/upload/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/upload/demo/src/app/upload/UploadAutoUploadComponent.ts b/src/upload/demo/src/app/upload/UploadAutoUploadComponent.ts new file mode 100644 index 0000000..ea5b62e --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadAutoUploadComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./upload-auto-upload.html` +}) +export class UploadAutoUploadComponent {} diff --git a/src/upload/demo/src/app/upload/UploadBasicComponent.ts b/src/upload/demo/src/app/upload/UploadBasicComponent.ts new file mode 100644 index 0000000..46aad7f --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadBasicComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./upload-basic.html` +}) +export class UploadBasicComponent {} diff --git a/src/upload/demo/src/app/upload/UploadBatchSendComponent.ts b/src/upload/demo/src/app/upload/UploadBatchSendComponent.ts new file mode 100644 index 0000000..069b419 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadBatchSendComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./upload-batch-send.html` +}) +export class UploadBatchSendComponent {} diff --git a/src/upload/demo/src/app/upload/UploadBeforeremoveComponent.ts b/src/upload/demo/src/app/upload/UploadBeforeremoveComponent.ts new file mode 100644 index 0000000..da2127e --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadBeforeremoveComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiFileItem } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-beforeRemove.html` +}) +export class UploadBeforeremoveComponent { + url: string = '/upload'; + + onBeforeRemoveItems(fileItemArry: Array): void { + setTimeout(() => { + fileItemArry[0].remove(); + }, 1000); + } +} diff --git a/src/upload/demo/src/app/upload/UploadButtonComponent.ts b/src/upload/demo/src/app/upload/UploadButtonComponent.ts new file mode 100644 index 0000000..46f6be1 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadButtonComponent.ts @@ -0,0 +1,6 @@ +import { Component, ViewChild } from '@angular/core'; + +@Component({ + templateUrl: `./upload-button.html` +}) +export class UploadButtonComponent {} diff --git a/src/upload/demo/src/app/upload/UploadButtonTestComponent.ts b/src/upload/demo/src/app/upload/UploadButtonTestComponent.ts new file mode 100644 index 0000000..9fb9a4e --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadButtonTestComponent.ts @@ -0,0 +1,77 @@ +import { Component } from '@angular/core'; +import { TiFileItem, TiFilter } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-button-test.html` +}) +export class UploadButtonTestComponent { + type: string = 'button'; + disabled: boolean = true; + url: string = '/upload'; + autoUpload: boolean = false; + batchSend: boolean = true; + method: string = 'get'; + alias: string = 'tiFileRename'; + formDataFirst: boolean = true; + headers: any = { + tiheadersConfig: 'aa' + }; + formData: any = {}; + filters: Array = [ + { + name: 'maxCount', + params: [5] + } + ]; + onAddItemSuccess(fileItem: TiFileItem): void { + console.log(`filename:${fileItem.file.name}`); + } + onAddItemFailed(fileItem: TiFileItem): void { + console.log(`filename:${fileItem.file.name}`); + } + onBeforeSendItems(fileItems: Array): void { + fileItems.forEach((item: TiFileItem) => { + item.formData = { + tinyFormdata: 'aa' + }; + }); + } + onProgressItems($event: any): void { + console.log('onProgressItems'); + console.log($event.fileItems); + console.log($event.progress); + } + onSuccessItems($event: any): void { + console.log('onSuccessItems'); + console.log($event.fileItems); + console.log($event.response); + console.log($event.status); + } + onErrorItems($event: any): void { + console.log('onErrorItems'); + console.log($event.fileItems); + console.log($event.response); + console.log($event.status); + } + onCancelItems($event: any): void { + console.log('onCancelItems'); + console.log($event.fileItems); + console.log($event.response); + console.log($event.status); + } + onRemoveItems($event: any): void { + console.log('onRemoveItems'); + console.log($event); + } + onCompleteItems($event: any): void { + // 根据状态码和返回消息设置详情信息 + console.log(`response${$event.response}`); + } + onCompleteAllItems($event: any): void { + console.log('onCompleteAllItems'); + console.log($event); + } + changeDisable(): void { + this.disabled = !this.disabled; + } +} diff --git a/src/upload/demo/src/app/upload/UploadCaseTestComponent.ts b/src/upload/demo/src/app/upload/UploadCaseTestComponent.ts new file mode 100644 index 0000000..7f678e7 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadCaseTestComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiFileItem } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-case-test.html` +}) +export class UploadCaseTestComponent { + url: string = '/upload'; + autoUpload: boolean = false; + + onBeforeSendItems(files: Array): void { + console.log('onBeforeSendItems', files); + files[0].uploader.cancelAll(); // 在上传前全部取消 + } +} diff --git a/src/upload/demo/src/app/upload/UploadChangesComponent.ts b/src/upload/demo/src/app/upload/UploadChangesComponent.ts new file mode 100644 index 0000000..8175ea1 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadChangesComponent.ts @@ -0,0 +1,29 @@ +import { Component, ViewChild } from '@angular/core'; +import { TiFilter, TiUploadComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-changes.html` +}) +export class UploadChangesComponent { + @ViewChild(TiUploadComponent) uploadCom: TiUploadComponent; + myLogs: Array = []; + buttonText: string = '按钮样式'; + inputFieldWidth: string = '500px'; + url: string = '/upload'; + headers: any = { + tiheadersConfig: 'aa' + }; + beforeSendItems($event: Event): void { + this.myLogs = [...this.myLogs, `url='${$event[0].url}';headers=${JSON.stringify($event[0].headers)}`]; + } + changeUrl(): void { + this.url = '/newUrl'; + this.myLogs = [...this.myLogs, `修改url='${this.url}'`]; + } + changeHeaders(): void { + this.headers = { + newTiheadersConfig: 'newConfig' + }; + this.myLogs = [...this.myLogs, `修改headers=${JSON.stringify(this.headers)}`]; + } +} diff --git a/src/upload/demo/src/app/upload/UploadChunksizeComponent.ts b/src/upload/demo/src/app/upload/UploadChunksizeComponent.ts new file mode 100644 index 0000000..efecaa5 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadChunksizeComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TiFileItem } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-chunksize.html` +}) +export class UploadChunksizeComponent { + myLogs: Array = []; + chunkSize: number = 1024 * 1024; + onAddItemSuccess(fileItem: TiFileItem): void { + this.myLogs = [...this.myLogs, `onAddItemSuccess() filename:${fileItem.file.name}`]; + } + onAddItemFailed(fileItem: TiFileItem): void { + this.myLogs = [...this.myLogs, `onAddItemFailed() filename:${fileItem.file.name}`]; + } + onBeforeSendItems(fileItems: Array): void { + this.myLogs = [...this.myLogs, `onBeforeSendItems()`]; + fileItems.forEach((item: TiFileItem) => { + item.formData = { + tinyFormdata: 'Custom data' + }; + }); + } + onProgressItems($event: any): void { + this.myLogs = [...this.myLogs, `onProgressItems() progress:${$event.progress}`]; + } + onSuccessItems($event: any): void { + this.myLogs = [...this.myLogs, `onSuccessItems() response:${$event.response} status:${$event.status}`]; + } + onErrorItems($event: any): void { + this.myLogs = [...this.myLogs, `onErrorItems() response:${$event.response} status:${$event.status}`]; + } + onCancelItems($event: any): void { + this.myLogs = [...this.myLogs, `onCancelItems() response:${$event.response} status:${$event.status}`]; + } + onRemoveItems($event: any): void { + this.myLogs = [...this.myLogs, `onRemoveItems()`]; + } + onBeforeRemoveItems(fileItemArry: Array): void { + this.myLogs = [...this.myLogs, `onBeforeRemoveItems()`]; + fileItemArry[0].remove(); + } + onCompleteItems($event: any): void { + this.myLogs = [...this.myLogs, `onCompleteItems() response:${$event.response}`]; + } + onCompleteAllItems($event: any): void { + this.myLogs = [...this.myLogs, `onCompleteAllItems()`]; + } +} diff --git a/src/upload/demo/src/app/upload/UploadCustomComponent.ts b/src/upload/demo/src/app/upload/UploadCustomComponent.ts new file mode 100644 index 0000000..527ea8d --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadCustomComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./upload-custom.html` +}) +export class UploadCustomComponent { + myLogs: Array = []; + customHeaders: any = { + tiCustomHeader: 'TinyNG' + }; + customFormData: any = { + tiCustomFormData: 'hello tiny' + }; + beforeSendItems($event: Event): void { + this.myLogs = [ + ...this.myLogs, + `请求方式:${$event[0].method}; 文件名:'${$event[0].alias}'; 头信息:${JSON.stringify( + $event[0].headers + )}; 附加信息:${JSON.stringify($event[0].formData)}` + ]; + } +} diff --git a/src/upload/demo/src/app/upload/UploadEventComponent.ts b/src/upload/demo/src/app/upload/UploadEventComponent.ts new file mode 100644 index 0000000..60c9ae3 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadEventComponent.ts @@ -0,0 +1,48 @@ +import { Component } from '@angular/core'; +import { TiFileItem, TiFilter } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-event.html` +}) +export class UploadEventComponent { + myLogs: Array = []; + onAddItemSuccess(fileItem: TiFileItem): void { + this.myLogs = [...this.myLogs, `onAddItemSuccess() filename:${fileItem.file.name}`]; + } + onAddItemFailed(fileItem: TiFileItem): void { + this.myLogs = [...this.myLogs, `onAddItemFailed() filename:${fileItem.file.name}`]; + } + onBeforeSendItems(fileItems: Array): void { + this.myLogs = [...this.myLogs, `onBeforeSendItems()`]; + fileItems.forEach((item: TiFileItem) => { + item.formData = { + tinyFormdata: 'Custom data' + }; + }); + } + onProgressItems($event: any): void { + this.myLogs = [...this.myLogs, `onProgressItems() progress:${$event.progress}`]; + } + onSuccessItems($event: any): void { + this.myLogs = [...this.myLogs, `onSuccessItems() response:${$event.response} status:${$event.status}`]; + } + onErrorItems($event: any): void { + this.myLogs = [...this.myLogs, `onErrorItems() response:${$event.response} status:${$event.status}`]; + } + onCancelItems($event: any): void { + this.myLogs = [...this.myLogs, `onCancelItems() response:${$event.response} status:${$event.status}`]; + } + onRemoveItems($event: any): void { + this.myLogs = [...this.myLogs, `onRemoveItems()`]; + } + onBeforeRemoveItems(fileItemArry: Array): void { + this.myLogs = [...this.myLogs, `onBeforeRemoveItems()`]; + fileItemArry[0].remove(); + } + onCompleteItems($event: any): void { + this.myLogs = [...this.myLogs, `onCompleteItems() response:${$event.response}`]; + } + onCompleteAllItems($event: any): void { + this.myLogs = [...this.myLogs, `onCompleteItems()`]; + } +} diff --git a/src/upload/demo/src/app/upload/UploadFilterComponent.ts b/src/upload/demo/src/app/upload/UploadFilterComponent.ts new file mode 100644 index 0000000..c7de7c5 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadFilterComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiFilter } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-filter.html` +}) +export class UploadFilterComponent { + filters: Array = [ + { + name: 'type', + params: ['.png,.img'] + }, + { + name: 'maxSize', + params: [102400] + }, + { + name: 'maxCount', + params: [2] + } + ]; + myLogs: Array = []; + onAddItemFailed(event: any): void { + this.myLogs = [...this.myLogs, `errorType:${event.validResults.toString()}----filename:${event.file.name}`]; + } + onAddItemSuccess(event: any): void { + this.myLogs = [...this.myLogs, `success----filename:${event.file.name}`]; + } +} diff --git a/src/upload/demo/src/app/upload/UploadFormDataComponent.ts b/src/upload/demo/src/app/upload/UploadFormDataComponent.ts new file mode 100644 index 0000000..21ae5a5 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadFormDataComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { TiFileItem } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-form-data.html` +}) +export class UploadFormDataComponent { + inputValue: string = 'aa'; + url: string = '/upload'; + onBeforeSendItems(fileItems: Array): void { + // 上传前动态添加formData + fileItems.forEach((item: TiFileItem) => { + item.formData = { + inputValut: this.inputValue, + name: 'xxy' + }; + }); + } +} diff --git a/src/upload/demo/src/app/upload/UploadInitfilesTestComponent.ts b/src/upload/demo/src/app/upload/UploadInitfilesTestComponent.ts new file mode 100644 index 0000000..380c791 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadInitfilesTestComponent.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core'; +import { TiFileItem, TiFilter } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-initfiles-test.html` +}) +export class UploadInitfilesTestComponent { + initFiles: Array = [ + { + name: 'demo.txt' + }, + { + name: 'demo.sh' + } + ]; + filters: Array = [ + { + name: 'maxCount', + params: [4] + } + ]; + initFiles1: Array = [ + { + name: 'demo.txt' + } + ]; + filters1: Array = [ + { + name: 'maxCount', + params: [1] + } + ]; + onBeforeRemoveItems(fileItemArry: Array): void { + fileItemArry[0].remove(); + } +} diff --git a/src/upload/demo/src/app/upload/UploadInputFieldTestComponent.ts b/src/upload/demo/src/app/upload/UploadInputFieldTestComponent.ts new file mode 100644 index 0000000..239e30d --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadInputFieldTestComponent.ts @@ -0,0 +1,83 @@ +import { Component } from '@angular/core'; +import { TiFileInfo, TiFileItem, TiFilter } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-input-field-test.html` +}) +export class UploadInputFieldTestComponent { + disabled: boolean = true; + url: string = '/upload'; + autoUpload: boolean = false; + batchSend: boolean = true; + method: string = 'get'; + alias: string = 'tiFileRename'; + placeholder: string = 'please select file'; + formDataFirst: boolean = true; + headers: any = { + tiheadersConfig: 'aa' + }; + formData: any = {}; + filters: Array = [ + { + name: 'maxCount', + params: [5] + } + ]; + filters1: Array = [ + { + name: 'maxCount', + params: [1] + } + ]; + onAddItemSuccess(fileItem: TiFileItem): void { + console.log(`filename:${fileItem.file.name}`); + } + onAddItemFailed(fileInfo: TiFileInfo): void { + console.log(`filename:${fileInfo.name}`); + } + onBeforeSendItems(fileItems: Array): void { + fileItems.forEach((item: TiFileItem) => { + item.formData = { + tinyFormdata: 'aa' + }; + }); + } + onProgressItems($event: any): void { + console.log('onProgressItems'); + console.log($event.fileItems); + console.log($event.progress); + } + onSuccessItems($event: any): void { + console.log('onSuccessItems'); + console.log($event.fileItems); + console.log($event.response); + console.log($event.status); + } + onErrorItems($event: any): void { + console.log('onErrorItems'); + console.log($event.fileItems); + console.log($event.response); + console.log($event.status); + } + onCancelItems($event: any): void { + console.log('onCancelItems'); + console.log($event.fileItems); + console.log($event.response); + console.log($event.status); + } + onRemoveItems($event: any): void { + console.log('onRemoveItems'); + console.log($event); + } + onCompleteItems($event: any): void { + // 根据状态码和返回消息设置详情信息 + console.log(`response${$event.response}`); + } + onCompleteAllItems($event: any): void { + console.log('onCompleteAllItems'); + console.log($event); + } + changeDisable(): void { + this.disabled = false; + } +} diff --git a/src/upload/demo/src/app/upload/UploadPropsComponent.ts b/src/upload/demo/src/app/upload/UploadPropsComponent.ts new file mode 100644 index 0000000..a401497 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadPropsComponent.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./upload-props.html` +}) +export class UploadPropsComponent { + public isOpen: boolean = true; + public showUploadList: boolean = true; + public showErrorMessage: boolean = true; + initFiles: Array = [ + { + name: 'demo.txt', + allowDelete: false + }, + { + name: 'demo.sh' + } + ]; + changeDisabled(): void { + this.isOpen = !this.isOpen; + } + changeShowList(): void { + this.showUploadList = !this.showUploadList; + } + changeShowError(): void { + this.showErrorMessage = !this.showErrorMessage; + } +} diff --git a/src/upload/demo/src/app/upload/UploadServiceComponent.ts b/src/upload/demo/src/app/upload/UploadServiceComponent.ts new file mode 100644 index 0000000..d73a7c5 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadServiceComponent.ts @@ -0,0 +1,72 @@ +import { Component } from '@angular/core'; +import { TiFileInfo, TiFileItem, TiUploadConfig, TiUploadRef, TiUploadService } from '@opentiny/ng'; + +@Component({ + templateUrl: './upload-service.html' +}) +export class UploadServiceComponent { + uploader: TiUploadRef; + constructor(uploaderService: TiUploadService) { + const config: TiUploadConfig = { + url: '/upload', + onAddItemFailed: (fileObject: TiFileInfo, validResults: Array): void => { + console.log('onAddItemFailed'); + console.log('onAddItemFailed.fileObject:', fileObject); + console.log('onAddItemFailed.validResults:', validResults); + }, + onAddItemSuccess: (fileItem: TiFileItem): void => { + // 需要给每个文件设置allowDelete和allowReload才能调用相应的事件 + // 单文件上传需要设置allowDelete = true才能替换原有的文件 + fileItem.allowDelete = true; + fileItem.allowReload = true; + }, + onBeforeSendItems: (fileItems: Array): void => { + for (let i: number = 0; i < fileItems.length; i++) { + fileItems[i].formData = { + data: `data${i}` + }; + } + }, + onProgressItems: (fileItems: Array, progress: number): void => { + console.log(`onProgressItems`); + console.log('onProgressItems.fileItems:', fileItems); + console.log('onProgressItems.progress:', progress); + }, + onCompleteItems: (fileItems: Array, response: string, status: number): void => { + console.log('onCompleteItems'); + console.log('onCompleteItems.fileItems:', fileItems); + console.log(`onCompleteItems.response:${response}`); + console.log(`onCompleteItems.status:${status}`); + }, + onSuccessItems: (fileItems: Array, response: string, status: number): void => { + // 根据状态码和返回消息设置详情信息 + console.log('onSuccessItems'); + console.log('onSuccessItems.fileItems:', fileItems); + console.log(`onSuccessItems.response:${response}`); + console.log(`onSuccessItems.status:${status}`); + }, + onErrorItems: (fileItems: Array, response: string, status: number): void => { + // 根据状态码和返回消息设置详情信息 + console.log('onErrorItems'); + console.log('onErrorItems.fileItems:', fileItems); + console.log(`onErrorItems.response:${response}`); + console.log(`onErrorItems.status:${status}`); + }, + onCancelItems: (fileItems: Array, response: string, status: number): void => { + console.log('onCancelItems'); + console.log('onCancelItems.fileItems:', fileItems); + console.log(`onCancelItems.response:${response}`); + console.log(`onCancelItems.status:${status}`); + }, + onRemoveItems: (fileItems: Array): void => { + console.log('onRemoveItems'); + console.log('onRemoveItems.fileItems:', fileItems); + }, + onCompleteAllItems: (fileItems: Array): void => { + console.log('onCompleteAllItems'); + console.log('onCompleteAllItems.fileItems', fileItems); + } + }; + this.uploader = uploaderService.create(config); + } +} diff --git a/src/upload/demo/src/app/upload/UploadServiceTestComponent.ts b/src/upload/demo/src/app/upload/UploadServiceTestComponent.ts new file mode 100644 index 0000000..583799c --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadServiceTestComponent.ts @@ -0,0 +1,89 @@ +import { Component } from '@angular/core'; +import { TiFileInfo, TiFileItem, TiUploadConfig, TiUploadRef, TiUploadService } from '@opentiny/ng'; + +@Component({ + templateUrl: './upload-service-test.html' +}) +export class UploadServiceTestComponent { + uploader: TiUploadRef; + uploader1: TiUploadRef; + constructor(uploaderService: TiUploadService) { + const config: TiUploadConfig = { + url: '/upload', + alias: 'myFileDdd', + headers: { + 'header-tt': 'aa' + }, + filters: [ + { + name: 'maxCount', + params: [5] + } + ], + onAddItemFailed: (fileObject: TiFileInfo, validResults: Array): void => { + console.log('onAddItemFailed'); + console.log(fileObject); + console.log(validResults); + }, + onAddItemSuccess: (fileItem: TiFileItem): void => { + console.log(`filename:${fileItem.file.name}`); + }, + onBeforeSendItems: (fileItems: Array): void => { + for (let i: number = 0; i < fileItems.length; i++) { + fileItems[i].formData = { + data: `data${i}` + }; + } + }, + onProgressItems: (fileItems: Array, progress: number): void => { + console.log(`onProgressItems`); + console.log(fileItems); + console.log(progress); + }, + onCompleteItems: (fileItems: Array, response: string, status: number): void => { + // 根据状态码和返回消息设置详情信息 + console.log('onCompleteItems'); + console.log(fileItems); + console.log(`response:${response}`); + console.log(`status:${status}`); + }, + onSuccessItems: (fileItems: Array, response: string, status: number): void => { + // 根据状态码和返回消息设置详情信息 + console.log('onSuccessItems'); + console.log(fileItems); + console.log(`response:${response}`); + console.log(`status:${status}`); + }, + onErrorItems: (fileItems: Array, response: string, status: number): void => { + // 根据状态码和返回消息设置详情信息 + console.log('onErrorItems'); + console.log(fileItems); + console.log(`response:${response}`); + console.log(`status:${status}`); + }, + onCancelItems: (fileItems: Array, response: string, status: number): void => { + console.log('onCancelItems'); + console.log(fileItems); + console.log(`response:${response}`); + console.log(`status:${status}`); + }, + onRemoveItems: (fileItems: Array): void => { + console.log('onRemoveItems'); + console.log(fileItems); + }, + onCompleteAllItems: (fileItems: Array): void => { + console.log('onCompleteAllItems'); + console.log(fileItems); + } + }; + this.uploader = uploaderService.create({ + ...config, + autoUpload: true + }); + this.uploader1 = uploaderService.create({ + ...config, + batchSend: true, + autoUpload: false + }); + } +} diff --git a/src/upload/demo/src/app/upload/UploadSingleComponent.ts b/src/upload/demo/src/app/upload/UploadSingleComponent.ts new file mode 100644 index 0000000..932210e --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadSingleComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { TiFilter } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-single.html` +}) +export class UploadSingleComponent { + url: string = '/upload'; + placeholder: string = '单文件上传'; + // maxCount定义为1时,代表单文件上传 + filters: Array = [ + { + name: 'maxCount', + params: [1] + } + ]; +} diff --git a/src/upload/demo/src/app/upload/UploadTestModule.ts b/src/upload/demo/src/app/upload/UploadTestModule.ts new file mode 100644 index 0000000..abd6bad --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadTestModule.ts @@ -0,0 +1,132 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiSwitchModule, TiTextModule, TiUploadModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { UploadBasicComponent } from './UploadBasicComponent'; +import { UploadButtonComponent } from './UploadButtonComponent'; +import { UploadPropsComponent } from './UploadPropsComponent'; +import { UploadAutoUploadComponent } from './UploadAutoUploadComponent'; +import { UploadBatchSendComponent } from './UploadBatchSendComponent'; +import { UploadEventComponent } from './UploadEventComponent'; +import { UploadFilterComponent } from './UploadFilterComponent'; +import { UploadFormDataComponent } from './UploadFormDataComponent'; +import { UploadServiceComponent } from './UploadServiceComponent'; +import { UploadSingleComponent } from './UploadSingleComponent'; +import { UploadServiceTestComponent } from './UploadServiceTestComponent'; +import { UploadInputFieldTestComponent } from './UploadInputFieldTestComponent'; +import { UploadButtonTestComponent } from './UploadButtonTestComponent'; +import { UploadCaseTestComponent } from './UploadCaseTestComponent'; +import { UploadBeforeremoveComponent } from './UploadBeforeremoveComponent'; +import { UploadChangesComponent } from './UploadChangesComponent'; +import { UploadCustomComponent } from './UploadCustomComponent'; +import { UploadInitfilesTestComponent } from './UploadInitfilesTestComponent'; +import { UploadChunksizeComponent } from './UploadChunksizeComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiTextModule, + TiButtonModule, + TiUploadModule, + TiSwitchModule, + DemoLogModule, + RouterModule.forChild(UploadTestModule.ROUTES) + ], + declarations: [ + UploadBasicComponent, + UploadButtonComponent, + UploadPropsComponent, + UploadServiceComponent, + UploadAutoUploadComponent, + UploadFormDataComponent, + UploadFilterComponent, + UploadSingleComponent, + UploadBatchSendComponent, + UploadEventComponent, + UploadServiceTestComponent, + UploadInputFieldTestComponent, + UploadButtonTestComponent, + UploadBeforeremoveComponent, + UploadChangesComponent, + UploadCaseTestComponent, + UploadCustomComponent, + UploadInitfilesTestComponent, + UploadChunksizeComponent + ] +}) +export class UploadTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiUploadComponent.html', label: 'Upload' }, + { href: 'directives/TiFileSelectDirective.html', label: 'FileSelect' } + ]; + static readonly ROUTES: Routes = [ + { + path: 'upload/upload-basic', + component: UploadBasicComponent + }, + { + path: 'upload/upload-button', + component: UploadButtonComponent + }, + { + path: 'upload/upload-props', + component: UploadPropsComponent + }, + { + path: 'upload/upload-service', + component: UploadServiceComponent + }, + { + path: 'upload/upload-auto-upload', + component: UploadAutoUploadComponent + }, + { + path: 'upload/upload-form-data', + component: UploadFormDataComponent + }, + { + path: 'upload/upload-filter', + component: UploadFilterComponent + }, + { + path: 'upload/upload-single', + component: UploadSingleComponent + }, + { + path: 'upload/upload-batch-send', + component: UploadBatchSendComponent + }, + { + path: 'upload/upload-beforeRemove', + component: UploadBeforeremoveComponent + }, + { + path: 'upload/upload-event', + component: UploadEventComponent + }, + { + path: 'upload/upload-changes', + component: UploadChangesComponent + }, + { path: 'upload/upload-chunksize', component: UploadChunksizeComponent }, + { + path: 'upload/uploadupload-service-test', + component: UploadServiceTestComponent + }, + { + path: 'upload/upload-input-field-test', + component: UploadInputFieldTestComponent + }, + { path: 'upload/upload-button-test', component: UploadButtonTestComponent }, + { path: 'upload/upload-case-test', component: UploadCaseTestComponent }, + { + path: 'upload/upload-initfiles-test', + component: UploadInitfilesTestComponent + } + ]; +} diff --git a/src/upload/demo/src/app/upload/upload-auto-upload.html b/src/upload/demo/src/app/upload/upload-auto-upload.html new file mode 100644 index 0000000..d0a86af --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-auto-upload.html @@ -0,0 +1,2 @@ + + diff --git a/src/upload/demo/src/app/upload/upload-basic.html b/src/upload/demo/src/app/upload/upload-basic.html new file mode 100644 index 0000000..2266669 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-basic.html @@ -0,0 +1 @@ + diff --git a/src/upload/demo/src/app/upload/upload-batch-send.html b/src/upload/demo/src/app/upload/upload-batch-send.html new file mode 100644 index 0000000..94f2c3e --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-batch-send.html @@ -0,0 +1 @@ + diff --git a/src/upload/demo/src/app/upload/upload-beforeremove.html b/src/upload/demo/src/app/upload/upload-beforeremove.html new file mode 100644 index 0000000..f129263 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-beforeremove.html @@ -0,0 +1,5 @@ +

    描述

    +

    通过beforeRemoveItems定义用户点击删除按钮,文件删除前事件

    +

    示例

    +

    点击删除按钮,延时1秒后文件删除

    + diff --git a/src/upload/demo/src/app/upload/upload-button-test.html b/src/upload/demo/src/app/upload/upload-button-test.html new file mode 100644 index 0000000..82c8aa1 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-button-test.html @@ -0,0 +1,78 @@ +

    基本功能+灰化设置

    + + + +

    合并上传测试

    + + + +

    非自动上传,不带filter测试

    + + + +

    第三个文件上传的详情显示

    +
    +

    {{item.file.name}}

    +

    {{item.progress}}

    +

    item isReady:{{item.isReady}}

    +

    item isUploading:{{item.isUploading}}

    +

    item isUploaded:{{item.isUploaded}}

    +

    item isCancel:{{item.isCancel}}

    +

    item isSuccess:{{item.isSuccess}}

    +

    item isError:{{item.isError}}

    + 重新上传 + 取消 + 删除 +
    diff --git a/src/upload/demo/src/app/upload/upload-button.html b/src/upload/demo/src/app/upload/upload-button.html new file mode 100644 index 0000000..de9c2d9 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-button.html @@ -0,0 +1,3 @@ + +
    + diff --git a/src/upload/demo/src/app/upload/upload-case-test.html b/src/upload/demo/src/app/upload/upload-case-test.html new file mode 100644 index 0000000..f34cb0f --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-case-test.html @@ -0,0 +1,5 @@ +

    描述

    +

    开发者在使用时组件有问题的业务场景,供测试使用。

    +

    场景1:

    +

    合并上传时,在beforeSendItems中取消全部上传:

    + diff --git a/src/upload/demo/src/app/upload/upload-changes.html b/src/upload/demo/src/app/upload/upload-changes.html new file mode 100644 index 0000000..1c3ec8a --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-changes.html @@ -0,0 +1,46 @@ +

    描述

    +

    文件上传组件 - 动态变更展示

    +

    动态变更支持url/headers

    +

    导入

    +

    import {{ '{' }} TiUploadModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    + +

    事件日志:

    +
    +
    +

    + {{i+' '+log}} +

    +
    +
    +
    + +

    1、默认按钮样式

    +
    +
    + + +
    +
    +

    3、inputField样式

    + + +
    +

    3、操作

    +
    +url = '{{url}}'' +
    +headers = '{{headers | json}}' diff --git a/src/upload/demo/src/app/upload/upload-chunksize.html b/src/upload/demo/src/app/upload/upload-chunksize.html new file mode 100644 index 0000000..e74cef5 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-chunksize.html @@ -0,0 +1,17 @@ + + diff --git a/src/upload/demo/src/app/upload/upload-custom.html b/src/upload/demo/src/app/upload/upload-custom.html new file mode 100644 index 0000000..028dc22 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-custom.html @@ -0,0 +1,11 @@ + + diff --git a/src/upload/demo/src/app/upload/upload-event.html b/src/upload/demo/src/app/upload/upload-event.html new file mode 100644 index 0000000..d2a58a9 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-event.html @@ -0,0 +1,16 @@ + + diff --git a/src/upload/demo/src/app/upload/upload-filter.html b/src/upload/demo/src/app/upload/upload-filter.html new file mode 100644 index 0000000..1760284 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-filter.html @@ -0,0 +1,8 @@ + + diff --git a/src/upload/demo/src/app/upload/upload-form-data.html b/src/upload/demo/src/app/upload/upload-form-data.html new file mode 100644 index 0000000..8b1a700 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-form-data.html @@ -0,0 +1,9 @@ +

    描述

    +

    + 文件上传附加信息定义使用示例,可通过两种方式添加附加信息:
    + 1.静态信息:可通过定义formData属性方式添加
    + 2.动态信息:可通过在beforeSendItems事件回调中动态定义item的formData属性方式添加(本示例中展示的是该种用法) +

    +

    示例

    + + diff --git a/src/upload/demo/src/app/upload/upload-initfiles-test.html b/src/upload/demo/src/app/upload/upload-initfiles-test.html new file mode 100644 index 0000000..cb806ad --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-initfiles-test.html @@ -0,0 +1,9 @@ +

    描述

    +

    同时设置 initFiles 和 maxCount 时,供测试使用。

    +

    场景1:

    +

    直接删除 initFiles 中文件:

    + +

    场景2:

    +

    调用 remove 方法删除 initFiles 中文件:

    + + diff --git a/src/upload/demo/src/app/upload/upload-input-field-test.html b/src/upload/demo/src/app/upload/upload-input-field-test.html new file mode 100644 index 0000000..1529ca4 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-input-field-test.html @@ -0,0 +1,93 @@ +

    基本功能+灰化设置

    + + + +

    合并上传测试

    + + +

    单文件上传测试

    + + +

    非自动上传,不带filter测试

    + + + +

    第三个文件上传的详情显示

    +
    +

    {{item.file.name}}

    +

    {{item.progress}}

    +

    item isReady:{{item.isReady}}

    +

    item isUploading:{{item.isUploading}}

    +

    item isUploaded:{{item.isUploaded}}

    +

    item isCancel:{{item.isCancel}}

    +

    item isSuccess:{{item.isSuccess}}

    +

    item isError:{{item.isError}}

    + 重新上传 + 取消 + 删除 +
    diff --git a/src/upload/demo/src/app/upload/upload-props.html b/src/upload/demo/src/app/upload/upload-props.html new file mode 100644 index 0000000..32fe72a --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-props.html @@ -0,0 +1,18 @@ + + +
    + + + diff --git a/src/upload/demo/src/app/upload/upload-service-test.html b/src/upload/demo/src/app/upload/upload-service-test.html new file mode 100644 index 0000000..fd3ddcb --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-service-test.html @@ -0,0 +1,39 @@ +

    描述

    +

    + 文件上传服务使用示例,适用于自定义上传样式场景:
    + 通过tiFileSelect指令定义上传实例对象;通过上传实例的queue属性读取上传文件信息及调用方法 +

    +

    示例

    +

    文件逐个上传,自动上传

    + +
    + {{item.file.name}} + {{item.progress}} + item isReady:{{item.isReady}} + item isUploading:{{item.isUploading}} + item isUploaded:{{item.isUploaded}} + item isCancel:{{item.isCancel}} + item isSuccess:{{item.isSuccess}} + item isError:{{item.isError}} + 重新上传 + 取消 + 删除 +
    + +

    文件合并上传,手动上传

    + + + +
    + {{item.file.name}} + {{item.progress}} + item isReady:{{item.isReady}} + item isUploading:{{item.isUploading}} + item isUploaded:{{item.isUploaded}} + item isCancel:{{item.isCancel}} + item isSuccess:{{item.isSuccess}} + item isError:{{item.isError}} + 重新上传 + 取消 + 删除 +
    diff --git a/src/upload/demo/src/app/upload/upload-service.html b/src/upload/demo/src/app/upload/upload-service.html new file mode 100644 index 0000000..1ee6b43 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-service.html @@ -0,0 +1,37 @@ + +
    + {{item.file.name}} + -- + {{item.progress}} + -- + item isReady:{{item.isReady}} + -- + item isUploading:{{item.isUploading}} + -- + item isUploaded:{{item.isUploaded}} + -- + item isCancel:{{item.isCancel}} + -- + item isSuccess:{{item.isSuccess}} + -- + item isError:{{item.isError}} + -- + 重新上传 + -- + 取消 + -- + 删除 + -- +
    + diff --git a/src/upload/demo/src/app/upload/upload-single.html b/src/upload/demo/src/app/upload/upload-single.html new file mode 100644 index 0000000..0b16faa --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-single.html @@ -0,0 +1,4 @@ +

    描述

    +

    单文件上传使用示例: 定义filters过滤条件maxCount为1时,代表单文件上传

    +

    示例

    + diff --git a/src/upload/demo/src/app/upload/webdoc/upload-demos.js b/src/upload/demo/src/app/upload/webdoc/upload-demos.js new file mode 100644 index 0000000..469b3b1 --- /dev/null +++ b/src/upload/demo/src/app/upload/webdoc/upload-demos.js @@ -0,0 +1,157 @@ +export default { + column: '2', + demos: [ + { + demoId: 'upload-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    Upload 组件的最简用法。通过属性url配置上传地址。

    ', + 'en-US': '

    ', + }, + apis: ['TiUploadComponent.properties.url'], + }, + { + demoId: 'upload-button', + name: { + 'zh-CN': '按钮类型', + 'en-US': 'Button Type', + }, + desc: { + 'zh-CN': + '

    通过属性type配置上传按钮的类型,包括buttontextButtoninputField,通过属性buttonText配置按钮文字。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadComponent.properties.type', + 'TiUploadComponent.properties.buttonText', + ], + }, + { + demoId: 'upload-props', + name: { + 'zh-CN': '常用属性', + 'en-US': 'Used props', + }, + desc: { + 'zh-CN': + '

    通过属性note配置 note 信息;通过属性errorMessage配置文件上传失败的提示信息;通过属性listMaxHeight配置文件列表区域最大高度;通过属性method配置请求方式,包括getpost;通过属性disabled配置禁用状态;通过属性showUploadList配置是否显示文件列表,注意:不会隐藏初始文件;通过属性showErrorMessage配置是否在文件上传失败时显示提示信息;通过属性initFiles配置初始文件列表;

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadComponent.properties.note', + 'TiUploadComponent.properties.errorMessage', + 'TiUploadComponent.properties.listMaxHeight', + 'TiUploadComponent.properties.disabled', + 'TiUploadComponent.properties.showUploadList', + 'TiUploadComponent.properties.showErrorMessage', + 'TiUploadComponent.properties.initFiles', + ], + }, + { + demoId: 'upload-custom', + name: { + 'zh-CN': '自定义上传信息', + 'en-US': 'Used props', + }, + desc: { + 'zh-CN': + '

    通过属性method配置请求方式,包括getpost;通过属性headers配置上传时的头信息;通过属性alias配置上传文件字段唯一标识的键值;通过属性formData配置文件上传的附带信息;通过属性formDataFirst配置附带信息是否先于上传文件对象。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadComponent.properties.method', + 'TiUploadComponent.properties.alias', + 'TiUploadComponent.properties.headers', + 'TiUploadComponent.properties.formData', + 'TiUploadComponent.properties.formDataFirst', + ], + }, + { + demoId: 'upload-filter', + name: { + 'zh-CN': '条件过滤', + 'en-US': 'Filter', + }, + desc: { + 'zh-CN': + '

    通过属性filter配置文件过滤条件,通过事件addItemFailed处理不符合条件的文件,通过事件addItemSuccess处理符合条件的文件。

    ', + 'en-US': '

    ', + }, + apis: ['TiUploadComponent.properties.filters'], + }, + { + demoId: 'upload-auto-upload', + name: { + 'zh-CN': '手动上传', + 'en-US': 'Manual Upload', + }, + desc: { + 'zh-CN': + '

    通过属性autoUpload定义是否自动上传;通过上传实例的upload方法触发上传。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadComponent.properties.autoUpload', + 'TiUploadComponent.methods.upload', + ], + }, + { + demoId: 'upload-batch-send', + name: { + 'zh-CN': '合并上传', + 'en-US': 'Batch', + }, + desc: { + 'zh-CN': + '

    通过属性batchSend配置是否将所有文件上传任务合并为一次请求,默认状态会单独依次处理每个文件上传任务。仅适用于手动上传场景。

    ', + 'en-US': '

    ', + }, + apis: ['TiUploadComponent.properties.batchSend'], + }, + { + demoId: 'upload-event', + name: { + 'zh-CN': '常用事件', + 'en-US': 'Used events', + }, + desc: { + 'zh-CN': + '

    通过事件addItemFailed配置添加文件失败事件回调;通过事件addItemSuccess配置添加文件成功事件回调;通过事件beforeSendItems配置发送文件前置事件回调,常用于上传前添加表单内容;通过事件progressItems配置发送文件过程事件回调;通过事件successItems配置发送文件成功事件回调;通过事件errorItems配置发送文件失败事件回调;通过事件cancelItems配置发送文件取消事件回调;通过事件removeItems配置删除文件事件回调;completeItems事件在文件上传后,不论成功或失败都必定被调用;completeAllItems事件在所有文件上传后,不论成功或失败都必定被调用。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadComponent.events.addItemFailed', + 'TiUploadComponent.events.addItemSuccess', + 'TiUploadComponent.events.beforeSendItems', + 'TiUploadComponent.events.progressItems', + 'TiUploadComponent.events.successItems', + 'TiUploadComponent.events.errorItems', + 'TiUploadComponent.events.cancelItems', + 'TiUploadComponent.events.beforeRemoveItems', + 'TiUploadComponent.events.removeItems', + 'TiUploadComponent.events.completeAllItems', + 'TiUploadComponent.events.completeItems', + ], + }, + { + demoId: 'upload-service', + name: { + 'zh-CN': '指令', + 'en-US': 'Directives', + }, + desc: { + 'zh-CN': + '

    通过指令tiFileSelect定义上传实例对象;通过上传实例的queue属性访问文件信息及方法;通过属性accept配置文件类型,使用逗号分隔。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiFileSelectDirective.properties.tiFileSelect', + 'TiUploadComponent.properties.accept', + ], + }, + ], +}; diff --git a/src/upload/demo/src/app/upload/webdoc/upload.cn.md b/src/upload/demo/src/app/upload/webdoc/upload.cn.md new file mode 100644 index 0000000..7be5d6a --- /dev/null +++ b/src/upload/demo/src/app/upload/webdoc/upload.cn.md @@ -0,0 +1,22 @@ +--- +title: Upload 文件上传 +--- +# Upload 文件上传 + +
    + +upload 是用于将本地文件上传至服务器的组件。 + +```typescript +import { TiUploadModule } from '@opentiny/ng'; +``` +
    + +
    + +upload 是用于将本地的图片或文件上传至服务器的组件。 + +```typescript +import { TiUploadModule } from '@opentiny/ng'; +``` +
    diff --git a/src/upload/demo/src/app/upload/webdoc/upload.en.md b/src/upload/demo/src/app/upload/webdoc/upload.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/upload/demo/src/app/upload/webdoc/upload.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/upload/demo/src/app/uploadimage/UploadimageAutoUploadComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageAutoUploadComponent.ts new file mode 100644 index 0000000..ce73d07 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageAutoUploadComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-auto-upload.html` +}) +export class UploadimageAutoUploadComponent {} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageBasicComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageBasicComponent.ts new file mode 100644 index 0000000..ebe4637 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageBasicComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-basic.html` +}) +export class UploadimageBasicComponent {} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageChangesComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageChangesComponent.ts new file mode 100644 index 0000000..9962598 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageChangesComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-changes.html` +}) +export class UploadimageChangesComponent { + myLogs: Array = []; + customHeaders: any = { + tiheadersConfig: 'TinyNG' + }; + customFormData: any = { + tiCustomFormData: 'hello tiny' + }; + changeHeaders(): void { + this.customHeaders.tiCustomHeader = 'custom header'; + this.myLogs = [...this.myLogs, `添加头信息:${JSON.stringify(this.customHeaders)}`]; + } + beforeSendItems($event: Event): void { + this.myLogs = [ + ...this.myLogs, + `请求方式:${$event[0].method}; 文件名:'${$event[0].alias}'; 头信息:${JSON.stringify( + $event[0].headers + )}; 附加信息:${JSON.stringify($event[0].formData)}` + ]; + } +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageDeletableComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageDeletableComponent.ts new file mode 100644 index 0000000..be165e5 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageDeletableComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-deletable.html` +}) +export class UploadimageDeletableComponent { + deletable: boolean = false; +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageDisabledComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageDisabledComponent.ts new file mode 100644 index 0000000..73b86f2 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageDisabledComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-disabled.html` +}) +export class UploadimageDisabledComponent { + disabled: boolean = true; +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageDragComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageDragComponent.ts new file mode 100644 index 0000000..2996e12 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageDragComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-drag.html` +}) +export class UploadimageDragComponent { + url: string = '/upload'; +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageEventComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageEventComponent.ts new file mode 100644 index 0000000..d2c8663 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageEventComponent.ts @@ -0,0 +1,47 @@ +import { Component } from '@angular/core'; +import { TiFileItem } from '@opentiny/ng'; + +@Component({ + templateUrl: `./uploadimage-event.html` +}) +export class UploadimageEventComponent { + myLogs: Array = []; + onAddItemSuccess(fileItem: TiFileItem): void { + this.myLogs = [...this.myLogs, `onAddItemSuccess() filename:${fileItem.file.name}`]; + } + onAddItemFailed(fileItem: TiFileItem): void { + this.myLogs = [...this.myLogs, `onAddItemFailed() filename:${fileItem.file.name}`]; + } + onBeforeSendItems(fileItems: Array): void { + this.myLogs = [...this.myLogs, `onBeforeSendItems()`]; + fileItems.forEach((item: TiFileItem) => { + item.formData = { + tinyFormdata: 'Custom data' + }; + }); + } + onProgressItems($event: any): void { + this.myLogs = [...this.myLogs, `onProgressItems() progress:${$event.progress}`]; + } + onSuccessItems($event: any): void { + this.myLogs = [...this.myLogs, `onSuccessItems() response:${$event.response} status:${$event.status}`]; + } + onErrorItems($event: any): void { + this.myLogs = [...this.myLogs, `onErrorItems() response:${$event.response} status:${$event.status}`]; + } + onCancelItems($event: any): void { + this.myLogs = [...this.myLogs, `onCancelItems() response:${$event.response} status:${$event.status}`]; + } + onRemoveItems($event: any): void { + this.myLogs = [...this.myLogs, `onRemoveItems()`]; + } + onBeforeRemoveItems(fileItemArry: Array): void { + this.myLogs = [...this.myLogs, `onBeforeRemoveItems()`]; + } + onCompleteItems($event: any): void { + this.myLogs = [...this.myLogs, `onCompleteItems() response:${$event.response}`]; + } + onCompleteAllItems($event: any): void { + this.myLogs = [...this.myLogs, `onCompleteItems()`]; + } +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageFilterComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageFilterComponent.ts new file mode 100644 index 0000000..6643189 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageFilterComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiFilter } from '@opentiny/ng'; + +@Component({ + templateUrl: `./uploadimage-filter.html` +}) +export class UploadimageFilterComponent { + filters: Array = [ + { + name: 'type', + params: ['.png,.img'] + }, + { + name: 'maxSize', + params: [102400] + }, + { + name: 'maxCount', + params: [2] + } + ]; + myLogs: Array = []; + onAddItemFailed(event: any): void { + this.myLogs = [...this.myLogs, `errorType:${event.validResults.toString()}----filename:${event.file.name}`]; + } + onAddItemSuccess(event: any): void { + this.myLogs = [...this.myLogs, `success----filename:${event.file.name}`]; + } +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageInitfilesComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageInitfilesComponent.ts new file mode 100644 index 0000000..8d418e2 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageInitfilesComponent.ts @@ -0,0 +1,25 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiUploadimageInitFile } from '@opentiny/ng'; + +@Component({ + templateUrl: `./uploadimage-initfiles.html`, + styles: ['.preview-modal-class { width: 400px !important; }'], + encapsulation: ViewEncapsulation.None // 要想设置的样式生效,此处必须配置成 ViewEncapsulation.None +}) +export class UploadimageInitfilesComponent { + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL']; + initFiles: Array = [ + { + name: 'first.jpg', + previewUrl: `${this.baseUrl}assets/image/1.jpg` + }, + { + name: 'second.jpg', + previewUrl: `${this.baseUrl}assets/image/2.jpg` + }, + { + name: 'third.jpg', + previewUrl: `${this.baseUrl}assets/image/3.jpg` + } + ]; +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageMaxcountComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageMaxcountComponent.ts new file mode 100644 index 0000000..195ae95 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageMaxcountComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-maxcount.html` +}) +export class UploadimageMaxcountComponent { + myLogs: Array = []; + uploadLimit(event: any): void { + this.myLogs = [...this.myLogs, `图片数量已达上限。`]; + } +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageTemplateComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageTemplateComponent.ts new file mode 100644 index 0000000..a6af919 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageTemplateComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-template.html` +}) +export class UploadimageTemplateComponent { + url: string = '/upload'; +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageTestModule.ts b/src/upload/demo/src/app/uploadimage/UploadimageTestModule.ts new file mode 100644 index 0000000..1fede57 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageTestModule.ts @@ -0,0 +1,91 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiAlertModule, TiButtonModule, TiIconModule, TiTipModule, TiUploadModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { UploadimageBasicComponent } from './UploadimageBasicComponent'; +import { UploadimageAutoUploadComponent } from './UploadimageAutoUploadComponent'; +import { UploadimageMaxcountComponent } from './UploadimageMaxcountComponent'; +import { UploadimageDeletableComponent } from './UploadimageDeletableComponent'; +import { UploadimageEventComponent } from './UploadimageEventComponent'; +import { UploadimageDragComponent } from './UploadimageDragComponent'; +import { UploadimageFilterComponent } from './UploadimageFilterComponent'; +import { UploadimageDisabledComponent } from './UploadimageDisabledComponent'; +import { UploadimageTemplateComponent } from './UploadimageTemplateComponent'; +import { UploadimageInitfilesComponent } from './UploadimageInitfilesComponent'; +import { UploadimageChangesComponent } from './UploadimageChangesComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiTipModule, + TiIconModule, + TiUploadModule, + TiAlertModule, + TiButtonModule, + DemoLogModule, + RouterModule.forChild(UploadimageTestModule.ROUTES) + ], + declarations: [ + UploadimageBasicComponent, + UploadimageAutoUploadComponent, + UploadimageMaxcountComponent, + UploadimageDeletableComponent, + UploadimageEventComponent, + UploadimageDragComponent, + UploadimageFilterComponent, + UploadimageDisabledComponent, + UploadimageTemplateComponent, + UploadimageInitfilesComponent, + UploadimageChangesComponent + ] +}) +export class UploadimageTestModule { + static readonly LINKS: Array = [{ href: 'components/TiUploadimageComponent.html', label: 'Uploadimage' }]; + static readonly ROUTES: Routes = [ + { + path: 'uploadimage/uploadimage-basic', + component: UploadimageBasicComponent + }, + { + path: 'uploadimage/uploadimage-filter', + component: UploadimageFilterComponent + }, + { + path: 'uploadimage/uploadimage-maxcount', + component: UploadimageMaxcountComponent + }, + { + path: 'uploadimage/uploadimage-deletable', + component: UploadimageDeletableComponent + }, + { + path: 'uploadimage/uploadimage-event', + component: UploadimageEventComponent + }, + { + path: 'uploadimage/uploadimage-initfiles', + component: UploadimageInitfilesComponent + }, + { + path: 'uploadimage/uploadimage-changes', + component: UploadimageChangesComponent + }, + { + path: 'uploadimage/uploadimage-drag', + component: UploadimageDragComponent + }, + { + path: 'uploadimage/uploadimage-disabled', + component: UploadimageDisabledComponent + }, + { + path: 'uploadimage/uploadimage-template', + component: UploadimageTemplateComponent + } + ]; +} diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-auto-upload.html b/src/upload/demo/src/app/uploadimage/uploadimage-auto-upload.html new file mode 100644 index 0000000..0d35e60 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-auto-upload.html @@ -0,0 +1,3 @@ + +
    + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-basic.html b/src/upload/demo/src/app/uploadimage/uploadimage-basic.html new file mode 100644 index 0000000..eb571c2 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-basic.html @@ -0,0 +1 @@ + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-changes.html b/src/upload/demo/src/app/uploadimage/uploadimage-changes.html new file mode 100644 index 0000000..464748b --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-changes.html @@ -0,0 +1,15 @@ + + +
    + + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-deletable.html b/src/upload/demo/src/app/uploadimage/uploadimage-deletable.html new file mode 100644 index 0000000..b3d6706 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-deletable.html @@ -0,0 +1 @@ + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-disabled.html b/src/upload/demo/src/app/uploadimage/uploadimage-disabled.html new file mode 100644 index 0000000..939995d --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-disabled.html @@ -0,0 +1 @@ + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-drag.html b/src/upload/demo/src/app/uploadimage/uploadimage-drag.html new file mode 100644 index 0000000..5fd3578 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-drag.html @@ -0,0 +1,16 @@ +

    描述

    +

    + 10.1.13 + 版本开始对外隐藏这个示例,与谭莉沟通后发现drag类型和block类型没有什么太大区别,而且没有对应的实际应用场景,所以先隐藏,后续看情况在决定是否公开次此示例。
    +
    + 设置type接口为drag。与type为block的区别在于以下两点:
    + 1、drag类型下,没有提供上传结果展示列表,服务可以自定义;
    + 2、drag类型下,上传区域宽高可以自定义,不过这个没有对外展示。
    +
    + 默认为自动上传,如需手动上传,可设置autoUpload为false,并需要自定义上传按钮,调用组件的upload方法;
    + 手动上传时,可以通过addItemSuccess事件获取添加成功的文件,通过addItemFailed事件获取添加失败的文件。
    +

    +

    示例

    + + + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-event.html b/src/upload/demo/src/app/uploadimage/uploadimage-event.html new file mode 100644 index 0000000..8ae20e1 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-event.html @@ -0,0 +1,17 @@ + + + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-filter.html b/src/upload/demo/src/app/uploadimage/uploadimage-filter.html new file mode 100644 index 0000000..0530b14 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-filter.html @@ -0,0 +1,8 @@ + + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-initfiles.html b/src/upload/demo/src/app/uploadimage/uploadimage-initfiles.html new file mode 100644 index 0000000..99dd1db --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-initfiles.html @@ -0,0 +1 @@ + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-maxcount.html b/src/upload/demo/src/app/uploadimage/uploadimage-maxcount.html new file mode 100644 index 0000000..6110def --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-maxcount.html @@ -0,0 +1,2 @@ + + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-template.html b/src/upload/demo/src/app/uploadimage/uploadimage-template.html new file mode 100644 index 0000000..b40dc1f --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-template.html @@ -0,0 +1,8 @@ +

    描述

    +

    自定义样式:

    +

    示例

    + + +

    添加文件

    +
    +
    diff --git a/src/upload/demo/src/app/uploadimage/uploadimagetest.less b/src/upload/demo/src/app/uploadimage/uploadimagetest.less new file mode 100644 index 0000000..0145d45 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimagetest.less @@ -0,0 +1,5 @@ +.custom-alert-style { + position: absolute; + top: 0px; + right: 20px; +} diff --git a/src/upload/demo/src/app/uploadimage/webdoc/uploadimage-demos.js b/src/upload/demo/src/app/uploadimage/webdoc/uploadimage-demos.js new file mode 100644 index 0000000..8f3d7d3 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/webdoc/uploadimage-demos.js @@ -0,0 +1,143 @@ +export default { + column: '2', + demos: [ + { + demoId: 'uploadimage-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    Uploadimage 组件的最简用法。通过属性url配置上传地址。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadimageComponent.properties.url', + 'TiUploadimageComponent.properties.type', + ], + }, + { + demoId: 'uploadimage-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'Disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置禁用状态。

    ', + 'en-US': '

    ', + }, + apis: ['TiUploadimageComponent.properties.disabled'], + }, + { + demoId: 'uploadimage-maxcount', + name: { + 'zh-CN': '数量限制', + 'en-US': 'Max length', + }, + desc: { + 'zh-CN': + '

    通过属性maxCount配置最大可上传的图片数;当元素失去焦点的时候触发uploadLimit事件。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadimageComponent.properties.maxCount', + 'TiUploadimageComponent.events.uploadLimit', + ], + }, + { + demoId: 'uploadimage-deletable', + name: { + 'zh-CN': '图片可删除', + 'en-US': 'Deletable', + }, + desc: { + 'zh-CN': + '

    通过属性deletable配置已上传的图片是否可以删除。注意:只对上传成功的图片生效。

    ', + 'en-US': '

    ', + }, + apis: ['TiUploadimageComponent.properties.deletable'], + }, + { + demoId: 'uploadimage-initfiles', + name: { + 'zh-CN': '初始图片', + 'en-US': 'Initfiles', + }, + desc: { + 'zh-CN': + '

    通过属性initfiles配置初始图片列表;通过属性modalClass配置图片预览时弹出框样式。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadimageComponent.properties.initFiles', + 'TiUploadimageComponent.properties.modalClass', + ], + }, + { + demoId: 'uploadimage-changes', + name: { + 'zh-CN': '常用属性', + 'en-US': 'Used props', + }, + desc: { + 'zh-CN': + '

    通过属性method配置请求方式,包括getpost;通过属性headers配置请求头信息;通过属性accept配置合法的本地文件类型;通过属性alias配置上传文件字段唯一标识的键值;通过属性formData配置文件上传的附带信息;通过属性formDataFirst配置附带信息是否先于上传文件对象。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadimageComponent.properties.method', + 'TiUploadimageComponent.properties.headers', + 'TiUploadimageComponent.properties.accept', + 'TiUploadimageComponent.properties.alias', + 'TiUploadimageComponent.properties.formData', + 'TiUploadimageComponent.properties.formDataFirst', + ], + }, + { + demoId: 'uploadimage-filter', + name: { + 'zh-CN': '条件过滤', + 'en-US': 'Filter', + }, + desc: { + 'zh-CN': + '

    通过属性filter配置文件过滤条件,通过事件addItemFailed处理不符合条件的文件,通过事件addItemSuccess处理符合条件的文件。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadimageComponent.properties.filters', + 'TiUploadimageComponent.events.addItemFailed', + 'TiUploadimageComponent.events.addItemSuccess', + ], + }, + { + demoId: 'uploadimage-event', + name: { + 'zh-CN': '常用事件', + 'en-US': 'Used events', + }, + desc: { + 'zh-CN': + '

    通过事件addItemFailed配置添加图片失败事件回调;通过事件addItemSuccess配置添加图片成功事件回调;通过事件beforeSendItems配置发送图片前置事件回调,常用于上传前添加表单内容;通过事件progressItems配置发送图片过程事件回调;通过事件successItems配置发送图片成功事件回调;通过事件errorItems配置发送图片失败事件回调;通过事件cancelItems配置发送图片取消事件回调;通过事件removeItems配置删除图片事件回调;completeItems事件在图片上传后,不论成功或失败都必定被调用;completeAllItems事件在所有图片上传后,不论成功或失败都必定被调用。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadimageComponent.events.beforeSendItems', + 'TiUploadimageComponent.events.progressItems', + 'TiUploadimageComponent.events.successItems', + 'TiUploadimageComponent.events.errorItems', + 'TiUploadimageComponent.events.cancelItems', + 'TiUploadimageComponent.events.beforeRemoveItems', + 'TiUploadimageComponent.events.removeItems', + 'TiUploadimageComponent.events.completeAllItems', + 'TiUploadimageComponent.events.completeItems', + ], + }, + ], + ignoreApis: [ + 'TiUploadimageComponent.properties.autoUpload', + 'TiUploadimageComponent.properties.batchSend', + 'TiUploadimageComponent.methods.upload', + ], +}; diff --git a/src/upload/demo/src/app/uploadimage/webdoc/uploadimage.cn.md b/src/upload/demo/src/app/uploadimage/webdoc/uploadimage.cn.md new file mode 100644 index 0000000..5937560 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/webdoc/uploadimage.cn.md @@ -0,0 +1,22 @@ +--- +title: Uploadimage 图片上传 +--- +# Uploadimage 图片上传 + +
    + +Uploadimage 是用于将本地图片上传至服务器的组件。 + +```typescript +import { TiUploadModule } from '@opentiny/ng'; +``` +
    + +
    + +Uploadimage 是用于将本地图片上传至服务器的组件。 + +```typescript +import { TiUploadModule } from '@opentiny/ng'; +``` +
    diff --git a/src/upload/demo/src/app/uploadimage/webdoc/uploadimage.en.md b/src/upload/demo/src/app/uploadimage/webdoc/uploadimage.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/webdoc/uploadimage.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/upload/demo/src/favicon.ico b/src/upload/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/upload/demo/src/index.html b/src/upload/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/upload/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/upload/demo/src/main.ts b/src/upload/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/upload/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/upload/demo/test.ts b/src/upload/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/upload/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/upload/demo/tsconfig.app.json b/src/upload/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/upload/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/upload/demo/tsconfig.spec.json b/src/upload/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/upload/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/upload/lib/index.ts b/src/upload/lib/index.ts new file mode 100644 index 0000000..c1d2eb8 --- /dev/null +++ b/src/upload/lib/index.ts @@ -0,0 +1,4 @@ +export * from './src/TiUploadModule'; +export * from './src/TiUploadService'; +export * from './src/TiUploadServiceModule'; +export * from './src/TiFileInterface'; diff --git a/src/upload/lib/ng-package.json b/src/upload/lib/ng-package.json new file mode 100644 index 0000000..9c9092e --- /dev/null +++ b/src/upload/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/upload", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/upload/lib/package.json b/src/upload/lib/package.json new file mode 100644 index 0000000..b27c394 --- /dev/null +++ b/src/upload/lib/package.json @@ -0,0 +1,23 @@ +{ + "name": "@opentiny/ng-upload", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-modal": "~1.0.0-beta.0", + "@opentiny/ng-imagepreview": "~1.0.0-beta.0", + "@angular/platform-browser": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-progresspie": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-validation": "~1.0.0-beta.0", + "@opentiny/ng-progressbar": "~1.0.0-beta.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/upload/lib/project.json b/src/upload/lib/project.json new file mode 100644 index 0000000..11ddb62 --- /dev/null +++ b/src/upload/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/upload/lib", + "sourceRoot": "src/upload/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/upload"], + "options": { + "project": "src/upload/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/upload"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js upload" + }, + { + "command": "ng default-build upload" + }, + { + "command": "node build/clear-default-theme.js upload" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/upload && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build upload && ng pack upload && node build/publish.js upload --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/upload/lib/src/TiDisabledDirective.ts b/src/upload/lib/src/TiDisabledDirective.ts new file mode 100644 index 0000000..b7cb499 --- /dev/null +++ b/src/upload/lib/src/TiDisabledDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; +/** + * @ignore + */ +@Directive({ + selector: '[tiDisabled]' +}) +export class TiDisabledDirective { + constructor(private hostEle: ElementRef, private renderer: Renderer2) {} + @Input() + set tiDisabled(value: boolean) { + if (value) { + this.renderer.setAttribute(this.hostEle.nativeElement, 'disabled', 'disabled'); + } else { + this.renderer.removeAttribute(this.hostEle.nativeElement, 'disabled'); + } + } +} diff --git a/src/upload/lib/src/TiFileInterface.ts b/src/upload/lib/src/TiFileInterface.ts new file mode 100644 index 0000000..0fe9cd8 --- /dev/null +++ b/src/upload/lib/src/TiFileInterface.ts @@ -0,0 +1,564 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * 单个文件基础信息接口,与浏览器中读取到的文件信息基本一致 + * + * 以下使用会用到该类型: + * + * 1.[TiUploadComponent.addItemFailed]{@link ../components/TiUploadComponent.html#addItemFailed}回调的参数类型 + * + * 2.[TiFileItem.file]{@link TiFileItem#file}的对象类型 + */ +export interface TiFileInfo { + /** + * 文件修改时间 + */ + lastModifiedDate: Date; + /** + * 文件真实大小值,单位为B + */ + size: number; + /** + * 文件大小,此处做了单位转换,方便界面详情显示,该数值根据文件大小单位区间显示,并保留两位小数 + */ + sizeWithUnit: string; // 做单位转换后的文件大小,方便界面详情显示 + /** + * 文件名称 + */ + name: string; + /** + * 文件类型,此处是取的文件后缀名 + */ + type: string; // 确保浏览器形式的一致性 + /** + * @ignore + */ + _file: File; // 文件对象,只在H5方式下有效 + /** + * @ignore + */ + _input: Element; // 文件input对象,只在IE9 form表单提交方式下有效 +} +/** + * 单个文件详细信息接口,包含业务配置的上传信息、上传状态及方法等 + * + * 作为文件上传回调中的参数类型使用,如[TiUploadComponent.completeAllItems]{@link ../components/TiUploadComponent.html#completeAllItems}回调中的参数类型 + */ +export interface TiFileItem { + /** + * 文件是否处于待上传状态(beforeSend前设置) + */ + isReady: boolean; + /** + * 文件是否正在上传 + */ + isUploading: boolean; + /** + * 文件是否已上传 + */ + isUploaded: boolean; + /** + * 文件是否取消上传 + */ + isCancel: boolean; + /** + * 文件是否上传成功 + */ + isSuccess: boolean; + /** + * 文件是否上传错误 + */ + isError: boolean; + /** + * 文件上传进度值 + */ + progress: number; + + /** + * @ignore + */ + isHover: boolean; + /** + * @ignore + * 文件名称是否溢出 + */ + isOverflow: boolean; + /** + * @ignore + */ + _xhr: any; + /** + * 文件在已选文件队列中的次序 + */ + index: number; + /** + * @ignore + * 文件上传地址,为业务配置信息,只可获取 + */ + url: string; // 后台地址 + /** + * 单个文件基础信息,只包含文件名及类型等信息,为业务配置信息,只可获取 + */ + file: TiFileInfo; // 文件详细信息情况 + /** + * @ignore + * 向后台发送请求时对应的文件对象name属性,该name属性是后台读取文件的入口值,为业务配置信息,只可获取 + */ + alias: string; // 文件name + /** + * @ignore + */ + _file: File; + /** + * @ignore + */ + _input: Element; + /** + * @ignore + */ + _form: any; + /** + * 上传文件附带信息,为对象形式,可在beforeSend事件回调中进行单个文件的formData动态设置 + */ + formData: object; // 业务可自定义为对象形式 + /** + * @ignore + */ + formDataFirst: boolean; + /** + * @ignore + */ + batchSend: boolean; + /** + * @ignore + */ + headers: object; // 业务可自定义为对象形式 + /** + * @ignore + */ + method: string; + /** + * 该文件对应的上传实例对象 + */ + uploader: TiUploadRef; // 上传实例对象 + /** + * 是否允许重新上传reload + */ + allowReload?: boolean; + /** + * 是否允许reload + */ + allowDelete?: boolean; + /** + * 上传该文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + upload(): void; + /** + * 取消上传该文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + cancel(): void; + /** + * 删除该文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + remove(): void; + /** + * 销毁该文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + destroy(): void; +} +/** + * 文件过滤条件自定义接口 + * + * 作为[TiUploadComponent.filters]{@link ../components/TiUploadComponent.html#filters}中的类型使用 + * + * 当前支持四种过滤类型: + * + * maxCount:最大选择文件个数,当参数为1时,代表单文件上传 + * + * type:文件选择类型,参数需使用文件扩展名分开,为了确保各浏览器的一致性,文件类型使用扩展名判断 + * + * minSize/maxSize:文件大小限制,参数为number类型,代表文件大小,单位为b,IE9不支持文件大小的获取,因此不支持该条规则 + */ +export interface TiFilter { + /** + * 规则名称 + */ + name: string; + /** + * 规则参数 + */ + params?: Array; + /** + * 规则函数 + * + * **参数:** + * + * fileItem: [TiFileInfo]{@link TiFileInfo} 文件信息 + * + * params: Array 传入的规则参数 + * + * fileQueue: Array<[TiFileItem]{@link TiFileItem}> 文件队列 + * + * **返回值:** + * + * 是否符合规则 boolean + */ + fn?(fileObj: TiFileInfo, params: Array, fileQueue: Array): boolean; +} +/** + * 创建文件实例方法的配置信息接口 + * + * 作为[TiUploadService.create]{@link ../injectables/TiUploadService.html#create}方法中的参数类型使用 + */ +export interface TiUploadConfig { + /** + * 文件上传地址配置 + */ + url: string; // 文件上传地址配置 + /** + * 上传方式,可选值为:get、post(其他方式IE9不支持) + * @default 'post' + */ + method?: string; // 上传方式,可选值为:get、post(其他方式IE9不支持) + /** + * 文件有效性判断条件数组 + */ + filters?: Array; // 文件有效性判断条件数组 + /** + * 向后台发送请求时对应的文件对象name属性,该name属性是后台读取文件的入口值 + * @default 'tiFile' + */ + alias?: string; // 向后台发送请求时对应的文件对象name属性,该name属性是后台读取文件的入口值,默认值为 'tiFile' + /** + * 上传文件附带信息对象 + */ + formData?: object; // 上传文件附带信息 + /** + * 是否自动上传 + * @default true + */ + autoUpload?: boolean; + /** + * 是否一次请求传输多个文件,默认情况下一次请求上传一个文件 + * @default false + */ + batchSend?: boolean; + /** + * 上传数据中,formData是否在file信息之前 + * @default false + */ + formDataFirst?: boolean; + /** + * 上传文件请求头配置,自定义为对象形式 + */ + headers?: object; // 上传文件请求头配置 + /** + * 分片上传每片大小 + */ + chunkSize?: number; // 分片大小,设置后开启分片上传单位是 b + /** + * 文件添加失败回调,可使用该回调定义上传错误时的错误提示 + * + * **参数:** + * + * file: [TiFileInfo]{@link TiFileInfo} 上传文件信息 + * + * validResults: Array<string> 校验不合法的规则name数组 + */ + onAddItemFailed?(fileObject: TiFileInfo, validResults: Array): void; // 文件添加失败回调,可使用该回调定义上传错误时的错误提示 + /** + * 文件添加成功回调 + * + * **参数:** + * + * fileItem: [TiFileItem]{@link TiFileItem} 上传文件对象 + */ + onAddItemSuccess?(fileItem: TiFileItem): void; + // 以下回调对单个或多个文件在同一url同时上传有效,可使用其进行文件上传过程中的业务处理 + /** + * 上传文件前回调,可在该回调中动态设置formData + * + * **参数:** + * fileItems: Array<[TiFileItem]{@link TiFileItem}> 上传的文件对象 + */ + onBeforeSendItems?(fileItems: Array): void; + /** + * 上传进度更新回调 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}> 上传的文件对象 + * + * progress: number 上传进度信息 + */ + onProgressItems?(fileItems: Array, progress: number): void; + /** + * 文件上传完成回调,成功/失败都会触发 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}>, // 上传文件对象数组 + * + * response: string // 文件上传响应信息 + * + * status: number 文件上传响应状态码 + */ + onCompleteItems?(fileItems: Array, response: string, status: number): void; + /** + * 文件上传成功回调 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}>, // 上传文件对象数组 + * + * response: string // 文件上传响应信息 + * + * status: number 文件上传响应状态码 + */ + onSuccessItems?(fileItems: Array, response: string, status: number): void; + /** + * 文件上传失败回调 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}>, // 上传文件对象数组 + * + * response: string // 文件上传响应信息 + * + * status: number 文件上传响应状态码 + */ + onErrorItems?(fileItems: Array, response: string, status: number): void; + /** + * 文件上传取消回调 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}>, // 上传文件对象数组 + * + * response: string // 文件上传响应信息 + * + * status: number 文件上传响应状态码 + */ + onCancelItems?(fileItems: Array, response: string, status: number): void; + /** + * 文件删除回调 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}>, // 上传文件对象数组 + */ + onRemoveItems?(fileItems: Array): void; + /** + * 文件删除前回调 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}>, // 上传文件对象数组 + */ + onBeforeRemoveItems?(fileItems: Array): void; + /** + * 所有文件上传完成回调 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}>, // 上传文件对象数组 + */ + onCompleteAllItems?(fileItems: Array): void; // 一个序列中所有文件上传完成回调:适用于串行上传,当最后一个文件上传完成时,触发该回调 +} + +/** + * 文件上传实例对象接口 + * + * 以下使用会用到该类型: + * + * 1.[TiUploadService.create]{@link ../injectables/TiUploadService.html#create}方法的返回值类型 + * + * 2.[TiFileItem.uploader]{@link TiFileItem#uploader}对象类型 + * + * 3.[tiFileSelect]{@link ../directives/TiFileSelectDirective.html}属性的配置类型 + */ +export interface TiUploadRef { + /** + * 上传文件队列,只可读 + */ + queue: Array; + /** + * @ignore + * 文件是否有单文件选择限制,只可读 + */ + isSingleFile: boolean; + /** + * @ignore + */ + config: TiUploadConfig; + /** + * @ignore + */ + _uploadComponentInstance: /* TiUploadComponent | TiUploadimageComponent | */ any; + /** + * @ignore + */ + _addToQueue(files: FileList): Array; // 文件添加方法 + /** + * 获取文件队列中未上传完成文件方法 + * + * **参数:**无 + * + * **返回值:**文件对象数组 Array<[TiFileItem]{@link TiFileItem}> + */ + getNotUploadedItems(): Array; + /** + * 获取文件队列中已提交上传,但还未上传文件方法 + * + * **参数:**无 + * + * **返回值:**文件对象数组 Array<[TiFileItem]{@link TiFileItem}> + */ + getReadyItems(): Array; + /** + * 上传队列中所有还未执行过上传的文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + uploadAll(): void; + /** + * 获取上传队列中是否有未上传过的文件方法 + * + * **参数:**无 + * + * **返回值:**boolean + */ + isUploadedAll(): boolean; + /** + * 删除队列中所有文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + removeAll(): void; + /** + * 取消队列中所有上传文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + cancelAll(): void; + /** + * 重新上传队列中所有文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + reloadAll(): void; + /** + * 重新上传队列中所有上传错误文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + reloadAllError(): void; + /** + * 上传队列中某几项文件方法 + * + * **参数:**Array<[TiFileItem]{@link TiFileItem}> + * + * **返回值:**void + */ + uploadItems(items: Array): void; + /** + * 删除队列中某几项文件方法 + * + * **参数:**Array<[TiFileItem]{@link TiFileItem}> + * + * **返回值:**void + */ + removeItems(items: Array): void; + /** + * 取消队列中某几项文件方法 + * + * **参数:**Array<[TiFileItem]{@link TiFileItem}> + * + * **返回值:**void + */ + cancelItems(items: Array): void; +} +/** + * @ignore + * + * 分片上传的每片文件接口;暂时没有对外暴露 + */ +export interface TiChunked { + /** + * 文件上传地址配置 + */ + url: string; + /** + * 上传方式,可选值为:get、post + */ + method?: string; + /** + * 单个文件基础信息,只包含文件名及类型等信息,为业务配置信息,只可获取 + */ + file: TiFileInfo; + /** + * @ignore + * 向后台发送请求时对应的文件对象name属性,该name属性是后台读取文件的入口值,为业务配置信息,只可获取 + */ + alias: string; + /** + * 上传文件附带信息对象 + */ + formData?: object; + /** + * 单个文件基础信息,只包含文件名及类型等信息,为业务配置信息,只可获取 + */ + _file: any; + /** + * 上传数据中,formData是否在file信息之前 + * @default false + */ + formDataFirst?: boolean; + /** + * 上传文件请求头配置,自定义为对象形式 + */ + headers?: object; + /** + * @ignore + */ + _xhr: any; + /** + * 分片上传时第几项文件 + */ + index: number; +} diff --git a/src/upload/lib/src/TiFileSelectDirective.ts b/src/upload/lib/src/TiFileSelectDirective.ts new file mode 100644 index 0000000..bb82b56 --- /dev/null +++ b/src/upload/lib/src/TiFileSelectDirective.ts @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; +import { TiFileItem, TiUploadRef } from './TiFileInterface'; +import { TiUploadUtil } from './TiUploadUtil'; + +/** + * 该指令适用于自定义上传场景,指令定义在input type='file'元素上,传入的是文件上传实例 + * 文件上传实例的生成,请参考[TiUploadService.create]{@link ../injectables/TiUploadService.html#create} + * + * 除自定义使用方式外,Tiny还提供了两种已进行设计的上传组件供业务使用,具体见[TiUploadComponent]{@link ../components/TiUploadComponent.html} + * + */ +@Directive({ + selector: '[tiFileSelect]' +}) +export class TiFileSelectDirective { + /** + * 文件上传实例 + */ + @Input() tiFileSelect: TiUploadRef; + private nativeElement: any; + private fileChangeEvent: any; + constructor(private hostElementRef: ElementRef, private renderer: Renderer2) { + this.nativeElement = this.hostElementRef.nativeElement; + this.fileChangeEvent = this.renderer.listen(this.nativeElement, 'change', () => { + if (!this.tiFileSelect) { + // 配置的上传文件实例不存在情况下,不做后续处理 + return; + } + this.onFileChange(this.nativeElement); // 点选文件后的处理 + }); + } + /** + * @ignore + * 选择文件后,根据浏览器差异进行处理 + */ + onFileChange(fileSel: any): void { + const uploadInst: any = this.tiFileSelect; + const files: any = TiUploadUtil.isHTML5 ? fileSel.files : fileSel; // 获取文件信息 + const addedItems: Array = uploadInst._addToQueue(files); // 文件选择队列 + if (TiUploadUtil.isHTML5) { + // H5情况下,重置表单元素值,确保可重复选择文件 + this.nativeElement.value = ''; + } else { + // 非H5情况下,确保文件下次可继续选择,分两种情况:1.已选文件有效情况下,,重新替换表单元素,确保文件点选元素不会随表单上传消失; + // 2.文件未加入到队列(文件校验失败情况)情况下,重置点选表单,确保文件可重复选择(校验失败可能是文件队列长度不符,所以为确保用户体验,执行该操作) + const isRemoveInput: boolean = addedItems.length === 0; // 根据文件是否有效情况确定是否移除input + this.replaceFileInput(fileSel, isRemoveInput); + } + + // 自动上传情况下,进行文件上传 + if (uploadInst.config.autoUpload !== false && addedItems.length !== 0) { + uploadInst.uploadItems(addedItems); + } + // onpush模式下,点击添加文件后,上传列表详情不展示。 + if (this.tiFileSelect._uploadComponentInstance) { + this.tiFileSelect._uploadComponentInstance.changeDetectorRef.markForCheck(); + } + } + + /** + * @ignore + * 替换单个文件选择按钮,确保后续文件选择可继续点选 + * 当前文件选择框 + * 文件选择实例对象 + */ + replaceFileInput(fileSel: any, isRemoveInput: boolean): void { + // 清除当前文件输入框选择事件 + this.fileChangeEvent(); + + // 新增input,并处理当前input + const fileSelNew: any = fileSel.cloneNode(); // 文件元素克隆时,不会复用原有已选文件信息 + if (isRemoveInput) { + // 文件选择无效情况下移除input,确保下次可正常选择 + fileSel.parentNode.appendChild(fileSelNew); + fileSel.remove(); + } else { + fileSel.style.display = 'none'; // 隐藏已选文件,确保点选框页面呈现的唯一性 + fileSel.parentNode.insertBefore(fileSelNew, fileSel.nextSibling); // 点选文件已放入文件队列中,替换新的文件选择按钮,确保下次点选生效 + } + + // 增加当前input事件 + this.fileChangeEvent = this.renderer.listen(fileSelNew, 'change', () => { + this.onFileChange(fileSelNew); + }); + } +} diff --git a/src/upload/lib/src/TiUploadComponent.ts b/src/upload/lib/src/TiUploadComponent.ts new file mode 100644 index 0000000..60160b9 --- /dev/null +++ b/src/upload/lib/src/TiUploadComponent.ts @@ -0,0 +1,297 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ElementRef, Input, Renderer2, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; +import { TiFileItem } from './TiFileInterface'; +import { TiUploadService } from './TiUploadService'; +import { Util } from '@opentiny/ng-utils'; +import { TiOverflowService } from '@opentiny/ng-overflow'; +import { TiUploadbaseComponent, TiUploadInitFile } from './TiUploadbaseComponent'; + +/** + * 该组件用于实现已设计好的交互完整的文件上传组件,包含两种样式: + * + * 1.带输入框的样式(type为inputField) + * + * 2.按钮样式(type为button) + * + * 如果以上两种样式不满足业务场景,可使用自定义方式实现,具体实现请参考[tiFileSelect]{@link ../directives/TiFileSelectDirective.html} + * + */ +@Component({ + selector: 'ti-upload:not([type]), ti-upload[type="inputField"], ti-upload[type="button"], ti-upload[type="textButton"]', + templateUrl: './upload.html', + styleUrls: ['./upload.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiUploadComponent extends TiUploadbaseComponent { + constructor( + private uploaderService: TiUploadService, + hostEle: ElementRef, + renderer: Renderer2, + private tiOverflow: TiOverflowService, + public changeDetectorRef: ChangeDetectorRef + ) { + super(hostEle, renderer); + } + /** + * 上传样式类型,'textButton' 类型 + */ + @Input() type: 'inputField' | 'button' | 'textButton' = 'inputField'; + /** + * 按钮文字 + */ + @Input() buttonText: string; // 配置按钮文字,type 为 button 形式时,为文件选择按钮文字;type 为 inputField 时,为上传按钮文字 + + // inputField 形式文件上传的相关配置 + /** + * 设置上传文件选择框宽度,只适用于 inputField 类型 + * @ignore + */ + @Input() inputFieldWidth: string; + /** + * 是否显示提交按钮,只适用于 inputField 类型 + * @ignore + */ + @Input() showSubmitButton: boolean; + /** + * 占位文本,只适用于 inputField 类型 + * @ignore + */ + @Input() placeholder: string; + /** + * 是否自动聚焦,只适用于 inputField 类型 + * @ignore + */ + @Input() autofocus: boolean = false; + /** + * 设置已上传文件列表的提示信息,只适用于 button/textButton 类型 + */ + @Input() note: string; + /** + * 初始化显示的文件列表,只适用于 button/textButton 类型 + */ + @Input() initFiles: Array; + /** + * 是否显示上传列表 + */ + @Input() showUploadList: boolean = true; + /** + * 错误提示信息,只适用于 button/textButton 类型 + */ + @Input() errorMessage: string; + /** + * 是否显示错误提示,只适用于 button/textButton 类型 + */ + @Input() showErrorMessage: boolean = true; + /** + * 文件列表区域最大高度,只适用于 button/textButton 类型 + */ + @Input() listMaxHeight: string; + /** + * @ignore + */ + @ViewChild('fileInput') fileInput: any; + /** + * @ignore 多文件上传的状态提示信息 + */ + stateInfo: string; + /** + * 是否禁用重新上传 + */ + public reloadAllDisable: boolean = true; + /** + * @ignore 点击上传按钮时触发的回调 + */ + onSelectClick(): void { + this.fileInput.nativeElement.click(); + } + /** + * @ignore + */ + ngOnInit(): void { + super.ngOnInit(); + // 创建uploader实例 + const autoUpload: boolean = Util.isUndefined(this.autoUpload) ? true : this.autoUpload; + this.uploadConfig.autoUpload = autoUpload; + this.uploadInst = this.uploaderService.create(this.uploadConfig, this); + if (this.initFiles && this.filters) { + const maxCountIndex: number = this.getMaxCountIndex(); + if (maxCountIndex !== -1) { + this.uploadConfig.filters[maxCountIndex].params[0] -= this.initFiles.length; + } + } + this.placeholder = this.setPlaceholder(); + // 设置submit按钮的显示状态 + this.showSubmitButton = Util.isUndefined(this.showSubmitButton) ? !autoUpload : this.showSubmitButton; + // 是否定义beforeRemoveItems事件 + // 为初始显示文件增加各自的remove方法(仅在button/textButton类型时) + if ((this.type === 'button' || this.type === 'textButton') && this.initFiles) { + this.initFiles.forEach((item: TiUploadInitFile) => { + if (Util.isUndefined(item.allowDelete)) { + item.allowDelete = true; + } + item.remove = (): void => { + if (!item.allowDelete) { + return; + } + const index: number = this.initFiles.findIndex((_item: TiUploadInitFile) => { + return _item === item; + }); + this.removeItem(index); + this.changeDetectorRef.markForCheck(); + }; + }); + } + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + // 处理autofocus + if (this.autofocus === true) { + this.focus(); + } + } + + /** + * @ignore + * 移除显示的文件项目(button/textButton类型的showList项目接口) + */ + removeInitFiles(item: TiUploadInitFile): void { + if (!item.allowDelete) { + return; + } + const index: number = this.initFiles.findIndex((_item: TiUploadInitFile) => { + return _item === item; + }); + // 若有beforeRemoveItems事件,则触发该事件 + if (this.isRemove) { + this.beforeRemoveItems.emit([item]); + } else { + // 否则直接删除,并触发removeItems事件 + this.removeItem(index); + this.removeItems.emit([item]); + } + } + // 提供focus/blur方法,供外部调用 + /** + * @ignore + * 聚焦方法,只适用于inputField类型 + */ + focus(): void { + this.fileInput.nativeElement.nextElementSibling.focus(); + } + /** + * @ignore + * 失焦方法,只适用于inputField类型 + */ + blur(): void { + this.fileInput.nativeElement.nextElementSibling.blur(); + } + // 通过item元素的移入移出事件控制元素的hover状态,该hover状态会决定item中部分按钮的显示状态 + /** + * @ignore + */ + onItemMouseenter(item: TiFileItem, event: any): void { + if (this.disabled) { + return; + } + item.isHover = true; + // 文件名超长溢出时,显示名称 + const fileNameEle: Element = event.target.querySelector('.ti3-aui-file-name'); + item.isOverflow = this.tiOverflow.isOverflow(fileNameEle); + } + /** + * @ignore + */ + onItemMouseleave(item: TiFileItem): void { + if (this.disabled) { + return; + } + item.isHover = false; + } + // 获取上传文件状态,同时根据状态设置状态详细信息 + /** + * @ignore + */ + getUploadState(): string { + let uploadingLen: number = 0; + let uploadErrLen: number = 0; + let uploadSuccLen: number = 0; + this.uploadInst.queue.forEach((item: TiFileItem) => { + if (item.isUploading) { + uploadingLen++; + } else if (item.isError) { + uploadErrLen++; + } else if (item.isSuccess) { + uploadSuccLen++; + } + if (item.allowReload) { + this.reloadAllDisable = false; + } + }); + const fileQueueLen: number = this.uploadInst.queue.length; + if (uploadSuccLen === fileQueueLen) { + this.stateInfo = this.uploadLan.successInfo; + + return 'success'; + } + if (uploadErrLen !== 0) { + this.stateInfo = Util.formatEntry(this.uploadLan.errorMultiInfo, [uploadErrLen]); + + return 'error'; + } + if (uploadingLen !== 0) { + this.stateInfo = Util.formatEntry(this.uploadLan.uploadingMutiInfo, [`${uploadingLen}/${fileQueueLen}`]); + + return 'uploading'; + } + this.stateInfo = Util.formatEntry(this.uploadLan.addSuccessMutiInfo, [fileQueueLen]); + + return 'addSuccess'; + } + /** + * 初始设置输入框内提示文本 + * 四种场景:自动单文件上传,自动多文件上传,手动单文件上传,手动多文件上传 + */ + private setPlaceholder(): string { + if (this.placeholder) { + return this.placeholder; + } + + if (this.autoUpload) { + if (this.uploadInst.isSingleFile) { + return this.uploadLan.autoUploadFilePlaceholder; + } + + return this.uploadLan.autoUploadFilesPlaceholder; + } + + if (this.uploadInst.isSingleFile) { + return this.uploadLan.notAutoUploadFilePlaceholder; + } + + return this.uploadLan.notAutoUploadFilesPlaceholder; + } + + /** + * 移除初始文件 + */ + private removeItem(index: number): void { + this.initFiles.splice(index, 1); + if (this.filters) { + const maxCountIndex: number = this.getMaxCountIndex(); + if (maxCountIndex !== -1) { + this.uploadConfig.filters[maxCountIndex].params[0] += 1; + } + } + } +} diff --git a/src/upload/lib/src/TiUploadModule.ts b/src/upload/lib/src/TiUploadModule.ts new file mode 100644 index 0000000..00795a8 --- /dev/null +++ b/src/upload/lib/src/TiUploadModule.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiFileSelectDirective } from './TiFileSelectDirective'; +import { TiUploadComponent } from './TiUploadComponent'; +import { TiDisabledDirective } from './TiDisabledDirective'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiProgresspieModule } from '@opentiny/ng-progresspie'; +import { TiUploadServiceModule } from './TiUploadServiceModule'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiValidationModule } from '@opentiny/ng-validation'; +import { TiProgressbarModule } from '@opentiny/ng-progressbar'; +import { TiUploadbaseComponent } from './TiUploadbaseComponent'; +import { TiUploadimageComponent } from './TiUploadimageComponent'; +import { TiModalModule } from '@opentiny/ng-modal'; +import { TiImagepreviewModule } from '@opentiny/ng-imagepreview'; +import { TiButtonModule } from '@opentiny/ng-button'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +@NgModule({ + imports: [ + CommonModule, + TiIconModule, + TiTipModule, + TiProgresspieModule, + TiUploadServiceModule, + TiOverflowModule, + TiValidationModule, + TiProgressbarModule, + TiModalModule, + TiImagepreviewModule, + TiButtonModule, + TiOutlineModule + ], + exports: [TiFileSelectDirective, TiUploadComponent, TiDisabledDirective, TiUploadbaseComponent, TiUploadimageComponent], + declarations: [TiFileSelectDirective, TiUploadComponent, TiDisabledDirective, TiUploadbaseComponent, TiUploadimageComponent] +}) +export class TiUploadModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiUploadComponent } from './TiUploadComponent'; +export { TiFileSelectDirective } from './TiFileSelectDirective'; +export { TiDisabledDirective } from './TiDisabledDirective'; +export { TiUploadbaseComponent, TiUploadInitFile, TiUploadimageInitFile } from './TiUploadbaseComponent'; +export { TiUploadimageComponent } from './TiUploadimageComponent'; diff --git a/src/upload/lib/src/TiUploadService.ts b/src/upload/lib/src/TiUploadService.ts new file mode 100644 index 0000000..7f18540 --- /dev/null +++ b/src/upload/lib/src/TiUploadService.ts @@ -0,0 +1,395 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Injectable } from '@angular/core'; +import { Util } from '@opentiny/ng-utils'; +import { TiFileItemUtil } from './TiUploadUtil'; +import { TiUploadUtil } from './TiUploadUtil'; +import { TiUploadServiceModule } from './TiUploadServiceModule'; +import { TiFileInfo, TiFileItem, TiFilter, TiUploadConfig, TiUploadRef } from './TiFileInterface'; +import { TiUploadComponent } from './TiUploadComponent'; +import { TiUploadimageComponent } from './TiUploadimageComponent'; + +// 文件上传服务封装,一个实例对应一个上传文件队列 +// @dynamic +/** + * 文件上传服务,通过该服务生成上传文件实例对象,一个实例对应一个上传文件队列 + * + * 该服务适用于自定义文件上传实例方式,使用该服务时需要引入模块TiUploadServiceModule,与[tiFileSelect]{@link ../directives/TiFileSelectDirective.html}配合使用 + * + * 除自定义使用方式外,Tiny还提供了两种已设计的上传样式供业务使用,具体见[TiUploadComponent]{@link ../components/TiUploadComponent.html} + * + */ +@Injectable({ + providedIn: TiUploadServiceModule +}) +export class TiUploadService { + // 文件校验规则定义 + private static readonly filterRules: any = { + maxSize: (fileObj: TiFileInfo, params: Array): boolean => { + const size: number = fileObj.size; + if (!Util.isNumber(size)) { + // 文件大小获取不到情况下,忽略该条校验规则 + return true; + } + + return !(size > params[0]); + }, + minSize: (fileObj: TiFileInfo, params: Array): boolean => { + const size: number = fileObj.size; + if (!Util.isNumber(size)) { + // 文件大小获取不到情况下,忽略该条校验规则 + return true; + } + + return !(size < params[0]); + }, + type: (fileObj: TiFileInfo, params: Array): boolean => { + // param参数需使用文件扩展名分开,为了确保各浏览器的一致性,文件类型使用扩展名判断,如果产品需要在选择时限制,请在input上设置accept属性 + let isValidType: boolean = false; + params[0].split(',').forEach((type: string) => { + if (fileObj.name.match(new RegExp(`.(${type.replace(/\./g, '\\.')})$`, 'i')) !== null) { + isValidType = true; + } + }); + + return isValidType; + }, + maxCount: (fileObj: TiFileInfo, params: Array, fileQueue: Array): boolean => { + return !(fileQueue.length >= params[0]); + } + }; + + /** + * 对单选进行过滤条件重置,单选去除maxCount条件,函数返回过滤后的生效规则 + */ + private static initFilter(rules: Array, isSingleFile: boolean): Array { + if (isSingleFile) { + // 单文件情况下去除maxCount设置 + return rules.filter((rule: TiFilter) => { + return rule.name !== 'maxCount'; + }); + } + + // 多文件情况下 filter不做处理 + return rules || []; + } + /** + * 根据用户配置的maxCount过滤条件,判断文件是否为单文件上传 + * 用户配置的过滤条件 + * 是否为单文件上传 + */ + private static isSingleFileFn(filters: Array): boolean { + if (!filters || !filters.length) { + // 不设置filter情况下,为多文件上传 + return false; + } + const maxCountIndex: number = filters.findIndex((item: TiFilter): boolean => { + return item.name === 'maxCount'; + }); + // 存在maxCount规则,并且其参数不为1的情况下,为多文件上传 + if (maxCountIndex === -1 || (filters[maxCountIndex].params && filters[maxCountIndex].params[0] !== 1)) { + return false; + } + + return true; + } + + /** + * 批量上传文件 + * {Array} 上传文件items数组 + * 返回 无 + */ + private static uploadItems(items: Array): void { + if (!items.length) { + return; + } + // 设置上传文件标志位,文件逐个上传情况下,会根据该标志位决定下个上传文件 + items.forEach((item: TiFileItem) => { + item.isReady = true; + }); + + // 开始上传文件 + if (items[0].batchSend) { + TiUploadUtil.uploadItems(items); + } else { + // 上传单个文件,在upload方法中会依据isReady的设置进行其他文件的串行上传 + TiUploadUtil.uploadItems([items[0]]); + } + } + + /** + * 批量取消上传文件 + * 返回 无 + */ + private static cancelItems(items: Array): void { + if (!items.length) { + return; + } + if (items[0].batchSend) { + // 一个链接上传情况下,一次取消多个items,仅触发一次事件 + TiUploadUtil.cancelItems(items); + } else { + // 逐个链接上传情况下,一次仅取消一个items,触发多次事件 + items.forEach((item: TiFileItem) => { + item.cancel(); + }); + } + } + + /** + * 批量删除文件,只涉及上传队列的文件删除,具体的后台删除还需要产品向后台发送文件删除请求实现 + * {Array} 上传文件items数组 + * 返回 无 + */ + private static removeItems(items: Array): void { + if (!items.length) { + return; + } + if (items[0].batchSend) { + // 一个链接上传情况下,一次删除多个items,仅触发一次事件 + TiUploadUtil.removeItems(items); + } else { + // 逐个链接上传情况下,一次仅删除一个items,触发多次事件 + items.forEach((item: TiFileItem) => { + item.remove(); + }); + } + } + + /** + * 单个文件的有效性校验 + * 返回 {Array} 由不符合的规则name组成的数组 + */ + private static getInvalidRules(tifileObject: TiFileInfo, filtersRules: Array, fileQueue: Array): Array { + // 无效判断 + const filterLen: number = filtersRules.length; + if (filterLen === 0) { + return []; + } + + // 逐条规则校验,并返回结果数组 + const invalidRetArr: Array = []; // 校验返回结果数组,该数组中返回的是不符合的校验规则name + for (let i: number = 0; i < filterLen; i++) { + const filterConfig: TiFilter = filtersRules[i]; // 单条校验规则配置 + + // 根据配置寻找规则函数(规则分为默认规则 和 自定义规则) + const filterName: string = filterConfig.name; // 配置规则名称 + let ruleFn: (fileObj: TiFileInfo, params: Array, fileQueue: Array) => boolean; // 规则函数 + if (typeof filterConfig.fn === 'function') { + ruleFn = filterConfig.fn; + } else if (TiUploadService.filterRules[filterName]) { + // 未定义fn的情况下,从默认规则中找 + ruleFn = TiUploadService.filterRules[filterName]; + } + + // 调用规则函数,判断文件有效性 + if (typeof ruleFn === 'function' && !ruleFn(tifileObject, filterConfig.params, fileQueue)) { + invalidRetArr.push(filterName); + } + } + + return invalidRetArr; + } + /** + * 获取未上传文件队列 + */ + private static getNotUploadedItems(fileQueue: Array): Array { + return fileQueue.filter((item: TiFileItem): boolean => { + return !item.isUploaded; + }); + } + /** + * 获取待上传文件,该方法用于文件批量上传时获取上传文件队列 + * 返回 {Array} 返回待上传文件,且文件队列返回值是根据文件点选次序排列 + */ + private static getReadyItems(fileQueue: Array): Array { + return fileQueue + .filter((item: TiFileItem): boolean => { + return item.isReady && !item.isUploading; + }) + .sort((item1: TiFileItem, item2: TiFileItem): number => { + return item1.index - item2.index; + }); + } + + /** + * 获取上传失败文件,该方法用于文件批量上传时获取上传失败文件队列 + * 返回 {Array} 返回上传失败文件,且文件队列返回值是根据文件点选次序排列 + */ + private static getErrorItems(fileQueue: Array): Array { + return fileQueue + .filter((item: TiFileItem): boolean => { + return item.isError; + }) + .sort((item1: TiFileItem, item2: TiFileItem): number => { + return item1.index - item2.index; + }); + } + + /** + * 重新上传列表中所有先前上传错误文件 + * 返回 无 + */ + private static reloadAllError(fileQueue: Array): void { + const allowReloadFileItemLength = fileQueue.filter((item: TiFileItem) => item.allowReload).length; + // 没有允许重新上传的实例对象 + if (allowReloadFileItemLength === 0) { + return; + } + TiUploadService.uploadItems(TiUploadService.getErrorItems(fileQueue)); + } + + /** + * 重新上传列表中所有文件 + * 返回 无 + */ + private static reloadAll(fileQueue: Array): void { + TiUploadService.uploadItems(fileQueue); + } + + /** + * 上传列表中所有未上传过的文件 + * 返回 无 + */ + private static uploadAll(fileQueue: Array): void { + const items: Array = this.getNotUploadedItems(fileQueue).filter((item: TiFileItem): boolean => { + return !item.isUploading; + }); + TiUploadService.uploadItems(items); + } + + /** + * 判断是否有未上传的 + * 返回 boolean + */ + private static isUploadedAll(fileQueue: Array): boolean { + const items: Array = this.getNotUploadedItems(fileQueue).filter((item: TiFileItem): boolean => { + return !item.isUploading; + }); + + return items.length === 0; + } + + /** + * 删除所有队列中的文件 + */ + private static removeAll(fileQueue: Array): void { + while (fileQueue.length !== 0) { + fileQueue[0].remove(); + } + } + + /** + * 取消所有队列中的文件(未上传和待上传文件) + */ + private static cancelAll(fileQueue: Array): void { + const items: Array = this.getNotUploadedItems(fileQueue); + TiUploadService.cancelItems(items); + } + + /** + * 创建文件上传实例 + */ + create(config: TiUploadConfig, uploadComInst?: TiUploadComponent | TiUploadimageComponent | any): TiUploadRef { + const isSingleFile: boolean = TiUploadService.isSingleFileFn(config.filters); // 文件是否为单文件设置 + const filters: Array = TiUploadService.initFilter(config.filters, isSingleFile); // 文件校验过滤规则 + + let uploader: TiUploadRef; + const fileQueue: Array = []; // 文件队列列表,一个实例对应一个文件列表 + let fileIndex: number = 0; // 文件次序定义,用作文件索引 + /** + * 将选择的文件进行有效验证后加入到队列中,外部调用该方法时,认为该队列中的文件和当前uploader的配置项一致 + * files {FileList|FileInput} 文件对象 + * 返回 {Array} 已添加文件数组 + */ + const addToQueue: (files: FileList | Element) => Array = (files: FileList | Element): Array => { + // 循环列表添加文件 + const addedItems: Array = []; // 用于记录本次选择文件列表 + if (files && !Util.isUndefined((files as FileList).length)) { + // H5方式 + for (let i: number = 0; i < (files as FileList).length; i++) { + addItem(files[i], addedItems); + } + } else { + addItem(files, addedItems); + } + + return addedItems; + }; + const addItem: (fileOrInput: any, addedItems: Array) => void = (fileOrInput: any, addedItems: Array): void => { + // 文件对象,该对象包含文件的基本信息,统一了浏览器的差异性,会在文件过滤失败回调中作为参数传递给外部 + const tifileObject: TiFileInfo = TiFileItemUtil.createFileObject(fileOrInput); + + // 校验单个文件的有效性,并根据校验结果进行文件操作 + const invalidArr: Array = TiUploadService.getInvalidRules(tifileObject, filters, fileQueue); // 校验结果数组,数组中定义校验错误规则的name + let isValid: boolean = !invalidArr.length; // 校验结果返回数组为空时有效 + + // 单文件情况下,需要覆盖原有有效文件 + if (isSingleFile) { + fileQueue[0]?.remove(); + uploadComInst?.initFiles && uploadComInst.initFiles[0]?.remove(); + if (invalidArr.length === 0) { + isValid = true; // 置位校验结果值 + } + } + if (isValid) { + // 校验成功情况下,将文件加入到上传队列中,并触发 + // 上传文件对象,该对象中包含文件及状态信息等,在回调中传递 + const fileItem: TiFileItem = TiFileItemUtil.createFileItem(tifileObject, fileOrInput, config, uploader); + addedItems.push(fileItem); + fileQueue.push(fileItem); + fileItem.index = ++fileIndex; // 文件序列数增加,确保整个文件队列中该值唯一 + if (typeof config.onAddItemSuccess === 'function') { + config.onAddItemSuccess(fileItem); + } + } else if (typeof config.onAddItemFailed === 'function') { + config.onAddItemFailed(tifileObject, invalidArr); + } + }; + uploader = { + queue: fileQueue, // 上传文件队列,可读属性 + isSingleFile, // 文件是否有单文件选择限制 + config, // 上传文件配置信息 + _uploadComponentInstance: uploadComInst, + _addToQueue: addToQueue, // 文件添加方法 + getNotUploadedItems(): Array { + return TiUploadService.getNotUploadedItems(fileQueue); + }, // 未上传完成文件获取 + getReadyItems(): Array { + return TiUploadService.getReadyItems(fileQueue); + }, // 已提交上传,但还未上传文件获取 + uploadAll(): void { + TiUploadService.uploadAll(fileQueue); + }, // 上传队列中所有还未执行过上传的文件 + isUploadedAll(): boolean { + return TiUploadService.isUploadedAll(fileQueue); + }, // 上传队列中是否有未上传过的 + removeAll(): void { + TiUploadService.removeAll(fileQueue); + }, // 删除队列中所有文件 + cancelAll(): void { + TiUploadService.cancelAll(fileQueue); + }, // 取消队列中所有上传文件 + reloadAll(): void { + TiUploadService.reloadAll(fileQueue); + }, // 重新上传队列中所有文件 + reloadAllError(): void { + TiUploadService.reloadAllError(fileQueue); + }, // 重新上传队列中所有上传错误文件 + uploadItems: TiUploadService.uploadItems, // 上传队列中某几项文件 + removeItems: TiUploadService.removeItems, // 删除队列中某几项文件 + cancelItems: TiUploadService.cancelItems // 取消队列中某几项文件 + }; + + return uploader; + } +} diff --git a/src/upload/lib/src/TiUploadServiceModule.ts b/src/upload/lib/src/TiUploadServiceModule.ts new file mode 100644 index 0000000..20d624a --- /dev/null +++ b/src/upload/lib/src/TiUploadServiceModule.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +/** + * @ignore + * 此TiUploadServiceModule不对用户暴露,用户仅需import TiUploadModule,则间接引入了TiUploadServiceModule + */ +@NgModule({ + imports: [CommonModule] +}) +export class TiUploadServiceModule {} diff --git a/src/upload/lib/src/TiUploadUtil.ts b/src/upload/lib/src/TiUploadUtil.ts new file mode 100644 index 0000000..771bfea --- /dev/null +++ b/src/upload/lib/src/TiUploadUtil.ts @@ -0,0 +1,749 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiChunked, TiFileInfo, TiFileItem, TiUploadConfig, TiUploadRef } from './TiFileInterface'; +import { Util } from '@opentiny/ng-utils'; +import { map, mergeAll } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; + +// 上传文件服务,提供上传过程中的通用方法 +/** + * @ignore + */ +export class TiUploadUtil { + // 已支持SSR + public static readonly isHTML5: boolean = typeof window !== 'undefined' && (window as any).File && (window as any).FormData; + /** + * 上传多个文件 + * 上传一个或多个文件item对象 + * 无 + */ + public static uploadItems(items: Array): void { + // 异常处理 1.入参不合法,2.正在上传的文件不允许重复上传 + for (let i: number = items.length - 1; i >= 0; i--) { + if (!TiUploadUtil.isValidFileItem(items[i]) || items[i].isUploading) { + items.splice(i, 1); + } + } + TiUploadUtil.onBeforeSend(items); // 文件上传前处理 + for (let j: number = items.length - 1; j >= 0; j--) { + if (items[j].isCancel) { + // 已取消上传的文件不再上传(此处主要是对beforeSend中取消上传的文件进行处理) + items.splice(j, 1); + } + } + if (items.length > 0) { + TiUploadUtil.isHTML5 ? TiUploadUtil.uploadXhr(items) : TiUploadUtil.uploadForm(items); + } + } + + /** + * 取消队列中多个正在上传中的文件(只在上传时可取消) + * 文件item对象数组 + * 无 + */ + public static cancelItems(items: Array): void { + for (let i: number = items.length - 1; i >= 0; i--) { + if (!TiUploadUtil.isValidFileItem(items[i])) { + // 异常处理:入参不合法情况下,不做处理 + items.splice(i, 1); + } else { + items[i].isCancel = true; + } + } + if (items[0].isUploading) { + // 正在上传过程中,取消上传,上传事件中,会处理响应赋值及事件触发 + TiUploadUtil.isHTML5 ? items[0]._xhr.abort() : items[0]._form.abort(); + } else { + // 未在上传过程中情况下,依然触发cancel和complete事件 + const response: string = undefined; + const status: number = 0; + TiUploadUtil.onCancel(items, response, status); + TiUploadUtil.onComplete(items, response, status); + } + } + + /** + * 删除队列中单个或多个上传文件 + * 文件item对象数组 + */ + public static removeItems(items: Array): void { + const itemsArr: Array = { ...items }; + for (let i: number = items.length - 1; i >= 0; i--) { + if (!TiUploadUtil.isValidFileItem(items[i])) { + items.splice(i, 1); + } + } + // 阻止文件上传进程 + if (items[0].isUploading) { + TiUploadUtil.cancelItems(items); + } + // 删除队列中的文件对象及文件对象相关引用 + for (let j: number = items.length - 1; j >= 0; j--) { + items[j].uploader.queue.splice(TiUploadUtil.getItemIndex(items[j]), 1); + itemsArr[j].destroy(); + } + // 触发外部定义的删除事件 + TiUploadUtil.onRemove(itemsArr); + } + + /** + * 触发onBeforeRemoveItems事件 + * 文件item对象数组 + */ + public static onBeforeRemove(items: Array): void { + TiUploadUtil.handleEvent('BeforeRemove', items); + } + + /** + * 获取文件对象 index值,获取该值用于后续判断item有效性 + * 上传文件item对象 + */ + private static getItemIndex(item: TiFileItem): number { + if (item && item.uploader) { + return item.uploader.queue.findIndex((itemInQueue: TiFileItem) => { + return itemInQueue.index === item.index; + }); + } + + return -1; + } + + /** + * 判断文件是否为有效文件 + * 上传文件item对象 + */ + private static isValidFileItem(item: TiFileItem): boolean { + return TiUploadUtil.getItemIndex(item) !== -1; + } + // 生成上传数据 + private static generateUploadData(items: Array): any { + // 上传数据组装 + const uploadDataObj: any = new FormData(); + if (items[0].formDataFirst) { + // formData先于文件信息情况下,先添加formData + addFormData(); + addFile(); + } else { + // formData后于文件信息情况下,后添加formData + addFile(); + addFormData(); + } + function addFile(): void { + items.forEach((item: TiFileItem | TiChunked) => { + uploadDataObj.append(item.alias, item._file, item.file.name); // 添加上传文件 + }); + } + function addFormData(): any { + // 添加单个文件的formData对象,加入到上传对象中 + for (const key in items[0].formData) { + if (Object.prototype.hasOwnProperty.call(items[0].formData, key)) { + uploadDataObj.append(key, items[0].formData[key]); + } + } + } + + return uploadDataObj; + } + private static setXhr(xhr: any, items: Array): void { + // 文件上传信息配置(使用XHR,支持跨域请求) + xhr.upload.onprogress = (event: any): void => { + // 文件进度获取事件 + const { lengthComputable, loaded, total } = event; + const progress: number = Math.round(lengthComputable ? (loaded * 100) / total : 0); // 读取当前进度信息 + TiUploadUtil.onProgress(items, progress); + }; + + xhr.onload = (): void => { + // 上传完成事件 + const { response, status } = xhr; + if (TiUploadUtil.isSuccessCode(status)) { + TiUploadUtil.onSuccess(items, response, status); + } else { + TiUploadUtil.onError(items, response, status); + } + TiUploadUtil.onComplete(items, response, status); + }; + + xhr.onerror = (): void => { + // 上传失败事件 + const { response, status } = xhr; + TiUploadUtil.onError(items, response, status); + TiUploadUtil.onComplete(items, response, status); + }; + + xhr.onabort = (): void => { + // 取消回调 + const { response, status } = xhr; + TiUploadUtil.onCancel(items, response, status); + TiUploadUtil.onComplete(items, response, status); + }; + + // 设置单个item的_xhr,该对象用于操作单个item的上传取消等处理 + items.forEach((item: TiFileItem) => { + item._xhr = xhr; + }); + } + private static setheaders(xhr: any, items: Array): void { + // 设置请求头 + for (const key in items[0].headers) { + if (Object.prototype.hasOwnProperty.call(items[0].headers, key)) { + xhr.setRequestHeader(key, items[0].headers[key]); + } + } + } + /** + * xhr方式上传文件 + * 上传一个或多个文件item对象,上传单个文件的情况下 + */ + private static uploadXhr(items: Array): void { + if (items.length === 1 && items[0].uploader.config.chunkSize && items[0].file.size > items[0].uploader.config.chunkSize) { + TiUploadUtil.uploadChunkedFile(items, items[0].uploader.config.chunkSize); + return; + } + const uploadDataObj: TiFileInfo = TiUploadUtil.generateUploadData(items); // 组装上传对象 + // 生成并设置xhr + const xhr: any = new XMLHttpRequest(); + TiUploadUtil.setXhr(xhr, items); + + // 开始上传 + xhr.open(items[0].method, items[0].url, true); + TiUploadUtil.setheaders(xhr, items); + xhr.send(uploadDataObj); + + // 设置对象的上传状态 + items.forEach((item: TiFileItem) => { + item.isUploading = true; + }); + } + private static uploadForm(items: Array): void { + const form: any = document.createElement('form'); + form.style.display = 'none'; + for (const item of items) { + // 清除原有form表单 + if (item._form) { + item._form.parentNode.replaceChild(items[0]._input, items[0]._form); // 清除先前对应的form表单对象,确保上传文件表单元素外层不被form包裹 + } + item._form = form; // 保存当前form + } + items[0]._input.parentNode.insertBefore(form, items[0]._input); // form插入inputSubmit之前,使其在页面显示 + + if (items[0].formDataFirst) { + // formData先于文件信息情况下,先添加formData + addFormData(); + addFile(); + } else { + // formData后于文件信息情况下,后添加formData + addFile(); + addFormData(); + } + + // 文件信息组装 + function addFile(): void { + for (const item of items) { + const inputSubmit: Element = item._input; + inputSubmit.setAttribute('name', item.alias); + form.appendChild(inputSubmit); // 在form中添加上传文件元素 + } + } + + // formData信息组装 + function addFormData(): void { + // 添加formData元素 + for (const key in items[0].formData) { + if (Object.prototype.hasOwnProperty.call(items[0].formData, key)) { + const formDataDomItem: any = document.createElement('input'); + formDataDomItem.setAttribute('type', 'hidden'); + formDataDomItem.setAttribute('name', key); + formDataDomItem.value = items[0].formData[key]; + form.appendChild(formDataDomItem); + } + } + } + + // 生成iframe元素,并将form表单和iframe元素结合 + const iframe: any = document.createElement('iframe'); + const name: string = Util.getUniqueId('tiFileIframe'); // 确保iframe唯一性,保证各文件上传最终能独立返回到相应的iframe + iframe.setAttribute('name', name); + form.setAttribute('action', items[0].url); + form.setAttribute('method', items[0].method); + form.setAttribute('target', name); + form.setAttribute('enctype', 'multipart/form-data'); + form.appendChild(iframe); + + // 表单提交 + form.submit(); + // 设置对象的上传状态 + items.forEach((item: TiFileItem) => { + item.isUploading = true; + }); + + // 设置假进度 + let newProgress: number = 0; + const progressInterval: any = setInterval(() => { + if (newProgress !== 98) { + newProgress += 2; + this.onProgress(items, newProgress); + } + }, 10); + + // 表单完成事件 + const loadEvent: () => void = (): void => { + let response: string = ''; + let status: number = 200; + + clearInterval(progressInterval); + this.onProgress(items, 100); + try { + response = iframe.contentDocument.body.innerHTML; // 后台正常返回情况获取返回结果 + this.onSuccess(items, response, status); + } catch (e) { + response = e; + status = 520; // 为方便使用者处理,失败情况下,统一返回520状态码 未知错误 + this.onError(items, response, status); + } + + this.onComplete(items, response, status); + }; + iframe.addEventListener('load', loadEvent); + + // 表单取消方法定义 + form.abort = (): void => { + clearInterval(progressInterval); + iframe.removeEventListener('load', loadEvent); // 去除load事件 + // 表单元素还原 + for (const node of form.childNodes) { + if ((node as any).tagName !== 'IFRAME') { + if (form.parentNode.lastChild === form) { + form.parentNode.appendChild(node); + } else { + form.parentNode.insertBefore(node, form.nextSibling); + } + } + } + form.remove(); + items.forEach((item: TiFileItem) => { + item._form = null; + }); + + const status: number = 0; + const response: string = undefined; + this.onCancel(items, response, status); + this.onComplete(items, response, status); + }; + } + + private static isSuccessCode(status: number): boolean { + // 和ajax请求一致,304 代表客户端已经执行了GET,但文件未变化 + return (status >= 200 && status < 300) || status === 304; + } + + /** + * 上传前处理 + * 上传一个或多个文件item对象 + */ + private static onBeforeSend(items: Array): void { + // 设置当前上传文件的状态信息 + for (const item of items) { + item.isReady = true; + item.isUploading = false; + item.isUploaded = false; + item.isSuccess = false; + item.isError = false; + item.isCancel = false; + item.progress = 0; + } + TiUploadUtil.handleEvent('BeforeSend', items); + } + + private static handleEvent(type: string, items: Array, params: Array = []): void { + const onEventTypeItems: any = items[0].uploader.config && items[0].uploader.config[`on${type}Items`]; + if (onEventTypeItems && typeof onEventTypeItems === 'function') { + onEventTypeItems.apply(null, [items, ...params]); + } + // onpush模式下文件上传实例状态未及时刷新 + if (items[0].uploader._uploadComponentInstance) { + items[0].uploader._uploadComponentInstance.changeDetectorRef.markForCheck(); + } + } + + private static onProgress(items: Array, progress: number): void { + TiUploadUtil.handleEvent('Progress', items, [progress]); + for (const item of items) { + item.progress = progress; // 设置单个文件的进度信息 + } + } + + private static onSuccess(items: Array, response: string, status: number): void { + // 设置当前上传文件的状态信息 + for (const item of items) { + item.isReady = false; + item.isUploading = false; + item.isUploaded = true; + item.isSuccess = true; + item.isError = false; + item.isCancel = false; + item.progress = 100; + } + TiUploadUtil.handleEvent('Success', items, [response, status]); + } + + private static onError(items: Array, response: string, status: number): void { + // 设置当前上传文件的状态信息 + for (const item of items) { + // 设置当前上传文件的状态信息 + item.isReady = false; + item.isUploading = false; + item.isUploaded = true; + item.isSuccess = false; + item.isError = true; + item.isCancel = false; + item.progress = 0; + } + TiUploadUtil.handleEvent('Error', items, [response, status]); + } + + private static onComplete(items: Array, response: string, status: number): void { + // 置位状态 + items.forEach((item: TiFileItem) => { + item.isUploading = false; + }); + // 上传下一个文件,对于同一序列的上传文件来说,其isReady状态均已被标识,因此此处可以通过该方法进行筛选 + const readyItemsArr: Array = items[0].uploader.getReadyItems(); + if (readyItemsArr && readyItemsArr.length !== 0) { + this.uploadItems([readyItemsArr[0]]); + } else { + // 本次上传序列中,所有文件上传完成后回调,因为文件上传是串行上传: + // 在一个文件上传完成后才执行下一个文件的上传操作,所以此处可以确保同一序列最终的上传完成 + TiUploadUtil.handleEvent('CompleteAll', items, [response, status]); + } + TiUploadUtil.handleEvent('Complete', items, [response, status]); + } + + private static onCancel(items: Array, response: string, status: number): void { + for (const item of items) { + item.isReady = false; + item.isUploading = false; + item.isUploaded = false; + item.isSuccess = false; + item.isError = false; + item.isCancel = true; + item.progress = 0; + } + TiUploadUtil.handleEvent('Cancel', items, [response, status]); + } + + private static onRemove(items: Array): void { + TiUploadUtil.handleEvent('Remove', items); + } + + private static chunkedFilePromise(items: Array, fileChunkItem: Array, totalNum: number): any { + const uploadDataObj: TiFileInfo = TiUploadUtil.generateUploadData(fileChunkItem); // 组装上传对象 + // 生成并设置xhr + const xhr: any = new XMLHttpRequest(); + + return new Promise((resolve: any): void => { + if (fileChunkItem[0].index === totalNum - 1) { + // 上传是并发上传的没法知道多个请求的进度,暂时以发起的倒数第二个请求的进度作为总体的进度吧 + xhr.upload.onprogress = (event: any): void => { + // 文件进度获取事件 + const { lengthComputable, loaded, total } = event; + const progress: number = Math.round(lengthComputable ? (loaded * 100) / total : 0); // 读取当前进度信息 + TiUploadUtil.onProgress(items, progress); + }; + } + + xhr.onload = (): void => { + // 上传完成事件 + const { response, status } = xhr; + resolve({ fileChunkItem, response, status }); + }; + + xhr.onerror = (): void => { + // 上传失败事件 + const { response, status } = xhr; + resolve({ fileChunkItem, response, status }); + }; + + xhr.onabort = (): void => { + // 取消回调,在请求发送时可能因为浏览器导致请求未发出,此时 reponse 也为''为了区分这里传出一个自定义的值 + const response: string = 'tiCancel'; + const status: number = xhr.status; + resolve({ fileChunkItem, response, status }); + }; + // 开始上传 + xhr.open(fileChunkItem[0].method, fileChunkItem[0].url, true); + TiUploadUtil.setheaders(xhr, fileChunkItem); + xhr.send(uploadDataObj); + fileChunkItem[0]._xhr = xhr; + }); + } + + private static uploadChunkedFile(items: Array, chunkSize: number): void { + const file: File = items[0]._file; // 拿到文件H5下存在_file;不考虑 ie 下_input + const { name, type, size } = file; + const fileTime: number = new Date().getTime(); + const fileChunkList: Array = []; + let fileSliceStart: number = 0; + let chunkedFileIndex: number = 0; + const chunks: number = Math.ceil(size / chunkSize); + while (fileSliceStart < file.size) { + chunkedFileIndex = chunkedFileIndex + 1; + const slicedFile: Blob = file.slice(fileSliceStart, fileSliceStart + chunkSize); + const newChunkFile: File = new File([slicedFile], `${fileTime}-${chunkedFileIndex}/${chunks}-${name}`, { type }); + const fileItem: TiChunked = { + url: items[0].url, + file: TiFileItemUtil.createFileObject(newChunkFile), + alias: items[0].alias, + _file: newChunkFile, + formData: items[0].formData || {}, + formDataFirst: items[0].formDataFirst, + headers: items[0].headers, + method: items[0].method, + _xhr: {}, + index: chunkedFileIndex + }; + fileSliceStart += chunkSize; + fileChunkList.push(fileItem); + } + TiUploadUtil.uploadChunkedXhr(items, fileChunkList); + } + + private static uploadChunkedXhr(items: Array, fileChunkList: Array): void { + items[0].isUploading = true; + let isSuccess: boolean = true; + let currentFileIndex: number = 0; + const source: Observable = of(...fileChunkList); + const chunkedUploadSub: Observable = source.pipe( + map((fileChunkItem: TiChunked) => TiUploadUtil.chunkedFilePromise(items, [fileChunkItem], fileChunkList.length)), + mergeAll() + ); + chunkedUploadSub.subscribe((val: any) => { + currentFileIndex++; + if (isSuccess && currentFileIndex === fileChunkList.length && TiUploadUtil.isSuccessCode(val.status)) { + // 如果之前都是成功的并且是最后一次且当次也成功 那么都上传成功 走 success 回调 + TiUploadUtil.onSuccess(items, val.response, val.status); + TiUploadUtil.onComplete(items, val.response, val.status); + } else if (isSuccess && val.response === 'tiCancel') { + // 如果之前都是成功的且触发了取消;那么是因为主动取消上传;走 cancel 的回调;这里需要修改标记值保证只有第一次取消才触发 + isSuccess = false; + TiUploadUtil.onCancel(items, '', val.status); + TiUploadUtil.onComplete(items, '', val.status); + } else if (isSuccess && !TiUploadUtil.isSuccessCode(val.status)) { + // 如果之前都是成功的并且上传状态码不是正确的那么上传失败;一旦失败修改标记值 走 error 回调并取消所有上传 + isSuccess = false; + TiUploadUtil.onError(items, val.response, val.status); + TiUploadUtil.onComplete(items, val.response, val.status); + // 有失败且不是最后一项取消上传 + currentFileIndex !== fileChunkList.length && TiUploadUtil.cancelFileChunkList(fileChunkList); + } + // 这里还应该有其他情况但不需要处理,暂时考虑以下两种: + // 1.因为 error 所以 cancel ,此时 isSuccess 为 false + // 2.因为 cancel 所以 cancel ,此时 isSuccess 为 false + }); + TiUploadUtil.replaceFileMethod(items, fileChunkList); + } + + // 这里替换掉原有 items[0] 的 cancel/remove/upload 方法;因为应该触发的不是原文件而是分片之后的文件队列 + private static replaceFileMethod(items: Array, fileChunkList: Array): void { + // 覆盖原有取消方法,原有方法不是箭头函数 this 指向items[0]这个对象本身;所以以下也用funcation去定义 + items[0].cancel = function (): void { + items[0].isUploading && TiUploadUtil.cancelFileChunkList(fileChunkList); + TiUploadUtil.onCancel(items, fileChunkList[0]._xhr.response, fileChunkList[0]._xhr.status); + TiUploadUtil.onComplete(items, fileChunkList[0]._xhr.response, fileChunkList[0]._xhr.status); + }; + // 覆盖原有移除方法 + items[0].remove = function (isRemove?: boolean): void { + if (!this.allowDelete) { + return; + } + if (isRemove) { + // 触发外部定义的删除前的事件 + TiUploadUtil.onBeforeRemove([this]); + + return; + } + // 阻止文件上传进程 + items[0].isUploading && TiUploadUtil.cancelFileChunkList(fileChunkList); + items[0].uploader.queue.splice(TiUploadUtil.getItemIndex(items[0]), 1); + items[0].destroy(); + TiUploadUtil.onRemove(items); + }; + // 覆盖原有上传方法 + items[0].upload = function (): void { + if (!this.allowReload) { + return; + } + TiUploadUtil.onBeforeSend(items); + TiUploadUtil.uploadChunkedFile(items, items[0].uploader.config.chunkSize); + }; + } + + private static cancelFileChunkList(fileChunkList: Array): void { + fileChunkList.forEach((i: TiChunked) => { + i._xhr.abort(); + }); + } +} + +// 单个文件对象服务封装,包括文件基本信息统一、文件状态信息、文件基本方法封装 +/** + * @ignore + */ +export class TiFileItemUtil { + /** + * 创建文件对象,该返回值用作后续文件的上传和文件操作,可作为文件回调参数传递 + * fileObject 文件信息对象 + * fileOrInpt 原始文件对象 H5下为file对象,非H5下为fileInput + * config 文件上传配置信息 + * uploader 上传文件对应的文件实例 + * return 文件对象 + */ + public static createFileItem( + tifileObject: TiFileInfo, + fileOrInput: Element | File, + config: TiUploadConfig, + uploader: TiUploadRef + ): TiFileItem { + function upload(): void { + if (!this.allowReload) { + return; + } + TiUploadUtil.uploadItems([this]); + } + function cancel(): void { + TiUploadUtil.cancelItems([this]); + } + + function remove(isRemove?: boolean): void { + if (!this.allowDelete) { + return; + } + if (isRemove) { + // 触发外部定义的删除前的事件 + TiUploadUtil.onBeforeRemove([this]); + + return; + } + + TiUploadUtil.removeItems([this]); + } + + function destroy(): void { + if (this._input) { + // 删除页面残留dom + this._input.remove(); + } + if (this._form) { + this._form.remove(); + } + this._input = null; // 清除引用 + this._form = null; + } + + // 上传文件对象赋值 + let _file: any = null; + let _input: any = null; + if (!(fileOrInput instanceof Element)) { + _file = fileOrInput; + } else { + _input = fileOrInput; + } + + return { + url: config.url || '/', // 后台地址 + file: tifileObject, + alias: config.alias || 'tiFile', // 文件name + _file, + _input, + formData: config.formData || {}, + formDataFirst: config.formDataFirst || false, + headers: config.headers || {}, + method: config.method || 'post', + batchSend: config.batchSend || false, + uploader, // 上传实例对象 + + upload, + cancel, + remove, + destroy, + + isReady: false, + isUploading: false, + isUploaded: false, + isCancel: false, + isSuccess: false, + isError: false, + progress: 0, + + isHover: false, + isOverflow: false, + _xhr: {}, + index: 0, + _form: undefined + }; + } + + /** + * 创建文件对象,统一H5和非H5情况下的文件信息 + * file {FileList|FileInput} 原始文件对象 H5为file对象形式,非H5为fileinput对象 + * return 类文件对象 + */ + public static createFileObject(fileThis: any): TiFileInfo { + if (TiUploadUtil.isHTML5) { + const fileName: string = fileThis.name; + + return { + lastModifiedDate: fileThis.lastModifiedDate, + size: fileThis.size, // 读取的文件真实大小值,单位为B + sizeWithUnit: TiFileItemUtil.formatSize(fileThis.size), // 做单位转换后的文件大小,方便界面详情显示显示 + name: fileThis.name, + type: fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase(), // 确保浏览器形式的一致性 + _file: fileThis, // 文件对象,只在H5方式下有效 + _input: null // 文件input对象,只在IE9 form表单提交方式下有效 + }; + } + const path: string = fileThis.value; + // 非H5情况下,浏览器不打开ActiveX,获取不到文件大小,该种情况下文件大小为null + const fileSize: number = Util.isUndefined(fileThis.size) ? null : fileThis.size; + + return { + lastModifiedDate: null, // 非H5情况下,获取不到该值 + size: fileSize, + sizeWithUnit: isNaN(fileSize) ? '' : this.formatSize(fileSize / 1024), // 做单位转换后的文件大小,方便界面详情显示显示 + name: path.slice(path.lastIndexOf('/') + path.lastIndexOf('\\') + 2), + type: path.slice(path.lastIndexOf('.') + 1).toLowerCase(), + _file: null, // 文件对象,只在H5方式下有效 + _input: fileThis // 文件input对象,只在IE9 form表单提交方式下有效 + }; + } + + /** + * 将文件大小显示标准化:根据文件大小做不同单位的显示,文件大小保留两位小数 + * 文件大小 + * 带单位文件大小 + */ + private static formatSize(size: number): string { + let sizeWithUnit: string; + const kbSize: number = size / 1024; + if (kbSize < 1) { + sizeWithUnit = size.toFixed(2) + 'B'; + } else if (kbSize < 1024) { + sizeWithUnit = kbSize.toFixed(2) + 'KB'; + } else if (kbSize < 1024 * 1024) { + sizeWithUnit = (kbSize / 1024).toFixed(2) + 'MB'; + } else { + sizeWithUnit = (kbSize / 1024 / 1024).toFixed(2) + 'GB'; + } + + return sizeWithUnit; + } +} diff --git a/src/upload/lib/src/TiUploadbaseComponent.ts b/src/upload/lib/src/TiUploadbaseComponent.ts new file mode 100644 index 0000000..c1678ef --- /dev/null +++ b/src/upload/lib/src/TiUploadbaseComponent.ts @@ -0,0 +1,401 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ElementRef, EventEmitter, Input, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core'; +import { TiFileInfo, TiFileItem, TiFilter, TiUploadConfig, TiUploadRef } from './TiFileInterface'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiBrowser, Util } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; +/** + * 初始化显示的文件列表项目(只适用于button/textButton类型) + */ +export interface TiUploadInitFile { + /** + * 文件名 + */ + name: string; + /** + * 操作按钮(删除按钮)是否禁用 + */ + allowDelete: boolean; + /** + * 删除该文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + remove?(): void; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} + +/** + * 初始化显示的已上传的图片文件信息 + * + * + */ +export interface TiUploadimageInitFile { + /** + * 图片文件名 + */ + name: string; + /** + * 图片预览地址 + */ + previewUrl?: string; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} + +/** + * + * upload基类,继承于TiBaseComponent组件 + */ +@Component({ + selector: 'ti-uploadbase', + template: '' +}) +export class TiUploadbaseComponent extends TiBaseComponent implements OnInit { + /** + * 必选,上传地址 + */ + @Input() url: string; + /** + * 上传方式 + */ + @Input() method: 'get' | 'post' = 'post'; + /** + * 自定义过滤器 + */ + @Input() filters: Array; + /** + * 上传的文件字段名 + */ + @Input() alias: string = 'tiFile'; + /** + * 上传文件的附带信息 + */ + @Input() formData: object; + /** + * 上传对象的附带信息是否先于file对象 + */ + @Input() formDataFirst: boolean = false; + /** + * 是否自动上传 + */ + @Input() autoUpload: boolean = true; + /** + * 是否一次上传多个文件。默认情况下,一次上传一个文件 + */ + @Input() batchSend: boolean = false; + /** + * + * 上传文件请求头配置,自定义为对象形式 + * + * 设置'X-Requested-With'可以判断是Ajax请求还是其他请求;'X-Requested-With':'XMLHttpRequest'返回数据是json,不设置返回普通html文本 + */ + /** + * 上传文件请求头 + */ + @Input() headers: object; + // 文件类型过滤配置,适用于IE10+浏览器,能做到文件弹窗中不出现非法类型的文件,主要适用于两种场景: + // 1.过滤规则中带类型过滤,但是想要去掉文件选择框中文件过滤的场景(设置accept为空字符串即可):此种场景应用于浏览器设置accept后导致文件窗打开很慢的情况 + // 2.不需要定义文件类型过滤条件,但是通过accept能精确的确保文件类型的场景 + /** + * 限制文件上传类型。设置此属性后,选择文件时的资源管理窗口将自动隐藏不符合规则的文件 + */ + @Input() accept: string; + + /** + * 是否禁用 + */ + @Input() disabled: boolean = false; + + /** + * 分片大小,设置后开启分片上传单位是 b + */ + @Input() chunkSize: number; + + /** + * @ignore + */ + title: string = TiBrowser.isChrome() ? ' ' : ''; // 禁用文件上传默认tip提示,Chrome下设置title为""时会显示默认提示,所以Chrome下需要设置为" ";而IE下设置为" "会显示文本为空的tip + + // 文件添加失败回调,可使用该回调定义上传错误提示 + // 参数为对象形式,结构如下: + // { + // file: [TiFileInfo]{@link ../interfaces/TiFileInfo.html}, // 上传文件信息 + // validResults: Array<string> // 校验不合法的规则name数组 + // } + /** + * 文件添加失败时触发的回调,可使用该回调定义上传错误提示,参数:文件对象 + */ + @Output() readonly addItemFailed: EventEmitter<{ + file: TiFileInfo; + validResults: Array; + }> = new EventEmitter<{ + file: TiFileInfo; + validResults: Array; + }>(); + /** + * 文件添加成功时触发的回调,参数:文件对象 + */ + @Output() readonly addItemSuccess: EventEmitter = new EventEmitter(); + // 以下回调对单个或多个文件在同一url同时上传有效,可使用其进行文件上传过程中的业务处理 + /** + * 上传文件前触发的回调,可在该回调中动态设置formData,参数:文件对象列表 + */ + @Output() readonly beforeSendItems: EventEmitter> = new EventEmitter>(); + // 上传文件进度改变回调 + // 该回调对单个或多个文件在同一url同时上传有效,可使用其进行文件上传过程中的业务处理 + // 参数为对象形式,结构如下: + // { + // fileItems: Array<[TiFileItem]{@link ../interfaces/TiFileItem.html}>, // 上传文件对象数组 + // progress: number // 进度信息 + // } + /** + * 上传进度改变时触发的回调,参数:文件对象 + */ + @Output() readonly progressItems: EventEmitter<{ + fileItems: Array; + progress: number; + }> = new EventEmitter<{ + fileItems: Array; + progress: number; + }>(); + // 文件上传完成回调,成功/失败都会触发 + // 该回调对单个或多个文件在同一url同时上传有效,可使用其进行文件上传过程中的业务处理 + // 参数为对象形式,结构如下: + // { + // fileItems: Array<[TiFileItem]{@link ../interfaces/TiFileItem.html}>, // 上传文件对象数组 + // response: string // 文件上传响应信息 + // status: number 文件上传响应状态码 + // } + /** + * 上传完成(成功或失败)时触发的回调,参数:文件对象 + */ + @Output() readonly completeItems: EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }> = new EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }>(); + // 文件上传成功回调 + // 该回调对单个或多个文件在同一url同时上传有效,可使用其进行文件上传过程中的业务处理 + // 参数为对象形式,结构如下: + // { + // fileItems: Array<[TiFileItem]{@link ../interfaces/TiFileItem.html}>, // 上传文件对象数组 + // response: string // 文件上传响应信息 + // status: number 文件上传响应状态码 + // } + /** + * 文件上传成功时触发的回调,参数:文件对象 + */ + @Output() readonly successItems: EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }> = new EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }>(); + // 文件上传取消回调 + // 该回调对单个或多个文件在同一url同时上传有效,可使用其进行文件上传过程中的业务处理 + // 参数为对象形式,结构如下: + // { + // fileItems: Array<[TiFileItem]{@link ../interfaces/TiFileItem.html}>, // 上传文件对象数组 + // response: string // 文件上传响应信息 + // status: number 文件上传响应状态码 + // } + /** + * 文件上传失败时触发的回调,参数:文件对象 + */ + @Output() readonly errorItems: EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }> = new EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }>(); + /** + * @ignore + * 文件上传取消回调 + * + * 该回调对单个或多个文件在同一url同时上传有效,可使用其进行文件上传过程中的业务处理 + * + * 参数为对象形式,结构如下: + * + * { + * + * fileItems: Array<[TiFileItem]{@link ../interfaces/TiFileItem.html}>, // 上传文件对象数组 + * + * response: string // 文件上传响应信息 + * + * status: number 文件上传响应状态码 + * + * } + */ + /** + * 文件上传取消时触发的回调,参数:文件对象 + */ + @Output() readonly cancelItems: EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }> = new EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }>(); + /** + * 上传删除时触发的回调,参数:被删除的文件对象列表 + */ + @Output() readonly removeItems: EventEmitter> = new EventEmitter< + Array + >(); + /** + * 所有文件上传完成时触发的回调,参数:文件对象列表 + */ + @Output() readonly completeAllItems: EventEmitter> = new EventEmitter>(); + /** + * 上传文件删除前触发的回调,参数:将被删除的文件对象列表 + */ + @Output() readonly beforeRemoveItems: EventEmitter> = new EventEmitter< + Array + >(); + + /** + * 上传实例配置信息 + */ + public uploadConfig: TiUploadConfig; + + /** + * @ignore 上传文件实例 + */ + public uploadInst: TiUploadRef; + + /** + * @ignore 是否定义beforeRemoveItems事件 + */ + isRemove: boolean; + + /** + * @ignore 词条 + */ + protected versionInfo: string = super.getVersion(packageInfo); + public uploadLan = TiLocale.getLocaleWords().tiUpload; + + constructor(protected hostRef: ElementRef, protected renderer: Renderer2) { + super(hostRef, renderer); + } + ngOnChanges(changes: SimpleChanges): void { + // 如果uploadConfig存在,并监听到url和headers变化 + if (this.uploadConfig) { + // 由于uploadConfig的引用地址与实例对象的config相同,更新到实例对象的config中 + if (changes['url']) { + this.uploadConfig.url = this.url; + } + if (changes['headers']) { + this.uploadConfig.headers = this.headers; + } + } + } + ngOnInit(): void { + super.ngOnInit(); + this.isRemove = this.beforeRemoveItems.observers.length !== 0; + this.uploadConfig = { + url: this.url, // 文件上传地址配置 + method: this.method, // 上传方式,可选值为:get、post(其他方式IE9不支持) + filters: this.filters, // 文件有效性判断条件数组 + alias: this.alias, // 向后台发送请求时对应的文件对象name属性,该name属性是后台读取文件的入口值,默认值为 'tiFile' + formData: this.formData, // 上传文件附带信息 + autoUpload: this.autoUpload, // 是否自动上传,默认为true + batchSend: this.batchSend || false, // 是否一次性上传多个文件 + formDataFirst: this.formDataFirst || false, // formData是否先于file对象 + headers: this.headers, // 上传文件请求头配置 + chunkSize: this.chunkSize, // 分片大小,设置后开启分片上传单位是 b + onAddItemFailed: (file: TiFileInfo, validResults: Array): void => { + this.addItemFailed.emit({ file, validResults }); + }, + onAddItemSuccess: (fileItem: TiFileItem): void => { + // 默认删除、禁用图标为非禁用状态 + fileItem.allowDelete = true; + fileItem.allowReload = true; + this.addItemSuccess.emit(fileItem); + }, + onBeforeSendItems: (fileItems: Array): void => { + this.beforeSendItems.emit(fileItems); + }, + onProgressItems: (fileItems: Array, progress: number): void => { + this.progressItems.emit({ fileItems, progress }); + }, + onCompleteItems: (fileItems: Array, response: string, status: number): void => { + this.completeItems.emit({ fileItems, response, status }); + }, + onSuccessItems: (fileItems: Array, response: string, status: number): void => { + this.successItems.emit({ fileItems, response, status }); + }, + onErrorItems: (fileItems: Array, response: string, status: number): void => { + this.errorItems.emit({ fileItems, response, status }); + }, + onCancelItems: (fileItems: Array, response: string, status: number): void => { + this.cancelItems.emit({ fileItems, response, status }); + }, + onRemoveItems: (fileItems: Array): void => { + this.removeItems.emit(fileItems); + }, + onBeforeRemoveItems: (fileItems: Array): void => { + this.beforeRemoveItems.emit(fileItems); + }, + onCompleteAllItems: (fileItems: Array): void => { + this.completeAllItems.emit(fileItems); + } + }; + // 根据配置的文件类型过滤属性设置H5下的文件过滤属性,如果accept未定义,则按照type过滤条件进行设置; + // 如果accept已定义,则按照accept定义设置:此种场景应用于浏览器设置accept后导致文件窗打开很慢的情况 + if (Util.isUndefined(this.accept) && this.filters && this.filters.length) { + this.filters.forEach((filter: TiFilter) => { + if (filter.name === 'type') { + this.accept = filter.params[0]; + } + + return; + }); + } + } + /** + * 手动上传,可通过组件调用 + */ + public upload(): void { + this.uploadInst.uploadAll(); + } + + /** + * @ignore 获取maxCount条件下标 + */ + public getMaxCountIndex(): number { + return this.uploadConfig.filters?.findIndex((filter: TiFilter) => { + return filter.name === 'maxCount'; + }); + } +} diff --git a/src/upload/lib/src/TiUploadimageComponent.ts b/src/upload/lib/src/TiUploadimageComponent.ts new file mode 100644 index 0000000..7d27c83 --- /dev/null +++ b/src/upload/lib/src/TiUploadimageComponent.ts @@ -0,0 +1,383 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + Input, + NgZone, + Output, + Renderer2, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { TiFileInfo, TiFileItem, TiFilter } from './TiFileInterface'; +import { TiUploadService } from './TiUploadService'; +import { TiModalService } from '@opentiny/ng-modal'; +import { TiFilePreviewInfo, TiImagepreviewComponent } from '@opentiny/ng-imagepreview'; +import { TiUploadbaseComponent, TiUploadimageInitFile } from './TiUploadbaseComponent'; +import { DomSanitizer } from '@angular/platform-browser'; + +/** + * + * 该组件基于TiUploadService和已设计好的交互规范,实现上传图片功能(但不限于上传图片),包含两种样式: + * + * 1.带上传结果展示的样式(type为block) + * + * 2.不展示上传结果的样式(type为drag) + * + * 负责人:谭莉 + */ +@Component({ + selector: 'ti-upload[type="block"], ti-upload[type="drag"]', + templateUrl: './uploadimage.html', + styleUrls: ['./uploadimage.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiUploadimageComponent extends TiUploadbaseComponent { + /** + * 上传文件的最大数量 + */ + @Input() maxCount: number; + + /** + * 上传成功后是否可删除 + */ + @Input() deletable: boolean = true; + + /** + * 必选,上传按钮样式 + * @customType 'block' + */ + @Input() type: 'block' | 'drag'; + + /** + * 初始时显示已上传的图片列表 + */ + @Input() initFiles: Array; + + /** + * 图片预览时弹出框样式 + */ + @Input() modalClass: string = 'ti3-image-preview-modal'; + + /** + * @ignore + * 上传结果展示,目前只有一种,picture-card; + * 当type设置为drag时无效 + */ + listType: string = 'picture-card'; + + /** + * 文件上传数量达到上限时触发的回调,参数:文件预览信息列表 + */ + @Output() readonly uploadLimit: EventEmitter> = new EventEmitter>(); + + /** + * @ignore 内部变量 + */ + @ViewChild('uploadBtn', { static: false }) uploadBtn: ElementRef; + /** + * @ignore + * 上传组件对应的input元素 + */ + @ViewChild('uploadInput', { static: false }) uploadInputEle: ElementRef; + /** + * 自定义上传样式 + * @ignore + */ + @ContentChild('uploadContainer', { static: true }) + uploadContainerTemplate: ElementRef; + + /** + * 上传按钮的宽度,只适用于 type 为 drag 的上传按钮 + * @ignore + */ + @Input() uploadBtnWidth: string = '100px'; + + /** + * 上传按钮的高度,只适用于 type 为 drag 的上传按钮 + * @ignore + */ + @Input() uploadBtnHeight: string = '100px'; + + /** + * @ignore 需要展示的文件列表 + */ + fileList: Array = []; + constructor( + private uploaderService: TiUploadService, + hostEle: ElementRef, + renderer: Renderer2, + private tiModal: TiModalService, + public sanitizer: DomSanitizer, + private zone: NgZone, + public changeDetectorRef: ChangeDetectorRef + ) { + super(hostEle, renderer); + } + + /** + * @ignore + */ + ngOnInit(): void { + super.ngOnInit(); + // 设置上传实例配置 + this.setUploadConfig(); + // 创建uploader实例 + this.uploadInst = this.uploaderService.create(this.uploadConfig, this); + if (this.initFiles && this.maxCount) { + // 在创建服务实例时,如果存在特殊情况最大数量-初始化文件长度=1时,service会判断文件为单文件上传,但实际上场景应该为多文件上传 + // 因此在setUploadConfig时根据实际传入的maxcount设置一次,在实例判断是否是单文件上传之后再修改为还允许上传的文件数 + const maxCountIndex: number = this.getMaxCountIndex(); + if (maxCountIndex !== -1) { + this.uploadConfig.filters[maxCountIndex].params[0] = this.maxCount - this.initFiles.length; + } + } + if (this.type === 'drag') { + this.listType = ''; + } + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + if (changes['initFiles']) { + if (changes['initFiles'].currentValue?.length > 0) { + this.setInitFiles(); + } + if (!changes['initFiles'].firstChange) { + const maxCountIndex: number = this.getMaxCountIndex(); + if (maxCountIndex !== -1) { + this.uploadConfig.filters[maxCountIndex].params[0] = this.maxCount - this.initFiles.length; + } + } + } + } + + ngAfterViewChecked(): void { + super.ngAfterViewChecked(); + // 拖拽添加/上传时,设置上传按钮的高度和宽度 + if (this.uploadBtn && this.type === 'drag') { + this.renderer.setStyle(this.uploadBtn.nativeElement, 'width', this.uploadBtnWidth); + this.renderer.setStyle(this.uploadBtn.nativeElement, 'height', this.uploadBtnHeight); + } + } + + /** + * 重新设置上传实例配置 + */ + private setUploadConfig(): void { + this.setFilters(); + this.uploadConfig.onAddItemFailed = (file: TiFileInfo, validResults: Array): void => { + // 文件上传达上限,触发回调 + if (validResults.includes('maxCount')) { + this.uploadLimit.emit(this.fileList); + } + this.addItemFailed.emit({ file, validResults }); + }; + this.uploadConfig.onAddItemSuccess = (fileItem: TiFileItem): void => { + // 为了兼容适配 TiUploadComponent 对相关 allowDelete 和 allowReload 的处理 + fileItem.allowDelete = true; + fileItem.allowReload = true; + this.addPreviewList(fileItem); + this.addItemSuccess.emit(fileItem); + }; + this.uploadConfig.onSuccessItems = (fileItems: Array, response: string, status: number): void => { + this.setPreviewInfo(fileItems[0]); + this.successItems.emit({ fileItems, response, status }); + }; + this.uploadConfig.onRemoveItems = (fileItems: Array): void => { + this.removePreviewList(fileItems[0]); + this.removeItems.emit(fileItems); + }; + } + + /** + * 将初始时要显示的已上传的图片文件添加到预览列表 + */ + private setInitFiles(): void { + this.initFiles.forEach((item: TiUploadimageInitFile) => { + const file: TiFilePreviewInfo = { + imgFileItem: { isSuccess: true, isUploaded: true }, + name: item.name, + previewUrl: item.previewUrl, + isImage: this.isImage(item.name, item.previewUrl), + fromInitFiles: true + }; + item.remove = (): void => { + const index: number = this.fileList.findIndex((fileItem: TiFilePreviewInfo): boolean => { + return fileItem === file; + }); + if (index !== -1) { + this.fileList.splice(index, 1); + this.changeDetectorRef.markForCheck(); + } + }; + file.imgFileItem.remove = (): void => { + // 获取需要删除的数据的索引 + const deleteIndex: number = this.fileList.findIndex((fileItem: TiFilePreviewInfo): boolean => { + return fileItem === file; + }); + const maxCountIndex: number = this.getMaxCountIndex(); + if (maxCountIndex !== -1) { + this.uploadConfig.filters[maxCountIndex].params[0] += 1; + } + if (this.isRemove) { + this.beforeRemoveItems.emit([item]); + } else { + this.fileList.splice(deleteIndex, 1); + this.removeItems.emit([item]); + } + }; + this.fileList.push(file); + }); + } + + /** + * 设置过滤条件 + */ + private setFilters(): void { + let uploadFilters: Array = this.filters || []; + if (typeof this.maxCount === 'number' && this.maxCount > 0) { + uploadFilters = uploadFilters.concat({ + name: 'maxCount', + params: [this.maxCount] + }); + } + this.uploadConfig.filters = uploadFilters; + } + + /** + * 添加文件成功后,增加一个预览对象 + */ + private addPreviewList(fileItem: TiFileItem): void { + const imgInfoItem: TiFilePreviewInfo = { + imgFileItem: fileItem, + name: fileItem._file.name, + previewUrl: '', + isImage: false + }; + this.fileList.push(imgInfoItem); + } + + /** + * 删除时,将文件信息从预览列表中移除 + */ + private removePreviewList(fileItem: TiFileItem): void { + // 获取需要删除的数据的索引 + const deleteIndex: number = this.fileList.findIndex((item: TiFilePreviewInfo): boolean => { + return fileItem._file === item.imgFileItem._file; + }); + this.fileList.splice(deleteIndex, 1); + } + + /** + * 上传成功后,更新预览地址 + */ + private setPreviewInfo(fileItem: TiFileItem): void { + // 生成预览url + const url: string = window.URL.createObjectURL(fileItem._file); + const isImagePromise: Promise = this.isImage(fileItem._file.name, url); + isImagePromise.then((result: boolean) => { + this.fileList.forEach((item: TiFilePreviewInfo): void => { + if (item.imgFileItem._file === fileItem._file) { + item.previewUrl = url; + item.isImage = result; + } + }); + this.changeDetectorRef.markForCheck(); + }); + } + + /** + * @ignore 在弹框中预览上传的图片 + * 参数i: 表示被点击图片的索引 + */ + public preview(i: number): void { + const previewList: Array = []; + let firstIndex: number = i; + this.fileList.forEach((item: TiFilePreviewInfo, index: number): void => { + if (item.imgFileItem.isSuccess && item.isImage) { + // 上传成功并且是图片 + previewList.push(item); + } else { + // 非图片或上传失败,并且在被点击的文件之前,第一个预览的索引需要-1 + if (i >= index) { + firstIndex -= 1; + } + } + }); + this.tiModal.open(TiImagepreviewComponent, { + id: 'imagePreviewModal', + modalClass: this.modalClass, + context: { + index: firstIndex, // 当前文件索引 + fileList: previewList // 预览列表 + } + }); + } + + /** + * 判断是否是图片类型 + * 参数fileName: 文件名称 + * 参数previewUrl: 预览地址 + */ + private isImage(fileName: string, previewUrl: string): Promise { + const tempArray: Array = fileName.split('.'); + const extension: string = tempArray[tempArray.length - 1].toLocaleLowerCase(); + const supportTypes: Array = ['jpg', 'png', 'jpeg', 'svg', 'gif', 'bmp']; + + return new Promise((resolve) => { + if (!supportTypes.includes(extension)) { + resolve(false); + + return; + } + this.zone.runOutsideAngular(() => { + // 测试图片能否加载成功,能加载成功,则认为可预览;否则,认为不可预览 + const img: any = document.createElement('img'); + img.src = previewUrl; + img.onerror = (event: any): void => { + resolve(false); + }; + img.onload = (event: any): void => { + resolve(true); + }; + }); + }); + } + + /** + * @ignore 禁用上传时,阻止默认事件、停止冒泡 + */ + selectFile(event: any): void { + if (this.disabled) { + event.stopPropagation(); + event.preventDefault(); + + return; + } + // 焦点转移 + this.uploadBtn.nativeElement.focus(); + } + + /** + * @ignore + */ + + public onKeydown(): void { + this.uploadInputEle.nativeElement.focus(); + } +} diff --git a/src/upload/lib/src/i18n/TiUploadWords.ts b/src/upload/lib/src/i18n/TiUploadWords.ts new file mode 100644 index 0000000..4f8f127 --- /dev/null +++ b/src/upload/lib/src/i18n/TiUploadWords.ts @@ -0,0 +1,21 @@ +export interface TiUploadWords { + tiUpload: { + addFile: string; + error: string; + successInfo: string; + uploadingSingleInfo: string; + errorSingleInfo: string; + addSuccessMutiInfo: string; + uploadingMutiInfo: string; + errorMultiInfo: string; + clearAll: string; + upload: string; + cancel: string; + reload: string; + delete: string; + autoUploadFilePlaceholder: string; + autoUploadFilesPlaceholder: string; + notAutoUploadFilePlaceholder: string; + notAutoUploadFilesPlaceholder: string; + }; +} diff --git a/src/upload/lib/src/i18n/en_US.ts b/src/upload/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..b692119 --- /dev/null +++ b/src/upload/lib/src/i18n/en_US.ts @@ -0,0 +1,23 @@ +import { TiUploadWords } from './TiUploadWords'; + +export const en_US: TiUploadWords = { + tiUpload: { + addFile: 'Select File', // upload_add_file_btn + error: 'Failed to upload the file.', // upload_error_info + successInfo: 'File uploaded successfully.', // upload_success_info + uploadingSingleInfo: 'Uploading', // upload_single_uploading_general_info + errorSingleInfo: 'Failed to upload the file.', // upload_single_error_general_info + addSuccessMutiInfo: 'You have added {0} files.', // upload_add_success_general_info + uploadingMutiInfo: 'Uploading: {0}', // upload_uploading_general_info + errorMultiInfo: 'Failed to upload {0} files.', // upload_error_general_info + clearAll: 'Clear All', // upload_remove_files + upload: 'Upload', // upload_file_btn + cancel: 'Cancel', // upload_cancel_files + reload: 'Upload Again', // upload_reload_files + delete: 'Delete', + autoUploadFilePlaceholder: 'Select a file to upload.', + autoUploadFilesPlaceholder: 'Select files to upload.', + notAutoUploadFilePlaceholder: 'Add a file and upload it.', + notAutoUploadFilesPlaceholder: 'Add files and upload them.' + } +}; diff --git a/src/upload/lib/src/i18n/es_US.ts b/src/upload/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..edcc5af --- /dev/null +++ b/src/upload/lib/src/i18n/es_US.ts @@ -0,0 +1,23 @@ +import { TiUploadWords } from './TiUploadWords'; + +export const es_US: TiUploadWords = { + tiUpload: { + addFile: 'Seleccionar archivo', + error: 'Error al cargar el archivo.', + successInfo: 'Se cargó el archivo.', + uploadingSingleInfo: 'Cargando', + errorSingleInfo: 'Error al cargar el archivo.', + addSuccessMutiInfo: 'Se agregaron {0} archivos.', + uploadingMutiInfo: 'Cargando: {0}', + errorMultiInfo: 'Error al cargar {0} archivos.', + clearAll: 'Eliminar todo', + upload: 'Cargar', + cancel: 'Cancelar', + reload: 'Volver a cargar', + delete: 'Delete', + autoUploadFilePlaceholder: 'Seleccione un archivo para cargar.', + autoUploadFilesPlaceholder: 'Seleccione archivos para cargar.', + notAutoUploadFilePlaceholder: 'Agregue un archivo y cárguelo.', + notAutoUploadFilesPlaceholder: 'Agregue archivos y cárguelos.' + } +}; diff --git a/src/upload/lib/src/i18n/fr_FR.ts b/src/upload/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..942afdf --- /dev/null +++ b/src/upload/lib/src/i18n/fr_FR.ts @@ -0,0 +1,23 @@ +import { TiUploadWords } from './TiUploadWords'; + +export const fr_FR: TiUploadWords = { + tiUpload: { + addFile: 'Ajouter le fichier', + error: 'Échec à télécharger le fichier.', + successInfo: 'Etape réalisée avec succès.', + uploadingSingleInfo: 'Téléchargement', + errorSingleInfo: 'Échec à télécharger le fichier.', + addSuccessMutiInfo: 'Vous avez ajouté {0} fichiers.', + uploadingMutiInfo: 'Téléchargement : {0}', + errorMultiInfo: 'Echec à télécharger {0} fichiers.', + clearAll: 'Tout effacer', + upload: 'Télécharger', + cancel: 'Annuler', + reload: 'Télécharger à nouveau', + delete: 'Delete', + autoUploadFilePlaceholder: 'Sélectionnez un fichier à télécharger.', + autoUploadFilesPlaceholder: 'Sélectionnez les fichiers à télécharger.', + notAutoUploadFilePlaceholder: 'Ajoutez un fichier et le télécharger.', + notAutoUploadFilesPlaceholder: 'Ajoutez des fichiers et les télécharger.' + } +}; diff --git a/src/upload/lib/src/i18n/index.ts b/src/upload/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/upload/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/upload/lib/src/i18n/pt_BR.ts b/src/upload/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..8d5117c --- /dev/null +++ b/src/upload/lib/src/i18n/pt_BR.ts @@ -0,0 +1,23 @@ +import { TiUploadWords } from './TiUploadWords'; + +export const pt_BR: TiUploadWords = { + tiUpload: { + addFile: 'Selecionar arquivo', + error: 'Não foi possível carregar o arquivo.', + successInfo: 'Arquivo carregado com sucesso.', + uploadingSingleInfo: 'Carregando', + errorSingleInfo: 'Não foi possível carregar o arquivo.', + addSuccessMutiInfo: 'Você adicionou {0} arquivos.', + uploadingMutiInfo: 'Carregando: {0}', + errorMultiInfo: 'Não foi possível carregar {0} arquivos.', + clearAll: 'Limpar tudo', + upload: 'Carregar', + cancel: 'Cancelar', + reload: 'Carregar novamente', + delete: 'Delete', + autoUploadFilePlaceholder: 'Selecione um arquivo para ser carregado.', + autoUploadFilesPlaceholder: 'Selecione arquivos para serem carregados.', + notAutoUploadFilePlaceholder: 'Adicione um arquivo e carregue-o.', + notAutoUploadFilesPlaceholder: 'Adicione arquivos e carregue-os.' + } +}; diff --git a/src/upload/lib/src/i18n/zh_CN.ts b/src/upload/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..5b6605f --- /dev/null +++ b/src/upload/lib/src/i18n/zh_CN.ts @@ -0,0 +1,23 @@ +import { TiUploadWords } from './TiUploadWords'; + +export const zh_CN: TiUploadWords = { + tiUpload: { + addFile: '添加文件', + error: '上传失败!', // upload_error_info + successInfo: '上传成功!', + uploadingSingleInfo: '正在上传', + errorSingleInfo: '上传失败!', + addSuccessMutiInfo: '已添加{0}个文件', + uploadingMutiInfo: '{0} 正在上传', + errorMultiInfo: '{0}个文件上传失败!', + clearAll: '清空选择', + upload: '上传文件', + cancel: '取消上传', + reload: '重新上传', + delete: '删除', + autoUploadFilePlaceholder: '点击右侧按钮上传文件', + autoUploadFilesPlaceholder: '点击右侧按钮上传文件', + notAutoUploadFilePlaceholder: '点击右侧按钮先添加再上传', + notAutoUploadFilesPlaceholder: '点击右侧按钮先添加再上传' + } +}; diff --git a/src/upload/lib/src/upload.html b/src/upload/lib/src/upload.html new file mode 100644 index 0000000..e79fc66 --- /dev/null +++ b/src/upload/lib/src/upload.html @@ -0,0 +1,293 @@ +
    + +
    + + {{buttonText || uploadLan.addFile}} +
    +
    + + {{buttonText || uploadLan.addFile}} +
    +
    {{note}}
    +
      + +
    • +
      + + {{item.name}} +
      +
      +
      + + +
      +
      +
    • + + +
    • +
      + + {{item.file.name}} +
      +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + + + +
      +
      + + +
    • +
      +
    +
    +
    + +
    + {{uploadLan.reload}} +
    +
    +
    +
    + +
    + {{uploadLan.reload}} +
    +
    +
    +
    +
    + +
    +
    +
    +
      + +
    • +
      +
      {{item.file.name}}
      +
      + ( + {{item.file.sizeWithUnit}} + ) +
      +
      +
      +
      + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      +
    • +
    • {{placeholder}}
    • +
    +
    + + +
    + + +
    + + + + + + +
    +
    +
    + {{stateInfo}} +
    +
    + {{stateInfo}} +
    {{uploadLan.cancel}}
    +
    +
    + + {{stateInfo}} +
    +
    + + {{stateInfo}} +
    + {{uploadLan.reload}} +
    +
    +
    +
    +
    + {{uploadLan.uploadingSingleInfo}} +
    +
    + + {{uploadLan.successInfo}} +
    +
    + + {{uploadLan.errorSingleInfo}} +
    + {{uploadLan.reload}} +
    +
    +
    +
    + + + + + {{item.file.name}} + diff --git a/src/upload/lib/src/upload.less b/src/upload/lib/src/upload.less new file mode 100644 index 0000000..f857e8c --- /dev/null +++ b/src/upload/lib/src/upload.less @@ -0,0 +1,356 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-upload-container-height: var(--ti-common-size-7x); + --ti-upload-icon-size: var(--ti-common-size-4x); + --ti-upload-file-item-height: 22px; + --ti-upload-item-container-padding-vertical: calc(var(--ti-upload-item-space) / 2); + --ti-upload-item-space: calc(var(--ti-common-space-1) * 2); + --ti-upload-item-height: (var(--ti-upload-container-height) - 2 * (var(--ti-upload-item-space)) - 2px); + --ti-upload-item-operate-container-width: (var(--ti-upload-icon-size) * 2 + var(--ti-common-space-2x) * 2); + --ti-upload-item-file-name-font-size: var(--ti-common-font-size-base); + --ti-upload-msg-font-size: var(--ti-common-font-size-base); + --ti-upload-textbutton-font-size: var(--ti-common-font-size-base); +} + +.ti3-file-btn { + position: relative; + overflow: hidden; +} +.ti3-file-input { + opacity: 0; + z-index: 1; + position: absolute; + font-size: 1000px; + margin: 0; + padding: 0; + cursor: pointer; + bottom: 0; + right: 0; + &[disabled] { + cursor: not-allowed; + } +} +.ti3-file-text { + line-height: calc(var(--ti-upload-container-height) - 2px); +} + +// Aui2文件上传样式 +.ti3-aui-file-disable { + .ti3-aui-file-field-container { + border-color: var(--ti-common-color-line-disabled); + background-color: var(--ti-common-color-bg-disabled); + cursor: not-allowed; + box-sizing: border-box; + } + .ti3-aui-file-select-input { + cursor: not-allowed; + } +} +.ti3-aui-file-container { + font-size: var(--ti-common-font-size-base); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-primary); +} +.ti3-aui-file-upload-container { + .clearfix(); +} +.ti3-aui-file-field-container { + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + background: var(--ti-common-color-bg-white-normal); + position: relative; + float: left; + width: 300px; // 默认宽度 +} +.ti3-aui-file-items-container { + overflow-y: auto; + overflow-x: hidden; + width: calc(100% - 2px); // 多出的2px用于防止缩放情况下右侧块掉下来 + min-height: calc(var(--ti-upload-container-height) - 2px); + max-height: calc((var(--ti-upload-container-height) - 2px) * 2 - var(--ti-upload-item-space)); + padding: var(--ti-upload-item-container-padding-vertical) 0; + .box-sizing(border-box); +} + +.ti3-aui-file-item { + float: left; + .box-sizing(border-box); + background-color: var(--ti-common-color-bg-normal); + max-width: 100%; + height: calc(var(--ti-upload-item-height)); + line-height: calc(var(--ti-upload-item-height)); + padding: 0 var(--ti-common-space-10); + margin: calc(var(--ti-upload-item-space) / 2) 0 calc(var(--ti-upload-item-space) / 2) var(--ti-upload-item-space); + cursor: pointer; + display: flex; + justify-content: space-between; + &.ti3-aui-single-file-item { + width: 100%; + } +} +.ti3-aui-file-name-size-container { + width: calc(100% - var(--ti-upload-item-operate-container-width)); + display: flex; + flex-shrink: 0; // 设置该属性为0,空间不足时,元素宽度不被压缩 +} +.ti3-aui-file-name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + height: 100%; + color: var(--ti-common-color-text-secondary); +} +.ti3-aui-file-size { + color: var(--ti-common-color-text-weaken); + padding-left: var(--ti-common-space-base); + .box-sizing(border-box); + display: flex; + flex-shrink: 0; +} +.ti3-aui-file-item-size { + max-width: 70px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.ti3-aui-file-operates { + width: calc(var(--ti-upload-item-operate-container-width)); +} +.ti3-aui-file-x, +.ti3-aui-file-error-reupload, +.ti3-aui-file-error-icon, +.ti3-aui-file-success { + color: var(--ti-common-color-icon-normal); + font-size: var(--ti-upload-icon-size); + float: right; +} +.ti3-aui-file-success { + color: var(--ti-common-color-success); +} +.ti3-aui-file-error-state .ti3-aui-file-x { + margin-right: var(--ti-common-space-2x); +} +.ti3-aui-file-error-icon { + color: var(--ti-common-color-error); + margin-right: var(--ti-common-space-2x); +} +.ti3-aui-file-progress-pie { + width: var(--ti-upload-icon-size); + height: var(--ti-upload-icon-size); + float: right; + position: relative; + top: calc((var(--ti-upload-item-height) - var(--ti-upload-icon-size)) / 2); +} +.ti3-aui-file-select-input { + opacity: 0; + width: 0; + height: 0; +} +.ti3-aui-file-tip-name { + display: inline-block; + margin-top: var(--ti-common-space-base); + font-size: var(--ti-common-font-size-base); +} +.ti3-aui-file-placeholder { + color: var(--ti-common-color-text-disabled); + line-height: calc(var(--ti-upload-container-height) - 2px - var(--ti-upload-item-container-padding-vertical) * 2); + padding-left: var(--ti-common-space-10); +} +.ti3-aui-upload-btn { + float: left; + margin-left: var(--ti-common-space-2x); + height: var(--ti-upload-container-height); +} +.ti3-aui-file-state-general { + margin-top: var(--ti-common-space-2x); +} +.ti3-aui-file-state-general-info { + display: flex; + align-items: center; +} +.ti3-aui-file-state-general-info-uploading, +.ti3-aui-file-state-general-info-addSuccess { + color: var(--ti-common-color-text-weaken); +} +.ti3-aui-file-state-general-info-error { + color: var(--ti-common-color-error-text); +} +.ti3-aui-file-state-general-info-success { + color: var(--ti-common-color-text-primary); +} +.ti3-aui-file-state-general-operate { + display: inline-block; + vertical-align: middle; + margin-left: var(--ti-common-space-base); + color: var(--ti-common-color-text-link); + cursor: pointer; + &:hover { + color: var(--ti-common-color-text-link-hover); + } +} +.ti3-aui-file-state-general-success, +.ti3-aui-file-state-general-error { + color: var(--ti-common-color-success); + margin-right: var(--ti-common-space-2x); + font-size: var(--ti-upload-icon-size); + line-height: 18px; +} +.ti3-aui-file-state-general-error { + color: var(--ti-common-color-error); +} + +.ti3-file-upload-btnCon { + float: left; + position: relative; +} + +.ti3-file-upload-btn-cover { + position: absolute; + top: 0; + right: 0; + left: var(--ti-common-space-2x); + bottom: 0; + cursor: not-allowed; +} + +// button/textButton类型 +.ti3-file-upload-container { + .ti3-file-text-btn { + display: inline-block; + overflow: hidden; + font-size: var(--ti-upload-textbutton-font-size); + font-weight: var(--ti-common-font-weight-4); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-link); + position: relative; + &:hover { + color: var(--ti-common-color-text-link-hover); + } + &.ti3-file-text-btn-disabled { + color: var(--ti-common-color-text-disabled); + } + } + .ti3-file-note { + margin-top: var(--ti-common-space-2x); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-weaken); + } + .ti3-file-items-container { + display: flex; + flex-direction: column; + margin-top: var(--ti-common-space-2x); + max-height: calc( + (var(--ti-upload-file-item-height) + var(--ti-common-space-10)) * 7 + + (var(--ti-upload-file-item-height) + var(--ti-common-space-base)) + ); // 列表默认最多显示8个文件,(22+10)*7 + (22+4)*1 + overflow-y: auto; + .ti3-file-item { + display: flex; + justify-content: space-between; + align-items: center; + padding-right: var(--ti-common-space-10); + margin-bottom: var(--ti-common-space-10); + height: var(--ti-upload-file-item-height); + cursor: pointer; + border-radius: var(--ti-common-border-radius-normal); + position: relative; + &:last-child { + margin-bottom: var(--ti-common-space-base); + } + &:hover { + background: var(--ti-common-color-bg-normal); + .ti3-file-operates { + div.ti3-file-operates-show, + div.ti3-file-operates-uploading, + div.ti3-file-operates-success { + display: flex; + } + } + } + .ti3-file-name-container { + display: flex; + align-items: center; + flex: 1; + height: var(--ti-upload-file-item-height); + overflow: hidden; + color: var(--ti-common-color-text-secondary); + &.ti3-file-state-general-info-error { + color: var(--ti-common-color-error-text); + } + .ti3-file-name { + margin-left: var(--ti-common-space-base); + font-size: var(--ti-upload-item-file-name-font-size); + font-weight: var(--ti-common-font-weight-4); + .ellipsis(); + } + } + .ti3-file-operates { + div { + display: flex; + align-items: center; + padding-left: var(--ti-common-space-4x); + .ti3-file-error-reupload { + margin-right: var(--ti-common-space-2x); + } + .ti3-icon:hover { + color: var(--ti-common-color-icon-hover); + } + &.ti3-file-operates-show, + &.ti3-file-operates-uploading, + &.ti3-file-operates-success { + display: none; + } + } + } + .ti3-file-progress-bar { + height: var(--ti-common-border-weight-1); + position: absolute; + left: 0; + top: var(--ti-common-space-6x); + } + .ti3-icon { + font-size: var(--ti-common-font-size-2); + } + } + } + .ti3-file-error-message { + .ti3-aui-file-state-general { + font-size: var(--ti-upload-msg-font-size); + font-weight: var(--ti-common-font-weight-4); + line-height: var(--ti-common-line-height-number); + ti-error-msg { + margin-top: 0; + } + } + } +} +:host ::ng-deep .ti3-file-progress-bar-color { + background-color: var(--ti-common-color-success) !important; +} + +.ti3-file-x-disabled { + color: var(--ti-common-color-icon-disabled); + cursor: not-allowed; + &:hover { + color: var(--ti-common-color-icon-disabled) !important; + } +} + +.ti3-aui-file-state-general-operate-disable { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + &:hover { + color: var(--ti-common-color-text-disabled); + } +} + +/*************************************************动效**********************************************************************/ + +// IE和FF下,文件上传按钮加动画效果情况下,按钮点击两次才打开文件选择框问题 +.ti3-file-upload-container { + .ti3-btn-default:not(:focus):not([disabled]):hover, + [ti-button]:not(:focus):not([disabled]):hover { + animation: none; + } +} diff --git a/src/upload/lib/src/uploadimage.html b/src/upload/lib/src/uploadimage.html new file mode 100644 index 0000000..3e96ef0 --- /dev/null +++ b/src/upload/lib/src/uploadimage.html @@ -0,0 +1,96 @@ + + +
    +
    + +
    + +
    + + {{uploadLan.cancel}} +
    +
    +
    + + + + +
    + {{item.name}} +
    +
    + + +
    + + +
    + + + +
    + +
    +
    + + + + + diff --git a/src/upload/lib/src/uploadimage.less b/src/upload/lib/src/uploadimage.less new file mode 100644 index 0000000..5a700e3 --- /dev/null +++ b/src/upload/lib/src/uploadimage.less @@ -0,0 +1,166 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-upload-image-btn-width: var(--ti-common-size-25x); + --ti-upload-image-btn-height: var(--ti-common-size-25x); + --ti-picture-card-operation-width: var(--ti-common-size-4x); + --ti-picture-card-operation-height: var(--ti-common-size-4x); + --ti-picture-card-operation-top: calc((var(--ti-upload-image-btn-height) - var(--ti-picture-card-operation-height)) / 2); + --ti-picture-card-operation-left-center: calc((var(--ti-upload-image-btn-width) - var(--ti-picture-card-operation-width)) / 2); + --ti-picture-card-operation-preview-left: var(--ti-common-space-7x); + --ti-upload-image-progress-top: 57px; + --ti-upload-image-progress-width: var(--ti-common-size-20x); + --ti-upload-image-icon-margin: 37px; +} + +.ti3-picture-card-container { + display: inline-block; + position: relative; + width: var(--ti-upload-image-btn-width); + &:not(:last-child) { + margin-right: var(--ti-common-space-2x); + } + .ti3-picture-card-item { + position: relative; + height: var(--ti-upload-image-btn-height); + box-sizing: border-box; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + .ti3-picture-card-item-img { + width: 100%; + height: 100%; + object-fit: cover; + border-style: none; + } + .ti3-picture-card-item-not-img { + background: data-uri('../../../themes/basic/img/upload-image-error.png') 50% no-repeat; + padding: calc(var(--ti-upload-image-btn-width) / 2); + } + .ti3-upload-image-progress { + position: absolute; + top: var(--ti-upload-image-progress-top); + width: var(--ti-upload-image-progress-width); + margin: 0 calc((var(--ti-upload-image-btn-width) - var(--ti-upload-image-progress-width)) / 2); + ti-progressbar { + position: absolute; + } + ::ng-deep .ti3-progress-bar { + background-color: var(--ti-common-color-success); + } + .ti3-upload-image-cancel-text { + width: 100%; + display: inline-block; + margin-top: calc(5px + var(--ti-common-space-2x)); // 5 + 8 + text-align: center; + cursor: pointer; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + color: var(--ti-common-color-prompt-text); + } + } + } + .ti3-picture-card-item-error { + border-color: var(--ti-common-color-error-border); + } + .ti3-picture-card-drop { + position: absolute; + width: var(--ti-upload-image-btn-width); + height: var(--ti-upload-image-btn-height); + top: 0; + background: rgba(0, 0, 0, 0.4); + opacity: 0; + border-radius: var(--ti-common-border-radius-normal); + &:hover { + opacity: 1; + } + .ti3-picture-card-operation { + display: inline-block; + height: var(--ti-picture-card-operation-height); + width: var(--ti-picture-card-operation-width); + position: absolute; + &:hover { + cursor: pointer; + } + } + .ti3-picture-card-operation-preview { + background-image: data-uri('../../../themes/basic/img/upload-image-preview.png'); + top: var(--ti-picture-card-operation-top); + left: var(--ti-picture-card-operation-preview-left); + } + .ti3-picture-card-operation-center { + left: var(--ti-picture-card-operation-left-center); + } + .ti3-picture-card-operation-success-center { + left: var(--ti-picture-card-operation-left-center) !important; + } + .ti3-picture-card-operation-delete { + background-image: data-uri('../../../themes/basic/img/upload-image-delete.png'); + top: var(--ti-picture-card-operation-top); + } + .ti3-picture-card-operation-delete-error { + left: var(--ti-picture-card-operation-left-center); + } + .ti3-picture-card-operation-delete-success { + left: calc(var(--ti-picture-card-operation-preview-left) * 2); + } + } + .ti3-picture-card-name { + display: inline-block; + width: 100%; + text-align: center; + margin-top: var(--ti-common-space-2x); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + } + .ti3-picture-card-name-error { + color: var(--ti-common-color-error-text); + } +} + +.ti3-upload-image-btn { + position: relative; + overflow: hidden; + width: var(--ti-upload-image-btn-width); + height: var(--ti-upload-image-btn-height); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-dashed) var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + padding: 0; + vertical-align: top; + &[disabled] { + cursor: not-allowed; + } + :hover { + cursor: pointer; + } + .ti3-upload-image-icon { + width: 100%; + display: inline-block; + margin: var(--ti-upload-image-icon-margin) auto; + position: absolute; + top: 0; + left: 0; + font-size: var(--ti-common-font-size-2); + } + + .ti3-upload-image-input { + position: absolute; + left: 0; + top: 0; + display: inline-block; + opacity: 0; + width: 100%; + height: 100%; + outline: none; + font-size: 0; // 解决IE下点击上传文件出现光标的问题 + &[disabled] { + cursor: not-allowed; + } + } +} + +::ng-deep .ti3-image-preview-modal { + width: 640px !important; +} diff --git a/src/utils/demo/project.json b/src/utils/demo/project.json new file mode 100644 index 0000000..5357107 --- /dev/null +++ b/src/utils/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/utils/demo", + "sourceRoot": "src/utils/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/utils", + "index": "src/utils/demo/src/index.html", + "main": "src/utils/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/utils/demo/tsconfig.app.json", + "assets": ["src/utils/demo/src/favicon.ico", "src/utils/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "utils-demo:build:production" + }, + "development": { + "browserTarget": "utils-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js utils" + } + ] + } + } + } +} diff --git a/src/utils/demo/src/app/AppComponent.ts b/src/utils/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/utils/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/utils/demo/src/app/AppModule.ts b/src/utils/demo/src/app/AppModule.ts new file mode 100644 index 0000000..e11de06 --- /dev/null +++ b/src/utils/demo/src/app/AppModule.ts @@ -0,0 +1,30 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { BrowserTestModule } from './browser/BrowserTestModule'; +import { KeymapTestModule } from './keymap/KeymapTestModule'; +import { LogTestModule } from './log/LogTestModule'; +import { ThemeTestModule } from './theme/ThemeTestModule'; + +@NgModule({ + imports: [ + BrowserTestModule, + KeymapTestModule, + LogTestModule, + ThemeTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/utils/demo/src/app/IndexComponent.ts b/src/utils/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..99ee696 --- /dev/null +++ b/src/utils/demo/src/app/IndexComponent.ts @@ -0,0 +1,54 @@ +import { Component } from '@angular/core'; +import { BrowserTestModule } from './browser/BrowserTestModule'; +import { KeymapTestModule } from './keymap/KeymapTestModule'; +import { LogTestModule } from './log/LogTestModule'; +import { ThemeTestModule } from './theme/ThemeTestModule'; + +@Component({ + template: ` + + + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + moudles: Array = [BrowserTestModule.ROUTES, KeymapTestModule.ROUTES, LogTestModule.ROUTES, ThemeTestModule.ROUTES]; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/utils/demo/src/app/app.html b/src/utils/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/utils/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/utils/demo/src/app/browser/BrowserTestModule.ts b/src/utils/demo/src/app/browser/BrowserTestModule.ts new file mode 100644 index 0000000..3d993af --- /dev/null +++ b/src/utils/demo/src/app/browser/BrowserTestModule.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { BrowserUsageComponent } from './BrowserUsageComponent'; + +@NgModule({ + imports: [CommonModule, RouterModule.forChild(BrowserTestModule.ROUTES)], + declarations: [BrowserUsageComponent] +}) +export class BrowserTestModule { + static readonly LINKS: Array = [{ href: 'classes/TiBrowser.html', label: 'TiBrowser' }]; + static readonly ROUTES: Routes = [ + { + path: 'browser/browser-basic', + component: BrowserUsageComponent + } + ]; +} diff --git a/src/utils/demo/src/app/browser/BrowserUsageComponent.ts b/src/utils/demo/src/app/browser/BrowserUsageComponent.ts new file mode 100644 index 0000000..91dfa4b --- /dev/null +++ b/src/utils/demo/src/app/browser/BrowserUsageComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { TiBrowser } from '@opentiny/ng'; + +@Component({ + templateUrl: './browser-usage.html' +}) +export class BrowserUsageComponent { + browser: string = TiBrowser.browser(); + version: number = TiBrowser.version(); + isEdge: boolean = TiBrowser.isEdge(); + isIE: boolean = TiBrowser.isIE(); + isFirefox: boolean = TiBrowser.isFirefox(); + isChrome: boolean = TiBrowser.isChrome(); + isOpera: boolean = TiBrowser.isOpera(); + isSafari: boolean = TiBrowser.isSafari(); + isOther: boolean = TiBrowser.isOther(); +} diff --git a/src/utils/demo/src/app/browser/browser-usage.html b/src/utils/demo/src/app/browser/browser-usage.html new file mode 100644 index 0000000..8f7930a --- /dev/null +++ b/src/utils/demo/src/app/browser/browser-usage.html @@ -0,0 +1,10 @@ +

    browser:{{browser}}

    +

    version:{{version}}

    +

    version>5:{{version>5}}

    +

    isEdge:{{isEdge}}

    +

    isIE:{{isIE}}

    +

    isFirefox:{{isFirefox}}

    +

    isChrome:{{isChrome}}

    +

    isOpera:{{isOpera}}

    +

    isSafari:{{isSafari}}

    +

    isOther:{{isOther}}

    diff --git a/src/utils/demo/src/app/browser/webdoc/browser-demos.js b/src/utils/demo/src/app/browser/webdoc/browser-demos.js new file mode 100644 index 0000000..7e24545 --- /dev/null +++ b/src/utils/demo/src/app/browser/webdoc/browser-demos.js @@ -0,0 +1,27 @@ +export default { + column: '2', + demos: [ + { + demoId: 'browser-usage', + name: { + 'zh-CN': '基本用法', + 'en-US': 'browser usage', + }, + desc: { + 'zh-CN': '

    检测当前浏览器的相关信息。

    ', + 'en-US': '

    browser usage

    ', + }, + apis: [ + 'TiBrowser.methods.browser', + 'TiBrowser.methods.isFirefox', + 'TiBrowser.methods.isOther', + 'TiBrowser.methods.isChrome', + 'TiBrowser.methods.isIE', + 'TiBrowser.methods.isEdge', + 'TiBrowser.methods.isOpera', + 'TiBrowser.methods.isSafari', + 'TiBrowser.methods.version', + ], + }, + ], +}; diff --git a/src/utils/demo/src/app/browser/webdoc/browser.cn.md b/src/utils/demo/src/app/browser/webdoc/browser.cn.md new file mode 100644 index 0000000..b035620 --- /dev/null +++ b/src/utils/demo/src/app/browser/webdoc/browser.cn.md @@ -0,0 +1,19 @@ +--- +title: Browser 浏览器信息 +--- +# Browser 浏览器信息 + +
    + +Browser 是浏览器工具类,提供浏览器类别和版本等信息。 + +
    + +
    + +Browser 是浏览器工具类,提供浏览器类别和版本等信息。 + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
    diff --git a/src/utils/demo/src/app/browser/webdoc/browser.en.md b/src/utils/demo/src/app/browser/webdoc/browser.en.md new file mode 100644 index 0000000..9fcb90f --- /dev/null +++ b/src/utils/demo/src/app/browser/webdoc/browser.en.md @@ -0,0 +1,28 @@ +--- +title: Accordion +--- +# Accordion + +
    + +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
    + +
    + +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/utils/demo/src/app/keymap/KeymapTestModule.ts b/src/utils/demo/src/app/keymap/KeymapTestModule.ts new file mode 100644 index 0000000..121e35e --- /dev/null +++ b/src/utils/demo/src/app/keymap/KeymapTestModule.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiTextModule } from '@opentiny/ng'; + +import { KeymapUsageComponent } from './KeymapUsageComponent'; + +@NgModule({ + imports: [CommonModule, TiTextModule, RouterModule.forChild(KeymapTestModule.ROUTES)], + declarations: [KeymapUsageComponent] +}) +export class KeymapTestModule { + static readonly LINKS: Array = [{ href: 'classes/TiKeymap.html', label: 'TiKeymap' }]; + static readonly ROUTES: Routes = [ + { + path: 'keymap/keymap-usage', + component: KeymapUsageComponent + } + ]; +} diff --git a/src/utils/demo/src/app/keymap/KeymapUsageComponent.ts b/src/utils/demo/src/app/keymap/KeymapUsageComponent.ts new file mode 100644 index 0000000..9a3955e --- /dev/null +++ b/src/utils/demo/src/app/keymap/KeymapUsageComponent.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { TiKeymap } from '@opentiny/ng'; +@Component({ + templateUrl: './keymap-usage.html' +}) +export class KeymapUsageComponent { + charCode: string; + charStr: string; + keyCode: string; + keypressStr: string; + + onKeypress(event: KeyboardEvent): void { + const charCode: number = event.charCode; + this.charCode = `字符编码为--${charCode}`; + this.charStr = `字符为--'${String.fromCharCode(charCode)}'`; + this.keyCode = `键值为--${event.keyCode}`; + // 此处示例,是为了展示keymap的用法 + if (charCode === TiKeymap.KEY_ENTER) { + this.keypressStr = '此时回车键用TiKeymap表示为: TiKeymap.KEY_ENTER'; + } else { + this.keypressStr = ''; + } + } +} diff --git a/src/utils/demo/src/app/keymap/keymap-usage.html b/src/utils/demo/src/app/keymap/keymap-usage.html new file mode 100644 index 0000000..0e76b0b --- /dev/null +++ b/src/utils/demo/src/app/keymap/keymap-usage.html @@ -0,0 +1,8 @@ + +
    +

    Current Keymap information:

    +

    {{ charCode }}

    +

    {{ charStr }}

    +

    {{ keyCode }}

    +

    {{ keypressStr }}

    +
    diff --git a/src/utils/demo/src/app/keymap/webdoc/keymap-demos.js b/src/utils/demo/src/app/keymap/webdoc/keymap-demos.js new file mode 100644 index 0000000..2728fb5 --- /dev/null +++ b/src/utils/demo/src/app/keymap/webdoc/keymap-demos.js @@ -0,0 +1,17 @@ +export default { + column: '2', + demos: [ + { + demoId: 'keymap-usage', + name: { + 'zh-CN': '基本用法', + 'en-US': 'keymap usage', + }, + desc: { + 'zh-CN': '

    键盘按键被按下时,获取字符编码和键值等相关信息。

    ', + 'en-US': '

    keymap usage

    ', + }, + apis: ['TiKeymap.properties.KEY_0', 'TiKeymap.properties.KEY_1'], + }, + ], +}; diff --git a/src/utils/demo/src/app/keymap/webdoc/keymap.cn.md b/src/utils/demo/src/app/keymap/webdoc/keymap.cn.md new file mode 100644 index 0000000..6c63b05 --- /dev/null +++ b/src/utils/demo/src/app/keymap/webdoc/keymap.cn.md @@ -0,0 +1,16 @@ +--- +title: Keymap 键值查询 +--- +# Keymap 键值查询 + +
    + +Keymap 是键值映射工具类,可以提高键值可读性,已经屏蔽浏览器差异。 + +
    + +
    + +Keymap 是键值映射工具类,可以提高键值可读性,已经屏蔽浏览器差异。 + +
    diff --git a/src/utils/demo/src/app/keymap/webdoc/keymap.en.md b/src/utils/demo/src/app/keymap/webdoc/keymap.en.md new file mode 100644 index 0000000..9fcb90f --- /dev/null +++ b/src/utils/demo/src/app/keymap/webdoc/keymap.en.md @@ -0,0 +1,28 @@ +--- +title: Accordion +--- +# Accordion + +
    + +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
    + +
    + +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/utils/demo/src/app/log/LogTestModule.ts b/src/utils/demo/src/app/log/LogTestModule.ts new file mode 100644 index 0000000..baf24ab --- /dev/null +++ b/src/utils/demo/src/app/log/LogTestModule.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { LogUsageComponent } from './LogUsageComponent'; + +@NgModule({ + imports: [CommonModule, RouterModule.forChild(LogTestModule.ROUTES)], + declarations: [LogUsageComponent] +}) +export class LogTestModule { + static readonly LINKS: Array = [{ href: 'classes/TiLog.html', label: 'TiLog' }]; + static readonly ROUTES: Routes = [ + { + path: 'log/log-usage', + component: LogUsageComponent + } + ]; +} diff --git a/src/utils/demo/src/app/log/LogUsageComponent.ts b/src/utils/demo/src/app/log/LogUsageComponent.ts new file mode 100644 index 0000000..8dbf1aa --- /dev/null +++ b/src/utils/demo/src/app/log/LogUsageComponent.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; +import { TiLog } from '@opentiny/ng'; + +@Component({ + templateUrl: './log-usage.html' +}) +export class LogUsageComponent { + constructor() { + // 当前日志的数值大于等于日志等级时,当前日志才会正常输出 + // 当前日志等级:LEVEL_LOG为1 + TiLog.setLevel(TiLog.LEVEL_LOG); + // log的数值是1,大于等于1,正常输出 + TiLog.log('LEVEL_LOG:log'); + // warn的数值是2,大于等于1,正常输出 + TiLog.warn('LEVEL_LOG:warn'); + // error的数值是3,大于等于1,正常输出 + TiLog.error('LEVEL_LOG:error'); + + // 当前日志等级:LEVEL_WARN为 2 + TiLog.setLevel(TiLog.LEVEL_WARN); + // log的数值是1,小于2,不会输出 + TiLog.log('LEVEL_WARN: log'); + // warn的数值是2,大于等于2,正常输出 + TiLog.warn('LEVEL_WARN: warn'); + // error的数值是3,大于等于2,正常输出 + TiLog.error('LEVEL_WARN: error'); + + // 以下同理 + TiLog.setLevel(TiLog.LEVEL_ERROR); + // 不输出 + TiLog.log('LEVEL_ERROR: log'); + // 不输出 + TiLog.warn('LEVEL_ERROR: warn'); + // 输出 + TiLog.error('LEVEL_ERROR: error'); + + TiLog.setLevel(TiLog.LEVEL_OFF); + // 不输出 + TiLog.log('LEVEL_OFF:log'); + // 不输出 + TiLog.warn('LEVEL_OFF:warn'); + // 不输出 + TiLog.error('LEVEL_OFF:error'); + } +} diff --git a/src/utils/demo/src/app/log/log-usage.html b/src/utils/demo/src/app/log/log-usage.html new file mode 100644 index 0000000..e020de6 --- /dev/null +++ b/src/utils/demo/src/app/log/log-usage.html @@ -0,0 +1 @@ +

    请打开控制台查看,日志是否可以正常输出,与当前Log等级有关

    diff --git a/src/utils/demo/src/app/log/webdoc/log-demos.js b/src/utils/demo/src/app/log/webdoc/log-demos.js new file mode 100644 index 0000000..4429411 --- /dev/null +++ b/src/utils/demo/src/app/log/webdoc/log-demos.js @@ -0,0 +1,23 @@ +export default { + column: '2', + demos: [ + { + demoId: 'log-usage', + name: { + 'zh-CN': '基本用法', + 'en-US': 'log usage', + }, + desc: { + 'zh-CN': + '

    当前日志的数值大于等于日志等级时,当前日志才会被打印输出

    ', + 'en-US': '

    log usage

    ', + }, + apis: [ + 'TiLog.methods.log', + 'TiLog.methods.error', + 'TiLog.methods.warn', + 'TiLog.methods.setLevel', + ], + }, + ], +}; diff --git a/src/utils/demo/src/app/log/webdoc/log.cn.md b/src/utils/demo/src/app/log/webdoc/log.cn.md new file mode 100644 index 0000000..4e5a48f --- /dev/null +++ b/src/utils/demo/src/app/log/webdoc/log.cn.md @@ -0,0 +1,16 @@ +--- +title: Log 日志 +--- +# Log 日志 + +
    + +Logger 工具类, 提供全局日志输出和级别控制。 + +
    + +
    + +Logger 工具类, 提供全局日志输出和级别控制。 + +
    diff --git a/src/utils/demo/src/app/log/webdoc/log.en.md b/src/utils/demo/src/app/log/webdoc/log.en.md new file mode 100644 index 0000000..e97a860 --- /dev/null +++ b/src/utils/demo/src/app/log/webdoc/log.en.md @@ -0,0 +1,26 @@ +--- +title: Accordion +--- +# Accordion + +
    + +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
    + +
    + +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
    \ No newline at end of file diff --git a/src/utils/demo/src/app/theme/ThemeBasicComponent.ts b/src/utils/demo/src/app/theme/ThemeBasicComponent.ts new file mode 100644 index 0000000..4c396be --- /dev/null +++ b/src/utils/demo/src/app/theme/ThemeBasicComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiTheme } from '@opentiny/ng'; + +@Component({ + templateUrl: './theme-basic.html' +}) +export class ThemeBasicComponent { + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL'] ? window['DEPLOY_URL'] + window['PUBLIC_URL'] : ''; + switchState: boolean = true; + + blue(): void { + TiTheme.loadCss(`${this.baseUrl}assets/themes/theme-blue.css`, 'tiny3theme'); + } + green(): void { + TiTheme.loadCss(`${this.baseUrl}assets/themes/theme-green.css`, 'tiny3theme'); + } + purple(): void { + TiTheme.loadCss(`${this.baseUrl}assets/themes/theme-purple.css`, 'tiny3theme'); + } + red(): void { + TiTheme.loadCss(`${this.baseUrl}assets/themes/theme-red.css`, 'tiny3theme'); + } + default(): void { + TiTheme.loadCss(`${this.baseUrl}assets/themes/theme-default.css`, 'tiny3theme'); + } +} diff --git a/src/utils/demo/src/app/theme/ThemeTestModule.ts b/src/utils/demo/src/app/theme/ThemeTestModule.ts new file mode 100644 index 0000000..16e360c --- /dev/null +++ b/src/utils/demo/src/app/theme/ThemeTestModule.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiSwitchModule } from '@opentiny/ng'; + +import { ThemeBasicComponent } from './ThemeBasicComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiButtonModule, TiSwitchModule, RouterModule.forChild(ThemeTestModule.ROUTES)], + declarations: [ThemeBasicComponent] +}) +export class ThemeTestModule { + static readonly LINKS: Array = [{ href: 'classes/TiThemeUtil.html', label: 'Theme' }]; + static readonly ROUTES: Routes = [ + { + path: 'theme/theme-basic', + component: ThemeBasicComponent + } + ]; +} diff --git a/src/utils/demo/src/app/theme/theme-basic.html b/src/utils/demo/src/app/theme/theme-basic.html new file mode 100644 index 0000000..3e0837d --- /dev/null +++ b/src/utils/demo/src/app/theme/theme-basic.html @@ -0,0 +1,5 @@ +    +    + 
    + + diff --git a/src/utils/demo/src/app/theme/webdoc/theme-demos.js b/src/utils/demo/src/app/theme/webdoc/theme-demos.js new file mode 100644 index 0000000..61e1c88 --- /dev/null +++ b/src/utils/demo/src/app/theme/webdoc/theme-demos.js @@ -0,0 +1,17 @@ +export default { + column: '2', + demos: [ + { + demoId: 'theme-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'theme-basic', + }, + desc: { + 'zh-CN': '在线换肤能力展示。(仅支持生产环境,不支持调试环境)', + 'en-US': '

    theme basic

    ', + }, + apis: ['TiTheme.methods.loadCss'], + }, + ], +}; diff --git a/src/utils/demo/src/app/theme/webdoc/theme.cn.md b/src/utils/demo/src/app/theme/webdoc/theme.cn.md new file mode 100644 index 0000000..8000247 --- /dev/null +++ b/src/utils/demo/src/app/theme/webdoc/theme.cn.md @@ -0,0 +1,16 @@ +--- +title: Theme 主题配置 +--- +# Theme 主题配置 + +
    + +样式主题工具类 + +
    + +
    + +样式主题工具类 + +
    diff --git a/src/utils/demo/src/app/theme/webdoc/theme.en.md b/src/utils/demo/src/app/theme/webdoc/theme.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/utils/demo/src/app/theme/webdoc/theme.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/utils/demo/src/favicon.ico b/src/utils/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/utils/demo/src/index.html b/src/utils/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/utils/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/utils/demo/src/main.ts b/src/utils/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/utils/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/utils/demo/tsconfig.app.json b/src/utils/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/utils/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/utils/lib/index.ts b/src/utils/lib/index.ts new file mode 100644 index 0000000..876c199 --- /dev/null +++ b/src/utils/lib/index.ts @@ -0,0 +1,9 @@ +export * from './src/ObservableMap'; +export * from './src/ObservableSet'; +export * from './src/Position'; +export * from './src/TiBrowser'; +export * from './src/TiDateUtil'; +export * from './src/TiKeymap'; +export * from './src/TiLog'; +export * from './src/TiTheme'; +export * from './src/Util'; diff --git a/src/utils/lib/ng-package.json b/src/utils/lib/ng-package.json new file mode 100644 index 0000000..be81921 --- /dev/null +++ b/src/utils/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/utils", + "lib": { + "entryFile": "index.ts" + } +} diff --git a/src/utils/lib/package.json b/src/utils/lib/package.json new file mode 100644 index 0000000..7e6b4a4 --- /dev/null +++ b/src/utils/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-utils", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/platform-browser-dynamic": ">=13.0.0" + }, + "dependencies": { + "tslib": "^2.3.0" + } +} \ No newline at end of file diff --git a/src/utils/lib/project.json b/src/utils/lib/project.json new file mode 100644 index 0000000..c67be66 --- /dev/null +++ b/src/utils/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/utils/lib", + "sourceRoot": "src/utils/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/utils"], + "options": { + "project": "src/utils/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/utils"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js utils" + }, + { + "command": "ng default-build utils" + }, + { + "command": "node build/clear-default-theme.js utils" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/utils && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build utils && ng pack utils && node build/publish.js utils --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/utils/lib/src/ObservableMap.ts b/src/utils/lib/src/ObservableMap.ts new file mode 100644 index 0000000..6107270 --- /dev/null +++ b/src/utils/lib/src/ObservableMap.ts @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * @ignore + */ +export type HandlerFnType = (key: any, value: any, add: boolean, from?: any) => void; +// TODO:未来看typescript如何继承Map +/** + * @ignore + */ +export class ObservableMap { + // 代理模式 + private map: Map = new Map(); + get size(): number { + return this.map.size; + } + constructor() { + this.map = new Map(); + } + // 观察者模式 + private handlerSet: Set = new Set(); + public addObserver(handlerFn: HandlerFnType): void { + this.handlerSet.add(handlerFn); + } + public removeObserver(handlerFn: HandlerFnType): void { + this.handlerSet.delete(handlerFn); + } + // 通知函数,不会通知到发起源 + private notify(key: any, value: any, add: boolean, from: any): void { + this.handlerSet.forEach((handlerFn: HandlerFnType) => { + handlerFn(key, value, add, from); + }); + } + // 设置,可以添加发起源,那么消息不会通知到发起源。 + public set(key: any, value: any, from?: any): this { + if (!this.map.has(key) || (this.map.has(key) && this.map.get(key) !== value)) { + this.map.set(key, value); + this.notify(key, value, true, from); + } + + return this; + } + // 删除,可以添加发起源,那么消息不会通知到发起源。 + public delete(key: any, from?: any): boolean { + if (this.map.has(key)) { + this.map.delete(key); + this.notify(key, undefined, false, from); + + return true; + } + + return false; + } + public has(key: any): boolean { + return this.map.has(key); + } + public get(key: any): boolean { + return this.map.get(key); + } + public getMap(): Map { + // 方便遍历Map,所有有了这个方法。 + return this.map; + } +} diff --git a/src/utils/lib/src/ObservableSet.ts b/src/utils/lib/src/ObservableSet.ts new file mode 100644 index 0000000..b61ff55 --- /dev/null +++ b/src/utils/lib/src/ObservableSet.ts @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * @ignore + */ +export type SetHandlerFnType = (item: any, add: boolean, from?: any) => void; +// TODO:未来看typescript如何继承Set +/** + * @ignore + */ +export class ObservableSet { + // 代理模式 + private set: Set = new Set(); + get size(): number { + return this.set.size; + } + constructor(items?: any) { + this.set = new Set(items); + } + // 观察者模式 + private handlerSet: Set = new Set(); + public addObserver(handlerFn: SetHandlerFnType): void { + this.handlerSet.add(handlerFn); + } + public removeObserver(handlerFn: SetHandlerFnType): void { + this.handlerSet.delete(handlerFn); + } + private notify(item: any, isAdd: boolean, from: any): void { + this.handlerSet.forEach((handlerFn: SetHandlerFnType) => { + handlerFn(item, isAdd, from); + }); + } + public add(item: any, from?: any): this { + if (!this.set.has(item)) { + this.set.add(item); + this.notify(item, true, from); + } + + return this; + } + public delete(item: any, from?: any): boolean { + if (this.set.has(item)) { + this.set.delete(item); + this.notify(item, false, from); + + return true; + } + + return false; + } + public has(item: any): boolean { + return this.set.has(item); + } +} diff --git a/src/utils/lib/src/Position.ts b/src/utils/lib/src/Position.ts new file mode 100644 index 0000000..96d20d8 --- /dev/null +++ b/src/utils/lib/src/Position.ts @@ -0,0 +1,720 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiLog } from './TiLog'; +import { Util } from './Util'; +/** + * 位置显示类型定义,其中auto代表自适应位置显示 + */ +export type TiPositionType = + | 'top' + | 'top-left' + | 'top-right' + | 'bottom' + | 'bottom-left' + | 'bottom-right' + | 'left' + | 'left-top' + | 'left-bottom' + | 'right' + | 'right-top' + | 'right-bottom' + | 'center' + | 'auto' + | 'none'; +export type TiPositionEventType = 'tiScroll' | 'resize' | 'hashchange'; +interface TiPositionLayout { + hostLayout: TiHostLayout; + targetLayout: TiEleLayout; + avilableLayout: TiAvilableLayout; + hostOffsetLayout?: TiHostLayout; +} +export interface TiHostLayout { + top: number; + left: number; + width: number; + height: number; + // 最近的fixed定位的祖先元素 + fixedAncestor: any; +} +interface TiEleLayout { + top: number; + left: number; + width: number; + height: number; +} +interface TiAvilableLayout { + left: number; + maxLeft: number; + right: number; + maxRight: number; + top: number; + maxTop: number; + bottom: number; + maxBottom: number; + clientHeight: number; + clientWidth: number; +} +export interface TiPositionResult { + position: TiPositionType; + avilableHeight: number; + hostLayout?: TiHostLayout; +} +interface TiLayoutParams { + left: number; + top: number; + avilableHeight: number; +} +interface TiVerticalParams { + avilableHeight: number; + top: number; +} +/** + * @ignore + * 该类提供公共静态类方法,用于设置在body定位元素基于参照元素的位置信息,提供的核心方法如下: + * setPosition() + * 1. 入参为对象,参数信息如下: + * { + * targetEle:需要定位的元素 + * hostEle:参照元素 + * position:定位位置 + * hostSpace:待定位元素和参照元素间距 + * browserSpace :待定位元素和浏览器间距, 暂时只在上下位置生效 + * consoleHeaderHeight: console页面头部高度 + * hOffset: 自定义水平方向的偏移(在定位基础上的水平偏移,向左偏移为负值,向右偏移为正值) + * fixMaxHeight: 定位元素最大高度是否固定不变(显示不下时不用压缩高度) + * determinPositionFn:决定位置的函数 + * } + * 2. 返回值为对象,信息如下: + * { + * position:string 最终元素定位位置(当外部定义位置非13个可选位置时,会重新计算合适的位置进行定位,因此提供此位置信息供外部使用) + * avilableHeight:number 元素可用高度占位信息(外部可使用该信息进行高度的重新定义) + * } + */ +export class Position { + private static positionArr: Array = [ + 'top', + 'top-left', + 'top-right', + 'bottom', + 'bottom-left', + 'bottom-right', + 'left', + 'left-top', + 'left-bottom', + 'right', + 'right-top', + 'right-bottom', + 'center' + ]; + /* tip 针对较小元素做位置自适应处理,保证 tip 的箭头在被提示元素的中间,这是宽度或者高度的阈值 + * 取值为 15 的原因: 箭头定位偏移量9 + 箭头宽度的一半6 = 15px 相关变量变化时需要修改该常量 + */ + private static readonly ADAPTIVE_SIZE: number = 15; + public static setPosition(options: { + targetEle: any; + hostEle: any; + hostEleX?: any; // 决定定位元素水平方向的元素,用于宿主元素水平方向位置与host元素不一致的场景,暂不对外开放 + // 定位的参考元素,是离targetEle最近的absolute或relative元素,不传入时,是指targetEle以body为参考元素进行定位 + referElem?: any; + position?: TiPositionType; + hostSpace?: number; + browserSpace?: number; // 与浏览器距离像素 + consoleHeaderHeight?: number; + hOffset?: number; // 自定义水平方向的偏移(在定位基础上的水平偏移,向左偏移为负值,向右偏移为正值) + fixMaxHeight?: boolean; // 定位元素最大高度是否固定不变(显示不下时不用压缩高度) + hasOffsetFix?: boolean; // 是否存在定位偏差量(针对 tip 组件:当被提示元素尺寸较小时,保证 tip 的箭头在被提示元素的中间) + determinPositionFn?(layout: any): string; + }): TiPositionResult { + // 入参非有效元素情况下,不做处理 + if (!Util.isElement(options.hostEle) || !Util.isElement(options.targetEle)) { + TiLog.warn('position: hostEle or targetEle type is not element'); + + // 防止外部使用报错,此处做返回值处理 + return { + position: 'top', + avilableHeight: 9999 + }; + } + // 分别获取宿主和待定位元素当前位置 + const curLayout: TiPositionLayout = Position.getLayout( + options.hostEle, + options.targetEle, + options.hostEleX, + options.referElem, + options.consoleHeaderHeight || 0 + ); + const hostSpace: number = options.hostSpace || 0; + // 判断元素位置 + const position: any = options.determinPositionFn + ? options.determinPositionFn(curLayout) + : Position.determinPosition(options.position, curLayout, hostSpace, options.browserSpace || 0); + const elePos: TiLayoutParams = Position.getLayoutParam( + curLayout, + position, + hostSpace, + options.browserSpace || 0, + options.hasOffsetFix || false + ); + const hOffset: number = options.hOffset || 0; + Position.setLayout(options.targetEle, elePos, curLayout, hOffset); + Position.setMaxHeight(options.targetEle, elePos, curLayout, options.fixMaxHeight); + Position.setDominatorDropDetachState(options.targetEle, elePos, curLayout, options.fixMaxHeight); + + return { + position, // 最终元素定位位置 + avilableHeight: elePos.avilableHeight, // 元素可用高度占位 + hostLayout: curLayout.hostLayout + }; + } + + /** + * 添加影响host position的事件并返回其事件句柄 + * 由于Angular listen不支持多事件定义,因此在此处封装函数进行单独处理;此外,事件也不支持定义命名空间 + * 监听全局事件,用于处理页面位置出现变化导致宿主元素位置偏移,而tip的特殊情况下tip消失, + * 并定义组件事件监听句柄,事件取消时会用到 + * 特殊场景包括: + * 1. 拖动弹框位置导致的宿主元素位置变化 + * 2. 页面局部出滚动条,滚动条位置变化导致的宿主元素位置变化 + * 鼠标导致的滚轮事件可通过mousewheel/DOMMouseScroll监听(有冒泡的特性) + * 拖拽导致的滚轮变化,需要业务通过trigger Tiny自定义事件tiScroll进行处理(无事件冒泡的特性) + * 3.页面缩放 + * 4.路由切换页面 + */ + public static addPosChangeEvts(eventCallback: any, render: any, eventTypes?: Array): Array<() => void> { + const eventHandles: Array<() => void> = []; + // 修复SSR报错:ERROR ReferenceError: document is not defined + if (typeof document === 'undefined') { + return []; + } + // 需要添加的事件数据抽象 + let eventArr: Array = [ + { + // 鼠标拖动页面内滚动条场景,该事件需要业务通过trigger tiScroll事件进行处理 + ele: typeof document !== 'undefined' ? document : null, + eventType: 'tiScroll', + callback: eventCallback + }, + { + // 页面缩放 + ele: typeof window !== 'undefined' ? window : null, + eventType: 'resize', + callback: eventCallback + }, + { + // 路由切换页面 + ele: typeof window !== 'undefined' ? window : null, + eventType: 'hashchange', + callback: eventCallback + } + ]; + if (eventTypes) { + eventArr = eventArr.filter((item: any) => eventTypes.includes(item.eventType)); + } + eventArr.forEach((item: any) => { + // 修复SSR问题: ERROR TypeError: Cannot read property 'addEventListener' of null + if (item.ele) { + return eventHandles.push(render.listen(item.ele, item.eventType, item.callback)); + } + }); + + return eventHandles; + } + /** + * 清除绑定事件,与addPosChangeEvts对称 + */ + public static removePosChangeEvts(eventHandles: any): void { + eventHandles.forEach((item: any) => { + if (item) { + item(); + } + }); + } + // 获取host和target元素的布局参数 + private static getLayout( + hostEle: any, + targetEle: any, + horizonHostEle: any, + referElem?: any, + consoleHeaderHeight?: number + ): TiPositionLayout { + const hostLayout: TiHostLayout = Position.getHostEleLayout(hostEle, horizonHostEle); + const layout = { + hostLayout, + targetLayout: Position.getEleLayout(targetEle), + avilableLayout: Position.getAvilableLayout(hostLayout, consoleHeaderHeight) + }; + if (Util.isElement(referElem)) { + const hostOffsetLayout: any = + referElem === hostEle + ? { + left: 0, + top: 0 + } + : { + left: hostEle.offsetLeft, + top: hostEle.offsetTop + }; + layout['hostOffsetLayout'] = hostOffsetLayout; + } + + return layout; + } + /** + * 根据高亮浮层位置及提示弹窗位置情况计算具体显示位置 + * @param position 元素位置定义信息 + * @param layout 宿主和待定位元素位置信息 + * @param hostSpace 宿主和待定位元素间距 + * @param browserSpace 浏览器和待定位元素间距 + * @returns 提示弹窗呈现位置 + */ + private static determinPosition(position: TiPositionType, layout: TiPositionLayout, hostSpace: number, browserSpace: number): string { + // position定义有效情况下,不做处理 + if (Position.isValidPosition(position, layout, hostSpace, browserSpace)) { + return position; + } + const targetLayout: TiEleLayout = layout.targetLayout; + const avilableLayout: TiAvilableLayout = layout.avilableLayout; + // 元素位置距各部分位置 + const avilableLeft: number = avilableLayout.left; + const avilableRight: number = avilableLayout.right; + const avilableTop: number = avilableLayout.top; + const avilableBottom: number = avilableLayout.bottom; + const targetMaxWidth: number = targetLayout.width + hostSpace + browserSpace; + const targetMaxHeight: number = targetLayout.height + hostSpace + browserSpace; + const hostLayout: TiHostLayout = layout.hostLayout; + if (avilableTop >= targetMaxHeight) { + // 上方可显示完全 + const positionTop: string = Position.determinHorizon(layout, hostLayout); + if (positionTop !== undefined) { + return 'top' + positionTop; + } + } + if (avilableRight >= targetMaxWidth) { + // 右侧可显示完全,具体思路同上方可显示完全情况 + const positionRight: string = Position.determinVertical(layout, hostLayout); + if (positionRight !== undefined) { + return 'right' + positionRight; + } + } + if (avilableBottom >= targetMaxHeight) { + // 下方可显示完全,具体思路同上方可显示完全情况 + const positionBottom: string = Position.determinHorizon(layout, hostLayout); + if (positionBottom !== undefined) { + return 'bottom' + positionBottom; + } + } + if (avilableLeft >= targetMaxWidth) { + // 左侧足够显示情况下,左侧居中显示 + const positionLeft: string = Position.determinVertical(layout, hostLayout); + if (positionLeft !== undefined) { + return 'left' + positionLeft; + } + } + + return 'center'; // 上下左右均无法足够显示情况下,居中显示内容 + } + /** + * 垂直位置确定后,确定水平位置 + */ + private static determinHorizon(layout: TiPositionLayout, hostLayout: TiHostLayout): string { + const targetLayout: TiEleLayout = layout.targetLayout; + const avilableLayout: TiAvilableLayout = layout.avilableLayout; + const avilableLeft: number = avilableLayout.left; + const avilableMaxLeft: number = avilableLayout.maxLeft; + const avilableRight: number = avilableLayout.right; + const avilableMaxRight: number = avilableLayout.maxRight; + const targetWidth: number = targetLayout.width; + const targetCenterWidth: number = (targetLayout.width - hostLayout.width) / 2; // 水平居中情况下,左右侧占用的宽度分别比宿主元素宽多少 + if (avilableRight >= targetCenterWidth && avilableLeft >= targetCenterWidth) { + // 提示元素超出被提示元素部分足够显示的情况下,优先居中显示 + return ''; + } + if (avilableMaxRight >= targetWidth) { + // 提示元素可在被提示元素左边界靠右完全显示情况下,向右显示 + return '-left'; + } + if (avilableMaxLeft >= targetWidth) { + // 提示元素可在被提示元素左边界靠左完全显示情况下,向左显示 + return '-right'; + } + } + /** + * 水平位置确定后,确定垂直位置 + */ + private static determinVertical(layout: TiPositionLayout, hostLayout: TiHostLayout): string { + const targetLayout: TiEleLayout = layout.targetLayout; + const avilableLayout: TiAvilableLayout = layout.avilableLayout; + const avilableTop: number = avilableLayout.top; + const avilableMaxTop: number = avilableLayout.maxTop; + const avilableBottom: number = avilableLayout.bottom; + const avilableMaxBottom: number = avilableLayout.maxBottom; + const targetHeight: number = targetLayout.height; + const targetMiddleHeight: number = (targetLayout.height - hostLayout.height) / 2; // 垂直居中情况下,上下两侧占用的高度分别比宿主元素高多少 + if (avilableTop >= targetMiddleHeight && avilableBottom >= targetMiddleHeight) { + return ''; + } + if (avilableMaxBottom >= targetHeight) { + return '-top'; + } + if (avilableMaxTop >= targetHeight) { + return '-bottom'; + } + } + + // 判断定义的position是否为有效位置 + private static isValidPosition(position: TiPositionType, layout: TiPositionLayout, hostSpace: number, browserSpace: number): boolean { + if (Position.positionArr.indexOf(position) === -1) { + return false; + } + // 判断在指定的position是否能显示下 + const positionFragments: Array = position.split('-'); + const targetLayout: TiEleLayout = layout.targetLayout; + const avilableLayout: TiAvilableLayout = layout.avilableLayout; + // 元素位置距各部分位置 + const avilableLeft: number = avilableLayout.left; + const avilableRight: number = avilableLayout.right; + const avilableTop: number = avilableLayout.top; + const avilableBottom: number = avilableLayout.bottom; + const targetMaxWidth: number = targetLayout.width + hostSpace + browserSpace; + const targetMaxHeight: number = targetLayout.height + hostSpace + browserSpace; + if (positionFragments[0] === 'top') { + if (avilableTop < targetMaxHeight) { + return false; + } + + return Position.isValidHorizonPosition(positionFragments[1], layout); + } + if (positionFragments[0] === 'bottom') { + if (avilableBottom < targetMaxHeight) { + return false; + } + + return Position.isValidHorizonPosition(positionFragments[1], layout); + } + if (positionFragments[0] === 'left') { + if (avilableLeft < targetMaxWidth) { + return false; + } + + return Position.isValidVerticalPosition(positionFragments[1], layout); + } + if (positionFragments[0] === 'right') { + if (avilableRight < targetMaxWidth) { + return false; + } + + return Position.isValidVerticalPosition(positionFragments[1], layout); + } + + return true; + } + + // 确定垂直位置是有效的后,判断水平位置是否有效 + private static isValidHorizonPosition(horizonPosition: string, layout: TiPositionLayout): boolean { + const targetLayout: TiEleLayout = layout.targetLayout; + const avilableLayout: TiAvilableLayout = layout.avilableLayout; + const hostLayout: TiHostLayout = layout.hostLayout; + const avilableLeft: number = avilableLayout.left; + const avilableRight: number = avilableLayout.right; + const avilableMaxLeft: number = avilableLayout.maxLeft; + const avilableMaxRight: number = avilableLayout.maxRight; + const targetWidth: number = targetLayout.width; + const targetCenterWidth: number = (targetLayout.width - hostLayout.width) / 2; // 水平居中情况下,左右侧占用的宽度分别比宿主元素宽多少 + if (!horizonPosition) { + return avilableRight >= targetCenterWidth && avilableLeft >= targetCenterWidth; + } + if (horizonPosition === 'left') { + return avilableMaxRight >= targetWidth; + } + if (horizonPosition === 'right') { + return avilableMaxLeft >= targetWidth; + } + + return false; + } + + // 确定水平位置是有效的后,判断垂直位置是否有效 + private static isValidVerticalPosition(verticalPosition: string, layout: TiPositionLayout): boolean { + const targetLayout: TiEleLayout = layout.targetLayout; + const avilableLayout: TiAvilableLayout = layout.avilableLayout; + const hostLayout: TiHostLayout = layout.hostLayout; + const avilableTop: number = avilableLayout.top; + const avilableBottom: number = avilableLayout.bottom; + const avilableMaxTop: number = avilableLayout.maxTop; + const avilableMaxBottom: number = avilableLayout.maxBottom; + const targetHeight: number = targetLayout.height; + const targetMiddleHeight: number = (targetLayout.height - hostLayout.height) / 2; // 垂直居中情况下,上下两侧占用的高度分别比宿主元素高多少 + if (!verticalPosition) { + return avilableTop >= targetMiddleHeight && avilableBottom >= targetMiddleHeight; + } + if (verticalPosition === 'top') { + return avilableMaxBottom >= targetHeight; + } + if (verticalPosition === 'bottom') { + return avilableMaxTop >= targetHeight; + } + + return false; + } + + // 设置提示弹窗位置 + private static setLayout(ele: any, position: TiLayoutParams, layoutParam: TiPositionLayout, hOffset: number): void { + if (layoutParam.hostLayout.fixedAncestor && !layoutParam.hostOffsetLayout) { + // fixed定位情况下,滚动条对其不受影响,因此此处需要减掉滚动条的位置影响 + // 如果是跟随宿主元素,也不能改为fixed定位,会脱离文档流,定位偏离 + ele.style.position = 'fixed'; + ele.style.left = `${position.left - window.pageXOffset + hOffset}px`; + ele.style.top = `${position.top - window.pageYOffset}px`; + } else { + ele.style.left = `${position.left + hOffset}px`; + ele.style.top = `${position.top}px`; + } + } + // 设置提示弹窗的MaxHeight + private static setMaxHeight(ele: any, position: any, layoutParam: any, fixMaxHeight: boolean): void { + if (fixMaxHeight || position.avilableHeight >= layoutParam.targetLayout.height) { + return; + } + ele.style.maxHeight = `${position.avilableHeight}px`; + } + /** + * 当drop固定高度(设置fixMaxHeight=ture),且空间不足时,dominator和drop不相邻,此场景需要隐藏drop中的边框覆盖线, + * 添加样式类'ti3-detach-dominator-drop'标记drop的这种场景状态,通过css样式控制drop中边框覆盖线的隐藏和显示 + * 目前该场景出现在日期类组件中 + */ + public static setDominatorDropDetachState(ele: HTMLElement, position: any, layoutParam: any, fixMaxHeight: boolean): void { + if (!fixMaxHeight) { + return; + } + if (position.avilableHeight < layoutParam.targetLayout.height) { + ele.classList.add('ti3-detach-dominator-drop'); + } else { + ele.classList.remove('ti3-detach-dominator-drop'); + } + } + + private static getEleLayout(ele: any): TiEleLayout { + // 修复SSR报错:ERROR TypeError: ele.getBoundingClientRect is not a function + return { + top: ele.offsetTop, + left: ele.offsetLeft, + width: typeof ele.getBoundingClientRect === 'function' ? ele.getBoundingClientRect().width : 0, + height: typeof ele.getBoundingClientRect === 'function' ? ele.getBoundingClientRect().height : 0 + }; + } + public static getHostEleLayout(hostEle: any, horizonHostEle?: any): TiHostLayout { + let horizonEle = hostEle; + if (Util.isElement(horizonHostEle)) { + horizonEle = horizonHostEle; + } + // 修复SSR报错:ERROR TypeError: hostEle.getBoundingClientRect is not a function + // 元素相对于windows的位置 + let top: number = typeof hostEle.getBoundingClientRect === 'function' ? hostEle.getBoundingClientRect().top : 0; + let left: number = typeof hostEle.getBoundingClientRect === 'function' ? horizonEle.getBoundingClientRect().left : 0; + // 修复SSR报错:ERROR ReferenceError: getComputedStyle is not defined + // 下面利用body相对于windows的位置,计算元素相对于body的位置。 + const bodyPositon: string = typeof getComputedStyle === 'function' ? getComputedStyle(document.body, null).position : 'static'; + // body是static|relative|absolute|fixed都已测试,Chrome高低版本/Firefox/IE11已测。 + if (bodyPositon === 'static') { + // static时,body对window没有偏移,但是需要考虑页面滚动条 + if (typeof window !== 'undefined') { + top += window.pageYOffset; + left += window.pageXOffset; + } + } else { + // relative|fix|absolute时,需要考虑body相对于windows的偏移。 + top -= document.body.getBoundingClientRect().top; + left -= document.body.getBoundingClientRect().left; + } + + return { + top, + left, + width: horizonEle.offsetWidth, + height: hostEle.offsetHeight, + fixedAncestor: this.getFixedAncestor(hostEle) + }; + } + + private static getAvilableLayout(hostLayout: TiHostLayout, consoleHeaderHeight?: number): TiAvilableLayout { + // 修复报错:ERROR ReferenceError: document is not defined + // 当前浏览器可视区域的宽高 + const curBrowseWidth: number = typeof document !== 'undefined' ? document.documentElement.clientWidth : 0; + const curBrowseheight: number = typeof document !== 'undefined' ? document.documentElement.clientHeight : 0; + // 元素位置距各部分位置: + // 1.以元素四个边为边界,上下左右预留的可用宽高,用avilableLeft等标识 + // 2.去掉元素的宽高占位,计算的最大可用宽高,用avilableMax...标识 + // 修复SSR错误:ERROR ReferenceError: pageXOffset is not defined + const avilableLeft: number = hostLayout.left - (typeof window !== 'undefined' ? window.pageXOffset : 0); + const avilableMaxLeft: number = avilableLeft + hostLayout.width; + const avilableRight: number = curBrowseWidth - avilableMaxLeft; + const avilableMaxRight: number = curBrowseWidth - avilableLeft; + // document.body.scrollTop document.documentElement.scrollTop存在浏览器差异,chrome高低版本表现不同。 + const curTop: number = hostLayout.top - (typeof window !== 'undefined' ? window.pageYOffset : 0); // pageYOffset支持IE9以上。 + const curMaxTop: number = curTop + hostLayout.height; + const avilableTop: number = curTop - consoleHeaderHeight; + const avilableMaxTop: number = avilableTop + hostLayout.height; + const avilableBottom: number = curBrowseheight - curMaxTop; + const avilableMaxBottom: number = curBrowseheight - curTop; + + return { + left: avilableLeft, + maxLeft: avilableMaxLeft, + right: avilableRight, + maxRight: avilableMaxRight, + top: avilableTop, + maxTop: avilableMaxTop, + bottom: avilableBottom, + maxBottom: avilableMaxBottom, + clientHeight: curBrowseheight, + clientWidth: curBrowseWidth + }; + } + // 确定元素是否为fixed定位 + private static getFixedAncestor(ele: any): any { + if (!ele || ele.nodeName === 'HTML') { + return null; + } + // 修复SSR报错:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle === 'undefined') { + return null; + } + if (getComputedStyle(ele).position === 'fixed') { + return ele; + } + + return this.getFixedAncestor(ele.parentNode); + } + private static getLayoutParam( + layout: TiPositionLayout, + position: string, + hostSpace: number, + browserSpace: number, + hasOffsetFix: boolean + ): TiLayoutParams { + const positionArr: Array = position.split('-'); // 跟上面静态变量重名,最好改一下 + const left: number = Position.getHorizionParam(layout, positionArr, hostSpace, hasOffsetFix); + const verticalParam: TiVerticalParams = Position.getVerticalParam(layout, positionArr, hostSpace, browserSpace, hasOffsetFix); + + return { + left, + top: verticalParam.top, + avilableHeight: verticalParam.avilableHeight + }; + } + private static getHorizionParam(curLayout: TiPositionLayout, posArr: Array, space: number, hasOffsetFix: boolean): number { + const { + hostOffsetLayout, + hostLayout, + targetLayout + }: { + hostOffsetLayout?: any; + hostLayout: TiHostLayout; + targetLayout: TiEleLayout; + } = curLayout; + + // 定位偏差调整量(该偏差量针对较小元素,保证 tip 的箭头在被提示元素的中间) + const offset = hasOffsetFix ? this.getOffsetFixVal(curLayout.hostLayout.width) : 0; + const hostLeft: number = hostOffsetLayout ? hostOffsetLayout.left : hostLayout.left; + + // 'left', 'left-top', 'left-bottom' + if (posArr[0] === 'left') { + return hostLeft - targetLayout.width - space; + } + // 'right', 'right-top', 'right-bottom' + if (posArr[0] === 'right') { + return hostLeft + hostLayout.width + space; + } + // 'top-left', 'bottom-left' + if (posArr[1] === 'left') { + return hostLeft - offset; + } + // 'top-right', 'bottom-right' + if (posArr[1] === 'right') { + return hostLeft + hostLayout.width - targetLayout.width + offset; + } + + // 'top', 'bottom', 'center' + /** + * 问题修复:当left为负值时,tip会超出页面,显示不全,此处进行优化。 + */ + const leftTemp: number = hostLeft - (targetLayout.width - hostLayout.width) / 2; + + return leftTemp >= 0 || hostOffsetLayout ? leftTemp : 0; + } + private static getVerticalParam( + curLayout: TiPositionLayout, + posArr: Array, + hostSpace: number, + browserSpace: number, + hasOffsetFix: boolean + ): TiVerticalParams { + const avilableLayout: TiAvilableLayout = curLayout.avilableLayout; // 宿主元素对应的可用位置参数 + const hostHeight: number = curLayout.hostLayout.height; // 宿主元素本身高度 + const targetLayout: TiEleLayout = curLayout.targetLayout; + const hostLayout: TiHostLayout = curLayout.hostLayout; + const hostOffsetLayout: TiHostLayout = curLayout.hostOffsetLayout; + + // 定位偏差调整量(该偏差量针对较小元素,保证 tip 的箭头在被提示元素的中间) + const offset = hasOffsetFix ? this.getOffsetFixVal(curLayout.hostLayout.height) : 0; + const hostTop: number = hostOffsetLayout ? hostOffsetLayout.top : hostLayout.top; + // 纵向可用高度是根据位置中top、bottom、居中情况进行的处理,因此,此处先将position进行分割,再根据其情况进行可用高度计算 + // 弹出元素下边线与宿主元素上边线相接后,弹出元素向上延伸情况,对应位置包含'top', 'top-left', 'top-right' + if (posArr[0] === 'top') { + const avilableHeight: number = avilableLayout.top - hostSpace - browserSpace; + const top: number = + avilableHeight >= targetLayout.height ? hostTop - targetLayout.height - hostSpace : hostTop - avilableLayout.top + browserSpace; + + return { + avilableHeight, + top + }; + } + // 弹出元素上边线与宿主元素下边线相接后,弹出元素向下延伸情况,对应位置包含'bottom', 'bottom-left', 'bottom-right' + if (posArr[0] === 'bottom') { + return { + avilableHeight: avilableLayout.bottom - hostSpace - browserSpace, + top: hostTop + hostLayout.height + hostSpace + }; + } + // 弹出元素上边线与宿主元素上边线对齐情况,对应位置包含'left-top', 'right-top' + if (posArr[1] === 'top') { + return { + avilableHeight: avilableLayout.bottom + hostHeight, + top: hostTop - offset + }; + } + // 弹出元素上边线与宿主元素下边线对齐情况,对应位置包含'right-bottom', 'left-bottom' + if (posArr[1] === 'bottom') { + return { + avilableHeight: avilableLayout.top + hostHeight, + top: hostTop + hostLayout.height - targetLayout.height + offset + }; + } + + // 弹出元素中线与宿主元素中线对齐情况,对应位置包含'left', 'right', 'center' + const avilableHeight: number = avilableLayout.clientHeight - browserSpace; + const topTemp: number = + avilableHeight >= targetLayout.height + ? hostTop - (targetLayout.height - hostLayout.height) / 2 + : hostTop - avilableLayout.top + browserSpace; + + return { + avilableHeight, + top: topTemp >= 0 || hostOffsetLayout ? topTemp : 0 + }; + } + // 获取定位偏差调整量 + private static getOffsetFixVal(hostLayoutSize: number): number { + return hostLayoutSize / 2 < Position.ADAPTIVE_SIZE ? Position.ADAPTIVE_SIZE - hostLayoutSize / 2 : 0; + } +} diff --git a/src/utils/lib/src/TiBrowser.ts b/src/utils/lib/src/TiBrowser.ts new file mode 100644 index 0000000..ca99498 --- /dev/null +++ b/src/utils/lib/src/TiBrowser.ts @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/* eslint-disable no-cond-assign */ +// 另,angular也有brower-util.ts,但是没有对浏览器版本号的判断。 + +/* + * 浏览器类型的枚举定义。字符串枚举,提供了运行时可读性。 + */ + +export class TiBrowser { + /** + * EDGE 浏览器字符串 + */ + public static readonly EDGE: string = 'EDGE'; + /** + * IE 浏览器字符串 + */ + public static readonly IE: string = 'IE'; + /** + * FIREFOX 浏览器字符串 + */ + public static readonly FIREFOX: string = 'FIREFOX'; + /** + * CHROME 浏览器字符串 + */ + public static readonly CHROME: string = 'CHROME'; + /** + * OPERA 浏览器字符串 + */ + public static readonly OPERA: string = 'OPERA'; + /** + * SAFARI 浏览器字符串 + */ + public static readonly SAFARI: string = 'SAFARI'; + /** + * OTHER 浏览器字符串 + */ + public static readonly OTHER: string = 'OTHER'; + + private static _browser: string = TiBrowser.OTHER; + private static _version: number = 0; // 浏览器的大版本号,整数。取第一个小数点前 + private static isInit: boolean = false; + + /** + * 浏览器类型 + * @returns 返回枚举型浏览器类型字符串 + */ + public static browser(): string { + TiBrowser.init(); + + return TiBrowser._browser; + } + /** + * 浏览器版本 + */ + public static version(): number { + TiBrowser.init(); + + return TiBrowser._version; + } + /** + * 是否 Edge + */ + public static isEdge(): boolean { + TiBrowser.init(); + + return TiBrowser.browser() === TiBrowser.EDGE; + } + /** + * 是否 IE + */ + public static isIE(): boolean { + TiBrowser.init(); + + return TiBrowser.browser() === TiBrowser.IE; + } + /** + * 是否 Firefox + */ + public static isFirefox(): boolean { + TiBrowser.init(); + + return TiBrowser.browser() === TiBrowser.FIREFOX; + } + /** + * 是否 Chrome + */ + public static isChrome(): boolean { + TiBrowser.init(); + + return TiBrowser.browser() === TiBrowser.CHROME; + } + /** + * 是否 Opera + */ + public static isOpera(): boolean { + TiBrowser.init(); + + return TiBrowser.browser() === TiBrowser.OPERA; + } + /** + * 是否 Safari + */ + public static isSafari(): boolean { + TiBrowser.init(); + + return TiBrowser.browser() === TiBrowser.SAFARI; + } + /** + * 是否 Other + */ + public static isOther(): boolean { + TiBrowser.init(); + + return TiBrowser.browser() === TiBrowser.OTHER; + } + + private static init(): void { + if (TiBrowser.isInit) { + // 如果已经初始化,那么返回 + return; + } + // 浏览器判断,已支持SSR + const userAgent: string = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase() : ''; + let aAgentInfo: Array | null; + + if (userAgent.match(/edge\/([\d.]+)/)) { + aAgentInfo = userAgent.match(/edge\/([\d.]+)/); + TiBrowser._browser = TiBrowser.EDGE; + } else if (userAgent.match(/rv:([\d.]+)\) like gecko/)) { + aAgentInfo = userAgent.match(/rv:([\d.]+)\) like gecko/); + TiBrowser._browser = TiBrowser.IE; + } else if (userAgent.match(/msie ([\d.]+)/)) { + aAgentInfo = userAgent.match(/msie ([\d.]+)/); + TiBrowser._browser = TiBrowser.IE; + } else if (userAgent.match(/firefox\/([\d.]+)/)) { + aAgentInfo = userAgent.match(/firefox\/([\d.]+)/); + TiBrowser._browser = TiBrowser.FIREFOX; + } else if (userAgent.match(/chrome\/([\d.]+)/)) { + aAgentInfo = userAgent.match(/chrome\/([\d.]+)/); + TiBrowser._browser = TiBrowser.CHROME; + } else if (userAgent.match(/version\/([\d.]+).*safari/)) { + aAgentInfo = userAgent.match(/version\/([\d.]+).*safari/); + TiBrowser._browser = TiBrowser.SAFARI; + } else { + TiBrowser._browser = TiBrowser.OTHER; + } + // 版本号取整 + TiBrowser._version = aAgentInfo !== null ? parseInt(aAgentInfo[1].split('.')[0], 10) : 0; + // 改变初始化值 + TiBrowser.isInit = true; + } +} diff --git a/src/utils/lib/src/TiDateUtil.ts b/src/utils/lib/src/TiDateUtil.ts new file mode 100644 index 0000000..52d15dc --- /dev/null +++ b/src/utils/lib/src/TiDateUtil.ts @@ -0,0 +1,470 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * @ignore + */ +export class TiDateUtil { + /* + * 区分中英文冒号 + */ + public static COLON_REGEXP: RegExp = /[:\uff1a]/; + /* + * 默认时间格式 + */ + public static DEFAULT_TIME_FORMAT: string = 'HH:mm:ss'; + /** + * 日期类组件的默认最小值 + */ + public static minDate(): Date { + return new Date(1970, 0, 1, 0, 0, 0); + } + /** + * 日期类组件的默认最大值 + */ + public static maxDate(): Date { + return new Date(2099, 11, 31, 23, 59, 59); + } + /** + * 判断是不是合法的时间对象 + */ + public static isDate(date: object | string): boolean { + if (Object.prototype.toString.call(date) === '[object Date]' && String(date) !== 'Invalid Date') { + return true; + } + + return false; + } + + /** + * @description 根据当前格式,格式化最大值 + * @param maxValue 最大值 + * @param picker 面板格式 + * @param isDateTime 是否是日期时间组件 + */ + public static changeMaxTime(maxValue: Date, picker?: string, isDateTime?: boolean): Date { + let value: Date = maxValue; + + if (!TiDateUtil.isDate(value)) { + value = TiDateUtil.maxDate(); + } + + if (isDateTime) { + return TiDateUtil.transformDatetimeToExactDatetime(value, picker, true); + } + + if (picker === 'quarter') { + const transfromDateToString: string = TiDateUtil.transformDateToQuarter(value); + + return TiDateUtil.getValidQuarterDate(transfromDateToString, true); + } + + return TiDateUtil.transformDateToExactDate(value, picker, true); + } + + /** + * @description 根据当前格式,格式化最小值 + * @param minValue 最小值 + * @param picker 面板格式 + * @param isDateTime 是否是日期时间组件 + */ + public static changeMinTime(minValue: Date, picker?: string, isDateTime?: boolean): Date { + let value: Date = minValue; + + if (!TiDateUtil.isDate(value)) { + value = TiDateUtil.minDate(); + } + + if (isDateTime) { + return TiDateUtil.transformDatetimeToExactDatetime(value, picker); + } + + if (picker === 'quarter') { + const transfromDateToString: string = TiDateUtil.transformDateToQuarter(value); + + return TiDateUtil.getValidQuarterDate(transfromDateToString); + } + + return TiDateUtil.transformDateToExactDate(value, picker); + } + + /** + * @description 判断两个日期是不是相等:只包括年月日 + * @param newValue 新值 + * @param oldValue 旧值 + */ + public static isDateEqual(newValue: Date, oldValue: Date): boolean { + if (newValue === oldValue) { + return true; + } + + // 当两个value都是Date类型时,要判断两个值的年月日是否相等,直接用‘===’判断不准确 + if (TiDateUtil.isDate(newValue) && TiDateUtil.isDate(oldValue)) { + // 转换成年月日,然后重新生成一个Date,再将其转换成毫秒数进行判断 + const newDate: Date = new Date(newValue.getFullYear(), newValue.getMonth(), newValue.getDate()); + const oldDate: Date = new Date(oldValue.getFullYear(), oldValue.getMonth(), oldValue.getDate()); + if (newDate.getTime() === oldDate.getTime()) { + return true; + } + } + + return false; + } + + /** + * @description 判断两个时间日期是不是相等; + * @param newValue 新值 + * @param oldValue 旧值 + */ + public static isDatetimeEqual(newValue: Date, oldValue: Date): boolean { + if (newValue === oldValue) { + return true; + } + + if (TiDateUtil.isDate(newValue) && TiDateUtil.isDate(oldValue)) { + if (newValue.getTime() === oldValue.getTime()) { + return true; + } + } + + return false; + } + + /** + * @description 判断是不是合法的NowDateTime值 + * 存在nowDateTime值,并且是时间对象,且大于最小值小于最大值 + */ + public static isValidNowDateTime(nowDateTime: Date, min: Date, max: Date): boolean { + return ( + nowDateTime && TiDateUtil.isDate(nowDateTime) && nowDateTime.getTime() >= min.getTime() && nowDateTime.getTime() <= max.getTime() + ); + } + + /** + * @description 判断value1是否大于value2 + */ + public static isBigger(value1: Date, value2: Date): boolean { + return value1.getTime() > value2.getTime(); + } + + /** + * @description 判断value1是否大于value2 + */ + public static isBiggerOrEqual(value1: Date, value2: Date): boolean { + return value1.getTime() >= value2.getTime(); + } + + /** + * @description 判断value1是否小于value2 + */ + public static isSmaller(value1: Date, value2: Date): boolean { + return value1.getTime() < value2.getTime(); + } + + /** + * @description 将Date类型的日期转换成年月日字符串 + * @param date 要进行转换的日期值 + */ + public static getDateStr(date: Date): string { + return date === null ? '' : `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; + } + + /** + * @description 将Date类型的日期转换成时分秒字符串 + * @param date 要进行转换的日期值 + */ + public static getTimeStr(date: Date): string { + const hour = date.getHours() < 10 ? '0' + date.getHours() : date.getHours(); + const minute = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes(); + const second = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds(); + + return `${hour}:${minute}:${second}`; + } + + /** + * @description 判断是不是一个合法的最大值 + * @param min 最小值 + * @param max 最大值 + */ + public static isValidMaxValue(min: Date, max: Date): boolean { + return TiDateUtil.isDate(max) && max.getTime() >= min.getTime(); + } + + /** + * @description 判断是不是一个合法的最小值 + * @param min 最小值 + * @param max 最大值 + */ + public static isValidMinValue(min: Date, max: Date): boolean { + return TiDateUtil.isDate(min) && max.getTime() >= min.getTime(); + } + + /** + * @description 是否在最大值最小值区间 + * @param min 最小值 + * @param max 最大值 + */ + public static isBetweenMaxAndmin(value: Date, min: Date, max: Date): boolean { + return value.getTime() >= min.getTime() && value.getTime() <= max.getTime(); + } + + /** + * @description 12小时制添加AMPM + */ + public static addAmPm(timeFormat: string): string { + if (timeFormat.match(/h/) && !timeFormat.match(/a/)) { + // 已设置ampm格式情况下,不做处理 + return timeFormat + ' a'; // Angular 的datePipe 设置时间时,"a"代表AMPM + } + + return timeFormat; + } + + /** + * @description: 若时间字符串没有一个冒号时处理成合法的事件字符串,根据时间字符串冒号的个数和是否是结束时间添加为完整的时间(包含时分秒) + * 例如 value: 12, isEndTime: true, 则返回12:59:59 + * @param value 时间字符串 + * @param isEndTime 是否是结束时间 + */ + public static addColon(value: string, isEndTime?: boolean): string { + const colonNumber: number = value.split(':').length - 1; + if (colonNumber) { + if (isEndTime && colonNumber === 1) { + return `${value}:59`; + } + + return value; + } + + const ampm: any = value.match(/am|AM|pm|PM/) || ''; + + if (isEndTime) { + return `${parseInt(value, 10)}:59:59 ${ampm}`; + } + + return `${parseInt(value, 10)}: ${ampm}`; + } + + /** + * @ignore + * date类型转换为季度 + */ + public static transformDateToQuarter(val: Date): string { + switch (val?.getMonth()) { + case 0: + case 1: + case 2: + return `${val.getFullYear()}/Q1`; + case 3: + case 4: + case 5: + return `${val.getFullYear()}/Q2`; + case 6: + case 7: + case 8: + return `${val.getFullYear()}/Q3`; + case 9: + case 10: + case 11: + return `${val.getFullYear()}/Q4`; + default: + return undefined; + } + } + /** + * 季度值转换为date类型 + */ + public static transFormQuarterToDate(year: number, val: string, isEnd?: boolean): any { + if (isEnd) { + switch (val) { + case 'Q1': + return new Date(year, 2, 31, 23, 59, 59, 999); + case 'Q2': + return new Date(year, 5, 30, 23, 59, 59, 999); + case 'Q3': + return new Date(year, 8, 30, 23, 59, 59, 999); + case 'Q4': + return new Date(year, 11, 31, 23, 59, 59, 999); + default: + return undefined; + } + } else { + switch (val) { + case 'Q1': + return new Date(year, 0, 1, 0, 0, 0); + case 'Q2': + return new Date(year, 3, 1, 0, 0, 0); + case 'Q3': + return new Date(year, 6, 1, 0, 0, 0); + case 'Q4': + return new Date(year, 9, 1, 0, 0, 0); + default: + return undefined; + } + } + } + /** + * @description 将当前日期转化为对应具体日期时间 + * + * @param value 要进行转换的日期值 + * @param picker 面板格式 + * @param isEnd 是否是结束日期 + */ + public static transformDateToExactDate(value: Date, picker: string, isEnd?: boolean): Date { + if (!this.isDate(value)) { + return; + } + const year: number = value.getFullYear(); + const month: number = value.getMonth() || value.getMonth() === 0 ? value.getMonth() : 11; + + if (isEnd) { + switch (picker) { + case 'onlyYear': + case 'year': + // 纯年份格式下,结束具体日期时间为12月31号23时59分59秒 + return new Date(year, 11, 31, 23, 59, 59, 999); + case 'onlyYearMonth': + case 'month': + // 年月格式下,结束具体日期时间当月最后一天的23时59分59秒 + // new Date(val.getFullYear(), month + 1, 0).getDate()获取当月的总天数, + // 此处month需要加1,否则获取的是上月总天数。 + return new Date(year, month, new Date(value.getFullYear(), month + 1, 0).getDate(), 23, 59, 59, 999); + default: + // 正常情况下,结束时间值取23时59分59秒,开始时间不处理,默认为零点。 + return new Date(year, month, value.getDate(), 23, 59, 59, 999); + } + } else { + switch (picker) { + case 'onlyYear': + case 'year': + // 纯年份格式下,开始具体日期时间为1月1号零点 + return new Date(year, 0, 1); + case 'onlyYearMonth': + case 'month': + // 年月格式下,开始具体日期时间当月第一天零点 + return new Date(year, month); + default: + // 正常情况下,开始时间值零点 + return new Date(year, month, value.getDate()); + } + } + } + + /** + * @description 将当前日期时间转化为对应具体日期时间 + * + * @param value 要进行转换的日期时间值 + * @param dateTimePicker 时间格式 + */ + public static transformDatetimeToExactDatetime(value: Date, dateTimePicker: string, isEnd?: boolean): Date { + if (!TiDateUtil.isDate(value)) { + return; + } + + const year: number = value.getFullYear(); + const month: number = value.getMonth(); + const date: number = value.getDate(); + const hours: number = value.getHours(); + const minutes: number = value.getMinutes(); + const seconds: number = value.getSeconds(); + + if (isEnd) { + switch (dateTimePicker) { + case 'onlyHours': + // 纯小时格式下,结束具体日期时间到59分59秒 + return new Date(year, month, date, hours, 59, 59, 999); + case 'onlyHoursMinutes': + // 纯时分格式下,结束具体日期时间到59秒 + return new Date(year, month, date, hours, minutes, 59, 999); + default: + // 正常情况下,结束时间值具体到秒 + return new Date(year, month, date, hours, minutes, seconds, 999); + } + } else { + switch (dateTimePicker) { + case 'onlyHours': + // 纯小时格式下,结束具体日期时间到时 + return new Date(year, month, date, hours); + case 'onlyHoursMinutes': + // 纯时分格式下,结束具体日期时间到分 + return new Date(year, month, date, hours, minutes); + default: + // 正常情况下,结束时间值具体到秒 + return new Date(year, month, date, hours, minutes, seconds); + } + } + } + + /** + * @description 获取当前合法的季度值并转化为对应的日期 + * + * @param value 季度值 + * @param isEnd 是否是结束日期 + */ + public static getValidQuarterDate(value: string, isEnd?: boolean): Date { + const quarters: Array = ['Q1', 'Q2', 'Q3', 'Q4']; + const reg: RegExp = /[年月日\-/._]/; + const dateArr: Array = value.split(reg); + const index: number = quarters.findIndex((item: string) => item === dateArr[1].toUpperCase()); + if (dateArr.length < 2 || index === -1) { + return; + } + + return TiDateUtil.transFormQuarterToDate(Number(dateArr[0]), dateArr[1].toUpperCase(), isEnd); + } + + /** + * @description 判断是否为禁用日期 + * + * @param disabledDays 禁用的日期 + * @param value 日期值 + */ + public static isDisabledDays(disabledDays: Array, value: Date): boolean { + if (!Array.isArray(disabledDays)) { + return false; + } + + return disabledDays.some((item: Date) => item.getTime() === value.getTime()); + } + + /** + * @description 根据时区获取当前选中本地时间 + * + * @param dateString 当前选择的时间 + * @param timeZoneable 时区选择开关 + * @param selectedOption 时区选择项 + */ + public static getLocalDate(dateString: string, timeZoneable: boolean, selectedOption: string): Date { + if (dateString === 'null') { + return null; + } + + // 将UTC时间转化为本地时间 + return timeZoneable && selectedOption === 'UTC/GMT' ? new Date(`${dateString} UTC`) : new Date(`${dateString}`); + } + + /** + * @description 根据时区获取当前选中UTC时间(此处的UTC时间指的并不是实际意义上的UTC时间,而是根据UTC时间转化为对应的本地时间呈现给用户) + * + * @param date 当前选择的时间 + * @param timeZoneable 时区选择开关 + * @param selectedOption 时区选择项 + */ + public static getUtcDate(date: Date, timeZoneable: boolean, selectedOption: string): Date { + if (date === null) { + return null; + } + + // 将UTC时间转化为new Date()形式时间格式 + // getTimezoneOffset() 返回 UTC 时间与本地时间之间的时差,以分钟为单位。 + return timeZoneable && selectedOption === 'UTC/GMT' ? new Date(date.getTime() + date.getTimezoneOffset() * 60 * 1000) : date; + } +} diff --git a/src/utils/lib/src/TiKeymap.ts b/src/utils/lib/src/TiKeymap.ts new file mode 100644 index 0000000..f92df46 --- /dev/null +++ b/src/utils/lib/src/TiKeymap.ts @@ -0,0 +1,381 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiBrowser } from './TiBrowser'; + +// 另,Angular官方键值调用方法: + +/* 使用键值请参考: +https://blog.csdn.net/q1054261752/article/details/50359617 +http://www.runoob.com/try/try.php?filename=tryjsref_event_key_keycode2 +https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent +事件顺序:onkeydown > onkeypress >onkeyup +早期标准: +keydown和keyup捕获具体物理按键,event.keyCode输出键值,可以区分主副键盘的具体按键; +keypress捕获按键经过处理后的字符,event.charCode输出ASCII字符编码,可以区分大小写字母,主副键盘相同符号取值相同。 +event.which根据情况输出charCode(keypress时)或keyCode(keydown时)。 +IE8只有keyCode属性,IE9起已支持which和charCode。 +Chrome和IE等,在keypress时,keyCode也存放了charCode值。(是为了兼容只有keyCode属性的IE8) +FireFox,keypress只输出event.charCode,keydown只输出event.keycode;另一个值为0。 +现今情况: +keyCode/charCode/which都已废弃,但keyCode依然是目前最好选择!(2018年4月,须兼容IE9) +未来情况: +keypress和keydown时,都可用event.key("z")和event.code("KeyZ"),是推荐标准,都是字符串可读性值。 +IE9不支持event.code。 +IE9的event.key是旧标准。小键盘符号支持很差:和大键盘取值不同,且keypress和keydown时取值不同。有时符号,有时符号名称。 + +综上:建议只使用keydow和keyup事件和event.keyCode键值,不使用keypress/which/charCode +很少有需求使用keypress,仅配合 event.charCode取出字符编码即可。 +再配合'A'.charCodeAt(0)===event.charCode或者'A'===String.fromCharCode(event.charCode); +*/ +/* HTML W3C标准键值查找类,已经屏蔽浏览器差异 + * + * 参考文档: + * https://www.w3.org/TR/uievents/ + * + * https://www.w3.org/TR/uievents-code/ + * + * https://www.w3.org/TR/uievents-key + * + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code + * + * https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button + * + * https://github.com/kabirbaidhya/keycode-js 这份代码采用了Mac命名方式,比如回车键叫Return + * + * https://github.com/wesbos/keycodes/blob/gh-pages/scripts.js + * + * http://blog.csdn.net/joeblackzqq/article/details/46442121 + * + * + * W3C规定了一些固定键值,其他键值可能随硬件(PC/Mac)、操作系统、浏览器而不同。 + * + * 命名规范统一按照W3C名称,已涵盖标准101键盘,其他键盘暂未涵盖。 + * + * js中左右Ctrl、大小键盘Enter是同一键值,所以比windows编程环境的键值要少一些。 + * + * 多余Mac键盘值已删除。 + * + * 标点符号,因为Firefox与其他浏览器有三个不同,已做兼容性处理 + */ + +export class TiKeymap { + /* 此部分为W3C规定的固定键值 start↓ */ + /** + * Mac称作Delete 【注意: Backspace 8/Delete 46(windows用后向删除)是两个不同键值。】 + */ + public static readonly KEY_BACKSPACE: number = 8; + /** + * Table键 + */ + public static readonly KEY_TAB: number = 9; + /** + * 回车键,数字小键盘回车也是13。 Mac称作Return。【注意:108是Mac数字小键盘Enter】 + */ + public static readonly KEY_ENTER: number = 13; + /** + * 左右Shift + */ + public static readonly KEY_SHIFT: number = 16; + /** + * 左右Ctrl + */ + public static readonly KEY_CONTROL: number = 17; + /** + * 左右Alt Mac称作Option + */ + public static readonly KEY_ALT: number = 18; + /** + * W3C未规定固定或者不固定,是PC(非Mac)独有 + */ + public static readonly KEY_PAUSE_BREAK: number = 19; + /** + * 大小写锁定 + */ + public static readonly KEY_CAPS_LOCK: number = 20; + /** + * 左上角Esc + */ + public static readonly KEY_ESCAPE: number = 27; + public static readonly KEY_SPACE: number = 32; + public static readonly KEY_PAGE_UP: number = 33; + public static readonly KEY_PAGE_DOWN: number = 34; + public static readonly KEY_END: number = 35; + public static readonly KEY_HOME: number = 36; + /** + * 箭头方向键 + */ + public static readonly KEY_ARROW_LEFT: number = 37; + public static readonly KEY_ARROW_UP: number = 38; + public static readonly KEY_ARROW_RIGHT: number = 39; + public static readonly KEY_ARROW_DOWN: number = 40; + /** + * W3C未规定固定或非固定,是PC(非Mac)独有 + */ + public static readonly KEY_PRINT_SCREEN: number = 44; // W3C未规定固定或非固定,是PC(非Mac)独有 + /** + * W3C未规定固定或非固定,是PC(非Mac)独有 + */ + public static readonly KEY_INSERT: number = 45; // W3C未规定固定或非固定,是PC(非Mac)独有 + /** + * 后向删除 + */ + public static readonly KEY_DELETE: number = 46; + public static readonly KEY_0: number = 48; + public static readonly KEY_1: number = 49; + public static readonly KEY_2: number = 50; + public static readonly KEY_3: number = 51; + public static readonly KEY_4: number = 52; + public static readonly KEY_5: number = 53; + public static readonly KEY_6: number = 54; + public static readonly KEY_7: number = 55; + public static readonly KEY_8: number = 56; + public static readonly KEY_9: number = 57; + public static readonly KEY_A: number = 65; + public static readonly KEY_B: number = 66; + public static readonly KEY_C: number = 67; + public static readonly KEY_D: number = 68; + public static readonly KEY_E: number = 69; + public static readonly KEY_F: number = 70; + public static readonly KEY_G: number = 71; + public static readonly KEY_H: number = 72; + public static readonly KEY_I: number = 73; + public static readonly KEY_J: number = 74; + public static readonly KEY_K: number = 75; + public static readonly KEY_L: number = 76; + public static readonly KEY_M: number = 77; + public static readonly KEY_N: number = 78; + public static readonly KEY_O: number = 79; + public static readonly KEY_P: number = 80; + public static readonly KEY_Q: number = 81; + public static readonly KEY_R: number = 82; + public static readonly KEY_S: number = 83; + public static readonly KEY_T: number = 84; + public static readonly KEY_U: number = 85; + public static readonly KEY_V: number = 86; + public static readonly KEY_W: number = 87; + public static readonly KEY_X: number = 88; + public static readonly KEY_Y: number = 89; + public static readonly KEY_Z: number = 90; + /* 此部分为W3C规定的固定键值 end↑ */ + /** + * "Windows Key Left/ Left Command⌘ (Mac)/ Chromebook Search key"; + */ + public static readonly KEY_META_LEFT: number = 91; + /** + * "Windows Key Right + */ + public static readonly KEY_META_RIGHT: number = 92; + /** + * "Windows Menu / Right Command⌘(Mac)"; + */ + public static readonly KEY_CONTEXT_MENU: number = 93; + public static readonly KEY_NUMPAD_0: number = 96; + public static readonly KEY_NUMPAD_1: number = 97; + public static readonly KEY_NUMPAD_2: number = 98; + public static readonly KEY_NUMPAD_3: number = 99; + public static readonly KEY_NUMPAD_4: number = 100; + public static readonly KEY_NUMPAD_5: number = 101; + public static readonly KEY_NUMPAD_6: number = 102; + public static readonly KEY_NUMPAD_7: number = 103; + public static readonly KEY_NUMPAD_8: number = 104; + public static readonly KEY_NUMPAD_9: number = 105; + /** + * 小键盘* + */ + public static readonly KEY_NUMPAD_MULTIPLY: number = 106; + /** + * 小键盘+ + */ + public static readonly KEY_NUMPAD_ADD: number = 107; + /** + * Mac小键盘Enter。可能某些键盘除法符号。且这个键值可能在Firefox与其他浏览器不同。 + */ + public static readonly KEY_NUMPAD_ENTER: number = 108; + /** + * 小键盘- + */ + public static readonly KEY_NUMPAD_SUBTRACT: number = 109; + /** + * 小键盘. (小数点) + */ + public static readonly KEY_NUMPAD_DECIMAL: number = 110; + /** + * 小键盘/ + */ + public static readonly KEY_NUMPAD_DIVIDE: number = 111; + public static readonly KEY_F1: number = 112; + public static readonly KEY_F2: number = 113; + public static readonly KEY_F3: number = 114; + public static readonly KEY_F4: number = 115; + public static readonly KEY_F5: number = 116; + public static readonly KEY_F6: number = 117; + public static readonly KEY_F7: number = 118; + public static readonly KEY_F8: number = 119; + public static readonly KEY_F9: number = 120; + public static readonly KEY_F10: number = 121; + public static readonly KEY_F11: number = 122; + public static readonly KEY_F12: number = 123; + public static readonly KEY_F13: number = 124; + public static readonly KEY_F14: number = 125; + public static readonly KEY_F15: number = 126; + public static readonly KEY_F16: number = 127; + public static readonly KEY_F17: number = 128; + public static readonly KEY_F18: number = 129; + public static readonly KEY_F19: number = 130; + public static readonly KEY_F20: number = 131; + public static readonly KEY_F21: number = 132; + public static readonly KEY_F22: number = 133; + public static readonly KEY_F23: number = 134; + public static readonly KEY_F24: number = 135; + /** + * W3C未规定固定或不固定,是PC(非Mac)独有。 在Mac上这个键值给numpad_clear用了。 + */ + public static readonly KEY_NUM_LOCK: number = 144; + /** + * W3C未规定固定或不固定,是PC(非Mac)独有 + */ + public static readonly KEY_SCROLL_LOCK: number = 145; + /* 下面是W3C符号美标键值,不固定。Firefox符号键值有3个与其他浏览器不同 */ + /** + * ";" Firefox键值是59,使用时已经屏蔽差异 + */ + public static KEY_SEMICOLON: number = 186; + /** + * ":" Firefox键值是59,使用时已经屏蔽差异 + */ + public static KEY_COLON: number = TiKeymap.KEY_SEMICOLON; + /** + * "=" Firefox键值是61,使用时已经屏蔽差异 + */ + public static KEY_EQUALS_SIGN: number = 187; + /** + * "+" Firefox键值是61,使用时已经屏蔽差异 + */ + public static KEY_PLUS: number = TiKeymap.KEY_EQUALS_SIGN; + /** + * "-" Firefox键值是173,使用时已经屏蔽差异 + */ + public static KEY_MINUS: number = 189; + /** + * "_" Firefox键值是173,使用时已经屏蔽差异 + */ + public static KEY_UNDERSCORE: number = TiKeymap.KEY_MINUS; + /** + * "," + */ + public static readonly KEY_COMMA: number = 188; + /** + * "<" + */ + public static readonly KEY_LESS_THAN_SIGN: number = 188; + /** + * "." + */ + public static readonly KEY_PERIOD: number = 190; + /** + * ">" + */ + public static readonly KEY_GREATER_THAN_SIGN: number = 190; + /** + * "/" + */ + public static readonly KEY_FORWARD_SLASH: number = 191; + /** + * "?" + */ + public static readonly KEY_QUESTION_MARK: number = 191; + /** + * "`" + */ + public static readonly KEY_BACKTICK: number = 192; + /** + * "~" + */ + public static readonly KEY_TILDE: number = 192; + /** + * "[" + */ + public static readonly KEY_OPENING_SQUACE_BRACKET: number = 219; + /** + * "{" + */ + public static readonly KEY_OPENING_CURLY_BRACE: number = 219; + /** + * "\" + */ + public static readonly KEY_BACKSLASH: number = 220; + /** + * "|" + */ + public static readonly KEY_PIPE: number = 220; + /** + * "]" + */ + public static readonly KEY_CLOSING_SQUARE_BRACKET: number = 221; + /** + * "}" + */ + public static readonly KEY_CLOSING_CURLY_BRACE: number = 221; + /** + * "'" + */ + public static readonly KEY_SINGLE_QUOTE: number = 222; + /** + * """ + */ + public static readonly KEY_DOUBLE_QUOTE: number = 222; + + /* 鼠标键值,前三个为W3C固定,命名暂时按照程序员习惯。 */ + /** + * 鼠标键值:primary button + */ + public static readonly MOUSE_LEFT_BUTTON: number = 0; // primary button + /** + * 鼠标键值:auxiliary button wheel button + */ + public static readonly MOUSE_MIDDLE_BUTTON: number = 1; // auxiliary button wheel button + /** + * 鼠标键值:secondary button + */ + public static readonly MOUSE_RIGHT_BUTTON: number = 2; // secondary button + /** + * 鼠标键值:浏览器后退 + */ + public static readonly MOUSE_BACK_BUTTON: number = 3; // 浏览器后退 + /** + * 鼠标键值:浏览器前进 + */ + public static readonly MOUSE_FORWARD_BUTTON: number = 4; // 浏览器前进 + /** + * Typescript没有静态代码段,所以这样代替静态代码段 + */ + protected static staticCode: void = TiKeymap.initFirefox(); + /* Firefox符号键值有3个与其他浏览器不同 + Semicolon ";" 186 //Firefox 59 + Colon ":" 186 //Firefox 59 + Equals sign "=" 187 //Firefox 61 + Plus "+" 187 //Firefox 61 + Minus "-" 189 //Firefox 173 + Underscore "_" 189 //Firefox 173 + */ + private static initFirefox(): void { + if (TiBrowser.isFirefox()) { + TiKeymap.KEY_SEMICOLON = 59; // ";" + TiKeymap.KEY_COLON = TiKeymap.KEY_SEMICOLON; // ":" + TiKeymap.KEY_EQUALS_SIGN = 61; // "=" + TiKeymap.KEY_PLUS = TiKeymap.KEY_EQUALS_SIGN; // "+" + TiKeymap.KEY_MINUS = 173; // "-" + TiKeymap.KEY_UNDERSCORE = TiKeymap.KEY_MINUS; // "_" + } + } +} +// 因为Typescript没有类的静态代码段。所以对类的static变量初始化,写在这里。 diff --git a/src/utils/lib/src/TiLog.ts b/src/utils/lib/src/TiLog.ts new file mode 100644 index 0000000..0ee8af0 --- /dev/null +++ b/src/utils/lib/src/TiLog.ts @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/* + * Logger工具类提供全局日志输出级别控制。 + * + * 提供三个静态日志方法,log()/warn()/error() + * + * log()是调试期日志,产品时一定关闭。组件对打包对外提供时也关闭,不干扰用户开发。 + * + * info()是产品期也可以输出的重要日志。暂不提供。 + * + * warn()/error()产品期,可酌情打开。后期可以考虑,error日志回传给服务器。 + * + */ + +export class TiLog { + /** + * 输出 Log 以上日志,包含 Log/Warn/Error,log 表示一般的调试和运行信息。 + */ + public static readonly LEVEL_LOG: number = 1; + /** + * 输出 Warn 以上日志,包含 Warn/Error 日志,Warn 表示会出现潜在错误的提示。 + */ + public static readonly LEVEL_WARN: number = 2; + /** + * 输出 Error 日志,Error 表示发生错误事件,已经影响系统的正常运行。 + */ + public static readonly LEVEL_ERROR: number = 3; + /** + * 关闭输出所有日志 + */ + public static readonly LEVEL_OFF: number = 10; + + private static level: number = TiLog.LEVEL_OFF; + /** + * 设置输出日志级别:LOG/WARN/ERROR/OFF. 默认是 OFF + */ + public static setLevel(level: number): void { + TiLog.level = level; + } + /** + * 打印一般的调试和运行信息 + * @param [message] 信息 + * @param optionalParams 参数 + */ + public static log(message?: any, ...optionalParams: Array): void { + if (TiLog.LEVEL_LOG >= TiLog.level && console.log) { + console.log(message, ...optionalParams); + } + } + + /** + * 打印会出现潜在错误的提示 + * @param [message] 信息 + * @param optionalParams 参数 + */ + public static warn(message?: any, ...optionalParams: Array): void { + if (TiLog.LEVEL_WARN >= TiLog.level && console.warn) { + console.warn(message, ...optionalParams); + } + } + /** + * 打印在发生错误,已经影响系统的正常运行的信息 + * @param [message] 信息 + * @param optionalParams 参数 + */ + public static error(message?: any, ...optionalParams: Array): void { + if (TiLog.LEVEL_ERROR >= TiLog.level && console.error) { + console.error(message, ...optionalParams); + } + } +} diff --git a/src/utils/lib/src/TiTheme.ts b/src/utils/lib/src/TiTheme.ts new file mode 100644 index 0000000..8ce6648 --- /dev/null +++ b/src/utils/lib/src/TiTheme.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +export class TiTheme { + /** + * 加载CSS文件,挂在head link + * @param cssPath css路径 + * @param id link id + */ + public static loadCss(cssPath: string, id: string): HTMLLinkElement { + let link: HTMLLinkElement = document.getElementById(id) as HTMLLinkElement; + + // 原本没有link,那么创建link + if (!link) { + link = document.createElement('link'); + document.head.appendChild(link); + } + // 如果link href没变,那么就不处理了。 + if (link.href === cssPath) { + return; + } + link.href = cssPath; + link.id = id; + link.rel = 'stylesheet'; + link.type = 'text/css'; + + // 被CSSVar补丁处理后,补丁会给link打上disabled。去除disabled,才能再次补丁生效。 + if (link.hasAttribute('disabled')) { + link.removeAttribute('disabled'); + } + + return link; + } + /** + * 检测浏览器是否原生支持CSSVar + * @returns 是否支持CSSVar + */ + public static isNativeSupportCssVar(): boolean { + const isBrowser: boolean = typeof window !== 'undefined'; + const isNativeSupport: boolean = isBrowser && window.CSS && window.CSS.supports && window.CSS.supports('(--a: 0)'); + + return isNativeSupport; + } + /** + * 启动AppModule + * @param app 传入的AppModule + */ + public static bootstrapModule(app: any): void { + platformBrowserDynamic() + .bootstrapModule(app) + .catch((err: any) => console.error(err)); + } +} diff --git a/src/utils/lib/src/Util.ts b/src/utils/lib/src/Util.ts new file mode 100644 index 0000000..80fe772 --- /dev/null +++ b/src/utils/lib/src/Util.ts @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * @ignore + * 工具类 + */ +export class Util { + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + private static idCounter: number = 0; + /** + * 获得一个唯一标示 + * @param prefix 前缀 + * @returns 唯一标示字符串 + */ + public static getUniqueId(prefix: string): string { + Util.idCounter++; + const id: string = `${prefix}_${Util.idCounter}`; + + return id; + } + // TODO: 是否可以去除? + /** + * 是否未定义 + * @param value 值 + * @returns 是否 + */ + public static isUndefined(value: any): boolean { + return value === undefined; + } + // TODO: 是否可以去除? + /** + * 是否空 + * @param value 值 + * @returns 是否 + */ + public static isNull(value: any): boolean { + return value === null; + } + // TODO: 是否可以去除? + /** + * 是否String + * @param value 值 + * @returns 是否 + */ + public static isString(value: any): boolean { + return typeof value === 'string'; + } + // TODO: 是否可以去除? + /** + * 是否Number + * @param value 值 + * @returns 是否 + */ + public static isNumber(value: any): boolean { + return typeof value === 'number' && !isNaN(value); + } + + /** + * 是否空字符串 + * @param value 值 + * @returns 是否 + */ + public static isEmptyString(value: string): boolean { + return value === undefined || value === null || value === ''; + } + // TODO: 是否可以去除 + /** + * 是否Array + * @param value 值 + * @returns 是否 + */ + public static isArray(value: any): boolean { + return Array.isArray(value); + } + // TODO: 是否可以去除 + /** + * 是否函数 + * @param value 值 + * @returns 是否 + */ + public static isFunction(value: any): boolean { + return typeof value === 'function'; + } + + /** + * 使用固定参数值格式化填充字串 + * @param source 源字串,其中使用{N}代表需要匹配的参数次序,N从0开始 + * @param params Array 参数数组 + */ + public static formatEntry(source: string, params: Array): string { + let formatSource: string = source; + if (!this.isArray(params) || formatSource === '') { + return formatSource; + } + params.forEach((param: any, i: number) => { + formatSource = formatSource.replace(new RegExp(`\\{${i}\\}`, 'g'), () => { + return param; + }); + }); + + return formatSource; + } + + /** + * 发出一个HTML事件 + * @param: ele 触发事件的DOM + * @param: eventName 事件名称 + * @param: [canBubble] 是否冒泡,可选参数默认不冒泡 false + */ + public static trigger(ele: any, eventName: string, canBubble?: boolean): void { + // 修复SSR错误:ERROR ReferenceError: document is not defined + if (typeof document === 'undefined') { + return; + } + const event: Event = document.createEvent('HTMLEvents'); + event.initEvent(eventName, canBubble || false, true); + // event.initEvent(eventType,canBubble,cancelable) + ele?.dispatchEvent(event); + } + /** + * 判断是否为dom元素 + */ + public static isElement(ele: any): boolean { + if (ele && ele.nodeType) { + return ele.nodeType === 1; + } else { + return false; + } + } + /** + * 判断是否支持CSS Var + */ + public static supportsCssVars(): boolean { + return typeof window !== 'undefined' && (window as any).CSS && (window as any).CSS.supports && (window as any).CSS.supports('(--a: 0)'); + } + /** + * 通过tab键在弹窗内切换的时候,焦点需要一直在弹窗内部循环。 + */ + public static focusInDialogOnTabchange(event: KeyboardEvent, focusableElements: NodeList): void { + if (!focusableElements || focusableElements.length === 0) { + return; + } + const firstFocusableEle: any = focusableElements[0]; + const lastFocusableEle: any = focusableElements[focusableElements.length - 1]; + const target: EventTarget = event.target; // 获得触发事件的元素 + + // 按下tab+shift键时,如果当前已获取焦点元素是弹出框中的第一个可获取焦点元素,则聚焦最后一个元素 + if (event.shiftKey) { + if (target === firstFocusableEle) { + lastFocusableEle.focus(); + event.preventDefault(); // 阻止默认事件,确保此处手动focus生效 + } + } else if (target === lastFocusableEle) { + // 按下tab键时,如当前已获取焦点元素是最后一个可获取焦点元素,则聚焦第一个元素 + firstFocusableEle.focus(); + event.preventDefault(); // 阻止默认事件,确保此处手动focus生效 + } + } +} diff --git a/src/validation/demo/karma.conf.js b/src/validation/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/validation/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/validation/demo/project.json b/src/validation/demo/project.json new file mode 100644 index 0000000..a7f1e24 --- /dev/null +++ b/src/validation/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/validation/demo", + "sourceRoot": "src/validation/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/validation", + "index": "src/validation/demo/src/index.html", + "main": "src/validation/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/validation/demo/tsconfig.app.json", + "assets": ["src/validation/demo/src/favicon.ico", "src/validation/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "validation-demo:build:production" + }, + "development": { + "browserTarget": "validation-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js validation" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/validation/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/validation/demo/tsconfig.spec.json", + "karmaConfig": "src/validation/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/validation/demo/src/app/AppComponent.ts b/src/validation/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/validation/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/validation/demo/src/app/AppModule.ts b/src/validation/demo/src/app/AppModule.ts new file mode 100644 index 0000000..34d499d --- /dev/null +++ b/src/validation/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ValidationTestModule } from './validation/ValidationTestModule'; + +@NgModule({ + imports: [ + ValidationTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/validation/demo/src/app/IndexComponent.ts b/src/validation/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..289595c --- /dev/null +++ b/src/validation/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ValidationTestModule } from './validation/ValidationTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = ValidationTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/validation/demo/src/app/app.html b/src/validation/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/validation/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/validation/demo/src/app/validation/ValidationAsyncCheckComponent.ts b/src/validation/demo/src/app/validation/ValidationAsyncCheckComponent.ts new file mode 100644 index 0000000..6b76777 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationAsyncCheckComponent.ts @@ -0,0 +1,53 @@ +import { Component } from '@angular/core'; +import { AsyncValidatorFn, AbstractControl, FormControl, ValidationErrors } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { debounceTime, delay, distinctUntilChanged, first, map, catchError, switchMap } from 'rxjs/operators'; +import { TiValidators, TiValidationDirective } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-async-check.html' +}) +export class ValidationAsyncCheckComponent { + asyncControl: FormControl = new FormControl('', null, CustomAsyncValidators.isRightUserName()); + + mixControl: FormControl = new FormControl( + '', + [TiValidators.required, TiValidators.minLength(5)], + CustomAsyncValidators.isRightUserName() + ); +} + +export class CustomAsyncValidators { + static isRightUserName(): AsyncValidatorFn { + return (control: AbstractControl): Promise | Observable => { + if (control.valueChanges) { + return control.valueChanges.pipe( + debounceTime(TiValidationDirective.ASYNC_DEBOUNCE_TIME), + distinctUntilChanged(), + switchMap((value: string) => CustomAsyncValidators.isRight(value)), + map((isRight: boolean) => { + return isRight + ? { + rightUserName: { + actualValue: control.value, + tiAsyncErrorMessage: '用户名 {0} 不正确' + } + } + : null; + }), + first(), + catchError(() => null) + ); + } else { + return of(null); + } + }; + } + + static isRight(value: string): Observable { + return of(value !== 'hello tiny').pipe( + delay(2000), + catchError(() => of(false)) + ); + } +} diff --git a/src/validation/demo/src/app/validation/ValidationAsyncCheckTestComponent.ts b/src/validation/demo/src/app/validation/ValidationAsyncCheckTestComponent.ts new file mode 100644 index 0000000..6150d09 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationAsyncCheckTestComponent.ts @@ -0,0 +1,58 @@ +import { Component } from '@angular/core'; +import { AsyncValidatorFn, AbstractControl, FormControl, ValidationErrors } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { debounceTime, delay, distinctUntilChanged, first, map, catchError, switchMap } from 'rxjs/operators'; +import { TiValidators, TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-async-check-test.html' +}) +export class ValidationAsyncCheckTestComponent { + label: string = '用户名:'; + ccControl: FormControl = new FormControl('', [TiValidators.required, TiValidators.minLength(5)], CustomAsyncValidators.isRightUserName()); + ccControl1: FormControl = new FormControl( + '', + [TiValidators.required, TiValidators.minLength(5)], + CustomAsyncValidators.isRightUserName() + ); +} + +export class CustomAsyncValidators { + // 自定义异步校验规则 + static isRightUserName(): AsyncValidatorFn { + return (control: AbstractControl): Promise | Observable => { + if (control.valueChanges) { + // 初始时control中可能没有valueChanges属性 + return control.valueChanges.pipe( + debounceTime(500), // 防抖处理(输入停顿后再进行校验) + distinctUntilChanged(), // 防止对前后相同的值进行校验 + switchMap((value: string) => CustomAsyncValidators.isRight(value)), // 进行后台请求校验 + map((isRight: boolean) => { + // 拿到后台返回值 + // 异步校验需要在校验错误信息中通过 tiAsyncErrorMessage 属性来设置校验错误提示信息 + return isRight + ? { + rightUserName: { + actualValue: control.value, + tiAsyncErrorMessage: '用户名 {0} 不正确' + } + } + : null; + }), + first(), // complete control.valueChanges + catchError(() => null) + ); + } else { + return of(null); + } + }; + } + + // 模拟后台异步请求 + static isRight(value: string): Observable { + return of(value !== 'happy').pipe( + delay(2000), + catchError(() => of(false)) + ); + } +} diff --git a/src/validation/demo/src/app/validation/ValidationBasicControlComponent.ts b/src/validation/demo/src/app/validation/ValidationBasicControlComponent.ts new file mode 100644 index 0000000..de5452b --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationBasicControlComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-basic-control.html' +}) +export class ValidationBasicControlComponent { + customFormControl: FormControl = new FormControl('hello tiny', [TiValidators.required, TiValidators.equal('hello tiny')]); +} diff --git a/src/validation/demo/src/app/validation/ValidationBasicDirectiveComponent.ts b/src/validation/demo/src/app/validation/ValidationBasicDirectiveComponent.ts new file mode 100644 index 0000000..a5e40dd --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationBasicDirectiveComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './validation-basic-directive.html' +}) +export class ValidationBasicDirectiveComponent { + templateRequiredValue: string = ''; + templateEqualValue: string = 'hello tiny'; +} diff --git a/src/validation/demo/src/app/validation/ValidationBlurCheckComponent.ts b/src/validation/demo/src/app/validation/ValidationBlurCheckComponent.ts new file mode 100644 index 0000000..2615742 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationBlurCheckComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-blur-check.html' +}) +export class ValidationBlurCheckComponent { + inputValue: string; + typeConfig: TiValidationConfig = { + type: 'blur' + }; +} diff --git a/src/validation/demo/src/app/validation/ValidationErrorMsgComponent.ts b/src/validation/demo/src/app/validation/ValidationErrorMsgComponent.ts new file mode 100644 index 0000000..5002917 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationErrorMsgComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-error-msg.html' +}) +export class ValidationErrorMsgComponent { + errorMsgBaseValue: string = 'hello'; + errorMsgMultirowValue: string = 'hello'; + errorMsgClearValue: string = 'hello'; + + errorMsgBaseConfig: TiValidationConfig = { + type: 'blur', + errorMessage: { + equal: 'not equal to {0}' + } + }; + errorMsgMultirowConfig: TiValidationConfig = { + errorMessage: { + equal: '输入值
    应该为{0}。' + } + }; + errorMsgClearConfig: TiValidationConfig = { + type: 'blur', + errorMessage: { + equal: '' + } + }; +} diff --git a/src/validation/demo/src/app/validation/ValidationFormGroupComponent.ts b/src/validation/demo/src/app/validation/ValidationFormGroupComponent.ts new file mode 100644 index 0000000..3e914e0 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationFormGroupComponent.ts @@ -0,0 +1,150 @@ +import { Component, ElementRef, Input } from '@angular/core'; +import { AbstractControl, AsyncValidatorFn, FormBuilder, FormControl, FormGroup, ValidationErrors } from '@angular/forms'; +import { Observable, of, Subscription, zip } from 'rxjs'; +import { catchError, debounceTime, delay, distinctUntilChanged, first, map, switchMap } from 'rxjs/operators'; +import { TiValidationDirective, TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-form-group.html' +}) +export class ValidationFormGroupComponent { + countries: Array = [ + { + label: '中国', + englishname: 'China' + }, + { + label: '美国', + englishname: 'America' + }, + { + label: '加拿大', + englishname: 'Canada' + } + ]; + groupFormControl: FormGroup; + showRequiredController: boolean = true; + templateEqualValue: string = 'hello tiny'; + templateRequiredValue: string = ''; + defaultEmail: string = 'hello@example.com'; + asyncFormGroup: FormGroup; + constructor(builder: FormBuilder, private elementRef: ElementRef) { + this.groupFormControl = builder.group({ + emailController: new FormControl('hello tiny', [TiValidators.email, TiValidators.required]), + requiredController: new FormControl('', [TiValidators.required]), + selectControllor: new FormControl('', [TiValidators.required]) + }); + this.asyncFormGroup = builder.group({ + syncInput: new FormControl('', [TiValidators.required]), + asyncInput: new FormControl('', null, CustomAsyncValidators.isRightUserName(2000)) + }); + } + checkGroupFormControl(): void { + const errors: ValidationErrors | null = TiValidators.check(this.groupFormControl); + if (errors) { + const firstError: any = Object.keys(errors)[0]; + this.elementRef.nativeElement.querySelector(`[formControlName=${firstError}]`).focus(); + } + } + hideAndCheck(): void { + this.showRequiredController = false; + this.groupFormControl.removeControl('requiredController'); + } + resetGroup(): void { + this.groupFormControl.reset({ + emailController: 'hello tiny', + requiredController: '', + selectControllor: '' + }); + } + selectDisabled(): void { + if (this.groupFormControl.controls.selectControllor.disabled) { + this.groupFormControl.controls.selectControllor.enable(); + } else { + this.groupFormControl.controls.selectControllor.disable(); + } + } + + checkTemplateForm(form: FormGroup): void { + const errors: ValidationErrors | null = TiValidators.check(form); + console.log('errors', errors); + + // 整体校验后如果需要聚焦到第一个校验不通过元素,请参考以下代码 + if (errors) { + // 注意:要保证fb.group时各个FormControl的顺序与对应表单元素dom放置顺序一致 + const firstError: any = Object.keys(errors)[0]; + this.elementRef.nativeElement.querySelector(`[name=${firstError}]`).focus(); + } + } + + checkAsyncForm(): void { + let errors: ValidationErrors | null = TiValidators.check(this.asyncFormGroup); + const pendingStatusChangesArray: Array = []; + const pendingControlsArray: Array = []; + for (const key in this.asyncFormGroup.controls) { + if (Object.prototype.hasOwnProperty.call(this.asyncFormGroup.controls, key)) { + const control: AbstractControl = this.asyncFormGroup.controls[key]; + if (control.pending) { + pendingControlsArray.push({ name: key, control: control }); + pendingStatusChangesArray.push(control.statusChanges); + } + } + } + if (pendingStatusChangesArray.length > 0) { + const pendingStatusChanges: Subscription = zip(...pendingStatusChangesArray).subscribe((resultArray: Array) => { + resultArray.forEach((result: string, index: number) => { + if (result === 'INVALID') { + if (errors === null) { + errors = {}; + } + errors[pendingControlsArray[index].name] = pendingControlsArray[index].control.errors; + } + }); + pendingStatusChanges.unsubscribe(); + if (errors) { + const firstError: any = Object.keys(errors)[0]; + this.elementRef.nativeElement.querySelector(`[formControlName=${firstError}]`).focus(); + } + }); + } + } + + setDefaultEmail(): void { + this.groupFormControl.controls.emailController.setValue(this.defaultEmail); + } +} + +export class CustomAsyncValidators { + static isRightUserName(delay: number): AsyncValidatorFn { + return (control: AbstractControl): Promise | Observable => { + if (control.valueChanges) { + return control.valueChanges.pipe( + debounceTime(TiValidationDirective.ASYNC_DEBOUNCE_TIME), + distinctUntilChanged(), + switchMap((value: string) => CustomAsyncValidators.isRight(value, delay)), + map((isRight: boolean) => { + return isRight + ? { + rightUserName: { + actualValue: control.value, + tiAsyncErrorMessage: '用户名 {0} 不正确' + } + } + : null; + }), + first(), + catchError(() => null) + ); + } else { + return of(null); + } + }; + } + + static isRight(value: string, time: number): Observable { + return of(value !== 'tiny').pipe( + delay(time), + catchError(() => of(false)) + ); + } +} diff --git a/src/validation/demo/src/app/validation/ValidationFormGroupConfigComponent.ts b/src/validation/demo/src/app/validation/ValidationFormGroupConfigComponent.ts new file mode 100644 index 0000000..a5a9271 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationFormGroupConfigComponent.ts @@ -0,0 +1,60 @@ +import { Component, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, ValidationErrors } from '@angular/forms'; +import { TiValidationConfig, TiValidationCheckConfig, TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-form-group-config.html' +}) +export class ValidationFormGroupConfigComponent { + myLogs: Array = []; + groupFormControl: FormGroup; + pwdConfig: TiValidationConfig = { + type: 'password' + }; + constructor(fb: FormBuilder, private elementRef: ElementRef) { + this.groupFormControl = fb.group({ + pwdInput: new FormControl('', [ + TiValidators.password({ + notEqualPosRev: () => { + return this.groupFormControl.get('emailInput'); + } + }) + ]), + emailInput: new FormControl('hello', [TiValidators.email]), + nicknameInput: new FormControl('', [TiValidators.required]), + childGroupFormControl: new FormGroup({ + usernameInput: new FormControl('', [TiValidators.required]) + }) + }); + } + checkGroup(): void { + const errors: ValidationErrors | null = TiValidators.check(this.groupFormControl); + this.myLogs = [...this.myLogs, `整体校验结果:${JSON.stringify(errors)}`]; + if (errors) { + const firstError: any = Object.keys(errors)[0]; + this.elementRef.nativeElement.querySelector(`[formControlName=${firstError}]`).focus(); + } + } + checkGroupWithConfig(): void { + const config: TiValidationCheckConfig = { + emitEvent: false, + ignoreNames: ['pwdInput'], + onlySelf: true, + errorsFlatted: false + }; + const errors: ValidationErrors | null = TiValidators.check(this.groupFormControl, config); + if (errors) { + this.myLogs = [...this.myLogs, `屏蔽密码输入框的校验结果:${JSON.stringify(errors)}`]; + const firstError: any = Object.keys(errors)[0]; + this.elementRef.nativeElement.querySelector(`[formControlName=${firstError}]`).focus(); + } + } + checkGroupWithErrorsFlatted(): void { + const errors: ValidationErrors | null = TiValidators.check(this.groupFormControl, { errorsFlatted: true }); + this.myLogs = [...this.myLogs, `校验结果扁平化结构:${JSON.stringify(errors)}`]; + if (errors) { + const firstError: any = Object.keys(errors)[0]; + this.elementRef.nativeElement.querySelector(`[formControlName=${firstError}]`).focus(); + } + } +} diff --git a/src/validation/demo/src/app/validation/ValidationFormGroupTestComponent.ts b/src/validation/demo/src/app/validation/ValidationFormGroupTestComponent.ts new file mode 100644 index 0000000..8a0ae22 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationFormGroupTestComponent.ts @@ -0,0 +1,69 @@ +import { Component } from '@angular/core'; +import { FormArray, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-form-group-test.html' +}) +export class ValidationFormGroupTestComponent { + form: FormGroup; + nest: FormArray; + labelData: Array = [ + ['Grade', 'Class'], + ['Hobby1', 'Hobby2'] + ]; + constructor(fb: FormBuilder) { + this.form = fb.group({ + myInput: new FormControl('N', [TiValidators.required]), + myInput1: new FormControl('', [TiValidators.digits, TiValidators.rangeValue(0, 100)]), + nest: fb.array([ + fb.group({ + myInput01: new FormControl('One', [TiValidators.required]), + myInput02: new FormControl('', [TiValidators.required]) + }), + fb.group( + { + myInput11: new FormControl('study', [TiValidators.required]), + myInput12: new FormControl('', [TiValidators.required]) + }, + { validators: this.myValidator } + ) + ]) + }); + } + + ngOnInit(): void { + this.nest = this.form.get('nest') as FormArray; + + // 测试 checkGroup 和 checkGroup1WithConfig 的区别。checkGroup 会触发此事件,checkGroupWithConfig不会触发此事件 + this.form.controls.myInput.valueChanges.subscribe(() => { + console.log('form myInput valueChanges'); + }); + } + + // 表单整体校验,通过调用check方法实现整体校验的相关呈现 + checkGroup(): void { + const errors: ValidationErrors | null = TiValidators.check(this.form); + console.log(errors); + } + + checkGroupWithConfig(): void { + // 从10.0.1版本开始TiValidators.check方法支持传入第二个参数,第二个参数可选,具体参数意义可参考https://angular.io/api/forms/AbstractControl#updatevalueandvalidity + const errors: ValidationErrors | null = TiValidators.check(this.form, { + emitEvent: false + }); + console.log(errors); + } + + myValidator: ValidatorFn = (control: FormGroup): ValidationErrors | null => { + const value1 = control.get('myInput11'); + const value2 = control.get('myInput12'); + + return value1 && value2 && value2.value === value1.value + ? { + myValidator: true, + errorMessage: `${this.labelData[1][0]} and ${this.labelData[1][0]} cannot be the same. ` + } + : null; + }; +} diff --git a/src/validation/demo/src/app/validation/ValidationParamChangeComponent.ts b/src/validation/demo/src/app/validation/ValidationParamChangeComponent.ts new file mode 100644 index 0000000..4c8bdeb --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationParamChangeComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './validation-param-change.html' +}) +export class ValidationParamChangeComponent { + myValue: string = 'a'; + myValue1: number = 11; + equal: string = 'aa'; + number: boolean = true; + + onClick(): void { + this.equal = 'bb'; + } + + onClick1(): void { + this.number = !this.number; + } +} diff --git a/src/validation/demo/src/app/validation/ValidationPwdCheckComponent.ts b/src/validation/demo/src/app/validation/ValidationPwdCheckComponent.ts new file mode 100644 index 0000000..8392ff6 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationPwdCheckComponent.ts @@ -0,0 +1,166 @@ +import { Component } from '@angular/core'; +import { AbstractControl, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'; +import { TiPasswordValidatorConfig, TiValidationConfig, TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-pwd-check.html' +}) +export class ValidationPwdCheckComponent { + passwordConfig1: TiValidationConfig = { + type: 'password' + }; + + passwordConfig2: TiValidationConfig = { + type: 'password', + passwordConfig: { + validator: { + params: { + rangeSize: [8, 10], + minCharType: [3] + }, + message: { + rangeSize: '����Ϊ{0}��{1}��', + minCharType: '���ٰ�����д��ĸ��Сд��ĸ�����ּ��������`!=�е�{0}��' + } + } + } + }; + passwordConfig3: TiValidationConfig = { + type: 'password', + errorMessage: { + password: 'error msg define' + } + }; + passwordConfig4: TiValidationConfig = { + type: 'password', + passwordConfig: { + validator: { + params: { + custom1: ['bb'] + }, + message: { + custom1: 'should not equal to {0}' + } + } + } + }; + passwordConfig5: TiValidationConfig = { + errorMessage: { + custom1: 'error msg define', + custom2: 'error msg define' + }, + passwordConfig: { + validator: { + rule: 'custiomRule2', + params: { + custom1: ['cc'], + custom2: [8] + }, + message: { + custom1: 'should not equal to {0}', + custom2: 'length should be {0}' + } + }, + levelFn(value: string, validator: TiPasswordValidatorConfig): number { + if (value.length === 2) { + return 0; + } + if (value.length > 2 && value.length < validator.params.custom2[0]) { + return 1; + } + if (value.length === validator.params.custom2[0]) { + return 2; + } + } + } + }; + passwordConfig6: TiValidationConfig = { + type: 'password', + errorMessage: { + password: '' + } + }; + form: FormGroup; + constructor(fb: FormBuilder) { + const custiomRule1: (params: any) => ValidatorFn = (params: any): ValidatorFn => { + return Validators.compose([ + TiValidators.password({ + notEqualPosRev: () => { + return this.form.get('usernameInput'); + } + }), + this.x(params.t) + ]); + }; + + const custiomRule2: (params: any) => ValidatorFn = (params: any): ValidatorFn => { + return Validators.compose([this.x(params.m), this.y(params.n)]); + }; + + this.form = fb.group({ + usernameInput: new FormControl('TinyNG'), + passwordInput1: new FormControl('', [ + TiValidators.password({ + notEqualPosRev: () => { + return this.form.get('usernameInput'); + } + }) + ]), + passwordInput2: new FormControl('', [ + TiValidators.password({ + notEqualPosRev: () => { + return this.form.get('usernameInput'); + }, + rangeSize: [8, 10], + minCharType: [ + 3, + { + digitsCharReg: /[0-9]+/, + lowerCharReg: /[a-z]+/, + upperCharReg: /[A-Z]+/, + specialCharReg: /[`!=]/ + } + ] + }) + ]), + passwordInput3: new FormControl('', [ + TiValidators.password({ + notEqualPosRev: () => { + return this.form.get('usernameInput'); + } + }) + ]), + passwordInput4: new FormControl('', custiomRule1({ t: 'bb' })), + passwordInput5: new FormControl('', custiomRule2({ m: 'cc', n: 8 })), + passwordInput6: new FormControl('', [ + TiValidators.password({ + notEqualPosRev: () => { + return this.form.get('usernameInput'); + } + }) + ]) + }); + } + checkGroup(): void { + console.log(TiValidators.check(this.form)); + } + + x(param: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return control.value !== param ? null : { custom1: { notEqualValue: param, actualValue: control.value } }; + }; + } + + y(length: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return control.value.length === length + ? null + : { + custom2: { + requiredLength: length, + actualLength: control.value.length + } + }; + }; + } +} diff --git a/src/validation/demo/src/app/validation/ValidationRulesCustomComponent.ts b/src/validation/demo/src/app/validation/ValidationRulesCustomComponent.ts new file mode 100644 index 0000000..c356135 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationRulesCustomComponent.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { AbstractControl, FormControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +@Component({ + templateUrl: './validation-rules-custom.html' +}) +export class ValidationRulesCustomComponent { + rulesCustomControl: FormControl = new FormControl('hello', CustomValidators.isEqualTo('hello tiny')); +} +export class CustomValidators { + static isEqualTo(pvalue: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return pvalue !== control.value + ? { + isEqualTo: { + requiredValue: pvalue, + actualValue: control.value, + tiErrorMessage: 'error, should input {0}' + } + } + : null; + }; + } +} diff --git a/src/validation/demo/src/app/validation/ValidationRulesCustomDirectiveComponent.ts b/src/validation/demo/src/app/validation/ValidationRulesCustomDirectiveComponent.ts new file mode 100644 index 0000000..894f2b1 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationRulesCustomDirectiveComponent.ts @@ -0,0 +1,40 @@ +import { Component, Directive, forwardRef, Input } from '@angular/core'; +import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn } from '@angular/forms'; + +@Component({ + templateUrl: './validation-rules-custom-directive.html' +}) +export class ValidationRulesCustomDirectiveComponent { + text: string = ''; +} + +@Directive({ + selector: '[myIsEqualTo][formControlName],[myIsEqualTo][formControl],[myIsEqualTo][ngModel]', + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CustomValidatorsDirective), + multi: true + } + ] +}) +export class CustomValidatorsDirective implements Validator { + @Input() myIsEqualTo: string; + + validate(control: AbstractControl): ValidationErrors | null { + return this.isEqualTo(this.myIsEqualTo)(control); + } + isEqualTo(pvalue: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return pvalue !== control.value + ? { + isEqualTo: { + requiredValue: pvalue, + actualValue: control.value, + tiErrorMessage: 'custom rule is error, should input {0}' + } + } + : null; + }; + } +} diff --git a/src/validation/demo/src/app/validation/ValidationRulesTestComponent.ts b/src/validation/demo/src/app/validation/ValidationRulesTestComponent.ts new file mode 100644 index 0000000..dee39af --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationRulesTestComponent.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; +import { TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-rules-test.html' +}) +export class ValidationRulesTestComponent { + myValue: string; + myValue1: string; + myValue2: string; + myValue3: string; + myValue4: string; + myValue5: string; + myValue6: string; + myValue7: string; + myValue8: string; + myValue9: string; + myValue10: string; + myValue11: string; + myValue12: string; + myValue13: string; + myValue14: string; + myValue15: string; + myValue16: string; + myValue17: string; + myValue18: string; + myValue19: string; + myValue20: string; + myValue21: string; + myValue22: string; + myValue23: string; + myValue24: string; + disableStatus: boolean = false; + validationObj1: TiValidationConfig = {}; + validationObj: TiValidationConfig = { + type: 'change' + }; + changeDisable(): void { + this.disableStatus = !this.disableStatus; + } +} diff --git a/src/validation/demo/src/app/validation/ValidationTemplateFormNestedComponent.ts b/src/validation/demo/src/app/validation/ValidationTemplateFormNestedComponent.ts new file mode 100644 index 0000000..27f6951 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationTemplateFormNestedComponent.ts @@ -0,0 +1,63 @@ +import { Component, ElementRef } from '@angular/core'; +import { ControlContainer, FormGroup, NgForm, ValidationErrors } from '@angular/forms'; +import { TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-template-form-nested.html' +}) +export class ValidationTemplateFormNestedComponent { + value1: string = 'a'; + value2: string = ''; + value3: string = 'b'; + value4: string = ''; + + constructor(private elementRef: ElementRef) {} + + // 表单整体校验,通过调用check方法实现整体校验的相关呈现 + checkGroup(form: FormGroup): void { + console.log(form.controls); + const errors: ValidationErrors | null = TiValidators.check(form); + console.log(errors); + + // 整体校验后如果需要聚焦到第一个校验不通过元素,请参考以下代码 + if (errors) { + const firstError: any = Object.keys(errors)[0]; + this.elementRef.nativeElement.querySelector(`[name=${firstError}]`).focus(); + } + } +} + +@Component({ + selector: 'custom-child1-test', + template: ` +
    +

    子组件区域:

    +

    + +
    + `, + viewProviders: [{ provide: ControlContainer, useExisting: NgForm }] // 有该行的配置时,才会将该组件中的表单控件添加到父级组件的 FormGroup(NgForm) 中去 +}) +export class CustomChild1TestComponent { + value1: string = 'c'; + value2: string = ''; +} + +@Component({ + selector: 'custom-child2-test', + template: ` +
    +

    子组件区域:

    + +
    + + +
    +
    + `, + viewProviders: [{ provide: ControlContainer, useExisting: NgForm }] // 有该行的配置时,才会将该组件中的表单控件添加到父级组件的 FormGroup(NgForm) 中去 +}) +export class CustomChild2TestComponent { + value1: string = 'c'; + value2: string = ''; +} diff --git a/src/validation/demo/src/app/validation/ValidationTestModule.ts b/src/validation/demo/src/app/validation/ValidationTestModule.ts new file mode 100644 index 0000000..558f2fc --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationTestModule.ts @@ -0,0 +1,153 @@ +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { + TiButtonModule, + TiFormfieldModule, + TiScrollModule, + TiSelectModule, + TiTextareaModule, + TiTextModule, + TiValidationModule +} from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { ValidationBasicDirectiveComponent } from './ValidationBasicDirectiveComponent'; +import { ValidationBasicControlComponent } from './ValidationBasicControlComponent'; +import { ValidationFormGroupComponent } from './ValidationFormGroupComponent'; +import { ValidationFormGroupConfigComponent } from './ValidationFormGroupConfigComponent'; +import { ValidationBlurCheckComponent } from './ValidationBlurCheckComponent'; +import { ValidationRulesCustomComponent } from './ValidationRulesCustomComponent'; +import { ValidationPwdCheckComponent } from './ValidationPwdCheckComponent'; +import { ValidationTipComponent } from './ValidationTipComponent'; +import { ValidationErrorMsgComponent } from './ValidationErrorMsgComponent'; +import { ValidationRulesTestComponent } from './ValidationRulesTestComponent'; +import { ValidationFormGroupTestComponent } from './ValidationFormGroupTestComponent'; +import { ValidationTiscrollComponent } from './ValidationTiscrollComponent'; +import { ValidationParamChangeComponent } from './ValidationParamChangeComponent'; +import { ValidationAsyncCheckComponent } from './ValidationAsyncCheckComponent'; +import { ValidationAsyncCheckTestComponent } from './ValidationAsyncCheckTestComponent'; +import { CustomValidatorsDirective, ValidationRulesCustomDirectiveComponent } from './ValidationRulesCustomDirectiveComponent'; +import { + CustomChild1TestComponent, + CustomChild2TestComponent, + ValidationTemplateFormNestedComponent +} from './ValidationTemplateFormNestedComponent'; + +@NgModule({ + imports: [ + CommonModule, + TiFormfieldModule, + TiValidationModule, + TiTextareaModule, + TiTextModule, + TiScrollModule, + TiSelectModule, + FormsModule, + ReactiveFormsModule, + TiButtonModule, + DemoLogModule, + RouterModule.forChild(ValidationTestModule.ROUTES) + ], + declarations: [ + ValidationBasicDirectiveComponent, + ValidationBasicControlComponent, + ValidationBlurCheckComponent, + ValidationFormGroupComponent, + ValidationFormGroupConfigComponent, + ValidationRulesCustomComponent, + ValidationPwdCheckComponent, + ValidationTipComponent, + ValidationErrorMsgComponent, + ValidationRulesTestComponent, + ValidationFormGroupTestComponent, + ValidationTiscrollComponent, + ValidationParamChangeComponent, + ValidationAsyncCheckComponent, + ValidationAsyncCheckTestComponent, + ValidationTemplateFormNestedComponent, + CustomChild1TestComponent, + CustomChild2TestComponent, + ValidationRulesCustomDirectiveComponent, + CustomValidatorsDirective + ] +}) +export class ValidationTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiErrorMsgComponent.html', label: 'ErrorMsg' }, + { href: 'components/TiPwdMsgComponent.html', label: 'PwdMsg' }, + { href: 'directives/TiValidationDirective.html', label: 'Validation' } + ]; + static readonly ROUTES: Routes = [ + { + path: 'validation/validation-basic-directive', + component: ValidationBasicDirectiveComponent + }, + { + path: 'validation/validation-basic-control', + component: ValidationBasicControlComponent + }, + { + path: 'validation/validation-form-group', + component: ValidationFormGroupComponent + }, + { + path: 'validation/validation-form-group-config', + component: ValidationFormGroupConfigComponent + }, + { + path: 'validation/validation-blur-check', + component: ValidationBlurCheckComponent + }, + { + path: 'validation/validation-pwd-check', + component: ValidationPwdCheckComponent + }, + { + path: 'validation/validation-tip', + component: ValidationTipComponent + }, + { + path: 'validation/validation-error-msg', + component: ValidationErrorMsgComponent + }, + { + path: 'validation/validation-rules-custom', + component: ValidationRulesCustomComponent + }, + { + path: 'validation/validation-rules-custom-directive', + component: ValidationRulesCustomDirectiveComponent + }, + { + path: 'validation/validation-async-check', + component: ValidationAsyncCheckComponent + }, + { + path: 'validation/validation-tiscroll', + component: ValidationTiscrollComponent + }, + { + path: 'validation/validation-rules-test', + component: ValidationRulesTestComponent + }, + { + path: 'validation/validation-form-group-test', + component: ValidationFormGroupTestComponent + }, + { + path: 'validation/validation-param-change', + component: ValidationParamChangeComponent + }, + { + path: 'validation/validation-async-check-test', + component: ValidationAsyncCheckTestComponent + }, + { + path: 'validation/validation-template-form-nested', + component: ValidationTemplateFormNestedComponent + } + ]; +} diff --git a/src/validation/demo/src/app/validation/ValidationTipComponent.ts b/src/validation/demo/src/app/validation/ValidationTipComponent.ts new file mode 100644 index 0000000..2968d14 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationTipComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-tip.html' +}) +export class ValidationTipComponent { + inputValue: string = ''; + tipConfig: TiValidationConfig = { + tip: '自定义文案:请输入 hello tiny', + tipPosition: 'bottom', + tipMaxWidth: '100px' + }; +} diff --git a/src/validation/demo/src/app/validation/validation-async-check-test.html b/src/validation/demo/src/app/validation/validation-async-check-test.html new file mode 100644 index 0000000..931a9e2 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-async-check-test.html @@ -0,0 +1,24 @@ +

    描述

    +

    异步校验需要在校验错误信息中通过 tiAsyncErrorMessage 属性来设置校验错误提示信息。

    +

    异步校验不支持type:blur和type:password的校验类型;异步校验只支持 input[tiText] 输入框组件。

    + +

    示例

    + +

    1. 在formfield中pending状态的loading图标是否正常:

    +

    同步校验规则:1.输入不能为空; 2.字符最小长度为5。

    +

    异步校验规则:1.后台进行校验(2秒)后返回结果,输入 'happy' 则成功,否则失败

    + + + + + + +

    2. input后面有元素的时候,loading图标的出现会不会影响后面元素的位置:

    +

    同步校验规则:1.输入不能为空; 2.字符最小长度为5。

    +

    异步校验规则:1.后台进行校验(2秒)后返回结果,输入 'happy' 则成功,否则失败

    +测试元素 +
    + +

    场景待续...... diff --git a/src/validation/demo/src/app/validation/validation-async-check.html b/src/validation/demo/src/app/validation/validation-async-check.html new file mode 100644 index 0000000..235cf13 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-async-check.html @@ -0,0 +1,5 @@ +

    1. 仅异步校验(2秒后校验,输入 'hello tiny' 视为通过)

    + +

    +

    2. 同步校验(非空且最小长度为5),同时加入异步校验(2秒后校验,输入 'hello tiny' 视为通过)

    + diff --git a/src/validation/demo/src/app/validation/validation-basic-control.html b/src/validation/demo/src/app/validation/validation-basic-control.html new file mode 100644 index 0000000..915dd3f --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-basic-control.html @@ -0,0 +1 @@ + diff --git a/src/validation/demo/src/app/validation/validation-basic-directive.html b/src/validation/demo/src/app/validation/validation-basic-directive.html new file mode 100644 index 0000000..a187d93 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-basic-directive.html @@ -0,0 +1,4 @@ +非空校验: +
    +
    +内容校验: diff --git a/src/validation/demo/src/app/validation/validation-blur-check.html b/src/validation/demo/src/app/validation/validation-blur-check.html new file mode 100644 index 0000000..0c0204b --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-blur-check.html @@ -0,0 +1 @@ + diff --git a/src/validation/demo/src/app/validation/validation-error-msg.html b/src/validation/demo/src/app/validation/validation-error-msg.html new file mode 100644 index 0000000..3bc0002 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-error-msg.html @@ -0,0 +1,6 @@ +

    1. 自定义错误信息

    + +
    自定义检验信息容器:
    + +

    2. 清空错误信息

    + diff --git a/src/validation/demo/src/app/validation/validation-form-group-config.html b/src/validation/demo/src/app/validation/validation-form-group-config.html new file mode 100644 index 0000000..b11210b --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-form-group-config.html @@ -0,0 +1,10 @@ +
    + 密码:

    + 邮箱:

    + 昵称:

    + 子表单:
    + + + + + diff --git a/src/validation/demo/src/app/validation/validation-form-group-test.html b/src/validation/demo/src/app/validation/validation-form-group-test.html new file mode 100644 index 0000000..06bdbb2 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-form-group-test.html @@ -0,0 +1,39 @@ +

    描述

    +

    表单整体校验调用check方法实现

    +

    示例

    +

    多层FormGroup嵌套,并且在formGroup上添加自定义校验规则

    + +
    + name:

    + age:

    +
    +
    +
    + {{labelData[i][0]}}: +

    + {{labelData[i][1]}}: +
    + + {{item.errors.errorMessage}} + +

    +
    +
    +

    + + + + +
    + diff --git a/src/validation/demo/src/app/validation/validation-form-group.html b/src/validation/demo/src/app/validation/validation-form-group.html new file mode 100644 index 0000000..848d9f6 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-form-group.html @@ -0,0 +1,36 @@ +

    1. 响应式表单整体校验

    +
    +

    邮箱校验:

    +
    +

    邮箱取值:{{ this.groupFormControl.controls.emailController.value }}

    +
    +

    非空校验:

    +
    + +
    + + + +
    + + +
    +

    +

    2. 模板驱动式表单整体校验

    +
    +

    表单项一:

    +
    +

    表单项二:

    + +
    +

    +

    3. 异步整体校验

    +
    +

    同步校验:(非空)

    + +

    +

    异步校验:(2秒后校验,输入 'tiny' 视为通过)

    + +
    + +
    diff --git a/src/validation/demo/src/app/validation/validation-param-change.html b/src/validation/demo/src/app/validation/validation-param-change.html new file mode 100644 index 0000000..7efd03a --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-param-change.html @@ -0,0 +1,11 @@ +

    描述

    +

    带参数的校验指令:支持参数变更

    +

    注意:使用 tiText 组件,请导入 TiTextModule。

    + +

    示例

    +

    1.tiEqual(带参数):输入值等于{{equal}}

    +

    + +

    1.tiNumber(不带参数)

    +

    + diff --git a/src/validation/demo/src/app/validation/validation-pwd-check.html b/src/validation/demo/src/app/validation/validation-pwd-check.html new file mode 100644 index 0000000..e7b36f2 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-pwd-check.html @@ -0,0 +1,30 @@ +
    + 用户名: + +

    +

    1.基础用法

    + 密码: + +

    +

    2.配置校验规则及提示信息

    + 密码: + +

    +

    3.配置错误提示信息

    + 密码: + +

    +

    4.增量添加自定义规则

    + 密码: + +

    +

    5.完全自定义规则

    + 密码: + +

    +

    6.关闭错误信息提示

    + 密码: + +

    + +
    diff --git a/src/validation/demo/src/app/validation/validation-rules-custom-directive.html b/src/validation/demo/src/app/validation/validation-rules-custom-directive.html new file mode 100644 index 0000000..857059b --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-rules-custom-directive.html @@ -0,0 +1 @@ + diff --git a/src/validation/demo/src/app/validation/validation-rules-custom.html b/src/validation/demo/src/app/validation/validation-rules-custom.html new file mode 100644 index 0000000..bce656c --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-rules-custom.html @@ -0,0 +1 @@ + diff --git a/src/validation/demo/src/app/validation/validation-rules-test.html b/src/validation/demo/src/app/validation/validation-rules-test.html new file mode 100644 index 0000000..dad6db7 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-rules-test.html @@ -0,0 +1,75 @@ +
    + 1.Equal:aa + +

    + 2.MaxLength:5 + +

    + 3.Email: + +

    + 4.Digits: + +

    + 5.Script: + +

    + 6.tiContains:aa + +

    + 7.tiNotContains:aa + +

    + 8.Date + +

    + 9.Integer + +

    + 10.IPv4 + +

    + 11.IPv6 + +

    + 12.MaxValue:6 + +

    + 13.MinLength:3 + +

    + 14.MinValue:5 + +

    + 15.NotEqual:aa + +

    + 16.Number + +

    + 17.tiPassword + +

    + 18.port + +

    + 19.MinLength:3 required + +

    + 20.RangeSize:[3,5] + +

    + 21.RangeValue:[5,10] + +

    + 22.regExp: + +

    + 23.Required + +

    + 24.URL + +

    + +
    diff --git a/src/validation/demo/src/app/validation/validation-template-form-nested.html b/src/validation/demo/src/app/validation/validation-template-form-nested.html new file mode 100644 index 0000000..3b90c36 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-template-form-nested.html @@ -0,0 +1,33 @@ +

    描述

    +

    + 模板驱动表单,form 标签中嵌套子组件,且子组件中也有表单控件, 调用 check 方法进行表单 form + 整体校验时可连带子组件中的表单控件一起进行校验。 +

    +

    示例

    + +

    1.模板驱动式表单 + 嵌套含有表单控件的子组件 + 整体校验: 在 FormGroup 里父组件中的表单控件与子组件中的表单控件平级

    +
    +
    +

    +

    + +

    + + +
    +

    + +

    + 2.模板驱动式表单 + 嵌套含有表单控件的子组件 + 整体校验: 在 FormGroup + 里父组件中的表单控件与子组件中的表单控件平级不是平级的,子组件中的表单控件是 FormGroup 里的子级 +

    +
    +
    +

    +

    + +

    + + +
    +

    diff --git a/src/validation/demo/src/app/validation/validation-tip.html b/src/validation/demo/src/app/validation/validation-tip.html new file mode 100644 index 0000000..a0ee35b --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-tip.html @@ -0,0 +1 @@ + diff --git a/src/validation/demo/src/app/validation/validation-tiscroll.html b/src/validation/demo/src/app/validation/validation-tiscroll.html new file mode 100644 index 0000000..32819e0 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-tiscroll.html @@ -0,0 +1,24 @@ +

    描述

    +

    校验规则使用提供了指令和TS中声明两种方式,凡是Tiny封装的校验规则均以ti开头

    +

    + 当在页面局部有滚动条的容器中使用校验指令,鼠标拖动容器滚动条时,为了防止校验信息错位, + 开发者需要在对应的局部容器中添加tiScroll属性,使校验信息在拖动局部滚动条时消失。 +

    +

    注意:使用 tiText 组件,请导入 TiTextModule。使用tiScroll指令,请导入 TiScrollModule。

    +

    导入

    +

    import {{ '{' }} TiScrollModule, TiTextModule, TiValidationModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    +

    1.基本:

    +
    + +

    +











    +
    +

    2.注册tiScroll事件

    +

    除上述方式,还可以通过手动方式给滚动容器注册事件,参考 tiSelect 组件 tiScroll(下拉列表分离) 示例

    diff --git a/src/validation/demo/src/app/validation/validationTiscrollComponent.ts b/src/validation/demo/src/app/validation/validationTiscrollComponent.ts new file mode 100644 index 0000000..a3ffb70 --- /dev/null +++ b/src/validation/demo/src/app/validation/validationTiscrollComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './validation-tiscroll.html' +}) +export class ValidationTiscrollComponent { + myValue: string = 'a'; +} diff --git a/src/validation/demo/src/app/validation/webdoc/validation-demos.js b/src/validation/demo/src/app/validation/webdoc/validation-demos.js new file mode 100644 index 0000000..597602e --- /dev/null +++ b/src/validation/demo/src/app/validation/webdoc/validation-demos.js @@ -0,0 +1,165 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'validation-basic-directive', + name: { + 'zh-CN': '模板表单基本用法', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    tiValidation指令的基本用法之一。使用模板驱动表单,TinyNG 实现的校验规则及方法见 TiValidators,你也可以使用 Angular 表单校验规则或自定义校验规则。

    ', + 'en-US': '

    ', + }, + apis: ['TiValidationDirective.properties.tiValidation'], + }, + { + demoId: 'validation-basic-control', + name: { + 'zh-CN': '响应式表单基本用法', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    tiValidation指令的另一种基本用法。使用响应式表单,TinyNG 实现的校验规则及方法见 TiValidators,你也可以使用 Angular 表单校验规则或自定义校验规则。

    ', + 'en-US': '

    ', + }, + }, + { + demoId: 'validation-blur-check', + name: { + 'zh-CN': '失焦校验', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    通过将TiValidationConfig实例的属性type配置为blur开启失焦校验。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiValidationDirective.properties.errorMessageWrapper', + 'TiValidationConfig.properties.type', + ], + }, + { + demoId: 'validation-tip', + name: { + 'zh-CN': 'Tip', + 'en-US': 'Tip', + }, + desc: { + 'zh-CN': + '

    通过TiValidationConfig实例的属性tip配置 Tip 内容,通过属性tipPosition配置 Tip 弹出位置。注意:Tip 指提示语,是在用户输入内容前对用户输入预期的描述。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiValidationConfig.properties.tip', + 'TiValidationConfig.properties.tipPosition', + 'TiValidationConfig.properties.tipMaxWidth', + ], + }, + { + demoId: 'validation-error-msg', + name: { + 'zh-CN': '自定义错误信息', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    通过TiValidationConfig实例的属性errorMessage自定义错误提示信息;当typeblur/password时,可以通过属性errorMessageWrapper配置校验信息容器。注意:错误信息指用户输入内容后,对输入内容不符合校验规则部分的提示。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiValidationConfig.properties.errorMessage', + 'TiValidationConfig.properties.errorMessageWrapper', + ], + }, + { + demoId: 'validation-pwd-check', + name: { + 'zh-CN': '密码校验', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    通过将TiValidationConfig实例的属性type配置为password开启密码校验。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiValidationConfig.properties.passwordConfig', + 'TiPasswordValidatorConfig.properties.rule', + 'TiPasswordValidatorConfig.properties.message', + 'TiPasswordValidatorConfig.properties.params', + ], + }, + { + demoId: 'validation-async-check', + name: { + 'zh-CN': '异步校验', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    支持在 tiText 组件中通过自定义规则进行异步校验,支持在校验错误信息中通过tiAsyncErrorMessage属性配置校验错误提示信息。注意:异步校验不支持blurpassword类型;异步校验只支持 tiText 组件。

    ', + 'en-US': '

    ', + }, + }, + { + demoId: 'validation-rules-custom-directive', + name: { + 'zh-CN': '模板表单中自定义规则', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    支持在模板表单中自定义校验规则,规则定义请参考 Angular 官方说明

    ', + 'en-US': '

    ', + }, + }, + { + demoId: 'validation-rules-custom', + name: { + 'zh-CN': '方法调用形式自定义规则', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    支持在响应式表单中自定义校验规则,通过FormControl构造函数中的参数配置规则,规则定义请参考 Angular 官方说明

    ', + 'en-US': '

    ', + }, + }, + { + demoId: 'validation-form-group', + name: { + 'zh-CN': '表单整体校验', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    使用TiValidators.check方法调用表单整体校验。

    ', + 'en-US': '

    ', + }, + apis: ['TiValidationDirective.properties.tiValidation'], + }, + { + demoId: 'validation-form-group-config', + name: { + 'zh-CN': '表单整体校验自定义配置', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    使用check方法调用表单整体校验;通过TiValidationCheckConfig实例配置校验规则。注意:关于onlySelfemitEvent属性请参考 Angular 官方文档

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiValidationCheckConfig.properties.emitEvent', + 'TiValidationCheckConfig.properties.ignoreNames', + 'TiValidationCheckConfig.properties.onlySelf', + 'TiValidationCheckConfig.properties.errorsFlatted', + ], + }, + ], +}; diff --git a/src/validation/demo/src/app/validation/webdoc/validation.cn.md b/src/validation/demo/src/app/validation/webdoc/validation.cn.md new file mode 100644 index 0000000..8e55e84 --- /dev/null +++ b/src/validation/demo/src/app/validation/webdoc/validation.cn.md @@ -0,0 +1,52 @@ +--- +title: Validation 表单校验 +--- +# Validation 表单校验 + +
    + +TinyNG 表单校验是基于 Angular 提供的表单校验进行的封装,在使用 TinyNG 组件前,请先了解 Angular 表单校验。 + +Angular 提供了两种表单处理方式:响应式表单(Reactive forms)和 模板驱动表单(Template-driven forms)。 关于二者的区别请**务必仔细阅读** Angular 表单简介。 + +- 支持响应式表单和模板表单两种方式驱动表单。 +- TinyNG 实现的校验规则及方法见 TiValidators,你也可以使用 Angular 表单校验规则或自定义校验规则。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +如需使用响应式表单,请导入。 +```typescript +import { FormControl } from '@opentiny/ng'; +``` + +如需使用 TinyNG 封装的校验方法,请导入。 +```typescript +import { TiValidators } from '@opentiny/ng'; +``` +
    + +
    + +TinyNG 表单校验是基于 Angular 提供的表单校验进行的封装,在使用 TinyNG 组件前,请先了解 Angular表单校验。 + +Angular 提供了两种表单处理方式:响应式表单(Reactive forms)和 模板驱动表单(Template-driven forms)。 关于二者的区别请**务必仔细阅读** Angular 表单简介。 + +- 支持响应式表单和模板表单两种方式驱动表单。 +- TinyNG 实现的校验规则及方法见 TiValidators,你也可以使用 Angular 表单校验规则或自定义校验规则。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +如需使用响应式表单,请导入。 +```typescript +import { FormControl } from '@opentiny/ng'; +``` + +如需使用 TinyNG 封装的校验方法,请导入。 +```typescript +import { TiValidators } from '@opentiny/ng'; +``` +
    diff --git a/src/validation/demo/src/app/validation/webdoc/validation.en.md b/src/validation/demo/src/app/validation/webdoc/validation.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/validation/demo/src/app/validation/webdoc/validation.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/validation/demo/src/favicon.ico b/src/validation/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/validation/demo/src/index.html b/src/validation/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/validation/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/validation/demo/src/main.ts b/src/validation/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/validation/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/validation/demo/test.ts b/src/validation/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/validation/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/validation/demo/tsconfig.app.json b/src/validation/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/validation/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/validation/demo/tsconfig.spec.json b/src/validation/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/validation/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/validation/lib/index.ts b/src/validation/lib/index.ts new file mode 100644 index 0000000..35d9fd4 --- /dev/null +++ b/src/validation/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiValidationModule'; diff --git a/src/validation/lib/ng-package.json b/src/validation/lib/ng-package.json new file mode 100644 index 0000000..15d4be9 --- /dev/null +++ b/src/validation/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/validation", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/validation/lib/package.json b/src/validation/lib/package.json new file mode 100644 index 0000000..3f17f5e --- /dev/null +++ b/src/validation/lib/package.json @@ -0,0 +1,18 @@ +{ + "name": "@opentiny/ng-validation", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/forms": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@opentiny/ng-popup": "~1.0.0-beta.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-loading": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/validation/lib/project.json b/src/validation/lib/project.json new file mode 100644 index 0000000..348a21d --- /dev/null +++ b/src/validation/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/validation/lib", + "sourceRoot": "src/validation/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/validation"], + "options": { + "project": "src/validation/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/validation"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js validation" + }, + { + "command": "ng default-build validation" + }, + { + "command": "node build/clear-default-theme.js validation" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/validation && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build validation && ng pack validation && node build/publish.js validation --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/validation/lib/src/TiErrorMsgComponent.ts b/src/validation/lib/src/TiErrorMsgComponent.ts new file mode 100644 index 0000000..bdd8801 --- /dev/null +++ b/src/validation/lib/src/TiErrorMsgComponent.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, ViewEncapsulation, ChangeDetectionStrategy } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * @ignore + */ +@Component({ + selector: 'ti-error-msg', + template: ` + + + + + `, + styleUrls: ['./errorMsg.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, // 由于样式文件中存在全局样式,因此使用该方式 + host: { + '[class.ti3-unifyvalid-error-container]': '!appendToTip', + '[class.ti3-unifyvalid-tip-error-container]': 'true' + } +}) +export class TiErrorMsgComponent extends TiBaseComponent { + @Input() errorMessage: string; + @Input() appendToTip: boolean; + protected versionInfo: string = super.getVersion(packageInfo); +} diff --git a/src/validation/lib/src/TiPendingStateComponent.ts b/src/validation/lib/src/TiPendingStateComponent.ts new file mode 100644 index 0000000..6a637c3 --- /dev/null +++ b/src/validation/lib/src/TiPendingStateComponent.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * @ignore + */ +@Component({ + selector: 'ti-pending-state', + templateUrl: './pending-state.html', + styleUrls: ['./pending-state.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-validation-pending-container]': 'true' + } +}) +export class TiPendingStateComponent extends TiBaseComponent { + // 校验元素 + @Input() validElement: any; + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + this.renderer.addClass(this.validElement, 'ti3-text-input-show-pending'); + } + + ngOnDestroy(): void { + if (this.validElement) { + this.renderer.removeClass(this.validElement, 'ti3-text-input-show-pending'); + } + } +} diff --git a/src/validation/lib/src/TiPwdMsgComponent.html b/src/validation/lib/src/TiPwdMsgComponent.html new file mode 100644 index 0000000..c5f520b --- /dev/null +++ b/src/validation/lib/src/TiPwdMsgComponent.html @@ -0,0 +1,24 @@ +
    +
    + + + + + + + + {{item.msg}} +
    +
    + {{msgModel.securityText}} +
    + + + +
    + {{msgModel.securityStatus}} +
    +
    diff --git a/src/validation/lib/src/TiPwdMsgComponent.ts b/src/validation/lib/src/TiPwdMsgComponent.ts new file mode 100644 index 0000000..285650a --- /dev/null +++ b/src/validation/lib/src/TiPwdMsgComponent.ts @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { FormControl, ValidationErrors } from '@angular/forms'; +import { Util } from '@opentiny/ng-utils'; +import { TiLocale } from '@opentiny/ng-locale'; +/** + * @ignore + */ +export interface MsgItem { + msg: string; + validStatus: any; +} +/** + * @ignore + */ +export interface MsgModel { + level: number; + msgItems: Array; + securityText: string; + securityStatus: string; +} +/** + * @ignore + */ +@Component({ + selector: 'ti-pwd-msg', + templateUrl: './TiPwdMsgComponent.html', + styleUrls: ['./pwdMsg.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiPwdMsgComponent implements OnInit { + @Input() control: FormControl; + @Input() msgItems: Array; + @Input() validator: any; + @Input() levelFn: Function; + public msgModel: MsgModel = { + level: -1, + msgItems: [], + securityText: '', + securityStatus: '' + }; + + private static _getPwdStrengthLan(key: string): string { + return TiLocale.getLocaleWords().tiValid.passwordStrength[key]; + } + + // 获取各项规则提示 + private static _getMsgItems(msgObj: any, control: FormControl): Array { + let validStatus: number | boolean; + if (control.untouched && control.pristine) { + // 初次聚焦,未输入情况下,不校验 + validStatus = -1; + } + + const msgInfoArr: Array = []; + let msgItem: string = ''; + for (const key in msgObj) { + if (Object.prototype.hasOwnProperty.call(msgObj, key)) { + msgItem = msgObj[key]; + msgInfoArr.push({ + msg: msgItem, + validStatus: TiPwdMsgComponent._getValidStatus(validStatus, key, control) + }); + } + } + + return msgInfoArr; + } + + private static _getValidStatus(validStatus: number | boolean, key: string, control: FormControl): number | boolean { + const errors: ValidationErrors | null = control.errors; + // validStatus已定义情况下,直接返回 + if (!Util.isUndefined(validStatus)) { + return validStatus; + } + // errors中有该条消息,说明校验未通过 + if (errors && !Util.isUndefined(errors[key])) { + return false; + } + // 值为空的情况下,除notEqualPosRev外,其他规则均为错误 + if (control.value === '' && key !== 'notEqualPosRev') { + return false; + } + + return true; + } + + /** + * 获取密码强度等级 + * @param value 输入值 + * @param validator 校验规则配置 + * @param levelFn 密码等级计算函数 + * @returns 密码强度等级 + */ + private static _getLevel(value: string, validator: any, levelFn: Function): number { + return levelFn(value, validator); + } + ngOnInit(): void { + const value: string = this.control.value; + // level计算 + let level: number = -1; + if (!(value === null || value.length === 0)) { + // value值为空情况下,level无效处理 + level = TiPwdMsgComponent._getLevel(value, this.validator, this.levelFn); + } + + // 设置呈现时需要使用的字段 + this.msgModel = { + level, + msgItems: TiPwdMsgComponent._getMsgItems(this.msgItems, this.control), + securityText: TiPwdMsgComponent._getPwdStrengthLan('securityText'), + securityStatus: level === -1 ? '' : TiPwdMsgComponent._getPwdStrengthLan('levelDecArr')[level] + }; + } +} diff --git a/src/validation/lib/src/TiValidationConfig.ts b/src/validation/lib/src/TiValidationConfig.ts new file mode 100644 index 0000000..2fa9653 --- /dev/null +++ b/src/validation/lib/src/TiValidationConfig.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiPwdConfig } from './checkHandle/TiPwdConfig'; +import { TiPositionType } from '@opentiny/ng-utils'; +// 定义组件默认配置参数 +/** + * @ignore + */ +export class TiValidationDefaultConfig extends TiPwdConfig { + // 默认校验方式 + public static type: 'change' | 'blur' | 'password' = 'change'; + public static tipPosition: TiPositionType = 'right'; +} diff --git a/src/validation/lib/src/TiValidationDirective.ts b/src/validation/lib/src/TiValidationDirective.ts new file mode 100644 index 0000000..a7c2656 --- /dev/null +++ b/src/validation/lib/src/TiValidationDirective.ts @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, Renderer2, RendererFactory2, SimpleChanges } from '@angular/core'; +import { NgControl } from '@angular/forms'; +import { CheckStyleService } from './checkHandle/CheckStyleService'; +import { CommonService } from './checkHandle/CommonService'; +import { TiValidationConfig, TiValidationType } from './TiValidationInterface'; +import { Subscription, timer } from 'rxjs'; +import { debounce } from 'rxjs/operators'; + +/** + * Tiny校验是基于Angular提供的表单校验进行的封装,在使用Tiny组件前,请先了解[Angular表单校验]{@link https://angular.io/guide/form-validation} + * + * 组件支持的校验规则及方法见 [TiValidators]{@link ../classes/TiValidators.html} + * + */ +@Directive({ + selector: '[tiValidation]', + // 声明该组件定义时需要用到的服务 + providers: [CheckStyleService, CommonService], + host: { + '(focus)': 'onFocus()', + '(blur)': 'onBlur()' + } +}) +export class TiValidationDirective implements OnInit, OnChanges, OnDestroy { + public static readonly ASYNC_DEBOUNCE_TIME: number = 500; // 这个数值跟tiny2的异步校验一致 + /** + * 校验配置信息 + */ + @Input() tiValidation: TiValidationConfig | ''; + /** + * 错误提示信息显示容器,适用于blur/password类型校验形式下,错误提示信息位置自定义场景 + */ + @Input() errorMessageWrapper: Element; + private renderer: Renderer2; + // 当前formControl的statusChanges订阅,在指令销毁时取消 + private formStatusSubscription: Subscription; + private asyncFormStatusSubscription: Subscription; + constructor( + private formControl: NgControl, + private checkStyleFactory: CheckStyleService, + private element: ElementRef, + rendererFactory: RendererFactory2 + ) { + this.renderer = rendererFactory.createRenderer(null, null); + } + private _validationHandleFn: any; + private eleNative: Element = this.element.nativeElement; + ngOnInit(): void { + // 初始化handleFn,用于处理tiValidation指令声明但未定义值场景 + this._setHandleFn(); + // 订阅onStatusChange事件,传递校验时机 + this.formStatusSubscription = this.formControl.statusChanges.subscribe(() => { + this._validationHandleFn.onStatusChange(this.element, this.formControl); + }); + // 订阅onStatusChange事件来特别处理异步校验pending状态的loading图标 + if (this.formControl.control.asyncValidator) { + this.asyncFormStatusSubscription = this.formControl.statusChanges + .pipe( + // 由于异步校验有防抖处理(输入停顿后再进行异步校验),所以需要在异步校验开始时才能出现loading图标 + debounce(() => timer(Number(this.formControl.pending) * TiValidationDirective.ASYNC_DEBOUNCE_TIME)) + ) + .subscribe(() => { + if (this._validationHandleFn.onAsyncStatusChange) { + this._validationHandleFn.onAsyncStatusChange(this.element, this.formControl); + } + }); + } + } + ngOnChanges(changes: SimpleChanges): void { + if (changes['tiValidation'] && !changes['tiValidation'].firstChange) { + // validation动态修改支持,此处根据validation重新创建校验实体方法函数 + this._setHandleFn(); + } + } + ngOnDestroy(): void { + // 宿主元素销毁时,销毁其附属tip + this._validationHandleFn.destroy(this.element); + // 取消formControl中statusChanges的订阅 + this.formStatusSubscription.unsubscribe(); + if (this.asyncFormStatusSubscription) { + this.asyncFormStatusSubscription.unsubscribe(); + } + } + /** + * @ignore + */ + onFocus(): void { + this._markAsFocus(); + this._validationHandleFn.onFocus(this.element, this.formControl); + } + /** + * @ignore + */ + onBlur(): void { + this._markAsBlur(); + this._validationHandleFn.onBlur(this.element, this.formControl); + } + private _setHandleFn(): void { + // 将errorMessageWrapper和tiValidation属性合并,方便后续处理 + const validationConfig: TiValidationConfig = { + ...this.tiValidation, + errorMessageWrapper: this.errorMessageWrapper + }; + let type: TiValidationType; + if (['TI-CHECKBOX-GROUP', 'TI-RADIO-GROUP'].includes(this.eleNative.tagName) || this.eleNative.matches('input[ticheckbox]')) { + type = 'radiobase'; + this.renderer.setAttribute(this.eleNative, 'tiRadiobaseCheck', ''); + } + this._validationHandleFn = this.checkStyleFactory.createHandler( + type || (this.tiValidation && this.tiValidation.type), + validationConfig + ); + + // 失焦校验时给宿主元素添加tiBlurCheck属性标识 + if (this.tiValidation && this.tiValidation.type === 'blur') { + this.renderer.setAttribute(this.eleNative, 'tiBlurCheck', ''); + } + } + + // 设置focus状态标志,标志包括两部分(如下markAsBlur逻辑类似): + // 1.样式类,用于根据focus/blur状态设置CSS中表单边框颜色; + // 2.标志位,用于根据focus/blur判断是否显示提示信息 + private _markAsFocus(): void { + this.renderer.setAttribute(this.eleNative, 'tiFocused', 'tiFocused'); + } + private _markAsBlur(): void { + this.renderer.removeAttribute(this.eleNative, 'tiFocused'); + } +} diff --git a/src/validation/lib/src/TiValidationInterface.ts b/src/validation/lib/src/TiValidationInterface.ts new file mode 100644 index 0000000..0ba6be2 --- /dev/null +++ b/src/validation/lib/src/TiValidationInterface.ts @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * @ignore + */ +export type TiValidationType = 'change' | 'blur' | 'password' | 'radiobase'; +/** + * tiValidation指令接口类型 + */ +export interface TiValidationConfig { + /** + * 校验方式,包括'change' 、 'blur' 、 'password' + * + * @default 'change' + */ + type?: TiValidationType; + /** + * + * 自定义已封装的校验规则的错误提示信息 + */ + errorMessage?: any; + /** + * tip 提示内容,在表单元素聚焦且没有错误提示信息显示 + */ + tip?: string; + /** + * tip 提示方向,默认为 'right' + * + * @default 'right' + */ + tipPosition?: + | 'top' + | 'top-left' + | 'top-right' + | 'bottom' + | 'bottom-left' + | 'bottom-right' + | 'left' + | 'left-top' + | 'left-bottom' + | 'right' + | 'right-top' + | 'right-bottom' + | 'center'; + /** + * tip 最大宽度,默认为 '276px' + * + * @default '276px' + */ + tipMaxWidth?: string; + /** + * 失焦校验或密码校验时,自定义错误信息展示区域 + */ + errorMessageWrapper?: Element; + /** + * 密码校验配置项,支持两个可选参数:
    + * validator?: TiPasswordValidator。关于 TiPasswordValidator,请查看下方对 TiPasswordValidator 接口的说明。
    + * levelFn?: (value:string, validator: TiPasswordValidator) => number。value 为用户输入的内容;关于 TiPasswordValidator,请查看下方对 TiPasswordValidator 接口的说明;返回值为当前输入内容对应的强度等级,0:弱、1:中、2:强 + */ + passwordConfig?: { + validator?: TiPasswordValidatorConfig; + levelFn?(value: string, validator: TiPasswordValidatorConfig): number; + }; +} +/** + * 密码校验规则定义接口 + */ +export interface TiPasswordValidatorConfig { + /** + * 校验规则名称 + */ + rule?: string; + /** + * 自定义校验规则的参数。key 值支持:rangeSize、minCharType 以及自定义规则;value 为需要向校验规则中传递的参数。
    + * rangeSize:定义长度范围; + * minCharType:至少需要的类型数量,类型包括数字、小写字母、大写字母、特殊字符。 + */ + params?: { + [propName: string]: Array; + }; + /** + * 自定义规则对应的提示信息。key 值支持:rangeSize、minCharType 以及自定义规则;value 为对应的提示信息内容。
    + * 支持在提示信息中通过{index: number}获取校验规则中的参数,如{0}代表第1个参数,{1}代表第2个参数 + */ + message?: { + [propName: string]: string; + }; +} + +/** + * + * check方法内部使用的校验方法是AbstractControl的updateValueAndValidity方法 + * 此处将方法参数开放给开发者,onlySelf和emitEvent属性意义具体可参考https://angular.io/api/forms/AbstractControl#updatevalueandvalidity + */ +export interface TiValidationCheckConfig { + /** + * 应用更新和有效性检查后如何传递状态。值为 false 标记所有直系祖先,值为 true 只标记当前控件。默认为 false + */ + onlySelf?: boolean; + /** + * 应用更新和有效性检查后是否回调相关函数。当值为 false 时不会回调 statusChanges、valueChanges 函数。默认为 true + */ + emitEvent?: boolean; + /** + * 指定要忽略校验的表单项键值 + */ + ignoreNames?: Array; + /** + * 是否对错误信息数据结构做扁平化处理。默认为 false。 + */ + errorsFlatted?: boolean; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} diff --git a/src/validation/lib/src/TiValidationModule.ts b/src/validation/lib/src/TiValidationModule.ts new file mode 100644 index 0000000..cda34a7 --- /dev/null +++ b/src/validation/lib/src/TiValidationModule.ts @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiLoadingModule } from '@opentiny/ng-loading'; +import { TiValidationDirective } from './TiValidationDirective'; +import { TiPwdMsgComponent } from './TiPwdMsgComponent'; +import { TiErrorMsgComponent } from './TiErrorMsgComponent'; +import { TiPendingStateComponent } from './TiPendingStateComponent'; +import { MaxLengthValidatorDirective } from './validators/directives/MaxLengthValidatorDirective'; +import { RangeSizeValidatorDirective } from './validators/directives/RangeSizeValidatorDirective'; +import { RangeValueValidatorDirective } from './validators/directives/RangeValueValidatorDirective'; +import { MinLengthValidatorDirective } from './validators/directives/MinLengthValidatorDirective'; +import { EmailValidatorDirective } from './validators/directives/EmailValidatorDirective'; +import { NotScriptValidatorDirective } from './validators/directives/NotScriptValidatorDirective'; +import { ContainsValidatorDirective } from './validators/directives/ContainsValidatorDirective'; +import { NotContainsValidatorDirective } from './validators/directives/NotContainsValidatorDirective'; +import { DateValidatorDirective } from './validators/directives/DateValidatorDirective'; +import { DigitsValidatorDirective } from './validators/directives/DigitsValidatorDirective'; +import { EqualValidatorDirective } from './validators/directives/EqualValidatorDirective'; +import { Ipv4ValidatorDirective } from './validators/directives/Ipv4ValidatorDirective'; +import { Ipv6ValidatorDirective } from './validators/directives/Ipv6ValidatorDirective'; +import { MaxValueValidatorDirective } from './validators/directives/MaxValueValidatorDirective'; +import { IntegerValidatorDirective } from './validators/directives/IntegerValidatorDirective'; +import { MinValueValidatorDirective } from './validators/directives/MinValueValidatorDirective'; +import { NotEqualValidatorDirective } from './validators/directives/NotEqualValidatorDirective'; +import { NumberValidatorDirective } from './validators/directives/NumberValidatorDirective'; +import { PasswordValidatorDirective } from './validators/directives/PasswordValidatorDirective'; +import { PortValidatorDirective } from './validators/directives/PortValidatorDirective'; +import { RegExpValidatorDirective } from './validators/directives/RegExpValidatorDirective'; +import { UrlValidatorDirective } from './validators/directives/UrlValidatorDirective'; +import { RequiredValidatorDirective } from './validators/directives/RequiredValidatorDirective'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { BaseValidator } from './validators/directives/BaseValidator'; +import { MaxValueByStringValidatorDirective } from './validators/directives/MaxValueByStringValidatorDirective'; +import { MinValueByStringValidatorDirective } from './validators/directives/MinValueByStringValidatorDirective'; +import { RangeValueByStringValidatorDirective } from './validators/directives/RangeValueByStringValidatorDirective'; +import { BigIntegerValidatorDirective } from './validators/directives/BigIntegerValidatorDirective'; +import { BigNumberValidatorDirective } from './validators/directives/BigNumberValidatorDirective'; +import { BigDigitsValidatorDirective } from './validators/directives/BigDigitsValidatorDirective'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +@NgModule({ + imports: [ + CommonModule, + TiIconModule, + TiRendererModule, + TiTipModule, // 引用TiTipModule,可以使用Tip指令和Tip服务 + TiLoadingModule + ], + // 下面必须写两遍,因为生产环境不允许编译之前执行函数。 + exports: [ + BaseValidator, + TiErrorMsgComponent, + TiPendingStateComponent, + TiValidationDirective, // 额外加的 + MaxLengthValidatorDirective, + RangeSizeValidatorDirective, + RangeValueValidatorDirective, + MinLengthValidatorDirective, + EmailValidatorDirective, + NotScriptValidatorDirective, + ContainsValidatorDirective, + NotContainsValidatorDirective, + DateValidatorDirective, + DigitsValidatorDirective, + EqualValidatorDirective, + Ipv4ValidatorDirective, + Ipv6ValidatorDirective, + MaxValueValidatorDirective, + IntegerValidatorDirective, + MinValueValidatorDirective, + NotEqualValidatorDirective, + NumberValidatorDirective, + PasswordValidatorDirective, + PortValidatorDirective, + RegExpValidatorDirective, + UrlValidatorDirective, + RequiredValidatorDirective, + MaxValueByStringValidatorDirective, + MinValueByStringValidatorDirective, + RangeValueByStringValidatorDirective, + BigIntegerValidatorDirective, + BigNumberValidatorDirective, + BigDigitsValidatorDirective + ], + declarations: [ + BaseValidator, + TiValidationDirective, + TiPwdMsgComponent, + TiPendingStateComponent, + TiErrorMsgComponent, // 额外加的 + MaxLengthValidatorDirective, + RangeSizeValidatorDirective, + RangeValueValidatorDirective, + MinLengthValidatorDirective, + EmailValidatorDirective, + NotScriptValidatorDirective, + ContainsValidatorDirective, + NotContainsValidatorDirective, + DateValidatorDirective, + DigitsValidatorDirective, + EqualValidatorDirective, + Ipv4ValidatorDirective, + Ipv6ValidatorDirective, + MaxValueValidatorDirective, + IntegerValidatorDirective, + MinValueValidatorDirective, + NotEqualValidatorDirective, + NumberValidatorDirective, + PasswordValidatorDirective, + PortValidatorDirective, + RegExpValidatorDirective, + UrlValidatorDirective, + RequiredValidatorDirective, + MaxValueByStringValidatorDirective, + MinValueByStringValidatorDirective, + RangeValueByStringValidatorDirective, + BigIntegerValidatorDirective, + BigNumberValidatorDirective, + BigDigitsValidatorDirective + ], + entryComponents: [TiPwdMsgComponent, TiErrorMsgComponent, TiPendingStateComponent] +}) +export class TiValidationModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiValidationDirective } from './TiValidationDirective'; +export { TiPasswordValidatorConfig, TiValidationConfig, TiValidationType, TiValidationCheckConfig } from './TiValidationInterface'; +export { TiValidators } from './validators/TiValidators'; +export { BaseValidator } from './validators/directives/BaseValidator'; +export { TiErrorMsgComponent } from './TiErrorMsgComponent'; +export { TiPendingStateComponent } from './TiPendingStateComponent'; +export { MaxLengthValidatorDirective } from './validators/directives/MaxLengthValidatorDirective'; +export { RangeSizeValidatorDirective } from './validators/directives/RangeSizeValidatorDirective'; +export { RangeValueValidatorDirective } from './validators/directives/RangeValueValidatorDirective'; +export { MinLengthValidatorDirective } from './validators/directives/MinLengthValidatorDirective'; +export { EmailValidatorDirective } from './validators/directives/EmailValidatorDirective'; +export { NotScriptValidatorDirective } from './validators/directives/NotScriptValidatorDirective'; +export { ContainsValidatorDirective } from './validators/directives/ContainsValidatorDirective'; +export { NotContainsValidatorDirective } from './validators/directives/NotContainsValidatorDirective'; +export { DateValidatorDirective } from './validators/directives/DateValidatorDirective'; +export { DigitsValidatorDirective } from './validators/directives/DigitsValidatorDirective'; +export { EqualValidatorDirective } from './validators/directives/EqualValidatorDirective'; +export { Ipv4ValidatorDirective } from './validators/directives/Ipv4ValidatorDirective'; +export { Ipv6ValidatorDirective } from './validators/directives/Ipv6ValidatorDirective'; +export { MaxValueValidatorDirective } from './validators/directives/MaxValueValidatorDirective'; +export { IntegerValidatorDirective } from './validators/directives/IntegerValidatorDirective'; +export { MinValueValidatorDirective } from './validators/directives/MinValueValidatorDirective'; +export { NotEqualValidatorDirective } from './validators/directives/NotEqualValidatorDirective'; +export { NumberValidatorDirective } from './validators/directives/NumberValidatorDirective'; +export { PasswordValidatorDirective } from './validators/directives/PasswordValidatorDirective'; +export { PortValidatorDirective } from './validators/directives/PortValidatorDirective'; +export { RegExpValidatorDirective } from './validators/directives/RegExpValidatorDirective'; +export { UrlValidatorDirective } from './validators/directives/UrlValidatorDirective'; +export { RequiredValidatorDirective } from './validators/directives/RequiredValidatorDirective'; +export { MaxValueByStringValidatorDirective } from './validators/directives/MaxValueByStringValidatorDirective'; +export { MinValueByStringValidatorDirective } from './validators/directives/MinValueByStringValidatorDirective'; +export { RangeValueByStringValidatorDirective } from './validators/directives/RangeValueByStringValidatorDirective'; +export { BigIntegerValidatorDirective } from './validators/directives/BigIntegerValidatorDirective'; +export { BigNumberValidatorDirective } from './validators/directives/BigNumberValidatorDirective'; +export { BigDigitsValidatorDirective } from './validators/directives/BigDigitsValidatorDirective'; diff --git a/src/validation/lib/src/checkHandle/AsyncCheck.ts b/src/validation/lib/src/checkHandle/AsyncCheck.ts new file mode 100644 index 0000000..b9ae069 --- /dev/null +++ b/src/validation/lib/src/checkHandle/AsyncCheck.ts @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Util } from '@opentiny/ng-utils'; +import { CheckHandle } from './CheckHandle'; +import { NgControl, ValidationErrors } from '@angular/forms'; +import { ComponentRef, ElementRef } from '@angular/core'; +import { TiPendingStateComponent } from '../TiPendingStateComponent'; + +/** + * @ignore + * + */ +export class AsyncCheck extends CheckHandle { + // 处理异步校验错误提示信息 + public onStatusChange(ele: ElementRef, formControl: NgControl): void { + // 清除先前校验信息 + this.commonHandle.clearValidMsg(ele); + + const errors: ValidationErrors = formControl.control.errors; + if (!errors) { + return; + } + + const ruleKey: string = Object.keys(errors)[0]; + const ruleErrors: any = errors[ruleKey]; + // 添加异步校验错误提示信息 + if (typeof ruleErrors === 'object' && Util.isString(ruleErrors.tiAsyncErrorMessage)) { + this.commonHandle.addValidMsg(ele, this.validationConf, formControl, true); + } + } + + // 处理异步校验pending状态时的loading图标 + public onAsyncStatusChange(ele: ElementRef, formControl: NgControl): void { + if (formControl.pending) { + this.addPendingIcon(ele, formControl); + } else { + this.clearPendingIcon(ele); + } + } + + public addPendingIcon(ele: ElementRef, formControl: NgControl): void { + if (ele.nativeElement.tiPendingStateRef) { + return; + } + const pendingStateRef: ComponentRef = this.getPendingStateRef(ele); + const pendingDom: Element = pendingStateRef.location.nativeElement; + if (pendingDom === null || pendingDom.childNodes.length === 0) { + return; + } + this.commonHandle._tiRenderer.insertAfter(pendingDom, ele.nativeElement); + + ele.nativeElement.tiPendingStateRef = pendingStateRef; + } + + public clearPendingIcon(ele: ElementRef): void { + const pendingStateRef: ComponentRef = ele.nativeElement.tiPendingStateRef; + if (pendingStateRef) { + pendingStateRef.destroy(); + ele.nativeElement.tiPendingStateRef = undefined; + } + } + + private getPendingStateRef(ele: ElementRef): ComponentRef { + const pendignComponentRef: ComponentRef = this.commonHandle._tiPopupService.createCompoentRef({ + componentType: TiPendingStateComponent, + context: { + validElement: ele.nativeElement + } + }); + + return pendignComponentRef; + } +} diff --git a/src/validation/lib/src/checkHandle/BlurCheck.ts b/src/validation/lib/src/checkHandle/BlurCheck.ts new file mode 100644 index 0000000..3e569c1 --- /dev/null +++ b/src/validation/lib/src/checkHandle/BlurCheck.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Util } from '@opentiny/ng-utils'; +import { CheckHandle } from './CheckHandle'; +import { NgControl } from '@angular/forms'; +import { ElementRef } from '@angular/core'; +import { TiValidationConfig } from '../TiValidationInterface'; +/** + * @ignore + */ +export class BlurCheck extends CheckHandle { + public onStatusChange(ele: ElementRef, formControl: NgControl): void { + if (this.commonHandle.isFocused(ele)) { + // 获取焦点/正在输入情况下,显示/修改tip提示 + this.addTip(ele, this.validationConf, formControl); + + return; + } + // 未输入过且未聚焦的情况下,先清除先前校验信息(存在reset即改变untounced状态情况下清除校验信息) + if (formControl.untouched) { + this.commonHandle.clearValidMsg(ele); // 清除先前校验信息 + + return; + } + // 失焦情况下,根据校验结果,显示/清除错误提示 + this.commonHandle.clearValidMsg(ele); // 清除先前校验信息 + this.commonHandle.addValidMsg(ele, this.validationConf, formControl); + } + public onFocus(ele: ElementRef, formControl: NgControl): void { + this.commonHandle.clearValidMsg(ele); // 清除先前错误提示信息 + this.addTip(ele, this.validationConf, formControl); + } + public onBlur(ele: ElementRef, formControl: NgControl): void { + this.commonHandle.destroyTip(ele); // 清除先前校验信息 + this.commonHandle.addValidMsg(ele, this.validationConf, formControl); + } + + public addTip(ele: ElementRef, validationConf: TiValidationConfig, formControl: NgControl): void { + // 该方法子类会复写 + this.commonHandle.destroyTip(ele); // 清除先前tip提示信息 + const tipContent: string = validationConf && validationConf.tip; + // tip内容无效情况下不做处理 + if (tipContent === '' || Util.isUndefined(tipContent)) { + return; + } + // 添加Tip提示并缓存 + this.commonHandle.generateTip(ele, tipContent, validationConf); + } +} diff --git a/src/validation/lib/src/checkHandle/ChangeCheck.ts b/src/validation/lib/src/checkHandle/ChangeCheck.ts new file mode 100644 index 0000000..feb5590 --- /dev/null +++ b/src/validation/lib/src/checkHandle/ChangeCheck.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Util } from '@opentiny/ng-utils'; +import { AsyncCheck } from './AsyncCheck'; +import { NgControl, ValidationErrors } from '@angular/forms'; +import { ElementRef } from '@angular/core'; +import { TiValidationConfig } from '../TiValidationInterface'; + +/** + * @ignore + */ +export class ChangeCheck extends AsyncCheck { + public onStatusChange(ele: ElementRef, formControl: NgControl): void { + super.onStatusChange(ele, formControl); + this._checkHandle(ele, this.validationConf, formControl); + } + public onFocus(ele: ElementRef, formControl: NgControl): void { + this._checkHandle(ele, this.validationConf, formControl); + } + public onBlur(ele: ElementRef, formControl: NgControl): void { + // 清除先前校验信息,已失焦情况不做校验提示或tip提示,只边框标红,边框标红在样式类中通过ng-invalid处理 + this.commonHandle.destroyTip(ele); + } + + // 校验处理函数,该函数覆盖了focus和statusChange状态下的校验处理 + private _checkHandle(ele: ElementRef, validationConf: TiValidationConfig, formControl: NgControl): void { + this.commonHandle.destroyTip(ele); // 清除先前校验信息 + // 已失焦情况不做校验提示或tip提示,只边框标红,边框标红只在样式类中通过ng-invalid处理,JS中无需处理 + if (!this.commonHandle.isFocused(ele)) { + return; + } + + // 三种情况只做Tip提示: + // 1.初次focus且初次focus情况下输入值未发生改变时不进行校验 + // (初次focus值一旦发生变化且出错情况下,就要改变为错误提示) + // 2.校验正确 + // 3.是弹窗中autofocus的元素 + // (弹窗中autofocus的元素,在弹窗打开动画结束后会先blur再focus,会使其由untouched变成touched, + // 但需要使其表现与初次focus一致) + const errors: ValidationErrors = formControl.control.errors; + if ((formControl.pristine && formControl.untouched) || errors === null || ele.nativeElement.querySelector('[tiautofocusinmodal]')) { + this._addTip(ele, validationConf, validationConf && validationConf.tip); + + return; + } + // 其它情况下做错误提示 + this._addValidTip(ele, errors, validationConf); + } + + // 显示错误提示信息 + private _addValidTip(ele: ElementRef, errors: ValidationErrors, validationConf: TiValidationConfig): void { + const tipContentDom: any = this.commonHandle.getErrorMsg(errors, validationConf, true); + if (tipContentDom === null) { + // 无错误信息情况下,不做处理 + return; + } + this._addTip(ele, validationConf, tipContentDom); + } + + // 显示tip提示 + private _addTip(ele: ElementRef, validationConf: TiValidationConfig, tipContent: string): /* ViewContainerData */ void { + // tip内容无效情况下不做处理 + if (tipContent === '' || Util.isUndefined(tipContent)) { + return; + } + // 添加Tip提示 + this.commonHandle.generateTip(ele, tipContent, validationConf); + } +} diff --git a/src/validation/lib/src/checkHandle/CheckHandle.ts b/src/validation/lib/src/checkHandle/CheckHandle.ts new file mode 100644 index 0000000..0d0099f --- /dev/null +++ b/src/validation/lib/src/checkHandle/CheckHandle.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgControl } from '@angular/forms'; +import { ElementRef } from '@angular/core'; +import { TiValidationConfig } from '../TiValidationInterface'; + +/** + * @ignore + */ +export class CheckHandle { + constructor(protected validationConf: TiValidationConfig, protected commonHandle: any) {} + onStatusChange(ele: ElementRef, formControl: NgControl): void {} + onFocus(ele: ElementRef, formControl: NgControl): void {} + onBlur(ele: ElementRef, formControl: NgControl): void {} + destroy(ele: ElementRef): void { + this.commonHandle.destroyTip(ele); + } +} diff --git a/src/validation/lib/src/checkHandle/CheckStyleModule.ts b/src/validation/lib/src/checkHandle/CheckStyleModule.ts new file mode 100644 index 0000000..aa41b5c --- /dev/null +++ b/src/validation/lib/src/checkHandle/CheckStyleModule.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonServiceModule } from './CommonServiceModule'; +// 命名含有Service的Module,都不是对外暴露的Service,是内部使用。外部使用此Service,导入TiDragModule即可 +/** + * @ignore + */ +@NgModule({ + imports: [CommonServiceModule] +}) +export class CheckStyleModule {} diff --git a/src/validation/lib/src/checkHandle/CheckStyleService.ts b/src/validation/lib/src/checkHandle/CheckStyleService.ts new file mode 100644 index 0000000..c95d2fe --- /dev/null +++ b/src/validation/lib/src/checkHandle/CheckStyleService.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +// 该功能类用于根据校验方式定义校验实体执行函数 +import { Injectable } from '@angular/core'; +import { ChangeCheck } from './ChangeCheck'; +import { BlurCheck } from './BlurCheck'; +import { PwdCheck } from './PwdCheck'; +import { RadiobaseCheck } from './RadiobaseCheck'; +import { CommonService } from './CommonService'; +import { TiValidationDefaultConfig } from '../TiValidationConfig'; +import { Util } from '@opentiny/ng-utils'; +import { TiValidationConfig, TiValidationType } from '../TiValidationInterface'; +import { CheckStyleModule } from './CheckStyleModule'; + +/** + * @ignore + */ +@Injectable({ + providedIn: CheckStyleModule +}) +export class CheckStyleService { + constructor(private commonService: CommonService) {} + public createHandler(type: TiValidationType | '', config: TiValidationConfig): any { + let resultType: any = type; + // 设置checkStyle默认值 + if (Util.isUndefined(type)) { + resultType = TiValidationDefaultConfig.type; + // 设置passwordConfig情况下,type默认为'password' + if (!Util.isUndefined(config.passwordConfig)) { + resultType = 'password'; + } + } + // 根据不同的配置,设置校验方式执行函数 + let handle: any; + switch (resultType) { + case 'radiobase': + handle = new RadiobaseCheck(config, this.commonService); + break; + case 'blur': + handle = new BlurCheck(config, this.commonService); + break; + case 'password': + handle = new PwdCheck(config, this.commonService); + break; + default: // 定义为'change'及其他无效方式,均采用change校验方式 + handle = new ChangeCheck(config, this.commonService); + } + + return handle; + } +} diff --git a/src/validation/lib/src/checkHandle/CommonService.ts b/src/validation/lib/src/checkHandle/CommonService.ts new file mode 100644 index 0000000..a203350 --- /dev/null +++ b/src/validation/lib/src/checkHandle/CommonService.ts @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ComponentRef, ElementRef, Injectable, Renderer2 } from '@angular/core'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiPopupService } from '@opentiny/ng-popup'; +import { Util } from '@opentiny/ng-utils'; +import { TiTipRef, TiTipService } from '@opentiny/ng-tip'; +import { TiValidationDefaultConfig } from '../TiValidationConfig'; +import { TiValidationConfig } from '../TiValidationInterface'; +import { TiErrorMsgComponent } from '../TiErrorMsgComponent'; +import { NgControl, ValidationErrors } from '@angular/forms'; +import { CommonServiceModule } from './CommonServiceModule'; + +/** + * @ignore + */ +@Injectable({ + providedIn: CommonServiceModule +}) +export class CommonService { + constructor( + private _renderer: Renderer2, + private _tiRenderer: TiRenderer, + private _tipService: TiTipService, + private _tiPopupService: TiPopupService + ) {} + + // 获取错误信息源字串 + private static _getSourceStr(ruleKey: string, ruleErrors: any, validationConf: TiValidationConfig, isAsync: boolean): string { + // 优先使用tiValidation中的规则项配置errorMsg + const errMsgConf: any = validationConf && validationConf.errorMessage; + if (!Util.isUndefined(errMsgConf) && Util.isString(errMsgConf[ruleKey])) { + return errMsgConf[ruleKey]; + } + + if (typeof ruleErrors === 'object') { + // 获取自定义校验规则中的 tiErrorMsg + if (!isAsync && Util.isString(ruleErrors.tiErrorMessage)) { + return ruleErrors.tiErrorMessage; + } + + // 获取自定义异步校验规则中的 tiAsyncErrorMessage + if (isAsync && Util.isString(ruleErrors.tiAsyncErrorMessage)) { + return ruleErrors.tiAsyncErrorMessage; + } + } + + // 获取到的tiValidation errorMsg无效情况下,使用默认规则提示 + return TiLocale.getLocaleWords().tiValid.errorMsg[ruleKey] || ''; + } + + // 获取错误提示字符串,思路是: + // 1.先获取错误信息源字串(可能带params标识{0}/{1}等); + // 2.将获取的源字串中的参数替换为真实数据 + private static _getErrorStr(errors: ValidationErrors, validationConf: TiValidationConfig, isAsync: boolean): string { + const ruleKey: string = Object.keys(errors)[0]; + const ruleErrors: any = errors[ruleKey]; + const msgStr: string = CommonService._getSourceStr(ruleKey, ruleErrors, validationConf, isAsync); + // 获取错误信息参数,无参数情况下,不需做格式匹配直接返回 + // errors格式示例:{required:true,{'maxlength': {'requiredLength': maxLength, 'actualLength': length}}} + if (typeof ruleErrors !== 'object') { + return msgStr; + } + const params: Array = Object.values(ruleErrors); // 此处对错误信息定义有要求:要求错误返回对象与词条中的参数次序与一致 + + return Util.formatEntry(msgStr, params); + } + public isFocused(ele: any): boolean { + return ele.nativeElement.attributes.tiFocused !== undefined; + } + // 获取密码校验中校验规则信息 + public getMsg(ruleKey: string, params: any): string { + const messageStr: string = TiLocale.getLocaleWords().tiValid.message[ruleKey]; + + return Util.formatEntry(messageStr, params); + } + // 获取错误提示信息DOM文本,此处TiErrorMsgComponent组件处理的不仅是提示信息,还包括错误时的组件边框变红样式加载 + public getErrorMsg( + errors: ValidationErrors, + validationConf: TiValidationConfig, + appendToTip: boolean = false, + isAsync?: boolean + ): Element { + const errorMsg: string = CommonService._getErrorStr(errors, validationConf, isAsync); + if (!errorMsg) { + return null; + } + const errorMsgComponentRef: ComponentRef = this._tiPopupService.createCompoentRef({ + componentType: TiErrorMsgComponent, + context: { + errorMessage: errorMsg, + appendToTip + } + }); + + return errorMsgComponentRef.location.nativeElement; + } + + // 调用ti-Tip组件生成Tip提示,该公共方法中同时传递了tip的位置信息等 + public generateTip(ele: ElementRef, tipContent: string, validationConf: TiValidationConfig, context?: any): TiTipRef { + const tipInstance: TiTipRef = this._tipService.create(ele.nativeElement, { + position: validationConf.tipPosition || TiValidationDefaultConfig.tipPosition, + maxWidth: validationConf.tipMaxWidth + }); + tipInstance.show(tipContent, context); + ele.nativeElement.tiValidTip = tipInstance; + + return tipInstance; + } + // 销毁Tip提示 + public destroyTip(ele: ElementRef): void { + const tipEle: TiTipRef = ele.nativeElement.tiValidTip; + if (!Util.isUndefined(tipEle)) { + tipEle.hide(); + } + } + // 根据errors结果生成可占位的错误提示信息(blurCheck和pwdCheck中使用) + public addValidMsg(ele: ElementRef, validationConf: TiValidationConfig, formControl: NgControl, isAsync?: boolean): void { + const errors: ValidationErrors = formControl.control.errors; + // 校验正确情况下不做处理 + if (errors === null) { + return; + } + const errorDom: Element = this.getErrorMsg(errors, validationConf, false, isAsync); + if (errorDom === null || errorDom.childNodes.length === 0) { + return; + } + // 添加错误信息 + if (validationConf && validationConf.errorMessageWrapper) { + this._renderer.appendChild(validationConf.errorMessageWrapper, errorDom); + } else if (ele.nativeElement.hasAttribute('tiTextarea')) { + this._tiRenderer.insertAfter(errorDom, this._renderer.parentNode(ele.nativeElement)); + } else { + this._tiRenderer.insertAfter(errorDom, ele.nativeElement); + } + ele.nativeElement.tiErrorMessage = errorDom; + } + // 销毁可占位的错误提示信息(blurCheck和pwdCheck中使用) + public clearValidMsg(ele: ElementRef): void { + const errMsgDom: any = ele.nativeElement.tiErrorMessage; + if (!Util.isUndefined(errMsgDom)) { + this._renderer.removeChild(errMsgDom.parentNode, errMsgDom); + } + } +} diff --git a/src/validation/lib/src/checkHandle/CommonServiceModule.ts b/src/validation/lib/src/checkHandle/CommonServiceModule.ts new file mode 100644 index 0000000..cb79bb5 --- /dev/null +++ b/src/validation/lib/src/checkHandle/CommonServiceModule.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +// 命名含有Service的Module,都不是对外暴露的Service,是内部使用。外部使用此Service,导入TiDragModule即可 +/** + * @ignore + */ +@NgModule({}) +export class CommonServiceModule {} diff --git a/src/validation/lib/src/checkHandle/PwdCheck.ts b/src/validation/lib/src/checkHandle/PwdCheck.ts new file mode 100644 index 0000000..ca82587 --- /dev/null +++ b/src/validation/lib/src/checkHandle/PwdCheck.ts @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Util } from '@opentiny/ng-utils'; +import { TiPwdMsgComponent } from '../TiPwdMsgComponent'; +import { BlurCheck } from './BlurCheck'; +import { CheckHandle } from './CheckHandle'; +import { TiValidationDefaultConfig } from '../TiValidationConfig'; +import { TiPasswordValidatorConfig, TiValidationConfig } from '../TiValidationInterface'; +import { NgControl } from '@angular/forms'; +import { ElementRef } from '@angular/core'; + +/** + * @ignore + */ +export class PwdCheck extends BlurCheck implements CheckHandle { + private msgItems: any; + private validator: TiPasswordValidatorConfig; + private levelFn: (value: string, validator: TiPasswordValidatorConfig) => void; + constructor(private config: TiValidationConfig, private common: any) { + super(config, common); + const pwdConfig: any = this.config && this.config.passwordConfig; + // 设置参数,用于规则提示组件数据组装 + this._setValidator(pwdConfig); + this._setMsgItems(); + this._setLevelFn(pwdConfig); + } + + onFocus(ele: ElementRef, formControl: NgControl): void { + // 由于密码组件需要依赖于其他输入框,因此在focus时需要更新其状态 + formControl.control.updateValueAndValidity(); + super.onFocus(ele, formControl); + } + + // 设置validator params和msg + private _setValidator(pwdConfig: any): void { + let pwdValidator: any = TiValidationDefaultConfig.pwdValidator; + if (pwdConfig && pwdConfig.validator) { + // validator定义情况下,处理validator + const pwdValidatorConfig: TiPasswordValidatorConfig = pwdConfig.validator; + if (Util.isUndefined(pwdValidatorConfig.rule) || pwdValidatorConfig.rule === pwdValidator.rule) { + // 依然使用默认规则情况下,只做参数合并 + pwdValidator = { + params: { ...pwdValidator.params, ...pwdValidatorConfig.params }, + message: { ...pwdValidator.message, ...pwdValidatorConfig.message } + }; + } else { + pwdValidator = { + params: pwdValidatorConfig.params, + message: pwdValidatorConfig.message + }; + } + } + this.validator = pwdValidator; + } + + // 设置msgItems,msgItems数据格式为:{ruleKey: msgStr,...} + // 其中msgStr为格式化后的页面可显示字串 + private _setMsgItems(): void { + const validatorConfig: TiPasswordValidatorConfig = this.validator; + const msgConfig: any = validatorConfig.message; + const paramsConfig: any = validatorConfig.params; + let msgStr: string; + const msgItems: any = {}; + // msgConfig中定义的规则提示才会在页面中显示 + for (const key in msgConfig) { + if (Object.prototype.hasOwnProperty.call(msgConfig, key)) { + // 规则提示优先从config中读取,如果读取到的是无效字串,则从国际化默认配置中读取 + msgStr = msgConfig[key] || this.common.getMsg(key); + // 如果对应的规则有参数的情况下,则根据参数做字串格式化 + if (paramsConfig && Object.prototype.hasOwnProperty.call(paramsConfig, key)) { + msgStr = Util.formatEntry(msgStr, paramsConfig[key]); + } + msgItems[key] = msgStr; + } + } + this.msgItems = msgItems; + } + + // 设置levelFn:levelFn支持业务自定义 + private _setLevelFn(pwdConfig: any): void { + let levelFn: (value: string, validator: TiPasswordValidatorConfig) => void = TiValidationDefaultConfig.pwdLevelFn; + if (pwdConfig && Util.isFunction(pwdConfig.levelFn)) { + levelFn = pwdConfig.levelFn; + } + this.levelFn = levelFn; + } + public addTip(ele: ElementRef, validationConf: TiValidationConfig, formControl: NgControl): void { + this.commonHandle.destroyTip(ele); // 清除先前提示信息 + const inputsObj: any = { + msgItems: this.msgItems, + validator: this.validator, + control: formControl, + levelFn: this.levelFn + }; + // tip默认值设置 + const configAssigned: TiValidationDefaultConfig = { + tipPosition: TiValidationDefaultConfig.pwdTipPostion, + tipMaxWidth: TiValidationDefaultConfig.pwdTipMaxWidth, + ...validationConf + }; + this.common.generateTip(ele, TiPwdMsgComponent, configAssigned, inputsObj); + } +} diff --git a/src/validation/lib/src/checkHandle/RadiobaseCheck.ts b/src/validation/lib/src/checkHandle/RadiobaseCheck.ts new file mode 100644 index 0000000..b0f434c --- /dev/null +++ b/src/validation/lib/src/checkHandle/RadiobaseCheck.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ElementRef } from '@angular/core'; +import { NgControl, NgModel } from '@angular/forms'; +import { TiValidationConfig } from '../TiValidationInterface'; +import { CheckHandle } from './CheckHandle'; + +export class RadiobaseCheck extends CheckHandle { + constructor(protected validationConf: TiValidationConfig, protected commonHandle: any) { + super(validationConf, commonHandle); + } + public onStatusChange(ele: ElementRef, formControl: NgControl): void { + if (formControl instanceof NgModel && !ele.nativeElement.isInit) { + ele.nativeElement.isInit = true; + + return; + } + this.commonHandle.clearValidMsg(ele, this.validationConf, formControl); + if (formControl.invalid) { + this.commonHandle.addValidMsg(ele, this.validationConf, formControl); + } + } + + public destroy(ele: ElementRef): void { + this.commonHandle.clearValidMsg(ele); + } +} diff --git a/src/validation/lib/src/checkHandle/TiPwdConfig.ts b/src/validation/lib/src/checkHandle/TiPwdConfig.ts new file mode 100644 index 0000000..d4cdac5 --- /dev/null +++ b/src/validation/lib/src/checkHandle/TiPwdConfig.ts @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Util } from '@opentiny/ng-utils'; +import { TiPasswordValidatorConfig } from '../TiValidationInterface'; +/** + * @ignore + */ +export interface TiPwdParams { + minCharLen: number; + minCharTypeNum: number; + charTypeRegObj: any; +} +/** + * @ignore + */ +export class TiPwdConfig { + public static pwdTipPostion: string = 'right'; + public static pwdTipMaxWidth: string = '440px'; + // pwd校验规则 + public static pwdCharTypeRegObj: any = { + // 默认字串类型集合,特殊规则可在此处进行配置 + // specialCharReg: /[~`!\?,.:;\-_'"\(\)\{\}\[\]\/<>@#\$%\^&\*\+\|\\=\s]/, + // https://www.bejson.com/zhuanyi/ Json转义工具有错,单引号未转义 + // eslint-disable-next-line prefer-regex-literals + specialCharReg: new RegExp('[~`!\\?,.:;\\-_\'"\\(\\)\\{\\}\\[\\]\\/<>@#\\$%\\^&\\*\\+\\|\\\\=\\s]'), + // eslint-disable-next-line prefer-regex-literals + lowerCharReg: new RegExp('[a-z]+'), + // eslint-disable-next-line prefer-regex-literals + upperCharReg: new RegExp('[A-Z]+'), + // eslint-disable-next-line prefer-regex-literals + digitsCharReg: new RegExp('[0-9]+') + }; + public static pwdValidator: any = { + rule: 'password', + params: { + rangeSize: [6, 32], + minCharType: [2] // 该校验方法支持两个参数传递,第二个参数为charTypeRegObj,不传递第二个参数情况下,代表使用该配置中的默认值 + }, + message: { + rangeSize: '', // ''代表使用组件国际化语言中的默认提示文本 + minCharType: '', + notEqualPosRev: '' + } + }; + // /** + // * 计算密码熵值,具体的计算密码熵值的算法如下: + // * 1、第一个字符为4bits + // * 2、2-8个字符,每一个字符为2bits + // * 3、9-20个字符之间,每一个字符为1.5bits + // * 4、21个字符之后,每一个字符为1bit + // * 5、如果密码中同时存在大写字母和特殊字符,则熵值+6 + // * @param {String} str 密码字串 + // * @param {Number} minLen 密码最小长度 + // * @param {Number} minType 密码最小组合种类 + // * @returns {Number} level 密码等级 + // */ + public static pwdLevelFn(value: string, validator: TiPasswordValidatorConfig): number { + const validatorParam: TiPwdParams = getValidatorParam(); + const minCharLen: number = validatorParam.minCharLen; + const minCharTypeNum: number = validatorParam.minCharTypeNum; + const charTypeRegObj: any = validatorParam.charTypeRegObj as any; + // 根据字符个数初步计算熵值 + const len: number = value.length; + let pwdScore: number = 0; + if (len >= 21) { + pwdScore = len - 21 + 1.5 * 12 + 2 * 7 + 4; + } else if (len >= 9 && len < 21) { + pwdScore = (len - 9 + 1) * 1.5 + 2 * 7 + 4; + } else if (len >= 2 && len < 9) { + pwdScore = (len - 2 + 1) * 2 + 4; + } else if (len >= 1 && len < 2) { + pwdScore = 4; + } + // 同时存在大写字母和特殊字符,熵值+6 + if (charTypeRegObj.upperCharReg && charTypeRegObj.upperCharReg.test(value) && charTypeRegObj.specialCharReg.test(value)) { + pwdScore += 6; + + // 重置正则表达式的lastIndex属性:只有正则表达式使用了表示全局检索的 "g" 标志时,该属性才会起作用, + // 当前正则匹配到字串后,下次使用同样的正则匹配该字串时,正则检查位置会发生变化 + charTypeRegObj.upperCharReg.lastIndex = 0; + charTypeRegObj.specialCharReg.lastIndex = 0; + } + + // 根据熵值计算等级 + return countLevel(pwdScore); + + function getValidatorParam(): TiPwdParams { + const params: any = validator.params; + // 默认值设定 + let minCharLength: number = 0; + let minCharTypeNumber: number = 0; + let charTypeRegObject: any = { + // 默认字串类型集合,特殊规则可在此处进行配置 + specialCharReg: /[~`!\?,.:;\-_'"\(\)\{\}\[\]\/<>@#\$%\^&\*\+\|\\=\s]/, + lowerCharReg: /[a-z]+/, + upperCharReg: /[A-Z]+/, + digitsCharReg: /[0-9]+/ + }; + + // 通过rangeSize规则获取最小长度 + const rangeSizeParams: number = params.rangeSize; + if (!Util.isUndefined(rangeSizeParams)) { + minCharLength = parseInt(rangeSizeParams[0], 10) || 0; + } + // 通过minCharType规则获取最小字符类型及其正则 + const minCharTypeParams: number = params.minCharType; + if (!Util.isUndefined(minCharTypeParams)) { + minCharTypeNumber = parseInt(minCharTypeParams[0], 10) || 0; + charTypeRegObject = minCharTypeParams[1] || charTypeRegObject; + } + + // 返回对象值 + return { + minCharLen: minCharLength, + minCharTypeNum: minCharTypeNumber, + charTypeRegObj: charTypeRegObject + }; + } + + /** + * 计算密码等级 + * 低: 0~25.5( 小于等于25 .5) + * 中: 25.5~30(大于25 .5 小于等于30) + * 高: 30~(大于30) + * @param score 密码分数 + * @returns 密码等级 + */ + function countLevel(score: number): number { + let level: number = 0; + if (score > 30) { + level = 2; + } else if (score > 25.5 && score <= 30) { + level = 1; + } else if (score >= 0 && score <= 25.5) { + level = 0; + } + level = levelHandle(minCharLen, minCharTypeNum, level); + + return level; + } + + // 密码降级处理 + function levelHandle(psdLen: number, psdType: number, level: number): number { + const type: number = getCharType(value); + let pwdLevel: number = level; + if (level > 0) { + if ((psdLen && len < psdLen) || (psdType && type < psdType)) { + pwdLevel = level - 1; + } + } + + return pwdLevel; + } + + // 获取当前密码字符种类 + function getCharType(str: string): number { + let typeNum: number = 0; + for (const key in charTypeRegObj) { + if (Object.prototype.hasOwnProperty.call(charTypeRegObj, key)) { + const regExp: RegExp = charTypeRegObj[key]; + if (regExp.test(str)) { + typeNum++; + regExp.lastIndex = 0; + } + } + } + + return typeNum; + } + } +} diff --git a/src/validation/lib/src/errorMsg.less b/src/validation/lib/src/errorMsg.less new file mode 100644 index 0000000..41dbd00 --- /dev/null +++ b/src/validation/lib/src/errorMsg.less @@ -0,0 +1,40 @@ +@import '../../../themes/basic/base-all.less'; + +ti-error-msg { + --ti-error-icon-size: var(--ti-common-size-4x); +} + +ti-error-msg { + color: var(--ti-common-color-text-gray); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + line-height: var(--ti-common-line-height-number); +} + +.ti3-error-icon-bg { + width: var(--ti-error-icon-size); + height: var(--ti-error-icon-size); + border-radius: 50%; + text-align: center; + background-color: var(--ti-common-color-error); + margin-right: var(--ti-common-space-2x); + flex-shrink: 0; // 校验文本过多时不允许挤压图标部分 + margin-top: calc( + (var(--ti-common-font-size-base) * var(--ti-common-line-height-number) - var(--ti-error-icon-size)) / 2 + ); // 左侧图标16px 与右侧文字18px 保持垂直居中 +} +.ti3-error-icon { + color: var(--ti-common-color-text-white); + font-size: calc(var(--ti-error-icon-size) * 0.75); + line-height: var(--ti-error-icon-size); +} + +.ti3-unifyvalid-error-container { + color: var(--ti-common-color-error-text); + display: block; // IE9不支持flex,故使用block代替 + margin-top: var(--ti-common-space-2x); +} +.ti3-unifyvalid-tip-error-container { + display: flex; + align-content: center; +} diff --git a/src/validation/lib/src/i18n/TiValidationWords.ts b/src/validation/lib/src/i18n/TiValidationWords.ts new file mode 100644 index 0000000..b8d7e48 --- /dev/null +++ b/src/validation/lib/src/i18n/TiValidationWords.ts @@ -0,0 +1,39 @@ +export interface TiValidationWords { + tiValid: { + errorMsg: { + required: string; + maxLength: string; + minLength: string; + rangeSize: string; + maxValue: string; + minValue: string; + rangeValue: string; + regExp: string; + contains: string; + notContains: string; + notScript: string; + equal: string; + notEqual: string; + port: string; + path: string; + email: string; + date: string; + url: string; + integer: string; + number: string; + digits: string; + ipv4: string; + ipv6: string; + password: string; + }; + message: { + rangeSize: string; + minCharType: string; + notEqualPosRev: string; + }; + passwordStrength: { + securityText: string; + levelDecArr: Array; + }; + }; +} diff --git a/src/validation/lib/src/i18n/en_US.ts b/src/validation/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..043de72 --- /dev/null +++ b/src/validation/lib/src/i18n/en_US.ts @@ -0,0 +1,43 @@ +import { TiValidationWords } from './TiValidationWords'; + +export const en_US: TiValidationWords = { + tiValid: { + errorMsg: { + required: 'This field cannot be left blank.', + maxLength: 'Enter a maximum of {0} characters.', + minLength: 'Enter at least {0} characters.', + rangeSize: 'Enter {0} to {1} characters.', + maxValue: 'Enter a value less than or equal to {0}.', + minValue: 'Enter a value greater than or equal to {0}.', + rangeValue: 'Enter a value from {0} to {1}.', + regExp: 'Invalid value.', + contains: 'The value must contain the following characters: {0}.', + notContains: 'The value cannot contain the following invalid characters: {0}.', + notScript: 'The value cannot contain script tags.', + equal: 'The value must be {0}.', + notEqual: 'The value cannot be {0}.', + port: 'Enter an integer from {0} to {1}.', + path: 'Enter a valid file path.', + email: 'Enter a valid email address.', + date: 'Enter a valid date.', + url: 'Enter a valid URL.', + integer: 'Enter a valid integer.', + number: 'Enter a valid number.', + digits: 'Enter a valid number.', + ipv4: 'Enter a valid IPv4 address.', + ipv6: 'Enter a valid IPv6 address.', + password: 'Invalid password.' + }, + message: { + rangeSize: 'Must be {0} to {1} characters long.', + minCharType: + 'Must contain at least {0} of the following character types: ' + + 'letters, digits, and special characters ( `~!@#$%^&*()-_=+\\|[{}];:\'",<.>/? and spaces). ', + notEqualPosRev: 'Cannot be the username or the username spelled backwards.' + }, + passwordStrength: { + securityText: 'Password Strength:', + levelDecArr: ['Weak', 'Medium', 'Strong'] + } + } +}; diff --git a/src/validation/lib/src/i18n/es_US.ts b/src/validation/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..8513ed5 --- /dev/null +++ b/src/validation/lib/src/i18n/es_US.ts @@ -0,0 +1,43 @@ +import { TiValidationWords } from './TiValidationWords'; + +export const es_US: TiValidationWords = { + tiValid: { + errorMsg: { + required: 'Este campo es obligatorio.', + maxLength: 'Ingrese {0} caracteres como máximo.', + minLength: 'Ingrese {0} caracteres como mínimo.', + rangeSize: 'Ingrese entre {0} y {1} caracteres.', + maxValue: 'Ingrese un valor inferior o igual a {0}.', + minValue: 'Ingrese un valor superior o igual a {0}.', + rangeValue: 'Ingrese un valor entre {0} y {1}.', + regExp: 'Valor no válido.', + contains: 'El valor debe contener los siguientes caracteres: {0}.', + notContains: 'El valor no puede contener los siguientes caracteres no válidos: {0}.', + notScript: 'El valor no puede contener etiquetas de scripts.', + equal: 'El valor debe ser {0}', + notEqual: 'El valor no puede ser {0}.', + port: 'Ingrese un número entero entre {0} y {1}.', + path: 'Ingrese una ruta de archivo válida.', + email: 'Ingrese una dirección de correo electrónico válida.', + date: 'Ingrese una fecha válida.', + url: 'Ingrese un URL válido.', + integer: 'Ingrese un número entero válido.', + number: 'Ingrese un número válido.', + digits: 'Ingrese un número válido.', + ipv4: 'Ingrese una dirección IPv4 válida.', + ipv6: 'Ingrese una dirección IPv6 válida.', + password: 'Contraseña no válida.' + }, + message: { + rangeSize: 'Debe contener entre {0} y {1} caracteres.', + minCharType: + 'Debe contener al menos {0} de los siguientes tipos de caracteres: ' + + 'letras, dígitos y caracteres especiales ( `~!@#$%^&*()-_=+\\|[{}];:\'",<.>/? o espacios). ', + notEqualPosRev: 'No puede ser el nombre de usuario ni el nombre de usuario escrito al revés.' + }, + passwordStrength: { + securityText: 'Nivel de seguridad:', + levelDecArr: ['Bajo', 'Medio', 'Alto'] + } + } +}; diff --git a/src/validation/lib/src/i18n/fr_FR.ts b/src/validation/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..ff28ca7 --- /dev/null +++ b/src/validation/lib/src/i18n/fr_FR.ts @@ -0,0 +1,43 @@ +import { TiValidationWords } from './TiValidationWords'; + +export const fr_FR: TiValidationWords = { + tiValid: { + errorMsg: { + required: 'Ce champs ne peut pas être vide.', + maxLength: 'Saisissez un maximum de {0} caractères.', + minLength: 'Saisissez au moins {0} caractères.', + rangeSize: 'Saisissez les caractères {0} à {1}.', + maxValue: 'Saisissez une valeur inférieure ou égale à {0}.', + minValue: 'Saisissez une valeur supérieure ou égale à {0}.', + rangeValue: 'Saisissez une valeur comprise entre {0} et {1}.', + regExp: 'Valeur non valable.', + contains: 'La valeur doit contenir les caractères suivants : {0}.', + notContains: 'La valeur ne peut pas contenir les caractères non valables suivants : {0}.', + notScript: 'La valeur ne peut pas contenir de script tags.', + equal: "La valeur d'entrée doit être égale à {0}.", + notEqual: 'La valeur ne peut pas être {0}', + port: 'Le numéro de port est un nombre entier allant de {0} à {1}.', + path: 'Entrez un chemin de fichier valide.', + email: 'Entrez une adresse électronique valide.', + date: 'Saisissez une date valide.', + url: 'Saisissez une URL valide.', + integer: 'Saisissez un nombre entier valide.', + number: 'Saisissez un nombre valide.', + digits: 'Saisissez un nombre valide.', + ipv4: 'Saisissez une adresse IPv4 valide.', + ipv6: 'Saisissez une adresse IPv6 valide.', + password: 'Mot de passe non valide.' + }, + message: { + rangeSize: 'Doit comporter de {0} à {1} caractères.', + minCharType: + 'Doit contenir au moins {0} des types de caractères suivants: ' + + 'lettres, chiffres et caractères spéciaux ( `~!@#$%^&*()-_=+|[{}];:\'",<.>/ ? et espaces). ', + notEqualPosRev: "Il ne peut s'agir du nom d'utilisateur ou du nom d'utilisateur orthographié à l'envers." + }, + passwordStrength: { + securityText: 'Force du mot de passe :', + levelDecArr: ['Faible', 'Moyen', 'Fort'] + } + } +}; diff --git a/src/validation/lib/src/i18n/index.ts b/src/validation/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/validation/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/validation/lib/src/i18n/pt_BR.ts b/src/validation/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..777e1d3 --- /dev/null +++ b/src/validation/lib/src/i18n/pt_BR.ts @@ -0,0 +1,43 @@ +import { TiValidationWords } from './TiValidationWords'; + +export const pt_BR: TiValidationWords = { + tiValid: { + errorMsg: { + required: 'Este campo não pode ser deixado em branco.', + maxLength: 'Insira um máximo de {0} caracteres.', + minLength: 'Insira pelo menos {0} caracteres.', + rangeSize: 'Insira de {0} a {1} caracteres.', + maxValue: 'Insira um valor inferior ou igual a {0}.', + minValue: 'Insira um valor superior ou igual a {0}.', + rangeValue: 'Insira um valor de {0} a {1}.', + regExp: 'Valor inválido.', + contains: 'O valor deve conter os caracteres a seguir: {0}.', + notContains: 'O valor não pode conter os caracteres a seguir: {0}.', + notScript: 'O valor não pode conter tags de scripts.', + equal: 'O valor deve ser {0}.', + notEqual: 'O valor não pode ser {0}.', + port: 'Insira um número inteiro de {0} a {1}.', + path: 'Insira um caminho de arquivo válido.', + email: 'Insira um endereço de e-mail válido.', + date: 'Insira uma data válida.', + url: 'Insira uma URL válida.', + integer: 'Insira um número inteiro válido.', + number: 'Insira um número válido.', + digits: 'Insira um número válido.', + ipv4: 'Insira um endereço IPv4 válido.', + ipv6: 'Insira um endereço IPv6 válido.', + password: 'Senha inválida.' + }, + message: { + rangeSize: 'Deve ter de {0} a {1} caracteres.', + minCharType: + 'Deve conter pelo menos {0} dos seguintes tipos de caracteres: ' + + 'letras, dígitos e caracteres especiais ( `~!@#$%^&*()-_=+\\|[{}];:\'",<.>/? e espaços). ', + notEqualPosRev: 'Não pode ser o nome de usuário escrito normalmente ou de trás para frente.' + }, + passwordStrength: { + securityText: 'Força da senha:', + levelDecArr: ['Fraca', 'Média', 'Forte'] + } + } +}; diff --git a/src/validation/lib/src/i18n/zh_CN.ts b/src/validation/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..7f62e88 --- /dev/null +++ b/src/validation/lib/src/i18n/zh_CN.ts @@ -0,0 +1,41 @@ +import { TiValidationWords } from './TiValidationWords'; + +export const zh_CN: TiValidationWords = { + tiValid: { + errorMsg: { + required: '输入不能为空。', + maxLength: '输入长度不能超过{0}个字符。', + minLength: '输入长度不能小于{0}个字符。', + rangeSize: '输入长度范围为{0}到{1}个字符。', + maxValue: '输入值不能超过{0}。', + minValue: '输入值不能小于{0}。', + rangeValue: '输入值必须在{0}到{1}之间。', + regExp: '输入值无效。', + contains: '输入值必须包含有字符{0}。', + notContains: '输入值不能含有非法字符{0}。', + notScript: '输入值不能含有script标签。', + equal: '输入值必须等于{0}。', + notEqual: '输入值不能等于{0}。', + port: '端口号为{0}到{1}的整数。', + path: '输入值未满足路径格式要求。', + email: '输入email地址无效。', + date: '输入日期无效。', + url: '输入URL无效。', + integer: '输入值不是有效整数。', + number: '输入值不是有效数字。', + digits: '输入值不是有效数字字符。', + ipv4: '输入值不是有效IPv4地址。', + ipv6: '输入值不是有效IPv6地址。', + password: '密码输入不符合要求,请重新输入。' + }, + message: { + rangeSize: '{0}~{1}个字符。', + minCharType: '至少包含以下字符中的{0}种:大写字母、小写字母、数字、特殊字符`~!@#$%^&*()-_=+\\|[{}];:\'",<.>/? 和空格。', + notEqualPosRev: '不能与用户名或倒序的用户名相同。' + }, + passwordStrength: { + securityText: '安全程度:', + levelDecArr: ['弱', '中', '强'] + } + } +}; diff --git a/src/validation/lib/src/pending-state.html b/src/validation/lib/src/pending-state.html new file mode 100644 index 0000000..94040cf --- /dev/null +++ b/src/validation/lib/src/pending-state.html @@ -0,0 +1 @@ + diff --git a/src/validation/lib/src/pending-state.less b/src/validation/lib/src/pending-state.less new file mode 100644 index 0000000..40f04c2 --- /dev/null +++ b/src/validation/lib/src/pending-state.less @@ -0,0 +1,37 @@ +:host { + --ti-pending-state-container-height: var(--ti-common-size-7x); + --ti-pending-state-loading-icon-top: calc( + (var(--ti-pending-state-container-height) - var(--ti-common-border-weight-normal) * 2 - var(--ti-common-size-3x)) / 2 + ); // 7px (container高度 - 上下边框 - loading默认高度) / 2 + --ti-pending-state-loading-icon-left: calc((var(--ti-common-space-10) + var(--ti-common-size-3x)) * -1); // -22px 右间距 + loading默认宽度 +} + +:host.ti3-validation-pending-container { + position: relative; // 用于给ti-loading定位,但是宽度要为0,不能占位置,免得影响input后面的元素位置 + vertical-align: top; + display: inline-block; + height: var(--ti-pending-state-container-height); + width: 0; +} + +.ti3-validation-pending-loading { + position: absolute; // 定位,脱离文档流 + top: var(--ti-pending-state-loading-icon-top); + left: var(--ti-pending-state-loading-icon-left); +} + +::ng-deep [tiText].ti3-text-input-show-clear + .ti3-validation-pending-container .ti3-validation-pending-loading { + left: calc( + (var(--ti-common-space-10) + var(--ti-common-size-3x) + var(--ti-common-size-4x)) * -1 + ); // -38px 右间距 + loading默认宽度 + clear默认宽度 +} + +::ng-deep [tiText].ti3-text-input-show-pending { + padding-right: calc(var(--ti-common-space-10) * 2 + var(--ti-common-size-3x)) !important; // 32px 左右间距 + loading默认宽度 +} + +::ng-deep [tiText].ti3-text-input-show-pending.ti3-text-input-show-clear { + padding-right: calc( + var(--ti-common-space-10) * 2 + var(--ti-common-size-3x) + var(--ti-common-size-4x) + ) !important; // 48px 左右间距 + loading默认宽度 + clear默认宽度 +} diff --git a/src/validation/lib/src/pwdMsg.less b/src/validation/lib/src/pwdMsg.less new file mode 100644 index 0000000..19a56b4 --- /dev/null +++ b/src/validation/lib/src/pwdMsg.less @@ -0,0 +1,161 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-valid-pwd-tip-line-height: calc(var(--ti-common-font-size-base) * var(--ti-common-line-height-number)); + --ti-valid-pwd-status-icon-size: var(--ti-common-size-4x); + --ti-valid-pwd-status-null-border-weight: var(--ti-common-border-weight-normal); + --ti-valid-pwd-status-null-border-color: rgba(255, 255, 255, 0.1); + --ti-valid-pwd-status-null-size: calc(var(--ti-valid-pwd-status-icon-size) - var(--ti-valid-pwd-status-null-border-weight) * 2); + --ti-valid-pwd-space: var(--ti-common-space-2x); + --ti-valid-pwd-level-bar-height: 2px; + --ti-valid-pwd-level-bar-item-width: var(--ti-common-size-15x); + --ti-valid-pwd-circle-size: 6px; +} + +.ti3-valid-pwd-tip-container { + line-height: var(--ti-valid-pwd-tip-line-height); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + color: var(--ti-common-color-text-gray); +} + +.ti3-valid-pwd-tip-item { + margin-bottom: var(--ti-common-space-3x); + .clearfix(); +} +.ti3-valid-pwd-tip-status-null { + width: var(--ti-valid-pwd-status-null-size); + height: var(--ti-valid-pwd-status-null-size); + float: left; + margin-right: var(--ti-valid-pwd-space); + border: var(--ti-valid-pwd-status-null-border-weight) var(--ti-common-border-style-solid) var(--ti-valid-pwd-status-null-border-color); + border-radius: var(--ti-valid-pwd-status-null-size); + .box-sizing(content-box); + position: relative; + top: calc((var(--ti-valid-pwd-tip-line-height) - var(--ti-valid-pwd-status-icon-size)) / 2); + // 内嵌圆圈 + &:after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: var(--ti-valid-pwd-circle-size); + height: var(--ti-valid-pwd-circle-size); + background-color: var(--ti-common-color-icon-darkbg-normal); + border-radius: var(--ti-common-border-radius-3); + .box-sizing(border-box); + } +} + +.ti3-valid-pwd-tip-success { + .ti3-valid-pwd-tip(var(--ti-common-color-success)); +} +.ti3-valid-pwd-tip-error { + .ti3-valid-pwd-tip(var(--ti-common-color-error)); +} + +.ti3-valid-pwd-tip-msg { + float: left; + width: calc(100% - var(--ti-valid-pwd-status-icon-size) - var(--ti-valid-pwd-space)); +} +.ti3-valid-pwd-tip-security-text { + float: left; + margin-right: var(--ti-common-space-3x); +} +.ti3-valid-pwd-level-bars { + margin-right: var(--ti-common-space-3x); + float: left; + margin-top: calc(var(--ti-valid-pwd-tip-line-height) / 2); + &.ti3-valid-pwd-level-strong { + .active { + .ti3-valid-pwd-level-active(var(--ti-common-color-success)); + } + } + &.ti3-valid-pwd-level-medium { + .active { + .ti3-valid-pwd-level-active(var(--ti-common-color-warn)); + } + } + &.ti3-valid-pwd-level-weak { + .active { + .ti3-valid-pwd-level-active(var(--ti-common-color-error)); + } + } +} + +.ti3-valid-pwd-level-bar { + float: left; + width: calc(var(--ti-valid-pwd-level-bar-item-width) + var(--ti-valid-pwd-circle-size)); + height: var(--ti-valid-pwd-level-bar-height); + background: var(--ti-common-color-text-weaken); + position: relative; + &:first-child { + width: calc(var(--ti-valid-pwd-level-bar-item-width) + var(--ti-valid-pwd-circle-size) * 2); + &:before { + .ti3-valid-pwd-level-circle(); + left: 0; + } + } + + &:after { + .ti3-valid-pwd-level-circle(); + right: 0; + } +} + +.ti3-valid-pwd-level { + margin-left: calc(var(--ti-valid-pwd-status-icon-size) + var(--ti-valid-pwd-space)); + margin-top: var(--ti-common-space-4x); + color: var(--ti-common-color-text-gray); + .clearfix(); +} +.ti3-valid-pwd-tip-security-status { + float: left; + height: var(--ti-common-size-5x); //设置该字段高度为20px,确保其换行状态下显示状态切换时不会发生跳变 +} + +// 提示图标样式 +.ti3-valid-pwd-tip (@bgColor) { + width: var(--ti-valid-pwd-status-icon-size); + height: var(--ti-valid-pwd-status-icon-size); + font-size: var(--ti-valid-pwd-status-icon-size); + color: var(--ti-common-color-text-gray); + float: left; + margin-right: var(--ti-valid-pwd-space); + border-radius: var(--ti-common-border-radius-3); + margin-top: calc((var(--ti-valid-pwd-tip-line-height) - var(--ti-valid-pwd-status-icon-size)) / 2); + background-color: @bgColor; + text-align: center; + ti-icon[local] { + font-size: calc(var(--ti-valid-pwd-status-icon-size) * 0.75); + vertical-align: top; + line-height: var(--ti-valid-pwd-status-icon-size); + } +} + +// 密码等级激活样式 +.ti3-valid-pwd-level-active (@bgColor) { + background: @bgColor; + &:first-child { + &:before { + background-color: @bgColor; + } + } + + &:after { + background-color: @bgColor; + } +} + +// 密码等级圆圈 +.ti3-valid-pwd-level-circle { + content: ''; + position: absolute; + top: calc((var(--ti-valid-pwd-level-bar-height) - var(--ti-valid-pwd-circle-size)) / 2); + width: var(--ti-valid-pwd-circle-size); + height: var(--ti-valid-pwd-circle-size); + background-color: var(--ti-common-color-text-weaken); + border-radius: var(--ti-common-border-radius-3); + .box-sizing(border-box); +} diff --git a/src/validation/lib/src/validators/TiValidators.ts b/src/validation/lib/src/validators/TiValidators.ts new file mode 100644 index 0000000..fc464a3 --- /dev/null +++ b/src/validation/lib/src/validators/TiValidators.ts @@ -0,0 +1,792 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/* eslint-disable no-eq-null */ +/* eslint-disable eqeqeq */ +import { AbstractControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'; +import { Util } from '@opentiny/ng-utils'; +import { TiPwdConfig } from '../checkHandle/TiPwdConfig'; +import { TiValidationCheckConfig } from '../TiValidationInterface'; + +// 1:比较值高的; 0:比较值相等 ;-1:比较值低的 +type compareResult = 1 | 0 | -1; +// 下面注释,可以避免编译lib时正则报错。原理未知,副作用未知。 +// @dynamic +/** + * Tiny封装的常用校验规则及校验方法 + * + * ### 规则使用 + * **提供了两种使用方式:** + * + * 1.静态方法:通过TiValidators.XXX(规则名)在JS中定义 + * + * 2.指令方式:对应规则的指令名为tiXXX(规则名首字母大写),如required规则的指令名为 tiRequired + * + * **支持的校验规则如下:** + * + * | 规则名称 | 配置参数意义/类型 | 规则描述 | + * | -------- | :----- | :---- | + * | required | 无 | 为空校验 | + * | maxLength | 最大字符长度/number | 字符长度最大值校验 | + * | minLength | 最小字符长度/number | 字符长度最小值校验 | + * | rangeSize | 最小长度限制/number
    最大长度限制/number | 字符长度大小区间校验 | + * | integer | 无 | 整数校验 | + * | number | 无 | 数字校验 | + * | digits | 无 | 自然数校验 | + * | maxValue | 最大数值/number | 数字最大数值校验 | + * | minValue | 最小数值/number | 数字最小数值校验 | + * | rangeValue | 最小数值限制/number
    最大数值限制/number | 数字大小区间校验 | + * | bigInteger | 无 | 校验字符串是否为整数,不支持科学计数法 | + * | bigNumber | 无 | 校验字符串是否为数字,不支持科学计数法 | + * | bigDigits | 无 | 校验字符串是否为自然数,不支持科学计数法 | + * | maxValueByString | 最大数值/string | 最大数值校验(支持任意大小和精度的数字),不支持科学计数法 | + * | minValueByString | 最小数值/string | 最小数值校验(支持任意大小和精度的数字),不支持科学计数法 | + * | rangeValueByString | 最小数值/string | 数值范围校验(支持任意大小和精度的数字),不支持科学计数法 | + * | regExp | 正则表达式参数/RegExp|string(不包括正则表达式头尾标识符'^(?:'、')$') | 正则校验 | + * | email | 无 | 邮箱校验 | + * | contains | 包含的内容/(string/number) | 包含校验 | + * | notContains | 不包含的内容/(string/number) | 不包含校验 | + * | equal | 相等的内容/(string/number/boolean ) | 相等校验 | + * | notEqual | 不相等的内容/(string/number) | 不相等校验 | + * | notScript | 无 | 包含script标签校验 | + * | port | 无 | 端口号校验,范围为0~65535 | + * | date | 无 | 日期类型校验 | + * | url | 无 | url校验 | + * | ipv4 | 无 | ipv4校验 | + * | ipv6 | 无 | ipv6校验 | + * | minCharType | 1. 符合要求的字符种类/number;
    2. 字符集对象类型(可选)/{string:RegExp}。
    默认的字符种类分别为:大写字母、小写字母、数字、特殊字符`~!@#$%^&*()-_=+\\\|[{}];:\'\",<.>/? 和空格 | 符合最小字符种类校验,默认情况为至少包含2种字符类型`。 | + * | notEqualPosRev | 需要比对的表单formControl对象获取函数/ () => AbstractControl | 不能和表单对象的正序或倒序相同 | + * | password | 密码校验各项规则参数/对象形式 | 密码校验 | + * + * + * **password规则的参数类型** + * + * { + * rangeSize?: [number, number], + * minCharType?: [bumber, { + * digitsCharReg: RegExp, + * specialCharReg: RegExp, + * lowerCharReg: RegExp, + * upperCharReg: RegExp}], + * notEqualPosRev: () => AbstractControl + * } + */ +export class TiValidators { + private static readonly EMAIL_REGEXP: RegExp = + /^((([A-Za-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([A-Za-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([A-Za-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([A-Za-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([A-Za-z]|\d|-|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([A-Za-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([A-Za-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([A-Za-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([A-Za-z]|\d|-|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([A-Za-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/; + private static readonly SCRIPT_REGEXP: RegExp = /<+\/?[Ss][Cc][Rr][Ii][Pp][Tt] *.*>*/; + private static readonly DIGITS_REGEXP: RegExp = /^\d+$/; + private static readonly PORT_RANGE: Array = [0, 65535]; + private static readonly INTEGER_REGEXP: RegExp = /^-?\d+$/; + private static readonly NUMBER_REGEXP: RegExp = /^(?:-?\d+|[+-]?[\d]+([\.][\d]+)?([Ee][+-]?[\d]+)?|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/; // 支持科学计数法 + private static readonly BIG_NUMBER_REGEXP: RegExp = /^[-]?([1-9]\d*\.\d+$|0\.\d+$|[1-9]\d*$|0$)/; + // 采用自研的ipv4正则表达式 + private static readonly IPV4_REGEXP: RegExp = + /^(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))$/i; + // 采用自研的ipv6正则表达式 + private static readonly IPV6_REGEXP: RegExp = + /^(((([\da-f]{1,4}):){7}([\da-f]{1,4}))|(((([\da-f]{1,4}):){1,7}:)|((([\da-f]{1,4}):){6}:([\da-f]{1,4}))|((([\da-f]{1,4}):){5}:(([\da-f]{1,4}):)?([\da-f]{1,4}))|((([\da-f]{1,4}):){4}:(([\da-f]{1,4}):){0,2}([\da-f]{1,4}))|((([\da-f]{1,4}):){3}:(([\da-f]{1,4}):){0,3}([\da-f]{1,4}))|((([\da-f]{1,4}):){2}:(([\da-f]{1,4}):){0,4}([\da-f]{1,4}))|((([\da-f]{1,4}):){1}:(([\da-f]{1,4}):){0,5}([\da-f]{1,4}))|(::(([\da-f]{1,4}):){0,6}([\da-f]{1,4}))|(::([\da-f]{1,4})?))|(((([\da-f]{1,4}):){6}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){5}:(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){4}:(([\da-f]{1,4}):)?(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){3}:(([\da-f]{1,4}):){0,2}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){2}:(([\da-f]{1,4}):){0,3}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|(([\da-f]{1,4})::(([\da-f]{1,4}):){0,4}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|(::(([\da-f]{1,4}):){0,5}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))))$/i; + private static isEmptyInputValue(value: any): boolean { + // we don't check for string here so it also works with arrays + return value == null || value.length === 0; + } + + // 字符串大小比较 + private static comparePlaces(aPlaces: string, bPlaces: string, type: 'integer' | 'decimal'): compareResult { + const len: number = Math.max(aPlaces.length, bPlaces.length); + const aVal: string = type === 'integer' ? aPlaces.padStart(len, '0') : aPlaces.padEnd(len, '0'); + const bVal: string = type === 'integer' ? bPlaces.padStart(len, '0') : bPlaces.padEnd(len, '0'); + if (aVal === bVal) { + return 0; + } + return [aVal, bVal].sort()[0] === aVal ? -1 : 1; + } + + private static realNumberStringCondition(aInfo, bInfo): compareResult { + function zeroDeal(aInfo, bInfo) { + if (aInfo.isZero && bInfo.isZero) { + return 0; + } else if (aInfo.isZero) { + return bInfo.isNegative ? 1 : -1; + } else { + return aInfo.isNegative ? -1 : 1; + } + } + + function zeroCompare(aInfo, bInfo) { + if (aInfo.isNegative === bInfo.isNegative) { + let result: compareResult = TiValidators.comparePlaces(aInfo.integerPlaces, bInfo.integerPlaces, 'integer'); + if (result === 0) { + result = TiValidators.comparePlaces(aInfo.decimalPlaces, bInfo.decimalPlaces, 'decimal'); + } + // To avoid ambiguity, do not return -0 + if (result === 0) { + return 0; + } else { + return (result * (aInfo.isNegative ? -1 : 1)) as 1 | -1; + } + } else { + return aInfo.isNegative ? -1 : 1; + } + } + + if (aInfo.isZero || bInfo.isZero) { + return zeroDeal(aInfo, bInfo); + } else { + return zeroCompare(aInfo, bInfo); + } + } + // 返回字符串的 整型,十进制 ,字符串第一个值,是否存在0 + private static getInfo(value: string) { + const num: string = ['+', '-'].includes(value[0]) ? value.slice(1) : value; + const index: number = num.indexOf('.'); + const integerPlaces: string = index > -1 ? num.slice(0, index) : num; + const decimalPlaces: string = index > -1 ? num.slice(index + 1) : '0'; + return { + integerPlaces, + decimalPlaces, + isNegative: value[0] === '-', + isZero: Array.from([...integerPlaces, ...decimalPlaces]).every((e) => e === '0') + }; + } + + private static realNumberStringCompareTo = function (a: string, b: string): compareResult { + // a字符串的拆分 + // tslint:disable-next-line: typedef + const aInfo = TiValidators.getInfo(a); + // b字符串的拆分 + // tslint:disable-next-line: typedef + const bInfo = TiValidators.getInfo(b); + + // tslint:disable-next-line: typedef + return TiValidators.realNumberStringCondition(aInfo, bInfo); + }; + + /** + * @ignore + */ + public static required(control: AbstractControl): ValidationErrors | null { + return Validators.required(control); + } + /** + * @ignore + */ + public static maxLength(maxLength: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const length: number = control.value ? control.value.length : 0; + + return length > maxLength ? { maxLength: { requiredLength: maxLength, actualLength: length } } : null; + }; + } + /** + * @ignore + */ + public static minLength(minLength: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + const length: number = control.value ? control.value.length : 0; + + return length < minLength ? { minLength: { requiredLength: minLength, actualLength: length } } : null; + }; + } + /** + * @ignore + */ + public static rangeSize(minLength: number, maxLength: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + const length: number = control.value ? control.value.length : 0; + + return length > maxLength || length < minLength + ? { + rangeSize: { + requiredMinLength: minLength, + requiredMaxLength: maxLength, + actualLength: length + } + } + : null; + }; + } + /** + * @ignore + */ + public static maxValue(max: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(max)) { + return null; // don't validate empty values to allow optional controls + } + const value: number = parseFloat(control.value); + + // Controls with NaN values after parsing should be treated as not having a + // maximum, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-max + return !isNaN(value) && value > max ? { maxValue: { requiredMaxValue: max, actual: control.value } } : null; + }; + } + /** + * @ignore + */ + public static minValue(min: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(min)) { + return null; // don't validate empty values to allow optional controls + } + const value: number = parseFloat(control.value); + + // Controls with NaN values after parsing should be treated as not having a + // maximum, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-max + return !isNaN(value) && value < min ? { minValue: { requiredMinValue: min, actual: control.value } } : null; + }; + } + /** + * @ignore + */ + public static maxValueByString(max: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(max)) { + return null; // don't validate empty values to allow optional controls + } + const value: any = control.value; + const err: ValidationErrors = { + maxValue: { requiredMaxValue: max, actual: value } + }; + if (typeof max !== 'string' || !TiValidators.BIG_NUMBER_REGEXP.test(max)) { + return err; + } + if (typeof value !== 'string' || !TiValidators.BIG_NUMBER_REGEXP.test(value)) { + return null; + } + + return TiValidators.realNumberStringCompareTo(value, max) > 0 ? err : null; + }; + } + /** + * @ignore + */ + public static minValueByString(min: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(min)) { + return null; // don't validate empty values to allow optional controls + } + const value: any = control.value; + const err: ValidationErrors = { + minValue: { requiredMinValue: min, actual: control.value } + }; + if (typeof min !== 'string' || !TiValidators.BIG_NUMBER_REGEXP.test(min)) { + return err; + } + if (typeof value !== 'string' || !TiValidators.BIG_NUMBER_REGEXP.test(value)) { + return null; + } + + return TiValidators.realNumberStringCompareTo(min, value) > 0 ? err : null; + }; + } + /** + * @ignore + */ + public static rangeValue(min: number, max: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(min) || TiValidators.isEmptyInputValue(max)) { + return null; // don't validate empty values to allow optional controls + } + const value: number = parseFloat(control.value); + + // Controls with NaN values after parsing should be treated as not having a + // maximum, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-max + return !isNaN(value) && (value < min || value > max) + ? { + rangeValue: { + requiredMinValue: min, + requiredMaxValue: max, + actual: control.value + } + } + : null; + }; + } + /** + * @ignore + */ + public static rangeValueByString(min: string, max: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(min) || TiValidators.isEmptyInputValue(max)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.minValueByString(min)(control) || TiValidators.maxValueByString(max)(control); + }; + } + /** + * @ignore + */ + public static regExp(pattern: string | RegExp): ValidatorFn { + if (!pattern) { + return Validators.nullValidator; + } + let regex: RegExp; + let regexStr: string; + if (typeof pattern === 'string') { + regexStr = ''; + + if (pattern.charAt(0) !== '^') { + regexStr += '^'; + } + + regexStr += pattern; + + if (pattern.charAt(pattern.length - 1) !== '$') { + regexStr += '$'; + } + + regex = new RegExp(regexStr); + } else { + regexStr = pattern.toString(); + regex = pattern; + } + + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + const value: string = control.value; + + return regex.test(value) ? null : { regExp: { requiredPattern: regexStr, actualValue: value } }; + }; + } + /** + * @ignore + */ + public static email(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.EMAIL_REGEXP.test(control.value) ? null : { email: true }; + } + /** + * @ignore + */ + public static contains(contain: string | number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(contain)) { + return null; // don't validate empty values to allow optional controls + } + + return control.value.indexOf(contain) === -1 + ? { + contains: { requiredContains: contain, actualValue: control.value } + } + : null; + }; + } + /** + * @ignore + */ + public static notContains(contain: string | number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(contain)) { + return null; // don't validate empty values to allow optional controls + } + + return control.value.indexOf(contain) !== -1 + ? { + notContains: { + requiredNotContains: contain, + actualValue: control.value + } + } + : null; + }; + } + /** + * @ignore + */ + public static equal(equal: string | number | boolean): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(equal)) { + return null; // don't validate empty values to allow optional controls + } + + return control.value !== equal ? { equal: { requiredEqual: equal, actualValue: control.value } } : null; + }; + } + /** + * @ignore + */ + public static notEqual(equal: string | number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(equal)) { + return null; // don't validate empty values to allow optional controls + } + + return control.value === equal ? { notEqual: { requiredNotEqual: equal, actualValue: control.value } } : null; + }; + } + /** + * @ignore + */ + public static notScript(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.SCRIPT_REGEXP.test(control.value) ? { notScript: true } : null; + } + /** + * @ignore + */ + public static port(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.DIGITS_REGEXP.test(control.value) && + control.value >= TiValidators.PORT_RANGE[0] && + control.value <= TiValidators.PORT_RANGE[1] + ? null + : { + port: { + min: TiValidators.PORT_RANGE[0], + max: TiValidators.PORT_RANGE[1] + } + }; + } + /** + * @ignore + */ + public static date(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return !/Invalid|NaN/.test(new Date(control.value).toString()) ? null : { date: true }; + } + /** + * @ignore + */ + public static url(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + try { + new URL(control.value); + return null; + } catch (e) { + return { url: true }; + } + } + /** + * @ignore + */ + public static integer(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.INTEGER_REGEXP.test(control.value) ? null : { integer: true }; + } + /** + * @ignore + */ + public static number(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.NUMBER_REGEXP.test(control.value) ? null : { number: true }; + } + /** + * @ignore + */ + public static digits(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.DIGITS_REGEXP.test(control.value) ? null : { digits: true }; + } + /** + * @ignore + */ + public static bigInteger(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.INTEGER_REGEXP.test(control.value) && + TiValidators.BIG_NUMBER_REGEXP.test(control.value) && + typeof control.value === 'string' + ? null + : { integer: true }; + } + /** + * @ignore + */ + public static bigNumber(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.NUMBER_REGEXP.test(control.value) && + TiValidators.BIG_NUMBER_REGEXP.test(control.value) && + typeof control.value === 'string' + ? null + : { number: true }; + } + /** + * @ignore + */ + public static bigDigits(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.DIGITS_REGEXP.test(control.value) && + TiValidators.BIG_NUMBER_REGEXP.test(control.value) && + typeof control.value === 'string' + ? null + : { digits: true }; + } + /** + * @ignore + */ + public static ipv4(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.IPV4_REGEXP.test(control.value) ? null : { ipv4: true }; + } + /** + * @ignore + */ + public static ipv6(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.IPV6_REGEXP.test(control.value) ? null : { ipv6: true }; + } + + /** + * 正则表达式转为字符串并首尾去除“/”符号,为添加g修饰符做准备 + */ + private static regExpToString(regExp: RegExp): string { + if (typeof regExp !== 'object') { + return ''; + } + const regExpStr: string = String(regExp); + + return regExpStr.substring(1, regExpStr.length - 1); + } + + /** + * @ignore + * 密码字符类型最小种类校验 + * 用于密码校验,其规则如下: + * 口令必须包含且只能包含如下至少两种字符的组合: + * -至少一个小写字母; + * -至少一个大写字母; + * -至少一个数字; + * -至少一个特殊字符:`~!@#$%^&*()-_=+\|[{}];:'",<.>/?和空格 + * @param num 字符类型最小个数 + * @param charTypeRegObj 字符类型校验正则表达式,涉及到强度计算,参考defaults中定义 + */ + + public static minCharType(num: number, charTypeRegObj?: any): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(num) || num === 0) { + return null; // don't validate empty values to allow optional controls + } + // 各类字符校验正则表达式集合,默认使用pwdConfig中的默认配置 + const charRegObj: any = typeof charTypeRegObj === 'object' ? charTypeRegObj : TiPwdConfig.pwdCharTypeRegObj; + + // 校验输入是否符合单条校验字符规则,并记录符合的单条规则个数 + let checkValidNum: number = 0; // 校验通过结果个数 + let replacedValue: string = control.value; // 正则替换后的value值 + + for (const key in charRegObj) { + if (Object.prototype.hasOwnProperty.call(charRegObj, key)) { + const regExp: RegExp = charRegObj[key]; + const checkResult = regExp.test(control.value); + if (checkResult) { + checkValidNum++; + // 使用全局替换方式,用于替换字串中所有符合正则规则的字符 + replacedValue = regExp.global + ? replacedValue.replace(regExp, '') + : replacedValue.replace(new RegExp(TiValidators.regExpToString(regExp), 'g'), ''); + } + } + } + // 替换后的值非空情况:代表值中存在非法字符 + return replacedValue.length > 0 || checkValidNum < num ? { minCharType: true } : null; + }; + } + + private static notEqualPosRevValue(value: string, refreValue: string): any { + return value.toLowerCase() !== refreValue.split('').reverse().join('').toLowerCase() && value.toLowerCase() !== refreValue.toLowerCase() + ? null + : { notEqualPosRev: true }; + } + + /** + * @ignore + * 用于密码校验:密码不能和用户名或其倒序相同 + * @param getControlFn 获取对比的表单元素formControl函数 + */ + public static notEqualPosRev(getControlFn: () => AbstractControl): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || Util.isUndefined(getControlFn && getControlFn())) { + return null; // don't validate empty values to allow optional controls + } + const value: string = control.value; + const refreValue: string = getControlFn().value; + + // 使用如下方式会出现卡顿的情况: + // 对比formControl value修改时,需要同步修改该表单的状态,否则会存在下次focus该元素或表单整体校验时 + // 该元素校验结果依然是上次的结果 + + return TiValidators.notEqualPosRevValue(value, refreValue); + }; + } + + private static _executeValidators(control: AbstractControl, validators: Array): Array { + return validators.map((v: any) => v(control)); + } + private static _mergeErrorsPwd(arrayOfErrors: Array): ValidationErrors | null { + const res: { [key: string]: any } = arrayOfErrors.reduce((resource: ValidationErrors | null, errors: ValidationErrors | null) => { + return errors != null ? { ...resource, ...errors } : resource; + }, {}); + + return Object.keys(res).length === 0 ? null : { password: true, ...res }; + } + + /** + * @ignore + * 密码校验规则 + * @param pValue 规则参数:{ruleKey: param} + */ + public static password(pValue: { + rangeSize?: [number, number]; + minCharType?: [number, any]; + notEqualPosRev?(): AbstractControl; + }): ValidatorFn { + const rangeSizeParamArr: Array = pValue.rangeSize || TiPwdConfig.pwdValidator.params.rangeSize; + const minCharTypeParamArr: Array = pValue.minCharType || TiPwdConfig.pwdValidator.params.minCharType; + if (Util.isUndefined(minCharTypeParamArr[1])) { + minCharTypeParamArr[1] = TiPwdConfig.pwdCharTypeRegObj; + } + const validatorComposeArr: Array = [ + TiValidators.required, + TiValidators.rangeSize(rangeSizeParamArr[0], rangeSizeParamArr[1]), + TiValidators.minCharType(minCharTypeParamArr[0], minCharTypeParamArr[1]), + TiValidators.notEqualPosRev(pValue.notEqualPosRev) + ]; + + // 由于错误提示为password专有提示,需要自定义password error,而compose方法不能自定义,因此此处未使用compose + // 此外,各子规则的错误依然需要保留,用于规则提示信息 + return (control: AbstractControl): any => TiValidators._mergeErrorsPwd(TiValidators._executeValidators(control, validatorComposeArr)); + } + + private static checkErrors( + form: FormGroup, + config: TiValidationCheckConfig, + errorsFlatted: boolean, + ignoreForms: string | string[], + updateValueAndValidityConfig: TiValidationCheckConfig + ) { + let errors = form.errors ? { ...form.errors } : {}; + + for (const key in form.controls) { + if (Object.prototype.hasOwnProperty.call(form.controls, key)) { + const control: AbstractControl | any = form.controls[key]; + if (control.controls) { + // 嵌套的FormArray,FormGroup + const checkedErrors: any = TiValidators.check(control, config); + errors = TiValidators.getErrors(errors, checkedErrors, errorsFlatted, key); + } else if (control.tiGroup && !control.disabled) { + // 类似ipsection组件场景 FormControl嵌套FormGroup 表单禁用时不验证 + const checkedErrors: any = TiValidators.check(control.tiGroup, config); + errors = TiValidators.getErrors(errors, checkedErrors, errorsFlatted, key); + } else { + // 配置忽略校验的表单元素不做处理 + if (ignoreForms && ignoreForms.indexOf(key) !== -1) { + continue; + } + + control.markAsTouched(); + // 由于表单间可能有关联关系(如密码校验的notEqualPosRev,该关联关系需要对单个表单元素再次校验 + control.updateValueAndValidity(updateValueAndValidityConfig); + // 读取errors信息 + if (control.errors !== null) { + errors[key] = control.errors; + } + } + } + } + return errors; + } + + // 该方法用于整体校验:由于表单未foucs时,即使错误也不会显示红框, + // 而整体校验时需要将错误的表单边框标红显示,因此需要显示调用整体校验方法 + /** + * 该方法用于整体校验 + * + * 参数form:需要校验的表单族 + * + * 参数config:可选 表单族的配置 + * + * 返回:表单错误信息 + */ + public static check(form: FormGroup, config?: TiValidationCheckConfig): ValidationErrors | null { + let ignoreForms: Array; // 忽略校验的表单元素 + let updateValueAndValidityConfig: TiValidationCheckConfig; // updateValueAndValidity方法的参数 + let errorsFlatted: boolean; // 错误信息是否扁平化 + + if (config) { + ignoreForms = config.ignoreNames; + updateValueAndValidityConfig = { + onlySelf: config.onlySelf, + emitEvent: config.emitEvent + }; + errorsFlatted = config.errorsFlatted; + } + + let errors = TiValidators.checkErrors(form, config, errorsFlatted, ignoreForms, updateValueAndValidityConfig); + // 无errors信息时,设置为null,方便业务处理 + if (JSON.stringify(errors) === '{}') { + errors = null; + } + + return errors; + } + + private static getErrors(errors: any, checkedErrors: any, flatten: boolean, key: string): any { + if (checkedErrors === null) { + return errors; + } + + let result: any = { ...errors }; + + if (flatten) { + result = { ...result, ...checkedErrors }; + } else { + result[key] = checkedErrors; + } + + return result; + } +} diff --git a/src/validation/lib/src/validators/directives/BaseValidator.ts b/src/validation/lib/src/validators/directives/BaseValidator.ts new file mode 100644 index 0000000..aed3be3 --- /dev/null +++ b/src/validation/lib/src/validators/directives/BaseValidator.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, forwardRef, OnChanges, SimpleChanges, Type } from '@angular/core'; +import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn, Validators } from '@angular/forms'; +import { TiValidators } from '../TiValidators'; +import { Util } from '@opentiny/ng-utils'; +// 下面注释,是为了避免lib编译错误。具体原理未知,副作用未知。 +// @dynamic +/** + * @ignore + */ +@Directive({ selector: '[BaseValidator]' }) +export class BaseValidator implements Validator, OnChanges { + public validatorStr: string; + public baseValue: string; // 需要传参的校验规则的参数 + public enabled: boolean = true; // 无需传参的校验规则是否开启校验 + private _validator: ValidatorFn = Validators.nullValidator; + private _onChange?: () => void; + + /** + * 获取各directive中的参数配置 + * @param validatorClass 校验实现类 + * @param name 校验指令名:由于该方法在类外部调用,无法使用BaseValidator中的参数,因此需要传入 + */ + public static getDirectiveConfig(validatorClass: Type, name: string): Directive { + return { + selector: `[${name}][formControlName],[${name}][formControl],[${name}][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => validatorClass), + multi: true + } + ] + }; + } + public static getValidatorStr(name: string): any { + return name.replace(/ti/, '').replace(/^\S/, (s: string) => { + return s.toLowerCase(); + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['baseValue'] || changes['enabled']) { + this._createValidator(); + if (this._onChange) { + this._onChange(); + } + } + } + + validate(c: AbstractControl): ValidationErrors | null { + return this.enabled === false || (this.enabled as any) === 'false' ? null : this._validator(c); + } + + registerOnValidatorChange(fn: () => void): void { + this._onChange = fn; + } + + private _createValidator(): void { + const validatorStr: string = this.validatorStr; + const validatorParam: string = this.baseValue; + if (Util.isArray(validatorParam)) { + this._validator = TiValidators[validatorStr].apply(null, validatorParam); + } else if (!Util.isUndefined(validatorParam)) { + this._validator = TiValidators[validatorStr](validatorParam); + } else { + this._validator = TiValidators[validatorStr]; + } + } +} diff --git a/src/validation/lib/src/validators/directives/BigDigitsValidatorDirective.ts b/src/validation/lib/src/validators/directives/BigDigitsValidatorDirective.ts new file mode 100644 index 0000000..c9828e4 --- /dev/null +++ b/src/validation/lib/src/validators/directives/BigDigitsValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; + +@Directive({ + selector: `[tiBigDigits][formControlName],[tiBigDigits][formControl],[tiBigDigits][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => BigDigitsValidatorDirective), + multi: true + } + ] +}) +export class BigDigitsValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiBigDigits'; + @Input(BigDigitsValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(BigDigitsValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/BigIntegerValidatorDirective.ts b/src/validation/lib/src/validators/directives/BigIntegerValidatorDirective.ts new file mode 100644 index 0000000..2a13b3a --- /dev/null +++ b/src/validation/lib/src/validators/directives/BigIntegerValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; + +@Directive({ + selector: `[tiBigInteger][formControlName],[tiBigInteger][formControl],[tiBigInteger][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => BigIntegerValidatorDirective), + multi: true + } + ] +}) +export class BigIntegerValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiBigInteger'; + @Input(BigIntegerValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(BigIntegerValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/BigNumberValidatorDirective.ts b/src/validation/lib/src/validators/directives/BigNumberValidatorDirective.ts new file mode 100644 index 0000000..5c22423 --- /dev/null +++ b/src/validation/lib/src/validators/directives/BigNumberValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; + +@Directive({ + selector: `[tiBigNumber][formControlName][tiValidation],[tiBigNumber][formControl][tiValidation],[tiBigNumber][ngModel][tiValidation]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => BigNumberValidatorDirective), + multi: true + } + ] +}) +export class BigNumberValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiBigNumber'; + @Input(BigNumberValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(BigNumberValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/ContainsValidatorDirective.ts b/src/validation/lib/src/validators/directives/ContainsValidatorDirective.ts new file mode 100644 index 0000000..a3c7c56 --- /dev/null +++ b/src/validation/lib/src/validators/directives/ContainsValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; + +@Directive({ + selector: `[tiContains][formControlName],[tiContains][formControl],[tiContains][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ContainsValidatorDirective), + multi: true + } + ] +}) +export class ContainsValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiContains'; + @Input(ContainsValidatorDirective.NAME) baseValue: string; + validatorStr: string = BaseValidator.getValidatorStr(ContainsValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/DateValidatorDirective.ts b/src/validation/lib/src/validators/directives/DateValidatorDirective.ts new file mode 100644 index 0000000..0aebb28 --- /dev/null +++ b/src/validation/lib/src/validators/directives/DateValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiDate][formControlName],[tiDate][formControl],[tiDate][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DateValidatorDirective), + multi: true + } + ] +}) +export class DateValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiDate'; + @Input(DateValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(DateValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/DigitsValidatorDirective.ts b/src/validation/lib/src/validators/directives/DigitsValidatorDirective.ts new file mode 100644 index 0000000..0b11d60 --- /dev/null +++ b/src/validation/lib/src/validators/directives/DigitsValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiDigits][formControlName],[tiDigits][formControl],[tiDigits][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DigitsValidatorDirective), + multi: true + } + ] +}) +export class DigitsValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiDigits'; + @Input(DigitsValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(DigitsValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/EmailValidatorDirective.ts b/src/validation/lib/src/validators/directives/EmailValidatorDirective.ts new file mode 100644 index 0000000..54144f8 --- /dev/null +++ b/src/validation/lib/src/validators/directives/EmailValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; + +@Directive({ + selector: `[tiEmail][formControlName],[tiEmail][formControl],[tiEmail][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EmailValidatorDirective), + multi: true + } + ] +}) +export class EmailValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiEmail'; + @Input(EmailValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(EmailValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/EqualValidatorDirective.ts b/src/validation/lib/src/validators/directives/EqualValidatorDirective.ts new file mode 100644 index 0000000..67678cb --- /dev/null +++ b/src/validation/lib/src/validators/directives/EqualValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; + +@Directive({ + selector: `[tiEqual][formControlName],[tiEqual][formControl],[tiEqual][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EqualValidatorDirective), + multi: true + } + ] +}) +export class EqualValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiEqual'; + @Input(EqualValidatorDirective.NAME) baseValue: string; + validatorStr: string = BaseValidator.getValidatorStr(EqualValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/IntegerValidatorDirective.ts b/src/validation/lib/src/validators/directives/IntegerValidatorDirective.ts new file mode 100644 index 0000000..aaff757 --- /dev/null +++ b/src/validation/lib/src/validators/directives/IntegerValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; + +@Directive({ + selector: `[tiInteger][formControlName],[tiInteger][formControl],[tiInteger][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => IntegerValidatorDirective), + multi: true + } + ] +}) +export class IntegerValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiInteger'; + @Input(IntegerValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(IntegerValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/Ipv4ValidatorDirective.ts b/src/validation/lib/src/validators/directives/Ipv4ValidatorDirective.ts new file mode 100644 index 0000000..a6c907b --- /dev/null +++ b/src/validation/lib/src/validators/directives/Ipv4ValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiIpv4][formControlName],[tiIpv4][formControl],[tiIpv4][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Ipv4ValidatorDirective), + multi: true + } + ] +}) +export class Ipv4ValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiIpv4'; + @Input(Ipv4ValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(Ipv4ValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/Ipv6ValidatorDirective.ts b/src/validation/lib/src/validators/directives/Ipv6ValidatorDirective.ts new file mode 100644 index 0000000..7e5e448 --- /dev/null +++ b/src/validation/lib/src/validators/directives/Ipv6ValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiIpv6][formControlName],[tiIpv6][formControl],[tiIpv6][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Ipv6ValidatorDirective), + multi: true + } + ] +}) +export class Ipv6ValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiIpv6'; + @Input(Ipv6ValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(Ipv6ValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/MaxLengthValidatorDirective.ts b/src/validation/lib/src/validators/directives/MaxLengthValidatorDirective.ts new file mode 100644 index 0000000..83638a7 --- /dev/null +++ b/src/validation/lib/src/validators/directives/MaxLengthValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiMaxLength][formControlName],[tiMaxLength][formControl],[tiMaxLength][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MaxLengthValidatorDirective), + multi: true + } + ] +}) +export class MaxLengthValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiMaxLength'; + @Input(MaxLengthValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(MaxLengthValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/MaxValueByStringValidatorDirective.ts b/src/validation/lib/src/validators/directives/MaxValueByStringValidatorDirective.ts new file mode 100644 index 0000000..fc2a17a --- /dev/null +++ b/src/validation/lib/src/validators/directives/MaxValueByStringValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiMaxValueByString][formControlName],[tiMaxValueByString][formControl],[tiMaxValueByString][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MaxValueByStringValidatorDirective), + multi: true + } + ] +}) +export class MaxValueByStringValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiMaxValueByString'; + @Input(MaxValueByStringValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(MaxValueByStringValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/MaxValueValidatorDirective.ts b/src/validation/lib/src/validators/directives/MaxValueValidatorDirective.ts new file mode 100644 index 0000000..68df4ee --- /dev/null +++ b/src/validation/lib/src/validators/directives/MaxValueValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiMaxValue][formControlName],[tiMaxValue][formControl],[tiMaxValue][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MaxValueValidatorDirective), + multi: true + } + ] +}) +export class MaxValueValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiMaxValue'; + @Input(MaxValueValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(MaxValueValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/MinLengthValidatorDirective.ts b/src/validation/lib/src/validators/directives/MinLengthValidatorDirective.ts new file mode 100644 index 0000000..23d02e6 --- /dev/null +++ b/src/validation/lib/src/validators/directives/MinLengthValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiMinLength][formControlName],[tiMinLength][formControl],[tiMinLength][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MinLengthValidatorDirective), + multi: true + } + ] +}) +export class MinLengthValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiMinLength'; + @Input(MinLengthValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(MinLengthValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/MinValueByStringValidatorDirective.ts b/src/validation/lib/src/validators/directives/MinValueByStringValidatorDirective.ts new file mode 100644 index 0000000..8d342e2 --- /dev/null +++ b/src/validation/lib/src/validators/directives/MinValueByStringValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiMinValueByString][formControlName],[tiMinValueByString][formControl],[tiMinValueByString][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MinValueByStringValidatorDirective), + multi: true + } + ] +}) +export class MinValueByStringValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiMinValueByString'; + @Input(MinValueByStringValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(MinValueByStringValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/MinValueValidatorDirective.ts b/src/validation/lib/src/validators/directives/MinValueValidatorDirective.ts new file mode 100644 index 0000000..5e443d2 --- /dev/null +++ b/src/validation/lib/src/validators/directives/MinValueValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiMinValue][formControlName],[tiMinValue][formControl],[tiMinValue][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MinValueValidatorDirective), + multi: true + } + ] +}) +export class MinValueValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiMinValue'; + @Input(MinValueValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(MinValueValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/NotContainsValidatorDirective.ts b/src/validation/lib/src/validators/directives/NotContainsValidatorDirective.ts new file mode 100644 index 0000000..991f532 --- /dev/null +++ b/src/validation/lib/src/validators/directives/NotContainsValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiNotContains][formControlName],[tiNotContains][formControl],[tiNotContains][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => NotContainsValidatorDirective), + multi: true + } + ] +}) +export class NotContainsValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiNotContains'; + @Input(NotContainsValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(NotContainsValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/NotEqualValidatorDirective.ts b/src/validation/lib/src/validators/directives/NotEqualValidatorDirective.ts new file mode 100644 index 0000000..0d2f189 --- /dev/null +++ b/src/validation/lib/src/validators/directives/NotEqualValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiNotEqual][formControlName],[tiNotEqual][formControl],[tiNotEqual][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => NotEqualValidatorDirective), + multi: true + } + ] +}) +export class NotEqualValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiNotEqual'; + @Input(NotEqualValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(NotEqualValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/NotScriptValidatorDirective.ts b/src/validation/lib/src/validators/directives/NotScriptValidatorDirective.ts new file mode 100644 index 0000000..1aaa836 --- /dev/null +++ b/src/validation/lib/src/validators/directives/NotScriptValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiNotScript][formControlName],[tiNotScript][formControl],[tiNotScript][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => NotScriptValidatorDirective), + multi: true + } + ] +}) +export class NotScriptValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiNotScript'; + @Input(NotScriptValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(NotScriptValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/NumberValidatorDirective.ts b/src/validation/lib/src/validators/directives/NumberValidatorDirective.ts new file mode 100644 index 0000000..33725b2 --- /dev/null +++ b/src/validation/lib/src/validators/directives/NumberValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + // 数字校验与inputNumber组件重名,故此处选择器加tiValidation,其余校验规则不变 + selector: `[tiNumber][formControlName][tiValidation],[tiNumber][formControl][tiValidation],[tiNumber][ngModel][tiValidation]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => NumberValidatorDirective), + multi: true + } + ] +}) +export class NumberValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiNumber'; + @Input(NumberValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(NumberValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/PasswordValidatorDirective.ts b/src/validation/lib/src/validators/directives/PasswordValidatorDirective.ts new file mode 100644 index 0000000..678191f --- /dev/null +++ b/src/validation/lib/src/validators/directives/PasswordValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiPassword][formControlName],[tiPassword][formControl],[tiPassword][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => PasswordValidatorDirective), + multi: true + } + ] +}) +export class PasswordValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiPassword'; + @Input(PasswordValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(PasswordValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/PortValidatorDirective.ts b/src/validation/lib/src/validators/directives/PortValidatorDirective.ts new file mode 100644 index 0000000..6fe5ebc --- /dev/null +++ b/src/validation/lib/src/validators/directives/PortValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiPort][formControlName],[tiPort][formControl],[tiPort][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => PortValidatorDirective), + multi: true + } + ] +}) +export class PortValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiPort'; + @Input(PortValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(PortValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/RangeSizeValidatorDirective.ts b/src/validation/lib/src/validators/directives/RangeSizeValidatorDirective.ts new file mode 100644 index 0000000..c467d81 --- /dev/null +++ b/src/validation/lib/src/validators/directives/RangeSizeValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiRangeSize][formControlName],[tiRangeSize][formControl],[tiRangeSize][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RangeSizeValidatorDirective), + multi: true + } + ] +}) +export class RangeSizeValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiRangeSize'; + @Input(RangeSizeValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(RangeSizeValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/RangeValueByStringValidatorDirective.ts b/src/validation/lib/src/validators/directives/RangeValueByStringValidatorDirective.ts new file mode 100644 index 0000000..d505d23 --- /dev/null +++ b/src/validation/lib/src/validators/directives/RangeValueByStringValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiRangeValueByString][formControlName],[tiRangeValueByString][formControl],[tiRangeValueByString][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RangeValueByStringValidatorDirective), + multi: true + } + ] +}) +export class RangeValueByStringValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiRangeValueByString'; + @Input(RangeValueByStringValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(RangeValueByStringValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/RangeValueValidatorDirective.ts b/src/validation/lib/src/validators/directives/RangeValueValidatorDirective.ts new file mode 100644 index 0000000..96a9236 --- /dev/null +++ b/src/validation/lib/src/validators/directives/RangeValueValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiRangeValue][formControlName],[tiRangeValue][formControl],[tiRangeValue][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RangeValueValidatorDirective), + multi: true + } + ] +}) +export class RangeValueValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiRangeValue'; + @Input(RangeValueValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(RangeValueValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/RegExpValidatorDirective.ts b/src/validation/lib/src/validators/directives/RegExpValidatorDirective.ts new file mode 100644 index 0000000..3ad4f38 --- /dev/null +++ b/src/validation/lib/src/validators/directives/RegExpValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiRegExp][formControlName],[tiRegExp][formControl],[tiRegExp][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RegExpValidatorDirective), + multi: true + } + ] +}) +export class RegExpValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiRegExp'; + @Input(RegExpValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(RegExpValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/RequiredValidatorDirective.ts b/src/validation/lib/src/validators/directives/RequiredValidatorDirective.ts new file mode 100644 index 0000000..a63073b --- /dev/null +++ b/src/validation/lib/src/validators/directives/RequiredValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiRequired][formControlName],[tiRequired][formControl],[tiRequired][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RequiredValidatorDirective), + multi: true + } + ] +}) +export class RequiredValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiRequired'; + @Input(RequiredValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(RequiredValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/UrlValidatorDirective.ts b/src/validation/lib/src/validators/directives/UrlValidatorDirective.ts new file mode 100644 index 0000000..08a3d5c --- /dev/null +++ b/src/validation/lib/src/validators/directives/UrlValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiUrl][formControlName],[tiUrl][formControl],[tiUrl][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => UrlValidatorDirective), + multi: true + } + ] +}) +export class UrlValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiUrl'; + @Input(UrlValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(UrlValidatorDirective.NAME); +} diff --git a/src/zoom/demo/project.json b/src/zoom/demo/project.json new file mode 100644 index 0000000..b4eb46b --- /dev/null +++ b/src/zoom/demo/project.json @@ -0,0 +1,86 @@ +{ + "projectType": "application", + "root": "src/zoom/demo", + "sourceRoot": "src/zoom/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/zoom", + "index": "src/zoom/demo/src/index.html", + "main": "src/zoom/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/zoom/demo/tsconfig.app.json", + "assets": [ + "src/zoom/demo/src/favicon.ico", + "src/zoom/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/image", + "output": "/assets/image/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "zoom-demo:build:production" + }, + "development": { + "browserTarget": "zoom-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js zoom" + } + ] + } + } + } +} diff --git a/src/zoom/demo/src/app/AppComponent.ts b/src/zoom/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/zoom/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/zoom/demo/src/app/AppModule.ts b/src/zoom/demo/src/app/AppModule.ts new file mode 100644 index 0000000..9845c80 --- /dev/null +++ b/src/zoom/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ZoomTestModule } from './zoom/ZoomTestModule'; + +@NgModule({ + imports: [ + ZoomTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/zoom/demo/src/app/IndexComponent.ts b/src/zoom/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..89f1f05 --- /dev/null +++ b/src/zoom/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ZoomTestModule } from './zoom/ZoomTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = ZoomTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/zoom/demo/src/app/app.html b/src/zoom/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/zoom/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/zoom/demo/src/app/zoom/ZoomBasicComponent.ts b/src/zoom/demo/src/app/zoom/ZoomBasicComponent.ts new file mode 100644 index 0000000..96984e4 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/ZoomBasicComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './zoom-basic.html', + styleUrls: ['./zoom.less'] +}) +export class ZoomBasicComponent {} diff --git a/src/zoom/demo/src/app/zoom/ZoomChangeSrcComponent.ts b/src/zoom/demo/src/app/zoom/ZoomChangeSrcComponent.ts new file mode 100644 index 0000000..7f55e30 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/ZoomChangeSrcComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './zoom-change-src.html' +}) +export class ZoomChangeSrcComponent { + srcArray: Array = ['assets/image/1.jpg', 'assets/image/2.jpg', 'assets/image/3.jpg']; + index: number = 0; + count: number = 0; + src: string = this.srcArray[this.index]; + // 循环切换 + changeSrc(event: any): void { + if (!this.srcArray.length) { + return; + } + this.count += 1; + this.index = this.count % this.srcArray.length; + this.src = this.srcArray[this.index]; + } +} diff --git a/src/zoom/demo/src/app/zoom/ZoomRatioComponent.ts b/src/zoom/demo/src/app/zoom/ZoomRatioComponent.ts new file mode 100644 index 0000000..1263380 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/ZoomRatioComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './zoom-ratio.html', + styleUrls: ['./zoom.less'] +}) +export class ZoomRatioComponent {} diff --git a/src/zoom/demo/src/app/zoom/ZoomTestComponent.ts b/src/zoom/demo/src/app/zoom/ZoomTestComponent.ts new file mode 100644 index 0000000..0bcbcf7 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/ZoomTestComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './zoom-test.html', + styleUrls: ['./zoom.less'] +}) +export class ZoomTestComponent {} diff --git a/src/zoom/demo/src/app/zoom/ZoomTestModule.ts b/src/zoom/demo/src/app/zoom/ZoomTestModule.ts new file mode 100644 index 0000000..33d0ae2 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/ZoomTestModule.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiZoomModule } from '@opentiny/ng'; + +import { ZoomBasicComponent } from './ZoomBasicComponent'; +import { ZoomChangeSrcComponent } from './ZoomChangeSrcComponent'; +import { ZoomTestComponent } from './ZoomTestComponent'; +import { ZoomRatioComponent } from './ZoomRatioComponent'; + +@NgModule({ + imports: [CommonModule, TiZoomModule, RouterModule.forChild(ZoomTestModule.ROUTES)], + declarations: [ZoomBasicComponent, ZoomChangeSrcComponent, ZoomTestComponent, ZoomRatioComponent] +}) +export class ZoomTestModule { + static readonly LINKS: Array = [{ href: 'directives/TiZoomDirective.html', label: 'Zoom' }]; + static readonly ROUTES: Routes = [ + { + path: 'zoom/zoom-basic', + component: ZoomBasicComponent + }, + { + path: 'zoom/zoom-ratio', + component: ZoomRatioComponent + }, + { path: 'zoom/zoom-change-src', component: ZoomChangeSrcComponent }, // 自测用例,改变src属性 + { path: 'zoom/zoom-test', component: ZoomTestComponent } // 自测用例 + ]; +} diff --git a/src/zoom/demo/src/app/zoom/zoom-basic.html b/src/zoom/demo/src/app/zoom/zoom-basic.html new file mode 100644 index 0000000..79b4c19 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/zoom-basic.html @@ -0,0 +1,11 @@ +

    描述

    +

    + 基本用法,通过在img元素上添加tiZoom属性可以实现图片放大效果。
    + 在其他元素上添加tiZoom无效。 +

    +

    导入

    +

    import {{ '{' }} TiZoomModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    +
    + +
    diff --git a/src/zoom/demo/src/app/zoom/zoom-change-src.html b/src/zoom/demo/src/app/zoom/zoom-change-src.html new file mode 100644 index 0000000..834cedc --- /dev/null +++ b/src/zoom/demo/src/app/zoom/zoom-change-src.html @@ -0,0 +1,5 @@ +

    描述

    +

    改变img元素src属性。

    +

    示例

    + + diff --git a/src/zoom/demo/src/app/zoom/zoom-ratio.html b/src/zoom/demo/src/app/zoom/zoom-ratio.html new file mode 100644 index 0000000..9f7a783 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/zoom-ratio.html @@ -0,0 +1,9 @@ +

    描述

    +

    + 通过设置zoomSelectorLength、zoomViewerLength可以自定义放大比例。分别默认150和300。
    + 放大比例为(zoomViewerLength / zoomSelectorLength) * (zoomViewerLength / zoomSelectorLength) +

    +

    示例

    +
    + +
    diff --git a/src/zoom/demo/src/app/zoom/zoom-test.html b/src/zoom/demo/src/app/zoom/zoom-test.html new file mode 100644 index 0000000..57ac7c3 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/zoom-test.html @@ -0,0 +1,15 @@ +

    描述

    +

    自测用例

    +

    示例

    +

    1、非img元素,无效

    +
    +

    2、居中

    +
    + +
    +

    3、不居中

    +
    + +
    +

    4、无父元素

    + diff --git a/src/zoom/demo/src/app/zoom/zoom.less b/src/zoom/demo/src/app/zoom/zoom.less new file mode 100644 index 0000000..f767b3e --- /dev/null +++ b/src/zoom/demo/src/app/zoom/zoom.less @@ -0,0 +1,11 @@ +.zoom-container { + width: 400px; + height: 400px; + background-color: rgb(245, 245, 245); + .zoom-img { + box-sizing: border-box; + border: 1px solid #dfe1e6; + width: 100%; + height: 100%; + } +} diff --git a/src/zoom/demo/src/favicon.ico b/src/zoom/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/zoom/demo/src/index.html b/src/zoom/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/zoom/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/zoom/demo/src/main.ts b/src/zoom/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/zoom/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/zoom/demo/tsconfig.app.json b/src/zoom/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/zoom/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/zoom/lib/index.ts b/src/zoom/lib/index.ts new file mode 100644 index 0000000..db7e256 --- /dev/null +++ b/src/zoom/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiZoomModule'; diff --git a/src/zoom/lib/ng-package.json b/src/zoom/lib/ng-package.json new file mode 100644 index 0000000..6f5e1d0 --- /dev/null +++ b/src/zoom/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/zoom", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/zoom/lib/package.json b/src/zoom/lib/package.json new file mode 100644 index 0000000..9aebea7 --- /dev/null +++ b/src/zoom/lib/package.json @@ -0,0 +1,10 @@ +{ + "name": "@opentiny/ng-zoom", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/zoom/lib/project.json b/src/zoom/lib/project.json new file mode 100644 index 0000000..99c8fab --- /dev/null +++ b/src/zoom/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/zoom/lib", + "sourceRoot": "src/zoom/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/zoom"], + "options": { + "project": "src/zoom/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/zoom"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js zoom" + }, + { + "command": "ng default-build zoom" + }, + { + "command": "node build/clear-default-theme.js zoom" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/zoom && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build zoom && ng pack zoom && node build/publish.js zoom --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/zoom/lib/src/TiZoomComponent.ts b/src/zoom/lib/src/TiZoomComponent.ts new file mode 100644 index 0000000..bc3f335 --- /dev/null +++ b/src/zoom/lib/src/TiZoomComponent.ts @@ -0,0 +1,257 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ElementRef, HostListener, Input, OnDestroy, OnInit, Renderer2, ChangeDetectionStrategy } from '@angular/core'; +import { TiRenderer } from '@opentiny/ng-renderer'; + +/** + * @ignore + * + * 10.0.3版本增加,暂时不对外开放 + * + * 该指令可以实现图片放大镜功能,添加tiZoom后会创建放大区域选择器元素(div)和放大结果呈现元素(div) + * + * 通过设置放大结果的background-image、background-size和background-position来达到放大效果 + * + * 10.1.0版本从指令调整为组件形式 + * + */ +@Component({ + // 非img元素,tiZoom指令无效 + selector: 'img[tiZoom]', + template: '', + styleUrls: ['./zoom.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiZoomComponent implements OnInit, OnDestroy { + // 放大结果的边框宽度 + private static readonly ZOOM_VIEWER_BORDER_WIDTH: number = 1; + + // 选择遮罩的宽高,默认150px + @Input() zoomSelectorLength: number = 150; + + // 放大展示的宽高,默认300px + @Input() zoomViewerLength: number = 300; + + // 比例 + private ratio: number; + + // 宿主元素 + private hostEle: any; + + // 宿主元素的父元素 + private parentEle: any; + + // 宿主元素的图片路径 + private imgSrc: string = ''; + + // 放大区域选择器 + private zoomSelectorEle: any; + + // 放大结果展示区域 + private zoomViewerEle: any; + + // 选择遮罩监听事件 + private selectorMouseMoveHandler: () => void; + private selectorMouseLeaveHandler: () => void; + + constructor(hostRef: ElementRef, private renderer: Renderer2, private tiRenderer: TiRenderer) { + this.hostEle = hostRef.nativeElement; + } + + ngOnInit(): void { + // 设置父元素 + this.setParentEle(); + // 设置放大比例 + this.ratio = this.zoomViewerLength / this.zoomSelectorLength; + } + + /** + * @ignore + * 图片加载成功事件处理 + */ + @HostListener('load', ['$event']) public onHostLoad(event: any): void { + this.imgSrc = event.target.src; + } + + /** + * @ignore + * 鼠标进入宿主元素,创建放大效果相关元素 + */ + @HostListener('mouseenter', ['$event']) public onHostMouseEnter(event: any): void { + this.createZoomSelector(); + this.createZoomViewer(); + this.reStyleResults(event); + } + + /** + * @ignore + * 鼠标离开宿主元素,移除放大效果相关元素 + */ + @HostListener('mouseleave', ['$event']) public onHostMouseLeave(event: any): void { + // 当鼠标离开宿主元素并且不是进入选择遮罩时,移除选择遮罩和放大结果 + if (event.relatedTarget !== this.zoomSelectorEle) { + this.removeZoomEle(); + } + } + + /** + * 设置父元素 + */ + private setParentEle(): void { + const parentEle: any = this.hostEle.parentElement; + if (parentEle !== null && parentEle.clientWidth !== 0 && parentEle.clientHeight !== 0) { + if (this.hostEle.offsetParent !== parentEle) { + // 父元素必须能够定位 + this.renderer.setStyle(parentEle, 'position', 'relative'); + } + this.parentEle = parentEle; + } else { + this.parentEle = document.body; + } + } + + /** + * 创建选择遮罩,初始化样式,增加事件监听 + */ + private createZoomSelector(): void { + this.zoomSelectorEle = this.renderer.createElement('div'); + + // 添加样式,tinyplus3的productpreview需要根据分辨率修改元素尺寸 + this.renderer.addClass(this.zoomSelectorEle, 'ti3-img-zoom-selector'); + this.tiRenderer.setStyles(this.zoomSelectorEle, { + width: this.zoomSelectorLength + 'px', + height: this.zoomSelectorLength + 'px' + }); + + // 鼠标在选择遮罩上移动时的事件 + this.selectorMouseMoveHandler = this.renderer.listen(this.zoomSelectorEle, 'mousemove', (event: any): void => { + this.reStyleResults(event); + }); + // 鼠标离开选择遮罩的事件,销毁遮罩元素和放大结果呈现元素 + this.selectorMouseLeaveHandler = this.renderer.listen(this.zoomSelectorEle, 'mouseleave', (event: MouseEvent) => { + this.renderer.removeChild(this.parentEle, this.zoomSelectorEle); + this.renderer.removeChild(document.body, this.zoomViewerEle); + this.zoomSelectorEle = undefined; + this.zoomViewerEle = undefined; + }); + + this.renderer.appendChild(this.parentEle, this.zoomSelectorEle); + } + + /** + * 创建放大结果呈现元素,初始化样式 + */ + private createZoomViewer(): void { + this.zoomViewerEle = this.renderer.createElement('div'); + const hostEleRect: DOMRect = this.hostEle.getBoundingClientRect(); + const parentEleRect: DOMRect = this.parentEle.getBoundingClientRect(); + + // 放大结果元素相对于父元素进行定位 + // left = 父元素宽度 - 边框宽度 + 滚动距离 + let left: number = this.parentEle.offsetWidth - TiZoomComponent.ZOOM_VIEWER_BORDER_WIDTH + parentEleRect.left + window.pageXOffset; + // top = 父元素到视口距离 + 滚动距离 + let top: number = parentEleRect.top + window.pageYOffset; + // 当父元素为body时,调整为相对于宿主元素定位 + if (this.parentEle === document.body) { + left = this.hostEle.offsetWidth - TiZoomComponent.ZOOM_VIEWER_BORDER_WIDTH + hostEleRect.left + window.pageXOffset; + top = hostEleRect.top + window.pageYOffset; + } + + // 添加样式,tinyplus3的productpreview需要根据分辨率修改元素尺寸 + this.renderer.addClass(this.zoomViewerEle, 'ti3-img-zoom-viewer'); + this.tiRenderer.setStyles(this.zoomViewerEle, { + left: left + 'px', + top: top + 'px', + width: `${this.zoomViewerLength}px`, + height: `${this.zoomViewerLength}px`, + background: `url('${this.imgSrc}')`, + 'background-size': `${this.hostEle.width * this.ratio}px ${this.hostEle.height * this.ratio}px` + }); + + this.renderer.appendChild(document.body, this.zoomViewerEle); + } + + // 计算鼠标相对宿主元素的位置 + private getCursorPos(mouseEvent: MouseEvent): any { + const hostEleRect: DOMRect = this.hostEle.getBoundingClientRect(); + // 考虑滚动情况 + const left: number = mouseEvent.pageX - hostEleRect.left - window.pageXOffset; + const top: number = mouseEvent.pageY - hostEleRect.top - window.pageYOffset; + + return { x: left, y: top }; + } + + /** + * 计算选择遮罩偏移和放大区域 + */ + private reStyleResults(mouseEvent: MouseEvent): void { + // 1. 获取计算鼠标相对于宿主元素左上角的位置 + const cursorPos: any = this.getCursorPos(mouseEvent); + // 获取遮罩宽度 + const zoomSelectorWidth: number = this.zoomSelectorEle.offsetWidth; + + // 2. 计算放大结果的位置 + let viewerPosX: number = cursorPos.x - zoomSelectorWidth / 2; + let viewerPosY: number = cursorPos.y - zoomSelectorWidth / 2; + + // 3. 调整 + // 3.1 选择遮罩移出宿主元素的右侧 + if (viewerPosX > this.hostEle.offsetWidth - zoomSelectorWidth) { + viewerPosX = this.hostEle.offsetWidth - zoomSelectorWidth; + } + // 3.2 选择遮罩移出宿主元素的左侧 + if (viewerPosX < 0) { + viewerPosX = 0; + } + // 3.3 选择遮罩移出宿主元素的下方 + if (viewerPosY > this.hostEle.offsetHeight - zoomSelectorWidth) { + viewerPosY = this.hostEle.offsetHeight - zoomSelectorWidth; + } + // 3.4 选择遮罩移出宿主元素的上方 + if (viewerPosY < 0) { + viewerPosY = 0; + } + + // 4. 设置选择区域偏移,选择区域偏移=放大结果位置 + 宿主元素的偏移 + this.tiRenderer.setStyles(this.zoomSelectorEle, { + left: `${viewerPosX + this.hostEle.offsetLeft}px`, + top: `${viewerPosY + this.hostEle.offsetTop}px` + }); + + // 5. 设置放大结果 + this.renderer.setStyle(this.zoomViewerEle, 'background-position', `-${viewerPosX * this.ratio}px -${viewerPosY * this.ratio}px`); + } + + ngOnDestroy(): void { + this.removeZoomEle(); + } + + /** + * 移除选择遮罩和放大结果元素、解绑相关事件 + */ + private removeZoomEle(): void { + if (this.zoomSelectorEle !== undefined) { + this.renderer.removeChild(this.parentEle, this.zoomSelectorEle); + this.zoomSelectorEle = undefined; + } + if (this.zoomViewerEle !== undefined) { + this.renderer.removeChild(document.body, this.zoomViewerEle); + this.zoomViewerEle = undefined; + } + if (this.selectorMouseMoveHandler) { + this.selectorMouseMoveHandler(); + } + if (this.selectorMouseLeaveHandler) { + this.selectorMouseLeaveHandler(); + } + } +} diff --git a/src/zoom/lib/src/TiZoomModule.ts b/src/zoom/lib/src/TiZoomModule.ts new file mode 100644 index 0000000..2b754f8 --- /dev/null +++ b/src/zoom/lib/src/TiZoomModule.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiZoomComponent } from './TiZoomComponent'; +import { TiRendererModule } from '@opentiny/ng-renderer'; + +@NgModule({ + imports: [CommonModule, TiRendererModule], + exports: [TiZoomComponent], + declarations: [TiZoomComponent] +}) +export class TiZoomModule {} +export { TiZoomComponent } from './TiZoomComponent'; diff --git a/src/zoom/lib/src/zoom.less b/src/zoom/lib/src/zoom.less new file mode 100644 index 0000000..4bf6e41 --- /dev/null +++ b/src/zoom/lib/src/zoom.less @@ -0,0 +1,19 @@ +.ti3-img-zoom-selector { + position: absolute; + top: 0; + left: 0; + background-color: var(--ti-common-color-icon-hover); + opacity: 0.2; + display: block; + cursor: move; +} + +.ti3-img-zoom-viewer { + position: absolute; + top: 0; + border: 1px solid var(--ti-common-color-line-dividing); + box-sizing: border-box; + display: block; + // 图片宽度极小或长度极小时,允许放大区域留白 + background-repeat: no-repeat !important; +} diff --git a/tools/generators/ti-lib-generator/files/demo/project.json__tmpl__ b/tools/generators/ti-lib-generator/files/demo/project.json__tmpl__ new file mode 100644 index 0000000..fe4751f --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/project.json__tmpl__ @@ -0,0 +1,85 @@ +{ + "projectType": "application", + "root": "src/<%= name %>/demo", + "sourceRoot": "src/<%= name %>/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/<%= name %>", + "index": "src/<%= name %>/demo/src/index.html", + "main": "src/<%= name %>/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/<%= name %>/demo/tsconfig.app.json", + "assets": [ + "src/<%= name %>/demo/src/favicon.ico", + "src/<%= name %>/demo/src/assets" + ], + "styles": [ + "src/themes/basic/build.less", + "src/themes/theme-default/build.less", + "src/styles.less" + ], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "<%= name %>-demo:build:production" + }, + "development": { + "browserTarget": "<%= name %>-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js <%= name %>" + } + ] + } + } + } +} diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/AppComponent.ts__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/AppComponent.ts__tmpl__ new file mode 100644 index 0000000..ed76202 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/AppComponent.ts__tmpl__ @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html', +}) +export class AppComponent {} diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/AppModule.ts__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/AppModule.ts__tmpl__ new file mode 100644 index 0000000..f5645b4 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/AppModule.ts__tmpl__ @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { <%= demoModuleName %> } from './<%= name %>/<%= demoModuleName %>'; + +@NgModule({ + imports: [ + <%= demoModuleName %>, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent }, // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/IndexComponent.ts__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/IndexComponent.ts__tmpl__ new file mode 100644 index 0000000..12a58f7 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/IndexComponent.ts__tmpl__ @@ -0,0 +1,54 @@ +import { Component } from '@angular/core'; +import { <%= demoModuleName %> } from './<%= name %>/<%= demoModuleName %>'; + +@Component({ + template: ` + + + + + + + +
    {{routes[0]?.path.split('/')[0]}} demo
    + + {{ path2name(route.path) }} + +
    `, +}) +export class IndexComponent { + routes: any = <%= demoModuleName %>.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoComponentName__.ts__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoComponentName__.ts__tmpl__ new file mode 100644 index 0000000..a9563a6 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoComponentName__.ts__tmpl__ @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './<%= name %>-basic.html' +}) +export class <%= demoComponentName %> { +} diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoModuleName__.ts__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoModuleName__.ts__tmpl__ new file mode 100644 index 0000000..2b975d8 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoModuleName__.ts__tmpl__ @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { <%= moduleName %> } from '@opentiny/ng'; +import { <%= demoComponentName %> } from './<%= demoComponentName %>'; + +@NgModule({ + imports: [ + CommonModule, + <%= moduleName %>, + RouterModule.forChild(<%= demoModuleName %>.ROUTES) + ], + declarations: [<%= demoComponentName %>] +}) +export class <%= demoModuleName %> { + static readonly ROUTES: Routes = [ + { + path: '<%= name %>/<%= name %>-basic', + component: <%= demoComponentName %> + } + ]; +} diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__name__-basic.html__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__name__-basic.html__tmpl__ new file mode 100644 index 0000000..5ac3822 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__name__-basic.html__tmpl__ @@ -0,0 +1 @@ +

    <%= name %> demo

    \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__-demos.js__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__-demos.js__tmpl__ new file mode 100644 index 0000000..97b0f9c --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__-demos.js__tmpl__ @@ -0,0 +1,17 @@ +export default { + column: '2', + demos: [ + { + demoId: '<%= name %>-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN':'', + 'en-US':'' + }, + apis: [] + } + ] +}; diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.cn.md__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.cn.md__tmpl__ new file mode 100644 index 0000000..0a92345 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.cn.md__tmpl__ @@ -0,0 +1,15 @@ +--- +title: <%= camelName %> 组件中文名 +--- +# <%= camelName %> 组件中文名 + +
    + +组件总述。 + ++ 可列举的特性。 + +```typescript +import { <%= moduleName %> } from '@opentiny/ng'; +``` +
    diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.en.md__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.en.md__tmpl__ new file mode 100644 index 0000000..0eaa10a --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.en.md__tmpl__ @@ -0,0 +1,13 @@ +--- +title: <%= camelName %> +--- +# <%= camelName %> + +
    + +```typescript +import { <%= moduleName %> } from '@opentiny/ng'; +``` +
    + + diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/app.html__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/app.html__tmpl__ new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/app.html__tmpl__ @@ -0,0 +1,3 @@ +
    + +
    diff --git a/tools/generators/ti-lib-generator/files/demo/src/favicon.ico__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/favicon.ico__tmpl__ new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/tools/generators/ti-lib-generator/files/demo/src/index.html__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/index.html__tmpl__ new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/index.html__tmpl__ @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/tools/generators/ti-lib-generator/files/demo/src/main.ts__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/main.ts__tmpl__ new file mode 100644 index 0000000..e1cdf67 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/main.ts__tmpl__ @@ -0,0 +1,14 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); + diff --git a/tools/generators/ti-lib-generator/files/demo/tsconfig.app.json__tmpl__ b/tools/generators/ti-lib-generator/files/demo/tsconfig.app.json__tmpl__ new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/tsconfig.app.json__tmpl__ @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/lib/index.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/index.ts__tmpl__ new file mode 100644 index 0000000..3335bb0 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/index.ts__tmpl__ @@ -0,0 +1,2 @@ +export * from './src/<%= moduleName %>'; +export * from './src/<%= componentName %>'; \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/lib/ng-package.json__tmpl__ b/tools/generators/ti-lib-generator/files/lib/ng-package.json__tmpl__ new file mode 100644 index 0000000..0c7626e --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/ng-package.json__tmpl__ @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/<%= name %>", + "lib": { + "entryFile": "./index.ts" + } +} \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/lib/package.json__tmpl__ b/tools/generators/ti-lib-generator/files/lib/package.json__tmpl__ new file mode 100644 index 0000000..54ac1b7 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/package.json__tmpl__ @@ -0,0 +1,8 @@ +{ + "name": "<%= importPath %>", + "version": "<%= version %>", + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@angular/core": ">=13.0.0" + } +} \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/lib/project.json__tmpl__ b/tools/generators/ti-lib-generator/files/lib/project.json__tmpl__ new file mode 100644 index 0000000..8cf2e91 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/project.json__tmpl__ @@ -0,0 +1,63 @@ +{ + "projectType": "library", + "root": "src/<%= name %>/lib", + "sourceRoot": "src/<%= name %>/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": [ + "dist/libs/<%= name %>" + ], + "options": { + "project": "src/<%= name %>/lib/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/<%= name %>"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js <%= name %>" + }, + { + "command": "ng default-build <%= name %>" + }, + { + "command": "node build/clear-default-theme.js <%= name %>" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/<%= name %> && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build <%= name %> && ng pack <%= name %> && node build/publish.js <%= name %> --tag={args.tag}" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/lib/src/__componentName__.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/__componentName__.ts__tmpl__ new file mode 100644 index 0000000..b60e24f --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/__componentName__.ts__tmpl__ @@ -0,0 +1,14 @@ +import { Component, Input } from '@angular/core'; +<% if(i18n === 'y') { %> +import { TiLocale } from '@opentiny/ng-locale'; +<% } %> +/** + * 组件描述 + */ +@Component({ + selector: 'ti-<%= name %>', + templateUrl: '<%= name %>.html', + styleUrls: ['<%= name %>.less'] +}) +export class <%= componentName %> { +} diff --git a/tools/generators/ti-lib-generator/files/lib/src/__moduleName__.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/__moduleName__.ts__tmpl__ new file mode 100644 index 0000000..1bf0fe7 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/__moduleName__.ts__tmpl__ @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { <%= componentName %> } from './<%= componentName %>'; +<% if(i18n === 'y') { %> +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; +<% } %> + +@NgModule({ + imports: [CommonModule], + exports: [<%= componentName %>], + declarations: [<%= componentName %>] +}) +export class <%= moduleName %> { +<% if(i18n === 'y') { %> + constructor() { + TiLocale.setTiWords(locales); + } +<% } %> +} \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/lib/src/__name__.html__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/__name__.html__tmpl__ new file mode 100644 index 0000000..e69de29 diff --git a/tools/generators/ti-lib-generator/files/lib/src/__name__.less__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/__name__.less__tmpl__ new file mode 100644 index 0000000..e69de29 diff --git a/tools/generators/ti-lib-generator/files/lib/src/i18n/__i18nWords__.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/i18n/__i18nWords__.ts__tmpl__ new file mode 100644 index 0000000..289181f --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/i18n/__i18nWords__.ts__tmpl__ @@ -0,0 +1,5 @@ +export interface <%= i18nWords %> { + <%= i18nWordsName %>: { + label: string + } +} \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/lib/src/i18n/en_US.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/i18n/en_US.ts__tmpl__ new file mode 100644 index 0000000..5b4d14c --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/i18n/en_US.ts__tmpl__ @@ -0,0 +1,7 @@ +import { <%= i18nWords %> } from './<%= i18nWords %>'; + +export const en_US: <%= i18nWords %> = { + <%= i18nWordsName %>: { + label: 'Confirm' + } +}; diff --git a/tools/generators/ti-lib-generator/files/lib/src/i18n/es_US.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/i18n/es_US.ts__tmpl__ new file mode 100644 index 0000000..87276be --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/i18n/es_US.ts__tmpl__ @@ -0,0 +1,7 @@ +import { <%= i18nWords %> } from './<%= i18nWords %>'; + +export const es_US: <%= i18nWords %> = { + <%= i18nWordsName %>: { + label: 'Confirmar' + } +}; diff --git a/tools/generators/ti-lib-generator/files/lib/src/i18n/fr_FR.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/i18n/fr_FR.ts__tmpl__ new file mode 100644 index 0000000..71d1549 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/i18n/fr_FR.ts__tmpl__ @@ -0,0 +1,7 @@ +import { <%= i18nWords %> } from './<%= i18nWords %>'; + +export const fr_FR: <%= i18nWords %> = { + <%= i18nWordsName %>: { + label: 'Confirmer' + } +}; diff --git a/tools/generators/ti-lib-generator/files/lib/src/i18n/index.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/i18n/index.ts__tmpl__ new file mode 100644 index 0000000..4d3a7eb --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/i18n/index.ts__tmpl__ @@ -0,0 +1,8 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; + diff --git a/tools/generators/ti-lib-generator/files/lib/src/i18n/pt_BR.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/i18n/pt_BR.ts__tmpl__ new file mode 100644 index 0000000..333b049 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/i18n/pt_BR.ts__tmpl__ @@ -0,0 +1,7 @@ +import { <%= i18nWords %> } from './<%= i18nWords %>'; + +export const pt_BR: <%= i18nWords %> = { + <%= i18nWordsName %>: { + label: 'Confirmar' + } +}; diff --git a/tools/generators/ti-lib-generator/index.ts b/tools/generators/ti-lib-generator/index.ts new file mode 100644 index 0000000..7770e70 --- /dev/null +++ b/tools/generators/ti-lib-generator/index.ts @@ -0,0 +1,76 @@ +import { Tree, formatFiles, generateFiles, joinPathFragments, updateJson } from '@nrwl/devkit'; +import { TiLibGeneratorSchema } from './schema'; + +export default async function (tree: Tree, schema: TiLibGeneratorSchema) { + const importPath = `@opentiny/ng-${schema.name}`; + const libraryRoot = `src/${schema.name}`; + const camelName = transform2CamelCase(schema.name); + const opentinyNgDir = 'src/ng'; + schema.i18n = schema.i18n?.toLocaleLowerCase(); + + updateJson(tree, 'tsconfig.base.json', (json) => { + const c = json.compilerOptions; + if (c.paths[importPath]) { + throw new Error(`You already have a library using the name "${schema.name}". Make sure to specify a unique one.`); + } + + c.paths[importPath] = [joinPathFragments(libraryRoot, '/lib/index.ts')]; + + return json; + }); + + updateJson(tree, 'angular.json', (json) => { + json.projects[schema.name] = joinPathFragments(libraryRoot, '/lib'); + const demoProjectName = `${schema.name}-demo`; + json.projects[demoProjectName] = joinPathFragments(libraryRoot, '/demo'); + + return json; + }); + + updateJson(tree, joinPathFragments(opentinyNgDir, 'lib/package.json'), (json) => { + json.peerDependencies[importPath] = schema.version; + + return json; + }); + + const file = tree.read(joinPathFragments(opentinyNgDir, 'lib/index.ts'), 'utf8'); + tree.write( + joinPathFragments(opentinyNgDir, 'lib/index.ts'), + `${file} +export * from '${importPath}'; + ` + ); + + generateFiles( + tree, // the virtual file system + joinPathFragments(__dirname, './files'), // path to the file templates + libraryRoot, // destination path of the files + { + ...schema, + tmpl: '', + camelName, + importPath, + componentName: `Ti${camelName}Component`, + moduleName: `Ti${camelName}Module`, + demoComponentName: `${camelName}BasicComponent`, + demoModuleName: `${camelName}TestModule`, + i18nWords: `Ti${camelName}Words`, + i18nWordsName: `ti${camelName}` + } // config object to replace variable in file templates + ); + + if (schema.i18n === 'n') { + tree.delete(joinPathFragments(libraryRoot, '/lib/src/i18n')); + } + + await formatFiles(tree); + return () => {}; +} + +function transform2CamelCase(str) { + const arr = str.split('-'); + const result = arr.map((item) => { + return `${item.slice(0, 1).toUpperCase()}${item.slice(1)}`; + }); + return result.join(''); +} diff --git a/tools/generators/ti-lib-generator/schema.d.ts b/tools/generators/ti-lib-generator/schema.d.ts new file mode 100644 index 0000000..3bc3251 --- /dev/null +++ b/tools/generators/ti-lib-generator/schema.d.ts @@ -0,0 +1,5 @@ +export interface TiLibGeneratorSchema { + name: string; + version?: string; + i18n?: string; +} diff --git a/tools/generators/ti-lib-generator/schema.json b/tools/generators/ti-lib-generator/schema.json new file mode 100644 index 0000000..3ef06d8 --- /dev/null +++ b/tools/generators/ti-lib-generator/schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "$id": "TiLibGeneratorSchematics", + "title": "", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "lib库名", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "你的 library 库名称是什么?(比如, alert 或 base-shared)" + }, + "version": { + "type": "string", + "description": "lib库版本号", + "$default": { + "$source": "argv", + "index": 1 + }, + "default": "1.0.0", + "x-prompt": "你的 library 库版本号是什么?(例如 1.0.3,默认是 1.0.0)" + }, + "i18n": { + "type": "string", + "description": "lib库国际化词条", + "$default": { + "$source": "argv", + "index": 2 + }, + "default": "Y", + "x-prompt": "你的 library 库是否需要国际化词条(Y/n)" + } + }, + "required": ["name"] +} diff --git a/tools/tsconfig.tools.json b/tools/tsconfig.tools.json new file mode 100644 index 0000000..addb016 --- /dev/null +++ b/tools/tsconfig.tools.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "outDir": "../dist/out-tsc/tools", + "rootDir": ".", + "noUnusedLocals": true, + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "types": ["node"], + "typeRoots": ["node_modules/@types"], + "lib": ["es2017", "dom"], + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "baseUrl": "." + }, + "exclude": ["node_modules", "tmp"], + "include": ["**/*.ts"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..e38d1d3 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,126 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": ".", + "downlevelIteration": true, + "importHelpers": true, + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "module": "esnext", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "resolveJsonModule": true, + "target": "es2015", + "typeRoots": ["node_modules/@types"], + "lib": ["es2017", "dom"], + "rootDir": ".", + "paths": { + "@opentiny/ng": ["src/ng/lib/index.ts"], + "@opentiny/ng-accordion": ["src/accordion/lib/index.ts"], + "@opentiny/ng-actionmenu": ["src/actionmenu/lib/index.ts"], + "@opentiny/ng-alert": ["src/alert/lib/index.ts"], + "@opentiny/ng-anchor": ["src/anchor/lib/index.ts"], + "@opentiny/ng-autocomplete": ["src/autocomplete/lib/index.ts"], + "@opentiny/ng-avatar": ["src/avatar/lib/index.ts"], + "@opentiny/ng-base": ["src/base/lib/index.ts"], + "@opentiny/ng-button": ["src/button/lib/index.ts"], + "@opentiny/ng-buttongroup": ["src/buttongroup/lib/index.ts"], + "@opentiny/ng-buttonselect": ["src/buttonselect/lib/index.ts"], + "@opentiny/ng-card": ["src/card/lib/index.ts"], + "@opentiny/ng-cascader": ["src/cascader/lib/index.ts"], + "@opentiny/ng-checkbox": ["src/checkbox/lib/index.ts"], + "@opentiny/ng-collapse": ["src/collapse/lib/index.ts"], + "@opentiny/ng-collapsebox": ["src/collapsebox/lib/index.ts"], + "@opentiny/ng-collapsebutton": ["src/collapsebutton/lib/index.ts"], + "@opentiny/ng-collapsetext": ["src/collapsetext/lib/index.ts"], + "@opentiny/ng-copy": ["src/copy/lib/index.ts"], + "@opentiny/ng-crumb": ["src/crumb/lib/index.ts"], + "@opentiny/ng-date": ["src/date/lib/index.ts"], + "@opentiny/ng-datebase": ["src/datebase/lib/index.ts"], + "@opentiny/ng-datedominator": ["src/datedominator/lib/index.ts"], + "@opentiny/ng-dateedit": ["src/dateedit/lib/index.ts"], + "@opentiny/ng-datepanel": ["src/datepanel/lib/index.ts"], + "@opentiny/ng-daterange": ["src/daterange/lib/index.ts"], + "@opentiny/ng-datetime": ["src/datetime/lib/index.ts"], + "@opentiny/ng-datetimerange": ["src/datetimerange/lib/index.ts"], + "@opentiny/ng-dominator": ["src/dominator/lib/index.ts"], + "@opentiny/ng-drag": ["src/drag/lib/index.ts"], + "@opentiny/ng-drop": ["src/drop/lib/index.ts"], + "@opentiny/ng-droplist": ["src/droplist/lib/index.ts"], + "@opentiny/ng-dropsearch": ["src/dropsearch/lib/index.ts"], + "@opentiny/ng-foldtext": ["src/foldtext/lib/index.ts"], + "@opentiny/ng-formfield": ["src/formfield/lib/index.ts"], + "@opentiny/ng-grid": ["src/grid/lib/index.ts"], + "@opentiny/ng-guides": ["src/guides/lib/index.ts"], + "@opentiny/ng-guidesteps": ["src/guidesteps/lib/index.ts"], + "@opentiny/ng-halfmodal": ["src/halfmodal/lib/index.ts"], + "@opentiny/ng-icon": ["src/icon/lib/index.ts"], + "@opentiny/ng-iconaction": ["src/iconaction/lib/index.ts"], + "@opentiny/ng-imagepreview": ["src/imagepreview/lib/index.ts"], + "@opentiny/ng-include": ["src/include/lib/index.ts"], + "@opentiny/ng-inputnumber": ["src/inputnumber/lib/index.ts"], + "@opentiny/ng-intro": ["src/intro/lib/index.ts"], + "@opentiny/ng-ip": ["src/ip/lib/index.ts"], + "@opentiny/ng-ipsection": ["src/ipsection/lib/index.ts"], + "@opentiny/ng-labeleditor": ["src/labeleditor/lib/index.ts"], + "@opentiny/ng-layout": ["src/layout/lib/index.ts"], + "@opentiny/ng-leftmenu": ["src/leftmenu/lib/index.ts"], + "@opentiny/ng-linkbutton": ["src/linkbutton/lib/index.ts"], + "@opentiny/ng-list": ["src/list/lib/index.ts"], + "@opentiny/ng-loading": ["src/loading/lib/index.ts"], + "@opentiny/ng-locale": ["src/locale/lib/index.ts"], + "@opentiny/ng-menu": ["src/menu/lib/index.ts"], + "@opentiny/ng-message": ["src/message/lib/index.ts"], + "@opentiny/ng-modal": ["src/modal/lib/index.ts"], + "@opentiny/ng-nav": ["src/nav/lib/index.ts"], + "@opentiny/ng-notification": ["src/notification/lib/index.ts"], + "@opentiny/ng-outline": ["src/outline/lib/index.ts"], + "@opentiny/ng-overflow": ["src/overflow/lib/index.ts"], + "@opentiny/ng-pagination": ["src/pagination/lib/index.ts"], + "@opentiny/ng-path": ["src/path/lib/index.ts"], + "@opentiny/ng-phonenumber": ["src/phonenumber/lib/index.ts"], + "@opentiny/ng-popconfirm": ["src/popconfirm/lib/index.ts"], + "@opentiny/ng-popup": ["src/popup/lib/index.ts"], + "@opentiny/ng-productpreview": ["src/productpreview/lib/index.ts"], + "@opentiny/ng-progressbar": ["src/progressbar/lib/index.ts"], + "@opentiny/ng-progresspie": ["src/progresspie/lib/index.ts"], + "@opentiny/ng-radio": ["src/radio/lib/index.ts"], + "@opentiny/ng-rate": ["src/rate/lib/index.ts"], + "@opentiny/ng-renderer": ["src/renderer/lib/index.ts"], + "@opentiny/ng-rights": ["src/rights/lib/index.ts"], + "@opentiny/ng-score": ["src/score/lib/index.ts"], + "@opentiny/ng-scroll": ["src/scroll/lib/index.ts"], + "@opentiny/ng-searchbox": ["src/searchbox/lib/index.ts"], + "@opentiny/ng-select": ["src/select/lib/index.ts"], + "@opentiny/ng-selectgroup": ["src/selectgroup/lib/index.ts"], + "@opentiny/ng-skeleton": ["src/skeleton/lib/index.ts"], + "@opentiny/ng-slider": ["src/slider/lib/index.ts"], + "@opentiny/ng-spinner": ["src/spinner/lib/index.ts"], + "@opentiny/ng-steps": ["src/steps/lib/index.ts"], + "@opentiny/ng-subtitle": ["src/subtitle/lib/index.ts"], + "@opentiny/ng-swiper": ["src/swiper/lib/index.ts"], + "@opentiny/ng-switch": ["src/switch/lib/index.ts"], + "@opentiny/ng-tab": ["src/tab/lib/index.ts"], + "@opentiny/ng-table": ["src/table/lib/index.ts"], + "@opentiny/ng-tag": ["src/tag/lib/index.ts"], + "@opentiny/ng-tagsinput": ["src/tagsinput/lib/index.ts"], + "@opentiny/ng-text": ["src/text/lib/index.ts"], + "@opentiny/ng-textarea": ["src/textarea/lib/index.ts"], + "@opentiny/ng-themes": ["src/themes"], + "@opentiny/ng-time": ["src/time/lib/index.ts"], + "@opentiny/ng-timeline": ["src/timeline/lib/index.ts"], + "@opentiny/ng-tip": ["src/tip/lib/index.ts"], + "@opentiny/ng-transfer": ["src/transfer/lib/index.ts"], + "@opentiny/ng-tree": ["src/tree/lib/index.ts"], + "@opentiny/ng-treeselect": ["src/treeselect/lib/index.ts"], + "@opentiny/ng-upload": ["src/upload/lib/index.ts"], + "@opentiny/ng-utils": ["src/utils/lib/index.ts"], + "@opentiny/ng-validation": ["src/validation/lib/index.ts"], + "@opentiny/ng-zoom": ["src/zoom/lib/index.ts"] + } + }, + "exclude": ["node_modules", "tmp"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c4d6468 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "files": [], + "include": ["**/*.ts"], + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/website-tinydoc/build/copy.js b/website-tinydoc/build/copy.js new file mode 100644 index 0000000..a2bfa2c --- /dev/null +++ b/website-tinydoc/build/copy.js @@ -0,0 +1,34 @@ +const fs = require('fs-extra'); +const path = require('path'); +const log = require('fancy-log'); + +const baseDir = process.cwd(); +const copyContent = { + webdoc: ['../src/ng/demo/src/webdoc', './webdoc'], + changelog: ['../CHANGELOG.md', './webdoc/changelog.md'], + contributing: ['../CONTRIBUTING.md', './webdoc/contributing.md'], + app: ['../dist/tinydoc/tinyapis', './app'], + assets: ['../dist/apps/ng/assets', './scripts/assets'], + wc: ['../dist/apps/ng/web-components.js', './scripts/web-components.js'], + wcCss: ['../dist/apps/ng/styles.css', './scripts/styles.css'], +}; + +function copyFiles(content) { + log('[notice]: 请确认是否已在根目录执行 `npm run build:docs` 生成文件'); + + if (!content) { + return; + } + + Object.keys(content).forEach(async (key) => { + console.log(key, baseDir); + const [srcPath, distPath] = content[key].map((item) => path.resolve(baseDir, item)); + const distDir = path.dirname(distPath); + log(`[copy ${key}]: ${srcPath} -> ${distPath}`); + + await fs.ensureDir(distDir); + fs.copy(srcPath, distPath); + }); +} + +copyFiles(copyContent); diff --git a/website-tinydoc/config.js b/website-tinydoc/config.js new file mode 100644 index 0000000..e5f1961 --- /dev/null +++ b/website-tinydoc/config.js @@ -0,0 +1,3 @@ +export default { + isMobile: false +} \ No newline at end of file diff --git a/website-tinydoc/package.json b/website-tinydoc/package.json new file mode 100644 index 0000000..7cd2e4f --- /dev/null +++ b/website-tinydoc/package.json @@ -0,0 +1,19 @@ +{ + "name": "@opentiny/ng-tinydoc", + "private": false, + "version": "1.0.0", + "scripts": { + "copy": "node ./build/copy.js", + "pub": "npm run copy && npm publish --verbose" + }, + "files": [ + "app", + "webdoc", + "scripts", + "config.js" + ], + "devDependencies": { + "fs-extra": "^10.1.0", + "fancy-log": "^2.0.0" + } +} -- Gitee From 6a5a9f3baacdcdd0bf1463251fc2b88321279b84 Mon Sep 17 00:00:00 2001 From: lizy <2855793251@qq.com> Date: Sun, 9 Apr 2023 00:30:54 +0800 Subject: [PATCH 3/3] merge --- src/inputnumber/demo/src/app/AppComponent.ts | 7 + src/inputnumber/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/inputnumber/demo/src/app/app.html | 3 + .../inputnumber/InputnumberBasicComponent.ts | 8 + .../inputnumber/InputnumberEventComponent.ts | 21 + .../inputnumber/InputnumberFocusComponent.ts | 8 + .../inputnumber/InputnumberFormatComponent.ts | 9 + .../inputnumber/InputnumberLoadComponent.ts | 24 + .../InputnumberLocaleableComponent.ts | 9 + .../InputnumberMaxlengthComponent.ts | 8 + .../app/inputnumber/InputnumberTestModule.ts | 70 + .../app/inputnumber/inputnumber-basic.html | 4 + .../app/inputnumber/inputnumber-event.html | 11 + .../app/inputnumber/inputnumber-focus.html | 9 + .../app/inputnumber/inputnumber-format.html | 1 + .../src/app/inputnumber/inputnumber-load.html | 20 + .../inputnumber/inputnumber-localeable.html | 1 + .../inputnumber/inputnumber-maxlength.html | 1 + .../inputnumber/webdoc/inputnumber-demos.js | 66 + .../app/inputnumber/webdoc/inputnumber.cn.md | 27 + .../app/inputnumber/webdoc/inputnumber.en.md | 29 + src/inputnumber/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/inputnumber/demo/src/index.html | 16 + src/inputnumber/demo/src/main.ts | 13 + src/inputnumber/demo/tsconfig.spec.json | 20 + src/inputnumber/lib/index.ts | 1 + src/inputnumber/lib/ng-package.json | 7 + src/inputnumber/lib/package.json | 12 + src/inputnumber/lib/project.json | 62 + .../lib/src/TiInputNumberDirective.ts | 273 + .../lib/src/TiInputNumberModule.ts | 22 + src/intro/demo/karma.conf.js | 44 + src/intro/demo/project.json | 98 + src/intro/demo/src/app/AppComponent.ts | 7 + src/intro/demo/src/app/AppModule.ts | 26 + src/intro/demo/src/app/IndexComponent.ts | 49 + src/intro/demo/src/app/app.html | 3 + .../demo/src/app/intro/IntroBasicComponent.ts | 66 + .../demo/src/app/intro/IntroEventComponent.ts | 66 + .../demo/src/app/intro/IntroModalComponent.ts | 54 + .../src/app/intro/IntroSkipableComponent.ts | 57 + .../src/app/intro/IntroTemplateComponent.ts | 58 + .../demo/src/app/intro/IntroTestModule.ts | 67 + .../demo/src/app/intro/IntroTipComponent.ts | 74 + .../src/app/intro/IntroTiscrollComponent.ts | 37 + src/intro/demo/src/app/intro/intro-basic.html | 10 + src/intro/demo/src/app/intro/intro-event.html | 10 + src/intro/demo/src/app/intro/intro-modal.html | 22 + .../demo/src/app/intro/intro-skipable.html | 24 + .../demo/src/app/intro/intro-template.html | 29 + src/intro/demo/src/app/intro/intro-tip.html | 45 + .../demo/src/app/intro/intro-tiscroll.html | 27 + .../demo/src/app/intro/webdoc/intro-demos.js | 47 + .../demo/src/app/intro/webdoc/intro.cn.md | 30 + .../demo/src/app/intro/webdoc/intro.en.md | 29 + src/intro/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/intro/demo/src/index.html | 16 + src/intro/demo/src/main.ts | 13 + src/intro/demo/test.ts | 24 + src/intro/demo/tsconfig.app.json | 15 + src/intro/demo/tsconfig.spec.json | 20 + src/intro/lib/index.ts | 2 + src/intro/lib/ng-package.json | 7 + src/intro/lib/package.json | 17 + src/intro/lib/project.json | 62 + src/intro/lib/src/TiIntroInterface.ts | 107 + src/intro/lib/src/TiIntroModule.ts | 38 + src/intro/lib/src/TiIntroService.ts | 456 ++ src/intro/lib/src/TiIntromodalComponent.ts | 55 + src/intro/lib/src/TiIntrotipComponent.ts | 27 + src/intro/lib/src/i18n/TiIntroWords.ts | 8 + src/intro/lib/src/i18n/en_US.ts | 10 + src/intro/lib/src/i18n/es_US.ts | 10 + src/intro/lib/src/i18n/fr_FR.ts | 10 + src/intro/lib/src/i18n/index.ts | 7 + src/intro/lib/src/i18n/pt_BR.ts | 10 + src/intro/lib/src/i18n/zh_CN.ts | 10 + src/intro/lib/src/intro.less | 87 + src/intro/lib/src/intromodal.html | 32 + src/intro/lib/src/introtip.html | 49 + src/ip/demo/karma.conf.js | 44 + src/ip/demo/project.json | 90 + src/ip/demo/src/app/AppComponent.ts | 7 + src/ip/demo/src/app/AppModule.ts | 24 + src/ip/demo/src/app/IndexComponent.ts | 49 + src/ip/demo/src/app/app.html | 3 + src/ip/demo/src/app/ip/IpBasicComponent.ts | 10 + src/ip/demo/src/app/ip/IpDisabledComponent.ts | 9 + .../demo/src/app/ip/IpFormcontrolComponent.ts | 15 + src/ip/demo/src/app/ip/IpTestModule.ts | 45 + src/ip/demo/src/app/ip/IpValidComponent.ts | 9 + src/ip/demo/src/app/ip/ip-basic.html | 16 + src/ip/demo/src/app/ip/ip-disabled.html | 1 + src/ip/demo/src/app/ip/ip-formcontrol.html | 6 + src/ip/demo/src/app/ip/ip-valid.html | 9 + src/ip/demo/src/app/ip/webdoc/ip-demos.js | 53 + src/ip/demo/src/app/ip/webdoc/ip.cn.md | 23 + src/ip/demo/src/app/ip/webdoc/ip.en.md | 29 + src/ip/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/ip/demo/src/index.html | 16 + src/ip/demo/src/main.ts | 13 + src/ip/demo/test.ts | 24 + src/ip/demo/tsconfig.app.json | 15 + src/ip/demo/tsconfig.spec.json | 20 + src/ip/lib/index.ts | 1 + src/ip/lib/ng-package.json | 7 + src/ip/lib/package.json | 13 + src/ip/lib/project.json | 62 + src/ip/lib/src/TiIpComponent.ts | 464 ++ src/ip/lib/src/TiIpModule.ts | 23 + src/ip/lib/src/ip.html | 20 + src/ip/lib/src/ip.less | 64 + src/ipsection/demo/karma.conf.js | 44 + src/ipsection/demo/project.json | 90 + src/ipsection/demo/src/app/AppComponent.ts | 7 + src/ipsection/demo/src/app/AppModule.ts | 24 + src/ipsection/demo/src/app/IndexComponent.ts | 49 + src/ipsection/demo/src/app/app.html | 3 + .../app/ipsection/IpsectionBasicComponent.ts | 23 + .../ipsection/IpsectionDisabledComponent.ts | 22 + .../app/ipsection/IpsectionEventsComponent.ts | 23 + .../app/ipsection/IpsectionFocusComponent.ts | 22 + .../app/ipsection/IpsectionTestComponent.ts | 43 + .../src/app/ipsection/IpsectionTestModule.ts | 68 + .../app/ipsection/IpsectionValidComponent.ts | 31 + .../IpsectionValidFormgroupComponent.ts | 37 + .../src/app/ipsection/ipsection-basic.html | 11 + .../src/app/ipsection/ipsection-disabled.html | 5 + .../src/app/ipsection/ipsection-events.html | 13 + .../src/app/ipsection/ipsection-focus.html | 12 + .../src/app/ipsection/ipsection-test.html | 12 + .../ipsection/ipsection-valid-formgroup.html | 6 + .../src/app/ipsection/ipsection-valid.html | 4 + .../app/ipsection/webdoc/ipsection-demos.js | 66 + .../src/app/ipsection/webdoc/ipsection.cn.md | 28 + .../src/app/ipsection/webdoc/ipsection.en.md | 29 + src/ipsection/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/ipsection/demo/src/index.html | 16 + src/ipsection/demo/src/main.ts | 13 + src/ipsection/demo/test.ts | 24 + src/ipsection/demo/tsconfig.app.json | 15 + src/ipsection/demo/tsconfig.spec.json | 20 + src/ipsection/lib/index.ts | 1 + src/ipsection/lib/ng-package.json | 7 + src/ipsection/lib/package.json | 14 + src/ipsection/lib/project.json | 62 + src/ipsection/lib/src/TiIpsectionComponent.ts | 267 + src/ipsection/lib/src/TiIpsectionModule.ts | 26 + src/ipsection/lib/src/ipsection.html | 35 + src/ipsection/lib/src/ipsection.less | 52 + src/labeleditor/demo/project.json | 90 + src/labeleditor/demo/src/app/AppComponent.ts | 7 + src/labeleditor/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/labeleditor/demo/src/app/app.html | 3 + .../LabeleditorAutotipComponent.ts | 18 + .../labeleditor/LabeleditorBasicComponent.ts | 17 + .../LabeleditorDisabledComponent.ts | 19 + .../labeleditor/LabeleditorEventsComponent.ts | 26 + .../LabeleditorIcontipcontextComponent.ts | 49 + .../LabeleditorMaxlengthComponent.ts | 19 + .../LabeleditorMaxlineComponent.ts | 17 + .../LabeleditorMultilineSizeComponent.ts | 17 + .../labeleditor/LabeleditorResizeComponent.ts | 17 + .../app/labeleditor/LabeleditorTestModule.ts | 70 + .../LabeleditorValidationAsyncComponent.ts | 56 + .../LabeleditorValidationComponent.ts | 25 + .../app/labeleditor/labeleditor-autotip.html | 2 + .../app/labeleditor/labeleditor-basic.html | 5 + .../app/labeleditor/labeleditor-disabled.html | 2 + .../app/labeleditor/labeleditor-events.html | 10 + .../labeleditor-icontipcontext.html | 25 + .../labeleditor/labeleditor-maxlength.html | 11 + .../app/labeleditor/labeleditor-maxline.html | 1 + .../labeleditor-multiline-size.html | 9 + .../app/labeleditor/labeleditor-resize.html | 24 + .../labeleditor-validation-async.html | 4 + .../labeleditor/labeleditor-validation.html | 17 + .../labeleditor/webdoc/labeleditor-demos.js | 150 + .../app/labeleditor/webdoc/labeleditor.cn.md | 15 + .../app/labeleditor/webdoc/labeleditor.en.md | 13 + src/labeleditor/demo/src/favicon.ico | Bin 0 -> 300852 bytes src/labeleditor/demo/src/index.html | 16 + src/labeleditor/demo/src/main.ts | 13 + src/labeleditor/demo/tsconfig.app.json | 10 + src/labeleditor/lib/index.ts | 2 + src/labeleditor/lib/ng-package.json | 7 + src/labeleditor/lib/package.json | 18 + src/labeleditor/lib/project.json | 62 + .../lib/src/TiLabeleditorComponent.ts | 259 + .../lib/src/TiLabeleditorModule.ts | 42 + src/labeleditor/lib/src/labeleditor.html | 103 + src/labeleditor/lib/src/labeleditor.less | 65 + src/layout/demo/karma.conf.js | 44 + src/layout/demo/project.json | 90 + src/layout/demo/src/app/AppComponent.ts | 7 + src/layout/demo/src/app/AppModule.ts | 24 + src/layout/demo/src/app/IndexComponent.ts | 49 + src/layout/demo/src/app/app.html | 3 + .../src/app/layout/LayoutBasicComponent.ts | 37 + .../app/layout/LayoutBasicSimpleComponent.ts | 7 + .../LayoutBasicSimpleResponsiveComponent.ts | 7 + .../app/layout/LayoutDetailColumnComponent.ts | 61 + .../src/app/layout/LayoutDetailComponent.ts | 69 + .../src/app/layout/LayoutListComponent.ts | 157 + .../layout/LayoutListLargedataComponent.ts | 153 + .../app/layout/LayoutMultiColumnComponent.ts | 7 + .../src/app/layout/LayoutOverviewComponent.ts | 49 + .../layout/LayoutOverviewVerticalComponent.ts | 49 + .../src/app/layout/LayoutPurchaseComponent.ts | 52 + ...LayoutPurchaseResponsiveChangeComponent.ts | 56 + .../LayoutPurchaseResponsiveComponent.ts | 51 + .../layout/LayoutPurchaseSimpleComponent.ts | 14 + ...LayoutPurchaseSimpleResponsiveComponent.ts | 14 + .../src/app/layout/LayoutSingleComponent.ts | 7 + .../demo/src/app/layout/LayoutTestModule.ts | 160 + .../layout-basic-simple-responsive.html | 14 + .../src/app/layout/layout-basic-simple.html | 14 + .../demo/src/app/layout/layout-basic.html | 31 + .../demo/src/app/layout/layout-basic.less | 30 + .../demo/src/app/layout/layout-column.less | 18 + .../src/app/layout/layout-detail-column.html | 67 + .../demo/src/app/layout/layout-detail.html | 56 + .../src/app/layout/layout-list-largedata.html | 57 + .../demo/src/app/layout/layout-list.html | 56 + .../src/app/layout/layout-multi-column.html | 21 + .../app/layout/layout-overview-vertical.html | 36 + .../demo/src/app/layout/layout-overview.html | 38 + .../layout-purchase-responsive-change.html | 41 + .../layout/layout-purchase-responsive.html | 40 + .../layout-purchase-simple-responsive.html | 26 + .../app/layout/layout-purchase-simple.html | 21 + .../demo/src/app/layout/layout-purchase.html | 34 + .../demo/src/app/layout/layout-simple.less | 14 + .../demo/src/app/layout/layout-single.html | 14 + src/layout/demo/src/app/layout/layout.less | 21 + .../src/app/layout/webdoc/layout-demos.js | 90 + .../demo/src/app/layout/webdoc/layout.cn.md | 23 + .../demo/src/app/layout/webdoc/layout.en.md | 29 + src/layout/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/layout/demo/src/index.html | 16 + src/layout/demo/src/main.ts | 13 + src/layout/demo/test.ts | 24 + src/layout/demo/tsconfig.app.json | 15 + src/layout/demo/tsconfig.spec.json | 20 + src/layout/lib/index.ts | 1 + src/layout/lib/ng-package.json | 7 + src/layout/lib/package.json | 9 + src/layout/lib/project.json | 62 + src/layout/lib/src/TiLayoutColumnComponent.ts | 26 + .../lib/src/TiLayoutContentBodyComponent.ts | 26 + .../lib/src/TiLayoutContentComponent.ts | 37 + .../lib/src/TiLayoutContentHeaderComponent.ts | 26 + src/layout/lib/src/TiLayoutHeaderComponent.ts | 37 + src/layout/lib/src/TiLayoutModule.ts | 46 + .../lib/src/TiLayoutSectionComponent.ts | 29 + src/layout/lib/src/layout-column.less | 11 + src/layout/lib/src/layout-content-body.less | 39 + src/layout/lib/src/layout-content-header.less | 5 + src/layout/lib/src/layout-content.less | 68 + src/layout/lib/src/layout-header.less | 31 + src/layout/lib/src/layout-section.less | 30 + src/layout/lib/src/layout.less | 4 + src/leftmenu/demo/karma.conf.js | 44 + src/leftmenu/demo/project.json | 90 + src/leftmenu/demo/src/app/AppComponent.ts | 7 + src/leftmenu/demo/src/app/AppModule.ts | 24 + src/leftmenu/demo/src/app/IndexComponent.ts | 49 + src/leftmenu/demo/src/app/app.html | 3 + .../leftmenu/LeftmenuActiveChangeComponent.ts | 41 + .../app/leftmenu/LeftmenuBasicComponent.ts | 36 + .../LeftmenuCandeactivateComponent.ts | 32 + .../leftmenu/LeftmenuCollapsedComponent.ts | 37 + .../app/leftmenu/LeftmenuDisabledComponent.ts | 38 + .../app/leftmenu/LeftmenuDividingComponent.ts | 38 + .../src/app/leftmenu/LeftmenuFootComponent.ts | 37 + .../app/leftmenu/LeftmenuGroupComponent.ts | 49 + .../src/app/leftmenu/LeftmenuHrefComponent.ts | 36 + .../src/app/leftmenu/LeftmenuIdComponent.ts | 65 + .../app/leftmenu/LeftmenuNoRouterComponent.ts | 34 + .../app/leftmenu/LeftmenuParamsComponent.ts | 43 + .../leftmenu/LeftmenuReloadStateComponent.ts | 35 + .../leftmenu/LeftmenuRouterlistComponent.ts | 38 + .../app/leftmenu/LeftmenuScrollComponent.ts | 63 + .../app/leftmenu/LeftmenuSecurityComponent.ts | 57 + .../src/app/leftmenu/LeftmenuTestModule.ts | 277 + .../leftmenu/LeftmenuToggleableComponent.ts | 32 + .../demo/src/app/leftmenu/OpenerComponent.ts | 32 + .../src/app/leftmenu/Router11Component.ts | 7 + .../src/app/leftmenu/Router12Component.ts | 25 + .../demo/src/app/leftmenu/Router2Component.ts | 24 + .../src/app/leftmenu/Router31Component.ts | 26 + .../src/app/leftmenu/Router32Component.ts | 28 + .../demo/src/app/leftmenu/RouterAComponent.ts | 12 + .../demo/src/app/leftmenu/RouterBComponent.ts | 16 + .../demo/src/app/leftmenu/RouterCComponent.ts | 12 + .../demo/src/app/leftmenu/RouterDComponent.ts | 12 + .../src/app/leftmenu/RouterparamsComponent.ts | 27 + .../app/leftmenu/leftmenu-active-change.html | 21 + .../demo/src/app/leftmenu/leftmenu-basic.html | 13 + .../app/leftmenu/leftmenu-candeactivate.html | 13 + .../src/app/leftmenu/leftmenu-collapsed.html | 19 + .../src/app/leftmenu/leftmenu-disabled.html | 13 + .../src/app/leftmenu/leftmenu-dividing.html | 13 + .../demo/src/app/leftmenu/leftmenu-foot.html | 19 + .../demo/src/app/leftmenu/leftmenu-group.html | 29 + .../demo/src/app/leftmenu/leftmenu-href.html | 13 + .../demo/src/app/leftmenu/leftmenu-id.html | 29 + .../src/app/leftmenu/leftmenu-no-router.html | 23 + .../src/app/leftmenu/leftmenu-params.html | 13 + .../app/leftmenu/leftmenu-reload-state.html | 13 + .../src/app/leftmenu/leftmenu-routerlist.html | 13 + .../src/app/leftmenu/leftmenu-scroll.html | 26 + .../src/app/leftmenu/leftmenu-security.html | 25 + .../src/app/leftmenu/leftmenu-toggleable.html | 13 + .../demo/src/app/leftmenu/leftmenuTest.less | 9 + src/leftmenu/demo/src/app/leftmenu/unsave.ts | 8 + .../src/app/leftmenu/webdoc/leftmenu-demos.js | 163 + .../src/app/leftmenu/webdoc/leftmenu.cn.md | 36 + .../src/app/leftmenu/webdoc/leftmenu.en.md | 29 + ...eftmenuActiveChangeWebsiteViewComponent.ts | 39 + .../LeftmenuBasicWebsiteViewComponent.ts | 34 + .../LeftmenuCollapsedWebsiteViewComponent.ts | 35 + .../LeftmenuDisabledWebsiteViewComponent.ts | 36 + .../LeftmenuDividingWebsiteViewComponent.ts | 35 + .../LeftmenuFootWebsiteViewComponent.ts | 34 + .../LeftmenuGroupWebsiteViewComponent.ts | 46 + .../LeftmenuHrefWebsiteViewComponent.ts | 36 + .../LeftmenuNoRouterWebsiteViewComponent.ts | 34 + .../LeftmenuParamsWebsiteViewComponent.ts | 34 + ...LeftmenuReloadStateWebsiteViewComponent.ts | 33 + .../LeftmenuRouterlistWebsiteViewComponent.ts | 42 + .../LeftmenuToggleableWebsiteViewComponent.ts | 30 + .../leftmenu-active-change-website-view.html | 27 + .../leftmenu-basic-website-view.html | 25 + .../leftmenu-collapsed-website-view.html | 26 + .../leftmenu-disabled-website-view.html | 25 + .../leftmenu-dividing-website-view.html | 26 + .../leftmenu-foot-website-view.html | 29 + .../leftmenu-group-website-view.html | 40 + .../leftmenu-href-website-view.html | 23 + .../leftmenu-no-router-website-view.html | 28 + .../leftmenu-params-website-view.html | 27 + .../leftmenu-reload-state-website-view.html | 19 + .../leftmenu-routerlist-website-view.html | 27 + .../leftmenu-toggleable-website-view.html | 19 + src/leftmenu/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/leftmenu/demo/src/index.html | 16 + src/leftmenu/demo/src/main.ts | 13 + src/leftmenu/demo/test.ts | 24 + src/leftmenu/demo/tsconfig.app.json | 15 + src/leftmenu/demo/tsconfig.spec.json | 20 + src/leftmenu/lib/index.ts | 1 + src/leftmenu/lib/ng-package.json | 7 + src/leftmenu/lib/package.json | 16 + src/leftmenu/lib/project.json | 62 + src/leftmenu/lib/src/TiLeftmenuComponent.ts | 513 ++ .../lib/src/TiLeftmenuFootComponent.ts | 40 + .../lib/src/TiLeftmenuGroupComponent.ts | 66 + .../lib/src/TiLeftmenuGroupItemComponent.ts | 23 + .../lib/src/TiLeftmenuHeadComponent.ts | 24 + .../lib/src/TiLeftmenuItemComponent.ts | 32 + .../lib/src/TiLeftmenuLevel1Component.ts | 261 + .../lib/src/TiLeftmenuLevel2Component.ts | 158 + src/leftmenu/lib/src/TiLeftmenuModule.ts | 58 + src/leftmenu/lib/src/leftmenu-foot.html | 1 + src/leftmenu/lib/src/leftmenu-group-item.html | 1 + src/leftmenu/lib/src/leftmenu-group.html | 4 + src/leftmenu/lib/src/leftmenu-head.html | 1 + src/leftmenu/lib/src/leftmenu-item.html | 1 + src/leftmenu/lib/src/leftmenu-level1.html | 28 + src/leftmenu/lib/src/leftmenu-level2.html | 3 + src/leftmenu/lib/src/leftmenu.html | 17 + src/leftmenu/lib/src/leftmenu.less | 340 ++ src/linkbutton/demo/karma.conf.js | 41 + src/linkbutton/demo/project.json | 90 + src/linkbutton/demo/src/app/AppComponent.ts | 7 + src/linkbutton/demo/src/app/AppModule.ts | 26 + src/linkbutton/demo/src/app/IndexComponent.ts | 49 + src/linkbutton/demo/src/app/app.html | 3 + .../linkbutton/LinkbuttonBasicComponent.ts | 8 + .../app/linkbutton/LinkbuttonTestModule.ts | 13 + .../src/app/linkbutton/linkbutton-basic.html | 1 + .../app/linkbutton/webdoc/linkbutton-demos.js | 25 + .../app/linkbutton/webdoc/linkbutton.cn.md | 24 + .../app/linkbutton/webdoc/linkbutton.en.md | 29 + src/linkbutton/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/linkbutton/demo/src/index.html | 16 + src/linkbutton/demo/src/main.ts | 13 + src/linkbutton/demo/test.ts | 24 + src/linkbutton/demo/tsconfig.app.json | 10 + src/linkbutton/demo/tsconfig.spec.json | 15 + src/linkbutton/lib/index.ts | 1 + src/linkbutton/lib/ng-package.json | 7 + src/linkbutton/lib/package.json | 11 + src/linkbutton/lib/project.json | 62 + .../lib/src/TiLinkbuttonComponent.ts | 30 + src/linkbutton/lib/src/TiLinkbuttonModule.ts | 24 + src/linkbutton/lib/src/linkbutton.html | 4 + src/linkbutton/lib/src/linkbutton.less | 33 + src/list/demo/project.json | 78 + src/list/demo/src/app/AppComponent.ts | 7 + src/list/demo/src/app/AppModule.ts | 24 + src/list/demo/src/app/IndexComponent.ts | 49 + src/list/demo/src/app/app.html | 3 + .../demo/src/app/list/ListDefaultComponent.ts | 32 + .../demo/src/app/list/ListGroupComponent.ts | 92 + .../demo/src/app/list/ListMultiComponent.ts | 32 + .../src/app/list/ListSelectallComponent.ts | 181 + src/list/demo/src/app/list/ListTestModule.ts | 27 + .../demo/src/app/list/ListTipComponent.ts | 21 + src/list/demo/src/app/list/list-default.html | 46 + src/list/demo/src/app/list/list-group.html | 35 + src/list/demo/src/app/list/list-multi.html | 64 + .../demo/src/app/list/list-selectall.html | 40 + src/list/demo/src/app/list/list-tip.html | 41 + src/list/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/list/demo/src/index.html | 16 + src/list/demo/src/main.ts | 13 + src/list/demo/tsconfig.app.json | 15 + src/list/lib/index.ts | 1 + src/list/lib/ng-package.json | 7 + src/list/lib/package.json | 16 + src/list/lib/project.json | 62 + src/list/lib/src/TiListComponent.ts | 764 +++ src/list/lib/src/TiListModule.ts | 34 + src/list/lib/src/i18n/TiListWords.ts | 6 + src/list/lib/src/i18n/en_US.ts | 8 + src/list/lib/src/i18n/es_US.ts | 8 + src/list/lib/src/i18n/fr_FR.ts | 8 + src/list/lib/src/i18n/index.ts | 7 + src/list/lib/src/i18n/pt_BR.ts | 8 + src/list/lib/src/i18n/zh_CN.ts | 8 + src/list/lib/src/list-multi.less | 53 + src/list/lib/src/list.html | 108 + src/list/lib/src/list.less | 149 + src/loading/demo/karma.conf.js | 44 + src/loading/demo/project.json | 90 + src/loading/demo/src/app/AppComponent.ts | 7 + src/loading/demo/src/app/AppModule.ts | 24 + src/loading/demo/src/app/IndexComponent.ts | 49 + src/loading/demo/src/app/app.html | 3 + .../src/app/loading/LoadingAreaComponent.ts | 31 + .../src/app/loading/LoadingBasicComponent.ts | 6 + .../src/app/loading/LoadingSizeComponent.ts | 6 + .../demo/src/app/loading/LoadingTestModule.ts | 36 + .../src/app/loading/LoadingTypeComponent.ts | 6 + .../demo/src/app/loading/loading-area.html | 9 + .../demo/src/app/loading/loading-basic.html | 1 + .../demo/src/app/loading/loading-size.html | 6 + .../demo/src/app/loading/loading-type.html | 1 + .../src/app/loading/webdoc/loading-demos.js | 54 + .../demo/src/app/loading/webdoc/loading.cn.md | 22 + .../demo/src/app/loading/webdoc/loading.en.md | 29 + src/loading/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/loading/demo/src/index.html | 16 + src/loading/demo/src/main.ts | 13 + src/loading/demo/test.ts | 24 + src/loading/demo/tsconfig.app.json | 15 + src/loading/demo/tsconfig.spec.json | 20 + src/loading/lib/index.ts | 1 + src/loading/lib/ng-package.json | 7 + src/loading/lib/package.json | 12 + src/loading/lib/project.json | 62 + src/loading/lib/src/TiLoadingComponent.ts | 68 + src/loading/lib/src/TiLoadingModule.ts | 32 + src/loading/lib/src/TiLoadingfailComponent.ts | 46 + src/loading/lib/src/i18n/TiLoadingWords.ts | 6 + src/loading/lib/src/i18n/en_US.ts | 8 + src/loading/lib/src/i18n/es_US.ts | 8 + src/loading/lib/src/i18n/fr_FR.ts | 8 + src/loading/lib/src/i18n/index.ts | 7 + src/loading/lib/src/i18n/pt_BR.ts | 8 + src/loading/lib/src/i18n/zh_CN.ts | 8 + src/loading/lib/src/loading.html | 20 + src/loading/lib/src/loading.less | 116 + src/loading/lib/src/loadingfail.html | 12 + src/loading/lib/src/loadingfail.less | 36 + src/locale/demo/karma.conf.js | 44 + src/locale/demo/project.json | 90 + src/locale/demo/src/app/AppComponent.ts | 7 + src/locale/demo/src/app/AppModule.ts | 24 + src/locale/demo/src/app/IndexComponent.ts | 49 + src/locale/demo/src/app/app.html | 3 + .../src/app/locale/LocaleBasicComponent.ts | 61 + .../src/app/locale/LocaleFormatComponent.ts | 12 + .../src/app/locale/LocaleReloadComponent.ts | 70 + .../demo/src/app/locale/LocaleTestModule.ts | 35 + .../demo/src/app/locale/locale-basic.html | 7 + .../demo/src/app/locale/locale-format.html | 5 + .../demo/src/app/locale/locale-reload.html | 6 + .../src/app/locale/webdoc/locale-demos.js | 40 + .../demo/src/app/locale/webdoc/locale.cn.md | 28 + .../demo/src/app/locale/webdoc/locale.en.md | 29 + src/locale/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/locale/demo/src/index.html | 16 + src/locale/demo/src/main.ts | 13 + src/locale/demo/test.ts | 24 + src/locale/demo/tsconfig.app.json | 15 + src/locale/demo/tsconfig.spec.json | 20 + src/locale/lib/index.ts | 1 + src/locale/lib/ng-package.json | 7 + src/locale/lib/package.json | 10 + src/locale/lib/project.json | 62 + src/locale/lib/src/TiLocale.ts | 141 + src/locale/lib/src/TiLocaleFormat.ts | 162 + src/locale/lib/src/TiLocaleModule.ts | 24 + src/locale/lib/src/TiTranslatePipe.ts | 25 + src/menu/demo/karma.conf.js | 44 + src/menu/demo/project.json | 90 + src/menu/demo/src/app/AppComponent.ts | 7 + src/menu/demo/src/app/AppModule.ts | 24 + src/menu/demo/src/app/IndexComponent.ts | 49 + src/menu/demo/src/app/app.html | 3 + .../demo/src/app/menu/MenuBasicComponent.ts | 40 + .../src/app/menu/MenuBeforeopenComponent.ts | 46 + .../demo/src/app/menu/MenuBorderComponent.ts | 55 + .../src/app/menu/MenuButtoncolorComponent.ts | 39 + .../demo/src/app/menu/MenuDefaultComponent.ts | 78 + .../src/app/menu/MenuDisabledComponent.ts | 42 + .../demo/src/app/menu/MenuEventComponent.ts | 45 + .../demo/src/app/menu/MenuGroupComponent.ts | 42 + src/menu/demo/src/app/menu/MenuIdComponent.ts | 76 + .../src/app/menu/MenuLabelkeyComponent.ts | 40 + .../src/app/menu/MenuPanelalignComponent.ts | 40 + .../src/app/menu/MenuPanelstyleComponent.ts | 40 + .../src/app/menu/MenuTempleteComponent.ts | 26 + .../src/app/menu/MenuTempleteTestComponent.ts | 74 + src/menu/demo/src/app/menu/MenuTestModule.ts | 100 + .../demo/src/app/menu/MenuTipsComponent.ts | 45 + src/menu/demo/src/app/menu/menu-basic.html | 2 + .../demo/src/app/menu/menu-beforeopen.html | 1 + src/menu/demo/src/app/menu/menu-border.html | 10 + .../demo/src/app/menu/menu-buttoncolor.html | 1 + src/menu/demo/src/app/menu/menu-default.html | 38 + src/menu/demo/src/app/menu/menu-disabled.html | 3 + src/menu/demo/src/app/menu/menu-event.html | 3 + src/menu/demo/src/app/menu/menu-group.html | 1 + src/menu/demo/src/app/menu/menu-id.html | 13 + src/menu/demo/src/app/menu/menu-labelkey.html | 1 + .../demo/src/app/menu/menu-panelalign.html | 1 + .../demo/src/app/menu/menu-panelstyle.html | 1 + .../demo/src/app/menu/menu-templete-test.html | 14 + src/menu/demo/src/app/menu/menu-templete.html | 11 + src/menu/demo/src/app/menu/menu-tips.html | 1 + .../demo/src/app/menu/webdoc/menu-demos.js | 166 + src/menu/demo/src/app/menu/webdoc/menu.cn.md | 24 + src/menu/demo/src/app/menu/webdoc/menu.en.md | 29 + src/menu/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/menu/demo/src/index.html | 16 + src/menu/demo/src/main.ts | 13 + src/menu/demo/test.ts | 24 + src/menu/demo/tsconfig.app.json | 15 + src/menu/demo/tsconfig.spec.json | 20 + src/menu/lib/index.ts | 1 + src/menu/lib/ng-package.json | 7 + src/menu/lib/package.json | 14 + src/menu/lib/project.json | 62 + src/menu/lib/src/TiMenuComponent.ts | 395 ++ src/menu/lib/src/TiMenuItem.ts | 49 + src/menu/lib/src/TiMenuListComponent.ts | 53 + src/menu/lib/src/TiMenuModule.ts | 27 + src/menu/lib/src/menu.html | 56 + src/menu/lib/src/menu.less | 115 + src/menu/lib/src/menulist.html | 39 + src/menu/lib/src/menulist.less | 103 + src/message/demo/karma.conf.js | 44 + src/message/demo/project.json | 90 + src/message/demo/src/app/AppComponent.ts | 7 + src/message/demo/src/app/AppModule.ts | 26 + src/message/demo/src/app/IndexComponent.ts | 49 + src/message/demo/src/app/app.html | 3 + .../src/app/message/MessageBasicComponent.ts | 20 + .../src/app/message/MessageBtnComponent.ts | 51 + .../app/message/MessageBtnTestComponent.ts | 66 + .../app/message/MessageContentComponent.ts | 39 + .../src/app/message/MessageIdComponent.ts | 21 + .../app/message/MessageSecurityComponent.ts | 24 + .../demo/src/app/message/MessageTestModule.ts | 79 + .../src/app/message/MessageTitleComponent.ts | 15 + .../src/app/message/MessageTypeComponent.ts | 32 + .../demo/src/app/message/message-basic.html | 6 + .../src/app/message/message-btn-test.html | 6 + .../demo/src/app/message/message-btn.html | 1 + .../demo/src/app/message/message-content.html | 6 + .../demo/src/app/message/message-id.html | 4 + .../src/app/message/message-security.html | 10 + .../demo/src/app/message/message-title.html | 1 + .../demo/src/app/message/message-type.html | 4 + .../src/app/message/webdoc/message-demos.js | 68 + .../demo/src/app/message/webdoc/message.cn.md | 34 + .../demo/src/app/message/webdoc/message.en.md | 29 + src/message/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/message/demo/src/index.html | 16 + src/message/demo/src/main.ts | 13 + src/message/demo/test.ts | 24 + src/message/demo/tsconfig.app.json | 15 + src/message/demo/tsconfig.spec.json | 20 + src/message/lib/index.ts | 2 + src/message/lib/ng-package.json | 7 + src/message/lib/package.json | 15 + src/message/lib/project.json | 62 + .../lib/src/TiContentWrapperComponent.ts | 21 + src/message/lib/src/TiMessageComponent.html | 39 + src/message/lib/src/TiMessageComponent.ts | 138 + src/message/lib/src/TiMessageInterface.ts | 89 + src/message/lib/src/TiMessageModule.ts | 37 + src/message/lib/src/TiMessageService.ts | 78 + src/message/lib/src/TiTranscludeDirective.ts | 40 + src/message/lib/src/i18n/TiMessageWords.ts | 10 + src/message/lib/src/i18n/en_US.ts | 12 + src/message/lib/src/i18n/es_US.ts | 12 + src/message/lib/src/i18n/fr_FR.ts | 12 + src/message/lib/src/i18n/index.ts | 7 + src/message/lib/src/i18n/pt_BR.ts | 12 + src/message/lib/src/i18n/zh_CN.ts | 12 + src/message/lib/src/message.less | 53 + src/modal/demo/karma.conf.js | 44 + src/modal/demo/project.json | 90 + src/modal/demo/src/app/AppComponent.ts | 7 + src/modal/demo/src/app/AppModule.ts | 26 + src/modal/demo/src/app/IndexComponent.ts | 49 + src/modal/demo/src/app/app.html | 3 + .../src/app/modal/ModalAnimationComponent.ts | 15 + .../src/app/modal/ModalBackdropComponent.ts | 15 + .../demo/src/app/modal/ModalClassComponent.ts | 17 + .../src/app/modal/ModalCloseIconComponent.ts | 15 + .../src/app/modal/ModalConfigTestComponent.ts | 57 + .../app/modal/ModalContentCompComponent.ts | 58 + .../app/modal/ModalContentTempComponent.ts | 15 + .../src/app/modal/ModalDraggableComponent.ts | 15 + .../demo/src/app/modal/ModalEscComponent.ts | 15 + .../demo/src/app/modal/ModalEventComponent.ts | 25 + .../app/modal/ModalHeaderAlignComponent.ts | 15 + .../app/modal/ModalHeaderStyleComponent.ts | 16 + .../demo/src/app/modal/ModalTestModule.ts | 133 + .../app/modal/ModalTwoBackdropComponent.ts | 25 + .../src/app/modal/ModalTwoTestComponent.ts | 78 + src/modal/demo/src/app/modal/TestComponent.ts | 72 + .../demo/src/app/modal/modal-animation.html | 11 + .../demo/src/app/modal/modal-backdrop.html | 11 + src/modal/demo/src/app/modal/modal-class.html | 11 + .../demo/src/app/modal/modal-close-icon.html | 11 + .../demo/src/app/modal/modal-config-test.html | 24 + .../src/app/modal/modal-content-comp.html | 2 + .../src/app/modal/modal-content-temp.html | 11 + .../demo/src/app/modal/modal-draggable.html | 11 + src/modal/demo/src/app/modal/modal-esc.html | 11 + src/modal/demo/src/app/modal/modal-event.html | 11 + .../src/app/modal/modal-header-align.html | 11 + .../src/app/modal/modal-header-style.html | 22 + .../src/app/modal/modal-two-backdrop.html | 6 + .../demo/src/app/modal/modal-two-test.html | 7 + .../demo/src/app/modal/webdoc/modal-demos.js | 138 + .../demo/src/app/modal/webdoc/modal.cn.md | 34 + .../demo/src/app/modal/webdoc/modal.en.md | 29 + src/modal/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/modal/demo/src/index.html | 16 + src/modal/demo/src/main.ts | 13 + src/modal/demo/test.ts | 24 + src/modal/demo/tsconfig.app.json | 15 + src/modal/demo/tsconfig.spec.json | 20 + src/modal/lib/index.ts | 2 + src/modal/lib/ng-package.json | 7 + src/modal/lib/package.json | 18 + src/modal/lib/project.json | 62 + src/modal/lib/src/TiBackdropComponent.ts | 85 + .../lib/src/TiBackdropNoAnimationComponent.ts | 28 + src/modal/lib/src/TiModalBodyComponent.ts | 24 + src/modal/lib/src/TiModalComponent.html | 16 + src/modal/lib/src/TiModalComponent.ts | 351 ++ .../lib/src/TiModalComponentNoAnimation.html | 15 + src/modal/lib/src/TiModalFooterComponent.ts | 24 + src/modal/lib/src/TiModalHeaderComponent.ts | 24 + src/modal/lib/src/TiModalInterface.ts | 134 + src/modal/lib/src/TiModalModule.ts | 56 + .../lib/src/TiModalNoAnimationComponent.ts | 29 + src/modal/lib/src/TiModalService.ts | 244 + src/modal/lib/src/backdrop.less | 17 + src/modal/lib/src/modal.less | 114 + src/nav/demo/karma.conf.js | 44 + src/nav/demo/project.json | 98 + src/nav/demo/src/app/AppComponent.ts | 7 + src/nav/demo/src/app/AppModule.ts | 24 + src/nav/demo/src/app/IndexComponent.ts | 49 + src/nav/demo/src/app/app.html | 3 + .../demo/src/app/nav/NavActiveComponent.ts | 84 + src/nav/demo/src/app/nav/NavAlignComponent.ts | 14 + src/nav/demo/src/app/nav/NavBasicComponent.ts | 15 + .../demo/src/app/nav/NavDisabledComponent.ts | 85 + src/nav/demo/src/app/nav/NavEventComponent.ts | 49 + src/nav/demo/src/app/nav/NavLeftComponent.ts | 16 + src/nav/demo/src/app/nav/NavRightComponent.ts | 14 + .../src/app/nav/NavSelectableComponent.ts | 91 + .../demo/src/app/nav/NavSubmenuComponent.ts | 45 + .../demo/src/app/nav/NavTemplateComponent.ts | 49 + src/nav/demo/src/app/nav/NavTestModule.ts | 100 + src/nav/demo/src/app/nav/NavThemeComponent.ts | 14 + src/nav/demo/src/app/nav/NavWidthComponent.ts | 14 + src/nav/demo/src/app/nav/nav-active.html | 19 + src/nav/demo/src/app/nav/nav-align.html | 23 + src/nav/demo/src/app/nav/nav-basic.html | 9 + src/nav/demo/src/app/nav/nav-disabled.html | 19 + src/nav/demo/src/app/nav/nav-event.html | 16 + src/nav/demo/src/app/nav/nav-left.html | 15 + src/nav/demo/src/app/nav/nav-right.html | 16 + src/nav/demo/src/app/nav/nav-selectable.html | 19 + src/nav/demo/src/app/nav/nav-submenu.html | 33 + src/nav/demo/src/app/nav/nav-template.html | 17 + src/nav/demo/src/app/nav/nav-theme.html | 9 + src/nav/demo/src/app/nav/nav-width.html | 4 + src/nav/demo/src/app/nav/webdoc/nav-demos.js | 149 + src/nav/demo/src/app/nav/webdoc/nav.cn.md | 25 + src/nav/demo/src/app/nav/webdoc/nav.en.md | 7 + src/nav/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/nav/demo/src/index.html | 16 + src/nav/demo/src/main.ts | 13 + src/nav/demo/test.ts | 24 + src/nav/demo/tsconfig.app.json | 15 + src/nav/demo/tsconfig.spec.json | 20 + src/nav/lib/index.ts | 1 + src/nav/lib/ng-package.json | 7 + src/nav/lib/package.json | 9 + src/nav/lib/project.json | 62 + src/nav/lib/src/TiNavComponent.ts | 35 + src/nav/lib/src/TiNavLeftComponent.ts | 28 + src/nav/lib/src/TiNavMenuComponent.ts | 257 + src/nav/lib/src/TiNavModule.ts | 30 + src/nav/lib/src/TiNavRightComponent.ts | 25 + src/nav/lib/src/common.less | 18 + src/nav/lib/src/interface.ts | 46 + src/nav/lib/src/nav.html | 1 + src/nav/lib/src/nav.less | 16 + src/nav/lib/src/navleft.html | 1 + src/nav/lib/src/navleft.less | 15 + src/nav/lib/src/navmenu.html | 72 + src/nav/lib/src/navmenu.less | 186 + src/nav/lib/src/navright.html | 1 + src/nav/lib/src/navright.less | 11 + src/ng/demo/karma.conf.js | 44 + src/ng/demo/project.json | 120 + src/ng/demo/src/app/AppComponent.ts | 32 + src/ng/demo/src/app/AppModule.ts | 32 + src/ng/demo/src/app/AppWcModule.ts | 5394 +++++++++++++++++ src/ng/demo/src/app/DemoModules.ts | 204 + src/ng/demo/src/app/IndexComponent.ts | 58 + src/ng/demo/src/app/app.html | 3 + src/ng/demo/src/assets/browser/chrome.PNG | Bin 0 -> 1994 bytes src/ng/demo/src/assets/browser/edge.PNG | Bin 0 -> 2067 bytes src/ng/demo/src/assets/browser/firefox.PNG | Bin 0 -> 2185 bytes src/ng/demo/src/assets/browser/safari.PNG | Bin 0 -> 2402 bytes src/ng/demo/src/assets/food/cake.png | Bin 0 -> 4044 bytes src/ng/demo/src/assets/food/coffee.png | Bin 0 -> 4799 bytes src/ng/demo/src/assets/food/cola.png | Bin 0 -> 4115 bytes src/ng/demo/src/assets/food/fried_chicken.png | Bin 0 -> 6156 bytes src/ng/demo/src/assets/food/fries.png | Bin 0 -> 6098 bytes src/ng/demo/src/assets/food/hamburger.png | Bin 0 -> 7032 bytes src/ng/demo/src/assets/food/milk.png | Bin 0 -> 4271 bytes src/ng/demo/src/assets/food/pizza.png | Bin 0 -> 6149 bytes src/ng/demo/src/assets/image/1.jpg | Bin 0 -> 107192 bytes src/ng/demo/src/assets/image/2.jpg | Bin 0 -> 46016 bytes src/ng/demo/src/assets/image/3.jpg | Bin 0 -> 100875 bytes src/ng/demo/src/assets/nav_logo/logo.png | Bin 0 -> 6847 bytes src/ng/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/ng/demo/src/index.html | 16 + src/ng/demo/src/main.ts | 49 + src/ng/demo/src/webdoc/LICENSE | 103 + src/ng/demo/src/webdoc/faq-en.md | 0 src/ng/demo/src/webdoc/faq.md | 79 + src/ng/demo/src/webdoc/getstart-en.md | 0 src/ng/demo/src/webdoc/getstart.md | 89 + src/ng/demo/src/webdoc/images/basecolor1.png | Bin 0 -> 28643 bytes src/ng/demo/src/webdoc/images/demo.png | Bin 0 -> 2042 bytes src/ng/demo/src/webdoc/images/theme.png | Bin 0 -> 18370 bytes src/ng/demo/src/webdoc/introduce-en.md | 0 src/ng/demo/src/webdoc/introduce.md | 34 + src/ng/demo/src/webdoc/joinus-en.md | 0 src/ng/demo/src/webdoc/joinus.md | 156 + src/ng/demo/src/webdoc/language-en.md | 0 src/ng/demo/src/webdoc/language.md | 209 + src/ng/demo/src/webdoc/menus.js | 206 + src/ng/demo/src/webdoc/themedoc-en.md | 0 src/ng/demo/src/webdoc/themedoc.md | 227 + src/ng/demo/src/webdoc/validators.md | 42 + src/ng/demo/test.ts | 24 + src/ng/demo/tsconfig.app.json | 10 + src/ng/demo/tsconfig.spec.json | 20 + src/ng/demolog/DemoLogComponent.ts | 22 + src/ng/demolog/DemoLogModule.ts | 10 + src/ng/demolog/log.html | 3 + src/ng/demolog/log.less | 12 + src/ng/lib/index.ts | 102 + src/ng/lib/ng-package.json | 7 + src/ng/lib/package.json | 110 + src/ng/lib/project.json | 49 + src/notification/demo/karma.conf.js | 44 + src/notification/demo/project.json | 90 + src/notification/demo/src/app/AppComponent.ts | 7 + src/notification/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/notification/demo/src/app/app.html | 3 + .../NotificationAnimationComponent.ts | 18 + .../NotificationBasicComponent.ts | 12 + .../NotificationCloseComponent.ts | 29 + .../NotificationConfigComponent.ts | 26 + .../NotificationDurationComponent.ts | 20 + .../NotificationEventsComponent.ts | 19 + .../NotificationHoverPauseComponent.ts | 18 + .../notification/NotificationNameComponent.ts | 23 + .../NotificationPositionComponent.ts | 49 + .../NotificationTemplateComponent.ts | 14 + .../notification/NotificationTestModule.ts | 99 + .../notification/NotificationTypeComponent.ts | 25 + .../notification/notification-animation.html | 2 + .../app/notification/notification-basic.html | 1 + .../app/notification/notification-close.html | 5 + .../app/notification/notification-config.html | 3 + .../notification/notification-duration.html | 3 + .../app/notification/notification-events.html | 1 + .../notification-hover-pause.html | 2 + .../app/notification/notification-name.html | 2 + .../notification/notification-position.html | 8 + .../notification/notification-template.html | 12 + .../app/notification/notification-type.html | 5 + .../notification/webdoc/notification-demos.js | 146 + .../notification/webdoc/notification.cn.md | 23 + .../notification/webdoc/notification.en.md | 29 + src/notification/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/notification/demo/src/index.html | 16 + src/notification/demo/src/main.ts | 13 + src/notification/demo/test.ts | 24 + src/notification/demo/tsconfig.app.json | 15 + src/notification/demo/tsconfig.spec.json | 20 + src/notification/lib/index.ts | 5 + src/notification/lib/ng-package.json | 7 + src/notification/lib/package.json | 15 + src/notification/lib/project.json | 62 + .../lib/src/TiNotificationComponent.html | 19 + .../lib/src/TiNotificationComponent.ts | 145 + .../src/TiNotificationContainerComponent.html | 48 + .../src/TiNotificationContainerComponent.ts | 180 + .../lib/src/TiNotificationInterface.ts | 138 + .../lib/src/TiNotificationMapper.ts | 37 + .../lib/src/TiNotificationModule.ts | 26 + .../lib/src/TiNotificationMotion.ts | 25 + .../lib/src/TiNotificationService.ts | 138 + src/notification/lib/src/notification.less | 34 + src/notification/lib/src/position.less | 33 + src/outline/lib/index.ts | 1 + src/outline/lib/ng-package.json | 7 + src/outline/lib/package.json | 9 + src/outline/lib/project.json | 62 + src/outline/lib/src/TiOutlineDirective.ts | 56 + src/outline/lib/src/TiOutlineModule.ts | 22 + src/overflow/demo/karma.conf.js | 44 + src/overflow/demo/project.json | 90 + src/overflow/demo/src/app/AppComponent.ts | 7 + src/overflow/demo/src/app/AppModule.ts | 24 + src/overflow/demo/src/app/IndexComponent.ts | 49 + src/overflow/demo/src/app/app.html | 3 + .../app/overflow/OverflowDestoryComponent.ts | 23 + .../overflow/OverflowDirectiveComponent.ts | 20 + .../app/overflow/OverflowMaxlineComponent.ts | 8 + .../app/overflow/OverflowMaxwidthComponent.ts | 8 + .../app/overflow/OverflowPositionComponent.ts | 8 + .../app/overflow/OverflowServiceComponent.ts | 42 + .../src/app/overflow/OverflowTestComponent.ts | 12 + .../src/app/overflow/OverflowTestModule.ts | 56 + .../overflow/OverflowTipcontentComponent.ts | 9 + .../src/app/overflow/overflow-destory.html | 10 + .../src/app/overflow/overflow-directive.html | 16 + .../src/app/overflow/overflow-maxline.html | 2 + .../src/app/overflow/overflow-maxwidth.html | 4 + .../src/app/overflow/overflow-position.html | 9 + .../src/app/overflow/overflow-service.html | 9 + .../demo/src/app/overflow/overflow-test.html | 60 + .../src/app/overflow/overflow-tipcontent.html | 7 + .../src/app/overflow/webdoc/overflow-demos.js | 71 + .../src/app/overflow/webdoc/overflow.cn.md | 27 + .../src/app/overflow/webdoc/overflow.en.md | 29 + src/overflow/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/overflow/demo/src/index.html | 16 + src/overflow/demo/src/main.ts | 13 + src/overflow/demo/test.ts | 24 + src/overflow/demo/tsconfig.app.json | 15 + src/overflow/demo/tsconfig.spec.json | 20 + src/overflow/lib/index.ts | 3 + src/overflow/lib/ng-package.json | 7 + src/overflow/lib/package.json | 12 + src/overflow/lib/project.json | 62 + src/overflow/lib/src/TiOverflowDirective.ts | 61 + .../lib/src/TiOverflowMaxlineDirective.ts | 290 + src/overflow/lib/src/TiOverflowModule.ts | 24 + src/overflow/lib/src/TiOverflowService.ts | 181 + .../lib/src/TiOverflowServiceModule.ts | 19 + src/pagination/demo/karma.conf.js | 44 + src/pagination/demo/project.json | 90 + src/pagination/demo/src/app/AppComponent.ts | 7 + src/pagination/demo/src/app/AppModule.ts | 24 + src/pagination/demo/src/app/IndexComponent.ts | 49 + src/pagination/demo/src/app/app.html | 3 + .../pagination/PaginationAutohideComponent.ts | 17 + .../pagination/PaginationDisabledComponent.ts | 10 + .../pagination/PaginationEventComponent.ts | 32 + .../pagination/PaginationFixedComponent.ts | 13 + .../pagination/PaginationLoadingComponent.ts | 17 + .../PaginationPageselectwidthComponent.ts | 10 + .../pagination/PaginationPagesizeComponent.ts | 30 + .../PaginationShowgotolinkComponent.ts | 10 + .../PaginationShowlastpageComponent.ts | 10 + .../PaginationShowtotalnumberComponent.ts | 9 + .../app/pagination/PaginationTestModule.ts | 92 + .../app/pagination/PaginationTypeComponent.ts | 17 + .../app/pagination/pagination-autohide.html | 4 + .../app/pagination/pagination-disabled.html | 1 + .../src/app/pagination/pagination-event.html | 10 + .../src/app/pagination/pagination-fixed.html | 9 + .../app/pagination/pagination-loading.html | 18 + .../pagination-pageselectwidth.html | 8 + .../app/pagination/pagination-pagesize.html | 28 + .../pagination/pagination-showgotolink.html | 6 + .../pagination/pagination-showlastpage.html | 6 + .../pagination-showtotalnumber.html | 6 + .../src/app/pagination/pagination-type.html | 16 + .../app/pagination/webdoc/pagination-demos.js | 161 + .../app/pagination/webdoc/pagination.cn.md | 24 + .../app/pagination/webdoc/pagination.en.md | 29 + src/pagination/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/pagination/demo/src/index.html | 16 + src/pagination/demo/src/main.ts | 13 + src/pagination/demo/test.ts | 24 + src/pagination/demo/tsconfig.app.json | 15 + src/pagination/demo/tsconfig.spec.json | 20 + src/pagination/lib/index.ts | 1 + src/pagination/lib/ng-package.json | 7 + src/pagination/lib/package.json | 18 + src/pagination/lib/project.json | 62 + .../lib/src/TiPaginationComponent.ts | 853 +++ src/pagination/lib/src/TiPaginationModule.ts | 36 + .../lib/src/i18n/TiPaginationWords.ts | 8 + src/pagination/lib/src/i18n/en_US.ts | 10 + src/pagination/lib/src/i18n/es_US.ts | 10 + src/pagination/lib/src/i18n/fr_FR.ts | 10 + src/pagination/lib/src/i18n/index.ts | 7 + src/pagination/lib/src/i18n/pt_BR.ts | 10 + src/pagination/lib/src/i18n/zh_CN.ts | 10 + src/pagination/lib/src/pagination.html | 134 + src/pagination/lib/src/pagination.less | 231 + src/path/demo/project.json | 78 + src/path/demo/src/app/AppComponent.ts | 7 + src/path/demo/src/app/AppModule.ts | 25 + src/path/demo/src/app/IndexComponent.ts | 49 + src/path/demo/src/app/app.html | 3 + .../demo/src/app/path/PathListComponent.ts | 32 + .../demo/src/app/path/PathSelectComponent.ts | 277 + src/path/demo/src/app/path/PathTestModule.ts | 56 + .../app/path/PathfieldEditableComponent.ts | 44 + .../src/app/path/PathfieldEventComponent.ts | 29 + .../src/app/path/PathfieldIspanelComponent.ts | 26 + .../src/app/path/PathfieldItemsComponent.ts | 25 + .../app/path/PathfieldPanelwidthComponent.ts | 26 + src/path/demo/src/app/path/path-list.html | 9 + src/path/demo/src/app/path/path-select.html | 111 + .../demo/src/app/path/pathfield-editable.html | 9 + .../demo/src/app/path/pathfield-event.html | 4 + .../demo/src/app/path/pathfield-ispanel.html | 3 + .../demo/src/app/path/pathfield-items.html | 3 + .../src/app/path/pathfield-panelwidth.html | 3 + .../demo/src/app/path/webdoc/path-demos.js | 85 + src/path/demo/src/app/path/webdoc/path.cn.md | 15 + src/path/demo/src/app/path/webdoc/path.en.md | 13 + src/path/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/path/demo/src/index.html | 16 + src/path/demo/src/main.ts | 13 + src/path/demo/tsconfig.app.json | 10 + src/path/lib/index.ts | 3 + src/path/lib/ng-package.json | 7 + src/path/lib/package.json | 16 + src/path/lib/project.json | 62 + src/path/lib/src/TiPathFieldComponent.ts | 393 ++ src/path/lib/src/TiPathListComponent.ts | 62 + src/path/lib/src/TiPathModule.ts | 31 + src/path/lib/src/path-field.html | 29 + src/path/lib/src/path-field.less | 78 + src/path/lib/src/path-list.html | 8 + src/path/lib/src/path-list.less | 42 + src/phonenumber/demo/project.json | 78 + src/phonenumber/demo/src/app/AppComponent.ts | 7 + src/phonenumber/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/phonenumber/demo/src/app/app.html | 3 + .../phonenumber/PhonenumberBasicComponent.ts | 30 + .../PhonenumberCountryComponent.ts | 33 + .../PhonenumberDisabledComponent.ts | 32 + .../phonenumber/PhonenumberEventComponent.ts | 39 + .../app/phonenumber/PhonenumberTestModule.ts | 32 + .../phonenumber/PhonenumberValidComponent.ts | 32 + .../app/phonenumber/phonenumber-basic.html | 1 + .../app/phonenumber/phonenumber-country.html | 1 + .../app/phonenumber/phonenumber-disabled.html | 1 + .../app/phonenumber/phonenumber-event.html | 8 + .../app/phonenumber/phonenumber-valid.html | 4 + .../phonenumber/webdoc/phonenumber-demos.js | 75 + .../app/phonenumber/webdoc/phonenumber.cn.md | 15 + .../app/phonenumber/webdoc/phonenumber.en.md | 13 + src/phonenumber/demo/src/favicon.ico | Bin 0 -> 300852 bytes src/phonenumber/demo/src/index.html | 16 + src/phonenumber/demo/src/main.ts | 13 + src/phonenumber/demo/tsconfig.app.json | 10 + src/phonenumber/lib/index.ts | 2 + src/phonenumber/lib/ng-package.json | 7 + src/phonenumber/lib/package.json | 15 + src/phonenumber/lib/project.json | 62 + .../lib/src/TiPhoneValidatorDirective.ts | 80 + .../lib/src/TiPhonenumberComponent.ts | 107 + .../lib/src/TiPhonenumberModule.ts | 34 + .../lib/src/i18n/TiPhonenumberWords.ts | 8 + src/phonenumber/lib/src/i18n/en_US.ts | 10 + src/phonenumber/lib/src/i18n/es_US.ts | 10 + src/phonenumber/lib/src/i18n/fr_FR.ts | 10 + src/phonenumber/lib/src/i18n/index.ts | 7 + src/phonenumber/lib/src/i18n/pt_BR.ts | 10 + src/phonenumber/lib/src/i18n/zh_CN.ts | 10 + src/phonenumber/lib/src/phonenumber.html | 34 + src/phonenumber/lib/src/phonenumber.less | 102 + src/popconfirm/demo/project.json | 78 + src/popconfirm/demo/src/app/AppComponent.ts | 7 + src/popconfirm/demo/src/app/AppModule.ts | 24 + src/popconfirm/demo/src/app/IndexComponent.ts | 49 + src/popconfirm/demo/src/app/app.html | 3 + .../popconfirm/PopconfirmBasicComponent.ts | 11 + .../popconfirm/PopconfirmDefineComponent.ts | 16 + .../popconfirm/PopconfirmEventComponent.ts | 20 + .../popconfirm/PopconfirmTableComponent.ts | 123 + .../PopconfirmTableDefineComponent.ts | 87 + .../app/popconfirm/PopconfirmTestModule.ts | 56 + .../src/app/popconfirm/popconfirm-basic.html | 1 + .../src/app/popconfirm/popconfirm-define.html | 1 + .../src/app/popconfirm/popconfirm-event.html | 3 + .../popconfirm/popconfirm-table-define.html | 21 + .../src/app/popconfirm/popconfirm-table.html | 27 + .../app/popconfirm/webdoc/popconfirm-demos.js | 68 + .../app/popconfirm/webdoc/popconfirm.cn.md | 23 + .../app/popconfirm/webdoc/popconfirm.en.md | 29 + src/popconfirm/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/popconfirm/demo/src/index.html | 16 + src/popconfirm/demo/src/main.ts | 13 + src/popconfirm/demo/tsconfig.app.json | 15 + src/popconfirm/lib/index.ts | 1 + src/popconfirm/lib/ng-package.json | 7 + src/popconfirm/lib/package.json | 15 + src/popconfirm/lib/project.json | 62 + .../lib/src/TiPopconfirmComponent.ts | 42 + .../lib/src/TiPopconfirmDirective.ts | 230 + src/popconfirm/lib/src/TiPopconfirmModule.ts | 34 + .../lib/src/i18n/TiPopconfirmWords.ts | 6 + src/popconfirm/lib/src/i18n/en_US.ts | 8 + src/popconfirm/lib/src/i18n/es_US.ts | 8 + src/popconfirm/lib/src/i18n/fr_FR.ts | 8 + src/popconfirm/lib/src/i18n/index.ts | 7 + src/popconfirm/lib/src/i18n/pt_BR.ts | 8 + src/popconfirm/lib/src/i18n/zh_CN.ts | 8 + src/popconfirm/lib/src/popconfirm.html | 20 + src/popconfirm/lib/src/popconfirm.less | 49 + src/popup/lib/index.ts | 2 + src/popup/lib/ng-package.json | 7 + src/popup/lib/package.json | 11 + src/popup/lib/project.json | 62 + src/popup/lib/src/TiPopupModule.ts | 19 + src/popup/lib/src/TiPopupService.ts | 296 + src/productpreview/demo/project.json | 86 + .../demo/src/app/AppComponent.ts | 7 + src/productpreview/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/productpreview/demo/src/app/app.html | 3 + .../ProductpreviewBasicComponent.ts | 29 + .../ProductpreviewTestModule.ts | 14 + .../productpreview/productpreview-basic.html | 3 + .../webdoc/productpreview-demos.js | 17 + .../webdoc/productpreview.cn.md | 32 + .../webdoc/productpreview.en.md | 13 + src/productpreview/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/productpreview/demo/src/index.html | 16 + src/productpreview/demo/src/main.ts | 13 + src/productpreview/demo/tsconfig.app.json | 10 + src/productpreview/lib/index.ts | 2 + src/productpreview/lib/ng-package.json | 7 + src/productpreview/lib/package.json | 14 + src/productpreview/lib/project.json | 62 + .../lib/src/TiProductpreviewComponent.ts | 198 + .../lib/src/TiProductpreviewModule.ts | 29 + .../lib/src/productpreview.html | 48 + .../lib/src/productpreview.less | 157 + src/progressbar/demo/karma.conf.js | 44 + src/progressbar/demo/project.json | 90 + src/progressbar/demo/src/app/AppComponent.ts | 7 + src/progressbar/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/progressbar/demo/src/app/app.html | 3 + .../ProgressbarAnimationComponent.ts | 27 + .../progressbar/ProgressbarBasicComponent.ts | 11 + .../progressbar/ProgressbarClassComponent.ts | 32 + .../app/progressbar/ProgressbarTestModule.ts | 32 + .../progressbar/progressbar-animation.html | 9 + .../app/progressbar/progressbar-basic.html | 18 + .../app/progressbar/progressbar-class.html | 16 + .../app/progressbar/progressbar-class.less | 52 + .../progressbar/webdoc/progressbar-demos.js | 48 + .../app/progressbar/webdoc/progressbar.cn.md | 24 + .../app/progressbar/webdoc/progressbar.en.md | 29 + src/progressbar/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/progressbar/demo/src/index.html | 16 + src/progressbar/demo/src/main.ts | 13 + src/progressbar/demo/test.ts | 24 + src/progressbar/demo/tsconfig.app.json | 15 + src/progressbar/demo/tsconfig.spec.json | 20 + src/progressbar/lib/index.ts | 1 + src/progressbar/lib/ng-package.json | 7 + src/progressbar/lib/package.json | 10 + src/progressbar/lib/project.json | 62 + .../lib/src/TiProgressbarComponent.ts | 100 + .../lib/src/TiProgressbarModule.ts | 22 + src/progressbar/lib/src/progressbar.html | 9 + src/progressbar/lib/src/progressbar.less | 54 + src/progresspie/demo/project.json | 78 + src/progresspie/demo/src/app/AppComponent.ts | 7 + src/progresspie/demo/src/app/AppModule.ts | 24 + .../demo/src/app/IndexComponent.ts | 49 + src/progresspie/demo/src/app/app.html | 3 + .../progresspie/ProgresspieTestComponent.ts | 26 + .../app/progresspie/ProgresspieTestModule.ts | 22 + src/progresspie/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/progresspie/demo/src/index.html | 16 + src/progresspie/demo/src/main.ts | 13 + src/progresspie/demo/tsconfig.app.json | 15 + src/progresspie/lib/index.ts | 1 + src/progresspie/lib/ng-package.json | 7 + src/progresspie/lib/package.json | 8 + src/progresspie/lib/project.json | 62 + .../lib/src/TiProgresspieComponent.ts | 123 + .../lib/src/TiProgresspieModule.ts | 22 + src/radio/demo/karma.conf.js | 44 + src/radio/demo/project.json | 90 + src/radio/demo/src/app/AppComponent.ts | 7 + src/radio/demo/src/app/AppModule.ts | 24 + src/radio/demo/src/app/IndexComponent.ts | 51 + src/radio/demo/src/app/app.html | 3 + .../demo/src/app/radio/RadioBasicComponent.ts | 8 + .../demo/src/app/radio/RadioDarkComponent.ts | 8 + .../src/app/radio/RadioDisabledComponent.ts | 49 + .../demo/src/app/radio/RadioEventComponent.ts | 61 + .../demo/src/app/radio/RadioFocusComponent.ts | 34 + .../demo/src/app/radio/RadioGroupComponent.ts | 26 + .../app/radio/RadioGroupDirectionComponent.ts | 26 + .../app/radio/RadioGroupLabelkeyComponent.ts | 26 + .../app/radio/RadioGroupLinewrapComponent.ts | 21 + .../radio/RadioGroupValidationComponent.ts | 72 + .../app/radio/RadioGroupValuekeyComponent.ts | 26 + .../demo/src/app/radio/RadioLabelComponent.ts | 15 + .../demo/src/app/radio/RadioTestModule.ts | 101 + src/radio/demo/src/app/radio/radio-basic.html | 10 + src/radio/demo/src/app/radio/radio-dark.html | 12 + .../demo/src/app/radio/radio-disabled.html | 43 + src/radio/demo/src/app/radio/radio-event.html | 23 + src/radio/demo/src/app/radio/radio-focus.html | 39 + .../src/app/radio/radio-group-direction.html | 4 + .../src/app/radio/radio-group-labelkey.html | 4 + .../src/app/radio/radio-group-linewrap.html | 1 + .../src/app/radio/radio-group-validation.html | 25 + .../src/app/radio/radio-group-valuekey.html | 4 + src/radio/demo/src/app/radio/radio-group.html | 4 + src/radio/demo/src/app/radio/radio-label.html | 40 + .../demo/src/app/radio/webdoc/radio-demos.js | 134 + .../demo/src/app/radio/webdoc/radio.cn.md | 29 + .../demo/src/app/radio/webdoc/radio.en.md | 29 + src/radio/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/radio/demo/src/index.html | 16 + src/radio/demo/src/main.ts | 13 + src/radio/demo/test.ts | 24 + src/radio/demo/tsconfig.app.json | 15 + src/radio/demo/tsconfig.spec.json | 20 + src/radio/lib/index.ts | 1 + src/radio/lib/ng-package.json | 7 + src/radio/lib/package.json | 13 + src/radio/lib/project.json | 62 + src/radio/lib/src/TiRadioComponent.ts | 36 + src/radio/lib/src/TiRadioGroupComponent.ts | 82 + src/radio/lib/src/TiRadioModule.ts | 26 + src/radio/lib/src/radio-group.html | 30 + src/radio/lib/src/radio.html | 14 + src/radio/lib/src/radio.less | 175 + src/radio/lib/src/radiogroup.less | 23 + src/rate/demo/karma.conf.js | 44 + src/rate/demo/project.json | 90 + src/rate/demo/src/app/AppComponent.ts | 7 + src/rate/demo/src/app/AppModule.ts | 24 + src/rate/demo/src/app/IndexComponent.ts | 49 + src/rate/demo/src/app/app.html | 3 + .../demo/src/app/rate/RateBasicComponent.ts | 8 + .../src/app/rate/RateDisabledComponent.ts | 9 + .../demo/src/app/rate/RateEventComponent.ts | 13 + src/rate/demo/src/app/rate/RateIdComponent.ts | 24 + .../demo/src/app/rate/RateLoadComponent.ts | 45 + src/rate/demo/src/app/rate/RateTestModule.ts | 37 + src/rate/demo/src/app/rate/rate-basic.html | 1 + src/rate/demo/src/app/rate/rate-disabled.html | 1 + src/rate/demo/src/app/rate/rate-event.html | 3 + src/rate/demo/src/app/rate/rate-id.html | 10 + src/rate/demo/src/app/rate/rate-load.html | 24 + .../demo/src/app/rate/webdoc/rate-demos.js | 39 + src/rate/demo/src/app/rate/webdoc/rate.cn.md | 23 + src/rate/demo/src/app/rate/webdoc/rate.en.md | 29 + src/rate/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/rate/demo/src/index.html | 16 + src/rate/demo/src/main.ts | 13 + src/rate/demo/test.ts | 24 + src/rate/demo/tsconfig.app.json | 15 + src/rate/demo/tsconfig.spec.json | 20 + src/rate/lib/index.ts | 1 + src/rate/lib/ng-package.json | 7 + src/rate/lib/package.json | 11 + src/rate/lib/project.json | 62 + src/rate/lib/src/TiRateComponent.ts | 108 + src/rate/lib/src/TiRateModule.ts | 24 + src/rate/lib/src/rate.html | 11 + src/rate/lib/src/rate.less | 30 + src/renderer/lib/index.ts | 2 + src/renderer/lib/ng-package.json | 7 + src/renderer/lib/package.json | 8 + src/renderer/lib/project.json | 62 + src/renderer/lib/src/TiRenderer.ts | 114 + src/renderer/lib/src/TiRendererModule.ts | 19 + src/rights/demo/project.json | 78 + src/rights/demo/src/app/AppComponent.ts | 7 + src/rights/demo/src/app/AppModule.ts | 24 + src/rights/demo/src/app/IndexComponent.ts | 49 + src/rights/demo/src/app/app.html | 3 + .../src/app/rights/RightsBasicComponent.ts | 31 + .../demo/src/app/rights/RightsTestModule.ts | 24 + .../src/app/rights/RightsTypeComponent.ts | 32 + .../demo/src/app/rights/rights-basic.html | 1 + .../demo/src/app/rights/rights-type.html | 1 + .../src/app/rights/webdoc/rights-demos.js | 29 + .../demo/src/app/rights/webdoc/rights.cn.md | 19 + .../demo/src/app/rights/webdoc/rights.en.md | 13 + src/rights/demo/src/favicon.ico | Bin 0 -> 300852 bytes src/rights/demo/src/index.html | 16 + src/rights/demo/src/main.ts | 13 + src/rights/demo/tsconfig.app.json | 10 + src/rights/lib/index.ts | 2 + src/rights/lib/ng-package.json | 7 + src/rights/lib/package.json | 12 + src/rights/lib/project.json | 62 + src/rights/lib/src/TiRightsComponent.ts | 64 + src/rights/lib/src/TiRightsModule.ts | 25 + src/rights/lib/src/fonts/rightsFont.svg | 28 + src/rights/lib/src/fonts/rightsFont.woff | Bin 0 -> 1832 bytes src/rights/lib/src/icon.less | 32 + src/rights/lib/src/rights.html | 15 + src/rights/lib/src/rights.less | 38 + src/score/demo/karma.conf.js | 44 + src/score/demo/project.json | 90 + src/score/demo/src/app/AppComponent.ts | 7 + src/score/demo/src/app/AppModule.ts | 24 + src/score/demo/src/app/IndexComponent.ts | 49 + src/score/demo/src/app/app.html | 3 + .../demo/src/app/score/ScoreBasicComponent.ts | 8 + .../src/app/score/ScoreEventsComponent.ts | 12 + .../src/app/score/ScoreLimittextComponent.ts | 9 + .../src/app/score/ScorePaddingComponent.ts | 8 + .../demo/src/app/score/ScoreTestModule.ts | 26 + src/score/demo/src/app/score/score-basic.html | 1 + .../demo/src/app/score/score-events.html | 1 + .../demo/src/app/score/score-limittext.html | 1 + .../demo/src/app/score/score-padding.html | 1 + .../demo/src/app/score/webdoc/score-demos.js | 32 + .../demo/src/app/score/webdoc/score.cn.md | 23 + .../demo/src/app/score/webdoc/score.en.md | 29 + src/score/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/score/demo/src/index.html | 16 + src/score/demo/src/main.ts | 13 + src/score/demo/test.ts | 24 + src/score/demo/tsconfig.app.json | 15 + src/score/demo/tsconfig.spec.json | 20 + src/score/lib/index.ts | 1 + src/score/lib/ng-package.json | 7 + src/score/lib/package.json | 12 + src/score/lib/project.json | 62 + src/score/lib/src/TiScoreComponent.ts | 61 + src/score/lib/src/TiScoreModule.ts | 30 + src/score/lib/src/i18n/TiScoreWords.ts | 6 + src/score/lib/src/i18n/en_US.ts | 8 + src/score/lib/src/i18n/es_US.ts | 8 + src/score/lib/src/i18n/fr_FR.ts | 8 + src/score/lib/src/i18n/index.ts | 7 + src/score/lib/src/i18n/pt_BR.ts | 8 + src/score/lib/src/i18n/zh_CN.ts | 8 + src/score/lib/src/score.html | 18 + src/score/lib/src/score.less | 48 + src/scroll/lib/index.ts | 1 + src/scroll/lib/ng-package.json | 7 + src/scroll/lib/package.json | 9 + src/scroll/lib/project.json | 62 + src/scroll/lib/src/TiScrollDirective.ts | 30 + src/scroll/lib/src/TiScrollModule.ts | 22 + src/searchbox/demo/karma.conf.js | 44 + src/searchbox/demo/project.json | 98 + src/searchbox/demo/src/app/AppComponent.ts | 7 + src/searchbox/demo/src/app/AppModule.ts | 24 + src/searchbox/demo/src/app/IndexComponent.ts | 49 + src/searchbox/demo/src/app/app.html | 3 + .../SearchboxAppendtobodyComponent.ts | 9 + .../app/searchbox/SearchboxBasicComponent.ts | 8 + .../searchbox/SearchboxDisabledComponent.ts | 8 + .../app/searchbox/SearchboxEventComponent.ts | 41 + .../searchbox/SearchboxMaxlengthComponent.ts | 8 + .../searchbox/SearchboxNotsearchComponent.ts | 13 + .../searchbox/SearchboxOptionsComponent.ts | 23 + .../searchbox/SearchboxPanelsizeComponent.ts | 28 + .../searchbox/SearchboxReactiveComponent.ts | 10 + .../searchbox/SearchboxSuggestComponent.ts | 31 + .../searchbox/SearchboxTemplateComponent.ts | 35 + .../app/searchbox/SearchboxTestComponent.ts | 58 + .../src/app/searchbox/SearchboxTestModule.ts | 115 + .../searchbox/SearchboxTrimmedComponent.ts | 15 + .../app/searchbox/SearchboxValidComponent.ts | 8 + .../SearchboxVirtualscrollComponent.ts | 41 + .../app/searchbox/searchbox-appendtobody.html | 7 + .../src/app/searchbox/searchbox-basic.html | 1 + .../src/app/searchbox/searchbox-disabled.html | 1 + .../src/app/searchbox/searchbox-event.html | 14 + .../app/searchbox/searchbox-maxlength.html | 1 + .../app/searchbox/searchbox-notsearch.html | 7 + .../src/app/searchbox/searchbox-options.html | 5 + .../app/searchbox/searchbox-panelsize.html | 8 + .../src/app/searchbox/searchbox-reactive.html | 1 + .../src/app/searchbox/searchbox-suggest.html | 5 + .../src/app/searchbox/searchbox-template.html | 8 + .../src/app/searchbox/searchbox-test.html | 37 + .../src/app/searchbox/searchbox-trimmed.html | 13 + .../src/app/searchbox/searchbox-valid.html | 1 + .../searchbox/searchbox-virtualscroll.html | 1 + .../app/searchbox/webdoc/searchbox-demos.js | 169 + .../src/app/searchbox/webdoc/searchbox.cn.md | 23 + .../src/app/searchbox/webdoc/searchbox.en.md | 29 + src/searchbox/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/searchbox/demo/src/index.html | 16 + src/searchbox/demo/src/main.ts | 13 + src/searchbox/demo/test.ts | 24 + src/searchbox/demo/tsconfig.app.json | 15 + src/searchbox/demo/tsconfig.spec.json | 20 + src/searchbox/lib/index.ts | 1 + src/searchbox/lib/ng-package.json | 7 + src/searchbox/lib/package.json | 18 + src/searchbox/lib/project.json | 62 + src/searchbox/lib/src/TiSearchboxComponent.ts | 138 + src/searchbox/lib/src/TiSearchboxModule.ts | 36 + .../lib/src/TiSearchboxNotsearchComponent.ts | 43 + .../lib/src/i18n/TiSearchboxWords.ts | 5 + src/searchbox/lib/src/i18n/en_US.ts | 7 + src/searchbox/lib/src/i18n/es_US.ts | 7 + src/searchbox/lib/src/i18n/fr_FR.ts | 7 + src/searchbox/lib/src/i18n/index.ts | 7 + src/searchbox/lib/src/i18n/pt_BR.ts | 7 + src/searchbox/lib/src/i18n/zh_CN.ts | 7 + .../lib/src/searchbox-notsearch.less | 26 + src/searchbox/lib/src/searchbox.html | 47 + src/searchbox/lib/src/searchbox.less | 101 + src/select/demo/karma.conf.js | 44 + src/select/demo/project.json | 98 + src/select/demo/src/app/AppComponent.ts | 7 + src/select/demo/src/app/AppModule.ts | 24 + src/select/demo/src/app/IndexComponent.ts | 49 + src/select/demo/src/app/app.html | 3 + src/select/demo/src/app/select/NoEmptyPipe.ts | 9 + .../app/select/SelectAppendtobodyComponent.ts | 8 + .../src/app/select/SelectBasicComponent.ts | 20 + .../app/select/SelectBeforesearchComponent.ts | 60 + .../select/SelectBeforesearchTestComponent.ts | 123 + .../select/SelectChangeSelectallComponent.ts | 126 + .../app/select/SelectClearableComponent.ts | 25 + .../src/app/select/SelectDisabledComponent.ts | 25 + .../select/SelectDisabledfocusComponent.ts | 25 + .../src/app/select/SelectEnumComponent.ts | 65 + .../src/app/select/SelectEventComponent.ts | 26 + .../src/app/select/SelectFocusComponent.ts | 53 + .../src/app/select/SelectGroupComponent.ts | 63 + .../demo/src/app/select/SelectIdComponent.ts | 161 + .../src/app/select/SelectIdkeyComponent.ts | 22 + .../src/app/select/SelectInputComponent.ts | 51 + .../src/app/select/SelectLabelkeyComponent.ts | 51 + .../src/app/select/SelectLazyComponent.ts | 33 + .../src/app/select/SelectLeakComponent.ts | 87 + .../src/app/select/SelectLoadComponent.ts | 108 + .../src/app/select/SelectManyComponent.ts | 95 + .../src/app/select/SelectMaxlineComponent.ts | 41 + .../src/app/select/SelectMuchComponent.ts | 63 + .../src/app/select/SelectMultiComponent.ts | 20 + .../src/app/select/SelectNoborderComponent.ts | 53 + .../src/app/select/SelectNodataComponent.ts | 9 + .../src/app/select/SelectNoemptyComponent.ts | 51 + .../src/app/select/SelectNullComponent.ts | 6 + .../SelectPaginBeforesearchComponent.ts | 188 + .../app/select/SelectPaginationComponent.ts | 65 + .../src/app/select/SelectPanelComponent.ts | 21 + .../SelectReservesearchwordComponent.ts | 41 + .../app/select/SelectScrollLoadComponent.ts | 116 + .../src/app/select/SelectSearchComponent.ts | 41 + .../app/select/SelectSearchkeysComponent.ts | 63 + .../app/select/SelectSelectallComponent.ts | 20 + .../SelectShowselectednumberComponent.ts | 21 + .../src/app/select/SelectSmallComponent.ts | 20 + .../demo/src/app/select/SelectTagComponent.ts | 84 + .../src/app/select/SelectTemplateComponent.ts | 50 + .../demo/src/app/select/SelectTestModule.ts | 293 + .../demo/src/app/select/SelectTipComponent.ts | 29 + .../src/app/select/SelectTiscrollComponent.ts | 61 + .../src/app/select/SelectTworowComponent.ts | 28 + .../src/app/select/SelectValidComponent.ts | 40 + .../app/select/SelectValidGroupComponent.ts | 75 + .../src/app/select/SelectValuekeyComponent.ts | 51 + .../app/select/SelectValuekeyTestComponent.ts | 111 + .../select/SelectVirtualscrollComponent.ts | 51 + .../SelectVirtualscrollMultiComponent.ts | 201 + .../src/app/select/select-appendtobody.html | 1 + .../demo/src/app/select/select-basic.html | 5 + .../app/select/select-beforesearch-test.html | 41 + .../src/app/select/select-beforesearch.html | 9 + .../app/select/select-change-selectall.html | 34 + .../demo/src/app/select/select-clearable.html | 24 + .../demo/src/app/select/select-disabled.html | 5 + .../src/app/select/select-disabledfocus.html | 10 + .../demo/src/app/select/select-enum.html | 46 + .../demo/src/app/select/select-event.html | 3 + .../demo/src/app/select/select-focus.html | 38 + .../demo/src/app/select/select-group.html | 1 + src/select/demo/src/app/select/select-id.html | 53 + .../demo/src/app/select/select-idkey.html | 1 + .../demo/src/app/select/select-input.html | 29 + .../demo/src/app/select/select-labelkey.html | 1 + .../demo/src/app/select/select-lazy.html | 2 + .../demo/src/app/select/select-leak.html | 10 + .../demo/src/app/select/select-load.html | 35 + .../demo/src/app/select/select-many.html | 29 + .../demo/src/app/select/select-maxline.html | 1 + .../demo/src/app/select/select-much.html | 9 + .../demo/src/app/select/select-multi.html | 5 + .../demo/src/app/select/select-noborder.html | 53 + .../demo/src/app/select/select-nodata.html | 1 + .../demo/src/app/select/select-noempty.html | 21 + .../demo/src/app/select/select-null.html | 42 + .../app/select/select-pagin-beforesearch.html | 110 + .../src/app/select/select-pagination.html | 11 + .../demo/src/app/select/select-panel.html | 2 + .../app/select/select-reservesearchword.html | 9 + .../src/app/select/select-scroll-load.html | 12 + .../demo/src/app/select/select-search.html | 1 + .../src/app/select/select-searchkeys.html | 9 + .../demo/src/app/select/select-selectall.html | 2 + .../app/select/select-showselectednumber.html | 11 + .../demo/src/app/select/select-small.html | 1 + .../demo/src/app/select/select-tag.html | 70 + .../demo/src/app/select/select-tag.less | 70 + .../demo/src/app/select/select-template.html | 25 + .../demo/src/app/select/select-tip.html | 12 + .../demo/src/app/select/select-tiscroll.html | 56 + .../demo/src/app/select/select-tworow.html | 13 + .../demo/src/app/select/select-valid.html | 10 + .../src/app/select/select-validgroup.html | 30 + .../src/app/select/select-valuekey-test.html | 73 + .../demo/src/app/select/select-valuekey.html | 5 + .../select/select-virtualscroll-multi.html | 141 + .../src/app/select/select-virtualscroll.html | 1 + .../src/app/select/webdoc/select-demos.js | 324 + .../demo/src/app/select/webdoc/select.cn.md | 28 + .../demo/src/app/select/webdoc/select.en.md | 29 + ...3\350\257\225\347\224\250\344\276\213.txt" | 83 + src/select/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/select/demo/src/index.html | 16 + src/select/demo/src/main.ts | 13 + src/select/demo/test.ts | 24 + src/select/demo/tsconfig.app.json | 15 + src/select/demo/tsconfig.spec.json | 20 + src/select/lib/index.ts | 1 + src/select/lib/ng-package.json | 7 + src/select/lib/package.json | 17 + src/select/lib/project.json | 62 + src/select/lib/src/TiSelectComponent.ts | 429 ++ src/select/lib/src/TiSelectModule.ts | 27 + src/select/lib/src/select-small.less | 9 + src/select/lib/src/select.html | 64 + src/select/lib/src/select.less | 37 + src/selectgroup/demo/project.json | 86 + src/selectgroup/demo/src/app/AppComponent.ts | 7 + src/selectgroup/demo/src/app/AppModule.ts | 26 + .../demo/src/app/IndexComponent.ts | 49 + src/selectgroup/demo/src/app/app.html | 3 + .../selectgroup/SelectgroupBasicComponent.ts | 22 + .../SelectgroupMultipleComponent.ts | 22 + .../selectgroup/SelectgroupSelectComponent.ts | 21 + .../SelectgroupTemplateComponent.ts | 21 + .../app/selectgroup/SelectgroupTestModule.ts | 40 + .../SelectgroupValuekeyComponent.ts | 21 + .../app/selectgroup/selectgroup-basic.html | 3 + .../app/selectgroup/selectgroup-multiple.html | 9 + .../app/selectgroup/selectgroup-select.html | 10 + .../app/selectgroup/selectgroup-template.html | 28 + .../app/selectgroup/selectgroup-valuekey.html | 6 + .../selectgroup/webdoc/selectgroup-demos.js | 73 + .../app/selectgroup/webdoc/selectgroup.cn.md | 15 + .../app/selectgroup/webdoc/selectgroup.en.md | 13 + src/selectgroup/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/selectgroup/demo/src/index.html | 16 + src/selectgroup/demo/src/main.ts | 13 + src/selectgroup/demo/tsconfig.app.json | 10 + src/selectgroup/lib/index.ts | 2 + src/selectgroup/lib/ng-package.json | 7 + src/selectgroup/lib/package.json | 11 + src/selectgroup/lib/project.json | 62 + .../lib/src/TiSelectgroupComponent.ts | 32 + .../lib/src/TiSelectgroupModule.ts | 27 + .../lib/src/TiSelectitemComponent.ts | 129 + src/selectgroup/lib/src/selectgroup.html | 1 + src/selectgroup/lib/src/selectgroup.less | 180 + src/selectgroup/lib/src/selectitem.html | 23 + src/skeleton/demo/karma.conf.js | 44 + src/skeleton/demo/project.json | 90 + src/skeleton/demo/src/app/AppComponent.ts | 7 + src/skeleton/demo/src/app/AppModule.ts | 24 + src/skeleton/demo/src/app/IndexComponent.ts | 49 + src/skeleton/demo/src/app/app.html | 3 + .../src/app/skeleton/SkeletonPageComponent.ts | 8 + .../src/app/skeleton/SkeletonTestModule.ts | 31 + .../app/skeleton/SkeletonTitleComponent.ts | 7 + .../src/app/skeleton/SkeletonTypeComponent.ts | 7 + .../demo/src/app/skeleton/skeleton-page.html | 24 + .../demo/src/app/skeleton/skeleton-page.less | 43 + .../demo/src/app/skeleton/skeleton-title.html | 6 + .../demo/src/app/skeleton/skeleton-type.html | 6 + .../demo/src/app/skeleton/skeleton.less | 8 + .../src/app/skeleton/webdoc/skeleton-demos.js | 43 + .../src/app/skeleton/webdoc/skeleton.cn.md | 26 + .../src/app/skeleton/webdoc/skeleton.en.md | 29 + src/skeleton/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/skeleton/demo/src/index.html | 16 + src/skeleton/demo/src/main.ts | 13 + src/skeleton/demo/test.ts | 24 + src/skeleton/demo/tsconfig.app.json | 15 + src/skeleton/demo/tsconfig.spec.json | 20 + src/skeleton/lib/index.ts | 1 + src/skeleton/lib/ng-package.json | 7 + src/skeleton/lib/package.json | 9 + src/skeleton/lib/project.json | 62 + src/skeleton/lib/src/TiSkeletonComponent.ts | 34 + src/skeleton/lib/src/TiSkeletonModule.ts | 24 + src/skeleton/lib/src/skeleton.html | 10 + src/skeleton/lib/src/skeleton.less | 35 + src/slider/demo/karma.conf.js | 44 + src/slider/demo/project.json | 90 + src/slider/demo/src/app/AppComponent.ts | 7 + src/slider/demo/src/app/AppModule.ts | 24 + src/slider/demo/src/app/IndexComponent.ts | 49 + src/slider/demo/src/app/app.html | 3 + .../src/app/slider/SliderEventComponent.ts | 16 + .../app/slider/SliderFormcontrolComponent.ts | 25 + .../src/app/slider/SliderHiddenComponent.ts | 22 + .../src/app/slider/SliderLimitsComponent.ts | 16 + .../src/app/slider/SliderRatiosComponent.ts | 14 + .../src/app/slider/SliderScalesComponent.ts | 29 + .../src/app/slider/SliderTemplateComponent.ts | 54 + .../demo/src/app/slider/SliderTestModule.ts | 72 + .../demo/src/app/slider/SliderTipComponent.ts | 27 + .../demo/src/app/slider/slider-event.html | 5 + .../src/app/slider/slider-formcontrol.html | 4 + .../demo/src/app/slider/slider-hidden.html | 20 + .../demo/src/app/slider/slider-limits.html | 2 + .../demo/src/app/slider/slider-ratios.html | 2 + .../demo/src/app/slider/slider-scales.html | 6 + .../demo/src/app/slider/slider-template.html | 17 + .../demo/src/app/slider/slider-tip.html | 11 + .../src/app/slider/webdoc/slider-demos.js | 100 + .../demo/src/app/slider/webdoc/slider.cn.md | 24 + .../demo/src/app/slider/webdoc/slider.en.md | 29 + src/slider/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/slider/demo/src/index.html | 16 + src/slider/demo/src/main.ts | 13 + src/slider/demo/test.ts | 24 + src/slider/demo/tsconfig.app.json | 15 + src/slider/demo/tsconfig.spec.json | 20 + src/slider/lib/index.ts | 1 + src/slider/lib/ng-package.json | 7 + src/slider/lib/package.json | 15 + src/slider/lib/project.json | 62 + src/slider/lib/src/TiSliderComponent.ts | 1027 ++++ src/slider/lib/src/TiSliderModule.ts | 25 + src/slider/lib/src/slider.html | 43 + src/slider/lib/src/slider.less | 179 + src/spinner/demo/karma.conf.js | 44 + src/spinner/demo/project.json | 90 + src/spinner/demo/src/app/AppComponent.ts | 7 + src/spinner/demo/src/app/AppModule.ts | 24 + src/spinner/demo/src/app/IndexComponent.ts | 49 + src/spinner/demo/src/app/app.html | 3 + .../src/app/spinner/SpinnerBasicComponent.ts | 10 + .../app/spinner/SpinnerBasicTestComponent.ts | 119 + .../spinner/SpinnerCorrectableComponent.ts | 18 + .../app/spinner/SpinnerDisabledComponent.ts | 10 + .../src/app/spinner/SpinnerEventComponent.ts | 15 + .../src/app/spinner/SpinnerFormatComponent.ts | 10 + .../src/app/spinner/SpinnerIdComponent.ts | 9 + .../src/app/spinner/SpinnerLoadComponent.ts | 39 + .../app/spinner/SpinnerLocaleableComponent.ts | 10 + .../src/app/spinner/SpinnerMaxMinComponent.ts | 38 + .../app/spinner/SpinnerMaxlengthComponent.ts | 11 + .../src/app/spinner/SpinnerStepComponent.ts | 12 + .../src/app/spinner/SpinnerStepfnComponent.ts | 26 + .../demo/src/app/spinner/SpinnerTestModule.ts | 118 + .../src/app/spinner/SpinnerTipComponent.ts | 10 + .../app/spinner/SpinnerTipTestComponent.ts | 28 + .../app/spinner/SpinnerValidationComponent.ts | 8 + .../spinner/SpinnerValidationTestComponent.ts | 32 + .../src/app/spinner/spinner-basic-test.html | 129 + .../demo/src/app/spinner/spinner-basic.html | 1 + .../src/app/spinner/spinner-correctable.html | 17 + .../src/app/spinner/spinner-disabled.html | 1 + .../demo/src/app/spinner/spinner-event.html | 9 + .../demo/src/app/spinner/spinner-format.html | 1 + .../demo/src/app/spinner/spinner-load.html | 21 + .../src/app/spinner/spinner-localeable.html | 8 + .../demo/src/app/spinner/spinner-max-min.html | 18 + .../src/app/spinner/spinner-maxlength.html | 8 + .../demo/src/app/spinner/spinner-step.html | 1 + .../demo/src/app/spinner/spinner-stepfn.html | 2 + .../src/app/spinner/spinner-tip-test.html | 32 + .../demo/src/app/spinner/spinner-tip.html | 9 + .../app/spinner/spinner-validation-test.html | 18 + .../src/app/spinner/spinner-validation.html | 9 + .../demo/src/app/spinner/spinnerTest.less | 22 + .../src/app/spinner/webdoc/spinner-demos.js | 137 + .../demo/src/app/spinner/webdoc/spinner.cn.md | 36 + .../demo/src/app/spinner/webdoc/spinner.en.md | 29 + src/spinner/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/spinner/demo/src/index.html | 16 + src/spinner/demo/src/main.ts | 13 + src/spinner/demo/test.ts | 24 + src/spinner/demo/tsconfig.app.json | 15 + src/spinner/demo/tsconfig.spec.json | 20 + src/spinner/lib/index.ts | 1 + src/spinner/lib/ng-package.json | 7 + src/spinner/lib/package.json | 17 + src/spinner/lib/project.json | 62 + src/spinner/lib/src/TiSpinnerComponent.ts | 484 ++ src/spinner/lib/src/TiSpinnerModule.ts | 32 + src/spinner/lib/src/i18n/TiSpinnerWords.ts | 7 + src/spinner/lib/src/i18n/en_US.ts | 9 + src/spinner/lib/src/i18n/es_US.ts | 9 + src/spinner/lib/src/i18n/fr_FR.ts | 9 + src/spinner/lib/src/i18n/index.ts | 7 + src/spinner/lib/src/i18n/pt_BR.ts | 9 + src/spinner/lib/src/i18n/zh_CN.ts | 9 + src/spinner/lib/src/spinner.html | 32 + src/spinner/lib/src/spinner.less | 109 + src/steps/demo/karma.conf.js | 44 + src/steps/demo/project.json | 90 + src/steps/demo/src/app/AppComponent.ts | 7 + src/steps/demo/src/app/AppModule.ts | 24 + src/steps/demo/src/app/IndexComponent.ts | 49 + src/steps/demo/src/app/app.html | 3 + .../src/app/steps/StepsActiveComponent.ts | 33 + .../src/app/steps/StepsAdaptiveComponent.ts | 23 + .../app/steps/StepsAdaptiveTestComponent.ts | 69 + .../demo/src/app/steps/StepsBaseComponent.ts | 23 + .../src/app/steps/StepsBeforeComponent.ts | 40 + .../src/app/steps/StepsClickableComponent.ts | 26 + .../src/app/steps/StepsEventsComponent.ts | 27 + .../demo/src/app/steps/StepsLabelComponent.ts | 27 + .../src/app/steps/StepsMaxwidthComponent.ts | 23 + .../src/app/steps/StepsTemplateComponent.ts | 36 + .../demo/src/app/steps/StepsTestModule.ts | 78 + .../demo/src/app/steps/steps-active.html | 9 + .../src/app/steps/steps-adaptive-test.html | 29 + .../demo/src/app/steps/steps-adaptive.html | 3 + src/steps/demo/src/app/steps/steps-base.html | 1 + .../demo/src/app/steps/steps-before.html | 2 + .../demo/src/app/steps/steps-clickable.html | 4 + .../demo/src/app/steps/steps-events.html | 8 + src/steps/demo/src/app/steps/steps-label.html | 1 + .../demo/src/app/steps/steps-maxwidth.html | 1 + .../demo/src/app/steps/steps-template.html | 11 + .../demo/src/app/steps/webdoc/steps-demos.js | 107 + .../demo/src/app/steps/webdoc/steps.cn.md | 23 + .../demo/src/app/steps/webdoc/steps.en.md | 29 + src/steps/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/steps/demo/src/index.html | 16 + src/steps/demo/src/main.ts | 13 + src/steps/demo/test.ts | 24 + src/steps/demo/tsconfig.app.json | 15 + src/steps/demo/tsconfig.spec.json | 20 + src/steps/lib/index.ts | 1 + src/steps/lib/ng-package.json | 7 + src/steps/lib/package.json | 14 + src/steps/lib/project.json | 62 + src/steps/lib/src/TiStepsComponent.ts | 283 + src/steps/lib/src/TiStepsModule.ts | 25 + src/steps/lib/src/steps.html | 51 + src/steps/lib/src/steps.less | 227 + src/subtitle/demo/karma.conf.js | 44 + src/subtitle/demo/project.json | 90 + src/subtitle/demo/src/app/AppComponent.ts | 7 + src/subtitle/demo/src/app/AppModule.ts | 24 + src/subtitle/demo/src/app/IndexComponent.ts | 49 + src/subtitle/demo/src/app/app.html | 3 + .../app/subtitle/SubtitleBasicComponent.ts | 34 + .../subtitle/SubtitleBeforeSearchComponent.ts | 85 + .../src/app/subtitle/SubtitleDarkComponent.ts | 27 + .../app/subtitle/SubtitleEventComponent.ts | 35 + .../app/subtitle/SubtitleIdkeyComponent.ts | 27 + .../app/subtitle/SubtitleItemsComponent.ts | 29 + .../app/subtitle/SubtitleMaxwidthComponent.ts | 29 + .../subtitle/SubtitlePanelwidthComponent.ts | 23 + .../app/subtitle/SubtitleRouteComponent.ts | 19 + .../subtitle/SubtitleScrollLoadComponent.ts | 74 + .../subtitle/SubtitleSearchableComponent.ts | 23 + .../app/subtitle/SubtitleTargetComponent.ts | 29 + .../src/app/subtitle/SubtitleTestModule.ts | 85 + .../subtitle/SubtitleTipPositionComponent.ts | 29 + .../demo/src/app/subtitle/subtitle-basic.html | 40 + .../app/subtitle/subtitle-before-search.html | 8 + .../demo/src/app/subtitle/subtitle-dark.html | 10 + .../demo/src/app/subtitle/subtitle-event.html | 3 + .../demo/src/app/subtitle/subtitle-idkey.html | 1 + .../demo/src/app/subtitle/subtitle-items.html | 3 + .../src/app/subtitle/subtitle-maxwidth.html | 3 + .../src/app/subtitle/subtitle-panelwidth.html | 1 + .../demo/src/app/subtitle/subtitle-route.html | 14 + .../app/subtitle/subtitle-scroll-load.html | 1 + .../src/app/subtitle/subtitle-searchable.html | 1 + .../src/app/subtitle/subtitle-target.html | 3 + .../app/subtitle/subtitle-tip-position.html | 3 + .../src/app/subtitle/webdoc/subtitle-demos.js | 147 + .../src/app/subtitle/webdoc/subtitle.cn.md | 23 + .../src/app/subtitle/webdoc/subtitle.en.md | 29 + src/subtitle/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/subtitle/demo/src/index.html | 16 + src/subtitle/demo/src/main.ts | 13 + src/subtitle/demo/test.ts | 24 + src/subtitle/demo/tsconfig.app.json | 15 + src/subtitle/demo/tsconfig.spec.json | 20 + src/subtitle/lib/index.ts | 1 + src/subtitle/lib/ng-package.json | 7 + src/subtitle/lib/package.json | 18 + src/subtitle/lib/project.json | 62 + src/subtitle/lib/src/TiSubtitleComponent.ts | 233 + src/subtitle/lib/src/TiSubtitleModule.ts | 29 + src/subtitle/lib/src/subtitle.html | 73 + src/subtitle/lib/src/subtitle.less | 106 + src/swiper/demo/karma.conf.js | 44 + src/swiper/demo/project.json | 90 + src/swiper/demo/src/app/AppComponent.ts | 7 + src/swiper/demo/src/app/AppModule.ts | 24 + src/swiper/demo/src/app/IndexComponent.ts | 49 + src/swiper/demo/src/app/app.html | 3 + .../app/swiper/SwiperActiveindexComponent.ts | 10 + .../src/app/swiper/SwiperAutoplayComponent.ts | 9 + .../src/app/swiper/SwiperBasicComponent.ts | 9 + .../src/app/swiper/SwiperEventsComponent.ts | 14 + .../SwiperIndicatorpositionComponent.ts | 9 + .../src/app/swiper/SwiperLoopComponent.ts | 9 + .../app/swiper/SwiperShowcardnumComponent.ts | 18 + .../swiper/SwiperShowcardnumTestComponent.ts | 32 + .../demo/src/app/swiper/SwiperTestModule.ts | 76 + .../src/app/swiper/swiper-activeindex.html | 10 + .../demo/src/app/swiper/swiper-autoplay.html | 7 + .../demo/src/app/swiper/swiper-basic.html | 7 + .../demo/src/app/swiper/swiper-events.html | 8 + .../app/swiper/swiper-indicatorposition.html | 27 + .../demo/src/app/swiper/swiper-loop.html | 7 + .../app/swiper/swiper-showcardnum-test.html | 36 + .../src/app/swiper/swiper-showcardnum.html | 7 + src/swiper/demo/src/app/swiper/swiper.less | 13 + .../src/app/swiper/webdoc/swiper-demos.js | 96 + .../demo/src/app/swiper/webdoc/swiper.cn.md | 31 + .../demo/src/app/swiper/webdoc/swiper.en.md | 29 + src/swiper/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/swiper/demo/src/index.html | 16 + src/swiper/demo/src/main.ts | 13 + src/swiper/demo/test.ts | 24 + src/swiper/demo/tsconfig.app.json | 15 + src/swiper/demo/tsconfig.spec.json | 20 + src/swiper/lib/index.ts | 1 + src/swiper/lib/ng-package.json | 7 + src/swiper/lib/package.json | 12 + src/swiper/lib/project.json | 62 + src/swiper/lib/src/TiSwiperComponent.ts | 397 ++ src/swiper/lib/src/TiSwiperModule.ts | 27 + src/swiper/lib/src/TiSwipercardComponent.ts | 26 + src/swiper/lib/src/swiper.html | 51 + src/swiper/lib/src/swiper.less | 155 + src/swiper/lib/src/swipercard.less | 4 + src/switch/demo/karma.conf.js | 44 + src/switch/demo/project.json | 90 + src/switch/demo/src/app/AppComponent.ts | 7 + src/switch/demo/src/app/AppModule.ts | 24 + src/switch/demo/src/app/IndexComponent.ts | 49 + src/switch/demo/src/app/app.html | 3 + .../src/app/switch/SwitchBasicComponent.ts | 8 + .../src/app/switch/SwitchBeforeComponent.ts | 17 + .../src/app/switch/SwitchDisabledComponent.ts | 8 + .../src/app/switch/SwitchEventComponent.ts | 21 + .../app/switch/SwitchExplanationComponent.ts | 10 + .../src/app/switch/SwitchFocusComponent.ts | 14 + .../demo/src/app/switch/SwitchIdComponent.ts | 8 + .../src/app/switch/SwitchLoadComponent.ts | 29 + .../src/app/switch/SwitchTemplateComponent.ts | 8 + .../demo/src/app/switch/SwitchTestModule.ts | 69 + .../demo/src/app/switch/switch-basic.html | 1 + .../demo/src/app/switch/switch-before.html | 2 + .../demo/src/app/switch/switch-disabled.html | 1 + .../demo/src/app/switch/switch-event.html | 19 + .../src/app/switch/switch-explanation.html | 1 + .../demo/src/app/switch/switch-focus.html | 29 + src/switch/demo/src/app/switch/switch-id.html | 8 + .../demo/src/app/switch/switch-load.html | 15 + .../demo/src/app/switch/switch-template.html | 4 + .../src/app/switch/webdoc/switch-demos.js | 74 + .../demo/src/app/switch/webdoc/switch.cn.md | 23 + .../demo/src/app/switch/webdoc/switch.en.md | 29 + src/switch/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/switch/demo/src/index.html | 16 + src/switch/demo/src/main.ts | 13 + src/switch/demo/test.ts | 24 + src/switch/demo/tsconfig.app.json | 15 + src/switch/demo/tsconfig.spec.json | 20 + src/switch/lib/index.ts | 1 + src/switch/lib/ng-package.json | 7 + src/switch/lib/package.json | 10 + src/switch/lib/project.json | 62 + src/switch/lib/src/TiSwitchComponent.ts | 113 + src/switch/lib/src/TiSwitchModule.ts | 22 + src/switch/lib/src/switch.html | 30 + src/switch/lib/src/switch.less | 125 + src/tab/demo/karma.conf.js | 44 + src/tab/demo/project.json | 90 + src/tab/demo/src/app/AppComponent.ts | 7 + src/tab/demo/src/app/AppModule.ts | 24 + src/tab/demo/src/app/IndexComponent.ts | 49 + src/tab/demo/src/app/app.html | 3 + src/tab/demo/src/app/tab/TabBasicComponent.ts | 20 + .../app/tab/TabBeforeactivechangeComponent.ts | 34 + .../src/app/tab/TabContentCompComponent.ts | 100 + .../src/app/tab/TabCustomHeadComponent.ts | 27 + src/tab/demo/src/app/tab/TabDarkComponent.ts | 18 + .../src/app/tab/TabDefaultTestComponent.ts | 89 + .../demo/src/app/tab/TabLazyLoadComponent.ts | 19 + .../demo/src/app/tab/TabLevel2Component.ts | 18 + .../src/app/tab/TabLevel2TestComponent.ts | 50 + .../demo/src/app/tab/TabOverflowComponent.ts | 46 + src/tab/demo/src/app/tab/TabRouteComponent.ts | 66 + .../demo/src/app/tab/TabScrollComponent.ts | 31 + src/tab/demo/src/app/tab/TabSmallComponent.ts | 18 + src/tab/demo/src/app/tab/TabTestModule.ts | 102 + src/tab/demo/src/app/tab/tab-basic.html | 11 + .../src/app/tab/tab-beforeactivechange.html | 23 + .../demo/src/app/tab/tab-content-comp.html | 34 + src/tab/demo/src/app/tab/tab-custom-head.html | 17 + src/tab/demo/src/app/tab/tab-dark.html | 11 + .../demo/src/app/tab/tab-default-test.html | 51 + src/tab/demo/src/app/tab/tab-lazy-load.html | 18 + src/tab/demo/src/app/tab/tab-level2-test.html | 24 + src/tab/demo/src/app/tab/tab-level2.html | 11 + src/tab/demo/src/app/tab/tab-overflow.html | 7 + src/tab/demo/src/app/tab/tab-route.html | 8 + src/tab/demo/src/app/tab/tab-scroll.html | 40 + src/tab/demo/src/app/tab/tab-small.html | 11 + src/tab/demo/src/app/tab/webdoc/tab-demos.js | 126 + src/tab/demo/src/app/tab/webdoc/tab.cn.md | 23 + src/tab/demo/src/app/tab/webdoc/tab.en.md | 29 + src/tab/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/tab/demo/src/index.html | 16 + src/tab/demo/src/main.ts | 13 + src/tab/demo/test.ts | 24 + src/tab/demo/tsconfig.app.json | 15 + src/tab/demo/tsconfig.spec.json | 20 + src/tab/lib/index.ts | 1 + src/tab/lib/ng-package.json | 7 + src/tab/lib/package.json | 16 + src/tab/lib/project.json | 62 + src/tab/lib/src/TiTabComponent.ts | 149 + src/tab/lib/src/TiTabHeaderComponent.ts | 66 + src/tab/lib/src/TiTabModule.ts | 31 + src/tab/lib/src/TiTabsComponent.ts | 507 ++ src/tab/lib/src/tab-head.html | 1 + src/tab/lib/src/tab.html | 4 + src/tab/lib/src/tab.less | 19 + src/tab/lib/src/tabs.html | 44 + src/tab/lib/src/tabs.less | 265 + src/table/demo/karma.conf.js | 44 + src/table/demo/project.json | 90 + src/table/demo/src/app/AppComponent.ts | 7 + src/table/demo/src/app/AppModule.ts | 24 + src/table/demo/src/app/IndexComponent.ts | 49 + src/table/demo/src/app/app.html | 3 + .../src/app/table/TableActionmenuComponent.ts | 81 + .../demo/src/app/table/TableBasicComponent.ts | 52 + .../src/app/table/TableBasicTestComponent.ts | 100 + .../src/app/table/TableCellTipComponent.ts | 56 + .../TableCelliconsColsresizableComponent.ts | 77 + .../src/app/table/TableCheckboxComponent.ts | 65 + .../table/TableCheckboxPaginationComponent.ts | 146 + ...ableCheckboxPaginationHeadmenuComponent.ts | 133 + .../src/app/table/TableColAlignComponent.ts | 52 + ...TableColalignSortResizableTestComponent.ts | 72 + .../app/table/TableColsResizableComponent.ts | 59 + .../src/app/table/TableColsToggleComponent.ts | 65 + .../table/TableColsToggleDetailsComponent.ts | 81 + .../app/table/TableColsToggleTestComponent.ts | 103 + .../table/TableColsresizableBasicComponent.ts | 72 + .../TableColsresizableColstoggleComponent.ts | 79 + ...lsresizableColstoggleFixedheadComponent.ts | 79 + .../TableColsresizableLoadfailComponent.ts | 56 + .../table/TableColsresizableSortComponent.ts | 77 + ...bleColsresizableSortHeadfilterComponent.ts | 120 + .../app/table/TableColumnFixedComponent.ts | 80 + .../TableColumnfixedCheckboxComponent.ts | 110 + .../TableColumnfixedColstoggleComponent.ts | 125 + .../table/TableColumnfixedEditrowComponent.ts | 188 + ...xedheadColsresizablePaginationComponent.ts | 99 + .../TableColumnfixedHeadfixedComponent.ts | 112 + .../TableColumnfixedLeftmenuComponent.ts | 119 + .../table/TableColumnfixedNodataComponent.ts | 71 + .../TableColumnfixedPaginationComponent.ts | 109 + .../TableColumnfixedResizableComponent.ts | 85 + .../app/table/TableComprehensiveComponent.ts | 121 + .../TableDetailsCloseotherdetailsComponent.ts | 77 + .../src/app/table/TableDetailsComponent.ts | 55 + .../table/TableDetailsNesttableComponent.ts | 129 + .../table/TableDetailsPaginationComponent.ts | 82 + .../app/table/TableDynamicDetailsComponent.ts | 66 + .../src/app/table/TableEditallComponent.ts | 80 + .../app/table/TableEditallTestComponent.ts | 168 + .../src/app/table/TableEditrowComponent.ts | 123 + .../app/table/TableEditrowTestComponent.ts | 184 + .../src/app/table/TableFilterComponent.ts | 138 + .../app/table/TableFilterStrictComponent.ts | 140 + .../TableFixedHeadColsResizableComponent.ts | 74 + .../src/app/table/TableFixedHeadComponent.ts | 76 + .../TableFixedHeadInAccordionComponent.ts | 76 + .../table/TableFixedHeadNodataComponent.ts | 58 + ...ableFixedHeadPaginationDetailsComponent.ts | 83 + ...ColsresizablePaginationDetailsComponent.ts | 83 + .../app/table/TableFixheadScrollComponent.ts | 81 + .../demo/src/app/table/TableGroupComponent.ts | 151 + .../demo/src/app/table/TableGuideComponent.ts | 55 + .../src/app/table/TableHeadFilterComponent.ts | 183 + .../table/TableHeadFilterDatetimeComponent.ts | 124 + .../TableHeadFilterDatetimeTestComponent.ts | 213 + .../table/TableHeadFilterMultiComponent.ts | 152 + .../TableHeadFilterMultiValuekeyComponent.ts | 222 + .../app/table/TableHeadFilterTestComponent.ts | 184 + .../table/TableHeadFilterValuekeyComponent.ts | 226 + .../TableHeadFilterVirtualscrollComponent.ts | 178 + .../src/app/table/TableLoadFailComponent.ts | 56 + .../src/app/table/TableNodataComponent.ts | 28 + .../app/table/TableNodataSimpleComponent.ts | 50 + .../src/app/table/TableNodataTestComponent.ts | 50 + .../app/table/TableOverflowLinkComponent.ts | 90 + .../app/table/TablePagiWithFilterComponent.ts | 95 + .../src/app/table/TablePaginationComponent.ts | 70 + .../demo/src/app/table/TableRadioComponent.ts | 65 + .../src/app/table/TableRadioTestComponent.ts | 108 + .../src/app/table/TableRowDrag2Component.ts | 77 + .../src/app/table/TableRowspanComponent.ts | 58 + .../src/app/table/TableSearchComponent.ts | 103 + .../src/app/table/TableServerPagiComponent.ts | 111 + .../TableServerPagiSearchSortComponent.ts | 152 + .../demo/src/app/table/TableSmallComponent.ts | 52 + .../src/app/table/TableSoldoutComponent.ts | 91 + .../src/app/table/TableSortBasicComponent.ts | 93 + .../app/table/TableSortComparefnComponent.ts | 85 + .../TableSortComparefnLocaleComponent.ts | 82 + .../demo/src/app/table/TableSortComponent.ts | 72 + .../app/table/TableSortDetailsComponent.ts | 89 + .../src/app/table/TableSortResetComponent.ts | 71 + .../src/app/table/TableSortTestComponent.ts | 93 + .../src/app/table/TableStorageComponent.ts | 69 + .../app/table/TableStorageConfigComponent.ts | 101 + .../app/table/TableStorageFilterComponent.ts | 123 + .../app/table/TableStorageServeComponent.ts | 115 + .../demo/src/app/table/TableTestModule.ts | 642 ++ .../demo/src/app/table/TableTreeComponent.ts | 310 + .../table/TableTreeMulitiselectComponent.ts | 356 ++ .../table/TableTreeUnknowdeepthComponent.ts | 151 + .../table/TableVirtualscrollBasicComponent.ts | 88 + .../app/table/TableVirtualscrollComponent.ts | 64 + ...ableVirtualscrollComprehensiveComponent.ts | 139 + .../table/TableVirtualscrollSizesComponent.ts | 88 + .../table/TableVirtualscrollTreeComponent.ts | 308 + .../demo/src/app/table/table-actionmenu.html | 20 + .../demo/src/app/table/table-basic-test.html | 43 + src/table/demo/src/app/table/table-basic.html | 17 + .../demo/src/app/table/table-cell-tip.html | 20 + .../table/table-cellicons-colsresizable.html | 62 + .../table-checkbox-pagination-headmenu.html | 69 + .../app/table/table-checkbox-pagination.html | 87 + .../demo/src/app/table/table-checkbox.html | 27 + .../demo/src/app/table/table-col-align.html | 20 + .../table-colalign-sort-resizable-test.html | 46 + .../src/app/table/table-cols-resizable.html | 34 + .../app/table/table-cols-toggle-details.html | 52 + .../src/app/table/table-cols-toggle-test.html | 55 + .../demo/src/app/table/table-cols-toggle.html | 21 + .../app/table/table-colsresizable-basic.html | 36 + ...le-colsresizable-colstoggle-fixedhead.html | 52 + .../table/table-colsresizable-colstoggle.html | 36 + .../table/table-colsresizable-loadfail.html | 34 + .../table-colsresizable-sort-headfilter.html | 67 + .../app/table/table-colsresizable-sort.html | 43 + .../src/app/table/table-column-fixed.html | 22 + .../app/table/table-columnfixed-checkbox.html | 41 + .../table/table-columnfixed-colstoggle.html | 49 + .../app/table/table-columnfixed-editrow.html | 95 + ...ed-fixedhead-colsresizable-pagination.html | 44 + .../table/table-columnfixed-headfixed.html | 31 + .../app/table/table-columnfixed-leftmenu.html | 54 + .../app/table/table-columnfixed-nodata.html | 35 + .../table/table-columnfixed-pagination.html | 33 + .../table/table-columnfixed-resizable.html | 37 + .../src/app/table/table-comprehensive.html | 82 + .../table-details-closeotherdetails.html | 51 + .../app/table/table-details-nesttable.html | 33 + .../app/table/table-details-pagination.html | 52 + .../demo/src/app/table/table-details.html | 34 + .../src/app/table/table-dynamic-details.html | 33 + .../src/app/table/table-editall-test.html | 60 + .../demo/src/app/table/table-editall.html | 34 + .../src/app/table/table-editrow-test.html | 87 + .../demo/src/app/table/table-editrow.html | 39 + .../src/app/table/table-filter-strict.html | 93 + .../demo/src/app/table/table-filter.html | 85 + .../table-fixed-head-cols-resizable.html | 41 + .../table/table-fixed-head-in-accordion.html | 49 + .../app/table/table-fixed-head-nodata.html | 187 + .../table-fixed-head-pagination-details.html | 72 + .../demo/src/app/table/table-fixed-head.html | 28 + ...head-colsresizable-pagination-details.html | 72 + .../src/app/table/table-fixhead-scroll.html | 51 + src/table/demo/src/app/table/table-group.html | 42 + src/table/demo/src/app/table/table-guide.html | 65 + .../table-head-filter-datetime-test.html | 65 + .../app/table/table-head-filter-datetime.html | 37 + .../table-head-filter-multi-valuekey.html | 59 + .../app/table/table-head-filter-multi.html | 57 + .../src/app/table/table-head-filter-test.html | 84 + .../app/table/table-head-filter-valuekey.html | 52 + .../table-head-filter-virtualscroll.html | 55 + .../demo/src/app/table/table-head-filter.html | 55 + .../demo/src/app/table/table-load-fail.html | 65 + .../src/app/table/table-nodata-simple.html | 28 + .../demo/src/app/table/table-nodata-test.html | 57 + .../demo/src/app/table/table-nodata.html | 22 + .../src/app/table/table-overflow-link.html | 49 + .../src/app/table/table-pagi-with-filter.html | 36 + .../demo/src/app/table/table-pagination.html | 19 + .../demo/src/app/table/table-radio-test.html | 46 + src/table/demo/src/app/table/table-radio.html | 24 + .../demo/src/app/table/table-row-drag2.html | 25 + .../demo/src/app/table/table-rowspan.html | 17 + .../demo/src/app/table/table-search.html | 70 + .../table/table-server-pagi-search-sort.html | 46 + .../demo/src/app/table/table-server-pagi.html | 32 + src/table/demo/src/app/table/table-small.html | 17 + .../demo/src/app/table/table-soldout.html | 46 + .../demo/src/app/table/table-sort-basic.html | 51 + .../table/table-sort-comparefn-locale.html | 50 + .../src/app/table/table-sort-comparefn.html | 47 + .../src/app/table/table-sort-details.html | 67 + .../demo/src/app/table/table-sort-reset.html | 32 + .../demo/src/app/table/table-sort-test.html | 37 + src/table/demo/src/app/table/table-sort.html | 34 + .../src/app/table/table-storage-config.html | 49 + .../src/app/table/table-storage-filter.html | 49 + .../src/app/table/table-storage-serve.html | 34 + .../demo/src/app/table/table-storage.html | 29 + .../app/table/table-tree-mulitiselect.html | 76 + .../app/table/table-tree-unknowdeepth.html | 21 + src/table/demo/src/app/table/table-tree.html | 44 + .../app/table/table-virtualscroll-basic.html | 65 + .../table-virtualscroll-comprehensive.html | 106 + .../app/table/table-virtualscroll-sizes.html | 204 + .../app/table/table-virtualscroll-tree.html | 67 + .../src/app/table/table-virtualscroll.html | 29 + src/table/demo/src/app/table/tableTest.less | 19 + .../demo/src/app/table/webdoc/table-demos.js | 504 ++ .../demo/src/app/table/webdoc/table.cn.md | 44 + .../demo/src/app/table/webdoc/table.en.md | 29 + src/table/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/table/demo/src/index.html | 16 + src/table/demo/src/main.ts | 13 + src/table/demo/test.ts | 24 + src/table/demo/tsconfig.app.json | 15 + src/table/demo/tsconfig.spec.json | 20 + src/table/lib/index.ts | 1 + src/table/lib/ng-package.json | 7 + src/table/lib/package.json | 28 + src/table/lib/project.json | 62 + src/table/lib/src/TiCellIconsComponent.ts | 29 + src/table/lib/src/TiCellTextComponent.ts | 87 + src/table/lib/src/TiColClickDirective.ts | 43 + src/table/lib/src/TiColsResizableDirective.ts | 604 ++ src/table/lib/src/TiColsToggleComponent.ts | 185 + .../lib/src/TiColsToggleDropComponent.ts | 157 + src/table/lib/src/TiColspanDirective.ts | 55 + src/table/lib/src/TiColumnFixedDirective.ts | 266 + src/table/lib/src/TiColumnsPipe.ts | 24 + src/table/lib/src/TiDetailsIconComponent.ts | 118 + src/table/lib/src/TiDetailsTrDirective.ts | 75 + src/table/lib/src/TiHeadFilterComponent.ts | 154 + .../lib/src/TiHeadFilterDropComponent.ts | 270 + src/table/lib/src/TiHeadMenuComponent.ts | 98 + src/table/lib/src/TiHeadSortComponent.ts | 110 + src/table/lib/src/TiTableComponent.ts | 1088 ++++ src/table/lib/src/TiTableFixedHeadService.ts | 290 + .../lib/src/TiTableFixedHeadServiceModule.ts | 19 + .../TiTableFixedSizeVirtualScrollDirective.ts | 44 + src/table/lib/src/TiTableModule.ts | 133 + .../TiTableVirtualScrollViewportComponent.ts | 159 + src/table/lib/src/cell-icons.html | 1 + src/table/lib/src/cell-text.html | 3 + src/table/lib/src/cols-toggle-drop.html | 31 + src/table/lib/src/cols-toggle.html | 22 + src/table/lib/src/details-icon.html | 7 + src/table/lib/src/head-filter-drop.html | 138 + src/table/lib/src/head-filter-drop.less | 40 + src/table/lib/src/head-filter.html | 27 + src/table/lib/src/head-filter.less | 20 + src/table/lib/src/head-menu.html | 15 + src/table/lib/src/head-menu.less | 26 + src/table/lib/src/head-sort.html | 3 + src/table/lib/src/i18n/TiTableWords.ts | 8 + src/table/lib/src/i18n/en_US.ts | 10 + src/table/lib/src/i18n/es_US.ts | 10 + src/table/lib/src/i18n/fr_FR.ts | 10 + src/table/lib/src/i18n/index.ts | 7 + src/table/lib/src/i18n/pt_BR.ts | 10 + src/table/lib/src/i18n/zh_CN.ts | 10 + .../src/table-nodata-small-nest-resize.less | 198 + .../lib/src/table-toggle-sort-details.less | 124 + src/table/lib/src/table-tree-fix.less | 145 + .../src/table-virtual-scroll-viewport.html | 3 + .../src/table-virtual-scroll-viewport.less | 34 + src/table/lib/src/table.html | 1 + src/table/lib/src/table.less | 187 + src/tag/demo/karma.conf.js | 44 + src/tag/demo/project.json | 90 + src/tag/demo/src/app/AppComponent.ts | 7 + src/tag/demo/src/app/AppModule.ts | 24 + src/tag/demo/src/app/IndexComponent.ts | 49 + src/tag/demo/src/app/app.html | 3 + src/tag/demo/src/app/tag/TagBasicComponent.ts | 12 + .../demo/src/app/tag/TagDefaultComponent.ts | 23 + .../demo/src/app/tag/TagDisabledComponent.ts | 6 + src/tag/demo/src/app/tag/TagEditComponent.ts | 62 + src/tag/demo/src/app/tag/TagTestModule.ts | 48 + src/tag/demo/src/app/tag/tag-basic.html | 1 + src/tag/demo/src/app/tag/tag-default.html | 25 + src/tag/demo/src/app/tag/tag-disabled.html | 1 + src/tag/demo/src/app/tag/tag-edit.html | 9 + src/tag/demo/src/app/tag/webdoc/tag-demos.js | 40 + src/tag/demo/src/app/tag/webdoc/tag.cn.md | 23 + src/tag/demo/src/app/tag/webdoc/tag.en.md | 29 + src/tag/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/tag/demo/src/index.html | 16 + src/tag/demo/src/main.ts | 13 + src/tag/demo/test.ts | 24 + src/tag/demo/tsconfig.app.json | 15 + src/tag/demo/tsconfig.spec.json | 20 + src/tag/lib/index.ts | 1 + src/tag/lib/ng-package.json | 7 + src/tag/lib/package.json | 12 + src/tag/lib/project.json | 62 + src/tag/lib/src/TiTagComponent.ts | 72 + src/tag/lib/src/TiTagModule.ts | 23 + src/tag/lib/src/tag-arrow.less | 44 + src/tag/lib/src/tag-rect.less | 66 + src/tag/lib/src/tag.html | 2 + src/tagsinput/demo/karma.conf.js | 44 + src/tagsinput/demo/project.json | 90 + src/tagsinput/demo/src/app/AppComponent.ts | 7 + src/tagsinput/demo/src/app/AppModule.ts | 24 + src/tagsinput/demo/src/app/IndexComponent.ts | 49 + src/tagsinput/demo/src/app/app.html | 3 + .../src/app/tagsinput/TagsInputTestModule.ts | 105 + .../app/tagsinput/TagsinputBasicComponent.ts | 18 + .../tagsinput/TagsinputDisabledComponent.ts | 18 + .../app/tagsinput/TagsinputEventsComponent.ts | 23 + .../tagsinput/TagsinputLabelkeyComponent.ts | 16 + .../tagsinput/TagsinputMaxlengthComponent.ts | 18 + .../app/tagsinput/TagsinputNullComponent.ts | 10 + .../tagsinput/TagsinputPanelwidthComponent.ts | 21 + .../tagsinput/TagsinputReactiveComponent.ts | 25 + .../tagsinput/TagsinputSeparatorsComponent.ts | 18 + .../tagsinput/TagsinputSuggestionComponent.ts | 18 + .../tagsinput/TagsinputTemplateComponent.ts | 18 + .../app/tagsinput/TagsinputValidComponent.ts | 18 + .../tagsinput/TagsinputValuekeyComponent.ts | 16 + .../src/app/tagsinput/tagsinput-basic.html | 10 + .../src/app/tagsinput/tagsinput-disabled.html | 7 + .../src/app/tagsinput/tagsinput-events.html | 8 + .../src/app/tagsinput/tagsinput-labelkey.html | 1 + .../app/tagsinput/tagsinput-maxlength.html | 11 + .../src/app/tagsinput/tagsinput-null.html | 17 + .../app/tagsinput/tagsinput-panelwidth.html | 1 + .../src/app/tagsinput/tagsinput-reactive.html | 3 + .../app/tagsinput/tagsinput-separators.html | 10 + .../app/tagsinput/tagsinput-suggestion.html | 1 + .../src/app/tagsinput/tagsinput-template.html | 5 + .../src/app/tagsinput/tagsinput-valid.html | 1 + .../src/app/tagsinput/tagsinput-valuekey.html | 4 + .../app/tagsinput/webdoc/tagsinput-demos.js | 135 + .../src/app/tagsinput/webdoc/tagsinput.cn.md | 40 + .../src/app/tagsinput/webdoc/tagsinput.en.md | 29 + src/tagsinput/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/tagsinput/demo/src/index.html | 16 + src/tagsinput/demo/src/main.ts | 13 + src/tagsinput/demo/test.ts | 24 + src/tagsinput/demo/tsconfig.app.json | 15 + src/tagsinput/demo/tsconfig.spec.json | 20 + src/tagsinput/lib/index.ts | 1 + src/tagsinput/lib/ng-package.json | 7 + src/tagsinput/lib/package.json | 15 + src/tagsinput/lib/project.json | 62 + src/tagsinput/lib/src/TiTagsInputComponent.ts | 479 ++ src/tagsinput/lib/src/TiTagsInputModule.ts | 28 + src/tagsinput/lib/src/tagsinput.html | 63 + src/tagsinput/lib/src/tagsinput.less | 37 + src/text/demo/karma.conf.js | 44 + src/text/demo/project.json | 90 + src/text/demo/src/app/AppComponent.ts | 7 + src/text/demo/src/app/AppModule.ts | 24 + src/text/demo/src/app/IndexComponent.ts | 49 + src/text/demo/src/app/app.html | 3 + .../demo/src/app/text/TextBasicComponent.ts | 8 + .../demo/src/app/text/TextClearComponent.ts | 8 + .../src/app/text/TextDisabledComponent.ts | 9 + .../demo/src/app/text/TextEventsComponent.ts | 25 + .../demo/src/app/text/TextFocusComponent.ts | 8 + .../src/app/text/TextMaskinputComponent.ts | 9 + .../src/app/text/TextNoborderTestComponent.ts | 9 + .../src/app/text/TextPasswordComponent.ts | 8 + .../app/text/TextPasswordVisibleComponent.ts | 21 + .../src/app/text/TextReactiveComponent.ts | 16 + .../src/app/text/TextReadonlyComponent.ts | 8 + src/text/demo/src/app/text/TextTestModule.ts | 90 + src/text/demo/src/app/text/text-basic.html | 4 + src/text/demo/src/app/text/text-clear.html | 1 + src/text/demo/src/app/text/text-disabled.html | 1 + src/text/demo/src/app/text/text-events.html | 13 + src/text/demo/src/app/text/text-focus.html | 1 + .../demo/src/app/text/text-maskinput.html | 4 + .../demo/src/app/text/text-noborder-test.html | 2 + .../src/app/text/text-password-visible.html | 12 + src/text/demo/src/app/text/text-password.html | 5 + src/text/demo/src/app/text/text-reactive.html | 6 + src/text/demo/src/app/text/text-readonly.html | 1 + src/text/demo/src/app/text/text.spec.ts | 54 + .../demo/src/app/text/webdoc/text-demos.js | 131 + src/text/demo/src/app/text/webdoc/text.cn.md | 29 + src/text/demo/src/app/text/webdoc/text.en.md | 29 + src/text/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/text/demo/src/index.html | 16 + src/text/demo/src/main.ts | 13 + src/text/demo/test.ts | 24 + src/text/demo/tsconfig.app.json | 15 + src/text/demo/tsconfig.spec.json | 20 + src/text/lib/index.ts | 1 + src/text/lib/ng-package.json | 7 + src/text/lib/package.json | 13 + src/text/lib/project.json | 62 + src/text/lib/src/TiMaskDirective.ts | 283 + src/text/lib/src/TiTextComponent.ts | 389 ++ src/text/lib/src/TiTextModule.ts | 25 + src/text/lib/src/clear.svg | 1 + src/text/lib/src/invisible.svg | 1 + src/text/lib/src/text.less | 101 + src/text/lib/src/visible.svg | 1 + src/textarea/demo/karma.conf.js | 44 + src/textarea/demo/project.json | 90 + src/textarea/demo/src/app/AppComponent.ts | 7 + src/textarea/demo/src/app/AppModule.ts | 24 + src/textarea/demo/src/app/IndexComponent.ts | 49 + src/textarea/demo/src/app/app.html | 3 + .../textarea/TextareaAutofocusComponent.ts | 9 + .../app/textarea/TextareaDisabledComponent.ts | 9 + .../textarea/TextareaMaxlengthComponent.ts | 10 + .../src/app/textarea/TextareaNoneComponent.ts | 12 + .../app/textarea/TextareaResizeComponent.ts | 11 + .../app/textarea/TextareaScrollComponent.ts | 20 + .../src/app/textarea/TextareaTestModule.ts | 63 + .../app/textarea/TextareaValidComponent.ts | 15 + .../app/textarea/TextareaWidthComponent.ts | 9 + .../src/app/textarea/textarea-autofocus.html | 11 + .../src/app/textarea/textarea-disabled.html | 7 + .../src/app/textarea/textarea-maxlength.html | 8 + .../demo/src/app/textarea/textarea-none.html | 18 + .../src/app/textarea/textarea-resize.html | 5 + .../src/app/textarea/textarea-scroll.html | 19 + .../demo/src/app/textarea/textarea-valid.html | 8 + .../demo/src/app/textarea/textarea-width.html | 7 + .../src/app/textarea/webdoc/textarea-demos.js | 54 + .../src/app/textarea/webdoc/textarea.cn.md | 27 + .../src/app/textarea/webdoc/textarea.en.md | 29 + src/textarea/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/textarea/demo/src/index.html | 16 + src/textarea/demo/src/main.ts | 13 + src/textarea/demo/test.ts | 24 + src/textarea/demo/tsconfig.app.json | 15 + src/textarea/demo/tsconfig.spec.json | 20 + src/textarea/lib/index.ts | 1 + src/textarea/lib/ng-package.json | 7 + src/textarea/lib/package.json | 14 + src/textarea/lib/project.json | 62 + src/textarea/lib/src/TiFormatNumPipe.ts | 26 + src/textarea/lib/src/TiTextareaComponent.ts | 456 ++ src/textarea/lib/src/TiTextareaModule.ts | 24 + src/textarea/lib/src/textarea.html | 5 + src/textarea/lib/src/textarea.less | 144 + src/themes/README.md | 30 + src/themes/basic/base-all.less | 6 + src/themes/basic/basic-var.css | 346 ++ src/themes/basic/build.less | 4 + src/themes/basic/common.less | 21 + .../basic/compnent-container-border.less | 33 + src/themes/basic/img/table-loadfail-bg.png | Bin 0 -> 3437 bytes src/themes/basic/img/table-nodata-bg.png | Bin 0 -> 3430 bytes src/themes/basic/img/upload-image-delete.png | Bin 0 -> 400 bytes src/themes/basic/img/upload-image-error.png | Bin 0 -> 1097 bytes src/themes/basic/img/upload-image-preview.png | Bin 0 -> 230 bytes src/themes/basic/link-no-decoration.less | 10 + src/themes/basic/mixins.less | 242 + src/themes/basic/normalize.less | 201 + src/themes/package.json | 5 + src/themes/project.json | 38 + src/themes/theme-blue/basic-var.less | 16 + src/themes/theme-blue/build.less | 5 + src/themes/theme-default/build.less | 3 + src/themes/theme-green/basic-var.less | 16 + src/themes/theme-green/build.less | 5 + src/themes/theme-purple/basic-var.less | 16 + src/themes/theme-purple/build.less | 5 + src/themes/theme-red/basic-var.less | 16 + src/themes/theme-red/build.less | 5 + src/time/demo/karma.conf.js | 44 + src/time/demo/project.json | 90 + src/time/demo/src/app/AppComponent.ts | 7 + src/time/demo/src/app/AppModule.ts | 24 + src/time/demo/src/app/IndexComponent.ts | 49 + src/time/demo/src/app/app.html | 3 + .../src/app/time/TimeCleariconComponent.ts | 7 + .../src/app/time/TimeDisabledComponent.ts | 7 + .../demo/src/app/time/TimeEventComponent.ts | 12 + .../demo/src/app/time/TimeFormatComponent.ts | 14 + .../demo/src/app/time/TimeMaxComponent.ts | 40 + .../demo/src/app/time/TimeMaxminComponent.ts | 11 + .../demo/src/app/time/TimeMinComponent.ts | 43 + .../app/time/TimeOptionDisabledComponent.ts | 23 + .../src/app/time/TimePanelalignComponent.ts | 7 + .../src/app/time/TimeReactiveComponent.ts | 16 + src/time/demo/src/app/time/TimeTestModule.ts | 94 + .../src/app/time/TimeValidationComponent.ts | 8 + .../demo/src/app/time/time-clearicon.html | 1 + src/time/demo/src/app/time/time-disabled.html | 1 + src/time/demo/src/app/time/time-event.html | 3 + src/time/demo/src/app/time/time-format.html | 10 + src/time/demo/src/app/time/time-max.html | 16 + src/time/demo/src/app/time/time-maxmin.html | 4 + src/time/demo/src/app/time/time-min.html | 17 + .../src/app/time/time-option-disabled.html | 21 + .../demo/src/app/time/time-panelalign.html | 1 + src/time/demo/src/app/time/time-reactive.html | 6 + .../demo/src/app/time/time-validation.html | 1 + .../demo/src/app/time/webdoc/time-demos.js | 100 + src/time/demo/src/app/time/webdoc/time.cn.md | 23 + src/time/demo/src/app/time/webdoc/time.en.md | 29 + src/time/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/time/demo/src/index.html | 16 + src/time/demo/src/main.ts | 13 + src/time/demo/test.ts | 24 + src/time/demo/tsconfig.app.json | 15 + src/time/demo/tsconfig.spec.json | 20 + src/time/lib/index.ts | 1 + src/time/lib/ng-package.json | 7 + src/time/lib/package.json | 18 + src/time/lib/project.json | 62 + src/time/lib/src/TiTimeComponent.ts | 943 +++ src/time/lib/src/TiTimeModule.ts | 34 + src/time/lib/src/i18n/TiTimeWords.ts | 7 + src/time/lib/src/i18n/en_US.ts | 9 + src/time/lib/src/i18n/es_US.ts | 9 + src/time/lib/src/i18n/fr_FR.ts | 9 + src/time/lib/src/i18n/index.ts | 7 + src/time/lib/src/i18n/pt_BR.ts | 9 + src/time/lib/src/i18n/zh_CN.ts | 9 + src/time/lib/src/time.html | 93 + src/time/lib/src/time.less | 89 + src/timeline/demo/karma.conf.js | 44 + src/timeline/demo/project.json | 90 + src/timeline/demo/src/app/AppComponent.ts | 7 + src/timeline/demo/src/app/AppModule.ts | 24 + src/timeline/demo/src/app/IndexComponent.ts | 49 + src/timeline/demo/src/app/app.html | 3 + .../app/timeline/TimelineBasicComponent.ts | 25 + .../src/app/timeline/TimelineDarkComponent.ts | 56 + .../app/timeline/TimelineHelptipComponent.ts | 58 + .../app/timeline/TimelineMultiComponent.ts | 56 + .../app/timeline/TimelineTempleteComponent.ts | 34 + .../src/app/timeline/TimelineTestComponent.ts | 113 + .../src/app/timeline/TimelineTestModule.ts | 63 + .../src/app/timeline/TimelineTypeComponent.ts | 34 + .../demo/src/app/timeline/timeline-basic.html | 1 + .../demo/src/app/timeline/timeline-dark.html | 3 + .../src/app/timeline/timeline-helptip.html | 1 + .../demo/src/app/timeline/timeline-multi.html | 1 + .../src/app/timeline/timeline-templete.html | 6 + .../demo/src/app/timeline/timeline-test.html | 23 + .../demo/src/app/timeline/timeline-type.html | 1 + .../src/app/timeline/webdoc/timeline-demos.js | 89 + .../src/app/timeline/webdoc/timeline.cn.md | 23 + .../src/app/timeline/webdoc/timeline.en.md | 29 + src/timeline/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/timeline/demo/src/index.html | 16 + src/timeline/demo/src/main.ts | 13 + src/timeline/demo/test.ts | 24 + src/timeline/demo/tsconfig.app.json | 15 + src/timeline/demo/tsconfig.spec.json | 20 + src/timeline/lib/index.ts | 1 + src/timeline/lib/ng-package.json | 7 + src/timeline/lib/package.json | 13 + src/timeline/lib/project.json | 62 + src/timeline/lib/src/TiTimelineComponent.ts | 165 + src/timeline/lib/src/TiTimelineModule.ts | 24 + src/timeline/lib/src/timeline.html | 60 + src/timeline/lib/src/timeline.less | 220 + src/tip/demo/karma.conf.js | 44 + src/tip/demo/project.json | 90 + src/tip/demo/src/app/AppComponent.ts | 7 + src/tip/demo/src/app/AppModule.ts | 24 + src/tip/demo/src/app/IndexComponent.ts | 49 + src/tip/demo/src/app/app.html | 3 + src/tip/demo/src/app/tip/TipBasicComponent.ts | 8 + .../src/app/tip/TipContentCompComponent.ts | 51 + .../app/tip/TipContentTemplateComponent.ts | 9 + src/tip/demo/src/app/tip/TipEmptyComponent.ts | 59 + .../demo/src/app/tip/TipHasArrowComponent.ts | 6 + .../app/tip/TipLongTextPositionComponent.ts | 63 + .../demo/src/app/tip/TipMaxWidthComponent.ts | 8 + .../demo/src/app/tip/TipPositionComponent.ts | 19 + .../src/app/tip/TipPositionTestComponent.ts | 8 + .../demo/src/app/tip/TipServiceComponent.ts | 42 + .../src/app/tip/TipServiceDestroyComponent.ts | 32 + src/tip/demo/src/app/tip/TipTestModule.ts | 111 + .../demo/src/app/tip/TipTriggerComponent.ts | 15 + .../app/tip/TipValidPositionTestComponent.ts | 22 + .../demo/src/app/tip/TipZindexComponent.ts | 15 + src/tip/demo/src/app/tip/tip-basic.html | 1 + .../demo/src/app/tip/tip-content-comp.html | 2 + .../src/app/tip/tip-content-template.html | 8 + src/tip/demo/src/app/tip/tip-empty.html | 21 + src/tip/demo/src/app/tip/tip-has-arrow.html | 1 + .../src/app/tip/tip-long-text-position.html | 17 + src/tip/demo/src/app/tip/tip-max-width.html | 1 + .../demo/src/app/tip/tip-position-test.html | 18 + src/tip/demo/src/app/tip/tip-position.html | 23 + .../demo/src/app/tip/tip-service-destroy.html | 9 + src/tip/demo/src/app/tip/tip-service.html | 2 + src/tip/demo/src/app/tip/tip-trigger.html | 1 + .../src/app/tip/tip-valid-position-test.html | 17 + src/tip/demo/src/app/tip/tip-zindex.html | 6 + src/tip/demo/src/app/tip/tipTest.less | 38 + src/tip/demo/src/app/tip/webdoc/tip-demos.js | 123 + src/tip/demo/src/app/tip/webdoc/tip.cn.md | 23 + src/tip/demo/src/app/tip/webdoc/tip.en.md | 29 + src/tip/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/tip/demo/src/index.html | 16 + src/tip/demo/src/main.ts | 13 + src/tip/demo/test.ts | 24 + src/tip/demo/tsconfig.app.json | 15 + src/tip/demo/tsconfig.spec.json | 20 + src/tip/lib/index.ts | 4 + src/tip/lib/ng-package.json | 7 + src/tip/lib/package.json | 13 + src/tip/lib/project.json | 62 + src/tip/lib/src/TiTipContainerComponent.ts | 29 + src/tip/lib/src/TiTipDirective.ts | 141 + src/tip/lib/src/TiTipInterface.ts | 121 + src/tip/lib/src/TiTipModule.ts | 23 + src/tip/lib/src/TiTipService.ts | 343 ++ src/tip/lib/src/TiTipServiceModule.ts | 26 + src/tip/lib/src/tip.less | 343 ++ src/transfer/demo/karma.conf.js | 44 + src/transfer/demo/project.json | 90 + src/transfer/demo/src/app/AppComponent.ts | 7 + src/transfer/demo/src/app/AppModule.ts | 24 + src/transfer/demo/src/app/IndexComponent.ts | 49 + src/transfer/demo/src/app/app.html | 3 + .../app/transfer/TransferBasicComponent.ts | 9 + .../app/transfer/TransferDisabledComponent.ts | 44 + .../app/transfer/TransferEventComponent.ts | 22 + .../src/app/transfer/TransferIdComponent.ts | 38 + .../app/transfer/TransferIdkeyComponent.ts | 55 + .../app/transfer/TransferLabelkeyComponent.ts | 10 + .../src/app/transfer/TransferLazyComponent.ts | 21 + .../src/app/transfer/TransferLoadComponent.ts | 85 + .../transfer/TransferNodatatextComponent.ts | 10 + .../transfer/TransferPaginationComponent.ts | 34 + .../transfer/TransferPlaceholderComponent.ts | 16 + .../transfer/TransferSearchableComponent.ts | 10 + .../transfer/TransferSearchkeysComponent.ts | 19 + .../src/app/transfer/TransferSizeComponent.ts | 9 + .../app/transfer/TransferTableComponent.ts | 56 + .../src/app/transfer/TransferTestModule.ts | 122 + .../app/transfer/TransferTitlesComponent.ts | 9 + src/transfer/demo/src/app/transfer/data.js | 81 + .../demo/src/app/transfer/transfer-basic.html | 4 + .../src/app/transfer/transfer-disabled.html | 1 + .../demo/src/app/transfer/transfer-event.html | 9 + .../demo/src/app/transfer/transfer-id.html | 15 + .../demo/src/app/transfer/transfer-idkey.html | 1 + .../src/app/transfer/transfer-labelkey.html | 1 + .../demo/src/app/transfer/transfer-lazy.html | 1 + .../demo/src/app/transfer/transfer-load.html | 14 + .../src/app/transfer/transfer-nodatatext.html | 1 + .../src/app/transfer/transfer-pagination.html | 11 + .../app/transfer/transfer-placeholder.html | 7 + .../src/app/transfer/transfer-searchable.html | 1 + .../src/app/transfer/transfer-searchkeys.html | 8 + .../demo/src/app/transfer/transfer-size.html | 1 + .../demo/src/app/transfer/transfer-table.html | 37 + .../src/app/transfer/transfer-titles.html | 1 + .../demo/src/app/transfer/transfer.less | 4 + .../src/app/transfer/webdoc/transfer-demos.js | 248 + .../src/app/transfer/webdoc/transfer.cn.md | 23 + .../src/app/transfer/webdoc/transfer.en.md | 29 + src/transfer/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/transfer/demo/src/index.html | 16 + src/transfer/demo/src/main.ts | 13 + src/transfer/demo/test.ts | 24 + src/transfer/demo/tsconfig.app.json | 15 + src/transfer/demo/tsconfig.spec.json | 20 + src/transfer/lib/index.ts | 1 + src/transfer/lib/ng-package.json | 7 + src/transfer/lib/package.json | 22 + src/transfer/lib/project.json | 62 + src/transfer/lib/src/TiTransferColumn.ts | 19 + src/transfer/lib/src/TiTransferComponent.ts | 325 + src/transfer/lib/src/TiTransferModule.ts | 54 + src/transfer/lib/src/i18n/TiTransferWords.ts | 7 + src/transfer/lib/src/i18n/en_US.ts | 9 + src/transfer/lib/src/i18n/es_US.ts | 9 + src/transfer/lib/src/i18n/fr_FR.ts | 9 + src/transfer/lib/src/i18n/index.ts | 7 + src/transfer/lib/src/i18n/pt_BR.ts | 9 + src/transfer/lib/src/i18n/zh_CN.ts | 9 + src/transfer/lib/src/transfer.html | 73 + src/transfer/lib/src/transfer.less | 75 + src/transfer/lib/src/transferUtil.ts | 27 + .../transferlist/TiTransferListComponent.ts | 404 ++ .../lib/src/transferlist/transfer-list.html | 153 + .../lib/src/transferlist/transfer-list.less | 156 + src/tree/demo/karma.conf.js | 44 + src/tree/demo/project.json | 90 + src/tree/demo/src/app/AppComponent.ts | 7 + src/tree/demo/src/app/AppModule.ts | 24 + src/tree/demo/src/app/IndexComponent.ts | 49 + src/tree/demo/src/app/app.html | 3 + .../src/app/tree/TreeBeforeExpandComponent.ts | 70 + .../src/app/tree/TreeBeforeMoreComponent.ts | 100 + .../tree/TreeChangedbycheckboxComponent.ts | 46 + .../app/tree/TreeCheckRelationComponent.ts | 55 + .../src/app/tree/TreeDisabledComponent.ts | 48 + .../app/tree/TreeDragBeforedropComponent.ts | 95 + .../demo/src/app/tree/TreeDragComponent.ts | 84 + .../demo/src/app/tree/TreeEventComponent.ts | 74 + .../demo/src/app/tree/TreeIconComponent.ts | 106 + .../demo/src/app/tree/TreeLoadComponent.ts | 132 + .../demo/src/app/tree/TreeManyComponent.ts | 169 + .../src/app/tree/TreeMultiselectComponent.ts | 57 + .../demo/src/app/tree/TreeOperateComponent.ts | 110 + .../app/tree/TreeParentcheckableComponent.ts | 48 + .../src/app/tree/TreeRadioselectComponent.ts | 84 + .../demo/src/app/tree/TreeSearchComponent.ts | 238 + .../src/app/tree/TreeShortcutkeyComponent.ts | 88 + .../demo/src/app/tree/TreeSmallComponent.ts | 58 + .../src/app/tree/TreeTemplateComponent.ts | 72 + src/tree/demo/src/app/tree/TreeTestModule.ts | 160 + .../demo/src/app/tree/TreeUtilComponent.ts | 278 + .../app/tree/TreeVirtualscrollComponent.ts | 48 + .../tree/TreeVirtualscrollDragComponent.ts | 43 + .../tree/TreeVirtualscrollSmallComponent.ts | 42 + .../demo/src/app/tree/tree-before-expand.html | 1 + .../demo/src/app/tree/tree-before-more.html | 7 + .../src/app/tree/tree-changedbycheckbox.html | 1 + .../src/app/tree/tree-check-relation.html | 1 + src/tree/demo/src/app/tree/tree-disabled.html | 1 + .../src/app/tree/tree-drag-beforedrop.html | 7 + src/tree/demo/src/app/tree/tree-drag.html | 2 + src/tree/demo/src/app/tree/tree-event.html | 10 + src/tree/demo/src/app/tree/tree-icon.html | 6 + src/tree/demo/src/app/tree/tree-load.html | 35 + src/tree/demo/src/app/tree/tree-many.html | 60 + .../demo/src/app/tree/tree-multiselect.html | 1 + src/tree/demo/src/app/tree/tree-operate.html | 11 + .../src/app/tree/tree-parentcheckable.html | 1 + .../demo/src/app/tree/tree-radioselect.html | 5 + src/tree/demo/src/app/tree/tree-search.html | 30 + .../demo/src/app/tree/tree-shortcutkey.html | 1 + src/tree/demo/src/app/tree/tree-small.html | 1 + src/tree/demo/src/app/tree/tree-template.html | 14 + src/tree/demo/src/app/tree/tree-util.html | 18 + .../src/app/tree/tree-virtualscroll-drag.html | 41 + .../app/tree/tree-virtualscroll-small.html | 35 + .../demo/src/app/tree/tree-virtualscroll.html | 4 + src/tree/demo/src/app/tree/treeTest.less | 4 + .../demo/src/app/tree/webdoc/tree-demos.js | 249 + src/tree/demo/src/app/tree/webdoc/tree.cn.md | 31 + src/tree/demo/src/app/tree/webdoc/tree.en.md | 29 + src/tree/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/tree/demo/src/index.html | 16 + src/tree/demo/src/main.ts | 13 + src/tree/demo/test.ts | 24 + src/tree/demo/tsconfig.app.json | 15 + src/tree/demo/tsconfig.spec.json | 20 + src/tree/lib/index.ts | 1 + src/tree/lib/ng-package.json | 7 + src/tree/lib/package.json | 20 + src/tree/lib/project.json | 62 + src/tree/lib/src/TiAutoSelectDirective.ts | 28 + src/tree/lib/src/TiHighlightPipe.ts | 36 + src/tree/lib/src/TiTreeComponent.ts | 1111 ++++ src/tree/lib/src/TiTreeModule.ts | 51 + src/tree/lib/src/TiTreeUtil.ts | 620 ++ src/tree/lib/src/i18n/TiTreeWords.ts | 9 + src/tree/lib/src/i18n/en_US.ts | 11 + src/tree/lib/src/i18n/es_US.ts | 11 + src/tree/lib/src/i18n/fr_FR.ts | 11 + src/tree/lib/src/i18n/index.ts | 7 + src/tree/lib/src/i18n/pt_BR.ts | 11 + src/tree/lib/src/i18n/zh_CN.ts | 11 + src/tree/lib/src/tree.html | 293 + src/tree/lib/src/tree.less | 360 ++ src/treeselect/demo/karma.conf.js | 44 + src/treeselect/demo/project.json | 90 + src/treeselect/demo/src/app/AppComponent.ts | 7 + src/treeselect/demo/src/app/AppModule.ts | 24 + src/treeselect/demo/src/app/IndexComponent.ts | 49 + src/treeselect/demo/src/app/app.html | 3 + .../treeselect/TreeselectBasicComponent.ts | 84 + .../TreeselectBeforeExpandComponent.ts | 98 + .../TreeselectBeforeMoreComponent.ts | 143 + .../TreeselectClearableComponent.ts | 90 + .../treeselect/TreeselectDisabledComponent.ts | 36 + .../TreeselectDropmaxheightComponent.ts | 84 + .../treeselect/TreeselectEventComponent.ts | 89 + .../treeselect/TreeselectFocusComponent.ts | 82 + .../treeselect/TreeselectLabelkeyComponent.ts | 84 + .../treeselect/TreeselectLazyloadComponent.ts | 97 + .../app/treeselect/TreeselectLoadComponent.ts | 130 + .../treeselect/TreeselectMaxlineComponent.ts | 84 + .../treeselect/TreeselectModalComponent.ts | 112 + .../treeselect/TreeselectMultiComponent.ts | 83 + .../treeselect/TreeselectNodataComponent.ts | 11 + .../TreeselectOptionsChangeComponent.ts | 69 + .../TreeselectPanelwidthComponent.ts | 84 + .../treeselect/TreeselectSearchComponent.ts | 82 + .../TreeselectSelectallComponent.ts | 84 + .../app/treeselect/TreeselectTestModule.ts | 153 + .../TreeselectValidationComponent.ts | 83 + .../src/app/treeselect/treeselect-basic.html | 5 + .../treeselect/treeselect-before-expand.html | 8 + .../treeselect/treeselect-before-more.html | 10 + .../app/treeselect/treeselect-clearable.html | 11 + .../app/treeselect/treeselect-disabled.html | 6 + .../treeselect/treeselect-dropmaxheight.html | 8 + .../src/app/treeselect/treeselect-event.html | 4 + .../src/app/treeselect/treeselect-focus.html | 33 + .../app/treeselect/treeselect-labelkey.html | 1 + .../app/treeselect/treeselect-lazyload.html | 8 + .../src/app/treeselect/treeselect-load.html | 19 + .../app/treeselect/treeselect-maxline.html | 2 + .../src/app/treeselect/treeselect-modal.html | 1 + .../src/app/treeselect/treeselect-multi.html | 5 + .../src/app/treeselect/treeselect-nodata.html | 2 + .../treeselect/treeselect-options-change.html | 5 + .../app/treeselect/treeselect-panelwidth.html | 2 + .../src/app/treeselect/treeselect-search.html | 10 + .../app/treeselect/treeselect-selectall.html | 9 + .../app/treeselect/treeselect-validation.html | 10 + .../app/treeselect/webdoc/treeselect-demos.js | 202 + .../app/treeselect/webdoc/treeselect.cn.md | 25 + .../app/treeselect/webdoc/treeselect.en.md | 29 + src/treeselect/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/treeselect/demo/src/index.html | 16 + src/treeselect/demo/src/main.ts | 13 + src/treeselect/demo/test.ts | 24 + src/treeselect/demo/tsconfig.app.json | 15 + src/treeselect/demo/tsconfig.spec.json | 20 + src/treeselect/lib/index.ts | 1 + src/treeselect/lib/ng-package.json | 7 + src/treeselect/lib/package.json | 20 + src/treeselect/lib/project.json | 62 + .../lib/src/TiTreeselectComponent.ts | 625 ++ src/treeselect/lib/src/TiTreeselectModule.ts | 40 + src/treeselect/lib/src/treeselect.html | 73 + src/treeselect/lib/src/treeselect.less | 49 + src/upload/demo/karma.conf.js | 44 + src/upload/demo/project.json | 98 + src/upload/demo/src/app/AppComponent.ts | 7 + src/upload/demo/src/app/AppModule.ts | 28 + src/upload/demo/src/app/IndexComponent.ts | 52 + src/upload/demo/src/app/app.html | 3 + .../app/upload/UploadAutoUploadComponent.ts | 6 + .../src/app/upload/UploadBasicComponent.ts | 6 + .../app/upload/UploadBatchSendComponent.ts | 6 + .../app/upload/UploadBeforeremoveComponent.ts | 15 + .../src/app/upload/UploadButtonComponent.ts | 6 + .../app/upload/UploadButtonTestComponent.ts | 77 + .../src/app/upload/UploadCaseTestComponent.ts | 15 + .../src/app/upload/UploadChangesComponent.ts | 29 + .../app/upload/UploadChunksizeComponent.ts | 49 + .../src/app/upload/UploadCustomComponent.ts | 22 + .../src/app/upload/UploadEventComponent.ts | 48 + .../src/app/upload/UploadFilterComponent.ts | 29 + .../src/app/upload/UploadFormDataComponent.ts | 19 + .../upload/UploadInitfilesTestComponent.ts | 36 + .../upload/UploadInputFieldTestComponent.ts | 83 + .../src/app/upload/UploadPropsComponent.ts | 28 + .../src/app/upload/UploadServiceComponent.ts | 72 + .../app/upload/UploadServiceTestComponent.ts | 89 + .../src/app/upload/UploadSingleComponent.ts | 17 + .../demo/src/app/upload/UploadTestModule.ts | 132 + .../src/app/upload/upload-auto-upload.html | 2 + .../demo/src/app/upload/upload-basic.html | 1 + .../src/app/upload/upload-batch-send.html | 1 + .../src/app/upload/upload-beforeremove.html | 5 + .../src/app/upload/upload-button-test.html | 78 + .../demo/src/app/upload/upload-button.html | 3 + .../demo/src/app/upload/upload-case-test.html | 5 + .../demo/src/app/upload/upload-changes.html | 46 + .../demo/src/app/upload/upload-chunksize.html | 17 + .../demo/src/app/upload/upload-custom.html | 11 + .../demo/src/app/upload/upload-event.html | 16 + .../demo/src/app/upload/upload-filter.html | 8 + .../demo/src/app/upload/upload-form-data.html | 9 + .../src/app/upload/upload-initfiles-test.html | 9 + .../app/upload/upload-input-field-test.html | 93 + .../demo/src/app/upload/upload-props.html | 18 + .../src/app/upload/upload-service-test.html | 39 + .../demo/src/app/upload/upload-service.html | 37 + .../demo/src/app/upload/upload-single.html | 4 + .../src/app/upload/webdoc/upload-demos.js | 157 + .../demo/src/app/upload/webdoc/upload.cn.md | 22 + .../demo/src/app/upload/webdoc/upload.en.md | 29 + .../UploadimageAutoUploadComponent.ts | 6 + .../uploadimage/UploadimageBasicComponent.ts | 6 + .../UploadimageChangesComponent.ts | 26 + .../UploadimageDeletableComponent.ts | 8 + .../UploadimageDisabledComponent.ts | 8 + .../uploadimage/UploadimageDragComponent.ts | 8 + .../uploadimage/UploadimageEventComponent.ts | 47 + .../uploadimage/UploadimageFilterComponent.ts | 29 + .../UploadimageInitfilesComponent.ts | 25 + .../UploadimageMaxcountComponent.ts | 11 + .../UploadimageTemplateComponent.ts | 8 + .../app/uploadimage/UploadimageTestModule.ts | 91 + .../uploadimage/uploadimage-auto-upload.html | 3 + .../app/uploadimage/uploadimage-basic.html | 1 + .../app/uploadimage/uploadimage-changes.html | 15 + .../uploadimage/uploadimage-deletable.html | 1 + .../app/uploadimage/uploadimage-disabled.html | 1 + .../src/app/uploadimage/uploadimage-drag.html | 16 + .../app/uploadimage/uploadimage-event.html | 17 + .../app/uploadimage/uploadimage-filter.html | 8 + .../uploadimage/uploadimage-initfiles.html | 1 + .../app/uploadimage/uploadimage-maxcount.html | 2 + .../app/uploadimage/uploadimage-template.html | 8 + .../src/app/uploadimage/uploadimagetest.less | 5 + .../uploadimage/webdoc/uploadimage-demos.js | 143 + .../app/uploadimage/webdoc/uploadimage.cn.md | 22 + .../app/uploadimage/webdoc/uploadimage.en.md | 29 + src/upload/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/upload/demo/src/index.html | 16 + src/upload/demo/src/main.ts | 13 + src/upload/demo/test.ts | 24 + src/upload/demo/tsconfig.app.json | 15 + src/upload/demo/tsconfig.spec.json | 20 + src/upload/lib/index.ts | 4 + src/upload/lib/ng-package.json | 7 + src/upload/lib/package.json | 23 + src/upload/lib/project.json | 62 + src/upload/lib/src/TiDisabledDirective.ts | 29 + src/upload/lib/src/TiFileInterface.ts | 564 ++ src/upload/lib/src/TiFileSelectDirective.ts | 97 + src/upload/lib/src/TiUploadComponent.ts | 297 + src/upload/lib/src/TiUploadModule.ts | 60 + src/upload/lib/src/TiUploadService.ts | 395 ++ src/upload/lib/src/TiUploadServiceModule.ts | 21 + src/upload/lib/src/TiUploadUtil.ts | 749 +++ src/upload/lib/src/TiUploadbaseComponent.ts | 401 ++ src/upload/lib/src/TiUploadimageComponent.ts | 383 ++ src/upload/lib/src/i18n/TiUploadWords.ts | 21 + src/upload/lib/src/i18n/en_US.ts | 23 + src/upload/lib/src/i18n/es_US.ts | 23 + src/upload/lib/src/i18n/fr_FR.ts | 23 + src/upload/lib/src/i18n/index.ts | 7 + src/upload/lib/src/i18n/pt_BR.ts | 23 + src/upload/lib/src/i18n/zh_CN.ts | 23 + src/upload/lib/src/upload.html | 293 + src/upload/lib/src/upload.less | 356 ++ src/upload/lib/src/uploadimage.html | 96 + src/upload/lib/src/uploadimage.less | 166 + src/utils/demo/project.json | 78 + src/utils/demo/src/app/AppComponent.ts | 7 + src/utils/demo/src/app/AppModule.ts | 30 + src/utils/demo/src/app/IndexComponent.ts | 54 + src/utils/demo/src/app/app.html | 3 + .../demo/src/app/browser/BrowserTestModule.ts | 19 + .../src/app/browser/BrowserUsageComponent.ts | 17 + .../demo/src/app/browser/browser-usage.html | 10 + .../src/app/browser/webdoc/browser-demos.js | 27 + .../demo/src/app/browser/webdoc/browser.cn.md | 19 + .../demo/src/app/browser/webdoc/browser.en.md | 28 + .../demo/src/app/keymap/KeymapTestModule.ts | 21 + .../src/app/keymap/KeymapUsageComponent.ts | 24 + .../demo/src/app/keymap/keymap-usage.html | 8 + .../src/app/keymap/webdoc/keymap-demos.js | 17 + .../demo/src/app/keymap/webdoc/keymap.cn.md | 16 + .../demo/src/app/keymap/webdoc/keymap.en.md | 28 + src/utils/demo/src/app/log/LogTestModule.ts | 19 + .../demo/src/app/log/LogUsageComponent.ts | 45 + src/utils/demo/src/app/log/log-usage.html | 1 + .../demo/src/app/log/webdoc/log-demos.js | 23 + src/utils/demo/src/app/log/webdoc/log.cn.md | 16 + src/utils/demo/src/app/log/webdoc/log.en.md | 26 + .../demo/src/app/theme/ThemeBasicComponent.ts | 26 + .../demo/src/app/theme/ThemeTestModule.ts | 22 + src/utils/demo/src/app/theme/theme-basic.html | 5 + .../demo/src/app/theme/webdoc/theme-demos.js | 17 + .../demo/src/app/theme/webdoc/theme.cn.md | 16 + .../demo/src/app/theme/webdoc/theme.en.md | 29 + src/utils/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/utils/demo/src/index.html | 16 + src/utils/demo/src/main.ts | 13 + src/utils/demo/tsconfig.app.json | 15 + src/utils/lib/index.ts | 9 + src/utils/lib/ng-package.json | 7 + src/utils/lib/package.json | 11 + src/utils/lib/project.json | 62 + src/utils/lib/src/ObservableMap.ts | 73 + src/utils/lib/src/ObservableSet.ts | 63 + src/utils/lib/src/Position.ts | 720 +++ src/utils/lib/src/TiBrowser.ts | 162 + src/utils/lib/src/TiDateUtil.ts | 470 ++ src/utils/lib/src/TiKeymap.ts | 381 ++ src/utils/lib/src/TiLog.ts | 81 + src/utils/lib/src/TiTheme.ts | 62 + src/utils/lib/src/Util.ts | 169 + src/validation/demo/karma.conf.js | 44 + src/validation/demo/project.json | 90 + src/validation/demo/src/app/AppComponent.ts | 7 + src/validation/demo/src/app/AppModule.ts | 24 + src/validation/demo/src/app/IndexComponent.ts | 49 + src/validation/demo/src/app/app.html | 3 + .../ValidationAsyncCheckComponent.ts | 53 + .../ValidationAsyncCheckTestComponent.ts | 58 + .../ValidationBasicControlComponent.ts | 10 + .../ValidationBasicDirectiveComponent.ts | 9 + .../ValidationBlurCheckComponent.ts | 12 + .../validation/ValidationErrorMsgComponent.ts | 29 + .../ValidationFormGroupComponent.ts | 150 + .../ValidationFormGroupConfigComponent.ts | 60 + .../ValidationFormGroupTestComponent.ts | 69 + .../ValidationParamChangeComponent.ts | 19 + .../validation/ValidationPwdCheckComponent.ts | 166 + .../ValidationRulesCustomComponent.ts | 24 + ...ValidationRulesCustomDirectiveComponent.ts | 40 + .../ValidationRulesTestComponent.ts | 41 + .../ValidationTemplateFormNestedComponent.ts | 63 + .../app/validation/ValidationTestModule.ts | 153 + .../app/validation/ValidationTipComponent.ts | 14 + .../validation-async-check-test.html | 24 + .../validation/validation-async-check.html | 5 + .../validation/validation-basic-control.html | 1 + .../validation-basic-directive.html | 4 + .../app/validation/validation-blur-check.html | 1 + .../app/validation/validation-error-msg.html | 6 + .../validation-form-group-config.html | 10 + .../validation-form-group-test.html | 39 + .../app/validation/validation-form-group.html | 36 + .../validation/validation-param-change.html | 11 + .../app/validation/validation-pwd-check.html | 30 + .../validation-rules-custom-directive.html | 1 + .../validation/validation-rules-custom.html | 1 + .../app/validation/validation-rules-test.html | 75 + .../validation-template-form-nested.html | 33 + .../src/app/validation/validation-tip.html | 1 + .../app/validation/validation-tiscroll.html | 24 + .../validation/validationTiscrollComponent.ts | 7 + .../app/validation/webdoc/validation-demos.js | 165 + .../app/validation/webdoc/validation.cn.md | 52 + .../app/validation/webdoc/validation.en.md | 29 + src/validation/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/validation/demo/src/index.html | 16 + src/validation/demo/src/main.ts | 13 + src/validation/demo/test.ts | 24 + src/validation/demo/tsconfig.app.json | 15 + src/validation/demo/tsconfig.spec.json | 20 + src/validation/lib/index.ts | 1 + src/validation/lib/ng-package.json | 7 + src/validation/lib/package.json | 18 + src/validation/lib/project.json | 62 + src/validation/lib/src/TiErrorMsgComponent.ts | 38 + .../lib/src/TiPendingStateComponent.ts | 41 + src/validation/lib/src/TiPwdMsgComponent.html | 24 + src/validation/lib/src/TiPwdMsgComponent.ts | 125 + src/validation/lib/src/TiValidationConfig.ts | 22 + .../lib/src/TiValidationDirective.ts | 140 + .../lib/src/TiValidationInterface.ts | 125 + src/validation/lib/src/TiValidationModule.ts | 176 + .../lib/src/checkHandle/AsyncCheck.ts | 82 + .../lib/src/checkHandle/BlurCheck.ts | 58 + .../lib/src/checkHandle/ChangeCheck.ts | 78 + .../lib/src/checkHandle/CheckHandle.ts | 27 + .../lib/src/checkHandle/CheckStyleModule.ts | 21 + .../lib/src/checkHandle/CheckStyleService.ts | 60 + .../lib/src/checkHandle/CommonService.ts | 155 + .../src/checkHandle/CommonServiceModule.ts | 18 + .../lib/src/checkHandle/PwdCheck.ts | 112 + .../lib/src/checkHandle/RadiobaseCheck.ts | 36 + .../lib/src/checkHandle/TiPwdConfig.ts | 180 + src/validation/lib/src/errorMsg.less | 40 + .../lib/src/i18n/TiValidationWords.ts | 39 + src/validation/lib/src/i18n/en_US.ts | 43 + src/validation/lib/src/i18n/es_US.ts | 43 + src/validation/lib/src/i18n/fr_FR.ts | 43 + src/validation/lib/src/i18n/index.ts | 7 + src/validation/lib/src/i18n/pt_BR.ts | 43 + src/validation/lib/src/i18n/zh_CN.ts | 41 + src/validation/lib/src/pending-state.html | 1 + src/validation/lib/src/pending-state.less | 37 + src/validation/lib/src/pwdMsg.less | 161 + .../lib/src/validators/TiValidators.ts | 792 +++ .../validators/directives/BaseValidator.ts | 80 + .../directives/BigDigitsValidatorDirective.ts | 30 + .../BigIntegerValidatorDirective.ts | 30 + .../directives/BigNumberValidatorDirective.ts | 30 + .../directives/ContainsValidatorDirective.ts | 30 + .../directives/DateValidatorDirective.ts | 29 + .../directives/DigitsValidatorDirective.ts | 29 + .../directives/EmailValidatorDirective.ts | 30 + .../directives/EqualValidatorDirective.ts | 30 + .../directives/IntegerValidatorDirective.ts | 30 + .../directives/Ipv4ValidatorDirective.ts | 29 + .../directives/Ipv6ValidatorDirective.ts | 29 + .../directives/MaxLengthValidatorDirective.ts | 29 + .../MaxValueByStringValidatorDirective.ts | 29 + .../directives/MaxValueValidatorDirective.ts | 29 + .../directives/MinLengthValidatorDirective.ts | 29 + .../MinValueByStringValidatorDirective.ts | 29 + .../directives/MinValueValidatorDirective.ts | 29 + .../NotContainsValidatorDirective.ts | 29 + .../directives/NotEqualValidatorDirective.ts | 29 + .../directives/NotScriptValidatorDirective.ts | 29 + .../directives/NumberValidatorDirective.ts | 30 + .../directives/PasswordValidatorDirective.ts | 29 + .../directives/PortValidatorDirective.ts | 29 + .../directives/RangeSizeValidatorDirective.ts | 29 + .../RangeValueByStringValidatorDirective.ts | 29 + .../RangeValueValidatorDirective.ts | 29 + .../directives/RegExpValidatorDirective.ts | 29 + .../directives/RequiredValidatorDirective.ts | 29 + .../directives/UrlValidatorDirective.ts | 29 + src/zoom/demo/project.json | 86 + src/zoom/demo/src/app/AppComponent.ts | 7 + src/zoom/demo/src/app/AppModule.ts | 24 + src/zoom/demo/src/app/IndexComponent.ts | 49 + src/zoom/demo/src/app/app.html | 3 + .../demo/src/app/zoom/ZoomBasicComponent.ts | 7 + .../src/app/zoom/ZoomChangeSrcComponent.ts | 20 + .../demo/src/app/zoom/ZoomRatioComponent.ts | 7 + .../demo/src/app/zoom/ZoomTestComponent.ts | 7 + src/zoom/demo/src/app/zoom/ZoomTestModule.ts | 30 + src/zoom/demo/src/app/zoom/zoom-basic.html | 11 + .../demo/src/app/zoom/zoom-change-src.html | 5 + src/zoom/demo/src/app/zoom/zoom-ratio.html | 9 + src/zoom/demo/src/app/zoom/zoom-test.html | 15 + src/zoom/demo/src/app/zoom/zoom.less | 11 + src/zoom/demo/src/favicon.ico | Bin 0 -> 187502 bytes src/zoom/demo/src/index.html | 16 + src/zoom/demo/src/main.ts | 13 + src/zoom/demo/tsconfig.app.json | 15 + src/zoom/lib/index.ts | 1 + src/zoom/lib/ng-package.json | 7 + src/zoom/lib/package.json | 10 + src/zoom/lib/project.json | 62 + src/zoom/lib/src/TiZoomComponent.ts | 257 + src/zoom/lib/src/TiZoomModule.ts | 23 + src/zoom/lib/src/zoom.less | 19 + .../files/demo/project.json__tmpl__ | 85 + .../demo/src/app/AppComponent.ts__tmpl__ | 7 + .../files/demo/src/app/AppModule.ts__tmpl__ | 24 + .../demo/src/app/IndexComponent.ts__tmpl__ | 54 + .../__name__/__demoComponentName__.ts__tmpl__ | 7 + .../__name__/__demoModuleName__.ts__tmpl__ | 23 + .../app/__name__/__name__-basic.html__tmpl__ | 1 + .../__name__/webdoc/__name__-demos.js__tmpl__ | 17 + .../__name__/webdoc/__name__.cn.md__tmpl__ | 15 + .../__name__/webdoc/__name__.en.md__tmpl__ | 13 + .../files/demo/src/app/app.html__tmpl__ | 3 + .../files/demo/src/favicon.ico__tmpl__ | Bin 0 -> 187502 bytes .../files/demo/src/index.html__tmpl__ | 16 + .../files/demo/src/main.ts__tmpl__ | 14 + .../files/demo/tsconfig.app.json__tmpl__ | 15 + .../files/lib/index.ts__tmpl__ | 2 + .../files/lib/ng-package.json__tmpl__ | 7 + .../files/lib/package.json__tmpl__ | 8 + .../files/lib/project.json__tmpl__ | 63 + .../lib/src/__componentName__.ts__tmpl__ | 14 + .../files/lib/src/__moduleName__.ts__tmpl__ | 20 + .../files/lib/src/__name__.html__tmpl__ | 0 .../files/lib/src/__name__.less__tmpl__ | 0 .../lib/src/i18n/__i18nWords__.ts__tmpl__ | 5 + .../files/lib/src/i18n/en_US.ts__tmpl__ | 7 + .../files/lib/src/i18n/es_US.ts__tmpl__ | 7 + .../files/lib/src/i18n/fr_FR.ts__tmpl__ | 7 + .../files/lib/src/i18n/index.ts__tmpl__ | 8 + .../files/lib/src/i18n/pt_BR.ts__tmpl__ | 7 + tools/generators/ti-lib-generator/index.ts | 76 + tools/generators/ti-lib-generator/schema.d.ts | 5 + tools/generators/ti-lib-generator/schema.json | 39 + tools/tsconfig.tools.json | 24 + 3040 files changed, 134986 insertions(+) create mode 100644 src/inputnumber/demo/src/app/AppComponent.ts create mode 100644 src/inputnumber/demo/src/app/AppModule.ts create mode 100644 src/inputnumber/demo/src/app/IndexComponent.ts create mode 100644 src/inputnumber/demo/src/app/app.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberBasicComponent.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberEventComponent.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberFocusComponent.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberFormatComponent.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberLoadComponent.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberLocaleableComponent.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberMaxlengthComponent.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/InputnumberTestModule.ts create mode 100644 src/inputnumber/demo/src/app/inputnumber/inputnumber-basic.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/inputnumber-event.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/inputnumber-focus.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/inputnumber-format.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/inputnumber-load.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/inputnumber-localeable.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/inputnumber-maxlength.html create mode 100644 src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber-demos.js create mode 100644 src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.cn.md create mode 100644 src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.en.md create mode 100644 src/inputnumber/demo/src/favicon.ico create mode 100644 src/inputnumber/demo/src/index.html create mode 100644 src/inputnumber/demo/src/main.ts create mode 100644 src/inputnumber/demo/tsconfig.spec.json create mode 100644 src/inputnumber/lib/index.ts create mode 100644 src/inputnumber/lib/ng-package.json create mode 100644 src/inputnumber/lib/package.json create mode 100644 src/inputnumber/lib/project.json create mode 100644 src/inputnumber/lib/src/TiInputNumberDirective.ts create mode 100644 src/inputnumber/lib/src/TiInputNumberModule.ts create mode 100644 src/intro/demo/karma.conf.js create mode 100644 src/intro/demo/project.json create mode 100644 src/intro/demo/src/app/AppComponent.ts create mode 100644 src/intro/demo/src/app/AppModule.ts create mode 100644 src/intro/demo/src/app/IndexComponent.ts create mode 100644 src/intro/demo/src/app/app.html create mode 100644 src/intro/demo/src/app/intro/IntroBasicComponent.ts create mode 100644 src/intro/demo/src/app/intro/IntroEventComponent.ts create mode 100644 src/intro/demo/src/app/intro/IntroModalComponent.ts create mode 100644 src/intro/demo/src/app/intro/IntroSkipableComponent.ts create mode 100644 src/intro/demo/src/app/intro/IntroTemplateComponent.ts create mode 100644 src/intro/demo/src/app/intro/IntroTestModule.ts create mode 100644 src/intro/demo/src/app/intro/IntroTipComponent.ts create mode 100644 src/intro/demo/src/app/intro/IntroTiscrollComponent.ts create mode 100644 src/intro/demo/src/app/intro/intro-basic.html create mode 100644 src/intro/demo/src/app/intro/intro-event.html create mode 100644 src/intro/demo/src/app/intro/intro-modal.html create mode 100644 src/intro/demo/src/app/intro/intro-skipable.html create mode 100644 src/intro/demo/src/app/intro/intro-template.html create mode 100644 src/intro/demo/src/app/intro/intro-tip.html create mode 100644 src/intro/demo/src/app/intro/intro-tiscroll.html create mode 100644 src/intro/demo/src/app/intro/webdoc/intro-demos.js create mode 100644 src/intro/demo/src/app/intro/webdoc/intro.cn.md create mode 100644 src/intro/demo/src/app/intro/webdoc/intro.en.md create mode 100644 src/intro/demo/src/favicon.ico create mode 100644 src/intro/demo/src/index.html create mode 100644 src/intro/demo/src/main.ts create mode 100644 src/intro/demo/test.ts create mode 100644 src/intro/demo/tsconfig.app.json create mode 100644 src/intro/demo/tsconfig.spec.json create mode 100644 src/intro/lib/index.ts create mode 100644 src/intro/lib/ng-package.json create mode 100644 src/intro/lib/package.json create mode 100644 src/intro/lib/project.json create mode 100644 src/intro/lib/src/TiIntroInterface.ts create mode 100644 src/intro/lib/src/TiIntroModule.ts create mode 100644 src/intro/lib/src/TiIntroService.ts create mode 100644 src/intro/lib/src/TiIntromodalComponent.ts create mode 100644 src/intro/lib/src/TiIntrotipComponent.ts create mode 100644 src/intro/lib/src/i18n/TiIntroWords.ts create mode 100644 src/intro/lib/src/i18n/en_US.ts create mode 100644 src/intro/lib/src/i18n/es_US.ts create mode 100644 src/intro/lib/src/i18n/fr_FR.ts create mode 100644 src/intro/lib/src/i18n/index.ts create mode 100644 src/intro/lib/src/i18n/pt_BR.ts create mode 100644 src/intro/lib/src/i18n/zh_CN.ts create mode 100644 src/intro/lib/src/intro.less create mode 100644 src/intro/lib/src/intromodal.html create mode 100644 src/intro/lib/src/introtip.html create mode 100644 src/ip/demo/karma.conf.js create mode 100644 src/ip/demo/project.json create mode 100644 src/ip/demo/src/app/AppComponent.ts create mode 100644 src/ip/demo/src/app/AppModule.ts create mode 100644 src/ip/demo/src/app/IndexComponent.ts create mode 100644 src/ip/demo/src/app/app.html create mode 100644 src/ip/demo/src/app/ip/IpBasicComponent.ts create mode 100644 src/ip/demo/src/app/ip/IpDisabledComponent.ts create mode 100644 src/ip/demo/src/app/ip/IpFormcontrolComponent.ts create mode 100644 src/ip/demo/src/app/ip/IpTestModule.ts create mode 100644 src/ip/demo/src/app/ip/IpValidComponent.ts create mode 100644 src/ip/demo/src/app/ip/ip-basic.html create mode 100644 src/ip/demo/src/app/ip/ip-disabled.html create mode 100644 src/ip/demo/src/app/ip/ip-formcontrol.html create mode 100644 src/ip/demo/src/app/ip/ip-valid.html create mode 100644 src/ip/demo/src/app/ip/webdoc/ip-demos.js create mode 100644 src/ip/demo/src/app/ip/webdoc/ip.cn.md create mode 100644 src/ip/demo/src/app/ip/webdoc/ip.en.md create mode 100644 src/ip/demo/src/favicon.ico create mode 100644 src/ip/demo/src/index.html create mode 100644 src/ip/demo/src/main.ts create mode 100644 src/ip/demo/test.ts create mode 100644 src/ip/demo/tsconfig.app.json create mode 100644 src/ip/demo/tsconfig.spec.json create mode 100644 src/ip/lib/index.ts create mode 100644 src/ip/lib/ng-package.json create mode 100644 src/ip/lib/package.json create mode 100644 src/ip/lib/project.json create mode 100644 src/ip/lib/src/TiIpComponent.ts create mode 100644 src/ip/lib/src/TiIpModule.ts create mode 100644 src/ip/lib/src/ip.html create mode 100644 src/ip/lib/src/ip.less create mode 100644 src/ipsection/demo/karma.conf.js create mode 100644 src/ipsection/demo/project.json create mode 100644 src/ipsection/demo/src/app/AppComponent.ts create mode 100644 src/ipsection/demo/src/app/AppModule.ts create mode 100644 src/ipsection/demo/src/app/IndexComponent.ts create mode 100644 src/ipsection/demo/src/app/app.html create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionBasicComponent.ts create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionDisabledComponent.ts create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionEventsComponent.ts create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionFocusComponent.ts create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionTestComponent.ts create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionTestModule.ts create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionValidComponent.ts create mode 100644 src/ipsection/demo/src/app/ipsection/IpsectionValidFormgroupComponent.ts create mode 100644 src/ipsection/demo/src/app/ipsection/ipsection-basic.html create mode 100644 src/ipsection/demo/src/app/ipsection/ipsection-disabled.html create mode 100644 src/ipsection/demo/src/app/ipsection/ipsection-events.html create mode 100644 src/ipsection/demo/src/app/ipsection/ipsection-focus.html create mode 100644 src/ipsection/demo/src/app/ipsection/ipsection-test.html create mode 100644 src/ipsection/demo/src/app/ipsection/ipsection-valid-formgroup.html create mode 100644 src/ipsection/demo/src/app/ipsection/ipsection-valid.html create mode 100644 src/ipsection/demo/src/app/ipsection/webdoc/ipsection-demos.js create mode 100644 src/ipsection/demo/src/app/ipsection/webdoc/ipsection.cn.md create mode 100644 src/ipsection/demo/src/app/ipsection/webdoc/ipsection.en.md create mode 100644 src/ipsection/demo/src/favicon.ico create mode 100644 src/ipsection/demo/src/index.html create mode 100644 src/ipsection/demo/src/main.ts create mode 100644 src/ipsection/demo/test.ts create mode 100644 src/ipsection/demo/tsconfig.app.json create mode 100644 src/ipsection/demo/tsconfig.spec.json create mode 100644 src/ipsection/lib/index.ts create mode 100644 src/ipsection/lib/ng-package.json create mode 100644 src/ipsection/lib/package.json create mode 100644 src/ipsection/lib/project.json create mode 100644 src/ipsection/lib/src/TiIpsectionComponent.ts create mode 100644 src/ipsection/lib/src/TiIpsectionModule.ts create mode 100644 src/ipsection/lib/src/ipsection.html create mode 100644 src/ipsection/lib/src/ipsection.less create mode 100644 src/labeleditor/demo/project.json create mode 100644 src/labeleditor/demo/src/app/AppComponent.ts create mode 100644 src/labeleditor/demo/src/app/AppModule.ts create mode 100644 src/labeleditor/demo/src/app/IndexComponent.ts create mode 100644 src/labeleditor/demo/src/app/app.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorAutotipComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorBasicComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorDisabledComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorEventsComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorIcontipcontextComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlengthComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlineComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorMultilineSizeComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorResizeComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorTestModule.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationAsyncComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationComponent.ts create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-autotip.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-basic.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-disabled.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-events.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-icontipcontext.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-maxlength.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-maxline.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-multiline-size.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-resize.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-validation-async.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/labeleditor-validation.html create mode 100644 src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor-demos.js create mode 100644 src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.cn.md create mode 100644 src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.en.md create mode 100644 src/labeleditor/demo/src/favicon.ico create mode 100644 src/labeleditor/demo/src/index.html create mode 100644 src/labeleditor/demo/src/main.ts create mode 100644 src/labeleditor/demo/tsconfig.app.json create mode 100644 src/labeleditor/lib/index.ts create mode 100644 src/labeleditor/lib/ng-package.json create mode 100644 src/labeleditor/lib/package.json create mode 100644 src/labeleditor/lib/project.json create mode 100644 src/labeleditor/lib/src/TiLabeleditorComponent.ts create mode 100644 src/labeleditor/lib/src/TiLabeleditorModule.ts create mode 100644 src/labeleditor/lib/src/labeleditor.html create mode 100644 src/labeleditor/lib/src/labeleditor.less create mode 100644 src/layout/demo/karma.conf.js create mode 100644 src/layout/demo/project.json create mode 100644 src/layout/demo/src/app/AppComponent.ts create mode 100644 src/layout/demo/src/app/AppModule.ts create mode 100644 src/layout/demo/src/app/IndexComponent.ts create mode 100644 src/layout/demo/src/app/app.html create mode 100644 src/layout/demo/src/app/layout/LayoutBasicComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutBasicSimpleComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutBasicSimpleResponsiveComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutDetailColumnComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutDetailComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutListComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutListLargedataComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutMultiColumnComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutOverviewComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutOverviewVerticalComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutPurchaseComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutPurchaseResponsiveChangeComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutPurchaseResponsiveComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutPurchaseSimpleComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutPurchaseSimpleResponsiveComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutSingleComponent.ts create mode 100644 src/layout/demo/src/app/layout/LayoutTestModule.ts create mode 100644 src/layout/demo/src/app/layout/layout-basic-simple-responsive.html create mode 100644 src/layout/demo/src/app/layout/layout-basic-simple.html create mode 100644 src/layout/demo/src/app/layout/layout-basic.html create mode 100644 src/layout/demo/src/app/layout/layout-basic.less create mode 100644 src/layout/demo/src/app/layout/layout-column.less create mode 100644 src/layout/demo/src/app/layout/layout-detail-column.html create mode 100644 src/layout/demo/src/app/layout/layout-detail.html create mode 100644 src/layout/demo/src/app/layout/layout-list-largedata.html create mode 100644 src/layout/demo/src/app/layout/layout-list.html create mode 100644 src/layout/demo/src/app/layout/layout-multi-column.html create mode 100644 src/layout/demo/src/app/layout/layout-overview-vertical.html create mode 100644 src/layout/demo/src/app/layout/layout-overview.html create mode 100644 src/layout/demo/src/app/layout/layout-purchase-responsive-change.html create mode 100644 src/layout/demo/src/app/layout/layout-purchase-responsive.html create mode 100644 src/layout/demo/src/app/layout/layout-purchase-simple-responsive.html create mode 100644 src/layout/demo/src/app/layout/layout-purchase-simple.html create mode 100644 src/layout/demo/src/app/layout/layout-purchase.html create mode 100644 src/layout/demo/src/app/layout/layout-simple.less create mode 100644 src/layout/demo/src/app/layout/layout-single.html create mode 100644 src/layout/demo/src/app/layout/layout.less create mode 100644 src/layout/demo/src/app/layout/webdoc/layout-demos.js create mode 100644 src/layout/demo/src/app/layout/webdoc/layout.cn.md create mode 100644 src/layout/demo/src/app/layout/webdoc/layout.en.md create mode 100644 src/layout/demo/src/favicon.ico create mode 100644 src/layout/demo/src/index.html create mode 100644 src/layout/demo/src/main.ts create mode 100644 src/layout/demo/test.ts create mode 100644 src/layout/demo/tsconfig.app.json create mode 100644 src/layout/demo/tsconfig.spec.json create mode 100644 src/layout/lib/index.ts create mode 100644 src/layout/lib/ng-package.json create mode 100644 src/layout/lib/package.json create mode 100644 src/layout/lib/project.json create mode 100644 src/layout/lib/src/TiLayoutColumnComponent.ts create mode 100644 src/layout/lib/src/TiLayoutContentBodyComponent.ts create mode 100644 src/layout/lib/src/TiLayoutContentComponent.ts create mode 100644 src/layout/lib/src/TiLayoutContentHeaderComponent.ts create mode 100644 src/layout/lib/src/TiLayoutHeaderComponent.ts create mode 100644 src/layout/lib/src/TiLayoutModule.ts create mode 100644 src/layout/lib/src/TiLayoutSectionComponent.ts create mode 100644 src/layout/lib/src/layout-column.less create mode 100644 src/layout/lib/src/layout-content-body.less create mode 100644 src/layout/lib/src/layout-content-header.less create mode 100644 src/layout/lib/src/layout-content.less create mode 100644 src/layout/lib/src/layout-header.less create mode 100644 src/layout/lib/src/layout-section.less create mode 100644 src/layout/lib/src/layout.less create mode 100644 src/leftmenu/demo/karma.conf.js create mode 100644 src/leftmenu/demo/project.json create mode 100644 src/leftmenu/demo/src/app/AppComponent.ts create mode 100644 src/leftmenu/demo/src/app/AppModule.ts create mode 100644 src/leftmenu/demo/src/app/IndexComponent.ts create mode 100644 src/leftmenu/demo/src/app/app.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuActiveChangeComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuBasicComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuCandeactivateComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuCollapsedComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuDisabledComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuDividingComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuFootComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuGroupComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuHrefComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuIdComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuNoRouterComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuParamsComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuReloadStateComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuRouterlistComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuScrollComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuSecurityComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuTestModule.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/LeftmenuToggleableComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/OpenerComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/Router11Component.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/Router12Component.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/Router2Component.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/Router31Component.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/Router32Component.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/RouterAComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/RouterBComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/RouterCComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/RouterDComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/RouterparamsComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-active-change.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-basic.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-candeactivate.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-collapsed.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-disabled.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-dividing.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-foot.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-group.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-href.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-id.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-no-router.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-params.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-reload-state.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-routerlist.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-scroll.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-security.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenu-toggleable.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/leftmenuTest.less create mode 100644 src/leftmenu/demo/src/app/leftmenu/unsave.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu-demos.js create mode 100644 src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.cn.md create mode 100644 src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.en.md create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuActiveChangeWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuBasicWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuCollapsedWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDisabledWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDividingWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuFootWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuGroupWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuHrefWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuNoRouterWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuParamsWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuReloadStateWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuRouterlistWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuToggleableWebsiteViewComponent.ts create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-active-change-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-basic-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-collapsed-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-disabled-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-dividing-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-foot-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-group-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-href-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-no-router-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-params-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-reload-state-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-routerlist-website-view.html create mode 100644 src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-toggleable-website-view.html create mode 100644 src/leftmenu/demo/src/favicon.ico create mode 100644 src/leftmenu/demo/src/index.html create mode 100644 src/leftmenu/demo/src/main.ts create mode 100644 src/leftmenu/demo/test.ts create mode 100644 src/leftmenu/demo/tsconfig.app.json create mode 100644 src/leftmenu/demo/tsconfig.spec.json create mode 100644 src/leftmenu/lib/index.ts create mode 100644 src/leftmenu/lib/ng-package.json create mode 100644 src/leftmenu/lib/package.json create mode 100644 src/leftmenu/lib/project.json create mode 100644 src/leftmenu/lib/src/TiLeftmenuComponent.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuFootComponent.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuGroupComponent.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuGroupItemComponent.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuHeadComponent.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuItemComponent.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuLevel1Component.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuLevel2Component.ts create mode 100644 src/leftmenu/lib/src/TiLeftmenuModule.ts create mode 100644 src/leftmenu/lib/src/leftmenu-foot.html create mode 100644 src/leftmenu/lib/src/leftmenu-group-item.html create mode 100644 src/leftmenu/lib/src/leftmenu-group.html create mode 100644 src/leftmenu/lib/src/leftmenu-head.html create mode 100644 src/leftmenu/lib/src/leftmenu-item.html create mode 100644 src/leftmenu/lib/src/leftmenu-level1.html create mode 100644 src/leftmenu/lib/src/leftmenu-level2.html create mode 100644 src/leftmenu/lib/src/leftmenu.html create mode 100644 src/leftmenu/lib/src/leftmenu.less create mode 100644 src/linkbutton/demo/karma.conf.js create mode 100644 src/linkbutton/demo/project.json create mode 100644 src/linkbutton/demo/src/app/AppComponent.ts create mode 100644 src/linkbutton/demo/src/app/AppModule.ts create mode 100644 src/linkbutton/demo/src/app/IndexComponent.ts create mode 100644 src/linkbutton/demo/src/app/app.html create mode 100644 src/linkbutton/demo/src/app/linkbutton/LinkbuttonBasicComponent.ts create mode 100644 src/linkbutton/demo/src/app/linkbutton/LinkbuttonTestModule.ts create mode 100644 src/linkbutton/demo/src/app/linkbutton/linkbutton-basic.html create mode 100644 src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton-demos.js create mode 100644 src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.cn.md create mode 100644 src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.en.md create mode 100644 src/linkbutton/demo/src/favicon.ico create mode 100644 src/linkbutton/demo/src/index.html create mode 100644 src/linkbutton/demo/src/main.ts create mode 100644 src/linkbutton/demo/test.ts create mode 100644 src/linkbutton/demo/tsconfig.app.json create mode 100644 src/linkbutton/demo/tsconfig.spec.json create mode 100644 src/linkbutton/lib/index.ts create mode 100644 src/linkbutton/lib/ng-package.json create mode 100644 src/linkbutton/lib/package.json create mode 100644 src/linkbutton/lib/project.json create mode 100644 src/linkbutton/lib/src/TiLinkbuttonComponent.ts create mode 100644 src/linkbutton/lib/src/TiLinkbuttonModule.ts create mode 100644 src/linkbutton/lib/src/linkbutton.html create mode 100644 src/linkbutton/lib/src/linkbutton.less create mode 100644 src/list/demo/project.json create mode 100644 src/list/demo/src/app/AppComponent.ts create mode 100644 src/list/demo/src/app/AppModule.ts create mode 100644 src/list/demo/src/app/IndexComponent.ts create mode 100644 src/list/demo/src/app/app.html create mode 100644 src/list/demo/src/app/list/ListDefaultComponent.ts create mode 100644 src/list/demo/src/app/list/ListGroupComponent.ts create mode 100644 src/list/demo/src/app/list/ListMultiComponent.ts create mode 100644 src/list/demo/src/app/list/ListSelectallComponent.ts create mode 100644 src/list/demo/src/app/list/ListTestModule.ts create mode 100644 src/list/demo/src/app/list/ListTipComponent.ts create mode 100644 src/list/demo/src/app/list/list-default.html create mode 100644 src/list/demo/src/app/list/list-group.html create mode 100644 src/list/demo/src/app/list/list-multi.html create mode 100644 src/list/demo/src/app/list/list-selectall.html create mode 100644 src/list/demo/src/app/list/list-tip.html create mode 100644 src/list/demo/src/favicon.ico create mode 100644 src/list/demo/src/index.html create mode 100644 src/list/demo/src/main.ts create mode 100644 src/list/demo/tsconfig.app.json create mode 100644 src/list/lib/index.ts create mode 100644 src/list/lib/ng-package.json create mode 100644 src/list/lib/package.json create mode 100644 src/list/lib/project.json create mode 100644 src/list/lib/src/TiListComponent.ts create mode 100644 src/list/lib/src/TiListModule.ts create mode 100644 src/list/lib/src/i18n/TiListWords.ts create mode 100644 src/list/lib/src/i18n/en_US.ts create mode 100644 src/list/lib/src/i18n/es_US.ts create mode 100644 src/list/lib/src/i18n/fr_FR.ts create mode 100644 src/list/lib/src/i18n/index.ts create mode 100644 src/list/lib/src/i18n/pt_BR.ts create mode 100644 src/list/lib/src/i18n/zh_CN.ts create mode 100644 src/list/lib/src/list-multi.less create mode 100644 src/list/lib/src/list.html create mode 100644 src/list/lib/src/list.less create mode 100644 src/loading/demo/karma.conf.js create mode 100644 src/loading/demo/project.json create mode 100644 src/loading/demo/src/app/AppComponent.ts create mode 100644 src/loading/demo/src/app/AppModule.ts create mode 100644 src/loading/demo/src/app/IndexComponent.ts create mode 100644 src/loading/demo/src/app/app.html create mode 100644 src/loading/demo/src/app/loading/LoadingAreaComponent.ts create mode 100644 src/loading/demo/src/app/loading/LoadingBasicComponent.ts create mode 100644 src/loading/demo/src/app/loading/LoadingSizeComponent.ts create mode 100644 src/loading/demo/src/app/loading/LoadingTestModule.ts create mode 100644 src/loading/demo/src/app/loading/LoadingTypeComponent.ts create mode 100644 src/loading/demo/src/app/loading/loading-area.html create mode 100644 src/loading/demo/src/app/loading/loading-basic.html create mode 100644 src/loading/demo/src/app/loading/loading-size.html create mode 100644 src/loading/demo/src/app/loading/loading-type.html create mode 100644 src/loading/demo/src/app/loading/webdoc/loading-demos.js create mode 100644 src/loading/demo/src/app/loading/webdoc/loading.cn.md create mode 100644 src/loading/demo/src/app/loading/webdoc/loading.en.md create mode 100644 src/loading/demo/src/favicon.ico create mode 100644 src/loading/demo/src/index.html create mode 100644 src/loading/demo/src/main.ts create mode 100644 src/loading/demo/test.ts create mode 100644 src/loading/demo/tsconfig.app.json create mode 100644 src/loading/demo/tsconfig.spec.json create mode 100644 src/loading/lib/index.ts create mode 100644 src/loading/lib/ng-package.json create mode 100644 src/loading/lib/package.json create mode 100644 src/loading/lib/project.json create mode 100644 src/loading/lib/src/TiLoadingComponent.ts create mode 100644 src/loading/lib/src/TiLoadingModule.ts create mode 100644 src/loading/lib/src/TiLoadingfailComponent.ts create mode 100644 src/loading/lib/src/i18n/TiLoadingWords.ts create mode 100644 src/loading/lib/src/i18n/en_US.ts create mode 100644 src/loading/lib/src/i18n/es_US.ts create mode 100644 src/loading/lib/src/i18n/fr_FR.ts create mode 100644 src/loading/lib/src/i18n/index.ts create mode 100644 src/loading/lib/src/i18n/pt_BR.ts create mode 100644 src/loading/lib/src/i18n/zh_CN.ts create mode 100644 src/loading/lib/src/loading.html create mode 100644 src/loading/lib/src/loading.less create mode 100644 src/loading/lib/src/loadingfail.html create mode 100644 src/loading/lib/src/loadingfail.less create mode 100644 src/locale/demo/karma.conf.js create mode 100644 src/locale/demo/project.json create mode 100644 src/locale/demo/src/app/AppComponent.ts create mode 100644 src/locale/demo/src/app/AppModule.ts create mode 100644 src/locale/demo/src/app/IndexComponent.ts create mode 100644 src/locale/demo/src/app/app.html create mode 100644 src/locale/demo/src/app/locale/LocaleBasicComponent.ts create mode 100644 src/locale/demo/src/app/locale/LocaleFormatComponent.ts create mode 100644 src/locale/demo/src/app/locale/LocaleReloadComponent.ts create mode 100644 src/locale/demo/src/app/locale/LocaleTestModule.ts create mode 100644 src/locale/demo/src/app/locale/locale-basic.html create mode 100644 src/locale/demo/src/app/locale/locale-format.html create mode 100644 src/locale/demo/src/app/locale/locale-reload.html create mode 100644 src/locale/demo/src/app/locale/webdoc/locale-demos.js create mode 100644 src/locale/demo/src/app/locale/webdoc/locale.cn.md create mode 100644 src/locale/demo/src/app/locale/webdoc/locale.en.md create mode 100644 src/locale/demo/src/favicon.ico create mode 100644 src/locale/demo/src/index.html create mode 100644 src/locale/demo/src/main.ts create mode 100644 src/locale/demo/test.ts create mode 100644 src/locale/demo/tsconfig.app.json create mode 100644 src/locale/demo/tsconfig.spec.json create mode 100644 src/locale/lib/index.ts create mode 100644 src/locale/lib/ng-package.json create mode 100644 src/locale/lib/package.json create mode 100644 src/locale/lib/project.json create mode 100644 src/locale/lib/src/TiLocale.ts create mode 100644 src/locale/lib/src/TiLocaleFormat.ts create mode 100644 src/locale/lib/src/TiLocaleModule.ts create mode 100644 src/locale/lib/src/TiTranslatePipe.ts create mode 100644 src/menu/demo/karma.conf.js create mode 100644 src/menu/demo/project.json create mode 100644 src/menu/demo/src/app/AppComponent.ts create mode 100644 src/menu/demo/src/app/AppModule.ts create mode 100644 src/menu/demo/src/app/IndexComponent.ts create mode 100644 src/menu/demo/src/app/app.html create mode 100644 src/menu/demo/src/app/menu/MenuBasicComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuBeforeopenComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuBorderComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuButtoncolorComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuDefaultComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuDisabledComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuEventComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuGroupComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuIdComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuLabelkeyComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuPanelalignComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuPanelstyleComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuTempleteComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuTempleteTestComponent.ts create mode 100644 src/menu/demo/src/app/menu/MenuTestModule.ts create mode 100644 src/menu/demo/src/app/menu/MenuTipsComponent.ts create mode 100644 src/menu/demo/src/app/menu/menu-basic.html create mode 100644 src/menu/demo/src/app/menu/menu-beforeopen.html create mode 100644 src/menu/demo/src/app/menu/menu-border.html create mode 100644 src/menu/demo/src/app/menu/menu-buttoncolor.html create mode 100644 src/menu/demo/src/app/menu/menu-default.html create mode 100644 src/menu/demo/src/app/menu/menu-disabled.html create mode 100644 src/menu/demo/src/app/menu/menu-event.html create mode 100644 src/menu/demo/src/app/menu/menu-group.html create mode 100644 src/menu/demo/src/app/menu/menu-id.html create mode 100644 src/menu/demo/src/app/menu/menu-labelkey.html create mode 100644 src/menu/demo/src/app/menu/menu-panelalign.html create mode 100644 src/menu/demo/src/app/menu/menu-panelstyle.html create mode 100644 src/menu/demo/src/app/menu/menu-templete-test.html create mode 100644 src/menu/demo/src/app/menu/menu-templete.html create mode 100644 src/menu/demo/src/app/menu/menu-tips.html create mode 100644 src/menu/demo/src/app/menu/webdoc/menu-demos.js create mode 100644 src/menu/demo/src/app/menu/webdoc/menu.cn.md create mode 100644 src/menu/demo/src/app/menu/webdoc/menu.en.md create mode 100644 src/menu/demo/src/favicon.ico create mode 100644 src/menu/demo/src/index.html create mode 100644 src/menu/demo/src/main.ts create mode 100644 src/menu/demo/test.ts create mode 100644 src/menu/demo/tsconfig.app.json create mode 100644 src/menu/demo/tsconfig.spec.json create mode 100644 src/menu/lib/index.ts create mode 100644 src/menu/lib/ng-package.json create mode 100644 src/menu/lib/package.json create mode 100644 src/menu/lib/project.json create mode 100644 src/menu/lib/src/TiMenuComponent.ts create mode 100644 src/menu/lib/src/TiMenuItem.ts create mode 100644 src/menu/lib/src/TiMenuListComponent.ts create mode 100644 src/menu/lib/src/TiMenuModule.ts create mode 100644 src/menu/lib/src/menu.html create mode 100644 src/menu/lib/src/menu.less create mode 100644 src/menu/lib/src/menulist.html create mode 100644 src/menu/lib/src/menulist.less create mode 100644 src/message/demo/karma.conf.js create mode 100644 src/message/demo/project.json create mode 100644 src/message/demo/src/app/AppComponent.ts create mode 100644 src/message/demo/src/app/AppModule.ts create mode 100644 src/message/demo/src/app/IndexComponent.ts create mode 100644 src/message/demo/src/app/app.html create mode 100644 src/message/demo/src/app/message/MessageBasicComponent.ts create mode 100644 src/message/demo/src/app/message/MessageBtnComponent.ts create mode 100644 src/message/demo/src/app/message/MessageBtnTestComponent.ts create mode 100644 src/message/demo/src/app/message/MessageContentComponent.ts create mode 100644 src/message/demo/src/app/message/MessageIdComponent.ts create mode 100644 src/message/demo/src/app/message/MessageSecurityComponent.ts create mode 100644 src/message/demo/src/app/message/MessageTestModule.ts create mode 100644 src/message/demo/src/app/message/MessageTitleComponent.ts create mode 100644 src/message/demo/src/app/message/MessageTypeComponent.ts create mode 100644 src/message/demo/src/app/message/message-basic.html create mode 100644 src/message/demo/src/app/message/message-btn-test.html create mode 100644 src/message/demo/src/app/message/message-btn.html create mode 100644 src/message/demo/src/app/message/message-content.html create mode 100644 src/message/demo/src/app/message/message-id.html create mode 100644 src/message/demo/src/app/message/message-security.html create mode 100644 src/message/demo/src/app/message/message-title.html create mode 100644 src/message/demo/src/app/message/message-type.html create mode 100644 src/message/demo/src/app/message/webdoc/message-demos.js create mode 100644 src/message/demo/src/app/message/webdoc/message.cn.md create mode 100644 src/message/demo/src/app/message/webdoc/message.en.md create mode 100644 src/message/demo/src/favicon.ico create mode 100644 src/message/demo/src/index.html create mode 100644 src/message/demo/src/main.ts create mode 100644 src/message/demo/test.ts create mode 100644 src/message/demo/tsconfig.app.json create mode 100644 src/message/demo/tsconfig.spec.json create mode 100644 src/message/lib/index.ts create mode 100644 src/message/lib/ng-package.json create mode 100644 src/message/lib/package.json create mode 100644 src/message/lib/project.json create mode 100644 src/message/lib/src/TiContentWrapperComponent.ts create mode 100644 src/message/lib/src/TiMessageComponent.html create mode 100644 src/message/lib/src/TiMessageComponent.ts create mode 100644 src/message/lib/src/TiMessageInterface.ts create mode 100644 src/message/lib/src/TiMessageModule.ts create mode 100644 src/message/lib/src/TiMessageService.ts create mode 100644 src/message/lib/src/TiTranscludeDirective.ts create mode 100644 src/message/lib/src/i18n/TiMessageWords.ts create mode 100644 src/message/lib/src/i18n/en_US.ts create mode 100644 src/message/lib/src/i18n/es_US.ts create mode 100644 src/message/lib/src/i18n/fr_FR.ts create mode 100644 src/message/lib/src/i18n/index.ts create mode 100644 src/message/lib/src/i18n/pt_BR.ts create mode 100644 src/message/lib/src/i18n/zh_CN.ts create mode 100644 src/message/lib/src/message.less create mode 100644 src/modal/demo/karma.conf.js create mode 100644 src/modal/demo/project.json create mode 100644 src/modal/demo/src/app/AppComponent.ts create mode 100644 src/modal/demo/src/app/AppModule.ts create mode 100644 src/modal/demo/src/app/IndexComponent.ts create mode 100644 src/modal/demo/src/app/app.html create mode 100644 src/modal/demo/src/app/modal/ModalAnimationComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalBackdropComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalClassComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalCloseIconComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalConfigTestComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalContentCompComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalContentTempComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalDraggableComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalEscComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalEventComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalHeaderAlignComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalHeaderStyleComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalTestModule.ts create mode 100644 src/modal/demo/src/app/modal/ModalTwoBackdropComponent.ts create mode 100644 src/modal/demo/src/app/modal/ModalTwoTestComponent.ts create mode 100644 src/modal/demo/src/app/modal/TestComponent.ts create mode 100644 src/modal/demo/src/app/modal/modal-animation.html create mode 100644 src/modal/demo/src/app/modal/modal-backdrop.html create mode 100644 src/modal/demo/src/app/modal/modal-class.html create mode 100644 src/modal/demo/src/app/modal/modal-close-icon.html create mode 100644 src/modal/demo/src/app/modal/modal-config-test.html create mode 100644 src/modal/demo/src/app/modal/modal-content-comp.html create mode 100644 src/modal/demo/src/app/modal/modal-content-temp.html create mode 100644 src/modal/demo/src/app/modal/modal-draggable.html create mode 100644 src/modal/demo/src/app/modal/modal-esc.html create mode 100644 src/modal/demo/src/app/modal/modal-event.html create mode 100644 src/modal/demo/src/app/modal/modal-header-align.html create mode 100644 src/modal/demo/src/app/modal/modal-header-style.html create mode 100644 src/modal/demo/src/app/modal/modal-two-backdrop.html create mode 100644 src/modal/demo/src/app/modal/modal-two-test.html create mode 100644 src/modal/demo/src/app/modal/webdoc/modal-demos.js create mode 100644 src/modal/demo/src/app/modal/webdoc/modal.cn.md create mode 100644 src/modal/demo/src/app/modal/webdoc/modal.en.md create mode 100644 src/modal/demo/src/favicon.ico create mode 100644 src/modal/demo/src/index.html create mode 100644 src/modal/demo/src/main.ts create mode 100644 src/modal/demo/test.ts create mode 100644 src/modal/demo/tsconfig.app.json create mode 100644 src/modal/demo/tsconfig.spec.json create mode 100644 src/modal/lib/index.ts create mode 100644 src/modal/lib/ng-package.json create mode 100644 src/modal/lib/package.json create mode 100644 src/modal/lib/project.json create mode 100644 src/modal/lib/src/TiBackdropComponent.ts create mode 100644 src/modal/lib/src/TiBackdropNoAnimationComponent.ts create mode 100644 src/modal/lib/src/TiModalBodyComponent.ts create mode 100644 src/modal/lib/src/TiModalComponent.html create mode 100644 src/modal/lib/src/TiModalComponent.ts create mode 100644 src/modal/lib/src/TiModalComponentNoAnimation.html create mode 100644 src/modal/lib/src/TiModalFooterComponent.ts create mode 100644 src/modal/lib/src/TiModalHeaderComponent.ts create mode 100644 src/modal/lib/src/TiModalInterface.ts create mode 100644 src/modal/lib/src/TiModalModule.ts create mode 100644 src/modal/lib/src/TiModalNoAnimationComponent.ts create mode 100644 src/modal/lib/src/TiModalService.ts create mode 100644 src/modal/lib/src/backdrop.less create mode 100644 src/modal/lib/src/modal.less create mode 100644 src/nav/demo/karma.conf.js create mode 100644 src/nav/demo/project.json create mode 100644 src/nav/demo/src/app/AppComponent.ts create mode 100644 src/nav/demo/src/app/AppModule.ts create mode 100644 src/nav/demo/src/app/IndexComponent.ts create mode 100644 src/nav/demo/src/app/app.html create mode 100644 src/nav/demo/src/app/nav/NavActiveComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavAlignComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavBasicComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavDisabledComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavEventComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavLeftComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavRightComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavSelectableComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavSubmenuComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavTemplateComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavTestModule.ts create mode 100644 src/nav/demo/src/app/nav/NavThemeComponent.ts create mode 100644 src/nav/demo/src/app/nav/NavWidthComponent.ts create mode 100644 src/nav/demo/src/app/nav/nav-active.html create mode 100644 src/nav/demo/src/app/nav/nav-align.html create mode 100644 src/nav/demo/src/app/nav/nav-basic.html create mode 100644 src/nav/demo/src/app/nav/nav-disabled.html create mode 100644 src/nav/demo/src/app/nav/nav-event.html create mode 100644 src/nav/demo/src/app/nav/nav-left.html create mode 100644 src/nav/demo/src/app/nav/nav-right.html create mode 100644 src/nav/demo/src/app/nav/nav-selectable.html create mode 100644 src/nav/demo/src/app/nav/nav-submenu.html create mode 100644 src/nav/demo/src/app/nav/nav-template.html create mode 100644 src/nav/demo/src/app/nav/nav-theme.html create mode 100644 src/nav/demo/src/app/nav/nav-width.html create mode 100644 src/nav/demo/src/app/nav/webdoc/nav-demos.js create mode 100644 src/nav/demo/src/app/nav/webdoc/nav.cn.md create mode 100644 src/nav/demo/src/app/nav/webdoc/nav.en.md create mode 100644 src/nav/demo/src/favicon.ico create mode 100644 src/nav/demo/src/index.html create mode 100644 src/nav/demo/src/main.ts create mode 100644 src/nav/demo/test.ts create mode 100644 src/nav/demo/tsconfig.app.json create mode 100644 src/nav/demo/tsconfig.spec.json create mode 100644 src/nav/lib/index.ts create mode 100644 src/nav/lib/ng-package.json create mode 100644 src/nav/lib/package.json create mode 100644 src/nav/lib/project.json create mode 100644 src/nav/lib/src/TiNavComponent.ts create mode 100644 src/nav/lib/src/TiNavLeftComponent.ts create mode 100644 src/nav/lib/src/TiNavMenuComponent.ts create mode 100644 src/nav/lib/src/TiNavModule.ts create mode 100644 src/nav/lib/src/TiNavRightComponent.ts create mode 100644 src/nav/lib/src/common.less create mode 100644 src/nav/lib/src/interface.ts create mode 100644 src/nav/lib/src/nav.html create mode 100644 src/nav/lib/src/nav.less create mode 100644 src/nav/lib/src/navleft.html create mode 100644 src/nav/lib/src/navleft.less create mode 100644 src/nav/lib/src/navmenu.html create mode 100644 src/nav/lib/src/navmenu.less create mode 100644 src/nav/lib/src/navright.html create mode 100644 src/nav/lib/src/navright.less create mode 100644 src/ng/demo/karma.conf.js create mode 100644 src/ng/demo/project.json create mode 100644 src/ng/demo/src/app/AppComponent.ts create mode 100644 src/ng/demo/src/app/AppModule.ts create mode 100644 src/ng/demo/src/app/AppWcModule.ts create mode 100644 src/ng/demo/src/app/DemoModules.ts create mode 100644 src/ng/demo/src/app/IndexComponent.ts create mode 100644 src/ng/demo/src/app/app.html create mode 100644 src/ng/demo/src/assets/browser/chrome.PNG create mode 100644 src/ng/demo/src/assets/browser/edge.PNG create mode 100644 src/ng/demo/src/assets/browser/firefox.PNG create mode 100644 src/ng/demo/src/assets/browser/safari.PNG create mode 100644 src/ng/demo/src/assets/food/cake.png create mode 100644 src/ng/demo/src/assets/food/coffee.png create mode 100644 src/ng/demo/src/assets/food/cola.png create mode 100644 src/ng/demo/src/assets/food/fried_chicken.png create mode 100644 src/ng/demo/src/assets/food/fries.png create mode 100644 src/ng/demo/src/assets/food/hamburger.png create mode 100644 src/ng/demo/src/assets/food/milk.png create mode 100644 src/ng/demo/src/assets/food/pizza.png create mode 100644 src/ng/demo/src/assets/image/1.jpg create mode 100644 src/ng/demo/src/assets/image/2.jpg create mode 100644 src/ng/demo/src/assets/image/3.jpg create mode 100644 src/ng/demo/src/assets/nav_logo/logo.png create mode 100644 src/ng/demo/src/favicon.ico create mode 100644 src/ng/demo/src/index.html create mode 100644 src/ng/demo/src/main.ts create mode 100644 src/ng/demo/src/webdoc/LICENSE create mode 100644 src/ng/demo/src/webdoc/faq-en.md create mode 100644 src/ng/demo/src/webdoc/faq.md create mode 100644 src/ng/demo/src/webdoc/getstart-en.md create mode 100644 src/ng/demo/src/webdoc/getstart.md create mode 100644 src/ng/demo/src/webdoc/images/basecolor1.png create mode 100644 src/ng/demo/src/webdoc/images/demo.png create mode 100644 src/ng/demo/src/webdoc/images/theme.png create mode 100644 src/ng/demo/src/webdoc/introduce-en.md create mode 100644 src/ng/demo/src/webdoc/introduce.md create mode 100644 src/ng/demo/src/webdoc/joinus-en.md create mode 100644 src/ng/demo/src/webdoc/joinus.md create mode 100644 src/ng/demo/src/webdoc/language-en.md create mode 100644 src/ng/demo/src/webdoc/language.md create mode 100644 src/ng/demo/src/webdoc/menus.js create mode 100644 src/ng/demo/src/webdoc/themedoc-en.md create mode 100644 src/ng/demo/src/webdoc/themedoc.md create mode 100644 src/ng/demo/src/webdoc/validators.md create mode 100644 src/ng/demo/test.ts create mode 100644 src/ng/demo/tsconfig.app.json create mode 100644 src/ng/demo/tsconfig.spec.json create mode 100644 src/ng/demolog/DemoLogComponent.ts create mode 100644 src/ng/demolog/DemoLogModule.ts create mode 100644 src/ng/demolog/log.html create mode 100644 src/ng/demolog/log.less create mode 100644 src/ng/lib/index.ts create mode 100644 src/ng/lib/ng-package.json create mode 100644 src/ng/lib/package.json create mode 100644 src/ng/lib/project.json create mode 100644 src/notification/demo/karma.conf.js create mode 100644 src/notification/demo/project.json create mode 100644 src/notification/demo/src/app/AppComponent.ts create mode 100644 src/notification/demo/src/app/AppModule.ts create mode 100644 src/notification/demo/src/app/IndexComponent.ts create mode 100644 src/notification/demo/src/app/app.html create mode 100644 src/notification/demo/src/app/notification/NotificationAnimationComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationBasicComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationCloseComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationConfigComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationDurationComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationEventsComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationHoverPauseComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationNameComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationPositionComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationTemplateComponent.ts create mode 100644 src/notification/demo/src/app/notification/NotificationTestModule.ts create mode 100644 src/notification/demo/src/app/notification/NotificationTypeComponent.ts create mode 100644 src/notification/demo/src/app/notification/notification-animation.html create mode 100644 src/notification/demo/src/app/notification/notification-basic.html create mode 100644 src/notification/demo/src/app/notification/notification-close.html create mode 100644 src/notification/demo/src/app/notification/notification-config.html create mode 100644 src/notification/demo/src/app/notification/notification-duration.html create mode 100644 src/notification/demo/src/app/notification/notification-events.html create mode 100644 src/notification/demo/src/app/notification/notification-hover-pause.html create mode 100644 src/notification/demo/src/app/notification/notification-name.html create mode 100644 src/notification/demo/src/app/notification/notification-position.html create mode 100644 src/notification/demo/src/app/notification/notification-template.html create mode 100644 src/notification/demo/src/app/notification/notification-type.html create mode 100644 src/notification/demo/src/app/notification/webdoc/notification-demos.js create mode 100644 src/notification/demo/src/app/notification/webdoc/notification.cn.md create mode 100644 src/notification/demo/src/app/notification/webdoc/notification.en.md create mode 100644 src/notification/demo/src/favicon.ico create mode 100644 src/notification/demo/src/index.html create mode 100644 src/notification/demo/src/main.ts create mode 100644 src/notification/demo/test.ts create mode 100644 src/notification/demo/tsconfig.app.json create mode 100644 src/notification/demo/tsconfig.spec.json create mode 100644 src/notification/lib/index.ts create mode 100644 src/notification/lib/ng-package.json create mode 100644 src/notification/lib/package.json create mode 100644 src/notification/lib/project.json create mode 100644 src/notification/lib/src/TiNotificationComponent.html create mode 100644 src/notification/lib/src/TiNotificationComponent.ts create mode 100644 src/notification/lib/src/TiNotificationContainerComponent.html create mode 100644 src/notification/lib/src/TiNotificationContainerComponent.ts create mode 100644 src/notification/lib/src/TiNotificationInterface.ts create mode 100644 src/notification/lib/src/TiNotificationMapper.ts create mode 100644 src/notification/lib/src/TiNotificationModule.ts create mode 100644 src/notification/lib/src/TiNotificationMotion.ts create mode 100644 src/notification/lib/src/TiNotificationService.ts create mode 100644 src/notification/lib/src/notification.less create mode 100644 src/notification/lib/src/position.less create mode 100644 src/outline/lib/index.ts create mode 100644 src/outline/lib/ng-package.json create mode 100644 src/outline/lib/package.json create mode 100644 src/outline/lib/project.json create mode 100644 src/outline/lib/src/TiOutlineDirective.ts create mode 100644 src/outline/lib/src/TiOutlineModule.ts create mode 100644 src/overflow/demo/karma.conf.js create mode 100644 src/overflow/demo/project.json create mode 100644 src/overflow/demo/src/app/AppComponent.ts create mode 100644 src/overflow/demo/src/app/AppModule.ts create mode 100644 src/overflow/demo/src/app/IndexComponent.ts create mode 100644 src/overflow/demo/src/app/app.html create mode 100644 src/overflow/demo/src/app/overflow/OverflowDestoryComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowDirectiveComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowMaxlineComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowMaxwidthComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowPositionComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowServiceComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowTestComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowTestModule.ts create mode 100644 src/overflow/demo/src/app/overflow/OverflowTipcontentComponent.ts create mode 100644 src/overflow/demo/src/app/overflow/overflow-destory.html create mode 100644 src/overflow/demo/src/app/overflow/overflow-directive.html create mode 100644 src/overflow/demo/src/app/overflow/overflow-maxline.html create mode 100644 src/overflow/demo/src/app/overflow/overflow-maxwidth.html create mode 100644 src/overflow/demo/src/app/overflow/overflow-position.html create mode 100644 src/overflow/demo/src/app/overflow/overflow-service.html create mode 100644 src/overflow/demo/src/app/overflow/overflow-test.html create mode 100644 src/overflow/demo/src/app/overflow/overflow-tipcontent.html create mode 100644 src/overflow/demo/src/app/overflow/webdoc/overflow-demos.js create mode 100644 src/overflow/demo/src/app/overflow/webdoc/overflow.cn.md create mode 100644 src/overflow/demo/src/app/overflow/webdoc/overflow.en.md create mode 100644 src/overflow/demo/src/favicon.ico create mode 100644 src/overflow/demo/src/index.html create mode 100644 src/overflow/demo/src/main.ts create mode 100644 src/overflow/demo/test.ts create mode 100644 src/overflow/demo/tsconfig.app.json create mode 100644 src/overflow/demo/tsconfig.spec.json create mode 100644 src/overflow/lib/index.ts create mode 100644 src/overflow/lib/ng-package.json create mode 100644 src/overflow/lib/package.json create mode 100644 src/overflow/lib/project.json create mode 100644 src/overflow/lib/src/TiOverflowDirective.ts create mode 100644 src/overflow/lib/src/TiOverflowMaxlineDirective.ts create mode 100644 src/overflow/lib/src/TiOverflowModule.ts create mode 100644 src/overflow/lib/src/TiOverflowService.ts create mode 100644 src/overflow/lib/src/TiOverflowServiceModule.ts create mode 100644 src/pagination/demo/karma.conf.js create mode 100644 src/pagination/demo/project.json create mode 100644 src/pagination/demo/src/app/AppComponent.ts create mode 100644 src/pagination/demo/src/app/AppModule.ts create mode 100644 src/pagination/demo/src/app/IndexComponent.ts create mode 100644 src/pagination/demo/src/app/app.html create mode 100644 src/pagination/demo/src/app/pagination/PaginationAutohideComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationDisabledComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationEventComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationFixedComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationLoadingComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationPageselectwidthComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationPagesizeComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationShowgotolinkComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationShowlastpageComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationShowtotalnumberComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationTestModule.ts create mode 100644 src/pagination/demo/src/app/pagination/PaginationTypeComponent.ts create mode 100644 src/pagination/demo/src/app/pagination/pagination-autohide.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-disabled.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-event.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-fixed.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-loading.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-pageselectwidth.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-pagesize.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-showgotolink.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-showlastpage.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-showtotalnumber.html create mode 100644 src/pagination/demo/src/app/pagination/pagination-type.html create mode 100644 src/pagination/demo/src/app/pagination/webdoc/pagination-demos.js create mode 100644 src/pagination/demo/src/app/pagination/webdoc/pagination.cn.md create mode 100644 src/pagination/demo/src/app/pagination/webdoc/pagination.en.md create mode 100644 src/pagination/demo/src/favicon.ico create mode 100644 src/pagination/demo/src/index.html create mode 100644 src/pagination/demo/src/main.ts create mode 100644 src/pagination/demo/test.ts create mode 100644 src/pagination/demo/tsconfig.app.json create mode 100644 src/pagination/demo/tsconfig.spec.json create mode 100644 src/pagination/lib/index.ts create mode 100644 src/pagination/lib/ng-package.json create mode 100644 src/pagination/lib/package.json create mode 100644 src/pagination/lib/project.json create mode 100644 src/pagination/lib/src/TiPaginationComponent.ts create mode 100644 src/pagination/lib/src/TiPaginationModule.ts create mode 100644 src/pagination/lib/src/i18n/TiPaginationWords.ts create mode 100644 src/pagination/lib/src/i18n/en_US.ts create mode 100644 src/pagination/lib/src/i18n/es_US.ts create mode 100644 src/pagination/lib/src/i18n/fr_FR.ts create mode 100644 src/pagination/lib/src/i18n/index.ts create mode 100644 src/pagination/lib/src/i18n/pt_BR.ts create mode 100644 src/pagination/lib/src/i18n/zh_CN.ts create mode 100644 src/pagination/lib/src/pagination.html create mode 100644 src/pagination/lib/src/pagination.less create mode 100644 src/path/demo/project.json create mode 100644 src/path/demo/src/app/AppComponent.ts create mode 100644 src/path/demo/src/app/AppModule.ts create mode 100644 src/path/demo/src/app/IndexComponent.ts create mode 100644 src/path/demo/src/app/app.html create mode 100644 src/path/demo/src/app/path/PathListComponent.ts create mode 100644 src/path/demo/src/app/path/PathSelectComponent.ts create mode 100644 src/path/demo/src/app/path/PathTestModule.ts create mode 100644 src/path/demo/src/app/path/PathfieldEditableComponent.ts create mode 100644 src/path/demo/src/app/path/PathfieldEventComponent.ts create mode 100644 src/path/demo/src/app/path/PathfieldIspanelComponent.ts create mode 100644 src/path/demo/src/app/path/PathfieldItemsComponent.ts create mode 100644 src/path/demo/src/app/path/PathfieldPanelwidthComponent.ts create mode 100644 src/path/demo/src/app/path/path-list.html create mode 100644 src/path/demo/src/app/path/path-select.html create mode 100644 src/path/demo/src/app/path/pathfield-editable.html create mode 100644 src/path/demo/src/app/path/pathfield-event.html create mode 100644 src/path/demo/src/app/path/pathfield-ispanel.html create mode 100644 src/path/demo/src/app/path/pathfield-items.html create mode 100644 src/path/demo/src/app/path/pathfield-panelwidth.html create mode 100644 src/path/demo/src/app/path/webdoc/path-demos.js create mode 100644 src/path/demo/src/app/path/webdoc/path.cn.md create mode 100644 src/path/demo/src/app/path/webdoc/path.en.md create mode 100644 src/path/demo/src/favicon.ico create mode 100644 src/path/demo/src/index.html create mode 100644 src/path/demo/src/main.ts create mode 100644 src/path/demo/tsconfig.app.json create mode 100644 src/path/lib/index.ts create mode 100644 src/path/lib/ng-package.json create mode 100644 src/path/lib/package.json create mode 100644 src/path/lib/project.json create mode 100644 src/path/lib/src/TiPathFieldComponent.ts create mode 100644 src/path/lib/src/TiPathListComponent.ts create mode 100644 src/path/lib/src/TiPathModule.ts create mode 100644 src/path/lib/src/path-field.html create mode 100644 src/path/lib/src/path-field.less create mode 100644 src/path/lib/src/path-list.html create mode 100644 src/path/lib/src/path-list.less create mode 100644 src/phonenumber/demo/project.json create mode 100644 src/phonenumber/demo/src/app/AppComponent.ts create mode 100644 src/phonenumber/demo/src/app/AppModule.ts create mode 100644 src/phonenumber/demo/src/app/IndexComponent.ts create mode 100644 src/phonenumber/demo/src/app/app.html create mode 100644 src/phonenumber/demo/src/app/phonenumber/PhonenumberBasicComponent.ts create mode 100644 src/phonenumber/demo/src/app/phonenumber/PhonenumberCountryComponent.ts create mode 100644 src/phonenumber/demo/src/app/phonenumber/PhonenumberDisabledComponent.ts create mode 100644 src/phonenumber/demo/src/app/phonenumber/PhonenumberEventComponent.ts create mode 100644 src/phonenumber/demo/src/app/phonenumber/PhonenumberTestModule.ts create mode 100644 src/phonenumber/demo/src/app/phonenumber/PhonenumberValidComponent.ts create mode 100644 src/phonenumber/demo/src/app/phonenumber/phonenumber-basic.html create mode 100644 src/phonenumber/demo/src/app/phonenumber/phonenumber-country.html create mode 100644 src/phonenumber/demo/src/app/phonenumber/phonenumber-disabled.html create mode 100644 src/phonenumber/demo/src/app/phonenumber/phonenumber-event.html create mode 100644 src/phonenumber/demo/src/app/phonenumber/phonenumber-valid.html create mode 100644 src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber-demos.js create mode 100644 src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.cn.md create mode 100644 src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.en.md create mode 100644 src/phonenumber/demo/src/favicon.ico create mode 100644 src/phonenumber/demo/src/index.html create mode 100644 src/phonenumber/demo/src/main.ts create mode 100644 src/phonenumber/demo/tsconfig.app.json create mode 100644 src/phonenumber/lib/index.ts create mode 100644 src/phonenumber/lib/ng-package.json create mode 100644 src/phonenumber/lib/package.json create mode 100644 src/phonenumber/lib/project.json create mode 100644 src/phonenumber/lib/src/TiPhoneValidatorDirective.ts create mode 100644 src/phonenumber/lib/src/TiPhonenumberComponent.ts create mode 100644 src/phonenumber/lib/src/TiPhonenumberModule.ts create mode 100644 src/phonenumber/lib/src/i18n/TiPhonenumberWords.ts create mode 100644 src/phonenumber/lib/src/i18n/en_US.ts create mode 100644 src/phonenumber/lib/src/i18n/es_US.ts create mode 100644 src/phonenumber/lib/src/i18n/fr_FR.ts create mode 100644 src/phonenumber/lib/src/i18n/index.ts create mode 100644 src/phonenumber/lib/src/i18n/pt_BR.ts create mode 100644 src/phonenumber/lib/src/i18n/zh_CN.ts create mode 100644 src/phonenumber/lib/src/phonenumber.html create mode 100644 src/phonenumber/lib/src/phonenumber.less create mode 100644 src/popconfirm/demo/project.json create mode 100644 src/popconfirm/demo/src/app/AppComponent.ts create mode 100644 src/popconfirm/demo/src/app/AppModule.ts create mode 100644 src/popconfirm/demo/src/app/IndexComponent.ts create mode 100644 src/popconfirm/demo/src/app/app.html create mode 100644 src/popconfirm/demo/src/app/popconfirm/PopconfirmBasicComponent.ts create mode 100644 src/popconfirm/demo/src/app/popconfirm/PopconfirmDefineComponent.ts create mode 100644 src/popconfirm/demo/src/app/popconfirm/PopconfirmEventComponent.ts create mode 100644 src/popconfirm/demo/src/app/popconfirm/PopconfirmTableComponent.ts create mode 100644 src/popconfirm/demo/src/app/popconfirm/PopconfirmTableDefineComponent.ts create mode 100644 src/popconfirm/demo/src/app/popconfirm/PopconfirmTestModule.ts create mode 100644 src/popconfirm/demo/src/app/popconfirm/popconfirm-basic.html create mode 100644 src/popconfirm/demo/src/app/popconfirm/popconfirm-define.html create mode 100644 src/popconfirm/demo/src/app/popconfirm/popconfirm-event.html create mode 100644 src/popconfirm/demo/src/app/popconfirm/popconfirm-table-define.html create mode 100644 src/popconfirm/demo/src/app/popconfirm/popconfirm-table.html create mode 100644 src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm-demos.js create mode 100644 src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.cn.md create mode 100644 src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.en.md create mode 100644 src/popconfirm/demo/src/favicon.ico create mode 100644 src/popconfirm/demo/src/index.html create mode 100644 src/popconfirm/demo/src/main.ts create mode 100644 src/popconfirm/demo/tsconfig.app.json create mode 100644 src/popconfirm/lib/index.ts create mode 100644 src/popconfirm/lib/ng-package.json create mode 100644 src/popconfirm/lib/package.json create mode 100644 src/popconfirm/lib/project.json create mode 100644 src/popconfirm/lib/src/TiPopconfirmComponent.ts create mode 100644 src/popconfirm/lib/src/TiPopconfirmDirective.ts create mode 100644 src/popconfirm/lib/src/TiPopconfirmModule.ts create mode 100644 src/popconfirm/lib/src/i18n/TiPopconfirmWords.ts create mode 100644 src/popconfirm/lib/src/i18n/en_US.ts create mode 100644 src/popconfirm/lib/src/i18n/es_US.ts create mode 100644 src/popconfirm/lib/src/i18n/fr_FR.ts create mode 100644 src/popconfirm/lib/src/i18n/index.ts create mode 100644 src/popconfirm/lib/src/i18n/pt_BR.ts create mode 100644 src/popconfirm/lib/src/i18n/zh_CN.ts create mode 100644 src/popconfirm/lib/src/popconfirm.html create mode 100644 src/popconfirm/lib/src/popconfirm.less create mode 100644 src/popup/lib/index.ts create mode 100644 src/popup/lib/ng-package.json create mode 100644 src/popup/lib/package.json create mode 100644 src/popup/lib/project.json create mode 100644 src/popup/lib/src/TiPopupModule.ts create mode 100644 src/popup/lib/src/TiPopupService.ts create mode 100644 src/productpreview/demo/project.json create mode 100644 src/productpreview/demo/src/app/AppComponent.ts create mode 100644 src/productpreview/demo/src/app/AppModule.ts create mode 100644 src/productpreview/demo/src/app/IndexComponent.ts create mode 100644 src/productpreview/demo/src/app/app.html create mode 100644 src/productpreview/demo/src/app/productpreview/ProductpreviewBasicComponent.ts create mode 100644 src/productpreview/demo/src/app/productpreview/ProductpreviewTestModule.ts create mode 100644 src/productpreview/demo/src/app/productpreview/productpreview-basic.html create mode 100644 src/productpreview/demo/src/app/productpreview/webdoc/productpreview-demos.js create mode 100644 src/productpreview/demo/src/app/productpreview/webdoc/productpreview.cn.md create mode 100644 src/productpreview/demo/src/app/productpreview/webdoc/productpreview.en.md create mode 100644 src/productpreview/demo/src/favicon.ico create mode 100644 src/productpreview/demo/src/index.html create mode 100644 src/productpreview/demo/src/main.ts create mode 100644 src/productpreview/demo/tsconfig.app.json create mode 100644 src/productpreview/lib/index.ts create mode 100644 src/productpreview/lib/ng-package.json create mode 100644 src/productpreview/lib/package.json create mode 100644 src/productpreview/lib/project.json create mode 100644 src/productpreview/lib/src/TiProductpreviewComponent.ts create mode 100644 src/productpreview/lib/src/TiProductpreviewModule.ts create mode 100644 src/productpreview/lib/src/productpreview.html create mode 100644 src/productpreview/lib/src/productpreview.less create mode 100644 src/progressbar/demo/karma.conf.js create mode 100644 src/progressbar/demo/project.json create mode 100644 src/progressbar/demo/src/app/AppComponent.ts create mode 100644 src/progressbar/demo/src/app/AppModule.ts create mode 100644 src/progressbar/demo/src/app/IndexComponent.ts create mode 100644 src/progressbar/demo/src/app/app.html create mode 100644 src/progressbar/demo/src/app/progressbar/ProgressbarAnimationComponent.ts create mode 100644 src/progressbar/demo/src/app/progressbar/ProgressbarBasicComponent.ts create mode 100644 src/progressbar/demo/src/app/progressbar/ProgressbarClassComponent.ts create mode 100644 src/progressbar/demo/src/app/progressbar/ProgressbarTestModule.ts create mode 100644 src/progressbar/demo/src/app/progressbar/progressbar-animation.html create mode 100644 src/progressbar/demo/src/app/progressbar/progressbar-basic.html create mode 100644 src/progressbar/demo/src/app/progressbar/progressbar-class.html create mode 100644 src/progressbar/demo/src/app/progressbar/progressbar-class.less create mode 100644 src/progressbar/demo/src/app/progressbar/webdoc/progressbar-demos.js create mode 100644 src/progressbar/demo/src/app/progressbar/webdoc/progressbar.cn.md create mode 100644 src/progressbar/demo/src/app/progressbar/webdoc/progressbar.en.md create mode 100644 src/progressbar/demo/src/favicon.ico create mode 100644 src/progressbar/demo/src/index.html create mode 100644 src/progressbar/demo/src/main.ts create mode 100644 src/progressbar/demo/test.ts create mode 100644 src/progressbar/demo/tsconfig.app.json create mode 100644 src/progressbar/demo/tsconfig.spec.json create mode 100644 src/progressbar/lib/index.ts create mode 100644 src/progressbar/lib/ng-package.json create mode 100644 src/progressbar/lib/package.json create mode 100644 src/progressbar/lib/project.json create mode 100644 src/progressbar/lib/src/TiProgressbarComponent.ts create mode 100644 src/progressbar/lib/src/TiProgressbarModule.ts create mode 100644 src/progressbar/lib/src/progressbar.html create mode 100644 src/progressbar/lib/src/progressbar.less create mode 100644 src/progresspie/demo/project.json create mode 100644 src/progresspie/demo/src/app/AppComponent.ts create mode 100644 src/progresspie/demo/src/app/AppModule.ts create mode 100644 src/progresspie/demo/src/app/IndexComponent.ts create mode 100644 src/progresspie/demo/src/app/app.html create mode 100644 src/progresspie/demo/src/app/progresspie/ProgresspieTestComponent.ts create mode 100644 src/progresspie/demo/src/app/progresspie/ProgresspieTestModule.ts create mode 100644 src/progresspie/demo/src/favicon.ico create mode 100644 src/progresspie/demo/src/index.html create mode 100644 src/progresspie/demo/src/main.ts create mode 100644 src/progresspie/demo/tsconfig.app.json create mode 100644 src/progresspie/lib/index.ts create mode 100644 src/progresspie/lib/ng-package.json create mode 100644 src/progresspie/lib/package.json create mode 100644 src/progresspie/lib/project.json create mode 100644 src/progresspie/lib/src/TiProgresspieComponent.ts create mode 100644 src/progresspie/lib/src/TiProgresspieModule.ts create mode 100644 src/radio/demo/karma.conf.js create mode 100644 src/radio/demo/project.json create mode 100644 src/radio/demo/src/app/AppComponent.ts create mode 100644 src/radio/demo/src/app/AppModule.ts create mode 100644 src/radio/demo/src/app/IndexComponent.ts create mode 100644 src/radio/demo/src/app/app.html create mode 100644 src/radio/demo/src/app/radio/RadioBasicComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioDarkComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioDisabledComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioEventComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioFocusComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioGroupComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioGroupDirectionComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioGroupLabelkeyComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioGroupLinewrapComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioGroupValidationComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioGroupValuekeyComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioLabelComponent.ts create mode 100644 src/radio/demo/src/app/radio/RadioTestModule.ts create mode 100644 src/radio/demo/src/app/radio/radio-basic.html create mode 100644 src/radio/demo/src/app/radio/radio-dark.html create mode 100644 src/radio/demo/src/app/radio/radio-disabled.html create mode 100644 src/radio/demo/src/app/radio/radio-event.html create mode 100644 src/radio/demo/src/app/radio/radio-focus.html create mode 100644 src/radio/demo/src/app/radio/radio-group-direction.html create mode 100644 src/radio/demo/src/app/radio/radio-group-labelkey.html create mode 100644 src/radio/demo/src/app/radio/radio-group-linewrap.html create mode 100644 src/radio/demo/src/app/radio/radio-group-validation.html create mode 100644 src/radio/demo/src/app/radio/radio-group-valuekey.html create mode 100644 src/radio/demo/src/app/radio/radio-group.html create mode 100644 src/radio/demo/src/app/radio/radio-label.html create mode 100644 src/radio/demo/src/app/radio/webdoc/radio-demos.js create mode 100644 src/radio/demo/src/app/radio/webdoc/radio.cn.md create mode 100644 src/radio/demo/src/app/radio/webdoc/radio.en.md create mode 100644 src/radio/demo/src/favicon.ico create mode 100644 src/radio/demo/src/index.html create mode 100644 src/radio/demo/src/main.ts create mode 100644 src/radio/demo/test.ts create mode 100644 src/radio/demo/tsconfig.app.json create mode 100644 src/radio/demo/tsconfig.spec.json create mode 100644 src/radio/lib/index.ts create mode 100644 src/radio/lib/ng-package.json create mode 100644 src/radio/lib/package.json create mode 100644 src/radio/lib/project.json create mode 100644 src/radio/lib/src/TiRadioComponent.ts create mode 100644 src/radio/lib/src/TiRadioGroupComponent.ts create mode 100644 src/radio/lib/src/TiRadioModule.ts create mode 100644 src/radio/lib/src/radio-group.html create mode 100644 src/radio/lib/src/radio.html create mode 100644 src/radio/lib/src/radio.less create mode 100644 src/radio/lib/src/radiogroup.less create mode 100644 src/rate/demo/karma.conf.js create mode 100644 src/rate/demo/project.json create mode 100644 src/rate/demo/src/app/AppComponent.ts create mode 100644 src/rate/demo/src/app/AppModule.ts create mode 100644 src/rate/demo/src/app/IndexComponent.ts create mode 100644 src/rate/demo/src/app/app.html create mode 100644 src/rate/demo/src/app/rate/RateBasicComponent.ts create mode 100644 src/rate/demo/src/app/rate/RateDisabledComponent.ts create mode 100644 src/rate/demo/src/app/rate/RateEventComponent.ts create mode 100644 src/rate/demo/src/app/rate/RateIdComponent.ts create mode 100644 src/rate/demo/src/app/rate/RateLoadComponent.ts create mode 100644 src/rate/demo/src/app/rate/RateTestModule.ts create mode 100644 src/rate/demo/src/app/rate/rate-basic.html create mode 100644 src/rate/demo/src/app/rate/rate-disabled.html create mode 100644 src/rate/demo/src/app/rate/rate-event.html create mode 100644 src/rate/demo/src/app/rate/rate-id.html create mode 100644 src/rate/demo/src/app/rate/rate-load.html create mode 100644 src/rate/demo/src/app/rate/webdoc/rate-demos.js create mode 100644 src/rate/demo/src/app/rate/webdoc/rate.cn.md create mode 100644 src/rate/demo/src/app/rate/webdoc/rate.en.md create mode 100644 src/rate/demo/src/favicon.ico create mode 100644 src/rate/demo/src/index.html create mode 100644 src/rate/demo/src/main.ts create mode 100644 src/rate/demo/test.ts create mode 100644 src/rate/demo/tsconfig.app.json create mode 100644 src/rate/demo/tsconfig.spec.json create mode 100644 src/rate/lib/index.ts create mode 100644 src/rate/lib/ng-package.json create mode 100644 src/rate/lib/package.json create mode 100644 src/rate/lib/project.json create mode 100644 src/rate/lib/src/TiRateComponent.ts create mode 100644 src/rate/lib/src/TiRateModule.ts create mode 100644 src/rate/lib/src/rate.html create mode 100644 src/rate/lib/src/rate.less create mode 100644 src/renderer/lib/index.ts create mode 100644 src/renderer/lib/ng-package.json create mode 100644 src/renderer/lib/package.json create mode 100644 src/renderer/lib/project.json create mode 100644 src/renderer/lib/src/TiRenderer.ts create mode 100644 src/renderer/lib/src/TiRendererModule.ts create mode 100644 src/rights/demo/project.json create mode 100644 src/rights/demo/src/app/AppComponent.ts create mode 100644 src/rights/demo/src/app/AppModule.ts create mode 100644 src/rights/demo/src/app/IndexComponent.ts create mode 100644 src/rights/demo/src/app/app.html create mode 100644 src/rights/demo/src/app/rights/RightsBasicComponent.ts create mode 100644 src/rights/demo/src/app/rights/RightsTestModule.ts create mode 100644 src/rights/demo/src/app/rights/RightsTypeComponent.ts create mode 100644 src/rights/demo/src/app/rights/rights-basic.html create mode 100644 src/rights/demo/src/app/rights/rights-type.html create mode 100644 src/rights/demo/src/app/rights/webdoc/rights-demos.js create mode 100644 src/rights/demo/src/app/rights/webdoc/rights.cn.md create mode 100644 src/rights/demo/src/app/rights/webdoc/rights.en.md create mode 100644 src/rights/demo/src/favicon.ico create mode 100644 src/rights/demo/src/index.html create mode 100644 src/rights/demo/src/main.ts create mode 100644 src/rights/demo/tsconfig.app.json create mode 100644 src/rights/lib/index.ts create mode 100644 src/rights/lib/ng-package.json create mode 100644 src/rights/lib/package.json create mode 100644 src/rights/lib/project.json create mode 100644 src/rights/lib/src/TiRightsComponent.ts create mode 100644 src/rights/lib/src/TiRightsModule.ts create mode 100644 src/rights/lib/src/fonts/rightsFont.svg create mode 100644 src/rights/lib/src/fonts/rightsFont.woff create mode 100644 src/rights/lib/src/icon.less create mode 100644 src/rights/lib/src/rights.html create mode 100644 src/rights/lib/src/rights.less create mode 100644 src/score/demo/karma.conf.js create mode 100644 src/score/demo/project.json create mode 100644 src/score/demo/src/app/AppComponent.ts create mode 100644 src/score/demo/src/app/AppModule.ts create mode 100644 src/score/demo/src/app/IndexComponent.ts create mode 100644 src/score/demo/src/app/app.html create mode 100644 src/score/demo/src/app/score/ScoreBasicComponent.ts create mode 100644 src/score/demo/src/app/score/ScoreEventsComponent.ts create mode 100644 src/score/demo/src/app/score/ScoreLimittextComponent.ts create mode 100644 src/score/demo/src/app/score/ScorePaddingComponent.ts create mode 100644 src/score/demo/src/app/score/ScoreTestModule.ts create mode 100644 src/score/demo/src/app/score/score-basic.html create mode 100644 src/score/demo/src/app/score/score-events.html create mode 100644 src/score/demo/src/app/score/score-limittext.html create mode 100644 src/score/demo/src/app/score/score-padding.html create mode 100644 src/score/demo/src/app/score/webdoc/score-demos.js create mode 100644 src/score/demo/src/app/score/webdoc/score.cn.md create mode 100644 src/score/demo/src/app/score/webdoc/score.en.md create mode 100644 src/score/demo/src/favicon.ico create mode 100644 src/score/demo/src/index.html create mode 100644 src/score/demo/src/main.ts create mode 100644 src/score/demo/test.ts create mode 100644 src/score/demo/tsconfig.app.json create mode 100644 src/score/demo/tsconfig.spec.json create mode 100644 src/score/lib/index.ts create mode 100644 src/score/lib/ng-package.json create mode 100644 src/score/lib/package.json create mode 100644 src/score/lib/project.json create mode 100644 src/score/lib/src/TiScoreComponent.ts create mode 100644 src/score/lib/src/TiScoreModule.ts create mode 100644 src/score/lib/src/i18n/TiScoreWords.ts create mode 100644 src/score/lib/src/i18n/en_US.ts create mode 100644 src/score/lib/src/i18n/es_US.ts create mode 100644 src/score/lib/src/i18n/fr_FR.ts create mode 100644 src/score/lib/src/i18n/index.ts create mode 100644 src/score/lib/src/i18n/pt_BR.ts create mode 100644 src/score/lib/src/i18n/zh_CN.ts create mode 100644 src/score/lib/src/score.html create mode 100644 src/score/lib/src/score.less create mode 100644 src/scroll/lib/index.ts create mode 100644 src/scroll/lib/ng-package.json create mode 100644 src/scroll/lib/package.json create mode 100644 src/scroll/lib/project.json create mode 100644 src/scroll/lib/src/TiScrollDirective.ts create mode 100644 src/scroll/lib/src/TiScrollModule.ts create mode 100644 src/searchbox/demo/karma.conf.js create mode 100644 src/searchbox/demo/project.json create mode 100644 src/searchbox/demo/src/app/AppComponent.ts create mode 100644 src/searchbox/demo/src/app/AppModule.ts create mode 100644 src/searchbox/demo/src/app/IndexComponent.ts create mode 100644 src/searchbox/demo/src/app/app.html create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxAppendtobodyComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxBasicComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxDisabledComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxEventComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxMaxlengthComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxNotsearchComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxOptionsComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxPanelsizeComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxReactiveComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxSuggestComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxTemplateComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxTestComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxTestModule.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxTrimmedComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxValidComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/SearchboxVirtualscrollComponent.ts create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-appendtobody.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-basic.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-disabled.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-event.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-maxlength.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-notsearch.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-options.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-panelsize.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-reactive.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-suggest.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-template.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-test.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-trimmed.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-valid.html create mode 100644 src/searchbox/demo/src/app/searchbox/searchbox-virtualscroll.html create mode 100644 src/searchbox/demo/src/app/searchbox/webdoc/searchbox-demos.js create mode 100644 src/searchbox/demo/src/app/searchbox/webdoc/searchbox.cn.md create mode 100644 src/searchbox/demo/src/app/searchbox/webdoc/searchbox.en.md create mode 100644 src/searchbox/demo/src/favicon.ico create mode 100644 src/searchbox/demo/src/index.html create mode 100644 src/searchbox/demo/src/main.ts create mode 100644 src/searchbox/demo/test.ts create mode 100644 src/searchbox/demo/tsconfig.app.json create mode 100644 src/searchbox/demo/tsconfig.spec.json create mode 100644 src/searchbox/lib/index.ts create mode 100644 src/searchbox/lib/ng-package.json create mode 100644 src/searchbox/lib/package.json create mode 100644 src/searchbox/lib/project.json create mode 100644 src/searchbox/lib/src/TiSearchboxComponent.ts create mode 100644 src/searchbox/lib/src/TiSearchboxModule.ts create mode 100644 src/searchbox/lib/src/TiSearchboxNotsearchComponent.ts create mode 100644 src/searchbox/lib/src/i18n/TiSearchboxWords.ts create mode 100644 src/searchbox/lib/src/i18n/en_US.ts create mode 100644 src/searchbox/lib/src/i18n/es_US.ts create mode 100644 src/searchbox/lib/src/i18n/fr_FR.ts create mode 100644 src/searchbox/lib/src/i18n/index.ts create mode 100644 src/searchbox/lib/src/i18n/pt_BR.ts create mode 100644 src/searchbox/lib/src/i18n/zh_CN.ts create mode 100644 src/searchbox/lib/src/searchbox-notsearch.less create mode 100644 src/searchbox/lib/src/searchbox.html create mode 100644 src/searchbox/lib/src/searchbox.less create mode 100644 src/select/demo/karma.conf.js create mode 100644 src/select/demo/project.json create mode 100644 src/select/demo/src/app/AppComponent.ts create mode 100644 src/select/demo/src/app/AppModule.ts create mode 100644 src/select/demo/src/app/IndexComponent.ts create mode 100644 src/select/demo/src/app/app.html create mode 100644 src/select/demo/src/app/select/NoEmptyPipe.ts create mode 100644 src/select/demo/src/app/select/SelectAppendtobodyComponent.ts create mode 100644 src/select/demo/src/app/select/SelectBasicComponent.ts create mode 100644 src/select/demo/src/app/select/SelectBeforesearchComponent.ts create mode 100644 src/select/demo/src/app/select/SelectBeforesearchTestComponent.ts create mode 100644 src/select/demo/src/app/select/SelectChangeSelectallComponent.ts create mode 100644 src/select/demo/src/app/select/SelectClearableComponent.ts create mode 100644 src/select/demo/src/app/select/SelectDisabledComponent.ts create mode 100644 src/select/demo/src/app/select/SelectDisabledfocusComponent.ts create mode 100644 src/select/demo/src/app/select/SelectEnumComponent.ts create mode 100644 src/select/demo/src/app/select/SelectEventComponent.ts create mode 100644 src/select/demo/src/app/select/SelectFocusComponent.ts create mode 100644 src/select/demo/src/app/select/SelectGroupComponent.ts create mode 100644 src/select/demo/src/app/select/SelectIdComponent.ts create mode 100644 src/select/demo/src/app/select/SelectIdkeyComponent.ts create mode 100644 src/select/demo/src/app/select/SelectInputComponent.ts create mode 100644 src/select/demo/src/app/select/SelectLabelkeyComponent.ts create mode 100644 src/select/demo/src/app/select/SelectLazyComponent.ts create mode 100644 src/select/demo/src/app/select/SelectLeakComponent.ts create mode 100644 src/select/demo/src/app/select/SelectLoadComponent.ts create mode 100644 src/select/demo/src/app/select/SelectManyComponent.ts create mode 100644 src/select/demo/src/app/select/SelectMaxlineComponent.ts create mode 100644 src/select/demo/src/app/select/SelectMuchComponent.ts create mode 100644 src/select/demo/src/app/select/SelectMultiComponent.ts create mode 100644 src/select/demo/src/app/select/SelectNoborderComponent.ts create mode 100644 src/select/demo/src/app/select/SelectNodataComponent.ts create mode 100644 src/select/demo/src/app/select/SelectNoemptyComponent.ts create mode 100644 src/select/demo/src/app/select/SelectNullComponent.ts create mode 100644 src/select/demo/src/app/select/SelectPaginBeforesearchComponent.ts create mode 100644 src/select/demo/src/app/select/SelectPaginationComponent.ts create mode 100644 src/select/demo/src/app/select/SelectPanelComponent.ts create mode 100644 src/select/demo/src/app/select/SelectReservesearchwordComponent.ts create mode 100644 src/select/demo/src/app/select/SelectScrollLoadComponent.ts create mode 100644 src/select/demo/src/app/select/SelectSearchComponent.ts create mode 100644 src/select/demo/src/app/select/SelectSearchkeysComponent.ts create mode 100644 src/select/demo/src/app/select/SelectSelectallComponent.ts create mode 100644 src/select/demo/src/app/select/SelectShowselectednumberComponent.ts create mode 100644 src/select/demo/src/app/select/SelectSmallComponent.ts create mode 100644 src/select/demo/src/app/select/SelectTagComponent.ts create mode 100644 src/select/demo/src/app/select/SelectTemplateComponent.ts create mode 100644 src/select/demo/src/app/select/SelectTestModule.ts create mode 100644 src/select/demo/src/app/select/SelectTipComponent.ts create mode 100644 src/select/demo/src/app/select/SelectTiscrollComponent.ts create mode 100644 src/select/demo/src/app/select/SelectTworowComponent.ts create mode 100644 src/select/demo/src/app/select/SelectValidComponent.ts create mode 100644 src/select/demo/src/app/select/SelectValidGroupComponent.ts create mode 100644 src/select/demo/src/app/select/SelectValuekeyComponent.ts create mode 100644 src/select/demo/src/app/select/SelectValuekeyTestComponent.ts create mode 100644 src/select/demo/src/app/select/SelectVirtualscrollComponent.ts create mode 100644 src/select/demo/src/app/select/SelectVirtualscrollMultiComponent.ts create mode 100644 src/select/demo/src/app/select/select-appendtobody.html create mode 100644 src/select/demo/src/app/select/select-basic.html create mode 100644 src/select/demo/src/app/select/select-beforesearch-test.html create mode 100644 src/select/demo/src/app/select/select-beforesearch.html create mode 100644 src/select/demo/src/app/select/select-change-selectall.html create mode 100644 src/select/demo/src/app/select/select-clearable.html create mode 100644 src/select/demo/src/app/select/select-disabled.html create mode 100644 src/select/demo/src/app/select/select-disabledfocus.html create mode 100644 src/select/demo/src/app/select/select-enum.html create mode 100644 src/select/demo/src/app/select/select-event.html create mode 100644 src/select/demo/src/app/select/select-focus.html create mode 100644 src/select/demo/src/app/select/select-group.html create mode 100644 src/select/demo/src/app/select/select-id.html create mode 100644 src/select/demo/src/app/select/select-idkey.html create mode 100644 src/select/demo/src/app/select/select-input.html create mode 100644 src/select/demo/src/app/select/select-labelkey.html create mode 100644 src/select/demo/src/app/select/select-lazy.html create mode 100644 src/select/demo/src/app/select/select-leak.html create mode 100644 src/select/demo/src/app/select/select-load.html create mode 100644 src/select/demo/src/app/select/select-many.html create mode 100644 src/select/demo/src/app/select/select-maxline.html create mode 100644 src/select/demo/src/app/select/select-much.html create mode 100644 src/select/demo/src/app/select/select-multi.html create mode 100644 src/select/demo/src/app/select/select-noborder.html create mode 100644 src/select/demo/src/app/select/select-nodata.html create mode 100644 src/select/demo/src/app/select/select-noempty.html create mode 100644 src/select/demo/src/app/select/select-null.html create mode 100644 src/select/demo/src/app/select/select-pagin-beforesearch.html create mode 100644 src/select/demo/src/app/select/select-pagination.html create mode 100644 src/select/demo/src/app/select/select-panel.html create mode 100644 src/select/demo/src/app/select/select-reservesearchword.html create mode 100644 src/select/demo/src/app/select/select-scroll-load.html create mode 100644 src/select/demo/src/app/select/select-search.html create mode 100644 src/select/demo/src/app/select/select-searchkeys.html create mode 100644 src/select/demo/src/app/select/select-selectall.html create mode 100644 src/select/demo/src/app/select/select-showselectednumber.html create mode 100644 src/select/demo/src/app/select/select-small.html create mode 100644 src/select/demo/src/app/select/select-tag.html create mode 100644 src/select/demo/src/app/select/select-tag.less create mode 100644 src/select/demo/src/app/select/select-template.html create mode 100644 src/select/demo/src/app/select/select-tip.html create mode 100644 src/select/demo/src/app/select/select-tiscroll.html create mode 100644 src/select/demo/src/app/select/select-tworow.html create mode 100644 src/select/demo/src/app/select/select-valid.html create mode 100644 src/select/demo/src/app/select/select-validgroup.html create mode 100644 src/select/demo/src/app/select/select-valuekey-test.html create mode 100644 src/select/demo/src/app/select/select-valuekey.html create mode 100644 src/select/demo/src/app/select/select-virtualscroll-multi.html create mode 100644 src/select/demo/src/app/select/select-virtualscroll.html create mode 100644 src/select/demo/src/app/select/webdoc/select-demos.js create mode 100644 src/select/demo/src/app/select/webdoc/select.cn.md create mode 100644 src/select/demo/src/app/select/webdoc/select.en.md create mode 100644 "src/select/demo/src/app/select/\346\265\213\350\257\225\347\224\250\344\276\213.txt" create mode 100644 src/select/demo/src/favicon.ico create mode 100644 src/select/demo/src/index.html create mode 100644 src/select/demo/src/main.ts create mode 100644 src/select/demo/test.ts create mode 100644 src/select/demo/tsconfig.app.json create mode 100644 src/select/demo/tsconfig.spec.json create mode 100644 src/select/lib/index.ts create mode 100644 src/select/lib/ng-package.json create mode 100644 src/select/lib/package.json create mode 100644 src/select/lib/project.json create mode 100644 src/select/lib/src/TiSelectComponent.ts create mode 100644 src/select/lib/src/TiSelectModule.ts create mode 100644 src/select/lib/src/select-small.less create mode 100644 src/select/lib/src/select.html create mode 100644 src/select/lib/src/select.less create mode 100644 src/selectgroup/demo/project.json create mode 100644 src/selectgroup/demo/src/app/AppComponent.ts create mode 100644 src/selectgroup/demo/src/app/AppModule.ts create mode 100644 src/selectgroup/demo/src/app/IndexComponent.ts create mode 100644 src/selectgroup/demo/src/app/app.html create mode 100644 src/selectgroup/demo/src/app/selectgroup/SelectgroupBasicComponent.ts create mode 100644 src/selectgroup/demo/src/app/selectgroup/SelectgroupMultipleComponent.ts create mode 100644 src/selectgroup/demo/src/app/selectgroup/SelectgroupSelectComponent.ts create mode 100644 src/selectgroup/demo/src/app/selectgroup/SelectgroupTemplateComponent.ts create mode 100644 src/selectgroup/demo/src/app/selectgroup/SelectgroupTestModule.ts create mode 100644 src/selectgroup/demo/src/app/selectgroup/SelectgroupValuekeyComponent.ts create mode 100644 src/selectgroup/demo/src/app/selectgroup/selectgroup-basic.html create mode 100644 src/selectgroup/demo/src/app/selectgroup/selectgroup-multiple.html create mode 100644 src/selectgroup/demo/src/app/selectgroup/selectgroup-select.html create mode 100644 src/selectgroup/demo/src/app/selectgroup/selectgroup-template.html create mode 100644 src/selectgroup/demo/src/app/selectgroup/selectgroup-valuekey.html create mode 100644 src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup-demos.js create mode 100644 src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.cn.md create mode 100644 src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.en.md create mode 100644 src/selectgroup/demo/src/favicon.ico create mode 100644 src/selectgroup/demo/src/index.html create mode 100644 src/selectgroup/demo/src/main.ts create mode 100644 src/selectgroup/demo/tsconfig.app.json create mode 100644 src/selectgroup/lib/index.ts create mode 100644 src/selectgroup/lib/ng-package.json create mode 100644 src/selectgroup/lib/package.json create mode 100644 src/selectgroup/lib/project.json create mode 100644 src/selectgroup/lib/src/TiSelectgroupComponent.ts create mode 100644 src/selectgroup/lib/src/TiSelectgroupModule.ts create mode 100644 src/selectgroup/lib/src/TiSelectitemComponent.ts create mode 100644 src/selectgroup/lib/src/selectgroup.html create mode 100644 src/selectgroup/lib/src/selectgroup.less create mode 100644 src/selectgroup/lib/src/selectitem.html create mode 100644 src/skeleton/demo/karma.conf.js create mode 100644 src/skeleton/demo/project.json create mode 100644 src/skeleton/demo/src/app/AppComponent.ts create mode 100644 src/skeleton/demo/src/app/AppModule.ts create mode 100644 src/skeleton/demo/src/app/IndexComponent.ts create mode 100644 src/skeleton/demo/src/app/app.html create mode 100644 src/skeleton/demo/src/app/skeleton/SkeletonPageComponent.ts create mode 100644 src/skeleton/demo/src/app/skeleton/SkeletonTestModule.ts create mode 100644 src/skeleton/demo/src/app/skeleton/SkeletonTitleComponent.ts create mode 100644 src/skeleton/demo/src/app/skeleton/SkeletonTypeComponent.ts create mode 100644 src/skeleton/demo/src/app/skeleton/skeleton-page.html create mode 100644 src/skeleton/demo/src/app/skeleton/skeleton-page.less create mode 100644 src/skeleton/demo/src/app/skeleton/skeleton-title.html create mode 100644 src/skeleton/demo/src/app/skeleton/skeleton-type.html create mode 100644 src/skeleton/demo/src/app/skeleton/skeleton.less create mode 100644 src/skeleton/demo/src/app/skeleton/webdoc/skeleton-demos.js create mode 100644 src/skeleton/demo/src/app/skeleton/webdoc/skeleton.cn.md create mode 100644 src/skeleton/demo/src/app/skeleton/webdoc/skeleton.en.md create mode 100644 src/skeleton/demo/src/favicon.ico create mode 100644 src/skeleton/demo/src/index.html create mode 100644 src/skeleton/demo/src/main.ts create mode 100644 src/skeleton/demo/test.ts create mode 100644 src/skeleton/demo/tsconfig.app.json create mode 100644 src/skeleton/demo/tsconfig.spec.json create mode 100644 src/skeleton/lib/index.ts create mode 100644 src/skeleton/lib/ng-package.json create mode 100644 src/skeleton/lib/package.json create mode 100644 src/skeleton/lib/project.json create mode 100644 src/skeleton/lib/src/TiSkeletonComponent.ts create mode 100644 src/skeleton/lib/src/TiSkeletonModule.ts create mode 100644 src/skeleton/lib/src/skeleton.html create mode 100644 src/skeleton/lib/src/skeleton.less create mode 100644 src/slider/demo/karma.conf.js create mode 100644 src/slider/demo/project.json create mode 100644 src/slider/demo/src/app/AppComponent.ts create mode 100644 src/slider/demo/src/app/AppModule.ts create mode 100644 src/slider/demo/src/app/IndexComponent.ts create mode 100644 src/slider/demo/src/app/app.html create mode 100644 src/slider/demo/src/app/slider/SliderEventComponent.ts create mode 100644 src/slider/demo/src/app/slider/SliderFormcontrolComponent.ts create mode 100644 src/slider/demo/src/app/slider/SliderHiddenComponent.ts create mode 100644 src/slider/demo/src/app/slider/SliderLimitsComponent.ts create mode 100644 src/slider/demo/src/app/slider/SliderRatiosComponent.ts create mode 100644 src/slider/demo/src/app/slider/SliderScalesComponent.ts create mode 100644 src/slider/demo/src/app/slider/SliderTemplateComponent.ts create mode 100644 src/slider/demo/src/app/slider/SliderTestModule.ts create mode 100644 src/slider/demo/src/app/slider/SliderTipComponent.ts create mode 100644 src/slider/demo/src/app/slider/slider-event.html create mode 100644 src/slider/demo/src/app/slider/slider-formcontrol.html create mode 100644 src/slider/demo/src/app/slider/slider-hidden.html create mode 100644 src/slider/demo/src/app/slider/slider-limits.html create mode 100644 src/slider/demo/src/app/slider/slider-ratios.html create mode 100644 src/slider/demo/src/app/slider/slider-scales.html create mode 100644 src/slider/demo/src/app/slider/slider-template.html create mode 100644 src/slider/demo/src/app/slider/slider-tip.html create mode 100644 src/slider/demo/src/app/slider/webdoc/slider-demos.js create mode 100644 src/slider/demo/src/app/slider/webdoc/slider.cn.md create mode 100644 src/slider/demo/src/app/slider/webdoc/slider.en.md create mode 100644 src/slider/demo/src/favicon.ico create mode 100644 src/slider/demo/src/index.html create mode 100644 src/slider/demo/src/main.ts create mode 100644 src/slider/demo/test.ts create mode 100644 src/slider/demo/tsconfig.app.json create mode 100644 src/slider/demo/tsconfig.spec.json create mode 100644 src/slider/lib/index.ts create mode 100644 src/slider/lib/ng-package.json create mode 100644 src/slider/lib/package.json create mode 100644 src/slider/lib/project.json create mode 100644 src/slider/lib/src/TiSliderComponent.ts create mode 100644 src/slider/lib/src/TiSliderModule.ts create mode 100644 src/slider/lib/src/slider.html create mode 100644 src/slider/lib/src/slider.less create mode 100644 src/spinner/demo/karma.conf.js create mode 100644 src/spinner/demo/project.json create mode 100644 src/spinner/demo/src/app/AppComponent.ts create mode 100644 src/spinner/demo/src/app/AppModule.ts create mode 100644 src/spinner/demo/src/app/IndexComponent.ts create mode 100644 src/spinner/demo/src/app/app.html create mode 100644 src/spinner/demo/src/app/spinner/SpinnerBasicComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerBasicTestComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerCorrectableComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerDisabledComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerEventComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerFormatComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerIdComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerLoadComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerLocaleableComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerMaxMinComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerMaxlengthComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerStepComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerStepfnComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerTestModule.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerTipComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerTipTestComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerValidationComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/SpinnerValidationTestComponent.ts create mode 100644 src/spinner/demo/src/app/spinner/spinner-basic-test.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-basic.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-correctable.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-disabled.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-event.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-format.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-load.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-localeable.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-max-min.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-maxlength.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-step.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-stepfn.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-tip-test.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-tip.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-validation-test.html create mode 100644 src/spinner/demo/src/app/spinner/spinner-validation.html create mode 100644 src/spinner/demo/src/app/spinner/spinnerTest.less create mode 100644 src/spinner/demo/src/app/spinner/webdoc/spinner-demos.js create mode 100644 src/spinner/demo/src/app/spinner/webdoc/spinner.cn.md create mode 100644 src/spinner/demo/src/app/spinner/webdoc/spinner.en.md create mode 100644 src/spinner/demo/src/favicon.ico create mode 100644 src/spinner/demo/src/index.html create mode 100644 src/spinner/demo/src/main.ts create mode 100644 src/spinner/demo/test.ts create mode 100644 src/spinner/demo/tsconfig.app.json create mode 100644 src/spinner/demo/tsconfig.spec.json create mode 100644 src/spinner/lib/index.ts create mode 100644 src/spinner/lib/ng-package.json create mode 100644 src/spinner/lib/package.json create mode 100644 src/spinner/lib/project.json create mode 100644 src/spinner/lib/src/TiSpinnerComponent.ts create mode 100644 src/spinner/lib/src/TiSpinnerModule.ts create mode 100644 src/spinner/lib/src/i18n/TiSpinnerWords.ts create mode 100644 src/spinner/lib/src/i18n/en_US.ts create mode 100644 src/spinner/lib/src/i18n/es_US.ts create mode 100644 src/spinner/lib/src/i18n/fr_FR.ts create mode 100644 src/spinner/lib/src/i18n/index.ts create mode 100644 src/spinner/lib/src/i18n/pt_BR.ts create mode 100644 src/spinner/lib/src/i18n/zh_CN.ts create mode 100644 src/spinner/lib/src/spinner.html create mode 100644 src/spinner/lib/src/spinner.less create mode 100644 src/steps/demo/karma.conf.js create mode 100644 src/steps/demo/project.json create mode 100644 src/steps/demo/src/app/AppComponent.ts create mode 100644 src/steps/demo/src/app/AppModule.ts create mode 100644 src/steps/demo/src/app/IndexComponent.ts create mode 100644 src/steps/demo/src/app/app.html create mode 100644 src/steps/demo/src/app/steps/StepsActiveComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsAdaptiveComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsAdaptiveTestComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsBaseComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsBeforeComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsClickableComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsEventsComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsLabelComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsMaxwidthComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsTemplateComponent.ts create mode 100644 src/steps/demo/src/app/steps/StepsTestModule.ts create mode 100644 src/steps/demo/src/app/steps/steps-active.html create mode 100644 src/steps/demo/src/app/steps/steps-adaptive-test.html create mode 100644 src/steps/demo/src/app/steps/steps-adaptive.html create mode 100644 src/steps/demo/src/app/steps/steps-base.html create mode 100644 src/steps/demo/src/app/steps/steps-before.html create mode 100644 src/steps/demo/src/app/steps/steps-clickable.html create mode 100644 src/steps/demo/src/app/steps/steps-events.html create mode 100644 src/steps/demo/src/app/steps/steps-label.html create mode 100644 src/steps/demo/src/app/steps/steps-maxwidth.html create mode 100644 src/steps/demo/src/app/steps/steps-template.html create mode 100644 src/steps/demo/src/app/steps/webdoc/steps-demos.js create mode 100644 src/steps/demo/src/app/steps/webdoc/steps.cn.md create mode 100644 src/steps/demo/src/app/steps/webdoc/steps.en.md create mode 100644 src/steps/demo/src/favicon.ico create mode 100644 src/steps/demo/src/index.html create mode 100644 src/steps/demo/src/main.ts create mode 100644 src/steps/demo/test.ts create mode 100644 src/steps/demo/tsconfig.app.json create mode 100644 src/steps/demo/tsconfig.spec.json create mode 100644 src/steps/lib/index.ts create mode 100644 src/steps/lib/ng-package.json create mode 100644 src/steps/lib/package.json create mode 100644 src/steps/lib/project.json create mode 100644 src/steps/lib/src/TiStepsComponent.ts create mode 100644 src/steps/lib/src/TiStepsModule.ts create mode 100644 src/steps/lib/src/steps.html create mode 100644 src/steps/lib/src/steps.less create mode 100644 src/subtitle/demo/karma.conf.js create mode 100644 src/subtitle/demo/project.json create mode 100644 src/subtitle/demo/src/app/AppComponent.ts create mode 100644 src/subtitle/demo/src/app/AppModule.ts create mode 100644 src/subtitle/demo/src/app/IndexComponent.ts create mode 100644 src/subtitle/demo/src/app/app.html create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleBasicComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleBeforeSearchComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleDarkComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleEventComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleIdkeyComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleItemsComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleMaxwidthComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitlePanelwidthComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleRouteComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleScrollLoadComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleSearchableComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleTargetComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleTestModule.ts create mode 100644 src/subtitle/demo/src/app/subtitle/SubtitleTipPositionComponent.ts create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-basic.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-before-search.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-dark.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-event.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-idkey.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-items.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-maxwidth.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-panelwidth.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-route.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-scroll-load.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-searchable.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-target.html create mode 100644 src/subtitle/demo/src/app/subtitle/subtitle-tip-position.html create mode 100644 src/subtitle/demo/src/app/subtitle/webdoc/subtitle-demos.js create mode 100644 src/subtitle/demo/src/app/subtitle/webdoc/subtitle.cn.md create mode 100644 src/subtitle/demo/src/app/subtitle/webdoc/subtitle.en.md create mode 100644 src/subtitle/demo/src/favicon.ico create mode 100644 src/subtitle/demo/src/index.html create mode 100644 src/subtitle/demo/src/main.ts create mode 100644 src/subtitle/demo/test.ts create mode 100644 src/subtitle/demo/tsconfig.app.json create mode 100644 src/subtitle/demo/tsconfig.spec.json create mode 100644 src/subtitle/lib/index.ts create mode 100644 src/subtitle/lib/ng-package.json create mode 100644 src/subtitle/lib/package.json create mode 100644 src/subtitle/lib/project.json create mode 100644 src/subtitle/lib/src/TiSubtitleComponent.ts create mode 100644 src/subtitle/lib/src/TiSubtitleModule.ts create mode 100644 src/subtitle/lib/src/subtitle.html create mode 100644 src/subtitle/lib/src/subtitle.less create mode 100644 src/swiper/demo/karma.conf.js create mode 100644 src/swiper/demo/project.json create mode 100644 src/swiper/demo/src/app/AppComponent.ts create mode 100644 src/swiper/demo/src/app/AppModule.ts create mode 100644 src/swiper/demo/src/app/IndexComponent.ts create mode 100644 src/swiper/demo/src/app/app.html create mode 100644 src/swiper/demo/src/app/swiper/SwiperActiveindexComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperAutoplayComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperBasicComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperEventsComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperIndicatorpositionComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperLoopComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperShowcardnumComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperShowcardnumTestComponent.ts create mode 100644 src/swiper/demo/src/app/swiper/SwiperTestModule.ts create mode 100644 src/swiper/demo/src/app/swiper/swiper-activeindex.html create mode 100644 src/swiper/demo/src/app/swiper/swiper-autoplay.html create mode 100644 src/swiper/demo/src/app/swiper/swiper-basic.html create mode 100644 src/swiper/demo/src/app/swiper/swiper-events.html create mode 100644 src/swiper/demo/src/app/swiper/swiper-indicatorposition.html create mode 100644 src/swiper/demo/src/app/swiper/swiper-loop.html create mode 100644 src/swiper/demo/src/app/swiper/swiper-showcardnum-test.html create mode 100644 src/swiper/demo/src/app/swiper/swiper-showcardnum.html create mode 100644 src/swiper/demo/src/app/swiper/swiper.less create mode 100644 src/swiper/demo/src/app/swiper/webdoc/swiper-demos.js create mode 100644 src/swiper/demo/src/app/swiper/webdoc/swiper.cn.md create mode 100644 src/swiper/demo/src/app/swiper/webdoc/swiper.en.md create mode 100644 src/swiper/demo/src/favicon.ico create mode 100644 src/swiper/demo/src/index.html create mode 100644 src/swiper/demo/src/main.ts create mode 100644 src/swiper/demo/test.ts create mode 100644 src/swiper/demo/tsconfig.app.json create mode 100644 src/swiper/demo/tsconfig.spec.json create mode 100644 src/swiper/lib/index.ts create mode 100644 src/swiper/lib/ng-package.json create mode 100644 src/swiper/lib/package.json create mode 100644 src/swiper/lib/project.json create mode 100644 src/swiper/lib/src/TiSwiperComponent.ts create mode 100644 src/swiper/lib/src/TiSwiperModule.ts create mode 100644 src/swiper/lib/src/TiSwipercardComponent.ts create mode 100644 src/swiper/lib/src/swiper.html create mode 100644 src/swiper/lib/src/swiper.less create mode 100644 src/swiper/lib/src/swipercard.less create mode 100644 src/switch/demo/karma.conf.js create mode 100644 src/switch/demo/project.json create mode 100644 src/switch/demo/src/app/AppComponent.ts create mode 100644 src/switch/demo/src/app/AppModule.ts create mode 100644 src/switch/demo/src/app/IndexComponent.ts create mode 100644 src/switch/demo/src/app/app.html create mode 100644 src/switch/demo/src/app/switch/SwitchBasicComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchBeforeComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchDisabledComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchEventComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchExplanationComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchFocusComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchIdComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchLoadComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchTemplateComponent.ts create mode 100644 src/switch/demo/src/app/switch/SwitchTestModule.ts create mode 100644 src/switch/demo/src/app/switch/switch-basic.html create mode 100644 src/switch/demo/src/app/switch/switch-before.html create mode 100644 src/switch/demo/src/app/switch/switch-disabled.html create mode 100644 src/switch/demo/src/app/switch/switch-event.html create mode 100644 src/switch/demo/src/app/switch/switch-explanation.html create mode 100644 src/switch/demo/src/app/switch/switch-focus.html create mode 100644 src/switch/demo/src/app/switch/switch-id.html create mode 100644 src/switch/demo/src/app/switch/switch-load.html create mode 100644 src/switch/demo/src/app/switch/switch-template.html create mode 100644 src/switch/demo/src/app/switch/webdoc/switch-demos.js create mode 100644 src/switch/demo/src/app/switch/webdoc/switch.cn.md create mode 100644 src/switch/demo/src/app/switch/webdoc/switch.en.md create mode 100644 src/switch/demo/src/favicon.ico create mode 100644 src/switch/demo/src/index.html create mode 100644 src/switch/demo/src/main.ts create mode 100644 src/switch/demo/test.ts create mode 100644 src/switch/demo/tsconfig.app.json create mode 100644 src/switch/demo/tsconfig.spec.json create mode 100644 src/switch/lib/index.ts create mode 100644 src/switch/lib/ng-package.json create mode 100644 src/switch/lib/package.json create mode 100644 src/switch/lib/project.json create mode 100644 src/switch/lib/src/TiSwitchComponent.ts create mode 100644 src/switch/lib/src/TiSwitchModule.ts create mode 100644 src/switch/lib/src/switch.html create mode 100644 src/switch/lib/src/switch.less create mode 100644 src/tab/demo/karma.conf.js create mode 100644 src/tab/demo/project.json create mode 100644 src/tab/demo/src/app/AppComponent.ts create mode 100644 src/tab/demo/src/app/AppModule.ts create mode 100644 src/tab/demo/src/app/IndexComponent.ts create mode 100644 src/tab/demo/src/app/app.html create mode 100644 src/tab/demo/src/app/tab/TabBasicComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabBeforeactivechangeComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabContentCompComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabCustomHeadComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabDarkComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabDefaultTestComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabLazyLoadComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabLevel2Component.ts create mode 100644 src/tab/demo/src/app/tab/TabLevel2TestComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabOverflowComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabRouteComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabScrollComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabSmallComponent.ts create mode 100644 src/tab/demo/src/app/tab/TabTestModule.ts create mode 100644 src/tab/demo/src/app/tab/tab-basic.html create mode 100644 src/tab/demo/src/app/tab/tab-beforeactivechange.html create mode 100644 src/tab/demo/src/app/tab/tab-content-comp.html create mode 100644 src/tab/demo/src/app/tab/tab-custom-head.html create mode 100644 src/tab/demo/src/app/tab/tab-dark.html create mode 100644 src/tab/demo/src/app/tab/tab-default-test.html create mode 100644 src/tab/demo/src/app/tab/tab-lazy-load.html create mode 100644 src/tab/demo/src/app/tab/tab-level2-test.html create mode 100644 src/tab/demo/src/app/tab/tab-level2.html create mode 100644 src/tab/demo/src/app/tab/tab-overflow.html create mode 100644 src/tab/demo/src/app/tab/tab-route.html create mode 100644 src/tab/demo/src/app/tab/tab-scroll.html create mode 100644 src/tab/demo/src/app/tab/tab-small.html create mode 100644 src/tab/demo/src/app/tab/webdoc/tab-demos.js create mode 100644 src/tab/demo/src/app/tab/webdoc/tab.cn.md create mode 100644 src/tab/demo/src/app/tab/webdoc/tab.en.md create mode 100644 src/tab/demo/src/favicon.ico create mode 100644 src/tab/demo/src/index.html create mode 100644 src/tab/demo/src/main.ts create mode 100644 src/tab/demo/test.ts create mode 100644 src/tab/demo/tsconfig.app.json create mode 100644 src/tab/demo/tsconfig.spec.json create mode 100644 src/tab/lib/index.ts create mode 100644 src/tab/lib/ng-package.json create mode 100644 src/tab/lib/package.json create mode 100644 src/tab/lib/project.json create mode 100644 src/tab/lib/src/TiTabComponent.ts create mode 100644 src/tab/lib/src/TiTabHeaderComponent.ts create mode 100644 src/tab/lib/src/TiTabModule.ts create mode 100644 src/tab/lib/src/TiTabsComponent.ts create mode 100644 src/tab/lib/src/tab-head.html create mode 100644 src/tab/lib/src/tab.html create mode 100644 src/tab/lib/src/tab.less create mode 100644 src/tab/lib/src/tabs.html create mode 100644 src/tab/lib/src/tabs.less create mode 100644 src/table/demo/karma.conf.js create mode 100644 src/table/demo/project.json create mode 100644 src/table/demo/src/app/AppComponent.ts create mode 100644 src/table/demo/src/app/AppModule.ts create mode 100644 src/table/demo/src/app/IndexComponent.ts create mode 100644 src/table/demo/src/app/app.html create mode 100644 src/table/demo/src/app/table/TableActionmenuComponent.ts create mode 100644 src/table/demo/src/app/table/TableBasicComponent.ts create mode 100644 src/table/demo/src/app/table/TableBasicTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableCellTipComponent.ts create mode 100644 src/table/demo/src/app/table/TableCelliconsColsresizableComponent.ts create mode 100644 src/table/demo/src/app/table/TableCheckboxComponent.ts create mode 100644 src/table/demo/src/app/table/TableCheckboxPaginationComponent.ts create mode 100644 src/table/demo/src/app/table/TableCheckboxPaginationHeadmenuComponent.ts create mode 100644 src/table/demo/src/app/table/TableColAlignComponent.ts create mode 100644 src/table/demo/src/app/table/TableColalignSortResizableTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsResizableComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsToggleComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsToggleDetailsComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsToggleTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsresizableBasicComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsresizableColstoggleComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsresizableColstoggleFixedheadComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsresizableLoadfailComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsresizableSortComponent.ts create mode 100644 src/table/demo/src/app/table/TableColsresizableSortHeadfilterComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnFixedComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedCheckboxComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedColstoggleComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedEditrowComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedFixedheadColsresizablePaginationComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedHeadfixedComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedLeftmenuComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedNodataComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedPaginationComponent.ts create mode 100644 src/table/demo/src/app/table/TableColumnfixedResizableComponent.ts create mode 100644 src/table/demo/src/app/table/TableComprehensiveComponent.ts create mode 100644 src/table/demo/src/app/table/TableDetailsCloseotherdetailsComponent.ts create mode 100644 src/table/demo/src/app/table/TableDetailsComponent.ts create mode 100644 src/table/demo/src/app/table/TableDetailsNesttableComponent.ts create mode 100644 src/table/demo/src/app/table/TableDetailsPaginationComponent.ts create mode 100644 src/table/demo/src/app/table/TableDynamicDetailsComponent.ts create mode 100644 src/table/demo/src/app/table/TableEditallComponent.ts create mode 100644 src/table/demo/src/app/table/TableEditallTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableEditrowComponent.ts create mode 100644 src/table/demo/src/app/table/TableEditrowTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableFilterComponent.ts create mode 100644 src/table/demo/src/app/table/TableFilterStrictComponent.ts create mode 100644 src/table/demo/src/app/table/TableFixedHeadColsResizableComponent.ts create mode 100644 src/table/demo/src/app/table/TableFixedHeadComponent.ts create mode 100644 src/table/demo/src/app/table/TableFixedHeadInAccordionComponent.ts create mode 100644 src/table/demo/src/app/table/TableFixedHeadNodataComponent.ts create mode 100644 src/table/demo/src/app/table/TableFixedHeadPaginationDetailsComponent.ts create mode 100644 src/table/demo/src/app/table/TableFixedheadColsresizablePaginationDetailsComponent.ts create mode 100644 src/table/demo/src/app/table/TableFixheadScrollComponent.ts create mode 100644 src/table/demo/src/app/table/TableGroupComponent.ts create mode 100644 src/table/demo/src/app/table/TableGuideComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterDatetimeComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterDatetimeTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterMultiComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterMultiValuekeyComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterValuekeyComponent.ts create mode 100644 src/table/demo/src/app/table/TableHeadFilterVirtualscrollComponent.ts create mode 100644 src/table/demo/src/app/table/TableLoadFailComponent.ts create mode 100644 src/table/demo/src/app/table/TableNodataComponent.ts create mode 100644 src/table/demo/src/app/table/TableNodataSimpleComponent.ts create mode 100644 src/table/demo/src/app/table/TableNodataTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableOverflowLinkComponent.ts create mode 100644 src/table/demo/src/app/table/TablePagiWithFilterComponent.ts create mode 100644 src/table/demo/src/app/table/TablePaginationComponent.ts create mode 100644 src/table/demo/src/app/table/TableRadioComponent.ts create mode 100644 src/table/demo/src/app/table/TableRadioTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableRowDrag2Component.ts create mode 100644 src/table/demo/src/app/table/TableRowspanComponent.ts create mode 100644 src/table/demo/src/app/table/TableSearchComponent.ts create mode 100644 src/table/demo/src/app/table/TableServerPagiComponent.ts create mode 100644 src/table/demo/src/app/table/TableServerPagiSearchSortComponent.ts create mode 100644 src/table/demo/src/app/table/TableSmallComponent.ts create mode 100644 src/table/demo/src/app/table/TableSoldoutComponent.ts create mode 100644 src/table/demo/src/app/table/TableSortBasicComponent.ts create mode 100644 src/table/demo/src/app/table/TableSortComparefnComponent.ts create mode 100644 src/table/demo/src/app/table/TableSortComparefnLocaleComponent.ts create mode 100644 src/table/demo/src/app/table/TableSortComponent.ts create mode 100644 src/table/demo/src/app/table/TableSortDetailsComponent.ts create mode 100644 src/table/demo/src/app/table/TableSortResetComponent.ts create mode 100644 src/table/demo/src/app/table/TableSortTestComponent.ts create mode 100644 src/table/demo/src/app/table/TableStorageComponent.ts create mode 100644 src/table/demo/src/app/table/TableStorageConfigComponent.ts create mode 100644 src/table/demo/src/app/table/TableStorageFilterComponent.ts create mode 100644 src/table/demo/src/app/table/TableStorageServeComponent.ts create mode 100644 src/table/demo/src/app/table/TableTestModule.ts create mode 100644 src/table/demo/src/app/table/TableTreeComponent.ts create mode 100644 src/table/demo/src/app/table/TableTreeMulitiselectComponent.ts create mode 100644 src/table/demo/src/app/table/TableTreeUnknowdeepthComponent.ts create mode 100644 src/table/demo/src/app/table/TableVirtualscrollBasicComponent.ts create mode 100644 src/table/demo/src/app/table/TableVirtualscrollComponent.ts create mode 100644 src/table/demo/src/app/table/TableVirtualscrollComprehensiveComponent.ts create mode 100644 src/table/demo/src/app/table/TableVirtualscrollSizesComponent.ts create mode 100644 src/table/demo/src/app/table/TableVirtualscrollTreeComponent.ts create mode 100644 src/table/demo/src/app/table/table-actionmenu.html create mode 100644 src/table/demo/src/app/table/table-basic-test.html create mode 100644 src/table/demo/src/app/table/table-basic.html create mode 100644 src/table/demo/src/app/table/table-cell-tip.html create mode 100644 src/table/demo/src/app/table/table-cellicons-colsresizable.html create mode 100644 src/table/demo/src/app/table/table-checkbox-pagination-headmenu.html create mode 100644 src/table/demo/src/app/table/table-checkbox-pagination.html create mode 100644 src/table/demo/src/app/table/table-checkbox.html create mode 100644 src/table/demo/src/app/table/table-col-align.html create mode 100644 src/table/demo/src/app/table/table-colalign-sort-resizable-test.html create mode 100644 src/table/demo/src/app/table/table-cols-resizable.html create mode 100644 src/table/demo/src/app/table/table-cols-toggle-details.html create mode 100644 src/table/demo/src/app/table/table-cols-toggle-test.html create mode 100644 src/table/demo/src/app/table/table-cols-toggle.html create mode 100644 src/table/demo/src/app/table/table-colsresizable-basic.html create mode 100644 src/table/demo/src/app/table/table-colsresizable-colstoggle-fixedhead.html create mode 100644 src/table/demo/src/app/table/table-colsresizable-colstoggle.html create mode 100644 src/table/demo/src/app/table/table-colsresizable-loadfail.html create mode 100644 src/table/demo/src/app/table/table-colsresizable-sort-headfilter.html create mode 100644 src/table/demo/src/app/table/table-colsresizable-sort.html create mode 100644 src/table/demo/src/app/table/table-column-fixed.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-checkbox.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-colstoggle.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-editrow.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-fixedhead-colsresizable-pagination.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-headfixed.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-leftmenu.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-nodata.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-pagination.html create mode 100644 src/table/demo/src/app/table/table-columnfixed-resizable.html create mode 100644 src/table/demo/src/app/table/table-comprehensive.html create mode 100644 src/table/demo/src/app/table/table-details-closeotherdetails.html create mode 100644 src/table/demo/src/app/table/table-details-nesttable.html create mode 100644 src/table/demo/src/app/table/table-details-pagination.html create mode 100644 src/table/demo/src/app/table/table-details.html create mode 100644 src/table/demo/src/app/table/table-dynamic-details.html create mode 100644 src/table/demo/src/app/table/table-editall-test.html create mode 100644 src/table/demo/src/app/table/table-editall.html create mode 100644 src/table/demo/src/app/table/table-editrow-test.html create mode 100644 src/table/demo/src/app/table/table-editrow.html create mode 100644 src/table/demo/src/app/table/table-filter-strict.html create mode 100644 src/table/demo/src/app/table/table-filter.html create mode 100644 src/table/demo/src/app/table/table-fixed-head-cols-resizable.html create mode 100644 src/table/demo/src/app/table/table-fixed-head-in-accordion.html create mode 100644 src/table/demo/src/app/table/table-fixed-head-nodata.html create mode 100644 src/table/demo/src/app/table/table-fixed-head-pagination-details.html create mode 100644 src/table/demo/src/app/table/table-fixed-head.html create mode 100644 src/table/demo/src/app/table/table-fixedhead-colsresizable-pagination-details.html create mode 100644 src/table/demo/src/app/table/table-fixhead-scroll.html create mode 100644 src/table/demo/src/app/table/table-group.html create mode 100644 src/table/demo/src/app/table/table-guide.html create mode 100644 src/table/demo/src/app/table/table-head-filter-datetime-test.html create mode 100644 src/table/demo/src/app/table/table-head-filter-datetime.html create mode 100644 src/table/demo/src/app/table/table-head-filter-multi-valuekey.html create mode 100644 src/table/demo/src/app/table/table-head-filter-multi.html create mode 100644 src/table/demo/src/app/table/table-head-filter-test.html create mode 100644 src/table/demo/src/app/table/table-head-filter-valuekey.html create mode 100644 src/table/demo/src/app/table/table-head-filter-virtualscroll.html create mode 100644 src/table/demo/src/app/table/table-head-filter.html create mode 100644 src/table/demo/src/app/table/table-load-fail.html create mode 100644 src/table/demo/src/app/table/table-nodata-simple.html create mode 100644 src/table/demo/src/app/table/table-nodata-test.html create mode 100644 src/table/demo/src/app/table/table-nodata.html create mode 100644 src/table/demo/src/app/table/table-overflow-link.html create mode 100644 src/table/demo/src/app/table/table-pagi-with-filter.html create mode 100644 src/table/demo/src/app/table/table-pagination.html create mode 100644 src/table/demo/src/app/table/table-radio-test.html create mode 100644 src/table/demo/src/app/table/table-radio.html create mode 100644 src/table/demo/src/app/table/table-row-drag2.html create mode 100644 src/table/demo/src/app/table/table-rowspan.html create mode 100644 src/table/demo/src/app/table/table-search.html create mode 100644 src/table/demo/src/app/table/table-server-pagi-search-sort.html create mode 100644 src/table/demo/src/app/table/table-server-pagi.html create mode 100644 src/table/demo/src/app/table/table-small.html create mode 100644 src/table/demo/src/app/table/table-soldout.html create mode 100644 src/table/demo/src/app/table/table-sort-basic.html create mode 100644 src/table/demo/src/app/table/table-sort-comparefn-locale.html create mode 100644 src/table/demo/src/app/table/table-sort-comparefn.html create mode 100644 src/table/demo/src/app/table/table-sort-details.html create mode 100644 src/table/demo/src/app/table/table-sort-reset.html create mode 100644 src/table/demo/src/app/table/table-sort-test.html create mode 100644 src/table/demo/src/app/table/table-sort.html create mode 100644 src/table/demo/src/app/table/table-storage-config.html create mode 100644 src/table/demo/src/app/table/table-storage-filter.html create mode 100644 src/table/demo/src/app/table/table-storage-serve.html create mode 100644 src/table/demo/src/app/table/table-storage.html create mode 100644 src/table/demo/src/app/table/table-tree-mulitiselect.html create mode 100644 src/table/demo/src/app/table/table-tree-unknowdeepth.html create mode 100644 src/table/demo/src/app/table/table-tree.html create mode 100644 src/table/demo/src/app/table/table-virtualscroll-basic.html create mode 100644 src/table/demo/src/app/table/table-virtualscroll-comprehensive.html create mode 100644 src/table/demo/src/app/table/table-virtualscroll-sizes.html create mode 100644 src/table/demo/src/app/table/table-virtualscroll-tree.html create mode 100644 src/table/demo/src/app/table/table-virtualscroll.html create mode 100644 src/table/demo/src/app/table/tableTest.less create mode 100644 src/table/demo/src/app/table/webdoc/table-demos.js create mode 100644 src/table/demo/src/app/table/webdoc/table.cn.md create mode 100644 src/table/demo/src/app/table/webdoc/table.en.md create mode 100644 src/table/demo/src/favicon.ico create mode 100644 src/table/demo/src/index.html create mode 100644 src/table/demo/src/main.ts create mode 100644 src/table/demo/test.ts create mode 100644 src/table/demo/tsconfig.app.json create mode 100644 src/table/demo/tsconfig.spec.json create mode 100644 src/table/lib/index.ts create mode 100644 src/table/lib/ng-package.json create mode 100644 src/table/lib/package.json create mode 100644 src/table/lib/project.json create mode 100644 src/table/lib/src/TiCellIconsComponent.ts create mode 100644 src/table/lib/src/TiCellTextComponent.ts create mode 100644 src/table/lib/src/TiColClickDirective.ts create mode 100644 src/table/lib/src/TiColsResizableDirective.ts create mode 100644 src/table/lib/src/TiColsToggleComponent.ts create mode 100644 src/table/lib/src/TiColsToggleDropComponent.ts create mode 100644 src/table/lib/src/TiColspanDirective.ts create mode 100644 src/table/lib/src/TiColumnFixedDirective.ts create mode 100644 src/table/lib/src/TiColumnsPipe.ts create mode 100644 src/table/lib/src/TiDetailsIconComponent.ts create mode 100644 src/table/lib/src/TiDetailsTrDirective.ts create mode 100644 src/table/lib/src/TiHeadFilterComponent.ts create mode 100644 src/table/lib/src/TiHeadFilterDropComponent.ts create mode 100644 src/table/lib/src/TiHeadMenuComponent.ts create mode 100644 src/table/lib/src/TiHeadSortComponent.ts create mode 100644 src/table/lib/src/TiTableComponent.ts create mode 100644 src/table/lib/src/TiTableFixedHeadService.ts create mode 100644 src/table/lib/src/TiTableFixedHeadServiceModule.ts create mode 100644 src/table/lib/src/TiTableFixedSizeVirtualScrollDirective.ts create mode 100644 src/table/lib/src/TiTableModule.ts create mode 100644 src/table/lib/src/TiTableVirtualScrollViewportComponent.ts create mode 100644 src/table/lib/src/cell-icons.html create mode 100644 src/table/lib/src/cell-text.html create mode 100644 src/table/lib/src/cols-toggle-drop.html create mode 100644 src/table/lib/src/cols-toggle.html create mode 100644 src/table/lib/src/details-icon.html create mode 100644 src/table/lib/src/head-filter-drop.html create mode 100644 src/table/lib/src/head-filter-drop.less create mode 100644 src/table/lib/src/head-filter.html create mode 100644 src/table/lib/src/head-filter.less create mode 100644 src/table/lib/src/head-menu.html create mode 100644 src/table/lib/src/head-menu.less create mode 100644 src/table/lib/src/head-sort.html create mode 100644 src/table/lib/src/i18n/TiTableWords.ts create mode 100644 src/table/lib/src/i18n/en_US.ts create mode 100644 src/table/lib/src/i18n/es_US.ts create mode 100644 src/table/lib/src/i18n/fr_FR.ts create mode 100644 src/table/lib/src/i18n/index.ts create mode 100644 src/table/lib/src/i18n/pt_BR.ts create mode 100644 src/table/lib/src/i18n/zh_CN.ts create mode 100644 src/table/lib/src/table-nodata-small-nest-resize.less create mode 100644 src/table/lib/src/table-toggle-sort-details.less create mode 100644 src/table/lib/src/table-tree-fix.less create mode 100644 src/table/lib/src/table-virtual-scroll-viewport.html create mode 100644 src/table/lib/src/table-virtual-scroll-viewport.less create mode 100644 src/table/lib/src/table.html create mode 100644 src/table/lib/src/table.less create mode 100644 src/tag/demo/karma.conf.js create mode 100644 src/tag/demo/project.json create mode 100644 src/tag/demo/src/app/AppComponent.ts create mode 100644 src/tag/demo/src/app/AppModule.ts create mode 100644 src/tag/demo/src/app/IndexComponent.ts create mode 100644 src/tag/demo/src/app/app.html create mode 100644 src/tag/demo/src/app/tag/TagBasicComponent.ts create mode 100644 src/tag/demo/src/app/tag/TagDefaultComponent.ts create mode 100644 src/tag/demo/src/app/tag/TagDisabledComponent.ts create mode 100644 src/tag/demo/src/app/tag/TagEditComponent.ts create mode 100644 src/tag/demo/src/app/tag/TagTestModule.ts create mode 100644 src/tag/demo/src/app/tag/tag-basic.html create mode 100644 src/tag/demo/src/app/tag/tag-default.html create mode 100644 src/tag/demo/src/app/tag/tag-disabled.html create mode 100644 src/tag/demo/src/app/tag/tag-edit.html create mode 100644 src/tag/demo/src/app/tag/webdoc/tag-demos.js create mode 100644 src/tag/demo/src/app/tag/webdoc/tag.cn.md create mode 100644 src/tag/demo/src/app/tag/webdoc/tag.en.md create mode 100644 src/tag/demo/src/favicon.ico create mode 100644 src/tag/demo/src/index.html create mode 100644 src/tag/demo/src/main.ts create mode 100644 src/tag/demo/test.ts create mode 100644 src/tag/demo/tsconfig.app.json create mode 100644 src/tag/demo/tsconfig.spec.json create mode 100644 src/tag/lib/index.ts create mode 100644 src/tag/lib/ng-package.json create mode 100644 src/tag/lib/package.json create mode 100644 src/tag/lib/project.json create mode 100644 src/tag/lib/src/TiTagComponent.ts create mode 100644 src/tag/lib/src/TiTagModule.ts create mode 100644 src/tag/lib/src/tag-arrow.less create mode 100644 src/tag/lib/src/tag-rect.less create mode 100644 src/tag/lib/src/tag.html create mode 100644 src/tagsinput/demo/karma.conf.js create mode 100644 src/tagsinput/demo/project.json create mode 100644 src/tagsinput/demo/src/app/AppComponent.ts create mode 100644 src/tagsinput/demo/src/app/AppModule.ts create mode 100644 src/tagsinput/demo/src/app/IndexComponent.ts create mode 100644 src/tagsinput/demo/src/app/app.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsInputTestModule.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputBasicComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputDisabledComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputEventsComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputLabelkeyComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputMaxlengthComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputNullComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputPanelwidthComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputReactiveComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputSeparatorsComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputSuggestionComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputTemplateComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputValidComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/TagsinputValuekeyComponent.ts create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-basic.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-disabled.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-events.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-labelkey.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-maxlength.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-null.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-panelwidth.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-reactive.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-separators.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-suggestion.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-template.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-valid.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/tagsinput-valuekey.html create mode 100644 src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput-demos.js create mode 100644 src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.cn.md create mode 100644 src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.en.md create mode 100644 src/tagsinput/demo/src/favicon.ico create mode 100644 src/tagsinput/demo/src/index.html create mode 100644 src/tagsinput/demo/src/main.ts create mode 100644 src/tagsinput/demo/test.ts create mode 100644 src/tagsinput/demo/tsconfig.app.json create mode 100644 src/tagsinput/demo/tsconfig.spec.json create mode 100644 src/tagsinput/lib/index.ts create mode 100644 src/tagsinput/lib/ng-package.json create mode 100644 src/tagsinput/lib/package.json create mode 100644 src/tagsinput/lib/project.json create mode 100644 src/tagsinput/lib/src/TiTagsInputComponent.ts create mode 100644 src/tagsinput/lib/src/TiTagsInputModule.ts create mode 100644 src/tagsinput/lib/src/tagsinput.html create mode 100644 src/tagsinput/lib/src/tagsinput.less create mode 100644 src/text/demo/karma.conf.js create mode 100644 src/text/demo/project.json create mode 100644 src/text/demo/src/app/AppComponent.ts create mode 100644 src/text/demo/src/app/AppModule.ts create mode 100644 src/text/demo/src/app/IndexComponent.ts create mode 100644 src/text/demo/src/app/app.html create mode 100644 src/text/demo/src/app/text/TextBasicComponent.ts create mode 100644 src/text/demo/src/app/text/TextClearComponent.ts create mode 100644 src/text/demo/src/app/text/TextDisabledComponent.ts create mode 100644 src/text/demo/src/app/text/TextEventsComponent.ts create mode 100644 src/text/demo/src/app/text/TextFocusComponent.ts create mode 100644 src/text/demo/src/app/text/TextMaskinputComponent.ts create mode 100644 src/text/demo/src/app/text/TextNoborderTestComponent.ts create mode 100644 src/text/demo/src/app/text/TextPasswordComponent.ts create mode 100644 src/text/demo/src/app/text/TextPasswordVisibleComponent.ts create mode 100644 src/text/demo/src/app/text/TextReactiveComponent.ts create mode 100644 src/text/demo/src/app/text/TextReadonlyComponent.ts create mode 100644 src/text/demo/src/app/text/TextTestModule.ts create mode 100644 src/text/demo/src/app/text/text-basic.html create mode 100644 src/text/demo/src/app/text/text-clear.html create mode 100644 src/text/demo/src/app/text/text-disabled.html create mode 100644 src/text/demo/src/app/text/text-events.html create mode 100644 src/text/demo/src/app/text/text-focus.html create mode 100644 src/text/demo/src/app/text/text-maskinput.html create mode 100644 src/text/demo/src/app/text/text-noborder-test.html create mode 100644 src/text/demo/src/app/text/text-password-visible.html create mode 100644 src/text/demo/src/app/text/text-password.html create mode 100644 src/text/demo/src/app/text/text-reactive.html create mode 100644 src/text/demo/src/app/text/text-readonly.html create mode 100644 src/text/demo/src/app/text/text.spec.ts create mode 100644 src/text/demo/src/app/text/webdoc/text-demos.js create mode 100644 src/text/demo/src/app/text/webdoc/text.cn.md create mode 100644 src/text/demo/src/app/text/webdoc/text.en.md create mode 100644 src/text/demo/src/favicon.ico create mode 100644 src/text/demo/src/index.html create mode 100644 src/text/demo/src/main.ts create mode 100644 src/text/demo/test.ts create mode 100644 src/text/demo/tsconfig.app.json create mode 100644 src/text/demo/tsconfig.spec.json create mode 100644 src/text/lib/index.ts create mode 100644 src/text/lib/ng-package.json create mode 100644 src/text/lib/package.json create mode 100644 src/text/lib/project.json create mode 100644 src/text/lib/src/TiMaskDirective.ts create mode 100644 src/text/lib/src/TiTextComponent.ts create mode 100644 src/text/lib/src/TiTextModule.ts create mode 100644 src/text/lib/src/clear.svg create mode 100644 src/text/lib/src/invisible.svg create mode 100644 src/text/lib/src/text.less create mode 100644 src/text/lib/src/visible.svg create mode 100644 src/textarea/demo/karma.conf.js create mode 100644 src/textarea/demo/project.json create mode 100644 src/textarea/demo/src/app/AppComponent.ts create mode 100644 src/textarea/demo/src/app/AppModule.ts create mode 100644 src/textarea/demo/src/app/IndexComponent.ts create mode 100644 src/textarea/demo/src/app/app.html create mode 100644 src/textarea/demo/src/app/textarea/TextareaAutofocusComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaDisabledComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaMaxlengthComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaNoneComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaResizeComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaScrollComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaTestModule.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaValidComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/TextareaWidthComponent.ts create mode 100644 src/textarea/demo/src/app/textarea/textarea-autofocus.html create mode 100644 src/textarea/demo/src/app/textarea/textarea-disabled.html create mode 100644 src/textarea/demo/src/app/textarea/textarea-maxlength.html create mode 100644 src/textarea/demo/src/app/textarea/textarea-none.html create mode 100644 src/textarea/demo/src/app/textarea/textarea-resize.html create mode 100644 src/textarea/demo/src/app/textarea/textarea-scroll.html create mode 100644 src/textarea/demo/src/app/textarea/textarea-valid.html create mode 100644 src/textarea/demo/src/app/textarea/textarea-width.html create mode 100644 src/textarea/demo/src/app/textarea/webdoc/textarea-demos.js create mode 100644 src/textarea/demo/src/app/textarea/webdoc/textarea.cn.md create mode 100644 src/textarea/demo/src/app/textarea/webdoc/textarea.en.md create mode 100644 src/textarea/demo/src/favicon.ico create mode 100644 src/textarea/demo/src/index.html create mode 100644 src/textarea/demo/src/main.ts create mode 100644 src/textarea/demo/test.ts create mode 100644 src/textarea/demo/tsconfig.app.json create mode 100644 src/textarea/demo/tsconfig.spec.json create mode 100644 src/textarea/lib/index.ts create mode 100644 src/textarea/lib/ng-package.json create mode 100644 src/textarea/lib/package.json create mode 100644 src/textarea/lib/project.json create mode 100644 src/textarea/lib/src/TiFormatNumPipe.ts create mode 100644 src/textarea/lib/src/TiTextareaComponent.ts create mode 100644 src/textarea/lib/src/TiTextareaModule.ts create mode 100644 src/textarea/lib/src/textarea.html create mode 100644 src/textarea/lib/src/textarea.less create mode 100644 src/themes/README.md create mode 100644 src/themes/basic/base-all.less create mode 100644 src/themes/basic/basic-var.css create mode 100644 src/themes/basic/build.less create mode 100644 src/themes/basic/common.less create mode 100644 src/themes/basic/compnent-container-border.less create mode 100644 src/themes/basic/img/table-loadfail-bg.png create mode 100644 src/themes/basic/img/table-nodata-bg.png create mode 100644 src/themes/basic/img/upload-image-delete.png create mode 100644 src/themes/basic/img/upload-image-error.png create mode 100644 src/themes/basic/img/upload-image-preview.png create mode 100644 src/themes/basic/link-no-decoration.less create mode 100644 src/themes/basic/mixins.less create mode 100644 src/themes/basic/normalize.less create mode 100644 src/themes/package.json create mode 100644 src/themes/project.json create mode 100644 src/themes/theme-blue/basic-var.less create mode 100644 src/themes/theme-blue/build.less create mode 100644 src/themes/theme-default/build.less create mode 100644 src/themes/theme-green/basic-var.less create mode 100644 src/themes/theme-green/build.less create mode 100644 src/themes/theme-purple/basic-var.less create mode 100644 src/themes/theme-purple/build.less create mode 100644 src/themes/theme-red/basic-var.less create mode 100644 src/themes/theme-red/build.less create mode 100644 src/time/demo/karma.conf.js create mode 100644 src/time/demo/project.json create mode 100644 src/time/demo/src/app/AppComponent.ts create mode 100644 src/time/demo/src/app/AppModule.ts create mode 100644 src/time/demo/src/app/IndexComponent.ts create mode 100644 src/time/demo/src/app/app.html create mode 100644 src/time/demo/src/app/time/TimeCleariconComponent.ts create mode 100644 src/time/demo/src/app/time/TimeDisabledComponent.ts create mode 100644 src/time/demo/src/app/time/TimeEventComponent.ts create mode 100644 src/time/demo/src/app/time/TimeFormatComponent.ts create mode 100644 src/time/demo/src/app/time/TimeMaxComponent.ts create mode 100644 src/time/demo/src/app/time/TimeMaxminComponent.ts create mode 100644 src/time/demo/src/app/time/TimeMinComponent.ts create mode 100644 src/time/demo/src/app/time/TimeOptionDisabledComponent.ts create mode 100644 src/time/demo/src/app/time/TimePanelalignComponent.ts create mode 100644 src/time/demo/src/app/time/TimeReactiveComponent.ts create mode 100644 src/time/demo/src/app/time/TimeTestModule.ts create mode 100644 src/time/demo/src/app/time/TimeValidationComponent.ts create mode 100644 src/time/demo/src/app/time/time-clearicon.html create mode 100644 src/time/demo/src/app/time/time-disabled.html create mode 100644 src/time/demo/src/app/time/time-event.html create mode 100644 src/time/demo/src/app/time/time-format.html create mode 100644 src/time/demo/src/app/time/time-max.html create mode 100644 src/time/demo/src/app/time/time-maxmin.html create mode 100644 src/time/demo/src/app/time/time-min.html create mode 100644 src/time/demo/src/app/time/time-option-disabled.html create mode 100644 src/time/demo/src/app/time/time-panelalign.html create mode 100644 src/time/demo/src/app/time/time-reactive.html create mode 100644 src/time/demo/src/app/time/time-validation.html create mode 100644 src/time/demo/src/app/time/webdoc/time-demos.js create mode 100644 src/time/demo/src/app/time/webdoc/time.cn.md create mode 100644 src/time/demo/src/app/time/webdoc/time.en.md create mode 100644 src/time/demo/src/favicon.ico create mode 100644 src/time/demo/src/index.html create mode 100644 src/time/demo/src/main.ts create mode 100644 src/time/demo/test.ts create mode 100644 src/time/demo/tsconfig.app.json create mode 100644 src/time/demo/tsconfig.spec.json create mode 100644 src/time/lib/index.ts create mode 100644 src/time/lib/ng-package.json create mode 100644 src/time/lib/package.json create mode 100644 src/time/lib/project.json create mode 100644 src/time/lib/src/TiTimeComponent.ts create mode 100644 src/time/lib/src/TiTimeModule.ts create mode 100644 src/time/lib/src/i18n/TiTimeWords.ts create mode 100644 src/time/lib/src/i18n/en_US.ts create mode 100644 src/time/lib/src/i18n/es_US.ts create mode 100644 src/time/lib/src/i18n/fr_FR.ts create mode 100644 src/time/lib/src/i18n/index.ts create mode 100644 src/time/lib/src/i18n/pt_BR.ts create mode 100644 src/time/lib/src/i18n/zh_CN.ts create mode 100644 src/time/lib/src/time.html create mode 100644 src/time/lib/src/time.less create mode 100644 src/timeline/demo/karma.conf.js create mode 100644 src/timeline/demo/project.json create mode 100644 src/timeline/demo/src/app/AppComponent.ts create mode 100644 src/timeline/demo/src/app/AppModule.ts create mode 100644 src/timeline/demo/src/app/IndexComponent.ts create mode 100644 src/timeline/demo/src/app/app.html create mode 100644 src/timeline/demo/src/app/timeline/TimelineBasicComponent.ts create mode 100644 src/timeline/demo/src/app/timeline/TimelineDarkComponent.ts create mode 100644 src/timeline/demo/src/app/timeline/TimelineHelptipComponent.ts create mode 100644 src/timeline/demo/src/app/timeline/TimelineMultiComponent.ts create mode 100644 src/timeline/demo/src/app/timeline/TimelineTempleteComponent.ts create mode 100644 src/timeline/demo/src/app/timeline/TimelineTestComponent.ts create mode 100644 src/timeline/demo/src/app/timeline/TimelineTestModule.ts create mode 100644 src/timeline/demo/src/app/timeline/TimelineTypeComponent.ts create mode 100644 src/timeline/demo/src/app/timeline/timeline-basic.html create mode 100644 src/timeline/demo/src/app/timeline/timeline-dark.html create mode 100644 src/timeline/demo/src/app/timeline/timeline-helptip.html create mode 100644 src/timeline/demo/src/app/timeline/timeline-multi.html create mode 100644 src/timeline/demo/src/app/timeline/timeline-templete.html create mode 100644 src/timeline/demo/src/app/timeline/timeline-test.html create mode 100644 src/timeline/demo/src/app/timeline/timeline-type.html create mode 100644 src/timeline/demo/src/app/timeline/webdoc/timeline-demos.js create mode 100644 src/timeline/demo/src/app/timeline/webdoc/timeline.cn.md create mode 100644 src/timeline/demo/src/app/timeline/webdoc/timeline.en.md create mode 100644 src/timeline/demo/src/favicon.ico create mode 100644 src/timeline/demo/src/index.html create mode 100644 src/timeline/demo/src/main.ts create mode 100644 src/timeline/demo/test.ts create mode 100644 src/timeline/demo/tsconfig.app.json create mode 100644 src/timeline/demo/tsconfig.spec.json create mode 100644 src/timeline/lib/index.ts create mode 100644 src/timeline/lib/ng-package.json create mode 100644 src/timeline/lib/package.json create mode 100644 src/timeline/lib/project.json create mode 100644 src/timeline/lib/src/TiTimelineComponent.ts create mode 100644 src/timeline/lib/src/TiTimelineModule.ts create mode 100644 src/timeline/lib/src/timeline.html create mode 100644 src/timeline/lib/src/timeline.less create mode 100644 src/tip/demo/karma.conf.js create mode 100644 src/tip/demo/project.json create mode 100644 src/tip/demo/src/app/AppComponent.ts create mode 100644 src/tip/demo/src/app/AppModule.ts create mode 100644 src/tip/demo/src/app/IndexComponent.ts create mode 100644 src/tip/demo/src/app/app.html create mode 100644 src/tip/demo/src/app/tip/TipBasicComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipContentCompComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipContentTemplateComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipEmptyComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipHasArrowComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipLongTextPositionComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipMaxWidthComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipPositionComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipPositionTestComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipServiceComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipServiceDestroyComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipTestModule.ts create mode 100644 src/tip/demo/src/app/tip/TipTriggerComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipValidPositionTestComponent.ts create mode 100644 src/tip/demo/src/app/tip/TipZindexComponent.ts create mode 100644 src/tip/demo/src/app/tip/tip-basic.html create mode 100644 src/tip/demo/src/app/tip/tip-content-comp.html create mode 100644 src/tip/demo/src/app/tip/tip-content-template.html create mode 100644 src/tip/demo/src/app/tip/tip-empty.html create mode 100644 src/tip/demo/src/app/tip/tip-has-arrow.html create mode 100644 src/tip/demo/src/app/tip/tip-long-text-position.html create mode 100644 src/tip/demo/src/app/tip/tip-max-width.html create mode 100644 src/tip/demo/src/app/tip/tip-position-test.html create mode 100644 src/tip/demo/src/app/tip/tip-position.html create mode 100644 src/tip/demo/src/app/tip/tip-service-destroy.html create mode 100644 src/tip/demo/src/app/tip/tip-service.html create mode 100644 src/tip/demo/src/app/tip/tip-trigger.html create mode 100644 src/tip/demo/src/app/tip/tip-valid-position-test.html create mode 100644 src/tip/demo/src/app/tip/tip-zindex.html create mode 100644 src/tip/demo/src/app/tip/tipTest.less create mode 100644 src/tip/demo/src/app/tip/webdoc/tip-demos.js create mode 100644 src/tip/demo/src/app/tip/webdoc/tip.cn.md create mode 100644 src/tip/demo/src/app/tip/webdoc/tip.en.md create mode 100644 src/tip/demo/src/favicon.ico create mode 100644 src/tip/demo/src/index.html create mode 100644 src/tip/demo/src/main.ts create mode 100644 src/tip/demo/test.ts create mode 100644 src/tip/demo/tsconfig.app.json create mode 100644 src/tip/demo/tsconfig.spec.json create mode 100644 src/tip/lib/index.ts create mode 100644 src/tip/lib/ng-package.json create mode 100644 src/tip/lib/package.json create mode 100644 src/tip/lib/project.json create mode 100644 src/tip/lib/src/TiTipContainerComponent.ts create mode 100644 src/tip/lib/src/TiTipDirective.ts create mode 100644 src/tip/lib/src/TiTipInterface.ts create mode 100644 src/tip/lib/src/TiTipModule.ts create mode 100644 src/tip/lib/src/TiTipService.ts create mode 100644 src/tip/lib/src/TiTipServiceModule.ts create mode 100644 src/tip/lib/src/tip.less create mode 100644 src/transfer/demo/karma.conf.js create mode 100644 src/transfer/demo/project.json create mode 100644 src/transfer/demo/src/app/AppComponent.ts create mode 100644 src/transfer/demo/src/app/AppModule.ts create mode 100644 src/transfer/demo/src/app/IndexComponent.ts create mode 100644 src/transfer/demo/src/app/app.html create mode 100644 src/transfer/demo/src/app/transfer/TransferBasicComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferDisabledComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferEventComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferIdComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferIdkeyComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferLabelkeyComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferLazyComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferLoadComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferNodatatextComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferPaginationComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferPlaceholderComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferSearchableComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferSearchkeysComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferSizeComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferTableComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferTestModule.ts create mode 100644 src/transfer/demo/src/app/transfer/TransferTitlesComponent.ts create mode 100644 src/transfer/demo/src/app/transfer/data.js create mode 100644 src/transfer/demo/src/app/transfer/transfer-basic.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-disabled.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-event.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-id.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-idkey.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-labelkey.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-lazy.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-load.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-nodatatext.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-pagination.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-placeholder.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-searchable.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-searchkeys.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-size.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-table.html create mode 100644 src/transfer/demo/src/app/transfer/transfer-titles.html create mode 100644 src/transfer/demo/src/app/transfer/transfer.less create mode 100644 src/transfer/demo/src/app/transfer/webdoc/transfer-demos.js create mode 100644 src/transfer/demo/src/app/transfer/webdoc/transfer.cn.md create mode 100644 src/transfer/demo/src/app/transfer/webdoc/transfer.en.md create mode 100644 src/transfer/demo/src/favicon.ico create mode 100644 src/transfer/demo/src/index.html create mode 100644 src/transfer/demo/src/main.ts create mode 100644 src/transfer/demo/test.ts create mode 100644 src/transfer/demo/tsconfig.app.json create mode 100644 src/transfer/demo/tsconfig.spec.json create mode 100644 src/transfer/lib/index.ts create mode 100644 src/transfer/lib/ng-package.json create mode 100644 src/transfer/lib/package.json create mode 100644 src/transfer/lib/project.json create mode 100644 src/transfer/lib/src/TiTransferColumn.ts create mode 100644 src/transfer/lib/src/TiTransferComponent.ts create mode 100644 src/transfer/lib/src/TiTransferModule.ts create mode 100644 src/transfer/lib/src/i18n/TiTransferWords.ts create mode 100644 src/transfer/lib/src/i18n/en_US.ts create mode 100644 src/transfer/lib/src/i18n/es_US.ts create mode 100644 src/transfer/lib/src/i18n/fr_FR.ts create mode 100644 src/transfer/lib/src/i18n/index.ts create mode 100644 src/transfer/lib/src/i18n/pt_BR.ts create mode 100644 src/transfer/lib/src/i18n/zh_CN.ts create mode 100644 src/transfer/lib/src/transfer.html create mode 100644 src/transfer/lib/src/transfer.less create mode 100644 src/transfer/lib/src/transferUtil.ts create mode 100644 src/transfer/lib/src/transferlist/TiTransferListComponent.ts create mode 100644 src/transfer/lib/src/transferlist/transfer-list.html create mode 100644 src/transfer/lib/src/transferlist/transfer-list.less create mode 100644 src/tree/demo/karma.conf.js create mode 100644 src/tree/demo/project.json create mode 100644 src/tree/demo/src/app/AppComponent.ts create mode 100644 src/tree/demo/src/app/AppModule.ts create mode 100644 src/tree/demo/src/app/IndexComponent.ts create mode 100644 src/tree/demo/src/app/app.html create mode 100644 src/tree/demo/src/app/tree/TreeBeforeExpandComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeBeforeMoreComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeChangedbycheckboxComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeCheckRelationComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeDisabledComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeDragBeforedropComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeDragComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeEventComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeIconComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeLoadComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeManyComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeMultiselectComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeOperateComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeParentcheckableComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeRadioselectComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeSearchComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeShortcutkeyComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeSmallComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeTemplateComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeTestModule.ts create mode 100644 src/tree/demo/src/app/tree/TreeUtilComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeVirtualscrollComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeVirtualscrollDragComponent.ts create mode 100644 src/tree/demo/src/app/tree/TreeVirtualscrollSmallComponent.ts create mode 100644 src/tree/demo/src/app/tree/tree-before-expand.html create mode 100644 src/tree/demo/src/app/tree/tree-before-more.html create mode 100644 src/tree/demo/src/app/tree/tree-changedbycheckbox.html create mode 100644 src/tree/demo/src/app/tree/tree-check-relation.html create mode 100644 src/tree/demo/src/app/tree/tree-disabled.html create mode 100644 src/tree/demo/src/app/tree/tree-drag-beforedrop.html create mode 100644 src/tree/demo/src/app/tree/tree-drag.html create mode 100644 src/tree/demo/src/app/tree/tree-event.html create mode 100644 src/tree/demo/src/app/tree/tree-icon.html create mode 100644 src/tree/demo/src/app/tree/tree-load.html create mode 100644 src/tree/demo/src/app/tree/tree-many.html create mode 100644 src/tree/demo/src/app/tree/tree-multiselect.html create mode 100644 src/tree/demo/src/app/tree/tree-operate.html create mode 100644 src/tree/demo/src/app/tree/tree-parentcheckable.html create mode 100644 src/tree/demo/src/app/tree/tree-radioselect.html create mode 100644 src/tree/demo/src/app/tree/tree-search.html create mode 100644 src/tree/demo/src/app/tree/tree-shortcutkey.html create mode 100644 src/tree/demo/src/app/tree/tree-small.html create mode 100644 src/tree/demo/src/app/tree/tree-template.html create mode 100644 src/tree/demo/src/app/tree/tree-util.html create mode 100644 src/tree/demo/src/app/tree/tree-virtualscroll-drag.html create mode 100644 src/tree/demo/src/app/tree/tree-virtualscroll-small.html create mode 100644 src/tree/demo/src/app/tree/tree-virtualscroll.html create mode 100644 src/tree/demo/src/app/tree/treeTest.less create mode 100644 src/tree/demo/src/app/tree/webdoc/tree-demos.js create mode 100644 src/tree/demo/src/app/tree/webdoc/tree.cn.md create mode 100644 src/tree/demo/src/app/tree/webdoc/tree.en.md create mode 100644 src/tree/demo/src/favicon.ico create mode 100644 src/tree/demo/src/index.html create mode 100644 src/tree/demo/src/main.ts create mode 100644 src/tree/demo/test.ts create mode 100644 src/tree/demo/tsconfig.app.json create mode 100644 src/tree/demo/tsconfig.spec.json create mode 100644 src/tree/lib/index.ts create mode 100644 src/tree/lib/ng-package.json create mode 100644 src/tree/lib/package.json create mode 100644 src/tree/lib/project.json create mode 100644 src/tree/lib/src/TiAutoSelectDirective.ts create mode 100644 src/tree/lib/src/TiHighlightPipe.ts create mode 100644 src/tree/lib/src/TiTreeComponent.ts create mode 100644 src/tree/lib/src/TiTreeModule.ts create mode 100644 src/tree/lib/src/TiTreeUtil.ts create mode 100644 src/tree/lib/src/i18n/TiTreeWords.ts create mode 100644 src/tree/lib/src/i18n/en_US.ts create mode 100644 src/tree/lib/src/i18n/es_US.ts create mode 100644 src/tree/lib/src/i18n/fr_FR.ts create mode 100644 src/tree/lib/src/i18n/index.ts create mode 100644 src/tree/lib/src/i18n/pt_BR.ts create mode 100644 src/tree/lib/src/i18n/zh_CN.ts create mode 100644 src/tree/lib/src/tree.html create mode 100644 src/tree/lib/src/tree.less create mode 100644 src/treeselect/demo/karma.conf.js create mode 100644 src/treeselect/demo/project.json create mode 100644 src/treeselect/demo/src/app/AppComponent.ts create mode 100644 src/treeselect/demo/src/app/AppModule.ts create mode 100644 src/treeselect/demo/src/app/IndexComponent.ts create mode 100644 src/treeselect/demo/src/app/app.html create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectBasicComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectBeforeExpandComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectBeforeMoreComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectClearableComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectDisabledComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectDropmaxheightComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectEventComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectFocusComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectLabelkeyComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectLazyloadComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectLoadComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectMaxlineComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectModalComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectMultiComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectNodataComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectOptionsChangeComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectPanelwidthComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectSearchComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectSelectallComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectTestModule.ts create mode 100644 src/treeselect/demo/src/app/treeselect/TreeselectValidationComponent.ts create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-basic.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-before-expand.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-before-more.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-clearable.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-disabled.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-dropmaxheight.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-event.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-focus.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-labelkey.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-lazyload.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-load.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-maxline.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-modal.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-multi.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-nodata.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-options-change.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-panelwidth.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-search.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-selectall.html create mode 100644 src/treeselect/demo/src/app/treeselect/treeselect-validation.html create mode 100644 src/treeselect/demo/src/app/treeselect/webdoc/treeselect-demos.js create mode 100644 src/treeselect/demo/src/app/treeselect/webdoc/treeselect.cn.md create mode 100644 src/treeselect/demo/src/app/treeselect/webdoc/treeselect.en.md create mode 100644 src/treeselect/demo/src/favicon.ico create mode 100644 src/treeselect/demo/src/index.html create mode 100644 src/treeselect/demo/src/main.ts create mode 100644 src/treeselect/demo/test.ts create mode 100644 src/treeselect/demo/tsconfig.app.json create mode 100644 src/treeselect/demo/tsconfig.spec.json create mode 100644 src/treeselect/lib/index.ts create mode 100644 src/treeselect/lib/ng-package.json create mode 100644 src/treeselect/lib/package.json create mode 100644 src/treeselect/lib/project.json create mode 100644 src/treeselect/lib/src/TiTreeselectComponent.ts create mode 100644 src/treeselect/lib/src/TiTreeselectModule.ts create mode 100644 src/treeselect/lib/src/treeselect.html create mode 100644 src/treeselect/lib/src/treeselect.less create mode 100644 src/upload/demo/karma.conf.js create mode 100644 src/upload/demo/project.json create mode 100644 src/upload/demo/src/app/AppComponent.ts create mode 100644 src/upload/demo/src/app/AppModule.ts create mode 100644 src/upload/demo/src/app/IndexComponent.ts create mode 100644 src/upload/demo/src/app/app.html create mode 100644 src/upload/demo/src/app/upload/UploadAutoUploadComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadBasicComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadBatchSendComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadBeforeremoveComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadButtonComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadButtonTestComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadCaseTestComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadChangesComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadChunksizeComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadCustomComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadEventComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadFilterComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadFormDataComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadInitfilesTestComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadInputFieldTestComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadPropsComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadServiceComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadServiceTestComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadSingleComponent.ts create mode 100644 src/upload/demo/src/app/upload/UploadTestModule.ts create mode 100644 src/upload/demo/src/app/upload/upload-auto-upload.html create mode 100644 src/upload/demo/src/app/upload/upload-basic.html create mode 100644 src/upload/demo/src/app/upload/upload-batch-send.html create mode 100644 src/upload/demo/src/app/upload/upload-beforeremove.html create mode 100644 src/upload/demo/src/app/upload/upload-button-test.html create mode 100644 src/upload/demo/src/app/upload/upload-button.html create mode 100644 src/upload/demo/src/app/upload/upload-case-test.html create mode 100644 src/upload/demo/src/app/upload/upload-changes.html create mode 100644 src/upload/demo/src/app/upload/upload-chunksize.html create mode 100644 src/upload/demo/src/app/upload/upload-custom.html create mode 100644 src/upload/demo/src/app/upload/upload-event.html create mode 100644 src/upload/demo/src/app/upload/upload-filter.html create mode 100644 src/upload/demo/src/app/upload/upload-form-data.html create mode 100644 src/upload/demo/src/app/upload/upload-initfiles-test.html create mode 100644 src/upload/demo/src/app/upload/upload-input-field-test.html create mode 100644 src/upload/demo/src/app/upload/upload-props.html create mode 100644 src/upload/demo/src/app/upload/upload-service-test.html create mode 100644 src/upload/demo/src/app/upload/upload-service.html create mode 100644 src/upload/demo/src/app/upload/upload-single.html create mode 100644 src/upload/demo/src/app/upload/webdoc/upload-demos.js create mode 100644 src/upload/demo/src/app/upload/webdoc/upload.cn.md create mode 100644 src/upload/demo/src/app/upload/webdoc/upload.en.md create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageAutoUploadComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageBasicComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageChangesComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageDeletableComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageDisabledComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageDragComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageEventComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageFilterComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageInitfilesComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageMaxcountComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageTemplateComponent.ts create mode 100644 src/upload/demo/src/app/uploadimage/UploadimageTestModule.ts create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-auto-upload.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-basic.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-changes.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-deletable.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-disabled.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-drag.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-event.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-filter.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-initfiles.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-maxcount.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimage-template.html create mode 100644 src/upload/demo/src/app/uploadimage/uploadimagetest.less create mode 100644 src/upload/demo/src/app/uploadimage/webdoc/uploadimage-demos.js create mode 100644 src/upload/demo/src/app/uploadimage/webdoc/uploadimage.cn.md create mode 100644 src/upload/demo/src/app/uploadimage/webdoc/uploadimage.en.md create mode 100644 src/upload/demo/src/favicon.ico create mode 100644 src/upload/demo/src/index.html create mode 100644 src/upload/demo/src/main.ts create mode 100644 src/upload/demo/test.ts create mode 100644 src/upload/demo/tsconfig.app.json create mode 100644 src/upload/demo/tsconfig.spec.json create mode 100644 src/upload/lib/index.ts create mode 100644 src/upload/lib/ng-package.json create mode 100644 src/upload/lib/package.json create mode 100644 src/upload/lib/project.json create mode 100644 src/upload/lib/src/TiDisabledDirective.ts create mode 100644 src/upload/lib/src/TiFileInterface.ts create mode 100644 src/upload/lib/src/TiFileSelectDirective.ts create mode 100644 src/upload/lib/src/TiUploadComponent.ts create mode 100644 src/upload/lib/src/TiUploadModule.ts create mode 100644 src/upload/lib/src/TiUploadService.ts create mode 100644 src/upload/lib/src/TiUploadServiceModule.ts create mode 100644 src/upload/lib/src/TiUploadUtil.ts create mode 100644 src/upload/lib/src/TiUploadbaseComponent.ts create mode 100644 src/upload/lib/src/TiUploadimageComponent.ts create mode 100644 src/upload/lib/src/i18n/TiUploadWords.ts create mode 100644 src/upload/lib/src/i18n/en_US.ts create mode 100644 src/upload/lib/src/i18n/es_US.ts create mode 100644 src/upload/lib/src/i18n/fr_FR.ts create mode 100644 src/upload/lib/src/i18n/index.ts create mode 100644 src/upload/lib/src/i18n/pt_BR.ts create mode 100644 src/upload/lib/src/i18n/zh_CN.ts create mode 100644 src/upload/lib/src/upload.html create mode 100644 src/upload/lib/src/upload.less create mode 100644 src/upload/lib/src/uploadimage.html create mode 100644 src/upload/lib/src/uploadimage.less create mode 100644 src/utils/demo/project.json create mode 100644 src/utils/demo/src/app/AppComponent.ts create mode 100644 src/utils/demo/src/app/AppModule.ts create mode 100644 src/utils/demo/src/app/IndexComponent.ts create mode 100644 src/utils/demo/src/app/app.html create mode 100644 src/utils/demo/src/app/browser/BrowserTestModule.ts create mode 100644 src/utils/demo/src/app/browser/BrowserUsageComponent.ts create mode 100644 src/utils/demo/src/app/browser/browser-usage.html create mode 100644 src/utils/demo/src/app/browser/webdoc/browser-demos.js create mode 100644 src/utils/demo/src/app/browser/webdoc/browser.cn.md create mode 100644 src/utils/demo/src/app/browser/webdoc/browser.en.md create mode 100644 src/utils/demo/src/app/keymap/KeymapTestModule.ts create mode 100644 src/utils/demo/src/app/keymap/KeymapUsageComponent.ts create mode 100644 src/utils/demo/src/app/keymap/keymap-usage.html create mode 100644 src/utils/demo/src/app/keymap/webdoc/keymap-demos.js create mode 100644 src/utils/demo/src/app/keymap/webdoc/keymap.cn.md create mode 100644 src/utils/demo/src/app/keymap/webdoc/keymap.en.md create mode 100644 src/utils/demo/src/app/log/LogTestModule.ts create mode 100644 src/utils/demo/src/app/log/LogUsageComponent.ts create mode 100644 src/utils/demo/src/app/log/log-usage.html create mode 100644 src/utils/demo/src/app/log/webdoc/log-demos.js create mode 100644 src/utils/demo/src/app/log/webdoc/log.cn.md create mode 100644 src/utils/demo/src/app/log/webdoc/log.en.md create mode 100644 src/utils/demo/src/app/theme/ThemeBasicComponent.ts create mode 100644 src/utils/demo/src/app/theme/ThemeTestModule.ts create mode 100644 src/utils/demo/src/app/theme/theme-basic.html create mode 100644 src/utils/demo/src/app/theme/webdoc/theme-demos.js create mode 100644 src/utils/demo/src/app/theme/webdoc/theme.cn.md create mode 100644 src/utils/demo/src/app/theme/webdoc/theme.en.md create mode 100644 src/utils/demo/src/favicon.ico create mode 100644 src/utils/demo/src/index.html create mode 100644 src/utils/demo/src/main.ts create mode 100644 src/utils/demo/tsconfig.app.json create mode 100644 src/utils/lib/index.ts create mode 100644 src/utils/lib/ng-package.json create mode 100644 src/utils/lib/package.json create mode 100644 src/utils/lib/project.json create mode 100644 src/utils/lib/src/ObservableMap.ts create mode 100644 src/utils/lib/src/ObservableSet.ts create mode 100644 src/utils/lib/src/Position.ts create mode 100644 src/utils/lib/src/TiBrowser.ts create mode 100644 src/utils/lib/src/TiDateUtil.ts create mode 100644 src/utils/lib/src/TiKeymap.ts create mode 100644 src/utils/lib/src/TiLog.ts create mode 100644 src/utils/lib/src/TiTheme.ts create mode 100644 src/utils/lib/src/Util.ts create mode 100644 src/validation/demo/karma.conf.js create mode 100644 src/validation/demo/project.json create mode 100644 src/validation/demo/src/app/AppComponent.ts create mode 100644 src/validation/demo/src/app/AppModule.ts create mode 100644 src/validation/demo/src/app/IndexComponent.ts create mode 100644 src/validation/demo/src/app/app.html create mode 100644 src/validation/demo/src/app/validation/ValidationAsyncCheckComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationAsyncCheckTestComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationBasicControlComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationBasicDirectiveComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationBlurCheckComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationErrorMsgComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationFormGroupComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationFormGroupConfigComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationFormGroupTestComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationParamChangeComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationPwdCheckComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationRulesCustomComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationRulesCustomDirectiveComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationRulesTestComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationTemplateFormNestedComponent.ts create mode 100644 src/validation/demo/src/app/validation/ValidationTestModule.ts create mode 100644 src/validation/demo/src/app/validation/ValidationTipComponent.ts create mode 100644 src/validation/demo/src/app/validation/validation-async-check-test.html create mode 100644 src/validation/demo/src/app/validation/validation-async-check.html create mode 100644 src/validation/demo/src/app/validation/validation-basic-control.html create mode 100644 src/validation/demo/src/app/validation/validation-basic-directive.html create mode 100644 src/validation/demo/src/app/validation/validation-blur-check.html create mode 100644 src/validation/demo/src/app/validation/validation-error-msg.html create mode 100644 src/validation/demo/src/app/validation/validation-form-group-config.html create mode 100644 src/validation/demo/src/app/validation/validation-form-group-test.html create mode 100644 src/validation/demo/src/app/validation/validation-form-group.html create mode 100644 src/validation/demo/src/app/validation/validation-param-change.html create mode 100644 src/validation/demo/src/app/validation/validation-pwd-check.html create mode 100644 src/validation/demo/src/app/validation/validation-rules-custom-directive.html create mode 100644 src/validation/demo/src/app/validation/validation-rules-custom.html create mode 100644 src/validation/demo/src/app/validation/validation-rules-test.html create mode 100644 src/validation/demo/src/app/validation/validation-template-form-nested.html create mode 100644 src/validation/demo/src/app/validation/validation-tip.html create mode 100644 src/validation/demo/src/app/validation/validation-tiscroll.html create mode 100644 src/validation/demo/src/app/validation/validationTiscrollComponent.ts create mode 100644 src/validation/demo/src/app/validation/webdoc/validation-demos.js create mode 100644 src/validation/demo/src/app/validation/webdoc/validation.cn.md create mode 100644 src/validation/demo/src/app/validation/webdoc/validation.en.md create mode 100644 src/validation/demo/src/favicon.ico create mode 100644 src/validation/demo/src/index.html create mode 100644 src/validation/demo/src/main.ts create mode 100644 src/validation/demo/test.ts create mode 100644 src/validation/demo/tsconfig.app.json create mode 100644 src/validation/demo/tsconfig.spec.json create mode 100644 src/validation/lib/index.ts create mode 100644 src/validation/lib/ng-package.json create mode 100644 src/validation/lib/package.json create mode 100644 src/validation/lib/project.json create mode 100644 src/validation/lib/src/TiErrorMsgComponent.ts create mode 100644 src/validation/lib/src/TiPendingStateComponent.ts create mode 100644 src/validation/lib/src/TiPwdMsgComponent.html create mode 100644 src/validation/lib/src/TiPwdMsgComponent.ts create mode 100644 src/validation/lib/src/TiValidationConfig.ts create mode 100644 src/validation/lib/src/TiValidationDirective.ts create mode 100644 src/validation/lib/src/TiValidationInterface.ts create mode 100644 src/validation/lib/src/TiValidationModule.ts create mode 100644 src/validation/lib/src/checkHandle/AsyncCheck.ts create mode 100644 src/validation/lib/src/checkHandle/BlurCheck.ts create mode 100644 src/validation/lib/src/checkHandle/ChangeCheck.ts create mode 100644 src/validation/lib/src/checkHandle/CheckHandle.ts create mode 100644 src/validation/lib/src/checkHandle/CheckStyleModule.ts create mode 100644 src/validation/lib/src/checkHandle/CheckStyleService.ts create mode 100644 src/validation/lib/src/checkHandle/CommonService.ts create mode 100644 src/validation/lib/src/checkHandle/CommonServiceModule.ts create mode 100644 src/validation/lib/src/checkHandle/PwdCheck.ts create mode 100644 src/validation/lib/src/checkHandle/RadiobaseCheck.ts create mode 100644 src/validation/lib/src/checkHandle/TiPwdConfig.ts create mode 100644 src/validation/lib/src/errorMsg.less create mode 100644 src/validation/lib/src/i18n/TiValidationWords.ts create mode 100644 src/validation/lib/src/i18n/en_US.ts create mode 100644 src/validation/lib/src/i18n/es_US.ts create mode 100644 src/validation/lib/src/i18n/fr_FR.ts create mode 100644 src/validation/lib/src/i18n/index.ts create mode 100644 src/validation/lib/src/i18n/pt_BR.ts create mode 100644 src/validation/lib/src/i18n/zh_CN.ts create mode 100644 src/validation/lib/src/pending-state.html create mode 100644 src/validation/lib/src/pending-state.less create mode 100644 src/validation/lib/src/pwdMsg.less create mode 100644 src/validation/lib/src/validators/TiValidators.ts create mode 100644 src/validation/lib/src/validators/directives/BaseValidator.ts create mode 100644 src/validation/lib/src/validators/directives/BigDigitsValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/BigIntegerValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/BigNumberValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/ContainsValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/DateValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/DigitsValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/EmailValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/EqualValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/IntegerValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/Ipv4ValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/Ipv6ValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/MaxLengthValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/MaxValueByStringValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/MaxValueValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/MinLengthValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/MinValueByStringValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/MinValueValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/NotContainsValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/NotEqualValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/NotScriptValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/NumberValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/PasswordValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/PortValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/RangeSizeValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/RangeValueByStringValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/RangeValueValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/RegExpValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/RequiredValidatorDirective.ts create mode 100644 src/validation/lib/src/validators/directives/UrlValidatorDirective.ts create mode 100644 src/zoom/demo/project.json create mode 100644 src/zoom/demo/src/app/AppComponent.ts create mode 100644 src/zoom/demo/src/app/AppModule.ts create mode 100644 src/zoom/demo/src/app/IndexComponent.ts create mode 100644 src/zoom/demo/src/app/app.html create mode 100644 src/zoom/demo/src/app/zoom/ZoomBasicComponent.ts create mode 100644 src/zoom/demo/src/app/zoom/ZoomChangeSrcComponent.ts create mode 100644 src/zoom/demo/src/app/zoom/ZoomRatioComponent.ts create mode 100644 src/zoom/demo/src/app/zoom/ZoomTestComponent.ts create mode 100644 src/zoom/demo/src/app/zoom/ZoomTestModule.ts create mode 100644 src/zoom/demo/src/app/zoom/zoom-basic.html create mode 100644 src/zoom/demo/src/app/zoom/zoom-change-src.html create mode 100644 src/zoom/demo/src/app/zoom/zoom-ratio.html create mode 100644 src/zoom/demo/src/app/zoom/zoom-test.html create mode 100644 src/zoom/demo/src/app/zoom/zoom.less create mode 100644 src/zoom/demo/src/favicon.ico create mode 100644 src/zoom/demo/src/index.html create mode 100644 src/zoom/demo/src/main.ts create mode 100644 src/zoom/demo/tsconfig.app.json create mode 100644 src/zoom/lib/index.ts create mode 100644 src/zoom/lib/ng-package.json create mode 100644 src/zoom/lib/package.json create mode 100644 src/zoom/lib/project.json create mode 100644 src/zoom/lib/src/TiZoomComponent.ts create mode 100644 src/zoom/lib/src/TiZoomModule.ts create mode 100644 src/zoom/lib/src/zoom.less create mode 100644 tools/generators/ti-lib-generator/files/demo/project.json__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/AppComponent.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/AppModule.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/IndexComponent.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoComponentName__.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoModuleName__.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/__name__/__name__-basic.html__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__-demos.js__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.cn.md__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.en.md__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/app/app.html__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/favicon.ico__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/index.html__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/src/main.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/demo/tsconfig.app.json__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/index.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/ng-package.json__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/package.json__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/project.json__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/__componentName__.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/__moduleName__.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/__name__.html__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/__name__.less__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/i18n/__i18nWords__.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/i18n/en_US.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/i18n/es_US.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/i18n/fr_FR.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/i18n/index.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/files/lib/src/i18n/pt_BR.ts__tmpl__ create mode 100644 tools/generators/ti-lib-generator/index.ts create mode 100644 tools/generators/ti-lib-generator/schema.d.ts create mode 100644 tools/generators/ti-lib-generator/schema.json create mode 100644 tools/tsconfig.tools.json diff --git a/src/inputnumber/demo/src/app/AppComponent.ts b/src/inputnumber/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/inputnumber/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/inputnumber/demo/src/app/AppModule.ts b/src/inputnumber/demo/src/app/AppModule.ts new file mode 100644 index 0000000..869a74a --- /dev/null +++ b/src/inputnumber/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { InputnumberTestModule } from './inputnumber/InputnumberTestModule'; + +@NgModule({ + imports: [ + InputnumberTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/inputnumber/demo/src/app/IndexComponent.ts b/src/inputnumber/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..41f1206 --- /dev/null +++ b/src/inputnumber/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { InputnumberTestModule } from './inputnumber/InputnumberTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = InputnumberTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/inputnumber/demo/src/app/app.html b/src/inputnumber/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/inputnumber/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberBasicComponent.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberBasicComponent.ts new file mode 100644 index 0000000..481751b --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './inputnumber-basic.html' +}) +export class InputnumberBasicComponent { + inputValue: number = 11111.45; +} diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberEventComponent.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberEventComponent.ts new file mode 100644 index 0000000..f53a469 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberEventComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './inputnumber-event.html' +}) +export class InputnumberEventComponent { + myLogs: Array = []; + value: number = 189.421; + + onModelChange(value: any): void { + this.myLogs = [...this.myLogs, `modelChange:${value}`]; + } + + onFocus(value: any): void { + this.myLogs = [...this.myLogs, `focus:${value}`]; + } + + onBlur(value: any): void { + this.myLogs = [...this.myLogs, `blur:${value}`]; + } +} diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberFocusComponent.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberFocusComponent.ts new file mode 100644 index 0000000..d160d3e --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberFocusComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './inputnumber-focus.html' +}) +export class InputnumberFocusComponent { + inputValue: number = 2345.565678; +} diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberFormatComponent.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberFormatComponent.ts new file mode 100644 index 0000000..2423dd8 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberFormatComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './inputnumber-format.html' +}) +export class InputnumberFormatComponent { + format: string = 'n2'; + formatValue: number = 33333.45678; +} diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberLoadComponent.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberLoadComponent.ts new file mode 100644 index 0000000..c3ce33b --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberLoadComponent.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './inputnumber-load.html' +}) +export class InputnumberLoadComponent { + inputValue: any = undefined; + inputValue1: any = 1234.56789; + changeA(): void { + this.inputValue = 123456.789; + } + changeB(): void { + this.inputValue = 123456.782199; + } + changeC(): void { + this.inputValue1 = 2222.4567; + } + changeInvalid(): void { + this.inputValue1 = 'dfasd'; + } + clear(): void { + this.inputValue1 = undefined; + } +} diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberLocaleableComponent.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberLocaleableComponent.ts new file mode 100644 index 0000000..752abd6 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberLocaleableComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './inputnumber-localeable.html' +}) +export class InputnumberLocaleableComponent { + localValue: number = 33333.45678; + localeable: boolean = false; +} diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberMaxlengthComponent.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberMaxlengthComponent.ts new file mode 100644 index 0000000..91282fa --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberMaxlengthComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './inputnumber-maxlength.html' +}) +export class InputnumberMaxlengthComponent { + inputValue: number = 2345.565678; +} diff --git a/src/inputnumber/demo/src/app/inputnumber/InputnumberTestModule.ts b/src/inputnumber/demo/src/app/inputnumber/InputnumberTestModule.ts new file mode 100644 index 0000000..6167002 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/InputnumberTestModule.ts @@ -0,0 +1,70 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiInputNumberModule, TiTextModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { InputnumberBasicComponent } from './InputnumberBasicComponent'; +import { InputnumberMaxlengthComponent } from './InputnumberMaxlengthComponent'; +import { InputnumberLoadComponent } from './InputnumberLoadComponent'; +import { InputnumberFocusComponent } from './InputnumberFocusComponent'; +import { InputnumberFormatComponent } from './InputnumberFormatComponent'; +import { InputnumberLocaleableComponent } from './InputnumberLocaleableComponent'; +import { InputnumberEventComponent } from './InputnumberEventComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiTextModule, + TiInputNumberModule, + TiButtonModule, + DemoLogModule, + RouterModule.forChild(InputnumberTestModule.ROUTES) + ], + declarations: [ + InputnumberBasicComponent, + InputnumberMaxlengthComponent, + InputnumberLoadComponent, + InputnumberFocusComponent, + InputnumberFormatComponent, + InputnumberLocaleableComponent, + InputnumberEventComponent + ] +}) +export class InputnumberTestModule { + static readonly LINKS: Array = [{ href: 'directives/TiInputNumberDirective.html', label: 'InputNumber' }]; + static readonly ROUTES: Routes = [ + { + path: 'inputnumber/inputnumber-basic', + component: InputnumberBasicComponent + }, + { + path: 'inputnumber/inputnumber-format', + component: InputnumberFormatComponent + }, + { + path: 'inputnumber/inputnumber-localeable', + component: InputnumberLocaleableComponent + }, + { + path: 'inputnumber/inputnumber-maxlength', + component: InputnumberMaxlengthComponent + }, + { + path: 'inputnumber/inputnumber-load', + component: InputnumberLoadComponent + }, + { + path: 'inputnumber/inputnumber-focus', + component: InputnumberFocusComponent + }, + { + path: 'inputnumber/inputnumber-event', + component: InputnumberEventComponent + } + ]; +} diff --git a/src/inputnumber/demo/src/app/inputnumber/inputnumber-basic.html b/src/inputnumber/demo/src/app/inputnumber/inputnumber-basic.html new file mode 100644 index 0000000..d70ad73 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/inputnumber-basic.html @@ -0,0 +1,4 @@ + +
    +
    Current value: {{ inputValue }}
    +
    diff --git a/src/inputnumber/demo/src/app/inputnumber/inputnumber-event.html b/src/inputnumber/demo/src/app/inputnumber/inputnumber-event.html new file mode 100644 index 0000000..ef85fad --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/inputnumber-event.html @@ -0,0 +1,11 @@ + + diff --git a/src/inputnumber/demo/src/app/inputnumber/inputnumber-focus.html b/src/inputnumber/demo/src/app/inputnumber/inputnumber-focus.html new file mode 100644 index 0000000..b7cb83c --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/inputnumber-focus.html @@ -0,0 +1,9 @@ +

    1.autofocus方式聚焦:

    + +

    +

    2.focus()方式聚焦:

    + +

    + +  + diff --git a/src/inputnumber/demo/src/app/inputnumber/inputnumber-format.html b/src/inputnumber/demo/src/app/inputnumber/inputnumber-format.html new file mode 100644 index 0000000..de391df --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/inputnumber-format.html @@ -0,0 +1 @@ + diff --git a/src/inputnumber/demo/src/app/inputnumber/inputnumber-load.html b/src/inputnumber/demo/src/app/inputnumber/inputnumber-load.html new file mode 100644 index 0000000..b2f5ceb --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/inputnumber-load.html @@ -0,0 +1,20 @@ +

    描述

    +

    inputnumber组件,数据加载

    +

    示例

    +

    1.使用典型场景:空数据->数据A->数据B

    +
    + +

    + +   + +
    +

    2.使用典型场景:数据A->数据C->空数据(当用户给组件传递为非法值时,数据不会改变)

    +
    + +

    + +   + +   + diff --git a/src/inputnumber/demo/src/app/inputnumber/inputnumber-localeable.html b/src/inputnumber/demo/src/app/inputnumber/inputnumber-localeable.html new file mode 100644 index 0000000..0d9b0e1 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/inputnumber-localeable.html @@ -0,0 +1 @@ + diff --git a/src/inputnumber/demo/src/app/inputnumber/inputnumber-maxlength.html b/src/inputnumber/demo/src/app/inputnumber/inputnumber-maxlength.html new file mode 100644 index 0000000..fa84f30 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/inputnumber-maxlength.html @@ -0,0 +1 @@ + diff --git a/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber-demos.js b/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber-demos.js new file mode 100644 index 0000000..db560f6 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber-demos.js @@ -0,0 +1,66 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'inputnumber-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    InputNumber 指令的最简用法。

    ', + 'en-US': '', + }, + }, + { + demoId: 'inputnumber-format', + name: { + 'zh-CN': '数字精度', + 'en-US': 'format', + }, + desc: { + 'zh-CN': + '

    通过属性format配置小数点位数,使用“n+数字”方式设置,数字代表保留几位小数。不配置 format 时,默认最少保留 0 位小数,最多保留 3 位小数。

    ', + 'en-US': '', + }, + apis: ['TiInputNumberDirective.properties.format'], + }, + { + demoId: 'inputnumber-localeable', + name: { + 'zh-CN': '国际化', + 'en-US': 'localeable', + }, + desc: { + 'zh-CN': + '

    通过属性localeable配置是否开启国际化。默认值为 true,开启国际化。

    ', + 'en-US': '', + }, + apis: ['TiInputNumberDirective.properties.localeable'], + }, + { + demoId: 'inputnumber-maxlength', + name: { + 'zh-CN': '可输入最大长度', + 'en-US': 'maxlength', + }, + desc: { + 'zh-CN': '

    通过属性maxlength配置输入的最大字符数。

    ', + 'en-US': '', + }, + }, + { + demoId: 'inputnumber-event', + name: { + 'zh-CN': '事件', + 'en-US': 'events', + }, + desc: { + 'zh-CN': + '

    当元素聚焦的时候触发focus事件。当元素失焦的时候触发blur事件。当元素内容发生变化的时候触发ngModelChange事件。

    ', + 'en-US': '', + }, + }, + ], +}; diff --git a/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.cn.md b/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.cn.md new file mode 100644 index 0000000..32b67f2 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.cn.md @@ -0,0 +1,27 @@ +--- +title: InputNumber 数字输入框 +--- +# InputNumber 数字输入框 + +
    + +InputNumber 是数字输入框指令。   + ++ 支持输入框数字保留精度、国际化等显示场景。 + +```typescript +import { TiInputNumberModule } from '@opentiny/ng'; +``` + +
    + +
    + +InputNumber 是数字输入框指令。   + ++ 支持输入框数字保留精度、国际化等显示场景。 + +```typescript +import { TiInputNumberModule } from '@cloud/tiny-config'; +``` +
    diff --git a/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.en.md b/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/inputnumber/demo/src/app/inputnumber/webdoc/inputnumber.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/inputnumber/demo/src/favicon.ico b/src/inputnumber/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/inputnumber/demo/src/index.html b/src/inputnumber/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/inputnumber/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/inputnumber/demo/src/main.ts b/src/inputnumber/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/inputnumber/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/inputnumber/demo/tsconfig.spec.json b/src/inputnumber/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/inputnumber/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/inputnumber/lib/index.ts b/src/inputnumber/lib/index.ts new file mode 100644 index 0000000..18fc0c7 --- /dev/null +++ b/src/inputnumber/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiInputNumberModule'; diff --git a/src/inputnumber/lib/ng-package.json b/src/inputnumber/lib/ng-package.json new file mode 100644 index 0000000..a6816eb --- /dev/null +++ b/src/inputnumber/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/inputnumber", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/inputnumber/lib/package.json b/src/inputnumber/lib/package.json new file mode 100644 index 0000000..3f0f99d --- /dev/null +++ b/src/inputnumber/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-inputnumber", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/forms": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/inputnumber/lib/project.json b/src/inputnumber/lib/project.json new file mode 100644 index 0000000..df423c9 --- /dev/null +++ b/src/inputnumber/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/inputnumber/lib", + "sourceRoot": "src/inputnumber/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/inputnumber"], + "options": { + "project": "src/inputnumber/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/inputnumber"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js inputnumber" + }, + { + "command": "ng default-build inputnumber" + }, + { + "command": "node build/clear-default-theme.js inputnumber" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/inputnumber && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build inputnumber && ng pack inputnumber && node build/publish.js inputnumber --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/inputnumber/lib/src/TiInputNumberDirective.ts b/src/inputnumber/lib/src/TiInputNumberDirective.ts new file mode 100644 index 0000000..2e107d7 --- /dev/null +++ b/src/inputnumber/lib/src/TiInputNumberDirective.ts @@ -0,0 +1,273 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { DefaultValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Directive, ElementRef, forwardRef, Input, Renderer2, Inject } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +import { TiLocaleFormat } from '@opentiny/ng-locale'; +import { Util } from '@opentiny/ng-utils'; +/** + * TiNumber 数字输入框指令 + * + * 该指令主要作用于输入框上,限制只能输入数字。用户可以通过设置 数字保留精度、是否国际化 来设置数字显示格式 + * + * 输入框处于焦点状态时,输入框中数字标准化显示。失去焦点时,根据用户配置是否支持国际化进行格式化显示 + * + * 目前JS可以解析的范围是[-2^53, 2^53],即16位数字。当超过16位整数时,此时数字范围已经超过JS解析方位,不能精确表示。 + * + */ +@Directive({ + // 指令元数据 注意:该指令和数字校验指令重复,后期开发tiny4需要修改组件名 + selector: '[tiNumber]', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TiInputNumberDirective), + multi: true + } + ], + host: { + '(input)': 'handleInput($event.target.value)', + '(focus)': 'focusFn();', + '(blur)': 'blurFn()' + } +}) +// The default ControlValueAccessor for writing a value and listening to changes on input elements. +// The accessor is used by the FormControlDirective, FormControlName, and NgModel directives. +export class TiInputNumberDirective extends DefaultValueAccessor { + /** + * 是否开启国际化 + */ + @Input() localeable: boolean = true; + /** + * 小数保留位数。使用 n +'数字' 形式,例如:'n4',代表保留4位小数。spinner 保持一致。 + * + * 不设置时,10.0.1 版本后小数保留位数最少 0 位,最多 3 位;10.0.0 版本前小数保留位数为 3 位 + */ + @Input() format: string; + private numberFormat: string = '1.0-3'; + private oldInputValue: string = ''; // 没有千位分隔符的input中的值 + private onChangeFn: (_: any) => void; + private element: HTMLInputElement; + private oldModel: number; + constructor(private renderer: Renderer2, private elementRef: ElementRef, @Inject(DOCUMENT) private document) { + super(renderer, elementRef, true); + this.element = this.elementRef.nativeElement; + } + + ngOnInit(): void { + if (!TiLocaleFormat.isInvalidFormat(this.format)) { + const precision: number = parseInt(this.format.slice(1), 10); + this.numberFormat = '1.' + precision + '-' + precision; + } + } + + // 检测当前值是否合法 如果合法 返回true;否则返回false. + private isValidInput(value: string): boolean { + const decimalSep: string = this.localeable ? TiLocaleFormat.getNumberSymbol('Decimal') : '.'; // 当前局点小数点的形式 + let regex: any; // regex是正则表达式,不明其类型,所以 + if (decimalSep === ',') { + regex = /^\-{0,1}\d{0,},{0,1}\d{0,}$/; + } else if (decimalSep === '.') { + regex = /^\-{0,1}\d{0,}\.{0,1}\d{0,}$/; + } + + return regex.test(value); + } + /** + * 功能描述:当该值合法,直接返回. 如果不合法,则返回之前保存的有效值。 + * @memberOf TiInputNumberDirective + */ + private getCorrectValue(value: string): string { + if (!this.isValidInput(value)) { + return this.oldInputValue; + } + + return value; + } + // 根据精度和是否支持国际化,格式化数字 + private formatValue(value: number): string { + if (Number.isNaN(value)) { + return; + } + // https://angular.cn/api/common/DecimalPipe + // digitsInfo: {minIntegerDigits}.{minFractionDigits}-{maxFractionDigits} + // minIntegerDigits: 小数点前最小位数 默认为1 + // minFractionDigits: 小数点后最小位数 默认0 + // maxFractionDigits: 小数点后最大位数 默认3 + const localeValue: string = TiLocaleFormat.formatNumber(value, this.numberFormat); + if (this.localeable) { + return this.document.activeElement === this.element ? TiLocaleFormat.deleteGroupSep(localeValue) : localeValue; + } else { + // 不支持国际化时,也可利用TiLocaleFormat.formatNumber精度处理能力 + return TiLocaleFormat.parseNumber(localeValue).toString(); + } + } + // 将国际化数字转换为标准化数字 + private parseValue(value: string): number { + return this.localeable ? TiLocaleFormat.parseNumber(value) : parseFloat(value); + } + // 将国际化数字的千位分隔符去掉(输入框聚焦状态需要) + private deleteGroupSepValue(formatValue: string): string { + return this.localeable ? TiLocaleFormat.deleteGroupSep(formatValue) : formatValue; + } + /** + * @ignore + * 实现继承来自父类的方法 + * Sets the "value" property on the input element. + * model->view + */ + writeValue(value: any): void { + // writeValue()方法会执行两遍。第一遍为null, 第二遍为用户配置的数据。 + if (value === null) { + return; + } + + // 由于10.0.0及之前版本中开发者只能给ngModel传入空字符串时才能清空值,但是该组件ngModel应该是传入number类型,所以要清空时应该设置undefined。 + // 10.0.1版本开始进行纠正,为了兼容旧版本,设置空字符串时也能清空。 + if (value === undefined || value === '') { + super.writeValue(''); + this.oldInputValue = ''; + this.oldModel = undefined; + return; + } + + let normalValue: number; + if (Number.isNaN(parseFloat(value))) { + if (this.oldModel === undefined) { + super.writeValue(''); + this.oldInputValue = ''; + } else { + const formatValue: string = this.formatValue(this.oldModel); + super.writeValue(formatValue); + this.oldInputValue = this.deleteGroupSepValue(formatValue); + } + normalValue = this.oldModel; + } else { + const formatValue: string = this.formatValue(value); + super.writeValue(formatValue); + this.oldInputValue = this.deleteGroupSepValue(formatValue); + normalValue = this.parseValue(formatValue); + } + + if (normalValue !== value) { + if (Util.isUndefined(this.onChangeFn)) { + // 在reactive-form中使用,初始化赋值调用writeValue时, + // 此时registerOnChange还未被调用,onChangeFn还未被赋值, + // 所以要使用setTimeout等onChangeFn被赋值后再调用 + setTimeout(() => { + this.changeModel(normalValue); + }, 0); + } else { + this.changeModel(normalValue); + } + } else { + this.oldModel = normalValue; + } + } + /** + * @ignore + * Registers a function called when the control value changes + * @param fn The callback function + * 注册当控件接收到change事件之后,调用的函数fn + * viewValue和model value值的同步 + */ + registerOnChange(fn: (value: any) => void): void { + this.onChangeFn = fn; + } + + /** + * @ignore + * view -> model + * 非法字符不能输入。如果是合法则更新,如果是非法,则设置为oldInputValue... + */ + public handleInput(value: string): void { + // IE9 cut delete backspace下支持这三种方式引起的改变。 + // text组件针对这三种方式做了兼容性处理,并且触发input事件 + // 所以 inputnumber无需再做兼容性处理 + this.parser(value); + } + + private parser(value: string): void { + const validationValue: string = this.getCorrectValue(value); + if (!this.isValidInput(value)) { + let cursorPos: number = this.element.selectionStart; + const diff: number = value.length - validationValue.length; + cursorPos = cursorPos - diff; + this.renderer.setProperty(this.element, 'value', this.oldInputValue); // Sets the "value" property on the input element. + this.element.setSelectionRange(cursorPos, cursorPos); + } else { + this.oldInputValue = validationValue; + const parseValue: number = this.parseValue(validationValue); + this.changeModel(Number.isNaN(parseValue) ? undefined : parseValue); + } + } + + /** + * @ignore + * 得到焦点数据标准化处理 + * @memberOf TiInputNumberDirective + * renderer.setProperty: Implement this callback to set the value of a property of an element in the DOM. + */ + public focusFn(): void { + if (this.element.value === '') { + return; + } + // 为了得到各个浏览器下正确的光标位置,所以添加延时处理 + if (this.localeable) { + setTimeout(() => { + const start: number = this.element.selectionStart; + const end: number = this.element.selectionEnd; + if (end - start === this.element.value.length) { + this.renderer.setProperty(this.element, 'value', TiLocaleFormat.deleteGroupSep(this.element.value)); + this.element.setSelectionRange(0, this.element.value.length); + } else { + const groupSep: string = TiLocaleFormat.getNumberSymbol('Group'); + const old: string = this.element.value; + const sub: string = old.substr(0, start); + const groupSepNum: number = sub.split(groupSep).length - 1; + + this.renderer.setProperty(this.element, 'value', TiLocaleFormat.deleteGroupSep(this.element.value)); + this.element.setSelectionRange(start - groupSepNum, start - groupSepNum); + } + }, 0); + } + } + + /** + * @ignore + * 失去焦点数字国际化处理 + * @memberOf TiInputNumberDirective + */ + public blurFn(): void { + const value: number = this.parseValue(this.element.value); + if (Number.isNaN(value)) { + this.renderer.setProperty(this.element, 'value', ''); + this.oldInputValue = ''; + return; + } + const formatValue: string = this.formatValue(value); + this.renderer.setProperty(this.element, 'value', formatValue); + this.oldInputValue = this.deleteGroupSepValue(formatValue); + const normalValue: number = this.parseValue(formatValue); + if (value !== normalValue) { + this.changeModel(normalValue); + } + } + + private changeModel(model: number): void { + if (model !== this.oldModel) { + this.onChangeFn(model); + this.oldModel = model; + } + } +} diff --git a/src/inputnumber/lib/src/TiInputNumberModule.ts b/src/inputnumber/lib/src/TiInputNumberModule.ts new file mode 100644 index 0000000..0bf105d --- /dev/null +++ b/src/inputnumber/lib/src/TiInputNumberModule.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiInputNumberDirective } from './TiInputNumberDirective'; + +@NgModule({ + imports: [CommonModule], + exports: [TiInputNumberDirective], + declarations: [TiInputNumberDirective] +}) +export class TiInputNumberModule {} +export { TiInputNumberDirective } from './TiInputNumberDirective'; diff --git a/src/intro/demo/karma.conf.js b/src/intro/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/intro/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/intro/demo/project.json b/src/intro/demo/project.json new file mode 100644 index 0000000..19336a9 --- /dev/null +++ b/src/intro/demo/project.json @@ -0,0 +1,98 @@ +{ + "projectType": "application", + "root": "src/intro/demo", + "sourceRoot": "src/intro/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/intro", + "index": "src/intro/demo/src/index.html", + "main": "src/intro/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/intro/demo/tsconfig.app.json", + "assets": [ + "src/intro/demo/src/favicon.ico", + "src/intro/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/food", + "output": "/assets/food/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "intro-demo:build:production" + }, + "development": { + "browserTarget": "intro-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js intro" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/intro/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/intro/demo/tsconfig.spec.json", + "karmaConfig": "src/intro/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/intro/demo/src/app/AppComponent.ts b/src/intro/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/intro/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/intro/demo/src/app/AppModule.ts b/src/intro/demo/src/app/AppModule.ts new file mode 100644 index 0000000..c65ff5c --- /dev/null +++ b/src/intro/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { IntroTestModule } from './intro/IntroTestModule'; + +@NgModule({ + imports: [ + IntroTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/intro/demo/src/app/IndexComponent.ts b/src/intro/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..2c30ca0 --- /dev/null +++ b/src/intro/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { IntroTestModule } from './intro/IntroTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = IntroTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/intro/demo/src/app/app.html b/src/intro/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/intro/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/intro/demo/src/app/intro/IntroBasicComponent.ts b/src/intro/demo/src/app/intro/IntroBasicComponent.ts new file mode 100644 index 0000000..8e0fc86 --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroBasicComponent.ts @@ -0,0 +1,66 @@ +import { AfterViewInit, Component, ElementRef, TemplateRef, ViewChild } from '@angular/core'; +import { TiIntroRef, TiIntroService, TiIntroStep } from '@opentiny/ng'; + +@Component({ + templateUrl: './intro-basic.html' +}) +export class IntroBasicComponent implements AfterViewInit { + @ViewChild('introStep1', { static: true }) introStep1: ElementRef; + @ViewChild('introStep2', { static: true }) introStep2: ElementRef; + @ViewChild('introStep3', { static: true }) introStep3: ElementRef; + @ViewChild('introStep4', { static: true }) introStep4: ElementRef; + + @ViewChild('step3Tem', { static: true }) public step3Tem: TemplateRef; + + constructor(private TiIntro: TiIntroService) {} + + intro: TiIntroRef; + + ngAfterViewInit(): void { + const steps: Array = [ + // 第一步没有指定element,则以弹窗的形式呈现 + { + step: 0, + title: '总览页标题', + content: '总览页内容' + }, + { + element: this.introStep1.nativeElement, + step: 1, + title: '第一条标题', + content: '可通过 position 属性设置提示的位置:此处设置为bottom-left', + position: 'bottom-left' + }, + { + element: this.introStep2.nativeElement, + step: 2, + title: '第二条标题', + content: '可通过 shape 属性设置高亮区域的形状: 此处设置为圆形', + shape: 'circle' + }, + { + element: this.introStep3.nativeElement, + step: 3, + title: '第三条标题', + content: this.step3Tem, + position: 'left' + }, + { + element: this.introStep4.nativeElement, + step: 4, + title: '第四条标题', + content: '第四条内容' + } + ]; + + this.intro = this.TiIntro.create({ + id: 'intro_id', + steps, + skipable: true // 可配置是否显示跳过按钮 + }); + } + + start(): void { + this.intro.start(); + } +} diff --git a/src/intro/demo/src/app/intro/IntroEventComponent.ts b/src/intro/demo/src/app/intro/IntroEventComponent.ts new file mode 100644 index 0000000..bc1a279 --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroEventComponent.ts @@ -0,0 +1,66 @@ +import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; +import { TiIntroRef, TiIntroService, TiIntroStep } from '@opentiny/ng'; + +@Component({ + templateUrl: './intro-event.html' +}) +export class IntroEventComponent implements AfterViewInit { + @ViewChild('introStep1', { static: true }) introStep1: ElementRef; + @ViewChild('introStep2', { static: true }) introStep2: ElementRef; + @ViewChild('introStep3', { static: true }) introStep3: ElementRef; + @ViewChild('introStep4', { static: true }) introStep4: ElementRef; + + constructor(private TiIntro: TiIntroService) {} + + intro: TiIntroRef; + myLogs: Array = []; + + ngAfterViewInit(): void { + const steps: Array = [ + { + step: 0, + title: '第一条标题', + content: '第一条内容' + }, + { + element: this.introStep2.nativeElement, + step: 1, + title: '第二条标题', + content: '第二条内容' + }, + { + element: this.introStep3.nativeElement, + step: 2, + title: '第三条标题', + content: '第三条内容', + position: 'left' + }, + { + element: this.introStep4.nativeElement, + step: 3, + title: '第四条标题', + content: '第四条内容' + } + ]; + + this.intro = this.TiIntro.create({ + steps, + finishButtonText: '马上开始', + beforeStep: (introRef: TiIntroRef, currentNumber: number): void => { + this.myLogs = [...this.myLogs, `beforeStep:即将进入新手引导的${currentNumber}步`]; + introRef.proceed(); + }, + onFinish: (): void => { + this.myLogs = [...this.myLogs, 'onFinish:看完所有引导信息']; + }, + onExit: (): void => { + this.myLogs = [...this.myLogs, 'onExit:退出引导']; + }, + skipable: true + }); + } + + start(): void { + this.intro.start(); + } +} diff --git a/src/intro/demo/src/app/intro/IntroModalComponent.ts b/src/intro/demo/src/app/intro/IntroModalComponent.ts new file mode 100644 index 0000000..f66b718 --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroModalComponent.ts @@ -0,0 +1,54 @@ +import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; +import { TiIntroRef, TiIntroService, TiIntroStep } from '@opentiny/ng'; + +@Component({ + templateUrl: './intro-modal.html' +}) +export class IntroModalComponent implements AfterViewInit { + @ViewChild('introStep1', { static: true }) introStep1: ElementRef; + @ViewChild('introStep2', { static: true }) introStep2: ElementRef; + @ViewChild('introStep3', { static: true }) introStep3: ElementRef; + + constructor(private TiIntro: TiIntroService) {} + intro: TiIntroRef; + + ngAfterViewInit(): void { + const steps: Array = [ + { + step: 0, + title: '总览页标题', + content: '总览页内容' + }, + { + element: this.introStep1.nativeElement, + step: 1, + title: '第一条标题', + content: '第一条内容', + position: 'bottom-left' + }, + { + element: this.introStep2.nativeElement, + step: 2, + title: '第二条标题', + content: '第二条内容' + }, + { + element: this.introStep3.nativeElement, + step: 3, + title: '第三条', + content: '第三条内容', + position: 'left' + } + ]; + + this.intro = this.TiIntro.create({ + steps + }); + + this.intro.start(); + } + + start(): void { + this.intro.start(); + } +} diff --git a/src/intro/demo/src/app/intro/IntroSkipableComponent.ts b/src/intro/demo/src/app/intro/IntroSkipableComponent.ts new file mode 100644 index 0000000..1240694 --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroSkipableComponent.ts @@ -0,0 +1,57 @@ +import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; +import { TiIntroRef, TiIntroService, TiIntroStep } from '@opentiny/ng'; + +@Component({ + templateUrl: './intro-skipable.html' +}) +export class IntroSkipableComponent implements AfterViewInit { + @ViewChild('introStep1', { static: true }) introStep1: ElementRef; + @ViewChild('introStep2', { static: true }) introStep2: ElementRef; + @ViewChild('introStep3', { static: true }) introStep3: ElementRef; + constructor(private TiIntro: TiIntroService) {} + intro: TiIntroRef; + ngAfterViewInit(): void { + const steps: Array = [ + { + element: this.introStep1.nativeElement, + step: 0, + title: '第一条标题', + content: '第一条内容', + position: 'bottom-left' + }, + { + element: this.introStep2.nativeElement, + step: 1, + title: '第二条标题', + content: '第二条内容', + position: 'bottom-left' + }, + { + element: this.introStep3.nativeElement, + step: 2, + title: '第三条标题', + content: '第三条内容', + position: 'left' + } + ]; + + this.intro = this.TiIntro.create({ + id: 'intro_id', + steps, + skipable: true + }); + + /** + * 此处使用延时,是因为组件示例的component和展示源码的tabs组件是兄弟元素, + * 组件ViewInit被调用时,tabs没有渲染完成,页面高度没有完全被撑开,遮罩层的高度不足 + * 实际业务不需要setTimeout + */ + setTimeout(() => { + this.intro.start(); + }, 0); + } + + start(): void { + this.intro.start(); + } +} diff --git a/src/intro/demo/src/app/intro/IntroTemplateComponent.ts b/src/intro/demo/src/app/intro/IntroTemplateComponent.ts new file mode 100644 index 0000000..c4fdfd8 --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroTemplateComponent.ts @@ -0,0 +1,58 @@ +import { AfterViewInit, Component, ElementRef, TemplateRef, ViewChild } from '@angular/core'; +import { TiIntroRef, TiIntroService, TiIntroStep } from '@opentiny/ng'; + +@Component({ + templateUrl: './intro-template.html' +}) +export class IntroTemplateComponent implements AfterViewInit { + @ViewChild('introStep1', { static: true }) introStep1: ElementRef; + @ViewChild('introStep2', { static: true }) introStep2: ElementRef; + @ViewChild('introStep3', { static: true }) introStep3: ElementRef; + + @ViewChild('introTem', { static: true }) introTem: TemplateRef; + @ViewChild('modalTemp') modalTemp: TemplateRef; + constructor(private TiIntro: TiIntroService) {} + intro: TiIntroRef; + isNotShowagain: boolean = false; + ngAfterViewInit(): void { + const steps: Array = [ + { + step: 0, + title: '总览页标题', + content: this.modalTemp + }, + { + element: this.introStep1.nativeElement, + step: 1, + title: '第一条标题', + content: '第一条内容', + position: 'bottom-left' + }, + { + element: this.introStep2.nativeElement, + step: 2, + title: '第二条标题', + content: this.introTem + }, + { + element: this.introStep3.nativeElement, + step: 3, + title: '第三条', + content: '第三条内容', + position: 'left' + } + ]; + + this.intro = this.TiIntro.create({ + steps + }); + + this.intro.start(); + } + + start(): void { + if (!this.isNotShowagain) { + this.intro.start(); + } + } +} diff --git a/src/intro/demo/src/app/intro/IntroTestModule.ts b/src/intro/demo/src/app/intro/IntroTestModule.ts new file mode 100644 index 0000000..3af8e9d --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroTestModule.ts @@ -0,0 +1,67 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiCheckboxModule, TiIconModule, TiIntroServiceModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { IntroBasicComponent } from './IntroBasicComponent'; +import { IntroTipComponent } from './IntroTipComponent'; +import { IntroEventComponent } from './IntroEventComponent'; +import { IntroModalComponent } from './IntroModalComponent'; +import { IntroTemplateComponent } from './IntroTemplateComponent'; +import { IntroTiscrollComponent } from './IntroTiscrollComponent'; +import { IntroSkipableComponent } from './IntroSkipableComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiButtonModule, + TiCheckboxModule, + TiIntroServiceModule, + TiIconModule, + DemoLogModule, + RouterModule.forChild(IntroTestModule.ROUTES) + ], + declarations: [ + IntroBasicComponent, + IntroTipComponent, + IntroEventComponent, + IntroModalComponent, + IntroTemplateComponent, + IntroTiscrollComponent, + IntroSkipableComponent + ] +}) +export class IntroTestModule { + static readonly LINKS: Array = [{ href: 'components/TiIntroComponent.html', label: 'Intro' }]; + static readonly ROUTES: Routes = [ + { + path: 'intro/intro-basic', + component: IntroBasicComponent + }, + { + path: 'intro/intro-tip', + component: IntroTipComponent + }, + { + path: 'intro/intro-modal', + component: IntroModalComponent + }, + { + path: 'intro/intro-template', + component: IntroTemplateComponent + }, + { + path: 'intro/intro-skipable', + component: IntroSkipableComponent + }, + { + path: 'intro/intro-event', + component: IntroEventComponent + }, + { path: 'intro/intro-tiscroll', component: IntroTiscrollComponent } + ]; +} diff --git a/src/intro/demo/src/app/intro/IntroTipComponent.ts b/src/intro/demo/src/app/intro/IntroTipComponent.ts new file mode 100644 index 0000000..c54dbb1 --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroTipComponent.ts @@ -0,0 +1,74 @@ +import { AfterViewInit, Component, ElementRef, TemplateRef, ViewChild } from '@angular/core'; +import { TiIntroRef, TiIntroService, TiIntroStep } from '@opentiny/ng'; + +@Component({ + templateUrl: './intro-tip.html' +}) +export class IntroTipComponent implements AfterViewInit { + @ViewChild('introStep1', { static: true }) introStep1: ElementRef; + @ViewChild('introStep2', { static: true }) introStep2: ElementRef; + @ViewChild('introStep3', { static: true }) introStep3: ElementRef; + @ViewChild('introStep4', { static: true }) introStep4: ElementRef; + @ViewChild('introStep5', { static: true }) introStep5: ElementRef; + @ViewChild('finish', { static: true }) finish: ElementRef; + + constructor(private TiIntro: TiIntroService) {} + intro: TiIntroRef; + ngAfterViewInit(): void { + const steps: Array = [ + { + element: this.introStep1.nativeElement, + step: 0, + title: '第一条标题', + content: '第一条内容', + position: 'bottom-left' + }, + { + element: this.introStep2.nativeElement, + step: 1, + title: '第二条标题', + content: '第二条内容' + }, + { + element: this.introStep3.nativeElement, + step: 2, + title: '第三条', + content: '第三条内容', + position: 'left' + }, + { + element: this.introStep4.nativeElement, + step: 3, + content: '第四条内容,以tip形式显示' + }, + { + element: this.introStep5.nativeElement, + step: 3, + shape: 'circle', + content: '第五条内容,以tip形式显示' + }, + { + element: this.finish.nativeElement, + step: 3, + isAction: true // 标识为结束该步的操作按钮 + } + ]; + + this.intro = this.TiIntro.create({ + steps + }); + + /** + * 此处使用延时,是因为组件示例的component和展示源码的tabs组件是兄弟元素, + * 组件ViewInit被调用时,tabs没有渲染完成,页面高度没有完全被撑开,遮罩层的高度不足 + * 实际业务不需要setTimeout + */ + setTimeout(() => { + this.intro.start(); + }, 0); + } + + start(): void { + this.intro.start(); + } +} diff --git a/src/intro/demo/src/app/intro/IntroTiscrollComponent.ts b/src/intro/demo/src/app/intro/IntroTiscrollComponent.ts new file mode 100644 index 0000000..29f58f1 --- /dev/null +++ b/src/intro/demo/src/app/intro/IntroTiscrollComponent.ts @@ -0,0 +1,37 @@ +import { AfterViewInit, Component, ElementRef, TemplateRef, ViewChild } from '@angular/core'; +import { TiIntroRef, TiIntroService, TiIntroStep, TiMessageService } from '@opentiny/ng'; + +@Component({ + templateUrl: './intro-tiscroll.html' +}) +export class IntroTiscrollComponent implements AfterViewInit { + @ViewChild('introStep1', { static: true }) introStep1: ElementRef; + @ViewChild('introStep2', { static: true }) introStep2: ElementRef; + constructor(private TiIntro: TiIntroService, tiMessage: TiMessageService) {} + intro: TiIntroRef; + ngAfterViewInit(): void { + const steps: Array = [ + { + element: this.introStep1.nativeElement, + step: 0, + title: '第一条标题', + content: '第一条内容', + position: 'bottom-left' + }, + { + element: this.introStep2.nativeElement, + step: 1, + title: '第二条标题', + content: '第二条内容' + } + ]; + + this.intro = this.TiIntro.create({ + id: 'intro_id', + steps + }); + } + start(): void { + this.intro.start(); + } +} diff --git a/src/intro/demo/src/app/intro/intro-basic.html b/src/intro/demo/src/app/intro/intro-basic.html new file mode 100644 index 0000000..d760eb8 --- /dev/null +++ b/src/intro/demo/src/app/intro/intro-basic.html @@ -0,0 +1,10 @@ + + +
    + 1.位置 + 2.形状 + 3.模板 + 4:最后一步 +
    + + 自定义模板 diff --git a/src/intro/demo/src/app/intro/intro-event.html b/src/intro/demo/src/app/intro/intro-event.html new file mode 100644 index 0000000..980a3c0 --- /dev/null +++ b/src/intro/demo/src/app/intro/intro-event.html @@ -0,0 +1,10 @@ + + +
    + 第一步 + 第二步 + 第三步 + 第四步 +
    + + diff --git a/src/intro/demo/src/app/intro/intro-modal.html b/src/intro/demo/src/app/intro/intro-modal.html new file mode 100644 index 0000000..f39bfcc --- /dev/null +++ b/src/intro/demo/src/app/intro/intro-modal.html @@ -0,0 +1,22 @@ +

    描述

    +

    第一步是总览页

    +

    示例

    + + +
    + 1号新功能 + 2号新功能 + 3号新功能 +
    + diff --git a/src/intro/demo/src/app/intro/intro-skipable.html b/src/intro/demo/src/app/intro/intro-skipable.html new file mode 100644 index 0000000..994f802 --- /dev/null +++ b/src/intro/demo/src/app/intro/intro-skipable.html @@ -0,0 +1,24 @@ +

    描述

    +

    是否显示跳过按钮(可跳过/退出引导),设置skipable属性

    +

    示例

    + + +
    + 1号新功能 + 2号新功能 + 3号新功能 +
    + diff --git a/src/intro/demo/src/app/intro/intro-template.html b/src/intro/demo/src/app/intro/intro-template.html new file mode 100644 index 0000000..3fd18b2 --- /dev/null +++ b/src/intro/demo/src/app/intro/intro-template.html @@ -0,0 +1,29 @@ +

    描述

    +

    自定义content内容的功能

    +

    示例

    + +下次是否显示引导信息:{{!isNotShowagain}} + +
    + 1号新功能 + 2号新功能 + 3号新功能 +
    + + +
    + +
    + + + diff --git a/src/intro/demo/src/app/intro/intro-tip.html b/src/intro/demo/src/app/intro/intro-tip.html new file mode 100644 index 0000000..07113b9 --- /dev/null +++ b/src/intro/demo/src/app/intro/intro-tip.html @@ -0,0 +1,45 @@ +

    描述

    +

    第一步是总览页,并且同时显示多个引导信息

    +

    只支持在最后一步显示多个引导信息,且是以tip的形式呈现,需要传入关闭引导的元素

    +

    示例

    + + + +
    + 1号新功能 + 2号新功能(固定定位) + 3号新功能(transform定位) + 4号新功能(不在视口) + 5号 +
    + + + + diff --git a/src/intro/demo/src/app/intro/intro-tiscroll.html b/src/intro/demo/src/app/intro/intro-tiscroll.html new file mode 100644 index 0000000..12d38da --- /dev/null +++ b/src/intro/demo/src/app/intro/intro-tiscroll.html @@ -0,0 +1,27 @@ +

    描述

    +

    在控制台输入触发tiScroll的代码,intro不能消失

    +

    示例

    + + +

    控制台触发tiScroll事件

    +

    + const event = document.createEvent('HTMLEvents');
    + event.initEvent('tiScroll')
    + document.dispatchEvent(event);
    +

    + +
    + 1号新功能 + 2号新功能 +
    + diff --git a/src/intro/demo/src/app/intro/webdoc/intro-demos.js b/src/intro/demo/src/app/intro/webdoc/intro-demos.js new file mode 100644 index 0000000..e8ba0e8 --- /dev/null +++ b/src/intro/demo/src/app/intro/webdoc/intro-demos.js @@ -0,0 +1,47 @@ +export default { + column: '2', + demos: [ + { + demoId: 'intro-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    通过create方法创建 intro 实例。

    ', + 'en-US': '

    basic

    ', + }, + apis: [ + 'TiIntroService.methods.create', + 'TiIntroStep.properties.element', + 'TiIntroStep.properties.step', + 'TiIntroStep.properties.shape', + 'TiIntroStep.properties.title', + 'TiIntroStep.properties.content', + 'TiIntroStep.properties.position', + 'TiIntroStep.properties.isAction', + ], + }, + { + demoId: 'intro-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event', + }, + desc: { + 'zh-CN': + '

    当每一步打开前的时候触发beforeStep事件,当完成引导的时候触发onFinish事件,当退出引导的时候触发onExit事件。

    ', + 'en-US': '

    event

    ', + }, + apis: [ + 'TiIntroConfig.properties.steps', + 'TiIntroConfig.properties.skipable', + 'TiIntroConfig.methods.beforeStep', + 'TiIntroConfig.methods.onExit', + 'TiIntroConfig.methods.onFinish', + 'TiIntroConfig.properties.finishButtonText', + 'TiIntroConfig.properties.id', + ], + }, + ], +}; diff --git a/src/intro/demo/src/app/intro/webdoc/intro.cn.md b/src/intro/demo/src/app/intro/webdoc/intro.cn.md new file mode 100644 index 0000000..e7670ce --- /dev/null +++ b/src/intro/demo/src/app/intro/webdoc/intro.cn.md @@ -0,0 +1,30 @@ +--- +title: Intro 新手引导 +--- +# Intro 新手引导 + +
    + +Intro 是提供服务中内容(入口)新增和变动等指引提醒的组件。   + +```typescript +import { TiIntroServiceModule } from '@opentiny/ng'; +``` + +需要在项目中(建议在根模块)中导入`BrowserAnimationsModule`。 + +```typescript +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +``` + +
    + +
    + +Intro 是提供服务中内容(入口)新增和变动等指引提醒的组件。   + +```typescript +import { TiIntroServiceModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/intro/demo/src/app/intro/webdoc/intro.en.md b/src/intro/demo/src/app/intro/webdoc/intro.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/intro/demo/src/app/intro/webdoc/intro.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/intro/demo/src/favicon.ico b/src/intro/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/intro/demo/src/index.html b/src/intro/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/intro/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/intro/demo/src/main.ts b/src/intro/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/intro/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/intro/demo/test.ts b/src/intro/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/intro/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/intro/demo/tsconfig.app.json b/src/intro/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/intro/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/intro/demo/tsconfig.spec.json b/src/intro/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/intro/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/intro/lib/index.ts b/src/intro/lib/index.ts new file mode 100644 index 0000000..060772c --- /dev/null +++ b/src/intro/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiIntroModule'; +export * from './src/TiIntroService'; diff --git a/src/intro/lib/ng-package.json b/src/intro/lib/ng-package.json new file mode 100644 index 0000000..ddd4a1f --- /dev/null +++ b/src/intro/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/intro", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/intro/lib/package.json b/src/intro/lib/package.json new file mode 100644 index 0000000..54cd9c8 --- /dev/null +++ b/src/intro/lib/package.json @@ -0,0 +1,17 @@ +{ + "name": "@opentiny/ng-intro", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-modal": "~1.0.0-beta.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/intro/lib/project.json b/src/intro/lib/project.json new file mode 100644 index 0000000..82a424b --- /dev/null +++ b/src/intro/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/intro/lib", + "sourceRoot": "src/intro/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/intro"], + "options": { + "project": "src/intro/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/intro"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js intro" + }, + { + "command": "ng default-build intro" + }, + { + "command": "node build/clear-default-theme.js intro" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/intro && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build intro && ng pack intro && node build/publish.js intro --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/intro/lib/src/TiIntroInterface.ts b/src/intro/lib/src/TiIntroInterface.ts new file mode 100644 index 0000000..1216692 --- /dev/null +++ b/src/intro/lib/src/TiIntroInterface.ts @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiPositionType } from '@opentiny/ng-utils'; +import { TemplateRef } from '@angular/core'; + +export type TiIntroShape = 'rect' | 'circle'; + +/** + * 某一步的intro信息对象 + */ +export interface TiIntroStep { + /** + * 被引导元素,第一步没有指定 element 则以弹窗的形式呈现 + */ + element?: any; + /** + * 必选,第几步引导,从 0 开始 + */ + step: number; + /** + * 元素被引导时高亮区域的形状 + */ + shape?: TiIntroShape; + /** + * 引导信息标题 + */ + title?: string; + /** + * 引导信息内容 + */ + content?: string | TemplateRef; + /** + * 引导信息方向 + */ + position?: TiPositionType; + /** + * @ignore + * 是否为关闭多个 tip 的操作元素 + */ + isAction?: boolean; +} + +export interface TiIntroConfig { + /** + * 组件整体 id + */ + id?: string; + /** + * 必选,引导数据集 + */ + steps: Array; + /** + * 最后一步引导信息的重要按钮文本 + */ + finishButtonText?: string; + /** + * 是否显示跳过按钮 + */ + skipable?: boolean; + /** + * 每一步引导前触发的回调 + */ + beforeStep?(TiIntroRef: TiIntroRef, currentNumber?: number): void; + /** + * 完成引导信息时触发的回调 + */ + onFinish?(): void; + /** + * 退出引导触发的回调 + */ + onExit?(): void; +} +export interface TiIntroRef { + /** + * 开始新手引导(intro)的方法 + * + * **函数类型:**() => void; + */ + start(): void; + /** + * 结束新手引导(intro)的方法 + * + * **函数类型:**() => void; + */ + end(): void; + /** + * 跳转至某一步的方法 + * + * **函数类型:**() => void; + */ + step(number: number): void; + /** + * 在beforeStep中调用introRef.proceed(),以便进入该步,否则不会进入 + * + * **函数类型:**() => void; + */ + proceed(): void; +} diff --git a/src/intro/lib/src/TiIntroModule.ts b/src/intro/lib/src/TiIntroModule.ts new file mode 100644 index 0000000..052268b --- /dev/null +++ b/src/intro/lib/src/TiIntroModule.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TiButtonModule } from '@opentiny/ng-button'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiModalModule } from '@opentiny/ng-modal'; +import { TiTipServiceModule } from '@opentiny/ng-tip'; +import { TiLocaleModule, TiLocale } from '@opentiny/ng-locale'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiIntromodalComponent } from './TiIntromodalComponent'; +import { TiIntrotipComponent } from './TiIntrotipComponent'; +import { locales } from './i18n'; + +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule, TiButtonModule, TiIconModule, TiLocaleModule, TiModalModule, TiTipServiceModule, TiOutlineModule], + declarations: [TiIntrotipComponent, TiIntromodalComponent], + entryComponents: [TiIntrotipComponent, TiIntromodalComponent] +}) +export class TiIntroServiceModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiIntroConfig, TiIntroRef, TiIntroStep, TiIntroShape } from './TiIntroInterface'; diff --git a/src/intro/lib/src/TiIntroService.ts b/src/intro/lib/src/TiIntroService.ts new file mode 100644 index 0000000..3c3ba42 --- /dev/null +++ b/src/intro/lib/src/TiIntroService.ts @@ -0,0 +1,456 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +import { TiIntroServiceModule } from './TiIntroModule'; +import { TiModalRef } from '@opentiny/ng-modal'; +import { TiModalService } from '@opentiny/ng-modal'; +import { TiTipService } from '@opentiny/ng-tip'; +import { TiTipRef } from '@opentiny/ng-tip'; +import { Position, TiKeymap, TiLog, TiPositionEventType, Util } from '@opentiny/ng-utils'; + +import { TiIntromodalComponent } from './TiIntromodalComponent'; +import { TiIntrotipComponent } from './TiIntrotipComponent'; +import { TiIntroConfig, TiIntroRef, TiIntroShape, TiIntroStep } from './TiIntroInterface'; + +/** + * 新手引导组件,适用于服务中内容(入口)新增和变动等指引提醒 + * + *

    使用此组件时需要开发者在项目模块(建议在根模块)中引入BrowserAnimationsModule。 + * 这是因为此组件中使用了 TiModalService,TiModalService需要BrowserAnimationsModule + * (具体原因可以查看 [TiModalService]{@link ../injectables/TiModalService.html}), + * 但是 BrowserAnimationsModule 不能在懒加载模块被重复引入,所以需要开发者来引入BrowserAnimationsModule,保证其引入一次。

    + * + */ +@Injectable({ + providedIn: TiIntroServiceModule +}) +export class TiIntroService { + // 视觉规范调整:modal、halfmodal、intro的蒙层统一调整为1200;弹窗体、intro的气泡是1300 + private static readonly BACKDROP_Z_INDEX: number = 1200; + private static readonly TIP_Z_INDEX: number = 1300; + private render: Renderer2; + private unListenWindowResize: () => void; + private unListenWindowHashchange: () => void; + private closeMultiTipEleClickListener: () => void; + private unListenDocumentKeydown: () => void; + // 可聚焦元素 + private focusableElementsString: string = `a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), + button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), + textarea:not([disabled]):not([tabindex=\'-1\']), + iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]`; + constructor( + private tipService: TiTipService, + private modalService: TiModalService, + rendererFactory: RendererFactory2, + @Inject(DOCUMENT) private document + ) { + this.render = rendererFactory.createRenderer(null, null); + } + /** + * intro的公共配置,不是默认配置,不能修改 + */ + private commonConfig: any = { + trigger: 'manual', + theme: 'white', + maxWidth: '400px', + zIndex: TiIntroService.TIP_Z_INDEX, + registerVisibilityChangeEvent: false, + /** + * position在定位时注册了三个全局事件,intro都不需要,用空数组覆盖position中的事件类型数组 + */ + positionEventTypes: [] + }; + + private static clearCircle(x: number, y: number, radius: number, stepClear: number, ctx: any): void { + const calcWidth: number = radius - stepClear; + const calcHeight: number = Math.sqrt(radius * radius - calcWidth * calcWidth); + + const posX: number = x - calcWidth; + const posY: number = y - calcHeight; + + const widthX: number = calcWidth * 2; + const heightY: number = calcHeight * 2; + + if (stepClear < radius) { + ctx.clearRect(posX, posY, widthX, heightY); + const newStepClear: number = stepClear + 1; + TiIntroService.clearCircle(x, y, radius, newStepClear, ctx); + } + } + + private static scrollToViewport(element: any): void { + // 修复SSR错误:ERROR TypeError: element.getBoundingClientRect is not a function + if (typeof element.getBoundingClientRect !== 'function') { + return; + } + const layout: any = element.getBoundingClientRect(); + + const scrollTop: number = document.body.scrollTop || document.documentElement.scrollTop; + // 修复SSR报错:ERROR ReferenceError: window is not defined + if (typeof window === 'undefined') { + return; + } + if (layout.top < 0) { + // 上边界溢出屏幕 + window.scrollTo({ top: layout.top + scrollTop - 20 }); + } else if (layout.bottom > document.documentElement.clientHeight) { + // 下边界溢出屏幕 + window.scrollTo({ + top: scrollTop + layout.bottom + 20 - document.documentElement.clientHeight + }); + } + } + + /** + * 创建intro实例 + */ + public create(config: TiIntroConfig): TiIntroRef { + let introRef: TiIntroRef; // create方法创建并返回的intro实例 + const steps: Array = config.steps; + let introModalRef: TiModalRef; // modal实例,用于隐藏弹窗 + let introTipRef: TiTipRef; // 是生成的tip,有show & hide + let introMultiTipRefs: Array = []; // 多个tip场景下生成的tip实例数组 + let curStep: number = 0; // 在整个create方法中,标识当前步数 + let onResize: () => void; // 广义的resize事件,包括正常的resize和console UI顶部的弹出内容出现或隐藏 + let isAddConsoleDataOnChange: boolean = false; // 是否监听了 consoleDataService 的 OnChange 事件 + let consoleDataService: any; + const lastStep: number = steps[steps.length - 1].step; // 最后一TiIntroStep的step属性值,即总步数 + // 关闭多个tip的操作元素 + const closeMultiTipEle: Element = + steps.filter((item: TiIntroStep) => item.isAction).length > 0 + ? steps.filter((item: TiIntroStep) => item.isAction)[0].element + : undefined; + // 先隐藏closeMultiTipEle,并注册事件 + if (closeMultiTipEle) { + this.render.setStyle(closeMultiTipEle, 'display', 'none'); + } + this.unListenDocumentKeydown = this.render.listen(document, 'keydown', (event: KeyboardEvent): void => { + switch (event.which) { + case TiKeymap.KEY_TAB: // tab键用于处理在提示框内循环获取焦点 + this.clickTab(event); + break; + default: + break; + } + }); + + /** + * @param flag boolean true:完成intro引导;false:未完成intro引导,但是退出 + */ + const close: (flag: boolean) => void = (flag: boolean): void => { + if (introMultiTipRefs.length > 0) { + introMultiTipRefs.forEach((item: TiTipRef) => { + this.hidePreStep(item); + }); + introMultiTipRefs = []; + this.render.setStyle(closeMultiTipEle, 'display', 'none'); + } else { + this.hidePreStep(introTipRef); + } + + this.destroyBackdrop(); + + if (flag && typeof config.onFinish === 'function') { + config.onFinish(); + } else if (!flag && typeof config.onExit === 'function') { + config.onExit(); + } + if (this.unListenWindowResize) { + this.unListenWindowResize(); + this.unListenWindowResize = undefined; + } + if (this.unListenWindowHashchange) { + this.unListenWindowHashchange(); + this.unListenWindowHashchange = undefined; + } + if (this.closeMultiTipEleClickListener) { + this.closeMultiTipEleClickListener(); + } + if (this.unListenDocumentKeydown) { + this.unListenDocumentKeydown(); + this.unListenDocumentKeydown = undefined; + } + if (consoleDataService?.offChange) { + consoleDataService.offChange(onResize); + } + }; + + /** + * 无效步数条件: + * 1.不是有效数字 + * 2.等于currentNumber,等于0除外 + * 3.小于0 或 大于 totalNumber + * 使用curStep 和 lastStep,没有提为private方法 + */ + const isInvalidStepNum: (number: number) => boolean = (num: number): boolean => { + return isNaN(num) || (curStep === num && num !== 0) || num < 0 || num > lastStep; + }; + /** + * 即将进入某一步的引导 + * + * 需要在进入前做一些合法性判断,清除上一步信息等 + */ + const wantStep: (number: number) => void = (num: number): void => { + if (isInvalidStepNum(num)) { + TiLog.error('stepNumber is not valid'); + + return; + } + this.hidePreStep(introTipRef); + curStep = num; + // 如果是在console环境下,进入第一步时,onChange的回调函数传入onResize事件,会在console变化时,重新绘制intro和backdrop + if ((window).getConsoleContext && !isAddConsoleDataOnChange) { + consoleDataService = (window).getConsoleContext()?.get({ name: 'safearea' }); + // 头部高度变化会触发此事件 + if (consoleDataService?.onChange) { + consoleDataService.onChange(onResize); + isAddConsoleDataOnChange = true; + } + } + if (typeof config.beforeStep === 'function') { + config.beforeStep(introRef, num); + } else { + goStep(num); + } + // 延时为了保证遮罩dom已经绘制完毕。 + setTimeout((): void => { + // 在引导的时候,把焦点调整为遮罩层。为后续把焦点圈在提示框内做准备。 + const backdropEle: HTMLElement = document.querySelector('#ti3-intro-backdrop'); + if (backdropEle) { + backdropEle.focus(); + } + }, 100); + }; + + // 开始某一步的引导 + const goStep: (number: number) => void = (num: number): void => { + const currentSteps: Array = steps.filter((item: TiIntroStep) => item.step === num); + // curIntroStep curIntroStep + let currentStep: TiIntroStep; + if (currentSteps.length === 1) { + currentStep = currentSteps[0]; + } + // 先关闭之前的canvas + this.destroyBackdrop(); + /** + * 具体的引导步骤分为3种 + * 1.总览页: 一个正常弹窗,用modal实现 intromodal + * 2.分布页:带按钮的tip,用tipService实现 + * 3.提示: 没有按钮的普通tip,用tipService实现 + */ + // 第一种情况,弹窗 + if (currentStep && !currentStep.element) { + introModalRef = this.modalService.open(TiIntromodalComponent, { + id: 'ti3-intro-modal', + context: { + title: currentStep.title, + content: currentStep.content, + totalNumber: lastStep, + currentNumber: curStep, + finishButtonText: config.finishButtonText, + id: config.id + '_' + num, + close: (): void => { + introModalRef.close(); + }, + wantStep, + skipable: config.skipable + }, + dismiss(): void { + close(false); + }, + draggable: false + }); + + return; + } + // 如果有弹窗的话,关闭弹窗 + if (introModalRef) { + introModalRef._remove(); + introModalRef = undefined; + } + // 重新绘制canvas + this.drawBackdrop(); + + // 第二种情况,带按钮的tip + if (currentStep && currentStep.title) { + TiIntroService.scrollToViewport(currentStep.element); + introTipRef = this.tipService.create(currentStep.element, { + ...this.commonConfig, + position: currentStep.position + }); + introTipRef.show(TiIntrotipComponent, { + title: currentStep.title, + content: currentStep.content, + totalNumber: lastStep, + currentNumber: curStep, + finishButtonText: config.finishButtonText, + id: config.id + '_' + num, + close, + wantStep, + skipable: config.skipable + }); + + this.highlight(currentStep.element, currentStep.shape); + } + + // 第三种情况,普通tip,并且是多个,有一个是关闭按钮。和第二种情况是if else 的关系,重新列为一个if分支是为了让三种情况称为并列的if + if (currentSteps && currentSteps.length > 1) { + currentSteps.forEach((item: TiIntroStep): void => { + if (item.isAction) { + this.render.setStyle(item.element, 'display', 'block'); + this.closeMultiTipEleClickListener = this.render.listen(item.element, 'click', () => { + close(true); + }); + } else { + TiIntroService.scrollToViewport(item.element); + const introtipRef: TiTipRef = this.tipService.create(item.element, { + ...this.commonConfig, + position: item.position + }); + introtipRef.show(item.content); + introMultiTipRefs.push(introtipRef); + + this.highlight(item.element, item.shape); + } + }); + } + // 修复SSR报错:ERROR ReferenceError: window is not defined + if (!this.unListenWindowResize && typeof window !== 'undefined') { + this.unListenWindowResize = this.render.listen(window, 'resize', () => { + onResize(); + }); + } + if (!this.unListenWindowHashchange && typeof window !== 'undefined') { + this.unListenWindowHashchange = this.render.listen(window, 'hashchange', () => { + close(false); + }); + } + }; + // 定义resize事件 + onResize = () => { + this.hidePreStep(introTipRef); + this.destroyBackdrop(); + goStep(curStep); + }; + + // 对外暴露的intro实例 + introRef = { + start(): void { + wantStep(0); + }, + end(): void { + close(true); + }, + step: (number: number): void => { + wantStep(number); + }, + proceed(): void { + goStep(curStep); + } + }; + + return introRef; + } + /** + * @ignore + */ + public clickTab(event: KeyboardEvent): void { + const introModal: HTMLElement = document.querySelector('.ti3-intromodal-wrapper'); + const focusableElements: NodeList = introModal?.querySelectorAll(this.focusableElementsString); + + Util.focusInDialogOnTabchange(event, focusableElements); + } + private destroyBackdrop(): void { + const canvas: any = this.document.body.querySelector('#ti3-intro-backdrop'); + + if (canvas) { + // canvas.remove() IE报错;canvas.removeNode() Chrome报错;可以使用removeChild方法 + canvas.parentNode.removeChild(canvas); + } + } + + private hidePreStep(introTipRef: TiTipRef): void { + if (introTipRef) { + introTipRef.hide(); + } + } + + private drawBackdrop(): void { + if (typeof document === 'undefined') { + return; + } + const canvas: any = this.render.createElement('canvas'); + this.render.setStyle(canvas, 'z-index', TiIntroService.BACKDROP_Z_INDEX); + this.render.setStyle(canvas, 'position', 'absolute'); + this.render.setStyle(canvas, 'top', 0); + this.render.setStyle(canvas, 'left', 0); + this.render.setStyle(canvas, 'outline', 'none'); + this.render.setAttribute(canvas, 'tabindex', '0'); // 非表单元素想要使用focus()函数聚焦,需要设置tabindex属性。 + canvas.id = 'ti3-intro-backdrop'; + const ctx: any = canvas.getContext('2d'); + // _drawBackDrop(canvas, ctx) { + // 获取整个页面宽高,用于计算canvas宽高 + // 获取整个页面的宽高,包括滚动条部分及body margin等 + let browserWidth: number = this.document.documentElement.scrollWidth; + let browserHeight: number = this.document.documentElement.scrollHeight; + // 获取整个页面可视区域宽高,页面不出滚动条情况下,该值原则上应该是与上述browser宽高相等的 + // 但是由于浏览器的计算差异,可能存在不相等的情况 + const clientWidth: number = this.document.documentElement.clientWidth; + const clientHeight: number = this.document.documentElement.clientHeight; + + // 由于在IE下该宽高计算不精确,可能会导致页面本身无滚动条的情况下, + // 当使用该值设置canvas宽度后,出现滚动条,因此需要对宽高进行特殊处理 + // 根据多次试验经验,发现当实际页面实际宽度为小数时,clientWidth取值为去掉小数点后的值, + // scrollWidth则会自动+1,导致使用scrollWidth设置会出现滚动条, + // 因此,通过两者之差小于1的方式判断该情况下,取小值设置canvas宽度即可解决该问题 + if (browserWidth - clientWidth <= 1) { + browserWidth = clientWidth; + } + // 纵向情况下处理同上 + if (browserHeight - clientHeight <= 1) { + browserHeight = clientHeight; + } + + canvas.width = browserWidth; + canvas.height = browserHeight; + ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; + ctx.fillRect(0, 0, browserWidth, browserHeight); + + this.render.appendChild(this.document.body, canvas); + } + private highlight(element: any, shape: TiIntroShape): void { + const layout: any = Position.getHostEleLayout(element, element); + const canvas: any = this.document.body.querySelector('#ti3-intro-backdrop'); + // 修复SSR报错:ERROR TypeError: Cannot read property 'getContext' of undefined + if (!canvas) { + return; + } + const ctx: any = canvas.getContext('2d'); + const highlightLayout: any = { ...layout }; + if (layout.fixedAncestor) { + // 如果被高亮的元素是fixed定位,以元素相对于视口的位置为高亮区域的定位 + this.render.setStyle(canvas, 'position', 'fixed'); + highlightLayout.top = element.getBoundingClientRect().top; + highlightLayout.left = element.getBoundingClientRect().left; + } + if (shape === 'circle') { + const radius: number = (highlightLayout.width < highlightLayout.height ? highlightLayout.width : highlightLayout.height) / 2; + const x: number = highlightLayout.left + highlightLayout.width / 2; + const y: number = highlightLayout.top + highlightLayout.height / 2; + TiIntroService.clearCircle(x, y, radius, 1, ctx); + } else { + ctx.clearRect(highlightLayout.left, highlightLayout.top, highlightLayout.width, highlightLayout.height); + } + } +} diff --git a/src/intro/lib/src/TiIntromodalComponent.ts b/src/intro/lib/src/TiIntromodalComponent.ts new file mode 100644 index 0000000..8b604fd --- /dev/null +++ b/src/intro/lib/src/TiIntromodalComponent.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, OnInit, TemplateRef, ChangeDetectionStrategy } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * @ignore + */ +@Component({ + selector: 'ti-intromodal', + templateUrl: './intromodal.html', + styleUrls: ['./intro.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-intromodal-wrapper]': 'true' + } +}) +export class TiIntromodalComponent extends TiBaseComponent implements OnInit { + @Input() title: string; + @Input() content: string | TemplateRef; + @Input() image: string; + @Input() totalNumber: number; + @Input() currentNumber: number; + @Input() finishButtonText: string; + @Input() skipable: boolean; + @Input() close: (flag?: boolean) => void; + @Input() wantStep: (num: number) => void; + + public totalArray: Array; // 通过totalNumber生成的数组 + public isStringContent: boolean; // content是否为string + public templateContent: TemplateRef; // 用户传入的templateRef类型content + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + super.ngOnInit(); + this.totalArray = new Array(this.totalNumber + 1); + this.isStringContent = typeof this.content === 'string'; + this.templateContent = this.content instanceof TemplateRef ? this.content : undefined; + } + + /** + * @ignore + * 模板中实际调用的是Modal服务dismiss方法,并非此处定义的方法;在此处定义dismiss方法只是为了避免生产环境打包时报错 + */ + public dismiss(): void {} +} diff --git a/src/intro/lib/src/TiIntrotipComponent.ts b/src/intro/lib/src/TiIntrotipComponent.ts new file mode 100644 index 0000000..be935e9 --- /dev/null +++ b/src/intro/lib/src/TiIntrotipComponent.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { TiIntromodalComponent } from './TiIntromodalComponent'; + +/** + * @ignore + */ +@Component({ + selector: 'ti-intromodal', + templateUrl: './introtip.html', + styleUrls: ['./intro.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-intromodal-wrapper]': 'true' + } +}) +export class TiIntrotipComponent extends TiIntromodalComponent {} diff --git a/src/intro/lib/src/i18n/TiIntroWords.ts b/src/intro/lib/src/i18n/TiIntroWords.ts new file mode 100644 index 0000000..017f6a7 --- /dev/null +++ b/src/intro/lib/src/i18n/TiIntroWords.ts @@ -0,0 +1,8 @@ +export interface TiIntroWords { + tiIntro: { + skip: string; + previous: string; + next: string; + finish: string; + }; +} diff --git a/src/intro/lib/src/i18n/en_US.ts b/src/intro/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..3cdc71c --- /dev/null +++ b/src/intro/lib/src/i18n/en_US.ts @@ -0,0 +1,10 @@ +import { TiIntroWords } from './TiIntroWords'; + +export const en_US: TiIntroWords = { + tiIntro: { + skip: 'Skip', + previous: 'Previous', + next: 'Next', + finish: 'Finish' + } +}; diff --git a/src/intro/lib/src/i18n/es_US.ts b/src/intro/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..19a0709 --- /dev/null +++ b/src/intro/lib/src/i18n/es_US.ts @@ -0,0 +1,10 @@ +import { TiIntroWords } from './TiIntroWords'; + +export const es_US: TiIntroWords = { + tiIntro: { + skip: 'Omitir', + previous: 'Anterior', + next: 'Siguiente', + finish: 'Finalizar' + } +}; diff --git a/src/intro/lib/src/i18n/fr_FR.ts b/src/intro/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..020e7fa --- /dev/null +++ b/src/intro/lib/src/i18n/fr_FR.ts @@ -0,0 +1,10 @@ +import { TiIntroWords } from './TiIntroWords'; + +export const fr_FR: TiIntroWords = { + tiIntro: { + skip: 'Sauter', + previous: 'Précédent', + next: 'Suivant', + finish: 'Terminer' + } +}; diff --git a/src/intro/lib/src/i18n/index.ts b/src/intro/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/intro/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/intro/lib/src/i18n/pt_BR.ts b/src/intro/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..64947dd --- /dev/null +++ b/src/intro/lib/src/i18n/pt_BR.ts @@ -0,0 +1,10 @@ +import { TiIntroWords } from './TiIntroWords'; + +export const pt_BR: TiIntroWords = { + tiIntro: { + skip: 'Pular', + previous: 'Anterior', + next: 'Próximo', + finish: 'Finalizar' + } +}; diff --git a/src/intro/lib/src/i18n/zh_CN.ts b/src/intro/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..d7dd29a --- /dev/null +++ b/src/intro/lib/src/i18n/zh_CN.ts @@ -0,0 +1,10 @@ +import { TiIntroWords } from './TiIntroWords'; + +export const zh_CN: TiIntroWords = { + tiIntro: { + skip: '跳过', + previous: '上一步', + next: '下一步', + finish: '立即体验' + } +}; diff --git a/src/intro/lib/src/intro.less b/src/intro/lib/src/intro.less new file mode 100644 index 0000000..d27059c --- /dev/null +++ b/src/intro/lib/src/intro.less @@ -0,0 +1,87 @@ +:host { + --ti-intro-step-list-item-width: 5px; + --ti-intro-modal-padding-horizontal: calc(var(--ti-common-space-base) * 12); + --ti-intro-tip-container-width: 400px; +} +.ti3-intro-steplist-item() { + background-color: var(--ti-common-color-line-normal); + float: left; + width: var(--ti-intro-step-list-item-width); + height: var(--ti-intro-step-list-item-width); + border-radius: calc(var(--ti-intro-step-list-item-width) * 0.5); // IE下 不识别 "/ 2",改为" * 0.5" + margin: 0 3px; + cursor: pointer; + &:last-child { + margin-right: 0; + } + &.ti-step-showed { + background-color: var(--ti-common-color-line-hover); + } + &.active { + width: var(--ti-common-size-5x); + } +} +::ng-deep ti-tip-container ti-intromodal { + display: block; + // 366px = 400(最终宽度) - 16 * 2 (tip的横向padding) - 1 * 2(border); + width: calc(var(--ti-intro-tip-container-width) - var(--ti-common-space-4x) * 2 - 2px); +} +:host.ti3-intromodal-wrapper { + .ti3-intro-close { + position: absolute; + top: 12px; + right: 12px; + cursor: pointer; + } + ti-modal-header { + text-align: center; + padding: var(--ti-common-space-8x) var(--ti-common-space-10x) var(--ti-common-space-3x); + } + ti-modal-body.ti3-intro-body { + text-align: center; + padding: 0 var(--ti-intro-modal-padding-horizontal); + .ti3-intro-steplist-wrapper { + margin-top: var(--ti-common-space-3x); + line-height: 0; + font-size: 0; + } + .ti3-intro-steplist-container { + display: inline-block; + .ti3-intro-steplist-item { + .ti3-intro-steplist-item(); + } + } + } + .ti3-intro-header { + font-size: var(--ti-common-font-size-2); + line-height: var(--ti-common-line-height-number); + font-weight: var(--ti-common-font-weight-7); + color: var(--ti-common-color-text-primary); + margin-right: var(--ti-common-space-10); + } + + .ti3-intro-body { + padding: var(--ti-common-space-3x) 0 var(--ti-common-space-8x); + color: var(--ti-common-color-text-secondary); + line-height: var(--ti-common-line-height-number); + } + + .ti3-intro-footer { + .ti3-intro-steplist-container { + float: left; + margin-top: var(--ti-common-space-2x); + .ti3-intro-steplist-item { + .ti3-intro-steplist-item(); + } + } + .ti3-intro-button-container { + float: right; + button { + margin-right: var(--ti-common-space-2x); + &:last-child { + margin-right: 0; + } + } + } + } +} diff --git a/src/intro/lib/src/intromodal.html b/src/intro/lib/src/intromodal.html new file mode 100644 index 0000000..e238f57 --- /dev/null +++ b/src/intro/lib/src/intromodal.html @@ -0,0 +1,32 @@ +{{title}} + + + {{content}} + +
    +
    + + + +
    +
    + +
    + + + +
    +
    diff --git a/src/intro/lib/src/introtip.html b/src/intro/lib/src/introtip.html new file mode 100644 index 0000000..16059b3 --- /dev/null +++ b/src/intro/lib/src/introtip.html @@ -0,0 +1,49 @@ + +
    {{title}}
    +
    + + {{content}} + +
    +
    + diff --git a/src/ip/demo/karma.conf.js b/src/ip/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/ip/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/ip/demo/project.json b/src/ip/demo/project.json new file mode 100644 index 0000000..78bf546 --- /dev/null +++ b/src/ip/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/ip/demo", + "sourceRoot": "src/ip/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/ip", + "index": "src/ip/demo/src/index.html", + "main": "src/ip/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/ip/demo/tsconfig.app.json", + "assets": ["src/ip/demo/src/favicon.ico", "src/ip/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "ip-demo:build:production" + }, + "development": { + "browserTarget": "ip-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js ip" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/ip/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/ip/demo/tsconfig.spec.json", + "karmaConfig": "src/ip/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/ip/demo/src/app/AppComponent.ts b/src/ip/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/ip/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/ip/demo/src/app/AppModule.ts b/src/ip/demo/src/app/AppModule.ts new file mode 100644 index 0000000..5fc8911 --- /dev/null +++ b/src/ip/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { IpTestModule } from './ip/IpTestModule'; + +@NgModule({ + imports: [ + IpTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/ip/demo/src/app/IndexComponent.ts b/src/ip/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..ac04b5f --- /dev/null +++ b/src/ip/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { IpTestModule } from './ip/IpTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = IpTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/ip/demo/src/app/app.html b/src/ip/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/ip/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/ip/demo/src/app/ip/IpBasicComponent.ts b/src/ip/demo/src/app/ip/IpBasicComponent.ts new file mode 100644 index 0000000..add193d --- /dev/null +++ b/src/ip/demo/src/app/ip/IpBasicComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './ip-basic.html' +}) +export class IpBasicComponent { + ipv4Value: string = ''; + ipv6Value: string = '0:0:0:0:0:0:0:1'; + ipv6ValueAbbreviated: string = '::1'; +} diff --git a/src/ip/demo/src/app/ip/IpDisabledComponent.ts b/src/ip/demo/src/app/ip/IpDisabledComponent.ts new file mode 100644 index 0000000..67f7de6 --- /dev/null +++ b/src/ip/demo/src/app/ip/IpDisabledComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './ip-disabled.html' +}) +export class IpDisabledComponent { + ipv4Value: string = '127.0.0.1'; + disabled: boolean = true; +} diff --git a/src/ip/demo/src/app/ip/IpFormcontrolComponent.ts b/src/ip/demo/src/app/ip/IpFormcontrolComponent.ts new file mode 100644 index 0000000..102fa4d --- /dev/null +++ b/src/ip/demo/src/app/ip/IpFormcontrolComponent.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; + +@Component({ + templateUrl: './ip-formcontrol.html' +}) +export class IpFormcontrolComponent implements OnInit { + ipForm: FormGroup; + constructor(private fb: FormBuilder) {} + ngOnInit(): void { + this.ipForm = this.fb.group({ + ipValue: '127.0.0.1' + }); + } +} diff --git a/src/ip/demo/src/app/ip/IpTestModule.ts b/src/ip/demo/src/app/ip/IpTestModule.ts new file mode 100644 index 0000000..6576e22 --- /dev/null +++ b/src/ip/demo/src/app/ip/IpTestModule.ts @@ -0,0 +1,45 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiButtonModule, TiIpModule, TiValidationModule } from '@opentiny/ng'; + +import { IpBasicComponent } from './IpBasicComponent'; +import { IpValidComponent } from './IpValidComponent'; +import { IpDisabledComponent } from './IpDisabledComponent'; +import { IpFormcontrolComponent } from './IpFormcontrolComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiValidationModule, + TiButtonModule, + TiIpModule, + RouterModule.forChild(IpTestModule.ROUTES) + ], + declarations: [IpBasicComponent, IpValidComponent, IpDisabledComponent, IpFormcontrolComponent] +}) +export class IpTestModule { + static readonly LINKS: Array = [{ href: 'components/TiIpComponent.html', label: 'Ip' }]; + static readonly ROUTES: Routes = [ + { + path: 'ip/ip-basic', + component: IpBasicComponent + }, + { + path: 'ip/ip-formcontrol', + component: IpFormcontrolComponent + }, + { + path: 'ip/ip-disabled', + component: IpDisabledComponent + }, + { + path: 'ip/ip-valid', + component: IpValidComponent + } + ]; +} diff --git a/src/ip/demo/src/app/ip/IpValidComponent.ts b/src/ip/demo/src/app/ip/IpValidComponent.ts new file mode 100644 index 0000000..98b3dca --- /dev/null +++ b/src/ip/demo/src/app/ip/IpValidComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './ip-valid.html' +}) +export class IpValidComponent { + ipv4Value: string = ''; + ipv6Value: string = '0:0:0:0:0:0:0:1'; +} diff --git a/src/ip/demo/src/app/ip/ip-basic.html b/src/ip/demo/src/app/ip/ip-basic.html new file mode 100644 index 0000000..97eb27f --- /dev/null +++ b/src/ip/demo/src/app/ip/ip-basic.html @@ -0,0 +1,16 @@ +

    1.IPv4

    + +
    +
    Current Value: {{ ipv4Value }}
    +
    + +

    2.IPv6

    + +
    +
    Current Value: {{ ipv6Value }}
    +
    +
    + +
    +
    Current Value: {{ ipv6ValueAbbreviated }}
    +
    diff --git a/src/ip/demo/src/app/ip/ip-disabled.html b/src/ip/demo/src/app/ip/ip-disabled.html new file mode 100644 index 0000000..2926367 --- /dev/null +++ b/src/ip/demo/src/app/ip/ip-disabled.html @@ -0,0 +1 @@ + diff --git a/src/ip/demo/src/app/ip/ip-formcontrol.html b/src/ip/demo/src/app/ip/ip-formcontrol.html new file mode 100644 index 0000000..1078ad8 --- /dev/null +++ b/src/ip/demo/src/app/ip/ip-formcontrol.html @@ -0,0 +1,6 @@ +
    + +
    +
    +
    Current Value: {{ ipForm.value.ipValue }}
    +
    diff --git a/src/ip/demo/src/app/ip/ip-valid.html b/src/ip/demo/src/app/ip/ip-valid.html new file mode 100644 index 0000000..3bc1efa --- /dev/null +++ b/src/ip/demo/src/app/ip/ip-valid.html @@ -0,0 +1,9 @@ + +
    +
    Current Value: {{ ipv4Value }}
    +
    +
    + +
    +
    Current Value: {{ ipv6Value }}
    +
    diff --git a/src/ip/demo/src/app/ip/webdoc/ip-demos.js b/src/ip/demo/src/app/ip/webdoc/ip-demos.js new file mode 100644 index 0000000..dac5e53 --- /dev/null +++ b/src/ip/demo/src/app/ip/webdoc/ip-demos.js @@ -0,0 +1,53 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'ip-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'ip basic', + }, + desc: { + 'zh-CN': + '

    通过属性version配置 ip 版本号;IPv6 支持缩略格式,例如 1111::8888。

    ', + 'en-US': '

    ip basic

    ', + }, + apis: ['TiIpComponent.properties.version'], + }, + { + demoId: 'ip-formcontrol', + name: { + 'zh-CN': '响应式表单', + 'en-US': 'ip formcontrol', + }, + desc: { + 'zh-CN': '

    响应式表单的用法。

    ', + 'en-US': '

    ip formcontrol

    ', + }, + }, + { + demoId: 'ip-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'ip disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否禁用。

    ', + 'en-US': '

    ip disabled

    ', + }, + apis: ['TiIpComponent.properties.disabled'], + }, + { + demoId: 'ip-valid', + name: { + 'zh-CN': '表单校验', + 'en-US': 'ip valid', + }, + desc: { + 'zh-CN': '

    表单校验的用法。

    ', + 'en-US': '

    ip valid

    ', + }, + }, + ], +}; diff --git a/src/ip/demo/src/app/ip/webdoc/ip.cn.md b/src/ip/demo/src/app/ip/webdoc/ip.cn.md new file mode 100644 index 0000000..eede21a --- /dev/null +++ b/src/ip/demo/src/app/ip/webdoc/ip.cn.md @@ -0,0 +1,23 @@ +--- +title: IP 输入IP +--- +# IP 输入IP + +
    + +Ip 是显示和设置IP地址的组件。   + +```typescript +import { TiIpModule } from '@opentiny/ng'; +``` + +
    + +
    + +Ip 是显示和设置IP地址的组件。   + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    diff --git a/src/ip/demo/src/app/ip/webdoc/ip.en.md b/src/ip/demo/src/app/ip/webdoc/ip.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/ip/demo/src/app/ip/webdoc/ip.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/ip/demo/src/favicon.ico b/src/ip/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/ip/demo/src/index.html b/src/ip/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/ip/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/ip/demo/src/main.ts b/src/ip/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/ip/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/ip/demo/test.ts b/src/ip/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/ip/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/ip/demo/tsconfig.app.json b/src/ip/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/ip/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/ip/demo/tsconfig.spec.json b/src/ip/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/ip/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/ip/lib/index.ts b/src/ip/lib/index.ts new file mode 100644 index 0000000..610d250 --- /dev/null +++ b/src/ip/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiIpModule'; diff --git a/src/ip/lib/ng-package.json b/src/ip/lib/ng-package.json new file mode 100644 index 0000000..353978f --- /dev/null +++ b/src/ip/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/ip", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/ip/lib/package.json b/src/ip/lib/package.json new file mode 100644 index 0000000..2adba1d --- /dev/null +++ b/src/ip/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-ip", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/forms": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/ip/lib/project.json b/src/ip/lib/project.json new file mode 100644 index 0000000..2f88f7f --- /dev/null +++ b/src/ip/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/ip/lib", + "sourceRoot": "src/ip/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/ip"], + "options": { + "project": "src/ip/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/ip"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js ip" + }, + { + "command": "ng default-build ip" + }, + { + "command": "node build/clear-default-theme.js ip" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/ip && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build ip && ng pack ip && node build/publish.js ip --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/ip/lib/src/TiIpComponent.ts b/src/ip/lib/src/TiIpComponent.ts new file mode 100644 index 0000000..789be79 --- /dev/null +++ b/src/ip/lib/src/TiIpComponent.ts @@ -0,0 +1,464 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/* eslint-disable no-eq-null */ +/* eslint-disable eqeqeq */ +import { Component, ElementRef, Input, QueryList, Renderer2, ViewChildren, ChangeDetectionStrategy } from '@angular/core'; +import { TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +// 下面注释,可以避免编译lib时正则报错。原理未知,副作用未知。 +// @dynamic +/** + * Ip组件 + * + * Ip组件提供了一种方便的显示和设置IP地址的功能。 + * + */ +@Component({ + selector: 'ti-ip', + templateUrl: './ip.html', + styleUrls: ['./ip.less'], + providers: [TiFormComponent.getValueAccessor(TiIpComponent)], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti_input_ip_container_ipv4]': 'version !== 6', + '[class.ti_input_ip_container_ipv6]': 'version === 6', + '(paste)': 'pasteHandle($event)', + '(blur)': 'blurHandle($event)' + } +}) +export class TiIpComponent extends TiFormComponent { + protected versionInfo: string = super.getVersion(packageInfo); + // ipv4 配置参数 + private static readonly ipv4Version: number = 4; + // 采用自研的ipv4正则表达式 + private static readonly ipv4Reg: RegExp = + /^(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))$/i; + private static readonly ipv4 = { + ipvCheckReg: /[\D]/g, + label: '...', + placeReg: /\s/g, + separator: '.', + ipValueArray: new Array(4).fill('') + }; + // ipv6 配置参数 + private static readonly ipv6Version: number = 6; + // 采用自研的ipv6正则表达式 + private static readonly ipv6Reg: RegExp = + /^(((([\da-f]{1,4}):){7}([\da-f]{1,4}))|(((([\da-f]{1,4}):){1,7}:)|((([\da-f]{1,4}):){6}:([\da-f]{1,4}))|((([\da-f]{1,4}):){5}:(([\da-f]{1,4}):)?([\da-f]{1,4}))|((([\da-f]{1,4}):){4}:(([\da-f]{1,4}):){0,2}([\da-f]{1,4}))|((([\da-f]{1,4}):){3}:(([\da-f]{1,4}):){0,3}([\da-f]{1,4}))|((([\da-f]{1,4}):){2}:(([\da-f]{1,4}):){0,4}([\da-f]{1,4}))|((([\da-f]{1,4}):){1}:(([\da-f]{1,4}):){0,5}([\da-f]{1,4}))|(::(([\da-f]{1,4}):){0,6}([\da-f]{1,4}))|(::([\da-f]{1,4})?))|(((([\da-f]{1,4}):){6}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){5}:(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){4}:(([\da-f]{1,4}):)?(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){3}:(([\da-f]{1,4}):){0,2}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){2}:(([\da-f]{1,4}):){0,3}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|(([\da-f]{1,4})::(([\da-f]{1,4}):){0,4}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|(::(([\da-f]{1,4}):){0,5}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))))$/i; + private static readonly ipv6 = { + ipvCheckReg: /[^a-f\d]/gi, + label: ':::::::', + placeReg: /\s/g, + separator: ':', + ipValueArray: new Array(8).fill('') + }; + private static getGlbInfo(ver: number): any { + return ver === 4 ? TiIpComponent.ipv4 : TiIpComponent.ipv6; + } + + /** + * Ip 版本号 + */ + @Input() version: number = 4; + /** + * @ignore + */ + @ViewChildren('ipInput') ipInputs: QueryList; + /** + * @ignore + * 存放 ip 组件各个input框值的数组 + */ + public ipsValues: Array; + + /** + * @ignore + */ + public pasteHandle = (event: any) => { + if (this.disabled) { + return; + } + event.preventDefault(); // 阻止默认的input原生的粘贴事件(把粘贴板的值粘贴到input框) + let pasteValue: string = ''; + if (window['clipboardData'] && window['clipboardData'].getData) { + // for IE + pasteValue = window['clipboardData'].getData('Text'); + } else { + // For other browser + try { + pasteValue = event.clipboardData.getData('Text'); + } catch (err) {} + } + const unmaskpaste: string = this.unmask(this.version, pasteValue); + if (this.isValidIP(this.version, unmaskpaste)) { + this.updateValue(unmaskpaste); // 粘贴之后更新值 + this.ipsValues = this.splitToIPArray(this.version, this.model); // 内部的操作手动更新数组 + } + }; + /** + * @ignore + */ + public blurHandle = (event: any) => { + const ipValue: string = this.generateIPValue(0); + if (ipValue === '') { + this.updateValue(ipValue); + } else { + const fixedVaule: string = this.blurFixed(this.version, ipValue); + this.updateValue(this.mask(this.version, fixedVaule)); + } + }; + private blurFixed(ver: number, ip: string): string { + const arr: Array = this.splitToIPArray(ver, ip); + for (let i: number = 0; i < arr.length; i++) { + if (arr[i] === '') { + arr[i] = '0'; + } + } + + return this.joinToIPValue(ver, arr); + } + + // 组件声明周期钩子--start + + ngOnInit(): void { + super.ngOnInit(); + this.init(); + } + + private init(): void { + // 默认是IPv4 + this.version = parseInt(String(this.version), 10) === TiIpComponent.ipv6Version ? TiIpComponent.ipv6Version : TiIpComponent.ipv4Version; + // 初始化ipValueArray 赋值新的数组 + this.ipsValues = TiIpComponent.getGlbInfo(this.version).ipValueArray.concat(); + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + // 处理 聚焦 并使第一段ip获焦(放在这个钩子的原因:在oninit中拿不到 QueryList 返回的结果集) + const elements = []; + this.ipInputs.forEach((item) => { + elements.push(item.nativeElement); + }); + // 推荐在onInit()时调用setFocusableElems(), 但是ngFor/ngIf中的元素在ngAfterViewInit()才能获取到 + this.setFocusableElems(elements); + } + // 组件声明周期钩子--end + + // 实现ControlValueAccessor接口--start + /** + * @ignore + */ + writeValue(value: any): void { + if (value === '') { + super.writeValue(value); + // 对空值做特殊处理,为空时清空IP框 + this.ipsValues = TiIpComponent.getGlbInfo(this.version).ipValueArray.concat(); + } else if (value === this.joinToIPValue(this.version, this.ipsValues)) { + // 和当前IP框的值相同不处理 + } else if (this.isValidIP(this.version, value) || this.isInputting(this.version, value)) { + super.writeValue(this.mask(this.version, value)); + this.ipsValues = this.splitToIPArray(this.version, this.model); + } else { + // 非法不处理 恢复之前值 + super.writeValue(this.joinToIPValue(this.version, this.ipsValues)); + } + } + // 实现ControlValueAccessor接口--end + + // 组件交互方法集合--start + /** + * @ignore + */ + public keydown(e: any, index: number): void { + const ipInputs = this.ipInputs; + const keyCode = e.keyCode; + this.keydownHandle(e, ipInputs['_results'][index], index, this.version); + // 按下键为左或右键时,不做重新赋值,避免IE浏览器下重新赋值之后光标位置不准确问题 + if (keyCode === TiKeymap.KEY_ARROW_LEFT || keyCode === TiKeymap.KEY_ARROW_RIGHT) { + return; + } + } + /** + * @ignore + */ + public change(index: number): void { + const ipValues: string = this.generateIPValue(index); + this.updateValue(ipValues); + } + + // 组件交互方法集合--end + + // 内部公共方法集合--start + /** + * @description 将 ip 数组拼接成ip值 + * @param: ver ip版本 + * @param: arr ip字符串中数字组成的数组 + * @returns + */ + private joinToIPValue(ver: number, arr: Array): string { + return arr.join(TiIpComponent.getGlbInfo(ver).separator); + } + /** + * @description 将 ip 值分割成ip数组 + * @param: ver ip版本 + * @param: ip ip字符串 + */ + private splitToIPArray(ver: number, ip: string): Array { + const res: Array = ip ? ip.split(TiIpComponent.getGlbInfo(ver).separator) : []; + + return res; + } + private isInputting(ver: number, value: string): boolean { + const arr: Array = this.splitToIPArray(ver, value); + + return arr.indexOf('') >= 0; + } + // 判断当前ip值是否合法 + private isValidIP(ver: number, ip: string): boolean { + if (ip === null || ip === undefined) { + return false; + } + const rgx = ver === TiIpComponent.ipv4Version ? TiIpComponent.ipv4Reg : TiIpComponent.ipv6Reg; + return rgx.test(ip); + } + /** + * @description IP需要去除前面的0,例如:,“010.0.0.0”经过该函数处理后变为:“10.0.0.0”;IPv6支持缩略格式 + * @param: ver ip版本 + * @param: ip ip字符串 + * @returns + */ + private mask(ver: number, ip: string): string { + const ipInfo = TiIpComponent.getGlbInfo(ver); + const part = ip.split(ipInfo.separator); + const partLength = part.length; + if (ver === 4) { + for (let i: number = 0; i < partLength; i++) { + if (part[i] !== '') { + let partNumber = parseInt(part[i], 10); + // parseInt(part[i], 10) /= 1; + partNumber /= 1; // 将part[j]转化为整数,目的是消除前面的0 + part[i] = String(partNumber); // 再转化为字符串 + } + } + } else { + // IPv6支持缩略格式,即任何由全0组成的1个或多个16位段的单个连续的字符串 都可以用一个双冒号“::”,且“::”只出现一次;例如:2001:d02::14:95 + // 用‘0’补齐缩略格式缺少个数 + while (part.length < 8) { + let index = part.indexOf(''); + part.splice(index, 0, '0'); + } + + part.forEach((partItem, index) => { + // 查找是否有1-9和字母,如果没有,替换为0,如果有,去除前面的0 + if (partItem.match(/[1-9a-f]/i)) { + part[index] = partItem.replace(/^0{1,3}/i, ''); + } else if (partItem !== '0') { + part[index] = '0'; + } + }); + } + + return part.join(ipInfo.separator); + } + /** + * @description 去掉空格后的IP地址;例如“ 10.0 .0.0 ”进过该函数处理后返回:“10.0.0” + * @param: ver ip版本 + * @param: ip ip字符串 + */ + private unmask(ver: number, ip: string): string { + const iPInfo = TiIpComponent.getGlbInfo(ver); + + return ip.replace(iPInfo.placeReg, ''); + } + + /** + * @description 获取当前ip段输入的值并进行判断,合法时更新到当前ip段输入框内,并拼接返回整体ip值 + * @param: index 当前输入框的下标 + */ + private generateIPValue(index: number): string { + const ipInfo = TiIpComponent.getGlbInfo(this.version); + const cellArray = this.ipInputs; + let value = cellArray['_results'][index].nativeElement.value; + if (ipInfo.ipvCheckReg.test(value)) { + value = value.replace(ipInfo.ipvCheckReg, ''); + } + + // ipv4下,当前输入框值大于255时,强制转换成255 + if (this.version === TiIpComponent.ipv4Version && value > 255) { + value = 255; + } + for (let i: number = 0; i < this.ipsValues.length; i++) { + if (i === index) { + this.ipsValues[i] = value; + cellArray['_results'][index].nativeElement.value = value; + } + } + let ipValues; + ipValues = this.ipsValues.join(ipInfo.separator); + if (ipValues === ipInfo.label) { + ipValues = ''; + } + return ipValues; + } + // 更新value值 //TODO: 考虑去除这个函数,用writeValue代替 + private updateValue(newValue: string): void { + if (newValue === this.model) { + return; + } + // 记录ip的值在value中,方便用户获取 + this.model = newValue; + this.writeValue(newValue); + } + // keydown事件触发时,判断并设置组件光标位置 + private keydownHandle(e: any, thisInput: ElementRef, index: number, ver: number): void { + const ipInputsLength = this.ipInputs.length; + const nextCell = index < ipInputsLength - 1 ? this.ipInputs['_results'][index + 1] : undefined; + const prevCell = index > 0 ? this.ipInputs['_results'][index - 1] : undefined; + + const keyCode = e.keyCode; + // 键盘码110代表. 190代表 >.---当键盘码为这两种时,跳至下一输入框中,并选中该输入框中的值 + if (keyCode === TiKeymap.KEY_NUMPAD_DECIMAL || keyCode === TiKeymap.KEY_PERIOD) { + if (!Util.isUndefined(nextCell)) { + // 非最后一个输入框时,跳至下一输入框 + nextCell.nativeElement.focus(); + nextCell.nativeElement.select(); + } + e.preventDefault(); // 不论是否为最后一个输入框,防止默认事件发生,否则输入框中会出现 ... + return; + } + // 光标后移到下一段ip + if (this.isMoveToNext(e, thisInput, nextCell, ver)) { + nextCell.nativeElement.focus(); + this.caret(nextCell, 0); // 设置光标位置 + } + + // 光标前移到上一段ip + if (this.isMoveToPrevous(keyCode, thisInput, prevCell)) { + this.caret(prevCell, prevCell.nativeElement.value.length); // 设置光标位置 + e.preventDefault(); // 防止默认事件发生,否则输入框中的回退光标位置会有问题 + } + } + // 判断当前是否需要将光标移至后一段ip + private isMoveToNext(e: any, thisInput: ElementRef, nextCell: ElementRef, ver: number): boolean { + // 如果下一段ip不存在直接返回false + if (Util.isUndefined(nextCell)) { + return false; + } + // 如果输入为合法数字,输入框中已有3个值,光标位于输入框末尾,并且存在下一输入框, 则跳至下一输入框 + let isFullToNext; + if (ver !== TiIpComponent.ipv6Version) { + isFullToNext = this.checkIpv4MoveToNext(thisInput, e); + } else { + isFullToNext = this.checkIpv6MoveToNext(thisInput, e); + } + // 当为向右键时→,且光标位置在该输入框末尾。则直接跳到下一输入框中 + const keyCode = e.keyCode; + const isRightToNext = keyCode === TiKeymap.KEY_ARROW_RIGHT && this.caret(thisInput)[0] === thisInput.nativeElement.value.length; + return isFullToNext || isRightToNext; + } + // 判断当前是否需要将光标移至前一段ip + private isMoveToPrevous(keyCode: number, thisInput: ElementRef, prevCell: ElementRef): boolean { + // 1.当为向左键(37),且光标位置位于输入框起始位置时,则跳至前一输入框中; + // 2.当为backspace键,且已回删至输入框起始位置 跳至前已输入框中 + // 3.输入框处于选中状态时,按backspace键不跳到前一输入框中 + return ( + !Util.isUndefined(prevCell) && + ((keyCode === TiKeymap.KEY_ARROW_LEFT && this.caret(thisInput)[0] === 0) || // 按左键 + (keyCode === TiKeymap.KEY_BACKSPACE && + this.caret(thisInput)[0] === 0 && // 按后退键并且删除完 + this.caret(thisInput)[0] === this.caret(thisInput)[1])) + ); // 输入框处于选中状态按后退键不跳到前一个输入框 + } + + // 判断当前段ipv4输入框是否已满3,末端是合法字符,且光标位置在此段ip最后 + private checkIpv4MoveToNext(thisInput: ElementRef, e: any): boolean { + return thisInput.nativeElement.value.length === 3 && this.isNumeric(e) && this.caret(thisInput)[0] === 3; + } + // 判断当前段ipv6输入框是否已满4未合法字符,且光标位置在此段ip最后 + private checkIpv6MoveToNext(thisInput: ElementRef, e: any): boolean { + return thisInput.nativeElement.value.length === 4 && this.isHexaDecimal(e) && this.caret(thisInput)[0] === 4; + } + // 判断输入是否为ipv4合法字符 + private isNumeric(e: any): boolean { + if (e.shiftKey || e.ctrlKey) { + return false; + } + const code = e.keyCode; + return (code >= TiKeymap.KEY_0 && code <= TiKeymap.KEY_9) || (code >= TiKeymap.KEY_NUMPAD_0 && code <= TiKeymap.KEY_NUMPAD_9); + } + // 判断输入是否为ipv6合法字符 + private isHexaDecimal(e: any): boolean { + const keyCode = e.keyCode; + // ctrl键组合其它情况,输入框中不会输入有效字符,返回 + if (e.ctrlKey) { + return false; + } + // 输入有效字符情况:1)a~f 2)有效数字 + const isValidInput = + (keyCode >= TiKeymap.KEY_A && keyCode <= TiKeymap.KEY_F) || + (keyCode >= TiKeymap.KEY_NUMPAD_0 && keyCode <= TiKeymap.KEY_NUMPAD_9) || + (keyCode >= TiKeymap.KEY_0 && keyCode <= TiKeymap.KEY_9); + return isValidInput; + } + + /** + * @description 获取或设置光标位置 + * @param: ele 光标所在dom + * @param: s 开始位置 + * @param: e 结束位置 + */ + private caret(ele: ElementRef, s?: number, e?: number): any { + const element = ele.nativeElement; + const setPosition = function (el, start, end) { + if (el.setSelectionRange) { + el.focus(); + el.setSelectionRange(start, end); + } else if (el.createTextRange) { + const range = el.createTextRange(); + range.collapse(true); + range.moveEnd('character', end); + range.moveStart('character', start); + range.select(); + } + }; + + if (s != null && e != null) { + // setting range + return setPosition(element, s, e); + } else if (s != null) { + // setting position + return setPosition(element, s, s); + } + + // getting + if (element.createTextRange && document['selection']) { + const r = document['selection'].createRange().duplicate(); + + const end = element.value.lastIndexOf(r.text) + r.text.length; + + r.moveEnd('character', element.value.length); + const start = r.text === '' ? element.value.length : element.value.lastIndexOf(r.text); + + return [start, end]; + } + + return [element.selectionStart, element.selectionEnd]; + } + /** + * @ignore + * ngFor遍历的 trackBy函数,防止数据更新导致所有DOM重新渲染 + */ + public trackByFn(index: any, item: any): any { + return index; + } + // 内部公共方法集合--end +} diff --git a/src/ip/lib/src/TiIpModule.ts b/src/ip/lib/src/TiIpModule.ts new file mode 100644 index 0000000..896cad9 --- /dev/null +++ b/src/ip/lib/src/TiIpModule.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIpComponent } from './TiIpComponent'; +import { TiTextModule } from '@opentiny/ng-text'; +@NgModule({ + imports: [CommonModule, FormsModule, TiTextModule], + exports: [TiIpComponent], + declarations: [TiIpComponent] +}) +export class TiIpModule {} +export { TiIpComponent } from './TiIpComponent'; diff --git a/src/ip/lib/src/ip.html b/src/ip/lib/src/ip.html new file mode 100644 index 0000000..ed1415e --- /dev/null +++ b/src/ip/lib/src/ip.html @@ -0,0 +1,20 @@ +{{version === 4 ? '.' : ':'}} diff --git a/src/ip/lib/src/ip.less b/src/ip/lib/src/ip.less new file mode 100644 index 0000000..91abfa7 --- /dev/null +++ b/src/ip/lib/src/ip.less @@ -0,0 +1,64 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-ip-container-height: var(--ti-common-size-7x); + --ti-ipv4-container-width: var(--ti-common-size-40x); + --ti-ipv6-container-width: 380px; + --ti-ip-division-width: var(--ti-common-size-base); +} + +.ti_input_ip_container (@width, @inputWidth) { + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + background-color: var(--ti-common-color-bg-white-normal); + display: inline-block; + width: @width; + height: var(--ti-ip-container-height); + display: flex; + align-items: center; + .box-sizing(border-box); + .form-border-animat-init(); + .ti_input_ip_anchor[tiText] { + text-align: center; + height: calc(var(--ti-ip-container-height) - var(--ti-common-border-weight-normal) * 2); + vertical-align: middle; + padding: 0; + // 由于IE无法正确解析 @{number} -1情况下的样式,因此此处新定义了divisionNum来做算式处理 + width: @inputWidth; + background-color: var(--ti-common-color-transparent); + } + .ti_input_ip_division { + color: var(--ti-common-color-icon-normal); + font-weight: var(--ti-common-font-weight-7); + display: inline-block; + width: var(--ti-ip-division-width); + text-align: center; + font-family: 'Helvetica', 'Myriad', Arial, 'Microsoft Yahei', '????', 'SimSun', Tahoma; + } + &:hover { + border-color: var(--ti-common-color-line-hover); + //默认状态下的hover动画 + .form-border-animat-enter(); + } + &[tiFocused] { + border-color: var(--ti-common-color-line-active); + } + &[disabled] { + background-color: var(--ti-common-color-bg-disabled); + border-color: var(--ti-common-color-line-disabled); + .ti_input_ip_octet, + .ti_input_ip_octet_v6, + .ti_input_ip_division { + color: var(--ti-common-color-icon-disabled); + cursor: not-allowed; + } + } +} + +:host.ti_input_ip_container_ipv4 { + .ti_input_ip_container(var(--ti-ipv4-container-width), calc((100% - var(--ti-ip-division-width) * 3) / 4)); +} + +:host.ti_input_ip_container_ipv6 { + .ti_input_ip_container(var(--ti-ipv6-container-width), calc((100% - var(--ti-ip-division-width) * 7) / 8)); +} diff --git a/src/ipsection/demo/karma.conf.js b/src/ipsection/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/ipsection/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/ipsection/demo/project.json b/src/ipsection/demo/project.json new file mode 100644 index 0000000..6b2890f --- /dev/null +++ b/src/ipsection/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/ipsection/demo", + "sourceRoot": "src/ipsection/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/ipsection", + "index": "src/ipsection/demo/src/index.html", + "main": "src/ipsection/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/ipsection/demo/tsconfig.app.json", + "assets": ["src/ipsection/demo/src/favicon.ico", "src/ipsection/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "ipsection-demo:build:production" + }, + "development": { + "browserTarget": "ipsection-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js ipsection" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/ipsection/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/ipsection/demo/tsconfig.spec.json", + "karmaConfig": "src/ipsection/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/ipsection/demo/src/app/AppComponent.ts b/src/ipsection/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/ipsection/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/ipsection/demo/src/app/AppModule.ts b/src/ipsection/demo/src/app/AppModule.ts new file mode 100644 index 0000000..74f5502 --- /dev/null +++ b/src/ipsection/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { IpsectionTestModule } from './ipsection/IpsectionTestModule'; + +@NgModule({ + imports: [ + IpsectionTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/ipsection/demo/src/app/IndexComponent.ts b/src/ipsection/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..29b2ffe --- /dev/null +++ b/src/ipsection/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { IpsectionTestModule } from './ipsection/IpsectionTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = IpsectionTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/ipsection/demo/src/app/app.html b/src/ipsection/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/ipsection/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionBasicComponent.ts b/src/ipsection/demo/src/app/ipsection/IpsectionBasicComponent.ts new file mode 100644 index 0000000..6ae6279 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionBasicComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiIpsectionConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './ipsection-basic.html' +}) +export class IpsectionBasicComponent { + ipValue: string = '127.0.0.1'; + ipSubnetmaskValue: string = '127.0.0.1/255'; + configs: Array = [ + { section: 0, bold: true }, + { section: 1, options: ['0'] }, + { section: 2, options: ['0'] }, + { section: 3 } + ]; + subnetmaskconfigs: Array = [ + { section: 0 }, + { section: 1, options: ['0'] }, + { section: 2, options: ['0'] }, + { section: 3 }, + { section: 4, options: ['198', '134', '255'] } + ]; +} diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionDisabledComponent.ts b/src/ipsection/demo/src/app/ipsection/IpsectionDisabledComponent.ts new file mode 100644 index 0000000..0077842 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionDisabledComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { TiIpsectionConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './ipsection-disabled.html' +}) +export class IpsectionDisabledComponent { + ipValue: string = '127.0.0.1'; + disabled: boolean = true; + configs: Array = [ + { section: 0, bold: true }, + { section: 1, options: ['0'] }, + { section: 2, options: ['0'] }, + { section: 3 } + ]; + configs1: Array = [ + { section: 0, disabled: false }, + { section: 1, options: ['0'], disabled: true }, + { section: 2, options: ['0'] }, + { section: 3, disabled: true } + ]; +} diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionEventsComponent.ts b/src/ipsection/demo/src/app/ipsection/IpsectionEventsComponent.ts new file mode 100644 index 0000000..0ac09a6 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionEventsComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiIpsectionConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './ipsection-events.html' +}) +export class IpsectionEventsComponent { + myLogs: Array = []; + ipValue: string = '127.0.0.1/255'; + configs: Array = [ + { section: 0, bold: true }, + { section: 1, options: ['0'] }, + { section: 2 }, + { section: 3, options: ['0'] }, + { section: 4 } + ]; + onBlur(event: FocusEvent): void { + this.myLogs = [...this.myLogs, `失焦事件 当前IP值为${this.ipValue}`]; + } + onChange(event: any): void { + this.myLogs = [...this.myLogs, `change事件 当前IP值为${event}`]; + } +} diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionFocusComponent.ts b/src/ipsection/demo/src/app/ipsection/IpsectionFocusComponent.ts new file mode 100644 index 0000000..08a8022 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionFocusComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { TiIpsectionConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './ipsection-focus.html' +}) +export class IpsectionFocusComponent { + ipValue: string = '127.0.0.1'; + ipValue1: string = '127.0.0.1'; + configs: Array = [ + { section: 0, bold: true }, + { section: 1, options: ['0'] }, + { section: 2, options: ['0'] }, + { section: 3 } + ]; + configs1: Array = [ + { section: 0, options: ['0'], disabled: true }, + { section: 1 }, + { section: 2, options: ['0'], disabled: true }, + { section: 3, disabled: true } + ]; +} diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionTestComponent.ts b/src/ipsection/demo/src/app/ipsection/IpsectionTestComponent.ts new file mode 100644 index 0000000..93102cd --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionTestComponent.ts @@ -0,0 +1,43 @@ +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { TiIpsectionConfig, TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './ipsection-test.html' +}) +export class IpsectionTestComponent { + ipValue: string; + control: FormControl = new FormControl(); + configs: Array = [ + { + section: 0, + bold: true, + validation: { + tip: '建议输入196-220', + tipPosition: 'top-right' + }, + validationRules: TiValidators.required + }, + { + section: 1, + validation: { + tip: '该网段为输入类型', + tipPosition: 'top' + } + }, + { + section: 2, + bold: true, + validation: { + tip: '建议输入196-220', + tipPosition: 'top-right' + } + }, + { + section: 3, + validation: { + tip: '该网段为输入类型' + } + } + ]; +} diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionTestModule.ts b/src/ipsection/demo/src/app/ipsection/IpsectionTestModule.ts new file mode 100644 index 0000000..7455d02 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionTestModule.ts @@ -0,0 +1,68 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiButtonModule, TiIpsectionModule, TiTextModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { IpsectionBasicComponent } from './IpsectionBasicComponent'; +import { IpsectionFocusComponent } from './IpsectionFocusComponent'; +import { IpsectionValidComponent } from './IpsectionValidComponent'; +import { IpsectionValidFormgroupComponent } from './IpsectionValidFormgroupComponent'; +import { IpsectionDisabledComponent } from './IpsectionDisabledComponent'; +import { IpsectionEventsComponent } from './IpsectionEventsComponent'; +import { IpsectionTestComponent } from './IpsectionTestComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiValidationModule, + TiButtonModule, + TiTextModule, + TiIpsectionModule, + DemoLogModule, + RouterModule.forChild(IpsectionTestModule.ROUTES) + ], + declarations: [ + IpsectionBasicComponent, + IpsectionFocusComponent, + IpsectionValidComponent, + IpsectionValidFormgroupComponent, + IpsectionDisabledComponent, + IpsectionEventsComponent, + IpsectionTestComponent + ] +}) +export class IpsectionTestModule { + static readonly LINKS: Array = [{ href: 'components/TiIpsectionComponent.html', label: 'Ipsection' }]; + static readonly ROUTES: Routes = [ + { + path: 'ipsection/ipsection-basic', + component: IpsectionBasicComponent + }, + { + path: 'ipsection/ipsection-disabled', + component: IpsectionDisabledComponent + }, + { + path: 'ipsection/ipsection-valid', + component: IpsectionValidComponent + }, + { + path: 'ipsection/ipsection-valid-formgroup', + component: IpsectionValidFormgroupComponent + }, + { + path: 'ipsection/ipsection-events', + component: IpsectionEventsComponent + }, + { + path: 'ipsection/ipsection-focus', + component: IpsectionFocusComponent + }, + { path: 'ipsection/ipsection-test', component: IpsectionTestComponent } + ]; +} diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionValidComponent.ts b/src/ipsection/demo/src/app/ipsection/IpsectionValidComponent.ts new file mode 100644 index 0000000..fbcb509 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionValidComponent.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; +import { TiIpsectionConfig, TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './ipsection-valid.html' +}) +export class IpsectionValidComponent { + ipValue: string = '127.0.0.1'; + configs: Array = [ + { + section: 0, + validation: { + tip: '建议输入127-220', + tipPosition: 'top-right' + }, + validationRules: TiValidators.required + }, + { + section: 1, + validation: { + tip: '该网段为输入类型' + }, + validationRules: [TiValidators.required, TiValidators.rangeValue(0, 255)] + }, + { section: 2, options: ['0'] }, + { + section: 3, + validationRules: TiValidators.rangeValue(0, 255) + } + ]; +} diff --git a/src/ipsection/demo/src/app/ipsection/IpsectionValidFormgroupComponent.ts b/src/ipsection/demo/src/app/ipsection/IpsectionValidFormgroupComponent.ts new file mode 100644 index 0000000..1dbb0b6 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/IpsectionValidFormgroupComponent.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { TiIpsectionConfig, TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './ipsection-valid-formgroup.html' +}) +export class IpsectionValidFormgroupComponent { + formGroup: FormGroup; + constructor(private fb: FormBuilder) { + this.formGroup = this.fb.group({ + ipsection: new FormControl('127.0.0.1') + }); + } + configs: Array = [ + { + section: 0, + validation: { + tip: '建议输入127-220', + tipPosition: 'top-right' + }, + validationRules: TiValidators.required + }, + { + section: 1, + validation: { + tip: '该网段为输入类型' + }, + validationRules: [TiValidators.required, TiValidators.rangeValue(0, 255)] + }, + { section: 2, options: ['0'] }, + { + section: 3, + validationRules: TiValidators.rangeValue(0, 255) + } + ]; +} diff --git a/src/ipsection/demo/src/app/ipsection/ipsection-basic.html b/src/ipsection/demo/src/app/ipsection/ipsection-basic.html new file mode 100644 index 0000000..8dafbcf --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/ipsection-basic.html @@ -0,0 +1,11 @@ +

    不含子网掩码

    + +
    +
    Current Value: {{ ipValue }}
    +
    +
    +

    含有子网掩码

    + +
    +
    Current Value: {{ ipSubnetmaskValue }}
    +
    diff --git a/src/ipsection/demo/src/app/ipsection/ipsection-disabled.html b/src/ipsection/demo/src/app/ipsection/ipsection-disabled.html new file mode 100644 index 0000000..c4e8fd8 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/ipsection-disabled.html @@ -0,0 +1,5 @@ +

    整体禁用

    + +

    +

    各网段禁用

    + diff --git a/src/ipsection/demo/src/app/ipsection/ipsection-events.html b/src/ipsection/demo/src/app/ipsection/ipsection-events.html new file mode 100644 index 0000000..f282c61 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/ipsection-events.html @@ -0,0 +1,13 @@ +

    描述

    +

    事件接口示例

    +

    示例

    +

    + +

    事件日志:

    + diff --git a/src/ipsection/demo/src/app/ipsection/ipsection-focus.html b/src/ipsection/demo/src/app/ipsection/ipsection-focus.html new file mode 100644 index 0000000..318bf24 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/ipsection-focus.html @@ -0,0 +1,12 @@ +

    描述

    +

    设置autofoucus属性,默认聚焦第一个非灰化元素

    +

    示例

    +

    1.不设置autofocus

    + +

    +

    当前Ip地址为:{{ipValue}}

    +
    +

    2.设置autofocus且设置灰化项

    + +

    +

    当前Ip地址为:{{ipValue1}}

    diff --git a/src/ipsection/demo/src/app/ipsection/ipsection-test.html b/src/ipsection/demo/src/app/ipsection/ipsection-test.html new file mode 100644 index 0000000..2c1400e --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/ipsection-test.html @@ -0,0 +1,12 @@ +

    描述

    +

    场景测试,测试组件功能是否正常

    +

    示例

    +

    不设置初始值

    +

    1.1 模板式表单(值为 undefined)

    + +

    +

    当前Ip地址为:{{ipValue}}

    +

    1.2.响应式表单,使用formControl绑定值(值为 null)

    + +

    +

    当前Ip地址为:{{control.value}}

    diff --git a/src/ipsection/demo/src/app/ipsection/ipsection-valid-formgroup.html b/src/ipsection/demo/src/app/ipsection/ipsection-valid-formgroup.html new file mode 100644 index 0000000..dc9718e --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/ipsection-valid-formgroup.html @@ -0,0 +1,6 @@ +
    + +
    +
    +
    Current Value: {{ formGroup.value.ipsection }}
    +
    diff --git a/src/ipsection/demo/src/app/ipsection/ipsection-valid.html b/src/ipsection/demo/src/app/ipsection/ipsection-valid.html new file mode 100644 index 0000000..fe6feb2 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/ipsection-valid.html @@ -0,0 +1,4 @@ + +
    +
    Current Value: {{ ipValue }}
    +
    diff --git a/src/ipsection/demo/src/app/ipsection/webdoc/ipsection-demos.js b/src/ipsection/demo/src/app/ipsection/webdoc/ipsection-demos.js new file mode 100644 index 0000000..5df8a04 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/webdoc/ipsection-demos.js @@ -0,0 +1,66 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'ipsection-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'ipsection basic', + }, + desc: { + 'zh-CN': '

    通过属性configs配置组件各网段的渲染数据。', + 'en-US': '

    ipsection basic

    ', + }, + apis: [ + 'TiIpsectionComponent.properties.configs', + 'TiIpsectionConfig.properties.section', + 'TiIpsectionConfig.properties.options', + 'TiIpsectionConfig.properties.bold', + ], + }, + { + demoId: 'ipsection-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'ipsection disabled', + }, + desc: { + 'zh-CN': + '通过属性disabled配置整体是否为禁用状态;通过属性configs配置各网段是否为禁用状态。', + 'en-US': '

    ipsection disabled

    ', + }, + apis: [ + 'TiIpsectionComponent.properties.disabled', + 'TiIpsectionConfig.properties.disabled', + ], + }, + { + demoId: 'ipsection-valid', + name: { + 'zh-CN': '表单校验', + 'en-US': 'ipsection valid', + }, + desc: { + 'zh-CN': + '通过属性configs配置组件各网段的校验规则和 tip 等配置项。', + 'en-US': '

    ipsection valid

    ', + }, + apis: [ + 'TiIpsectionConfig.properties.validation', + 'TiIpsectionConfig.properties.validationRules', + ], + }, + { + demoId: 'ipsection-valid-formgroup', + name: { + 'zh-CN': '响应式表单', + 'en-US': 'ipsection valid formgroup', + }, + desc: { + 'zh-CN': '响应式表单的用法。', + 'en-US': '

    ipsection valid formgroup

    ', + }, + }, + ], +}; diff --git a/src/ipsection/demo/src/app/ipsection/webdoc/ipsection.cn.md b/src/ipsection/demo/src/app/ipsection/webdoc/ipsection.cn.md new file mode 100644 index 0000000..e6594eb --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/webdoc/ipsection.cn.md @@ -0,0 +1,28 @@ +--- +title: IPsection IP分段 +--- +# IPsection IP分段 + +
    + +IPsection 是对 IP 各网段分开处理的组件。   + ++ 各网段有下拉框和输入框两种类型。 + +```typescript +import { TiIpsectionModule } from '@opentiny/ng';; +``` + +
    + +
    + +IPsection 是对 IP 各网段分开处理的组件。   + ++ 各网段有下拉框和输入框两种类型。 + +```typescript +import { TiIpsectionModule } from '@opentiny/ng';; +``` + +
    diff --git a/src/ipsection/demo/src/app/ipsection/webdoc/ipsection.en.md b/src/ipsection/demo/src/app/ipsection/webdoc/ipsection.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/ipsection/demo/src/app/ipsection/webdoc/ipsection.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/ipsection/demo/src/favicon.ico b/src/ipsection/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/ipsection/demo/src/index.html b/src/ipsection/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/ipsection/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/ipsection/demo/src/main.ts b/src/ipsection/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/ipsection/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/ipsection/demo/test.ts b/src/ipsection/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/ipsection/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/ipsection/demo/tsconfig.app.json b/src/ipsection/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/ipsection/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/ipsection/demo/tsconfig.spec.json b/src/ipsection/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/ipsection/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/ipsection/lib/index.ts b/src/ipsection/lib/index.ts new file mode 100644 index 0000000..9f0b4c9 --- /dev/null +++ b/src/ipsection/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiIpsectionModule'; diff --git a/src/ipsection/lib/ng-package.json b/src/ipsection/lib/ng-package.json new file mode 100644 index 0000000..bd5016c --- /dev/null +++ b/src/ipsection/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/ipsection", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/ipsection/lib/package.json b/src/ipsection/lib/package.json new file mode 100644 index 0000000..9867542 --- /dev/null +++ b/src/ipsection/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-ipsection", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-validation": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-select": "~1.0.0-beta.0", + "@opentiny/ng-text": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/ipsection/lib/project.json b/src/ipsection/lib/project.json new file mode 100644 index 0000000..884285d --- /dev/null +++ b/src/ipsection/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/ipsection/lib", + "sourceRoot": "src/ipsection/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/ipsection"], + "options": { + "project": "src/ipsection/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/ipsection"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js ipsection" + }, + { + "command": "ng default-build ipsection" + }, + { + "command": "node build/clear-default-theme.js ipsection" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/ipsection && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build ipsection && ng pack ipsection && node build/publish.js ipsection --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/ipsection/lib/src/TiIpsectionComponent.ts b/src/ipsection/lib/src/TiIpsectionComponent.ts new file mode 100644 index 0000000..8fada88 --- /dev/null +++ b/src/ipsection/lib/src/TiIpsectionComponent.ts @@ -0,0 +1,267 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Injector, + Input, + Renderer2, + SimpleChanges +} from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiValidationConfig } from '@opentiny/ng-validation'; +import { AbstractControl, FormBuilder, FormControl, FormGroup, NgControl, NgModel, ValidationErrors, ValidatorFn } from '@angular/forms'; +import packageInfo from '../package.json'; +export interface TiIpsectionConfig { + /** + * 网段唯一标识 + */ + section: number; + /** + * 网段为 select 类型的下拉数据集 + */ + options?: Array; + /** + * 网段为 input 类型时文本是否加粗 + */ + bold?: boolean; + /** + * 网段为 input 类型时的校验提示信息 + */ + validation?: TiValidationConfig; + /** + * 网段为 input 类型时的校验规则,10.1.15 支持 ValidatorFn 类型 + */ + validationRules?: Array | ValidatorFn; + /** + * 网段是否禁用 + */ + disabled?: boolean; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} +/** + * Ipsection组件,Ip各网段分开处理,各网段有下拉类和输入类两种类型; + * + * + */ +@Component({ + selector: 'ti-ipsection', + templateUrl: 'ipsection.html', + styleUrls: ['./ipsection.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-ipsection-container]': 'true', + '(blur)': 'onBlur($event)' + }, + providers: [TiFormComponent.getValueAccessor(TiIpsectionComponent)] +}) +export class TiIpsectionComponent extends TiFormComponent { + /** + * Ip 各网段数据集 + */ + @Input() configs: Array; + /** + * @ignore + * 存放 input 控件实例数组 + */ + private controls: Array = []; + /** + * @ignore + * 格式化输入 + */ + public maskInput: string = '000'; + /** + * @ignore + * 存放各网段 ip 值的数组 + */ + public ipValues: Array = []; + /** + * input控件 FormGroup 实例,服务可通过该实例动态更改某一FormControl实例相关属性(value,disabled,校验规则等) + */ + public formGroup: FormGroup; + protected versionInfo: string = super.getVersion(packageInfo); + + constructor( + protected hostRef: ElementRef, + protected renderer2: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + private fb: FormBuilder, + private injector: Injector + ) { + super(hostRef, renderer2, changeDetectorRef); + this.formGroup = fb.group({}); + } + + /** + * 将ip值分割为数组 + */ + private static splitToIPArray(ipValue: string): Array { + const ipArray: Array = ipValue ? ipValue.replace(/\//, '.').split('.') : []; + + return ipArray; + } + + /** + * 将ip数组拼接为ip值 + */ + private static joinToIPValue(ipArray: Array): string { + let ipValue: string = ipArray.join('.'); + if (ipArray.length > 4) { + ipValue = ipValue.replace(/(.*)\./, '$1/'); + } + + return ipValue; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['configs'] && !changes['configs'].firstChange) { + this.initIpValue(); + } + } + + ngOnInit(): void { + super.ngOnInit(); + // 创建表单控件实例 + for (let i: number = 0; i < this.configs.length; i++) { + if (!this.configs[i].options) { + this.controls[i] = new FormControl(); + this.formGroup.addControl(`input_${i}`, this.controls[i]); + // input类型网段值更改时触发事件 + this.controls[i].valueChanges.subscribe((value: any) => { + this.updateIpValue(i, value); + }); + } + } + } + + ngAfterViewInit(): void { + const itemElems: HTMLCollection = this.hostRef.nativeElement.getElementsByClassName('ti3-ipsection'); + const focusElements: Array = []; + // 获取ipsection对应控件 + const ipsectionControl: any = this.injector.get(NgControl); + // 添加tiGroup属性,方便业务整体校验时校验组件内部formGroup,获取正确的错误信息 + if (ipsectionControl) { + const control: any = ipsectionControl.control; + control.tiGroup = this.formGroup; + control.setValidators(this.validate.bind(this)); + } + + // 设置可聚焦元素 + for (let i: number = 0; i < this.configs.length; i++) { + if (itemElems[i].firstChild) { + focusElements.push(itemElems[i].firstChild); + } else { + this.setAttr(itemElems[i], 'disabled', this.configs[i].disabled || this.disabled); + // 此处添加formControlName|name属性原因:方便业务整体校验时通过formControlName|name找到对应元素 + const controlName: string = ipsectionControl instanceof NgModel ? 'name' : 'formControlName'; + this.renderer.setAttribute(itemElems[i], controlName, `input_${i}`); + focusElements.push(itemElems[i]); + } + } + this.setFocusableElems(focusElements); + } + + ngDoCheck(): void { + super.ngDoCheck(); + this.changeDetectorRef.markForCheck(); + } + + /** + * @ignore + * 获取model值,并进行初始化处理 + */ + writeValue(value: any): void { + super.writeValue(value); + this.ipValues = TiIpsectionComponent.splitToIPArray(value); + + this.initIpValue(); + } + /** + * @ignore + * 整体失焦触发事件 + */ + onBlur(event: any): void { + this.formatValue(); + } + + /** + * @ignore + * select类型网段值更改时触发事件 + */ + onChange(i: number, value: any): void { + this.updateIpValue(i, value); + } + + /** + * 初始化ip值 + */ + private initIpValue(): void { + for (let i: number = 0; i < this.configs.length; i++) { + if (this.configs[i].options) { + // 初始显示默认选中项,若value值在options存在,则显示该值,否则显示下拉配置项第一项 + this.configs[i].selected = this.configs[i].options.find((item) => item === this.ipValues[i]) + ? this.ipValues[i] + : this.configs[i].options[0]; + this.updateIpValue(i, this.configs[i].selected); + } else { + // 初始设置value,disabled及校验规则 + this.ipValues[i] = this.ipValues[i] ? parseInt(this.ipValues[i], 10).toString() : ''; + this.controls[i].setValue(this.ipValues[i]); + this.controls[i].setValidators(this.configs[i].validationRules); + this.configs[i].disabled || this.disabled ? this.controls[i].disable() : this.controls[i].enable(); + } + } + } + + /** + * 初始及失焦处理输入框值,清除Ip网段前面的0 + */ + private formatValue(): void { + for (let i: number = 0; i < this.configs.length; i++) { + if (!this.configs[i].options) { + this.controls[i].setValue(parseInt(this.controls[i].value, 10).toString()); + } + } + } + + /** + * 更新model值 + */ + private updateIpValue(index?: number, value?: any): void { + if (this.ipValues.length <= 0 || this.ipValues[index] === value) { + return; + } + + this.ipValues.splice(index, 1, value); + // 解决model值更新后ExpressionChangedAfterItHasBeenCheckedError报错问题,onpush正常default报错 + setTimeout(() => { + const ipValue: string = TiIpsectionComponent.joinToIPValue(this.ipValues); + this.model = ipValue === '...' || ipValue === '.../' ? '' : ipValue; + }, 0); + } + + // 自定义验证器,同步input表单控件组的状态 + private validate(control: AbstractControl): ValidationErrors | null { + return this.formGroup.invalid + ? { + 'input-formGroup': { + value: this.formGroup.value + } + } + : null; + } +} diff --git a/src/ipsection/lib/src/TiIpsectionModule.ts b/src/ipsection/lib/src/TiIpsectionModule.ts new file mode 100644 index 0000000..5c8b2f7 --- /dev/null +++ b/src/ipsection/lib/src/TiIpsectionModule.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TiIpsectionComponent } from './TiIpsectionComponent'; +import { TiSelectModule } from '@opentiny/ng-select'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiValidationModule } from '@opentiny/ng-validation'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiSelectModule, TiTextModule, ReactiveFormsModule, TiValidationModule], + exports: [TiIpsectionComponent], + declarations: [TiIpsectionComponent] +}) +export class TiIpsectionModule {} +export { TiIpsectionComponent, TiIpsectionConfig } from './TiIpsectionComponent'; diff --git a/src/ipsection/lib/src/ipsection.html b/src/ipsection/lib/src/ipsection.html new file mode 100644 index 0000000..969452d --- /dev/null +++ b/src/ipsection/lib/src/ipsection.html @@ -0,0 +1,35 @@ + + +
    + +
    +
    + + + {{option}} + + +
    +
    diff --git a/src/ipsection/lib/src/ipsection.less b/src/ipsection/lib/src/ipsection.less new file mode 100644 index 0000000..cb20651 --- /dev/null +++ b/src/ipsection/lib/src/ipsection.less @@ -0,0 +1,52 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-ipsection-dot-size: 2px; + --ti-ipsection-input-width: 50px; + --ti-ipsection-select-width: 70px; +} + +:host.ti3-ipsection-container { + display: inline-block; + // ipsection整体校验时无需红色背景 + @{tiny-invalid-class} { + background-color: transparent; + } +} + +.ti3-ipsection-input { + width: var(--ti-ipsection-input-width); + text-align: center; +} + +.ti3-ipsection-select { + width: var(--ti-ipsection-select-width); +} + +.ti3-ipsection-text-blod { + font-weight: var(--ti-common-font-weight-7); +} + +.ti3-ipsection-division { + display: inline-block; + vertical-align: middle; + background: var(--ti-common-color-icon-normal); +} + +.ti3-ipsection-dot { + width: var(--ti-ipsection-dot-size); + height: var(--ti-ipsection-dot-size); + border-radius: 50%; + .box-sizing(); + margin: 0 var(--ti-common-space-2x); +} + +.ti3-ipsection-maskdivision { + width: 18px; + height: 1px; + transform: rotate(110deg); +} + +.ti3-ipsection-division-disabled { + background: var(--ti-common-color-icon-disabled); +} diff --git a/src/labeleditor/demo/project.json b/src/labeleditor/demo/project.json new file mode 100644 index 0000000..d2a9d1b --- /dev/null +++ b/src/labeleditor/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/labeleditor/demo", + "sourceRoot": "src/labeleditor/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/labeleditor", + "index": "src/labeleditor/demo/src/index.html", + "main": "src/labeleditor/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/labeleditor/demo/tsconfig.app.json", + "assets": ["src/labeleditor/demo/src/favicon.ico", "src/labeleditor/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "labeleditor-demo:build:production" + }, + "development": { + "browserTarget": "labeleditor-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js labeleditor" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/labeleditor/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/labeleditor/demo/tsconfig.spec.json", + "karmaConfig": "src/labeleditor/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/labeleditor/demo/src/app/AppComponent.ts b/src/labeleditor/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/labeleditor/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/labeleditor/demo/src/app/AppModule.ts b/src/labeleditor/demo/src/app/AppModule.ts new file mode 100644 index 0000000..1074c19 --- /dev/null +++ b/src/labeleditor/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { LabeleditorTestModule } from './labeleditor/LabeleditorTestModule'; + +@NgModule({ + imports: [ + LabeleditorTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/labeleditor/demo/src/app/IndexComponent.ts b/src/labeleditor/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..78f3f13 --- /dev/null +++ b/src/labeleditor/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { LabeleditorTestModule } from './labeleditor/LabeleditorTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = LabeleditorTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/labeleditor/demo/src/app/app.html b/src/labeleditor/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/labeleditor/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorAutotipComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorAutotipComponent.ts new file mode 100644 index 0000000..bc38657 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorAutotipComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-autotip.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorAutotipComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + iconTip: string = '请点击进行编辑'; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorBasicComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorBasicComponent.ts new file mode 100644 index 0000000..23b6761 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorBasicComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-basic.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorBasicComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorDisabledComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorDisabledComponent.ts new file mode 100644 index 0000000..c68ca46 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorDisabledComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-disabled.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorDisabledComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + iconTip: string = '当前为受限状态,不可编辑'; + disabled: boolean = true; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorEventsComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorEventsComponent.ts new file mode 100644 index 0000000..e5eefb1 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorEventsComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './labeleditor-events.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorEventsComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + myLogs: Array = []; + confirm(val: string): void { + this.myLogs = [...this.myLogs, `on confirm`]; + } + cancel(): void { + this.myLogs = [...this.myLogs, `on cancel`]; + } + editor(): void { + this.myLogs = [...this.myLogs, `on editor`]; + } +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorIcontipcontextComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorIcontipcontextComponent.ts new file mode 100644 index 0000000..9af0c63 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorIcontipcontextComponent.ts @@ -0,0 +1,49 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-iconTipContext.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorIconTipContextComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + componentClass: any = TemplateComponent; + myLogs: Array = []; + // 定义tip显示内容的上下文 + tipContext: any = { + label: 'Click here to edit!', // 该属性与TemplateComponent组件中的@Input属性定义对应 + outputs: { + // 定义在outputs对象中的属性(例如click)与TemplateComponent组件中的@Output属性定义对应 + click: ($event: string): void => { + this.myLogs = [...this.myLogs, `on ${$event}`]; + } + } + }; + + iconContext: string = '这是一个 template'; +} +// 自定义组件,此处为了方便demo展示与tip生成组件写在同一文件,项目中请将组件单独写在一个文件中 +@Component({ + template: ` + + {{ label }} +
    + +
    + ` +}) +export class TemplateComponent { + @Input() label: string; + @Output() readonly click: EventEmitter = new EventEmitter(); + + onClick(): void { + this.click.emit('click'); + } +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlengthComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlengthComponent.ts new file mode 100644 index 0000000..93f4204 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlengthComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-maxlength.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorMaxlengthComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + label1: string = '限制文本长度'; + maxlength: number = 10; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlineComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlineComponent.ts new file mode 100644 index 0000000..5f48c10 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorMaxlineComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-maxline.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorMaxlineComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorMultilineSizeComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorMultilineSizeComponent.ts new file mode 100644 index 0000000..c8bce70 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorMultilineSizeComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-multiline-size.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 250px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorMultilineSizeComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorResizeComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorResizeComponent.ts new file mode 100644 index 0000000..f0367d8 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorResizeComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './labeleditor-resize.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorResizeComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorTestModule.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorTestModule.ts new file mode 100644 index 0000000..7730fc1 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorTestModule.ts @@ -0,0 +1,70 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TiIconModule, TiLabeleditorModule, TiButtonModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { LabeleditorBasicComponent } from './LabeleditorBasicComponent'; +import { LabeleditorAutotipComponent } from './LabeleditorAutotipComponent'; +import { LabeleditorResizeComponent } from './LabeleditorResizeComponent'; +import { LabeleditorMaxlengthComponent } from './LabeleditorMaxlengthComponent'; +import { LabeleditorEventsComponent } from './LabeleditorEventsComponent'; +import { LabeleditorValidationComponent } from './LabeleditorValidationComponent'; +import { LabeleditorDisabledComponent } from './LabeleditorDisabledComponent'; +import { LabeleditorMultilineSizeComponent } from './LabeleditorMultilineSizeComponent'; +import { LabeleditorMaxlineComponent } from './LabeleditorMaxlineComponent'; +import { LabeleditorValidationAsyncComponent } from './LabeleditorValidationAsyncComponent'; +import { LabeleditorIconTipContextComponent, TemplateComponent } from './LabeleditorIcontipcontextComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiIconModule, + TiButtonModule, + ReactiveFormsModule, + TiLabeleditorModule, + TiValidationModule, + DemoLogModule, + RouterModule.forChild(LabeleditorTestModule.ROUTES) + ], + declarations: [ + LabeleditorBasicComponent, + LabeleditorAutotipComponent, + LabeleditorResizeComponent, + LabeleditorMaxlengthComponent, + LabeleditorValidationComponent, + LabeleditorEventsComponent, + LabeleditorDisabledComponent, + LabeleditorMaxlineComponent, + LabeleditorMultilineSizeComponent, + TemplateComponent, + LabeleditorValidationAsyncComponent, + LabeleditorIconTipContextComponent + ], + entryComponents: [TemplateComponent] +}) +export class LabeleditorTestModule { + static readonly ROUTES: Routes = [ + { path: 'labeleditor/labeleditor-basic', component: LabeleditorBasicComponent, data: { label: '基础' } }, + { path: 'labeleditor/labeleditor-autotip', component: LabeleditorAutotipComponent, data: { label: '智能提示' } }, + { + path: 'labeleditor/labeleditor-iconTipContext', + component: LabeleditorIconTipContextComponent, + data: { label: '图标提示为插槽或组件' } + }, + { path: 'labeleditor/labeleditor-resize', component: LabeleditorResizeComponent, data: { label: '多行文本框调整大小' } }, + { path: 'labeleditor/labeleditor-maxline', component: LabeleditorMaxlineComponent, data: { label: '最大行数' } }, + { path: 'labeleditor/labeleditor-maxlength', component: LabeleditorMaxlengthComponent, data: { label: '文本长度' } }, + { path: 'labeleditor/labeleditor-validation', component: LabeleditorValidationComponent, data: { label: '校验' } }, + { path: 'labeleditor/labeleditor-validation-async', component: LabeleditorValidationAsyncComponent, data: { label: '异步校验' } }, + { path: 'labeleditor/labeleditor-disabled', component: LabeleditorDisabledComponent, data: { label: '禁用' } }, + { path: 'labeleditor/labeleditor-events', component: LabeleditorEventsComponent, data: { label: '事件' } }, + { + path: 'labeleditor/labeleditor-multiline-size', + component: LabeleditorMultilineSizeComponent, + data: { label: '多行文本框可设置宽高' } + } + ]; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationAsyncComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationAsyncComponent.ts new file mode 100644 index 0000000..a812d2a --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationAsyncComponent.ts @@ -0,0 +1,56 @@ +import { Component } from '@angular/core'; +import { AbstractControl, AsyncValidatorFn, FormControl, ValidationErrors } from '@angular/forms'; +import { TiValidationDirective } from '@opentiny/ng'; +import { Observable, of } from 'rxjs'; +import { catchError, debounceTime, delay, distinctUntilChanged, first, map, switchMap } from 'rxjs/operators'; + +@Component({ + templateUrl: './labeleditor-validation-async.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorValidationAsyncComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + asyncValidatorRules: any = [CustomAsyncValidators.isRightUserName()]; + control: FormControl = new FormControl(this.label); +} + +export class CustomAsyncValidators { + // 自定义异步校验规则 + static isRightUserName(): AsyncValidatorFn { + return (control: AbstractControl): Promise | Observable => { + if (control.valueChanges) { + // 初始时control中可能没有valueChanges属性 + return control.valueChanges.pipe( + debounceTime(TiValidationDirective.ASYNC_DEBOUNCE_TIME), // 防抖处理(输入停顿后再进行校验) + distinctUntilChanged(), // 防止对前后相同的值进行校验 + switchMap((value: string) => CustomAsyncValidators.isRight(value)), // 进行后台请求校验 + map((isRight: boolean) => { + // 拿到后台返回值 + // 异步校验需要在校验错误信息中通过 tiAsyncErrorMessage 属性来设置校验错误提示信息 + return isRight ? { rightUserName: { actualValue: control.value, tiAsyncErrorMessage: '用户名不正确' } } : null; + }), + first(), // complete control.valueChanges + catchError(() => null) + ); + } else { + return of(null); + } + }; + } + + // 模拟后台异步请求 + static isRight(value: string): Observable { + return of(value !== 'tiny').pipe( + delay(2000), + catchError(() => of(false)) + ); + } +} diff --git a/src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationComponent.ts b/src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationComponent.ts new file mode 100644 index 0000000..53df308 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/LabeleditorValidationComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { TiValidationConfig, TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './labeleditor-validation.html', + styles: [ + ` + .demo-labeleditor-container { + line-height: 30px; + max-width: 300px; + display: inline-block; + } + ` + ] +}) +export class LabeleditorValidationComponent { + label: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + label1: string = '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪,君不见高堂明镜悲白发,朝如青丝暮成雪'; + control: FormControl = new FormControl(this.label); + validation: TiValidationConfig = { + tip: '请输入' + }; + validationRules: any = [TiValidators.equal('aa'), TiValidators.required]; +} diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-autotip.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-autotip.html new file mode 100644 index 0000000..ba4f109 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-autotip.html @@ -0,0 +1,2 @@ + + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-basic.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-basic.html new file mode 100644 index 0000000..b70c6b3 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-basic.html @@ -0,0 +1,5 @@ +

    1.no multiline

    + +

    2.multiline: true

    + + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-disabled.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-disabled.html new file mode 100644 index 0000000..77fe39a --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-disabled.html @@ -0,0 +1,2 @@ + + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-events.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-events.html new file mode 100644 index 0000000..a824483 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-events.html @@ -0,0 +1,10 @@ + + + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-icontipcontext.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-icontipcontext.html new file mode 100644 index 0000000..a04c772 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-icontipcontext.html @@ -0,0 +1,25 @@ +

    1.component

    + + + +

    2.template

    + + + + + {{context}} + + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-maxlength.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-maxlength.html new file mode 100644 index 0000000..cb26a4d --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-maxlength.html @@ -0,0 +1,11 @@ +

    1.multiline: true + maxlength: 125

    + + +

    2.multiline: false + maxlength: 10

    + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-maxline.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-maxline.html new file mode 100644 index 0000000..e426321 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-maxline.html @@ -0,0 +1 @@ + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-multiline-size.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-multiline-size.html new file mode 100644 index 0000000..4a348c4 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-multiline-size.html @@ -0,0 +1,9 @@ + + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-resize.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-resize.html new file mode 100644 index 0000000..edf44a3 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-resize.html @@ -0,0 +1,24 @@ +

    1.resize: vertical,只能垂直方向展开

    + + +

    2.resize: horizontal,只能水平方向展开

    + + +

    3.resize: both,水平/垂直方向都可展开

    + + +

    4.resize: none,水平/垂直方向都不可展开

    + + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-validation-async.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-validation-async.html new file mode 100644 index 0000000..e47d3e3 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-validation-async.html @@ -0,0 +1,4 @@ +

    1.模板表单

    + +

    2.响应式表单

    + diff --git a/src/labeleditor/demo/src/app/labeleditor/labeleditor-validation.html b/src/labeleditor/demo/src/app/labeleditor/labeleditor-validation.html new file mode 100644 index 0000000..1e959cc --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/labeleditor-validation.html @@ -0,0 +1,17 @@ +

    1.模板式表单

    + + +

    2.响应式表单

    + + diff --git a/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor-demos.js b/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor-demos.js new file mode 100644 index 0000000..47afb63 --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor-demos.js @@ -0,0 +1,150 @@ +export default { + column: '2', + demos: [ + { + demoId: 'labeleditor-basic', + name: { + 'zh-CN': '基础', + 'en-US': 'labeleditor basic' + }, + desc: { + 'zh-CN': '

    通过属性multiline配置是多/单行文本编辑。

    ', + 'en-US': 'labeleditor basic' + }, + apis: ['TiLabeleditorComponent.properties.multiline'] + }, + { + demoId: 'labeleditor-maxline', + name: { + 'zh-CN': '最大行数', + 'en-US': 'labeleditor maxline' + }, + desc: { + 'zh-CN': '

    通过属性maxline配置最大行数,该属性只在autoTiptrue时生效。

    ', + 'en-US': 'labeleditor maxline' + }, + apis: ['TiLabeleditorComponent.properties.maxLine'] + }, + { + demoId: 'labeleditor-autotip', + name: { + 'zh-CN': '智能提示', + 'en-US': 'labeleditor autotip' + }, + desc: { + 'zh-CN': '

    通过属性autoTip配置编辑完成状态时文本是否自动溢出并提示。

    ', + 'en-US': 'labeleditor autotip' + }, + apis: ['TiLabeleditorComponent.properties.autoTip'] + }, + { + demoId: 'labeleditor-icontipcontext', + name: { + 'zh-CN': '图标提示为模板或组件', + 'en-US': 'labeleditor iconTipContext' + }, + desc: { + 'zh-CN': '

    通过属性iconTip配置编辑文本图标提示内容;通过属性iconTipContext配置图标提示上下文。

    ', + 'en-US': 'labeleditor iconTipContext' + }, + apis: ['TiLabeleditorComponent.properties.iconTip', 'TiLabeleditorComponent.properties.iconTipContext'] + }, + { + demoId: 'labeleditor-maxlength', + name: { + 'zh-CN': '文本长度', + 'en-US': 'labeleditor maxlength' + }, + desc: { + 'zh-CN': '

    通过属性maxlength配置文本最大长度。

    ', + 'en-US': 'labeleditor maxlength' + }, + apis: ['TiLabeleditorComponent.properties.maxlength'] + }, + { + demoId: 'labeleditor-resize', + name: { + 'zh-CN': '多行文本框调整大小', + 'en-US': 'labeleditor resize' + }, + desc: { + 'zh-CN': + '

    通过属性resize配置多行文本编辑框调整的方向,包含both、none、vertical、horizontal四种类型

    ', + 'en-US': 'labeleditor resize' + }, + apis: ['TiLabeleditorComponent.properties.resize'] + }, + { + demoId: 'labeleditor-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'labeleditor disabled' + }, + desc: { + 'zh-CN': '

    通过属性disabled配置禁用状态。

    ', + 'en-US': 'labeleditor disabled' + }, + apis: ['TiLabeleditorComponent.properties.disabled'] + }, + { + demoId: 'labeleditor-events', + name: { + 'zh-CN': '事件', + 'en-US': 'labeleditor events' + }, + desc: { + 'zh-CN': + '

    当点击编辑图标时触发editor事件;当点击完成图标时触发confirm事件;当点击取消图标时触发cancel事件。

    ', + 'en-US': 'labeleditor events' + }, + apis: ['TiLabeleditorComponent.events.editor', 'TiLabeleditorComponent.events.confirm', 'TiLabeleditorComponent.events.cancel'] + }, + { + demoId: 'labeleditor-multiline-size', + name: { + 'zh-CN': '多行文本框可设置宽高', + 'en-US': 'labeleditor multiline size' + }, + desc: { + 'zh-CN': '

    通过属性widthheight配置多行文本编辑框的宽高。

    ', + 'en-US': 'labeleditor multiline size' + }, + apis: ['TiLabeleditorComponent.properties.width', 'TiLabeleditorComponent.properties.height'] + }, + { + demoId: 'labeleditor-validation', + name: { + 'zh-CN': '同步校验', + 'en-US': 'labeleditor validation' + }, + desc: { + 'zh-CN': + '

    通过属性validation配置校验相关信息,详见接口TiValidationConfig;通过属性validationRules配置校验规则。

    ', + 'en-US': 'labeleditor validation' + }, + apis: [ + 'TiLabeleditorComponent.properties.validation', + 'TiLabeleditorComponent.properties.validationRules', + 'TiValidationConfig.properties.errorMessage', + 'TiValidationConfig.properties.errorMessageWrapper', + 'TiValidationConfig.properties.passwordConfig', + 'TiValidationConfig.properties.tip', + 'TiValidationConfig.properties.tipMaxWidth', + 'TiValidationConfig.properties.tipPosition', + 'TiValidationConfig.properties.type' + ] + }, + { + demoId: 'labeleditor-validation-async', + name: { + 'zh-CN': '异步校验', + 'en-US': 'labeleditor validation async' + }, + desc: { + 'zh-CN': '

    通过属性asyncValidatorRules配置单行文本编辑时异步校验规则。

    ', + 'en-US': 'labeleditor validation async' + }, + apis: ['TiLabeleditorComponent.properties.asyncValidatorRules'] + } + ] +}; diff --git a/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.cn.md b/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.cn.md new file mode 100644 index 0000000..68f90ed --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.cn.md @@ -0,0 +1,15 @@ +--- +title: Labeleditor 可编辑文本 +--- + +# Labeleditor 可编辑文本 + +
    + +主要实现了编辑文本功能的组件。 + +```typescript +import { TiLabeleditorModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.en.md b/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.en.md new file mode 100644 index 0000000..19e93ef --- /dev/null +++ b/src/labeleditor/demo/src/app/labeleditor/webdoc/labeleditor.en.md @@ -0,0 +1,13 @@ +--- +title: Labeleditor +--- + +# Labeleditor + +
    + +```typescript +import { TiLabeleditorModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/labeleditor/demo/src/favicon.ico b/src/labeleditor/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d7a7af634ee6d643ac81d81feadff569ac25de3d GIT binary patch literal 300852 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 + + + + + + Tiny + + + + + + + + + diff --git a/src/labeleditor/demo/src/main.ts b/src/labeleditor/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/labeleditor/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/labeleditor/demo/tsconfig.app.json b/src/labeleditor/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/labeleditor/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/labeleditor/lib/index.ts b/src/labeleditor/lib/index.ts new file mode 100644 index 0000000..5e44df5 --- /dev/null +++ b/src/labeleditor/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiLabeleditorModule'; +export * from './src/TiLabeleditorComponent'; diff --git a/src/labeleditor/lib/ng-package.json b/src/labeleditor/lib/ng-package.json new file mode 100644 index 0000000..306040f --- /dev/null +++ b/src/labeleditor/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/labeleditor", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/labeleditor/lib/package.json b/src/labeleditor/lib/package.json new file mode 100644 index 0000000..cd219b9 --- /dev/null +++ b/src/labeleditor/lib/package.json @@ -0,0 +1,18 @@ +{ + "name": "@opentiny/ng-labeleditor", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@angular/forms": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-validation": "~1.0.0-beta.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-textarea": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/labeleditor/lib/project.json b/src/labeleditor/lib/project.json new file mode 100644 index 0000000..1e236a3 --- /dev/null +++ b/src/labeleditor/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/labeleditor/lib", + "sourceRoot": "src/labeleditor/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/labeleditor"], + "options": { + "project": "src/labeleditor/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/labeleditor"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js labeleditor" + }, + { + "command": "ng default-build labeleditor" + }, + { + "command": "node build/clear-default-theme.js labeleditor" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/labeleditor && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build labeleditor && ng pack labeleditor && node build/publish.js labeleditor --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/labeleditor/lib/src/TiLabeleditorComponent.ts b/src/labeleditor/lib/src/TiLabeleditorComponent.ts new file mode 100644 index 0000000..1befcad --- /dev/null +++ b/src/labeleditor/lib/src/TiLabeleditorComponent.ts @@ -0,0 +1,259 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AsyncValidatorFn, FormControl, ValidatorFn } from '@angular/forms'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Input, + Output, + Renderer2, + TemplateRef, + ViewChild +} from '@angular/core'; +import { Subscription } from 'rxjs'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiValidationConfig } from '@opentiny/ng-validation'; +import { TiTextComponent } from '@opentiny/ng-text'; +import { TiTextareaComponent } from '@opentiny/ng-textarea'; +import { Util } from '@opentiny/ng-utils'; +/** + * 可编辑文本组件,主要实现了编辑文本的功能。组件有两种状态:编辑状态和非编辑状态。 + * 在非编辑状态:点击右侧笔图标时,切换到编辑状态。 + * 在编辑状态:点击右侧对号图标时,切换到非编辑状态, + * 并且将输入框中内容显示在非编辑状态的文本中; + * 点击右侧叉图标时,切换到非编辑状态,非编辑状态的文本保持之前的不变,用户输入不生效。 + * + * ../tinyplus3demo/#/labeleditor/labeleditor-all + */ +@Component({ + selector: 'ti-labeleditor', + templateUrl: 'labeleditor.html', + styleUrls: ['./labeleditor.less'], + host: { + '[class.ti3-labeleditor-editing]': 'isEditing' + }, + providers: [TiFormComponent.getValueAccessor(TiLabeleditorComponent)], + changeDetection: ChangeDetectionStrategy.OnPush +}) +// extends TiFormComponent +export class TiLabeleditorComponent extends TiFormComponent { + constructor(protected elementRef: ElementRef, protected renderer2: Renderer2, protected changeDetectorRef: ChangeDetectorRef) { + super(elementRef, renderer2, changeDetectorRef); + } + /** + * 是否超出显示 tip + */ + @Input() autoTip: boolean = true; + /** + * 是否为多行编辑 + */ + @Input() multiline: boolean; + /** + * 允许调整文本框大小的方向: + * + * none(不可调整组件大小): + * + * vertical(仅可调整垂直方向的大小,即调整组件的高度) + * + * horizontal(仅可调节水平方向的大小,即调整组件的宽度) + * + * both(水平和垂直方向均可调节,宽高都可调节) + */ + @Input() resize: 'none' | 'vertical' | 'horizontal' | 'both' = 'both'; + /** + * 文本最大长度 + */ + @Input() maxlength: number; + /** + * 编辑图标 tip 内容 + */ + @Input() iconTip: string | TemplateRef | any; + /** + * 图标提示内容对应的上下文,tip 内容类型为 templateRef 或 Component 形式时使用 + */ + @Input() iconTipContext: any; + /** + * 校验相关信息 + */ + @Input() validation: TiValidationConfig; + /** + * 用户自定义同步校验规则 + * + * 接口值被当作 new FormControl() 的第二个参数 + * + * 接口类型与 new FormControl() 的第二个参数格式一致,详情参考 angular 表单部分 + */ + @Input() validationRules: Array; + /** + * + * 用户自定义异步校验规则(仅支持单行编辑) + * + * 接口值被当作 new FormControl() 的第三个参数 + * + * 接口类型与 new FormControl() 的第三个参数格式一致,详情参考 angular 表单部分 + */ + @Input() asyncValidatorRules: Array; + /** + * 文本域的宽度 + */ + @Input() width: string = '200px'; + /** + * 文本域的高度 + */ + @Input() height: string; + /** + * 非编辑状态的文本最大行数 + */ + @Input() maxLine: number = 3; + /** + * 点击编辑按钮触发的回调 + */ + @Output() readonly editor: EventEmitter = new EventEmitter(); + /** + * 点击确认按钮触发的回调 + */ + @Output() readonly confirm: EventEmitter = new EventEmitter(); + /** + * 点击取消按钮触发的回调 + */ + @Output() readonly cancel: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + @ViewChild('textarea', { static: false }) textareaComp: TiTextareaComponent; + /** + * @ignore + */ + @ViewChild('text', { static: false }) textComp: TiTextComponent; + /** + * @ignore + */ + @ViewChild('confirmIcon') confirmIconEle: ElementRef; + /** + * @ignore + */ + @ViewChild('edit', { static: false }) editEle: ElementRef; + /** + * @ignore + */ + public isEditing: boolean = false; // 是否处于编辑状态 + /** + * @ignore + */ + public value: string; // value内容 + /** + * @ignore + */ + public oldValue: string; // value内容 + /** + * @ignore + */ + public emptyValue: string = '--'; // 内容为空时显示‘--’ + /** + * @ignore + */ + public control: FormControl; + /** + * @ignore + */ + public valueInvalid: boolean = false; + private subscription: Subscription; + // tslint:disable-next-line:use-life-cycle-interface + ngOnInit(): void { + super.ngOnInit(); + // 如果显示文本框长度的话,文本框高度要加上最大长度文本的高度 18px + this.height = this.height ? this.height : this.maxlength ? '98px' : '80px'; + this.asyncValidatorRules = this.multiline ? null : this.asyncValidatorRules; + this.control = new FormControl(this.model, this.validationRules, this.asyncValidatorRules); + // 校验提示tip默认top方向展开 + if (this.validation) { + this.validation.tipPosition = Util.isUndefined(this.validation.tipPosition) ? 'top' : this.validation.tipPosition; + } + this.subscription = this.control.statusChanges.subscribe((status: string) => { + this.valueInvalid = status === 'INVALID' || status === 'PENDING'; + // 手动触发变更 + this.changeDetectorRef.markForCheck(); + }); + } + /** + * @ignore + */ + // tslint:disable-next-line:typedef + ngOnModelChange() { + if (this.model) { + this.control.setValue(this.model.slice(0, this.maxlength)); + } else { + this.control.setValue(''); + } + } + + // tslint:disable-next-line:use-life-cycle-interface + ngOnDestroy(): void { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + /** + * @ignore + * 点击编辑图标事件 + */ + public onClickEdit(): void { + if (this.disabled) { + return; + } + + this.isEditing = true; + this.oldValue = this.model; + this.editor.emit(this.model); + if (this.model) { + this.control.setValue(this.model.slice(0, this.maxlength)); + } + // 这里使用setTimeout是为了延时能够获取到编辑态的输入框 + setTimeout((): void => { + if (this.multiline) { + this.textareaComp.nativeElement.focus(); + if (Util.isUndefined(this.maxlength)) { + this.renderer2.removeAttribute(this.textareaComp.nativeElement, 'maxlength'); + } else { + this.renderer2.setAttribute(this.textareaComp.nativeElement, 'maxlength', String(this.maxlength)); + } + } else { + this.textComp.nativeElement.focus(); + } + }, 0); + } + /** + * @ignore + * 点击确认图标事件 + */ + public onClickConfirm(): void { + if (this.valueInvalid) { + return; + } + this.isEditing = false; + this.model = this.control.value; + this.confirm.emit(this.model); + } + /** + * @ignore + * 点击取消图标事件 + */ + public onClickCancel(): void { + this.isEditing = false; + this.cancel.emit(this.model); + this.control.setValue(this.oldValue); + } +} diff --git a/src/labeleditor/lib/src/TiLabeleditorModule.ts b/src/labeleditor/lib/src/TiLabeleditorModule.ts new file mode 100644 index 0000000..b5e6fe7 --- /dev/null +++ b/src/labeleditor/lib/src/TiLabeleditorModule.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TiLabeleditorComponent } from './TiLabeleditorComponent'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiTextareaModule } from '@opentiny/ng-textarea'; +import { TiValidationModule } from '@opentiny/ng-validation'; +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiOutlineModule, + TiIconModule, + TiOverflowModule, + TiIconModule, + TiTextareaModule, + TiTipModule, + TiTextModule, + TiValidationModule + ], + exports: [TiLabeleditorComponent], + declarations: [TiLabeleditorComponent] +}) +export class TiLabeleditorModule {} +export { TiLabeleditorComponent } from './TiLabeleditorComponent'; diff --git a/src/labeleditor/lib/src/labeleditor.html b/src/labeleditor/lib/src/labeleditor.html new file mode 100644 index 0000000..ce753ce --- /dev/null +++ b/src/labeleditor/lib/src/labeleditor.html @@ -0,0 +1,103 @@ + + + + + + + {{ model ? model : emptyValue}} + + + + + + + + + + + + + + + + + + diff --git a/src/labeleditor/lib/src/labeleditor.less b/src/labeleditor/lib/src/labeleditor.less new file mode 100644 index 0000000..a32d116 --- /dev/null +++ b/src/labeleditor/lib/src/labeleditor.less @@ -0,0 +1,65 @@ +.ti3-labeleditor-icon { + vertical-align: top; + line-height: 28px; + margin-left: var(--ti-common-space-2x); + cursor: pointer; +} +.ti3-icon-confirm { + color: var(--ti-common-color-success); +} +.ti3-icon-cancel { + color: var(--ti-common-color-error); +} + +.ti3-confirm-disabled.ti3-icon-confirm { + color: var(--ti-common-color-icon-disabled); + cursor: not-allowed; +} + +.ti3-labeleditor-container { + position: relative; + display: inline-block; +} +.ti3-labeleditor-textarea-container { + display: inline-block; +} + +.ti3-labeleditor-icon-container .ti3-labeleditor-icon { + vertical-align: middle; +} + +.ti3-editor-icon, +::ng-deep span[tiOverflowEndicon] { + font-size: var(--ti-common-font-size-2); + margin-left: var(--ti-common-space-2x); + color: var(--ti-common-color-icon-normal); + cursor: pointer; + &:hover { + color: var(--ti-common-color-icon-hover); + } +} + +// 校验失败提示需要换行呈现 +.ti3-editor-errorMessageWrapper { + line-height: 16px; + display: block; +} + +// 禁用样式 +:host[disabled] { + .ti3-editor-icon, + ::ng-deep span[tiOverflowEndicon] { + color: var(--ti-common-color-icon-disabled); + cursor: not-allowed; + } +} + +:host { + word-wrap: break-word; + word-break: break-word; +} + +:hostti-labeleditor.ti3-labeleditor-editing { + white-space: nowrap; + line-height: 0; // 编辑状态,同步失焦校验 异步校验消除中间的空白 +} diff --git a/src/layout/demo/karma.conf.js b/src/layout/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/layout/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/layout/demo/project.json b/src/layout/demo/project.json new file mode 100644 index 0000000..66093cd --- /dev/null +++ b/src/layout/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/layout/demo", + "sourceRoot": "src/layout/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/layout", + "index": "src/layout/demo/src/index.html", + "main": "src/layout/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/layout/demo/tsconfig.app.json", + "assets": ["src/layout/demo/src/favicon.ico", "src/layout/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "layout-demo:build:production" + }, + "development": { + "browserTarget": "layout-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js layout" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/layout/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/layout/demo/tsconfig.spec.json", + "karmaConfig": "src/layout/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/layout/demo/src/app/AppComponent.ts b/src/layout/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/layout/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/layout/demo/src/app/AppModule.ts b/src/layout/demo/src/app/AppModule.ts new file mode 100644 index 0000000..cf90703 --- /dev/null +++ b/src/layout/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { LayoutTestModule } from './layout/LayoutTestModule'; + +@NgModule({ + imports: [ + LayoutTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/layout/demo/src/app/IndexComponent.ts b/src/layout/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..4fb3937 --- /dev/null +++ b/src/layout/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { LayoutTestModule } from './layout/LayoutTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = LayoutTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/layout/demo/src/app/app.html b/src/layout/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/layout/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/layout/demo/src/app/layout/LayoutBasicComponent.ts b/src/layout/demo/src/app/layout/LayoutBasicComponent.ts new file mode 100644 index 0000000..661c7f7 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutBasicComponent.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './layout-basic.html', + styleUrls: ['./layout-basic.less'] +}) +export class LayoutBasicComponent { + // 左侧菜单配置 + marginLeft: string = '192px'; + config: any = { + serviceName: '菜单栏', + reloadState: true, // 初始设置为true + toggleable: true, + collapsed: false, // 默认展开,当设置为true时会收起 + collapsedChangeFn: (collapsed: boolean): void => { + this.marginLeft = collapsed ? '0' : '192px'; + } + }; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + active: TiLeftmenuItem; +} diff --git a/src/layout/demo/src/app/layout/LayoutBasicSimpleComponent.ts b/src/layout/demo/src/app/layout/LayoutBasicSimpleComponent.ts new file mode 100644 index 0000000..f9794a1 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutBasicSimpleComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './layout-basic-simple.html', + styleUrls: ['./layout-simple.less'] +}) +export class LayoutBasicSimpleComponent {} diff --git a/src/layout/demo/src/app/layout/LayoutBasicSimpleResponsiveComponent.ts b/src/layout/demo/src/app/layout/LayoutBasicSimpleResponsiveComponent.ts new file mode 100644 index 0000000..d4fede8 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutBasicSimpleResponsiveComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './layout-basic-simple-responsive.html', + styleUrls: ['./layout-simple.less'] +}) +export class LayoutBasicSimpleResponsiveComponent {} diff --git a/src/layout/demo/src/app/layout/LayoutDetailColumnComponent.ts b/src/layout/demo/src/app/layout/LayoutDetailColumnComponent.ts new file mode 100644 index 0000000..db86749 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutDetailColumnComponent.ts @@ -0,0 +1,61 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './layout-detail-column.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutDetailColumnComponent { + public items1: Array = [ + { + id: '1', + label: 'ECS-name-1' + } + ]; + public textForm1: any = { + // 多列 + colsNumber: 2, + colsGap: ['150px'], + + fieldVerticalAlign: 'middle', + + firstItem: { + label: 'ID:', + value: '9fd3121221-fa4112' + }, + secondItem: { + label: '名称:', + value: '某某产品' + }, + thirdItem: { + label: '描述:', + value: 'some support services' + }, + fourthItem: { + label: '启动盘:', + value: '是' + }, + fifthItem: { + label: '创建时间:', + value: '2015-06-20 21:13' + }, + sixthItem: { + label: '挂载点:', + value: '/dev/happy' + } + }; + public tabs = [ + { + title: '基本信息', + content: + "Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse." + }, + { title: '云硬盘', content: 'Dynamic content 2' }, + { title: '网卡', content: 'Dynamic content 2' }, + { title: '安全组', content: 'Dynamic content 2' }, + { title: '弹性公网IP', content: 'Dynamic content 2' }, + { title: '监控', content: 'Dynamic content 2' }, + { title: '标签', content: 'Dynamic content 2' } + ]; +} diff --git a/src/layout/demo/src/app/layout/LayoutDetailComponent.ts b/src/layout/demo/src/app/layout/LayoutDetailComponent.ts new file mode 100644 index 0000000..9138701 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutDetailComponent.ts @@ -0,0 +1,69 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './layout-detail.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutDetailComponent { + public items: Array = [ + { + id: '1', + label: 'ECS-name-1' + }, + { + id: '2', + label: 'ECS-name-2' + }, + { + id: '3', + label: 'ECS-name-3' + } + ]; + public textForm1: any = { + // 多列 + colsNumber: 2, + colsGap: ['50%', '50%'], + + fieldVerticalAlign: 'middle', + + firstItem: { + label: 'ID:', + value: '9fd3121221-fa4112' + }, + secondItem: { + label: '名称:', + value: '某某产品' + }, + thirdItem: { + label: '描述:', + value: 'some support services' + }, + fourthItem: { + label: '启动盘:', + value: '是' + }, + fifthItem: { + label: '创建时间:', + value: '2015-06-20 21:13' + }, + sixthItem: { + label: '挂载点:', + value: '/dev/happy' + } + }; + public tabs = [ + { + title: '基本信息', + content: + "Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse." + }, + { title: '云硬盘', content: 'Dynamic content 2' }, + { title: '网卡', content: 'Dynamic content 2' }, + { title: '安全组', content: 'Dynamic content 2' }, + { title: '弹性公网IP', content: 'Dynamic content 2' }, + { title: '监控', content: 'Dynamic content 2' }, + { title: '标签', content: 'Dynamic content 2' } + ]; +} diff --git a/src/layout/demo/src/app/layout/LayoutListComponent.ts b/src/layout/demo/src/app/layout/LayoutListComponent.ts new file mode 100644 index 0000000..bda86d7 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutListComponent.ts @@ -0,0 +1,157 @@ +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { TiActionmenuItem, TiLeftmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './layout-list.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutListComponent implements OnInit { + public marginLeft: string = '192px'; + public config: any = { + serviceName: '云服务器控制台', + reloadState: true, // 初始设置为true + toggleable: true, + collapsed: false, // 默认展开,当设置为true时会收起 + collapsedChangeFn: (collapsed: boolean): void => { + this.marginLeft = collapsed ? '0' : '192px'; + } + }; + public items: Array = [ + { + label: '总览', + children: [ + { + label: '二级菜单1' + }, + { + label: '二级菜单2' + } + ] + }, + { + label: '弹性云服务器', + children: [ + { + label: '二级菜单' + }, + { + label: '二级' + } + ] + }, + { + label: '裸金属服务器' + } + ]; + public active: TiLeftmenuItem; + + public displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + public srcData: TiTableSrcData; + private data: Array = []; + public columns: Array = [ + { + label: 'first name', + width: '25%' + }, + { + label: 'last name', + width: '20%' + }, + { + label: 'birth date', + width: '20%' + }, + { + label: 'state', + width: '15%' + }, + { + label: 'email', + width: '20%' + } + ]; + public currentPage: number = 1; + public totalNumber: number = 62; + public pageSize: { options: Array; size: number } = { + options: [10, 20, 50, 100], + size: 10 + }; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、过滤、分页等操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为$scope.displayed显示出来 + // 本示例中,开发者没有设置分页、过滤和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + public onSelect(item: any): void { + console.log('onSelect()'); + console.log(item); + } + + public dataToItemsFn: (data: any) => Array = (data: any) => { + let items: Array = [ + { + label: '启用', + association: 'switch', + disabled: true + }, + { + label: '禁用', + association: 'switch' + }, + { + label: '制作镜像' + }, + { + label: '这是一个很长的选项' + }, + { + label: '制作镜像2', + disabled: true + } + ]; + if (data.state === '已停止') { + items[0].disabled = false; + items[1].disabled = true; + items = [items[0], items[1]]; + } + + return items; + }; + + private createRandomItem(id: number): any { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[Math.floor(Math.random() * 4)]; + const lastName: string = familyName[Math.floor(Math.random() * 4)]; + const age: number = Math.floor(Math.random() * 100); + const state: string = ['已启动', '已停止'][Math.floor(Math.random() * 2)]; + + return { + firstName, + lastName, + age, + state, + id + }; + } + + public trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/layout/demo/src/app/layout/LayoutListLargedataComponent.ts b/src/layout/demo/src/app/layout/LayoutListLargedataComponent.ts new file mode 100644 index 0000000..ba575bf --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutListLargedataComponent.ts @@ -0,0 +1,153 @@ +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { TiActionmenuItem, TiLeftmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './layout-list-largedata.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutListLargedataComponent implements OnInit { + public marginLeft: string = '192px'; + public config: any = { + serviceName: '云服务器控制台', + reloadState: true, // 初始设置为true + toggleable: true, + collapsed: false, // 默认展开,当设置为true时会收起 + collapsedChangeFn: (collapsed: boolean): void => { + this.marginLeft = collapsed ? '0' : '192px'; + } + }; + public items: Array = [ + { + label: '总览', + children: [ + { + label: '二级菜单1' + }, + { + label: '二级菜单2' + } + ] + }, + { + label: '弹性云服务器', + children: [ + { + label: '二级菜单' + }, + { + label: '二级' + } + ] + }, + { + label: '裸金属服务器' + } + ]; + public active: TiLeftmenuItem; + + public displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + public srcData: TiTableSrcData; + private data: Array = []; + public columns: Array = [ + { + label: 'first name', + width: '25%' + }, + { + label: 'last name', + width: '20%' + }, + { + label: 'birth date', + width: '20%' + }, + { + label: 'state', + width: '15%' + }, + { + label: 'email', + width: '20%' + } + ]; + public currentPage: number = 1; + public totalNumber: number = 800; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、过滤、分页等操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为$scope.displayed显示出来 + // 本示例中,开发者没有设置分页、过滤和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + public onSelect(item: any): void { + console.log('onSelect()'); + console.log(item); + } + + public dataToItemsFn: (data: any) => Array = (data: any) => { + let items: Array = [ + { + label: '启用', + association: 'switch', + disabled: true + }, + { + label: '禁用', + association: 'switch' + }, + { + label: '制作镜像' + }, + { + label: '这是一个很长的选项' + }, + { + label: '制作镜像2', + disabled: true + } + ]; + if (data.state === '已停止') { + items[0].disabled = false; + items[1].disabled = true; + items = [items[0], items[1]]; + } + + return items; + }; + + private createRandomItem(id: number): any { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[Math.floor(Math.random() * 4)]; + const lastName: string = familyName[Math.floor(Math.random() * 4)]; + const age: number = Math.floor(Math.random() * 100); + const state: string = ['已启动', '已停止'][Math.floor(Math.random() * 2)]; + + return { + firstName, + lastName, + age, + state, + id + }; + } + + public trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/layout/demo/src/app/layout/LayoutMultiColumnComponent.ts b/src/layout/demo/src/app/layout/LayoutMultiColumnComponent.ts new file mode 100644 index 0000000..6064cf2 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutMultiColumnComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './layout-multi-column.html', + styleUrls: ['./layout-column.less'] +}) +export class LayoutMultiColumnComponent {} diff --git a/src/layout/demo/src/app/layout/LayoutOverviewComponent.ts b/src/layout/demo/src/app/layout/LayoutOverviewComponent.ts new file mode 100644 index 0000000..e4b1e96 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutOverviewComponent.ts @@ -0,0 +1,49 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './layout-overview.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutOverviewComponent { + // 左侧菜单配置 + public marginLeft: string = '192px'; + public config: any = { + serviceName: '云服务器控制台', + reloadState: true, // 初始设置为true + toggleable: true, + collapsed: false, // 默认展开,当设置为true时会收起 + collapsedChangeFn: (collapsed: boolean): void => { + this.marginLeft = collapsed ? '0' : '192px'; + } + }; + public items: Array = [ + { + label: '总览', + children: [ + { + label: '二级菜单1' + }, + { + label: '二级菜单2' + } + ] + }, + { + label: '弹性云服务器', + children: [ + { + label: '二级菜单' + }, + { + label: '二级' + } + ] + }, + { + label: '裸金属服务器' + } + ]; + public active: TiLeftmenuItem; +} diff --git a/src/layout/demo/src/app/layout/LayoutOverviewVerticalComponent.ts b/src/layout/demo/src/app/layout/LayoutOverviewVerticalComponent.ts new file mode 100644 index 0000000..534815c --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutOverviewVerticalComponent.ts @@ -0,0 +1,49 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './layout-overview-vertical.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutOverviewVerticalComponent { + // 左侧菜单配置 + public marginLeft: string = '192px'; + public config: any = { + serviceName: '云服务器控制台', + reloadState: true, // 初始设置为true + toggleable: true, + collapsed: false, // 默认展开,当设置为true时会收起 + collapsedChangeFn: (collapsed: boolean): void => { + this.marginLeft = collapsed ? '0' : '192px'; + } + }; + public items: Array = [ + { + label: '总览', + children: [ + { + label: '二级菜单1' + }, + { + label: '二级菜单2' + } + ] + }, + { + label: '弹性云服务器', + children: [ + { + label: '二级菜单' + }, + { + label: '二级' + } + ] + }, + { + label: '裸金属服务器' + } + ]; + public active: TiLeftmenuItem; +} diff --git a/src/layout/demo/src/app/layout/LayoutPurchaseComponent.ts b/src/layout/demo/src/app/layout/LayoutPurchaseComponent.ts new file mode 100644 index 0000000..3a807ba --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutPurchaseComponent.ts @@ -0,0 +1,52 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './layout-purchase.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutPurchaseComponent { + // 返回标题 + public items: Array = [ + { + label: 'item1 test 测试长标题 测试长标题 测试长标题' + } + ]; + + // 步骤 + public steps: Array = [ + { + label: 'General' + }, + { + label: 'Host Group' + }, + { + label: 'Policy' + }, + { + label: 'Names' + } + ]; + public activeStep: any = this.steps[0]; + + public format: string = 'N0'; + public spinnerValue: number = 1; + + public myOptions: Array = [ + { label: '一个月' }, + { label: '二个月' }, + { label: '三个月' }, + { label: '四个月' }, + { label: '五个月' }, + { label: '六个月' }, + { label: '七个月' }, + { label: '八个月' }, + { label: '九个月' }, + { label: '一年' }, + { label: '两年' }, + { label: '三年' } + ]; + + public mySelected1: any = this.myOptions[3]; +} diff --git a/src/layout/demo/src/app/layout/LayoutPurchaseResponsiveChangeComponent.ts b/src/layout/demo/src/app/layout/LayoutPurchaseResponsiveChangeComponent.ts new file mode 100644 index 0000000..bf3f864 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutPurchaseResponsiveChangeComponent.ts @@ -0,0 +1,56 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './layout-purchase-responsive-change.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutPurchaseResponsiveChangeComponent { + public responsive: boolean = false; + // 返回标题 + public items: Array = [ + { + label: 'item1 test 测试长标题 测试长标题 测试长标题' + } + ]; + + // 步骤 + public steps: Array = [ + { + label: 'General' + }, + { + label: 'Host Group' + }, + { + label: 'Policy' + }, + { + label: 'Names' + } + ]; + public activeStep: any = this.steps[0]; + public format: string = 'N0'; + public spinnerValue: number = 1; + + public myOptions: Array = [ + { label: '一个月' }, + { label: '二个月' }, + { label: '三个月' }, + { label: '四个月' }, + { label: '五个月' }, + { label: '六个月' }, + { label: '七个月' }, + { label: '八个月' }, + { label: '九个月' }, + { label: '一年' }, + { label: '两年' }, + { label: '三年' } + ]; + + public mySelected1: any = this.myOptions[3]; + + public changeResponsive(): void { + this.responsive = !this.responsive; + } +} diff --git a/src/layout/demo/src/app/layout/LayoutPurchaseResponsiveComponent.ts b/src/layout/demo/src/app/layout/LayoutPurchaseResponsiveComponent.ts new file mode 100644 index 0000000..b66c689 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutPurchaseResponsiveComponent.ts @@ -0,0 +1,51 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './layout-purchase-responsive.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutPurchaseResponsiveComponent { + // 返回标题 + public items: Array = [ + { + label: 'item1 test 测试长标题 测试长标题 测试长标题' + } + ]; + + // 步骤 + public steps: Array = [ + { + label: 'General' + }, + { + label: 'Host Group' + }, + { + label: 'Policy' + }, + { + label: 'Names' + } + ]; + public activeStep: any = this.steps[0]; + public format: string = 'N0'; + public spinnerValue: number = 1; + + public myOptions: Array = [ + { label: '一个月' }, + { label: '二个月' }, + { label: '三个月' }, + { label: '四个月' }, + { label: '五个月' }, + { label: '六个月' }, + { label: '七个月' }, + { label: '八个月' }, + { label: '九个月' }, + { label: '一年' }, + { label: '两年' }, + { label: '三年' } + ]; + + public mySelected1: any = this.myOptions[3]; +} diff --git a/src/layout/demo/src/app/layout/LayoutPurchaseSimpleComponent.ts b/src/layout/demo/src/app/layout/LayoutPurchaseSimpleComponent.ts new file mode 100644 index 0000000..7ccb713 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutPurchaseSimpleComponent.ts @@ -0,0 +1,14 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './layout-purchase-simple.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutPurchaseSimpleComponent { + public items: Array = [ + { + label: 'item1 test 测试长标题 测试长标题 测试长标题' + } + ]; +} diff --git a/src/layout/demo/src/app/layout/LayoutPurchaseSimpleResponsiveComponent.ts b/src/layout/demo/src/app/layout/LayoutPurchaseSimpleResponsiveComponent.ts new file mode 100644 index 0000000..061e534 --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutPurchaseSimpleResponsiveComponent.ts @@ -0,0 +1,14 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './layout-purchase-simple-responsive.html', + styleUrls: ['./layout.less'], + encapsulation: ViewEncapsulation.None +}) +export class LayoutPurchaseSimpleResponsiveComponent { + public items: Array = [ + { + label: 'item1 test 测试长标题 测试长标题 测试长标题' + } + ]; +} diff --git a/src/layout/demo/src/app/layout/LayoutSingleComponent.ts b/src/layout/demo/src/app/layout/LayoutSingleComponent.ts new file mode 100644 index 0000000..da71aab --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutSingleComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './layout-single.html', + styleUrls: ['./layout-simple.less'] +}) +export class LayoutSingleComponent {} diff --git a/src/layout/demo/src/app/layout/LayoutTestModule.ts b/src/layout/demo/src/app/layout/LayoutTestModule.ts new file mode 100644 index 0000000..0583def --- /dev/null +++ b/src/layout/demo/src/app/layout/LayoutTestModule.ts @@ -0,0 +1,160 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { + TiActionmenuModule, + TiAlertModule, + TiButtonModule, + TiFormfieldModule, + TiLayoutModule, + TiLeftmenuModule, + TiPaginationModule, + TiSelectModule, + TiSpinnerModule, + TiStepsModule, + TiSubtitleModule, + TiTableModule, + TiTabModule +} from '@opentiny/ng'; + +import { LayoutOverviewComponent } from './LayoutOverviewComponent'; +import { LayoutBasicComponent } from './LayoutBasicComponent'; +import { LayoutBasicSimpleComponent } from './LayoutBasicSimpleComponent'; +import { LayoutBasicSimpleResponsiveComponent } from './LayoutBasicSimpleResponsiveComponent'; +import { LayoutSingleComponent } from './LayoutSingleComponent'; +import { LayoutMultiColumnComponent } from './LayoutMultiColumnComponent'; +import { LayoutListComponent } from './LayoutListComponent'; +import { LayoutPurchaseComponent } from './LayoutPurchaseComponent'; +import { LayoutPurchaseSimpleComponent } from './LayoutPurchaseSimpleComponent'; +import { LayoutPurchaseResponsiveComponent } from './LayoutPurchaseResponsiveComponent'; +import { LayoutPurchaseSimpleResponsiveComponent } from './LayoutPurchaseSimpleResponsiveComponent'; +import { LayoutDetailComponent } from './LayoutDetailComponent'; +import { LayoutDetailColumnComponent } from './LayoutDetailColumnComponent'; +import { LayoutOverviewVerticalComponent } from './LayoutOverviewVerticalComponent'; +import { LayoutListLargedataComponent } from './LayoutListLargedataComponent'; +import { LayoutPurchaseResponsiveChangeComponent } from './LayoutPurchaseResponsiveChangeComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiActionmenuModule, + TiAlertModule, + TiButtonModule, + TiLeftmenuModule, + TiPaginationModule, + TiStepsModule, + TiTableModule, + TiSelectModule, + TiSpinnerModule, + TiLayoutModule, + TiSubtitleModule, + TiFormfieldModule, + TiTabModule, + RouterModule.forChild(LayoutTestModule.ROUTES) + ], + declarations: [ + LayoutOverviewComponent, + LayoutBasicComponent, + LayoutBasicSimpleComponent, + LayoutBasicSimpleResponsiveComponent, + LayoutSingleComponent, + LayoutMultiColumnComponent, + LayoutListComponent, + LayoutPurchaseComponent, + LayoutPurchaseSimpleComponent, + LayoutPurchaseResponsiveComponent, + LayoutPurchaseSimpleResponsiveComponent, + LayoutDetailComponent, + LayoutDetailColumnComponent, + LayoutOverviewVerticalComponent, + LayoutListLargedataComponent, + LayoutPurchaseResponsiveChangeComponent + ] +}) +export class LayoutTestModule { + public static readonly ROUTES: Routes = [ + { + path: 'layout/layout-overview', + component: LayoutOverviewComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-basic', + component: LayoutBasicComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-basic-simple', + component: LayoutBasicSimpleComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-basic-simple-responsive', + component: LayoutBasicSimpleResponsiveComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-overview-vertical', + component: LayoutOverviewVerticalComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-list', + component: LayoutListComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-list-largedata', + component: LayoutListLargedataComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-purchase', + component: LayoutPurchaseComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-purchase-simple', + component: LayoutPurchaseSimpleComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-purchase-responsive', + component: LayoutPurchaseResponsiveComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-purchase-simple-responsive', + component: LayoutPurchaseSimpleResponsiveComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-detail', + component: LayoutDetailComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-detail-column', + component: LayoutDetailColumnComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-single', + component: LayoutSingleComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-multi-column', + component: LayoutMultiColumnComponent, + data: { less: 'layout' } + }, + { + path: 'layout/layout-purchase-responsive-change', + component: LayoutPurchaseResponsiveChangeComponent, + data: { less: 'layout' } + } + ]; +} diff --git a/src/layout/demo/src/app/layout/layout-basic-simple-responsive.html b/src/layout/demo/src/app/layout/layout-basic-simple-responsive.html new file mode 100644 index 0000000..1a7810c --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-basic-simple-responsive.html @@ -0,0 +1,14 @@ + +
    Header
    +
    + + + + +
    Content
    +
    + +
    Content
    +
    +
    +
    diff --git a/src/layout/demo/src/app/layout/layout-basic-simple.html b/src/layout/demo/src/app/layout/layout-basic-simple.html new file mode 100644 index 0000000..c6ae06f --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-basic-simple.html @@ -0,0 +1,14 @@ + +
    Header
    +
    + + + + +
    Content
    +
    + +
    Content
    +
    +
    +
    diff --git a/src/layout/demo/src/app/layout/layout-basic.html b/src/layout/demo/src/app/layout/layout-basic.html new file mode 100644 index 0000000..a64eb68 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-basic.html @@ -0,0 +1,31 @@ + +
    + + +
    {{config.serviceName}}
    +
    + + +
    {{m1.label}}
    +
    + + {{m2.label}} + +
    +
    + + + Header + + Content + Aside + + +
    diff --git a/src/layout/demo/src/app/layout/layout-basic.less b/src/layout/demo/src/app/layout/layout-basic.less new file mode 100644 index 0000000..b203a19 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-basic.less @@ -0,0 +1,30 @@ +.layout-content { + background: #eee !important; + padding: 20px 20px 0 20px !important; +} + +.layout-header { + background: #fff; + margin-bottom: 12px; + text-align: center; +} + +.basic-section { + width: calc(100% - 183px - 12px); + text-align: center; + line-height: 300px; +} + +.basic-aside { + width: 183px; + margin-left: 12px; + text-align: center; + line-height: 300px; +} + +// 示例呈现效果,开发者不用关注 +.clear-transform { + transform: scale(1, 1); + height: 460px; + overflow: auto; +} diff --git a/src/layout/demo/src/app/layout/layout-column.less b/src/layout/demo/src/app/layout/layout-column.less new file mode 100644 index 0000000..d2d83ed --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-column.less @@ -0,0 +1,18 @@ +.text-color { + color: #fff; +} + +.column-left { + width: calc(45% - 12px); +} + +.column-right { + width: 55%; +} + +.content-section { + background-color: #eee; + height: 200px; + text-align: center; + line-height: 200px; +} diff --git a/src/layout/demo/src/app/layout/layout-detail-column.html b/src/layout/demo/src/app/layout/layout-detail-column.html new file mode 100644 index 0000000..6adbbbc --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-detail-column.html @@ -0,0 +1,67 @@ + + + + + +
    + + + +
    + + + + + + + + + + + + + {{textForm1.firstItem.value}} + + + +
    {{textForm1.secondItem.label}}
    +
    + {{textForm1.secondItem.value}} +
    + + {{textForm1.thirdItem.label}} + {{textForm1.thirdItem.value}} + + + {{textForm1.fourthItem.value}} + + + {{textForm1.fifthItem.label}} + {{textForm1.fifthItem.value}} + + + {{textForm1.sixthItem.label}} + {{textForm1.sixthItem.value}} + +
    +
    + +
    Detail Content
    +
    + +
    Detail Content
    +
    +
    + + + +
    + linklist +
    +
    + +
    Detail Content
    +
    +
    +
    +
    diff --git a/src/layout/demo/src/app/layout/layout-detail.html b/src/layout/demo/src/app/layout/layout-detail.html new file mode 100644 index 0000000..1609fa0 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-detail.html @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + {{textForm1.firstItem.value}} + + + +
    {{textForm1.secondItem.label}}
    +
    + {{textForm1.secondItem.value}} +
    + + {{textForm1.thirdItem.label}} + {{textForm1.thirdItem.value}} + + + {{textForm1.fourthItem.value}} + + + {{textForm1.fifthItem.label}} + {{textForm1.fifthItem.value}} + + + {{textForm1.sixthItem.label}} + {{textForm1.sixthItem.value}} + +
    +
    + +
    + + + +
    +
    Detail Content
    +
    + +
    Detail Content
    +
    +
    +
    diff --git a/src/layout/demo/src/app/layout/layout-list-largedata.html b/src/layout/demo/src/app/layout/layout-list-largedata.html new file mode 100644 index 0000000..97f8a22 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-list-largedata.html @@ -0,0 +1,57 @@ + + + + +
    {{config.serviceName}}
    +
    + + +
    {{m1.label}}
    +
    + + {{m2.label}} + +
    +
    + + + + + + descandbuy + + 我是业务侧自定义内容 + + + + + + + + + + + + + + + + + + + +
    {{column.label}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.state}} + +
    + +
    +
    +
    +
    diff --git a/src/layout/demo/src/app/layout/layout-list.html b/src/layout/demo/src/app/layout/layout-list.html new file mode 100644 index 0000000..8b6e497 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-list.html @@ -0,0 +1,56 @@ + + + + +
    {{config.serviceName}}
    +
    + + +
    {{m1.label}}
    +
    + + {{m2.label}} + +
    +
    + + + + + descandbuy + + 我是业务侧自定义内容 + + + + + + + + + + + + + + + + + + + +
    {{column.label}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.state}} + +
    + +
    +
    +
    +
    diff --git a/src/layout/demo/src/app/layout/layout-multi-column.html b/src/layout/demo/src/app/layout/layout-multi-column.html new file mode 100644 index 0000000..6815f30 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-multi-column.html @@ -0,0 +1,21 @@ + + +
    Header
    +
    + + + + +
    Content
    +
    + +
    Content
    +
    +
    + + +
    Content
    +
    +
    +
    +
    diff --git a/src/layout/demo/src/app/layout/layout-overview-vertical.html b/src/layout/demo/src/app/layout/layout-overview-vertical.html new file mode 100644 index 0000000..b7e6628 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-overview-vertical.html @@ -0,0 +1,36 @@ + + + + + +
    {{config.serviceName}}
    +
    + + +
    {{m1.label}}
    +
    + + {{m2.label}} + +
    +
    + + + + + descandbuy + + + 资源列表 resources + + linklist + + + diff --git a/src/layout/demo/src/app/layout/layout-overview.html b/src/layout/demo/src/app/layout/layout-overview.html new file mode 100644 index 0000000..f6aeeff --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-overview.html @@ -0,0 +1,38 @@ + + + + +
    {{config.serviceName}}
    +
    + + +
    {{m1.label}}
    +
    + + {{m2.label}} + +
    +
    + + + + + descandbuy + + + + + 资源列表 resources + + + linklist + + + diff --git a/src/layout/demo/src/app/layout/layout-purchase-responsive-change.html b/src/layout/demo/src/app/layout/layout-purchase-responsive-change.html new file mode 100644 index 0000000..4fbf479 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-purchase-responsive-change.html @@ -0,0 +1,41 @@ + + + + + + + + +
    + + + + + + + + + + + +
    + +
    + buy content1 + +
    +
    + +
    buy content1
    +
    + +
    buy content1
    +
    + +
    buy content1
    +
    +
    +
    +
    +
    购买浮层 buylayer
    +
    diff --git a/src/layout/demo/src/app/layout/layout-purchase-responsive.html b/src/layout/demo/src/app/layout/layout-purchase-responsive.html new file mode 100644 index 0000000..5ebb4d1 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-purchase-responsive.html @@ -0,0 +1,40 @@ + + + + + + + + +
    + + + + + + + + + + + + + +
    + +
    buy content1
    +
    + +
    buy content1
    +
    + +
    buy content1
    +
    + +
    buy content1
    +
    +
    +
    +
    +
    购买浮层 buylayer
    +
    diff --git a/src/layout/demo/src/app/layout/layout-purchase-simple-responsive.html b/src/layout/demo/src/app/layout/layout-purchase-simple-responsive.html new file mode 100644 index 0000000..c153904 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-purchase-simple-responsive.html @@ -0,0 +1,26 @@ + + + + + + + + +
    + + + + + + + + + +
    buy content1
    +
    + +
    buy content1
    +
    +
    +
    +
    diff --git a/src/layout/demo/src/app/layout/layout-purchase-simple.html b/src/layout/demo/src/app/layout/layout-purchase-simple.html new file mode 100644 index 0000000..2f6d58b --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-purchase-simple.html @@ -0,0 +1,21 @@ + + + +
    + + + + + + + + + +
    buy content1
    +
    + +
    buy content1
    +
    +
    +
    +
    diff --git a/src/layout/demo/src/app/layout/layout-purchase.html b/src/layout/demo/src/app/layout/layout-purchase.html new file mode 100644 index 0000000..1342c56 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-purchase.html @@ -0,0 +1,34 @@ + + + +
    + + + + + + + + + + + + +
    + +
    buy content1
    +
    + +
    buy content1
    +
    + +
    buy content1
    +
    + +
    buy content1
    +
    +
    +
    +
    +
    购买浮层 buylayer
    +
    diff --git a/src/layout/demo/src/app/layout/layout-simple.less b/src/layout/demo/src/app/layout/layout-simple.less new file mode 100644 index 0000000..b0654a2 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-simple.less @@ -0,0 +1,14 @@ +.text-color { + color: #fff; +} + +.padding-bottom { + padding-bottom: 12px; +} + +.content-section { + background-color: #eee; + height: 200px; + text-align: center; + line-height: 200px; +} diff --git a/src/layout/demo/src/app/layout/layout-single.html b/src/layout/demo/src/app/layout/layout-single.html new file mode 100644 index 0000000..01ea615 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout-single.html @@ -0,0 +1,14 @@ + + +
    Header
    +
    + + + +
    Content
    +
    + +
    Content
    +
    +
    +
    diff --git a/src/layout/demo/src/app/layout/layout.less b/src/layout/demo/src/app/layout/layout.less new file mode 100644 index 0000000..85b0012 --- /dev/null +++ b/src/layout/demo/src/app/layout/layout.less @@ -0,0 +1,21 @@ +html, +body { + height: 100%; + background-color: #eef0f5; +} + +#section { + width: calc(100% - 183px - 12px); +} + +#aside { + width: 183px; + margin-left: 12px; +} +#column_left { + width: calc(45% - 12px); +} + +#column_right { + width: 55%; +} diff --git a/src/layout/demo/src/app/layout/webdoc/layout-demos.js b/src/layout/demo/src/app/layout/webdoc/layout-demos.js new file mode 100644 index 0000000..ae6e104 --- /dev/null +++ b/src/layout/demo/src/app/layout/webdoc/layout-demos.js @@ -0,0 +1,90 @@ +export default { + column: '1', + demos: [ + { + demoId: 'layout-basic', + name: { + 'zh-CN': '基本布局', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': '', + 'en-US': '

    basic

    ' + }, + codeFiles: [ + 'layout-basic.html', + 'LayoutBasicComponent.ts', + 'layout-basic.less' + ] + }, + { + demoId: 'layout-single', + name: { + 'zh-CN': '单列布局', + 'en-US': 'single' + }, + desc: { + 'zh-CN': '', + 'en-US': '

    single

    ' + }, + codeFiles: [ + 'layout-single.html', + 'LayoutSingleComponent.ts', + 'layout-simple.less' + ] + }, + { + demoId: 'layout-multi-column', + name: { + 'zh-CN': '多列布局', + 'en-US': 'multiColumn' + }, + desc: { + 'zh-CN': '', + 'en-US': '

    multiColumn

    ' + }, + codeFiles: [ + 'layout-multi-column.html', + 'LayoutMultiColumnComponent.ts', + 'layout-column.less' + ] + }, + { + demoId: 'layout-basic-simple', + name: { + 'zh-CN': '固定居中布局', + 'en-US': 'basic-simple' + }, + desc: { + 'zh-CN': '

    固定居中布局内容区宽度一直固定为1192px

    ', + 'en-US': '

    basic-simple

    ' + }, + codeFiles: [ + 'layout-basic-simple.html', + 'LayoutBasicSimpleComponent.ts', + 'layout-simple.less' + ] + }, + { + demoId: 'layout-basic-simple-responsive', + name: { + 'zh-CN': '响应式布局', + 'en-US': 'basic-simple-responsive' + }, + desc: { + 'zh-CN': + '

    响应式布局内容区宽度自适应策略:2560 ≥ 分辨率 > 1920 ,内容区宽度固定为1832px1920 ≥ 分辨率 ≥ 1280,左右间距固定为 20px,内容区宽度自适应;分辨率 < 1280时,显示不下则出滚动条。

    ', + 'en-US': '

    basic-simple-responsive

    ' + }, + apis: [ + 'TiLayoutContentComponent.properties.responsive', + 'TiLayoutHeaderComponent.properties.responsive' + ], + codeFiles: [ + 'layout-basic-simple-responsive.html', + 'LayoutBasicSimpleResponsiveComponent.ts', + 'layout-simple.less' + ] + }, + ], +} diff --git a/src/layout/demo/src/app/layout/webdoc/layout.cn.md b/src/layout/demo/src/app/layout/webdoc/layout.cn.md new file mode 100644 index 0000000..de5536e --- /dev/null +++ b/src/layout/demo/src/app/layout/webdoc/layout.cn.md @@ -0,0 +1,23 @@ +--- +title: Layout 布局 +--- +# Layout 布局 + +
    + +布局组件,提供整个页面内容区域的布局。   + +```typescript +import { TiLayoutModule } from '@opentiny/ng'; +``` + +
    + +
    + +布局组件,提供整个页面内容区域的布局。   + +```typescript +import { TiLayoutModule } from '@opentiny/ng'; +``` +
    diff --git a/src/layout/demo/src/app/layout/webdoc/layout.en.md b/src/layout/demo/src/app/layout/webdoc/layout.en.md new file mode 100644 index 0000000..b1366b4 --- /dev/null +++ b/src/layout/demo/src/app/layout/webdoc/layout.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiLayoutModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiLayoutModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/layout/demo/src/favicon.ico b/src/layout/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/layout/demo/src/index.html b/src/layout/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/layout/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/layout/demo/src/main.ts b/src/layout/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/layout/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/layout/demo/test.ts b/src/layout/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/layout/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/layout/demo/tsconfig.app.json b/src/layout/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/layout/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/layout/demo/tsconfig.spec.json b/src/layout/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/layout/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/layout/lib/index.ts b/src/layout/lib/index.ts new file mode 100644 index 0000000..070b473 --- /dev/null +++ b/src/layout/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiLayoutModule'; diff --git a/src/layout/lib/ng-package.json b/src/layout/lib/ng-package.json new file mode 100644 index 0000000..7dde01e --- /dev/null +++ b/src/layout/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/layout", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/layout/lib/package.json b/src/layout/lib/package.json new file mode 100644 index 0000000..b0fa160 --- /dev/null +++ b/src/layout/lib/package.json @@ -0,0 +1,9 @@ +{ + "name": "@opentiny/ng-layout", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/layout/lib/project.json b/src/layout/lib/project.json new file mode 100644 index 0000000..d45109d --- /dev/null +++ b/src/layout/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/layout/lib", + "sourceRoot": "src/layout/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/layout"], + "options": { + "project": "src/layout/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/layout"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js layout" + }, + { + "command": "ng default-build layout" + }, + { + "command": "node build/clear-default-theme.js layout" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/layout && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build layout && ng pack layout && node build/publish.js layout --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/layout/lib/src/TiLayoutColumnComponent.ts b/src/layout/lib/src/TiLayoutColumnComponent.ts new file mode 100644 index 0000000..e639977 --- /dev/null +++ b/src/layout/lib/src/TiLayoutColumnComponent.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * 布局组件,提供整个页面内容区域的布局 + * + * 内容容器 + * + */ +@Component({ + selector: 'ti-layout-column', + template: '', + styleUrls: ['./layout-column.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLayoutColumnComponent {} diff --git a/src/layout/lib/src/TiLayoutContentBodyComponent.ts b/src/layout/lib/src/TiLayoutContentBodyComponent.ts new file mode 100644 index 0000000..e356f8d --- /dev/null +++ b/src/layout/lib/src/TiLayoutContentBodyComponent.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * 布局组件,提供整个页面内容区域的布局 + * + * 内容主体 + * + */ +@Component({ + selector: 'ti-layout-content-body', + template: '', + styleUrls: ['./layout-content-body.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLayoutContentBodyComponent {} diff --git a/src/layout/lib/src/TiLayoutContentComponent.ts b/src/layout/lib/src/TiLayoutContentComponent.ts new file mode 100644 index 0000000..5f83974 --- /dev/null +++ b/src/layout/lib/src/TiLayoutContentComponent.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +/** + * 布局组件,提供整个页面内容区域的布局 + * + * 内容容器 + * + */ +@Component({ + selector: 'ti-layout-content', + template: '', + styleUrls: ['./layout-content.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-layout-content]': 'true', + // 这里用 false 判断是为了兼容 10.1.10 及之前的版本,之前使用方式是业务在ti-layout-content标签上直接添加responsive属性(非input接口)来使用响应式布局。 + // 所以responsive接口的值为true或undefined(以标签属性的方式使用)时都应该是响应式布局,只有为false时才不是响应式布局。 + '[class.ti3-layout-purchase-responsive]': 'responsive !== false' + } +}) +export class TiLayoutContentComponent { + /** + * 是否为响应式布局 + */ + @Input() responsive: boolean = false; +} diff --git a/src/layout/lib/src/TiLayoutContentHeaderComponent.ts b/src/layout/lib/src/TiLayoutContentHeaderComponent.ts new file mode 100644 index 0000000..01b8fb5 --- /dev/null +++ b/src/layout/lib/src/TiLayoutContentHeaderComponent.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * 布局组件,提供整个页面内容区域的布局 + * + * 内容头部 + * + */ +@Component({ + selector: 'ti-layout-content-header', + template: '', + styleUrls: ['./layout-content-header.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLayoutContentHeaderComponent {} diff --git a/src/layout/lib/src/TiLayoutHeaderComponent.ts b/src/layout/lib/src/TiLayoutHeaderComponent.ts new file mode 100644 index 0000000..2612157 --- /dev/null +++ b/src/layout/lib/src/TiLayoutHeaderComponent.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +/** + * 布局组件,提供整个页面内容区域的布局 + * + * 头部 + * + */ +@Component({ + selector: 'ti-layout-header', + template: '
    ', + styleUrls: ['./layout-header.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-layout-header]': 'true', + // 这里用 false 判断是为了兼容 10.1.10 及之前的版本,之前使用方式是业务在ti-layout-header标签上直接添加responsive属性(非input接口)来使用响应式布局。 + // 所以responsive接口的值为true或undefined(以标签属性的方式使用)时都应该是响应式布局,只有为false时才不是响应式布局。 + '[class.ti3-layout-purchase-responsive]': 'responsive !== false' + } +}) +export class TiLayoutHeaderComponent { + /** + * 是否为响应式布局 + */ + @Input() responsive: boolean = false; +} diff --git a/src/layout/lib/src/TiLayoutModule.ts b/src/layout/lib/src/TiLayoutModule.ts new file mode 100644 index 0000000..5f12f9e --- /dev/null +++ b/src/layout/lib/src/TiLayoutModule.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiLayoutHeaderComponent } from './TiLayoutHeaderComponent'; +import { TiLayoutContentComponent } from './TiLayoutContentComponent'; +import { TiLayoutSectionComponent } from './TiLayoutSectionComponent'; +import { TiLayoutContentBodyComponent } from './TiLayoutContentBodyComponent'; +import { TiLayoutContentHeaderComponent } from './TiLayoutContentHeaderComponent'; +import { TiLayoutColumnComponent } from './TiLayoutColumnComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [ + TiLayoutContentComponent, + TiLayoutContentBodyComponent, + TiLayoutHeaderComponent, + TiLayoutSectionComponent, + TiLayoutContentHeaderComponent, + TiLayoutColumnComponent + ], + declarations: [ + TiLayoutContentComponent, + TiLayoutContentBodyComponent, + TiLayoutHeaderComponent, + TiLayoutSectionComponent, + TiLayoutContentHeaderComponent, + TiLayoutColumnComponent + ] +}) +export class TiLayoutModule {} +export { TiLayoutContentComponent } from './TiLayoutContentComponent'; +export { TiLayoutContentBodyComponent } from './TiLayoutContentBodyComponent'; +export { TiLayoutHeaderComponent } from './TiLayoutHeaderComponent'; +export { TiLayoutSectionComponent } from './TiLayoutSectionComponent'; +export { TiLayoutContentHeaderComponent } from './TiLayoutContentHeaderComponent'; +export { TiLayoutColumnComponent } from './TiLayoutColumnComponent'; diff --git a/src/layout/lib/src/TiLayoutSectionComponent.ts b/src/layout/lib/src/TiLayoutSectionComponent.ts new file mode 100644 index 0000000..7ace9d6 --- /dev/null +++ b/src/layout/lib/src/TiLayoutSectionComponent.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * 布局组件,提供整个页面内容区域的布局 + * + * 内部分区 + * + */ +@Component({ + selector: 'ti-layout-section', + template: '', + styleUrls: ['./layout-section.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-layout-section]': 'true' + } +}) +export class TiLayoutSectionComponent {} diff --git a/src/layout/lib/src/layout-column.less b/src/layout/lib/src/layout-column.less new file mode 100644 index 0000000..6b94c33 --- /dev/null +++ b/src/layout/lib/src/layout-column.less @@ -0,0 +1,11 @@ +.ti3-layout-content[adaptive] ti-layout-content-body :host { + width: 100%; // flex布局下column容器宽度默认等分 + + &:not(:first-child) { + margin-left: var(--ti-common-space-3x); + } + + ::ng-deep .ti3-layout-section { + padding: var(--ti-common-space-5x) var(--ti-common-space-6x); + } +} diff --git a/src/layout/lib/src/layout-content-body.less b/src/layout/lib/src/layout-content-body.less new file mode 100644 index 0000000..cf3fa21 --- /dev/null +++ b/src/layout/lib/src/layout-content-body.less @@ -0,0 +1,39 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-buylayer-height: 70px; +} + +/**内容块**/ +.ti3-layout-content[leftmenu] :host { + width: 100%; + .flex-item(1); + .flex-container(); + padding-bottom: var(--ti-common-space-5x); + &[vertical] { + // 有leftmenu场景时,内容区竖向排列 + .flex-container(column); + } +} + +.ti3-layout-content:not([leftmenu]) :host { + display: block; + padding-bottom: calc(var(--ti-buylayer-height) + var(--ti-common-space-3x)); + &.ti3-no-buylayer { + padding-bottom: var(--ti-common-space-3x); + } + + &.ti3-no-steps ::ng-deep .ti3-layout-section:first-child { + margin-top: var(--ti-common-space-3x); + } +} + +.ti3-layout-content[adaptive] :host { + padding: var(--ti-common-space-3x) 0 var(--ti-common-space-5x); + + &[columns] { + // 多列模式下使用flex布局 + // .flex(); + .flex-container(); + } +} diff --git a/src/layout/lib/src/layout-content-header.less b/src/layout/lib/src/layout-content-header.less new file mode 100644 index 0000000..5572c3f --- /dev/null +++ b/src/layout/lib/src/layout-content-header.less @@ -0,0 +1,5 @@ +/**内容头*/ +.ti3-layout-content[leftmenu] :host { + display: block; + padding: var(--ti-common-space-4x) 0; +} diff --git a/src/layout/lib/src/layout-content.less b/src/layout/lib/src/layout-content.less new file mode 100644 index 0000000..de1e335 --- /dev/null +++ b/src/layout/lib/src/layout-content.less @@ -0,0 +1,68 @@ +@import '../../../themes/basic/base-all.less'; +@import './layout.less'; + +:host.ti3-layout-content[leftmenu] { + padding: 0 var(--ti-common-space-5x); + zoom: 1; + height: 100%; + .flex-container(column); +} + +:host.ti3-layout-content:not([leftmenu]) { + // 宽度固定居中,适用于购买页面固定居中布局 + display: block; + width: var(--ti-min-width); + margin: 0 auto; +} + +/**宽度自适应,左右间距固定, 适用于详情页面,详情页面可能有leftmnu,也可能没有。**/ +:host.ti3-layout-content[adaptive] { + width: 100%; + padding: 0 var(--ti-common-space-5x); + box-sizing: border-box; +} + +// 固定居中时,<1280时,出滚动条, 固定宽度1192 + 48 + 20 * 2 = 1280 +@media screen and (max-width: 1280px) { + ::ng-deep .ti3-layout-header:not([adaptive]):not(.ti3-layout-purchase-responsive) { + width: calc(var(--ti-min-width) + var(--ti-common-space-5x) * 2); + .ti3-layout-header-content { + margin: 0 var(--ti-common-space-5x); + } + } + :host.ti3-layout-content:not([leftmenu]):not([adaptive]):not(.ti3-layout-purchase-responsive) { + width: var(--ti-min-width); + margin: 0 var(--ti-common-space-5x); + } +} + +// 宽度响应式,适用于购买页面响应式布局: +// 2560 ≥ 分辨率 >1920 ,卡片固定宽度为1832px。 +// 1920 ≥ 分辨率 ≥ 1280,卡片距离左右间距20px +// 例:1920分辨率下卡片宽度1832px;1280分辨率下卡片宽度1192px +// 分辨率<1280时,显示不下则出滚动条。 +@media screen and (min-width: 1920px) { + ::ng-deep .ti3-layout-header.ti3-layout-purchase-responsive { + .ti3-layout-header-content { + margin: 0 calc(50% - var(--ti-max-width) / 2); + } + } + :host.ti3-layout-content.ti3-layout-purchase-responsive:not([leftmenu]) { + width: var(--ti-max-width); + margin: 0 auto; + } +} +@media screen and (max-width: 1920px) { + // 当为1920px时,max-width: 1920px和min-width: 1920px都生效,所以要注意这两个的样式优先级 + ::ng-deep .ti3-layout-header.ti3-layout-purchase-responsive { + min-width: calc(var(--ti-min-width) + var(--ti-common-space-5x) * 2); + .ti3-layout-header-content { + margin: 0 var(--ti-common-space-5x); + } + } + :host.ti3-layout-content.ti3-layout-purchase-responsive:not([leftmenu]) { + min-width: var(--ti-min-width); + width: auto; + margin: 0 var(--ti-common-space-5x); + } +} diff --git a/src/layout/lib/src/layout-header.less b/src/layout/lib/src/layout-header.less new file mode 100644 index 0000000..8943649 --- /dev/null +++ b/src/layout/lib/src/layout-header.less @@ -0,0 +1,31 @@ +@import './layout.less'; + +:host { + --ti-layout-header-height: var(--ti-common-size-10x); +} +// 头部容器 +:host { + display: block; + position: relative; + background-color: var(--ti-common-color-bg-dark-normal); + height: var(--ti-layout-header-height); + line-height: var(--ti-layout-header-height); + box-shadow: none; + border-top: none; + + // 直接给.ti3-layout-header设置带有百分比计算的左右padding来使header内容居中时,有竖向滚动条时刷新页面或者由前一个页面没有竖向滚动条页面调到该页面,这时 IE 下就会出现横向滚动条。 + // 发现用带有百分比计算的值来设置左右padding时存在该问题,但是margin就不会存在该问题。所以此处给.ti3-layout-header内再添加一层容器.ti3-layout-header-content,设置其margin来规避该问题。 + .ti3-layout-header-content { + height: 100%; + } + + // 宽度固定居中,适用于购买固定居中布局 + &:not([adaptive]):not(.ti3-layout-purchase-responsive) .ti3-layout-header-content { + margin: 0 calc(50% - var(--ti-min-width) / 2); + } + + // 宽度自适应,左右间距固定,适用于详情页面布局 + &[adaptive] .ti3-layout-header-content { + margin: 0 var(--ti-common-space-5x); + } +} diff --git a/src/layout/lib/src/layout-section.less b/src/layout/lib/src/layout-section.less new file mode 100644 index 0000000..855af36 --- /dev/null +++ b/src/layout/lib/src/layout-section.less @@ -0,0 +1,30 @@ +/**内容分片*/ +.ti3-layout-content[leftmenu] ti-layout-content-body :host { + display: block; + padding: var(--ti-common-space-5x) var(--ti-common-space-6x); + background: var(--ti-common-color-bg-white-normal); + box-shadow: var(--ti-common-shadow-1-down); +} + +.ti3-layout-content[leftmenu] ti-layout-content-body[vertical] :host { + margin-bottom: var(--ti-common-space-3x); + + &:last-child { + margin-bottom: 0; + } +} + +.ti3-layout-content:not([leftmenu]) ti-layout-content-body :host { + display: block; + padding: var(--ti-common-space-6x) var(--ti-common-space-8x); + background-color: var(--ti-common-color-bg-white-normal); + margin-top: var(--ti-common-space-3x); + box-shadow: var(--ti-common-shadow-1-down); + + &:first-child { + margin-top: 0; + } +} +.ti3-layout-content[adaptive] ti-layout-content-body :host { + padding: var(--ti-common-space-5x) var(--ti-common-space-6x); +} diff --git a/src/layout/lib/src/layout.less b/src/layout/lib/src/layout.less new file mode 100644 index 0000000..ba516b2 --- /dev/null +++ b/src/layout/lib/src/layout.less @@ -0,0 +1,4 @@ +:host { + --ti-min-width: 1192px; + --ti-max-width: 1832px; +} diff --git a/src/leftmenu/demo/karma.conf.js b/src/leftmenu/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/leftmenu/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/leftmenu/demo/project.json b/src/leftmenu/demo/project.json new file mode 100644 index 0000000..f4918f3 --- /dev/null +++ b/src/leftmenu/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/leftmenu/demo", + "sourceRoot": "src/leftmenu/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/leftmenu", + "index": "src/leftmenu/demo/src/index.html", + "main": "src/leftmenu/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/leftmenu/demo/tsconfig.app.json", + "assets": ["src/leftmenu/demo/src/favicon.ico", "src/leftmenu/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "leftmenu-demo:build:production" + }, + "development": { + "browserTarget": "leftmenu-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js leftmenu" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/leftmenu/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/leftmenu/demo/tsconfig.spec.json", + "karmaConfig": "src/leftmenu/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/leftmenu/demo/src/app/AppComponent.ts b/src/leftmenu/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/leftmenu/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/leftmenu/demo/src/app/AppModule.ts b/src/leftmenu/demo/src/app/AppModule.ts new file mode 100644 index 0000000..47caeed --- /dev/null +++ b/src/leftmenu/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { LeftmenuTestModule } from './leftmenu/LeftmenuTestModule'; + +@NgModule({ + imports: [ + LeftmenuTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/leftmenu/demo/src/app/IndexComponent.ts b/src/leftmenu/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..e4dc489 --- /dev/null +++ b/src/leftmenu/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { LeftmenuTestModule } from './leftmenu/LeftmenuTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = LeftmenuTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/leftmenu/demo/src/app/app.html b/src/leftmenu/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/leftmenu/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuActiveChangeComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuActiveChangeComponent.ts new file mode 100644 index 0000000..0ec85d2 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuActiveChangeComponent.ts @@ -0,0 +1,41 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-active-change.html', + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuActiveChangeComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + myLogs: Array = []; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } + + activeChange(event: TiLeftmenuItem): void { + this.myLogs = [...this.myLogs, `activeChange() event=${event}`]; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuBasicComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuBasicComponent.ts new file mode 100644 index 0000000..a98cea4 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuBasicComponent.ts @@ -0,0 +1,36 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-basic.html', + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuBasicComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuCandeactivateComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuCandeactivateComponent.ts new file mode 100644 index 0000000..f485a47 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuCandeactivateComponent.ts @@ -0,0 +1,32 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-candeactivate.html', + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuCandeactivateComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuCollapsedComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuCollapsedComponent.ts new file mode 100644 index 0000000..11b887e --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuCollapsedComponent.ts @@ -0,0 +1,37 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-collapsed.html', + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuCollapsedComponent { + marginLeft: string = '0'; + reloadState: boolean = true; + collapsed: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuDisabledComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuDisabledComponent.ts new file mode 100644 index 0000000..15e812a --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuDisabledComponent.ts @@ -0,0 +1,38 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-disabled.html', + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuDisabledComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'], + disabled: true + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'], + disabled: true + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuDividingComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuDividingComponent.ts new file mode 100644 index 0000000..23bc7ac --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuDividingComponent.ts @@ -0,0 +1,38 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-dividing.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuDividingComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + showDividingLine: true, + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'] + } + ]; + + active: TiLeftmenuItem = this.items[1]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuFootComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuFootComponent.ts new file mode 100644 index 0000000..f84d332 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuFootComponent.ts @@ -0,0 +1,37 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-foot.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuFootComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] // 相对路由路径 + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuGroupComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuGroupComponent.ts new file mode 100644 index 0000000..8fde0c7 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuGroupComponent.ts @@ -0,0 +1,49 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-group.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuGroupComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '分组一', + isGroup: true, + children: [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + } + ] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '分组二', + isGroup: true, + children: [ + { + label: '菜单二', + router: ['router2'] + } + ] + } + ]; + + active: TiLeftmenuItem = this.items[1].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuHrefComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuHrefComponent.ts new file mode 100644 index 0000000..546185f --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuHrefComponent.ts @@ -0,0 +1,36 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-href.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuHrefComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + toggleable: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '首页', + href: '' + } + ] + }, + { + label: '地图', + href: '' + } + ]; + active: TiLeftmenuItem = this.items[0].children[0]; + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuIdComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuIdComponent.ts new file mode 100644 index 0000000..7b355e5 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuIdComponent.ts @@ -0,0 +1,65 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-id.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuIdComponent { + leftmenuId: string = 'leftmenu'; + headId: string = 'leftmenu_head'; + footId: string = 'leftmenu_foot'; + + headLabel: string = '弹性云服务器 ECS'; + footLabel: string = '返回旧版(自定义)'; + marginLeft: string = '192px'; + collapsed: boolean = false; // 默认展开,当设置为true时会收起 + toggleable: boolean = true; + reloadState: boolean = true; + items: Array = [ + { + id: 'hardDisk', + label: '云硬盘', + children: [ + { + id: 'disk', + label: '磁盘', + router: ['./router11'] // 相对路由路径 + }, + { + id: 'snapshot', + label: '快照', + router: ['./router12'] + } + ] + }, + { + id: 'mirror', + label: '镜像服务', + router: ['router2'] + }, + { + id: 'scale', + label: '弹性伸缩', + children: [ + { + id: 'scaleInstance', + label: '伸缩实例', + router: ['router31'] // 相对路由路径 + }, + { + id: 'scaleBandwidth', + label: '伸缩带宽', + router: ['router32'] // 绝对路由路径 + } + ] + } + ]; + + active: TiLeftmenuItem = this.items[1]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '192px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuNoRouterComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuNoRouterComponent.ts new file mode 100644 index 0000000..546360f --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuNoRouterComponent.ts @@ -0,0 +1,34 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-no-router.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuNoRouterComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuParamsComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuParamsComponent.ts new file mode 100644 index 0000000..99eaf38 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuParamsComponent.ts @@ -0,0 +1,43 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-params.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuParamsComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['router11'] + }, + { + label: '子菜单1.2', + router: ['router12'] + } + ] + }, + { + label: '带参数路由', + router: ['Routerparams', 1], + routerExtras: { + queryParams: { + locale: 'zh-cn' + }, + fragment: 'tiny' + } + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuReloadStateComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuReloadStateComponent.ts new file mode 100644 index 0000000..9d057fe --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuReloadStateComponent.ts @@ -0,0 +1,35 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-reload-state.html', + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuReloadStateComponent { + marginLeft: string = '200px'; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuRouterlistComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuRouterlistComponent.ts new file mode 100644 index 0000000..9e1fd07 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuRouterlistComponent.ts @@ -0,0 +1,38 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-routerlist.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuRouterlistComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '携带多个路由', + router: ['./router2'], + routerList: [['routerC']] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[1]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuScrollComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuScrollComponent.ts new file mode 100644 index 0000000..0842fe1 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuScrollComponent.ts @@ -0,0 +1,63 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-scroll.html', + styleUrls: ['./leftmenuTest.less'], + styles: ['body {height: 200px;}'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuScrollComponent { + headLabel: string = '弹性云服务器 ECS'; + marginLeft: string = '192px'; + collapsed: boolean = false; + toggleable: boolean = true; + reloadState: boolean = true; + myOptions: Array = [ + { label: '美国', englishname: 'America' }, + { label: '巴西', englishname: 'Brazil' } + ]; + items: Array = [ + { + label: '云硬盘', + children: [ + { + label: '磁盘', + router: ['./router11'] + }, + { + label: '快照', + router: ['./router12'] + } + ] + }, + { + label: '镜像服务', + router: ['router2'] + }, + { + label: '弹性伸缩', + children: [ + { + label: '伸缩带宽', + router: ['./router31'] // 绝对路由路径 + } + ] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + // 需要业务侧在菜单收起/展开时,控制右侧内容的位置。3.1.3版本leftMenu默认宽度修改为192px + this.marginLeft = isHide ? '0' : '192px'; + } + + clickLevel1(m1: any): void { + console.log(`点击一级菜单${JSON.stringify(m1)}`); + } + + clickLevel2(m2: any): void { + console.log(`点击二级菜单${JSON.stringify(m2)}`); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuSecurityComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuSecurityComponent.ts new file mode 100644 index 0000000..666bebb --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuSecurityComponent.ts @@ -0,0 +1,57 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-security.html', + styleUrls: ['./leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuSecurityComponent { + headLabel: string = '弹性云服务器 ECS'; + marginLeft: string = '192px'; + collapsed: boolean = false; // 默认展开,当设置为true时会收起 + toggleable: boolean = true; + reloadState: boolean = true; + items: Array = [ + { + label: '一级菜单', + children: [ + { + label: '首页', + href: '' // 跳转至新页面打开链接 + }, + { + label: 'XSS攻击脚本', + // 点击该项时js脚本alert('Hi XSS')不会执行 + href: "javascript:alert('Hi XSS')" + } + ] + }, + { + label: '原页面重定向', + // 点击该项时原页面不会被重定向 + href: '#/leftmenu/opener' + }, + { + label: '弹性伸缩', + children: [ + { + label: '伸缩实例', + router: ['router31'] // 相对路由路径 + }, + { + label: '伸缩带宽', + router: ['router32'] // 绝对路由路径 + } + ] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + // 需要业务侧在菜单收起\展开时,控制右侧内容的位置 + // 3.1.3版本leftMenu默认宽度修改为192px + this.marginLeft = isHide ? '0' : '192px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuTestModule.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuTestModule.ts new file mode 100644 index 0000000..0ac2fa0 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuTestModule.ts @@ -0,0 +1,277 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiLeftmenuModule, TiModalModule, TiSelectModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { LeftmenuNoRouterComponent } from './LeftmenuNoRouterComponent'; +import { LeftmenuBasicWebsiteViewComponent } from './website-views/LeftmenuBasicWebsiteViewComponent'; +import { LeftmenuRouterlistWebsiteViewComponent } from './website-views/LeftmenuRouterlistWebsiteViewComponent'; +import { LeftmenuParamsWebsiteViewComponent } from './website-views/LeftmenuParamsWebsiteViewComponent'; +import { LeftmenuHrefWebsiteViewComponent } from './website-views/LeftmenuHrefWebsiteViewComponent'; +import { LeftmenuGroupWebsiteViewComponent } from './website-views/LeftmenuGroupWebsiteViewComponent'; +import { LeftmenuFootWebsiteViewComponent } from './website-views/LeftmenuFootWebsiteViewComponent'; +import { LeftmenuDividingWebsiteViewComponent } from './website-views/LeftmenuDividingWebsiteViewComponent'; +import { LeftmenuNoRouterWebsiteViewComponent } from './website-views/LeftmenuNoRouterWebsiteViewComponent'; +import { LeftmenuReloadStateWebsiteViewComponent } from './website-views/LeftmenuReloadStateWebsiteViewComponent'; +import { LeftmenuDisabledWebsiteViewComponent } from './website-views/LeftmenuDisabledWebsiteViewComponent'; +import { LeftmenuActiveChangeWebsiteViewComponent } from './website-views/LeftmenuActiveChangeWebsiteViewComponent'; +import { LeftmenuCollapsedWebsiteViewComponent } from './website-views/LeftmenuCollapsedWebsiteViewComponent'; +import { LeftmenuToggleableWebsiteViewComponent } from './website-views/LeftmenuToggleableWebsiteViewComponent'; + +import { LeftmenuBasicComponent } from './LeftmenuBasicComponent'; +import { LeftmenuRouterlistComponent } from './LeftmenuRouterlistComponent'; +import { LeftmenuParamsComponent } from './LeftmenuParamsComponent'; +import { LeftmenuReloadStateComponent } from './LeftmenuReloadStateComponent'; +import { LeftmenuHrefComponent } from './LeftmenuHrefComponent'; +import { LeftmenuGroupComponent } from './LeftmenuGroupComponent'; +import { LeftmenuFootComponent } from './LeftmenuFootComponent'; +import { LeftmenuDividingComponent } from './LeftmenuDividingComponent'; +import { LeftmenuIdComponent } from './LeftmenuIdComponent'; +import { LeftmenuSecurityComponent } from './LeftmenuSecurityComponent'; +import { LeftmenuScrollComponent } from './LeftmenuScrollComponent'; +import { LeftmenuCollapsedComponent } from './LeftmenuCollapsedComponent'; +import { LeftmenuToggleableComponent } from './LeftmenuToggleableComponent'; +import { LeftmenuActiveChangeComponent } from './LeftmenuActiveChangeComponent'; +import { LeftmenuDisabledComponent } from './LeftmenuDisabledComponent'; +import { LeftmenuCandeactivateComponent } from './LeftmenuCandeactivateComponent'; + +import { Router11Component } from './Router11Component'; +import { Router12Component } from './Router12Component'; +import { Router2Component } from './Router2Component'; +import { Router31Component } from './Router31Component'; +import { Router32Component } from './Router32Component'; + +import { RouterAComponent } from './RouterAComponent'; +import { RouterBComponent } from './RouterBComponent'; +import { RouterCComponent } from './RouterCComponent'; +import { RouterDComponent } from './RouterDComponent'; +import { OpenerComponent } from './OpenerComponent'; +import { RouterparamsComponent } from './RouterparamsComponent'; + +import { UnsaveGuard } from './unsave'; +@NgModule({ + imports: [ + CommonModule, + TiLeftmenuModule, + TiModalModule, + TiButtonModule, + TiSelectModule, + DemoLogModule, + RouterModule.forChild(LeftmenuTestModule.ROUTES) + ], + providers: [UnsaveGuard], + declarations: [ + LeftmenuBasicComponent, + LeftmenuBasicWebsiteViewComponent, + LeftmenuRouterlistWebsiteViewComponent, + LeftmenuParamsWebsiteViewComponent, + LeftmenuHrefWebsiteViewComponent, + LeftmenuGroupWebsiteViewComponent, + LeftmenuFootWebsiteViewComponent, + LeftmenuDividingWebsiteViewComponent, + LeftmenuNoRouterWebsiteViewComponent, + LeftmenuReloadStateWebsiteViewComponent, + LeftmenuDisabledWebsiteViewComponent, + LeftmenuActiveChangeWebsiteViewComponent, + LeftmenuCollapsedWebsiteViewComponent, + LeftmenuToggleableWebsiteViewComponent, + LeftmenuNoRouterComponent, + Router11Component, + Router12Component, + Router2Component, + Router31Component, + Router32Component, + RouterAComponent, + RouterBComponent, + RouterCComponent, + RouterDComponent, + LeftmenuRouterlistComponent, + LeftmenuParamsComponent, + LeftmenuReloadStateComponent, + LeftmenuCollapsedComponent, + LeftmenuToggleableComponent, + LeftmenuHrefComponent, + LeftmenuGroupComponent, + LeftmenuFootComponent, + LeftmenuDividingComponent, + LeftmenuCandeactivateComponent, + LeftmenuIdComponent, + LeftmenuSecurityComponent, + OpenerComponent, + RouterparamsComponent, + LeftmenuScrollComponent, + LeftmenuActiveChangeComponent, + LeftmenuDisabledComponent + ] +}) +export class LeftmenuTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiLeftmenuComponent.html', label: 'Leftmenu' }, + { href: 'components/TiLeftmenuHeadComponent.html', label: 'LeftmenuHead' }, + { href: 'components/TiLeftmenuItemComponent.html', label: 'LeftmenuItem' }, + { + href: 'components/TiLeftmenuLevel1Component.html', + label: 'LeftmenuLevel1' + }, + { + href: 'components/TiLeftmenuLevel2Component.html', + label: 'LeftmenuLevel2' + } + ]; + static readonly ROUTES: Routes = [ + { + path: 'leftmenu/leftmenu-basic', + component: LeftmenuBasicComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerD', component: RouterDComponent }, + { path: 'routerC', component: RouterCComponent } + ] + }, + { + path: 'leftmenu/leftmenu-routerlist', + component: LeftmenuRouterlistComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerA', component: RouterAComponent }, + { path: 'routerB', component: RouterBComponent }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/leftmenu-params', + component: LeftmenuParamsComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31/:id', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerA', component: RouterAComponent }, + { path: 'routerB', component: RouterBComponent }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent }, + { path: 'Routerparams/:id', component: RouterparamsComponent } + ] + }, + { + path: 'leftmenu/leftmenu-href', + component: LeftmenuHrefComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router31', component: Router31Component }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/leftmenu-group', + component: LeftmenuGroupComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/leftmenu-foot', + component: LeftmenuFootComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/leftmenu-dividing', + component: LeftmenuDividingComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/leftmenu-no-router', + component: LeftmenuNoRouterComponent + }, + { + path: 'leftmenu/leftmenu-id', + component: LeftmenuIdComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/leftmenu-candeactivate', + component: LeftmenuCandeactivateComponent, + children: [ + { + path: 'router11', + component: Router11Component, + canDeactivate: [UnsaveGuard] + }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/leftmenu-security', + component: LeftmenuSecurityComponent, + children: [ + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component }, + { path: 'routerC', component: RouterCComponent }, + { path: 'routerD', component: RouterDComponent } + ] + }, + { + path: 'leftmenu/opener', + component: OpenerComponent + }, + { + path: 'leftmenu/leftmenu-scroll', + component: LeftmenuScrollComponent, + children: [ + { path: 'router11', component: Router11Component }, + { path: 'router12', component: Router12Component }, + { path: 'router2', component: Router2Component }, + { path: 'router31', component: Router31Component }, + { path: 'router32', component: Router32Component } + ] + } + ]; +} diff --git a/src/leftmenu/demo/src/app/leftmenu/LeftmenuToggleableComponent.ts b/src/leftmenu/demo/src/app/leftmenu/LeftmenuToggleableComponent.ts new file mode 100644 index 0000000..3eb6a07 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/LeftmenuToggleableComponent.ts @@ -0,0 +1,32 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-toggleable.html', + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuToggleableComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + router: ['./router11'] + }, + { + label: '子菜单1.2', + router: ['./router12'] + } + ] + }, + { + label: '菜单二', + router: ['router2'] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; +} diff --git a/src/leftmenu/demo/src/app/leftmenu/OpenerComponent.ts b/src/leftmenu/demo/src/app/leftmenu/OpenerComponent.ts new file mode 100644 index 0000000..33da98b --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/OpenerComponent.ts @@ -0,0 +1,32 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +@Component({ + template: ` +
    +

    恶意攻击网站

    +

    opener类型安全漏洞

    +

    如果在项目中需要 打开新标签 进行跳转一般会有两种方式

    +

    1) HTML -> a标签, target = _blank

    +

    2) JS -> window.open('')

    +

    + 这两种方式看起来没有问题,但是存在漏洞。通过这两种方式打开的页面可以使用 window.opener 来访问源页面的 window 对象。 场景:A 页面通过 + a标签 或 window.open 方式,打开 B 页面。但是 B 页面存在恶意代码如下: window.opener.location.replace('') + 【此代码仅针对打开新标签有效】 此时,用户正在浏览新标签页,但是原来网站的标签页已经被导航到了其他页面。 + 恶意网站可以伪造一个足以欺骗用户的页面,使得进行恶意破坏。 即使在跨域状态下 opener 仍可以调用 location.replace 方法。 +

    +
    + + ` +}) +export class OpenerComponent implements OnInit { + myLogs: Array = []; + ngOnInit(): void { + try { + window.opener.location.replace(''); + } catch (error) { + if (error.name === 'TypeError') { + this.myLogs = [...this.myLogs, `已做安全处理:已经将 opner 赋值为 null,这里无法拿到 opener 的引用`]; + } + } + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/Router11Component.ts b/src/leftmenu/demo/src/app/leftmenu/Router11Component.ts new file mode 100644 index 0000000..99f7914 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/Router11Component.ts @@ -0,0 +1,7 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + template: `

    子菜单 1.1 的内容

    ` +}) +export class Router11Component {} diff --git a/src/leftmenu/demo/src/app/leftmenu/Router12Component.ts b/src/leftmenu/demo/src/app/leftmenu/Router12Component.ts new file mode 100644 index 0000000..af7dd8b --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/Router12Component.ts @@ -0,0 +1,25 @@ +import { Component, OnInit } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; +@Component({ + template: ` +

    子菜单 1.2 的内容

    + + 弹窗标题 + +
    弹窗内容
    +
    + + + +
    + + ` +}) +export class Router12Component { + constructor(private tiModal: TiModalService) {} + show(content: string): void { + this.tiModal.open(content, { + modalClass: 'modal-class' + }); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/Router2Component.ts b/src/leftmenu/demo/src/app/leftmenu/Router2Component.ts new file mode 100644 index 0000000..f7bbe91 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/Router2Component.ts @@ -0,0 +1,24 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +@Component({ + template: ` +

    欢迎来到镜像服务页面

    + 点击跳转到帮助中心页面 + ` +}) +export class Router2Component implements OnInit { + constructor(private activeRoute: ActivatedRoute) {} + + ngOnInit(): void { + console.log('router2Test Init', this.activeRoute.routeConfig.path); + // 这种是直接获取queryParam + this.activeRoute.queryParams.subscribe( + (res: any) => { + console.log('queryParams', res); + }, + (err: any) => { + console.log('网络错误'); + } + ); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/Router31Component.ts b/src/leftmenu/demo/src/app/leftmenu/Router31Component.ts new file mode 100644 index 0000000..7b9f526 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/Router31Component.ts @@ -0,0 +1,26 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + template: ` +

    子菜单 3.1 的内容

    + 点击跳转到使用指南页面 + ` +}) +export class Router31Component implements OnInit { + token: string; + queryParams: string; + constructor(private activeRoute: ActivatedRoute) {} + ngOnInit(): void { + console.log('router31Test Init'); + + this.activeRoute.paramMap.subscribe((paramMap: any) => { + this.token = JSON.stringify(paramMap.params); + }); + + // 这种是直接获取queryParam + this.activeRoute.queryParamMap.subscribe((res: any) => { + this.queryParams = JSON.stringify(res.params); + }); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/Router32Component.ts b/src/leftmenu/demo/src/app/leftmenu/Router32Component.ts new file mode 100644 index 0000000..bd170ce --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/Router32Component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +@Component({ + template: `

    欢迎来到伸缩带宽页面

    ` +}) +export class Router32Component implements OnInit { + label: string; + constructor(private activeRoute: ActivatedRoute) {} + + ngOnInit(): void { + console.log('router32Test Init', this.activeRoute.routeConfig.path); + this.label = 'init'; + + // 这种是直接获取queryParam + this.activeRoute.queryParams.subscribe( + (res: any) => { + console.log('queryParams', res); + }, + (err: any) => { + console.log('网络错误'); + } + ); + } + + clickFn(): void { + this.label = `${this.label}xxxx`; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/RouterAComponent.ts b/src/leftmenu/demo/src/app/leftmenu/RouterAComponent.ts new file mode 100644 index 0000000..59ba986 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/RouterAComponent.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +@Component({ + template: `

    欢迎来到问题咨询页面

    ` +}) +export class RouterAComponent implements OnInit { + constructor(private activeRoute: ActivatedRoute) {} + + ngOnInit(): void { + console.log('RouterAComponent Init', this.activeRoute.routeConfig.path); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/RouterBComponent.ts b/src/leftmenu/demo/src/app/leftmenu/RouterBComponent.ts new file mode 100644 index 0000000..4a97f50 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/RouterBComponent.ts @@ -0,0 +1,16 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +@Component({ + template: ` +
    Welcome to router B!
    + go to routerA(in router32 routerList) + ` +}) +export class RouterBComponent implements OnInit { + label: string; + constructor(private activeRoute: ActivatedRoute) {} + + ngOnInit(): void { + console.log('RouterBComponent Init', this.activeRoute.routeConfig.path); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/RouterCComponent.ts b/src/leftmenu/demo/src/app/leftmenu/RouterCComponent.ts new file mode 100644 index 0000000..76d999f --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/RouterCComponent.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +@Component({ + template: `

    欢迎来到帮助中心页面

    ` +}) +export class RouterCComponent implements OnInit { + constructor(private activeRoute: ActivatedRoute) {} + + ngOnInit(): void { + console.log('RouterCComponent Init', this.activeRoute.routeConfig.path); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/RouterDComponent.ts b/src/leftmenu/demo/src/app/leftmenu/RouterDComponent.ts new file mode 100644 index 0000000..041a724 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/RouterDComponent.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +@Component({ + template: `

    欢迎来到使用指南页面

    ` +}) +export class RouterDComponent implements OnInit { + constructor(private activeRoute: ActivatedRoute) {} + + ngOnInit(): void { + console.log('RouterDComponent Init RouterDComponentRouterDComponent', this.activeRoute.routeConfig.path); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/RouterparamsComponent.ts b/src/leftmenu/demo/src/app/leftmenu/RouterparamsComponent.ts new file mode 100644 index 0000000..3845bf1 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/RouterparamsComponent.ts @@ -0,0 +1,27 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + template: ` +

    参数: {{ token }}

    +

    参数: {{ queryParams }}

    +

    参数:{{ fragment }}

    + ` +}) +export class RouterparamsComponent implements OnInit { + token: string; + queryParams: string; + fragment: string; + constructor(private activeRoute: ActivatedRoute) {} + ngOnInit(): void { + this.activeRoute.paramMap.subscribe((paramMap: any) => { + this.token = JSON.stringify(paramMap.params); + }); + this.activeRoute.queryParamMap.subscribe((res: any) => { + this.queryParams = JSON.stringify(res.params); + }); + this.activeRoute.fragment.subscribe((res: any) => { + this.fragment = JSON.stringify(res); + }); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-active-change.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-active-change.html new file mode 100644 index 0000000..12640cb --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-active-change.html @@ -0,0 +1,21 @@ + + + +
    {{ m1.label }}
    +
    + + {{ m2.label }} + +
    +
    +
    + +
    +

    事件日志:

    + diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-basic.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-basic.html new file mode 100644 index 0000000..5085b6c --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-basic.html @@ -0,0 +1,13 @@ + + + +
    {{ m1.label }}
    +
    + + {{ m2.label }} + +
    +
    +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-candeactivate.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-candeactivate.html new file mode 100644 index 0000000..c02afb7 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-candeactivate.html @@ -0,0 +1,13 @@ + + + +
    {{ m1.label }}
    +
    + + {{ m2.label }} + +
    +
    +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-collapsed.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-collapsed.html new file mode 100644 index 0000000..8911bc0 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-collapsed.html @@ -0,0 +1,19 @@ + + + +
    {{ m1.label }}
    +
    + + {{ m2.label }} + +
    +
    +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-disabled.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-disabled.html new file mode 100644 index 0000000..aac1f5b --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-disabled.html @@ -0,0 +1,13 @@ + + + +
    {{ m1.label }}
    +
    + + {{ m2.label }} + +
    +
    +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-dividing.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-dividing.html new file mode 100644 index 0000000..aac1f5b --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-dividing.html @@ -0,0 +1,13 @@ + + + +
    {{ m1.label }}
    +
    + + {{ m2.label }} + +
    +
    +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-foot.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-foot.html new file mode 100644 index 0000000..c37b7b7 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-foot.html @@ -0,0 +1,19 @@ + + + +
    菜单标题内容
    +
    + +
    {{ m1.label }}
    +
    + + {{ m2.label }} + +
    + +
    菜单底部内容
    +
    +
    +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-group.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-group.html new file mode 100644 index 0000000..b71cba1 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-group.html @@ -0,0 +1,29 @@ + + + + + {{ group.label }} + + + +
    {{ m1.label }}
    +
    + + {{ m2.label }} + +
    +
    + + +
    {{ group.label }}
    +
    + + {{ m2.label }} + +
    +
    +
    + +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-href.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-href.html new file mode 100644 index 0000000..aac1f5b --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-href.html @@ -0,0 +1,13 @@ + + + +
    {{ m1.label }}
    +
    + + {{ m2.label }} + +
    +
    +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-id.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-id.html new file mode 100644 index 0000000..c5f7de8 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-id.html @@ -0,0 +1,29 @@ + + + +
    {{headLabel}}
    +
    + + +
    {{m1.label}}
    +
    + + {{m2.label}} + +
    + +
    {{footLabel}}
    +
    +
    + + +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-no-router.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-no-router.html new file mode 100644 index 0000000..ad1c290 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-no-router.html @@ -0,0 +1,23 @@ + + + +
    {{ m1.label }}
    +
    + + {{ m2.label }} + +
    +
    +
    +
    子菜单 1.1 的内容
    +
    子菜单 1.2 的内容
    +
    +

    菜单二的内容

    +
    +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-params.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-params.html new file mode 100644 index 0000000..aac1f5b --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-params.html @@ -0,0 +1,13 @@ + + + +
    {{ m1.label }}
    +
    + + {{ m2.label }} + +
    +
    +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-reload-state.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-reload-state.html new file mode 100644 index 0000000..22f3bca --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-reload-state.html @@ -0,0 +1,13 @@ + + + +
    {{ m1.label }}
    +
    + + {{ m2.label }} + +
    +
    +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-routerlist.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-routerlist.html new file mode 100644 index 0000000..5085b6c --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-routerlist.html @@ -0,0 +1,13 @@ + + + +
    {{ m1.label }}
    +
    + + {{ m2.label }} + +
    +
    +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-scroll.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-scroll.html new file mode 100644 index 0000000..dba4208 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-scroll.html @@ -0,0 +1,26 @@ + + + + 请选出您所在国家: + + + + +
    {{m1.label}}
    +
    + + {{m2.label}} + +
    +
    + + +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-security.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-security.html new file mode 100644 index 0000000..2c6f0d9 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-security.html @@ -0,0 +1,25 @@ + + + +
    {{headLabel}}
    +
    + + +
    {{m1.label}}
    +
    + + {{m2.label}} + +
    +
    + + +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenu-toggleable.html b/src/leftmenu/demo/src/app/leftmenu/leftmenu-toggleable.html new file mode 100644 index 0000000..62c85f5 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenu-toggleable.html @@ -0,0 +1,13 @@ + + + +
    {{ m1.label }}
    +
    + + {{ m2.label }} + +
    +
    +
    + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/leftmenuTest.less b/src/leftmenu/demo/src/app/leftmenu/leftmenuTest.less new file mode 100644 index 0000000..68ecf14 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/leftmenuTest.less @@ -0,0 +1,9 @@ +body { + background-color: #eef0f5; +} +.clear-transform { + background-color: #f5f2f0; + transform: scale(1, 1); + height: 460px; + overflow: auto; +} diff --git a/src/leftmenu/demo/src/app/leftmenu/unsave.ts b/src/leftmenu/demo/src/app/leftmenu/unsave.ts new file mode 100644 index 0000000..9e70543 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/unsave.ts @@ -0,0 +1,8 @@ +import { CanDeactivate } from '@angular/router'; +import { TiLeftmenuLevel2Component } from '@opentiny/ng'; + +export class UnsaveGuard implements CanDeactivate { + canDeactivate(component: TiLeftmenuLevel2Component) { + return window.confirm('确定要离开当前菜单吗?'); + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu-demos.js b/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu-demos.js new file mode 100644 index 0000000..26401b8 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu-demos.js @@ -0,0 +1,163 @@ +export default { + column: '1', + demos: [ + { + demoId: 'leftmenu-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    Leftmenu 组件的最简用法。

    ', + 'en-US': '

    leftmenu basic

    ' + }, + apis: [ + 'TiLeftmenuComponent.properties.active', + 'TiLeftmenuLevel1Component.properties.item', + 'TiLeftmenuLevel2Component.properties.item' + ] + }, + { + demoId: 'leftmenu-routerlist', + name: { + 'zh-CN': '同一菜单下挂载多个路由', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    支持在同一菜单下挂载多个路由。

    ', + 'en-US': '

    ' + } + }, + { + demoId: 'leftmenu-params', + name: { + 'zh-CN': '路由参数', + 'en-US': '' + }, + desc: { + 'zh-CN': + '

    通过TiLeftmenuItem实例的routerExtras属性配置路由参数,更多配置请参考 Angular 官方文档。注意:如果需要在路由路径中携带参数,请在注册路由时使用冒号 : 表示路径参数,例如下方示例中定义了 /:id。更多路由配置请参考 Angular 官方文档

    ', + 'en-US': '

    ' + }, + codeFiles: ['leftmenu-params.html', 'LeftmenuParamsComponent.ts', 'RouterparamsComponent.ts'] + }, + { + demoId: 'leftmenu-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    通过属性disabled配置菜单的禁用状态。

    ', + 'en-US': '

    ' + }, + apis: ['TiLeftmenuLevel1Component.properties.disabled', 'TiLeftmenuLevel2Component.properties.disabled'] + }, + { + demoId: 'leftmenu-reload-state', + name: { + 'zh-CN': '路由刷新', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    不设置属性reloadState,则点击当前已激活菜单时右侧内容区不会刷新。

    ', + 'en-US': '

    ' + }, + apis: ['TiLeftmenuComponent.properties.reloadState'] + }, + { + demoId: 'leftmenu-collapsed', + name: { + 'zh-CN': '面板水平方向展开状态', + 'en-US': '' + }, + desc: { + 'zh-CN': + '

    通过属性collapsed配置菜单面板水平方向的展开状态。当菜单面板水平方向折叠/展开状态改变的时候触发collapsedChange事件,传递参数为改变后面板是否处于折叠状态,true 表示面板被折叠,false 表示面板被展开。

    ', + 'en-US': '

    ' + }, + apis: ['TiLeftmenuComponent.properties.collapsed', 'TiLeftmenuComponent.events.collapsedChange'] + }, + { + demoId: 'leftmenu-toggleable', + name: { + 'zh-CN': '不可折叠面板', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    通过属性toggleable配置菜单面板是否可以水平方向折叠。

    ', + 'en-US': '

    ' + }, + apis: ['TiLeftmenuComponent.properties.toggleable'] + }, + { + demoId: 'leftmenu-active-change', + name: { + 'zh-CN': '切换激活菜单', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    支持通过监听菜单的 click 事件来拿到用户点击菜单的回调

    ', + 'en-US': '

    ' + }, + apis: ['TiLeftmenuComponent.events.activeChange'] + }, + { + demoId: 'leftmenu-href', + name: { + 'zh-CN': '链接跳转', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    通过TiLeftmenuItem实例的href属性配置链接地址。

    ', + 'en-US': '

    ' + } + }, + { + demoId: 'leftmenu-group', + name: { + 'zh-CN': '分组展示', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    通过 LeftmenuGroup 组件对路由进行分组。

    ', + 'en-US': '

    ' + } + }, + { + demoId: 'leftmenu-foot', + name: { + 'zh-CN': '自定义标题及底部', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    通过 LeftmenuHead 组件配置菜单底部内容;通过LeftmenuFoot 组件配置菜单底部内容。

    ', + 'en-US': '

    ' + } + }, + { + demoId: 'leftmenu-dividing', + name: { + 'zh-CN': '分割线', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    通过TiLeftmenuItem实例的showDividingLine属性配置分割线。注意:该属性仅对一级菜单生效。

    ', + 'en-US': '

    ' + } + }, + { + demoId: 'leftmenu-no-router', + name: { + 'zh-CN': '关闭路由模式', + 'en-US': '' + }, + desc: { + 'zh-CN': + '

    通过属性routable配置是否启用路由模式。注意:本组件路由使用 @angular/router,你可以通过 Angular 官方文档 查看更多关于路由的内容。

    ', + 'en-US': '

    ' + }, + apis: ['TiLeftmenuComponent.properties.routable'] + } + ] +}; diff --git a/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.cn.md b/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.cn.md new file mode 100644 index 0000000..54417d6 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.cn.md @@ -0,0 +1,36 @@ +--- +title: Leftmenu 左侧菜单 +--- +# Leftmenu 左侧菜单 + +
    + +Leftmenu 是提供页面导航的菜单列表。   + ++ 支持二级菜单。 + ++ 支持菜单分组。 + ++ 支持头部、底部区域自定义。 + +```typescript +import { TiLeftmenuModule } from '@opentiny/ng'; +``` + +
    + +
    + +Leftmenu 是提供页面导航的菜单列表。   + ++ 支持二级菜单。 + ++ 支持菜单分组。 + ++ 支持头部、底部区域自定义。 + +```typescript +import { TiLeftmenuModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.en.md b/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/webdoc/leftmenu.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuActiveChangeWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuActiveChangeWebsiteViewComponent.ts new file mode 100644 index 0000000..96b8e46 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuActiveChangeWebsiteViewComponent.ts @@ -0,0 +1,39 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-active-change-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuActiveChangeWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + myLogs: Array = []; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } + + itemClickHandle(event: TiLeftmenuItem): void { + this.myLogs = [...this.myLogs, `itemClickHandle() event=${JSON.stringify(event)}`]; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuBasicWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuBasicWebsiteViewComponent.ts new file mode 100644 index 0000000..fffc8cd --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuBasicWebsiteViewComponent.ts @@ -0,0 +1,34 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-basic-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuBasicWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuCollapsedWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuCollapsedWebsiteViewComponent.ts new file mode 100644 index 0000000..c105596 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuCollapsedWebsiteViewComponent.ts @@ -0,0 +1,35 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-collapsed-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuCollapsedWebsiteViewComponent { + marginLeft: string = '0'; + collapsed: boolean = true; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDisabledWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDisabledWebsiteViewComponent.ts new file mode 100644 index 0000000..b5a0b36 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDisabledWebsiteViewComponent.ts @@ -0,0 +1,36 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-disabled-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuDisabledWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1', + disabled: true + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二', + disabled: true + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDividingWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDividingWebsiteViewComponent.ts new file mode 100644 index 0000000..427352e --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDividingWebsiteViewComponent.ts @@ -0,0 +1,35 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-dividing-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuDividingWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + showDividingLine: true, + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuFootWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuFootWebsiteViewComponent.ts new file mode 100644 index 0000000..06fca92 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuFootWebsiteViewComponent.ts @@ -0,0 +1,34 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-foot-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuFootWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuGroupWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuGroupWebsiteViewComponent.ts new file mode 100644 index 0000000..05352b7 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuGroupWebsiteViewComponent.ts @@ -0,0 +1,46 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-group-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuGroupWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '分组一', + isGroup: true, + children: [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + } + ] + }, + { + label: '分组二', + isGroup: true, + children: [ + { + label: '菜单二' + } + ] + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuHrefWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuHrefWebsiteViewComponent.ts new file mode 100644 index 0000000..103041c --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuHrefWebsiteViewComponent.ts @@ -0,0 +1,36 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-href-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuHrefWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '首页', + href: '' + } + ] + }, + { + label: '地图', + href: '' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuNoRouterWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuNoRouterWebsiteViewComponent.ts new file mode 100644 index 0000000..1922112 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuNoRouterWebsiteViewComponent.ts @@ -0,0 +1,34 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-no-router-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuNoRouterWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuParamsWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuParamsWebsiteViewComponent.ts new file mode 100644 index 0000000..60582e0 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuParamsWebsiteViewComponent.ts @@ -0,0 +1,34 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-params-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuParamsWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '带参数路由' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuReloadStateWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuReloadStateWebsiteViewComponent.ts new file mode 100644 index 0000000..5d03cd8 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuReloadStateWebsiteViewComponent.ts @@ -0,0 +1,33 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-reload-state-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuReloadStateWebsiteViewComponent { + marginLeft: string = '200px'; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuRouterlistWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuRouterlistWebsiteViewComponent.ts new file mode 100644 index 0000000..fe2e0c1 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuRouterlistWebsiteViewComponent.ts @@ -0,0 +1,42 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-routerlist-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuRouterlistWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '携带多个路由' + } + ]; + showD: boolean = false; + active: TiLeftmenuItem = this.items[0].children[0]; + + toggleClick(isHide: boolean): void { + this.marginLeft = isHide ? '0' : '200px'; + } + linkToD(): void { + this.showD = true; + } + clickLevel2(m2: any): void { + if (m2 !== this.items[1]) { + this.showD = false; + } + } +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuToggleableWebsiteViewComponent.ts b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuToggleableWebsiteViewComponent.ts new file mode 100644 index 0000000..47585ed --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/LeftmenuToggleableWebsiteViewComponent.ts @@ -0,0 +1,30 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiLeftmenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './leftmenu-toggleable-website-view.html', + styleUrls: ['../leftmenuTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class LeftmenuToggleableWebsiteViewComponent { + marginLeft: string = '200px'; + reloadState: boolean = true; + items: Array = [ + { + label: '菜单一', + children: [ + { + label: '子菜单1.1' + }, + { + label: '子菜单1.2' + } + ] + }, + { + label: '菜单二' + } + ]; + + active: TiLeftmenuItem = this.items[0].children[0]; +} diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-active-change-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-active-change-website-view.html new file mode 100644 index 0000000..42c2092 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-active-change-website-view.html @@ -0,0 +1,27 @@ +
    + + + + {{ m1.label }} + + + {{ m2.label }} + + + +
    +
    子菜单 1.1 的内容
    +
    子菜单 1.2 的内容
    +
    +

    菜单二的内容

    +
    +
    +
    +

    事件日志:

    + diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-basic-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-basic-website-view.html new file mode 100644 index 0000000..111890d --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-basic-website-view.html @@ -0,0 +1,25 @@ +
    + + + + {{ m1.label }} + + + {{ m2.label }} + + + +
    +
    子菜单 1.1 的内容
    +
    子菜单 1.2 的内容
    +
    +

    菜单二的内容

    +
    +
    +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-collapsed-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-collapsed-website-view.html new file mode 100644 index 0000000..eb85ab8 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-collapsed-website-view.html @@ -0,0 +1,26 @@ +
    + + + + {{ m1.label }} + + + {{ m2.label }} + + + +
    +
    子菜单 1.1 的内容
    +
    子菜单 1.2 的内容
    +
    +

    菜单二的内容

    +
    +
    +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-disabled-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-disabled-website-view.html new file mode 100644 index 0000000..bd9b9a8 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-disabled-website-view.html @@ -0,0 +1,25 @@ +
    + + + + {{ m1.label }} + + + {{ m2.label }} + + + +
    +
    子菜单 1.1 的内容
    +
    子菜单 1.2 的内容
    +
    +

    菜单二的内容

    +
    +
    +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-dividing-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-dividing-website-view.html new file mode 100644 index 0000000..d3a0461 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-dividing-website-view.html @@ -0,0 +1,26 @@ +
    + + +
    菜单标题
    +
    + + + {{ m1.label }} + + + {{ m2.label }} + + +
    +
    +
    子菜单 1.1 的内容
    +
    子菜单 1.2 的内容
    +
    菜单二的内容
    +
    +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-foot-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-foot-website-view.html new file mode 100644 index 0000000..50b046e --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-foot-website-view.html @@ -0,0 +1,29 @@ +
    + + +
    菜单标题内容
    +
    + + + {{ m1.label }} + + + {{ m2.label }} + + + +
    菜单底部内容
    +
    +
    +
    +
    子菜单 1.1 的内容
    +
    子菜单 1.2 的内容
    +
    菜单二的内容
    +
    +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-group-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-group-website-view.html new file mode 100644 index 0000000..20143ba --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-group-website-view.html @@ -0,0 +1,40 @@ +
    + + + + + {{ group.label }} + + + +
    {{ m1.label }}
    +
    + + {{ m2.label }} + +
    +
    + + +
    {{ group.label }}
    +
    + + {{ m2.label }} + +
    +
    +
    +
    +
    子菜单 1.1 的内容
    +
    子菜单 1.2 的内容
    +
    +

    菜单二的内容

    +
    +
    +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-href-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-href-website-view.html new file mode 100644 index 0000000..00fa627 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-href-website-view.html @@ -0,0 +1,23 @@ +
    + + + + {{ m1.label }} + + + {{ m2.label }} + + + +
    +
    子菜单 1.1 的内容
    +
    子菜单 1.2 的内容
    +
    菜单二的内容
    +
    +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-no-router-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-no-router-website-view.html new file mode 100644 index 0000000..cd099c2 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-no-router-website-view.html @@ -0,0 +1,28 @@ +
    + + +
    菜单标题
    +
    + + + {{ m1.label }} + + + {{ m2.label }} + + +
    +
    +
    子菜单 1.1 的内容
    +
    子菜单 1.2 的内容
    +
    +

    菜单二的内容

    +
    +
    +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-params-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-params-website-view.html new file mode 100644 index 0000000..b8600fe --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-params-website-view.html @@ -0,0 +1,27 @@ +
    + + + + {{ m1.label }} + + + {{ m2.label }} + + + +
    +
    子菜单 1.1 的内容
    +
    子菜单 1.2 的内容
    +
    +

    参数: {{ '{' }}"id":"1"{{ '}' }}

    +

    参数: {{ '{' }}"locale":"zh-cn"{{ '}' }}

    +

    参数:"tiny"

    +
    +
    +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-reload-state-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-reload-state-website-view.html new file mode 100644 index 0000000..bc95ea4 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-reload-state-website-view.html @@ -0,0 +1,19 @@ +
    + + + + {{ m1.label }} + + + {{ m2.label }} + + + +
    +
    子菜单 1.1 的内容
    +
    子菜单 1.2 的内容
    +
    +

    菜单二的内容

    +
    +
    +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-routerlist-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-routerlist-website-view.html new file mode 100644 index 0000000..842a232 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-routerlist-website-view.html @@ -0,0 +1,27 @@ +
    + + + + {{ m1.label }} + + + {{ m2.label }} + + + +
    +
    子菜单 1.1 的内容
    +
    子菜单 1.2 的内容
    +
    +

    菜单二的内容

    +

    点击跳转到使用路由 D 所指向的页面

    +

    这里是路由 D 所指向的页面

    +
    +
    +
    diff --git a/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-toggleable-website-view.html b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-toggleable-website-view.html new file mode 100644 index 0000000..563d7a4 --- /dev/null +++ b/src/leftmenu/demo/src/app/leftmenu/website-views/leftmenu-toggleable-website-view.html @@ -0,0 +1,19 @@ +
    + + + + {{ m1.label }} + + + {{ m2.label }} + + + +
    +
    子菜单 1.1 的内容
    +
    子菜单 1.2 的内容
    +
    +

    菜单二的内容

    +
    +
    +
    diff --git a/src/leftmenu/demo/src/favicon.ico b/src/leftmenu/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/leftmenu/demo/src/index.html b/src/leftmenu/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/leftmenu/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/leftmenu/demo/src/main.ts b/src/leftmenu/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/leftmenu/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/leftmenu/demo/test.ts b/src/leftmenu/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/leftmenu/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/leftmenu/demo/tsconfig.app.json b/src/leftmenu/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/leftmenu/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/leftmenu/demo/tsconfig.spec.json b/src/leftmenu/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/leftmenu/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/leftmenu/lib/index.ts b/src/leftmenu/lib/index.ts new file mode 100644 index 0000000..287f1e6 --- /dev/null +++ b/src/leftmenu/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiLeftmenuModule'; diff --git a/src/leftmenu/lib/ng-package.json b/src/leftmenu/lib/ng-package.json new file mode 100644 index 0000000..64970cd --- /dev/null +++ b/src/leftmenu/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/leftmenu", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/leftmenu/lib/package.json b/src/leftmenu/lib/package.json new file mode 100644 index 0000000..9efac38 --- /dev/null +++ b/src/leftmenu/lib/package.json @@ -0,0 +1,16 @@ +{ + "name": "@opentiny/ng-leftmenu", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/router": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/platform-browser": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/leftmenu/lib/project.json b/src/leftmenu/lib/project.json new file mode 100644 index 0000000..8964023 --- /dev/null +++ b/src/leftmenu/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/leftmenu/lib", + "sourceRoot": "src/leftmenu/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/leftmenu"], + "options": { + "project": "src/leftmenu/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/leftmenu"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js leftmenu" + }, + { + "command": "ng default-build leftmenu" + }, + { + "command": "node build/clear-default-theme.js leftmenu" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/leftmenu && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build leftmenu && ng pack leftmenu && node build/publish.js leftmenu --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/leftmenu/lib/src/TiLeftmenuComponent.ts b/src/leftmenu/lib/src/TiLeftmenuComponent.ts new file mode 100644 index 0000000..95a8875 --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuComponent.ts @@ -0,0 +1,513 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ElementRef, + EventEmitter, + Input, + NgZone, + Optional, + Output, + Renderer2, + SimpleChange, + SimpleChanges, + ViewEncapsulation, + Inject, + ChangeDetectionStrategy, + IterableDiffer, + IterableDiffers +} from '@angular/core'; +import { ActivatedRoute, NavigationEnd, NavigationExtras, Router, UrlTree } from '@angular/router'; +import { DOCUMENT } from '@angular/common'; +import { Subscription } from 'rxjs'; + +import { Util } from '@opentiny/ng-utils'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiLeftmenuLevel1Component } from './TiLeftmenuLevel1Component'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import packageInfo from '../package.json'; + +/** + * 左侧菜单各项数据接口 + * + * 是TiLeftmenuLevel1 和 TiLeftmenuLevel2 组件的 item 接口的数据类型 + */ +export interface TiLeftmenuItem { + /** + * 设置当前菜单项对应的路由,与 Router.navigate方法的第一个参数一样 + */ + router?: Array; + /** + * 设置当前菜单项对应路由的配置信息,与 Router.navigate方法的第二个参数一样 + */ + routerExtras?: NavigationExtras; + /** + * 设置一个router路由列表,当该列表中的路由被激活时,其对应的菜单项被激活处于高亮状态。 + * 其使用场景为多个路由对应同一个左侧菜单项。 + * + * routerList 数组中每一个元素与 Router.navigate方法的第一个参数一样 + */ + routerList?: Array>; // 数组中元素也为数组 + /** + * 链接地址,点击后路由激活项不变,在新页面打开配置地址。 + * + */ + href?: string; + /** + * 设置分界线(只支持一级菜单),在设置该属性菜单项的底部生成一条区块分界线。 + * + */ + showDividingLine?: boolean; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} +/** + * TiLeftmenu组件提供了一种方便灵活实现页面布局左侧菜单的方式,支持两级菜单。 + * + * 其内部包含5个组件: + * + * TiLeftmenu : 左侧菜单最外层的指令; + * + * TiLeftmenuGroup: 菜单分组指令; + * + * TileftmenuGroupItem:分组文本内容指令; + * + * TiLeftmenuHead : 菜单头部内容指令; + * + * TiLeftmenuLevel1 :一级菜单指令; + * + * TiLeftmenuItem :一级菜单文本内容指令; + * + * TiLeftmenuLevel2 :二级菜单指令。 + * + */ +@Component({ + selector: 'ti-leftmenu', + templateUrl: './leftmenu.html', + styleUrls: ['./leftmenu.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-leftmenu-container]': 'true', + '[class.ti3-leftmenu-hide]': 'collapsed' + }, + encapsulation: ViewEncapsulation.None +}) +export class TiLeftmenuComponent extends TiBaseComponent { + /** + * 设置激活菜单项,值必须是 TiLeftmenuItem 类型,且需要是 TiLeftmenuLevel1 或 TiLeftmenuLevel2 的成员之一。 + * 注意:该属性必须双向绑定 + */ + @Input() active: TiLeftmenuItem; + /** + * 激活菜单项改变时向外通知 + * @ignore + */ + + @Output() readonly activeChange: EventEmitter = new EventEmitter(); + /** + * 当重复点击当前激活菜单项时,通过此属性控制路由刷新。 + * 注意:该属性必须双向绑定 + */ + @Input() reloadState: boolean = true; + /** + * 右侧内容区是否需要刷新的状态值改变时向外通知 + * @ignore + */ + @Output() readonly reloadStateChange: EventEmitter = new EventEmitter(); + // routable接口在3.0.3版本起对外隐藏,10.0.1重新开放 + /** + * 是否使用路由来控制菜单对应内容显示/隐藏,默认使用路由。 + */ + @Input() routable: boolean = true; + /** + * 是否开启左侧菜单面板可折叠功能,默认开启 + */ + @Input() toggleable: boolean = true; + /** + * 左侧菜单面板是否为收起状态,用于设置面板收起/展开状态,默认展开 + */ + @Input() collapsed: boolean = false; + /** + * 左侧菜单面板收起/展开状态改变时向外通知 + */ + @Output() readonly collapsedChange: EventEmitter = new EventEmitter(); + /** + * @ignore + * tiLeftmenu中包含的 TiLeftmenuLevel1Component 实例集合 + */ + public level1Items: Array = []; + protected versionInfo: string = super.getVersion(packageInfo); + private navigationSubscription: Subscription; + /** + * @ignore + */ + public readonly resolvedPromise: Promise = Promise.resolve(null); + /** + * 标志量,是否为url改变(需要去改变active) + */ + private isUrlChange: boolean = false; + /** + * @ignore + */ + private panelContainer: any; + + private onHeadChange: (e: CustomEvent) => void; // console头部高度变化时触发 + + private consoleDataService: any; + /** + * 滚动条的宽度, 在 TiLeftmenuFoot中也有使用 + * @ignore + */ + public scrollWidth: number; + private itemsDiffer: IterableDiffer; + private headElement: HTMLElement; + private placeholderEle: HTMLElement; // 占位元素 + constructor( + elementRef: ElementRef, + public renderer2: Renderer2, + @Optional() private router: Router, + @Optional() private activeRoute: ActivatedRoute, + private iterableDiffers: IterableDiffers, + private tiRenderer: TiRenderer, + private zone: NgZone, + @Inject(DOCUMENT) private document + ) { + super(elementRef, renderer2); + } + + // 判断item的router是否是父路由下默认激活的子路由(即子路由与父路由路径一致) + private static isDefaultActiveChildRoute(item: TiLeftmenuItem): boolean { + // 默认激活的子路由的router可设为[]或[''] + return !item.router || item.router.length === 0 || item.router[0] === ''; + } + + ngOnInit(): void { + super.ngOnInit(); + // 初始时右侧内容区默认显示,需使外部用户的reloadState值为true。 + // 双向绑定数据时,对初始传入的值立即修改并传出时会报错, + // 此处参考ngModel源码setValue的处理,使用promise延后执行时序 + this.resolvedPromise.then(() => { + this.reloadStateChange.emit(true); + }); + this.scrollWidth = this.getScrollbarSize('Y'); + this.itemsDiffer = this.iterableDiffers.find(this.level1Items || []).create(); + + // console头部高度变化需同步改变leftMenu的定位 + const consoleContext = (window).getConsoleContext && (window).getConsoleContext(); + this.consoleDataService = consoleContext?.get && consoleContext.get({ name: 'safearea' }); + this.onHeadChange = (e: CustomEvent) => { + this.renderer2.setStyle(this.nativeElement, 'top', e.detail.top + 'px'); + this.renderer2.setStyle(this.nativeElement, 'left', e.detail.left + 'px'); + // COC场景下console左侧无sidebar,但是CUI工具会添加left:48px important, + // 通过renderer2.setStyle方式添加的important不生效(原因不清楚),所以通过添加class增加样式权重 + this.changeLeftMenuLeftPosition(e.detail.left); + }; + // console初始化首次进来不会触发consoleDataService.onChange,需要根据getSafeArea()设置一次。 + if (this.consoleDataService?.getSafeArea) { + const safeArea = this.consoleDataService.getSafeArea(); + this.renderer2.setStyle(this.nativeElement, 'top', safeArea.top + 'px'); + this.renderer2.setStyle(this.nativeElement, 'left', safeArea.left + 'px'); + // COC场景下console左侧无sidebar,但是CUI工具会添加left:48px important, + // 通过renderer2.setStyle方式添加的important不生效(原因不清楚),所以通过添加class增加样式权重 + this.changeLeftMenuLeftPosition(safeArea.left); + } + // 头部高度变化会触发此事件 + if (this.consoleDataService?.onChange) { + this.consoleDataService.onChange(this.onHeadChange); + } + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + + if (!this.routable) { + return; + } + + this.changeLeftmenuState(); + // 从非leftMenu处跳转改变状态时,leftmenu能同步修改激活状态 + this.navigationSubscription = this.router.events.subscribe((evt: any) => { + if (evt instanceof NavigationEnd) { + const newActiveItem: TiLeftmenuItem = this.getActiveItemByUrl(this.level1Items); + if (newActiveItem && newActiveItem !== this.active) { + this.active = newActiveItem; + this.activeChange.emit(this.active); + this.isUrlChange = true; + } + } + }); + this.headElement = this.nativeElement.querySelector('.ti3-leftmenu-head'); + if (!this.headElement) { + return; // 如果头部元素ti3-leftmenu-head不存在,return停止以下克隆头部元素的操作 + } + this.panelContainer = this.nativeElement.querySelector('.ti3-leftmenu-panel-container'); + + let headElementRect: any; + const headParentnode: HTMLElement = this.renderer2.parentNode(this.headElement); + + let headEleWidth: number; + let headEleHeight: number; + this.zone.runOutsideAngular((): void => { + // 避免不停触发变化检测 + this.renderer2.listen(this.panelContainer, 'scroll', (): void => { + const scrollTop: number = this.panelContainer.scrollTop; + if (!headElementRect) { + headElementRect = this.headElement.getBoundingClientRect(); + headEleWidth = headElementRect.width; + headEleHeight = headElementRect.height; + } + if (scrollTop > 0) { + if (!this.placeholderEle) { + this.placeholderEle = this.renderer2.createElement('div'); + this.renderer2.setStyle(this.placeholderEle, 'height', `${headEleHeight}px`); + + this.renderer2.setStyle(this.headElement, 'width', `${headEleWidth}px`); + this.renderer2.insertBefore(headParentnode, this.placeholderEle, this.headElement); + + this.renderer2.addClass(this.headElement, 'ti3-leftmenu-head-clone'); + this.nativeElement.appendChild(this.headElement); + } + } else if (this.placeholderEle) { + this.renderer2.removeClass(this.headElement, 'ti3-leftmenu-head-clone'); + this.renderer2.insertBefore(headParentnode, this.headElement, this.placeholderEle); + this.renderer2.setStyle(this.headElement, 'width', '100%'); + + this.placeholderEle.parentNode.removeChild(this.placeholderEle); + this.placeholderEle = undefined; + } + }); + }); + } + + ngOnChanges(changes: SimpleChanges): void { + const activeObj: SimpleChange = changes['active']; + if (activeObj && !activeObj.firstChange) { + for (const level1Item of this.level1Items) { + level1Item.setShowChildren(); + level1Item.setActiveState(); + } + + if (this.routable && !this.isUrlChange) { + this.link(); + } + + this.isUrlChange = false; + } + // 监听到collapsed接口的值发生改变 + const collapsedObj: SimpleChange = changes['collapsed']; + if (collapsedObj && !collapsedObj.firstChange) { + this.collapsedHead(); + } + } + + ngAfterViewChecked(): void { + if (this.level1Items.length > 0 && this.routable) { + const differ: any = this.itemsDiffer.diff(this.level1Items); + if (differ) { + this.changeLeftmenuState(); + } + } + } + + ngOnDestroy(): void { + if (this.navigationSubscription) { + this.navigationSubscription.unsubscribe(); + } + if (this.consoleDataService?.offChange) { + this.consoleDataService.offChange(this.onHeadChange); + } + } + + private changeLeftMenuLeftPosition(val: number) { + if (val === 0) { + this.renderer2.addClass(this.nativeElement, 'ti3-leftmenu-panel-no-sidebar'); + } else { + this.renderer2.removeClass(this.nativeElement, 'ti3-leftmenu-panel-no-sidebar'); + } + } + + private changeLeftmenuState(): void { + // 直接输入url(刷新页面)改变状态时,leftmenu能同步修改激活状态 + const urlActiveItem: TiLeftmenuItem = this.getActiveItemByUrl(this.level1Items); + if (urlActiveItem) { + // 双向绑定数据时,对初始传入的值立即修改并传出时会报错, + // 此处参考ngModel源码setValue的处理,使用promise延后执行时序 + this.resolvedPromise.then(() => { + if (urlActiveItem !== this.active) { + this.isUrlChange = true; + this.active = urlActiveItem; + this.activeChange.emit(this.active); + } + }); + } else { + this.link(); + } + } + /** + * @ignore + * 模板中使用 + */ + public toggleClickFn(): void { + this.collapsed = !this.collapsed; + this.collapsedChange.emit(this.collapsed); + } + + // 头部元素显示和隐藏的方法 + private collapsedHead(): void { + if (this.placeholderEle) { + if (this.collapsed) { + this.renderer2.setStyle(this.headElement, 'display', 'none'); + } else { + this.renderer2.removeStyle(this.headElement, 'display'); + } + } + // leftmenu展开/收起状态完成后,再触发事件 + setTimeout(() => { + Util.trigger(document, 'tiReLayoutX'); + }, 0); + } + + private getActiveItemByUrl(arr: Array): TiLeftmenuItem { + let resultItem: TiLeftmenuItem; + for (const component of arr) { + if (this.isActived(component.item)) { + resultItem = component.item; + if (TiLeftmenuComponent.isDefaultActiveChildRoute(component.item)) { + continue; + } else { + return resultItem; + } + } + + if (component.children && component.children.length > 0) { + const result: TiLeftmenuItem = this.getActiveItemByUrl(component.children); + if (result) { + return result; + } + } + } + + return resultItem; + } + + private isActived(item: TiLeftmenuItem): boolean { + const relativeRoute: NavigationExtras = this.getRelativeRoute(item.routerExtras); + if (this.isMatchCurrentUrl(item.router, relativeRoute)) { + return true; + } + + if (Util.isArray(item.routerList) && item.routerList.length > 0) { + for (const router of item.routerList) { + if (this.isMatchCurrentUrl(router, relativeRoute)) { + return true; + } + } + } + + return false; + } + + private isMatchCurrentUrl(router: Array, relativeRoute: NavigationExtras): boolean { + if (!Util.isArray(router)) { + return false; + } + + const itemUrlTree: UrlTree = this.router.createUrlTree(router, relativeRoute); + + return this.router.isActive(itemUrlTree, false); + } + + private link(): void { + if (this.active) { + this.navigate(this.active); + } + } + /** + * @ignore + */ + public navigate(item: TiLeftmenuItem): void { + if (TiLeftmenuComponent.isDefaultActiveChildRoute(item)) { + // routeConfig 可能为null + if (!this.activeRoute.routeConfig) { + return; + } + const path: string = this.activeRoute.routeConfig.path; + const url: string = this.router.routerState.snapshot.url; + const rootUrl: string = url.split(path)[0]; + // 绝对路径跳转 + this.router.navigate([rootUrl + path], item.routerExtras); + } else { + this.router.navigate(item.router, this.getNavigationExtras(item.routerExtras)); + } + } + + /** + * @ignore + * 点击当前选中项时,触发内容区重载 + * @param item 当前点击项 + */ + public triggerReload(item: TiLeftmenuItem): void { + // 使reloadState由false变为true,配合内容区的ngIf做到内容区的重载 + this.reloadStateChange.emit(false); + setTimeout(() => { + this.reloadStateChange.emit(true); + // 如果当前路由就是item.router对应路由,则此导航不作用(路由相同); + // 如果当前路由是item.routerList中的路由,则需要跳转到item.router对应路由 + this.navigate(item); + }, 0); + } + + private getNavigationExtras(routerExtras: Object): Object { + return { relativeTo: this.activeRoute, ...routerExtras }; + } + + private getRelativeRoute(routerExtras: Object): NavigationExtras { + if (!routerExtras || !routerExtras['relativeTo']) { + return { relativeTo: this.activeRoute }; + } + + return { relativeTo: routerExtras['relativeTo'] }; + } + /** + * 得到浏览器滚动条的宽度和高度 + * 代表滚动条的方向,取值为X or Y + */ + private getScrollbarSize(direction: string): number { + const outerDiv: any = this.renderer.createElement('div'); + const innerDiv: any = this.renderer.createElement('div'); + + this.renderer.setStyle(innerDiv, 'width', '100%'); + this.renderer.setStyle(innerDiv, 'height', '100%'); + + this.renderer.appendChild(outerDiv, innerDiv); + this.tiRenderer.setStyles(outerDiv, { + position: 'absolute', + top: '-9999px', + left: '-9999px', + width: '100px', + height: '100px', + overflow: 'hidden' + }); + + this.renderer.appendChild(this.document.body, outerDiv); + + const field: string = direction === 'X' ? 'height' : 'width'; + const noScrollSize: number = typeof innerDiv.getBoundingClientRect === 'function' ? innerDiv.getBoundingClientRect()[field] : 0; + this.renderer.setStyle(outerDiv, 'overflow', 'scroll'); + const withScrollSize: number = typeof innerDiv.getBoundingClientRect === 'function' ? innerDiv.getBoundingClientRect()[field] : 0; + this.document.body.removeChild(outerDiv); + + return parseFloat((noScrollSize - withScrollSize).toFixed(1)); + } +} diff --git a/src/leftmenu/lib/src/TiLeftmenuFootComponent.ts b/src/leftmenu/lib/src/TiLeftmenuFootComponent.ts new file mode 100644 index 0000000..c624396 --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuFootComponent.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiLeftmenuComponent } from './TiLeftmenuComponent'; +import { Component, ElementRef, Renderer2, ChangeDetectionStrategy } from '@angular/core'; +@Component({ + selector: 'ti-leftmenu-foot', + templateUrl: './leftmenu-foot.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-leftmenu-foot]': 'true' + } +}) +export class TiLeftmenuFootComponent { + constructor(private renderer2: Renderer2, private elementRef: ElementRef, private tiLeftmenu: TiLeftmenuComponent) {} + + ngAfterViewInit(): void { + const templateDom: any = this.renderer2.createElement('div'); + const panelDom: any = this.tiLeftmenu.nativeElement.querySelector('.ti3-leftmenu-panel'); + const hostElement: any = this.elementRef.nativeElement; + // 修复SSR错误:ERROR ReferenceError: getComputedStyle is not defined + const spaceRight: string = + typeof getComputedStyle === 'function' ? getComputedStyle(hostElement).getPropertyValue('stroke-width') : '0'; + + this.renderer2.setStyle(templateDom, 'height', hostElement.offsetHeight + 'px'); + this.renderer2.appendChild(panelDom, templateDom); + + this.renderer2.setStyle(hostElement, 'width', `calc(100% - ${this.tiLeftmenu.scrollWidth}px)`); + this.renderer2.setStyle(hostElement, 'marginRight', `${this.tiLeftmenu.scrollWidth}px`); + this.renderer2.setStyle(hostElement, 'paddingRight', `calc(${spaceRight} - ${this.tiLeftmenu.scrollWidth}px)`); + } +} diff --git a/src/leftmenu/lib/src/TiLeftmenuGroupComponent.ts b/src/leftmenu/lib/src/TiLeftmenuGroupComponent.ts new file mode 100644 index 0000000..0535079 --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuGroupComponent.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiLeftmenuLevel1Component } from './TiLeftmenuLevel1Component'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, Renderer2 } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * TiLeftmenuGroup 是分组菜单组件,嵌在 TiLeftmenu组件中使用。 + * + * + */ +@Component({ + selector: 'ti-leftmenu-group', + templateUrl: './leftmenu-group.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLeftmenuGroupComponent extends TiBaseComponent { + /** + * @ignore + * 设置当前分组菜单项是否展开 + */ + @Input() expanded: boolean = false; + /** + * @ignore + * 当前TiLeftmenuGroup下的所有TiLeftmenuLevel1的集合 + */ + public children: Array = []; + protected versionInfo: string = super.getVersion(packageInfo); + + constructor(elementRef: ElementRef, renderer2: Renderer2, public changeRef: ChangeDetectorRef) { + super(elementRef, renderer2); + } + + ngAfterViewInit(): void { + if (this.nativeElement.parentNode.parentNode.nodeName === 'TI-LEFTMENU-LEVEL1') { + this.renderer.addClass(this.nativeElement, 'ti3-leftmenu-level2-group'); + } + } + + /** + * @ignore + * 在模板中使用,当前分组中的子项是否有激活项 + */ + public hasActivedChild(): boolean { + if (!this.children || this.children.length <= 0) { + return false; + } + + for (const level1 of this.children) { + if (level1.isActived || level1.hasActivedChildren()) { + return true; + } + } + + return false; + } +} diff --git a/src/leftmenu/lib/src/TiLeftmenuGroupItemComponent.ts b/src/leftmenu/lib/src/TiLeftmenuGroupItemComponent.ts new file mode 100644 index 0000000..3ca58ab --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuGroupItemComponent.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +/** + * TiLeftmenuGroupItem 是分组菜单文本内容组件,嵌在 TiLeftmenuGroupComponent组件中使用,其包裹的元素会作为 leftmenu 的分组菜单项内容显示 + * + * + */ +@Component({ + selector: 'ti-leftmenu-group-item', + templateUrl: './leftmenu-group-item.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLeftmenuGroupItemComponent {} diff --git a/src/leftmenu/lib/src/TiLeftmenuHeadComponent.ts b/src/leftmenu/lib/src/TiLeftmenuHeadComponent.ts new file mode 100644 index 0000000..8b4367c --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuHeadComponent.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +/** + * TiLeftmenuHead是菜单头部组件,其包裹的元素会作为leftmenu的菜单头部 + */ +@Component({ + selector: 'ti-leftmenu-head', + templateUrl: './leftmenu-head.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-leftmenu-head]': 'true' + } +}) +export class TiLeftmenuHeadComponent {} diff --git a/src/leftmenu/lib/src/TiLeftmenuItemComponent.ts b/src/leftmenu/lib/src/TiLeftmenuItemComponent.ts new file mode 100644 index 0000000..0c335f0 --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuItemComponent.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ElementRef, ChangeDetectionStrategy } from '@angular/core'; +/** + * TiLeftmenuItem 是一级菜单文本内容组件,嵌在 TiLeftmenuLevel1Component组件中使用,其包裹的元素会作为 leftmenu 的一级菜单的菜单项内容显示 + */ +@Component({ + selector: 'ti-leftmenu-item', + templateUrl: './leftmenu-item.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-leftmenu-level1-label]': 'element.textContent!==""' + } +}) +export class TiLeftmenuItemComponent { + /** + * @ignore + */ + public element: any; + constructor(elementRef: ElementRef) { + this.element = elementRef.nativeElement; + } +} diff --git a/src/leftmenu/lib/src/TiLeftmenuLevel1Component.ts b/src/leftmenu/lib/src/TiLeftmenuLevel1Component.ts new file mode 100644 index 0000000..491d7e5 --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuLevel1Component.ts @@ -0,0 +1,261 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + AfterViewInit, + ChangeDetectorRef, + ChangeDetectionStrategy, + Component, + ContentChild, + ElementRef, + Input, + OnDestroy, + OnInit, + Optional, + Renderer2, + SecurityContext +} from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; + +import { TiLeftmenuComponent, TiLeftmenuItem } from './TiLeftmenuComponent'; +import { TiLeftmenuGroupComponent } from './TiLeftmenuGroupComponent'; +import { TiLeftmenuLevel2Component } from './TiLeftmenuLevel2Component'; +import { TiLeftmenuItemComponent } from './TiLeftmenuItemComponent'; +import { Util } from '@opentiny/ng-utils'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * TiLeftmenuLevel1Component 是一级菜单组件,嵌在 TiLeftmenu 中使用 + * + */ +@Component({ + selector: 'ti-leftmenu-level1', + templateUrl: './leftmenu-level1.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-leftmenu-level1]': 'true', + '[class.ti3-leftmenu-level1-dividing]': 'item.showDividingLine' + } +}) +export class TiLeftmenuLevel1Component extends TiBaseComponent implements AfterViewInit, OnDestroy, OnInit { + /** + * 设置当前菜单项的内容信息 + * + */ + @Input() item: TiLeftmenuItem; + /** + * 是否禁用 + */ + @Input() disabled: boolean = false; + /** + * @ignore + */ + @ContentChild(TiLeftmenuItemComponent, { static: false }) + leftmenuItem: TiLeftmenuItemComponent; + /** + * @ignore + * 当前 TiLeftmenuLevel1下所有的 TiLeftmenuLevel2 的集合 + */ + public children: Array = []; + /** + * @ignore + * 模板中使用,二级菜单是否展开 + */ + public showChildren: boolean = false; + /** + * @ignore + * 模板中使用,是否是激活状态 + */ + public isActived: boolean = false; + protected versionInfo: string = super.getVersion(packageInfo); + /** + * @ignore + */ + private leftmenu: TiLeftmenuComponent; + private element: any; + private oldHref: string; + constructor( + leftmenu: TiLeftmenuComponent, + private elementRef: ElementRef, + renderer2: Renderer2, + private changeRef: ChangeDetectorRef, + @Optional() private leftmenuGroup: TiLeftmenuGroupComponent, + private domSanitizer: DomSanitizer + ) { + super(elementRef, renderer2); + this.element = this.elementRef.nativeElement; + this.leftmenu = leftmenu; + } + + ngOnInit(): void { + super.ngOnInit(); + this.oldHref = this.item?.href; + if (this.leftmenuGroup) { + this.leftmenuGroup.children.push(this); + } + } + + ngAfterViewInit(): void { + this.setShowChildren(); + this.setActiveState(); + this.leftmenu.level1Items.push(this); + this.changeRef.detectChanges(); + } + + ngDoCheck(): void { + if (this.item?.href !== this.oldHref) { + this.oldHref = this.item?.href; + this.changeRef.detectChanges(); + } + } + + ngOnDestroy(): void { + const index: number = this.leftmenu.level1Items.indexOf(this); + if (this.leftmenuGroup) { + const groupIndex: number = this.leftmenuGroup.children.indexOf(this); + if (groupIndex !== -1) { + this.leftmenuGroup.children.splice(groupIndex, 1); + } + } + + if (index !== -1) { + this.leftmenu.level1Items.splice(index, 1); + } + } + /** + * @ignore + * 模板中使用,点击一级菜单项时调用 + */ + public selectFn(): void { + if (this.disabled) { + return; + } + if (this.item.href) { + this.openHref(this.item.href); + this.element.querySelector('.ti3-leftmenu-level1-item').blur(); + + return; + } + const oldactive: TiLeftmenuItem = this.leftmenu.active; + + if (this.hasChildren()) { + this.showChildren = !this.showChildren; + this.isActived = !this.showChildren && this.hasActivedChildren(); + if (this.showChildren) { + for (const level1Items of this.leftmenu.level1Items) { + if (level1Items === this) { + continue; + } + if (level1Items.showChildren) { + level1Items.showChildren = false; + if (level1Items.hasActivedChildren()) { + level1Items.isActived = true; + } + // onpush模式下showChildren变更,视图未刷新 + level1Items.changeRef.markForCheck(); + break; + } + } + } + } else { + if (this.leftmenu.routable) { + // 点击当前已经激活的item时,刷新对应路由 + if (this.item === oldactive) { + this.leftmenu.triggerReload(this.item); + } else { + // 点击其他项需要进行跳转,来触发路由守卫,实际是否跳转取决于路由守卫返回值。 + this.leftmenu.navigate(this.item); + } + } else { + if (this.item !== oldactive) { + this.leftmenu.active = this.item; + this.leftmenu.activeChange.emit(this.item); + this.isActived = true; + } + } + } + // leftmenuGroup的模板中绑定的 hasActivedChild() 方法中用到了level1 的 isActived 值 + if (this.leftmenuGroup) { + this.leftmenuGroup.changeRef.markForCheck(); + } + } + /** + * @ignore + * 在 TiLeftmenuComponent.ts 中调用了 + */ + public setShowChildren(): void { + // 初始化时,子菜单中有当前选中状态时,显示子菜单 + this.showChildren = this.hasActivedChildren(); + this.changeRef.markForCheck(); + } + + /** + * @ignore + * 在模板上使用 + */ + public hasChildren(): boolean { + return this.children && this.children.length > 0; + } + /** + * @ignore + * 在模板上使用 + */ + public hasActivedChildren(): boolean { + if (!this.hasChildren()) { + return false; + } + + const result: Array = this.children.filter((level2: TiLeftmenuLevel2Component) => { + return level2.item === this.leftmenu.active; + }); + + return result.length > 0; + } + + /** + * @ignore + * 设置当前菜单是否处于激活状态,下边两种情况下将当前菜单设置为激活状态: + * + * 有子菜单时,当子菜单有有激活状态项并且子菜单关闭状态情况下; + * + * 没有子菜单时,当前菜单就是激活菜单项。 + */ + public setActiveState(): void { + this.isActived = (this.hasActivedChildren() && !this.showChildren) || this.item === this.leftmenu.active; + // onpush模式下,active状态视图未及时更新 + this.changeRef.markForCheck(); + // leftmenuGroup的模板中绑定的 hasActivedChild() 方法中用到了level1 的 isActived 值 + if (this.leftmenuGroup) { + this.leftmenuGroup.changeRef.markForCheck(); + } + } + + /** + * @ignore + */ + public onStateIconClick(): void { + if (!this.disabled) { + Util.trigger(this.leftmenuItem?.element, 'click'); + } + } + + /** + * @ignore + * 如果有链接,跳转新页面 + */ + public openHref(href: string): void { + const newTab: any = window.open(this.domSanitizer.sanitize(SecurityContext.URL, href), '_blank'); + // IE 下有时 newTab 不存在 + if (newTab) { + newTab.opener = null; + } + } +} diff --git a/src/leftmenu/lib/src/TiLeftmenuLevel2Component.ts b/src/leftmenu/lib/src/TiLeftmenuLevel2Component.ts new file mode 100644 index 0000000..3f23776 --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuLevel2Component.ts @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Inject, + Input, + NgZone, + OnDestroy, + OnInit, + Renderer2 +} from '@angular/core'; +import { TiLeftmenuComponent, TiLeftmenuItem } from './TiLeftmenuComponent'; +import { TiLeftmenuLevel1Component } from './TiLeftmenuLevel1Component'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { DOCUMENT } from '@angular/common'; +import packageInfo from '../package.json'; +/** + * TiLeftmenuLevel2Component 是二级菜单组件,嵌在 TiLeftmenuLevel1 中使用 + */ +@Component({ + selector: 'ti-leftmenu-level2', + templateUrl: './leftmenu-level2.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-leftmenu-level2-disabled]': 'disabled', + '[class.ti3-leftmenu-level2]': 'true', + '[class.ti3-leftmenu-level2-active]': 'this.item === this.leftmenu.active', + '[attr.tabindex]': 'disabled ? -1 : 0', + '[id]': 'appendId("leftmenu_level2")', + '(click)': 'selectFn()', + '(keyup.enter)': 'selectFn()' + } +}) +export class TiLeftmenuLevel2Component extends TiBaseComponent implements OnInit, OnDestroy, AfterViewInit { + /** + * 设置当前菜单项的内容信息 + */ + @Input() item: TiLeftmenuItem; + /** + * 是否禁用 + */ + @Input() disabled: boolean = false; + /** + * @ignore + */ + public leftmenu: TiLeftmenuComponent; + public level2Ele: any; + public renderer: any; + protected versionInfo: string = super.getVersion(packageInfo); + private oldHref: string; + private documentVisibilitychangeListener: () => void; + + constructor( + elementRef: ElementRef, + renderer2: Renderer2, + leftmenu: TiLeftmenuComponent, + private level1: TiLeftmenuLevel1Component, + private zone: NgZone, + @Inject(DOCUMENT) private document, + private changeRef: ChangeDetectorRef + ) { + super(elementRef, renderer2); + this.leftmenu = leftmenu; + this.level2Ele = elementRef; + this.renderer = renderer2; + } + + ngOnInit(): void { + super.ngOnInit(); + this.level1.children.push(this); + this.oldHref = this.item?.href; + } + ngAfterViewInit(): void { + this.zone.runOutsideAngular(() => { + let outlineColor: string; + const transparentColor: string = 'rgba(0, 0, 0, 0)'; // 透明色,跟transparent色值一致 + this.renderer.listen(this.level2Ele.nativeElement, 'mousedown', (): void => { + // 当点击的时候,不需要有outline。 + this.renderer.setStyle(this.level2Ele.nativeElement, 'outlineColor', 'transparent'); + }); + this.renderer.listen(this.level2Ele.nativeElement, 'blur', (): void => { + // blur时仅能读取到outlineColor,作为窗口切换前的状态标志。 + outlineColor = getComputedStyle(this.level2Ele.nativeElement).outlineColor; + // 恢复outline原生状态 + this.renderer.setStyle(this.level2Ele.nativeElement, 'outline', ''); + }); + this.documentVisibilitychangeListener = this.renderer.listen(this.document, 'visibilitychange', (): void => { + if ( + this.document.visibilityState === 'visible' && + outlineColor === transparentColor && + this.document.activeElement === this.level2Ele.nativeElement + ) { + this.renderer.setStyle(this.level2Ele.nativeElement, 'outlineColor', 'transparent'); + } + }); + }); + } + + ngDoCheck(): void { + if (this.item?.href !== this.oldHref) { + this.oldHref = this.item?.href; + this.changeRef.detectChanges(); + } + } + /** + * @ignore + * 模板中使用,点击二级菜单项时调用 + */ + public selectFn(): void { + if (this.disabled) { + return; + } + if (this.item.href) { + this.level1.openHref(this.item.href); + this.level2Ele.nativeElement.blur(); // 为了没有outline。 + + return; + } + + if (this.leftmenu.routable) { + // 点击当前已经激活的item时,刷新对应路由 + if (this.leftmenu.active === this.item) { + this.leftmenu.triggerReload(this.item); + } else { + // 点击其他项需要进行跳转,来触发路由守卫,实际是否跳转取决于路由守卫返回值。 + this.leftmenu.navigate(this.item); + } + } else { + if (this.leftmenu.active !== this.item) { + this.leftmenu.active = this.item; + this.leftmenu.activeChange.emit(this.item); + } + } + } + + ngOnDestroy(): void { + const index: number = this.level1.children.indexOf(this); + if (index !== -1) { + this.level1.children.splice(index, 1); + } + if (this.documentVisibilitychangeListener) { + this.documentVisibilitychangeListener(); + } + } +} diff --git a/src/leftmenu/lib/src/TiLeftmenuModule.ts b/src/leftmenu/lib/src/TiLeftmenuModule.ts new file mode 100644 index 0000000..73c1ac1 --- /dev/null +++ b/src/leftmenu/lib/src/TiLeftmenuModule.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiLeftmenuComponent } from './TiLeftmenuComponent'; +import { TiLeftmenuHeadComponent } from './TiLeftmenuHeadComponent'; +import { TiLeftmenuGroupItemComponent } from './TiLeftmenuGroupItemComponent'; +import { TiLeftmenuGroupComponent } from './TiLeftmenuGroupComponent'; +import { TiLeftmenuLevel1Component } from './TiLeftmenuLevel1Component'; +import { TiLeftmenuItemComponent } from './TiLeftmenuItemComponent'; +import { TiLeftmenuLevel2Component } from './TiLeftmenuLevel2Component'; +import { TiLeftmenuFootComponent } from './TiLeftmenuFootComponent'; +import { TiOutlineModule } from '@opentiny/ng-outline'; + +@NgModule({ + imports: [CommonModule, TiIconModule, TiRendererModule, TiOutlineModule], + exports: [ + TiLeftmenuComponent, + TiLeftmenuHeadComponent, + TiLeftmenuLevel1Component, + TiLeftmenuItemComponent, + TiLeftmenuLevel2Component, + TiLeftmenuGroupComponent, + TiLeftmenuGroupItemComponent, + TiLeftmenuFootComponent + ], + declarations: [ + TiLeftmenuComponent, + TiLeftmenuHeadComponent, + TiLeftmenuLevel1Component, + TiLeftmenuItemComponent, + TiLeftmenuLevel2Component, + TiLeftmenuGroupComponent, + TiLeftmenuGroupItemComponent, + TiLeftmenuFootComponent + ] +}) +export class TiLeftmenuModule {} +export * from './TiLeftmenuComponent'; +export { TiLeftmenuComponent, TiLeftmenuItem } from './TiLeftmenuComponent'; +export { TiLeftmenuHeadComponent } from './TiLeftmenuHeadComponent'; +export { TiLeftmenuLevel1Component } from './TiLeftmenuLevel1Component'; +export { TiLeftmenuItemComponent } from './TiLeftmenuItemComponent'; +export { TiLeftmenuLevel2Component } from './TiLeftmenuLevel2Component'; +export { TiLeftmenuGroupComponent } from './TiLeftmenuGroupComponent'; +export { TiLeftmenuGroupItemComponent } from './TiLeftmenuGroupItemComponent'; +export { TiLeftmenuFootComponent } from './TiLeftmenuFootComponent'; diff --git a/src/leftmenu/lib/src/leftmenu-foot.html b/src/leftmenu/lib/src/leftmenu-foot.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu-foot.html @@ -0,0 +1 @@ + diff --git a/src/leftmenu/lib/src/leftmenu-group-item.html b/src/leftmenu/lib/src/leftmenu-group-item.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu-group-item.html @@ -0,0 +1 @@ + diff --git a/src/leftmenu/lib/src/leftmenu-group.html b/src/leftmenu/lib/src/leftmenu-group.html new file mode 100644 index 0000000..f5a0655 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu-group.html @@ -0,0 +1,4 @@ +
    + +
    + diff --git a/src/leftmenu/lib/src/leftmenu-head.html b/src/leftmenu/lib/src/leftmenu-head.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu-head.html @@ -0,0 +1 @@ + diff --git a/src/leftmenu/lib/src/leftmenu-item.html b/src/leftmenu/lib/src/leftmenu-item.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu-item.html @@ -0,0 +1 @@ + diff --git a/src/leftmenu/lib/src/leftmenu-level1.html b/src/leftmenu/lib/src/leftmenu-level1.html new file mode 100644 index 0000000..ab1ce08 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu-level1.html @@ -0,0 +1,28 @@ +
    + + + + + +
    +
    + +
    diff --git a/src/leftmenu/lib/src/leftmenu-level2.html b/src/leftmenu/lib/src/leftmenu-level2.html new file mode 100644 index 0000000..ff39310 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu-level2.html @@ -0,0 +1,3 @@ + + + diff --git a/src/leftmenu/lib/src/leftmenu.html b/src/leftmenu/lib/src/leftmenu.html new file mode 100644 index 0000000..0cd8e62 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu.html @@ -0,0 +1,17 @@ +
    +
    + +
    +
    + +
    diff --git a/src/leftmenu/lib/src/leftmenu.less b/src/leftmenu/lib/src/leftmenu.less new file mode 100644 index 0000000..99ba898 --- /dev/null +++ b/src/leftmenu/lib/src/leftmenu.less @@ -0,0 +1,340 @@ +@import '../../../themes/basic/base-all.less'; +ti-leftmenu { + --ti-leftmenu-toggle-width: var(--ti-common-size-3x); + --ti-leftmenu-level2-left-line: 30px; // 二级菜单的左侧边线位置 + --ti-leftmenu-panel-left-padding: 2px; // 菜单容器的左右内边距 + --ti-leftmenu-width: 192px; + --ti-leftment-leftmenu-head-text-align: left; + --ti-leftmenu-angle-normal-width: calc(var(--ti-leftmenu-angle-normal-height) * 1.6); //三角按钮宽度 + --ti-leftmenu-angle-normal-height: 5px; //三角按钮高度 + --ti-leftmenu-group-padding-top: 18px; + --ti-leftmenu-group-padding-bottom: 2px; +} + +.ti3-leftmenu-container { + display: block; + position: fixed; + top: 0px; + left: 0px; + bottom: 0px; + z-index: 990; // 需要被halfmodal覆盖,halfmodal为999,halfmodal的遮罩为998 + width: var(--ti-leftmenu-width); + background-color: var(--ti-common-color-bg-white-normal); + box-shadow: var(--ti-common-shadow-1-right); + + &.ti3-leftmenu-hide { + width: 0px !important; + & .ti3-leftmenu-panel-container { + height: 0px !important; // 处理ie下面板关闭出现竖向滚动条 + } + & .ti3-leftmenu-foot { + padding: 0; + margin: 0; + } + & .ti3-leftmenu-head { + padding: 0; + margin: 0; + } + } +} + +.ti3-leftmenu-panel-no-sidebar { + left: 0 !important; +} + +.ti3-leftmenu-panel-container { + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + width: 100%; + overflow-y: auto; + overflow-x: hidden; + .box-sizing(border-box); +} + +.ti3-leftmenu-panel { + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + width: 100%; + padding: 0 var(--ti-leftmenu-panel-left-padding); + overflow-y: visible; + overflow-x: visible; + font-size: var(--ti-common-font-size-base); + color: var(--ti-common-color-text-secondary); + .box-sizing(border-box); +} + +.ti3-leftmenu-toggle { + position: absolute; + top: 50%; + width: var(--ti-leftmenu-toggle-width); + height: 80px; + background-color: var(--ti-common-color-bg-white-normal); + border-radius: 0 var(--ti-leftmenu-toggle-width) var(--ti-leftmenu-toggle-width) 0; + transform: translateY(-50%); + cursor: pointer; + &:after { + display: block; + content: ''; + position: relative; + // 小三角旋转后,中心点不变,因此以未旋转的三角形上下左右居中定位,旋转后仍在中间位置 + top: calc(50% - var(--ti-leftmenu-angle-normal-height) / 2); + left: calc(50% - var(--ti-leftmenu-angle-normal-width) / 2); + .triangle-down(var(--ti-leftmenu-angle-normal-width), var(--ti-leftmenu-angle-normal-height), var(--ti-common-color-icon-normal)); + transform: rotateZ(90deg); + } + &.ti3-leftmenu-toggle-show { + left: auto; + right: calc(-1 * var(--ti-leftmenu-toggle-width)); + } + &.ti3-leftmenu-toggle-hide { + left: 0px; + right: auto; + &:after { + transform: rotateZ(270deg); + } + } +} +.ti3-leftmenu-head { + display: block; + .box-sizing(border-box); + padding: var(--ti-common-space-5x) var(--ti-common-space-9x) var(--ti-common-space-5x) var(--ti-common-space-5x); + margin-bottom: 10px; + overflow: hidden; + font-size: var(--ti-common-font-size-1); + line-height: var(--ti-common-size-5x); + color: var(--ti-common-color-text-primary); + text-align: var(--ti-leftment-leftmenu-head-text-align); + + &:after { + display: block; + position: relative; + bottom: -20px; + content: ''; + width: 100%; + border-bottom: 1px solid var(--ti-common-color-line-dividing); + } +} +.ti3-leftmenu-head-clone { + position: absolute; + top: 0; + left: var(--ti-leftmenu-panel-left-padding); + background: var(--ti-common-color-bg-white-normal); + z-index: 3; +} + +.ti3-leftmenu-level1 { + display: block; + padding: 0; + margin: 0; +} + +.ti3-leftmenu-level1-dividing { + position: relative; + &:after { + .group-dividng(auto, 0); + } +} +.group-dividng(@top: auto, @bottom: auto) { + position: absolute; + left: var(--ti-common-space-9x); + top: @top; + bottom: @bottom; + border: 1px dashed var(--ti-common-color-line-dividing); + width: calc(100% - var(--ti-common-space-9x) - var(--ti-common-space-5x)); + content: ''; +} + +.ti3-leftmenu-level1-item { + position: relative; + cursor: pointer; + display: block; + line-height: var(--ti-common-line-height-number); + .box-sizing(border-box); + & .ti3-leftmenu-triangle.ti3-leftmenu-triangle-down { + .rotate(0); + } + & .ti3-leftmenu-triangle.ti3-leftmenu-triangle-up { + .rotate(180deg); + } + &.ti3-leftmenu-level1-active-item { + color: var(--ti-common-color-text-highlight); + } + &:not(.ti3-leftmenu-level1-disabled):hover { + background-color: var(--ti-common-color-bg-white-emphasize); + color: var(--ti-common-color-text-highlight); + + & .ti3-leftmenu-triangle.ti3-leftmenu-triangle-down { + border-top-color: var(--ti-common-color-icon-hover); + } + & .ti3-leftmenu-triangle.ti3-leftmenu-triangle-up { + border-top-color: var(--ti-common-color-icon-hover); + } + } +} + +.ti3-leftmenu-level1-active-item, +.ti3-leftmenu-level2-active { + position: relative; + & .ti3-leftmenu-triangle.ti3-leftmenu-triangle-down { + border-top-color: var(--ti-common-color-icon-hover); // 设置小三角的颜色 + } +} +// 激活菜单的左侧高亮边线 +.ti3-leftmenu-level1-active-item:before, +.ti3-leftmenu-level2-active:before { + content: ''; + position: absolute; + top: var(--ti-common-space-10); + z-index: 1; + width: 2px; + bottom: var(--ti-common-space-10); + background: var(--ti-common-color-line-active); +} +.ti3-leftmenu-level1-active-item:before { + left: var(--ti-common-space-10); +} +.ti3-leftmenu-level2-active:before { + left: var(--ti-leftmenu-level2-left-line); +} + +.ti3-leftmenu-level1-label { + padding: var(--ti-common-space-10) var(--ti-common-space-9x) var(--ti-common-space-10) var(--ti-common-space-5x); + display: block; + word-break: normal; +} + +.ti3-leftmenu-level1-state-icon { + position: absolute; + top: ~'calc(50% - 11px)'; + right: 9px; + font-size: 12px; +} + +.ti3-leftmenu-level2 { + position: relative; + display: block; + line-height: var(--ti-common-line-height-number); + padding: var(--ti-common-space-10) var(--ti-common-space-9x) var(--ti-common-space-10) var(--ti-common-space-10x); + cursor: pointer; + .box-sizing(border-box); + word-break: normal; + + // 二级菜单的左侧灰色边线 + &:after { + display: block; + content: ''; + position: absolute; + top: 0px; + bottom: 0px; + left: var(--ti-leftmenu-level2-left-line); + border-left: 1px solid var(--ti-common-color-line-dividing); + border-right: 1px solid var(--ti-common-color-line-dividing); + } + + &:not(.ti3-leftmenu-level2-disabled):hover { + background-color: var(--ti-common-color-bg-white-emphasize); + color: var(--ti-common-color-text-highlight); + } + + &.ti3-leftmenu-level2-active { + color: var(--ti-common-color-text-highlight); + } +} + +.ti3-leftmenu-level1-disabled, +.ti3-leftmenu-level2-disabled { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; +} + +.ti3-leftmenu-level2:first-of-type:after { + border-top: 1px solid var(--ti-common-color-line-dividing); + top: var(--ti-common-space-10); +} + +.ti3-leftmenu-level2:last-of-type:after { + border-bottom: 1px solid var(--ti-common-color-line-dividing); + bottom: var(--ti-common-space-10); +} + +// 向下的小三角,该组件中所有方向的小三角都是由该三角旋转生成的 +.ti3-leftmenu-triangle { + position: absolute; + right: 14px; + top: calc(50% - var(--ti-leftmenu-angle-normal-height) / 2); + content: ''; + .triangle-down(var(--ti-leftmenu-angle-normal-width), var(--ti-leftmenu-angle-normal-height), var(--ti-common-color-icon-normal)); +} + +.ti3-leftmenu-href { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + font-size: 16px; +} + +/************封装后的leftmenu样式start************/ +.ti3-leftmenu-logo { + height: 60px; + + .ti3-leftmenu-logo-ficon { + font-size: 60px; + line-height: 60px; + color: var(--ti-common-color-text-primary); + vertical-align: middle; + } +} + +.ti3-leftmenu-content { + background: #ffffff; + position: absolute; + top: 0; + bottom: 0; + left: 240px; + right: 0; + overflow: auto; +} + +.ti3-leftmenu-group-wrapper { + position: relative; + color: var(--ti-common-color-text-weaken); + line-height: var(--ti-common-line-height-number); + padding: var(--ti-leftmenu-group-padding-top) var(--ti-common-space-9x) var(--ti-leftmenu-group-padding-bottom) var(--ti-common-space-5x); +} + +.ti3-leftmenu-level2-group .ti3-leftmenu-group-wrapper { + padding-left: var(--ti-leftmenu-level2-left-line); + padding-top: var(--ti-common-space-10); +} + +.ti3-leftmenu-foot { + position: absolute; + bottom: 0; + text-align: var(--ti-leftment-leftmenu-head-text-align); + padding: var(--ti-common-space-10) 0 var(--ti-common-space-10) var(--ti-common-space-5x); + stroke-width: var(--ti-common-space-9x); // 在 TS 中读取。 + .box-sizing(border-box); + color: var(--ti-common-color-text-link); + overflow: hidden; + background: var(--ti-common-color-bg-white-normal); + z-index: 3; + line-height: var(--ti-common-line-height-number); + &:hover { + cursor: pointer; + color: var(--ti-common-color-text-link-hover); + } + &:before { + position: relative; + top: calc(-1 * var(--ti-common-space-10)); + left: 0; + content: ''; + border-bottom: 1px solid var(--ti-common-color-line-dividing); + display: block; + } +} +/************封装后的leftmenu样式end************/ diff --git a/src/linkbutton/demo/karma.conf.js b/src/linkbutton/demo/karma.conf.js new file mode 100644 index 0000000..22d9d1a --- /dev/null +++ b/src/linkbutton/demo/karma.conf.js @@ -0,0 +1,41 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [{ type: 'html' }, { type: 'text-summary' }] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/linkbutton/demo/project.json b/src/linkbutton/demo/project.json new file mode 100644 index 0000000..1629e29 --- /dev/null +++ b/src/linkbutton/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/linkbutton/demo", + "sourceRoot": "src/linkbutton/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/linkbutton", + "index": "src/linkbutton/demo/src/index.html", + "main": "src/linkbutton/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/linkbutton/demo/tsconfig.app.json", + "assets": ["src/linkbutton/demo/src/favicon.ico", "src/linkbutton/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "linkbutton-demo:build:production" + }, + "development": { + "browserTarget": "linkbutton-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js linkbutton" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/linkbutton/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/linkbutton/demo/tsconfig.spec.json", + "karmaConfig": "src/linkbutton/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/linkbutton/demo/src/app/AppComponent.ts b/src/linkbutton/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/linkbutton/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/linkbutton/demo/src/app/AppModule.ts b/src/linkbutton/demo/src/app/AppModule.ts new file mode 100644 index 0000000..951afe7 --- /dev/null +++ b/src/linkbutton/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { LinkbuttonTestModule } from './linkbutton/LinkbuttonTestModule'; + +@NgModule({ + imports: [ + LinkbuttonTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/linkbutton/demo/src/app/IndexComponent.ts b/src/linkbutton/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..b802109 --- /dev/null +++ b/src/linkbutton/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { LinkbuttonTestModule } from './linkbutton/LinkbuttonTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = LinkbuttonTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/linkbutton/demo/src/app/app.html b/src/linkbutton/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/linkbutton/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/linkbutton/demo/src/app/linkbutton/LinkbuttonBasicComponent.ts b/src/linkbutton/demo/src/app/linkbutton/LinkbuttonBasicComponent.ts new file mode 100644 index 0000000..036a1c2 --- /dev/null +++ b/src/linkbutton/demo/src/app/linkbutton/LinkbuttonBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './linkbutton-basic.html' +}) +export class LinkbuttonBasicComponent { + iconName: string = 'sold-out'; +} diff --git a/src/linkbutton/demo/src/app/linkbutton/LinkbuttonTestModule.ts b/src/linkbutton/demo/src/app/linkbutton/LinkbuttonTestModule.ts new file mode 100644 index 0000000..1243423 --- /dev/null +++ b/src/linkbutton/demo/src/app/linkbutton/LinkbuttonTestModule.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { TiLinkbuttonModule } from '@opentiny/ng'; + +import { LinkbuttonBasicComponent } from './LinkbuttonBasicComponent'; +@NgModule({ + imports: [CommonModule, TiLinkbuttonModule, RouterModule.forChild(LinkbuttonTestModule.ROUTES)], + declarations: [LinkbuttonBasicComponent] +}) +export class LinkbuttonTestModule { + public static readonly ROUTES: Routes = [{ path: 'linkbutton/linkbutton-basic', component: LinkbuttonBasicComponent }]; +} diff --git a/src/linkbutton/demo/src/app/linkbutton/linkbutton-basic.html b/src/linkbutton/demo/src/app/linkbutton/linkbutton-basic.html new file mode 100644 index 0000000..27d0e24 --- /dev/null +++ b/src/linkbutton/demo/src/app/linkbutton/linkbutton-basic.html @@ -0,0 +1 @@ +购买 diff --git a/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton-demos.js b/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton-demos.js new file mode 100644 index 0000000..357564a --- /dev/null +++ b/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton-demos.js @@ -0,0 +1,25 @@ +export default { + column: '2', + demos: [ + { + demoId: 'linkbutton-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    Linkbutton组件的最简用法。

    ', + 'en-US': '

    Basic usage

    ' + }, + apis: ['TiLinkbuttonComponent.properties.iconName'] + } + ], + ignoreApis: [ + 'TiLinkbuttonComponent.properties.disabled', + 'TiLinkbuttonComponent.properties.tabindex', + 'TiLinkbuttonComponent.events.blur', + 'TiLinkbuttonComponent.events.focus', + 'TiLinkbuttonComponent.methods.blur', + 'TiLinkbuttonComponent.methods.focus' + ] +}; diff --git a/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.cn.md b/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.cn.md new file mode 100644 index 0000000..b918a81 --- /dev/null +++ b/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.cn.md @@ -0,0 +1,24 @@ +--- +title: Linkbutton 链接按钮 +--- +# Linkbutton 链接按钮 + +
    + +Linkbutton 链接按钮组件。   + +```typescript +import { TiLinkbuttonModule } from '@opentiny/ng'; +``` + +
    + +
    + +Linkbutton 链接按钮组件。   + +```typescript +import { TiLinkbuttonModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.en.md b/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/linkbutton/demo/src/app/linkbutton/webdoc/linkbutton.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/linkbutton/demo/src/favicon.ico b/src/linkbutton/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/linkbutton/demo/src/index.html b/src/linkbutton/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/linkbutton/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/linkbutton/demo/src/main.ts b/src/linkbutton/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/linkbutton/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/linkbutton/demo/test.ts b/src/linkbutton/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/linkbutton/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/linkbutton/demo/tsconfig.app.json b/src/linkbutton/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/linkbutton/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/linkbutton/demo/tsconfig.spec.json b/src/linkbutton/demo/tsconfig.spec.json new file mode 100644 index 0000000..e12f108 --- /dev/null +++ b/src/linkbutton/demo/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": ["jasmine"] + }, + "files": ["./test.ts", "../../polyfills.ts"], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/linkbutton/lib/index.ts b/src/linkbutton/lib/index.ts new file mode 100644 index 0000000..6a5b61d --- /dev/null +++ b/src/linkbutton/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiLinkbuttonModule'; diff --git a/src/linkbutton/lib/ng-package.json b/src/linkbutton/lib/ng-package.json new file mode 100644 index 0000000..51cc62e --- /dev/null +++ b/src/linkbutton/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/linkbutton", + "lib": { + "entryFile": "index.ts" + } +} diff --git a/src/linkbutton/lib/package.json b/src/linkbutton/lib/package.json new file mode 100644 index 0000000..e119863 --- /dev/null +++ b/src/linkbutton/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-linkbutton", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/linkbutton/lib/project.json b/src/linkbutton/lib/project.json new file mode 100644 index 0000000..0cf7638 --- /dev/null +++ b/src/linkbutton/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/linkbutton/lib", + "sourceRoot": "src/linkbutton/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/linkbutton"], + "options": { + "project": "src/linkbutton/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/linkbutton"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js linkbutton" + }, + { + "command": "ng default-build linkbutton" + }, + { + "command": "node build/clear-default-theme.js linkbutton" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/linkbutton && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build linkbutton && ng pack linkbutton && node build/publish.js linkbutton --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/linkbutton/lib/src/TiLinkbuttonComponent.ts b/src/linkbutton/lib/src/TiLinkbuttonComponent.ts new file mode 100644 index 0000000..87760a8 --- /dev/null +++ b/src/linkbutton/lib/src/TiLinkbuttonComponent.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; + +/** + * 链接按钮组件 linkbutton + */ +@Component({ + selector: '[tiLinkbutton]', + templateUrl: './linkbutton.html', + styleUrls: ['./linkbutton.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLinkbuttonComponent extends TiFormComponent { + /** + * icon名称 + */ + @Input() iconName: string; +} diff --git a/src/linkbutton/lib/src/TiLinkbuttonModule.ts b/src/linkbutton/lib/src/TiLinkbuttonModule.ts new file mode 100644 index 0000000..01eadc0 --- /dev/null +++ b/src/linkbutton/lib/src/TiLinkbuttonModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiLinkbuttonComponent } from './TiLinkbuttonComponent'; +import { TiIconModule } from '@opentiny/ng-icon'; + +@NgModule({ + imports: [CommonModule, TiIconModule], + exports: [TiLinkbuttonComponent], + declarations: [TiLinkbuttonComponent] +}) +export class TiLinkbuttonModule {} +export { TiLinkbuttonComponent } from './TiLinkbuttonComponent'; diff --git a/src/linkbutton/lib/src/linkbutton.html b/src/linkbutton/lib/src/linkbutton.html new file mode 100644 index 0000000..01ea778 --- /dev/null +++ b/src/linkbutton/lib/src/linkbutton.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/linkbutton/lib/src/linkbutton.less b/src/linkbutton/lib/src/linkbutton.less new file mode 100644 index 0000000..785add2 --- /dev/null +++ b/src/linkbutton/lib/src/linkbutton.less @@ -0,0 +1,33 @@ +@import '../../../themes/basic/base-all.less'; + +::ng-deep :root { + --ti-link-btn-height: 24px; +} + +:host { + display: inline-block; + text-decoration: none; + height: var(--ti-link-btn-height); + line-height: calc(var(--ti-link-btn-height) - 2px); // border上下2px + font-size: var(--ti-common-font-size-base); + color: var(--ti-common-color-text-secondary); + .box-sizing(border-box); + border: 1px solid var(--ti-common-color-line-normal); + padding: 0 var(--ti-common-space-10); + + &:link, + &:active { + text-decoration: none; + color: var(--ti-common-color-text-secondary); + } + &:hover { + cursor: pointer; + text-decoration: none; + border-color: var(--ti-common-color-line-active); + color: var(--ti-common-color-icon-hover); + } +} + +.ti-link-btn-icon { + margin-right: var(--ti-common-space-base); +} diff --git a/src/list/demo/project.json b/src/list/demo/project.json new file mode 100644 index 0000000..a50e9e1 --- /dev/null +++ b/src/list/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/list/demo", + "sourceRoot": "src/list/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/list", + "index": "src/list/demo/src/index.html", + "main": "src/list/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/list/demo/tsconfig.app.json", + "assets": ["src/list/demo/src/favicon.ico", "src/list/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "list-demo:build:production" + }, + "development": { + "browserTarget": "list-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js list" + } + ] + } + } + } +} diff --git a/src/list/demo/src/app/AppComponent.ts b/src/list/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/list/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/list/demo/src/app/AppModule.ts b/src/list/demo/src/app/AppModule.ts new file mode 100644 index 0000000..bd9e4aa --- /dev/null +++ b/src/list/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ListTestModule } from './list/ListTestModule'; + +@NgModule({ + imports: [ + ListTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/list/demo/src/app/IndexComponent.ts b/src/list/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..4b9f3a4 --- /dev/null +++ b/src/list/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ListTestModule } from './list/ListTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = ListTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/list/demo/src/app/app.html b/src/list/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/list/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/list/demo/src/app/list/ListDefaultComponent.ts b/src/list/demo/src/app/list/ListDefaultComponent.ts new file mode 100644 index 0000000..fc52837 --- /dev/null +++ b/src/list/demo/src/app/list/ListDefaultComponent.ts @@ -0,0 +1,32 @@ +import { Component, HostBinding } from '@angular/core'; + +@Component({ + templateUrl: './list-default.html' +}) +export class ListDefaultComponent { + options: Array = []; + selected: any; + options1: Array = []; + selected1: any; + constructor() { + for (let i: number = 0; i < 20; i++) { + const option: any = { + id: String(i), + label: `很长很长很长很长很长很长很长很长很长很长很长很长myemail${i}@example.com很长很长`, + pic: `pic${i}.png'`, + disabled: i % 2 === 0 + }; + this.options.push(option); + } + this.selected = this.options[0]; + for (let i: number = 0; i < 20000; i++) { + const option: any = { + id: String(i), + label: `myemail${i}@example.com很长很长`, + pic: `pic${i}.png'`, + disabled: i % 2 === 0 + }; + this.options1.push(option); + } + } +} diff --git a/src/list/demo/src/app/list/ListGroupComponent.ts b/src/list/demo/src/app/list/ListGroupComponent.ts new file mode 100644 index 0000000..855418a --- /dev/null +++ b/src/list/demo/src/app/list/ListGroupComponent.ts @@ -0,0 +1,92 @@ +import { Component, HostBinding } from '@angular/core'; + +@Component({ + templateUrl: './list-group.html' +}) +export class ListGroupComponent { + options: Array = [ + { + id: 'group1', + label: 'Group1', + children: [ + { + id: '11', + label: '1111' + }, + { + id: '12', + label: '2222' + } + ] + }, + { + id: 'group2', + label: 'Group2', + children: [ + { + id: '21', + label: '3333ds' + }, + { + id: '22', + label: '4444' + } + ] + }, + { + id: 'group3', + label: 'Group3', + children: [ + { + id: '31', + label: '65476543' + }, + { + id: '32', + label: '231356' + }, + { + id: '33', + label: '23were 1356' + } + ] + }, + { + id: 'group4', + label: 'Group4', + children: [ + { + id: '41', + label: '刷卡了U的防护哈哈' + }, + { + id: '42', + label: '手动if玩儿' + }, + { + id: '43', + label: '2如价格的开发' + } + ] + }, + { + id: 'group5', + label: 'Group5', + children: [ + { + id: '51', + label: '问我我李开复路口IE我让' + }, + { + id: '52', + label: '我欧文UR了我二' + }, + { + id: '53', + label: '未入库虑多敷第三方了' + } + ] + } + ]; + selected: any = this.options[0].children[0]; +} diff --git a/src/list/demo/src/app/list/ListMultiComponent.ts b/src/list/demo/src/app/list/ListMultiComponent.ts new file mode 100644 index 0000000..8650b3a --- /dev/null +++ b/src/list/demo/src/app/list/ListMultiComponent.ts @@ -0,0 +1,32 @@ +import { Component, HostBinding } from '@angular/core'; + +@Component({ + templateUrl: './list-multi.html' +}) +export class ListMultiComponent { + options: Array = []; + options1: Array = []; + selected: any; + selected1: any; + constructor() { + for (let i: number = 0; i < 20; i++) { + const option: any = { + id: String(i), + label: `很长很长很长很长很长很长很长很长很长很长很长很长myemail${i}@example.com很长很长`, + pic: `pic${i}.png'`, + disabled: i % 2 === 0 + }; + this.options.push(option); + } + for (let i: number = 0; i < 20000; i++) { + const option: any = { + id: String(i), + label: `myemail${i}@example.com很长很长`, + pic: `pic${i}.png'`, + disabled: i % 6 === 0 + }; + this.options1.push(option); + } + this.selected = [this.options[0]]; + } +} diff --git a/src/list/demo/src/app/list/ListSelectallComponent.ts b/src/list/demo/src/app/list/ListSelectallComponent.ts new file mode 100644 index 0000000..e2233b1 --- /dev/null +++ b/src/list/demo/src/app/list/ListSelectallComponent.ts @@ -0,0 +1,181 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './list-selectall.html' +}) +export class ListSelectallComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + myOptions1: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil', + disabled: true + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: true + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey', + disabled: true + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + myOptions2: Array = [ + { + label: '北美洲', + children: [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + } + ] + }, + { + label: '南美洲', + children: [ + { + label: '巴西', + englishname: 'Brazil', + disabled: false + } + ] + }, + { + label: '亚洲', + children: [ + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + } + ] + }, + { + label: '欧洲', + children: [ + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ] + } + ]; + myOptions3: Array = []; + + mySelecteds1: any = [this.myOptions[3]]; + mySelecteds2: any = [this.myOptions1[1], this.myOptions1[7]]; + mySelecteds3: any = [this.myOptions2[0].children[0], this.myOptions2[3].children[3]]; + mySelecteds4: any; + ngOnInit(): void { + for (let i: number = 0; i < 1000; i++) { + const option: any = { + id: String(i), + label: 'label_' + i, + pic: `pic${i}.png` + }; + this.myOptions3.push(option); + } + } +} diff --git a/src/list/demo/src/app/list/ListTestModule.ts b/src/list/demo/src/app/list/ListTestModule.ts new file mode 100644 index 0000000..fd7c7ad --- /dev/null +++ b/src/list/demo/src/app/list/ListTestModule.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { TiListModule } from '@opentiny/ng'; + +import { ListGroupComponent } from './ListGroupComponent'; +import { ListMultiComponent } from './ListMultiComponent'; +import { ListTipComponent } from './ListTipComponent'; +import { ListSelectallComponent } from './ListSelectallComponent'; +import { ListDefaultComponent } from './ListDefaultComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiListModule, RouterModule.forChild(ListTestModule.ROUTES)], + declarations: [ListDefaultComponent, ListGroupComponent, ListMultiComponent, ListTipComponent, ListSelectallComponent] +}) +export class ListTestModule { + static readonly LINKS: Array = [{ href: 'components/TiListComponent.html', label: 'List' }]; + static readonly ROUTES: Routes = [ + { path: 'list', component: ListDefaultComponent }, + { path: 'list/group', component: ListGroupComponent }, + { path: 'list/multi', component: ListMultiComponent }, + { path: 'list/tip', component: ListTipComponent }, + { path: 'list/selectall', component: ListSelectallComponent } + ]; +} diff --git a/src/list/demo/src/app/list/ListTipComponent.ts b/src/list/demo/src/app/list/ListTipComponent.ts new file mode 100644 index 0000000..1d47925 --- /dev/null +++ b/src/list/demo/src/app/list/ListTipComponent.ts @@ -0,0 +1,21 @@ +import { Component, HostBinding } from '@angular/core'; + +@Component({ + templateUrl: './list-tip.html' +}) +export class ListTipComponent { + options: Array = []; + selected: any; + constructor() { + for (let i: number = 0; i < 20; i++) { + const option: any = { + id: String(i), + label: `很长很长很长很长很长很长很长很长很长很长很长很长myemail${i}@example.com很长很长`, + pic: `pic${i}.png'`, + disabled: i % 2 === 0 + }; + this.options.push(option); + } + this.selected = this.options[0]; + } +} diff --git a/src/list/demo/src/app/list/list-default.html b/src/list/demo/src/app/list/list-default.html new file mode 100644 index 0000000..d65258a --- /dev/null +++ b/src/list/demo/src/app/list/list-default.html @@ -0,0 +1,46 @@ + +

    Select组件数据双向绑定展示区域

    +
    +
    + 1.用例1的当前选中项为:
    {{selected | json}}
    +

    + 2.用例2的当前选中项为:
    {{selected | json}}
    +
    +
    +

    + + + +
    +
    + + +
    +
    + +
    + + + {{i}}+{{item.pic}}+{{item.label}} + + +
    + +
    + +
    + + +
    + +
    +


    + + +


    原生Select参考: + diff --git a/src/list/demo/src/app/list/list-group.html b/src/list/demo/src/app/list/list-group.html new file mode 100644 index 0000000..e2ea6c5 --- /dev/null +++ b/src/list/demo/src/app/list/list-group.html @@ -0,0 +1,35 @@ + +

    Select组件 Group数据

    +

    Select组件数据双向绑定展示区域

    +
    +
    + 1.用例1的当前选中项为:
    {{selected | json}}
    +

    + 2.用例2的当前选中项为:
    {{selected | json}}
    +
    +
    +

    + + + +
    + +
    + +
    + + + {{i}}+{{item.pic}}+{{item.label}} + + +
    +


    + + +


    原生Select参考: + diff --git a/src/list/demo/src/app/list/list-multi.html b/src/list/demo/src/app/list/list-multi.html new file mode 100644 index 0000000..0451122 --- /dev/null +++ b/src/list/demo/src/app/list/list-multi.html @@ -0,0 +1,64 @@ + +

    Select组件数据双向绑定展示区域

    +
    +
    + 1.用例1的当前选中项为:
    {{selected | json}}
    +

    + 2.用例2的当前选中项为:
    {{selected | json}}
    +
    +
    +

    + + + +
    +
    + + +
    +
    + +
    + + + {{i}}+{{item.pic}}+{{item.label}} + + +
    + +
    + + +
    + + +
    + + +
    +


    + + +


    原生Select参考: + diff --git a/src/list/demo/src/app/list/list-selectall.html b/src/list/demo/src/app/list/list-selectall.html new file mode 100644 index 0000000..f72aa7d --- /dev/null +++ b/src/list/demo/src/app/list/list-selectall.html @@ -0,0 +1,40 @@ +

    描述

    +

    List组件默认没有全选功能,当多选时设置 selectAll 接口为 true 时开启全选功能。

    +

    示例

    + +

    1.基本:

    +
    + +
    +

    选中项:{{mySelecteds1 | json}}

    +
    + +

    2.选项中有禁用的:

    +
    + +
    +

    选中项:{{mySelecteds2 | json}}

    +
    + +

    3.分组:

    +
    + +
    +

    选中项:{{mySelecteds3 | json}}

    +
    + +

    4.虚拟滚动:

    +
    + + +
    +

    选中项:{{mySelecteds4 | json}}

    +
    diff --git a/src/list/demo/src/app/list/list-tip.html b/src/list/demo/src/app/list/list-tip.html new file mode 100644 index 0000000..9ceb6a8 --- /dev/null +++ b/src/list/demo/src/app/list/list-tip.html @@ -0,0 +1,41 @@ + +

    Select组件数据双向绑定展示区域

    +
    +
    + 1.用例1的当前选中项为:
    {{selected | json}}
    +

    + 2.用例2的当前选中项为:
    {{selected | json}}
    +
    +
    +

    + + + +
    +
    + + +
    +
    + +
    + + + {{i}}+{{item.pic}}+{{item.label}} + + +
    + +
    + +
    +


    + + +


    原生Select参考: + diff --git a/src/list/demo/src/favicon.ico b/src/list/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/list/demo/src/index.html b/src/list/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/list/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/list/demo/src/main.ts b/src/list/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/list/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/list/demo/tsconfig.app.json b/src/list/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/list/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/list/lib/index.ts b/src/list/lib/index.ts new file mode 100644 index 0000000..12d5fbf --- /dev/null +++ b/src/list/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiListModule'; diff --git a/src/list/lib/ng-package.json b/src/list/lib/ng-package.json new file mode 100644 index 0000000..f48833c --- /dev/null +++ b/src/list/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/list", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/list/lib/package.json b/src/list/lib/package.json new file mode 100644 index 0000000..f59bd12 --- /dev/null +++ b/src/list/lib/package.json @@ -0,0 +1,16 @@ +{ + "name": "@opentiny/ng-list", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/cdk": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-loading": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/list/lib/project.json b/src/list/lib/project.json new file mode 100644 index 0000000..aec2ef0 --- /dev/null +++ b/src/list/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/list/lib", + "sourceRoot": "src/list/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/list"], + "options": { + "project": "src/list/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/list"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js list" + }, + { + "command": "ng default-build list" + }, + { + "command": "node build/clear-default-theme.js list" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/list && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build list && ng pack list && node build/publish.js list --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/list/lib/src/TiListComponent.ts b/src/list/lib/src/TiListComponent.ts new file mode 100644 index 0000000..ba7865b --- /dev/null +++ b/src/list/lib/src/TiListComponent.ts @@ -0,0 +1,764 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ContentChild, + ElementRef, + EventEmitter, + HostListener, + Input, + NgZone, + Output, + Renderer2, + SimpleChange, + SimpleChanges, + TemplateRef, + ViewChild, + ChangeDetectorRef, + ChangeDetectionStrategy +} from '@angular/core'; +import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { Subscription } from 'rxjs'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiBrowser, TiKeymap, TiPositionType } from '@opentiny/ng-utils'; +import { TiLocale } from '@opentiny/ng-locale'; // 获取词条 +import packageInfo from '../package.json'; + +/** + * scrollToBottom 事件回调参数 + * + */ +export interface TiListScrollLoad { + /** + * 是否正在加载数据 + */ + loading?: boolean; + /** + * scroll 事件的 event 参数 + */ + event?: Event; +} + +/** + * @ignore + * 列表组件,使用者有menu、droplist + */ +@Component({ + selector: 'ti-list', + templateUrl: './list.html', + styleUrls: ['./list.less', './list-multi.less'], + providers: [TiFormComponent.getValueAccessor(TiListComponent)], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-virtual-scroll-list]': 'virtual' + } +}) +export class TiListComponent extends TiFormComponent { + public static readonly OPTION_DEFAULT_HEIGHT: number = 30; // 每项的默认高度, 单位是px + public static readonly CONTAINER_VERTICAL_PADDING: number = 8; // list容器(出滚动条的容器)的上下padding总和值(上下各4px) + protected static readonly SCROLL_TOP_TIME: number = 200; // 设置选中项滚动到TOP所需时间。Chorme下简单测试。TODO:其他浏览器、复杂数据 + + // 列表数据配置 + @Input() multiple: boolean = false; // 是否多选 + @Input() options: Array; // 下拉选项的全部数据 + @Input() labelKey: string = 'label'; // 要显示的字段 + @Input() tipPosition: TiPositionType = 'right'; + @Input() noDataText: string; // 无数据时的显示文本 + @Input() hideEmptyOption: boolean = false; // 过滤空字符串选项 + @Input() selectAll: boolean = false; // 多选模式下是否有全选功能 + /** + * 用于配置是否显示children字段数据,大部分场景需要显示children,cascader不需要 + */ + @Input() showChildren: boolean = true; + /** + * @ignore + * 适配内部time类组件,点选滚动效果,暂不开放。 + */ + @Input() scrollTop: number = 0; + /** + * 是否开启虚拟滚动, 默认值false + */ + @Input() virtual: boolean = false; + /** + * @ignore + * TODO: 暂不对外开放该接口,后续根据使用场景进行优化 + * 当开启虚拟滚动时,可配置单条选项的高度(单位是px), 默认值30 + */ + @Input() itemSize: number = TiListComponent.OPTION_DEFAULT_HEIGHT; + + @Input() tipMaxWidth: string; + /** + * @ignore + * + * + * idKey指定的属性的值相等时即认为 option 选项是选中的。选中项 ngModel 中的数据(modelOption对象)跟 options 数据集中的选项(option对象)之间对应相等关系的依据属性。当 + * modelOption中的 idKey 设置的属性的值 与 option 中的 idKey 设置的属性的值相等时,则认为 modelOption 和 option 是对应的相等关系,即认为 option 选项是选中的。 + * + * 默认当 modelOption === option 或者 modelOption 中的 labelKey 设置的属性的值 与 option 中的 labelKey 设置的属性的值相等时,则认为 option 选项是选中的。 + * + * 该接口暂不对外开放,后续如果业务场景labelKey对应的值确实有重复时,再对外开放该接口。 + */ + @Input() idKey: string; + + /** + * 选中事件,向外通知option数据 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + @Output() readonly hover: EventEmitter = new EventEmitter(); + /** + * 下拉列表滚动到底部的回调 + * + * + */ + @Output() readonly scrollToBottom: EventEmitter = new EventEmitter(); + /** + * 鼠标移入选项的回调,与hover不同,hover是hoverOption改变的回调 + */ + @Output() readonly optionMouseenter: EventEmitter = new EventEmitter(); + // item模板 + @ContentChild(TemplateRef, { static: true }) itemTemplate: TemplateRef; + @ViewChild('ulRef', { static: false }) ulRef: ElementRef; + @ViewChild(CdkVirtualScrollViewport, { static: false }) + virtualScrollViewport: CdkVirtualScrollViewport; + + /** + * 需要阻止失焦标记: 点击面板时,会触发失焦,需要阻止失焦。 + * IE兼容性问题: 点击滚动条,触发失焦,面板会收起 + * 修复方案:点击list面板区域(包含滚动条)后,给失焦元素强制获取焦点。修改Form基类不触发组件整体失焦。 + */ + private stopBlur: boolean = false; + private unlistenIEScrollbarBugFns: Array<() => void> = []; + public selectAllState: boolean; + public selectAllText: string = TiLocale.getLocaleWords().tiList.selectAll; + public optionSelectAll: string = 'all'; + private scrollEventSub: Subscription; + public scrollLoadInfo: TiListScrollLoad = { loading: false }; + protected versionInfo: string = super.getVersion(packageInfo); + + private _hoverOption: any; + public get hoverOption(): any { + return this._hoverOption; + } + public set hoverOption(hoverOption: any) { + if (this._hoverOption !== hoverOption) { + this._hoverOption = hoverOption; + // onpush策略 异步获取数据更改hoverOption,需要手动更新视图 + this.changeDetectorRef.markForCheck(); + this.hover.emit(hoverOption); + } + } + protected optionScrollTopLastTime: number = 0; + // 判断是否禁用的函数: 函数类型是(any) => boolean,赋值是(item: any) => {} + @Input() isDisabledFn: (item: any) => boolean = (item: any) => { + return item && item.disabled === true; + }; // 这个分号是书写正确的 + constructor(protected hostRef: ElementRef, protected renderer: Renderer2, protected cd: ChangeDetectorRef, protected zone: NgZone) { + super(hostRef, renderer, cd); + } + + // 在Selected使用List时,Selected的聚焦元素是Dominator和Searchbox,它们接收按键,并调用list来处理。 + // 在表格列设置时,List自己聚焦,List处理按键。 + @HostListener('keydown', ['$event']) onKeydown(event: KeyboardEvent): void { + // 10.0.2删除 KEY_SPACE 空格快捷键的响应 + switch (event.keyCode) { + case TiKeymap.KEY_ENTER: // ENTER键 相当于click + case TiKeymap.KEY_NUMPAD_ENTER: // ENTER键(苹果数字小键盘) + if (this.hoverOption) { + this.selectOption(this.hoverOption); + } + break; + case TiKeymap.KEY_ESCAPE: // Esc键,仅可关闭 + case TiKeymap.KEY_TAB: // Tab键,仅可关闭 + break; + case TiKeymap.KEY_ARROW_UP: // 向上箭头,上移选项 + event.preventDefault(); // 防止上下按键默认行为:移动滚动条 + this.nextOption(true); + break; + case TiKeymap.KEY_ARROW_DOWN: // 向下箭头,下移选项 + event.preventDefault(); // 防止上下按键默认行为:移动滚动条 + this.nextOption(false); + break; + default: + break; + } + } + + ngOnInit(): void { + this.zone.runOutsideAngular(() => { + this.renderer.listen(this.nativeElement, 'mousedown', (event: MouseEvent): void => { + // Select时,点击滚动条和点击面板,不要触发默认行为:dominator blur + // 现在,list默认不聚焦。在所有场合,点击面板或者滚动条,都不触发默认焦点事件。 + event.preventDefault(); // 防止dominator blur行为 + // list自身可以落焦点时,需要聚焦的默认行为。 + }); + + // list内部滚动条会引起外部滚动条事件触发,引起弹框内的下拉组件无法使用鼠标拖动滚动条,因此此处阻止事件冒泡 + this.renderer.listen(this.nativeElement, 'mousewheel', (event: MouseEvent): void => { + event.stopPropagation(); + }); + this.renderer.listen(this.nativeElement, 'DOMMouseScroll', (event: MouseEvent): void => { + event.stopPropagation(); + }); + }); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + if (this.multiple && this.selectAll) { + const optionsChange: SimpleChange = changes['options']; + if (optionsChange) { + this.setSelectedAllState(); + } + } + // 全选时,selectAll改变且当前值为true,更新全选框状态 + const selectAllChange: SimpleChange = changes['selectAll']; + if (this.multiple && selectAllChange && selectAllChange.currentValue) { + this.setSelectedAllState(); + } + // time及datetime/datetimeRange组件使用list,点选时候需要滚动效果 + if (changes['scrollTop'] && !changes['scrollTop'].firstChange) { + this.setScrollFn(); + } + if (changes['options']) { + this.setHoverOption(); + } + } + + ngAfterViewInit(): void { + if (this.scrollTop > 0) { + this.setScrollFn(); + } + if (this.scrollToBottom.observers.length > 0 && this.virtualScrollViewport) { + this.scrollEventSub = this.virtualScrollViewport.elementScrolled().subscribe((event: Event): void => { + if ((event.target as any).scrollTop + (event.target as any).clientHeight >= (event.target as any).scrollHeight) { + this.scrollLoadInfo.event = event; + this.zone.run(() => { + this.scrollToBottom.emit(this.scrollLoadInfo); + }); + } + }); + } + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + // 解除IE滚动条Bug的监听 + this.unlistenIESrollbarBug(); + if (this.scrollEventSub) { + this.scrollEventSub.unsubscribe(); + } + } + + writeValue(model: any): void { + super.writeValue(model); + if (this.multiple && this.selectAll) { + this.setSelectedAllState(); + } + } + // time及datetime/datetimeRange组件使用list,点选时候需要滚动效果 + private setScrollFn(): void { + if (TiBrowser.isIE()) { + this.renderer.setProperty(this.hostRef.nativeElement, 'scrollTop', this.scrollTop); + return; + } + this.hostRef.nativeElement.scrollTo(0, this.scrollTop); + } + + // 下拉选项的鼠标按下操作 + public onMousedownItem(event: MouseEvent, own: HTMLElement): void { + event.preventDefault(); + event.stopPropagation(); + own.classList.add('ti3-menu-panel-list-active'); + } + + // 下拉选项的鼠标按下后的抬起操作 + public onMouseupItem(event: MouseEvent, own: HTMLElement): void { + this.removeClass(event, own); + } + + // 下拉选项的鼠标离开时的操作 + public onMouseleaveItem(event: MouseEvent, own: HTMLElement): void { + this.removeClass(event, own); + } + // 移除li元素上的ti3-menu-panel-list-active类 + public removeClass(event: MouseEvent, own: HTMLElement): void { + event.preventDefault(); + event.stopPropagation(); + own.classList.remove('ti3-menu-panel-list-active'); + } + + // 下拉选项的单击操作(包括组名所在的
  • 元素) + public onClickItem(event: MouseEvent, option: any): void { + // Selected时,点击选项,不要触发默认行为:dominator blur。 + event.preventDefault(); // 防止dominator blur行为 + // Group节点和禁用节点,无需响应。 + if (this.isGroup(option) || this.isDisabledFn(option)) { + event.stopPropagation(); + + return; + } + // 只处理左键和中键 + if (event.button !== TiKeymap.MOUSE_LEFT_BUTTON && event.button !== TiKeymap.MOUSE_MIDDLE_BUTTON) { + // Tiny2这里逻辑错误TODO: + return; + } + + // 点击选中项 + this.selectOption(option); + // 更新hover + this.hoverOption = option; + } + + public getVirtualScrollViewportHeight(): string { + let height: number = TiListComponent.CONTAINER_VERTICAL_PADDING; // 这里的8是上下各4px的padding + if (!this.options || this.options.length === 0) { + return height + 'px'; + } + height += this.options.length * this.itemSize; + if (this.selectAll && this.multiple) { + height += TiListComponent.OPTION_DEFAULT_HEIGHT; // 全选项的高度 + } + + return height + 'px'; + } + + private selectOption(option: any): void { + // 全选 + if (this.multiple && this.selectAll && option === this.optionSelectAll) { + this.selectAllOptions(); + this.select.emit(option); + + return; + } + + let index: number = -1; + if (this.multiple) { + // 多选 + if (!this.model) { + // 如果为空值 + this.model = []; + } + // 是否选中,取反 + index = this.getIndex(option, this.model); + if (index === -1) { + this.model.push(option); // 点击下拉项选中,选中项按用户操作顺序显示 + } else if (index !== -1) { + this.model.splice(index, 1); + } + // 强行向外通知model改变。 + this.model = this.model.concat(); + // 设置全选状态 + if (this.selectAll) { + this.setSelectedAllState(); + } + } else { + // 单选 + this.model = option; + } + this.select.emit(option); + } + + private selectAllOptions(): void { + if (!this.model) { + // 如果为空值 + this.model = []; + } + const listOptions: Array = this.getListOptions(); + let isChange: boolean = false; + if (this.selectAllState === false || this.selectAllState === null) { + listOptions.forEach((item: any) => { + if (this.getIndex(item, this.model) === -1 && this.isSelectable(item)) { + // 不在当前选中项且为可选数据项(排除禁用项和组名项) + // 点击全选按钮时的选中值顺序与option的顺序一致 + const insertIndex: number = this.getInsertIndex(item); + this.model.splice(insertIndex, 0, item); + isChange = true; + } + }); + + this.selectAllState = true; + } else { + listOptions.forEach((item: any) => { + const index: number = this.getIndex(item, this.model); + if (index !== -1 && !this.isDisabledFn(item)) { + this.model.splice(index, 1); + isChange = true; + } + }); + this.selectAllState = false; + } + if (isChange) { + // model的值在以上循环中,多次变化,或者不变,根据isChange,一次性触发变化检测 + this.model = this.model.concat(); + } + } + + public setSelectedAllState(): void { + if (!this.model || this.model.length === 0 || !this.options || this.options.length === 0) { + this.selectAllState = false; + + return; + } + const listOptions: Array = this.getListOptions(); + const selectedOptions: Array = listOptions.filter((item: any) => { + return this.isSelectable(item) && this.isSelected(item); + }); + if (selectedOptions.length === 0) { + this.selectAllState = false; + + return; + } + const selectableOptions: Array = listOptions.filter((item: any) => { + return this.isSelectable(item); + }); + this.selectAllState = selectedOptions.length === selectableOptions.length ? true : null; + } + + // 获取新插入元素的插入次序,该次序与list集合中的元素顺序保持一致 + private getInsertIndex(option: any): number { + const listOptions: Array = this.getListOptions(); + const indexOfList: number = listOptions.indexOf(option); // 当前元素在list中的排序 + // 将该元素与已选中在list中的次序进行比较,如果该元素在某选中项次序之后,则将其插入该元素之后 + for (let i: number = this.model.length; i > 0; i--) { + const itemIndexOfList: number = this.getIndex(this.model[i - 1], listOptions); + if (itemIndexOfList < indexOfList) { + return i; + } + } + + return 0; + } + public onMouseenterItem(event: MouseEvent, option: any): void { + // 是可选项,且不是在选中项置Top时鼠标经过。 + if (this.isSelectable(option) && new Date().getTime() - this.optionScrollTopLastTime > TiListComponent.SCROLL_TOP_TIME) { + this.hoverOption = option; // 更新hover + this.optionMouseenter.emit(option); + } + } + // 寻找下一个可选项。参数:向上/向下 + public nextOption(isUp: boolean): void { + const listOptions: Array = this.getListOptions(); + if (this.multiple && this.selectAll) { + listOptions.unshift(this.optionSelectAll); + } + if (listOptions.filter((x: any) => this.isSelectable(x)).length === 0) { + // 没有可选中的项目 + return; + } + let nextOption: any = this.hoverOption; + let curIndex: number = listOptions.indexOf(nextOption); + do { + if (isUp) { + if (curIndex > 0) { + // 找到了,且不是第一个元素 + curIndex = curIndex - 1; + } else { + // -1没有找到, 0第一个元素 + curIndex = listOptions.length - 1; // 跳到第一个元素。 + } + } else { + if (curIndex < listOptions.length - 1) { + // -1没有找到, 和不是最后一个 + curIndex = curIndex + 1; + } else { + // length - 1 最后一个元素 + curIndex = 0; // 跳到第一个元素。 + } + } + nextOption = listOptions[curIndex]; + } while (!(this.isSelectable(nextOption) || nextOption === this.hoverOption)); // 找到停止,或者寻找一圈停止 + // 没有找到结果 + if (nextOption === null || nextOption === this.hoverOption) { + return; + } + // 改变hover选项。 + this.hoverOption = nextOption; + this.cd.markForCheck(); + // 更新滚动条 + this.optionScollTop(this.hoverOption); + } + protected getListOptions(): Array { + let listOptions: Array = []; // 装有所有选项的数组 + if (this.options) { + this.options.forEach((x: any) => { + listOptions.push(x); + if (this.isGroup(x)) { + listOptions = listOptions.concat(x.children); + } + }); + } + + return listOptions; + } + // 滚动到选中项元素,多选定位到第一个选中项元素,无选中项时定义到第一项元素; + // 适用于每次下拉打开的场景 + public scrollToSelected(): void { + // 获取当前选中项 + let selectedItem: any; + if (this.multiple) { + selectedItem = this.model && this.model.length > 0 ? this.model[0] : null; + } else { + selectedItem = this.model; + } + if (selectedItem) { + // 有选中项时,自动定位到第一个选中元素 + this.scrollToEle(selectedItem, true); + } else { + // 无选中项时,定位在第一项元素 + if (this.virtual) { + this.virtualScrollViewport.elementRef.nativeElement.scrollTop = 0; + } else { + this.nativeElement.scrollTop = 0; + } + } + this.setHoverOption(); + } + /** + * 设置hoverOption + * 用于数据更新时,非鼠标或快捷键操作时 + */ + private setHoverOption(): void { + if (this.options?.length > 0) { + if (this.hasSelectedinList()) { + this.hoverOption = this.getHoverOptionInModel(); + } else { + if (this.multiple && this.selectAll) { + this.hoverOption = this.optionSelectAll; + } else { + if (this.options[0]?.children) { + this.hoverOption = this.getGroupHoverOption(); + } else { + this.hoverOption = this.options.find((option: any) => !this.isDisabledFn(option)); + } + } + } + } else { + this.hoverOption = undefined; + } + } + + /** + * 单选有model的时候,单选的model不会是禁用项,hoverOption应当是model, + * 多选有model的时候,hoverOption是第一个非禁用的model,如果model全部禁用,hoverOption是第一个禁用项往下的第一个可用项。 + */ + private getHoverOptionInModel(): any { + return this.multiple + ? this.options[ + this.getIndex( + this.model.find((item: any) => !this.isDisabledFn(item)), + this.options + ) + ] || + this.options.find((item: any, index: number) => { + return index > this.getIndex(this.model[0], this.options) && !this.isDisabledFn(item); + }) + : this.options[this.getIndex(this.model, this.options)] || this.model; + } + /** + * 判断当前list中是否有选中项,过滤后的数据有可能不含有选中项 + */ + private hasSelectedinList(): boolean { + const listOption: Array = this.getListOptions(); + if (this.multiple && this.model?.length > 0) { + return !!this.model.find((item: any) => this.getIndex(item, listOption) !== -1); + } else if (this.model) { + return this.getIndex(this.model, listOption) !== -1; + } else { + return false; + } + } + + // 分组情况下,找到hoverOption + private getGroupHoverOption(): any { + let groupHoverOption: any; + this.options.find((option: any): boolean => { + if (option.children.length > 0) { + groupHoverOption = option.children.find((child: any) => !this.isDisabledFn(child)); + } + + return groupHoverOption ? true : false; // 这个return只是为了结束find方法 + }); + + return groupHoverOption; // 这个return是为了返回查找结果 + } + private scrollToEle(option: any, isScrollBySelected: boolean = false): void { + if (!option || !this.ulRef) { + // 第一次model值改变时, ulRef不存在。 + return; + } + const listOptions: Array = this.getListOptions(); + if (this.multiple && this.selectAll) { + listOptions.unshift(this.optionSelectAll); + } + const curIndex: number = this.getIndex(option, listOptions); + if (curIndex < 2 && isScrollBySelected) { + if (this.virtual) { + this.virtualScrollViewport.elementRef.nativeElement.scrollTop = 0; + } else { + this.nativeElement.scrollTop = 0; + } + + return; // 前两项不需要滚动 + } + // 下拉框有滚动条时,若上下选项足够,选中项为下拉框的第二项,这样可以展示选中项的前后选项 + const scrollTopOptionIndex: number = Math.max(curIndex - 1, 0); + if (this.virtual) { + this.virtualScrollViewport.elementRef.nativeElement.scrollTop = this.itemSize * scrollTopOptionIndex; + } else { + const ele: HTMLElement = this.ulRef.nativeElement.getElementsByTagName('LI')[scrollTopOptionIndex]; + if (ele) { + this.nativeElement.scrollTop = ele.offsetTop; + } + } + } + private optionScollTop(option: any): void { + this.optionScrollTopLastTime = new Date().getTime(); + this.scrollToEle(option); + } + + // 是组数据项 + public isGroup(item: any): boolean { + return item && Object.prototype.hasOwnProperty.call(item, 'children') && this.showChildren; + } + // 是可选数据项 + protected isSelectable(item: any): boolean { + return !this.isGroup(item) && !this.isDisabledFn(item); + } + // 是已选中数据项 + public isSelected(item: any): boolean { + if (!this.model) { + return false; + } + + if (this.multiple) { + return this.getIndex(item, this.model) !== -1; + } + + return this.isEqual(this.model, item); + } + + // 选中项与options数据集中的选项的对应关系 + private isEqual(modelOption: any, option: any): boolean { + if (this.idKey) { + return modelOption[this.idKey] === option[this.idKey]; + } + + return ( + modelOption === option || + (modelOption[this.labelKey] !== undefined && + option[this.labelKey] !== undefined && + modelOption[this.labelKey] === option[this.labelKey]) + ); + } + + private getIndex(item: any, arr: Array): number { + if (!arr || !item) { + return -1; + } + for (let i: number = 0; i < arr.length; i++) { + if (this.isEqual(arr[i], item)) { + return i; + } + } + + return -1; + } + /** + * 修复点击IE滚动条,面板关闭。 + * 打开面板时监听,关闭和销毁面板时取消监听。 + */ + public listenIESrollbarBug(): void { + // IE兼容性涉及知识,见本文件底部。 + // IE兼容性问题: 点击滚动条,触发失焦,面板会收起 + // 修复方案:点击list面板区域(包含滚动条)后,给失焦元素强制获取焦点。修改Form基类不触发组件整体失焦。 + if (TiBrowser.isIE()) { + // 点击面板,阻止Blur + this.unlistenIEScrollbarBugFns.push( + this.renderer.listen(this.nativeElement, 'mousedown', (event: FocusEvent) => { + this.stopBlur = true; // 标记,以供focusin消耗 + // 阻止冒泡到document,因为document会把标志位改回false。和下面修复双击滚动条Bug配合 + event.stopPropagation(); + }) + ); + // 修复bug:双击滚动条,再点击面板之外,不失焦。 + this.unlistenIEScrollbarBugFns.push( + this.renderer.listen(document, 'mousedown', (event: FocusEvent) => { + // 点击面板之外区域,不阻止blur + this.stopBlur = false; + }) + ); + + // IE事件顺序:focusout->foucsin->blur->focus, 支持冒泡的focusin/focusout, 不支持冒泡的是focus/blur + this.unlistenIEScrollbarBugFns.push( + this.renderer.listen(document.body, 'focusin', (event: FocusEvent) => { + if (this.stopBlur && event.relatedTarget !== document.body) { + // 本来不需要!==document, 但连续多次点击太快,会发生:刚点击一下,上次的dominator->body focusin, 会进来 + + // 清零,因为下面的聚焦事件,又会触发focusin事件,并不想处理 + this.stopBlur = false; // 一般情况这里置为false是可以的。 + + // focusin事件,target是聚焦元素。relatedTarget是失焦元素。 + // 强制让失焦元素,重新获得焦点。 + if (event.relatedTarget) { + (event.relatedTarget as HTMLElement).focus(); + } + } + }) + ); + } + } + public unlistenIESrollbarBug(): void { + if (TiBrowser.isIE()) { + // 解绑监听 + for (const x of this.unlistenIEScrollbarBugFns) { + x(); + } + } + } +} +/* +IE兼容性问题:点击面板滚动条,触发失焦,无法阻止。 +解决方案:给失焦元素强制获取焦点。修改Form基类不触发组件整体失焦。 + +涉及知识点: +点击div面板的滚动条,浏览器会失焦,焦点到body。event.preventDefault();,可以阻止默认失焦行为。 +点击div面板的滚动条,浏览器会失焦,焦点到body。无法阻止。(问题产生) + +Chrome浏览器事件顺序: +blur->focusout->focus->foucsin +IE浏览器事件顺序: +focusout->focusin->blur->focus + +IE浏览器,在blur事件里,失焦元素.focus(), 会先显示失焦css,后显示聚焦css,会闪一下。 +IE浏览器,在focusout事件里,失焦元素.focus(), 无效。 +IE浏览器,在focusin事件里,失焦元素.focus(), 会一直显示聚焦css,不会闪。(作为解决问题的方案) + +Chrome blur事件时,document.activeElement是body +IE blur事件时,document.activeElement是将要聚焦的元素。 +IE 如果在focusin里,失焦元素.focus(),那么blur时document.activeElement刚好是这个失焦并强制聚焦的元素。 + +第一版方案: +强制聚焦,用了延时处理 +整体onBlur时,IE特殊处理,不闭合面板 +闭合面板时,处理标志位。 +解决双击滚动条bug,利用监听面板之外点击。 + +第二版方案: +强制聚焦,不用延时。 +修复Form基类,不再触发整体失焦/聚焦。 +闭合面板时,处理标志位 (TODO: 设法去除。尽量集中对标志位的管理) +解决双击滚动条bug,利用监听面板之外点击。(TODO: 设法去除。尽量集中对标志位的管理) + +第三版方案: +面板打开时监听mouse,面板闭合时不再监听 + + */ diff --git a/src/list/lib/src/TiListModule.ts b/src/list/lib/src/TiListModule.ts new file mode 100644 index 0000000..97e883c --- /dev/null +++ b/src/list/lib/src/TiListModule.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ScrollingModule } from '@angular/cdk/scrolling'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiListComponent } from './TiListComponent'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiLoadingModule } from '@opentiny/ng-loading'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule, ScrollingModule, TiIconModule, TiTipModule, TiLoadingModule], + exports: [TiListComponent], + declarations: [TiListComponent] +}) +export class TiListModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiListComponent, TiListScrollLoad } from './TiListComponent'; diff --git a/src/list/lib/src/i18n/TiListWords.ts b/src/list/lib/src/i18n/TiListWords.ts new file mode 100644 index 0000000..a4ca7d1 --- /dev/null +++ b/src/list/lib/src/i18n/TiListWords.ts @@ -0,0 +1,6 @@ +export interface TiListWords { + tiList: { + noDataText: string; + selectAll: string; + }; +} diff --git a/src/list/lib/src/i18n/en_US.ts b/src/list/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..178e723 --- /dev/null +++ b/src/list/lib/src/i18n/en_US.ts @@ -0,0 +1,8 @@ +import { TiListWords } from './TiListWords'; + +export const en_US: TiListWords = { + tiList: { + noDataText: 'No data available', + selectAll: '(Select all)' + } +}; diff --git a/src/list/lib/src/i18n/es_US.ts b/src/list/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..e74ede7 --- /dev/null +++ b/src/list/lib/src/i18n/es_US.ts @@ -0,0 +1,8 @@ +import { TiListWords } from './TiListWords'; + +export const es_US: TiListWords = { + tiList: { + noDataText: 'No hay datos disponibles', + selectAll: '(Seleccionar todo)' + } +}; diff --git a/src/list/lib/src/i18n/fr_FR.ts b/src/list/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..0dddc0a --- /dev/null +++ b/src/list/lib/src/i18n/fr_FR.ts @@ -0,0 +1,8 @@ +import { TiListWords } from './TiListWords'; + +export const fr_FR: TiListWords = { + tiList: { + noDataText: 'Aucune donnée disponible', + selectAll: '(Sélectionner tout)' + } +}; diff --git a/src/list/lib/src/i18n/index.ts b/src/list/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/list/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/list/lib/src/i18n/pt_BR.ts b/src/list/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..d84556a --- /dev/null +++ b/src/list/lib/src/i18n/pt_BR.ts @@ -0,0 +1,8 @@ +import { TiListWords } from './TiListWords'; + +export const pt_BR: TiListWords = { + tiList: { + noDataText: 'Nenhum dado disponível', + selectAll: '(Selecionar tudo)' + } +}; diff --git a/src/list/lib/src/i18n/zh_CN.ts b/src/list/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..cd76235 --- /dev/null +++ b/src/list/lib/src/i18n/zh_CN.ts @@ -0,0 +1,8 @@ +import { TiListWords } from './TiListWords'; + +export const zh_CN: TiListWords = { + tiList: { + noDataText: '暂无数据', + selectAll: '(全选)' + } +}; diff --git a/src/list/lib/src/list-multi.less b/src/list/lib/src/list-multi.less new file mode 100644 index 0000000..93ba7da --- /dev/null +++ b/src/list/lib/src/list-multi.less @@ -0,0 +1,53 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-list-multi-checkbox-container-size: var(--ti-common-size-4x); + --ti-list-multi-checkbox-container-line-height: calc( + var(--ti-list-multi-checkbox-container-size) - var(--ti-common-border-weight-normal) * 2 + ); + --ti-list-multi-checkbox-bg-color: var(--ti-common-color-bg-white-normal); + --ti-list-multi-checkbox-checked-bg-color: var(--ti-common-color-bg-emphasize); + --ti-list-multi-checkbox-icon-color: var(--ti-common-color-icon-white); +} +// 以下是多选使用的样式:复选框基础样式 +.ti3-multi-select-checkbox { + width: var(--ti-list-multi-checkbox-container-size); + height: var(--ti-list-multi-checkbox-container-size); + background-color: var(--ti-list-multi-checkbox-bg-color); + display: inline-block; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + .box-sizing; + color: var(--ti-list-multi-checkbox-icon-color); + line-height: var(--ti-list-multi-checkbox-container-line-height); + text-align: center; + margin-right: var(--ti-common-space-10); + cursor: pointer; +} + +// 正常选中 +.ti3-dropdown-option .ti3-multi-select-checkbox-selected { + border-color: var(--ti-common-color-line-active); + background: var(--ti-list-multi-checkbox-checked-bg-color); +} + +// 半选 +.ti3-dropdown-option .ti3-multi-select-checkbox-indeterminate { + border-color: var(--ti-common-color-line-active); + border-width: 5px; + vertical-align: sub; + &::before { + content: ''; + } +} + +// 灰化 / 灰化选中 +.ti3-dropdown-option-disabled .ti3-multi-select-checkbox { + border-color: var(--ti-common-color-line-disabled); + background: var(--ti-common-color-bg-disabled); + color: var(--ti-common-color-bg-disabled); + cursor: not-allowed; + &.ti3-multi-select-checkbox-selected { + color: var(--ti-common-color-icon-disabled); + } +} diff --git a/src/list/lib/src/list.html b/src/list/lib/src/list.html new file mode 100644 index 0000000..e4a28a4 --- /dev/null +++ b/src/list/lib/src/list.html @@ -0,0 +1,108 @@ + + + +
      + + + + + + + + + + +
    +
    +
    +
    + +
      + + + + + + + + + + + + +
    +
    + + + +
    +
    + + +
  • +
    + + + + + {{item[labelKey]}} +
    +
  • + + + +
  • +
    + + {{selectAllText}} +
    +
  • +
    diff --git a/src/list/lib/src/list.less b/src/list/lib/src/list.less new file mode 100644 index 0000000..65b3fc9 --- /dev/null +++ b/src/list/lib/src/list.less @@ -0,0 +1,149 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-list-container-vertical-padding: var(--ti-common-space-base); // list 滚动容器的上下内边距 + --ti-list-option-container-height: 30px; // 下拉项的行高 + --ti-list-fold-icon-width: var(--ti-common-size-2x); + --ti-list-fold-icon-height: calc(var(--ti-list-fold-icon-width) / 1.6); // 按照比例计算 8 / 5 = 1.6 + --ti-list-option-bg-color-active: var(--ti-common-color-bg-emphasize); + --ti-list-fold-icon-color-active: var(--ti-common-color-icon-white); + --ti-list-option-tworow-text-color: var(--ti-common-color-text-weaken); + --ti-list-option-group-title-color: var(--ti-common-color-text-weaken); +} + +:host { + display: inline-block; + + overflow-y: auto; + overflow-x: hidden; + position: relative; // 设置为非static之后,内部li定位滚动条时方便 + padding: var(--ti-list-container-vertical-padding) var(--ti-common-space-0); + box-sizing: border-box; + + &:focus { + //阻止聚焦时浏览器默认蓝框样式 + outline: none; + } +} + +.ti3-dropdown-group > .ti3-dropdown-option { + padding-left: var(--ti-common-space-3x); +} +.ti3-overflow-padding { + padding: var(--ti-common-space-0) var(--ti-common-space-10); + .ellipsis(); +} + +.ti3-dropdown-option { + list-style: none; + cursor: pointer; + color: inherit; + text-align: left; + line-height: var(--ti-common-line-height-number); + // 下拉框选项给一个默认的背景色(#fff,浏览器默认背景色),为了解决火狐浏览器下下拉选项的省略号是一条横线 issue#9436 + background-color: var(--ti-common-color-bg-white-normal); + padding: var(--ti-common-space-6) var(--ti-common-space-0); + /** + * tiTextweaken:对外暴露的属性,定义弱化信息的文本颜色 + * + * 此处用于select组件选项两行信息的场景下 + */ + ::ng-deep [tiTextweaken] { + color: var(--ti-list-option-tworow-text-color); + } +} +.ti3-dropdown-option:first-child { + margin-top: 0; +} + +.ti3-dropdown-group-list { + font-weight: bold; + color: var(--ti-list-option-group-title-color); + box-sizing: border-box; + height: var(--ti-list-option-container-height); + line-height: var(--ti-list-option-container-height); + text-align: left; +} +.ti3-dropdown-group-list-cascader { + position: relative; + padding-right: var(--ti-common-space-4x); + // 级联有下一级数据时,需要小三角箭头 + &::after { + content: ''; + position: absolute; + display: block; + top: 50%; + right: calc(var(--ti-common-space-3x) - 2px); // 视觉标注是12px,因为有旋转,需要减去2px + transform: translateY(-50%) rotate(90deg); + .triangle-up(var(--ti-list-fold-icon-width), var(--ti-list-fold-icon-height), var(--ti-common-color-icon-normal)); + } +} +.ti3-dropdown-option-selected.ti3-dropdown-group-list-cascader::after { + border-bottom-color: var(--ti-common-color-icon-white); +} +.ti3-dropdown-option-disabled.ti3-dropdown-group-list-cascader::after { + border-bottom-color: var(--ti-common-color-icon-disabled); +} + +.ti3-dropdown-option.ti3-dropdown-option-disabled, +.ti3-dropdown-option.ti3-dropdown-option-disabled:hover, +.ti3-dropdown-option.ti3-dropdown-option-disabled ::ng-deep [tiTextweaken], +.ti3-dropdown-option.ti3-dropdown-option-disabled ::ng-deep [tiTextweaken]:hover { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; +} + +.ti3-dropdown-no-data { + padding: var(--ti-common-space-6) var(--ti-common-space-10); + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + line-height: var(--ti-common-line-height-number); +} + +.ti3-dropdown-option-hover, +.ti3-dropdown-option-hover ::ng-deep [tiTextweaken] { + background-color: var(--ti-common-color-bg-white-emphasize); + color: var(--ti-common-color-text-highlight); +} +// 做修改 +.ti3-dropdown-option-selected, +.ti3-dropdown-option-selected ::ng-deep [tiTextweaken] { + background-color: var(--ti-list-option-bg-color-active); + color: var(--ti-common-color-text-white); +} +.ti3-drop-list-ul { + overflow-y: auto; + overflow-x: hidden; +} + +.ti3-list-scroll-to-bottom-loading-container { + position: absolute; + bottom: -8px; + width: calc(100% - 17px); + text-align: center; +} + +// 虚拟滚动 +:host.ti3-virtual-scroll-list { + display: block; + overflow: hidden; // 滚动容器为 .ti3-virtual-scroll-list-viewport + padding: var(--ti-common-space-0); +} +.ti3-virtual-scroll-list-viewport { + height: 100%; + width: 100%; + max-height: inherit; // 由于在下拉类组件中会给ti-list设置最大高度,这里直接继承ti-list的最大高度 + min-height: 38px; // 无数据时限制最小高度 无数据内容高度30px + 容器上下padding和8px + padding: var(--ti-list-container-vertical-padding) var(--ti-common-space-0); + box-sizing: border-box; +} +::ng-deep .ti3-virtual-scroll-list-viewport .cdk-virtual-scroll-content-wrapper { + width: 100%; + top: var(--ti-list-container-vertical-padding); +} +::ng-deep .ti3-virtual-scroll-list-viewport .cdk-virtual-scroll-spacer { + top: calc(var(--ti-list-container-vertical-padding) * 2); +} +::ng-deep .ti3-virtual-scroll-list-viewport.ti3-virtual-scroll-list-viewport-with-selectall .cdk-virtual-scroll-spacer { + top: calc(var(--ti-list-container-vertical-padding) * 2 + 30px); +} diff --git a/src/loading/demo/karma.conf.js b/src/loading/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/loading/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/loading/demo/project.json b/src/loading/demo/project.json new file mode 100644 index 0000000..d43ea4d --- /dev/null +++ b/src/loading/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/loading/demo", + "sourceRoot": "src/loading/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/loading", + "index": "src/loading/demo/src/index.html", + "main": "src/loading/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/loading/demo/tsconfig.app.json", + "assets": ["src/loading/demo/src/favicon.ico", "src/loading/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "loading-demo:build:production" + }, + "development": { + "browserTarget": "loading-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js loading" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/loading/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/loading/demo/tsconfig.spec.json", + "karmaConfig": "src/loading/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/loading/demo/src/app/AppComponent.ts b/src/loading/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/loading/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/loading/demo/src/app/AppModule.ts b/src/loading/demo/src/app/AppModule.ts new file mode 100644 index 0000000..9863337 --- /dev/null +++ b/src/loading/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { LoadingTestModule } from './loading/LoadingTestModule'; + +@NgModule({ + imports: [ + LoadingTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/loading/demo/src/app/IndexComponent.ts b/src/loading/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..0672756 --- /dev/null +++ b/src/loading/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { LoadingTestModule } from './loading/LoadingTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = LoadingTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/loading/demo/src/app/app.html b/src/loading/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/loading/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/loading/demo/src/app/loading/LoadingAreaComponent.ts b/src/loading/demo/src/app/loading/LoadingAreaComponent.ts new file mode 100644 index 0000000..2d5abf1 --- /dev/null +++ b/src/loading/demo/src/app/loading/LoadingAreaComponent.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './loading-area.html' +}) +export class LoadingAreaComponent { + showBlockLoading: boolean = false; + showFullscreenLoading: boolean = false; + showFullscreenLoadingWithMask: boolean = false; + + onClickShowBlockLoading(): void { + this.showBlockLoading = true; + setTimeout(() => { + this.showBlockLoading = false; // 3s后关闭 + }, 3000); + } + + onClickShowFullscreenLoading(): void { + this.showFullscreenLoading = true; + setTimeout(() => { + this.showFullscreenLoading = false; // 3s后关闭 + }, 3000); + } + + onClickShowFullscreenLoadingWithMask(): void { + this.showFullscreenLoadingWithMask = true; + setTimeout(() => { + this.showFullscreenLoadingWithMask = false; // 3s后关闭 + }, 3000); + } +} diff --git a/src/loading/demo/src/app/loading/LoadingBasicComponent.ts b/src/loading/demo/src/app/loading/LoadingBasicComponent.ts new file mode 100644 index 0000000..708bc41 --- /dev/null +++ b/src/loading/demo/src/app/loading/LoadingBasicComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './loading-basic.html' +}) +export class LoadingBasicComponent {} diff --git a/src/loading/demo/src/app/loading/LoadingSizeComponent.ts b/src/loading/demo/src/app/loading/LoadingSizeComponent.ts new file mode 100644 index 0000000..ef9bc78 --- /dev/null +++ b/src/loading/demo/src/app/loading/LoadingSizeComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './loading-size.html' +}) +export class LoadingSizeComponent {} diff --git a/src/loading/demo/src/app/loading/LoadingTestModule.ts b/src/loading/demo/src/app/loading/LoadingTestModule.ts new file mode 100644 index 0000000..3f5f205 --- /dev/null +++ b/src/loading/demo/src/app/loading/LoadingTestModule.ts @@ -0,0 +1,36 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiLoadingModule } from '@opentiny/ng'; + +import { LoadingBasicComponent } from './LoadingBasicComponent'; +import { LoadingTypeComponent } from './LoadingTypeComponent'; +import { LoadingSizeComponent } from './LoadingSizeComponent'; +import { LoadingAreaComponent } from './LoadingAreaComponent'; + +@NgModule({ + imports: [TiLoadingModule, TiButtonModule, RouterModule.forChild(LoadingTestModule.ROUTES)], + declarations: [LoadingTypeComponent, LoadingSizeComponent, LoadingBasicComponent, LoadingAreaComponent] +}) +export class LoadingTestModule { + static readonly LINKS: Array = [{ href: 'components/TiLoadingComponent.html', label: 'Loading' }]; + static readonly ROUTES: Routes = [ + { + path: 'loading/loading-basic', + component: LoadingBasicComponent + }, + { + path: 'loading/loading-type', + component: LoadingTypeComponent + }, + { + path: 'loading/loading-size', + component: LoadingSizeComponent + }, + { + path: 'loading/loading-area', + component: LoadingAreaComponent, + data: { label: '区域加载' } + } + ]; +} diff --git a/src/loading/demo/src/app/loading/LoadingTypeComponent.ts b/src/loading/demo/src/app/loading/LoadingTypeComponent.ts new file mode 100644 index 0000000..224012c --- /dev/null +++ b/src/loading/demo/src/app/loading/LoadingTypeComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './loading-type.html' +}) +export class LoadingTypeComponent {} diff --git a/src/loading/demo/src/app/loading/loading-area.html b/src/loading/demo/src/app/loading/loading-area.html new file mode 100644 index 0000000..34194a8 --- /dev/null +++ b/src/loading/demo/src/app/loading/loading-area.html @@ -0,0 +1,9 @@ + + + + +
    + +
    + + diff --git a/src/loading/demo/src/app/loading/loading-basic.html b/src/loading/demo/src/app/loading/loading-basic.html new file mode 100644 index 0000000..99bfd7d --- /dev/null +++ b/src/loading/demo/src/app/loading/loading-basic.html @@ -0,0 +1 @@ + diff --git a/src/loading/demo/src/app/loading/loading-size.html b/src/loading/demo/src/app/loading/loading-size.html new file mode 100644 index 0000000..e8c054b --- /dev/null +++ b/src/loading/demo/src/app/loading/loading-size.html @@ -0,0 +1,6 @@ +

    + +

    + +

    + diff --git a/src/loading/demo/src/app/loading/loading-type.html b/src/loading/demo/src/app/loading/loading-type.html new file mode 100644 index 0000000..7aaf5f7 --- /dev/null +++ b/src/loading/demo/src/app/loading/loading-type.html @@ -0,0 +1 @@ + diff --git a/src/loading/demo/src/app/loading/webdoc/loading-demos.js b/src/loading/demo/src/app/loading/webdoc/loading-demos.js new file mode 100644 index 0000000..c7398d1 --- /dev/null +++ b/src/loading/demo/src/app/loading/webdoc/loading-demos.js @@ -0,0 +1,54 @@ +export default { + column: '2', + demos: [ + { + demoId: 'loading-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    Loading 组件的最简用法。

    ', + 'en-US': '

    ' + } + }, + { + demoId: 'loading-type', + name: { + 'zh-CN': '加载类型', + 'en-US': 'Type' + }, + desc: { + 'zh-CN': '

    通过属性type配置加载显示类型,包括defaultweak

    ', + 'en-US': '

    ' + }, + apis: ['TiLoadingComponent.properties.type'] + }, + { + demoId: 'loading-size', + name: { + 'zh-CN': '大小', + 'en-US': 'Size' + }, + desc: { + 'zh-CN': + '

    通过属性size配置加载图标的大小,包括smallmiddlelarge,支持直接配置值。

    ', + 'en-US': '

    ' + }, + apis: ['TiLoadingComponent.properties.size'] + }, + { + demoId: 'loading-area', + name: { + 'zh-CN': '区域加载', + 'en-US': 'Area' + }, + desc: { + 'zh-CN': + '

    通过配置属性areablockfullscreen设置局部区域加载状态或全屏加载状态,通过配置showMasktrue显示遮罩层,遮罩层会阻止交互。

    ', + 'en-US': '

    ' + }, + apis: ['TiLoadingComponent.properties.area', 'TiLoadingComponent.properties.showMask'] + } + ] +}; diff --git a/src/loading/demo/src/app/loading/webdoc/loading.cn.md b/src/loading/demo/src/app/loading/webdoc/loading.cn.md new file mode 100644 index 0000000..351161b --- /dev/null +++ b/src/loading/demo/src/app/loading/webdoc/loading.cn.md @@ -0,0 +1,22 @@ +--- +title: Loading 加载 +--- +# Loading 加载 + +
    + +Loading 是加载数据时显示动效的组件。 + +```typescript +import { TiLoadingModule } from '@opentiny/ng'; +``` +
    + +
    + +Loading 是加载数据时显示动效的组件。 + +```typescript +import { TiLoadingModule } from '@opentiny/ng'; +``` +
    diff --git a/src/loading/demo/src/app/loading/webdoc/loading.en.md b/src/loading/demo/src/app/loading/webdoc/loading.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/loading/demo/src/app/loading/webdoc/loading.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/loading/demo/src/favicon.ico b/src/loading/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/loading/demo/src/index.html b/src/loading/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/loading/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/loading/demo/src/main.ts b/src/loading/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/loading/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/loading/demo/test.ts b/src/loading/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/loading/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/loading/demo/tsconfig.app.json b/src/loading/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/loading/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/loading/demo/tsconfig.spec.json b/src/loading/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/loading/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/loading/lib/index.ts b/src/loading/lib/index.ts new file mode 100644 index 0000000..eff0a58 --- /dev/null +++ b/src/loading/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiLoadingModule'; diff --git a/src/loading/lib/ng-package.json b/src/loading/lib/ng-package.json new file mode 100644 index 0000000..6c0c0ea --- /dev/null +++ b/src/loading/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/loading", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/loading/lib/package.json b/src/loading/lib/package.json new file mode 100644 index 0000000..3a1b3f8 --- /dev/null +++ b/src/loading/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-loading", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/loading/lib/project.json b/src/loading/lib/project.json new file mode 100644 index 0000000..660828d --- /dev/null +++ b/src/loading/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/loading/lib", + "sourceRoot": "src/loading/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/loading"], + "options": { + "project": "src/loading/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/loading"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js loading" + }, + { + "command": "ng default-build loading" + }, + { + "command": "node build/clear-default-theme.js loading" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/loading && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build loading && ng pack loading && node build/publish.js loading --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/loading/lib/src/TiLoadingComponent.ts b/src/loading/lib/src/TiLoadingComponent.ts new file mode 100644 index 0000000..4f2e180 --- /dev/null +++ b/src/loading/lib/src/TiLoadingComponent.ts @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * Loading组件用于加载场景,提供了两种类型 + * + * + */ +@Component({ + selector: 'ti-loading', + templateUrl: './loading.html', + styleUrls: ['./loading.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLoadingComponent extends TiBaseComponent { + /** + * @ignore + * 弱类型有4个圈 + */ + public items: Array = []; + protected versionInfo: string = super.getVersion(packageInfo); + + /** + * 加载类型 + */ + @Input() type: 'default' | 'weak' = 'default'; // 加载类型 + + /** + * 展示区域 + */ + @Input() area: 'default' | 'fullscreen' | 'block' = 'default'; + + /** + * 是否展示遮罩 + * + * 全屏场景下可以控制是否展示遮罩层 + */ + @Input() showMask: boolean = false; + + /** + * 加载尺寸 + */ + @Input() size: 'small' | 'middle' | 'large' | string = 'small'; // 加载尺寸 + + ngOnInit(): void { + super.ngOnInit(); + this.items.length = 4; + } + + /** + * @ignore + * 根据用户传入尺寸获取border的宽度 + */ + getBorderWidth(size: string): string { + return (parseInt(size, 10) * 0.3) / 2 + 'px'; + } +} diff --git a/src/loading/lib/src/TiLoadingModule.ts b/src/loading/lib/src/TiLoadingModule.ts new file mode 100644 index 0000000..4e17490 --- /dev/null +++ b/src/loading/lib/src/TiLoadingModule.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiLoadingComponent } from './TiLoadingComponent'; +import { TiLoadingfailComponent } from './TiLoadingfailComponent'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, TiIconModule], + exports: [TiLoadingComponent, TiLoadingfailComponent], + declarations: [TiLoadingComponent, TiLoadingfailComponent] +}) +export class TiLoadingModule { + constructor() { + const words = (window as any).tiWords; + TiLocale.setTiWords(locales); + } +} +export { TiLoadingComponent } from './TiLoadingComponent'; +export { TiLoadingfailComponent } from './TiLoadingfailComponent'; diff --git a/src/loading/lib/src/TiLoadingfailComponent.ts b/src/loading/lib/src/TiLoadingfailComponent.ts new file mode 100644 index 0000000..9aee383 --- /dev/null +++ b/src/loading/lib/src/TiLoadingfailComponent.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, EventEmitter, Output } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiLocale } from '@opentiny/ng-locale'; +import packageInfo from '../package.json'; +/** + * @ignore + * + * 加载失败,可以重新加载 + * + * + */ +@Component({ + selector: 'ti-loadingfail', + templateUrl: './loadingfail.html', + styleUrls: ['./loadingfail.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiLoadingfailComponent extends TiBaseComponent { + @Output() readonly reload: EventEmitter = new EventEmitter(); + + /** + * @ignore + */ + protected versionInfo: string = super.getVersion(packageInfo); + public tiLoadingWords: any = TiLocale.getLocaleWords().tiLoading; + + /** + * @ignore + * + * 重新加载的回调事件 + */ + onReload(event: MouseEvent): void { + this.reload.emit(event); + } +} diff --git a/src/loading/lib/src/i18n/TiLoadingWords.ts b/src/loading/lib/src/i18n/TiLoadingWords.ts new file mode 100644 index 0000000..3dbcd17 --- /dev/null +++ b/src/loading/lib/src/i18n/TiLoadingWords.ts @@ -0,0 +1,6 @@ +export interface TiLoadingWords { + tiLoading: { + loadingfail: string; + reload: string; + }; +} diff --git a/src/loading/lib/src/i18n/en_US.ts b/src/loading/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..2a48ce3 --- /dev/null +++ b/src/loading/lib/src/i18n/en_US.ts @@ -0,0 +1,8 @@ +import { TiLoadingWords } from './TiLoadingWords'; + +export const en_US: TiLoadingWords = { + tiLoading: { + loadingfail: 'Loading failed. ', + reload: 'Reload' + } +}; diff --git a/src/loading/lib/src/i18n/es_US.ts b/src/loading/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..dbaba34 --- /dev/null +++ b/src/loading/lib/src/i18n/es_US.ts @@ -0,0 +1,8 @@ +import { TiLoadingWords } from './TiLoadingWords'; + +export const es_US: TiLoadingWords = { + tiLoading: { + loadingfail: 'Error al cargar. ', + reload: 'Volver a cargar' + } +}; diff --git a/src/loading/lib/src/i18n/fr_FR.ts b/src/loading/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..1404dc8 --- /dev/null +++ b/src/loading/lib/src/i18n/fr_FR.ts @@ -0,0 +1,8 @@ +import { TiLoadingWords } from './TiLoadingWords'; + +export const fr_FR: TiLoadingWords = { + tiLoading: { + loadingfail: 'Le chargement a échoué. ', + reload: 'Recharger' + } +}; diff --git a/src/loading/lib/src/i18n/index.ts b/src/loading/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/loading/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/loading/lib/src/i18n/pt_BR.ts b/src/loading/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..21c8475 --- /dev/null +++ b/src/loading/lib/src/i18n/pt_BR.ts @@ -0,0 +1,8 @@ +import { TiLoadingWords } from './TiLoadingWords'; + +export const pt_BR: TiLoadingWords = { + tiLoading: { + loadingfail: 'Erro no carregamento. ', + reload: 'Recarregar' + } +}; diff --git a/src/loading/lib/src/i18n/zh_CN.ts b/src/loading/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..1bdaaa0 --- /dev/null +++ b/src/loading/lib/src/i18n/zh_CN.ts @@ -0,0 +1,8 @@ +import { TiLoadingWords } from './TiLoadingWords'; + +export const zh_CN: TiLoadingWords = { + tiLoading: { + loadingfail: '加载失败,', + reload: '重新加载' + } +}; diff --git a/src/loading/lib/src/loading.html b/src/loading/lib/src/loading.html new file mode 100644 index 0000000..594e0e4 --- /dev/null +++ b/src/loading/lib/src/loading.html @@ -0,0 +1,20 @@ + +
    +
    + +
    + +
    diff --git a/src/loading/lib/src/loading.less b/src/loading/lib/src/loading.less new file mode 100644 index 0000000..ffa98b4 --- /dev/null +++ b/src/loading/lib/src/loading.less @@ -0,0 +1,116 @@ +:host { + --ti-loading-weak-size: 5px; // 弱类型加载尺寸 + --ti-backdrop-bg-color: #000000; // 遮罩层颜色 +} + +.ti3-loading-size(@size) { + width: @size; + height: @size; + border-width: calc(@size * 0.3 / 2); // 内圈外径 = 0.7 * 外圈外径;故边框粗 = 0.3 * 外圈直径 /2 +} + +.ti3-loading-default { + display: inline-block; + box-sizing: border-box; + border-style: var(--ti-common-border-style-solid); + border-color: var(--ti-common-color-line-active); + border-radius: var(--ti-common-border-radius-3); + border-right-color: transparent; + animation: default-animation 0.8s linear 0s infinite; + &.ti3-loading-default-small { + .ti3-loading-size(var(--ti-common-size-3x)); // s类型尺寸 + } + &.ti3-loading-default-middle { + .ti3-loading-size(var(--ti-common-size-6x)); // m类型尺寸 + } + &.ti3-loading-default-large { + .ti3-loading-size(var(--ti-common-size-8x)); // l类型尺寸 + } +} + +.ti3-loading-weak-box { + height: var(--ti-loading-weak-size); + width: calc(var(--ti-loading-weak-size) * 4 + var(--ti-common-space-2x) * 3); +} + +.ti3-loading-weak { + display: inline-block; + width: var(--ti-loading-weak-size); + height: var(--ti-loading-weak-size); + border-radius: var(--ti-common-border-radius-3); + background-color: var(--ti-common-color-line-active); + opacity: 0.7; + animation: weak-animation 1.2s ease-in-out infinite; + &:not(:last-child) { + margin-right: var(--ti-common-space-2x); + } + &:first-child { + animation-delay: 0s; + } + &:nth-child(2) { + animation-delay: 0.2s; + } + &:nth-child(3) { + animation-delay: 0.4s; + } + &:nth-child(4) { + animation-delay: 0.6s; + } +} + +.ti3-loading-fullscreen-mask { + position: fixed; + z-index: 1200; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--ti-backdrop-bg-color); + opacity: 0.2; +} + +.ti3-loading-fullscreen { + position: fixed; + z-index: 1200; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; +} + +.ti3-loading-block { + position: absolute; + z-index: 1200; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; +} + +// 定义默认loading旋转动画 +@keyframes default-animation { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +// 定义弱loading动画 +@keyframes weak-animation { + 0% { + opacity: 0.2; + } + 25% { + opacity: 0.5; + } + 50% { + opacity: 0.7; + } + 100% { + opacity: 0.2; + } +} diff --git a/src/loading/lib/src/loadingfail.html b/src/loading/lib/src/loadingfail.html new file mode 100644 index 0000000..562b0fb --- /dev/null +++ b/src/loading/lib/src/loadingfail.html @@ -0,0 +1,12 @@ + +{{tiLoadingWords.loadingfail}} + {{tiLoadingWords.reload}} + + diff --git a/src/loading/lib/src/loadingfail.less b/src/loading/lib/src/loadingfail.less new file mode 100644 index 0000000..8223b3d --- /dev/null +++ b/src/loading/lib/src/loadingfail.less @@ -0,0 +1,36 @@ +:host { + --ti-loadingfail-icon-size: var(--ti-common-size-4x); + --ti-loadingfail-line-height: calc(var(--ti-common-font-size-base) * var(--ti-common-line-height-number)); + --ti-loadingfail-margin-vertical: calc((var(--ti-loadingfail-line-height) - var(--ti-loadingfail-icon-size)) / 2); +} + +:host { + display: inline-flex; + align-items: flex-start; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + line-height: var(--ti-common-line-height-number); +} + +.ti3-loadingfail-icon { + display: inline-block; + color: var(--ti-common-color-text-white); + width: var(--ti-loadingfail-icon-size); + height: var(--ti-loadingfail-icon-size); + line-height: var(--ti-loadingfail-icon-size); + font-size: calc(var(--ti-loadingfail-icon-size) * 0.75); + border-radius: var(--ti-common-border-radius-3); + text-align: center; + background-color: var(--ti-common-color-error); + margin: var(--ti-loadingfail-margin-vertical) var(--ti-common-space-2x) var(--ti-loadingfail-margin-vertical) var(--ti-common-space-0); + flex-shrink: 0; +} + +.ti3-loadingfail-link { + color: var(--ti-common-color-text-link); + text-decoration: none; + &:hover { + color: var(--ti-common-color-text-link-hover); + text-decoration: underline; + } +} diff --git a/src/locale/demo/karma.conf.js b/src/locale/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/locale/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/locale/demo/project.json b/src/locale/demo/project.json new file mode 100644 index 0000000..322a1f3 --- /dev/null +++ b/src/locale/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/locale/demo", + "sourceRoot": "src/locale/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/locale", + "index": "src/locale/demo/src/index.html", + "main": "src/locale/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/locale/demo/tsconfig.app.json", + "assets": ["src/locale/demo/src/favicon.ico", "src/locale/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "locale-demo:build:production" + }, + "development": { + "browserTarget": "locale-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js locale" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/locale/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/locale/demo/tsconfig.spec.json", + "karmaConfig": "src/locale/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/locale/demo/src/app/AppComponent.ts b/src/locale/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/locale/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/locale/demo/src/app/AppModule.ts b/src/locale/demo/src/app/AppModule.ts new file mode 100644 index 0000000..1a93347 --- /dev/null +++ b/src/locale/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { LocaleTestModule } from './locale/LocaleTestModule'; + +@NgModule({ + imports: [ + LocaleTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/locale/demo/src/app/IndexComponent.ts b/src/locale/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..5571792 --- /dev/null +++ b/src/locale/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { LocaleTestModule } from './locale/LocaleTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = LocaleTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/locale/demo/src/app/app.html b/src/locale/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/locale/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/locale/demo/src/app/locale/LocaleBasicComponent.ts b/src/locale/demo/src/app/locale/LocaleBasicComponent.ts new file mode 100644 index 0000000..3b762ac --- /dev/null +++ b/src/locale/demo/src/app/locale/LocaleBasicComponent.ts @@ -0,0 +1,61 @@ +import { Component } from '@angular/core'; +import { TiLocale } from '@opentiny/ng'; + +interface LocaleWords { + tiLocaleKey: string; + testPassWord: string; + testMaxValue: string; + testRangeValue: string; +} + +@Component({ + templateUrl: './locale-basic.html' +}) +export class LocaleBasicComponent { + // 用户自定义词条 + private static myzh_CN: LocaleWords = { + tiLocaleKey: 'zh-CN', + testPassWord: '密码输入不符合要求,请重新输入', + testMaxValue: '输入值不能超过{0}', + testRangeValue: '输入值必须在{0}到{1}之间' + }; + + // 用户自定义词条 + private static myen_US: LocaleWords = { + tiLocaleKey: 'en-US', + testPassWord: 'Invalid password.', + testMaxValue: 'Enter a value less than or equal to {0}.', + testRangeValue: 'Enter a value from {0} to {1}.' + }; + + constructor() { + const words = (window as any).tiWords; + // 添加用户的国际化词条 + TiLocale.setWords({ + 'zh-CN': { ...words['zh-CN'], ...LocaleBasicComponent.myzh_CN }, + 'en-US': { ...words['en-US'], ...LocaleBasicComponent.myen_US } + }); + this.setValues(); + } + + testPassWord: string; + testMaxValue: string; + testRangeValue: string; + okBtn: string; + + changeLocale(localeKey: string): void { + TiLocale.setLocale(localeKey); + this.setValues(); + } + + setValues(): void { + this.testPassWord = this.setLocaleValue('testPassWord'); + this.testMaxValue = this.setLocaleValue('testMaxValue', [1]); + this.testRangeValue = this.setLocaleValue('testRangeValue', [1, 2]); + this.okBtn = this.setLocaleValue('tiButton.ok'); + } + + setLocaleValue(key: string, params?: Array): string { + return TiLocale.translate(key, params); + } +} diff --git a/src/locale/demo/src/app/locale/LocaleFormatComponent.ts b/src/locale/demo/src/app/locale/LocaleFormatComponent.ts new file mode 100644 index 0000000..0bbd40e --- /dev/null +++ b/src/locale/demo/src/app/locale/LocaleFormatComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { TiLocaleFormat } from '@opentiny/ng'; +@Component({ + templateUrl: './locale-format.html' +}) +export class LocaleFormatComponent { + formatedNumber: string = TiLocaleFormat.formatNumber(1598565912.6574, '1.2-2'); + formatedNumber1: string = TiLocaleFormat.formatNumber(1000, '1.1-1'); + formatedDate: string = TiLocaleFormat.formatDate(new Date()); + formatedTime: string = TiLocaleFormat.formatTime(new Date()); + formatedDateTime: string = TiLocaleFormat.formatDateTime(new Date(), '', '+0430'); +} diff --git a/src/locale/demo/src/app/locale/LocaleReloadComponent.ts b/src/locale/demo/src/app/locale/LocaleReloadComponent.ts new file mode 100644 index 0000000..291c97a --- /dev/null +++ b/src/locale/demo/src/app/locale/LocaleReloadComponent.ts @@ -0,0 +1,70 @@ +import { Component } from '@angular/core'; +import { TiLocale } from '@opentiny/ng'; + +interface LocaleWords { + tiLocaleKey: string; + testPassWord: string; + testMaxValue: string; + testRangeValue: string; +} + +@Component({ + templateUrl: './locale-reload.html' +}) +export class LocaleReloadComponent { + // 用户自定义词条 + private static myzh_CN: LocaleWords = { + tiLocaleKey: 'zh-CN', + testPassWord: '密码输入不符合要求,请重新输入', + testMaxValue: '输入值不能超过{0}', + testRangeValue: '输入值必须在{0}到{1}之间' + }; + + // 用户自定义词条 + private static myen_US: LocaleWords = { + tiLocaleKey: 'en-US', + testPassWord: 'Invalid password.', + testMaxValue: 'Enter a value less than or equal to {0}.', + testRangeValue: 'Enter a value from {0} to {1}.' + }; + + constructor() { + const words = (window as any).tiWords; + // 添加用户的国际化词条 + TiLocale.setWords({ + 'zh-CN': { ...words['zh-CN'], ...LocaleReloadComponent.myzh_CN }, + 'en-US': { ...words['en-US'], ...LocaleReloadComponent.myen_US } + }); + + TiLocale.setLocale(this.getCookie('localeKey')); + } + + changeLocale(localeKey: string): void { + TiLocale.setLocale(localeKey); + } + + setLocaleValue(key: string, params?: Array): string { + return TiLocale.translate(key, params); + } + + setLocaleAndRefresh(localeKey: string): void { + this.changeLocale(localeKey); + document.cookie = `localeKey=${localeKey}`; + location.reload(); + } + + getCookie(key: string): string { + const name: string = key + '='; + const splitedCookie: Array = document.cookie.split(';'); + for (let word of splitedCookie) { + while (word.charAt(0) === ' ') { + word = word.substring(1); + } + if (word.indexOf(name) === 0) { + return word.substring(name.length, word.length); + } + } + + return ''; + } +} diff --git a/src/locale/demo/src/app/locale/LocaleTestModule.ts b/src/locale/demo/src/app/locale/LocaleTestModule.ts new file mode 100644 index 0000000..82d015c --- /dev/null +++ b/src/locale/demo/src/app/locale/LocaleTestModule.ts @@ -0,0 +1,35 @@ +import { LOCALE_ID, NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule, registerLocaleData } from '@angular/common'; + +import { TiButtonModule, TiLocaleModule } from '@opentiny/ng'; + +import { LocaleBasicComponent } from './LocaleBasicComponent'; +import { LocaleFormatComponent } from './LocaleFormatComponent'; +import { LocaleReloadComponent } from './LocaleReloadComponent'; +import localeZh from '@angular/common/locales/zh'; + +@NgModule({ + imports: [CommonModule, TiLocaleModule, TiButtonModule, RouterModule.forChild(LocaleTestModule.ROUTES)], + declarations: [LocaleBasicComponent, LocaleFormatComponent, LocaleReloadComponent], + providers: [{ provide: LOCALE_ID, useValue: 'zh' }] +}) +export class LocaleTestModule { + constructor() { + registerLocaleData(localeZh, 'zh'); // 用于设置时间日期、数字格式化信息的配置,组件内部使用的规则语种与国际化语种设置一致,具体使用参考angular国际化设置文档 + } + static readonly ROUTES: Routes = [ + { + path: 'locale/locale-basic', + component: LocaleBasicComponent + }, + { + path: 'locale/locale-reload', + component: LocaleReloadComponent + }, + { + path: 'locale/locale-format', + component: LocaleFormatComponent + } + ]; +} diff --git a/src/locale/demo/src/app/locale/locale-basic.html b/src/locale/demo/src/app/locale/locale-basic.html new file mode 100644 index 0000000..cd5a58d --- /dev/null +++ b/src/locale/demo/src/app/locale/locale-basic.html @@ -0,0 +1,7 @@ +

    {{ testPassWord }}

    +

    {{ testMaxValue }}

    +

    {{ testRangeValue }}

    +

    {{ okBtn }}

    + + + diff --git a/src/locale/demo/src/app/locale/locale-format.html b/src/locale/demo/src/app/locale/locale-format.html new file mode 100644 index 0000000..4d17fff --- /dev/null +++ b/src/locale/demo/src/app/locale/locale-format.html @@ -0,0 +1,5 @@ +

    1.数字,保留2位:{{ formatedNumber }}

    +

    2.数字,保留1位:{{ formatedNumber1 }}

    +

    3.日期:{{ formatedDate }}

    +

    4.时间:{{ formatedTime }}

    +

    5.时间日期:{{ formatedDateTime }}

    diff --git a/src/locale/demo/src/app/locale/locale-reload.html b/src/locale/demo/src/app/locale/locale-reload.html new file mode 100644 index 0000000..9abee68 --- /dev/null +++ b/src/locale/demo/src/app/locale/locale-reload.html @@ -0,0 +1,6 @@ +

    {{ 'testPassWord' | tiTranslate }}

    +

    {{ 'testMaxValue' | tiTranslate: [1] }}

    +

    {{ 'testRangeValue' | tiTranslate: [1,2] }}

    +

    {{ 'tiButton.ok' | tiTranslate }}

    + + diff --git a/src/locale/demo/src/app/locale/webdoc/locale-demos.js b/src/locale/demo/src/app/locale/webdoc/locale-demos.js new file mode 100644 index 0000000..c84b231 --- /dev/null +++ b/src/locale/demo/src/app/locale/webdoc/locale-demos.js @@ -0,0 +1,40 @@ +export default { + column: '2', + demos: [ + { + demoId: 'locale-basic', + name: { + 'zh-CN': '基础用法', + 'en-US': 'locale Basic', + }, + desc: { + 'zh-CN': '语言切换,页面不刷新场景。', + 'en-US': '

    locale Basic

    ', + }, + }, + { + demoId: 'locale-reload', + name: { + 'zh-CN': '过滤器', + 'en-US': 'locale reload', + }, + desc: { + 'zh-CN': + '使用过滤器做国际化转换,此方式只支持刷新页面切换语言,用户可结合 cookie 进行语言切换', + 'en-US': 'locale reload', + }, + apis: ['TiLocale.methods.setLocale'], + }, + { + demoId: 'locale-format', + name: { + 'zh-CN': '格式化', + 'en-US': 'format', + }, + desc: { + 'zh-CN': '使用组件 API 格式化本地数字、货币、时间日期。', + 'en-US': 'locale format', + }, + }, + ], +}; diff --git a/src/locale/demo/src/app/locale/webdoc/locale.cn.md b/src/locale/demo/src/app/locale/webdoc/locale.cn.md new file mode 100644 index 0000000..98ea608 --- /dev/null +++ b/src/locale/demo/src/app/locale/webdoc/locale.cn.md @@ -0,0 +1,28 @@ +--- +title: TiLocale 国际化语言 +--- +# TiLocale 国际化语言 + +
    + +Tiny 组件中使用的国际化词条配置方法类。   + ++ 可通过在启动模块中通过配置具体的国际化语种的方式设置组件的国际化。 + +```typescript +import { TiLocaleModule } from '@opentiny/ng'; +``` + +
    + +
    + +Tiny 组件中使用的国际化词条配置方法类。   + ++ 可通过在启动模块中通过配置具体的国际化语种的方式设置组件的国际化。 + +```typescript +import { TiLocaleModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/locale/demo/src/app/locale/webdoc/locale.en.md b/src/locale/demo/src/app/locale/webdoc/locale.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/locale/demo/src/app/locale/webdoc/locale.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/locale/demo/src/favicon.ico b/src/locale/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/locale/demo/src/index.html b/src/locale/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/locale/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/locale/demo/src/main.ts b/src/locale/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/locale/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/locale/demo/test.ts b/src/locale/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/locale/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/locale/demo/tsconfig.app.json b/src/locale/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/locale/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/locale/demo/tsconfig.spec.json b/src/locale/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/locale/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/locale/lib/index.ts b/src/locale/lib/index.ts new file mode 100644 index 0000000..d432ba7 --- /dev/null +++ b/src/locale/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiLocaleModule'; diff --git a/src/locale/lib/ng-package.json b/src/locale/lib/ng-package.json new file mode 100644 index 0000000..e2df576 --- /dev/null +++ b/src/locale/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/locale", + "lib": { + "entryFile": "index.ts" + } +} diff --git a/src/locale/lib/package.json b/src/locale/lib/package.json new file mode 100644 index 0000000..13fef71 --- /dev/null +++ b/src/locale/lib/package.json @@ -0,0 +1,10 @@ +{ + "name": "@opentiny/ng-locale", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/locale/lib/project.json b/src/locale/lib/project.json new file mode 100644 index 0000000..6aca8a8 --- /dev/null +++ b/src/locale/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/locale/lib", + "sourceRoot": "src/locale/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/locale"], + "options": { + "project": "src/locale/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/locale"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js locale" + }, + { + "command": "ng default-build locale" + }, + { + "command": "node build/clear-default-theme.js locale" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/locale && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build locale && ng pack locale && node build/publish.js locale --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/locale/lib/src/TiLocale.ts b/src/locale/lib/src/TiLocale.ts new file mode 100644 index 0000000..48308e7 --- /dev/null +++ b/src/locale/lib/src/TiLocale.ts @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Util } from '@opentiny/ng-utils'; + +declare let global: any; + +export class TiLocale { + /** + * 英文语种关键字,关键字均使用中划线形式命名,确保和浏览器保持一致 + */ + public static readonly EN_US: string = 'en-US'; + /** + * 语种关键字 + */ + public static readonly ZH_CN: string = 'zh-CN'; + /** + * 语种关键字 + */ + public static readonly ES_US: string = 'es-US'; + /** + * 语种关键字 + */ + public static readonly FR_FR: string = 'fr-FR'; + /** + * 语种关键字 + */ + public static readonly PT_BR: string = 'pt-BR'; + + // private static localeKey: string = TiLocale.ZH_CN; // 语种关键字 + + /** + * Typescript没有静态代码段,所以这样代替静态代码段 + */ + protected static staticCode: void = TiLocale.initWordsAndLocale(); + /** + * 静态代码段执行:如过word和locale未初始化,那么执行初始化。 + */ + private static initWordsAndLocale(): void { + if (!TiLocale.getWords()) { + // 默认所有语言都打包进来 + TiLocale.setWords({}); + } + if (!TiLocale.getLocale()) { + TiLocale.setLocale(TiLocale.ZH_CN); + } + } + + /** + * Sets words + * @param Words 全量的语言包,由语言名称:语言词条包组成。 + * + * @returns words + */ + public static setWords(words: any): void { + // 设计缺陷:如果tiWords放在类成员变量上,会更好。window在SSR环境找不到。 + if (typeof window !== 'undefined') { + (window as any).tiWords = words; + } else if (typeof global !== 'undefined') { + (global as any).tiWords = words; + } + } + public static getWords(): any { + if (typeof window !== 'undefined') { + return (window as any).tiWords; + } else if (typeof global !== 'undefined') { + return (global as any).tiWords; + } + } + /** + * 设置组件国际化语种 + * @param locale 国际化字符,可通过TiLocale.ZH_CN/TiLocale.EN_US等语种关键字设置参数 + */ + public static setLocale(locale: string): void { + if (typeof window !== 'undefined') { + (window as any).tiLocale = locale; + } else if (typeof global !== 'undefined') { + (global as any).tiLocale = locale; + } + } + public static getLocale(): string { + let locale: string; + if (typeof window !== 'undefined') { + locale = (window as any).tiLocale; + } else if (typeof global !== 'undefined') { + locale = (global as any).tiLocale; + } + + return locale ? locale : TiLocale.ZH_CN; + } + + /** + * 设置组件国际化语种 + * @param localeKey 国际化字符,可通过TiLocale.ZH_CN/TiLocale.EN_US等语种关键字设置参数 + */ + + /** + * @ignore + * 获取当前语言下,组件国际化语种对应的词条集合 + */ + public static getLocaleWords(): any { + return TiLocale.getWords()[TiLocale.getLocale()]; + } + /** + * @ignore + * 获取单个词条的国际化文本/对象 + * 参数: + */ + public static translate(keyValue: string, params?: Array): any { + const keyArr: Array = keyValue.split('.'); + let value: any = TiLocale.getLocaleWords(); + keyArr.forEach((key: string) => { + value = value[key]; + }); + + return Util.formatEntry(value, params); + } + /** + * @ignore + * 获取各组件的词条追加在 tiWords 中 + */ + public static setTiWords(locales): void { + const { en_US, es_US, fr_FR, pt_BR, zh_CN } = locales; + const words = (window as any).tiWords; + TiLocale.setWords({ + 'zh-CN': { ...words['zh-CN'], ...zh_CN }, + 'en-US': { ...words['en-US'], ...en_US }, + 'es-US': { ...words['es-US'], ...es_US }, + 'fr-FR': { ...words['fr-FR'], ...fr_FR }, + 'pt-BR': { ...words['pt-BR'], ...pt_BR } + }); + } +} diff --git a/src/locale/lib/src/TiLocaleFormat.ts b/src/locale/lib/src/TiLocaleFormat.ts new file mode 100644 index 0000000..7d8f4d2 --- /dev/null +++ b/src/locale/lib/src/TiLocaleFormat.ts @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * @ignore + */ +import { formatDate, formatNumber, getLocaleCurrencySymbol, getLocaleId, getLocaleNumberSymbol, NumberSymbol } from '@angular/common'; +import { TiLocale } from './TiLocale'; +export class TiLocaleFormat { + private static readonly TIME_FORMAT: string = 'mediumTime'; // 'h:mm:ss a' (e.g. 9:03:01 AM) + private static readonly DATE_FORMAT: string = 'mediumDate'; // 'MMM d, y' (e.g. Jun 15, 2015). + private static readonly DATETIME_FORMAT: string = 'medium'; // 'MMM d, y, h:mm:ss a' (e.g. Jun 15, 2015, 9:03:01 AM) + private static readonly DEFAULT_LOCALE: string = 'en'; + /** + * 获取语种关键字,默认为'en' + * @return 语种关键字 + */ + private static getLocaleKey(): string { + let locale: string = TiLocale.getLocale(); + try { + // local信息未注册情况下,getLocaleId该方法会报错,因此使用try catch的方式 + getLocaleId(locale); + } catch (error) { + locale = TiLocaleFormat.DEFAULT_LOCALE; + } + + return locale; + } + /** + * @description 对国际化数字进行处理(包含小数点) + * @param: number 国际化数字 + * @param: isIntegerValid 只针对integer的情况 + * @return 返回数字信息 + */ + private static parseNumWithDecimal(number: string, isIntegerValid?: boolean): any { + const groupSep: string = TiLocaleFormat.getNumberSymbol('Group'); + const decimalSep: string = TiLocaleFormat.getNumberSymbol('Decimal'); + const numberReg: RegExp = new RegExp('^(?:-?\\d*|-?\\d(?:\\' + groupSep + '?\\d*)+)?(?:\\' + decimalSep + '\\d*)?$'); + const numberFormat: string = number; + + if (!numberReg.test(numberFormat)) { + return NaN; + } + const groupReg: RegExp = new RegExp('\\' + groupSep, 'g'); + const decimalReg: RegExp = new RegExp('\\' + decimalSep, 'g'); + const numberFormatString: string = numberFormat.replace(groupReg, '').replace(decimalReg, '.'); + const numberFloat: number = parseFloat(numberFormatString); + /* + * *只针对integer的情况 + * 截取转换前的小数位字符串numFormatSlice + * 转换前的小数位字符串长度numFormatCount + * 转换后的小数位为0,就添加小数位个数的0,返回值是string类型 + * */ + + if (isIntegerValid && numberFormatString.indexOf('.') !== -1) { + return numberFormatString; + } + + return numberFloat; + } + /** + * 获取数字规则信息 + * key 规则关键字 + */ + public static getNumberSymbol(key: string): string { + const standardNumber: number = 10000.0; + const localeNumber: string = TiLocaleFormat.formatNumber(standardNumber, '1.1-1'); + + return key === 'Group' ? localeNumber.charAt(2) : localeNumber.charAt(6); + } + /** + * @description 对数字进行国际化处理 + * @param: number 数字 + * @param: numberFormat 数字格式化形式 + * @return 返回格式化后的信息 + */ + public static formatNumber(number: number, format: string): string { + let options: any; + if (format) { + options = { + minimumIntegerDigits: format.substring(0, format.indexOf('.')), + minimumFractionDigits: format.substring(format.indexOf('.') + 1, format.indexOf('-')), + maximumFractionDigits: format.substring(format.indexOf('-') + 1) + }; + } + + // TODO: new Intl.NumberFormat es西语下整数部分有四位时,本地化数字没有千位分隔 + const res: string = new Intl.NumberFormat(TiLocale.getLocale(), options).format(number); + + return res; + } + + /** + * @description 对国际化数字进行标准化处理 + * @param: number 国际化数字 + * @return 返回数字信息 + */ + public static parseNumber(number: string): number { + return parseFloat(TiLocaleFormat.parseNumWithDecimal(number)); + } + + /** + * @description 对时间进行国际化处理 + * @param: time 时间 + * @param: format 时间格式 + * @return 返回格式化后的信息 + */ + public static formatTime(time: string | number | Date, format?: string, timezone?: string): string { + return formatDate(time, format || TiLocaleFormat.TIME_FORMAT, TiLocaleFormat.getLocaleKey(), timezone); + } + + /** + * @description 对日期进行国际化处理 + * @param date 日期 + * @param format 日期格式 + * @return 返回格式化后的信息 + */ + public static formatDate(date: Date, format?: string, timezone?: string): string { + return formatDate(date, format || TiLocaleFormat.DATE_FORMAT, TiLocaleFormat.getLocaleKey(), timezone); + } + + /** + * @description 对时间日期进行国际化处理 + * @param: 时间日期 + * @param: 时间日期格式 + * @return 返回格式化后的信息 + */ + public static formatDateTime(dateTime: Date, format?: string, timezone?: string): string { + return formatDate(dateTime, format || TiLocaleFormat.DATETIME_FORMAT, TiLocaleFormat.getLocaleKey(), timezone); + } + + /** + * 功能描述:去除千位分隔符,得到标准化数字 + * value: 带有千位分隔符的数据 + */ + public static deleteGroupSep(value: any): string { + let res: any; + res = String(value); + const groupSep: string = TiLocaleFormat.getNumberSymbol('Group'); + const groupSepReg: any = new RegExp('\\' + groupSep, 'g'); + + return res.replace(groupSepReg, ''); + } + /** + * 检测format接口配置是否合法。目前spinner和inputnumber组件会用到 + */ + public static isInvalidFormat(format: string): boolean { + if (typeof format !== 'string' || format.charAt(0).toUpperCase() !== 'N' || Number.isNaN(parseInt(format.slice(1), 10))) { + return true; + } + + return false; + } +} diff --git a/src/locale/lib/src/TiLocaleModule.ts b/src/locale/lib/src/TiLocaleModule.ts new file mode 100644 index 0000000..0aa05ca --- /dev/null +++ b/src/locale/lib/src/TiLocaleModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiTranslatePipe } from './TiTranslatePipe'; +/** + * @ignore + */ +@NgModule({ + declarations: [TiTranslatePipe], + exports: [TiTranslatePipe] +}) +export class TiLocaleModule {} +export { TiLocale } from './TiLocale'; +export { TiLocaleFormat } from './TiLocaleFormat'; +export { TiTranslatePipe } from './TiTranslatePipe'; diff --git a/src/locale/lib/src/TiTranslatePipe.ts b/src/locale/lib/src/TiTranslatePipe.ts new file mode 100644 index 0000000..563d8ce --- /dev/null +++ b/src/locale/lib/src/TiTranslatePipe.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Pipe, PipeTransform } from '@angular/core'; +import { TiLocale } from './TiLocale'; + +/** + * @ignore + */ +@Pipe({ + name: 'tiTranslate' +}) +export class TiTranslatePipe implements PipeTransform { + transform(keyValue: string, params?: Array): string { + return TiLocale.translate(keyValue, params); + } +} diff --git a/src/menu/demo/karma.conf.js b/src/menu/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/menu/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/menu/demo/project.json b/src/menu/demo/project.json new file mode 100644 index 0000000..86ff6ce --- /dev/null +++ b/src/menu/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/menu/demo", + "sourceRoot": "src/menu/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/menu", + "index": "src/menu/demo/src/index.html", + "main": "src/menu/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/menu/demo/tsconfig.app.json", + "assets": ["src/menu/demo/src/favicon.ico", "src/menu/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "menu-demo:build:production" + }, + "development": { + "browserTarget": "menu-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js menu" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/menu/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/menu/demo/tsconfig.spec.json", + "karmaConfig": "src/menu/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/menu/demo/src/app/AppComponent.ts b/src/menu/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/menu/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/menu/demo/src/app/AppModule.ts b/src/menu/demo/src/app/AppModule.ts new file mode 100644 index 0000000..52a29ae --- /dev/null +++ b/src/menu/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { MenuTestModule } from './menu/MenuTestModule'; + +@NgModule({ + imports: [ + MenuTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/menu/demo/src/app/IndexComponent.ts b/src/menu/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..63e610e --- /dev/null +++ b/src/menu/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { MenuTestModule } from './menu/MenuTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = MenuTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/menu/demo/src/app/app.html b/src/menu/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/menu/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/menu/demo/src/app/menu/MenuBasicComponent.ts b/src/menu/demo/src/app/menu/MenuBasicComponent.ts new file mode 100644 index 0000000..59e8a96 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuBasicComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-basic.html' +}) +export class MenuBasicComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuBeforeopenComponent.ts b/src/menu/demo/src/app/menu/MenuBeforeopenComponent.ts new file mode 100644 index 0000000..516926c --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuBeforeopenComponent.ts @@ -0,0 +1,46 @@ +import { Component } from '@angular/core'; +import { TiMenuComponent, TiMenuItem } from '@opentiny/ng'; +@Component({ + templateUrl: './menu-beforeopen.html' +}) +export class MenuBeforeopenComponent { + items: Array = []; + onBeforeOpen(menucomp: TiMenuComponent): void { + setTimeout(() => { + // 模拟一秒后才获取到菜单数据 + this.items = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; + menucomp.open(); + }, 1000); + } +} diff --git a/src/menu/demo/src/app/menu/MenuBorderComponent.ts b/src/menu/demo/src/app/menu/MenuBorderComponent.ts new file mode 100644 index 0000000..c778c1b --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuBorderComponent.ts @@ -0,0 +1,55 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; +@Component({ + templateUrl: './menu-border.html' +}) +export class MenuBorderComponent { + disabled: boolean = false; + panelAlign: string = 'left'; + options: Array = [ + { + label: '一级菜单', + tip: '远程登录', + tipPosition: 'right', + click(): void { + console.log(this.label + 'clicked'); + } + }, + { + label: '变更规格', + disabled: true + }, + { + label: '制作镜像', + tip: '远程登录', + tipPosition: 'left' + }, + { + label: '云服务器这是一个很长的选项只有云服务器处于关机状态才能执行该操作' + }, + { + label: '规格' + }, + { + label: '镜像', + disabled: true + } + ]; + + onSelect(item: TiMenuItem): void { + if (typeof item.click === 'function') { + item.click(); + + return; + } + console.log(item); + } + + changeDisabled(): void { + this.disabled = !this.disabled; + } + + changePanelAlign(): void { + this.panelAlign = 'right'; + } +} diff --git a/src/menu/demo/src/app/menu/MenuButtoncolorComponent.ts b/src/menu/demo/src/app/menu/MenuButtoncolorComponent.ts new file mode 100644 index 0000000..1261ef7 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuButtoncolorComponent.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; +@Component({ + templateUrl: './menu-buttoncolor.html' +}) +export class MenuButtoncolorComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuDefaultComponent.ts b/src/menu/demo/src/app/menu/MenuDefaultComponent.ts new file mode 100644 index 0000000..d09b764 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuDefaultComponent.ts @@ -0,0 +1,78 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-default.html' +}) +export class MenuDefaultComponent { + labelKey: string = 'title'; + panelMaxWidth: string = '200px'; + + options: Array = []; + options1: Array = this.options; + options2: Array = this.options; + constructor() { + const num: number = 5; + + for (let i: number = 1; i < num; i++) { + const option: TiMenuItem = { + label: '', + title: '', + disabled: false, + children: [] + }; + if (i === 1) { + option.label = '定制行。。。。。。。。。。。。。。。' + '。。。。。。。。。。。。。。。。。。。。。。。'; + option.title = '这是定制行:显示指定key'; + option.disabled = true; + } else { + option.label = `第${i}条item1`; + option.title = `第${i}条title1`; + option.disabled = false; + } + + option.children = []; + for (let j: number = 1; j < num; j++) { + const option1: TiMenuItem = { + label: '', + title: '', + disabled: false, + children: [] + }; + + if (j === 2) { + option1.label = '定制行。。。。。。。。。。。。'; + option1.title = '定制行。。。。。。。。。。。。'; + option1.disabled = false; + } else { + option1.label = `第${j}条item2`; + option1.title = `第${j}条title2`; + option1.disabled = true; + } + + option1.children = []; + for (let k: number = 1; k < num; k++) { + const option2: TiMenuItem = { + label: '', + title: '', + disabled: false, + groupId: '' + }; + option2.label = `第${k}条item3`; + option2.title = `第${k}条title3`; + option2.disabled = false; + option2.groupId = k < 2 ? 'a' : k > 3 ? 'b' : 'c'; + option1.children.push(option2); + } + + option.children.push(option1); + } + + this.options.push(option); + } + } + + onSelect(item: TiMenuItem): void { + console.log(item); + } +} diff --git a/src/menu/demo/src/app/menu/MenuDisabledComponent.ts b/src/menu/demo/src/app/menu/MenuDisabledComponent.ts new file mode 100644 index 0000000..86881c7 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuDisabledComponent.ts @@ -0,0 +1,42 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; +@Component({ + templateUrl: './menu-disabled.html' +}) +export class MenuDisabledComponent { + disabled: boolean = true; + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机', + disabled: true + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组', + disabled: true + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuEventComponent.ts b/src/menu/demo/src/app/menu/MenuEventComponent.ts new file mode 100644 index 0000000..2ef86ab --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuEventComponent.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-event.html' +}) +export class MenuEventComponent { + myLogs: Array = []; + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; + + onSelect(item: any): void { + this.myLogs = [...this.myLogs, `onSelect() event = ${JSON.stringify(item)}`]; + } +} diff --git a/src/menu/demo/src/app/menu/MenuGroupComponent.ts b/src/menu/demo/src/app/menu/MenuGroupComponent.ts new file mode 100644 index 0000000..918fcb2 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuGroupComponent.ts @@ -0,0 +1,42 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; +@Component({ + templateUrl: './menu-group.html' +}) +export class MenuGroupComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机', + groupId: 'action' + }, + { + id: 'powerOff', + label: '关机', + groupId: 'action' + }, + { + id: 'reboot', + label: '重启', + groupId: 'action' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuIdComponent.ts b/src/menu/demo/src/app/menu/MenuIdComponent.ts new file mode 100644 index 0000000..79b5a28 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuIdComponent.ts @@ -0,0 +1,76 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-id.html' +}) +export class MenuIdComponent { + panelMaxWidth: string = '200px'; + + options: Array = []; + options1: Array = this.options; + + constructor() { + const num: number = 5; + + for (let i: number = 1; i < num; i++) { + const option: TiMenuItem = { + label: '', + title: '', + disabled: false, + id: i, + children: [] + }; + if (i === 1) { + option.label = '定制行。。。。。。。。。。。。。。。' + '。。。。。。。。。。。。。。。。。。。。。。。'; + option.title = '这是定制行:显示指定key'; + option.disabled = true; + } else { + option.label = `第${i}条item1`; + option.title = `第${i}条title1`; + option.disabled = false; + } + + option.children = []; + for (let j: number = 1; j < num; j++) { + const option1: TiMenuItem = { + label: '', + title: '', + id: `${i}_${j}`, + disabled: false, + children: [] + }; + + if (j === 2) { + option1.label = '定制行。。。。。。。。。。。。'; + option1.title = '定制行。。。。。。。。。。。。'; + option1.disabled = false; + } else { + option1.label = `第${j}条item2`; + option1.title = `第${j}条title2`; + option1.disabled = true; + } + + option1.children = []; + for (let k: number = 1; k < num; k++) { + const option2: TiMenuItem = { + label: '', + title: '', + id: `${i}_${j}_${k}`, + disabled: false, + groupId: '' + }; + option2.label = `第${k}条item3`; + option2.title = `第${k}条title3`; + option2.disabled = false; + option2.groupId = k < 2 ? 'a' : k > 3 ? 'b' : 'c'; + option1.children.push(option2); + } + + option.children.push(option1); + } + + this.options.push(option); + } + } +} diff --git a/src/menu/demo/src/app/menu/MenuLabelkeyComponent.ts b/src/menu/demo/src/app/menu/MenuLabelkeyComponent.ts new file mode 100644 index 0000000..e330008 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuLabelkeyComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-labelkey.html' +}) +export class MenuLabelkeyComponent { + items: Array = [ + { + id: 'telnet', + title: '远程登录' + }, + { + id: 'powerOn', + title: '开机' + }, + { + id: 'powerOff', + title: '关机' + }, + { + id: 'reboot', + title: '重启' + }, + { + id: 'setting', + title: '网络设置', + children: [ + { + id: 'modify', + title: '更改安全组' + }, + { + id: 'toggle', + title: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuPanelalignComponent.ts b/src/menu/demo/src/app/menu/MenuPanelalignComponent.ts new file mode 100644 index 0000000..e5dc99d --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuPanelalignComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-panelalign.html' +}) +export class MenuPanelalignComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuPanelstyleComponent.ts b/src/menu/demo/src/app/menu/MenuPanelstyleComponent.ts new file mode 100644 index 0000000..8b45f0f --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuPanelstyleComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-panelstyle.html' +}) +export class MenuPanelstyleComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机' + }, + { + id: 'reboot', + label: '重启' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuTempleteComponent.ts b/src/menu/demo/src/app/menu/MenuTempleteComponent.ts new file mode 100644 index 0000000..4950a86 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuTempleteComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-templete.html' +}) +export class MenuTempleteComponent { + items: Array = [ + { + id: 'refresh', + label: '刷新', + iconName: 'refresh', + tipIconName: 'warn', + customTip: '确定要刷新吗?', + tipPosition: 'left' + }, + { + id: 'delete', + label: '删除', + iconName: 'delete-1', + tipIconName: 'warn', + customTip: '确定要删除吗?', + tipPosition: 'left' + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuTempleteTestComponent.ts b/src/menu/demo/src/app/menu/MenuTempleteTestComponent.ts new file mode 100644 index 0000000..13fd570 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuTempleteTestComponent.ts @@ -0,0 +1,74 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-templete-test.html' +}) +export class MenuTempleteTestComponent { + labelKey: string = 'title'; + panelMaxWidth: string = '200px'; + + options: Array = []; + options1: Array = this.options; + options2: Array = this.options; + constructor() { + const num: number = 5; + + for (let i: number = 1; i < num; i++) { + const option: TiMenuItem = { + label: '', + title: '', + disabled: false, + children: [] + }; + if (i === 1) { + option.label = '定制行。。。。。。。。。。。。。。。' + '。。。。。。。。。。。。。。。。。。。。。。。'; + option.title = '这是定制行:显示指定key'; + option.disabled = true; + } else { + option.label = `第${i}条item1`; + option.title = `第${i}条title1`; + option.disabled = false; + } + + option.children = []; + for (let j: number = 1; j < num; j++) { + const option1: TiMenuItem = { + label: '', + title: '', + disabled: false, + children: [] + }; + + if (j === 2) { + option1.label = '定制行。。。。。。。。。。。。'; + option1.title = '定制行。。。。。。。。。。。。'; + option1.disabled = false; + } else { + option1.label = `第${j}条item2`; + option1.title = `第${j}条title2`; + option1.disabled = true; + } + + option1.children = []; + for (let k: number = 1; k < num; k++) { + const option2: TiMenuItem = { + label: '', + title: '', + disabled: false, + groupId: '' + }; + option2.label = `第${k}条item3`; + option2.title = `第${k}条title3`; + option2.disabled = false; + option2.groupId = k < 2 ? 'a' : k > 3 ? 'b' : 'c'; + option1.children.push(option2); + } + + option.children.push(option1); + } + + this.options.push(option); + } + } +} diff --git a/src/menu/demo/src/app/menu/MenuTestModule.ts b/src/menu/demo/src/app/menu/MenuTestModule.ts new file mode 100644 index 0000000..1b61351 --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuTestModule.ts @@ -0,0 +1,100 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { TiIconModule, TiMenuModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { MenuBorderComponent } from './MenuBorderComponent'; +import { MenuBasicComponent } from './MenuBasicComponent'; +import { MenuDisabledComponent } from './MenuDisabledComponent'; +import { MenuGroupComponent } from './MenuGroupComponent'; +import { MenuPanelalignComponent } from './MenuPanelalignComponent'; +import { MenuBeforeopenComponent } from './MenuBeforeopenComponent'; +import { MenuPanelstyleComponent } from './MenuPanelstyleComponent'; +import { MenuTipsComponent } from './MenuTipsComponent'; +import { MenuIdComponent } from './MenuIdComponent'; +import { MenuLabelkeyComponent } from './MenuLabelkeyComponent'; +import { MenuEventComponent } from './MenuEventComponent'; +import { MenuTempleteComponent } from './MenuTempleteComponent'; +import { MenuTempleteTestComponent } from './MenuTempleteTestComponent'; +import { MenuButtoncolorComponent } from './MenuButtoncolorComponent'; +import { MenuDefaultComponent } from './MenuDefaultComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiMenuModule, TiIconModule, DemoLogModule, RouterModule.forChild(MenuTestModule.ROUTES)], + declarations: [ + MenuBorderComponent, + MenuBasicComponent, + MenuDisabledComponent, + MenuGroupComponent, + MenuPanelalignComponent, + MenuBeforeopenComponent, + MenuPanelstyleComponent, + MenuIdComponent, + MenuTipsComponent, + MenuLabelkeyComponent, + MenuEventComponent, + MenuTempleteComponent, + MenuTempleteTestComponent, + MenuButtoncolorComponent, + MenuDefaultComponent + ] +}) +export class MenuTestModule { + static readonly LINKS: Array = [{ href: 'components/TiMenuComponent.html', label: 'Menu' }]; + static readonly ROUTES: Routes = [ + { + path: 'menu/menu-basic', + component: MenuBasicComponent + }, + { + path: 'menu/menu-labelkey', + component: MenuLabelkeyComponent + }, + { + path: 'menu/menu-disabled', + component: MenuDisabledComponent + }, + { + path: 'menu/menu-group', + component: MenuGroupComponent + }, + { + path: 'menu/menu-panelalign', + component: MenuPanelalignComponent + }, + { + path: 'menu/menu-beforeopen', + component: MenuBeforeopenComponent + }, + { + path: 'menu/menu-panelstyle', + component: MenuPanelstyleComponent + }, + { + path: 'menu/menu-tips', + component: MenuTipsComponent + }, + { + path: 'menu/menu-border', + component: MenuBorderComponent + }, + { + path: 'menu/menu-templete', + component: MenuTempleteComponent + }, + { + path: 'menu/menu-event', + component: MenuEventComponent + }, + { path: 'menu/menu-id', component: MenuIdComponent }, + { path: 'menu/menu-templete-test', component: MenuTempleteTestComponent }, + { path: 'menu/menu-buttoncolor', component: MenuButtoncolorComponent }, + { + path: 'menu/menu-default', + component: MenuDefaultComponent + } + ]; +} diff --git a/src/menu/demo/src/app/menu/MenuTipsComponent.ts b/src/menu/demo/src/app/menu/MenuTipsComponent.ts new file mode 100644 index 0000000..4d43d2e --- /dev/null +++ b/src/menu/demo/src/app/menu/MenuTipsComponent.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './menu-tips.html' +}) +export class MenuTipsComponent { + items: Array = [ + { + id: 'telnet', + label: '远程登录' + }, + { + id: 'powerOn', + label: '开机' + }, + { + id: 'powerOff', + label: '关机', + disabled: true, + tip: '已冻结,不能执行此操作。请联系客服。', + tipPosition: 'left' + }, + { + id: 'reboot', + label: '重启', + tip: '重新启动服务器。', + tipPosition: 'right' + }, + { + id: 'setting', + label: '网络设置', + children: [ + { + id: 'modify', + label: '更改安全组' + }, + { + id: 'toggle', + label: '切换VPC' + } + ] + } + ]; +} diff --git a/src/menu/demo/src/app/menu/menu-basic.html b/src/menu/demo/src/app/menu/menu-basic.html new file mode 100644 index 0000000..858c366 --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-basic.html @@ -0,0 +1,2 @@ +更多 +更多 diff --git a/src/menu/demo/src/app/menu/menu-beforeopen.html b/src/menu/demo/src/app/menu/menu-beforeopen.html new file mode 100644 index 0000000..2f4cedf --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-beforeopen.html @@ -0,0 +1 @@ +更多 diff --git a/src/menu/demo/src/app/menu/menu-border.html b/src/menu/demo/src/app/menu/menu-border.html new file mode 100644 index 0000000..2c0dc25 --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-border.html @@ -0,0 +1,10 @@ +

    描述

    +

    Menu菜单组件: 有边框的Menu组件(原buttonselect组件)展示,需添加hasborder属性

    +

    示例

    + +
    + 更多 +
    + + + diff --git a/src/menu/demo/src/app/menu/menu-buttoncolor.html b/src/menu/demo/src/app/menu/menu-buttoncolor.html new file mode 100644 index 0000000..129ddce --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-buttoncolor.html @@ -0,0 +1 @@ +更多 diff --git a/src/menu/demo/src/app/menu/menu-default.html b/src/menu/demo/src/app/menu/menu-default.html new file mode 100644 index 0000000..678ffd2 --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-default.html @@ -0,0 +1,38 @@ +

    描述

    +

    Menu菜单组件: 基础显示

    +

    导入

    +

    import {{ '{' }} TiMenuModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    + +

    1.默认显示label

    +
    + 导出 +
    + +

    2.labelKey:指定要显示文字的键值

    +
    + 导出 +
    + +

    3.自定义 item 模板(添加 #item 标签

    +
    + + 导出 + + + 自定义item模板 - index: {{i}} + item的label: {{item.label}} + + +
    + +

    4.兼容旧版模板测试(10.0.3 之前版本)

    +

    自定义 item 模板(未添加 #item 标签

    +
    + + 导出 + + + 自定义item模板 - index: {{i}} + item的label: {{item.label}} + + +
    diff --git a/src/menu/demo/src/app/menu/menu-disabled.html b/src/menu/demo/src/app/menu/menu-disabled.html new file mode 100644 index 0000000..e369b53 --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-disabled.html @@ -0,0 +1,3 @@ +更多 +更多 +更多 diff --git a/src/menu/demo/src/app/menu/menu-event.html b/src/menu/demo/src/app/menu/menu-event.html new file mode 100644 index 0000000..52c1414 --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-event.html @@ -0,0 +1,3 @@ +更多 + + diff --git a/src/menu/demo/src/app/menu/menu-group.html b/src/menu/demo/src/app/menu/menu-group.html new file mode 100644 index 0000000..05871e1 --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-group.html @@ -0,0 +1 @@ +更多 diff --git a/src/menu/demo/src/app/menu/menu-id.html b/src/menu/demo/src/app/menu/menu-id.html new file mode 100644 index 0000000..19629ef --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-id.html @@ -0,0 +1,13 @@ +

    描述

    +

    id属性和组件id配合使用,用于自定义每个菜单的id

    +

    示例

    + +

    1.默认:不设置id,设置item.id

    +
    + 更多 +
    + +

    2.设置id,设置item.id

    +
    + 更多 +
    diff --git a/src/menu/demo/src/app/menu/menu-labelkey.html b/src/menu/demo/src/app/menu/menu-labelkey.html new file mode 100644 index 0000000..b13a8c3 --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-labelkey.html @@ -0,0 +1 @@ +更多 diff --git a/src/menu/demo/src/app/menu/menu-panelalign.html b/src/menu/demo/src/app/menu/menu-panelalign.html new file mode 100644 index 0000000..7aa871d --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-panelalign.html @@ -0,0 +1 @@ +更多 diff --git a/src/menu/demo/src/app/menu/menu-panelstyle.html b/src/menu/demo/src/app/menu/menu-panelstyle.html new file mode 100644 index 0000000..1b7430b --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-panelstyle.html @@ -0,0 +1 @@ +更多 diff --git a/src/menu/demo/src/app/menu/menu-templete-test.html b/src/menu/demo/src/app/menu/menu-templete-test.html new file mode 100644 index 0000000..5e4d8de --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-templete-test.html @@ -0,0 +1,14 @@ +

    描述

    +

    自定义模板测试

    +

    示例

    +

    1.兼容旧版模板测试(10.0.3 之前版本)

    +

    自定义 item 模板(未添加 #item 标签

    +
    + + 更多 + + + 自定义item模板 - index: {{i}} + item的label: {{item.label}} + + +
    diff --git a/src/menu/demo/src/app/menu/menu-templete.html b/src/menu/demo/src/app/menu/menu-templete.html new file mode 100644 index 0000000..aea783c --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-templete.html @@ -0,0 +1,11 @@ + + 更多 + + + {{item.label}} + + + + {{item.customTip}} + + diff --git a/src/menu/demo/src/app/menu/menu-tips.html b/src/menu/demo/src/app/menu/menu-tips.html new file mode 100644 index 0000000..441185f --- /dev/null +++ b/src/menu/demo/src/app/menu/menu-tips.html @@ -0,0 +1 @@ +更多 diff --git a/src/menu/demo/src/app/menu/webdoc/menu-demos.js b/src/menu/demo/src/app/menu/webdoc/menu-demos.js new file mode 100644 index 0000000..736ccde --- /dev/null +++ b/src/menu/demo/src/app/menu/webdoc/menu-demos.js @@ -0,0 +1,166 @@ +export default { + column: '2', + demos: [ + { + demoId: 'menu-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Menu 组件的基本用法。', + 'en-US': '

    basic

    ', + }, + apis: [ + 'TiMenuComponent.properties.items', + 'TiMenuItem.properties.children', + 'TiMenuItem.properties.id', + 'TiMenuItem.properties.label', + ], + }, + { + demoId: 'menu-buttoncolor', + name: { + 'zh-CN': '按钮颜色', + 'en-US': '', + }, + desc: { + 'zh-CN': + '

    通过属性color配置按钮下拉类型菜单的按钮颜色,包括defaultdanger。', + 'en-US': '', + }, + apis: ['TiMenuComponent.properties.color'], + }, + { + demoId: 'menu-labelkey', + name: { + 'zh-CN': '显示文本', + 'en-US': 'labelkey', + }, + desc: { + 'zh-CN': '通过属性labelKey配置要显示文本的键值。', + 'en-US': '

    labelKey

    ', + }, + apis: ['TiMenuComponent.properties.labelKey'], + }, + { + demoId: 'menu-group', + name: { + 'zh-CN': '分组', + 'en-US': 'groupId', + }, + desc: { + 'zh-CN': '通过item.groupId配置下拉菜单项分组。', + 'en-US': '

    groupId

    ', + }, + apis: ['TiMenuItem.properties.groupId'], + }, + { + demoId: 'menu-panelalign', + name: { + 'zh-CN': '下拉面板对齐方向', + 'en-US': 'panelAlign', + }, + desc: { + 'zh-CN': + '通过属性panelAlign配置下拉面板对齐方向,包括leftright。', + 'en-US': '

    panelAlign

    ', + }, + apis: ['TiMenuComponent.properties.panelAlign'], + }, + { + demoId: 'menu-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled', + }, + desc: { + 'zh-CN': + '

    通过属性disabled配置是否为禁用状态;通过属性item.disabled配置当前下拉选项是否为禁用状态。', + 'en-US': '

    disabled

    ', + }, + apis: [ + 'TiMenuComponent.properties.disabled', + 'TiMenuItem.properties.disabled', + ], + }, + { + demoId: 'menu-tips', + name: { + 'zh-CN': '提示信息', + 'en-US': 'menu tips', + }, + desc: { + 'zh-CN': + '通过属性item.tip配置当前菜单项提示信息;通过属性item.tipPosition配置当前菜单项提示信息弹出方向;通过属性tipMaxWidth配置各菜单项提示信息最大宽度。', + 'en-US': '

    menu tips description

    ', + }, + apis: [ + 'TiMenuItem.properties.tip', + 'TiMenuItem.properties.tipPosition', + 'TiMenuComponent.properties.tipMaxWidth', + ], + }, + { + demoId: 'menu-panelstyle', + name: { + 'zh-CN': '下拉面板样式', + 'en-US': 'menu panelstyle', + }, + desc: { + 'zh-CN': + '通过属性panelMaxWidth配置下拉面板最大宽度;通过属性panelMaxHeight配置下拉面板最大高度。', + 'en-US': '

    menu panelstyle description

    ', + }, + apis: [ + 'TiMenuComponent.properties.panelMaxWidth', + 'TiMenuComponent.properties.panelMaxHeight', + ], + }, + { + demoId: 'menu-beforeopen', + name: { + 'zh-CN': '懒加载', + 'en-US': 'lazy', + }, + desc: { + 'zh-CN': + '当下拉面板展开前触发beforeOpen事件,传递出去的参数为组件实例(TiMenuComponent)。', + 'en-US': '

    lazy

    ', + }, + apis: [ + 'TiMenuComponent.events.beforeOpen', + 'TiMenuComponent.methods.open', + ], + }, + { + demoId: 'menu-templete', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'custom template', + }, + desc: { + 'zh-CN': + '通过#item配置下拉面板中选项的模板;通过#tip配置当前菜单项提示信息的模板。', + 'en-US': '

    custom template description

    ', + }, + apis: [ + 'TiMenuComponent.slots.itemTemplate', + 'TiMenuComponent.slots.tipTemplate', + ], + }, + { + demoId: 'menu-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event', + }, + desc: { + 'zh-CN': + '当选中菜单项时触发select事件,传递出去的参数为当前选中项的数据。', + 'en-US': '

    event

    ', + }, + apis: ['TiMenuComponent.events.select'], + }, + ], +}; diff --git a/src/menu/demo/src/app/menu/webdoc/menu.cn.md b/src/menu/demo/src/app/menu/webdoc/menu.cn.md new file mode 100644 index 0000000..4a1efd8 --- /dev/null +++ b/src/menu/demo/src/app/menu/webdoc/menu.cn.md @@ -0,0 +1,24 @@ +--- +title: Menu 下拉菜单 +--- +# Menu 下拉菜单 + +
    + +提供下拉菜单列表、按钮类型下拉菜单列表的组件。   + +```typescript +import { TiMenuModule } from '@opentiny/ng'; +``` + +
    + +
    + +提供下拉菜单列表、按钮类型下拉菜单列表的组件。   + +```typescript +import { TiMenuModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/menu/demo/src/app/menu/webdoc/menu.en.md b/src/menu/demo/src/app/menu/webdoc/menu.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/menu/demo/src/app/menu/webdoc/menu.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/menu/demo/src/favicon.ico b/src/menu/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/menu/demo/src/index.html b/src/menu/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/menu/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/menu/demo/src/main.ts b/src/menu/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/menu/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/menu/demo/test.ts b/src/menu/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/menu/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/menu/demo/tsconfig.app.json b/src/menu/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/menu/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/menu/demo/tsconfig.spec.json b/src/menu/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/menu/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/menu/lib/index.ts b/src/menu/lib/index.ts new file mode 100644 index 0000000..7643404 --- /dev/null +++ b/src/menu/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiMenuModule'; diff --git a/src/menu/lib/ng-package.json b/src/menu/lib/ng-package.json new file mode 100644 index 0000000..448e7b0 --- /dev/null +++ b/src/menu/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/menu", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/menu/lib/package.json b/src/menu/lib/package.json new file mode 100644 index 0000000..a8cbb39 --- /dev/null +++ b/src/menu/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-menu", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-list": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/menu/lib/project.json b/src/menu/lib/project.json new file mode 100644 index 0000000..c45abc4 --- /dev/null +++ b/src/menu/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/menu/lib", + "sourceRoot": "src/menu/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/menu"], + "options": { + "project": "src/menu/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/menu"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js menu" + }, + { + "command": "ng default-build menu" + }, + { + "command": "node build/clear-default-theme.js menu" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/menu && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build menu && ng pack menu && node build/publish.js menu --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/menu/lib/src/TiMenuComponent.ts b/src/menu/lib/src/TiMenuComponent.ts new file mode 100644 index 0000000..6c4e47c --- /dev/null +++ b/src/menu/lib/src/TiMenuComponent.ts @@ -0,0 +1,395 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ContentChild, + ElementRef, + EventEmitter, + Input, + Output, + QueryList, + SimpleChanges, + TemplateRef, + ViewChild, + ViewChildren, + ChangeDetectionStrategy +} from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiDropComponent } from '@opentiny/ng-drop'; +import { TiMenuListComponent } from './TiMenuListComponent'; +import { TiKeymap } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; +import { TiMenuItem } from './TiMenuItem'; +// TODO: beforOpen延时打开,需要动态转圈等待 + +/** + * Menu菜单组件 + * + * 提供了一种方便的生成菜单下拉列表的方式,提供分组、自定义内容等功能。 + * + */ +@Component({ + selector: 'ti-menu', + templateUrl: './menu.html', + styleUrls: ['./menu.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '(blur)': 'onBlur()', + '[class.ti-menu-danger-button]': 'color === "danger"' + } +}) +export class TiMenuComponent extends TiFormComponent { + // 本应继承自BaseComp,但是想要A标签焦点功能 + /** + * 下拉面板与按钮对齐方式 + */ + @Input() panelAlign: 'left' | 'right' = 'right'; + /** + * 必选,下拉面板数据集 + */ + @Input() items: Array; + /** + * 下拉面板最大宽度,超过时内容换行显示 + */ + @Input() panelMaxWidth: string = '9999px'; + /** + * 下拉面板最大高度,超过时面板出现竖向滚动条 + */ + @Input() panelMaxHeight: string = '9999px'; + /** + * 下拉面板要展示的键值 + */ + @Input() labelKey: string = 'label'; + /** + * 按钮颜色(配置 buttonselect 属性时生效) + */ + @Input() color: 'default' | 'danger' = 'default'; + /** + * tip 最大宽度 + */ + @Input() tipMaxWidth: string; + /** + * @ignore + */ + @ViewChild('mytoggle', { static: true }) mytoggleRef: ElementRef; + /** + * @ignore + */ + @ViewChildren(TiDropComponent) dropComs: QueryList; + /** + * @ignore + */ + @ViewChildren(TiMenuListComponent) listComs: QueryList; + /** + * @ignore + */ + @ContentChild(TemplateRef, { static: true }) firstTemplate: TemplateRef; + /** + * 下拉选项区域的模板 + */ + @ContentChild('item', { static: true }) itemTemplate: TemplateRef; + /** + * tip 提示区域的模板 + */ + @ContentChild('tip', { static: false }) tipTemplate: TemplateRef; + /** + * 选中菜单项时触发的回调,参数:当前选中项数据 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + /** + * 打开面板前触发的回调,一般用于懒加载场景,参数:组件实例 TiMenuComponent + */ + @Output() readonly beforeOpen: EventEmitter = new EventEmitter(); + /** + * @ignore + * itemsArr[0]存放根面板内容,itemArr[1]存放次级面板内容,itemArr[2]存放次次级面板内容 + */ + public itemsArr: Array> = []; + /** + * @ignore + * 与开关距离 + */ + public dominatorSpace: string = '10px'; + /** + * @ignore + * 设置该属性时为buttonselect组件 + */ + public buttonSelect: boolean; + /** + * menulist最大高度 + */ + public menulistMaxHeight: number; + protected versionInfo: string = super.getVersion(packageInfo); + ngOnInit(): void { + // 基类中做了设置宿主id的操作 + super.ngOnInit(); + // 10.1.17 版本之前在标签上写 hasborder 属性会呈现按钮下拉样式,10.1.17 版本之后修改为 buttonselect 属性,也兼容之前的写法 + this.buttonSelect = this.nativeElement.hasAttribute('hasborder') || this.nativeElement.hasAttribute('buttonselect'); + } + ngOnChanges(changes: SimpleChanges): void { + this.setFocusableElems([this.mytoggleRef.nativeElement]); + super.ngOnChanges(changes); + if (changes['items']) { + // 外部修改show属性的处理,初始定义在ngOnInit中处理 + this.itemsArr[0] = this.items; + } + } + /** + * 兼容旧版: + * 10.0.3 版本之前只能内嵌一个模板,无命名。 + * 新版可以内嵌两个模板,示例书写要求都命名(#item,#tip)。 + * 但需要兼容旧版无命名测试用例。 + */ + ngAfterContentInit(): void { + super.ngAfterContentInit(); + // 如果 item 模板为空 && 存在第一个模板,那么把第一个出现的 “非#tip 标签” 的模板作为 item 模板 + if ( + !this.itemTemplate && + this.firstTemplate && + this.firstTemplate.elementRef.nativeElement !== (this.tipTemplate && this.tipTemplate.elementRef.nativeElement) + ) { + this.itemTemplate = this.firstTemplate; + } + } + /** + * 打开面板 + */ + public open(): void { + if (this.disabled) { + return; + } + // 打开面板前重置menulist最大高度 + this.initListMaxHeight(); + this.dropComs.first.show(); + // 使用beforeOpen事件:异步加载数据,调用open事件,需要手动触发变更检测 + if (this.beforeOpen.observers.length > 0) { + this.changeDetectorRef.markForCheck(); + } + + this.listComs.first.hoverOption = null; + // 根据drop的最大高度设置menulist最大高度 + this.restyleListMaxHeight(); + // IE滚动条Bug的监听 + this.listComs.toArray().forEach((listCom) => { + listCom.listenIESrollbarBug(); + }); + } + /** + * 关闭面板 + */ + public close(): void { + this.dropComs.first.hide(); + this.onHover(null, 0); + // 解除IE滚动条Bug的监听 + this.listComs.toArray().forEach((listCom) => { + listCom.unlistenIESrollbarBug(); + }); + } + /** + * @ignore + * + * 切换面板状态:打开/关闭 + */ + public toggle(): void { + this.dropComs.first.isShow ? this.close() : this.open(); + } + /** + * @ignore + */ + public onKeydown(event: KeyboardEvent): void { + switch (event.keyCode) { + case TiKeymap.KEY_SPACE: // 原生SPACE键仅可打开。但Tiny2,3都跟Enter键保持一致 + case TiKeymap.KEY_ENTER: // ENTER键 相当于click + case TiKeymap.KEY_NUMPAD_ENTER: // ENTER键(苹果数字小键盘) + // 第一级已打开,但无选中项。或者第一级没有打开。相当于鼠标点击逻辑。 + if ((this.dropComs.first.isShow && !this.listComs.toArray()[0].hoverOption) || !this.dropComs.first.isShow) { + this.onMousedown(null); // 相当于鼠标点击 + break; // 注意:break在if内,如果if不满足,则走到default,list处理按键 + } + // 上面的回车键, 可能继续走到下面分支。 + // eslint-disable-next-line no-fallthrough + case TiKeymap.KEY_ARROW_LEFT: // 左右箭头,焦点在左右面板转移 + case TiKeymap.KEY_ARROW_RIGHT: // 左右箭头,焦点在左右面板转移 + case TiKeymap.KEY_ARROW_UP: // 上下箭头,上下移动选中项 + case TiKeymap.KEY_ARROW_DOWN: // 上下箭头,上下移动选中项 + for (let i = this.dropComs.length - 1; i >= 0; i--) { + // 寻找末尾面板且有hover,list处理按键 + if (this.dropComs.toArray()[i].isShow && this.listComs.toArray()[i].hoverOption) { + this.onKeydownLastHoverList(event, i); + + // break; //这里触发了for循环的break;并没有走到switch的break;所以改为return; + return; + } + } + // 没有hover的面板,那么第一级面板(肯定是展开的)处理按键 + this.listComs.first.onKeydown(event); + break; + default: + break; + } + } + private onKeydownLastHoverList(event: KeyboardEvent, i: number): void { + switch (event.keyCode) { + case TiKeymap.KEY_ARROW_LEFT: // 左右箭头,焦点在左右面板转移 + case TiKeymap.KEY_ARROW_RIGHT: // 左右箭头,焦点在左右面板转移 + if ( + (this.panelAlign === 'right' && event.keyCode === TiKeymap.KEY_ARROW_LEFT) || + (this.panelAlign === 'left' && event.keyCode === TiKeymap.KEY_ARROW_RIGHT) + ) { + // 次级面板方向键 + if (i + 1 < this.dropComs.length && this.dropComs.toArray()[i + 1].isShow) { + // 次级存在且展开 + this.listComs.toArray()[i + 1].nextOption(false); // 次级面板hover由null改为下一个 + } + } else { + // 上级面板方向键 + this.listComs.toArray()[i].hoverOption = null; + } + break; + case TiKeymap.KEY_SPACE: // 原生SPACE键仅可打开。但Tiny2,3都跟Enter键保持一致 + case TiKeymap.KEY_ENTER: // ENTER键 相当于click + case TiKeymap.KEY_NUMPAD_ENTER: // ENTER键(苹果数字小键盘) + if (!this.listComs.toArray()[i].isGroup(this.listComs.toArray()[i].hoverOption)) { + this.listComs.toArray()[i].onKeydown(event); + } + break; + default: // 上下键等,和上面的回车键也会走到这里。 + this.listComs.toArray()[i].onKeydown(event); + break; + } + } + /** + * @ignore + */ + public onMousedown(event: MouseEvent): void { + if (this.dropComs.first.isShow) { + // 已打开,则关闭 + this.close(); + } else if (this.beforeOpen.observers.length === 0) { + // 下拉菜单收起,且无beforeOpen,则打开 + this.open(); + } else { + // 下拉菜单收起,有beforeOpen + this.beforeOpen.emit(this); + } + } + /** + * @ignore + */ + public onSelect(panelIndex: number): void { + this.select.emit(this.listComs.toArray()[panelIndex].model); + this.close(); + } + /** + * @ignore + */ + public onBlur(): void { + this.close(); + } + /** + * @ignore + */ + public onHover(item: any, panelIndex: number): void { + // 当item为空,表示鼠标移出当前面板,也需要关闭次级面板。 + if (panelIndex + 1 === this.dropComs.length) { + // 当前hover面板,已经是最后一级面板 + return; + } + const dropComArr: Array = this.dropComs.toArray(); + const listComArr: Array = this.listComs.toArray(); + if (item && listComArr[panelIndex].isGroup(item) && !listComArr[panelIndex].isDisabledFn(item)) { + // 根据数据,是否展开下一级 + this.itemsArr[panelIndex + 1] = item.children; // 下一级数据,赋新值。 + listComArr[panelIndex + 1].hoverOption = null; // 下一级面板赋空hoverOption,会触发onHover,进而关闭下一级面板的后续面板。 + // 赋新值以后,等模板有了新的尺寸,再弹出。 + setTimeout(() => { + const currentPanel: HTMLElement = dropComArr[panelIndex].nativeElement; // 当前面板元素 + const nextPanel: HTMLElement = dropComArr[panelIndex + 1].nativeElement; // 下一级面板元素 + + dropComArr[panelIndex + 1].show(); // 打开下一级面板 + // 位置调整,X方向 // TODO: 使用Position是否可行? + const leftOffset: number = + this.panelAlign === 'left' ? currentPanel.offsetLeft + currentPanel.offsetWidth : currentPanel.offsetLeft - nextPanel.offsetWidth; + nextPanel.style.left = `${leftOffset}px`; + // 位置调整,Y方向 + const itemIndex: number = this.itemsArr[panelIndex].indexOf(item); + const itemElem: any = listComArr[panelIndex].nativeElement.getElementsByTagName('LI')[itemIndex]; + // 当前li距离面板顶部的距离 = 当前li距离可视区顶部的距离 - 当前面板到可视区顶部的距离 + const itemElmTop: number = itemElem.getBoundingClientRect().top - currentPanel.getBoundingClientRect().top; + // 当前li距离页面底部的距离 = 可视区窗口高度 + 文档滚动高度 - 当前面板的offsetTop - 当前li距离面板顶部的距离 + const bottomOffset: number = + document.documentElement.clientHeight + document.documentElement.scrollTop - currentPanel.offsetTop - itemElmTop; + // 下一级面板的的高度 + const itemHeight: number = nextPanel.offsetHeight; + // 底部空间能放下下一级面板:下一级面板的top = 当前面板的offsetTop + 当前li距离面板顶部的距离 - 面板的上下留白 + const nextPanelTop: number = currentPanel.offsetTop + itemElmTop - 4; + if (bottomOffset > itemHeight) { + nextPanel.style.top = nextPanelTop + 'px'; + + return; + } + + // 底部空间不足,和浏览器底部对齐 + nextPanel.style.top = nextPanelTop + bottomOffset - itemHeight + 'px'; + }, 0); + } else { + // 当hover到具体项目(非分组)或 null(移出当前面板),需要闭合所有后续面板 + for (let i: number = panelIndex + 1; i < dropComArr.length; i++) { + dropComArr[i].hide(); + } + } + } + /** + * @ignore + */ + public onMouseoutDrop(event: MouseEvent, panelIndex: number): void { + // 默认需要隐藏, 除非鼠标进入面板区域。 + for (let i: number = 0; i < this.dropComs.length; i++) { + const dropRect: DOMRect = this.dropComs.toArray()[i].nativeElement.getBoundingClientRect(); + // 因为慢慢移出本面板时,移出鼠标事件依然停留在本面板内(1px误差),所以面板rect四周范围要更小1px。 + if ( + dropRect.left < event.clientX && + event.clientX < dropRect.right && + dropRect.top < event.clientY && + event.clientY < dropRect.bottom + ) { + return; + } + } + // 给第一级面板置无选中,那么次级面板自然会关闭。 + this.listComs.first.hoverOption = null; // 会触发到上面this.onHover(null, 0) + } + /** + * @ignore + * + * 初始化menulist最大高度 + */ + public initListMaxHeight(): void { + this.menulistMaxHeight = parseInt(this.panelMaxHeight, 10); + this.listComs.toArray().forEach((listCom) => { + this.renderer.setStyle(listCom.nativeElement, 'max-height', this.menulistMaxHeight + 'px'); + }); + } + /** + * @ignore + * + * 考虑drop的压缩情况,设置menulist的max-height + */ + public restyleListMaxHeight(): void { + const dropCurMaxHeight: number = parseInt(this.dropComs.first.nativeElement.style.maxHeight, 10); + if (!isNaN(dropCurMaxHeight) && dropCurMaxHeight < this.menulistMaxHeight) { + this.listComs.toArray().forEach((listCom) => { + this.renderer.setStyle(listCom.nativeElement, 'max-height', dropCurMaxHeight + 'px'); + }); + } + } +} diff --git a/src/menu/lib/src/TiMenuItem.ts b/src/menu/lib/src/TiMenuItem.ts new file mode 100644 index 0000000..a3e08f0 --- /dev/null +++ b/src/menu/lib/src/TiMenuItem.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiPositionType } from '@opentiny/ng-utils'; +/** + * MenuItem是item项的数据接口 + */ +export interface TiMenuItem { + /** + * 菜单项文本 + */ + label?: string; + /** + * 菜单项 tip 提示内容 + */ + tip?: string; + /** + * 菜单项 tip 提示方向,默认值为‘right’ + */ + tipPosition?: TiPositionType; + /** + * 菜单项是否禁用 + */ + disabled?: boolean; + /** + * 菜单项的子菜单项,最多支持 3 级菜单 + */ + children?: Array; + /** + * 分组 id,当 id 不同时在当前菜单项上方增加分割线 + */ + groupId?: string; + /** + * 菜单项 id + */ + id?: any; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} diff --git a/src/menu/lib/src/TiMenuListComponent.ts b/src/menu/lib/src/TiMenuListComponent.ts new file mode 100644 index 0000000..8347fae --- /dev/null +++ b/src/menu/lib/src/TiMenuListComponent.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TiListComponent } from '@opentiny/ng-list'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiMenuItem } from './TiMenuItem'; +import packageInfo from '../package.json'; +/** + * @ignore + */ +@Component({ + selector: 'ti-menulist', + templateUrl: './menulist.html', + styleUrls: ['./menulist.less' /* , './menu.less' */], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiMenuListComponent)] +}) +export class TiMenuListComponent extends TiListComponent { + @Input() panelAlign: 'left' | 'right' = 'right'; // 设置下拉菜单面板与按钮对齐方式。这里是为了决定了面板上箭头方向。 + protected versionInfo: string = super.getVersion(packageInfo); + protected isSelectable(item: TiMenuItem): boolean { + // 与普通list区别:group也是可以选中的 + return !this.isDisabledFn(item); + } + protected getListOptions(): Array { + // 与普通list区别:次级节点不会放入本级别。 + return this.options; + } + public hasBorder(item: TiMenuItem, items: Array): boolean { + // 分组带线 + const index: number = items.indexOf(item); + const indexPre: number = index === 0 ? 0 : index - 1; + + return item.groupId !== items[indexPre].groupId; + } + public onMouseenterItem(event: MouseEvent, option: any): void { + // 不是在选中项置Top时鼠标经过。 + if (new Date().getTime() - this.optionScrollTopLastTime > TiListComponent.SCROLL_TOP_TIME) { + this.hoverOption = option; // 更新hover + } + } + // 防止继承list的ngOnChanges,初始化hoverOption变更触发hover事件,导致menu组件中的onhover事件报错 + ngOnChanges(): void {} +} diff --git a/src/menu/lib/src/TiMenuModule.ts b/src/menu/lib/src/TiMenuModule.ts new file mode 100644 index 0000000..c8fcff5 --- /dev/null +++ b/src/menu/lib/src/TiMenuModule.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiMenuComponent } from './TiMenuComponent'; +import { TiMenuListComponent } from './TiMenuListComponent'; + +@NgModule({ + imports: [CommonModule, TiDropModule, TiTipModule], + exports: [TiMenuComponent, TiMenuListComponent], + declarations: [TiMenuComponent, TiMenuListComponent] +}) +export class TiMenuModule {} +export { TiMenuComponent } from './TiMenuComponent'; +export { TiMenuListComponent } from './TiMenuListComponent'; +export { TiMenuItem } from './TiMenuItem'; diff --git a/src/menu/lib/src/menu.html b/src/menu/lib/src/menu.html new file mode 100644 index 0000000..cd51ca7 --- /dev/null +++ b/src/menu/lib/src/menu.html @@ -0,0 +1,56 @@ + + + + + + + + + + +
    + + + + {{item[labelKey]}} +
    +
    +
    +
    +
    diff --git a/src/menu/lib/src/menu.less b/src/menu/lib/src/menu.less new file mode 100644 index 0000000..8ac94fc --- /dev/null +++ b/src/menu/lib/src/menu.less @@ -0,0 +1,115 @@ +@import '../../../themes/basic/base-all.less'; +@import '../../../themes/basic/compnent-container-border.less'; +@import '../../../button/lib/src/button.less'; + +:host { + --ti-menu-triangle-width: var(--ti-common-size-2x); + --ti-menu-container-height: var(--ti-common-size-7x); +} + +:host { + display: inline-block; +} + +.ti3-menu-toggle-menu { + text-decoration: none; + display: inline-block; + color: var(--ti-common-color-text-link); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + cursor: pointer; + .user-select(); + &:focus { + color: var(--ti-common-color-text-link); + } + + &:hover { + color: var(--ti-common-color-text-link-hover); + + &:after { + border-top-color: var(--ti-common-color-text-link-hover); + } + } + + &:active { + color: var(--ti-common-color-text-link-hover); + &:after { + border-top-color: var(--ti-common-color-text-link-hover); + } + } + + &:after { + .triangle-down(var(--ti-menu-triangle-width), calc(var(--ti-menu-triangle-width) / 1.6), var(--ti-common-color-text-link)); + content: ''; + margin-left: var(--ti-common-space-base); + display: inline-block; + position: relative; + top: -1px; + } + + &.ti3-menu-toggle-menu-up:after { + .rotate(180deg); + } +} + +.ti3-menu-panel-container { + border-radius: var(--ti-common-border-radius-normal); + background-color: var(--ti-common-color-bg-white-normal); + box-shadow: var(--ti-common-shadow-2-down); //增加阴影尺寸 兼容IE下横向偏移量为 0 时,阴影半径无渲染的情况 + position: absolute; + list-style: none; + z-index: 1060; + display: none; + .box-sizing(border-box); +} + +:host[hasborder], +:host[buttonselect] { + &:extend(.ti3-compnent-container-border all); + & .ti3-menu-toggle-menu { + padding-left: var(--ti-common-space-5x); + padding-right: var(--ti-common-space-4x); + height: var(--ti-menu-container-height); + line-height: var(--ti-menu-container-height); + color: var(--ti-common-color-text-primary); + margin: -1px; + &:hover, + &:focus { + color: var(--ti-common-color-text-primary); + } + &:after, + &:hover:after { + border-top-color: var(--ti-common-color-icon-normal); + } + } + &:hover:not([disabled]) { + border-color: var(--ti-common-color-line-active); + } +} + +// danger按钮样式 +:host[buttonselect].ti-menu-danger-button { + & .ti3-menu-toggle-menu { + &:extend(.ti3-btn-danger all); + border-radius: var(--ti-common-border-radius-normal); + &:after, + &:hover:after { + border-top-color: var(--ti-common-color-icon-white); + } + } +} + +:host[disabled] .ti3-menu-toggle-menu { + cursor: not-allowed; + color: var(--ti-common-color-text-disabled) !important; + &:after { + border-top-color: var(--ti-common-color-icon-disabled) !important; + } +} + +.ti3-menu-border-drop { + & ti-menulist { + max-height: inherit; //继承最大高度 + overflow-y: auto; //超过最大高度时产生竖向滚动条 + } +} diff --git a/src/menu/lib/src/menulist.html b/src/menu/lib/src/menulist.html new file mode 100644 index 0000000..52d9a57 --- /dev/null +++ b/src/menu/lib/src/menulist.html @@ -0,0 +1,39 @@ +
      + + + +
    + + +
    +
    + + + +
  • +
    + + + + {{item[labelKey]}} +
  • +
    diff --git a/src/menu/lib/src/menulist.less b/src/menu/lib/src/menulist.less new file mode 100644 index 0000000..feb11eb --- /dev/null +++ b/src/menu/lib/src/menulist.less @@ -0,0 +1,103 @@ +@import '../../../list/lib/src/list.less'; + +:host { + --ti-menulist-triangle-width: var(--ti-common-size-2x); + --ti-menulist-triangle-margin-left: 3px; +} + +.ti3-menu-groupId { + border-top: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); +} + +.ti3-menu-panel-list { + text-decoration: none; + display: block; + position: relative; + padding: var(--ti-common-space-6) var(--ti-common-space-5x); + clear: both; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-primary); + background-color: var(--ti-common-color-bg-white-normal); + white-space: inherit; + cursor: pointer; + .user-select(); + .box-sizing(border-box); + + &.ti3-menu-panel-list-active { + color: var(--ti-common-color-text-white); + background: var(--ti-common-color-bg-emphasize); + + > .ti3-menu-list-left-icon { + border-bottom-color: var(--ti-common-color-text-white); + } + + > .ti3-menu-list-right-icon { + border-bottom-color: var(--ti-common-color-text-white); + } + } + + &.ti3-menu-panel-list-disabled { + cursor: not-allowed; + background-color: var(--ti-common-color-bg-white-normal); + + ::ng-deep * { + color: var(--ti-common-color-text-disabled) !important; + } + + > .ti3-menu-list-left-icon { + border-bottom-color: var(--ti-common-color-text-disabled); + } + + > .ti3-menu-list-right-icon { + border-bottom-color: var(--ti-common-color-text-disabled); + } + } + + &.ti3-menu-panel-list-selected-item { + &:extend(.ti3-menu-panel-list-active all); + } +} + +.ti3-menu-panel-list-hover { + color: var(--ti-common-color-text-highlight); + background: var(--ti-common-color-bg-white-emphasize); + + > .ti3-menu-list-left-icon { + border-bottom-color: var(--ti-common-color-text-highlight); + } + + > .ti3-menu-list-right-icon { + border-bottom-color: var(--ti-common-color-text-highlight); + } +} + +.ti3-menu-list-icon { + content: ''; + display: inline-block; + position: absolute; + top: 50%; + .triangle-up(var(--ti-menulist-triangle-width), calc(var(--ti-menulist-triangle-width) / 1.6), var(--ti-common-color-icon-normal)); + margin-left: var(--ti-menulist-triangle-margin-left); + vertical-align: middle; +} + +.ti3-menu-list-left-icon { + left: 5px; + transform: translateY(-50%) rotate(270deg); +} + +.ti3-menu-list-right-icon { + right: 5px; + transform: translateY(-50%) rotate(90deg); +} + +.ti3-menu-panel-list-tooltip { + position: absolute; + top: 0px; + left: 0px; + display: inline-block; + width: 100%; + height: 100%; +} diff --git a/src/message/demo/karma.conf.js b/src/message/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/message/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/message/demo/project.json b/src/message/demo/project.json new file mode 100644 index 0000000..124abf2 --- /dev/null +++ b/src/message/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/message/demo", + "sourceRoot": "src/message/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/message", + "index": "src/message/demo/src/index.html", + "main": "src/message/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/message/demo/tsconfig.app.json", + "assets": ["src/message/demo/src/favicon.ico", "src/message/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "message-demo:build:production" + }, + "development": { + "browserTarget": "message-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js message" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/message/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/message/demo/tsconfig.spec.json", + "karmaConfig": "src/message/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/message/demo/src/app/AppComponent.ts b/src/message/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/message/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/message/demo/src/app/AppModule.ts b/src/message/demo/src/app/AppModule.ts new file mode 100644 index 0000000..ee9512d --- /dev/null +++ b/src/message/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { MessageTestModule } from './message/MessageTestModule'; + +@NgModule({ + imports: [ + MessageTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/message/demo/src/app/IndexComponent.ts b/src/message/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..9a362f8 --- /dev/null +++ b/src/message/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { MessageTestModule } from './message/MessageTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = MessageTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/message/demo/src/app/app.html b/src/message/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/message/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/message/demo/src/app/message/MessageBasicComponent.ts b/src/message/demo/src/app/message/MessageBasicComponent.ts new file mode 100644 index 0000000..0ce6035 --- /dev/null +++ b/src/message/demo/src/app/message/MessageBasicComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { TiMessageService, TiModalRef } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-basic.html' +}) +export class MessageBasicComponent { + constructor(private tiMessage: TiMessageService) {} + showMsg(): void { + this.tiMessage.open({ + content: 'this is a message', + close(messageRef: TiModalRef): void { + console.log('on close', messageRef); + }, + dismiss(messageRef: TiModalRef): void { + console.log('on dismiss', messageRef); + } + }); + } +} diff --git a/src/message/demo/src/app/message/MessageBtnComponent.ts b/src/message/demo/src/app/message/MessageBtnComponent.ts new file mode 100644 index 0000000..b5fe43f --- /dev/null +++ b/src/message/demo/src/app/message/MessageBtnComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; +import { TiMessageService } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-btn.html' +}) +export class MessageBtnComponent { + constructor(private tiMessage: TiMessageService) {} + showMsg(): void { + const modalInstance: any = this.tiMessage.open({ + content: 'this is a message', + close: (): void => {}, + okButton: { + show: true, + disabled: false, + primary: true, + text: '确定', + autofocus: true, + click: (): void => { + // 在这里可以添加你的业务代码 + // 使用按钮自定义click时,需要显式调用close或者dismiss + modalInstance.close(); + } + }, + cancelButton: { + // 取消按钮的设置和okbutton的属性设置是一致的,参考okbutton设置即可。 + show: false // 是否显示,默认是true + } + }); + } + + showMsg1(): void { + const modalInstance: any = this.tiMessage.open({ + content: 'this is a message', + close: (): void => {}, + okButton: { + show: true + }, + cancelButton: { + show: true, + disabled: true, + primary: false, + text: '取消', + autofocus: false, + click: (): void => { + modalInstance.close(); + } + } + }); + } +} diff --git a/src/message/demo/src/app/message/MessageBtnTestComponent.ts b/src/message/demo/src/app/message/MessageBtnTestComponent.ts new file mode 100644 index 0000000..636390a --- /dev/null +++ b/src/message/demo/src/app/message/MessageBtnTestComponent.ts @@ -0,0 +1,66 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiMessageService, TiModalRef } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-btn-test.html', + styles: ['.modal-class { width:500px !important; }'], + encapsulation: ViewEncapsulation.None +}) +export class MessageBtnTestComponent { + constructor(private tiMessage: TiMessageService) {} + showDefaultBtn(): void { + this.tiMessage.open({ + content: 'this is a message' + }); + } + showFalseBtn(): void { + this.tiMessage.open({ + content: 'this is a message', + cancelButton: { + show: false + } + }); + } + showBtnClick(): void { + const modalInstance: TiModalRef = this.tiMessage.open({ + modalClass: 'modal-class', + content: 'this is a message', + okButton: { + click: (): void => { + console.log('ok btn click'); + modalInstance.close(); + } + }, + cancelButton: { + click: (): void => { + console.log('cancel btn click'); + modalInstance.dismiss(); + } + } + }); + } + showBtnTxt(): void { + this.tiMessage.open({ + content: 'this is a message', + okButton: { + text: 'text changed' + } + }); + } + showBtnFocus(): void { + this.tiMessage.open({ + content: 'this is a message', + cancelButton: { + autofocus: true + } + }); + } + showBtnPrimary(): void { + this.tiMessage.open({ + content: 'this is a message', + cancelButton: { + primary: true + } + }); + } +} diff --git a/src/message/demo/src/app/message/MessageContentComponent.ts b/src/message/demo/src/app/message/MessageContentComponent.ts new file mode 100644 index 0000000..710b1c8 --- /dev/null +++ b/src/message/demo/src/app/message/MessageContentComponent.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; +import { TiMessageService } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-content.html' +}) +export class MessageContentComponent { + constructor(private tiMessage: TiMessageService) {} + showString(): void { + this.tiMessage.open({ + // v10.1.3及之前的版本存在XSS攻击(html类型)风险, v10.1.4 版本做了安全处理,已不存在XSS攻击风险,建议业务尽快升级版本。 + content: 'this is a message' + }); + } + showTemplate(content: string): void { + this.tiMessage.open({ + content, + context: { + contentName: 'ng-template content' + } + }); + } + showComp(): void { + this.tiMessage.open({ + content: TestComponent, + context: { + contentName: 'component' + } + }); + } +} + +// 弹框内部内容组件定义 +@Component({ + template: ` I'm a {{ contentName }} msg! ` +}) +export class TestComponent { + contentName: string = ''; +} diff --git a/src/message/demo/src/app/message/MessageIdComponent.ts b/src/message/demo/src/app/message/MessageIdComponent.ts new file mode 100644 index 0000000..dafccda --- /dev/null +++ b/src/message/demo/src/app/message/MessageIdComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { TiMessageService, TiModalRef } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-id.html' +}) +export class MessageIdComponent { + constructor(private tiMessage: TiMessageService) {} + showMsg(): void { + this.tiMessage.open({ + id: 'message_id', + content: 'this is a message_id', + close(messageRef: TiModalRef): void { + console.log('on close', messageRef); + }, + dismiss(messageRef: TiModalRef): void { + console.log('on dismiss', messageRef); + } + }); + } +} diff --git a/src/message/demo/src/app/message/MessageSecurityComponent.ts b/src/message/demo/src/app/message/MessageSecurityComponent.ts new file mode 100644 index 0000000..9fbfbcc --- /dev/null +++ b/src/message/demo/src/app/message/MessageSecurityComponent.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { TiMessageService } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-security.html' +}) +export class MessageSecurityComponent { + constructor(private tiMessage: TiMessageService, private domSanitizer: DomSanitizer) {} + showString1(): void { + this.tiMessage.open({ + // 10.1.3及之前版本该接口存在XSS攻击风险;该接口在10.1.4版本已经做了安全处理,js脚本不会执行。 + content: `hello链接1链接2` + }); + } + + showString2(): void { + this.tiMessage.open({ + // 组件内部用的是Angular提供的 DomSanitizer.sanitize 方法做防XSS攻击安全处理的,它会把style设置会过滤掉,所以建议使用class的方式添加样式; + // 如果一定要使用style方式,且能确保传入的html字符串片段是安全的,可以使用Angular提供的 DomSanitizer 上的 bypassSecurityTrustHtml 方法去掉angular的安全过滤处理。 + content: this.domSanitizer.bypassSecurityTrustHtml(`hello链接`) + }); + } +} diff --git a/src/message/demo/src/app/message/MessageTestModule.ts b/src/message/demo/src/app/message/MessageTestModule.ts new file mode 100644 index 0000000..e291645 --- /dev/null +++ b/src/message/demo/src/app/message/MessageTestModule.ts @@ -0,0 +1,79 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiMessageModule } from '@opentiny/ng'; + +import { MessageContentComponent, TestComponent } from './MessageContentComponent'; +import { MessageTypeComponent } from './MessageTypeComponent'; +import { MessageBtnComponent } from './MessageBtnComponent'; +import { MessageTitleComponent } from './MessageTitleComponent'; +import { MessageBasicComponent } from './MessageBasicComponent'; +import { MessageBtnTestComponent } from './MessageBtnTestComponent'; +import { MessageIdComponent } from './MessageIdComponent'; +import { MessageSecurityComponent } from './MessageSecurityComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiMessageModule, + TiButtonModule, + RouterModule.forChild(MessageTestModule.ROUTES) + ], + declarations: [ + MessageContentComponent, + MessageBasicComponent, + MessageBtnTestComponent, + MessageTypeComponent, + TestComponent, + MessageBtnComponent, + MessageTitleComponent, + MessageIdComponent, + MessageSecurityComponent + ], + entryComponents: [TestComponent] +}) +export class MessageTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiMessageComponent.html', label: 'Message' }, + { + href: 'components/TiContentWrapperComponent.html', + label: 'ContentWrapper' + }, + { + href: 'directives/TiTranscludeDirective.html', + label: 'TiTranscludeDirective' + } + ]; + static readonly ROUTES: Routes = [ + { + path: 'message/message-basic', + component: MessageBasicComponent + }, + { + path: 'message/message-type', + component: MessageTypeComponent + }, + { + path: 'message/message-title', + component: MessageTitleComponent + }, + { + path: 'message/message-content', + component: MessageContentComponent + }, + { + path: 'message/message-btn', + component: MessageBtnComponent + }, + { path: 'message/message-btn-test', component: MessageBtnTestComponent }, + { + path: 'message/message-id', + component: MessageIdComponent + }, + { path: 'message/message-security', component: MessageSecurityComponent } + ]; +} diff --git a/src/message/demo/src/app/message/MessageTitleComponent.ts b/src/message/demo/src/app/message/MessageTitleComponent.ts new file mode 100644 index 0000000..7465887 --- /dev/null +++ b/src/message/demo/src/app/message/MessageTitleComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiMessageService } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-title.html' +}) +export class MessageTitleComponent { + constructor(private tiMessage: TiMessageService) {} + showMessage(): void { + this.tiMessage.open({ + title: 'this is message title', + content: 'message content' + }); + } +} diff --git a/src/message/demo/src/app/message/MessageTypeComponent.ts b/src/message/demo/src/app/message/MessageTypeComponent.ts new file mode 100644 index 0000000..e218d39 --- /dev/null +++ b/src/message/demo/src/app/message/MessageTypeComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { TiMessageService } from '@opentiny/ng'; + +@Component({ + templateUrl: './message-type.html' +}) +export class MessageTypeComponent { + constructor(private tiMessage: TiMessageService) {} + showDefault(): void { + this.tiMessage.open({ + content: 'confirm' + }); + } + showPrompt(): void { + this.tiMessage.open({ + type: 'prompt', + content: 'prompt' + }); + } + showWarn(): void { + this.tiMessage.open({ + type: 'warn', + content: 'warn' + }); + } + showError(): void { + this.tiMessage.open({ + type: 'error', + content: 'error' + }); + } +} diff --git a/src/message/demo/src/app/message/message-basic.html b/src/message/demo/src/app/message/message-basic.html new file mode 100644 index 0000000..8b62534 --- /dev/null +++ b/src/message/demo/src/app/message/message-basic.html @@ -0,0 +1,6 @@ +

    描述

    +

    基本使用,只定义内容,其余采用默认值

    +

    导入

    +

    import {{ '{' }} TiMessageModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    + diff --git a/src/message/demo/src/app/message/message-btn-test.html b/src/message/demo/src/app/message/message-btn-test.html new file mode 100644 index 0000000..cecf713 --- /dev/null +++ b/src/message/demo/src/app/message/message-btn-test.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/message/demo/src/app/message/message-btn.html b/src/message/demo/src/app/message/message-btn.html new file mode 100644 index 0000000..b1c93c1 --- /dev/null +++ b/src/message/demo/src/app/message/message-btn.html @@ -0,0 +1 @@ + diff --git a/src/message/demo/src/app/message/message-content.html b/src/message/demo/src/app/message/message-content.html new file mode 100644 index 0000000..c9348a8 --- /dev/null +++ b/src/message/demo/src/app/message/message-content.html @@ -0,0 +1,6 @@ + + {{context.contentName}} + + + + diff --git a/src/message/demo/src/app/message/message-id.html b/src/message/demo/src/app/message/message-id.html new file mode 100644 index 0000000..1315dc2 --- /dev/null +++ b/src/message/demo/src/app/message/message-id.html @@ -0,0 +1,4 @@ +

    描述

    +

    设置组件Id

    +

    示例

    + diff --git a/src/message/demo/src/app/message/message-security.html b/src/message/demo/src/app/message/message-security.html new file mode 100644 index 0000000..7b28fd6 --- /dev/null +++ b/src/message/demo/src/app/message/message-security.html @@ -0,0 +1,10 @@ +

    描述

    +

    + 内容通过content属性定义,支持传入字符串/template/组件形式。其中传入字符串形式时,是支持传入html字符串片段的, + v10.1.3及之前的版本存在XSS攻击风险,v10.1.4版本做了安全处理,v10.1.4及之后版本不存在XSS攻击风险。 +

    +

    示例

    + + +

    + diff --git a/src/message/demo/src/app/message/message-title.html b/src/message/demo/src/app/message/message-title.html new file mode 100644 index 0000000..bbc046a --- /dev/null +++ b/src/message/demo/src/app/message/message-title.html @@ -0,0 +1 @@ + diff --git a/src/message/demo/src/app/message/message-type.html b/src/message/demo/src/app/message/message-type.html new file mode 100644 index 0000000..b4584a0 --- /dev/null +++ b/src/message/demo/src/app/message/message-type.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/message/demo/src/app/message/webdoc/message-demos.js b/src/message/demo/src/app/message/webdoc/message-demos.js new file mode 100644 index 0000000..f23eb16 --- /dev/null +++ b/src/message/demo/src/app/message/webdoc/message-demos.js @@ -0,0 +1,68 @@ +export default { + column: '2', + demos: [ + { + demoId: 'message-type', + name: { + 'zh-CN': '消息弹框类型', + 'en-US': 'type', + }, + desc: { + 'zh-CN': + '

    通过属性type配置消息弹框类型,包含confirmpromptwarnerror类型。

    ', + 'en-US': '

    button color

    ', + }, + apis: ['TiMessageConfig.properties.type'], + }, + { + demoId: 'message-title', + name: { + 'zh-CN': '自定义标题', + 'en-US': 'title', + }, + desc: { + 'zh-CN': '

    通过属性title配置消息弹框标题。

    ', + 'en-US': '

    title

    ', + }, + apis: ['TiMessageConfig.properties.title'], + }, + { + demoId: 'message-content', + name: { + 'zh-CN': '内容', + 'en-US': 'content', + }, + desc: { + 'zh-CN': + '

    通过content配置内容,支持设置为字符串/ template 模板/组件形式。

    ', + 'en-US': 'content', + }, + apis: [ + 'TiMessageConfig.properties.content', + 'TiMessageConfig.properties.context', + ], + }, + { + demoId: 'message-btn', + name: { + 'zh-CN': '确认、取消按钮配置', + 'en-US': 'button', + }, + desc: { + 'zh-CN': + '

    通过okButton配置确认按钮,通过cancelButton配置取消按钮。

    ', + 'en-US': 'button', + }, + apis: [ + 'TiMessageConfig.properties.okButton', + 'TiMessageConfig.properties.cancelButton', + 'TiMessageButtonConfig.properties.text', + 'TiMessageButtonConfig.properties.primary', + 'TiMessageButtonConfig.properties.show', + 'TiMessageButtonConfig.properties.autofocus', + 'TiMessageButtonConfig.properties.disabled', + 'TiMessageButtonConfig.methods.click', + ], + }, + ], +}; diff --git a/src/message/demo/src/app/message/webdoc/message.cn.md b/src/message/demo/src/app/message/webdoc/message.cn.md new file mode 100644 index 0000000..7030fe1 --- /dev/null +++ b/src/message/demo/src/app/message/webdoc/message.cn.md @@ -0,0 +1,34 @@ +--- +title: Message 消息弹框 +--- +# Message 消息弹框 + +
    + +Message 是提供消息弹框的组件。   + ++ 提供服务方式供业务使用。使用该服务时需要引入模块`TiMessageModule`,通过调用`TiMessageService.open(config)`方法生成弹出框。 + +```typescript +import { TiMessageModule } from '@opentiny/ng'; +``` + +需要在项目中(建议在根模块)中导入`BrowserAnimationsModule`。 + +```typescript +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +``` + +
    + +
    + +Message 是提供消息弹框的组件。   + ++ 提供服务方式供业务使用。使用该服务时需要引入模块`TiMessageModule`,通过调用`TiMessageService.open(config)`方法生成弹出框。 + +```typescript +import { TiMessageModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/message/demo/src/app/message/webdoc/message.en.md b/src/message/demo/src/app/message/webdoc/message.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/message/demo/src/app/message/webdoc/message.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/message/demo/src/favicon.ico b/src/message/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/message/demo/src/index.html b/src/message/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/message/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/message/demo/src/main.ts b/src/message/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/message/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/message/demo/test.ts b/src/message/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/message/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/message/demo/tsconfig.app.json b/src/message/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/message/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/message/demo/tsconfig.spec.json b/src/message/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/message/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/message/lib/index.ts b/src/message/lib/index.ts new file mode 100644 index 0000000..69ca838 --- /dev/null +++ b/src/message/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiMessageModule'; +export * from './src/TiMessageService'; diff --git a/src/message/lib/ng-package.json b/src/message/lib/ng-package.json new file mode 100644 index 0000000..d393c26 --- /dev/null +++ b/src/message/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/message", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/message/lib/package.json b/src/message/lib/package.json new file mode 100644 index 0000000..fd838a5 --- /dev/null +++ b/src/message/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "@opentiny/ng-message", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-modal": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-popup": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/message/lib/project.json b/src/message/lib/project.json new file mode 100644 index 0000000..72b0a13 --- /dev/null +++ b/src/message/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/message/lib", + "sourceRoot": "src/message/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/message"], + "options": { + "project": "src/message/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/message"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js message" + }, + { + "command": "ng default-build message" + }, + { + "command": "node build/clear-default-theme.js message" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/message && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build message && ng pack message && node build/publish.js message --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/message/lib/src/TiContentWrapperComponent.ts b/src/message/lib/src/TiContentWrapperComponent.ts new file mode 100644 index 0000000..7b8c682 --- /dev/null +++ b/src/message/lib/src/TiContentWrapperComponent.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +/** + * @ignore + */ +@Component({ + selector: 'ti-content-wrapper', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiContentWrapperComponent {} diff --git a/src/message/lib/src/TiMessageComponent.html b/src/message/lib/src/TiMessageComponent.html new file mode 100644 index 0000000..10e26bd --- /dev/null +++ b/src/message/lib/src/TiMessageComponent.html @@ -0,0 +1,39 @@ + +
    + +
    + +
    +
    + + + diff --git a/src/message/lib/src/TiMessageComponent.ts b/src/message/lib/src/TiMessageComponent.ts new file mode 100644 index 0000000..06e2f84 --- /dev/null +++ b/src/message/lib/src/TiMessageComponent.ts @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { TiLocale } from '@opentiny/ng-locale'; +import { Util } from '@opentiny/ng-utils'; +import { TiMessageButtonConfig } from './TiMessageInterface'; + +/** + * messge类型设置 + */ +export type TiMessageType = 'prompt' | 'warn' | 'error' | 'confirm'; +/** + * @ignore + * message模板组件定义 + */ +@Component({ + selector: 'ti-message', + templateUrl: './TiMessageComponent.html', + styleUrls: ['./message.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiMessageComponent implements OnInit { + // 常量配置 + private CONST_CONFIG: any = { + validType: ['confirm', 'error', 'warn', 'prompt'], + defaultType: 'confirm' + }; + @Input() id: string; + @Input() title: string; + @Input() type: TiMessageType; + @Input() content: any; + @Input() context: any; + @Input() okButton: TiMessageButtonConfig; + @Input() cancelButton: TiMessageButtonConfig; + @Input() close: () => void; + @Input() dismiss: () => void; + public contentConfig: any; // 内容区域属性配置 + private readonly localeWords = TiLocale.getLocaleWords(); + + /** + * 设置按钮属性(除primary和autofocus外) + * @param options 外部配置属性 + * @param defaults 按钮text和click属性 + * @returns 合并后的属性 + */ + private static setBtn( + options: TiMessageButtonConfig, + defaults: { + text?: string; + click(): void; + } + ): TiMessageButtonConfig { + const defaultOpts: TiMessageButtonConfig = { + show: true, + disabled: false, + ...defaults + }; + if (typeof options === 'object') { + return { ...defaultOpts, ...options }; + } + + return defaultOpts; + } + + ngOnInit(): void { + this.setType(); + this.setTitle(); + this.setBtns(); + this.setContent(); + } + private setType(): void { + const validType: Array = this.CONST_CONFIG.validType; + const defaultType: TiMessageType = this.CONST_CONFIG.defaultType; + if (validType.indexOf(this.type) === -1) { + this.type = defaultType; + } + } + private setTitle(): void { + if (typeof this.title === 'string') { + return; + } + // 不同类型的message有默认title + this.title = this.localeWords.tiMessage[this.type]; + } + /** + * 设置按钮属性: + * 对按钮的设置支持Object类型 + * 为Object类型时,覆盖和扩展默认设置 + */ + private setBtns(): void { + this.okButton = TiMessageComponent.setBtn(this.okButton, { + text: this.localeWords.tiMessage.ok, + click: (): void => { + this.close(); + } + }); + this.cancelButton = TiMessageComponent.setBtn(this.cancelButton, { + text: this.localeWords.tiMessage.cancel, + click: (): void => { + this.dismiss(); + } + }); + + this.setBtnUniqueState('autofocus'); + this.setBtnUniqueState('primary'); + } + /** + * 设置按钮属性,确保默认状态只在一个按钮生效 + * @param prop 按钮属性 + */ + private setBtnUniqueState(prop: string): void { + if (Util.isUndefined(this.okButton[prop]) && Util.isUndefined(this.cancelButton[prop])) { + this.okButton[prop] = true; + } + } + private setContent(): void { + this.contentConfig = { + content: this.content, + context: this.context + }; + } + public setId(suffix: string): string { + if (Util.isUndefined(this.id)) { + return ''; + } + + return `${this.id}${suffix}`; + } +} diff --git a/src/message/lib/src/TiMessageInterface.ts b/src/message/lib/src/TiMessageInterface.ts new file mode 100644 index 0000000..3c7c5b7 --- /dev/null +++ b/src/message/lib/src/TiMessageInterface.ts @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TemplateRef } from '@angular/core'; +import { TiModalConfig } from '@opentiny/ng-modal'; +// 外部可传入的message配置项 +/** + * 消息组件配置项接口 + * + * 该接口用于消息弹框组件[open]{@link TiMessageService#open}方法的参数的定义 + */ +export interface TiMessageConfig extends TiModalConfig { + /** + * 消息弹框类型 + * @default 'confirm' + */ + type?: 'confirm' | 'warn' | 'error' | 'prompt'; + /** + * 消息弹框标题 + */ + title?: string; + /** + * 消息弹框内容,支持字符串、 template 模板、组件格式 + */ + content?: string | TemplateRef | any; + /** + * 内容区域的上下文,为对象形式 + */ + context?: any; + /** + * 底部 ok 按钮配置 + */ + okButton?: TiMessageButtonConfig; + /** + * 底部 cancel 按钮配置 + */ + cancelButton?: TiMessageButtonConfig; +} + +/** + * 消息弹框底部按钮的配置项接口 + * + * 该接口用于定义消息弹框的[okButton]{@link ../injectables/TiMessageService.html#okButton} + * 或[cancelButton]{@link ../injectables/TiMessageService.html#cancelButton}的接口类型 + */ +export interface TiMessageButtonConfig { + /** + * 按钮文本 + * + * @default okButton为确认/OK(国际化),cancelButton为取消/Cancel(国际化) + */ + text?: string; + /** + * 是否显示 + * + * @default true + */ + show?: boolean; + /** + * 是否禁用 + * + * @default false + */ + disabled?: boolean; + /** + * 是否为主按钮,主按钮为较醒目的按钮样式 + * + * @default okButton为主按钮 + */ + primary?: boolean; + /** + * 是否自动聚焦 + * + * @default okButton默认聚焦 + */ + autofocus?: boolean; + /** + * 点击事件 + */ + click?(): void; +} diff --git a/src/message/lib/src/TiMessageModule.ts b/src/message/lib/src/TiMessageModule.ts new file mode 100644 index 0000000..c785d06 --- /dev/null +++ b/src/message/lib/src/TiMessageModule.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiMessageComponent } from './TiMessageComponent'; +import { TiContentWrapperComponent } from './TiContentWrapperComponent'; +import { TiTranscludeDirective } from './TiTranscludeDirective'; +import { TiModalModule } from '@opentiny/ng-modal'; +import { TiButtonModule } from '@opentiny/ng-button'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +/** + * @ignore + */ +@NgModule({ + imports: [CommonModule, TiModalModule, TiButtonModule, TiIconModule], + declarations: [TiMessageComponent, TiTranscludeDirective, TiContentWrapperComponent], + entryComponents: [TiMessageComponent, TiContentWrapperComponent] +}) +export class TiMessageModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiMessageButtonConfig, TiMessageConfig } from './TiMessageInterface'; +export { TiMessageType } from './TiMessageComponent'; diff --git a/src/message/lib/src/TiMessageService.ts b/src/message/lib/src/TiMessageService.ts new file mode 100644 index 0000000..0acaff5 --- /dev/null +++ b/src/message/lib/src/TiMessageService.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * 该类对外暴露,以服务的方式提供方法生成message组件 + * open({ + * id:message id,string类型,用于生成唯一的按钮可测试id,并且防止弹框多次打开(同modal组件) + * type:message类型,支持'prompt' | 'warn' | 'error' | 'confirm',默认为confirm + * title:头部title,string类型,可支持string形式的DOM片段 + * content:内容区域DOM,支持类型为:TemplateRef | Function | string + * context:内容区域上下文,类型为Object + * okButton:{ + * text:string,按钮文本显示 + * click: Function,点击回调 + * primary: boolean,默认为true,程序中保证了两个按钮中只有一个可为true + * autofocus: boolean,默认为true,程序中保证了两个按钮中只有一个可为true + * show: boolean,标识按钮是否显示,默认为true + * } + * cancleBtn:{ + * text:string,按钮文本显示 + * click: Function,点击回调 + * primary: boolean,默认为false,程序中保证了两个按钮中只有一个可为true + * autofocus: boolean,默认为false,程序中保证了两个按钮中只有一个可为true + * show: boolean,标识按钮是否显示,默认为true + * } + * }) + */ +import { Injectable } from '@angular/core'; +import { TiModalRef } from '@opentiny/ng-modal'; +import { TiModalService } from '@opentiny/ng-modal'; +import { TiMessageComponent } from './TiMessageComponent'; +import { TiMessageModule } from './TiMessageModule'; +import { TiMessageConfig } from './TiMessageInterface'; + +/** + * 消息弹框组件,使用服务方式生成,使用该服务时需要引入模块TiMessageModule,通过调用TiMessageService.open(config)方法生成弹出框 + * + *

    使用此组件时需要开发者在项目模块(建议在根模块) + * 中引入BrowserAnimationsModule。这是因为此组件中使用了Angular动画,需要引入BrowserAnimationsModule, + * 但是 BrowserAnimationsModule 不能在懒加载模块被重复引入,所以需要开发者来引入BrowserAnimationsModule,保证其引入一次。

    + * + */ +@Injectable({ + providedIn: TiMessageModule +}) +export class TiMessageService { + constructor(private tiModal: TiModalService) {} + /** + * 生成消息弹框方法 + * [config] 弹框配置信息 + * 返回 弹框实例信息,使用该实例信息可调用弹框的close等方法;内容为component类型时,也可通过content属性获取内容结果数据 + */ + public open(config?: TiMessageConfig): TiModalRef { + const { type, title, content, context, okButton, cancelButton, ...modalConfig }: TiMessageConfig = config; + + return this.tiModal.open(TiMessageComponent, { + context: { + id: config.id, + type, + title, + content, + context, + okButton, + cancelButton + }, + modalClass: 'ti3-msg-default-width', + ...modalConfig + }); + } +} diff --git a/src/message/lib/src/TiTranscludeDirective.ts b/src/message/lib/src/TiTranscludeDirective.ts new file mode 100644 index 0000000..fc37a9c --- /dev/null +++ b/src/message/lib/src/TiTranscludeDirective.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; +import { TiPopupService } from '@opentiny/ng-popup'; +import { TiContentWrapperComponent } from './TiContentWrapperComponent'; +/** + * @ignore + * 生成并且compile内容为宿主元素的子元素 + */ +@Directive({ + selector: '[tiTransclude]' +}) +export class TiTranscludeDirective implements OnInit, OnDestroy { + @Input() tiTransclude: { + content: string | TemplateRef | any; + context?: any; + }; + private wrapperRef: any; + constructor(private eleRef: ElementRef, private popService: TiPopupService) {} + ngOnInit(): void { + this.wrapperRef = this.popService.create(TiContentWrapperComponent); + this.wrapperRef.show({ + content: this.tiTransclude.content, + contentContext: this.tiTransclude.context, + container: this.eleRef + }); + } + ngOnDestroy(): void { + this.wrapperRef.hide(); + } +} diff --git a/src/message/lib/src/i18n/TiMessageWords.ts b/src/message/lib/src/i18n/TiMessageWords.ts new file mode 100644 index 0000000..7773961 --- /dev/null +++ b/src/message/lib/src/i18n/TiMessageWords.ts @@ -0,0 +1,10 @@ +export interface TiMessageWords { + tiMessage: { + prompt: string; + warn: string; + confirm: string; + error: string; + ok: string; + cancel: string; + }; +} diff --git a/src/message/lib/src/i18n/en_US.ts b/src/message/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..e6f5e37 --- /dev/null +++ b/src/message/lib/src/i18n/en_US.ts @@ -0,0 +1,12 @@ +import { TiMessageWords } from './TiMessageWords'; + +export const en_US: TiMessageWords = { + tiMessage: { + prompt: 'Information', + warn: 'Warning', + confirm: 'Confirm', + error: 'Error', + ok: 'OK', + cancel: 'Cancel' + } +}; diff --git a/src/message/lib/src/i18n/es_US.ts b/src/message/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..43d4d4a --- /dev/null +++ b/src/message/lib/src/i18n/es_US.ts @@ -0,0 +1,12 @@ +import { TiMessageWords } from './TiMessageWords'; + +export const es_US: TiMessageWords = { + tiMessage: { + prompt: 'Información', + warn: 'Advertencia', + confirm: 'Confirmar', + error: 'Error', + ok: 'Aceptar', + cancel: 'Cancelar' + } +}; diff --git a/src/message/lib/src/i18n/fr_FR.ts b/src/message/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..ed2813a --- /dev/null +++ b/src/message/lib/src/i18n/fr_FR.ts @@ -0,0 +1,12 @@ +import { TiMessageWords } from './TiMessageWords'; + +export const fr_FR: TiMessageWords = { + tiMessage: { + prompt: 'Information', + warn: 'Attention', + confirm: 'Confirmer', + error: 'Erreur', + ok: 'OK', + cancel: 'Annuler' + } +}; diff --git a/src/message/lib/src/i18n/index.ts b/src/message/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/message/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/message/lib/src/i18n/pt_BR.ts b/src/message/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..d4c200f --- /dev/null +++ b/src/message/lib/src/i18n/pt_BR.ts @@ -0,0 +1,12 @@ +import { TiMessageWords } from './TiMessageWords'; + +export const pt_BR: TiMessageWords = { + tiMessage: { + prompt: 'Informação', + warn: 'Aviso', + confirm: 'Confirmar', + error: 'Erro', + ok: 'OK', + cancel: 'Cancelar' + } +}; diff --git a/src/message/lib/src/i18n/zh_CN.ts b/src/message/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..098cb76 --- /dev/null +++ b/src/message/lib/src/i18n/zh_CN.ts @@ -0,0 +1,12 @@ +import { TiMessageWords } from './TiMessageWords'; + +export const zh_CN: TiMessageWords = { + tiMessage: { + prompt: '提示', + warn: '警告', + confirm: '确认', + error: '错误', + ok: '确认', + cancel: '取消' + } +}; diff --git a/src/message/lib/src/message.less b/src/message/lib/src/message.less new file mode 100644 index 0000000..40030e7 --- /dev/null +++ b/src/message/lib/src/message.less @@ -0,0 +1,53 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-message-icon-size: var(--ti-common-size-6x); + --ti-message-icon-margin-right: var(--ti-common-space-2x); +} + +::ng-deep ti-modal-wrapper { + --ti-message-width: 400px; +} + +::ng-deep .ti3-modal-dialog.ti3-msg-default-width { + width: var(--ti-message-width); +} + +ti-modal-header.ti3-msg-header { + padding-bottom: var(--ti-common-space-3x); +} +.ti3-msg-icon { + float: left; + line-height: normal; + font-size: var(--ti-message-icon-size); + margin-right: var(--ti-message-icon-margin-right); + &.ti3-icon-check-circle { + color: var(--ti-common-color-success); + } + &.ti3-msg-icon-warn { + color: var(--ti-common-color-warn); + } + &.ti3-icon-info-circle { + color: var(--ti-common-color-prompt); + } + &.ti3-msg-icon-error { + color: var(--ti-common-color-error); + } +} + +.ti3-msg-content-wrapper { + overflow: inherit; + font-size: var(--ti-common-font-size-base); + color: var(--ti-common-color-text-secondary); + line-height: var(--ti-common-line-height-number); + word-wrap: break-word; +} + +.ti3-msg-content-title { + font-size: var(--ti-common-font-size-3); + line-height: 26px; + color: var(--ti-common-color-text-primary); + font-weight: var(--ti-common-font-weight-7); + display: inline-block; + width: calc(100% - var(--ti-message-icon-size) - var(--ti-message-icon-margin-right)); +} diff --git a/src/modal/demo/karma.conf.js b/src/modal/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/modal/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/modal/demo/project.json b/src/modal/demo/project.json new file mode 100644 index 0000000..f993e10 --- /dev/null +++ b/src/modal/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/modal/demo", + "sourceRoot": "src/modal/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/modal", + "index": "src/modal/demo/src/index.html", + "main": "src/modal/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/modal/demo/tsconfig.app.json", + "assets": ["src/modal/demo/src/favicon.ico", "src/modal/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "modal-demo:build:production" + }, + "development": { + "browserTarget": "modal-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js modal" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/modal/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/modal/demo/tsconfig.spec.json", + "karmaConfig": "src/modal/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/modal/demo/src/app/AppComponent.ts b/src/modal/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/modal/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/modal/demo/src/app/AppModule.ts b/src/modal/demo/src/app/AppModule.ts new file mode 100644 index 0000000..ec368ac --- /dev/null +++ b/src/modal/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ModalTestModule } from './modal/ModalTestModule'; + +@NgModule({ + imports: [ + ModalTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/modal/demo/src/app/IndexComponent.ts b/src/modal/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..afe13cc --- /dev/null +++ b/src/modal/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ModalTestModule } from './modal/ModalTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = ModalTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/modal/demo/src/app/app.html b/src/modal/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/modal/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/modal/demo/src/app/modal/ModalAnimationComponent.ts b/src/modal/demo/src/app/modal/ModalAnimationComponent.ts new file mode 100644 index 0000000..0489d80 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalAnimationComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-animation.html' +}) +export class ModalAnimationComponent { + constructor(private tiModal: TiModalService) {} + + show(content: string): void { + this.tiModal.open(content, { + animation: false + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalBackdropComponent.ts b/src/modal/demo/src/app/modal/ModalBackdropComponent.ts new file mode 100644 index 0000000..4662f23 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalBackdropComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-backdrop.html' +}) +export class ModalBackdropComponent { + constructor(private tiModal: TiModalService) {} + + show(content: string): void { + this.tiModal.open(content, { + backdrop: false + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalClassComponent.ts b/src/modal/demo/src/app/modal/ModalClassComponent.ts new file mode 100644 index 0000000..a4147c6 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalClassComponent.ts @@ -0,0 +1,17 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-class.html', + styles: ['.modal-class { width: 300px !important; }'], + encapsulation: ViewEncapsulation.None // 要想设置的样式生效,此处必须配置成 ViewEncapsulation.None +}) +export class ModalClassComponent { + constructor(private tiModal: TiModalService) {} + + show(content: string): void { + this.tiModal.open(content, { + modalClass: 'modal-class' + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalCloseIconComponent.ts b/src/modal/demo/src/app/modal/ModalCloseIconComponent.ts new file mode 100644 index 0000000..bc69571 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalCloseIconComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-close-icon.html' +}) +export class ModalCloseIconComponent { + constructor(private tiModal: TiModalService) {} + + show(content: string): void { + this.tiModal.open(content, { + closeIcon: false + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalConfigTestComponent.ts b/src/modal/demo/src/app/modal/ModalConfigTestComponent.ts new file mode 100644 index 0000000..fd6226c --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalConfigTestComponent.ts @@ -0,0 +1,57 @@ +import { Component } from '@angular/core'; +import { TiModalRef, TiModalService, TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './modal-config-test.html' +}) +export class ModalConfigTestComponent { + constructor(private tiModal: TiModalService) {} + // 定义公共变量接收弹窗实例 + openModal: TiModalRef; + // 弹窗外部的公共变量 + myNumber: number = 1; + myValue: string = ''; + myValidationConfig: TiValidationConfig = { + tip: 'should input email', + type: 'change' + }; + show(content: string): void { + // 弹窗打开,会返回一个弹窗实例,使用一个公共变量去接收它,从而获得弹窗实例 + this.openModal = this.tiModal.open(content, { + id: 'myModal', // 弹框id,设置id可防止同一页面生成多个相同弹框的问题 + animation: false, // 控制弹窗显示/隐藏是否带动画效果,不设置使用默认值,默认值为true,即表示带有动效 + draggable: false, // 控制弹窗是否可拖拽,不设置使用默认值,默认值为true,即表示可拖拽 + closeOnEsc: false, // 控制是否可通过ESC快捷键关闭弹窗,不设置使用默认值,默认值为true,即默认可通过ESC快捷键关闭弹窗 + backdrop: false, // 控制是否显示遮罩,不设置使用默认值,默认值为true,即默认展示遮罩 + closeIcon: false, // 控制头部是否有关闭按钮,不设置使用默认值,默认值为true,即默认头部有关闭按钮, + headerAlign: 'center', // 头部内容水平对齐位置,可设置的值有'center' | 'left' | 'right',默认值为left,表示左对齐,可参考“header自定义”示例 + modalClass: '', // 弹框样式,可通过该样式定义弹框的宽高等,可参考“modalClass功能”示例 + context: { + // 向弹框传递的数据,当弹框内容为组件或者template时,使用方法不同。可分别参考“弹框内容为组件形式”和“弹框内容为组件形式”示例 + headerTitle: "I'm a modal!", + // 在弹窗自定义的button,来关闭弹窗。 + closeModal: (): void => { + this.openModal.close(); + }, + // 自定义方法,此方法来改变弹窗外部的变量this.myNumber + changMyNumber: (): void => { + this.myNumber++; + } + }, + // beforeClose可以阻止弹框的关闭,需要手动调用modalRef.destroy(reason)来关闭弹框 + beforeClose: (modalRef: TiModalRef, reason: boolean): void => { + console.log(reason); + // 如果不调用modalRef.destroy(reason),无法关闭弹框 + // reason为true时,回调close() + // reason为false时,回调dismiss() + modalRef.destroy(reason); + }, + close: (): void => { + console.log('close'); + }, + dismiss: (): void => { + console.log('dismiss'); + } + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalContentCompComponent.ts b/src/modal/demo/src/app/modal/ModalContentCompComponent.ts new file mode 100644 index 0000000..2ad9544 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalContentCompComponent.ts @@ -0,0 +1,58 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { TiModalRef, TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: './modal-content-comp.html' +}) +export class ModalContentCompComponent { + constructor(private tiModal: TiModalService) {} + myLogs: Array = []; + show(): void { + this.tiModal.open(ModalTestComponent, { + id: 'myModal1', // 定义id防止同一页面出现多个相同弹框 + // 弹框内容组件上下文属性定义 + context: { + name: 'component', + bodyText: 'component modal body', + outputs: { + // 输出属性定义在这里 + buttonClick: (): void => { + this.myLogs = [...this.myLogs, 'buttonClick事件']; + } + } + }, + close: (modalRef: TiModalRef): void => {}, + dismiss: (modalRef: TiModalRef): void => {} + }); + } +} + +// 组件定义,此处为了示例方便呈现写在一个文件,业务请新建文件定义组件 +@Component({ + template: ` + + I'm a {{ name }} modal! + + + {{ bodyText }} + + + + + + + ` +}) +export class ModalTestComponent { + // 组件对外接口,modal可通过context属性设置/获取这些接口属性 + @Input() bodyText: string = ''; + @Input() name: string = ''; + @Output() readonly buttonClick: EventEmitter = new EventEmitter(); + // 模板中实际调用的是Modal服务提供的close和dismiss方法,并非此处定义的方法; + // 在此处定义close和dismiss方法只是为了避免生产环境打包时报错 + close(): void {} + dismiss(): void {} + onClick(): void { + this.buttonClick.emit(); + } +} diff --git a/src/modal/demo/src/app/modal/ModalContentTempComponent.ts b/src/modal/demo/src/app/modal/ModalContentTempComponent.ts new file mode 100644 index 0000000..5256243 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalContentTempComponent.ts @@ -0,0 +1,15 @@ +import { Component, TemplateRef } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-content-temp.html' +}) +export class ModalContentTempComponent { + constructor(private tiModal: TiModalService) {} + + show(content: TemplateRef): void { + this.tiModal.open(content, { + id: 'myModal' + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalDraggableComponent.ts b/src/modal/demo/src/app/modal/ModalDraggableComponent.ts new file mode 100644 index 0000000..2842c04 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalDraggableComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-draggable.html' +}) +export class ModalDraggableComponent { + constructor(private tiModal: TiModalService) {} + + show(content: string): void { + this.tiModal.open(content, { + draggable: false + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalEscComponent.ts b/src/modal/demo/src/app/modal/ModalEscComponent.ts new file mode 100644 index 0000000..378fa68 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalEscComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-esc.html' +}) +export class ModalEscComponent { + constructor(private tiModal: TiModalService) {} + + show(content: string): void { + this.tiModal.open(content, { + closeOnEsc: false + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalEventComponent.ts b/src/modal/demo/src/app/modal/ModalEventComponent.ts new file mode 100644 index 0000000..7585718 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalEventComponent.ts @@ -0,0 +1,25 @@ +import { Component, TemplateRef } from '@angular/core'; +import { TiModalRef, TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: './modal-event.html' +}) +export class ModalEventComponent { + constructor(private tiModal: TiModalService) {} + myLogs: Array = []; + showModal(modalTemplate: TemplateRef): void { + this.tiModal.open(modalTemplate, { + id: 'myModal', + // 只有点击确认按钮时弹窗关闭 + beforeClose: (modalRef: TiModalRef, reason: boolean): void => { + if (reason) { + modalRef.destroy(reason); + } else { + this.myLogs = [...this.myLogs, '关闭不了']; + } + }, + close: (): void => {}, + dismiss: (): void => {} + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalHeaderAlignComponent.ts b/src/modal/demo/src/app/modal/ModalHeaderAlignComponent.ts new file mode 100644 index 0000000..46e710f --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalHeaderAlignComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-header-align.html' +}) +export class ModalHeaderAlignComponent { + constructor(private tiModal: TiModalService) {} + + show(content: string): void { + this.tiModal.open(content, { + headerAlign: 'center' + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalHeaderStyleComponent.ts b/src/modal/demo/src/app/modal/ModalHeaderStyleComponent.ts new file mode 100644 index 0000000..60011e6 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalHeaderStyleComponent.ts @@ -0,0 +1,16 @@ +import { Component, TemplateRef } from '@angular/core'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: './modal-header-style.html' +}) +export class ModalHeaderStyleComponent { + constructor(private tiModal: TiModalService) {} + modalName: string = 'aaa'; + show(content: TemplateRef): void { + this.tiModal.open(content, { + closeIcon: false, // 隐藏上方关闭按钮 + headerAlign: 'center' // 设置头部标题居中对齐 + }); + } +} diff --git a/src/modal/demo/src/app/modal/ModalTestModule.ts b/src/modal/demo/src/app/modal/ModalTestModule.ts new file mode 100644 index 0000000..5d19f3d --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalTestModule.ts @@ -0,0 +1,133 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { + TiButtonModule, + TiFormfieldModule, + TiIpModule, + TiModalModule, + TiSelectModule, + TiSpinnerModule, + TiTextareaModule, + TiTextModule, + TiValidationModule +} from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { ModalContentTempComponent } from './ModalContentTempComponent'; +import { ModalContentCompComponent, ModalTestComponent } from './ModalContentCompComponent'; +import { ModalEventComponent } from './ModalEventComponent'; +import { ModalHeaderStyleComponent } from './ModalHeaderStyleComponent'; +import { ModalClassComponent } from './ModalClassComponent'; +import { ModalTwoTestComponent } from './ModalTwoTestComponent'; +import { ModalTwoBackdropComponent } from './ModalTwoBackdropComponent'; +import { ModalConfigTestComponent } from './ModalConfigTestComponent'; +import { TestComponent } from './TestComponent'; +import { ModalAnimationComponent } from './ModalAnimationComponent'; +import { ModalBackdropComponent } from './ModalBackdropComponent'; +import { ModalCloseIconComponent } from './ModalCloseIconComponent'; +import { ModalDraggableComponent } from './ModalDraggableComponent'; +import { ModalHeaderAlignComponent } from './ModalHeaderAlignComponent'; +import { ModalEscComponent } from './ModalEscComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiIpModule, + TiModalModule, + TiButtonModule, + TiSelectModule, + TiTextareaModule, + TiFormfieldModule, + TiTextModule, + TiValidationModule, + TiSpinnerModule, + DemoLogModule, + RouterModule.forChild(ModalTestModule.ROUTES) + ], + declarations: [ + ModalConfigTestComponent, + ModalContentTempComponent, + ModalContentCompComponent, + TestComponent, + ModalTestComponent, + ModalClassComponent, + ModalTwoTestComponent, + ModalEventComponent, + ModalHeaderStyleComponent, + ModalHeaderStyleComponent, + ModalTwoBackdropComponent, + ModalAnimationComponent, + ModalBackdropComponent, + ModalCloseIconComponent, + ModalDraggableComponent, + ModalHeaderAlignComponent, + ModalEscComponent + ], + entryComponents: [TestComponent, ModalTestComponent] +}) +export class ModalTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiModalComponent.html', label: 'Modal' }, + { href: 'components/TiBackdropComponent.html', label: 'Backdrop' }, + { href: 'components/TiModalBodyComponent.html', label: 'ModalBody' }, + { href: 'components/TiModalHeaderComponent.html', label: 'ModalHeader' }, + { href: 'components/TiModalFooterComponent.html', label: 'ModalFooter' } + ]; + static readonly ROUTES: Routes = [ + { + path: 'modal/modal-content-comp', + component: ModalContentCompComponent + }, + { + path: 'modal/modal-content-temp', + component: ModalContentTempComponent + }, + { + path: 'modal/modal-class', + component: ModalClassComponent + }, + { + path: 'modal/modal-animation', + component: ModalAnimationComponent + }, + { + path: 'modal/modal-backdrop', + component: ModalBackdropComponent + }, + { + path: 'modal/modal-close-icon', + component: ModalCloseIconComponent + }, + { + path: 'modal/modal-draggable', + component: ModalDraggableComponent + }, + { + path: 'modal/modal-header-align', + component: ModalHeaderAlignComponent + }, + { + path: 'modal/modal-event', + component: ModalEventComponent + }, + { + path: 'modal/modal-header-style', + component: ModalHeaderStyleComponent + }, + { + path: 'modal/modal-config-test', + component: ModalConfigTestComponent + }, + { + path: 'modal/modal-esc', + component: ModalEscComponent + }, + { path: 'modal/modal-two-backdrop', component: ModalTwoBackdropComponent }, + { path: 'modal/modal-two-test', component: ModalTwoTestComponent } + ]; +} diff --git a/src/modal/demo/src/app/modal/ModalTwoBackdropComponent.ts b/src/modal/demo/src/app/modal/ModalTwoBackdropComponent.ts new file mode 100644 index 0000000..99e2175 --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalTwoBackdropComponent.ts @@ -0,0 +1,25 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TestComponent } from './TestComponent'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-two-backdrop.html' +}) +export class ModalTwoBackdropComponent { + constructor(private tiModal: TiModalService, protected changeDetectorRef: ChangeDetectorRef) {} + myModal: TestComponent; + show(): void { + this.tiModal.open(TestComponent, { + id: 'myModal', + backdrop: false // 第一个弹窗没有遮罩 + }); + setTimeout(() => { + this.tiModal.open(TestComponent, { + id: 'myModalId', + backdrop: true // 第二个弹窗有遮罩 + }); + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + }, 0); + } +} diff --git a/src/modal/demo/src/app/modal/ModalTwoTestComponent.ts b/src/modal/demo/src/app/modal/ModalTwoTestComponent.ts new file mode 100644 index 0000000..b75635e --- /dev/null +++ b/src/modal/demo/src/app/modal/ModalTwoTestComponent.ts @@ -0,0 +1,78 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TestComponent } from './TestComponent'; +import { TiModalService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'modal-two-test.html' +}) +export class ModalTwoTestComponent { + constructor(private tiModal: TiModalService, protected changeDetectorRef: ChangeDetectorRef) {} + myModal: TestComponent; + show(): void { + this.tiModal.open(TestComponent, { + id: 'myModal' + }); + setTimeout(() => { + this.tiModal.open(TestComponent, {}); + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + }, 0); + } + showModal(): void { + this.tiModal.open(TestComponent, { + id: 'myModalId' + }); + setTimeout(() => { + this.tiModal.open(TestComponent, { + id: 'myModalId' + }); + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + }, 0); + } + showMoreModals(): void { + setTimeout(() => { + this.tiModal.open(TestComponent, { + // id: 'myModal' + }); + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + }, 0); + setTimeout(() => { + this.tiModal.open(TestComponent, { + // id: 'myModal' + }); + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + }, 0); + setTimeout(() => { + this.tiModal.open(TestComponent, { + // id: 'myModal' + }); + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + setTimeout(() => { + console.log('close modals'); + const openedModals = this.tiModal.openedModals; + for (let i = 0; i < openedModals.length; i++) { + openedModals[i].dismiss(); + } + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + }, 20000); + }, 0); + } + closeOneShowAnother(): void { + this.tiModal.open(TestComponent, { + close: (): void => { + setTimeout(() => { + this.tiModal.open(TestComponent, { + id: 'myModal' + }); + // onpush模式需要强制刷新,default模式不需要。 + this.changeDetectorRef.markForCheck(); + }, 500); // 300ms~600ms + } + }); + } +} diff --git a/src/modal/demo/src/app/modal/TestComponent.ts b/src/modal/demo/src/app/modal/TestComponent.ts new file mode 100644 index 0000000..feaa736 --- /dev/null +++ b/src/modal/demo/src/app/modal/TestComponent.ts @@ -0,0 +1,72 @@ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { TiValidationConfig } from '@opentiny/ng'; + +@Component({ + template: ` + + I'm a {{ name }} modal! + + + body + + + + + + + + + + + + + + + + + + + + + + + + `, + encapsulation: ViewEncapsulation.None +}) +export class TestComponent { + @Input() myValue: string = ''; + @Input() name: string = ''; + value: string = 'sdf'; + placeholder: string = '请输入'; + myValue1: string = ''; + options: Array = []; + label: string = 'aaa'; + constructor() { + for (let i: number = 0; i < 20; i++) { + const option: Object = { + id: String(i), + label: `myemail${i}@example.com很长很长`, + pic: `pic${i}.png`, + disabled: i % 2 === 1 && i > 10 + }; + this.options.push(option); + } + } + myValidationConfig: TiValidationConfig = { + tip: 'should input email', + type: 'change' + }; + tipValue: string = ''; + close(): void {} + dismiss(): void {} +} diff --git a/src/modal/demo/src/app/modal/modal-animation.html b/src/modal/demo/src/app/modal/modal-animation.html new file mode 100644 index 0000000..b645039 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-animation.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-backdrop.html b/src/modal/demo/src/app/modal/modal-backdrop.html new file mode 100644 index 0000000..b645039 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-backdrop.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-class.html b/src/modal/demo/src/app/modal/modal-class.html new file mode 100644 index 0000000..7729540 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-class.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-close-icon.html b/src/modal/demo/src/app/modal/modal-close-icon.html new file mode 100644 index 0000000..b645039 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-close-icon.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-config-test.html b/src/modal/demo/src/app/modal/modal-config-test.html new file mode 100644 index 0000000..06ae2dd --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-config-test.html @@ -0,0 +1,24 @@ +

    描述

    +

    + modal组件,对外提供TiModalService服务,开发者将TiModalService注入到组件中,可得到服务实例tiModal。
    + 可以调用tiModal.open(TemplateRef, TiModalConfig)方法来打开弹框。
    + 该用例介绍tiModal.open方法的第二个参数的使用方式,参数说明见ModalConfigTestComponent.ts文件。 +

    +

    示例

    + + {{ context.headerTitle }} + + body + + + + + + + + + + + + +
    通过弹窗内部的按钮,改变弹窗外面的变量myNumber的当前值:{{myNumber}}
    diff --git a/src/modal/demo/src/app/modal/modal-content-comp.html b/src/modal/demo/src/app/modal/modal-content-comp.html new file mode 100644 index 0000000..4795567 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-content-comp.html @@ -0,0 +1,2 @@ + + diff --git a/src/modal/demo/src/app/modal/modal-content-temp.html b/src/modal/demo/src/app/modal/modal-content-temp.html new file mode 100644 index 0000000..b645039 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-content-temp.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-draggable.html b/src/modal/demo/src/app/modal/modal-draggable.html new file mode 100644 index 0000000..b645039 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-draggable.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-esc.html b/src/modal/demo/src/app/modal/modal-esc.html new file mode 100644 index 0000000..b645039 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-esc.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-event.html b/src/modal/demo/src/app/modal/modal-event.html new file mode 100644 index 0000000..311a01e --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-event.html @@ -0,0 +1,11 @@ + + modal title + body content + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-header-align.html b/src/modal/demo/src/app/modal/modal-header-align.html new file mode 100644 index 0000000..b645039 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-header-align.html @@ -0,0 +1,11 @@ + + modal title + modal body + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-header-style.html b/src/modal/demo/src/app/modal/modal-header-style.html new file mode 100644 index 0000000..6b99129 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-header-style.html @@ -0,0 +1,22 @@ +

    描述

    +

    弹框头部关闭按钮可自定义,标题也可定义位置

    +

    示例

    + + + + I'm a {{modalName}} modal! + 跳过 + + + body content + + + + + + + + + diff --git a/src/modal/demo/src/app/modal/modal-two-backdrop.html b/src/modal/demo/src/app/modal/modal-two-backdrop.html new file mode 100644 index 0000000..c6ba457 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-two-backdrop.html @@ -0,0 +1,6 @@ +

    描述

    +

    + 此场景修复的问题:两个弹窗同时打开,第一个弹窗没有遮罩,第二个弹窗有遮罩的场景。当关闭两个弹窗后,遮罩无法正常关闭的问题。10.1.13版本进行了修复 +

    +

    示例

    + diff --git a/src/modal/demo/src/app/modal/modal-two-test.html b/src/modal/demo/src/app/modal/modal-two-test.html new file mode 100644 index 0000000..854ba43 --- /dev/null +++ b/src/modal/demo/src/app/modal/modal-two-test.html @@ -0,0 +1,7 @@ +TODO:页面滚动条/modal内滚动条,单独写用例吧? +
    + + + + +
    diff --git a/src/modal/demo/src/app/modal/webdoc/modal-demos.js b/src/modal/demo/src/app/modal/webdoc/modal-demos.js new file mode 100644 index 0000000..5c7c2f8 --- /dev/null +++ b/src/modal/demo/src/app/modal/webdoc/modal-demos.js @@ -0,0 +1,138 @@ +export default { + column: '2', + demos: [ + { + demoId: 'modal-content-temp', + name: { + 'zh-CN': '弹出框内容为 template 形式', + 'en-US': 'template', + }, + desc: { + 'zh-CN': '

    使用了ng-template方式自定义弹出框模板。

    ', + 'en-US': '

    template

    ', + }, + apis: ['TiModalConfig.properties.id'], + }, + { + demoId: 'modal-content-comp', + name: { + 'zh-CN': '弹出框内容定义为组件形式', + 'en-US': 'component', + }, + desc: { + 'zh-CN': + '

    弹出框内容定义为组件形式时,弹出框中的内容组件需要在ngModule中定义其为entryComponents

    ', + 'en-US': '

    component

    ', + }, + apis: [ + 'TiModalConfig.properties.context', + 'TiModalConfig.methods.close', + 'TiModalConfig.methods.dismiss', + ], + }, + { + demoId: 'modal-class', + name: { + 'zh-CN': '自定义弹出框样式', + 'en-US': 'modalClass', + }, + desc: { + 'zh-CN': '

    通过modalClass自定义弹出框样式。

    ', + 'en-US': 'modalClass', + }, + apis: ['TiModalConfig.properties.modalClass'], + }, + { + demoId: 'modal-animation', + name: { + 'zh-CN': '显示/隐藏不带动画效果', + 'en-US': 'animation', + }, + desc: { + 'zh-CN': + '

    通过animation控制弹出框显示/隐藏是否带动画效果。

    ', + 'en-US': 'animation', + }, + apis: ['TiModalConfig.properties.animation'], + }, + { + demoId: 'modal-backdrop', + name: { + 'zh-CN': '不呈现模态背景', + 'en-US': 'backdrop', + }, + desc: { + 'zh-CN': '

    通过backdrop控制是否呈现模态背景。

    ', + 'en-US': 'backdrop', + }, + apis: ['TiModalConfig.properties.backdrop'], + }, + { + demoId: 'modal-close-icon', + name: { + 'zh-CN': '头部不呈现关闭按钮', + 'en-US': 'closeIcon', + }, + desc: { + 'zh-CN': '

    通过closeIcon头部是否有关闭按钮。

    ', + 'en-US': 'closeIcon', + }, + apis: ['TiModalConfig.properties.closeIcon'], + }, + { + demoId: 'modal-draggable', + name: { + 'zh-CN': '不可拖拽', + 'en-US': 'draggable', + }, + desc: { + 'zh-CN': '

    通过draggable控制弹出框是否可拖拽。

    ', + 'en-US': 'draggable', + }, + apis: ['TiModalConfig.properties.draggable'], + }, + { + demoId: 'modal-esc', + name: { + 'zh-CN': 'Esc 键弹出框不关闭', + 'en-US': 'closeOnEsc', + }, + desc: { + 'zh-CN': + '

    通过closeOnEsc控制按 Esc 键弹出框是否关闭。

    ', + 'en-US': 'closeOnEsc', + }, + apis: ['TiModalConfig.properties.closeOnEsc'], + }, + { + demoId: 'modal-header-align', + name: { + 'zh-CN': '头部水平居中', + 'en-US': 'headerAlign', + }, + desc: { + 'zh-CN': + '

    通过headerAlign配置头部内容水平对齐方式。

    ', + 'en-US': 'headerAlign', + }, + apis: ['TiModalConfig.properties.headerAlign'], + }, + { + demoId: 'modal-event', + name: { + 'zh-CN': '事件', + 'en-US': 'beforeClose', + }, + desc: { + 'zh-CN': + '

    通过beforeClose配置关闭弹出框前的回调函数。

    ', + 'en-US': 'beforeClose', + }, + apis: [ + 'TiModalConfig.methods.beforeClose', + 'TiModalConfig.methods.close', + 'TiModalConfig.methods.dismiss', + ], + }, + ], +}; diff --git a/src/modal/demo/src/app/modal/webdoc/modal.cn.md b/src/modal/demo/src/app/modal/webdoc/modal.cn.md new file mode 100644 index 0000000..f3ae981 --- /dev/null +++ b/src/modal/demo/src/app/modal/webdoc/modal.cn.md @@ -0,0 +1,34 @@ +--- +title: Modal 弹出框 +--- +# Modal 弹出框 + +
    + +Modal 是提供模态对话框的组件。   + ++ 弹出框组件提供服务方式供业务使用,使用该服务时需要引入模块`TiModalModule`,开发者通过调用`TiModalService.open`方法生成弹出框。 + +```typescript +import { TiModalModule } from '@opentiny/ng'; +``` + +需要在项目中(建议在根模块)中导入`BrowserAnimationsModule`。 + +```typescript +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +``` + +
    + +
    + +Modal 是提供模态对话框的组件。   + ++ 弹出框组件提供服务方式供业务使用,使用该服务时需要引入模块`TiModalModule`,开发者通过调用`TiModalService.open`方法生成弹出框。 + +```typescript +import { TiModalModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/modal/demo/src/app/modal/webdoc/modal.en.md b/src/modal/demo/src/app/modal/webdoc/modal.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/modal/demo/src/app/modal/webdoc/modal.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/modal/demo/src/favicon.ico b/src/modal/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/modal/demo/src/index.html b/src/modal/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/modal/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/modal/demo/src/main.ts b/src/modal/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/modal/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/modal/demo/test.ts b/src/modal/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/modal/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/modal/demo/tsconfig.app.json b/src/modal/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/modal/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/modal/demo/tsconfig.spec.json b/src/modal/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/modal/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/modal/lib/index.ts b/src/modal/lib/index.ts new file mode 100644 index 0000000..c68c346 --- /dev/null +++ b/src/modal/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiModalModule'; +export * from './src/TiModalService'; diff --git a/src/modal/lib/ng-package.json b/src/modal/lib/ng-package.json new file mode 100644 index 0000000..e042faa --- /dev/null +++ b/src/modal/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/modal", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/modal/lib/package.json b/src/modal/lib/package.json new file mode 100644 index 0000000..b292be5 --- /dev/null +++ b/src/modal/lib/package.json @@ -0,0 +1,18 @@ +{ + "name": "@opentiny/ng-modal", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/router": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/animations": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-popup": "~1.0.0-beta.0", + "@opentiny/ng-drag": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/modal/lib/project.json b/src/modal/lib/project.json new file mode 100644 index 0000000..b975f65 --- /dev/null +++ b/src/modal/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/modal/lib", + "sourceRoot": "src/modal/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/modal"], + "options": { + "project": "src/modal/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/modal"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js modal" + }, + { + "command": "ng default-build modal" + }, + { + "command": "node build/clear-default-theme.js modal" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/modal && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build modal && ng pack modal && node build/publish.js modal --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/modal/lib/src/TiBackdropComponent.ts b/src/modal/lib/src/TiBackdropComponent.ts new file mode 100644 index 0000000..7226e01 --- /dev/null +++ b/src/modal/lib/src/TiBackdropComponent.ts @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Input, Renderer2 } from '@angular/core'; +import { animate, state, style, transition, trigger } from '@angular/animations'; + +/** + * @ignore + */ +export const animateStyle: string = '0.3s cubic-bezier(0.40, 0.00, 0.20, 1.00)'; +/** + * @ignore + */ +@Component({ + selector: 'ti-backdrop', + template: `
    `, + styleUrls: ['./backdrop.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger('backdropAnimate', [ + state( + 'show', + style({ + opacity: 0.2 + }) + ), + state( + 'hide, void', + style({ + opacity: 0 + }) + ), + transition('show => hide', animate(animateStyle)), + transition('hide => show', animate(animateStyle)) + ]) + ] +}) +export class TiBackdropComponent { + constructor(protected renderer: Renderer2, protected changeDetectorRef: ChangeDetectorRef) {} + + protected _showState: string; // 遮罩的显示状态,用于控制动画的状态切换 + + set showState(value: string) { + if (this._showState !== value) { + this._showState = value; + // onpush 模式下,点击 ok / cancel 按钮关闭弹窗和遮罩时;改变模板变量 showState,需要手动触发变更检测来强制更新。 + this.changeDetectorRef.markForCheck(); + } + } + + get showState(): string { + return this._showState; + } + + public animation: boolean; // 是否开启动画 + public index: number = 0; // 多个弹出框情况下,zindex层级设置辅助 + public destroy: () => void; + // 在微前端,多个子应用之间url路径跳转,等到弹出框隐藏结束后,this.destroy() 已经无效。 + @HostListener('window:hashchange', ['$event']) onhashchange(event: HashChangeEvent): void { + if (event.newURL !== event.oldURL) { + this.destroy(); + } + } + onAnimationDone($event: any): void { + // 弹框隐藏动画结束后,销毁弹框DOM + if ($event.toState === 'hide') { + this.destroy(); + + return; + } + } +} diff --git a/src/modal/lib/src/TiBackdropNoAnimationComponent.ts b/src/modal/lib/src/TiBackdropNoAnimationComponent.ts new file mode 100644 index 0000000..fd7e569 --- /dev/null +++ b/src/modal/lib/src/TiBackdropNoAnimationComponent.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { TiBackdropComponent } from './TiBackdropComponent'; +/** + * @ignore + */ +@Component({ + selector: 'ti-backdrop-no-animation', + template: `
    `, + styleUrls: ['./backdrop.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiBackdropNoAnimationComponent extends TiBackdropComponent { + set showState(value: string) { + this._showState = value; + this.onAnimationDone({ toState: value }); + } +} diff --git a/src/modal/lib/src/TiModalBodyComponent.ts b/src/modal/lib/src/TiModalBodyComponent.ts new file mode 100644 index 0000000..dcaea1a --- /dev/null +++ b/src/modal/lib/src/TiModalBodyComponent.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +/** + * 弹框body容器标签,可将弹框body内容定义在该容器内 + */ +@Component({ + selector: 'ti-modal-body', + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-modal-body]': 'true' + } +}) +export class TiModalBodyComponent {} diff --git a/src/modal/lib/src/TiModalComponent.html b/src/modal/lib/src/TiModalComponent.html new file mode 100644 index 0000000..3c19d34 --- /dev/null +++ b/src/modal/lib/src/TiModalComponent.html @@ -0,0 +1,16 @@ +
    +
    +
    + + +
    +
    +
    diff --git a/src/modal/lib/src/TiModalComponent.ts b/src/modal/lib/src/TiModalComponent.ts new file mode 100644 index 0000000..f9eacd6 --- /dev/null +++ b/src/modal/lib/src/TiModalComponent.ts @@ -0,0 +1,351 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + AfterContentInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + HostListener, + Inject, + Optional, + Renderer2, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { NavigationEnd, Router, RouterEvent } from '@angular/router'; +import { filter } from 'rxjs/operators'; +import { Subscription } from 'rxjs'; +import { DOCUMENT } from '@angular/common'; + +import { animate, state, style, transition, trigger } from '@angular/animations'; +import { TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * @ignore + */ +export const animateStyle: string = '0.3s cubic-bezier(0.40, 0.00, 0.20, 1.00)'; +/** + * @ignore + */ +@Component({ + selector: 'ti-modal-wrapper', + templateUrl: './TiModalComponent.html', + styleUrls: ['./modal.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + animations: [ + // 由于动画创建需要在元数据中解析,因此动画定义必须在此处定义 + trigger('modalAnimate', [ + state( + 'show', + style({ + opacity: 1 + }) + ), + state( + 'hide, void', + style({ + opacity: 0 + }) + ), + transition('show => hide', animate(animateStyle)), + transition('hide => show', animate(animateStyle)) + ]), + trigger('modalPosAnimate', [ + state( + 'show', + style({ + transform: 'translate(0, 0px)' + }) + ), + state( + 'hide, void', + style({ + transform: 'translate(0, -6px)' + }) + ), + transition('show => hide', animate(animateStyle)), + transition('hide => show', animate(animateStyle)) + ]) + ] +}) +export class TiModalComponent extends TiBaseComponent implements AfterContentInit { + /** + * 规范刷新,弹出框整体的最大高度为660px(按照最小分辨率1280*768计算) + */ + private static readonly MODAL_MAX_HEIGHT: number = 660; + // 可通过tab聚焦的元素选择器列表 + private static readonly tababbleSelector: string = `a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), + button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), + textarea:not([disabled]):not([tabindex=\'-1\']), + iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]`; + + @ViewChild('modalContent', { static: true }) private contentRef: ElementRef; + @ViewChild('modalEle', { static: true }) protected modalEleRef: ElementRef; + + protected _showState: string; // 组件显示状态,用于控制动画的状态切换 + set showState(value: string) { + if (this._showState !== value) { + this._showState = value; + // onpush模式下,点击ok/cancel按钮关闭面板。改变模板变量showState,却没有触发更新。强制更新。 + this.changeDetectorRef.markForCheck(); + } + } + get showState(): string { + return this._showState; + } + public animation: boolean; // 是否有动画 + public draggable: boolean; // 是否可拖拽 + public closeOnEsc: boolean; // 是否可通过ESC键关闭弹框 + public modalInstance: any; // 组件实例,可使用该实例调用方法控制组件行为 + public index: number = 0; // 多个弹出框情况下,zindex层级设置辅助 + public closeIcon: boolean; // 是否有头部关闭按钮 + public modalClass: string; // 弹框样式设置 + public headerAlign: 'center' | 'left' | 'right'; // 头部对齐方式 + public dragOptions: any; // 拖拽配置 + protected versionInfo: string = super.getVersion(packageInfo); + private focusableElements: Array = []; // 弹框中可聚焦元素列表 + private modalSubscribe: Subscription; + constructor( + ele: ElementRef, + private tiRenderer: TiRenderer, + protected renderer: Renderer2, + @Optional() private router: Router, + protected changeDetectorRef: ChangeDetectorRef, + @Inject(DOCUMENT) private document + ) { + super(ele, renderer); + } + + private static preventEvent(event: Event): void { + event.preventDefault(); + event.stopPropagation(); + } + + // 路由切换,关闭弹出框: 前进/后退/更改url等跳转,通过routerLink/navigate路由跳转监听不到 + @HostListener('window:hashchange', ['$event']) onhashchange(event: HashChangeEvent): void { + if (event.newURL !== event.oldURL) { + this.modalInstance._remove(); + } + } + + // 缩放时重置弹框位置 + @HostListener('window:resize') onResize(): void { + this.tiRenderer.setStyles(this.contentRef.nativeElement, { + left: 0, + top: 0 + }); + } + // 键盘事件处理 + @HostListener('document:keydown', ['$event']) onKeydown(event: KeyboardEvent): void { + switch (event.which) { + case TiKeymap.KEY_ESCAPE: // close on ESC + if (this.closeOnEsc) { + this.dismissModal(); + } + break; + case TiKeymap.KEY_TAB: // tab键用于处理在弹框内循环获取焦点 + this.onTabChange(event); + break; + default: + break; + } + } + // 弹出框外围出现的滚动条滚动会导致下拉等定位在body上的组件相对于宿主元素定位偏移, + // 因此触发tiScroll事件做处理,事件包括: + // 整个页面滚动条滚轮滚动事件, mousewheel处理Chrome和IE下事件,DOMMouseScroll处理firefox下事件 + // 整个页面滚动条鼠标滚动事件 + @HostListener('document:mousewheel') + @HostListener('document:DOMMouseScroll') + @HostListener('window:scroll') + onTiScroll(): void { + Util.trigger(document, 'tiScroll'); + } + + ngAfterContentInit(): void { + this.dragOptions = { + // 由于此处需要获取ngContent中的内容,因此在该时机中进行处理 + // 这时弹出框内容DOM还未解析,所以需要使用标签选择器('ti-modal-header')来获取元素 + handle: this.nativeElement.querySelector('ti-modal-header'), + drag: (opts: { + position: { + left: number; + top: number; + }; + helper: Element; + }): { left: number; top: number } => { + Util.trigger(document, 'tiScroll'); + // 当前浏览器可视区域的宽高 + const curBrowseWidth: number = document.documentElement.clientWidth; + const curBrowseheight: number = document.documentElement.clientHeight; + // 元素原始位置(由于元素本身left和top实时变化,因此取其父元素位置) + const eleRect: ClientRect | DOMRect = opts.helper.parentElement.getBoundingClientRect(); + // 元素距页面可视区域的可用位置范围 + const minLeft: number = -eleRect.left + document.body.scrollLeft; + const maxLeft: number = Math.max(curBrowseWidth + minLeft - eleRect.width, minLeft); + const minTop: number = -eleRect.top + document.body.scrollTop; + const maxTop: number = Math.max(curBrowseheight + minTop - eleRect.height, minTop); + const position: { left: number; top: number } = opts.position; + // 元素left位置根据可用left范围计算 + if (position.left < minLeft) { + position.left = minLeft; + } else if (position.left > maxLeft) { + position.left = maxLeft; + } + // 元素top位置根据可用top范围计算 + if (position.top < minTop) { + position.top = minTop; + } else if (position.top > maxTop) { + position.top = maxTop; + } + + return position; + } + }; + // 监听路由变化,关闭弹出框:hashchange监听不到通过routerLink/navigate路由跳转的场景 + this.modalSubscribe = this.router?.events.pipe(filter((event: RouterEvent) => event instanceof NavigationEnd)).subscribe(() => { + this.modalInstance._remove(); + }); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + this.modalSubscribe && this.modalSubscribe.unsubscribe(); + } + + public dismissModal(): void { + this.modalInstance.dismiss(); + } + + public onAnimationDone($event: any): void { + // 弹框toState有三种状态:'show'/'hide'/null + // 弹框显示动画结束后,重新将焦点聚焦在弹框元素/弹框中 + if ($event.toState === 'show') { + this.reStyleModal(); + this.focusInModal(); + + return; + } + // 弹框隐藏动画结束后,销毁弹框DOM + if ($event.toState === 'hide') { + this.modalInstance._remove(); + } + } + + // 弹框打开时,设置弹框元素样式 + private reStyleModal(): void { + const parentEle: Element = this.nativeElement.querySelector('.ti3-modal-header'); + if (this.draggable !== false) { + this.renderer.setStyle(parentEle, 'cursor', 'move'); + } + if (this.headerAlign === 'center' || this.headerAlign === 'right') { + this.renderer.setStyle(parentEle, 'text-align', this.headerAlign); + } + + this.renderer.addClass(this.document.body, 'ti3-modal-open'); + this.setMaxHeight(); + } + /** + * 设置弹框高度: + * 根据UCD3.0规范,modal最大高度不能超过弹框宽度,所以需要获取到modal的宽度值,用其减去modal的header和footer的高度 + * 后设为body的最大高度,当高度超出时,ti-modal-body垂直方向上出现滚动条 + * 10.1.1变更说明: + * 根据规范变更,弹框高度可以超过弹框宽度,同时弹出框的最大高度是660px。 + */ + private setMaxHeight(): void { + // 修复SSR问题:ERROR TypeError: modalHeaderEle.getBoundingClientRect is not a function + const modalHeaderEle: Element = this.nativeElement.querySelector('.ti3-modal-header'); + const modalHeaderHeight: number = + modalHeaderEle && typeof modalHeaderEle.getBoundingClientRect === 'function' ? modalHeaderEle.getBoundingClientRect().height : 0; + const modalFooterEle: Element = this.nativeElement.querySelector('.ti3-modal-footer'); + const modalFooterHeight: number = + modalFooterEle && typeof modalFooterEle.getBoundingClientRect === 'function' ? modalFooterEle.getBoundingClientRect().height : 0; + const modalBodyMaxHeight: number = TiModalComponent.MODAL_MAX_HEIGHT - modalHeaderHeight - modalFooterHeight; + this.renderer.setStyle(this.nativeElement.querySelector('.ti3-modal-body'), 'maxHeight', modalBodyMaxHeight + 'px'); + } + private focusInModal(): void { + /** + * 首先判断新打开的modal是否已存在获取焦点的元素 + * 如果存在,则需要对modal元素再次设置焦点 + * 否则,对modal容器设置焦点 + */ + const activeElement: any = this.document.activeElement; + const isfocusedInModal: boolean = activeElement && this.nativeElement.contains(activeElement); + if (isfocusedInModal) { + /** + * 对已经设置焦点的元素,再次设置焦点,是为了解决以下两个问题: + * 1>首次加载时,已经获取焦点的元素,其输入tip提示位置偏移 + * 2>首次加载时,IE浏览器中,已经获取焦点的元素input,光标位置出现偏移 + */ + if (activeElement) { + activeElement.blur(); + // tiautofocusinmodal属性需要在change校验中使用(ChangeCheck.ts) + this.renderer.setAttribute(activeElement, 'tiautofocusinmodal', ''); + activeElement.focus(); + this.renderer.removeAttribute(activeElement, 'tiautofocusinmodal'); + } + } else { + // 弹出框内未找到已聚焦元素情况下,优先处理 input 和 textarea 原生标签autofocus聚焦 + // (input,textarea.button原生标签的autofocus弹出框打开时无效(chrome是从第二次打开无效),因此要做此处理) + let focusEle: any = this.nativeElement.querySelector(`input[autofocus]:not([disabled]):not([tabindex=\'-1\']), + textarea[autofocus]:not([disabled]):not([tabindex=\'-1\']), button[autofocus]:not([disabled]):not([tabindex=\'-1\'])`); + // 如果通过上述处理,聚焦元素仍未获取到的情况下,则可聚焦元素为当前弹框 + focusEle = focusEle || this.modalEleRef.nativeElement; + focusEle.focus(); + } + } + // 用于处理tab键行为,确保tab时只可在弹框内循环获取焦点 + private onTabChange(event: KeyboardEvent): void { + const focusableElementList: Array = this.getFocusableElesInModal(); // 获取当前弹出框DOM中可以获取焦点的元素列表 + const focusableListLength: number = focusableElementList.length; + if (focusableListLength === 0) { + return; + } + const target: EventTarget = event.target; // 事件对象元素 + const firstFocusableEle: any = focusableElementList[0]; // 第一个可获取焦点的元素 + const lastFocusableEle: any = focusableElementList[focusableListLength - 1]; // 最后一个可获取焦点的元素 + // 按下tab+shift键时,如果当前已获取焦点元素是弹出框中的第一个可获取焦点元素或弹框本身,则聚焦最后一个元素 + if (event.shiftKey) { + if (target === firstFocusableEle || target === this.modalEleRef.nativeElement) { + lastFocusableEle.focus(); + TiModalComponent.preventEvent(event); // 阻止默认事件及继续冒泡,确保此处手动focus生效 + } + + return; + } + // 按下tab键时,如当前已获取焦点元素是弹框中最后一个可获取焦点元素,则聚焦第一个元素 + if (target === lastFocusableEle) { + firstFocusableEle.focus(); + TiModalComponent.preventEvent(event); + } + } + private getFocusableElesInModal(): Array { + if (this.focusableElements.length !== 0) { + return this.focusableElements; + } + const contentEle: Element = this.contentRef.nativeElement; + if (contentEle) { + const elements: NodeListOf = contentEle.querySelectorAll(TiModalComponent.tababbleSelector); + [].forEach.call(elements, (element: any) => { + // 找出页面真实显示的元素,只有显示的元素才可tab聚焦 + if (getComputedStyle(element).display !== 'none' && element.offsetWidth > 0 && element.offsetHeight > 0) { + this.focusableElements.push(element); + } + }); + } + + return this.focusableElements; + } +} diff --git a/src/modal/lib/src/TiModalComponentNoAnimation.html b/src/modal/lib/src/TiModalComponentNoAnimation.html new file mode 100644 index 0000000..88f9516 --- /dev/null +++ b/src/modal/lib/src/TiModalComponentNoAnimation.html @@ -0,0 +1,15 @@ +
    +
    +
    + + +
    +
    +
    diff --git a/src/modal/lib/src/TiModalFooterComponent.ts b/src/modal/lib/src/TiModalFooterComponent.ts new file mode 100644 index 0000000..893fe70 --- /dev/null +++ b/src/modal/lib/src/TiModalFooterComponent.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +/** + * 弹框header容器标签,可将弹框底部内容定义在该容器内 + */ +@Component({ + selector: 'ti-modal-footer', + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-modal-footer]': 'true' + } +}) +export class TiModalFooterComponent {} diff --git a/src/modal/lib/src/TiModalHeaderComponent.ts b/src/modal/lib/src/TiModalHeaderComponent.ts new file mode 100644 index 0000000..32163ee --- /dev/null +++ b/src/modal/lib/src/TiModalHeaderComponent.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +/** + * 弹框header容器标签,可将弹框头部内容定义在该容器内 + */ +@Component({ + selector: 'ti-modal-header', + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-modal-header]': 'true' + } +}) +export class TiModalHeaderComponent {} diff --git a/src/modal/lib/src/TiModalInterface.ts b/src/modal/lib/src/TiModalInterface.ts new file mode 100644 index 0000000..dd33c11 --- /dev/null +++ b/src/modal/lib/src/TiModalInterface.ts @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ComponentRef, Injector } from '@angular/core'; + +/** + * 弹出框组件生成配置参数接口 + * + * 该接口用于弹出框组件[open]{@link TiModalService#open}方法的参数的定义 + */ +export interface TiModalConfig { + /** + * 弹出框 id 唯一标识,避免同一页面生成多个相同弹出框 + */ + id?: string; + /** + * 头部内容水平对齐方向 + * + * @default 'left' + */ + headerAlign?: 'center' | 'left' | 'right'; // 头部内容水平对齐位置 + /** + * 是否有关闭按钮 + * + * @default true + */ + closeIcon?: boolean; + /** + * 弹出框样式定义,该样式定义在弹出框整体元素上,可通过该样式定义弹出框的宽高等 + */ + modalClass?: string; + /** + * 是否显示模态背景 + * + * @default true + */ + backdrop?: boolean; + /** + * 是否可拖拽 + * + * @default true + */ + draggable?: boolean; + /** + * 弹出框显示/隐藏是否带动画效果 + * + * @default true + */ + animation?: boolean; + /** + * 弹出框内容上下文,为自定义对象形式 + */ + context?: any; + /** + * 是否可通过 ESC 快捷键关闭弹出框 + * + * @default true + */ + closeOnEsc?: boolean; + /** + * 弹出框内容为组件时,内容组件的父级注入器 + */ + parentInjector?: Injector; + /** + * 关闭弹出框前的回调函数,可通过 beforeClose 事件阻止弹出框关闭:在 beforeClose 中通过 modalRef 调用 destroy 时,关闭弹出框,否则不关闭 + * + * 参数:TiModalRef:弹出框实例 reason:弹出框关闭原因(调用 close 关闭时,reason 为 true;调用 dismiss 关闭时,reason 为 false) + * + */ + beforeClose?(modalRef: TiModalRef, reason: boolean): void; + /** + * 关闭弹出框的事件回调(一般点确认时会进入该回调),参数:modalRef 弹出框实例 + */ + close?(modalRef: TiModalRef): void; + /** + * 关闭弹出框的事件回调(一般点击取消或右上角X关闭时会进入该回调),参数:modalRef 弹出框实例 + */ + dismiss?(modalRef: TiModalRef): void; +} + +/** + * 弹出框组件实例对象接口 + * + * 该接口用于弹出框组件[open]{@link TiModalService#open}方法的返回值定义 + */ +export interface TiModalRef { + /** + * 弹出框内容实例,适用于component形式的内容数据获取,具体用法可参考示例 + */ + content: ComponentRef; // 弹出框内容实例,可通过该实例获取content对应的相关数据,component形式的模板数据获取需要使用该方式 + /** + * @ignore + */ + _id: string; // 弹出框ID存储,该id用来标识弹出框的唯一性 + /** + * @ignore + */ + _backdrop: boolean; // 用于标记当前弹出框是否有遮罩 + /** + * 弹出框关闭方法,主要用在点击"确认(OK)"按钮时关闭弹出框 + * + * **函数类型:**() => void; + */ + close(): void; // 弹出框实例的方法,用来关闭弹出框,主要用在点击"确认(OK)"按钮时关闭弹出框 + /** + * 弹出框关闭方法,主要用在点击"取消(Cancel)"或右上方X按钮时关闭弹出框 + * + * **函数类型:**() => void; + */ + dismiss(): void; // 弹出框实例的方法,用来关闭弹出框,与close()的区别是:dismiss()主要用在点击"取消(Cancel)"按钮时关闭弹出框 + /** + * 弹出框销毁方法 + * + * **函数类型:**(reason: boolean) => void; + * + * **参数:** + * + * 弹出框关闭原因 boolean,同beforeClose参数中reason的意义 + */ + destroy(reason: boolean): void; // 销毁弹出框,外部调用 + /** + * @ignore + * 弹出框销毁方法,无关取消/关闭事件等 + */ + _remove(): void; +} diff --git a/src/modal/lib/src/TiModalModule.ts b/src/modal/lib/src/TiModalModule.ts new file mode 100644 index 0000000..b15e4d7 --- /dev/null +++ b/src/modal/lib/src/TiModalModule.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiPopupModule } from '@opentiny/ng-popup'; +import { TiModalComponent } from './TiModalComponent'; +import { TiModalNoAnimationComponent } from './TiModalNoAnimationComponent'; +import { TiBackdropComponent } from './TiBackdropComponent'; +import { TiBackdropNoAnimationComponent } from './TiBackdropNoAnimationComponent'; +import { TiModalHeaderComponent } from './TiModalHeaderComponent'; +import { TiModalBodyComponent } from './TiModalBodyComponent'; +import { TiModalFooterComponent } from './TiModalFooterComponent'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiDraggableModule } from '@opentiny/ng-drag'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; + +@NgModule({ + imports: [CommonModule, TiDraggableModule, TiIconModule, TiPopupModule, TiOutlineModule, TiRendererModule], + exports: [ + TiModalComponent, + TiModalNoAnimationComponent, + TiBackdropNoAnimationComponent, + TiModalHeaderComponent, + TiModalFooterComponent, + TiModalBodyComponent + ], + declarations: [ + TiModalComponent, + TiModalNoAnimationComponent, + TiBackdropNoAnimationComponent, + TiModalHeaderComponent, + TiModalFooterComponent, + TiModalBodyComponent, + TiBackdropComponent + ], + entryComponents: [TiModalComponent, TiModalNoAnimationComponent, TiBackdropNoAnimationComponent, TiBackdropComponent] +}) +export class TiModalModule {} + +export * from './TiModalInterface'; +export { TiModalHeaderComponent } from './TiModalHeaderComponent'; +export { TiModalFooterComponent } from './TiModalFooterComponent'; +export { TiModalBodyComponent } from './TiModalBodyComponent'; +export { TiModalComponent } from './TiModalComponent'; +export { TiModalNoAnimationComponent } from './TiModalNoAnimationComponent'; +export { TiBackdropNoAnimationComponent } from './TiBackdropNoAnimationComponent'; diff --git a/src/modal/lib/src/TiModalNoAnimationComponent.ts b/src/modal/lib/src/TiModalNoAnimationComponent.ts new file mode 100644 index 0000000..a10d8ff --- /dev/null +++ b/src/modal/lib/src/TiModalNoAnimationComponent.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ViewEncapsulation, ChangeDetectionStrategy } from '@angular/core'; +import { TiModalComponent } from './TiModalComponent'; +/** + * @ignore + */ +@Component({ + selector: 'ti-modal-wrapper-no-animation', + templateUrl: './TiModalComponentNoAnimation.html', + styleUrls: ['./modal.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None +}) +export class TiModalNoAnimationComponent extends TiModalComponent { + set showState(value: string) { + this._showState = value; + this.onAnimationDone({ toState: value }); + } +} diff --git a/src/modal/lib/src/TiModalService.ts b/src/modal/lib/src/TiModalService.ts new file mode 100644 index 0000000..1376c6e --- /dev/null +++ b/src/modal/lib/src/TiModalService.ts @@ -0,0 +1,244 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ComponentRef, Injectable, TemplateRef } from '@angular/core'; +import { TiPopUpRef, TiPopupService } from '@opentiny/ng-popup'; +import { TiBackdropComponent } from './TiBackdropComponent'; +import { TiBackdropNoAnimationComponent } from './TiBackdropNoAnimationComponent'; +import { TiModalComponent } from './TiModalComponent'; +import { TiModalNoAnimationComponent } from './TiModalNoAnimationComponent'; +import { TiBrowser, Util } from '@opentiny/ng-utils'; +import { TiModalModule } from './TiModalModule'; +import { TiModalConfig, TiModalRef } from './TiModalInterface'; + +/** + * 弹框组件提供服务方式供业务使用,使用该服务时需要引入模块TiModalModule,开发者通过调用TiModalService.open方法生成弹出框 + * + *

    使用此组件时需要开发者在项目模块(建议在根模块) + * 中引入BrowserAnimationsModule。这是因为此组件中使用了Angular动画,需要引入BrowserAnimationsModule, + * 但是 BrowserAnimationsModule 不能在懒加载模块被重复引入,所以需要开发者来引入BrowserAnimationsModule,保证其引入一次。

    + * + */ +@Injectable({ + providedIn: TiModalModule +}) +export class TiModalService { + /** + * 页面中存在的所有弹框实例,用于处理业务中无法明确当前有多少有效弹出框场景 + */ + public openedModals: Array = []; // 所有当前处于打开状态弹框缓存数组 + private backdropComponentRef: ComponentRef = null; // 单个页面中多个弹框出现时,只允许存在一个遮罩层,因此使用该标志位用来缓存遮罩实例 + private defaultConfig: TiModalConfig = { + backdrop: true, + draggable: true, + animation: true, + closeOnEsc: true, + closeIcon: true + }; + constructor(private popService: TiPopupService) {} + + /** + * 生成弹框方法 + * + * @param content 弹框内容,支持TemplateRef及组件形式,内容由ti-modal-header、ti-modal-body及ti-modal-footer组件构成弹框内容整体。 + * 不支持字符串形式,如果误传入字符串形式,不仅会有报错,还会存在XSS攻击风险,不过XSS攻击风险已在10.1.4版本已处理。 + * @param [config] 弹框配置信息 + * @returns 弹框实例信息,使用该实例信息可调用弹框的close等方法;弹框内容为component类型时,也可通过该实例信息的content属性获取弹框内容数据 + */ + public open(content: TemplateRef | any, config?: TiModalConfig): TiModalRef { + let modalInstance: TiModalRef; // 弹框最终返回的实例对象 + let modalComponentRef: ComponentRef; // 弹框组件实例,使用该实例获取弹框DOM元素及做属性赋值操作 + const modalConfig: TiModalConfig = { ...this.defaultConfig, ...config }; // 弹框配置合并 + let modal: { + modalComponentRef: ComponentRef; + hide(): void; + }; // generateModal方法返回值 + + // 通过id唯一标识防止重复打开相同弹框,id重复情况下返回先前实例 + if (modalConfig && !Util.isUndefined(modalConfig.id)) { + const index: number = this.openedModals.findIndex((item: TiModalRef) => { + return item._id === modalConfig.id; + }); + if (index !== -1) { + return this.openedModals[index]; + } + } + // 根据beforeClose返回处理弹出框的销毁:业务在beforeClose中调用弹框销毁 + const destroy: (reason: boolean) => void = (reason: boolean): void => { + if (typeof modalConfig.beforeClose === 'function') { + modalConfig.beforeClose(modalInstance, reason); + } else { + destroyModal(reason); + } + }; + // 销毁弹框:通过调用组件的动画状态方式实现动画处理 + const destroyModal: (reason?: boolean) => void = (reason?: boolean): void => { + if (reason) { + if (typeof modalConfig.close === 'function') { + modalConfig.close(modalInstance); + } + } else { + if (typeof modalConfig.dismiss === 'function') { + modalConfig.dismiss(modalInstance); + } + } + modalComponentRef.instance.showState = 'hide'; + }; + // 销毁弹框实体:弹框动画执行完成后调用该方法 + const removeModalEle: () => void = (): void => { + // 已经销毁,不做处理 + if (modalInstance === null) { + return; + } + + // 销毁弹框实体DOM + modal.hide(); + // 移除缓存实例 + const index: number = this.openedModals.indexOf(modalInstance); + this.openedModals.splice(index, 1); + modalInstance = null; + + // 销毁backdrop + destroyBackdrop(); + }; + const destroyBackdrop: () => void = (): void => { + // backdrop定义为false时,不进行处理 + // backdropComponentRef 已经销毁 + if (!modalConfig.backdrop || !this.backdropComponentRef) { + return; + } + // 修改backdrop zIndex,确保有多个弹出框情况下,最外层弹出框不被遮挡 + this.backdropComponentRef.instance.index = this.openedModals.length - 1; + /** + * 问题:两个弹出框,第一个打开的弹出框无遮罩,再此基础上打开第二个弹出框,第二个弹出框有遮罩。 + * 此时关闭第二个弹出框,关闭是有遮罩的弹出框,此处判断还有一个弹出框实例,遮罩没有关闭。 + * 关闭第一个弹出框时,因为没有设置遮罩,modalConfig.backdrop为false,关闭遮罩的逻辑就没执行,此时的现象是两个弹出框已经关闭了,但是遮罩无法关闭。 + * 解决:过滤出有遮罩的弹出框,如果还有打开的弹出框,配置了遮罩,则不关闭遮罩, + * 如果打开的弹出框,没有配置遮罩,那就直接关闭遮罩。 + */ + // 当前还有其它弹框情况下且其他弹出框也有遮罩时,不用销毁遮罩实例 + if (this.getOpenedModalsWithBackdrop().length !== 0) { + return; + } + this.backdropComponentRef.instance.showState = 'hide'; + }; + // 将当前要打开的modal放到openedModals列表中 + modalInstance = { + _id: modalConfig && modalConfig.id, + _backdrop: modalConfig && modalConfig.backdrop, + close(): void { + destroy(true); + }, + dismiss(): void { + destroy(false); + }, + destroy(reason: boolean): void { + destroyModal(reason); + }, + _remove: removeModalEle, + content: null + }; + modal = this.generateModal(content, modalConfig, modalInstance); + // 生成弹框window,并返回对应的组件实例 + modalComponentRef = modal.modalComponentRef; + // 生成模态背景backdrop + this.backdropComponentRef = this.generateBackdrop(modalConfig); + + return modalInstance; + } + + // 在打开的弹出框中,获取有遮罩的弹出框集合 + private getOpenedModalsWithBackdrop(): Array { + let backdropModal: Array = []; + backdropModal = this.openedModals.filter((item: TiModalRef) => { + return item._backdrop; + }); + + return backdropModal; + } + + private generateModal( + content: any, + config: TiModalConfig, + modalInstance: TiModalRef + ): { + modalComponentRef: ComponentRef; + hide(): void; + } { + const { context, ...modalConfig }: TiModalConfig = config; + // tiModalComponent生成需要使用到的上下文 + const modalContext: any = { + modalInstance, + index: this.openedModals.length, + ...modalConfig + }; + // 内容部分需要使用到的上下文 + const contentContext: any = { + close: modalInstance.close, + dismiss: modalInstance.dismiss, + ...context + }; + let modalComponent: any; + modalComponent = + TiBrowser.isIE() && TiBrowser.version() === 9 + ? this.popService.create(TiModalNoAnimationComponent) + : this.popService.create(TiModalComponent); + const modalComponentRef: any = modalComponent.show({ + content, + context: modalContext, + contentContext, + contentParentInjector: config.parentInjector, + container: 'body' + }); + // 控制元素动画呈现,元素呈现后修改动画状态才会有动画效果,因此在此处处理 + modalComponentRef.instance.showState = 'show'; + modalInstance.content = modalComponentRef.tiContentRef.componentRef; + this.openedModals.push(modalInstance); + + return { modalComponentRef, hide: modalComponent.hide }; + } + private generateBackdrop(config: TiModalConfig): ComponentRef { + // 配置中不需要backdrop,不生成 + if (!config.backdrop) { + return this.backdropComponentRef; + } + + if (this.backdropComponentRef) { + // backdrop已存在的情况下,修改其zIndex,并确保不生成backdrop + if (this.backdropComponentRef.instance.showState === 'show') { + this.backdropComponentRef.instance.index = this.openedModals.length - 1; + + return this.backdropComponentRef; + // 如果backdrop正在销毁(动画执行期间)时再新生成一个backdrop,这时应该将旧的backdrop立即销毁 + } else { + this.backdropComponentRef.destroy(); + } + } + const backdropComponent: TiPopUpRef = + TiBrowser.isIE() && TiBrowser.version() === 9 + ? this.popService.create(TiBackdropNoAnimationComponent) + : this.popService.create(TiBackdropComponent); + const backdropRef: ComponentRef = backdropComponent.show({ + context: { + animation: config.animation, + destroy: (): void => { + backdropComponent.hide(); + this.backdropComponentRef = null; + } + }, + container: 'body' + }); + // 控制元素动画呈现,元素呈现后修改动画状态才会有动画效果,因此在此处处理 + backdropRef.instance.showState = 'show'; + + return backdropRef; + } +} diff --git a/src/modal/lib/src/backdrop.less b/src/modal/lib/src/backdrop.less new file mode 100644 index 0000000..0efc0a1 --- /dev/null +++ b/src/modal/lib/src/backdrop.less @@ -0,0 +1,17 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-backdrop-bg-color: #000000; // 遮罩层颜色 +} + +.ti3-modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: @zindex-modal-background; + box-sizing: border-box; + background-color: var(--ti-backdrop-bg-color); + opacity: 0.2; +} diff --git a/src/modal/lib/src/modal.less b/src/modal/lib/src/modal.less new file mode 100644 index 0000000..e35ece8 --- /dev/null +++ b/src/modal/lib/src/modal.less @@ -0,0 +1,114 @@ +@import '../../../themes/basic/base-all.less'; + +ti-modal-wrapper { + --ti-modal-width: 550px; +} + +// Container that the modal scrolls within +.ti3-modal { + /* Positioning */ + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: @zindex-modal; + + /* Display & Box Model */ + .box-sizing(border-box); + overflow: hidden; + + /* Other */ + -webkit-overflow-scrolling: touch; + // Prevent Chrome on Windows from adding a focus outline. + outline: 0; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); +} + +.ti3-modal-open .ti3-modal { + overflow-x: hidden; + overflow-y: auto; +} + +// Shell div to position the modal with bottom padding +.ti3-modal-dialog { + position: relative; + .box-sizing(border-box); + width: auto; + margin: 10px; +} + +// Actual modal +.ti3-modal-content { + position: relative; + .box-sizing(border-box); + .border-radius(var(--ti-common-border-radius-normal)); + background-color: var(--ti-common-color-bg-white-normal); + //更改阴影颜色 + .box-shadow(var(--ti-common-shadow-4-down)); + background-clip: padding-box; + // Remove focus outline from opened modal + outline: 0; +} + +// Modal header +.ti3-modal-header { + /* Display & Box Model */ + .box-sizing(border-box); + padding: var(--ti-common-space-8x) var(--ti-common-space-8x) var(--ti-common-space-7x); + color: var(--ti-common-color-text-primary); + font-size: var(--ti-common-font-size-3); + font-weight: var(--ti-common-font-weight-7); + line-height: 100%; + display: block; + &.ti3-modal-title-center { + text-align: center; + } +} + +.ti3-modal-close { + position: absolute; + top: var(--ti-common-space-2x); + right: var(--ti-common-space-5x); + .box-sizing(border-box); + color: var(--ti-common-color-icon-normal); + cursor: pointer; + font: normal normal normal var(--ti-common-size-3x) / var(--ti-common-size-9x) ti3Font; + text-transform: none; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + &:hover { + color: var(--ti-common-color-icon-hover); + } +} + +// Modal body(sibling of .modal-header and .modal-footer) +.ti3-modal-body { + position: relative; + overflow-y: auto; + .box-sizing(border-box); + display: block; + padding: var(--ti-common-space-0) var(--ti-common-space-8x); +} + +// Footer (for actions) +.ti3-modal-footer { + padding: var(--ti-common-space-7x) var(--ti-common-space-8x) var(--ti-common-space-8x); + text-align: center; // center align buttons + display: block; + + & button:nth-of-type(2) { + margin-left: var(--ti-common-space-10); // 默认与HEC规范保持一致 + } +} + +// Scale up the modal +@media screen and (min-width: 768px) { + // Automatically set modal's width for larger viewports + .ti3-modal-dialog { + width: var(--ti-modal-width); + margin: 150px auto 0; + } +} diff --git a/src/nav/demo/karma.conf.js b/src/nav/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/nav/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/nav/demo/project.json b/src/nav/demo/project.json new file mode 100644 index 0000000..c3ba393 --- /dev/null +++ b/src/nav/demo/project.json @@ -0,0 +1,98 @@ +{ + "projectType": "application", + "root": "src/nav/demo", + "sourceRoot": "src/nav/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/nav", + "index": "src/nav/demo/src/index.html", + "main": "src/nav/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/nav/demo/tsconfig.app.json", + "assets": [ + "src/nav/demo/src/favicon.ico", + "src/nav/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/nav_logo", + "output": "/assets/nav_logo/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "nav-demo:build:production" + }, + "development": { + "browserTarget": "nav-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js nav" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/nav/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/nav/demo/tsconfig.spec.json", + "karmaConfig": "src/nav/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/nav/demo/src/app/AppComponent.ts b/src/nav/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/nav/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/nav/demo/src/app/AppModule.ts b/src/nav/demo/src/app/AppModule.ts new file mode 100644 index 0000000..c58c586 --- /dev/null +++ b/src/nav/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { NavTestModule } from './nav/NavTestModule'; + +@NgModule({ + imports: [ + NavTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/nav/demo/src/app/IndexComponent.ts b/src/nav/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..6e546db --- /dev/null +++ b/src/nav/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { NavTestModule } from './nav/NavTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = NavTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/nav/demo/src/app/app.html b/src/nav/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/nav/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/nav/demo/src/app/nav/NavActiveComponent.ts b/src/nav/demo/src/app/nav/NavActiveComponent.ts new file mode 100644 index 0000000..4afdf98 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavActiveComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-active.html' +}) +export class NavActiveComponent { + activeId: string = 'button'; + items: TiNavMenuItem[] = [ + { label: '首页', id: 'home' }, + { label: '组件总览', id: 'component' }, + { + label: '使用指南', + id: 'doc', + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start' }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本框', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ]; + items2: TiNavMenuItem[] = [ + { label: '首页', id: 'home' }, + { + label: '组件总览', + id: 'component', + children: [ + { + label: '使用指南', + id: 'doc', + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start' }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本框', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ] + } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavAlignComponent.ts b/src/nav/demo/src/app/nav/NavAlignComponent.ts new file mode 100644 index 0000000..07e1ba7 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavAlignComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-align.html' +}) +export class NavAlignComponent { + items: TiNavMenuItem[] = [ + { id: '1', label: '首页' }, + { id: '2', label: '文档' }, + { id: '3', label: '组件' }, + { id: '4', label: '关于' } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavBasicComponent.ts b/src/nav/demo/src/app/nav/NavBasicComponent.ts new file mode 100644 index 0000000..900b91d --- /dev/null +++ b/src/nav/demo/src/app/nav/NavBasicComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiMenuItem } from '@opentiny/ng'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-basic.html' +}) +export class NavBasicComponent { + items: TiNavMenuItem[] = [ + { id: '1', label: '首页' }, + { id: '2', label: '文档' }, + { id: '3', label: '组件' }, + { id: '4', label: '关于' } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavDisabledComponent.ts b/src/nav/demo/src/app/nav/NavDisabledComponent.ts new file mode 100644 index 0000000..3ff0e34 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavDisabledComponent.ts @@ -0,0 +1,85 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-disabled.html' +}) +export class NavDisabledComponent { + items: TiNavMenuItem[] = [ + { label: '首页', id: 'home' }, + { label: '组件总览', id: 'component', disabled: true }, + { + label: '使用指南', + id: 'doc', + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start', disabled: true }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + disabled: true, + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本框', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ]; + items2: TiNavMenuItem[] = [ + { label: '首页', id: 'home', disabled: true }, + { + label: '组件总览', + id: 'component', + children: [ + { + label: '使用指南', + id: 'doc', + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start', disabled: true }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + disabled: true, + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本框', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ] + } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavEventComponent.ts b/src/nav/demo/src/app/nav/NavEventComponent.ts new file mode 100644 index 0000000..b800ca8 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavEventComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-event.html' +}) +export class NavEventComponent { + logs: string[] = []; + onSelect({ item, itemPath }: { item: TiNavMenuItem; itemPath: TiNavMenuItem[] }) { + this.logs.push(`选择:${item.label},菜单路径:${itemPath.map((item) => item.label).join(' -> ')}`); + } + + activeId: string = 'home'; + items: TiNavMenuItem[] = [ + { label: '首页', id: 'home' }, + { label: '组件总览', id: 'component' }, + { + label: '使用指南', + id: 'doc', + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start' }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本框', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavLeftComponent.ts b/src/nav/demo/src/app/nav/NavLeftComponent.ts new file mode 100644 index 0000000..9082e15 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavLeftComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-left.html' +}) +export class NavLeftComponent { + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL']; + logoUrl: string = `${this.baseUrl}assets/nav_logo/logo.png`; + items: TiNavMenuItem[] = [ + { id: '1', label: '首页' }, + { id: '2', label: '文档' }, + { id: '3', label: '组件' }, + { id: '4', label: '关于' } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavRightComponent.ts b/src/nav/demo/src/app/nav/NavRightComponent.ts new file mode 100644 index 0000000..154de61 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavRightComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-right.html' +}) +export class NavRightComponent { + items: TiNavMenuItem[] = [ + { id: '1', label: '首页' }, + { id: '2', label: '文档' }, + { id: '3', label: '组件' }, + { id: '4', label: '关于' } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavSelectableComponent.ts b/src/nav/demo/src/app/nav/NavSelectableComponent.ts new file mode 100644 index 0000000..91e6ecd --- /dev/null +++ b/src/nav/demo/src/app/nav/NavSelectableComponent.ts @@ -0,0 +1,91 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-selectable.html' +}) +export class NavSelectableComponent { + activeId: string = 'component'; + items: TiNavMenuItem[] = [ + { label: '首页', id: 'home' }, + { label: '组件总览', id: 'component' }, + { + label: '使用指南', + id: 'doc', + selectable: false, + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start' }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + selectable: false, + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + selectable: false, + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本框', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ]; + items2: TiNavMenuItem[] = [ + { label: '首页', id: 'home' }, + { + label: '组件总览', + id: 'component', + selectable: false, + children: [ + { + label: '使用指南', + id: 'doc', + selectable: false, + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start' }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + selectable: false, + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + selectable: false, + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本框', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ] + } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavSubmenuComponent.ts b/src/nav/demo/src/app/nav/NavSubmenuComponent.ts new file mode 100644 index 0000000..b9f6a2f --- /dev/null +++ b/src/nav/demo/src/app/nav/NavSubmenuComponent.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-submenu.html' +}) +export class NavSubmenuComponent { + items: TiNavMenuItem[] = [ + { label: '首页' }, + { label: '组件总览' }, + { + label: '使用指南', + children: [{ label: '介绍' }, { label: '快速上手' }, { label: '主题配置' }, { label: '国际化' }, { label: '常见问题' }] + }, + { + label: '表单选择', + children: [{ label: '按钮' }, { label: '选择器' }, { label: '单选框' }, { label: '复选框' }] + }, + { + label: '表单文本', + children: [{ label: '文本框' }, { label: '多行文本框' }, { label: '自动补全' }, { label: '搜索框' }] + } + ]; + + items2: TiNavMenuItem[] = [ + { label: '首页' }, + { + label: '组件总览', + children: [ + { + label: '使用指南', + children: [{ label: '介绍' }, { label: '快速上手' }, { label: '主题配置' }, { label: '国际化' }, { label: '常见问题' }] + }, + { + label: '表单选择', + children: [{ label: '按钮' }, { label: '选择器' }, { label: '单选框' }, { label: '复选框' }] + }, + { + label: '表单文本', + children: [{ label: '文本框' }, { label: '多行文本框' }, { label: '自动补全' }, { label: '搜索框' }] + } + ] + } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavTemplateComponent.ts b/src/nav/demo/src/app/nav/NavTemplateComponent.ts new file mode 100644 index 0000000..5b92f67 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavTemplateComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-template.html' +}) +export class NavTemplateComponent { + items: TiNavMenuItem[] = [ + { label: '首页', id: 'home' }, + { + label: '组件总览', + id: 'component', + children: [ + { + label: '使用指南', + icon: 'check-circle', + id: 'doc', + children: [ + { label: '介绍', id: 'intro' }, + { label: '快速上手', id: 'start' }, + { label: '主题配置', id: 'theme' }, + { label: '国际化', id: 'locale' }, + { label: '常见问题', id: 'q&a' } + ] + }, + { + label: '表单选择', + id: 'form-select', + children: [ + { label: '按钮', id: 'button' }, + { label: '选择器', id: 'select' }, + { label: '单选框', id: 'radio' }, + { label: '复选框', id: 'checkbox' } + ] + }, + { + label: '表单文本', + id: 'form-text', + children: [ + { label: '文本框', id: 'input-text' }, + { label: '多行文本', id: 'input-textarea' }, + { label: '自动补全', id: 'autocomplete' }, + { label: '搜索框', id: 'search' } + ] + } + ] + } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavTestModule.ts b/src/nav/demo/src/app/nav/NavTestModule.ts new file mode 100644 index 0000000..cb885ec --- /dev/null +++ b/src/nav/demo/src/app/nav/NavTestModule.ts @@ -0,0 +1,100 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { TiButtonModule, TiIconModule, TiMenuModule, TiNavModule } from '@opentiny/ng'; +import { NavActiveComponent } from './NavActiveComponent'; +import { NavAlignComponent } from './NavAlignComponent'; +import { NavBasicComponent } from './NavBasicComponent'; +import { NavDisabledComponent } from './NavDisabledComponent'; +import { NavEventComponent } from './NavEventComponent'; +import { NavLeftComponent } from './NavLeftComponent'; +import { NavTemplateComponent } from './NavTemplateComponent'; +import { NavRightComponent } from './NavRightComponent'; +import { NavSelectableComponent } from './NavSelectableComponent'; +import { NavSubmenuComponent } from './NavSubmenuComponent'; +import { NavThemeComponent } from './NavThemeComponent'; +import { NavWidthComponent } from './NavWidthComponent'; + +@NgModule({ + imports: [CommonModule, TiNavModule, TiMenuModule, TiButtonModule, TiIconModule, RouterModule.forChild(NavTestModule.ROUTES)], + declarations: [ + NavBasicComponent, + NavWidthComponent, + NavThemeComponent, + NavLeftComponent, + NavRightComponent, + NavAlignComponent, + NavSubmenuComponent, + NavActiveComponent, + NavSelectableComponent, + NavDisabledComponent, + NavTemplateComponent, + NavEventComponent + ] +}) +export class NavTestModule { + static readonly LINKS: Array<{ href: string; label: string }> = [{ href: 'components/TiNavComponent.html', label: 'Nav' }]; + + static readonly ROUTES: Routes = [ + { + path: 'nav/nav-basic', + component: NavBasicComponent, + data: { label: '基础' } + }, + { + path: 'nav/nav-width', + component: NavWidthComponent, + data: { label: '自定义宽度' } + }, + { + path: 'nav/nav-theme', + component: NavThemeComponent, + data: { label: '深色主题' } + }, + { + path: 'nav/nav-left', + component: NavLeftComponent, + data: { label: '左侧Logo区' } + }, + { + path: 'nav/nav-right', + component: NavRightComponent, + data: { label: '右侧操作区' } + }, + { + path: 'nav/nav-align', + component: NavAlignComponent, + data: { label: '菜单区对齐' } + }, + { + path: 'nav/nav-submenu', + component: NavSubmenuComponent, + data: { label: '子菜单' } + }, + { + path: 'nav/nav-active', + component: NavActiveComponent, + data: { label: '激活项' } + }, + { + path: 'nav/nav-selectable', + component: NavSelectableComponent, + data: { label: '可选择项' } + }, + { + path: 'nav/nav-disabled', + component: NavDisabledComponent, + data: { label: '禁用项' } + }, + { + path: 'nav/nav-template', + component: NavTemplateComponent, + data: { label: '使用模板渲染标签' } + }, + { + path: 'nav/nav-event', + component: NavEventComponent, + data: { label: '事件' } + } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavThemeComponent.ts b/src/nav/demo/src/app/nav/NavThemeComponent.ts new file mode 100644 index 0000000..7b813e9 --- /dev/null +++ b/src/nav/demo/src/app/nav/NavThemeComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-theme.html' +}) +export class NavThemeComponent { + items: TiNavMenuItem[] = [ + { id: '1', label: '首页' }, + { id: '2', label: '文档' }, + { id: '3', label: '组件' }, + { id: '4', label: '关于' } + ]; +} diff --git a/src/nav/demo/src/app/nav/NavWidthComponent.ts b/src/nav/demo/src/app/nav/NavWidthComponent.ts new file mode 100644 index 0000000..2e6c09b --- /dev/null +++ b/src/nav/demo/src/app/nav/NavWidthComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { TiNavMenuItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './nav-width.html' +}) +export class NavWidthComponent { + items: TiNavMenuItem[] = [ + { id: '1', label: '首页' }, + { id: '2', label: '文档' }, + { id: '3', label: '组件' }, + { id: '4', label: '关于' } + ]; +} diff --git a/src/nav/demo/src/app/nav/nav-active.html b/src/nav/demo/src/app/nav/nav-active.html new file mode 100644 index 0000000..6f2802f --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-active.html @@ -0,0 +1,19 @@ +
    + + TinyNG + + +
    + +
    + + TinyNG + + +
    + + diff --git a/src/nav/demo/src/app/nav/nav-align.html b/src/nav/demo/src/app/nav/nav-align.html new file mode 100644 index 0000000..e2d3fc1 --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-align.html @@ -0,0 +1,23 @@ + + TinyNG + + + + + + + + TinyNG + + + + + + + + TinyNG + + + + + diff --git a/src/nav/demo/src/app/nav/nav-basic.html b/src/nav/demo/src/app/nav/nav-basic.html new file mode 100644 index 0000000..2d797f7 --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-basic.html @@ -0,0 +1,9 @@ + + TinyNG + + + + + + + diff --git a/src/nav/demo/src/app/nav/nav-disabled.html b/src/nav/demo/src/app/nav/nav-disabled.html new file mode 100644 index 0000000..cab6b0f --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-disabled.html @@ -0,0 +1,19 @@ +
    + + TinyNG + + +
    + +
    + + TinyNG + + +
    + + diff --git a/src/nav/demo/src/app/nav/nav-event.html b/src/nav/demo/src/app/nav/nav-event.html new file mode 100644 index 0000000..ca59135 --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-event.html @@ -0,0 +1,16 @@ +
    + + TinyNG + + + +
    +

    {{log}}

    +
    +
    + + diff --git a/src/nav/demo/src/app/nav/nav-left.html b/src/nav/demo/src/app/nav/nav-left.html new file mode 100644 index 0000000..d2fb5ea --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-left.html @@ -0,0 +1,15 @@ + + + + TinyNG + + + + + diff --git a/src/nav/demo/src/app/nav/nav-right.html b/src/nav/demo/src/app/nav/nav-right.html new file mode 100644 index 0000000..2e70140 --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-right.html @@ -0,0 +1,16 @@ + + TinyNG + + + + 欢迎使用TinyNG + 访问主页 + + + + + diff --git a/src/nav/demo/src/app/nav/nav-selectable.html b/src/nav/demo/src/app/nav/nav-selectable.html new file mode 100644 index 0000000..6f2802f --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-selectable.html @@ -0,0 +1,19 @@ +
    + + TinyNG + + +
    + +
    + + TinyNG + + +
    + + diff --git a/src/nav/demo/src/app/nav/nav-submenu.html b/src/nav/demo/src/app/nav/nav-submenu.html new file mode 100644 index 0000000..9462bd9 --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-submenu.html @@ -0,0 +1,33 @@ +
    + + TinyNG + + +
    + +
    + + TinyNG + + +
    + +
    + + TinyNG + + +
    + +
    + + TinyNG + + +
    + + diff --git a/src/nav/demo/src/app/nav/nav-template.html b/src/nav/demo/src/app/nav/nav-template.html new file mode 100644 index 0000000..1d22ba8 --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-template.html @@ -0,0 +1,17 @@ +
    + + TinyNG + + + {{ item.label }} + + + + +
    + + diff --git a/src/nav/demo/src/app/nav/nav-theme.html b/src/nav/demo/src/app/nav/nav-theme.html new file mode 100644 index 0000000..ff11021 --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-theme.html @@ -0,0 +1,9 @@ + + TinyNG + + + + + TinyNG + + diff --git a/src/nav/demo/src/app/nav/nav-width.html b/src/nav/demo/src/app/nav/nav-width.html new file mode 100644 index 0000000..a56939a --- /dev/null +++ b/src/nav/demo/src/app/nav/nav-width.html @@ -0,0 +1,4 @@ + + TinyNG + + diff --git a/src/nav/demo/src/app/nav/webdoc/nav-demos.js b/src/nav/demo/src/app/nav/webdoc/nav-demos.js new file mode 100644 index 0000000..89ee92f --- /dev/null +++ b/src/nav/demo/src/app/nav/webdoc/nav-demos.js @@ -0,0 +1,149 @@ +export default { + column: '1', + demos: [ + { + demoId: 'nav-basic', + name: { + 'zh-CN': '基础', + 'en-US': 'nav basic' + }, + desc: { + 'zh-CN': '

    提供 Nav 组件的基本使用示例,其中包含左侧站点标题,中间导航菜单,右侧按钮。

    ', + 'en-US': '

    nav basic

    ' + } + }, + { + demoId: 'nav-width', + name: { + 'zh-CN': '自定义宽度', + 'en-US': 'nav width' + }, + desc: { + 'zh-CN': '

    通过属性原生属性可以设置导航栏宽度,参考 CSS 的width属性。', + 'en-US': '

    nav width

    ' + } + }, + { + demoId: 'nav-theme', + name: { + 'zh-CN': '深色主题', + 'en-US': 'nav theme' + }, + desc: { + 'zh-CN': + '

    通过属性theme配置 nav 组件主题,提供了深色dark和浅色light两种选项,默认使用light浅色主题。

    ', + 'en-US': '

    nav theme

    ' + }, + apis: ['TiNavComponent.properties.theme'] + }, + { + demoId: 'nav-left', + name: { + 'zh-CN': '左侧 Logo 区', + 'en-US': 'nav left' + }, + desc: { + 'zh-CN': '

    提供一个容器用来放置站点 Logo 图片和站点标题,预设了img标签的样式,也可根据需要自定义内容。

    ', + 'en-US': '

    nav left

    ' + } + }, + { + demoId: 'nav-right', + name: { + 'zh-CN': '右侧操作区', + 'en-US': 'nav right' + }, + desc: { + 'zh-CN': '

    右侧操作区用于放置a标签、span标签和button标签等元素。

    ', + 'en-US': '

    nav right

    ' + } + }, + { + demoId: 'nav-align', + name: { + 'zh-CN': '菜单区对齐', + 'en-US': 'nav align' + }, + desc: { + 'zh-CN': + '

    导航菜单区域采用了display: flex布局,可以通过修改justify-content属性来设置导航菜单元素的对齐方式,参考 CSS 的justfy-content属性。

    ', + 'en-US': '

    nav align

    ' + } + }, + { + demoId: 'nav-submenu', + name: { + 'zh-CN': '子菜单', + 'en-US': 'nav submenu' + }, + desc: { + 'zh-CN': + '

    通过属性items传入菜单项数组,根据菜单对象的children属性嵌套层数决定了展开子菜单的方式,二级嵌套采用下拉菜单,三级嵌套采用下拉面板。

    ', + 'en-US': '

    nav submenu

    ' + }, + apis: ['TiNavMenuComponent.properties.items'] + }, + { + demoId: 'nav-active', + name: { + 'zh-CN': '激活项', + 'en-US': 'nav active' + }, + desc: { + 'zh-CN': '

    通过传入activeId修改当前激活项,被激活的菜单项及其父级菜单会有高亮显示。

    ', + 'en-US': '

    nav active

    ' + }, + apis: ['TiNavMenuComponent.properties.activeId'] + }, + { + demoId: 'nav-selectable', + name: { + 'zh-CN': '可选择项', + 'en-US': 'nav selectable' + }, + desc: { + 'zh-CN': + '

    通过在items中设置selectable控制对应菜单项是否可被选中,默认为true,被标记为不可选中的项目不会触发选择事件,建议在一级和二级菜单项上根据需要使用。

    ', + 'en-US': '

    nav selectable

    ' + } + }, + { + demoId: 'nav-disabled', + name: { + 'zh-CN': '禁用项', + 'en-US': 'nav disabled' + }, + desc: { + 'zh-CN': + '

    通过在items中设置disabled控制对应菜单项及其子项是否处于已禁用状态,默认为false

    ', + 'en-US': '

    nav disabled

    ' + } + }, + { + demoId: 'nav-template', + name: { + 'zh-CN': '使用模板渲染标签', + 'en-US': 'nav template' + }, + desc: { + 'zh-CN': + '

    提供了item插槽来自定义 item 标签的内容,用户可以编写自定义模板,从模板上下文中获取标签对象item: TiNavMenuItem,可以在传入的items中添加需要的自定义数据,例如这里的示例添加了icon字段,并根据是否有icon字段来展示图标。

    ', + 'en-US': '

    nav template

    ' + }, + apis: ['TiNavMenuComponent.slots.itemTemplate'] + }, + { + demoId: 'nav-event', + name: { + 'zh-CN': '事件', + 'en-US': 'nav event' + }, + desc: { + 'zh-CN': + '

    选择菜单项将触发选择事件,并返回对象,其中item为被选择的对象数据,itemPath是选项数组,保存了从顶层到底层被选择的每一级对象数据。

    ', + 'en-US': '

    nav event

    ' + }, + apis: ['TiNavMenuComponent.events.select'] + } + ] +}; diff --git a/src/nav/demo/src/app/nav/webdoc/nav.cn.md b/src/nav/demo/src/app/nav/webdoc/nav.cn.md new file mode 100644 index 0000000..462adee --- /dev/null +++ b/src/nav/demo/src/app/nav/webdoc/nav.cn.md @@ -0,0 +1,25 @@ +--- +title: Nav 导航栏 +--- + +# Nav 导航栏 + +
    + +Nav 组件提供了页面顶部导航栏,包含了左侧logo区域、中间的导航菜单和右侧操作区域 + +```typescript +import { TiNavModule } from '@opentiny/ng'; +``` + +
    + +
    + +Nav 组件提供了页面顶部导航栏,包含了左侧logo区域、中间的导航菜单和右侧操作区域 + +```typescript +import { TiNavModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/nav/demo/src/app/nav/webdoc/nav.en.md b/src/nav/demo/src/app/nav/webdoc/nav.en.md new file mode 100644 index 0000000..e1a4208 --- /dev/null +++ b/src/nav/demo/src/app/nav/webdoc/nav.en.md @@ -0,0 +1,7 @@ +--- +title: Nav +--- + +# Nav + +The nav component provides a navigation bar at the top of the page, including the logo area on the left, the navigation menu in the middle, and the operation area on the right. diff --git a/src/nav/demo/src/favicon.ico b/src/nav/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/nav/demo/src/index.html b/src/nav/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/nav/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/nav/demo/src/main.ts b/src/nav/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/nav/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/nav/demo/test.ts b/src/nav/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/nav/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/nav/demo/tsconfig.app.json b/src/nav/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/nav/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/nav/demo/tsconfig.spec.json b/src/nav/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/nav/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/nav/lib/index.ts b/src/nav/lib/index.ts new file mode 100644 index 0000000..930968a --- /dev/null +++ b/src/nav/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiNavModule'; diff --git a/src/nav/lib/ng-package.json b/src/nav/lib/ng-package.json new file mode 100644 index 0000000..cab05b2 --- /dev/null +++ b/src/nav/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/nav", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/nav/lib/package.json b/src/nav/lib/package.json new file mode 100644 index 0000000..ec26573 --- /dev/null +++ b/src/nav/lib/package.json @@ -0,0 +1,9 @@ +{ + "name": "@opentiny/ng-nav", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/nav/lib/project.json b/src/nav/lib/project.json new file mode 100644 index 0000000..cc49048 --- /dev/null +++ b/src/nav/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/nav/lib", + "sourceRoot": "src/nav/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/nav"], + "options": { + "project": "src/nav/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/nav"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js nav" + }, + { + "command": "ng default-build nav" + }, + { + "command": "node build/clear-default-theme.js nav" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/nav && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build nav && ng pack nav && node build/publish.js nav --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/nav/lib/src/TiNavComponent.ts b/src/nav/lib/src/TiNavComponent.ts new file mode 100644 index 0000000..081fbda --- /dev/null +++ b/src/nav/lib/src/TiNavComponent.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +/** + * Nav 顶部导航组件 + * + * 提供页面顶部导航组件,包含了logo区域、菜单区域、操作区域 + */ +@Component({ + selector: 'ti-nav', + templateUrl: 'nav.html', + styleUrls: ['./nav.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'ti3-nav', + '[class.ti3-nav-light]': 'theme === "light"', + '[class.ti3-nav-dark]': 'theme === "dark"' + } +}) +export class TiNavComponent { + /** + * 主题,light|dark 默认light + */ + @Input() theme: 'light' | 'dark' = 'light'; +} diff --git a/src/nav/lib/src/TiNavLeftComponent.ts b/src/nav/lib/src/TiNavLeftComponent.ts new file mode 100644 index 0000000..f2fd705 --- /dev/null +++ b/src/nav/lib/src/TiNavLeftComponent.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * NavLeft 顶部导航logo区域组件 + * + * 提供顶部导航logo区域,由logo图片和文本标题组成,可以监听click点击事件 + */ +@Component({ + selector: 'ti-nav-left', + templateUrl: './navleft.html', + styleUrls: ['./navleft.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'ti3-nav-left' + } +}) +export class TiNavLeftComponent {} diff --git a/src/nav/lib/src/TiNavMenuComponent.ts b/src/nav/lib/src/TiNavMenuComponent.ts new file mode 100644 index 0000000..3d8e04e --- /dev/null +++ b/src/nav/lib/src/TiNavMenuComponent.ts @@ -0,0 +1,257 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ContentChild, EventEmitter, Input, Output, SimpleChanges, TemplateRef } from '@angular/core'; +import { TiNavMenuItem } from './interface'; + +/** + * @ignore + * + * 增强菜单项 + * + * 为一级菜单增加了子菜单展开标记和原始菜单项 + */ +interface TiNavMenuItemClone extends TiNavMenuItem { + /** + * 是否展开子菜单 + */ + isExpand: boolean; + /** + * 展开模式: menu 菜单模式展开, panel 面板模式展开, undefined 不展开 + */ + expandMode?: 'menu' | 'panel' | undefined; + /** + * 原始菜单项 + */ + item: TiNavMenuItem; +} + +/** + * NavMenu 顶部导航菜单组件 + * + * 提供页面顶部导航菜单,包含一级菜单、二级菜单 + */ +@Component({ + selector: 'ti-nav-menu', + templateUrl: './navmenu.html', + styleUrls: ['./navmenu.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'ti3-nav-menu' + } +}) +export class TiNavMenuComponent { + constructor() {} + + /** + * 菜单标签占位文本区域的模板 + */ + @ContentChild('item', { static: true }) itemTemplate: TemplateRef; + + /** + * 菜单项 + */ + @Input() items: Array = []; + + /** + * @ignore + * + * 增强的菜单项 + */ + itemsClone: Array = []; + + /** + * 传入需要激活的选项的id + */ + @Input() activeId: string; + + /** + * @ignore + * + * 当前激活项目列表 + */ + activePath: Array = []; + + /** + * 选择菜单项触发回调 + */ + @Output() select = new EventEmitter<{ + item: TiNavMenuItem; + itemPath: Array; + }>(); + + /** + * @ignore + * + * 监听数据变化 + * + * @param changes 发生变动的数据 + */ + ngOnChanges(changes: SimpleChanges): void { + // 当出入菜单项列表发生改变时,更新增强菜单项列表 + if (changes.items) { + // 增强的菜单项,增加了expand表示是否展开子菜单,默认为false,增加了item用来保存原始item + const itemsClone = JSON.parse(JSON.stringify(changes.items.currentValue)) as Array; + this.checkDefault(itemsClone, 'default'); + this.itemsClone = itemsClone.map((item) => { + return { + ...item, + isExpand: false, + expandMode: this.getExpandMode(item), + item + } as TiNavMenuItemClone; + }); + } + if (changes.activeId) { + this.activePath = this.getPathById(this.items, changes.activeId.currentValue); + } + } + + /** + * @ignore + * + * 导航菜单点击事件 + * + * 点击导航菜单,传入鼠标事件,被点击的原始菜单项 item,激活 id 数组 + * + * @param event 鼠标点击事件 + * @param item 被点击的项目 + * @param activePath 激活id数组 + * @param isParentDisabled 父级是否被禁用 + */ + onClick(event: PointerEvent, item: TiNavMenuItem, activePath: Array, isParentDisabled = false): void { + event.stopPropagation(); + // 禁用项 和 不可选中项 不触发选中回调 + if (!item.disabled && !isParentDisabled && item.selectable) { + // 触发外部选择菜单事件回调 + this.select.emit({ item, itemPath: activePath }); + this.activePath = activePath; + } + } + + /** + * @ignore + * + * 鼠标移入菜单项事件 + * + * 当鼠标移入时,展开子菜单 + * + * @param event 鼠标事件 + * @param id 一级菜单id + */ + onMouseenter(event: MouseEvent, id: string): void { + // 根据一级菜单 id 值寻找对应项,更新它的 expand 值为 true + this.handleToggleExpand(id, true); + } + + /** + * @ignore + * + * 鼠标移出菜单项事件 + * + * 当鼠标移出时,关闭子菜单 + * + * @param event 鼠标事件 + * @param id 一级菜单id + */ + onMouseleave(event: MouseEvent, id: string): void { + // 根据一级菜单 id 值寻找对应项,更新它的 expand 值为 false + this.handleToggleExpand(id, false); + } + + /** + * 子菜单开关 + * + * @param id 一级菜单 id 值 + * @param expand 新的展开值 + */ + private handleToggleExpand(id: string, expand: boolean): void { + const item = this.itemsClone.find((item) => item.id === id && !item.disabled); + // 二级菜单展开和关闭逻辑 + if (item) { + item.isExpand = expand; + } + } + + /** + * 获取子菜单展开模式 + * + * @param item + * @returns + */ + private getExpandMode(item: TiNavMenuItem): string { + let res; + for (let subItem of item.children || []) { + if (subItem.children && subItem.children.length > 0) { + return 'panel'; + } + res = 'menu'; + } + return res; + } + + /** + * 检查默认值,如果没有则进行初始化 + * + * @param items + * @param id + */ + private checkDefault(items: Array, id: string): void { + let count = 0; + for (const item of items) { + // 设置唯一id + if (item.id === undefined) { + item.id = `${id}-${count}`; + count += 1; + } + // 设置是否可选,默认true + if (item.selectable === undefined) { + item.selectable = true; + } + // 设置是否禁用,默认false + if (item.disabled === undefined) { + item.disabled = false; + } + // 遍历子元素 + if (item.children) { + this.checkDefault(item.children, item.id); + } + } + } + + /** + * 嵌套遍历获取path + * + * @param list 表单对象列表 + * @param id 需要匹配的id + * @returns 匹配的路径 + */ + private getPathById(list: Array, id: string): Array { + for (const item of list) { + // 禁用项不能作为默认激活项 + if (item.disabled) { + continue; + } + // 匹配成功返回path + if (item.id === id) { + return [item]; + } + // 匹配子菜单 + const res = this.getPathById(item.children || [], id); + if (res.length > 0) { + // 子菜单匹配成功,返回拼接后的path + return [item, ...res]; + } + } + // 匹配失败返回空匹配 + return []; + } +} diff --git a/src/nav/lib/src/TiNavModule.ts b/src/nav/lib/src/TiNavModule.ts new file mode 100644 index 0000000..fb87aa2 --- /dev/null +++ b/src/nav/lib/src/TiNavModule.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { TiNavComponent } from './TiNavComponent'; +import { TiNavLeftComponent } from './TiNavLeftComponent'; +import { TiNavMenuComponent } from './TiNavMenuComponent'; +import { TiNavRightComponent } from './TiNavRightComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [TiNavComponent, TiNavLeftComponent, TiNavRightComponent, TiNavMenuComponent], + declarations: [TiNavComponent, TiNavLeftComponent, TiNavRightComponent, TiNavMenuComponent] +}) +export class TiNavModule {} + +export { TiNavComponent } from './TiNavComponent'; +export { TiNavLeftComponent } from './TiNavLeftComponent'; +export { TiNavRightComponent } from './TiNavRightComponent'; +export { TiNavMenuComponent } from './TiNavMenuComponent'; +export { TiNavMenuItem } from './interface'; diff --git a/src/nav/lib/src/TiNavRightComponent.ts b/src/nav/lib/src/TiNavRightComponent.ts new file mode 100644 index 0000000..20827c9 --- /dev/null +++ b/src/nav/lib/src/TiNavRightComponent.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * NavRight 组件 + * + * 提供顶部导航栏右侧操作区域展示,可添加任意行内元素 + */ +@Component({ + selector: 'ti-nav-right', + templateUrl: './navright.html', + styleUrls: ['./navright.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiNavRightComponent {} diff --git a/src/nav/lib/src/common.less b/src/nav/lib/src/common.less new file mode 100644 index 0000000..827777e --- /dev/null +++ b/src/nav/lib/src/common.less @@ -0,0 +1,18 @@ +:host { + --ti-nav-color-text-active: var(--ti-common-color-text-highlight); + --ti-nav-color-text-white: var(--ti-common-color-text-gray); + --ti-nav-color-bg-active: var(--ti-common-color-bg-emphasize); + &.ti3-nav-light { + --ti-nav-color-bg: var(--ti-common-color-bg-white-normal); + --ti-nav-color-text: var(--ti-common-color-text-secondary); + --ti-nav-color-brand: var(--ti-common-color-text-primary); + --ti-nav-color-text-disabled: var(--ti-common-color-text-disabled); + } + &.ti3-nav-dark { + --ti-nav-color-bg: var(--ti-common-color-bg-navigation); + --ti-nav-color-text: var(--ti-common-color-text-darkbg); + --ti-nav-color-brand: var(--ti-common-color-text-white); + --ti-nav-color-text-disabled: var(--ti-common-color-text-darkbg-disabled); + } + --ti-nav-height: var(--ti-common-size-16x); +} diff --git a/src/nav/lib/src/interface.ts b/src/nav/lib/src/interface.ts new file mode 100644 index 0000000..4d8ac07 --- /dev/null +++ b/src/nav/lib/src/interface.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * TiNavMenuItem 是顶部导航栏一级菜单项数据接口 + */ +export interface TiNavMenuItem { + /** + * 菜单项文本 + */ + label: string; + /** + * 菜单项id值,确保唯一 + * + * @default 初始化自动生成唯一id + */ + id?: string; + /** + * 是否禁用 + * + * @default false + */ + disabled?: boolean; + /** + * 是否可选中 + * + * @default true + */ + selectable?: boolean; + /** + * 子菜单 + */ + children?: TiNavMenuItem[]; + /** + * 其他参数,可自定义 + */ + [key: string]: any; +} diff --git a/src/nav/lib/src/nav.html b/src/nav/lib/src/nav.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/nav/lib/src/nav.html @@ -0,0 +1 @@ + diff --git a/src/nav/lib/src/nav.less b/src/nav/lib/src/nav.less new file mode 100644 index 0000000..fc773f0 --- /dev/null +++ b/src/nav/lib/src/nav.less @@ -0,0 +1,16 @@ +@import './common.less'; + +:host { + &.ti3-nav { + box-sizing: border-box; + position: relative; + z-index: 1002; + display: flex; + justify-content: space-between; + width: auto; + height: var(--ti-nav-height); + padding: var(--ti-common-space-0) var(--ti-common-space-6x); + box-shadow: var(--ti-common-shadow-2-down); + background-color: var(--ti-nav-color-bg); + } +} diff --git a/src/nav/lib/src/navleft.html b/src/nav/lib/src/navleft.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/nav/lib/src/navleft.html @@ -0,0 +1 @@ + diff --git a/src/nav/lib/src/navleft.less b/src/nav/lib/src/navleft.less new file mode 100644 index 0000000..37e0bd9 --- /dev/null +++ b/src/nav/lib/src/navleft.less @@ -0,0 +1,15 @@ +@import './common.less'; + +:host { + &.ti3-nav-left { + box-sizing: border-box; + flex-shrink: 0; + display: flex; + cursor: pointer; + color: var(--ti-nav-color-brand); + + font-size: var(--ti-common-font-size-3); + line-height: var(--ti-nav-height); + font-weight: var(--ti-common-font-weight-5); + } +} diff --git a/src/nav/lib/src/navmenu.html b/src/nav/lib/src/navmenu.html new file mode 100644 index 0000000..f5b60b9 --- /dev/null +++ b/src/nav/lib/src/navmenu.html @@ -0,0 +1,72 @@ + +
    + +
    + +
    + + +
      + +
    • + +
    • +
    + + +
    +
    + +
    + +
    + + +
    + +
    +
    +
    +
    + + {{ item.label }} diff --git a/src/nav/lib/src/navmenu.less b/src/nav/lib/src/navmenu.less new file mode 100644 index 0000000..7469284 --- /dev/null +++ b/src/nav/lib/src/navmenu.less @@ -0,0 +1,186 @@ +@import './common.less'; + +:host.ti3-nav-menu { + flex-grow: 1; + flex-shrink: 0; + display: flex; + flex-direction: row; + justify-content: flex-start; + padding: var(--ti-common-space-0) var(--ti-common-space-10x); +} + +.ti3-nav-menu-item-label { + position: relative; + margin: var(--ti-common-space-0) var(--ti-common-space-5x); + font-size: var(--ti-common-font-size-2); + font-weight: var(--ti-common-font-weight-4); + line-height: var(--ti-nav-height); + flex-shrink: 0; + color: var(--ti-nav-color-text); + cursor: default; + + &.ti3-nav-selectable { + cursor: pointer; + } + + &[disabled] { + color: var(--ti-nav-color-text-disabled); + cursor: not-allowed; + .ti3-nav-submenu-icon { + border-top-color: var(--ti-nav-color-text-disabled); + } + } + + &::before { + content: ''; + height: 3px; + width: 100%; + background-color: transparent; + position: absolute; + bottom: 0; + left: 0; + transform-origin: center; + transform: scaleX(0); + transition: transform 0.2s ease; + } + + &.ti3-nav-submenu-label::after { + content: ''; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid var(--ti-nav-color-text); + margin-left: 4px; + display: inline-block; + position: relative; + top: -3px; + } + + &.ti3-nav-active:not([disabled]), + &:hover:not([disabled]) { + color: var(--ti-nav-color-text-active); + &::before { + transform: scaleX(1); + background-color: var(--ti-nav-color-text-active); + } + &.ti3-nav-submenu-label::after { + border-top-color: var(--ti-nav-color-text-active); + } + } +} + +.ti3-nav-submenu { + position: absolute; + width: fit-content; + cursor: default; + padding: 0; + background-color: var(--ti-nav-color-bg); + box-shadow: var(--ti-common-shadow-2-down); + top: var(--ti-nav-height); + left: 50%; + transform: translateX(-50%); + height: 0; + overflow: hidden; + transition: all 0.2s ease; + &.expand { + padding: var(--ti-common-space-5x) var(--ti-common-space-0); + height: fit-content; + } +} + +.ti3-nav-submenu-item { + word-break: keep-all; + font-size: var(--ti-common-font-size-1); + font-weight: var(--ti-common-font-weight-4); + line-height: var(--ti-common-line-height-number); + color: var(--ti-nav-color-text); + padding: var(--ti-common-space-10) var(--ti-common-space-6x); + background-color: inherit; + cursor: default; + + &.ti3-nav-selectable { + cursor: pointer; + } + &[disabled] { + color: var(--ti-nav-color-text-disabled); + cursor: not-allowed; + } + + &:hover:not([disabled]) { + color: var(--ti-nav-color-text-active); + } + + &.ti3-nav-active:not([disabled]) { + color: var(--ti-nav-color-text-white); + background-color: var(--ti-nav-color-bg-active); + } +} + +.ti3-nav-panel { + position: absolute; + top: var(--ti-nav-height); + left: 0; + right: 0; + padding: 0; + background-color: var(--ti-common-color-bg-gray); + box-shadow: var(--ti-common-shadow-2-down); + display: flex; + flex-direction: row; + justify-content: center; + flex-wrap: wrap; + transform: scaleY(0); + transform-origin: top; + transition: transform 0.2s ease; + + &.expand { + transform: scaleY(1); + padding: var(--ti-common-space-3x) var(--ti-common-space-5x) var(--ti-common-space-10x) var(--ti-common-space-5x); + } +} + +.ti3-nav-panel-list { + flex-shrink: 0; + width: 255px; + & > * { + padding: var(--ti-common-space-2x) var(--ti-common-space-10); + } +} + +.ti3-nav-panel-list-title { + font-size: var(--ti-common-font-size-2); + line-height: var(--ti-common-line-height-number); + color: var(--ti-nav-color-text-light); + font-weight: var(--ti-common-font-weight-6); + cursor: default; + + &.ti3-nav-selectable { + cursor: pointer; + } + &[disabled] { + cursor: not-allowed; + color: var(--ti-nav-color-text-disabled); + } +} + +.ti3-nav-panel-item { + font-size: var(--ti-common-font-size-1); + line-height: var(--ti-common-line-height-number); + color: var(--ti-nav-color-text-light-brand); + font-weight: var(--ti-common-font-weight-4); + cursor: default; + + &.ti3-nav-selectable { + cursor: pointer; + } + + &[disabled] { + cursor: not-allowed; + color: var(--ti-nav-color-text-disabled); + } + + &:hover:not([disabled]), + &.ti3-nav-active:not([disabled]) { + color: var(--ti-nav-color-text-active); + } +} diff --git a/src/nav/lib/src/navright.html b/src/nav/lib/src/navright.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/nav/lib/src/navright.html @@ -0,0 +1 @@ + diff --git a/src/nav/lib/src/navright.less b/src/nav/lib/src/navright.less new file mode 100644 index 0000000..92725dd --- /dev/null +++ b/src/nav/lib/src/navright.less @@ -0,0 +1,11 @@ +@import './common.less'; + +:host { + box-sizing: border-box; + flex-shrink: 0; + display: flex; + align-items: center; + font-size: var(--ti-common-font-size-1); + font-weight: var(--ti-common-font-weight-4); + color: var(--ti-nav-color-text); +} diff --git a/src/ng/demo/karma.conf.js b/src/ng/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/ng/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/ng/demo/project.json b/src/ng/demo/project.json new file mode 100644 index 0000000..0cb3dc0 --- /dev/null +++ b/src/ng/demo/project.json @@ -0,0 +1,120 @@ +{ + "projectType": "application", + "root": "src/ng/demo", + "sourceRoot": "src/ng/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/ng", + "index": "src/ng/demo/src/index.html", + "main": "src/ng/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/ng/demo/tsconfig.app.json", + "assets": [ + "src/ng/demo/src/favicon.ico", + "src/ng/demo/src/assets", + { + "glob": "**/*", + "input": "node_modules/ionicons/dist/ionicons/svg", + "output": "/assets/ionicons/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "wc": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "30MB", + "maximumError": "30MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "30MB", + "maximumError": "30MB" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.wc.ts" + } + ], + "outputHashing": "none" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "ng-demo:build:production" + }, + "development": { + "browserTarget": "ng-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js ng" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/ng/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/ng/demo/tsconfig.spec.json", + "karmaConfig": "src/ng/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "assets": ["src/ng/demo/src/favicon.ico", "src/ng/demo/src/assets"], + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/ng/demo/src/app/AppComponent.ts b/src/ng/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..3364119 --- /dev/null +++ b/src/ng/demo/src/app/AppComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { ActivatedRouteSnapshot, ChildActivationEnd, Router } from '@angular/router'; +// import { TiLog } from '@cloud/tiny3'; +import { environment } from '../../../../environments/environment'; + +export interface DemoTab { + title: string; + src: any; + active?: boolean; +} + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent { + constructor(router: Router) { + // 日志控制 + // TiLog.setLevel(environment.production ? TiLog.LEVEL_OFF : TiLog.LEVEL_LOG); + + router.events.subscribe((event: any) => { + if (event instanceof ChildActivationEnd) { + const snapshot: ActivatedRouteSnapshot = event.snapshot; + const routerPath: string = snapshot.firstChild.routeConfig.path; + + // 设置网页标题 + const pathArray: Array = routerPath.split('/'); + document.getElementsByTagName('title')[0].innerText = pathArray[pathArray.length - 1]; + } + }); + } +} diff --git a/src/ng/demo/src/app/AppModule.ts b/src/ng/demo/src/app/AppModule.ts new file mode 100644 index 0000000..a2d3f51 --- /dev/null +++ b/src/ng/demo/src/app/AppModule.ts @@ -0,0 +1,32 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { HttpClientModule } from '@angular/common/http'; +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { DemoModules } from './DemoModules'; +import { TiLocale } from '@opentiny/ng'; + +@NgModule({ + imports: [ + DemoModules.allModules, + HttpClientModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, // 因为modal,message,collapse,accordion组件使用了angular动画,所以需要引入此模块 + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule { + constructor() { + // TiLocale.setLocale(TiLocale.EN_US); // 设置国际化语种 + } +} diff --git a/src/ng/demo/src/app/AppWcModule.ts b/src/ng/demo/src/app/AppWcModule.ts new file mode 100644 index 0000000..86c26ec --- /dev/null +++ b/src/ng/demo/src/app/AppWcModule.ts @@ -0,0 +1,5394 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { Component, Injector, NgModule } from '@angular/core'; +import { createCustomElement } from '@angular/elements'; +import { DemoModules } from './DemoModules'; +import { HttpClientModule } from '@angular/common/http'; + +import { AccordionBasicComponent } from './../../../../accordion/demo/src/app/accordion/AccordionBasicComponent'; + +import { AccordionClassComponent } from './../../../../accordion/demo/src/app/accordion/AccordionClassComponent'; + +import { AccordionClickToggleComponent } from './../../../../accordion/demo/src/app/accordion/AccordionClickToggleComponent'; + +import { AccordionCloseOthersComponent } from './../../../../accordion/demo/src/app/accordion/AccordionCloseOthersComponent'; + +import { AccordionDisabledComponent } from './../../../../accordion/demo/src/app/accordion/AccordionDisabledComponent'; + +import { AccordionOpenComponent } from './../../../../accordion/demo/src/app/accordion/AccordionOpenComponent'; + +import { ActionmenuBasicComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuBasicComponent'; + +import { ActionmenuDataComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuDataComponent'; + +import { ActionmenuData2Component } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuData2Component'; + +import { ActionmenuDisabledComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuDisabledComponent'; + +import { ActionmenuDividerComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuDividerComponent'; + +import { ActionmenuEventComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuEventComponent'; + +import { ActionmenuFocusComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuFocusComponent'; + +import { ActionmenuIdComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuIdComponent'; + +import { ActionmenuItemsChangeComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuItemsChangeComponent'; + +import { ActionmenuItemsComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuItemsComponent'; + +import { ActionmenuLabelkeyComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuLabelkeyComponent'; + +import { ActionmenuManyComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuManyComponent'; + +import { ActionmenuMenutextComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuMenutextComponent'; + +import { ActionmenuPanelstyleComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuPanelstyleComponent'; + +import { ActionmenuShownumComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuShownumComponent'; + +import { ActionmenuSpaceComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuSpaceComponent'; + +import { ActionmenuTableComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuTableComponent'; + +import { ActionmenuTempleteTestComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuTempleteTestComponent'; + +import { ActionmenuTempleteComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuTempleteComponent'; + +import { ActionmenuTipsTestComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuTipsTestComponent'; + +import { ActionmenuTipsComponent } from './../../../../actionmenu/demo/src/app/actionmenu/ActionmenuTipsComponent'; + +import { AlertBoxshadowComponent } from './../../../../alert/demo/src/app/alert/AlertBoxshadowComponent'; + +import { AlertDarkthemeComponent } from './../../../../alert/demo/src/app/alert/AlertDarkthemeComponent'; + +import { AlertDismissComponent } from './../../../../alert/demo/src/app/alert/AlertDismissComponent'; + +import { AlertEventComponent } from './../../../../alert/demo/src/app/alert/AlertEventComponent'; + +import { AlertIconComponent } from './../../../../alert/demo/src/app/alert/AlertIconComponent'; + +import { AlertMessagesComponent } from './../../../../alert/demo/src/app/alert/AlertMessagesComponent'; + +import { AlertOpenTestComponent } from './../../../../alert/demo/src/app/alert/AlertOpenTestComponent'; + +import { AlertOpenComponent } from './../../../../alert/demo/src/app/alert/AlertOpenComponent'; + +import { AlertSizeComponent } from './../../../../alert/demo/src/app/alert/AlertSizeComponent'; + +import { AlertTriggerScrollComponent } from './../../../../alert/demo/src/app/alert/AlertTriggerScrollComponent'; + +import { AlertTypeComponent } from './../../../../alert/demo/src/app/alert/AlertTypeComponent'; + +import { AnchorBasicComponent } from './../../../../anchor/demo/src/app/anchor/AnchorBasicComponent'; + +import { AnchorEventsComponent } from './../../../../anchor/demo/src/app/anchor/AnchorEventsComponent'; + +import { AnchorIdComponent } from './../../../../anchor/demo/src/app/anchor/AnchorIdComponent'; + +import { AnchorItemsComponent } from './../../../../anchor/demo/src/app/anchor/AnchorItemsComponent'; + +import { AnchorOffsettopComponent } from './../../../../anchor/demo/src/app/anchor/AnchorOffsettopComponent'; + +import { AnchorScrolltargetComponent } from './../../../../anchor/demo/src/app/anchor/AnchorScrolltargetComponent'; + +import { AnchorSpeedComponent } from './../../../../anchor/demo/src/app/anchor/AnchorSpeedComponent'; + +import { AnchorTemplateComponent } from './../../../../anchor/demo/src/app/anchor/AnchorTemplateComponent'; + +import { AnchorTestComponent } from './../../../../anchor/demo/src/app/anchor/AnchorTestComponent'; + +import { AutocompleteAppendtobodyComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteAppendtobodyComponent'; + +import { AutocompleteBasicComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteBasicComponent'; + +import { AutocompleteClearableComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteClearableComponent'; + +import { AutocompleteDisabledComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteDisabledComponent'; + +import { AutocompleteEventsComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteEventsComponent'; + +import { AutocompleteLabelkeyComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteLabelkeyComponent'; + +import { AutocompleteMaxlengthComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteMaxlengthComponent'; + +import { AutocompletePanelSizeComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompletePanelSizeComponent'; + +import { AutocompleteTemplateComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteTemplateComponent'; + +import { AutocompleteTestComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteTestComponent'; + +import { AutocompleteTipComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteTipComponent'; + +import { AutocompleteValidComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteValidComponent'; + +import { AutocompleteGroupComponent } from './../../../../autocomplete/demo/src/app/autocomplete/AutocompleteGroupComponent'; + +import { AvatarImageErrorTestComponent } from './../../../../avatar/demo/src/app/avatar/AvatarImageErrorTestComponent'; + +import { AvatarImageComponent } from './../../../../avatar/demo/src/app/avatar/AvatarImageComponent'; + +import { AvatarShapeComponent } from './../../../../avatar/demo/src/app/avatar/AvatarShapeComponent'; + +import { AvatarSizeComponent } from './../../../../avatar/demo/src/app/avatar/AvatarSizeComponent'; + +import { AvatarStyleComponent } from './../../../../avatar/demo/src/app/avatar/AvatarStyleComponent'; + +import { AvatarTextComponent } from './../../../../avatar/demo/src/app/avatar/AvatarTextComponent'; + +import { ButtonColorComponent } from './../../../../button/demo/src/app/button/ButtonColorComponent'; + +import { ButtonDisabledComponent } from './../../../../button/demo/src/app/button/ButtonDisabledComponent'; + +import { ButtonEventComponent } from './../../../../button/demo/src/app/button/ButtonEventComponent'; + +import { ButtonFocusComponent } from './../../../../button/demo/src/app/button/ButtonFocusComponent'; + +import { ButtonHasborderTestComponent } from './../../../../button/demo/src/app/button/ButtonHasborderTestComponent'; + +import { ButtonHasborderComponent } from './../../../../button/demo/src/app/button/ButtonHasborderComponent'; + +import { ButtonIconComponent } from './../../../../button/demo/src/app/button/ButtonIconComponent'; + +import { ButtonLoadingComponent } from './../../../../button/demo/src/app/button/ButtonLoadingComponent'; + +import { ButtonOnlyiconComponent } from './../../../../button/demo/src/app/button/ButtonOnlyiconComponent'; + +import { ButtonSizeComponent } from './../../../../button/demo/src/app/button/ButtonSizeComponent'; + +import { ButtonTipComponent } from './../../../../button/demo/src/app/button/ButtonTipComponent'; + +import { ButtongroupActiveclassComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupActiveclassComponent'; + +import { ButtongroupBeforeclickComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupBeforeclickComponent'; + +import { ButtongroupDeselectableComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupDeselectableComponent'; + +import { ButtongroupDisabledComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupDisabledComponent'; + +import { ButtongroupEnumComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupEnumComponent'; + +import { ButtongroupEventComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupEventComponent'; + +import { ButtongroupFocusComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupFocusComponent'; + +import { ButtongroupIdTestComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupIdTestComponent'; + +import { ButtongroupIdComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupIdComponent'; + +import { ButtongroupItemsTestComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupItemsTestComponent'; + +import { ButtongroupItemsComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupItemsComponent'; + +import { ButtongroupManyComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupManyComponent'; + +import { ButtongroupMinwidthComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupMinwidthComponent'; + +import { ButtongroupMultiTypeComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupMultiTypeComponent'; + +import { ButtongroupMultilineComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupMultilineComponent'; + +import { ButtongroupMultipleComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupMultipleComponent'; + +import { ButtongroupRadioTypeComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupRadioTypeComponent'; + +import { ButtongroupReactiveFormsComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupReactiveFormsComponent'; + +import { ButtongroupSupTestComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupSupTestComponent'; + +import { ButtongroupSupComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupSupComponent'; + +import { ButtongroupTemplateComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupTemplateComponent'; + +import { ButtongroupTipComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupTipComponent'; + +import { ButtongroupValuekeyTestComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupValuekeyTestComponent'; + +import { ButtongroupValuekeyComponent } from './../../../../buttongroup/demo/src/app/buttongroup/ButtongroupValuekeyComponent'; + +import { ButtonselectBasicComponent } from './../../../../buttonselect/demo/src/app/buttonselect/ButtonselectBasicComponent'; + +import { ButtonselectLabelkeyComponent } from './../../../../buttonselect/demo/src/app/buttonselect/ButtonselectLabelkeyComponent'; + +import { CardAddComponent } from './../../../../card/demo/src/app/card/CardAddComponent'; + +import { CardBasicComponent } from './../../../../card/demo/src/app/card/CardBasicComponent'; + +import { CardGridComponent } from './../../../../card/demo/src/app/card/CardGridComponent'; + +import { CardGrid2Component } from './../../../../card/demo/src/app/card/CardGrid2Component'; + +import { CardHeaderComponent } from './../../../../card/demo/src/app/card/CardHeaderComponent'; + +import { CascaderBasicComponent } from './../../../../cascader/demo/src/app/cascader/CascaderBasicComponent'; + +import { CascaderClearableComponent } from './../../../../cascader/demo/src/app/cascader/CascaderClearableComponent'; + +import { CascaderDisabledComponent } from './../../../../cascader/demo/src/app/cascader/CascaderDisabledComponent'; + +import { CascaderEventsComponent } from './../../../../cascader/demo/src/app/cascader/CascaderEventsComponent'; + +import { CascaderIdkeyComponent } from './../../../../cascader/demo/src/app/cascader/CascaderIdkeyComponent'; + +import { CascaderItemTestComponent } from './../../../../cascader/demo/src/app/cascader/CascaderItemTestComponent'; + +import { CascaderLabelkeyComponent } from './../../../../cascader/demo/src/app/cascader/CascaderLabelkeyComponent'; + +import { CascaderOnlyselectleafComponent } from './../../../../cascader/demo/src/app/cascader/CascaderOnlyselectleafComponent'; + +import { CascaderPanelComponent } from './../../../../cascader/demo/src/app/cascader/CascaderPanelComponent'; + +import { CascaderSearchComponent } from './../../../../cascader/demo/src/app/cascader/CascaderSearchComponent'; + +import { CascaderShowalllevelComponent } from './../../../../cascader/demo/src/app/cascader/CascaderShowalllevelComponent'; + +import { CascaderTriggerComponent } from './../../../../cascader/demo/src/app/cascader/CascaderTriggerComponent'; + +import { CascaderValidComponent } from './../../../../cascader/demo/src/app/cascader/CascaderValidComponent'; + +import { CascaderValuekeyComponent } from './../../../../cascader/demo/src/app/cascader/CascaderValuekeyComponent'; + +import { CheckboxBasicComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxBasicComponent'; + +import { CheckboxDisabledComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxDisabledComponent'; + +import { CheckboxEventComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxEventComponent'; + +import { CheckboxFocusedComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxFocusedComponent'; + +import { CheckboxGroupComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxGroupComponent'; + +import { CheckboxGroupDirectionComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxGroupDirectionComponent'; + +import { CheckboxGroupLabelkeyComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxGroupLabelkeyComponent'; + +import { CheckboxGroupLevelComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxGroupLevelComponent'; + +import { CheckboxGroupLinewrapComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxGroupLinewrapComponent'; + +import { CheckboxGroupValidationComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxGroupValidationComponent'; + +import { CheckboxGroupValuekeyComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxGroupValuekeyComponent'; + +import { CheckboxIndeterminateComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxIndeterminateComponent'; + +import { CheckboxLabelComponent } from './../../../../checkbox/demo/src/app/checkbox/CheckboxLabelComponent'; + +import { CollapseBasicComponent } from './../../../../collapse/demo/src/app/collapse/CollapseBasicComponent'; + +import { CollapseEventComponent } from './../../../../collapse/demo/src/app/collapse/CollapseEventComponent'; + +import { CollapseboxBasicComponent } from './../../../../collapsebox/demo/src/app/collapsebox/CollapseboxBasicComponent'; + +import { CollapseboxCloseableComponent } from './../../../../collapsebox/demo/src/app/collapsebox/CollapseboxCloseableComponent'; + +import { CollapseboxEventComponent } from './../../../../collapsebox/demo/src/app/collapsebox/CollapseboxEventComponent'; + +import { CollapsebuttonBasicComponent } from './../../../../collapsebutton/demo/src/app/collapsebutton/CollapsebuttonBasicComponent'; + +import { CollapsebuttonCustomtextComponent } from './../../../../collapsebutton/demo/src/app/collapsebutton/CollapsebuttonCustomtextComponent'; + +import { CollapsebuttonSearchcountComponent } from './../../../../collapsebutton/demo/src/app/collapsebutton/CollapsebuttonSearchcountComponent'; + +import { CollapsebuttonEventComponent } from './../../../../collapsebutton/demo/src/app/collapsebutton/CollapsebuttonEventComponent'; + +import { CollapsetextBasicComponent } from './../../../../collapsetext/demo/src/app/collapsetext/CollapsetextBasicComponent'; + +import { CollapsetextTypeComponent } from './../../../../collapsetext/demo/src/app/collapsetext/CollapsetextTypeComponent'; + +import { CollapsetextHighlightComponent } from './../../../../collapsetext/demo/src/app/collapsetext/CollapsetextHighlightComponent'; + +import { CollapsetextCollapsedComponent } from './../../../../collapsetext/demo/src/app/collapsetext/CollapsetextCollapsedComponent'; + +import { CollapsetextSceneComponent } from './../../../../collapsetext/demo/src/app/collapsetext/CollapsetextSceneComponent'; + +import { CopyBasicComponent } from './../../../../copy/demo/src/app/copy/CopyBasicComponent'; +import { CopyDarkComponent } from './../../../../copy/demo/src/app/copy/CopyDarkComponent'; + +import { CopyTipComponent } from './../../../../copy/demo/src/app/copy/CopyTipComponent'; + +import { CopyTableComponent } from './../../../../copy/demo/src/app/copy/CopyTableComponent'; + +import { CopyEventComponent } from './../../../../copy/demo/src/app/copy/CopyEventComponent'; + +import { CrumbBasicComponent } from './../../../../crumb/demo/src/app/crumb/CrumbBasicComponent'; + +import { CrumbEventsComponent } from './../../../../crumb/demo/src/app/crumb/CrumbEventsComponent'; + +import { CrumbHrefComponent } from './../../../../crumb/demo/src/app/crumb/CrumbHrefComponent'; + +import { CrumbRouterTestComponent } from './../../../../crumb/demo/src/app/crumb/CrumbRouterTestComponent'; + +import { CrumbRouterComponent } from './../../../../crumb/demo/src/app/crumb/CrumbRouterComponent'; + +import { DateCleariconComponent } from './../../../../date/demo/src/app/date/DateCleariconComponent'; + +import { DateCustomizeComponent } from './../../../../date/demo/src/app/date/DateCustomizeComponent'; + +import { DateDisabledComponent } from './../../../../date/demo/src/app/date/DateDisabledComponent'; + +import { DateDisableddaysComponent } from './../../../../date/demo/src/app/date/DateDisableddaysComponent'; + +import { DateEventComponent } from './../../../../date/demo/src/app/date/DateEventComponent'; + +import { DateFormComponent } from './../../../../date/demo/src/app/date/DateFormComponent'; + +import { DateFormatTestComponent } from './../../../../date/demo/src/app/date/DateFormatTestComponent'; + +import { DateFormatComponent } from './../../../../date/demo/src/app/date/DateFormatComponent'; + +import { DateMaxComponent } from './../../../../date/demo/src/app/date/DateMaxComponent'; + +import { DateMaxminTestComponent } from './../../../../date/demo/src/app/date/DateMaxminTestComponent'; + +import { DateMaxminComponent } from './../../../../date/demo/src/app/date/DateMaxminComponent'; + +import { DateMinComponent } from './../../../../date/demo/src/app/date/DateMinComponent'; + +import { DateNowdatetimeComponent } from './../../../../date/demo/src/app/date/DateNowdatetimeComponent'; + +import { DatePanelalignComponent } from './../../../../date/demo/src/app/date/DatePanelalignComponent'; + +import { DateValidationComponent } from './../../../../date/demo/src/app/date/DateValidationComponent'; + +import { DateValueTestComponent } from './../../../../date/demo/src/app/date/DateValueTestComponent'; + +import { DateValueComponent } from './../../../../date/demo/src/app/date/DateValueComponent'; + +import { DaterangeCustomizeComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeCustomizeComponent'; + +import { DaterangeDisabledComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeDisabledComponent'; + +import { DaterangeDisableddaysComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeDisableddaysComponent'; + +import { DaterangeEventComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeEventComponent'; + +import { DaterangeFixedvalueTestComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeFixedvalueTestComponent'; + +import { DaterangeFixedvalueComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeFixedvalueComponent'; + +import { DaterangeFormatTestComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeFormatTestComponent'; + +import { DaterangeFormatComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeFormatComponent'; + +import { DaterangeIsallowbeginequalendComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeIsallowbeginequalendComponent'; + +import { DaterangeMaxComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeMaxComponent'; + +import { DaterangeMaxminTestComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeMaxminTestComponent'; + +import { DaterangeMaxminComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeMaxminComponent'; + +import { DaterangeMinComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeMinComponent'; + +import { DaterangeNowdatetimeComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeNowdatetimeComponent'; + +import { DaterangePanelalignComponent } from './../../../../daterange/demo/src/app/daterange/DaterangePanelalignComponent'; + +import { DaterangeValidationComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeValidationComponent'; + +import { DaterangeValueTestComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeValueTestComponent'; + +import { DaterangeValueComponent } from './../../../../daterange/demo/src/app/daterange/DaterangeValueComponent'; + +import { DatetimeCleariconComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeCleariconComponent'; + +import { DatetimeCustomizeComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeCustomizeComponent'; + +import { DatetimeDisabledComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeDisabledComponent'; + +import { DatetimeEventComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeEventComponent'; + +import { DatetimeFormatTestComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeFormatTestComponent'; + +import { DatetimeFormatComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeFormatComponent'; + +import { DatetimeMaxComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeMaxComponent'; + +import { DatetimeMaxminTestComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeMaxminTestComponent'; + +import { DatetimeMaxminComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeMaxminComponent'; + +import { DatetimeMinComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeMinComponent'; + +import { DatetimeNowdatetimeComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeNowdatetimeComponent'; + +import { DatetimePanelalignComponent } from './../../../../datetime/demo/src/app/datetime/DatetimePanelalignComponent'; + +import { DatetimeValidationComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeValidationComponent'; + +import { DatetimeValueTestComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeValueTestComponent'; + +import { DatetimeValueComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeValueComponent'; + +import { DatetimeTimezoneableComponent } from './../../../../datetime/demo/src/app/datetime/DatetimeTimezoneableComponent'; + +import { DatetimerangeCleariconComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeCleariconComponent'; + +import { DatetimerangeCustomizeComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeCustomizeComponent'; + +import { DatetimerangeDisabledComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeDisabledComponent'; + +import { DatetimerangeEventComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeEventComponent'; + +import { DatetimerangeFormatTestComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeFormatTestComponent'; + +import { DatetimerangeFormatComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeFormatComponent'; + +import { DatetimerangeIsallowbeginequalendComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeIsallowbeginequalendComponent'; + +import { DatetimerangeManyTestComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeManyTestComponent'; + +import { DatetimerangeMaxComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeMaxComponent'; + +import { DatetimerangeMaxminTestComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeMaxminTestComponent'; + +import { DatetimerangeMaxminComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeMaxminComponent'; + +import { DatetimerangeMinComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeMinComponent'; + +import { DatetimerangeNowdatetimeComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeNowdatetimeComponent'; + +import { DatetimerangePanelalignComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangePanelalignComponent'; + +import { DatetimerangeTimezoneableComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeTimezoneableComponent'; + +import { DatetimerangeValidationComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeValidationComponent'; + +import { DatetimerangeValueTestComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeValueTestComponent'; + +import { DatetimerangeValueComponent } from './../../../../datetimerange/demo/src/app/datetimerange/DatetimerangeValueComponent'; + +import { GuidesBasicComponent } from './../../../../guides/demo/src/app/guides/GuidesBasicComponent'; + +import { GuidesTabComponent } from './../../../../guides/demo/src/app/guides/GuidesTabComponent'; + +import { GuidesGuidestepsComponent } from './../../../../guides/demo/src/app/guides/GuidesGuidestepsComponent'; + +import { GuidesTypeComponent } from './../../../../guides/demo/src/app/guides/GuidesTypeComponent'; + +import { FormfieldColspanRowspanTestComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldColspanRowspanTestComponent'; + +import { FormfieldColspanRowspanComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldColspanRowspanComponent'; + +import { FormfieldColswidthComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldColswidthComponent'; + +import { FormfieldFooComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldFooComponent'; + +import { FormfieldIndexComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldIndexComponent'; + +import { FormfieldLabelComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldLabelComponent'; + +import { FormfieldLabelwidthComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldLabelwidthComponent'; + +import { FormfieldMultiColumnComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldMultiColumnComponent'; + +import { FormfieldNestFormfiledComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldNestFormfiledComponent'; + +import { FormfieldNgforTestComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldNgforTestComponent'; + +import { FormfieldRequiredComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldRequiredComponent'; + +import { FormfieldRequiredspaceComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldRequiredspaceComponent'; + +import { FormfieldShowComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldShowComponent'; + +import { FormfieldSingleColumnComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldSingleColumnComponent'; + +import { FormfieldTestComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldTestComponent'; + +import { FormfieldTextFormComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldTextFormComponent'; + +import { FormfieldVerticalAlignComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldVerticalAlignComponent'; + +import { FormfieldVerticalComponent } from './../../../../formfield/demo/src/app/formfield/FormfieldVerticalComponent'; + +import { FoldtextBasicComponent } from './../../../../foldtext/demo/src/app/foldtext/FoldtextBasicComponent'; + +import { FoldtextTableComponent } from './../../../../foldtext/demo/src/app/foldtext/FoldtextTableComponent'; + +import { GuidestepsBasicComponent } from './../../../../guidesteps/demo/src/app/guidesteps/GuidestepsBasicComponent'; + +import { GuidestepsIscompleteComponent } from './../../../../guidesteps/demo/src/app/guidesteps/GuidestepsIscompleteComponent'; + +import { GuidestepsLargeComponent } from './../../../../guidesteps/demo/src/app/guidesteps/GuidestepsLargeComponent'; + +import { HalfmodalAsyncComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalAsyncComponent'; + +import { HalfmodalBackdropComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalBackdropComponent'; + +import { HalfmodalBasicComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalBasicComponent'; + +import { HalfmodalBeforehideComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalBeforehideComponent'; + +import { HalfmodalContentComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalContentComponent'; + +import { HalfmodalModalComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalModalComponent'; + +import { HalfmodalModalselectComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalModalselectComponent'; + +import { HalfmodalMultiComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalMultiComponent'; + +import { HalfmodalServiceTestComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalServiceTestComponent'; + +import { HalfmodalServiceComponent } from './../../../../halfmodal/demo/src/app/halfmodal/HalfmodalServiceComponent'; + +import { IconBasicComponent } from './../../../../icon/demo/src/app/icon/IconBasicComponent'; + +import { SvgSetpathComponent } from '../../../../icon/demo/src/app/icon/SvgSetpathComponent'; + +import { IconShowComponent } from './../../../../icon/demo/src/app/icon/IconShowComponent'; + +import { IconactionBasicComponent } from './../../../../iconaction/demo/src/app/iconaction/IconactionBasicComponent'; + +import { IconactionDarkComponent } from './../../../../iconaction/demo/src/app/iconaction/IconactionDarkComponent'; + +import { IconactionDisabledComponent } from './../../../../iconaction/demo/src/app/iconaction/IconactionDisabledComponent'; + +import { IconactionHrefComponent } from './../../../../iconaction/demo/src/app/iconaction/IconactionHrefComponent'; + +import { ImagepreviewBasicComponent } from './../../../../imagepreview/demo/src/app/imagepreview/ImagepreviewBasicComponent'; + +import { InputnumberBasicComponent } from './../../../../inputnumber/demo/src/app/inputnumber/InputnumberBasicComponent'; + +import { InputnumberEventComponent } from './../../../../inputnumber/demo/src/app/inputnumber/InputnumberEventComponent'; + +import { InputnumberFocusComponent } from './../../../../inputnumber/demo/src/app/inputnumber/InputnumberFocusComponent'; + +import { InputnumberFormatComponent } from './../../../../inputnumber/demo/src/app/inputnumber/InputnumberFormatComponent'; + +import { InputnumberLoadComponent } from './../../../../inputnumber/demo/src/app/inputnumber/InputnumberLoadComponent'; + +import { InputnumberLocaleableComponent } from './../../../../inputnumber/demo/src/app/inputnumber/InputnumberLocaleableComponent'; + +import { InputnumberMaxlengthComponent } from './../../../../inputnumber/demo/src/app/inputnumber/InputnumberMaxlengthComponent'; + +import { IntroBasicComponent } from './../../../../intro/demo/src/app/intro/IntroBasicComponent'; + +import { IntroEventComponent } from './../../../../intro/demo/src/app/intro/IntroEventComponent'; + +import { IntroModalComponent } from './../../../../intro/demo/src/app/intro/IntroModalComponent'; + +import { IntroSkipableComponent } from './../../../../intro/demo/src/app/intro/IntroSkipableComponent'; + +import { IntroTemplateComponent } from './../../../../intro/demo/src/app/intro/IntroTemplateComponent'; + +import { IntroTipComponent } from './../../../../intro/demo/src/app/intro/IntroTipComponent'; + +import { IntroTiscrollComponent } from './../../../../intro/demo/src/app/intro/IntroTiscrollComponent'; + +import { IpBasicComponent } from './../../../../ip/demo/src/app/ip/IpBasicComponent'; + +import { IpDisabledComponent } from './../../../../ip/demo/src/app/ip/IpDisabledComponent'; + +import { IpFormcontrolComponent } from './../../../../ip/demo/src/app/ip/IpFormcontrolComponent'; + +import { IpValidComponent } from './../../../../ip/demo/src/app/ip/IpValidComponent'; + +import { IpsectionBasicComponent } from './../../../../ipsection/demo/src/app/ipsection/IpsectionBasicComponent'; + +import { IpsectionDisabledComponent } from './../../../../ipsection/demo/src/app/ipsection/IpsectionDisabledComponent'; + +import { IpsectionEventsComponent } from './../../../../ipsection/demo/src/app/ipsection/IpsectionEventsComponent'; + +import { IpsectionFocusComponent } from './../../../../ipsection/demo/src/app/ipsection/IpsectionFocusComponent'; + +import { IpsectionTestComponent } from './../../../../ipsection/demo/src/app/ipsection/IpsectionTestComponent'; + +import { IpsectionValidFormgroupComponent } from './../../../../ipsection/demo/src/app/ipsection/IpsectionValidFormgroupComponent'; + +import { IpsectionValidComponent } from './../../../../ipsection/demo/src/app/ipsection/IpsectionValidComponent'; + +import { LabeleditorBasicComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorBasicComponent'; + +import { LabeleditorAutotipComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorAutotipComponent'; + +import { LabeleditorIconTipContextComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorIcontipcontextComponent'; + +import { LabeleditorResizeComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorResizeComponent'; + +import { LabeleditorMaxlineComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorMaxlineComponent'; + +import { LabeleditorMaxlengthComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorMaxlengthComponent'; + +import { LabeleditorValidationComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorValidationComponent'; + +import { LabeleditorValidationAsyncComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorValidationAsyncComponent'; + +import { LabeleditorDisabledComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorDisabledComponent'; + +import { LabeleditorEventsComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorEventsComponent'; + +import { LabeleditorMultilineSizeComponent } from './../../../../labeleditor/demo/src/app/labeleditor/LabeleditorMultilineSizeComponent'; + +import { LayoutBasicSimpleResponsiveComponent } from './../../../../layout/demo/src/app/layout/LayoutBasicSimpleResponsiveComponent'; + +import { LayoutBasicSimpleComponent } from './../../../../layout/demo/src/app/layout/LayoutBasicSimpleComponent'; + +import { LayoutBasicComponent } from './../../../../layout/demo/src/app/layout/LayoutBasicComponent'; + +import { LayoutDetailColumnComponent } from './../../../../layout/demo/src/app/layout/LayoutDetailColumnComponent'; + +import { LayoutDetailComponent } from './../../../../layout/demo/src/app/layout/LayoutDetailComponent'; + +import { LayoutListLargedataComponent } from './../../../../layout/demo/src/app/layout/LayoutListLargedataComponent'; + +import { LayoutListComponent } from './../../../../layout/demo/src/app/layout/LayoutListComponent'; + +import { LayoutMultiColumnComponent } from './../../../../layout/demo/src/app/layout/LayoutMultiColumnComponent'; + +import { LayoutOverviewVerticalComponent } from './../../../../layout/demo/src/app/layout/LayoutOverviewVerticalComponent'; + +import { LayoutOverviewComponent } from './../../../../layout/demo/src/app/layout/LayoutOverviewComponent'; + +import { LayoutPurchaseResponsiveChangeComponent } from './../../../../layout/demo/src/app/layout/LayoutPurchaseResponsiveChangeComponent'; + +import { LayoutPurchaseResponsiveComponent } from './../../../../layout/demo/src/app/layout/LayoutPurchaseResponsiveComponent'; + +import { LayoutPurchaseSimpleResponsiveComponent } from './../../../../layout/demo/src/app/layout/LayoutPurchaseSimpleResponsiveComponent'; + +import { LayoutPurchaseSimpleComponent } from './../../../../layout/demo/src/app/layout/LayoutPurchaseSimpleComponent'; + +import { LayoutPurchaseComponent } from './../../../../layout/demo/src/app/layout/LayoutPurchaseComponent'; + +import { LayoutSingleComponent } from './../../../../layout/demo/src/app/layout/LayoutSingleComponent'; + +import { LeftmenuActiveChangeWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuActiveChangeWebsiteViewComponent'; + +import { LeftmenuBasicWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuBasicWebsiteViewComponent'; + +import { LeftmenuCollapsedWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuCollapsedWebsiteViewComponent'; + +import { LeftmenuDisabledWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDisabledWebsiteViewComponent'; + +import { LeftmenuDividingWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuDividingWebsiteViewComponent'; + +import { LeftmenuFootWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuFootWebsiteViewComponent'; + +import { LeftmenuGroupWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuGroupWebsiteViewComponent'; + +import { LeftmenuHrefWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuHrefWebsiteViewComponent'; + +import { LeftmenuNoRouterWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuNoRouterWebsiteViewComponent'; + +import { LeftmenuParamsWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuParamsWebsiteViewComponent'; + +import { LeftmenuReloadStateWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuReloadStateWebsiteViewComponent'; + +import { LeftmenuRouterlistWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuRouterlistWebsiteViewComponent'; + +import { LeftmenuToggleableWebsiteViewComponent } from './../../../../leftmenu/demo/src/app/leftmenu/website-views/LeftmenuToggleableWebsiteViewComponent'; + +import { LoadingAreaComponent } from './../../../../loading/demo/src/app/loading/LoadingAreaComponent'; + +import { LoadingBasicComponent } from './../../../../loading/demo/src/app/loading/LoadingBasicComponent'; + +import { LoadingSizeComponent } from './../../../../loading/demo/src/app/loading/LoadingSizeComponent'; + +import { LoadingTypeComponent } from './../../../../loading/demo/src/app/loading/LoadingTypeComponent'; + +import { LocaleBasicComponent } from './../../../../locale/demo/src/app/locale/LocaleBasicComponent'; + +import { LocaleFormatComponent } from './../../../../locale/demo/src/app/locale/LocaleFormatComponent'; + +import { LocaleReloadComponent } from './../../../../locale/demo/src/app/locale/LocaleReloadComponent'; + +import { LinkbuttonBasicComponent } from './../../../../linkbutton/demo/src/app/linkbutton/LinkbuttonBasicComponent'; + +import { MenuBasicComponent } from './../../../../menu/demo/src/app/menu/MenuBasicComponent'; + +import { MenuBeforeopenComponent } from './../../../../menu/demo/src/app/menu/MenuBeforeopenComponent'; + +import { MenuBorderComponent } from './../../../../menu/demo/src/app/menu/MenuBorderComponent'; + +import { MenuButtoncolorComponent } from './../../../../menu/demo/src/app/menu/MenuButtoncolorComponent'; + +import { MenuDefaultComponent } from './../../../../menu/demo/src/app/menu/MenuDefaultComponent'; + +import { MenuDisabledComponent } from './../../../../menu/demo/src/app/menu/MenuDisabledComponent'; + +import { MenuEventComponent } from './../../../../menu/demo/src/app/menu/MenuEventComponent'; + +import { MenuGroupComponent } from './../../../../menu/demo/src/app/menu/MenuGroupComponent'; + +import { MenuIdComponent } from './../../../../menu/demo/src/app/menu/MenuIdComponent'; + +import { MenuLabelkeyComponent } from './../../../../menu/demo/src/app/menu/MenuLabelkeyComponent'; + +import { MenuPanelalignComponent } from './../../../../menu/demo/src/app/menu/MenuPanelalignComponent'; + +import { MenuPanelstyleComponent } from './../../../../menu/demo/src/app/menu/MenuPanelstyleComponent'; + +import { MenuTempleteTestComponent } from './../../../../menu/demo/src/app/menu/MenuTempleteTestComponent'; + +import { MenuTempleteComponent } from './../../../../menu/demo/src/app/menu/MenuTempleteComponent'; + +import { MenuTipsComponent } from './../../../../menu/demo/src/app/menu/MenuTipsComponent'; + +import { MessageBasicComponent } from './../../../../message/demo/src/app/message/MessageBasicComponent'; + +import { MessageBtnTestComponent } from './../../../../message/demo/src/app/message/MessageBtnTestComponent'; + +import { MessageBtnComponent } from './../../../../message/demo/src/app/message/MessageBtnComponent'; + +import { MessageContentComponent } from './../../../../message/demo/src/app/message/MessageContentComponent'; + +import { MessageIdComponent } from './../../../../message/demo/src/app/message/MessageIdComponent'; + +import { MessageSecurityComponent } from './../../../../message/demo/src/app/message/MessageSecurityComponent'; + +import { MessageTitleComponent } from './../../../../message/demo/src/app/message/MessageTitleComponent'; + +import { MessageTypeComponent } from './../../../../message/demo/src/app/message/MessageTypeComponent'; + +import { ModalAnimationComponent } from './../../../../modal/demo/src/app/modal/ModalAnimationComponent'; + +import { ModalBackdropComponent } from './../../../../modal/demo/src/app/modal/ModalBackdropComponent'; + +import { ModalClassComponent } from './../../../../modal/demo/src/app/modal/ModalClassComponent'; + +import { ModalCloseIconComponent } from './../../../../modal/demo/src/app/modal/ModalCloseIconComponent'; + +import { ModalConfigTestComponent } from './../../../../modal/demo/src/app/modal/ModalConfigTestComponent'; + +import { ModalContentCompComponent } from './../../../../modal/demo/src/app/modal/ModalContentCompComponent'; + +import { ModalContentTempComponent } from './../../../../modal/demo/src/app/modal/ModalContentTempComponent'; + +import { ModalDraggableComponent } from './../../../../modal/demo/src/app/modal/ModalDraggableComponent'; + +import { ModalEscComponent } from './../../../../modal/demo/src/app/modal/ModalEscComponent'; + +import { ModalEventComponent } from './../../../../modal/demo/src/app/modal/ModalEventComponent'; + +import { ModalHeaderAlignComponent } from './../../../../modal/demo/src/app/modal/ModalHeaderAlignComponent'; + +import { ModalHeaderStyleComponent } from './../../../../modal/demo/src/app/modal/ModalHeaderStyleComponent'; + +import { ModalTwoBackdropComponent } from './../../../../modal/demo/src/app/modal/ModalTwoBackdropComponent'; + +import { ModalTwoTestComponent } from './../../../../modal/demo/src/app/modal/ModalTwoTestComponent'; + +import { NavActiveComponent } from './../../../../nav/demo/src/app/nav/NavActiveComponent'; + +import { NavAlignComponent } from './../../../../nav/demo/src/app/nav/NavAlignComponent'; + +import { NavBasicComponent } from './../../../../nav/demo/src/app/nav/NavBasicComponent'; + +import { NavDisabledComponent } from './../../../../nav/demo/src/app/nav/NavDisabledComponent'; + +import { NavEventComponent } from './../../../../nav/demo/src/app/nav/NavEventComponent'; + +import { NavLeftComponent } from './../../../../nav/demo/src/app/nav/NavLeftComponent'; + +import { NavRightComponent } from './../../../../nav/demo/src/app/nav/NavRightComponent'; + +import { NavSelectableComponent } from './../../../../nav/demo/src/app/nav/NavSelectableComponent'; + +import { NavSubmenuComponent } from './../../../../nav/demo/src/app/nav/NavSubmenuComponent'; + +import { NavTemplateComponent } from './../../../../nav/demo/src/app/nav/NavTemplateComponent'; + +import { NavThemeComponent } from './../../../../nav/demo/src/app/nav/NavThemeComponent'; + +import { NavWidthComponent } from './../../../../nav/demo/src/app/nav/NavWidthComponent'; + +import { NotificationAnimationComponent } from './../../../../notification/demo/src/app/notification/NotificationAnimationComponent'; + +import { NotificationBasicComponent } from './../../../../notification/demo/src/app/notification/NotificationBasicComponent'; + +import { NotificationCloseComponent } from './../../../../notification/demo/src/app/notification/NotificationCloseComponent'; + +import { NotificationConfigComponent } from './../../../../notification/demo/src/app/notification/NotificationConfigComponent'; + +import { NotificationDurationComponent } from './../../../../notification/demo/src/app/notification/NotificationDurationComponent'; + +import { NotificationEventsComponent } from './../../../../notification/demo/src/app/notification/NotificationEventsComponent'; + +import { NotificationHoverPauseComponent } from './../../../../notification/demo/src/app/notification/NotificationHoverPauseComponent'; + +import { NotificationNameComponent } from './../../../../notification/demo/src/app/notification/NotificationNameComponent'; + +import { NotificationPositionComponent } from './../../../../notification/demo/src/app/notification/NotificationPositionComponent'; + +import { NotificationTemplateComponent } from './../../../../notification/demo/src/app/notification/NotificationTemplateComponent'; + +import { NotificationTypeComponent } from './../../../../notification/demo/src/app/notification/NotificationTypeComponent'; + +import { OverflowDestoryComponent } from './../../../../overflow/demo/src/app/overflow/OverflowDestoryComponent'; + +import { OverflowDirectiveComponent } from './../../../../overflow/demo/src/app/overflow/OverflowDirectiveComponent'; + +import { OverflowMaxlineComponent } from './../../../../overflow/demo/src/app/overflow/OverflowMaxlineComponent'; + +import { OverflowMaxwidthComponent } from './../../../../overflow/demo/src/app/overflow/OverflowMaxwidthComponent'; + +import { OverflowPositionComponent } from './../../../../overflow/demo/src/app/overflow/OverflowPositionComponent'; + +import { OverflowServiceComponent } from './../../../../overflow/demo/src/app/overflow/OverflowServiceComponent'; + +import { OverflowTestComponent } from './../../../../overflow/demo/src/app/overflow/OverflowTestComponent'; + +import { OverflowTipcontentComponent } from './../../../../overflow/demo/src/app/overflow/OverflowTipcontentComponent'; + +import { PhonenumberBasicComponent } from './../../../../phonenumber/demo/src/app/phonenumber/PhonenumberBasicComponent'; + +import { PhonenumberDisabledComponent } from './../../../../phonenumber/demo/src/app/phonenumber/PhonenumberDisabledComponent'; + +import { PhonenumberEventComponent } from './../../../../phonenumber/demo/src/app/phonenumber/PhonenumberEventComponent'; + +import { PhonenumberValidComponent } from './../../../../phonenumber/demo/src/app/phonenumber/PhonenumberValidComponent'; + +import { PhonenumberCountryComponent } from './../../../../phonenumber/demo/src/app/phonenumber/PhonenumberCountryComponent'; + +import { PaginationAutohideComponent } from './../../../../pagination/demo/src/app/pagination/PaginationAutohideComponent'; + +import { PaginationDisabledComponent } from './../../../../pagination/demo/src/app/pagination/PaginationDisabledComponent'; + +import { PaginationEventComponent } from './../../../../pagination/demo/src/app/pagination/PaginationEventComponent'; + +import { PaginationFixedComponent } from './../../../../pagination/demo/src/app/pagination/PaginationFixedComponent'; + +import { PaginationLoadingComponent } from './../../../../pagination/demo/src/app/pagination/PaginationLoadingComponent'; + +import { PaginationPageselectwidthComponent } from './../../../../pagination/demo/src/app/pagination/PaginationPageselectwidthComponent'; + +import { PaginationPagesizeComponent } from './../../../../pagination/demo/src/app/pagination/PaginationPagesizeComponent'; + +import { PaginationShowgotolinkComponent } from './../../../../pagination/demo/src/app/pagination/PaginationShowgotolinkComponent'; + +import { PaginationShowlastpageComponent } from './../../../../pagination/demo/src/app/pagination/PaginationShowlastpageComponent'; + +import { PaginationShowtotalnumberComponent } from './../../../../pagination/demo/src/app/pagination/PaginationShowtotalnumberComponent'; + +import { PaginationTypeComponent } from './../../../../pagination/demo/src/app/pagination/PaginationTypeComponent'; + +import { PathfieldItemsComponent } from './../../../../path/demo/src/app/path/PathfieldItemsComponent'; + +import { PathfieldIspanelComponent } from './../../../../path/demo/src/app/path/PathfieldIspanelComponent'; + +import { PathfieldPanelwidthComponent } from './../../../../path/demo/src/app/path/PathfieldPanelwidthComponent'; + +import { PathfieldEditableComponent } from './../../../../path/demo/src/app/path/PathfieldEditableComponent'; + +import { PathfieldEventComponent } from '../../../../path/demo/src/app/path/PathfieldEventComponent'; + +import { PathListComponent } from './../../../../path/demo/src/app/path/PathListComponent'; + +import { PathSelectComponent } from './../../../../path/demo/src/app/path/PathSelectComponent'; + +import { PopconfirmBasicComponent } from './../../../../popconfirm/demo/src/app/popconfirm/PopconfirmBasicComponent'; + +import { PopconfirmDefineComponent } from './../../../../popconfirm/demo/src/app/popconfirm/PopconfirmDefineComponent'; + +import { PopconfirmEventComponent } from './../../../../popconfirm/demo/src/app/popconfirm/PopconfirmEventComponent'; + +import { PopconfirmTableDefineComponent } from './../../../../popconfirm/demo/src/app/popconfirm/PopconfirmTableDefineComponent'; + +import { PopconfirmTableComponent } from './../../../../popconfirm/demo/src/app/popconfirm/PopconfirmTableComponent'; + +import { ProgressbarAnimationComponent } from './../../../../progressbar/demo/src/app/progressbar/ProgressbarAnimationComponent'; + +import { ProgressbarBasicComponent } from './../../../../progressbar/demo/src/app/progressbar/ProgressbarBasicComponent'; + +import { ProgressbarClassComponent } from './../../../../progressbar/demo/src/app/progressbar/ProgressbarClassComponent'; + +import { ProductpreviewBasicComponent } from './../../../../productpreview/demo/src/app/productpreview/ProductpreviewBasicComponent'; + +import { RadioBasicComponent } from './../../../../radio/demo/src/app/radio/RadioBasicComponent'; + +import { RadioDarkComponent } from './../../../../radio/demo/src/app/radio/RadioDarkComponent'; + +import { RadioDisabledComponent } from './../../../../radio/demo/src/app/radio/RadioDisabledComponent'; + +import { RadioEventComponent } from './../../../../radio/demo/src/app/radio/RadioEventComponent'; + +import { RadioFocusComponent } from './../../../../radio/demo/src/app/radio/RadioFocusComponent'; + +import { RadioGroupComponent } from './../../../../radio/demo/src/app/radio/RadioGroupComponent'; + +import { RadioGroupDirectionComponent } from './../../../../radio/demo/src/app/radio/RadioGroupDirectionComponent'; + +import { RadioGroupLabelkeyComponent } from './../../../../radio/demo/src/app/radio/RadioGroupLabelkeyComponent'; + +import { RadioGroupLinewrapComponent } from './../../../../radio/demo/src/app/radio//RadioGroupLinewrapComponent'; + +import { RadioGroupValidationComponent } from './../../../../radio/demo/src/app/radio//RadioGroupValidationComponent'; + +import { RadioGroupValuekeyComponent } from './../../../../radio/demo/src/app/radio/RadioGroupValuekeyComponent'; + +import { RadioLabelComponent } from './../../../../radio/demo/src/app/radio/RadioLabelComponent'; + +import { RateBasicComponent } from './../../../../rate/demo/src/app/rate/RateBasicComponent'; + +import { RateDisabledComponent } from './../../../../rate/demo/src/app/rate/RateDisabledComponent'; + +import { RateEventComponent } from './../../../../rate/demo/src/app/rate/RateEventComponent'; + +import { RateIdComponent } from './../../../../rate/demo/src/app/rate/RateIdComponent'; + +import { RateLoadComponent } from './../../../../rate/demo/src/app/rate/RateLoadComponent'; + +import { RightsBasicComponent } from './../../../../rights/demo/src/app/rights/RightsBasicComponent'; + +import { RightsTypeComponent } from './../../../../rights/demo/src/app/rights/RightsTypeComponent'; + +import { ScoreBasicComponent } from './../../../../score/demo/src/app/score/ScoreBasicComponent'; + +import { ScoreEventsComponent } from './../../../../score/demo/src/app/score/ScoreEventsComponent'; + +import { ScoreLimittextComponent } from './../../../../score/demo/src/app/score/ScoreLimittextComponent'; + +import { ScorePaddingComponent } from './../../../../score/demo/src/app/score/ScorePaddingComponent'; + +import { SearchboxAppendtobodyComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxAppendtobodyComponent'; + +import { SearchboxBasicComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxBasicComponent'; + +import { SearchboxDisabledComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxDisabledComponent'; + +import { SearchboxEventComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxEventComponent'; + +import { SearchboxMaxlengthComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxMaxlengthComponent'; + +import { SearchboxNotsearchComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxNotsearchComponent'; + +import { SearchboxOptionsComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxOptionsComponent'; + +import { SearchboxPanelsizeComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxPanelsizeComponent'; + +import { SearchboxReactiveComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxReactiveComponent'; + +import { SearchboxSuggestComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxSuggestComponent'; + +import { SearchboxTemplateComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxTemplateComponent'; + +import { SearchboxTestComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxTestComponent'; + +import { SearchboxTrimmedComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxTrimmedComponent'; + +import { SearchboxValidComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxValidComponent'; + +import { SearchboxVirtualscrollComponent } from './../../../../searchbox/demo/src/app/searchbox/SearchboxVirtualscrollComponent'; + +import { SelectgroupBasicComponent } from './../../../../selectgroup/demo/src/app/selectgroup/SelectgroupBasicComponent'; + +import { SelectgroupMultipleComponent } from './../../../../selectgroup/demo/src/app/selectgroup/SelectgroupMultipleComponent'; + +import { SelectgroupValuekeyComponent } from './../../../../selectgroup/demo/src/app/selectgroup/SelectgroupValuekeyComponent'; + +import { SelectgroupTemplateComponent } from './../../../../selectgroup/demo/src/app/selectgroup/SelectgroupTemplateComponent'; + +import { SelectgroupSelectComponent } from './../../../../selectgroup/demo/src/app/selectgroup/SelectgroupSelectComponent'; + +import { SelectAppendtobodyComponent } from './../../../../select/demo/src/app/select/SelectAppendtobodyComponent'; + +import { SelectBasicComponent } from './../../../../select/demo/src/app/select/SelectBasicComponent'; + +import { SelectBeforesearchTestComponent } from './../../../../select/demo/src/app/select/SelectBeforesearchTestComponent'; + +import { SelectBeforesearchComponent } from './../../../../select/demo/src/app/select/SelectBeforesearchComponent'; + +import { SelectChangeSelectallComponent } from './../../../../select/demo/src/app/select/SelectChangeSelectallComponent'; + +import { SelectClearableComponent } from './../../../../select/demo/src/app/select/SelectClearableComponent'; + +import { SelectDisabledComponent } from './../../../../select/demo/src/app/select/SelectDisabledComponent'; + +import { SelectDisabledfocusComponent } from './../../../../select/demo/src/app/select/SelectDisabledfocusComponent'; + +import { SelectEnumComponent } from './../../../../select/demo/src/app/select/SelectEnumComponent'; + +import { SelectEventComponent } from './../../../../select/demo/src/app/select/SelectEventComponent'; + +import { SelectFocusComponent } from './../../../../select/demo/src/app/select/SelectFocusComponent'; + +import { SelectGroupComponent } from './../../../../select/demo/src/app/select/SelectGroupComponent'; + +import { SelectIdComponent } from './../../../../select/demo/src/app/select/SelectIdComponent'; + +import { SelectIdkeyComponent } from './../../../../select/demo/src/app/select/SelectIdkeyComponent'; + +import { SelectInputComponent } from './../../../../select/demo/src/app/select/SelectInputComponent'; + +import { SelectLabelkeyComponent } from './../../../../select/demo/src/app/select/SelectLabelkeyComponent'; + +import { SelectLazyComponent } from './../../../../select/demo/src/app/select/SelectLazyComponent'; + +import { SelectLeakComponent } from './../../../../select/demo/src/app/select/SelectLeakComponent'; + +import { SelectLoadComponent } from './../../../../select/demo/src/app/select/SelectLoadComponent'; + +import { SelectManyComponent } from './../../../../select/demo/src/app/select/SelectManyComponent'; + +import { SelectMaxlineComponent } from './../../../../select/demo/src/app/select/SelectMaxlineComponent'; + +import { SelectMuchComponent } from './../../../../select/demo/src/app/select/SelectMuchComponent'; + +import { SelectMultiComponent } from './../../../../select/demo/src/app/select/SelectMultiComponent'; + +import { SelectNoborderComponent } from './../../../../select/demo/src/app/select/SelectNoborderComponent'; + +import { SelectNodataComponent } from './../../../../select/demo/src/app/select/SelectNodataComponent'; + +import { SelectNoemptyComponent } from './../../../../select/demo/src/app/select/SelectNoemptyComponent'; + +import { SelectNullComponent } from './../../../../select/demo/src/app/select/SelectNullComponent'; + +import { SelectPaginBeforesearchComponent } from './../../../../select/demo/src/app/select/SelectPaginBeforesearchComponent'; + +import { SelectPaginationComponent } from './../../../../select/demo/src/app/select/SelectPaginationComponent'; + +import { SelectPanelComponent } from './../../../../select/demo/src/app/select/SelectPanelComponent'; + +import { SelectReservesearchwordComponent } from './../../../../select/demo/src/app/select/SelectReservesearchwordComponent'; + +import { SelectScrollLoadComponent } from './../../../../select/demo/src/app/select/SelectScrollLoadComponent'; + +import { SelectSearchComponent } from './../../../../select/demo/src/app/select/SelectSearchComponent'; + +import { SelectSearchkeysComponent } from './../../../../select/demo/src/app/select/SelectSearchkeysComponent'; + +import { SelectSelectallComponent } from './../../../../select/demo/src/app/select/SelectSelectallComponent'; + +import { SelectShowselectednumberComponent } from './../../../../select/demo/src/app/select/SelectShowselectednumberComponent'; + +import { SelectSmallComponent } from './../../../../select/demo/src/app/select/SelectSmallComponent'; + +import { SelectTagComponent } from './../../../../select/demo/src/app/select/SelectTagComponent'; + +import { SelectTemplateComponent } from './../../../../select/demo/src/app/select/SelectTemplateComponent'; + +import { SelectTipComponent } from './../../../../select/demo/src/app/select/SelectTipComponent'; + +import { SelectTiscrollComponent } from './../../../../select/demo/src/app/select/SelectTiscrollComponent'; + +import { SelectTworowComponent } from './../../../../select/demo/src/app/select/SelectTworowComponent'; + +import { SelectValidComponent } from './../../../../select/demo/src/app/select/SelectValidComponent'; + +import { SelectValidgroupComponent } from './../../../../select/demo/src/app/select/SelectValidgroupComponent'; + +import { SelectValuekeyTestComponent } from './../../../../select/demo/src/app/select/SelectValuekeyTestComponent'; + +import { SelectValuekeyComponent } from './../../../../select/demo/src/app/select/SelectValuekeyComponent'; + +import { SelectVirtualscrollMultiComponent } from './../../../../select/demo/src/app/select/SelectVirtualscrollMultiComponent'; + +import { SelectVirtualscrollComponent } from './../../../../select/demo/src/app/select/SelectVirtualscrollComponent'; + +import { SkeletonPageComponent } from './../../../../skeleton/demo/src/app/skeleton/SkeletonPageComponent'; + +import { SkeletonTitleComponent } from './../../../../skeleton/demo/src/app/skeleton/SkeletonTitleComponent'; + +import { SkeletonTypeComponent } from './../../../../skeleton/demo/src/app/skeleton/SkeletonTypeComponent'; + +import { SliderEventComponent } from './../../../../slider/demo/src/app/slider/SliderEventComponent'; + +import { SliderFormcontrolComponent } from './../../../../slider/demo/src/app/slider/SliderFormcontrolComponent'; + +import { SliderHiddenComponent } from './../../../../slider/demo/src/app/slider/SliderHiddenComponent'; + +import { SliderLimitsComponent } from './../../../../slider/demo/src/app/slider/SliderLimitsComponent'; + +import { SliderRatiosComponent } from './../../../../slider/demo/src/app/slider/SliderRatiosComponent'; + +import { SliderScalesComponent } from './../../../../slider/demo/src/app/slider/SliderScalesComponent'; + +import { SliderTemplateComponent } from './../../../../slider/demo/src/app/slider/SliderTemplateComponent'; + +import { SliderTipComponent } from './../../../../slider/demo/src/app/slider/SliderTipComponent'; + +import { SpinnerBasicTestComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerBasicTestComponent'; + +import { SpinnerBasicComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerBasicComponent'; + +import { SpinnerCorrectableComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerCorrectableComponent'; + +import { SpinnerDisabledComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerDisabledComponent'; + +import { SpinnerEventComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerEventComponent'; + +import { SpinnerFormatComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerFormatComponent'; + +import { SpinnerLoadComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerLoadComponent'; + +import { SpinnerLocaleableComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerLocaleableComponent'; + +import { SpinnerMaxMinComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerMaxMinComponent'; + +import { SpinnerMaxlengthComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerMaxlengthComponent'; + +import { SpinnerStepComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerStepComponent'; + +import { SpinnerStepfnComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerStepfnComponent'; + +import { SpinnerTipTestComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerTipTestComponent'; + +import { SpinnerTipComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerTipComponent'; + +import { SpinnerValidationTestComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerValidationTestComponent'; + +import { SpinnerValidationComponent } from './../../../../spinner/demo/src/app/spinner/SpinnerValidationComponent'; + +import { StepsActiveComponent } from './../../../../steps/demo/src/app/steps/StepsActiveComponent'; + +import { StepsAdaptiveTestComponent } from './../../../../steps/demo/src/app/steps/StepsAdaptiveTestComponent'; + +import { StepsAdaptiveComponent } from './../../../../steps/demo/src/app/steps/StepsAdaptiveComponent'; + +import { StepsBaseComponent } from './../../../../steps/demo/src/app/steps/StepsBaseComponent'; + +import { StepsBeforeComponent } from './../../../../steps/demo/src/app/steps/StepsBeforeComponent'; + +import { StepsClickableComponent } from './../../../../steps/demo/src/app/steps/StepsClickableComponent'; + +import { StepsEventsComponent } from './../../../../steps/demo/src/app/steps/StepsEventsComponent'; + +import { StepsLabelComponent } from './../../../../steps/demo/src/app/steps/StepsLabelComponent'; + +import { StepsMaxwidthComponent } from './../../../../steps/demo/src/app/steps/StepsMaxwidthComponent'; + +import { StepsTemplateComponent } from './../../../../steps/demo/src/app/steps/StepsTemplateComponent'; + +import { SubtitleBasicComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleBasicComponent'; + +import { SubtitleBeforeSearchComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleBeforeSearchComponent'; + +import { SubtitleDarkComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleDarkComponent'; + +import { SubtitleEventComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleEventComponent'; + +import { SubtitleIdkeyComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleIdkeyComponent'; + +import { SubtitleItemsComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleItemsComponent'; + +import { SubtitleMaxwidthComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleMaxwidthComponent'; + +import { SubtitlePanelwidthComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitlePanelwidthComponent'; + +import { SubtitleRouteComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleRouteComponent'; + +import { SubtitleScrollLoadComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleScrollLoadComponent'; + +import { SubtitleSearchableComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleSearchableComponent'; + +import { SubtitleTargetComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleTargetComponent'; + +import { SubtitleTipPositionComponent } from './../../../../subtitle/demo/src/app/subtitle/SubtitleTipPositionComponent'; + +import { SwiperActiveindexComponent } from './../../../../swiper/demo/src/app/swiper/SwiperActiveindexComponent'; + +import { SwiperAutoplayComponent } from './../../../../swiper/demo/src/app/swiper/SwiperAutoplayComponent'; + +import { SwiperBasicComponent } from './../../../../swiper/demo/src/app/swiper/SwiperBasicComponent'; + +import { SwiperEventsComponent } from './../../../../swiper/demo/src/app/swiper/SwiperEventsComponent'; + +import { SwiperIndicatorpositionComponent } from './../../../../swiper/demo/src/app/swiper/SwiperIndicatorpositionComponent'; + +import { SwiperLoopComponent } from './../../../../swiper/demo/src/app/swiper/SwiperLoopComponent'; + +import { SwiperShowcardnumTestComponent } from './../../../../swiper/demo/src/app/swiper/SwiperShowcardnumTestComponent'; + +import { SwiperShowcardnumComponent } from './../../../../swiper/demo/src/app/swiper/SwiperShowcardnumComponent'; + +import { SwitchBasicComponent } from './../../../../switch/demo/src/app/switch/SwitchBasicComponent'; + +import { SwitchBeforeComponent } from './../../../../switch/demo/src/app/switch/SwitchBeforeComponent'; + +import { SwitchDisabledComponent } from './../../../../switch/demo/src/app/switch/SwitchDisabledComponent'; + +import { SwitchEventComponent } from './../../../../switch/demo/src/app/switch/SwitchEventComponent'; + +import { SwitchExplanationComponent } from './../../../../switch/demo/src/app/switch/SwitchExplanationComponent'; + +import { SwitchFocusComponent } from './../../../../switch/demo/src/app/switch/SwitchFocusComponent'; + +import { SwitchIdComponent } from './../../../../switch/demo/src/app/switch/SwitchIdComponent'; + +import { SwitchLoadComponent } from './../../../../switch/demo/src/app/switch/SwitchLoadComponent'; + +import { SwitchTemplateComponent } from './../../../../switch/demo/src/app/switch/SwitchTemplateComponent'; + +import { TabBasicComponent } from './../../../../tab/demo/src/app/tab/TabBasicComponent'; + +import { TabBeforeactivechangeComponent } from './../../../../tab/demo/src/app/tab/TabBeforeactivechangeComponent'; + +import { TabContentCompComponent } from './../../../../tab/demo/src/app/tab/TabContentCompComponent'; + +import { TabCustomHeadComponent } from './../../../../tab/demo/src/app/tab/TabCustomHeadComponent'; + +import { TabDarkComponent } from './../../../../tab/demo/src/app/tab/TabDarkComponent'; + +import { TabDefaultTestComponent } from './../../../../tab/demo/src/app/tab/TabDefaultTestComponent'; + +import { TabLazyLoadComponent } from './../../../../tab/demo/src/app/tab/TabLazyLoadComponent'; + +import { TabLevel2TestComponent } from './../../../../tab/demo/src/app/tab/TabLevel2TestComponent'; + +import { TabLevel2Component } from './../../../../tab/demo/src/app/tab/TabLevel2Component'; + +import { TabOverflowComponent } from './../../../../tab/demo/src/app/tab/TabOverflowComponent'; + +import { TabRouteComponent } from './../../../../tab/demo/src/app/tab/TabRouteComponent'; + +import { TabScrollComponent } from './../../../../tab/demo/src/app/tab/TabScrollComponent'; + +import { TabSmallComponent } from './../../../../tab/demo/src/app/tab/TabSmallComponent'; + +import { TableActionmenuComponent } from './../../../../table/demo/src/app/table/TableActionmenuComponent'; + +import { TableBasicTestComponent } from './../../../../table/demo/src/app/table/TableBasicTestComponent'; + +import { TableBasicComponent } from './../../../../table/demo/src/app/table/TableBasicComponent'; + +import { TableCellTipComponent } from './../../../../table/demo/src/app/table/TableCellTipComponent'; + +import { TableCelliconsColsresizableComponent } from './../../../../table/demo/src/app/table/TableCelliconsColsresizableComponent'; + +import { TableCheckboxPaginationHeadmenuComponent } from './../../../../table/demo/src/app/table/TableCheckboxPaginationHeadmenuComponent'; + +import { TableCheckboxPaginationComponent } from './../../../../table/demo/src/app/table/TableCheckboxPaginationComponent'; + +import { TableCheckboxComponent } from './../../../../table/demo/src/app/table/TableCheckboxComponent'; + +import { TableColAlignComponent } from './../../../../table/demo/src/app/table/TableColAlignComponent'; + +import { TableColalignSortResizableTestComponent } from './../../../../table/demo/src/app/table/TableColalignSortResizableTestComponent'; + +import { TableColsResizableComponent } from './../../../../table/demo/src/app/table/TableColsResizableComponent'; + +import { TableColsToggleDetailsComponent } from './../../../../table/demo/src/app/table/TableColsToggleDetailsComponent'; + +import { TableColsToggleTestComponent } from './../../../../table/demo/src/app/table/TableColsToggleTestComponent'; + +import { TableColsToggleComponent } from './../../../../table/demo/src/app/table/TableColsToggleComponent'; + +import { TableColsresizableBasicComponent } from './../../../../table/demo/src/app/table/TableColsresizableBasicComponent'; + +import { TableColsresizableColstoggleFixedheadComponent } from './../../../../table/demo/src/app/table/TableColsresizableColstoggleFixedheadComponent'; + +import { TableColsresizableColstoggleComponent } from './../../../../table/demo/src/app/table/TableColsresizableColstoggleComponent'; + +import { TableColsresizableLoadfailComponent } from './../../../../table/demo/src/app/table/TableColsresizableLoadfailComponent'; + +import { TableColsresizableSortHeadfilterComponent } from './../../../../table/demo/src/app/table/TableColsresizableSortHeadfilterComponent'; + +import { TableColsresizableSortComponent } from './../../../../table/demo/src/app/table/TableColsresizableSortComponent'; + +import { TableColumnFixedComponent } from './../../../../table/demo/src/app/table/TableColumnFixedComponent'; + +import { TableColumnfixedCheckboxComponent } from './../../../../table/demo/src/app/table/TableColumnfixedCheckboxComponent'; + +import { TableColumnfixedColstoggleComponent } from './../../../../table/demo/src/app/table/TableColumnfixedColstoggleComponent'; + +import { TableColumnfixedEditrowComponent } from './../../../../table/demo/src/app/table/TableColumnfixedEditrowComponent'; + +import { TableColumnfixedFixedheadColsresizablePaginationComponent } from './../../../../table/demo/src/app/table/TableColumnfixedFixedheadColsresizablePaginationComponent'; + +import { TableColumnfixedHeadfixedComponent } from './../../../../table/demo/src/app/table/TableColumnfixedHeadfixedComponent'; + +import { TableColumnfixedLeftmenuComponent } from './../../../../table/demo/src/app/table/TableColumnfixedLeftmenuComponent'; + +import { TableColumnfixedNodataComponent } from './../../../../table/demo/src/app/table/TableColumnfixedNodataComponent'; + +import { TableColumnfixedPaginationComponent } from './../../../../table/demo/src/app/table/TableColumnfixedPaginationComponent'; + +import { TableColumnfixedResizableComponent } from './../../../../table/demo/src/app/table/TableColumnfixedResizableComponent'; + +import { TableComprehensiveComponent } from './../../../../table/demo/src/app/table/TableComprehensiveComponent'; + +import { TableDetailsCloseotherdetailsComponent } from './../../../../table/demo/src/app/table/TableDetailsCloseotherdetailsComponent'; + +import { TableDetailsNesttableComponent } from './../../../../table/demo/src/app/table/TableDetailsNesttableComponent'; + +import { TableDetailsPaginationComponent } from './../../../../table/demo/src/app/table/TableDetailsPaginationComponent'; + +import { TableDetailsComponent } from './../../../../table/demo/src/app/table/TableDetailsComponent'; + +import { TableDynamicDetailsComponent } from './../../../../table/demo/src/app/table/TableDynamicDetailsComponent'; + +import { TableEditallTestComponent } from './../../../../table/demo/src/app/table/TableEditallTestComponent'; + +import { TableEditallComponent } from './../../../../table/demo/src/app/table/TableEditallComponent'; + +import { TableEditrowTestComponent } from './../../../../table/demo/src/app/table/TableEditrowTestComponent'; + +import { TableEditrowComponent } from './../../../../table/demo/src/app/table/TableEditrowComponent'; + +import { TableFilterStrictComponent } from './../../../../table/demo/src/app/table/TableFilterStrictComponent'; + +import { TableFilterComponent } from './../../../../table/demo/src/app/table/TableFilterComponent'; + +import { TableFixedHeadColsResizableComponent } from './../../../../table/demo/src/app/table/TableFixedHeadColsResizableComponent'; + +import { TableFixedHeadInAccordionComponent } from './../../../../table/demo/src/app/table/TableFixedHeadInAccordionComponent'; + +import { TableFixedHeadNodataComponent } from './../../../../table/demo/src/app/table/TableFixedHeadNodataComponent'; + +import { TableFixedHeadPaginationDetailsComponent } from './../../../../table/demo/src/app/table/TableFixedHeadPaginationDetailsComponent'; + +import { TableFixedHeadComponent } from './../../../../table/demo/src/app/table/TableFixedHeadComponent'; + +import { TableFixedheadColsresizablePaginationDetailsComponent } from './../../../../table/demo/src/app/table/TableFixedheadColsresizablePaginationDetailsComponent'; + +import { TableFixheadScrollComponent } from './../../../../table/demo/src/app/table/TableFixheadScrollComponent'; + +import { TableGroupComponent } from './../../../../table/demo/src/app/table/TableGroupComponent'; + +import { TableGuideComponent } from './../../../../table/demo/src/app/table/TableGuideComponent'; + +import { TableHeadFilterDatetimeTestComponent } from './../../../../table/demo/src/app/table/TableHeadFilterDatetimeTestComponent'; + +import { TableHeadFilterDatetimeComponent } from './../../../../table/demo/src/app/table/TableHeadFilterDatetimeComponent'; + +import { TableHeadFilterMultiValuekeyComponent } from './../../../../table/demo/src/app/table/TableHeadFilterMultiValuekeyComponent'; + +import { TableHeadFilterMultiComponent } from './../../../../table/demo/src/app/table/TableHeadFilterMultiComponent'; + +import { TableHeadFilterTestComponent } from './../../../../table/demo/src/app/table/TableHeadFilterTestComponent'; + +import { TableHeadFilterValuekeyComponent } from './../../../../table/demo/src/app/table/TableHeadFilterValuekeyComponent'; + +import { TableHeadFilterVirtualscrollComponent } from './../../../../table/demo/src/app/table/TableHeadFilterVirtualscrollComponent'; + +import { TableHeadFilterComponent } from './../../../../table/demo/src/app/table/TableHeadFilterComponent'; + +import { TableLoadFailComponent } from './../../../../table/demo/src/app/table/TableLoadFailComponent'; + +import { TableNodataSimpleComponent } from './../../../../table/demo/src/app/table/TableNodataSimpleComponent'; + +import { TableNodataTestComponent } from './../../../../table/demo/src/app/table/TableNodataTestComponent'; + +import { TableNodataComponent } from './../../../../table/demo/src/app/table/TableNodataComponent'; + +import { TableOverflowLinkComponent } from './../../../../table/demo/src/app/table/TableOverflowLinkComponent'; + +import { TablePagiWithFilterComponent } from './../../../../table/demo/src/app/table/TablePagiWithFilterComponent'; + +import { TablePaginationComponent } from './../../../../table/demo/src/app/table/TablePaginationComponent'; + +import { TableRadioTestComponent } from './../../../../table/demo/src/app/table/TableRadioTestComponent'; + +import { TableRadioComponent } from './../../../../table/demo/src/app/table/TableRadioComponent'; + +import { TableRowDrag2Component } from './../../../../table/demo/src/app/table/TableRowDrag2Component'; + +import { TableRowspanComponent } from './../../../../table/demo/src/app/table/TableRowspanComponent'; + +import { TableSearchComponent } from './../../../../table/demo/src/app/table/TableSearchComponent'; + +import { TableServerPagiSearchSortComponent } from './../../../../table/demo/src/app/table/TableServerPagiSearchSortComponent'; + +import { TableServerPagiComponent } from './../../../../table/demo/src/app/table/TableServerPagiComponent'; + +import { TableSmallComponent } from './../../../../table/demo/src/app/table/TableSmallComponent'; + +import { TableSoldoutComponent } from './../../../../table/demo/src/app/table/TableSoldoutComponent'; + +import { TableSortBasicComponent } from './../../../../table/demo/src/app/table/TableSortBasicComponent'; + +import { TableSortComparefnLocaleComponent } from './../../../../table/demo/src/app/table/TableSortComparefnLocaleComponent'; + +import { TableSortComparefnComponent } from './../../../../table/demo/src/app/table/TableSortComparefnComponent'; + +import { TableSortDetailsComponent } from './../../../../table/demo/src/app/table/TableSortDetailsComponent'; + +import { TableSortResetComponent } from './../../../../table/demo/src/app/table/TableSortResetComponent'; + +import { TableSortTestComponent } from './../../../../table/demo/src/app/table/TableSortTestComponent'; + +import { TableSortComponent } from './../../../../table/demo/src/app/table/TableSortComponent'; + +import { TableStorageConfigComponent } from './../../../../table/demo/src/app/table/TableStorageConfigComponent'; + +import { TableStorageFilterComponent } from './../../../../table/demo/src/app/table/TableStorageFilterComponent'; + +import { TableStorageServeComponent } from './../../../../table/demo/src/app/table/TableStorageServeComponent'; + +import { TableStorageComponent } from './../../../../table/demo/src/app/table/TableStorageComponent'; + +import { TableTreeMulitiselectComponent } from './../../../../table/demo/src/app/table/TableTreeMulitiselectComponent'; + +import { TableTreeUnknowdeepthComponent } from './../../../../table/demo/src/app/table/TableTreeUnknowdeepthComponent'; + +import { TableTreeComponent } from './../../../../table/demo/src/app/table/TableTreeComponent'; + +import { TableVirtualscrollBasicComponent } from './../../../../table/demo/src/app/table/TableVirtualscrollBasicComponent'; + +import { TableVirtualscrollComprehensiveComponent } from './../../../../table/demo/src/app/table/TableVirtualscrollComprehensiveComponent'; + +import { TableVirtualscrollSizesComponent } from './../../../../table/demo/src/app/table/TableVirtualscrollSizesComponent'; + +import { TableVirtualscrollTreeComponent } from './../../../../table/demo/src/app/table/TableVirtualscrollTreeComponent'; + +import { TableVirtualscrollComponent } from './../../../../table/demo/src/app/table/TableVirtualscrollComponent'; + +import { TagBasicComponent } from './../../../../tag/demo/src/app/tag/TagBasicComponent'; + +import { TagDefaultComponent } from './../../../../tag/demo/src/app/tag/TagDefaultComponent'; + +import { TagDisabledComponent } from './../../../../tag/demo/src/app/tag/TagDisabledComponent'; + +import { TagEditComponent } from './../../../../tag/demo/src/app/tag/TagEditComponent'; + +import { TagsinputBasicComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputBasicComponent'; + +import { TagsinputDisabledComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputDisabledComponent'; + +import { TagsinputEventsComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputEventsComponent'; + +import { TagsinputLabelkeyComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputLabelkeyComponent'; + +import { TagsinputNullComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputNullComponent'; + +import { TagsinputPanelwidthComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputPanelwidthComponent'; + +import { TagsinputSeparatorsComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputSeparatorsComponent'; + +import { TagsinputReactiveComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputReactiveComponent'; + +import { TagsinputSuggestionComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputSuggestionComponent'; + +import { TagsinputTemplateComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputTemplateComponent'; + +import { TagsinputValidComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputValidComponent'; + +import { TagsinputValuekeyComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputValuekeyComponent'; + +import { TagsinputMaxlengthComponent } from './../../../../tagsinput/demo/src/app/tagsinput/TagsinputMaxlengthComponent'; + +import { TextBasicComponent } from './../../../../text/demo/src/app/text/TextBasicComponent'; + +import { TextClearComponent } from './../../../../text/demo/src/app/text/TextClearComponent'; + +import { TextDisabledComponent } from './../../../../text/demo/src/app/text/TextDisabledComponent'; + +import { TextEventsComponent } from './../../../../text/demo/src/app/text/TextEventsComponent'; + +import { TextFocusComponent } from './../../../../text/demo/src/app/text/TextFocusComponent'; + +import { TextMaskinputComponent } from './../../../../text/demo/src/app/text/TextMaskinputComponent'; + +import { TextNoborderTestComponent } from './../../../../text/demo/src/app/text/TextNoborderTestComponent'; + +import { TextPasswordVisibleComponent } from './../../../../text/demo/src/app/text/TextPasswordVisibleComponent'; + +import { TextPasswordComponent } from './../../../../text/demo/src/app/text/TextPasswordComponent'; + +import { TextReactiveComponent } from './../../../../text/demo/src/app/text/TextReactiveComponent'; + +import { TextReadonlyComponent } from './../../../../text/demo/src/app/text/TextReadonlyComponent'; + +import { TextareaAutofocusComponent } from './../../../../textarea/demo/src/app/textarea/TextareaAutofocusComponent'; + +import { TextareaDisabledComponent } from './../../../../textarea/demo/src/app/textarea/TextareaDisabledComponent'; + +import { TextareaMaxlengthComponent } from './../../../../textarea/demo/src/app/textarea/TextareaMaxlengthComponent'; + +import { TextareaNoneComponent } from './../../../../textarea/demo/src/app/textarea/TextareaNoneComponent'; + +import { TextareaResizeComponent } from './../../../../textarea/demo/src/app/textarea/TextareaResizeComponent'; + +import { TextareaScrollComponent } from './../../../../textarea/demo/src/app/textarea/TextareaScrollComponent'; + +import { TextareaValidComponent } from './../../../../textarea/demo/src/app/textarea/TextareaValidComponent'; + +import { TextareaWidthComponent } from './../../../../textarea/demo/src/app/textarea/TextareaWidthComponent'; + +import { TimeCleariconComponent } from './../../../../time/demo/src/app/time/TimeCleariconComponent'; + +import { TimeDisabledComponent } from './../../../../time/demo/src/app/time/TimeDisabledComponent'; + +import { TimeEventComponent } from './../../../../time/demo/src/app/time/TimeEventComponent'; + +import { TimeFormatComponent } from './../../../../time/demo/src/app/time/TimeFormatComponent'; + +import { TimeMaxComponent } from './../../../../time/demo/src/app/time/TimeMaxComponent'; + +import { TimeMaxminComponent } from './../../../../time/demo/src/app/time/TimeMaxminComponent'; + +import { TimeMinComponent } from './../../../../time/demo/src/app/time/TimeMinComponent'; + +import { TimeOptionDisabledComponent } from './../../../../time/demo/src/app/time/TimeOptionDisabledComponent'; + +import { TimePanelalignComponent } from './../../../../time/demo/src/app/time/TimePanelalignComponent'; + +import { TimeReactiveComponent } from './../../../../time/demo/src/app/time/TimeReactiveComponent'; + +import { TimeValidationComponent } from './../../../../time/demo/src/app/time/TimeValidationComponent'; + +import { TimelineBasicComponent } from './../../../../timeline/demo/src/app/timeline/TimelineBasicComponent'; + +import { TimelineDarkComponent } from './../../../../timeline/demo/src/app/timeline/TimelineDarkComponent'; + +import { TimelineHelptipComponent } from './../../../../timeline/demo/src/app/timeline/TimelineHelptipComponent'; + +import { TimelineMultiComponent } from './../../../../timeline/demo/src/app/timeline/TimelineMultiComponent'; + +import { TimelineTempleteComponent } from './../../../../timeline/demo/src/app/timeline/TimelineTempleteComponent'; + +import { TimelineTestComponent } from './../../../../timeline/demo/src/app/timeline/TimelineTestComponent'; + +import { TimelineTypeComponent } from './../../../../timeline/demo/src/app/timeline/TimelineTypeComponent'; + +import { TipBasicComponent } from './../../../../tip/demo/src/app/tip/TipBasicComponent'; + +import { TipContentCompComponent } from './../../../../tip/demo/src/app/tip/TipContentCompComponent'; + +import { TipContentTemplateComponent } from './../../../../tip/demo/src/app/tip/TipContentTemplateComponent'; + +import { TipEmptyComponent } from './../../../../tip/demo/src/app/tip/TipEmptyComponent'; + +import { TipHasArrowComponent } from './../../../../tip/demo/src/app/tip/TipHasArrowComponent'; + +import { TipLongTextPositionComponent } from './../../../../tip/demo/src/app/tip/TipLongTextPositionComponent'; + +import { TipMaxWidthComponent } from './../../../../tip/demo/src/app/tip/TipMaxWidthComponent'; + +import { TipPositionTestComponent } from './../../../../tip/demo/src/app/tip/TipPositionTestComponent'; + +import { TipPositionComponent } from './../../../../tip/demo/src/app/tip/TipPositionComponent'; + +import { TipServiceDestroyComponent } from './../../../../tip/demo/src/app/tip/TipServiceDestroyComponent'; + +import { TipServiceComponent } from './../../../../tip/demo/src/app/tip/TipServiceComponent'; + +import { TipTriggerComponent } from './../../../../tip/demo/src/app/tip/TipTriggerComponent'; + +import { TipValidPositionTestComponent } from './../../../../tip/demo/src/app/tip/TipValidPositionTestComponent'; + +import { TipZindexComponent } from './../../../../tip/demo/src/app/tip/TipZindexComponent'; + +import { TransferBasicComponent } from './../../../../transfer/demo/src/app/transfer/TransferBasicComponent'; + +import { TransferDisabledComponent } from './../../../../transfer/demo/src/app/transfer/TransferDisabledComponent'; + +import { TransferEventComponent } from './../../../../transfer/demo/src/app/transfer/TransferEventComponent'; + +import { TransferIdComponent } from './../../../../transfer/demo/src/app/transfer/TransferIdComponent'; + +import { TransferIdkeyComponent } from './../../../../transfer/demo/src/app/transfer/TransferIdkeyComponent'; + +import { TransferLabelkeyComponent } from './../../../../transfer/demo/src/app/transfer/TransferLabelkeyComponent'; + +import { TransferLazyComponent } from './../../../../transfer/demo/src/app/transfer/TransferLazyComponent'; + +import { TransferLoadComponent } from './../../../../transfer/demo/src/app/transfer/TransferLoadComponent'; + +import { TransferNodatatextComponent } from './../../../../transfer/demo/src/app/transfer/TransferNodatatextComponent'; + +import { TransferPaginationComponent } from './../../../../transfer/demo/src/app/transfer/TransferPaginationComponent'; + +import { TransferPlaceholderComponent } from './../../../../transfer/demo/src/app/transfer/TransferPlaceholderComponent'; + +import { TransferSearchableComponent } from './../../../../transfer/demo/src/app/transfer/TransferSearchableComponent'; + +import { TransferSearchkeysComponent } from './../../../../transfer/demo/src/app/transfer/TransferSearchkeysComponent'; + +import { TransferSizeComponent } from './../../../../transfer/demo/src/app/transfer/TransferSizeComponent'; + +import { TransferTableComponent } from './../../../../transfer/demo/src/app/transfer/TransferTableComponent'; + +import { TransferTitlesComponent } from './../../../../transfer/demo/src/app/transfer/TransferTitlesComponent'; + +import { TreeBeforeExpandComponent } from './../../../../tree/demo/src/app/tree/TreeBeforeExpandComponent'; + +import { TreeBeforeMoreComponent } from './../../../../tree/demo/src/app/tree/TreeBeforeMoreComponent'; + +import { TreeChangedbycheckboxComponent } from './../../../../tree/demo/src/app/tree/TreeChangedbycheckboxComponent'; + +import { TreeCheckRelationComponent } from './../../../../tree/demo/src/app/tree/TreeCheckRelationComponent'; + +import { TreeDisabledComponent } from './../../../../tree/demo/src/app/tree/TreeDisabledComponent'; + +import { TreeDragBeforedropComponent } from './../../../../tree/demo/src/app/tree/TreeDragBeforedropComponent'; + +import { TreeDragComponent } from './../../../../tree/demo/src/app/tree/TreeDragComponent'; + +import { TreeEventComponent } from './../../../../tree/demo/src/app/tree/TreeEventComponent'; + +import { TreeIconComponent } from './../../../../tree/demo/src/app/tree/TreeIconComponent'; + +import { TreeLoadComponent } from './../../../../tree/demo/src/app/tree/TreeLoadComponent'; + +import { TreeManyComponent } from './../../../../tree/demo/src/app/tree/TreeManyComponent'; + +import { TreeMultiselectComponent } from './../../../../tree/demo/src/app/tree/TreeMultiselectComponent'; + +import { TreeOperateComponent } from './../../../../tree/demo/src/app/tree/TreeOperateComponent'; + +import { TreeParentcheckableComponent } from './../../../../tree/demo/src/app/tree/TreeParentcheckableComponent'; + +import { TreeRadioselectComponent } from './../../../../tree/demo/src/app/tree/TreeRadioselectComponent'; + +import { TreeSearchComponent } from './../../../../tree/demo/src/app/tree/TreeSearchComponent'; + +import { TreeShortcutkeyComponent } from './../../../../tree/demo/src/app/tree/TreeShortcutkeyComponent'; + +import { TreeSmallComponent } from './../../../../tree/demo/src/app/tree/TreeSmallComponent'; + +import { TreeTemplateComponent } from './../../../../tree/demo/src/app/tree/TreeTemplateComponent'; + +import { TreeUtilComponent } from './../../../../tree/demo/src/app/tree/TreeUtilComponent'; + +import { TreeVirtualscrollDragComponent } from './../../../../tree/demo/src/app/tree/TreeVirtualscrollDragComponent'; + +import { TreeVirtualscrollSmallComponent } from './../../../../tree/demo/src/app/tree/TreeVirtualscrollSmallComponent'; + +import { TreeVirtualscrollComponent } from './../../../../tree/demo/src/app/tree/TreeVirtualscrollComponent'; + +import { TreeselectBasicComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectBasicComponent'; + +import { TreeselectBeforeExpandComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectBeforeExpandComponent'; + +import { TreeselectBeforeMoreComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectBeforeMoreComponent'; + +import { TreeselectClearableComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectClearableComponent'; + +import { TreeselectDisabledComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectDisabledComponent'; + +import { TreeselectDropmaxheightComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectDropmaxheightComponent'; + +import { TreeselectEventComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectEventComponent'; + +import { TreeselectFocusComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectFocusComponent'; + +import { TreeselectLabelkeyComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectLabelkeyComponent'; + +import { TreeselectLazyloadComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectLazyloadComponent'; + +import { TreeselectLoadComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectLoadComponent'; + +import { TreeselectMaxlineComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectMaxlineComponent'; + +import { TreeselectModalComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectModalComponent'; + +import { TreeselectMultiComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectMultiComponent'; + +import { TreeselectNodataComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectNodataComponent'; + +import { TreeselectOptionsChangeComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectOptionsChangeComponent'; + +import { TreeselectPanelwidthComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectPanelwidthComponent'; + +import { TreeselectSearchComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectSearchComponent'; + +import { TreeselectSelectallComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectSelectallComponent'; + +import { TreeselectValidationComponent } from './../../../../treeselect/demo/src/app/treeselect/TreeselectValidationComponent'; + +import { UploadAutoUploadComponent } from './../../../../upload/demo/src/app/upload/UploadAutoUploadComponent'; + +import { UploadBasicComponent } from './../../../../upload/demo/src/app/upload/UploadBasicComponent'; + +import { UploadBatchSendComponent } from './../../../../upload/demo/src/app/upload/UploadBatchSendComponent'; + +import { UploadBeforeremoveComponent } from './../../../../upload/demo/src/app/upload/UploadBeforeremoveComponent'; + +import { UploadButtonTestComponent } from './../../../../upload/demo/src/app/upload/UploadButtonTestComponent'; + +import { UploadButtonComponent } from './../../../../upload/demo/src/app/upload/UploadButtonComponent'; + +import { UploadCaseTestComponent } from './../../../../upload/demo/src/app/upload/UploadCaseTestComponent'; + +import { UploadChangesComponent } from './../../../../upload/demo/src/app/upload/UploadChangesComponent'; + +import { UploadChunksizeComponent } from './../../../../upload/demo/src/app/upload/UploadChunksizeComponent'; + +import { UploadCustomComponent } from './../../../../upload/demo/src/app/upload/UploadCustomComponent'; + +import { UploadEventComponent } from './../../../../upload/demo/src/app/upload/UploadEventComponent'; + +import { UploadFilterComponent } from './../../../../upload/demo/src/app/upload/UploadFilterComponent'; + +import { UploadFormDataComponent } from './../../../../upload/demo/src/app/upload/UploadFormDataComponent'; + +import { UploadInitfilesTestComponent } from './../../../../upload/demo/src/app/upload/UploadInitfilesTestComponent'; + +import { UploadInputFieldTestComponent } from './../../../../upload/demo/src/app/upload/UploadInputFieldTestComponent'; + +import { UploadPropsComponent } from './../../../../upload/demo/src/app/upload/UploadPropsComponent'; + +import { UploadServiceTestComponent } from './../../../../upload/demo/src/app/upload/UploadServiceTestComponent'; + +import { UploadServiceComponent } from './../../../../upload/demo/src/app/upload/UploadServiceComponent'; + +import { UploadSingleComponent } from './../../../../upload/demo/src/app/upload/UploadSingleComponent'; + +import { UploadimageAutoUploadComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageAutoUploadComponent'; + +import { UploadimageBasicComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageBasicComponent'; + +import { UploadimageChangesComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageChangesComponent'; + +import { UploadimageDeletableComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageDeletableComponent'; + +import { UploadimageDisabledComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageDisabledComponent'; + +import { UploadimageDragComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageDragComponent'; + +import { UploadimageEventComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageEventComponent'; + +import { UploadimageFilterComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageFilterComponent'; + +import { UploadimageInitfilesComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageInitfilesComponent'; + +import { UploadimageMaxcountComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageMaxcountComponent'; + +import { UploadimageTemplateComponent } from './../../../../upload/demo/src/app/uploadimage/UploadimageTemplateComponent'; + +import { BrowserUsageComponent } from './../../../../utils/demo/src/app/browser/BrowserUsageComponent'; + +import { KeymapUsageComponent } from './../../../../utils/demo/src/app/keymap/KeymapUsageComponent'; + +import { LogUsageComponent } from './../../../../utils/demo/src/app/log/LogUsageComponent'; + +import { ThemeBasicComponent } from './../../../../utils/demo/src/app/theme/ThemeBasicComponent'; + +import { ValidationAsyncCheckTestComponent } from './../../../../validation/demo/src/app/validation/ValidationAsyncCheckTestComponent'; + +import { ValidationAsyncCheckComponent } from './../../../../validation/demo/src/app/validation/ValidationAsyncCheckComponent'; + +import { ValidationBasicControlComponent } from './../../../../validation/demo/src/app/validation/ValidationBasicControlComponent'; + +import { ValidationBasicDirectiveComponent } from './../../../../validation/demo/src/app/validation/ValidationBasicDirectiveComponent'; + +import { ValidationBlurCheckComponent } from './../../../../validation/demo/src/app/validation/ValidationBlurCheckComponent'; + +import { ValidationErrorMsgComponent } from './../../../../validation/demo/src/app/validation/ValidationErrorMsgComponent'; + +import { ValidationFormGroupConfigComponent } from './../../../../validation/demo/src/app/validation/ValidationFormGroupConfigComponent'; + +import { ValidationFormGroupTestComponent } from './../../../../validation/demo/src/app/validation/ValidationFormGroupTestComponent'; + +import { ValidationFormGroupComponent } from './../../../../validation/demo/src/app/validation/ValidationFormGroupComponent'; + +import { ValidationParamChangeComponent } from './../../../../validation/demo/src/app/validation/ValidationParamChangeComponent'; + +import { ValidationPwdCheckComponent } from './../../../../validation/demo/src/app/validation/ValidationPwdCheckComponent'; + +import { ValidationRulesCustomDirectiveComponent } from './../../../../validation/demo/src/app/validation/ValidationRulesCustomDirectiveComponent'; + +import { ValidationRulesCustomComponent } from './../../../../validation/demo/src/app/validation/ValidationRulesCustomComponent'; + +import { ValidationRulesTestComponent } from './../../../../validation/demo/src/app/validation/ValidationRulesTestComponent'; + +import { ValidationTemplateFormNestedComponent } from './../../../../validation/demo/src/app/validation/ValidationTemplateFormNestedComponent'; + +import { ValidationTipComponent } from './../../../../validation/demo/src/app/validation/ValidationTipComponent'; + +import { ValidationTiscrollComponent } from './../../../../validation/demo/src/app/validation/ValidationTiscrollComponent'; + +@Component({ + selector: `app-root`, + template: `` +}) +export class AppComponent {} + +const WCS: any = [ + { + selector: 'website-tiny-accordion-basic', + component: AccordionBasicComponent + }, + + { + selector: 'website-tiny-accordion-class', + component: AccordionClassComponent + }, + + { + selector: 'website-tiny-accordion-click-toggle', + component: AccordionClickToggleComponent + }, + + { + selector: 'website-tiny-accordion-close-others', + component: AccordionCloseOthersComponent + }, + + { + selector: 'website-tiny-accordion-disabled', + component: AccordionDisabledComponent + }, + + { + selector: 'website-tiny-accordion-open', + component: AccordionOpenComponent + }, + + { + selector: 'website-tiny-actionmenu-basic', + component: ActionmenuBasicComponent + }, + + { + selector: 'website-tiny-actionmenu-data', + component: ActionmenuDataComponent + }, + + { + selector: 'website-tiny-actionmenu-data2', + component: ActionmenuData2Component + }, + + { + selector: 'website-tiny-actionmenu-disabled', + component: ActionmenuDisabledComponent + }, + + { + selector: 'website-tiny-actionmenu-divider', + component: ActionmenuDividerComponent + }, + + { + selector: 'website-tiny-actionmenu-event', + component: ActionmenuEventComponent + }, + + { + selector: 'website-tiny-actionmenu-focus', + component: ActionmenuFocusComponent + }, + + { selector: 'website-tiny-actionmenu-id', component: ActionmenuIdComponent }, + + { + selector: 'website-tiny-actionmenu-items-change', + component: ActionmenuItemsChangeComponent + }, + + { + selector: 'website-tiny-actionmenu-items', + component: ActionmenuItemsComponent + }, + + { + selector: 'website-tiny-actionmenu-labelkey', + component: ActionmenuLabelkeyComponent + }, + + { + selector: 'website-tiny-actionmenu-many', + component: ActionmenuManyComponent + }, + + { + selector: 'website-tiny-actionmenu-menutext', + component: ActionmenuMenutextComponent + }, + + { + selector: 'website-tiny-actionmenu-panelstyle', + component: ActionmenuPanelstyleComponent + }, + + { + selector: 'website-tiny-actionmenu-shownum', + component: ActionmenuShownumComponent + }, + + { + selector: 'website-tiny-actionmenu-space', + component: ActionmenuSpaceComponent + }, + + { + selector: 'website-tiny-actionmenu-table', + component: ActionmenuTableComponent + }, + + { + selector: 'website-tiny-actionmenu-templete-test', + component: ActionmenuTempleteTestComponent + }, + + { + selector: 'website-tiny-actionmenu-templete', + component: ActionmenuTempleteComponent + }, + + { + selector: 'website-tiny-actionmenu-tips-test', + component: ActionmenuTipsTestComponent + }, + + { + selector: 'website-tiny-actionmenu-tips', + component: ActionmenuTipsComponent + }, + + { + selector: 'website-tiny-alert-boxshadow', + component: AlertBoxshadowComponent + }, + + { + selector: 'website-tiny-alert-darktheme', + component: AlertDarkthemeComponent + }, + + { selector: 'website-tiny-alert-dismiss', component: AlertDismissComponent }, + + { selector: 'website-tiny-alert-event', component: AlertEventComponent }, + + { selector: 'website-tiny-alert-icon', component: AlertIconComponent }, + + { + selector: 'website-tiny-alert-messages', + component: AlertMessagesComponent + }, + + { + selector: 'website-tiny-alert-open-test', + component: AlertOpenTestComponent + }, + + { selector: 'website-tiny-alert-open', component: AlertOpenComponent }, + + { selector: 'website-tiny-alert-size', component: AlertSizeComponent }, + + { + selector: 'website-tiny-alert-trigger-scroll', + component: AlertTriggerScrollComponent + }, + + { selector: 'website-tiny-alert-type', component: AlertTypeComponent }, + + { selector: 'website-tiny-anchor-basic', component: AnchorBasicComponent }, + + { selector: 'website-tiny-anchor-events', component: AnchorEventsComponent }, + + { selector: 'website-tiny-anchor-id', component: AnchorIdComponent }, + + { selector: 'website-tiny-anchor-items', component: AnchorItemsComponent }, + + { + selector: 'website-tiny-anchor-offsettop', + component: AnchorOffsettopComponent + }, + + { + selector: 'website-tiny-anchor-scrolltarget', + component: AnchorScrolltargetComponent + }, + + { selector: 'website-tiny-anchor-speed', component: AnchorSpeedComponent }, + + { + selector: 'website-tiny-anchor-template', + component: AnchorTemplateComponent + }, + + { selector: 'website-tiny-anchor-test', component: AnchorTestComponent }, + + { + selector: 'website-tiny-autocomplete-appendtobody', + component: AutocompleteAppendtobodyComponent + }, + + { + selector: 'website-tiny-autocomplete-basic', + component: AutocompleteBasicComponent + }, + + { + selector: 'website-tiny-autocomplete-clearable', + component: AutocompleteClearableComponent + }, + + { + selector: 'website-tiny-autocomplete-disabled', + component: AutocompleteDisabledComponent + }, + + { + selector: 'website-tiny-autocomplete-events', + component: AutocompleteEventsComponent + }, + + { + selector: 'website-tiny-autocomplete-labelkey', + component: AutocompleteLabelkeyComponent + }, + + { + selector: 'website-tiny-autocomplete-maxlength', + component: AutocompleteMaxlengthComponent + }, + + { + selector: 'website-tiny-autocomplete-panel-size', + component: AutocompletePanelSizeComponent + }, + + { + selector: 'website-tiny-autocomplete-template', + component: AutocompleteTemplateComponent + }, + + { + selector: 'website-tiny-autocomplete-test', + component: AutocompleteTestComponent + }, + + { + selector: 'website-tiny-autocomplete-tip', + component: AutocompleteTipComponent + }, + + { + selector: 'website-tiny-autocomplete-valid', + component: AutocompleteValidComponent + }, + + { selector: 'website-tiny-autocomplete-group', component: AutocompleteGroupComponent }, + + { + selector: 'website-tiny-avatar-image-error-test', + component: AvatarImageErrorTestComponent + }, + + { selector: 'website-tiny-avatar-image', component: AvatarImageComponent }, + + { selector: 'website-tiny-avatar-shape', component: AvatarShapeComponent }, + + { selector: 'website-tiny-avatar-size', component: AvatarSizeComponent }, + + { selector: 'website-tiny-avatar-style', component: AvatarStyleComponent }, + + { selector: 'website-tiny-avatar-text', component: AvatarTextComponent }, + + { selector: 'website-tiny-button-color', component: ButtonColorComponent }, + + { + selector: 'website-tiny-button-disabled', + component: ButtonDisabledComponent + }, + + { selector: 'website-tiny-button-event', component: ButtonEventComponent }, + + { selector: 'website-tiny-button-focus', component: ButtonFocusComponent }, + + { + selector: 'website-tiny-button-hasborder-test', + component: ButtonHasborderTestComponent + }, + + { + selector: 'website-tiny-button-hasborder', + component: ButtonHasborderComponent + }, + + { selector: 'website-tiny-button-icon', component: ButtonIconComponent }, + + { + selector: 'website-tiny-button-loading', + component: ButtonLoadingComponent + }, + + { + selector: 'website-tiny-button-onlyicon', + component: ButtonOnlyiconComponent + }, + + { selector: 'website-tiny-button-size', component: ButtonSizeComponent }, + + { selector: 'website-tiny-button-tip', component: ButtonTipComponent }, + + { + selector: 'website-tiny-buttongroup-activeclass', + component: ButtongroupActiveclassComponent + }, + + { + selector: 'website-tiny-buttongroup-beforeclick', + component: ButtongroupBeforeclickComponent + }, + + { + selector: 'website-tiny-buttongroup-deselectable', + component: ButtongroupDeselectableComponent + }, + + { + selector: 'website-tiny-buttongroup-disabled', + component: ButtongroupDisabledComponent + }, + + { + selector: 'website-tiny-buttongroup-enum', + component: ButtongroupEnumComponent + }, + + { + selector: 'website-tiny-buttongroup-event', + component: ButtongroupEventComponent + }, + + { + selector: 'website-tiny-buttongroup-focus', + component: ButtongroupFocusComponent + }, + + { + selector: 'website-tiny-buttongroup-id-test', + component: ButtongroupIdTestComponent + }, + + { + selector: 'website-tiny-buttongroup-id', + component: ButtongroupIdComponent + }, + + { + selector: 'website-tiny-buttongroup-items-test', + component: ButtongroupItemsTestComponent + }, + + { + selector: 'website-tiny-buttongroup-items', + component: ButtongroupItemsComponent + }, + + { + selector: 'website-tiny-buttongroup-many', + component: ButtongroupManyComponent + }, + + { + selector: 'website-tiny-buttongroup-minwidth', + component: ButtongroupMinwidthComponent + }, + + { + selector: 'website-tiny-buttongroup-multi-type', + component: ButtongroupMultiTypeComponent + }, + + { + selector: 'website-tiny-buttongroup-multiline', + component: ButtongroupMultilineComponent + }, + + { + selector: 'website-tiny-buttongroup-multiple', + component: ButtongroupMultipleComponent + }, + + { + selector: 'website-tiny-buttongroup-radio-type', + component: ButtongroupRadioTypeComponent + }, + + { + selector: 'website-tiny-buttongroup-reactive-forms', + component: ButtongroupReactiveFormsComponent + }, + + { + selector: 'website-tiny-buttongroup-sup-test', + component: ButtongroupSupTestComponent + }, + + { + selector: 'website-tiny-buttongroup-sup', + component: ButtongroupSupComponent + }, + + { + selector: 'website-tiny-buttongroup-template', + component: ButtongroupTemplateComponent + }, + + { + selector: 'website-tiny-buttongroup-tip', + component: ButtongroupTipComponent + }, + + { + selector: 'website-tiny-buttongroup-valuekey-test', + component: ButtongroupValuekeyTestComponent + }, + + { + selector: 'website-tiny-buttongroup-valuekey', + component: ButtongroupValuekeyComponent + }, + + { + selector: 'website-tiny-buttonselect-basic', + component: ButtonselectBasicComponent + }, + + { + selector: 'website-tiny-buttonselect-labelkey', + component: ButtonselectLabelkeyComponent + }, + + { selector: 'website-tiny-card-add', component: CardAddComponent }, + + { selector: 'website-tiny-card-basic', component: CardBasicComponent }, + + { selector: 'website-tiny-card-grid', component: CardGridComponent }, + + { selector: 'website-tiny-card-grid2', component: CardGrid2Component }, + + { selector: 'website-tiny-card-header', component: CardHeaderComponent }, + + { + selector: 'website-tiny-cascader-basic', + component: CascaderBasicComponent + }, + + { + selector: 'website-tiny-cascader-clearable', + component: CascaderClearableComponent + }, + + { + selector: 'website-tiny-cascader-disabled', + component: CascaderDisabledComponent + }, + + { + selector: 'website-tiny-cascader-events', + component: CascaderEventsComponent + }, + + { + selector: 'website-tiny-cascader-idkey', + component: CascaderIdkeyComponent + }, + + { + selector: 'website-tiny-cascader-item-test', + component: CascaderItemTestComponent + }, + + { + selector: 'website-tiny-cascader-labelkey', + component: CascaderLabelkeyComponent + }, + + { + selector: 'website-tiny-cascader-onlyselectleaf', + component: CascaderOnlyselectleafComponent + }, + + { + selector: 'website-tiny-cascader-panel', + component: CascaderPanelComponent + }, + + { + selector: 'website-tiny-cascader-search', + component: CascaderSearchComponent + }, + + { + selector: 'website-tiny-cascader-showalllevel', + component: CascaderShowalllevelComponent + }, + + { + selector: 'website-tiny-cascader-trigger', + component: CascaderTriggerComponent + }, + + { + selector: 'website-tiny-cascader-valid', + component: CascaderValidComponent + }, + + { + selector: 'website-tiny-cascader-valuekey', + component: CascaderValuekeyComponent + }, + + { + selector: 'website-tiny-checkbox-basic', + component: CheckboxBasicComponent + }, + + { + selector: 'website-tiny-checkbox-disabled', + component: CheckboxDisabledComponent + }, + + { + selector: 'website-tiny-checkbox-event', + component: CheckboxEventComponent + }, + + { + selector: 'website-tiny-checkbox-focused', + component: CheckboxFocusedComponent + }, + + { + selector: 'website-tiny-checkbox-group', + component: CheckboxGroupComponent + }, + + { + selector: 'website-tiny-checkbox-group-direction', + component: CheckboxGroupDirectionComponent + }, + + { + selector: 'website-tiny-checkbox-group-labelkey', + component: CheckboxGroupLabelkeyComponent + }, + + { + selector: 'website-tiny-checkbox-group-level', + component: CheckboxGroupLevelComponent + }, + + { + selector: 'website-tiny-checkbox-group-linewrap', + component: CheckboxGroupLinewrapComponent + }, + + { + selector: 'website-tiny-checkbox-group-validation', + component: CheckboxGroupValidationComponent + }, + + { + selector: 'website-tiny-checkbox-group-valuekey', + component: CheckboxGroupValuekeyComponent + }, + + { + selector: 'website-tiny-checkbox-indeterminate', + component: CheckboxIndeterminateComponent + }, + + { + selector: 'website-tiny-checkbox-label', + component: CheckboxLabelComponent + }, + + { + selector: 'website-tiny-collapse-basic', + component: CollapseBasicComponent + }, + + { + selector: 'website-tiny-collapse-event', + component: CollapseEventComponent + }, + + { + selector: 'website-tiny-collapsebox-basic', + component: CollapseboxBasicComponent + }, + + { + selector: 'website-tiny-collapsebox-closeable', + component: CollapseboxCloseableComponent + }, + + { + selector: 'website-tiny-collapsebox-event', + component: CollapseboxEventComponent + }, + + { + selector: 'website-tiny-collapsebutton-basic', + component: CollapsebuttonBasicComponent + }, + { + selector: 'website-tiny-collapsebutton-customtext', + component: CollapsebuttonCustomtextComponent + }, + { + selector: 'website-tiny-collapsebutton-searchcount', + component: CollapsebuttonSearchcountComponent + }, + { + selector: 'website-tiny-collapsebutton-event', + component: CollapsebuttonEventComponent + }, + + { selector: 'website-tiny-collapsetext-basic', component: CollapsetextBasicComponent }, + + { selector: 'website-tiny-collapsetext-type', component: CollapsetextTypeComponent }, + + { selector: 'website-tiny-collapsetext-highlight', component: CollapsetextHighlightComponent }, + + { selector: 'website-tiny-collapsetext-collapsed', component: CollapsetextCollapsedComponent }, + + { selector: 'website-tiny-collapsetext-scene', component: CollapsetextSceneComponent }, + + { + selector: 'website-tiny-copy-basic', + component: CopyBasicComponent + }, + { + selector: 'website-tiny-copy-dark', + component: CopyDarkComponent + }, + { + selector: 'website-tiny-copy-tip', + component: CopyTipComponent + }, + { + selector: 'website-tiny-copy-table', + component: CopyTableComponent + }, + { + selector: 'website-tiny-copy-event', + component: CopyEventComponent + }, + + { selector: 'website-tiny-crumb-basic', component: CrumbBasicComponent }, + + { selector: 'website-tiny-crumb-events', component: CrumbEventsComponent }, + + { selector: 'website-tiny-crumb-href', component: CrumbHrefComponent }, + + { + selector: 'website-tiny-crumb-router-test', + component: CrumbRouterTestComponent + }, + + { selector: 'website-tiny-crumb-router', component: CrumbRouterComponent }, + + { + selector: 'website-tiny-date-clearicon', + component: DateCleariconComponent + }, + + { + selector: 'website-tiny-date-customize', + component: DateCustomizeComponent + }, + + { selector: 'website-tiny-date-disabled', component: DateDisabledComponent }, + + { + selector: 'website-tiny-date-disableddays', + component: DateDisableddaysComponent + }, + + { selector: 'website-tiny-date-event', component: DateEventComponent }, + + { selector: 'website-tiny-date-form', component: DateFormComponent }, + + { + selector: 'website-tiny-date-format-test', + component: DateFormatTestComponent + }, + + { selector: 'website-tiny-date-format', component: DateFormatComponent }, + + { selector: 'website-tiny-date-max', component: DateMaxComponent }, + + { + selector: 'website-tiny-date-maxmin-test', + component: DateMaxminTestComponent + }, + + { selector: 'website-tiny-date-maxmin', component: DateMaxminComponent }, + + { selector: 'website-tiny-date-min', component: DateMinComponent }, + + { + selector: 'website-tiny-date-nowdatetime', + component: DateNowdatetimeComponent + }, + + { + selector: 'website-tiny-date-panelalign', + component: DatePanelalignComponent + }, + + { + selector: 'website-tiny-date-validation', + component: DateValidationComponent + }, + + { + selector: 'website-tiny-date-value-test', + component: DateValueTestComponent + }, + + { selector: 'website-tiny-date-value', component: DateValueComponent }, + + { + selector: 'website-tiny-daterange-customize', + component: DaterangeCustomizeComponent + }, + + { + selector: 'website-tiny-daterange-disabled', + component: DaterangeDisabledComponent + }, + + { + selector: 'website-tiny-daterange-disableddays', + component: DaterangeDisableddaysComponent + }, + + { + selector: 'website-tiny-daterange-event', + component: DaterangeEventComponent + }, + + { + selector: 'website-tiny-daterange-fixedvalue-test', + component: DaterangeFixedvalueTestComponent + }, + + { + selector: 'website-tiny-daterange-fixedvalue', + component: DaterangeFixedvalueComponent + }, + + { + selector: 'website-tiny-daterange-format-test', + component: DaterangeFormatTestComponent + }, + + { + selector: 'website-tiny-daterange-format', + component: DaterangeFormatComponent + }, + + { + selector: 'website-tiny-daterange-isallowbeginequalend', + component: DaterangeIsallowbeginequalendComponent + }, + + { selector: 'website-tiny-daterange-max', component: DaterangeMaxComponent }, + + { + selector: 'website-tiny-daterange-maxmin-test', + component: DaterangeMaxminTestComponent + }, + + { + selector: 'website-tiny-daterange-maxmin', + component: DaterangeMaxminComponent + }, + + { selector: 'website-tiny-daterange-min', component: DaterangeMinComponent }, + + { + selector: 'website-tiny-daterange-nowdatetime', + component: DaterangeNowdatetimeComponent + }, + + { + selector: 'website-tiny-daterange-panelalign', + component: DaterangePanelalignComponent + }, + + { + selector: 'website-tiny-daterange-validation', + component: DaterangeValidationComponent + }, + + { + selector: 'website-tiny-daterange-value-test', + component: DaterangeValueTestComponent + }, + + { + selector: 'website-tiny-daterange-value', + component: DaterangeValueComponent + }, + + { + selector: 'website-tiny-datetime-clearicon', + component: DatetimeCleariconComponent + }, + + { + selector: 'website-tiny-datetime-customize', + component: DatetimeCustomizeComponent + }, + + { + selector: 'website-tiny-datetime-disabled', + component: DatetimeDisabledComponent + }, + + { + selector: 'website-tiny-datetime-event', + component: DatetimeEventComponent + }, + + { + selector: 'website-tiny-datetime-format-test', + component: DatetimeFormatTestComponent + }, + + { + selector: 'website-tiny-datetime-format', + component: DatetimeFormatComponent + }, + + { selector: 'website-tiny-datetime-max', component: DatetimeMaxComponent }, + + { + selector: 'website-tiny-datetime-maxmin-test', + component: DatetimeMaxminTestComponent + }, + + { + selector: 'website-tiny-datetime-maxmin', + component: DatetimeMaxminComponent + }, + + { selector: 'website-tiny-datetime-min', component: DatetimeMinComponent }, + + { + selector: 'website-tiny-datetime-nowdatetime', + component: DatetimeNowdatetimeComponent + }, + + { + selector: 'website-tiny-datetime-panelalign', + component: DatetimePanelalignComponent + }, + + { + selector: 'website-tiny-datetime-validation', + component: DatetimeValidationComponent + }, + + { + selector: 'website-tiny-datetime-value-test', + component: DatetimeValueTestComponent + }, + + { + selector: 'website-tiny-datetime-value', + component: DatetimeValueComponent + }, + + { selector: 'website-tiny-datetime-timezoneable', component: DatetimeTimezoneableComponent }, + + { + selector: 'website-tiny-datetimerange-clearicon', + component: DatetimerangeCleariconComponent + }, + + { + selector: 'website-tiny-datetimerange-customize', + component: DatetimerangeCustomizeComponent + }, + + { + selector: 'website-tiny-datetimerange-disabled', + component: DatetimerangeDisabledComponent + }, + + { + selector: 'website-tiny-datetimerange-event', + component: DatetimerangeEventComponent + }, + + { + selector: 'website-tiny-datetimerange-format-test', + component: DatetimerangeFormatTestComponent + }, + + { + selector: 'website-tiny-datetimerange-format', + component: DatetimerangeFormatComponent + }, + + { + selector: 'website-tiny-datetimerange-isallowbeginequalend', + component: DatetimerangeIsallowbeginequalendComponent + }, + + { + selector: 'website-tiny-datetimerange-many-test', + component: DatetimerangeManyTestComponent + }, + + { + selector: 'website-tiny-datetimerange-max', + component: DatetimerangeMaxComponent + }, + + { + selector: 'website-tiny-datetimerange-maxmin-test', + component: DatetimerangeMaxminTestComponent + }, + + { + selector: 'website-tiny-datetimerange-maxmin', + component: DatetimerangeMaxminComponent + }, + + { + selector: 'website-tiny-datetimerange-min', + component: DatetimerangeMinComponent + }, + + { + selector: 'website-tiny-datetimerange-nowdatetime', + component: DatetimerangeNowdatetimeComponent + }, + + { + selector: 'website-tiny-datetimerange-panelalign', + component: DatetimerangePanelalignComponent + }, + + { + selector: 'website-tiny-datetimerange-timezoneable', + component: DatetimerangeTimezoneableComponent + }, + + { + selector: 'website-tiny-datetimerange-validation', + component: DatetimerangeValidationComponent + }, + + { + selector: 'website-tiny-datetimerange-value-test', + component: DatetimerangeValueTestComponent + }, + + { + selector: 'website-tiny-datetimerange-value', + component: DatetimerangeValueComponent + }, + + { + selector: 'website-tiny-formfield-colspan-rowspan-test', + component: FormfieldColspanRowspanTestComponent + }, + + { selector: 'website-tiny-guides-basic', component: GuidesBasicComponent }, + + { selector: 'website-tiny-guides-tab', component: GuidesTabComponent }, + + { + selector: 'website-tiny-guides-guidesteps', + component: GuidesGuidestepsComponent + }, + + { selector: 'website-tiny-guides-type', component: GuidesTypeComponent }, + + { + selector: 'website-tiny-formfield-colspan-rowspan', + component: FormfieldColspanRowspanComponent + }, + + { + selector: 'website-tiny-formfield-colswidth', + component: FormfieldColswidthComponent + }, + + { selector: 'website-tiny-formfield-foo', component: FormfieldFooComponent }, + + { + selector: 'website-tiny-formfield-index', + component: FormfieldIndexComponent + }, + + { + selector: 'website-tiny-formfield-label', + component: FormfieldLabelComponent + }, + + { + selector: 'website-tiny-formfield-labelwidth', + component: FormfieldLabelwidthComponent + }, + + { + selector: 'website-tiny-formfield-multi-column', + component: FormfieldMultiColumnComponent + }, + + { + selector: 'website-tiny-formfield-nest-formfiled', + component: FormfieldNestFormfiledComponent + }, + + { + selector: 'website-tiny-formfield-ngfor-test', + component: FormfieldNgforTestComponent + }, + + { + selector: 'website-tiny-formfield-required', + component: FormfieldRequiredComponent + }, + + { + selector: 'website-tiny-formfield-requiredspace', + component: FormfieldRequiredspaceComponent + }, + + { + selector: 'website-tiny-formfield-show', + component: FormfieldShowComponent + }, + + { + selector: 'website-tiny-formfield-single-column', + component: FormfieldSingleColumnComponent + }, + + { + selector: 'website-tiny-formfield-test', + component: FormfieldTestComponent + }, + + { + selector: 'website-tiny-formfield-text-form', + component: FormfieldTextFormComponent + }, + + { + selector: 'website-tiny-formfield-vertical-align', + component: FormfieldVerticalAlignComponent + }, + + { + selector: 'website-tiny-formfield-vertical', + component: FormfieldVerticalComponent + }, + + { + selector: 'website-tiny-foldtext-basic', + component: FoldtextBasicComponent + }, + + { + selector: 'website-tiny-foldtext-table', + component: FoldtextTableComponent + }, + + { + selector: 'website-tiny-guidesteps-basic', + component: GuidestepsBasicComponent + }, + + { + selector: 'website-tiny-guidesteps-iscomplete', + component: GuidestepsIscompleteComponent + }, + + { + selector: 'website-tiny-guidesteps-large', + component: GuidestepsLargeComponent + }, + + { + selector: 'website-tiny-halfmodal-async', + component: HalfmodalAsyncComponent + }, + + { + selector: 'website-tiny-halfmodal-backdrop', + component: HalfmodalBackdropComponent + }, + + { + selector: 'website-tiny-halfmodal-basic', + component: HalfmodalBasicComponent + }, + + { + selector: 'website-tiny-halfmodal-beforehide', + component: HalfmodalBeforehideComponent + }, + + { + selector: 'website-tiny-halfmodal-content', + component: HalfmodalContentComponent + }, + + { + selector: 'website-tiny-halfmodal-modal', + component: HalfmodalModalComponent + }, + + { + selector: 'website-tiny-halfmodal-modalselect', + component: HalfmodalModalselectComponent + }, + + { + selector: 'website-tiny-halfmodal-multi', + component: HalfmodalMultiComponent + }, + + { + selector: 'website-tiny-halfmodal-service-test', + component: HalfmodalServiceTestComponent + }, + + { + selector: 'website-tiny-halfmodal-service', + component: HalfmodalServiceComponent + }, + + { selector: 'website-tiny-icon-basic', component: IconBasicComponent }, + + { selector: 'website-tiny-svg-setpath', component: SvgSetpathComponent }, + + { selector: 'website-tiny-icon-show', component: IconShowComponent }, + + { selector: 'website-tiny-iconaction-basic', component: IconactionBasicComponent }, + + { selector: 'website-tiny-iconaction-dark', component: IconactionDarkComponent }, + + { selector: 'website-tiny-iconaction-disabled', component: IconactionDisabledComponent }, + + { selector: 'website-tiny-iconaction-href', component: IconactionHrefComponent }, + + { + selector: 'website-tiny-imagepreview-basic', + component: ImagepreviewBasicComponent + }, + + { + selector: 'website-tiny-inputnumber-basic', + component: InputnumberBasicComponent + }, + + { + selector: 'website-tiny-inputnumber-event', + component: InputnumberEventComponent + }, + + { + selector: 'website-tiny-inputnumber-focus', + component: InputnumberFocusComponent + }, + + { + selector: 'website-tiny-inputnumber-format', + component: InputnumberFormatComponent + }, + + { + selector: 'website-tiny-inputnumber-load', + component: InputnumberLoadComponent + }, + + { + selector: 'website-tiny-inputnumber-localeable', + component: InputnumberLocaleableComponent + }, + + { + selector: 'website-tiny-inputnumber-maxlength', + component: InputnumberMaxlengthComponent + }, + + { selector: 'website-tiny-intro-basic', component: IntroBasicComponent }, + + { selector: 'website-tiny-intro-event', component: IntroEventComponent }, + + { selector: 'website-tiny-intro-modal', component: IntroModalComponent }, + + { + selector: 'website-tiny-intro-skipable', + component: IntroSkipableComponent + }, + + { + selector: 'website-tiny-intro-template', + component: IntroTemplateComponent + }, + + { selector: 'website-tiny-intro-tip', component: IntroTipComponent }, + + { + selector: 'website-tiny-intro-tiscroll', + component: IntroTiscrollComponent + }, + + { selector: 'website-tiny-ip-basic', component: IpBasicComponent }, + + { selector: 'website-tiny-ip-disabled', component: IpDisabledComponent }, + + { + selector: 'website-tiny-ip-formcontrol', + component: IpFormcontrolComponent + }, + + { selector: 'website-tiny-ip-valid', component: IpValidComponent }, + + { + selector: 'website-tiny-ipsection-basic', + component: IpsectionBasicComponent + }, + + { + selector: 'website-tiny-ipsection-disabled', + component: IpsectionDisabledComponent + }, + + { + selector: 'website-tiny-ipsection-events', + component: IpsectionEventsComponent + }, + + { + selector: 'website-tiny-ipsection-focus', + component: IpsectionFocusComponent + }, + + { + selector: 'website-tiny-ipsection-test', + component: IpsectionTestComponent + }, + + { + selector: 'website-tiny-ipsection-valid-formgroup', + component: IpsectionValidFormgroupComponent + }, + + { + selector: 'website-tiny-ipsection-valid', + component: IpsectionValidComponent + }, + { selector: 'website-tiny-labeleditor-basic', component: LabeleditorBasicComponent }, + + { selector: 'website-tiny-labeleditor-autotip', component: LabeleditorAutotipComponent }, + + { selector: 'website-tiny-labeleditor-icontipcontext', component: LabeleditorIconTipContextComponent }, + + { selector: 'website-tiny-labeleditor-resize', component: LabeleditorResizeComponent }, + + { selector: 'website-tiny-labeleditor-maxline', component: LabeleditorMaxlineComponent }, + + { selector: 'website-tiny-labeleditor-maxlength', component: LabeleditorMaxlengthComponent }, + + { selector: 'website-tiny-labeleditor-validation', component: LabeleditorValidationComponent }, + + { selector: 'website-tiny-labeleditor-validation-async', component: LabeleditorValidationAsyncComponent }, + + { selector: 'website-tiny-labeleditor-disabled', component: LabeleditorDisabledComponent }, + + { selector: 'website-tiny-labeleditor-events', component: LabeleditorEventsComponent }, + + { selector: 'website-tiny-labeleditor-multiline-size', component: LabeleditorMultilineSizeComponent }, + + { + selector: 'website-tiny-layout-basic-simple-responsive', + component: LayoutBasicSimpleResponsiveComponent + }, + + { + selector: 'website-tiny-layout-basic-simple', + component: LayoutBasicSimpleComponent + }, + + { selector: 'website-tiny-layout-basic', component: LayoutBasicComponent }, + + { + selector: 'website-tiny-layout-detail-column', + component: LayoutDetailColumnComponent + }, + + { selector: 'website-tiny-layout-detail', component: LayoutDetailComponent }, + + { + selector: 'website-tiny-layout-list-largedata', + component: LayoutListLargedataComponent + }, + + { selector: 'website-tiny-layout-list', component: LayoutListComponent }, + + { + selector: 'website-tiny-layout-multi-column', + component: LayoutMultiColumnComponent + }, + + { + selector: 'website-tiny-layout-overview-vertical', + component: LayoutOverviewVerticalComponent + }, + + { + selector: 'website-tiny-layout-overview', + component: LayoutOverviewComponent + }, + + { + selector: 'website-tiny-layout-purchase-responsive-change', + component: LayoutPurchaseResponsiveChangeComponent + }, + + { + selector: 'website-tiny-layout-purchase-responsive', + component: LayoutPurchaseResponsiveComponent + }, + + { + selector: 'website-tiny-layout-purchase-simple-responsive', + component: LayoutPurchaseSimpleResponsiveComponent + }, + + { + selector: 'website-tiny-layout-purchase-simple', + component: LayoutPurchaseSimpleComponent + }, + + { + selector: 'website-tiny-layout-purchase', + component: LayoutPurchaseComponent + }, + + { selector: 'website-tiny-layout-single', component: LayoutSingleComponent }, + + { + selector: 'website-tiny-leftmenu-active-change', + component: LeftmenuActiveChangeWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-basic', + component: LeftmenuBasicWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-collapsed', + component: LeftmenuCollapsedWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-disabled', + component: LeftmenuDisabledWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-dividing', + component: LeftmenuDividingWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-foot', + component: LeftmenuFootWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-group', + component: LeftmenuGroupWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-href', + component: LeftmenuHrefWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-no-router', + component: LeftmenuNoRouterWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-params', + component: LeftmenuParamsWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-reload-state', + component: LeftmenuReloadStateWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-routerlist', + component: LeftmenuRouterlistWebsiteViewComponent + }, + + { + selector: 'website-tiny-leftmenu-toggleable', + component: LeftmenuToggleableWebsiteViewComponent + }, + + { selector: 'website-tiny-loading-area', component: LoadingAreaComponent }, + + { selector: 'website-tiny-loading-basic', component: LoadingBasicComponent }, + + { selector: 'website-tiny-loading-size', component: LoadingSizeComponent }, + + { selector: 'website-tiny-loading-type', component: LoadingTypeComponent }, + + { selector: 'website-tiny-locale-basic', component: LocaleBasicComponent }, + + { selector: 'website-tiny-locale-format', component: LocaleFormatComponent }, + + { selector: 'website-tiny-locale-reload', component: LocaleReloadComponent }, + + { selector: 'website-tiny-linkbutton-basic', component: LinkbuttonBasicComponent }, + + { selector: 'website-tiny-menu-basic', component: MenuBasicComponent }, + + { + selector: 'website-tiny-menu-beforeopen', + component: MenuBeforeopenComponent + }, + + { selector: 'website-tiny-menu-border', component: MenuBorderComponent }, + + { + selector: 'website-tiny-menu-buttoncolor', + component: MenuButtoncolorComponent + }, + + { selector: 'website-tiny-menu-default', component: MenuDefaultComponent }, + + { selector: 'website-tiny-menu-disabled', component: MenuDisabledComponent }, + + { selector: 'website-tiny-menu-event', component: MenuEventComponent }, + + { selector: 'website-tiny-menu-group', component: MenuGroupComponent }, + + { selector: 'website-tiny-menu-id', component: MenuIdComponent }, + + { selector: 'website-tiny-menu-labelkey', component: MenuLabelkeyComponent }, + + { + selector: 'website-tiny-menu-panelalign', + component: MenuPanelalignComponent + }, + + { + selector: 'website-tiny-menu-panelstyle', + component: MenuPanelstyleComponent + }, + + { + selector: 'website-tiny-menu-templete-test', + component: MenuTempleteTestComponent + }, + + { selector: 'website-tiny-menu-templete', component: MenuTempleteComponent }, + + { selector: 'website-tiny-menu-tips', component: MenuTipsComponent }, + + { selector: 'website-tiny-message-basic', component: MessageBasicComponent }, + + { + selector: 'website-tiny-message-btn-test', + component: MessageBtnTestComponent + }, + + { selector: 'website-tiny-message-btn', component: MessageBtnComponent }, + + { + selector: 'website-tiny-message-content', + component: MessageContentComponent + }, + + { selector: 'website-tiny-message-id', component: MessageIdComponent }, + + { + selector: 'website-tiny-message-security', + component: MessageSecurityComponent + }, + + { selector: 'website-tiny-message-title', component: MessageTitleComponent }, + + { selector: 'website-tiny-message-type', component: MessageTypeComponent }, + + { + selector: 'website-tiny-modal-animation', + component: ModalAnimationComponent + }, + + { + selector: 'website-tiny-modal-backdrop', + component: ModalBackdropComponent + }, + + { selector: 'website-tiny-modal-class', component: ModalClassComponent }, + + { + selector: 'website-tiny-modal-close-icon', + component: ModalCloseIconComponent + }, + + { + selector: 'website-tiny-modal-config-test', + component: ModalConfigTestComponent + }, + + { + selector: 'website-tiny-modal-content-comp', + component: ModalContentCompComponent + }, + + { + selector: 'website-tiny-modal-content-temp', + component: ModalContentTempComponent + }, + + { + selector: 'website-tiny-modal-draggable', + component: ModalDraggableComponent + }, + + { selector: 'website-tiny-modal-esc', component: ModalEscComponent }, + + { selector: 'website-tiny-modal-event', component: ModalEventComponent }, + + { + selector: 'website-tiny-modal-header-align', + component: ModalHeaderAlignComponent + }, + + { + selector: 'website-tiny-modal-header-style', + component: ModalHeaderStyleComponent + }, + + { + selector: 'website-tiny-modal-two-backdrop', + component: ModalTwoBackdropComponent + }, + + { selector: 'website-tiny-modal-two-test', component: ModalTwoTestComponent }, + + { selector: 'website-tiny-nav-active', component: NavActiveComponent }, + + { selector: 'website-tiny-nav-align', component: NavAlignComponent }, + + { selector: 'website-tiny-nav-basic', component: NavBasicComponent }, + + { selector: 'website-tiny-nav-disabled', component: NavDisabledComponent }, + + { selector: 'website-tiny-nav-event', component: NavEventComponent }, + + { selector: 'website-tiny-nav-left', component: NavLeftComponent }, + + { selector: 'website-tiny-nav-right', component: NavRightComponent }, + + { + selector: 'website-tiny-nav-selectable', + component: NavSelectableComponent + }, + + { selector: 'website-tiny-nav-submenu', component: NavSubmenuComponent }, + + { selector: 'website-tiny-nav-template', component: NavTemplateComponent }, + + { selector: 'website-tiny-nav-theme', component: NavThemeComponent }, + + { selector: 'website-tiny-nav-width', component: NavWidthComponent }, + + { + selector: 'website-tiny-notification-animation', + component: NotificationAnimationComponent + }, + + { + selector: 'website-tiny-notification-basic', + component: NotificationBasicComponent + }, + + { + selector: 'website-tiny-notification-close', + component: NotificationCloseComponent + }, + + { + selector: 'website-tiny-notification-config', + component: NotificationConfigComponent + }, + + { + selector: 'website-tiny-notification-duration', + component: NotificationDurationComponent + }, + + { + selector: 'website-tiny-notification-events', + component: NotificationEventsComponent + }, + + { + selector: 'website-tiny-notification-hover-pause', + component: NotificationHoverPauseComponent + }, + + { + selector: 'website-tiny-notification-name', + component: NotificationNameComponent + }, + + { + selector: 'website-tiny-notification-position', + component: NotificationPositionComponent + }, + + { + selector: 'website-tiny-notification-template', + component: NotificationTemplateComponent + }, + + { + selector: 'website-tiny-notification-type', + component: NotificationTypeComponent + }, + + { + selector: 'website-tiny-overflow-destory', + component: OverflowDestoryComponent + }, + + { + selector: 'website-tiny-overflow-directive', + component: OverflowDirectiveComponent + }, + + { + selector: 'website-tiny-overflow-maxline', + component: OverflowMaxlineComponent + }, + + { + selector: 'website-tiny-overflow-maxwidth', + component: OverflowMaxwidthComponent + }, + + { + selector: 'website-tiny-overflow-position', + component: OverflowPositionComponent + }, + + { + selector: 'website-tiny-overflow-service', + component: OverflowServiceComponent + }, + + { selector: 'website-tiny-overflow-test', component: OverflowTestComponent }, + + { + selector: 'website-tiny-overflow-tipcontent', + component: OverflowTipcontentComponent + }, + + { selector: 'website-tiny-phonenumber-basic', component: PhonenumberBasicComponent }, + + { selector: 'website-tiny-phonenumber-disabled', component: PhonenumberDisabledComponent }, + + { selector: 'website-tiny-phonenumber-event', component: PhonenumberEventComponent }, + + { selector: 'website-tiny-phonenumber-valid', component: PhonenumberValidComponent }, + + { selector: 'website-tiny-phonenumber-country', component: PhonenumberCountryComponent }, + + { + selector: 'website-tiny-pagination-autohide', + component: PaginationAutohideComponent + }, + + { + selector: 'website-tiny-pagination-disabled', + component: PaginationDisabledComponent + }, + + { + selector: 'website-tiny-pagination-event', + component: PaginationEventComponent + }, + + { + selector: 'website-tiny-pagination-fixed', + component: PaginationFixedComponent + }, + + { + selector: 'website-tiny-pagination-loading', + component: PaginationLoadingComponent + }, + + { + selector: 'website-tiny-pagination-pageselectwidth', + component: PaginationPageselectwidthComponent + }, + + { + selector: 'website-tiny-pagination-pagesize', + component: PaginationPagesizeComponent + }, + + { + selector: 'website-tiny-pagination-showgotolink', + component: PaginationShowgotolinkComponent + }, + + { + selector: 'website-tiny-pagination-showlastpage', + component: PaginationShowlastpageComponent + }, + + { + selector: 'website-tiny-pagination-showtotalnumber', + component: PaginationShowtotalnumberComponent + }, + + { + selector: 'website-tiny-pagination-type', + component: PaginationTypeComponent + }, + { selector: 'website-tiny-pathfield-items', component: PathfieldItemsComponent }, + + { selector: 'website-tiny-pathfield-ispanel', component: PathfieldIspanelComponent }, + + { selector: 'website-tiny-pathfield-panelwidth', component: PathfieldPanelwidthComponent }, + + { selector: 'website-tiny-pathfield-editable', component: PathfieldEditableComponent }, + + { selector: 'website-tiny-pathfield-event', component: PathfieldEventComponent }, + + { selector: 'website-tiny-path-list', component: PathListComponent }, + + { selector: 'website-tiny-path-select', component: PathSelectComponent }, + + { + selector: 'website-tiny-popconfirm-basic', + component: PopconfirmBasicComponent + }, + + { + selector: 'website-tiny-popconfirm-define', + component: PopconfirmDefineComponent + }, + + { + selector: 'website-tiny-popconfirm-event', + component: PopconfirmEventComponent + }, + + { + selector: 'website-tiny-popconfirm-table-define', + component: PopconfirmTableDefineComponent + }, + + { + selector: 'website-tiny-popconfirm-table', + component: PopconfirmTableComponent + }, + + { + selector: 'website-tiny-progressbar-animation', + component: ProgressbarAnimationComponent + }, + + { + selector: 'website-tiny-progressbar-basic', + component: ProgressbarBasicComponent + }, + + { + selector: 'website-tiny-progressbar-class', + component: ProgressbarClassComponent + }, + + { + selector: 'website-tiny-productpreview-basic', + component: ProductpreviewBasicComponent + }, + + { selector: 'website-tiny-radio-basic', component: RadioBasicComponent }, + + { selector: 'website-tiny-radio-dark', component: RadioDarkComponent }, + + { + selector: 'website-tiny-radio-disabled', + component: RadioDisabledComponent + }, + + { selector: 'website-tiny-radio-event', component: RadioEventComponent }, + + { selector: 'website-tiny-radio-focus', component: RadioFocusComponent }, + + { selector: 'website-tiny-radio-group', component: RadioGroupComponent }, + + { + selector: 'website-tiny-radio-group-direction', + component: RadioGroupDirectionComponent + }, + + { + selector: 'website-tiny-radio-group-labelkey', + component: RadioGroupLabelkeyComponent + }, + + { + selector: 'website-tiny-radio-group-linewrap', + component: RadioGroupLinewrapComponent + }, + + { + selector: 'website-tiny-radio-group-validation', + component: RadioGroupValidationComponent + }, + + { + selector: 'website-tiny-radio-group-valuekey', + component: RadioGroupValuekeyComponent + }, + + { selector: 'website-tiny-radio-label', component: RadioLabelComponent }, + + { selector: 'website-tiny-rate-basic', component: RateBasicComponent }, + + { selector: 'website-tiny-rate-disabled', component: RateDisabledComponent }, + + { selector: 'website-tiny-rate-event', component: RateEventComponent }, + + { selector: 'website-tiny-rate-id', component: RateIdComponent }, + + { selector: 'website-tiny-rate-load', component: RateLoadComponent }, + + { selector: 'website-tiny-rights-basic', component: RightsBasicComponent }, + + { selector: 'website-tiny-rights-type', component: RightsTypeComponent }, + + { + selector: 'website-tiny-score-basic', + component: ScoreBasicComponent + }, + + { + selector: 'website-tiny-score-events', + component: ScoreEventsComponent + }, + + { + selector: 'website-tiny-score-limittext', + component: ScoreLimittextComponent + }, + + { + selector: 'website-tiny-score-padding', + component: ScorePaddingComponent + }, + + { + selector: 'website-tiny-searchbox-appendtobody', + component: SearchboxAppendtobodyComponent + }, + + { + selector: 'website-tiny-searchbox-basic', + component: SearchboxBasicComponent + }, + + { + selector: 'website-tiny-searchbox-disabled', + component: SearchboxDisabledComponent + }, + + { + selector: 'website-tiny-searchbox-event', + component: SearchboxEventComponent + }, + + { + selector: 'website-tiny-searchbox-maxlength', + component: SearchboxMaxlengthComponent + }, + + { + selector: 'website-tiny-searchbox-notsearch', + component: SearchboxNotsearchComponent + }, + + { + selector: 'website-tiny-searchbox-options', + component: SearchboxOptionsComponent + }, + + { + selector: 'website-tiny-searchbox-panelsize', + component: SearchboxPanelsizeComponent + }, + + { + selector: 'website-tiny-searchbox-reactive', + component: SearchboxReactiveComponent + }, + + { + selector: 'website-tiny-searchbox-suggest', + component: SearchboxSuggestComponent + }, + + { + selector: 'website-tiny-searchbox-template', + component: SearchboxTemplateComponent + }, + + { + selector: 'website-tiny-searchbox-test', + component: SearchboxTestComponent + }, + + { + selector: 'website-tiny-searchbox-trimmed', + component: SearchboxTrimmedComponent + }, + + { + selector: 'website-tiny-searchbox-valid', + component: SearchboxValidComponent + }, + + { + selector: 'website-tiny-searchbox-virtualscroll', + component: SearchboxVirtualscrollComponent + }, + + { selector: 'website-tiny-selectgroup-basic', component: SelectgroupBasicComponent }, + + { selector: 'website-tiny-selectgroup-multiple', component: SelectgroupMultipleComponent }, + + { selector: 'website-tiny-selectgroup-valuekey', component: SelectgroupValuekeyComponent }, + + { selector: 'website-tiny-selectgroup-template', component: SelectgroupTemplateComponent }, + + { selector: 'website-tiny-selectgroup-select', component: SelectgroupSelectComponent }, + + { + selector: 'website-tiny-select-appendtobody', + component: SelectAppendtobodyComponent + }, + + { selector: 'website-tiny-select-basic', component: SelectBasicComponent }, + + { + selector: 'website-tiny-select-beforesearch-test', + component: SelectBeforesearchTestComponent + }, + + { + selector: 'website-tiny-select-beforesearch', + component: SelectBeforesearchComponent + }, + + { + selector: 'website-tiny-select-change-selectall', + component: SelectChangeSelectallComponent + }, + + { + selector: 'website-tiny-select-clearable', + component: SelectClearableComponent + }, + + { + selector: 'website-tiny-select-disabled', + component: SelectDisabledComponent + }, + + { + selector: 'website-tiny-select-disabledfocus', + component: SelectDisabledfocusComponent + }, + + { selector: 'website-tiny-select-enum', component: SelectEnumComponent }, + + { selector: 'website-tiny-select-event', component: SelectEventComponent }, + + { selector: 'website-tiny-select-focus', component: SelectFocusComponent }, + + { selector: 'website-tiny-select-group', component: SelectGroupComponent }, + + { selector: 'website-tiny-select-id', component: SelectIdComponent }, + + { selector: 'website-tiny-select-idkey', component: SelectIdkeyComponent }, + + { selector: 'website-tiny-select-input', component: SelectInputComponent }, + + { + selector: 'website-tiny-select-labelkey', + component: SelectLabelkeyComponent + }, + + { selector: 'website-tiny-select-lazy', component: SelectLazyComponent }, + + { selector: 'website-tiny-select-leak', component: SelectLeakComponent }, + + { selector: 'website-tiny-select-load', component: SelectLoadComponent }, + + { selector: 'website-tiny-select-many', component: SelectManyComponent }, + + { + selector: 'website-tiny-select-maxline', + component: SelectMaxlineComponent + }, + + { selector: 'website-tiny-select-much', component: SelectMuchComponent }, + + { selector: 'website-tiny-select-multi', component: SelectMultiComponent }, + + { + selector: 'website-tiny-select-noborder', + component: SelectNoborderComponent + }, + + { selector: 'website-tiny-select-nodata', component: SelectNodataComponent }, + + { + selector: 'website-tiny-select-noempty', + component: SelectNoemptyComponent + }, + + { selector: 'website-tiny-select-null', component: SelectNullComponent }, + + { + selector: 'website-tiny-select-pagin-beforesearch', + component: SelectPaginBeforesearchComponent + }, + + { + selector: 'website-tiny-select-pagination', + component: SelectPaginationComponent + }, + + { selector: 'website-tiny-select-panel', component: SelectPanelComponent }, + + { + selector: 'website-tiny-select-reservesearchword', + component: SelectReservesearchwordComponent + }, + + { + selector: 'website-tiny-select-scroll-load', + component: SelectScrollLoadComponent + }, + + { selector: 'website-tiny-select-search', component: SelectSearchComponent }, + + { + selector: 'website-tiny-select-searchkeys', + component: SelectSearchkeysComponent + }, + + { + selector: 'website-tiny-select-selectall', + component: SelectSelectallComponent + }, + + { + selector: 'website-tiny-select-showselectednumber', + component: SelectShowselectednumberComponent + }, + + { selector: 'website-tiny-select-small', component: SelectSmallComponent }, + + { selector: 'website-tiny-select-tag', component: SelectTagComponent }, + + { + selector: 'website-tiny-select-template', + component: SelectTemplateComponent + }, + + { selector: 'website-tiny-select-tip', component: SelectTipComponent }, + + { + selector: 'website-tiny-select-tiscroll', + component: SelectTiscrollComponent + }, + + { selector: 'website-tiny-select-tworow', component: SelectTworowComponent }, + + { selector: 'website-tiny-select-valid', component: SelectValidComponent }, + + { + selector: 'website-tiny-select-validgroup', + component: SelectValidgroupComponent + }, + + { + selector: 'website-tiny-select-valuekey-test', + component: SelectValuekeyTestComponent + }, + + { + selector: 'website-tiny-select-valuekey', + component: SelectValuekeyComponent + }, + + { + selector: 'website-tiny-select-virtualscroll-multi', + component: SelectVirtualscrollMultiComponent + }, + + { + selector: 'website-tiny-select-virtualscroll', + component: SelectVirtualscrollComponent + }, + + { selector: 'website-tiny-skeleton-page', component: SkeletonPageComponent }, + + { + selector: 'website-tiny-skeleton-title', + component: SkeletonTitleComponent + }, + + { selector: 'website-tiny-skeleton-type', component: SkeletonTypeComponent }, + + { selector: 'website-tiny-slider-event', component: SliderEventComponent }, + + { + selector: 'website-tiny-slider-formcontrol', + component: SliderFormcontrolComponent + }, + + { selector: 'website-tiny-slider-hidden', component: SliderHiddenComponent }, + + { selector: 'website-tiny-slider-limits', component: SliderLimitsComponent }, + + { selector: 'website-tiny-slider-ratios', component: SliderRatiosComponent }, + + { selector: 'website-tiny-slider-scales', component: SliderScalesComponent }, + + { + selector: 'website-tiny-slider-template', + component: SliderTemplateComponent + }, + + { selector: 'website-tiny-slider-tip', component: SliderTipComponent }, + + { + selector: 'website-tiny-spinner-basic-test', + component: SpinnerBasicTestComponent + }, + + { selector: 'website-tiny-spinner-basic', component: SpinnerBasicComponent }, + + { + selector: 'website-tiny-spinner-correctable', + component: SpinnerCorrectableComponent + }, + + { + selector: 'website-tiny-spinner-disabled', + component: SpinnerDisabledComponent + }, + + { selector: 'website-tiny-spinner-event', component: SpinnerEventComponent }, + + { + selector: 'website-tiny-spinner-format', + component: SpinnerFormatComponent + }, + + { selector: 'website-tiny-spinner-load', component: SpinnerLoadComponent }, + + { + selector: 'website-tiny-spinner-localeable', + component: SpinnerLocaleableComponent + }, + + { + selector: 'website-tiny-spinner-max-min', + component: SpinnerMaxMinComponent + }, + + { + selector: 'website-tiny-spinner-maxlength', + component: SpinnerMaxlengthComponent + }, + + { selector: 'website-tiny-spinner-step', component: SpinnerStepComponent }, + + { + selector: 'website-tiny-spinner-stepfn', + component: SpinnerStepfnComponent + }, + + { + selector: 'website-tiny-spinner-tip-test', + component: SpinnerTipTestComponent + }, + + { selector: 'website-tiny-spinner-tip', component: SpinnerTipComponent }, + + { + selector: 'website-tiny-spinner-validation-test', + component: SpinnerValidationTestComponent + }, + + { + selector: 'website-tiny-spinner-validation', + component: SpinnerValidationComponent + }, + + { selector: 'website-tiny-steps-active', component: StepsActiveComponent }, + + { + selector: 'website-tiny-steps-adaptive-test', + component: StepsAdaptiveTestComponent + }, + + { + selector: 'website-tiny-steps-adaptive', + component: StepsAdaptiveComponent + }, + + { selector: 'website-tiny-steps-base', component: StepsBaseComponent }, + + { selector: 'website-tiny-steps-before', component: StepsBeforeComponent }, + + { + selector: 'website-tiny-steps-clickable', + component: StepsClickableComponent + }, + + { selector: 'website-tiny-steps-events', component: StepsEventsComponent }, + + { selector: 'website-tiny-steps-label', component: StepsLabelComponent }, + + { + selector: 'website-tiny-steps-maxwidth', + component: StepsMaxwidthComponent + }, + + { + selector: 'website-tiny-steps-template', + component: StepsTemplateComponent + }, + + { + selector: 'website-tiny-subtitle-basic', + component: SubtitleBasicComponent + }, + + { + selector: 'website-tiny-subtitle-before-search', + component: SubtitleBeforeSearchComponent + }, + + { selector: 'website-tiny-subtitle-dark', component: SubtitleDarkComponent }, + + { + selector: 'website-tiny-subtitle-event', + component: SubtitleEventComponent + }, + + { + selector: 'website-tiny-subtitle-idkey', + component: SubtitleIdkeyComponent + }, + + { + selector: 'website-tiny-subtitle-items', + component: SubtitleItemsComponent + }, + + { + selector: 'website-tiny-subtitle-maxwidth', + component: SubtitleMaxwidthComponent + }, + + { + selector: 'website-tiny-subtitle-panelwidth', + component: SubtitlePanelwidthComponent + }, + + { + selector: 'website-tiny-subtitle-route', + component: SubtitleRouteComponent + }, + + { + selector: 'website-tiny-subtitle-scroll-load', + component: SubtitleScrollLoadComponent + }, + + { + selector: 'website-tiny-subtitle-searchable', + component: SubtitleSearchableComponent + }, + + { + selector: 'website-tiny-subtitle-target', + component: SubtitleTargetComponent + }, + + { + selector: 'website-tiny-subtitle-tip-position', + component: SubtitleTipPositionComponent + }, + + { + selector: 'website-tiny-swiper-activeindex', + component: SwiperActiveindexComponent + }, + + { + selector: 'website-tiny-swiper-autoplay', + component: SwiperAutoplayComponent + }, + + { selector: 'website-tiny-swiper-basic', component: SwiperBasicComponent }, + + { selector: 'website-tiny-swiper-events', component: SwiperEventsComponent }, + + { + selector: 'website-tiny-swiper-indicatorposition', + component: SwiperIndicatorpositionComponent + }, + + { selector: 'website-tiny-swiper-loop', component: SwiperLoopComponent }, + + { + selector: 'website-tiny-swiper-showcardnum-test', + component: SwiperShowcardnumTestComponent + }, + + { + selector: 'website-tiny-swiper-showcardnum', + component: SwiperShowcardnumComponent + }, + + { selector: 'website-tiny-switch-basic', component: SwitchBasicComponent }, + + { selector: 'website-tiny-switch-before', component: SwitchBeforeComponent }, + + { + selector: 'website-tiny-switch-disabled', + component: SwitchDisabledComponent + }, + + { selector: 'website-tiny-switch-event', component: SwitchEventComponent }, + + { + selector: 'website-tiny-switch-explanation', + component: SwitchExplanationComponent + }, + + { selector: 'website-tiny-switch-focus', component: SwitchFocusComponent }, + + { selector: 'website-tiny-switch-id', component: SwitchIdComponent }, + + { selector: 'website-tiny-switch-load', component: SwitchLoadComponent }, + + { + selector: 'website-tiny-switch-template', + component: SwitchTemplateComponent + }, + + { selector: 'website-tiny-tab-basic', component: TabBasicComponent }, + + { + selector: 'website-tiny-tab-beforeactivechange', + component: TabBeforeactivechangeComponent + }, + + { + selector: 'website-tiny-tab-content-comp', + component: TabContentCompComponent + }, + + { + selector: 'website-tiny-tab-custom-head', + component: TabCustomHeadComponent + }, + + { selector: 'website-tiny-tab-dark', component: TabDarkComponent }, + + { + selector: 'website-tiny-tab-default-test', + component: TabDefaultTestComponent + }, + + { selector: 'website-tiny-tab-lazy-load', component: TabLazyLoadComponent }, + + { + selector: 'website-tiny-tab-level2-test', + component: TabLevel2TestComponent + }, + + { selector: 'website-tiny-tab-level2', component: TabLevel2Component }, + + { selector: 'website-tiny-tab-overflow', component: TabOverflowComponent }, + + { selector: 'website-tiny-tab-route', component: TabRouteComponent }, + + { selector: 'website-tiny-tab-scroll', component: TabScrollComponent }, + + { selector: 'website-tiny-tab-small', component: TabSmallComponent }, + + { + selector: 'website-tiny-table-actionmenu', + component: TableActionmenuComponent + }, + + { + selector: 'website-tiny-table-basic-test', + component: TableBasicTestComponent + }, + + { selector: 'website-tiny-table-basic', component: TableBasicComponent }, + + { selector: 'website-tiny-table-cell-tip', component: TableCellTipComponent }, + + { + selector: 'website-tiny-table-cellicons-colsresizable', + component: TableCelliconsColsresizableComponent + }, + + { + selector: 'website-tiny-table-checkbox-pagination-headmenu', + component: TableCheckboxPaginationHeadmenuComponent + }, + + { + selector: 'website-tiny-table-checkbox-pagination', + component: TableCheckboxPaginationComponent + }, + + { + selector: 'website-tiny-table-checkbox', + component: TableCheckboxComponent + }, + + { + selector: 'website-tiny-table-col-align', + component: TableColAlignComponent + }, + + { + selector: 'website-tiny-table-colalign-sort-resizable-test', + component: TableColalignSortResizableTestComponent + }, + + { + selector: 'website-tiny-table-cols-resizable', + component: TableColsResizableComponent + }, + + { + selector: 'website-tiny-table-cols-toggle-details', + component: TableColsToggleDetailsComponent + }, + + { + selector: 'website-tiny-table-cols-toggle-test', + component: TableColsToggleTestComponent + }, + + { + selector: 'website-tiny-table-cols-toggle', + component: TableColsToggleComponent + }, + + { + selector: 'website-tiny-table-colsresizable-basic', + component: TableColsresizableBasicComponent + }, + + { + selector: 'website-tiny-table-colsresizable-colstoggle-fixedhead', + component: TableColsresizableColstoggleFixedheadComponent + }, + + { + selector: 'website-tiny-table-colsresizable-colstoggle', + component: TableColsresizableColstoggleComponent + }, + + { + selector: 'website-tiny-table-colsresizable-loadfail', + component: TableColsresizableLoadfailComponent + }, + + { + selector: 'website-tiny-table-colsresizable-sort-headfilter', + component: TableColsresizableSortHeadfilterComponent + }, + + { + selector: 'website-tiny-table-colsresizable-sort', + component: TableColsresizableSortComponent + }, + + { + selector: 'website-tiny-table-column-fixed', + component: TableColumnFixedComponent + }, + + { + selector: 'website-tiny-table-columnfixed-checkbox', + component: TableColumnfixedCheckboxComponent + }, + + { + selector: 'website-tiny-table-columnfixed-colstoggle', + component: TableColumnfixedColstoggleComponent + }, + + { + selector: 'website-tiny-table-columnfixed-editrow', + component: TableColumnfixedEditrowComponent + }, + + { + selector: 'website-tiny-table-columnfixed-fixedhead-colsresizable-pagination', + component: TableColumnfixedFixedheadColsresizablePaginationComponent + }, + + { + selector: 'website-tiny-table-columnfixed-headfixed', + component: TableColumnfixedHeadfixedComponent + }, + + { + selector: 'website-tiny-table-columnfixed-leftmenu', + component: TableColumnfixedLeftmenuComponent + }, + + { + selector: 'website-tiny-table-columnfixed-nodata', + component: TableColumnfixedNodataComponent + }, + + { + selector: 'website-tiny-table-columnfixed-pagination', + component: TableColumnfixedPaginationComponent + }, + + { + selector: 'website-tiny-table-columnfixed-resizable', + component: TableColumnfixedResizableComponent + }, + + { + selector: 'website-tiny-table-comprehensive', + component: TableComprehensiveComponent + }, + + { + selector: 'website-tiny-table-details-closeotherdetails', + component: TableDetailsCloseotherdetailsComponent + }, + + { + selector: 'website-tiny-table-details-nesttable', + component: TableDetailsNesttableComponent + }, + + { + selector: 'website-tiny-table-details-pagination', + component: TableDetailsPaginationComponent + }, + + { selector: 'website-tiny-table-details', component: TableDetailsComponent }, + + { + selector: 'website-tiny-table-dynamic-details', + component: TableDynamicDetailsComponent + }, + + { + selector: 'website-tiny-table-editall-test', + component: TableEditallTestComponent + }, + + { selector: 'website-tiny-table-editall', component: TableEditallComponent }, + + { + selector: 'website-tiny-table-editrow-test', + component: TableEditrowTestComponent + }, + + { selector: 'website-tiny-table-editrow', component: TableEditrowComponent }, + + { + selector: 'website-tiny-table-filter-strict', + component: TableFilterStrictComponent + }, + + { selector: 'website-tiny-table-filter', component: TableFilterComponent }, + + { + selector: 'website-tiny-table-fixed-head-cols-resizable', + component: TableFixedHeadColsResizableComponent + }, + + { + selector: 'website-tiny-table-fixed-head-in-accordion', + component: TableFixedHeadInAccordionComponent + }, + + { + selector: 'website-tiny-table-fixed-head-nodata', + component: TableFixedHeadNodataComponent + }, + + { + selector: 'website-tiny-table-fixed-head-pagination-details', + component: TableFixedHeadPaginationDetailsComponent + }, + + { + selector: 'website-tiny-table-fixed-head', + component: TableFixedHeadComponent + }, + + { + selector: 'website-tiny-table-fixedhead-colsresizable-pagination-details', + component: TableFixedheadColsresizablePaginationDetailsComponent + }, + + { + selector: 'website-tiny-table-fixhead-scroll', + component: TableFixheadScrollComponent + }, + + { selector: 'website-tiny-table-group', component: TableGroupComponent }, + + { selector: 'website-tiny-table-guide', component: TableGuideComponent }, + + { + selector: 'website-tiny-table-head-filter-datetime-test', + component: TableHeadFilterDatetimeTestComponent + }, + + { + selector: 'website-tiny-table-head-filter-datetime', + component: TableHeadFilterDatetimeComponent + }, + + { + selector: 'website-tiny-table-head-filter-multi-valuekey', + component: TableHeadFilterMultiValuekeyComponent + }, + + { + selector: 'website-tiny-table-head-filter-multi', + component: TableHeadFilterMultiComponent + }, + + { + selector: 'website-tiny-table-head-filter-test', + component: TableHeadFilterTestComponent + }, + + { + selector: 'website-tiny-table-head-filter-valuekey', + component: TableHeadFilterValuekeyComponent + }, + + { + selector: 'website-tiny-table-head-filter-virtualscroll', + component: TableHeadFilterVirtualscrollComponent + }, + + { + selector: 'website-tiny-table-head-filter', + component: TableHeadFilterComponent + }, + + { + selector: 'website-tiny-table-load-fail', + component: TableLoadFailComponent + }, + + { + selector: 'website-tiny-table-nodata-simple', + component: TableNodataSimpleComponent + }, + + { + selector: 'website-tiny-table-nodata-test', + component: TableNodataTestComponent + }, + + { selector: 'website-tiny-table-nodata', component: TableNodataComponent }, + + { + selector: 'website-tiny-table-overflow-link', + component: TableOverflowLinkComponent + }, + + { + selector: 'website-tiny-table-pagi-with-filter', + component: TablePagiWithFilterComponent + }, + + { + selector: 'website-tiny-table-pagination', + component: TablePaginationComponent + }, + + { + selector: 'website-tiny-table-radio-test', + component: TableRadioTestComponent + }, + + { selector: 'website-tiny-table-radio', component: TableRadioComponent }, + + { + selector: 'website-tiny-table-row-drag2', + component: TableRowDrag2Component + }, + + { selector: 'website-tiny-table-rowspan', component: TableRowspanComponent }, + + { selector: 'website-tiny-table-search', component: TableSearchComponent }, + + { + selector: 'website-tiny-table-server-pagi-search-sort', + component: TableServerPagiSearchSortComponent + }, + + { + selector: 'website-tiny-table-server-pagi', + component: TableServerPagiComponent + }, + + { selector: 'website-tiny-table-small', component: TableSmallComponent }, + + { selector: 'website-tiny-table-soldout', component: TableSoldoutComponent }, + + { + selector: 'website-tiny-table-sort-basic', + component: TableSortBasicComponent + }, + + { + selector: 'website-tiny-table-sort-comparefn-locale', + component: TableSortComparefnLocaleComponent + }, + + { + selector: 'website-tiny-table-sort-comparefn', + component: TableSortComparefnComponent + }, + + { + selector: 'website-tiny-table-sort-details', + component: TableSortDetailsComponent + }, + + { + selector: 'website-tiny-table-sort-reset', + component: TableSortResetComponent + }, + + { + selector: 'website-tiny-table-sort-test', + component: TableSortTestComponent + }, + + { selector: 'website-tiny-table-sort', component: TableSortComponent }, + + { + selector: 'website-tiny-table-storage-config', + component: TableStorageConfigComponent + }, + + { + selector: 'website-tiny-table-storage-filter', + component: TableStorageFilterComponent + }, + + { + selector: 'website-tiny-table-storage-serve', + component: TableStorageServeComponent + }, + + { selector: 'website-tiny-table-storage', component: TableStorageComponent }, + + { + selector: 'website-tiny-table-tree-mulitiselect', + component: TableTreeMulitiselectComponent + }, + + { + selector: 'website-tiny-table-tree-unknowdeepth', + component: TableTreeUnknowdeepthComponent + }, + + { selector: 'website-tiny-table-tree', component: TableTreeComponent }, + + { + selector: 'website-tiny-table-virtualscroll-basic', + component: TableVirtualscrollBasicComponent + }, + + { + selector: 'website-tiny-table-virtualscroll-comprehensive', + component: TableVirtualscrollComprehensiveComponent + }, + + { + selector: 'website-tiny-table-virtualscroll-sizes', + component: TableVirtualscrollSizesComponent + }, + + { + selector: 'website-tiny-table-virtualscroll-tree', + component: TableVirtualscrollTreeComponent + }, + + { + selector: 'website-tiny-table-virtualscroll', + component: TableVirtualscrollComponent + }, + + { selector: 'website-tiny-tag-basic', component: TagBasicComponent }, + + { selector: 'website-tiny-tag-default', component: TagDefaultComponent }, + + { selector: 'website-tiny-tag-disabled', component: TagDisabledComponent }, + + { selector: 'website-tiny-tag-edit', component: TagEditComponent }, + + { + selector: 'website-tiny-tagsinput-basic', + component: TagsinputBasicComponent + }, + + { + selector: 'website-tiny-tagsinput-disabled', + component: TagsinputDisabledComponent + }, + + { + selector: 'website-tiny-tagsinput-events', + component: TagsinputEventsComponent + }, + + { + selector: 'website-tiny-tagsinput-labelkey', + component: TagsinputLabelkeyComponent + }, + + { + selector: 'website-tiny-tagsinput-null', + component: TagsinputNullComponent + }, + + { + selector: 'website-tiny-tagsinput-panelwidth', + component: TagsinputPanelwidthComponent + }, + + { + selector: 'website-tiny-tagsinput-separators', + component: TagsinputSeparatorsComponent + }, + + { + selector: 'website-tiny-tagsinput-reactive', + component: TagsinputReactiveComponent + }, + + { + selector: 'website-tiny-tagsinput-suggestion', + component: TagsinputSuggestionComponent + }, + + { + selector: 'website-tiny-tagsinput-template', + component: TagsinputTemplateComponent + }, + + { + selector: 'website-tiny-tagsinput-valid', + component: TagsinputValidComponent + }, + + { + selector: 'website-tiny-tagsinput-valuekey', + component: TagsinputValuekeyComponent + }, + + { + selector: 'website-tiny-tagsinput-maxlength', + component: TagsinputMaxlengthComponent + }, + + { selector: 'website-tiny-text-basic', component: TextBasicComponent }, + + { selector: 'website-tiny-text-clear', component: TextClearComponent }, + + { selector: 'website-tiny-text-disabled', component: TextDisabledComponent }, + + { selector: 'website-tiny-text-events', component: TextEventsComponent }, + + { selector: 'website-tiny-text-focus', component: TextFocusComponent }, + + { + selector: 'website-tiny-text-maskinput', + component: TextMaskinputComponent + }, + + { + selector: 'website-tiny-text-noborder-test', + component: TextNoborderTestComponent + }, + + { + selector: 'website-tiny-text-password-visible', + component: TextPasswordVisibleComponent + }, + + { selector: 'website-tiny-text-password', component: TextPasswordComponent }, + + { selector: 'website-tiny-text-reactive', component: TextReactiveComponent }, + + { selector: 'website-tiny-text-readonly', component: TextReadonlyComponent }, + + { + selector: 'website-tiny-textarea-autofocus', + component: TextareaAutofocusComponent + }, + + { + selector: 'website-tiny-textarea-disabled', + component: TextareaDisabledComponent + }, + + { + selector: 'website-tiny-textarea-maxlength', + component: TextareaMaxlengthComponent + }, + + { selector: 'website-tiny-textarea-none', component: TextareaNoneComponent }, + + { + selector: 'website-tiny-textarea-resize', + component: TextareaResizeComponent + }, + + { + selector: 'website-tiny-textarea-scroll', + component: TextareaScrollComponent + }, + + { + selector: 'website-tiny-textarea-valid', + component: TextareaValidComponent + }, + + { + selector: 'website-tiny-textarea-width', + component: TextareaWidthComponent + }, + + { + selector: 'website-tiny-time-clearicon', + component: TimeCleariconComponent + }, + + { selector: 'website-tiny-time-disabled', component: TimeDisabledComponent }, + + { selector: 'website-tiny-time-event', component: TimeEventComponent }, + + { selector: 'website-tiny-time-format', component: TimeFormatComponent }, + + { selector: 'website-tiny-time-max', component: TimeMaxComponent }, + + { selector: 'website-tiny-time-maxmin', component: TimeMaxminComponent }, + + { selector: 'website-tiny-time-min', component: TimeMinComponent }, + + { + selector: 'website-tiny-time-option-disabled', + component: TimeOptionDisabledComponent + }, + + { + selector: 'website-tiny-time-panelalign', + component: TimePanelalignComponent + }, + + { selector: 'website-tiny-time-reactive', component: TimeReactiveComponent }, + + { + selector: 'website-tiny-time-validation', + component: TimeValidationComponent + }, + + { + selector: 'website-tiny-timeline-basic', + component: TimelineBasicComponent + }, + + { selector: 'website-tiny-timeline-dark', component: TimelineDarkComponent }, + + { + selector: 'website-tiny-timeline-helptip', + component: TimelineHelptipComponent + }, + + { + selector: 'website-tiny-timeline-multi', + component: TimelineMultiComponent + }, + + { + selector: 'website-tiny-timeline-templete', + component: TimelineTempleteComponent + }, + + { selector: 'website-tiny-timeline-test', component: TimelineTestComponent }, + + { selector: 'website-tiny-timeline-type', component: TimelineTypeComponent }, + + { selector: 'website-tiny-tip-basic', component: TipBasicComponent }, + + { + selector: 'website-tiny-tip-content-comp', + component: TipContentCompComponent + }, + + { + selector: 'website-tiny-tip-content-template', + component: TipContentTemplateComponent + }, + + { selector: 'website-tiny-tip-empty', component: TipEmptyComponent }, + + { selector: 'website-tiny-tip-has-arrow', component: TipHasArrowComponent }, + + { + selector: 'website-tiny-tip-long-text-position', + component: TipLongTextPositionComponent + }, + + { selector: 'website-tiny-tip-max-width', component: TipMaxWidthComponent }, + + { + selector: 'website-tiny-tip-position-test', + component: TipPositionTestComponent + }, + + { selector: 'website-tiny-tip-position', component: TipPositionComponent }, + + { + selector: 'website-tiny-tip-service-destroy', + component: TipServiceDestroyComponent + }, + + { selector: 'website-tiny-tip-service', component: TipServiceComponent }, + + { selector: 'website-tiny-tip-trigger', component: TipTriggerComponent }, + + { + selector: 'website-tiny-tip-valid-position-test', + component: TipValidPositionTestComponent + }, + + { selector: 'website-tiny-tip-zindex', component: TipZindexComponent }, + + { + selector: 'website-tiny-transfer-basic', + component: TransferBasicComponent + }, + + { + selector: 'website-tiny-transfer-disabled', + component: TransferDisabledComponent + }, + + { + selector: 'website-tiny-transfer-event', + component: TransferEventComponent + }, + + { selector: 'website-tiny-transfer-id', component: TransferIdComponent }, + + { + selector: 'website-tiny-transfer-idkey', + component: TransferIdkeyComponent + }, + + { + selector: 'website-tiny-transfer-labelkey', + component: TransferLabelkeyComponent + }, + + { selector: 'website-tiny-transfer-lazy', component: TransferLazyComponent }, + + { selector: 'website-tiny-transfer-load', component: TransferLoadComponent }, + + { + selector: 'website-tiny-transfer-nodatatext', + component: TransferNodatatextComponent + }, + + { + selector: 'website-tiny-transfer-pagination', + component: TransferPaginationComponent + }, + + { + selector: 'website-tiny-transfer-placeholder', + component: TransferPlaceholderComponent + }, + + { + selector: 'website-tiny-transfer-searchable', + component: TransferSearchableComponent + }, + + { + selector: 'website-tiny-transfer-searchkeys', + component: TransferSearchkeysComponent + }, + + { selector: 'website-tiny-transfer-size', component: TransferSizeComponent }, + + { + selector: 'website-tiny-transfer-table', + component: TransferTableComponent + }, + + { + selector: 'website-tiny-transfer-titles', + component: TransferTitlesComponent + }, + + { + selector: 'website-tiny-tree-before-expand', + component: TreeBeforeExpandComponent + }, + + { + selector: 'website-tiny-tree-before-more', + component: TreeBeforeMoreComponent + }, + + { + selector: 'website-tiny-tree-changedbycheckbox', + component: TreeChangedbycheckboxComponent + }, + + { + selector: 'website-tiny-tree-check-relation', + component: TreeCheckRelationComponent + }, + + { selector: 'website-tiny-tree-disabled', component: TreeDisabledComponent }, + + { + selector: 'website-tiny-tree-drag-beforedrop', + component: TreeDragBeforedropComponent + }, + + { selector: 'website-tiny-tree-drag', component: TreeDragComponent }, + + { selector: 'website-tiny-tree-event', component: TreeEventComponent }, + + { selector: 'website-tiny-tree-icon', component: TreeIconComponent }, + + { selector: 'website-tiny-tree-load', component: TreeLoadComponent }, + + { selector: 'website-tiny-tree-many', component: TreeManyComponent }, + + { + selector: 'website-tiny-tree-multiselect', + component: TreeMultiselectComponent + }, + + { selector: 'website-tiny-tree-operate', component: TreeOperateComponent }, + + { + selector: 'website-tiny-tree-parentcheckable', + component: TreeParentcheckableComponent + }, + + { + selector: 'website-tiny-tree-radioselect', + component: TreeRadioselectComponent + }, + + { selector: 'website-tiny-tree-search', component: TreeSearchComponent }, + + { + selector: 'website-tiny-tree-shortcutkey', + component: TreeShortcutkeyComponent + }, + + { selector: 'website-tiny-tree-small', component: TreeSmallComponent }, + + { selector: 'website-tiny-tree-template', component: TreeTemplateComponent }, + + { selector: 'website-tiny-tree-util', component: TreeUtilComponent }, + + { + selector: 'website-tiny-tree-virtualscroll-drag', + component: TreeVirtualscrollDragComponent + }, + + { + selector: 'website-tiny-tree-virtualscroll-small', + component: TreeVirtualscrollSmallComponent + }, + + { + selector: 'website-tiny-tree-virtualscroll', + component: TreeVirtualscrollComponent + }, + + { + selector: 'website-tiny-treeselect-basic', + component: TreeselectBasicComponent + }, + + { + selector: 'website-tiny-treeselect-before-expand', + component: TreeselectBeforeExpandComponent + }, + + { + selector: 'website-tiny-treeselect-before-more', + component: TreeselectBeforeMoreComponent + }, + + { + selector: 'website-tiny-treeselect-clearable', + component: TreeselectClearableComponent + }, + + { + selector: 'website-tiny-treeselect-disabled', + component: TreeselectDisabledComponent + }, + + { + selector: 'website-tiny-treeselect-dropmaxheight', + component: TreeselectDropmaxheightComponent + }, + + { + selector: 'website-tiny-treeselect-event', + component: TreeselectEventComponent + }, + + { + selector: 'website-tiny-treeselect-focus', + component: TreeselectFocusComponent + }, + + { + selector: 'website-tiny-treeselect-labelkey', + component: TreeselectLabelkeyComponent + }, + + { + selector: 'website-tiny-treeselect-lazyload', + component: TreeselectLazyloadComponent + }, + + { + selector: 'website-tiny-treeselect-load', + component: TreeselectLoadComponent + }, + + { + selector: 'website-tiny-treeselect-maxline', + component: TreeselectMaxlineComponent + }, + + { + selector: 'website-tiny-treeselect-modal', + component: TreeselectModalComponent + }, + + { + selector: 'website-tiny-treeselect-multi', + component: TreeselectMultiComponent + }, + + { + selector: 'website-tiny-treeselect-nodata', + component: TreeselectNodataComponent + }, + + { + selector: 'website-tiny-treeselect-options-change', + component: TreeselectOptionsChangeComponent + }, + + { + selector: 'website-tiny-treeselect-panelwidth', + component: TreeselectPanelwidthComponent + }, + + { + selector: 'website-tiny-treeselect-search', + component: TreeselectSearchComponent + }, + + { + selector: 'website-tiny-treeselect-selectall', + component: TreeselectSelectallComponent + }, + + { + selector: 'website-tiny-treeselect-validation', + component: TreeselectValidationComponent + }, + + { + selector: 'website-tiny-upload-auto-upload', + component: UploadAutoUploadComponent + }, + + { selector: 'website-tiny-upload-basic', component: UploadBasicComponent }, + + { + selector: 'website-tiny-upload-batch-send', + component: UploadBatchSendComponent + }, + + { + selector: 'website-tiny-upload-beforeremove', + component: UploadBeforeremoveComponent + }, + + { + selector: 'website-tiny-upload-button-test', + component: UploadButtonTestComponent + }, + + { selector: 'website-tiny-upload-button', component: UploadButtonComponent }, + + { + selector: 'website-tiny-upload-case-test', + component: UploadCaseTestComponent + }, + + { + selector: 'website-tiny-upload-changes', + component: UploadChangesComponent + }, + + { + selector: 'website-tiny-upload-chunksize', + component: UploadChunksizeComponent + }, + + { selector: 'website-tiny-upload-custom', component: UploadCustomComponent }, + + { selector: 'website-tiny-upload-event', component: UploadEventComponent }, + + { selector: 'website-tiny-upload-filter', component: UploadFilterComponent }, + + { + selector: 'website-tiny-upload-form-data', + component: UploadFormDataComponent + }, + + { + selector: 'website-tiny-upload-initfiles-test', + component: UploadInitfilesTestComponent + }, + + { + selector: 'website-tiny-upload-input-field-test', + component: UploadInputFieldTestComponent + }, + + { selector: 'website-tiny-upload-props', component: UploadPropsComponent }, + + { + selector: 'website-tiny-upload-service-test', + component: UploadServiceTestComponent + }, + + { + selector: 'website-tiny-upload-service', + component: UploadServiceComponent + }, + + { selector: 'website-tiny-upload-single', component: UploadSingleComponent }, + + { + selector: 'website-tiny-uploadimage-auto-upload', + component: UploadimageAutoUploadComponent + }, + + { + selector: 'website-tiny-uploadimage-basic', + component: UploadimageBasicComponent + }, + + { + selector: 'website-tiny-uploadimage-changes', + component: UploadimageChangesComponent + }, + + { + selector: 'website-tiny-uploadimage-deletable', + component: UploadimageDeletableComponent + }, + + { + selector: 'website-tiny-uploadimage-disabled', + component: UploadimageDisabledComponent + }, + + { + selector: 'website-tiny-uploadimage-drag', + component: UploadimageDragComponent + }, + + { + selector: 'website-tiny-uploadimage-event', + component: UploadimageEventComponent + }, + + { + selector: 'website-tiny-uploadimage-filter', + component: UploadimageFilterComponent + }, + + { + selector: 'website-tiny-uploadimage-initfiles', + component: UploadimageInitfilesComponent + }, + + { + selector: 'website-tiny-uploadimage-maxcount', + component: UploadimageMaxcountComponent + }, + + { + selector: 'website-tiny-uploadimage-template', + component: UploadimageTemplateComponent + }, + + { selector: 'website-tiny-browser-usage', component: BrowserUsageComponent }, + + { selector: 'website-tiny-keymap-usage', component: KeymapUsageComponent }, + + { selector: 'website-tiny-log-usage', component: LogUsageComponent }, + + { selector: 'website-tiny-theme-basic', component: ThemeBasicComponent }, + + { + selector: 'website-tiny-validation-async-check-test', + component: ValidationAsyncCheckTestComponent + }, + + { + selector: 'website-tiny-validation-async-check', + component: ValidationAsyncCheckComponent + }, + + { + selector: 'website-tiny-validation-basic-control', + component: ValidationBasicControlComponent + }, + + { + selector: 'website-tiny-validation-basic-directive', + component: ValidationBasicDirectiveComponent + }, + + { + selector: 'website-tiny-validation-blur-check', + component: ValidationBlurCheckComponent + }, + + { + selector: 'website-tiny-validation-error-msg', + component: ValidationErrorMsgComponent + }, + + { + selector: 'website-tiny-validation-form-group-config', + component: ValidationFormGroupConfigComponent + }, + + { + selector: 'website-tiny-validation-form-group-test', + component: ValidationFormGroupTestComponent + }, + + { + selector: 'website-tiny-validation-form-group', + component: ValidationFormGroupComponent + }, + + { + selector: 'website-tiny-validation-param-change', + component: ValidationParamChangeComponent + }, + + { + selector: 'website-tiny-validation-pwd-check', + component: ValidationPwdCheckComponent + }, + + { + selector: 'website-tiny-validation-rules-custom-directive', + component: ValidationRulesCustomDirectiveComponent + }, + + { + selector: 'website-tiny-validation-rules-custom', + component: ValidationRulesCustomComponent + }, + + { + selector: 'website-tiny-validation-rules-test', + component: ValidationRulesTestComponent + }, + + { + selector: 'website-tiny-validation-template-form-nested', + component: ValidationTemplateFormNestedComponent + }, + + { + selector: 'website-tiny-validation-tip', + component: ValidationTipComponent + }, + + { + selector: 'website-tiny-validation-tiscroll', + component: ValidationTiscrollComponent + } +]; + +@NgModule({ + imports: [ + DemoModules.allModules, + HttpClientModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule // 因为modal,message,collapse,accordion组件使用了angular动画,所以需要引入此模块 + ], + declarations: [AppComponent], + bootstrap: [AppComponent] +}) +export class AppModule { + constructor(private injector: Injector) { + // 国际化功能开启,测试其它语种的显示,可打开这段注释 + // 设置当前语言,默认为中文 + // TiLocale.setLocale(TiLocale.EN_US); // 设置国际化语种 + for (const item of WCS) { + if (!customElements.get(item.selector)) { + const el: any = createCustomElement(item.component, { + injector: this.injector + }); + customElements.define(item.selector, el); + } + } + } + + ngDoBootstrap(): void {} +} diff --git a/src/ng/demo/src/app/DemoModules.ts b/src/ng/demo/src/app/DemoModules.ts new file mode 100644 index 0000000..95ae083 --- /dev/null +++ b/src/ng/demo/src/app/DemoModules.ts @@ -0,0 +1,204 @@ +import { NgModule } from '@angular/core'; +// 以下按照字母排序 + +import { AccordionTestModule } from '../../../../accordion/demo/src/app/accordion/AccordionTestModule'; +import { ActionmenuTestModule } from '../../../../actionmenu/demo/src/app/actionmenu/ActionmenuTestModule'; +import { AlertTestModule } from '../../../../alert/demo/src/app/alert/AlertTestModule'; +import { AnchorTestModule } from '../../../../anchor/demo/src/app/anchor/AnchorTestModule'; +import { AutocompleteTestModule } from '../../../../autocomplete/demo/src/app/autocomplete/AutocompleteTestModule'; +import { AvatarTestModule } from '../../../../avatar/demo/src/app/avatar/AvatarTestModule'; +import { ButtonTestModule } from '../../../../button/demo/src/app/button/ButtonTestModule'; +import { ButtonGroupTestModule } from '../../../../buttongroup/demo/src/app/buttongroup/ButtonGroupTestModule'; +import { ButtonselectTestModule } from '../../../../buttonselect/demo/src/app/buttonselect/ButtonselectTestModule'; +import { CardTestModule } from '../../../../card/demo/src/app/card/CardTestModule'; +import { CascaderTestModule } from '../../../../cascader/demo/src/app/cascader/CascaderTestModule'; +import { CheckboxTestModule } from '../../../../checkbox/demo/src/app/checkbox/CheckboxTestModule'; +import { CollapseTestModule } from '../../../../collapse/demo/src/app/collapse/CollapseTestModule'; +import { CollapseboxTestModule } from '../../../../collapsebox/demo/src/app/collapsebox/CollapseboxTestModule'; +import { CollapsebuttonTestModule } from '../../../../collapsebutton/demo/src/app/collapsebutton/CollapsebuttonTestModule'; +import { CollapsetextTestModule } from '../../../../collapsetext/demo/src/app/collapsetext/CollapsetextTestModule'; +import { CopyTestModule } from '../../../../copy/demo/src/app/copy/CopyTestModule'; +import { CrumbTestModule } from '../../../../crumb/demo/src/app/crumb/CrumbTestModule'; +import { DateTestModule } from '../../../../date/demo/src/app/date/DateTestModule'; +import { DateDominatorTestModule } from '../../../../datedominator/demo/src/app/datedominator/DateDominatorTestModule'; +import { DateEditTestModule } from '../../../../dateedit/demo/src/app/dateedit/DateEditTestModule'; +import { DateRangeTestModule } from '../../../../daterange/demo/src/app/daterange/DateRangeTestModule'; +import { DatetimeTestModule } from '../../../../datetime/demo/src/app/datetime/DatetimeTestModule'; +import { DatetimeRangeTestModule } from '../../../../datetimerange/demo/src/app/datetimerange/DatetimeRangeTestModule'; +import { DominatorTestModule } from '../../../../dominator/demo/src/app/dominator/DominatorTestModule'; +import { DragTestModule } from '../../../../drag/demo/src/app/drag/DragTestModule'; +import { DropTestModule } from '../../../../drop/demo/src/app/drop/DropTestModule'; +import { DroplistTestModule } from '../../../../droplist/demo/src/app/droplist/DroplistTestModule'; +import { GuidesTestModule } from '../../../../guides/demo/src/app/guides/GuidesTestModule'; +import { FormfieldTestModule } from '../../../../formfield/demo/src/app/formfield/FormfieldTestModule'; +import { FoldtextTestModule } from '../../../../foldtext/demo/src/app/foldtext/FoldtextTestModule'; +import { GuidestepsTestModule } from '../../../../guidesteps/demo/src/app/guidesteps/GuidestepsTestModule'; +import { HalfmodalTestModule } from '../../../../halfmodal/demo/src/app/halfmodal/HalfmodalTestModule'; +import { IconTestModule } from '../../../../icon/demo/src/app/icon/IconTestModule'; +import { IconactionTestModule } from '../../../../iconaction/demo/src/app/iconaction/IconactionTestModule'; +import { ImagepreviewTestModule } from '../../../../imagepreview/demo/src/app/imagepreview/ImagepreviewTestModule'; +import { InputnumberTestModule } from '../../../../inputnumber/demo/src/app/inputnumber/InputnumberTestModule'; +import { IntroTestModule } from '../../../../intro/demo/src/app/intro/IntroTestModule'; +import { IpTestModule } from '../../../../ip/demo/src/app/ip/IpTestModule'; +import { IpsectionTestModule } from '../../../../ipsection/demo/src/app/ipsection/IpsectionTestModule'; +import { LabeleditorTestModule } from '../../../../labeleditor/demo/src/app/labeleditor/LabeleditorTestModule'; +import { LayoutTestModule } from '../../../../layout/demo/src/app/layout/LayoutTestModule'; +import { LeftmenuTestModule } from '../../../../leftmenu/demo/src/app/leftmenu/LeftmenuTestModule'; +import { ListTestModule } from '../../../../list/demo/src/app/list/ListTestModule'; +import { LoadingTestModule } from '../../../../loading/demo/src/app/loading/LoadingTestModule'; +import { LocaleTestModule } from '../../../../locale/demo/src/app/locale/LocaleTestModule'; +import { LinkbuttonTestModule } from '../../../../linkbutton/demo/src/app/linkbutton/LinkbuttonTestModule'; +import { MenuTestModule } from '../../../../menu/demo/src/app/menu/MenuTestModule'; +import { MessageTestModule } from '../../../../message/demo/src/app/message/MessageTestModule'; +import { ModalTestModule } from '../../../../modal/demo/src/app/modal/ModalTestModule'; +import { NavTestModule } from '../../../../nav/demo/src/app/nav/NavTestModule'; +import { NotificationTestModule } from '../../../../notification/demo/src/app/notification/NotificationTestModule'; +import { OverflowTestModule } from '../../../../overflow/demo/src/app/overflow/OverflowTestModule'; +import { PhonenumberTestModule } from '../../../../phonenumber/demo/src/app/phonenumber/PhonenumberTestModule'; +import { PaginationTestModule } from '../../../../pagination/demo/src/app/pagination/PaginationTestModule'; +import { PathTestModule } from '../../../../path/demo/src/app/path/PathTestModule'; +import { PopconfirmTestModule } from '../../../../popconfirm/demo/src/app/popconfirm/PopconfirmTestModule'; +import { ProgressbarTestModule } from '../../../../progressbar/demo/src/app/progressbar/ProgressbarTestModule'; +import { ProgresspieTestModule } from '../../../../progresspie/demo/src/app/progresspie/ProgresspieTestModule'; +import { ProductpreviewTestModule } from '../../../../productpreview/demo/src/app/productpreview/ProductpreviewTestModule'; +import { RadioTestModule } from '../../../../radio/demo/src/app/radio/RadioTestModule'; +import { RateTestModule } from '../../../../rate/demo/src/app/rate/RateTestModule'; +import { RightsTestModule } from '../../../../rights/demo/src/app/rights/RightsTestModule'; +import { ScoreTestModule } from '../../../../score/demo/src/app/score/ScoreTestModule'; +import { SearchboxTestModule } from '../../../../searchbox/demo/src/app/searchbox/SearchboxTestModule'; +import { SelectTestModule } from '../../../../select/demo/src/app/select/SelectTestModule'; +import { SelectgroupTestModule } from '../../../../selectgroup/demo/src/app/selectgroup/SelectgroupTestModule'; +import { SkeletonTestModule } from '../../../../skeleton/demo/src/app/skeleton/SkeletonTestModule'; +import { SliderTestModule } from '../../../../slider/demo/src/app/slider/SliderTestModule'; +import { SpinnerTestModule } from '../../../../spinner/demo/src/app/spinner/SpinnerTestModule'; +import { StepsTestModule } from '../../../../steps/demo/src/app/steps/StepsTestModule'; +import { SubtitleTestModule } from '../../../../subtitle/demo/src/app/subtitle/SubtitleTestModule'; +import { SwiperTestModule } from '../../../../swiper/demo/src/app/swiper/SwiperTestModule'; +import { SwitchTestModule } from '../../../../switch/demo/src/app/switch/SwitchTestModule'; +import { TabTestModule } from '../../../../tab/demo/src/app/tab/TabTestModule'; +import { TableTestModule } from '../../../../table/demo/src/app/table/TableTestModule'; +import { TagTestModule } from '../../../../tag/demo/src/app/tag/TagTestModule'; +import { TagsInputTestModule } from '../../../../tagsinput/demo/src/app/tagsinput/TagsInputTestModule'; +import { TextTestModule } from '../../../../text/demo/src/app/text/TextTestModule'; +import { TextareaTestModule } from '../../../../textarea/demo/src/app/textarea/TextareaTestModule'; +import { TimeTestModule } from '../../../../time/demo/src/app/time/TimeTestModule'; +import { TimelineTestModule } from '../../../../timeline/demo/src/app/timeline/TimelineTestModule'; +import { TipTestModule } from '../../../../tip/demo/src/app/tip/TipTestModule'; +import { TransferTestModule } from '../../../../transfer/demo/src/app/transfer/TransferTestModule'; +import { TreeTestModule } from '../../../../tree/demo/src/app/tree/TreeTestModule'; +import { TreeselectTestModule } from '../../../../treeselect/demo/src/app/treeselect/TreeselectTestModule'; +import { UploadTestModule } from '../../../../upload/demo/src/app/upload/UploadTestModule'; +import { UploadimageTestModule } from '../../../../upload/demo/src/app/uploadimage/UploadimageTestModule'; +import { BrowserTestModule } from '../../../../utils/demo/src/app/browser/BrowserTestModule'; +import { KeymapTestModule } from '../../../../utils/demo/src/app/keymap/KeymapTestModule'; +import { LogTestModule } from '../../../../utils/demo/src/app/log/LogTestModule'; +import { ThemeTestModule } from '../../../../utils/demo/src/app/theme/ThemeTestModule'; +import { ValidationTestModule } from '../../../../validation/demo/src/app/validation/ValidationTestModule'; +import { ZoomTestModule } from '../../../../zoom/demo/src/app/zoom/ZoomTestModule'; + +@NgModule({ + imports: DemoModules.allModules, + exports: DemoModules.allModules, + declarations: [] +}) +export class DemoModules { + // 增加新的测试模块,需配置此数组。 + static allModules: Array = [ + AccordionTestModule, + ActionmenuTestModule, + AlertTestModule, + AnchorTestModule, + AutocompleteTestModule, + AvatarTestModule, + ButtonTestModule, + ButtonGroupTestModule, + ButtonselectTestModule, + CardTestModule, + CascaderTestModule, + CheckboxTestModule, + CollapseTestModule, + CollapseboxTestModule, + CollapsebuttonTestModule, + CollapsetextTestModule, + CopyTestModule, + CrumbTestModule, + DateTestModule, + DateDominatorTestModule, + DateEditTestModule, + DateRangeTestModule, + DatetimeTestModule, + DatetimeRangeTestModule, + DominatorTestModule, + DragTestModule, + DropTestModule, + DroplistTestModule, + GuidesTestModule, + FormfieldTestModule, + FoldtextTestModule, + GuidestepsTestModule, + HalfmodalTestModule, + IconTestModule, + IconactionTestModule, + ImagepreviewTestModule, + InputnumberTestModule, + IntroTestModule, + IpTestModule, + IpsectionTestModule, + LabeleditorTestModule, + LayoutTestModule, + LeftmenuTestModule, + ListTestModule, + LoadingTestModule, + LocaleTestModule, + LinkbuttonTestModule, + MenuTestModule, + MessageTestModule, + ModalTestModule, + NavTestModule, + NotificationTestModule, + OverflowTestModule, + PaginationTestModule, + PathTestModule, + PhonenumberTestModule, + PopconfirmTestModule, + ProgressbarTestModule, + ProgresspieTestModule, + ProductpreviewTestModule, + RadioTestModule, + RateTestModule, + RightsTestModule, + ScoreTestModule, + SearchboxTestModule, + SelectTestModule, + SelectgroupTestModule, + SkeletonTestModule, + SliderTestModule, + SpinnerTestModule, + StepsTestModule, + SubtitleTestModule, + SwiperTestModule, + SwitchTestModule, + TabTestModule, + TableTestModule, + TagTestModule, + TagsInputTestModule, + TextTestModule, + TextareaTestModule, + TimeTestModule, + TimelineTestModule, + TipTestModule, + TransferTestModule, + TreeTestModule, + TreeselectTestModule, + UploadTestModule, + UploadimageTestModule, + // 一下四个顺序特殊 Utils Test Module + BrowserTestModule, + KeymapTestModule, + LogTestModule, + ThemeTestModule, + ValidationTestModule, + ZoomTestModule + // 以上按照字母排序,禁止在末尾添加 + ]; +} diff --git a/src/ng/demo/src/app/IndexComponent.ts b/src/ng/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..517186d --- /dev/null +++ b/src/ng/demo/src/app/IndexComponent.ts @@ -0,0 +1,58 @@ +import { Component, Inject } from '@angular/core'; +import { DemoModules } from './DemoModules'; +import { DOCUMENT } from '@angular/common'; + +@Component({ + template: ` +

    TinyNG All Demo Index

    + + + + + + + + + +
    NameDemo
    {{ module.ROUTES[0]?.path.split('/')[0] }} + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + allModules: Array = DemoModules.allModules; + + constructor(@Inject(DOCUMENT) private document) { + // 设置网页标题 + this.document.getElementsByTagName('title')[0].innerText = 'ui-ng all demo Index'; + } + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/ng/demo/src/app/app.html b/src/ng/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/ng/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/ng/demo/src/assets/browser/chrome.PNG b/src/ng/demo/src/assets/browser/chrome.PNG new file mode 100644 index 0000000000000000000000000000000000000000..c126db4d537bab6d98fa826e0c4b5e334223e80c GIT binary patch literal 1994 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 literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/browser/firefox.PNG b/src/ng/demo/src/assets/browser/firefox.PNG new file mode 100644 index 0000000000000000000000000000000000000000..c3770fc0fd95186523861acc3af5b4185ad7c964 GIT binary patch literal 2185 zcmV;42zK|0P)2(*XP~Y(4XP#xv}lixY)bn>*U4e&DrVZ(c}wm z|29Ye+(rLDK>ILJ{4!1d*+>85L;pNN|J6wUL_XaSU;p4j|2RbdRWtZ4SNSef{xeGA z5NiIyOaI(H|4BUmOgR5rF#ok9d{NB`zU|DGARJn33Gu<9RQ|IkbSU@clt z$M-8;?jv3HDp~(&JO9-;|70rvawPw+9kM~|xjf|S7-ajTQ2nh;{?A1J)jt1FH~(uY z|DqfJfg1m*8UK|SY+>kTUf*g^)NVbLgP6f>8RI zOaGxh|EV|s%Q64DBmcb^n@RAdM(&77>7_c=b~M!Gle+4Qr}D*$2)st{`>py?&G_zuCAvJwPk)t+zZ&000FTNkllJNs4NCP&0uIVd^)tA-{U0e zgyt>~OkK!gF^Jq4k*()V;++Oz*a*pD;$ZvbQYnkYBEvkH>_*;?sdsBW-+#_@6UUxd z?|=#kvE8Skp+N|SOr+k)r`a>A#d*$2Fge(N_U!o;YUp4ihoz?<_$;p%+DWNsoJyst zTxxc%kJCIoVMl^vc6NXY*8Rz`u(06Z;G@auT%jF}s%*cy>`c>zu3k268WN69qPz=_ zcZ7va#XB0Dd_ZW26B>JpAr=Z z>)*l=MBzFcn@Ok>r;`)k#BjN02$=cEi1*RO0T+$OieFQAdL0lO0KIt!dwq&wa?Q+W zW@c2GtRD`lt#C0X!O_uV`g9;`L^a9l7Ayc0{QCO?8q=Q%OAv6w&(y(E03yyw#Nll5 z@ZiLope0-S1}%VW68k=2u>Ad*XbIKL4ZqpWhK2$wt3ysA5fG1@RnTT(eJ`(|prCEr z3U_c>EUBL#BJjfThZwHgC^%?GPHt|l+^)xAFE1|;g^B6)QkK8O&yUFj;D#=?kG-z0 zp+F$8&W{ID<#C|L$&-%`2L=Xud2QWV7@8~;3S-1bu*g*MD0E3|cMw3Vtl~ITHBTQN zIkJ5D^0jN%0@)fGy5p-*E*Fa>5++MZqYebH+rZu49zT{KuBv8dQc_5WxA*$>U;+yY zLPNvvm$LwPAK&dxJu%<%R`rl4SZXgGjX0HP&wc?{RC zBY<6626O|qJrG@ox)c{AfB*^!*}naF_^H~QoSgFN=-AkpSSA-ebXN;Hg>C=@M1ES) zhRJhRteA;RNJ!H0@bH)Lay+VU)W*if!uwDI(EZAI9SWU}&c(LA_AC_y5HQY3Njp#N zyjGRR`EjFsF(mOjh@tjQ)X^ab`)8VKYQ*Hpb0KGEXBQWjlsZvf-u$x4%F4yHV#yvH zyt38Qge8a==-zTQHH`p*g7Kp3PTASn^8*4ZDk{s%#e;C5L9)=)BM2RYt7&R#L`1}f z4XG(b7ZdXH^WmEdB;fAtXyOaMcivbnJ=lT{Mq!tvy?`yK=S9~&`=Uikl9`#=1#3)|q;EcPA`qp?(xEJY==e3bE6M%R(o8k1sf{2eOu&tvo+hBI zEKqoKoZMHGmiB4>(|ev2#h+pU1|5w+a6CK;-Tj_Ip?HTiuUiHw%4pUU3kzZoAfyPV zRjg2az*^RoM2Z^6pEZRrfRMtW)}~Fx#h2j$TBeaf(jNz87VH583NdJtk&(IiWfiPd z-S(06qkxzrgPH&p!#JOBd#=?#3UOCB8HZ-fA-)mBx3#t1jkS@+ULatIfZ9UmhG1={ zdNzXu2sIDy%ErOi-y5>)?0G<_jC~tb)v)$7T0>P;wd>%a*#GDkDfuT43vZV#00000 LNkvXXu0mjfsXdH7 literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/browser/safari.PNG b/src/ng/demo/src/assets/browser/safari.PNG new file mode 100644 index 0000000000000000000000000000000000000000..e4b424308a8f7b56f8fe12c1a3bdd92144adcd9f GIT binary patch literal 2402 zcmV-o37z(dP)ut%<>BDq+9y`=iJ-d!NSA(`uODJ zFV5y!8ve?%UPQ!^6WOto|3b`31n`1CiSzZqNav!~ucB^YQNjn%v#n*aB$P19i{= zc*+5az;(O%k;V28lj|~rFewRquvI3-8FmI0jSpjiPZyo z)c~T=GJwqho5>oQ|6Q{B7OwXqe(MBK-UWl(19R3EXw?Cl(*csu0gBB(d(F|!$juTF zF`N9F#PVI6>kXdj42b3mkK_iP;tPY}2WZ<&eAWSf&`yER0fET@lD+|%z5Y>7O|bj9 z)cQ}H_YZ~f5w!3Ny6gse>j{?RbEM$`xZMSc+ya5tcbU**l+MV=$!wCx2e0Q%n&tz! z|uHQ_P-W6}$UX<9iudV+{MB57pi@N+ruKXdC{9c;-7^M1Pr1TY?^boD@ z3ZLaBisOZ^;2(qF8+hCrVcaNc*#=(O0kGC-PK&X!R(B^KYl|0LShtljl2;-yyZqj;hRBip&9U$O>|{3s|iPk*biIop6qu8E}#c zO^!8Rgw11ZQcY=pGeg}gE9NI7v>P78=KemP{G-b9E1T{zf$f{hk2fSa-enxwrs|w~BMC34@^@h@O>tj$(a{wQ_jtS6JgKDfJ%~ch25;x!yXt)gheE zOP#hghnGiykHUO=vq4##DKFzKEa56CAoai)0000WbW%=J06_kOnmpxSM+oQJx^N%y z=g7rt@aEgbyqlZVwS$8ngcvpH000I3Nkl#E>#b0(x&d)rL8eFwO)GY za_O5P+RgUZ|Ka63{N9)EeUJ5jQ_Ze#<#C(PCN8hFo?ZQKT+zluQ_0<@AiK#Fit@@T z{*KwLO$%BW*Eltq3^;IDyMQ*8voFUDZQP^-3d0Z|=ff~KAXou8k|?*V;Sycdl5)7i z4lf_#Ll6u@5GcHku-lPB>#F|HmCadqR7dLg3I!j>U>L*c3L+GvD? zAO-2f@Ql#u6lQSDNh%O744&3kT?w*rML!nSYv~aX72cv}UN~Lp>?xFsHRg={k znCD#iUTT@dG!cby99xJ4j5H8J0>n(FhnYhM+L@Mv(*)9S+Tk+Y>&bMbXF|7w4Q=CD z>5>*&=nX?_7tXfOqha3?6HhE^j0O)bQLlbeiXo!$;P3J>liz#!GH>4hdxEZ^X?jk7 zsjEMYPsocO?>~AuqE@T#JyR%T5A2U$+@DNH(@`ip!*9$c;d zc(n_*PABLyc1^f2J+~sP-d~KznNmrwXJsZqUzUU2i0yqC84+}oLI(OQe2uvx3AGQ9 zbRZg~HF}mcn@p3(<6y^P`@16m*r3no?Q}Gqrb(o?t!+?La#SJ?gU`pB&F0rld-Yq> zp43g6Oft|l;dmiDD?-{~S4GoIG(wlOKGtISIw8j!vHka6uNQ2Vi9f{IULo$&sOBT!bpkp~;+2V+xBe{pGP$zrjvK5aQ2srdO55*0U3 zQ$OiUF>9PbyNvA}ZRf_CfSk)&EH0ncU|`mmgp)*jF4A}qJ4*zN1a*v^A8va_M@O4} z4s4Ihr7dS1@kk(XhN11PB#@A&yubF)c67L)ETit{)Po%Qh6p=R)@R;QNh)vqn_j~sk!m+B8x7I^Mp0ENigp5|+S;cU? z`{woDG%U4YRkx9-TZt^kWeIDYFuJRlO^QNqy3TE}1SZUH1h~uskl_Lw^B_yRp!5Ba z8-o%lZo?3C9zYSyhD-4t=&OIFLp&e_a6lN*UUA1i?*#3)`cb5(ySH7q6Z{uH0mr^) Up2I=xw*UYD07*qoM6N<$f(qu@l>h($ literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/food/cake.png b/src/ng/demo/src/assets/food/cake.png new file mode 100644 index 0000000000000000000000000000000000000000..f4fe737be989057ef38b733375720273a250b2e7 GIT binary patch literal 4044 zcmV;-4>RzIP)HE^-_|f9{(BbaesP@m_0RaK>?dbZ^;_~d~`S$bd*{Au_<$eK_VVrv3JLtw

    7{1q1~8`1SGMs}~g(_}1t5^zixH?EBg36A=>q-S7O{?EBW| z^Wm-N)};0C>G;{{{rvj-;PC$0>Khsw|Ni~``}OMBrZ_Y-`u6bn^X+$KZT;5h_vEh` z77_mB_V&`_^W?8qNlzvtBq1Ib92yiA6AtRi+WYps`tG;-=&|H!QUCw{`}p$m@b3Ql zz_YQg?%%7ftEqT)b6rnWL_E`C;ySAOF>er>@)T73_kDP^% zmw<|jhlY%Kg07-{eQR@^l5k{JUFmF7Eh-@N^YQW2<>k}j?9t)c*w@p*&egie&A7$B zthn;)w!){h?c%PGeu%WGf`V~*a&T^LZE9<0W{rboSXNYWX;DBvJVrq=H7zmh+34Qe z*U-+&|Ng_m!NAS2zsRh*-_fD4m7uASoz=&goSK-GhK`SoioCIjgMod9fns=YR#Q?? zT~|XpIygloA1NXl_2KaE?dk02;oi;J$j8P1_PyE9pM`^gqnLJ=jcjmUWq^5Ie05pe zYgB7yO~qhLPfbfiIX*l$D)sgB=h@`t%G%e+)4j2|<=v{WnWU+gp`o0doQ#)dWnx@f zSV~Ao=i{`%x{#KWkgr!oOhz{C;Oxo7!0zU>wy3hKpsA>vcZhyr{dr!oRz*)pH$pux zST83#ARE`ysLso$vsgx0g>ZH^K`5nc(cZG zX_OyI9)a-S2N0`WW_-R#**G%`0tSinOb2lmw*2HOe_mB`y;4xqv`{FdX(`9sgzeb` zL5e1;bpNp))g){d_Heq*;0XqUS}3--572&TAws|pXXAiJD>M>`r&_%2J#2U5(l3&` zYol%CDwXWm2+($Z`+Gisu-cCtPhKXstwTy19*Gfdz{Iro=)^Olm1b9p+p!ZUAc)$D98 zf1{DE3gR$qF%bES7W02;B8=mB+&vG^?HrE}UAt}UyzTCwdDB>Jv@whQViLAy+o&|l zl4ZEWZ>44o1k21Hw2-1QzaU~sgcT`DMi5115ClO%U+6m&L_tCQ)(<_KMSsBQdxJfD zICwpu_lM8>j>|i<*?RZKlhdbZ;Xk>FgUgn@aE9jTvJI;Wi;MGDExTmlxL=>Ye}D3u z+S8;BRzeOL(xvF ztziH5(p~l~X46vO87439&?XwOZ&7JZ(ESDc0e?WtFc;{J3SJNo7fu05Vmj%@ z03z7f-r*`w*|-Z2?=Kdeb})ad!xDWrJ~^&H_P=-@0Kg-8FjN=z&?R=yxuUIAUV+%lnpcYR1ke@D;p@^fuArL?1uuWV$vnX_Cnfc&; zu5&nsiAUtwfAEas{)VF}{q+ZqdU&4Y8&6nL2_+Fb+F!tJ-(}BP40zto+Z;EfVo3E5 zPKop&v6H^RE)4>NAkQPU>T%s%1H#C2FJiTB+OWZ7e8^VpyeV)s%@WLx)Pi_TgQ6&U zDqNvymC^yV0jX1=(2$Pkbqzijx8v;=#&D>vG+*B#)Si<7uLipO3zb6_r$ev1kY}`` zU^49?FuN&Mc6SE}5IsDsVS4SKj z)s0y=&l4gNRs^k*7H^+$c{k?g8ikv$Uv`W&0DzR9RBhM;0dW#)a#dSU1VB=TLuI~s z75tq59^$hzk+6sxtIHi1=^dUiR(Qz`p&}fk)vCi|?`Y*3t_sprbE<^T1Wc^wS>2dS z{?REsovI?pInZzQ?pc*<2yQa(dG9(EfPkkwMb&tq&-Awu7C@BcK~3o`hz==;F6l)q zz@%zAjsZ~L6xF{iGz4cD^Npg(WFSEeU66aeqHL7H*}sBn+j#ACjKZ-*Se}0TGAj|R z4@MmO48qss?X!g>CjvYOVjjSFbD!XA$qpC45U;E%CH%DgV�LnAv~{=RkK`^9!Hw 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 Z{Q!wKTOZyy_a^`V002ovPDHLkV1jk*mAe1{ literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/food/cola.png b/src/ng/demo/src/assets/food/cola.png new file mode 100644 index 0000000000000000000000000000000000000000..efcc14a34aa5cce474b45211db0d45cb0b696a49 GIT binary patch literal 4115 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 Ri-iCH002ovPDHLkV1l|G5Gnuw literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/food/fried_chicken.png b/src/ng/demo/src/assets/food/fried_chicken.png new file mode 100644 index 0000000000000000000000000000000000000000..cbcdf42b5e789f56ba6d2c90162d07cd0ccee884 GIT binary patch literal 6156 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 Y10*L&q3N0zg8%>k07*qoM6N<$f`2_9PXGV_ literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/food/hamburger.png b/src/ng/demo/src/assets/food/hamburger.png new file mode 100644 index 0000000000000000000000000000000000000000..eba9ed8671809cf5336f407858f7f98685fd1939 GIT binary patch literal 7032 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 VREX{n;HwT8QAHJmw=|B&|JhLKFqZ%T literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/image/2.jpg b/src/ng/demo/src/assets/image/2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5382f6d5472e51d8b3709a8eb99904b0ca8b9035 GIT binary patch literal 46016 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 literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/image/3.jpg b/src/ng/demo/src/assets/image/3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4897cccf8e95b64502a1f95cf6f5b4ed11eb5f9b GIT binary patch literal 100875 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%0AF4SkvITPC+z=^Sv^M2G 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 z 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 literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/assets/nav_logo/logo.png b/src/ng/demo/src/assets/nav_logo/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..34f8774cb14fe6fb2352e487c4c6d16952abd11d GIT binary patch literal 6847 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 literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/favicon.ico b/src/ng/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/index.html b/src/ng/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/ng/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/ng/demo/src/main.ts b/src/ng/demo/src/main.ts new file mode 100644 index 0000000..656f1eb --- /dev/null +++ b/src/ng/demo/src/main.ts @@ -0,0 +1,49 @@ +import { enableProdMode } from '@angular/core'; +// import { TiTheme } from '@cloud/tiny3'; + +import { AppModule } from './app/AppModule'; +import { AppModule as WcAppModule } from './app/AppWcModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +// if (environment.production) { +// // 生产环境 +// // 主题名称 +// const themename: string = 'default'; +// // 加载主题CSS文件。只有生产环境支持在线切换皮肤,所以基础CSS在angular.json中配置,主题CSS在代码中加载,之后再应用。 +// // 会从assets/tiny3/themes/theme-${theme}.css 加载CSS文件,放在head link +// const link: HTMLLinkElement = TiTheme.loadCss( +// `assets/tiny3/themes/theme-${themename}.css`, +// 'tiny3theme' +// ); + +// // 原生支持CSSVars +// // 在Chrome下,新加入的CSS载入太迟,CSS样式生效迟,overflow等需要计算宽度的组件有问题,所以要等CSS加载完成后才启动App +// link.addEventListener( +// 'load', +// () => { +// // 监听DOMContentLoaded,是ng add @nguniversal/express-engine生成的。但是,在当前环境下,css load事件之后,再也没有DOMContentLoaded事件了,所以不需要监听。 +// // document.addEventListener('DOMContentLoaded', () => { +// TiTheme.bootstrapModule(environment.isWc ? WcAppModule : AppModule); +// // }); +// }, +// false +// ); +// link.addEventListener( +// 'error', +// () => { +// TiTheme.bootstrapModule(environment.isWc ? WcAppModule : AppModule); +// }, +// false +// ); +// } else { +// TiTheme.bootstrapModule(AppModule); +// } + +platformBrowserDynamic() + .bootstrapModule(environment.isWc ? WcAppModule : AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/ng/demo/src/webdoc/LICENSE b/src/ng/demo/src/webdoc/LICENSE new file mode 100644 index 0000000..0a8f884 --- /dev/null +++ b/src/ng/demo/src/webdoc/LICENSE @@ -0,0 +1,103 @@ +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. + +Section 1 – Definitions. + +a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. + +b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. + +c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + +d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. + +e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. + +f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. + +g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. + +h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. + +i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. + +j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. + +k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. + +Section 2 – Scope. + +a. License grant. +1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: + A. reproduce and Share the Licensed Material, in whole or in part; and + B. produce, reproduce, and Share Adapted Material. +2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. +3. Term. The term of this Public License is specified in Section 6(a). +4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. +5. Downstream recipients. +A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. +B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. +6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). + +b. Other rights. +1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. +2. Patent and trademark rights are not licensed under this Public License. +3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. + +Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following conditions. + +a. Attribution. +1. If You Share the Licensed Material (including in modified form), You must: +A. retain the following if it is supplied by the Licensor with the Licensed Material: + i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); + ii. a copyright notice; + iii. a notice that refers to this Public License; + iv. a notice that refers to the disclaimer of warranties; + v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; +B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and +C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. +2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. +3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. +4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. + +Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: + +a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; + +b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and +c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. +For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. + +Section 5 – Disclaimer of Warranties and Limitation of Liability. + +a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. +b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. +c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. + +Section 6 – Term and Termination. + +a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. +b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: +1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or +2. upon express reinstatement by the Licensor. +For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + +c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. +d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +Section 7 – Other Terms and Conditions. + +a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. +b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. + +Section 8 – Interpretation. + +a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. +b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. +c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. +d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. diff --git a/src/ng/demo/src/webdoc/faq-en.md b/src/ng/demo/src/webdoc/faq-en.md new file mode 100644 index 0000000..e69de29 diff --git a/src/ng/demo/src/webdoc/faq.md b/src/ng/demo/src/webdoc/faq.md new file mode 100644 index 0000000..e85d42e --- /dev/null +++ b/src/ng/demo/src/webdoc/faq.md @@ -0,0 +1,79 @@ +--- +title: 常见问题 | TinyNG +--- + +# 常见问题 +## 1. 运行报错:Can't bind to 'ngModel' since it isn't a known property of 'ti-xxxxx'? + +使用 `ngModel`,需要引入`FormsModule` + +```typescript +import { FormsModule } from '@angular/forms'; +@NgModule({ + imports: [ + ... + FormsModule, + ... + ], +}) +``` + +## 2. 编译报错:Can't bind to 'yyyyy' since it isn't a known property of 'ti-xxxxx'? +``` + If 'ti-xxxxx' is an Angular component and it has 'options' input, then verify that it is part of this module. + If 'ti-xxxxx' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. + To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. +``` +导入组件对应的`Module`即可。 + +## 3. 怎样找到`TinyNG`组件对应的模块名? + +在查看具体组件的示例时,页面顶部会显示该组件依赖的`Module`。 + +## 4. 运行报错:ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked? + +这是 Angular 调试环境在提示:Angular 已经绘制完模板了,但是在`ngAfterViewInit()`或之后,业务代码又去改变模板变量了。 + +两种解决方案:一、优化代码组织方式,提前更改模板变量。二、更改模板变量之后,去强制刷新模板(参考此文)。 + +## 5. 使用`TinyNG`库开发业务时,如何单步调试呢? + +TinyNG 库内置了 soucemap,很方便单步调试。单步调试方法: + +目前 Angular CLI 7.0 版,`ng serve --vendor-source-map`(官方已废弃此参数) + +Angular CLI 7.2 版,`ng serve --source-map=true` + +## 6. 我写的跟官网一模一样,为什么我这里出错了? + +请使用`Angular CLI`新建一个项目,复制官网代码,查看组件表现是否和官网一致。 + +## 7. 页面局部有滚动条,滚动该滚动条时其内部的下拉框错位? + +下拉框组件内部在 document 上注册了`tiScroll`事件,当监听到该事件时,组件会使下拉框消失。 + +开发者需要在对应的局部容器的滚动条滚动事件回调中触发 document上的`tiScroll`事件,使下拉框在拖动局部滚动条时消失。 + +```typescript +const event: Event = document.createEvent('HTMLEvents'); +event.initEvent('tiScroll', false, true); +element.dispatchEvent(event); +``` + +## 8. 运行报错: can't bind 'fromGroup'? + +使用响应式表单指令,需要引入`ReactiveFormsModule`, 可参考:[响应式表单](https://angular.io/guide/reactive-forms) + +```typescript +import { ReactiveFormsModule } from '@angular/forms'; +@NgModule({ +imports: [ +... +ReactiveFormsModule, +... +], +``` + +## 9. 部分组件接口支持传入 html 片段字符串,但是 html 中标签上的 style 样式设置不生效是为什么? + +为了防止`XSS`攻击,组件内部用的是 Angular 提供的`DomSanitizer.sanitize`方法做防`XSS`攻击安全处理的,它会把`style`设置会过滤掉,所以建议使用`class`的方式添加样式,如果一定要使用`style`方式,且能确保传入的`html`字符串片段是安全的,可以使用 Angular 提供的`DomSanitizer`上的`bypassSecurityTrustHtml`方法去掉 angular 的安全过滤处理,具体可参考:`message/message-security`。 diff --git a/src/ng/demo/src/webdoc/getstart-en.md b/src/ng/demo/src/webdoc/getstart-en.md new file mode 100644 index 0000000..e69de29 diff --git a/src/ng/demo/src/webdoc/getstart.md b/src/ng/demo/src/webdoc/getstart.md new file mode 100644 index 0000000..770517d --- /dev/null +++ b/src/ng/demo/src/webdoc/getstart.md @@ -0,0 +1,89 @@ +--- +title: 快速上手 | TinyNG +--- + +# 快速上手 + +`TinyNG`是基于 Angular + TypeScript 的 Web UI 组件库,本文将介绍在一个项目中如何使用`TinyNG`组件。 + +> 在使用`TinyNG`组件库之前,建议您提前学习 [HTML](https://www.w3school.com.cn/h.asp)、[CSS](https://www.w3school.com.cn/css/index.asp)、[TypeScript](https://www.tslang.cn/)、[Angular](https://angular.cn/docs)。 + +## 第一个本地实例 + +如果您已经拥有一个 Angular 应用项目,请直接进入[安装](#%E5%AE%89%E8%A3%85)步骤。 + +如果没用,我们强烈建议使用官方的`@angular/cli`工具创建项目,它在项目的构建、开发、调试、打包部署等环节贡献突出,对开发者帮助很大。 + +#### 安装脚手架工具 + +> 如果你想了解更多 CLI 工具链的功能和命令,建议访问 [@angular/cli](https://angular.cn/cli) 了解更多。 + +```bash +$ npm install -g @angular/cli +``` + +#### 创建一个项目 + +> 在创建项目之前,请确保 `@angular/cli` 已被成功安装。 + +通过 `@angular/cli` 在当前目录下新建一个名为 MYAPP 的 Angular 项目,并自动安装好相应依赖。 + +```bash +ng new MYAPP +``` + +## 安装及使用 + +#### 进入项目根目录,使用 npm 安装 `TinyNG` + +```bash +cd MYAPP +npm install @opentiny/ng @angular/cdk +``` +#### 引入模块 + +在使用某个组件前,需要引入对应的模块,此处以引入`Button`组件为例。 + +```typescript +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; // 部分组件依赖angular动画,可能需要引入animations模块 +import { NgModule } from '@angular/core'; +import { TiButtonModule } from '@opentiny/ng'; // 导入某个组件的模块 +import { AppComponent } from './app.component'; + +@NgModule({ + declarations: [AppComponent], + imports: [ + BrowserModule, + BrowserAnimationsModule, + TiButtonModule // 引入该模块 + ], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +#### 引入样式文件 + +在`angular.json`文件中, 引入`TinyNG`组件库样式文件。 + +```json +{ + "build": { + "styles": [ + "node_modules/@opentiny/ng/themes/styles.css", // 基础样式 + "node_modules/@opentiny/ng/themes/theme-default.css", // 主题样式 + "src/styles.css" + ] + } +} +``` + +`TinyNG`组件库内置了 5 套主题,分别是: theme-default.css、theme-blue.css、theme-green.css、theme-purple.css、theme-red.css,您可以根据业务场景进行选择,也可以在[主题配置](./themedoc)中选择更多样式配置方式。 + + +## 启动开发调试 + +```bash +ng serve --open +``` diff --git a/src/ng/demo/src/webdoc/images/basecolor1.png b/src/ng/demo/src/webdoc/images/basecolor1.png new file mode 100644 index 0000000000000000000000000000000000000000..d21bdfdd74ab8aebca72b41685f0de192c8c6a81 GIT binary patch literal 28643 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 F{vZ26yYm15 literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/webdoc/images/demo.png b/src/ng/demo/src/webdoc/images/demo.png new file mode 100644 index 0000000000000000000000000000000000000000..0b619c1e5336db72bf51e0d2817e66b3af0e55cd GIT binary patch literal 2042 zcmZ`(eKgYxAOA^4-bos>yhkP`FJs=O=@ybjLzI^dkM3>F)~MTyur(R?*%F;RxnA;+ zbcbSz>W)c6g>W(>A`~0LG+pBN=-mJAInVcV&i8!2pYu8A`^V>ezd5*Lhqcs=)d2v| z3OM2)3IIyXEk3BGyw&T-w9+kshC6!V5D0>3LE$7o69O9C`lr?iv_n~;D+#^9uRqXG z2!ivQve9*Zs*JB#0BDn?v)^O_8&c*tpfLx6y&yO@1cH5>wNjc#1NA!ye%%_u_pX9M zyPUjnzRaVHpeWT40u}B#VXmaGIY5I@Cl!{8gh1*f(7j5g*&*Vm$s-+FFpvS1RZpPc@@In<3npwWWNWtoVO#IsIn+=~dCtD_+C=vCQl z|B(1;`zuf;Xnro>@>*^_eMEj%8U3R4P}`kpqpSn?_+A_25?Nza1}@1!YXpbU2Y!{w zS3nUGANDW_tpGvUeeNVsu>mT+DiqQo*gVNbv*&x)+o(>ZIePBI|5kZy$${ z#35|0&Zb!H(>?_N5WfI_zY`b5(zZuhfUYjksuLcT6GH&j$z=^~DCz~Jt)%{MEjE9M zX8rl;WJA>*M2T}?3&rAfqVR02n0XJGUX5Vbzv~w>tIYP_`4jguk8F5qAAOA{`(glZ zccSc!C(1^A`SRl8q>sV``0y8c^Xk;t$87oQ^F6^K>@)k(K9v|rIiPKjykXFq`M%Ny zT_-msbAt_Rmm>T#-Jm}ZE{+pzA@U4;f*qIgyAa{jfh>Mq5CmDa9hxYl|I29E<{J44 zY#TN)(51y)m=@1(9Cf6q?P}@{9$TpD_afNc0*^*&zBm*-^K;3K1!UjAy#MKK$}rEX zd*+_cgm`W*I_%WSk>7ZOeOt@8K18*FR!?-PIsE34*4|uR(1Lkfq!eoseoWEyI4iS| zmtgg{p2@90R~=*8^)lxPy!jSvkHasl%xU8T2aKVWt6thrPI7A7s8>)8#3(Mg+Bt)D zkAOpU{`NM0K*+JaiHGE>K^J=-no#JU0`u&HW_HnoEQqQICqdkv^h@K*S2KJm*a34Ws~!Dgxuwm?^S`HaH6R7124MMN_Gp=FK=gJbvK$BH{5lmyKDHOh)IcQ%_Zuo{ zRwmLkQ(4N7GDL!Fc7dh_9fW{ult z^-8)bb93>cTluFMux=lwBaok3HR!LUZ*kHdZzyG+u+{XQiWr#dZWLhb=%%w3kbt0+ z#Kg99H&mH-4Ou<<$Ri;SeA6~k+&hnW3xK^7NX&;BGRi&TE%jDW%7PQSKa`Ek*n#xF zpiCX9jje(CM-`kM^ZQ|SYU9Yi2f9oZy~!S#lERvZ*3?D4SoK!Unu(rrOV;!GlMSTu zwQtai2)2ADlE(c4H4r@WT1rJxtTyc8T5qavd2uKLU8(DBz!(gk#azBw1yyfq$euEB zhZv>jQy4FCPcqu-JXeQ^XWp{PX)*N=)J!ZrFsYVAjK)U+J47YoLz!kq`F~I(YR0Eo zH{{@_3_rOefKG1TW4^BJQ-29NQ*LTlV$>+XsfNopi*Fo1v3ADo`ih1EbNdhPl8o)7 z3jIcjF7%ZCnFx_N-jvMmrgYU`MLdOAN>t|2+R)AQ@dEvvYg4F7EoQ7#Y`zOH(^Y93 zXK$;Pn9{TTJ|gejeNdm4$nk?1C2;(h3r13c&QA_AU8mz`s!inyDnE`tQu^LoU4YSL zPQXR3kyXyRJCb)x>EZd9%|&hVxP4=Mm*Y+z86B<9LtNkVgqyy$|HsF$59+Q_umG|$_6MDXWgvw`1ZbGl0I1p>eymB5Z)=Hmu^hu-NQN908)CZw&+J?246<9LGUZM@N zS7kOYf?6j20tZH!N-QWfl^%IDBs?(viC8O&Krs%mzq)fqof556AKGm~cWb3ojXRpN z*UOw%Bwr5B65C2GjxRm6W;PMkh`GzT>3u5id{8dSO|%jF;+H8jEPS#Iw*1npWgDE= z617&tpUq8=_@%N=;zc9a(?jj9X`*$WT~xu1%zwZW4xVQ`RK9lE+g4AxN#U~%)sovv zEVVHBuDh}lEYin8+&T*~=Lw0nDe|cu|H$<3vGwq}YM18jfQ`5z-VY_<&@unogK=5^ E13VYUqyPW_ literal 0 HcmV?d00001 diff --git a/src/ng/demo/src/webdoc/images/theme.png b/src/ng/demo/src/webdoc/images/theme.png new file mode 100644 index 0000000000000000000000000000000000000000..441612a5163dfa00a95f4d73c5ac0958ff7f87fc GIT binary patch literal 18370 zcma&M1za3Wvo|_80Tu}f?oQC)8Z{GgH;o)m8uQuI`zh@5&#fL1;v1004kwWh7Mq01 zy|RMZdngpTcXThMpmlY1-Da?^=egDgpg%r7(lQC$-#^^k+?15ppPijuTwL7U-JP7A z?3dZ^!^(_D@9*zJ4{D9dj$jE2ohmtijzOWc!Bb z$-cxyV0(M}>gMV8?&<8}ad&rjeSJNENucH6Swq(ob0S_0%lTzNKSl?_S$=p4OnEuA#L-ccH{2dUN1nrR8+*Xx!N&FeD;jENsu;xF#pRth~WnS%JcvR9g41(*M9y`e9=BARmQFY&Tz+ac`FE;L*r>_S*w-4r?449(K z0ybvm7Om_(R<=&TN!zWJy#|3(9=gdoOm@({i|V@8hqiC-I+oAJU1Py^8HRSBnFYRQ z>_B0N%$`g_57rDeJaqja*Ec7-_tRMiPqVptUZ-2n&zCF5?VUZV0D7Cj*~aG1;@s2u zajUwHpF5MlLeY_tsl#;!`sMA;_UOvx=2#z)Jz4280DLoIvfBsH`!eRPr+cQ_8%j$& zdN;45^Y&?(#Qi=CUUhBccy6TX1xUUX>;bRqjoiC4*$vos(pvgmA3aufoTH;+W(e-^%LE#qAW0&NBuy?T^ER z1xgO4WJAWyvxYZ-Y@?u<8ZVQOupaluPG=3Rl3;=3_7mIiF2AA%dEdICK)s=0b?4^8 z=8Uy$|7xp%qBGm~~p^cHHQ(gCWiCsAWAn7kFDW>MSaJbALg8PxA?<{z_ zEu{X1g^&@vEG0p;7E)jKDa9@Y-beM0$xb@exw-kdVnkv}L?#xlkhPASWT{jOIi?Q8 za2ytOAv~dv%IG^SvK$kLB>~}{BJZz97Wce;&LiCnU%`gmza*KwAZv2vO`8lmwSx1_ z^w8VL(8IE6JhdESEycjER1V!7RQ#_mM7}pk@fK3$U;iL7!|znEnfZ@DbTVu2~r5P~WW<3ntIf*KyC z8uy>!VXe=^R|YO!aWNo^57mkXoas&R*H~dq5QgD*RMN%gC3)W@-Sf`!%nR2&mSzcu zq3MZF5tAaMdMIU`$IICk;}Opi_B)J0k`xZzmD)5Wo!ryqbSt!>_~7S}`|FB>#H){f zb$uiWP?{|KLri*vm`%2>4v6Pzqp?YoKp#ws+0z|;bEvSr;wU(;1`hG8M9zKrx2 ziX}neWQ=6c$&9cyUiXE=v!YVymWZ1Fvr@;z z^!RD$fwSi{_50@<>qVw+5~@2~CH1Ry{o=n*z^R*k zQTD6G$dl=%U;kT(NQpEqKPPhi172rqsWnFHX2e|q(YrmZO@nGa4p+KMcULA6kSsC` ze5P#~4a%ds$b)T1>61%^xlH+XF9)xD{`(rA`F9Vc{Bta<-%qEaX(%1KiS$k(?y~o! zN}=?NR4+Y+d9C?EA_Hz#wRcn(3cnWz{X_P)ov>MeYfo8&J!%&53Q{crCc#xq?u9OjDEG$47$sCT)veXG+kb9t3yRC;`B z67V?uy<9771&o(kY}}U&CDro3vqlPkjfF-nhe9YE{$))3Z;V^h?aQJSjtN0MUcAX| z;qm6ny|(MPCxKdP0_0!7;JKW9xuJIK`}Tbu*ObEq?n%=~0FFca{t){IYCHoq_Q`9S z2dMAn_f$pV-U7@%ZQ-ClmJ&mfqZZ-)`VI1Xu7dGKe*~{)Reyb=+FA9n(;NKw(_YwI z;zDwCl}Cu}<5qss&^(`6M*m$+*iu9TlZOE*$VZI%in4E3mddX-mZ>tu<4&^xGq!SE95 z%t%_VK#lyk#X7+d^LK{+zfwD|#}4j-BzE8#N?OT#tlqU)3roo!k@`RWE@SQ$6a`K= zIrv!=6)3Ma6j8ItDv@(}3UG1)l#4^6!MU#eJ*pHaR|Xwn^c@moWL_sDmH`|pLv+5w zqfuW?oGYt?Z$@}+S8BW0?f6JAgS*xVggi{IuL4tW*iY{k&Gc`t2{A~1Ufho^y(QqW z!o2Hqn|9e2@q0g<>f35(f{bw~hCzkG&b|Wz^Z%3IOaxpQ;5_7z4`Y!X5gW7`s^d6t=) zS|-BwcsFMQbdft;vFYj9?4^5uLX`YnEFdb0mcnW<)xp24co3ZwXAYQ?Q+TNQ4R;OW zw;FM`2aK@{d@+iB7tuueNB_w)@U1lC8gVcaUw;)*v}cSqm}+EQ`V2!<{TbyrovYrRx}aaS4)Z>(UU(Ei*Wi@8+IRyD6tgmKgm_4dN^xEjgK1%K3ykM>5Q+vT^IqB>=RrgG>bqbFZn9*EHPY*t6=|<09cOi zJYk!1YxDM&Plzb0EJzECw1i+1c4e7N%sN|F>n}7&y|5-2$~S5g?!B=45jj(%<|_ba zbFZ4gnu?eu9_FJIQpKm=@wU1p)4B<64#(k2>9Q~pgTjjO({i&XT7XO-!TAGU*ac2x zR&J-d9|+7LB5;=LIaf<6BfzjDu7ZF@n@1!AymDx$zm9Q6Ucsaxjd!H{owXv55RH*Z z`X|q#ex~Kvql97}91lMSBo^}kg5s^&a5Bi_XvEXVrQfE#l<;YYqg4Q6QPv7%@W&vo zpp1c~9wtVtC%y_=P$B97dn6G%sO0I^0N=E0?{Q7l%F zH(r(0Fz#kj0B@4HpysrNSq$s>?~8JoJ#utvB~_*e-qU)5DIi@J$ohj(-^7Xxh$GoD zD)){_L&2uU)o0BHH88-=eT3k4gPKVkZqzV<0M!2!P#&v3ZH`t$aOL`Nw^E}I?)T3` z3BF_xJXZQm!A%pPX@Nb154A!0J-D$?C5J#OD%>cN>-Q&`OBvKM47h$msgjR{V|PGI ze#6cGkRP3h%&Dg#VhUmd504s`u-Zg8Z~-ATNjv@8!0YX754#GWAJ74#^$fYR~0 zzo|l2rDp=cmA;Uwmgx5TSn-EDZ&Tj-moydyI~+gD6$&-i*nxNZQEPB*Jq_O6`=#EV9|P@_$cVlo=5i2< z(pPW;v2bw$sd@h7(WsFdoRlJgmC^aV-nH7Kp?3-l4}vJ*P))ao)X_jsnc?OE341(2 z738$n8jaIgn#kAybv~3E8?Aa1y#{!r>i&TB#td()5lPGv)Sej4giFcpaG!A;l;W6C$r%Uaj=oFd-$YTMVz)y)%1wI6s0wN*Y zQNRkbfnyzmBFo|w#w+KC30+Vfsgbmxcen(CW+eRstG(N*8^M^WYXi zb!%eOcEGcPTUcPem#UUlzS|Fz@R5pCK3GUbFS`t<(fKI3MtPUoX@AmXSN63HNvtR` zx~C`kttZ(C)Uk2Op^SUrHof5I+fwAF7dv!2{kDDeM29^8*X;CPWOJug6Q&_fSake| zXG2uln?Fa}=K^$1pD389b_zhohp!o*Z02$t)Ryyq2Bq*oq#z_YbX-0^+)$Hd-@lQ9 zlHxUeTIO+CyjJYrq(X2Dy{aK+6vX-GW^Eo*}ainn|t6g^__894FQVY!j8Q(t* zc(cn>?X+oGZ5wk!U2Ydk@$Fp^ncIp2w7z!w~op-(&LUx66DG~dYa zNmK3Iir!vWKE^}_JPBP|?-Ch_Bs0Z0{t4ph)&yO=KU`#I5-%qoj_c>QkXY{B);NrV z>V2~&AaW=JX}#n}FM>yL}(J}XT;dYfDTG1W%~Ml_c>^Z;dV zn!rb-LH&-j^JUX?;UhzzxfFk4!Lj^i!IPAWR>7*(6Yq?j;bJ{~8uEcujWGciT8%Gvo}Y1eg-$;C^iRd= z#{&eT3la$|wiY$BNeZ|I{)#Gsn&Y71A3>`^_=qeSGUA#PsD5)OhsZ=70UK}tkxQXQ zsr$;E^t?;HTfKLmILf%t)#CcMbZ$>Cli_e=a8qp2*^+BP>4`rCzv|?}#p!jh1xKlT z`YNbS>2TEnUftjf#2c+KV^f54__pLX+2#a`7kMxC$E>l`EzDg<$Y-M-3D2%0L7k#O^qEc~yJtqQHF?FNJrh z2#oM>&Gg1jWH=5Q5mR`gFteZSE@MhSYF3v%8($<2#DmW~*3G5`+TQ^3K=Y082saa4 zbhW!pZ`P)r&jHGm`bjrCF7kTHh~pWv6L2h7+FQBYBq|SpKLLq=>xqX~xh*pcw>FX` z!Q|gQA&a7g$kp}_BOS#gna0)1n3Dt_l@2<|p2jhL6Df-%S64cK8Sw8JWXU0Q;4*V0}XKVcY*i;>hQ1@kCFnE5l#_145)$nL`q0i0q>G%j;(#;F51;F zAsQe}n?V4~@-}n8gNeXls6pRhJphL{dMWyeZ53DUOZO(2c$XXg+7`h9$b@4@0OJ7! z6$HRXY>87tbn#bV>O7ns)2zd(xtQ%A2k4OjB7hojg#*XP!qhw~1@0>OV|?>(allT= zK$aD6efNskrhEpDc|KnR$U(@A0ZosIydeObAWTF!#XkdDhFXLz%ghLD*5rKo`A)31 z!x8`X{tC}+EC7xa;H}I$s^?-So24D|k*)W5P-Mv#Hj7!v>X5CjN+u?o73B2~ALs4B z{KB03z<_iD@A*gL>58;{N`QoJ6^Q}UT!faYp(EY}PH(U1`5tr4oD>r`5w28TuM;as z!`es$pnBCP+Ydz|@b`e?uVKN$ME`dWxKk7vAiSdkfCZ537$Expnri0 zGigvdxV1<~1A>7dZTpqB+|j(@PCca$pbXEvd=t3>eF+@Pls+~qW1V(+%y~fN zQP|0C7_fnOAi|63>^TXrlR004YZq<@*~0x5zE}@}hiiXPeyjZnzMXa;K_8F)+>Kt}{_0F-#dc1_+TYmJban$IqFo-9$E+8++b)`J)>ztd={lUL3@ zN}L{y@i-ugUgRj{bc9Oz?CL}t3Ng`w5y9HASTDegH2Gwr@CM@injQs}TZ$#u_p#>i zR^_r5D2|PA#pcmG|HNW|s^^^n?>H(Q1RaR(_1-bTF0Y!z!=kT%GO>>7Yw#JW*s!o65Q_KRS;_KbQ;^DCV& ziEcx}CkmG|KDglkF<4~R5%S+^|2FW+aDX3fkoxY=*4j#k!jr@jvI=)*#G);x$rix! zWz-?MH^luj{`GFKxy&^9z9T0g0YPElHK3;W<7e$g%Duo|EN6I<4*`&R&({r$DngW) z{5>BO`b{?aOvXW_@}+3(lYA8rCNaf}?>L6-|~VRdlX*wgJ!2>Onp(zNa5 zjg)9ckB$EwJU_7vr%pSpH9`g>Ax&zqF?z2`c|S!Vv8-H7BDuZ85gT2G)e}T~?hF)% zJ#&RPOZ))Q-ad5^kONKuu&BxWBcP>7UKstqqh_1cw$)$%5@-iL{nZw0cXR#ovy{g; zRgHvyoY~H$P5aZrPiodtqu{rRleEjQIXnS5)VTU`gr3EU?;YU4Dfy7n%yByE z6gzf?(pA(-8j2zs;Kvi0VlRPI&)jf+lnHBYrR(E0WKn`3xJ>1nFUULwVnNwPzTQ;v zTrNy==d;9hlw$xh#Jnr{jdZ#xfOjt0YEO(I+SCsM^ALc0V~7E3b|8Y97g#U=h@@?R z>JES_atV+m>d~j#ek&7DHSt#Fs(%M|H}tljz8M64mLwIMHIUBEDA2yBcAj7_Bg5f?(arjvb+e_}KVeSO6_rU0`|!P0i~j|kypz3fJLW3Z zp-HmU%|iv71h7_Ied8Z8#o*xfx@ROqo?a_XYLWGTUAMaE2LqpvWF+~~k6v%tu3EG` zb6bh=Uk6F&TRjs&Z?O4!c;GJ$8l$hTy*_ecQQCTLvFs(W;%DphjQFoD;qZIcb?hib z3Ts{_y0k_4dv>`~bnJ@}TZlpUgfaa--+B<+VAkawG;wiY7K%Y`O-Z=+#CY|7=tMQo zvbBaI=Lhkqf&1;ynNiS@{r7yiW7y@ z3%l?Z)-@DehicTkNgq8KQ;R=nUolWSX5-XQ$KrfVNE-h;PLg^R+BhozdL&7w<43=J zGDpaW`q8JTeJKy!dKZgTj4!y-ua(k!<~}#v%*%4#GvjMb(%h}|eD-i4kBl5CP*?No zw@08iwSXK-f*hJGMeW(bD7%OH79bz@{E!xeCE&fm>}#3)rzuQD96s5 zM9IjnN%$6!4F|rdHwi>FV$a>dBOAGjM4w8kRV|i~1%A8 zFZynI_!2Ci+6PHik*7vq(hvAvLAMcA|nr2I%~dZNPS$2=pZG?c6A|Kmw`9gteq zXS3Kd*Znc#LiQ6=8~rO2g5}YT0fUX4{mrI?IO?wSaVqp3-}TuMbqX?JXG$Cq3^TcYIFzl1r}@H&Mi?`ao7bP-G1L zJMI1c-Xvi>O0Rg4JMFy!jG<8Uqc>9-Fly?fMqf}Ru_T1=&x3fKG^YjEcb6bsew!&C zPt?sw*gr(-D(?1ch%`hd@%ioWCQ;2#L@~(!2q^!afchI*f5I+nNOxIP&wZJf#{Jn2 zXM2FsA$%~?#i}9^x%woChnXV~#=$5R`Swi*jplyiE-B%Hazb?}MRS2nn^+ zO(*opO+&+;->lO?xrC26m4vFV9b-r=doe{N3viGp&I|27d)R8PF8lU1;?Ww9t*arc zGY6Yris(pfth(xMa2^P zI>-F-o|;*ocs^pHen zcmW&zr~d!EZ3)@rdk#)5FvFA9>`&B@z_s$z@7B(e=uj0PEM_J99QTXz>+m0!-8oaU zEQ#9U0nOc&{DR}r(16VE&99tpo|uDoR-ryV1L6Yz9?`!!Yfz5{0UJ%y7dq0Af|AkP zbx&G5(wJ=LM~D*Bq_nhN_*Rg_7hO@vt}^IjJueQSw+CuOCG)t8WMt%~0rn8tW;>~5 zQ()5pE|dd3w%_{bD1?_Dd&^^BAO6hwak=(iWmE{lNzfh+2D)9z<#e8 za|D^V2mRES>uA&Bkay@7JwG?}iA7(bZ??ZKOl%uVpmNCA>t(ZovTaGUz@?R&(K;7)inp1Cv}LGN-^B#$|# z?uLHtLq&=5Oc`hWkKd-X4HZ3W!1#g>NiKO%`N_;OgrgtDt0hXmG{;ab(<%kqXD)rd z_j}d0J!Bj_&*mh0l7Py7BkvrZJnQd1&xS0S`l=60mM%oW#q~&jja~@uv=`^D7dOAt zfv6Bl9#^&3|9rw={wq=uEhy3B*|ze$|GT^ zI@~E1TePm2l{uoKN97lbrAnL3JtxJ0i=hkD&PRtJz7uU{^bA*UW}YFcru-y&@$6%u zEwy#=%Y{bBn@0OVBJ=umy)2DL1O#_Lfut4CPump2ki#0rfZA(JVPB6C&@;4o(9ZTs z<>vWT;FK5Xe{@5^quK(J2UA;7)Dp;EtwEo>Q9c$8Mx3|55fZgbsdEF3R-u zC^P>q^B#}VVPL~3+Kk{n>tofmy@q$QF~Li0e1GhQua-R&gE}^k%w{FaSIY5UOkK8q zMyxBiK5rOdQzcoI5YWu7)S}srLUbypjj)@vpP7mo4_|}q|C3iw0Qtjpc~Y4AtN&|v4}~PFmJWu)z7T=4 zSTc5MOFh9~1T>4&0fo0BYam0SN|;RO3onS~>eIan#HY~v| z^dS{RRScq;Y)o3cFfA8o&d_H}^jYLh86Os%KWxcFN^tWDCE`r~XzN`u@Q}L!G8BO6 zscuwN3NL!dI$QlDvQ$5dWk@>wuqH_UA|F;{983BjbM%|(($%=v3fqpDpgq~+$jZ|N z4=BK%#7CKQFNgJ-F`v%aD9rUY!8uRkTT?$c%3NKJOu1ZKYTLj--fN;DA6b$k+ms^EeK^cYUo0yo7^_fs z?sOQymXoh}&SR>rMR14O7?^*%XA+5vL(I%IURb|}{H|F2e7DdRxE07@-10acLM{FO zJzE9eIIL)MwqXYxd&d-O>7Y~boLDELi`U8UFeBm|xP8@qOBQOZ5t{Ive?Fp&4epkJ zynWC{kSWUeL^m@ZES9DDh6nq5aQ~Ou7P$CttiSps?44MfNfD{eu-%5;M-wzPeu6}c zVl^+#Lwq|7AM^ z(ewO>g4;YnR?AV50r6Q@lqyPV>_UxucVJTnJM^#Zi!(s=XO0XM;}OPn0~)fw`P8Vo z8#t2T1ELd#9N6ltJoyNJ(QtPZIBogYKD;J#Xl>mdH^28|UUmoX%_Ry$#rZ7q(jwW> zl6at`cl%y@m7|v9yMv{ruRh?HOg;jqE7xW_+jO{haJHxp`%iNXO>wsf@tU;hEk_fh z%V`1qH{QcFMZMoL;d1Uc)g(R}SE>cL3odn6sX|E20Lf<(cc!{B+ombtM+ZqOk&z|} zwN`_t$9x*{I7U!E)-b7M-4Ue=rN++#_~s%GU1OWRq#70K81diDEvlEtf=$KJrQjbs zp_lmyqgIhlYtIxy56!)XMVaEGUvWi zH!jj(Qh;u<<#smlmkQ3XE=nAKwuyzbNgZPa_4`!0^ff9X*pu|x+I2_thQ8imm8F#) ziKZ;;81C&}`Hhenis^;80{508u^ArWr35}5UI3zu5D?W4^)a>TacYpE>ACa%BOtq& z%6bPK#c%q4-0oB?+!!fw{Fq1V7h&7t<`cXcI9z-yK)9Z*5KpY_m`Ab z9v0A^xCF(LYZ6#->~2IK^rpVM>dD}e!Ko>9?@Df>4}$+DuYykU0*2t4kz#d|Au0 zn^sHit-5cRs>aI*dh<@D#Z24s^P8ZgAojVh2c|9t?g%qB|R+ zVg^OlpTbkvC)%1g+X>yHPUh^BgfCRduAhlm^lQz_I|rgBC)Y|^ONW1Q#p@E~l+z~ISCisP@dGKl=`dEhGj*R3h!mf zm}F#YDxt|ZSqaDXC(++bCAtw+3mV%a|Y(&zI)?e|iR!iPioq%1- z^ix@y?Iz=TeqpycO?A&)v`$_$XAHZl7XVT%j;Yn6XlCH?C-y#d#;n4$@CRxhstqr>6keNl4Ifcv}YZ@*~ZE~2c7=q%~m_EZN$X5p88ss6 zCH~!3#zcPoWKE>U@{RC1!ipU+osqcAsZn`$nXXLbe+Qm-mCFeO2nmir3T0BK`aAY_97P=B7x=Tkbl;$Nj82l)ztFC* zqhxLG9Q{!74QpSKf&wmIhNEkk|NTO{r-*_c_J1_OTxr_84SbazMJ&W>{E&;rP5`|- zHg#kk^j6O8GYx$(^9*)Ld{vT={ERZ`fNXjX(XRxD>sZqPqC#r^fhtXjYL!vFbLOX! zNfER*WYdvO2*t3L1SQQ5lw0R8aP)SD2#uPbsR|>1r1Q+o?(C1#N^urfY?V7`*5l&9 zz>|N!)c?8lAX79Jw<0Ji@1io1d#O5&*s>oCk<>#OE6e8AMbS8lVwx4hq@jn+_&4r$ zu21#p+?Qkv*IA|@{^-<@gSDc)<}y>zdb9Od3+1pMgy#0c?(+)0xLY(y8`|$=$n7VY z9vXT#Jj)RE!Fl;~TFZh+2+f!gXhM$PW1#9QXgVepqE$@!nbDNZEwMODAT~}Yj9Zdv z>yUOV*5^A+nEC2aNzIxDMq&y9Uw?m3oSnbLwPI$r zwtm9F>+RLw9PaA}x3e}0;?6DKoW%<|-sqiqrns0;9%8DYh+J`1d-Y`Yh<^8V$TY|N zJUb?!fy)i@P4!PN^arMtjWS6HRwBGTZ)Tll9y(wQn@kd`De^lfiY0}vOxW6rR~eIs z>%?1Wn0B3a-|-$k9*+S<+nD{9W4@Kfle>c?zP!zDHLN$)YG@o=0}}N|0=O#4m@*z(@x%rW%}2VLjC0dM(JI*hTr->&+1+G|&DN6H(=1H9zj{A|P+IlUfkYHWCRT zfL=ORV-@FQW5HC4m(7ZE?F%7X1z}5jx$#WC{p%P600SoCEx){vSJC%o; zd+Kyw=_{$txzHmmS!wdWl@uU+E+ZFSCv-gzQ9n!;D36X506S#bPa)rV?!ES4TQm~b z2L^q;s@W#FA%q06>_Vs#u0bhitod5+FFf#gxo)Y&^-f5B8oIve!Qxqne}~FAxvm;!R`5L%!*@B z*L?z38B_ivgQEVFF?pM3)16cH^9e_u;Z65-s_EL@amt9Z`&*>|Wj+3_KFW)cQK{x) z|0Zo=khT7V;ZEn`^%dXGvLnu)*_WNVxIyQGj3U1=%~Wsq@}TO95~5Aw{8UGng2XRc ze-YuLyC)BpiT9ekXoY=SiQb{fvf(KQJeIv}FXD_1V5LMZjTpmpu#od<+%PM*z#R21fJhB`|4m5669 z!uo7jGBVyym~tf~IF@!~AS>i%D_15%aTaTcKS@D?o6KXcrx<~uEcHBYUsU*7dOP+s z={sTEWqlb!i-Np91se0*OhX+HR?e8mj--rD?X&%OWB{PfiK)ZG5yZvzUs5&h83%gfp;dSfs<=FUYG zM>gd5AfWk&n(VvTPHcIJK<4FLth;FMzI-Qb;XnRUz0^nf7*R2Q9M?Is4>mHUW)29Q zRzlOr#+-!PYG13zj2%!#8OX$ZMi}&!w|Y1~&&|fXhQ$m~Qh))a00G!)cOVUSL+)K{ zJ%p~bHxC>k#fVdgDa%}k+-ro5-lvfzfYaCx%G_wmKDPubSQ_$6e0NA47t@U!d zn`An@X5T>>?C4G0y0LTmXB@0yES_u}HpsC}{Orz09Q5rFBb{rG@~^slxlBh#s*Fh@ z-aDLP-J@Ey&=ME7LqqBk274jVP|XJlH00xE305z16|2Lei`W2vKxsB;!PL~vDMe(G zlZ1~1xgxN+BJegcgw#^l#Me6p0m8HFU7b!#Y-5n zs=xG>yT*&9be2kx2LzM;x}^}3PD;t+tTC7On^chX7sm#k_2@3$U}OE4zCU@TtcRB8 zx%6JYN?(gOYx3j1m*lmk!I0Aypf=~tkk=ivjeWC+Hu%HoJ&)C7%cxU2Cd~fy@`{gh z_caC>@v^4frSj#1i_>lVw8p+AH3uIx=4LlE)5XNV-}!Yw>Nh&9PP{ef)KQY)DpPH^W{tL3pl2+eeo|43^>CPwviUeCwW|_3H=F?S6#>mJ#0K_^!|MJ32hILHf-zO z-udcoLTL%~!37-f=~*Qy;)b?SctX+_k___U(R#P40&TB^5LpIS-9B!u2pSJEwy;N! z4&wTuMg%r>b%wHSr6h22dTg=D+PwF5HupLm4O5bSjvtK^!&n{^!FmJLQOir6q%{;^ zQbOwtVbV6Q{b7ua_ok?BIN_rfltC_JH6NkE$xTv-w0pdC>4B?Y=_;NvDj%V=Z!pe3 zQ|TQ4zVKm72sLgnBoLSWgH>3}1JlNhUuK}Mlch;-*_2sya3^O6b~S^GfBkt`{)e;$ zBI*Z??h_<2{mnr^RwkvF6Zfx?pw_M7(QHHKn@G_QC>u!QN^CciPky^zj_EIgGNs+H zyAx2fzaT}w$D4qQ#)T1ZOiDz4bOD}O(==KmNQ*xz*0Nh!RDg!} zl{XM1tbi;twPP%scsjjL7DygZ_DVU}f zXO}8xE8xcx_wecT+#2F#X(JYnbhIkV4aDHMc7Co6ebfY!SCY%Lm4WB9mwBtD+#I2A z!-st`)zUGxwA7B>@LqvEMiW$C;Q@L24R#BD@Vh#eU+%v$KoJ;8G1`~`X?eKjZg}wN zD}2#BF2X@-r14&OJ1#sRAio}vxl`s~%$;EYiXJQVfyggRtU!@IK!`G_ssp49D5t;Q zGK2%a>OnHl<)38&ehc*=MW-CX@0J40L+*F6Xzdl&u)OzZ?ao%L;0iw`Ky8H$u=`aH z6wAPY9azELFjkQUKu8J`LT|5V0D93%0OpeaqGH2z;4~4C4F03mNF6vkxNyS%i^cpM zUgm&Fyrvu=c>@mBolpiw0&ve4u6#$|QLGX6Xk;^P#xlr^>t~*I{K5Pmne zZZT)+O4Ci;cVexr=A#CZw=c`-RuMCxfMUih+BlYNs1too5yFI}h9`=xL55f?lojHc zbu4W~$+mY_mIH^!o{IdVyA0ozEBuAZvGc~Du=_KUIk?Pzi!>>L{`E(&u&7cJ;_NO` z7G=w%(oP3Et4!z)<{KB`=wFmA;;^Vw5z^eSfjojFS!%W&xPPgVJUY~ICZ-a)gPdUc zF?8cnMFbWx@O`1y^oiWCbG~wFq_O}TN*}819prt2M}~6fg42uKs&hny&d?FItCt_g zxWh7;NcV<+<$4Y_(sqt`$Er-mjG7T*GJS55cc~6l(fB->(j$K+;JaARAl4_rXfA82 zMlG7$@awp4duOYsNnxCJGIlhaq6gHBj^EsVBquBH?(loG)Arz4aY#uzFT?unWM`uX zIaUU57|Mz650)R>!BONpfAU3y-S;M|it^kb)Tsoy`H$nHdwU2)5g_UO_0%qUw=Zhf zN=iy&gs+@5t6>$d__#*JV8RHhcOWwZd!{z0Rb9j4lOd)k=quqE77vl5yvL^J3+v%A3n5erxG6c-HPUO_e^W2ih z8xsCgqnY+EeodtFES%w&Kp&{}oZ_I1IbFs0Fr(){>QLlVMvq8p>wA_eW+r1Q5>N92 zm2e?p&nup__yjidEeU*jI|b}hgB_Ce$xCqlMFqGI6N^Yd+3@@(dnyw}J%!vQOd?cC z;WgGPta>#0`s18T6CVFpXvB6f-|EjPgVlRHT19@8P5{Ii{Mm9?pRdeZYmv^^5UxKt zj^`6e`5#){BcCS3PJpFl4G}(Q-M!R!m&F8(2LzWI3M9YRUY?xcBs~m7E)%U$9EH`(NL%rlo@>^aRtN-+UGAaAT;=f1{B*(4vdsFG)gVxWtBk zo<0+9N36;0(opriTtr$#j2q(lMdiu?p`(&^z}VJhkD&3PGikj@9+2IGuHZv-HL;N3o%FU>*}|Mz$nw1V_}kVkIM( zV)H-i`TJN(G@}grQiqU4En+7bW}*{FehXP_C*kw!H6&b>}W@$K3pqWfdA7Sw*yuSPALf3CVHpRTno@QBHK zGt~5zcqXid@*VcMLUMmhsn3dq&S02@PGKpc&vdMAG>_A!YSyq=%Wt{3si5*-!(7jn zG#L>IKXR&l`6(Q}nt8U^zWJ{IPVHv4ly|CXrz5YhEuNoXUpg{4i!6goX#{fXR7`I| z^g~&M<11zeNlKyM1YSRC_of}OKB}$0B(a(laq)P@ptM@(nT&L<>(_n~Y<>>zYmZ64 z2NO?0*w&~Ujc3(Kk3b=!o1=j&h%Ucx0xvx0MK+G3C`7V#BjWVJQ1L_H1d)@+(?t*U zx>wn}2vkj#R<7DGdM4=iM_l2^MeOu<6G3n=dDqV^)a$&*WCC*5&sJ>PnN8D?+gVFf zYbwOoPG}7vlhKZqdoau^A;uNoLdn(T(g9MtwXtfK`Dwgc^={yJVC zd)Y)vTbO1c{ocC3T?LacII88&s{-TBk^M@7^oQME4EtXTpWVNXiD`bcsC<+#+=gv+ zM~3tFcahXb^v9Gt5GNz-;P5B=^M>_zkTjc1Mqa{zgAW?^RSh8_w8a?fBkt0bhV;9B zaV1+JIOd^3aXih#;lI6Nx1F)Q9^I+=P7%3Lu~<>2S$%e_WB(qGehL~p(`adA%T?pA3eGr944 zLudYDexNz&CT(7^GCq~TY7zA4PH>b3>F&eWB&O2Bbn=VE4a9Dv{-OV}>_vQP!4^eM zmE1N<8}pmdX<}A`V{sEQ$?Ba6Rkaw4#g%WYiOs(E;|KOoq!!>fq8 z`kIW#ve2|m>#fXb+N}P9oON6v^ta=@v+GJTjhrf&CW^c`jMN0~*T!Pur@LIZ3)K<;bl^|(g<^ql}k8Qr3f5M%vp6}oU`5q=M5NT()-iuFKb@W*72w_ zy6K#f#yleEL0M>^S{ZmRMH?2m8kjUK@8w!8ieeXwV49Lc$^E7cEu1)2`7lRA$=Nx6 z#9E4y3q|&bDk00XPO#ib{fO;DRi#XM`+4LmoUmp;>`9yt)NvT$?Q-nE!Fo9v$h?A`s(VnqM(tt+Gs3?v2tkT2x(OF;k>i$w-N)B^yLD**t>836GOfIRR5U>5?Q z$N-3b06=gh0E!F%2Efu%odFQH0H`wn7yw=X)Rh1zGXVM%ot=QL#ZEwB z1_VG+20*!7W&rdfLWcoe$!)tR0|1gU0QwP&!+^)8VTNL@CITSa%$Cd9+KiFmyFw!T zMxdjMLh?tCL;z$hA^G%uN*>oY0v&6>41(-*u?(`=MvHF?i*^85zT}Hia?7UVvH37i zZvZ%+8L$w96~vF@&<)Z0Fi>v*bb2N4bSQZ|J`UU`0am0LK-R@+r09m|d>pt>0(|e4 zylqkPcw(lERwF0{z+C~PXolzl$!)7!a?7UVF~v+8qmPwR07lZB4n`SBp1#{H`7kAq zE2jIUpx#2zqt!Gx9gH$U@^xF4++L^Tu?2QWH#ZwuNn@m$rWvA*kbHgE%1W%^bxIy% zgdIj({)s_aX__JOxRTrLj#aB!owiNMV~r`Mgrg0oH8Uel4Ti}e&j=H7Jjtz&egD4Q zu_$>sR3`A>*g{EMTrRyklF)^*g|CQOJ@=Xu9Pq&a$rIo z@{1=+rK!FC25^BJJLP<-RH`OqW2&Zlat5SAU#N?Ynel>{W<@VMK&zW6#BKggP*C$SbE47;?D*aqaTNj{E%UOak4Mjdf{C z$>S~gU@lL6KrhfI@vzIM-BHj7^n2|2z2w@|o3#mdNJHcNgm>YEbeIDphg_P>+rWX3 zM}&^yxL7vUr70zkwd7k)PS7Vb@bYBU9a4R8hx3hz=Z+_h3=Fa5pI5|uu!0#Day#UK z!H@&dVYvaJTsYMXt;oi@G^OM*mwbz^Roq8@xuTaFu<*g8O+M+c-GEgT+QYxGg(~v& zf4BiAmK(seaNHnzDI4q3l#<6>@`%m=Ad}bi@XNu3tCknq|EC+{2qA
    Edge | Firefox
    Firefox | Chrome
    Chrome | Safari
    Safari | +| --------- | --------- | --------- | --------- | +| last 3 versions | last 3 versions | last 3 versions | last 2 versions | + +### 支持 Angular 版本 +目前支持 Angular `^14.0.0`版本。 + +### [最新版本](./changelog) \ No newline at end of file diff --git a/src/ng/demo/src/webdoc/joinus-en.md b/src/ng/demo/src/webdoc/joinus-en.md new file mode 100644 index 0000000..e69de29 diff --git a/src/ng/demo/src/webdoc/joinus.md b/src/ng/demo/src/webdoc/joinus.md new file mode 100644 index 0000000..bdbef95 --- /dev/null +++ b/src/ng/demo/src/webdoc/joinus.md @@ -0,0 +1,156 @@ +--- +title: 加入我们 | TinyNG +--- +## 贡献指南 + +### Issue 规范 + +- Issue 仅用于提交 Bug 或 Feature 以及用户体验相关的内容,其它内容可能会被直接关闭。 + +- 在提交 Issue 之前,请搜索相关内容是否已被提出。 + +- 在提 Bug 时请说明清楚使用的 @opentiny/ng 的版本号及相关环境。 + +### Pull Request 规范 + +- 请先 fork 一份到自己的项目下,新建一个分支用于变更。 + + ```bash + git checkout -b my-branch master + ``` + +- commit 信息请遵循 [commit rules](https://github.com/opentiny/ng/blob/main/commit.template)。 + +- 提交 PR 前请先进行 rebase,确保 commit 记录的整洁。 + ```bash + git rebase master -i + git push -f + ``` + +- 如果是修复 `bug` 或者 `issues`,请在 PR 中描述清楚。 + + +### 开发 + +```bash + +# fork && git clone +... +# my-branch +npm install +npm start + +``` + +## 单元测试 + +### 整体测试 +```bash +$ npm test ng-demo +``` +或 +```bash +$ npx ng test ng-demo +``` + +### 指定组件测试 +```bash +$ npm test ${component name for test}-demo +``` +或 +```bash +$ npx ng test ${component name for test}-demo +``` + +例如: +```bash +$ npm test text-demo +``` + +### 添加测试用例 +- 由于库中每个组件都有单独版本,所以对于新加入的组件,需要开发者为其准备单元测试环境。(也可使用自动化脚本为新组件生成环境所需文件,例如: `npm build:test radio`) +- 对于已有组件,直接在组件 `demo/` 目录下新建 `${your component name}.spec.json` 文件后编写测试脚本。 +- 若组件 demo 目录中已经存在 `${your component name}.spec.json` 文件,则直接在此文件中修改用例。 + +TinyNG 使用 [Jasmine 测试框架](https://jasmine.github.io/) 对组件库内容进行单元测试。 +`npm test` 命令在**监视模式**下构建应用,并启动 [karma 测试运行器](https://karma-runner.github.io/)。 +运行结束后,控制台会输出测试结果,内容格式如下: + +``` +✔ Browser application bundle generation complete. +28 11 2022 08:40:17.804:WARN [karma]: No captured browser, open http://localhost:9876/ +28 11 2022 08:40:17.824:INFO [karma-server]: Karma v6.3.20 server started at http://localhost:9876/ +28 11 2022 08:40:17.825:INFO [launcher]: Launching browsers Chrome with concurrency unlimited +28 11 2022 08:40:17.833:INFO [launcher]: Starting browser Chrome +28 11 2022 08:40:19.278:INFO [Chrome 107.0.0.0 (Windows 10)]: Connected on socket swhnqYi_RwdYbu8uAAAB with id 55443332 +Chrome 107.0.0.0 (Windows 10): Executed 5 of 5 SUCCESS (0.562 secs / 0.815 secs) +TOTAL: 5 SUCCESS +``` + +它还会打开 Chrome 浏览器并在 [Jasmine HTML 报告器](https://github.com/dfederm/karma-jasmine-html-reporter)中显示测试输出。可以点击某一行用例,来单独重跑这个用例,或者点击一行描述信息来重跑所选的测试套件中的那些测试。 + +单元测试用例及相关配置在 `/src/test/` 目录下。 + +更多详细内容可以通过查阅 [Angular 官网关于测试的介绍](https://angular.cn/guide/testing)获得。 + +### 代码规范 +遵循 [ESLint](https://github.com/opentiny/ng/blob/main/.eslintrc.js) + +## 求贤纳士 +### Web前端开发工程师 + +#### 岗位职责 + ++ 团队目前前端框架使用 Angular、Vue,使用 ES6、7 进行具体开发工作; + ++ 前端工程化,打造从项目初始化、构建部署、发布、运维的端到端工程体系,打造前端 DevOps; + ++ 可视化搭建技术,基于少码或无码化的搭建方式,提升活动运营的开发效率; + ++ 面向对首屏和 SEO 注重的场景,研发基于 Nodejs+Vue 的同构方案,将页面首屏渲染性能做到极致; + ++ Nodejs 方面,自主研发前端工程体系相关工具,需要大量使用 Nodejs 来更高效率的实现 Web 层研发,并有高并发业务场景使用 Nodejs 做服务端开发; + ++ 基于 Serverless 方案,解决 Nodejs 运维及服务部署问题,提升前端研发效能。 + + +#### 技能要求 + ++ 熟练使用各种前端技术,包括 HTML、CSS、JavaScript 等; + ++ 具备跨终端的前端开发能力,在 Web(PC+Mobile)、Nodejs、Native App 三个方向上至少精通一个方向,具备多个的更佳,鼓励在 Native 和 Web 技术融合上的探索; + ++ 对前端工程化与模块化开发有一定了解,并有实践经验(如 Webpack、Babel、AMD、CMD 等); + ++ 具备优秀的团队协作精神,能利用自身技术能力提升团队整体研发效率,提高团队影响力; + ++ 对前端技术有持续的热情,个性乐观开朗,逻辑性强,沟通流畅; + ++ 乐于分享,善于总结归纳沉淀,并能将好的经验在团队内进行分享; + ++ 可选项:至少熟悉一门非前端的语言(如 Java、PHP、C、C++、Python、Ruby),并有实践经验。 +
    + +### Java后端开发工程师 + +

    岗位职责

    + ++ 参与核心基础框架的架构设计、详细方案设计与开发,为用户提供高可用、高性能的控制台服务; + ++ 负责云原生领域服务方案设计及开发,实现云服务业务大流量、高并发; + ++ 负责公共服务及中间件的孵化、设计及开发工作,构筑业界领先的平台能力。 + +

    技能要求

    + ++ 熟悉 J2EE、Java Web 编程技术,对各种开源的框架如 Spring 等有深入的应用和优化经验; + ++ 熟悉 Netty ,熟悉多线程模型编程和网络IO模型; + ++ 熟悉 Spring Boot、Spring MVC、Redis、MySQL; + ++ 具有很强的学习能力和技术敏感度,有强烈的责任心和进取心,乐于学习和技术分享。 + +
    + +如果您有意愿加入,请加微信咨询:ly5353523 diff --git a/src/ng/demo/src/webdoc/language-en.md b/src/ng/demo/src/webdoc/language-en.md new file mode 100644 index 0000000..e69de29 diff --git a/src/ng/demo/src/webdoc/language.md b/src/ng/demo/src/webdoc/language.md new file mode 100644 index 0000000..2da527f --- /dev/null +++ b/src/ng/demo/src/webdoc/language.md @@ -0,0 +1,209 @@ +--- +title: 国际化 | TinyNG +--- + +# 国际化 + +目前的默认文案是中文,如果需要使用其他语言,可以在初始化时进行配置,也可以在运行中随时修改,可以参考下面的方案。 + +## 设置语言 + +`TinyNG`支持 5 种语言,默认显示中文,在 Angular 项目应用入口文件`app.module.ts`,可设置语言。 + +```typescript +import { TiLocale } from '@opentiny/ng'; +// 导入其他项 + +@NgModule({ + imports: [ + // 导入模块... + ] + + // 配置其他项... +}) +export class AppModule { + constructor() { + // 配置Tiny国际化资源,默认为中文 + /** + * 可用的语言标识 + * TiLocale.EN_US 英文 + * TiLocale.ZH_CN 简体中文 + * TiLocale.ES_US 拉美西语 + * TiLocale.FR_FR 法语 + * TiLocale.PT_BR 葡语 + */ + TiLocale.setLocale(TiLocale.EN_US); + } + ... +} +``` + +## 国际化转化 +#### 使用管道符做国际化转换 +此种方式只支持通过页面刷新切换语言,业务可结合 cookie 切换语言 + +模板文件`locale-basic.html` + +```html +

    {{ 'testStr' | tiTranslate }}

    +

    {{ 'testStrArgs1' | tiTranslate: [1] }}

    +

    {{ 'testStrArgs2' | tiTranslate: [1,2] }}

    +

    {{ 'tiButton.ok' | tiTranslate }}

    + + +``` + +`locale-basic.component.ts`文件 +```typescript +import { Component } from '@angular/core'; +import { + en_US as tinyen_US, + zh_CN as tinyzh_CN, + TiLocale, + TiLocaleFormat, +} from '@opentiny/ng'; + +interface LocaleWords { + tiLocaleKey: string; + testStr: string; + testStrArgs1: string; + testStrArgs2: string; +} + +@Component({ + templateUrl: './locale-basic.html', +}) +export class LocaleBasicComponent { + // 业务自己的词条 + private static myzh_CN: any = { + tiLocaleKey: 'zh-CN', + testStr: '测试', + testStrArgs1: '测试单参数场景 {0}', + testStrArgs2: '测试单多参数场景 {0} 和 {1}', + }; + // 业务自己的词条 + private static myen_US: LocaleWords = { + tiLocaleKey: 'en-US', + testStr: 'test str with args', + testStrArgs1: 'test str with args {0}', + testStrArgs2: 'test str with args {0} and {1}', + }; + + constructor() { + // 添加业务的国际化词条 + TiLocale.setWords({ + 'zh-CN': { ...tinyzh_CN, ...LocaleBasicComponent.myzh_CN }, + 'en-US': { ...tinyen_US, ...LocaleBasicComponent.myen_US }, + }); + TiLocale.setLocale(this.getCookie('localeKey')); + this.setValues(); + } + + changeLocale(localeKey: string): void { + TiLocale.setLocale(localeKey); + } + + // 使用过滤器做国际化转换打开此代码: + setLocaleAndRefresh(localeKey: string): void { + this.changeLocale(localeKey); + document.cookie = `localeKey=${localeKey}`; + location.reload(); + } + + getCookie(key: string): string { + const name: string = key + '='; + const splitedCookie: Array = document.cookie.split(';'); + for (let word of splitedCookie) { + while (word.charAt(0) === ' ') { + word = word.substring(1); + } + if (word.indexOf(name) === 0) { + return word.substring(name.length, word.length); + } + } + + return ''; + } +} +``` + +#### 使用 JavaScript 方法做国际化转换 +此种方式可做到页面无刷新切换语言 + +模板文件`locale-basic.html` + +```html +

    {{testStr}}

    +

    {{testStrArgs1}}

    +

    {{testStrArgs2}}

    +

    {{okBtn}}

    + + +``` + +`locale-basic.component.ts`文件 + +```typescript +import { Component } from '@angular/core'; +import { + en_US as tinyen_US, + zh_CN as tinyzh_CN, + TiLocale, + TiLocaleFormat, +} from '@opentiny/ng'; + +interface LocaleWords { + tiLocaleKey: string; + testStr: string; + testStrArgs1: string; + testStrArgs2: string; +} + +@Component({ + templateUrl: './locale-basic.html', +}) +export class LocaleBasicComponent { + testStr: string; + testStrArgs1: string; + testStrArgs2: string; + okBtn: string; + // 业务自己的词条 + private static myzh_CN: any = { + tiLocaleKey: 'zh-CN', + testStr: '测试', + testStrArgs1: '测试单参数场景 {0}', + testStrArgs2: '测试单多参数场景 {0} 和 {1}', + }; + // 业务自己的词条 + private static myen_US: LocaleWords = { + tiLocaleKey: 'en-US', + testStr: 'test str with args', + testStrArgs1: 'test str with args {0}', + testStrArgs2: 'test str with args {0} and {1}', + }; + constructor() { + // 添加业务的国际化词条 + TiLocale.setWords({ + 'zh-CN': { ...tinyzh_CN, ...LocaleBasicComponent.myzh_CN }, + 'en-US': { ...tinyen_US, ...LocaleBasicComponent.myen_US }, + }); + this.setValues(); + } + + changeLocale(localeKey: string): void { + TiLocale.setLocale(localeKey); + this.setValues(); + } + + setValues(): void { + this.testStr = this.setLocaleValue('testStr'); + this.testStrArgs1 = this.setLocaleValue('testStrArgs1', [1]); + this.testStrArgs2 = this.setLocaleValue('testStrArgs2', [1, 2]); + this.okBtn = this.setLocaleValue('tiButton.ok'); + } + + setLocaleValue(key: string, params?: Array): string { + return TiLocale.translate(key, params); + } +} +``` diff --git a/src/ng/demo/src/webdoc/menus.js b/src/ng/demo/src/webdoc/menus.js new file mode 100644 index 0000000..9e63ded --- /dev/null +++ b/src/ng/demo/src/webdoc/menus.js @@ -0,0 +1,206 @@ +// 注意,删除了useFor属性 + +// title,label增加英文版,以应对将来的国际化功能 +export const docMenus = [ + { + label: '使用指南', + labelEn: 'Guide', // *********** + key: 'doc_use', + children: [ + { + title: '介绍', + titleEn: 'Introduce', + key: 'introduce' + }, + { + title: '快速上手', + titleEn: 'Quick Start', + key: 'getstart' + }, + { + title: '主题配置', + titleEn: 'Theme Customization', + key: 'themedoc' + }, + { + title: '国际化', + titleEn: 'Internationalization', + key: 'language' + }, + { + title: '常见问题', + titleEn: 'FAQ', + key: 'faq' + }, + { + title: '更新日志', + titleEn: 'Change Log', + key: 'changelog' + }, + { + title: '加入我们', + titleEn: 'Join Us', + key: 'joinus' + } + ] + } +]; + +// ------------------------------------------------------------------- +export const cmpMenus = [ + { + label: '表单选择', + labelEn: 'Form Selection', + key: 'cmp_formselect', + children: [ + { name: 'Button', nameCn: '按钮', key: 'button' }, + { name: 'Select', nameCn: '选择器', key: 'select' }, + { name: 'Radio', nameCn: '单选框', key: 'radio' }, + { name: 'Checkbox', nameCn: '复选框', key: 'checkbox' }, + { name: 'Slider', nameCn: '滑块', key: 'slider' }, + { name: 'Switch', nameCn: '开关', key: 'switch' }, + { name: 'Buttongroup', nameCn: '选块组', key: 'buttongroup' }, + { name: 'Spinner', nameCn: '数字微调', key: 'spinner' }, + { name: 'Treeselect', nameCn: '树选择', key: 'treeselect' }, + { name: 'Cascader', nameCn: '级联选择', key: 'cascader' }, + { name: 'Transfer', nameCn: '穿梭框', key: 'transfer' }, + { name: 'Score', nameCn: '评分', key: 'score' }, + { name: 'Linkbutton', nameCn: '按钮链接', key: 'linkbutton' }, + { name: 'Selectgroup', nameCn: '选择组', key: 'selectgroup' } + ] + }, + { + label: '表单文本', + labelEn: 'Form Text', + key: 'cmp_formtext', + children: [ + { name: 'Text', nameCn: '文本框', key: 'text' }, + { name: 'Textarea', nameCn: '多行文本框', key: 'textarea' }, + { name: 'Autocomplete', nameCn: '自动补全', key: 'autocomplete' }, + { name: 'Searchbox', nameCn: '搜索框', key: 'searchbox' }, + { name: 'IP', nameCn: '输入IP', key: 'ip' }, + { name: 'IPsection', nameCn: 'IP分段', key: 'ipsection' }, + { name: 'Tag', nameCn: '标签', key: 'tag' }, + { name: 'TagsInput', nameCn: '标签输入', key: 'tagsinput' }, + { name: 'InputNumber', nameCn: '数字输入框', key: 'inputnumber' }, + { name: 'Labeleditor', nameCn: '可编辑文本', key: 'labeleditor' }, + { name: 'PhoneNumber', nameCn: '电话号码', key: 'phonenumber' }, + { name: 'Path', nameCn: '路径', key: 'path' } + ] + }, + { + label: '表单日期', + labelEn: 'Form Date', + key: 'cmp_formdate', + children: [ + { name: 'Date', nameCn: '日期', key: 'date' }, + { name: 'DateRange', nameCn: '日期范围', key: 'daterange' }, + { name: 'Datetime', nameCn: '日期时间', key: 'datetime' }, + { name: 'DatetimeRange', nameCn: '日期时间范围', key: 'datetimerange' }, + { name: 'Time', nameCn: '时间', key: 'time' } + ] + }, + { + label: '表单辅助', + labelEn: 'Form Assist', + key: 'cmp_formassist', + children: [ + { name: 'Validation', nameCn: '表单校验', key: 'validation' }, + { name: 'Formfield', nameCn: '表单布局', key: 'formfield' }, + { name: 'Upload', nameCn: '文件上传', key: 'upload' }, + { name: 'Uploadimage', nameCn: '图片上传', key: 'uploadimage' }, + { name: 'Buttonselect', nameCn: '选块下拉', key: 'buttonselect' } + ] + }, + { + label: '导航', + labelEn: 'Menu navigation', + key: 'cmp_nav', + children: [ + { name: 'Leftmenu', nameCn: '左侧菜单', key: 'leftmenu' }, + { name: 'Menu', nameCn: '下拉菜单', key: 'menu' }, + { name: 'Nav', nameCn: '顶部导航', key: 'nav' }, + { name: 'Actionmenu', nameCn: '菜单按钮', key: 'actionmenu' }, + { name: 'Accordion', nameCn: '手风琴', key: 'accordion' }, + { name: 'Tabs', nameCn: '页签', key: 'tab' }, + { name: 'Steps', nameCn: '步骤导航', key: 'steps' }, + { name: 'Crumb', nameCn: '面包屑', key: 'crumb' }, + { name: 'Timeline', nameCn: '时间线', key: 'timeline' }, + { name: 'Subtitle', nameCn: '返回标题', key: 'subtitle' }, + { name: 'Anchor', nameCn: '锚点', key: 'anchor' } + ] + }, + { + label: '弹出提示', + labelEn: 'Popup Prompt', + key: 'cmp_popup', + children: [ + { name: 'Tip', nameCn: '气泡提示', key: 'tip' }, + { name: 'Overflow', nameCn: '溢出提示', key: 'overflow' }, + { name: 'Collapse', nameCn: '折叠面板', key: 'collapse' }, + { name: 'Collapsebox', nameCn: '折叠框', key: 'collapsebox' }, + { name: 'Alert', nameCn: '警告', key: 'alert' }, + { name: 'Modal', nameCn: '弹出框', key: 'modal' }, + { name: 'Message', nameCn: '消息弹框', key: 'message' }, + { name: 'Notification', nameCn: '通知弹框', key: 'notification' }, + { name: 'Popconfirm', nameCn: '气泡确认框', key: 'popconfirm' }, + { name: 'Halfmodal', nameCn: '半屏弹窗', key: 'halfmodal' } + ] + }, + { + label: '其他组件', + labelEn: 'Others', + key: 'cmp_others', + children: [ + { name: 'Avatar', nameCn: '头像', key: 'avatar' }, + { name: 'Table', nameCn: '表格', key: 'table' }, + { name: 'Progressbar', nameCn: '进度条', key: 'progressbar' }, + { name: 'Loading', nameCn: '加载', key: 'loading' }, + { name: 'Pagination', nameCn: '分页', key: 'pagination' }, + { name: 'Tree', nameCn: '树', key: 'tree' }, + { name: 'Icon', nameCn: '图标', key: 'icon' }, + { name: 'Iconaction', nameCn: '图标文本链接', key: 'iconaction' }, + { name: 'Intro', nameCn: '新手引导', key: 'intro' }, + { name: 'Swiper', nameCn: '轮播', key: 'swiper' }, + { name: 'Card', nameCn: '卡片', key: 'card' }, + { name: 'Layout', nameCn: '布局', key: 'layout' }, + { name: 'Rate', nameCn: '评分', key: 'rate' }, + { name: 'Rights', nameCn: '权益', key: 'rights' }, + { name: 'Skeleton', nameCn: '骨架屏', key: 'skeleton' }, + { name: 'Rate', nameCn: '评分', key: 'rate' }, + { name: 'Guides', nameCn: '情景引导', key: 'guides' }, + { name: 'Foldtext', nameCn: '折叠文本', key: 'foldtext' }, + { name: 'Productpreview', nameCn: '商品预览', key: 'productpreview' }, + { name: 'Collapsetext', nameCn: '下展文本', key: 'collapsetext' }, + { name: 'Guidesteps', nameCn: '小步骤引导', key: 'guidesteps' }, + { name: 'Collapsebutton', nameCn: '折叠按钮', key: 'collapsebutton' }, + { name: 'Copy', nameCn: '复制', key: 'copy' } + ] + }, + { + label: '工具组件', + labelEn: 'Tools', + key: 'cmp_tools', + children: [ + { name: 'Browser', nameCn: '浏览器信息', key: 'browser' }, + { name: 'Keymap', nameCn: '键值查询', key: 'keymap' }, + { name: 'Log', nameCn: '日志打印', key: 'log' }, + { name: 'Theme', nameCn: '主题配置', key: 'theme' } + ] + }, + { + label: '国际化', + labelEn: 'Locale', + key: 'cmp_locale', + children: [{ name: 'Locale', nameCn: '国际化语言', key: 'locale' }] + }, + { + label: '接口 & 类型', + labelEn: 'Interfaces & Types', + key: 'cmp_interfacesandtypes', + children: [ + { name: 'Interfaces', nameCn: '接口', key: 'interfaces' }, + { name: 'Types', nameCn: '类型', key: 'types' } + ] + } +]; diff --git a/src/ng/demo/src/webdoc/themedoc-en.md b/src/ng/demo/src/webdoc/themedoc-en.md new file mode 100644 index 0000000..e69de29 diff --git a/src/ng/demo/src/webdoc/themedoc.md b/src/ng/demo/src/webdoc/themedoc.md new file mode 100644 index 0000000..7b25224 --- /dev/null +++ b/src/ng/demo/src/webdoc/themedoc.md @@ -0,0 +1,227 @@ +--- +title: 主题配置 | TinyNG +--- + +# 主题配置 + +TinyNG 支持一定程度的样式定制,以满足业务和品牌上多样化的视觉需求,包括但不限于主色、圆角、边框和部分组件的视觉定制。 + +theme + +## 使用预定义主题 + +### 默认主题 + +修改`angular.json`的`styles`字段,全量引入`"node_modules/@opentiny/ng/themes/styles.css"`和`"node_modules/@opentiny/ng/themes/theme-default.css"`。 + +```json +{ + ... + "architect": { + "build": { + ... + "styles": [ + "node_modules/@opentiny/ng/themes/styles.css", + "node_modules/@opentiny/ng/themes/theme-default.css", + "src/styles.css", + ], + ... + } + } + ... +} +``` + +### 官方主题 + +除了默认主题外,我们还提供了 4 种官方主题,欢迎在项目中试用,并且给我们提供反馈。 + +- theme-blue.css +- theme-green.css +- theme-purple.css +- theme-red.css + +修改`angular.json`的`styles`字段,全量引入`"node_modules/@opentiny/ng/themes/styles.css"`和`"node_modules/@opentiny/ng/themes/theme-xxx.css"`。 + +```json +{ + ... + "architect": { + "build": { + ... + "styles": [ + "node_modules/@opentiny/ng/themes/styles.css", + "node_modules/@opentiny/ng/themes/theme-xxx.css", + "src/styles.css", + ], + ... + } + } + ... +} +``` + +## 自定义主题 + +如果需要自定义主题,引入官方主题文件之后,再根据实际需求自定义覆盖主题样式变量的参数。新增 `src/theme-my.css` 文件,修改 `angular.json` 的 `styles` 字段,引入 `src/theme-my.css`。 + +```json +{ + ... + "architect": { + "build": { + ... + "styles": [ + "node_modules/@cloud/tiny3/themes/styles.css", + "node_modules/@cloud/tiny3/themes/theme-default.css", + "src/theme-my.css", + "src/styles.css", + ], + ... + } + } + ... +} +``` + +例如,在以下样例中通过修改 `--ti-base-color-brand` 的数值将预定义默认主题的基础色修改为 `#999999`。 + +```css +:root { + --ti-base-color-brand: #999999; + ... + --ti-common-font-size-base: 16px; + ... + --ti-tag-text-color: #888888; + --ti-tag-icon-color: var(--ti-base-color-icon-graybg-normal); + ... +} +``` + +主题涉及到的变量,请查看 [basic-var.css](https://github.com/opentiny/ng/blob/main/%40opentiny/ng/themes/basic/basic-var.css)。 + +除了手动修改主题涉及到的变量外,通过[主题配置系统](../../designtheme/home)也可以修改主题。 + +## 动态切换主题 + +### 步骤一:将`TinyNG`主题 CSS 文件,复制到`assets`下 + +修改`angular.json`的`assets`字段,参考如下修改,下面`input`,`output`意思是打包时,`"node_modules/@opentiny/ng/themes/"`复制到`"/assets/tiny3/themes/"`。 + +```json +{ + ... + "architect": { + "build": { + ... + "assets": [ + "src/favicon.ico", + "src/assets", + { + "glob": "**/*", + "input": "node_modules/@opentiny/ng/themes/", + "output": "/assets/tiny3/themes/" + } + ], + ... + } + } + ... +} +``` + +### 步骤二:添加基础样式`"node_modules/@opentiny/ng/themes/styles.css"` + +修改`angular.json`的`styles`字段,引入`"node_modules/@opentiny/ng/themes/styles.css"`。 + +```json +{ + ... + "architect": { + "build": { + ... + "styles": [ + "node_modules/@opentiny/ng/themes/styles.css", + "src/styles.css", + ], + ... + } + } + ... +} +``` + +### 步骤三:加载主题 CSS 文件 + +修改Angular项目启动文件`main.ts`,加载主题 CSS 文件后,再启动`AppModule`。 + +```typescript +import { enableProdMode } from '@angular/core'; +import { TiTheme } from '@opentiny/ng'; + +import { AppModule } from './app/AppModule'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +if (environment.production) { + const themename: string = 'default'; + // 加载主题CSS文件。只有生产环境支持在线切换皮肤,所以基础CSS在angular.json中配置,主题CSS在代码中加载,之后再应用。 + // 会从assets/tiny3/themes/theme-${theme}.css 加载CSS文件,放在head link + const link: HTMLLinkElement = TiTheme.loadCss( + `assets/tiny3/themes/theme-${themename}.css`, + 'tiny3theme' + ); + + // 原生支持CSSVars + // 在Chrome下,新加入的CSS载入太迟,CSS样式生效迟,overflow等需要计算宽度的组件有问题,所以要等CSS加载完成后才启动App + link.addEventListener( + 'load', + () => { + TiTheme.bootstrapModule(AppModule); + }, + false + ); + link.addEventListener( + 'error', + () => { + TiTheme.bootstrapModule(AppModule); + }, + false + ); +} else { + TiTheme.bootstrapModule(AppModule); +} +``` + +### 步骤四:调用代码切换主题,详见 [theme](../components/theme) 用例 + +```typescript +import { TiTheme } from '@opentiny/ng'; + +... +TiTheme.loadThemeCss('blue', 'tiny3theme'); +... +``` + + + +## 网页里动态改变组件样式 + +JavaScript 操作 CSS 变量的写法如下:(Chrome、Firefox、Safari 原生支持) + +> IE 的兼容性补丁,暂时不支持 JavaScript 操作 CSS 变量,有可能后期会支持此能力。 + +```javascript +// 设置变量 +document.body.style.setProperty('--primary', '#7F583F'); + +// 读取变量 +document.body.style.getPropertyValue('--primary').trim(); +// '#7F583F' + +// 删除变量 +document.body.style.removeProperty('--primary'); +``` diff --git a/src/ng/demo/src/webdoc/validators.md b/src/ng/demo/src/webdoc/validators.md new file mode 100644 index 0000000..147233b --- /dev/null +++ b/src/ng/demo/src/webdoc/validators.md @@ -0,0 +1,42 @@ +--- +title: 表单校验规则 | TinyNG +--- + +## 说明 +本文介绍 TinyNG 实现的 `TiValidators` 的校验规则及校验方法。 + +## 用法介绍 +支持两种方式声明校验: + +- 1.静态方法:通过调用`TiValidators.${规则名称}`的方式,适用于响应式表单。如`required`声明为`TiValidators.required`。 +- 2.指令方式:通过`ti${规则名称}`指令声明,适用于模板驱动表单。如`required`规则的指令名为`tiRequired`。 + +## 校验规则 + +| 规则名称 | 参数类型 | 参数含义 | 规则说明 | +| :------- | -------: | -------: | -------: | +| required | - | - | 非空校验 | +| maxLength | number | 最大字符长度 | 字符长度最大值校验 | +| minLength | number | 最小字符长度 | 字符长度最小值校验 | +| rangeSize | number | 最小长度限制 | 字符长度大小区间校验 | +| maxValue | number | 最大数值 | 数字最大数值校验 | +| minValue | number | 最小数值 | 数字最小数值校验 | +| rangeValue | number | 最小数值限制 | 数字大小区间校验 | +| regExp | RegExp:string | 正则表达式参数 | 不包括正则表达式头尾标识符'^(?:'、')$' | +| email | - | - | 邮箱校验 | +| contains | string/number | 包含的内容 | 包含校验 | +| notContains | string/number | 不包含的内容 | 不包含校验 | +| equal | string/number/boolean | 相等的内容 | 相等校验。| +| notEqual | string/number | 不相等的内容 | 不相等校验 | +| notScript | - | - | 包含script标签校验 | +| port | - | - | 端口号校验,范围为0~65535 | +| date | - | - | 日期类型校验 | +| url | - | - | url校验 | +| integer | - | - | 整数校验 | +| number | - | - | 数字校验 | +| digits | - | - | 自然数校验 | +| ipv4 | - | - | ipv4校验 | +| ipv6 | - | - | ipv6校验 | +| minCharType | number/RegExp:string | 符合要求的字符种类/字符集对象类型
    默认的字符种类分别为:大写字母、小写字母、数字、特殊字符\`~!@#$%^&*()-_=+\|[{}];:'",<.>/? 和空格 | 符合最小字符种类校验,默认情况为至少包含2种字符类型 | +| notEqualPosRev | () => AbstractControl | 需要比对的表单formControl对象获取函数 | 不能和表单对象的正序或倒序相同 | +| password | TiPasswordValidatorConfig:Object | 密码校验各项规则参数 | 密码校验 | \ No newline at end of file diff --git a/src/ng/demo/test.ts b/src/ng/demo/test.ts new file mode 100644 index 0000000..5a8e0d5 --- /dev/null +++ b/src/ng/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('../../', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/ng/demo/tsconfig.app.json b/src/ng/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/ng/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/ng/demo/tsconfig.spec.json b/src/ng/demo/tsconfig.spec.json new file mode 100644 index 0000000..9c9ff4c --- /dev/null +++ b/src/ng/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "../../**/*.spec.ts", + "../../**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/ng/demolog/DemoLogComponent.ts b/src/ng/demolog/DemoLogComponent.ts new file mode 100644 index 0000000..c524caa --- /dev/null +++ b/src/ng/demolog/DemoLogComponent.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, SimpleChanges, ViewChild } from '@angular/core'; + +@Component({ + selector: 'demo-log', + templateUrl: './log.html', + styleUrls: ['./log.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DemoLogComponent { + @Input() logs: Array; + @ViewChild('log', { static: true }) logRef: ElementRef; + + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + ngOnChanges(changes: SimpleChanges): void { + if (changes['logs']) { + // 手动更新一次视图,使可以滚动到最底部 + this.changeDetectorRef.detectChanges(); + this.logRef.nativeElement.scrollTop = 100000; + } + } +} diff --git a/src/ng/demolog/DemoLogModule.ts b/src/ng/demolog/DemoLogModule.ts new file mode 100644 index 0000000..5299667 --- /dev/null +++ b/src/ng/demolog/DemoLogModule.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DemoLogComponent } from './DemoLogComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [DemoLogComponent], + declarations: [DemoLogComponent] +}) +export class DemoLogModule {} diff --git a/src/ng/demolog/log.html b/src/ng/demolog/log.html new file mode 100644 index 0000000..d66f3ab --- /dev/null +++ b/src/ng/demolog/log.html @@ -0,0 +1,3 @@ +
    +

    {{i + ' ' + log}}

    +
    diff --git a/src/ng/demolog/log.less b/src/ng/demolog/log.less new file mode 100644 index 0000000..e48c15c --- /dev/null +++ b/src/ng/demolog/log.less @@ -0,0 +1,12 @@ +.event-log-container { + max-height: 220px; + font-size: 12px; + overflow-y: auto; + padding: 8px 0 8px 8px; + margin-top: 8px; + background-color: rgba(243, 244, 246); + &:empty { + margin-top: 0px; + padding: 0px; + } +} diff --git a/src/ng/lib/index.ts b/src/ng/lib/index.ts new file mode 100644 index 0000000..ff297b4 --- /dev/null +++ b/src/ng/lib/index.ts @@ -0,0 +1,102 @@ +export * from '@opentiny/ng-accordion'; +export * from '@opentiny/ng-actionmenu'; +export * from '@opentiny/ng-alert'; +export * from '@opentiny/ng-anchor'; +export * from '@opentiny/ng-autocomplete'; +export * from '@opentiny/ng-avatar'; +export * from '@opentiny/ng-base'; +export * from '@opentiny/ng-button'; +export * from '@opentiny/ng-buttongroup'; +export * from '@opentiny/ng-card'; +export * from '@opentiny/ng-cascader'; +export * from '@opentiny/ng-checkbox'; +export * from '@opentiny/ng-collapse'; +export * from '@opentiny/ng-collapsebox'; +export * from '@opentiny/ng-collapsebutton'; +export * from '@opentiny/ng-copy'; +export * from '@opentiny/ng-crumb'; +export * from '@opentiny/ng-date'; +export * from '@opentiny/ng-datebase'; +export * from '@opentiny/ng-datedominator'; +export * from '@opentiny/ng-dateedit'; +export * from '@opentiny/ng-datepanel'; +export * from '@opentiny/ng-daterange'; +export * from '@opentiny/ng-datetime'; +export * from '@opentiny/ng-datetimerange'; +export * from '@opentiny/ng-dominator'; +export * from '@opentiny/ng-drag'; +export * from '@opentiny/ng-drop'; +export * from '@opentiny/ng-droplist'; +export * from '@opentiny/ng-dropsearch'; +export * from '@opentiny/ng-formfield'; +export * from '@opentiny/ng-foldtext'; +export * from '@opentiny/ng-grid'; +export * from '@opentiny/ng-halfmodal'; +export * from '@opentiny/ng-icon'; +export * from '@opentiny/ng-iconaction'; +export * from '@opentiny/ng-imagepreview'; +export * from '@opentiny/ng-include'; +export * from '@opentiny/ng-inputnumber'; +export * from '@opentiny/ng-intro'; +export * from '@opentiny/ng-ip'; +export * from '@opentiny/ng-ipsection'; +export * from '@opentiny/ng-layout'; +export * from '@opentiny/ng-leftmenu'; +export * from '@opentiny/ng-list'; +export * from '@opentiny/ng-loading'; +export * from '@opentiny/ng-locale'; +export * from '@opentiny/ng-linkbutton'; +export * from '@opentiny/ng-menu'; +export * from '@opentiny/ng-message'; +export * from '@opentiny/ng-modal'; +export * from '@opentiny/ng-nav'; +export * from '@opentiny/ng-notification'; +export * from '@opentiny/ng-outline'; +export * from '@opentiny/ng-overflow'; +export * from '@opentiny/ng-pagination'; +export * from '@opentiny/ng-popconfirm'; +export * from '@opentiny/ng-popup'; +export * from '@opentiny/ng-progressbar'; +export * from '@opentiny/ng-progresspie'; +export * from '@opentiny/ng-radio'; +export * from '@opentiny/ng-rate'; +export * from '@opentiny/ng-renderer'; +export * from '@opentiny/ng-rights'; +export * from '@opentiny/ng-score'; +export * from '@opentiny/ng-scroll'; +export * from '@opentiny/ng-searchbox'; +export * from '@opentiny/ng-select'; +export * from '@opentiny/ng-skeleton'; +export * from '@opentiny/ng-slider'; +export * from '@opentiny/ng-spinner'; +export * from '@opentiny/ng-steps'; +export * from '@opentiny/ng-subtitle'; +export * from '@opentiny/ng-swiper'; +export * from '@opentiny/ng-switch'; +export * from '@opentiny/ng-tab'; +export * from '@opentiny/ng-table'; +export * from '@opentiny/ng-tag'; +export * from '@opentiny/ng-tagsinput'; +export * from '@opentiny/ng-text'; +export * from '@opentiny/ng-textarea'; +export * from '@opentiny/ng-time'; +export * from '@opentiny/ng-timeline'; +export * from '@opentiny/ng-tip'; +export * from '@opentiny/ng-transfer'; +export * from '@opentiny/ng-tree'; +export * from '@opentiny/ng-treeselect'; +export * from '@opentiny/ng-upload'; +export * from '@opentiny/ng-utils'; +export * from '@opentiny/ng-validation'; +export * from '@opentiny/ng-zoom'; +export * from '@opentiny/ng-guides'; +export * from '@opentiny/ng-labeleditor'; +export * from '@opentiny/ng-phonenumber'; +export * from '@opentiny/ng-selectgroup'; +export * from '@opentiny/ng-productpreview'; +export * from '@opentiny/ng-buttonselect'; +export * from '@opentiny/ng-collapsetext'; + +export * from '@opentiny/ng-guidesteps'; + +export * from '@opentiny/ng-path'; diff --git a/src/ng/lib/ng-package.json b/src/ng/lib/ng-package.json new file mode 100644 index 0000000..e740ca5 --- /dev/null +++ b/src/ng/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/ng", + "lib": { + "entryFile": "index.ts" + } +} diff --git a/src/ng/lib/package.json b/src/ng/lib/package.json new file mode 100644 index 0000000..5936a90 --- /dev/null +++ b/src/ng/lib/package.json @@ -0,0 +1,110 @@ +{ + "name": "@opentiny/ng", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-accordion": "1.0.0-beta.0", + "@opentiny/ng-actionmenu": "1.0.0-beta.0", + "@opentiny/ng-alert": "1.0.0-beta.0", + "@opentiny/ng-anchor": "1.0.0-beta.0", + "@opentiny/ng-autocomplete": "1.0.0-beta.0", + "@opentiny/ng-avatar": "1.0.0-beta.0", + "@opentiny/ng-base": "1.0.0-beta.0", + "@opentiny/ng-button": "1.0.0-beta.0", + "@opentiny/ng-buttongroup": "1.0.0-beta.0", + "@opentiny/ng-card": "1.0.0-beta.0", + "@opentiny/ng-cascader": "1.0.0-beta.0", + "@opentiny/ng-checkbox": "1.0.0-beta.0", + "@opentiny/ng-collapse": "1.0.0-beta.0", + "@opentiny/ng-collapsebox": "1.0.0-beta.0", + "@opentiny/ng-collapsebutton": "1.0.0-beta.0", + "@opentiny/ng-copy": "1.0.0-beta.0", + "@opentiny/ng-crumb": "1.0.0-beta.0", + "@opentiny/ng-date": "1.0.0-beta.0", + "@opentiny/ng-datebase": "1.0.0-beta.0", + "@opentiny/ng-datedominator": "1.0.0-beta.0", + "@opentiny/ng-dateedit": "1.0.0-beta.0", + "@opentiny/ng-datepanel": "1.0.0-beta.0", + "@opentiny/ng-daterange": "1.0.0-beta.0", + "@opentiny/ng-datetime": "1.0.0-beta.0", + "@opentiny/ng-datetimerange": "1.0.0-beta.0", + "@opentiny/ng-dominator": "1.0.0-beta.0", + "@opentiny/ng-drag": "1.0.0-beta.0", + "@opentiny/ng-drop": "1.0.0-beta.0", + "@opentiny/ng-droplist": "1.0.0-beta.0", + "@opentiny/ng-dropsearch": "1.0.0-beta.0", + "@opentiny/ng-formfield": "1.0.0-beta.0", + "@opentiny/ng-foldtext": "1.0.0-beta.0", + "@opentiny/ng-grid": "1.0.0-beta.0", + "@opentiny/ng-halfmodal": "1.0.0-beta.0", + "@opentiny/ng-icon": "1.0.0-beta.0", + "@opentiny/ng-iconaction": "1.0.0-beta.0", + "@opentiny/ng-imagepreview": "1.0.0-beta.0", + "@opentiny/ng-include": "1.0.0-beta.0", + "@opentiny/ng-inputnumber": "1.0.0-beta.0", + "@opentiny/ng-intro": "1.0.0-beta.0", + "@opentiny/ng-ip": "1.0.0-beta.0", + "@opentiny/ng-ipsection": "1.0.0-beta.0", + "@opentiny/ng-layout": "1.0.0-beta.0", + "@opentiny/ng-leftmenu": "1.0.0-beta.0", + "@opentiny/ng-list": "1.0.0-beta.0", + "@opentiny/ng-loading": "1.0.0-beta.0", + "@opentiny/ng-locale": "1.0.0-beta.0", + "@opentiny/ng-linkbutton": "1.0.0-beta.0", + "@opentiny/ng-menu": "1.0.0-beta.0", + "@opentiny/ng-message": "1.0.0-beta.0", + "@opentiny/ng-modal": "1.0.0-beta.0", + "@opentiny/ng-nav": "1.0.0-beta.0", + "@opentiny/ng-notification": "1.0.0-beta.0", + "@opentiny/ng-outline": "1.0.0-beta.0", + "@opentiny/ng-overflow": "1.0.0-beta.0", + "@opentiny/ng-pagination": "1.0.0-beta.0", + "@opentiny/ng-popconfirm": "1.0.0-beta.0", + "@opentiny/ng-popup": "1.0.0-beta.0", + "@opentiny/ng-progressbar": "1.0.0-beta.0", + "@opentiny/ng-progresspie": "1.0.0-beta.0", + "@opentiny/ng-radio": "1.0.0-beta.0", + "@opentiny/ng-rate": "1.0.0-beta.0", + "@opentiny/ng-renderer": "1.0.0-beta.0", + "@opentiny/ng-rights": "1.0.0-beta.0", + "@opentiny/ng-score": "1.0.0-beta.0", + "@opentiny/ng-scroll": "1.0.0-beta.0", + "@opentiny/ng-searchbox": "1.0.0-beta.0", + "@opentiny/ng-select": "1.0.0-beta.0", + "@opentiny/ng-skeleton": "1.0.0-beta.0", + "@opentiny/ng-slider": "1.0.0-beta.0", + "@opentiny/ng-spinner": "1.0.0-beta.0", + "@opentiny/ng-steps": "1.0.0-beta.0", + "@opentiny/ng-subtitle": "1.0.0-beta.0", + "@opentiny/ng-swiper": "1.0.0-beta.0", + "@opentiny/ng-switch": "1.0.0-beta.0", + "@opentiny/ng-tab": "1.0.0-beta.0", + "@opentiny/ng-table": "1.0.0-beta.0", + "@opentiny/ng-tag": "1.0.0-beta.0", + "@opentiny/ng-tagsinput": "1.0.0-beta.0", + "@opentiny/ng-text": "1.0.0-beta.0", + "@opentiny/ng-textarea": "1.0.0-beta.0", + "@opentiny/ng-time": "1.0.0-beta.0", + "@opentiny/ng-timeline": "1.0.0-beta.0", + "@opentiny/ng-tip": "1.0.0-beta.0", + "@opentiny/ng-transfer": "1.0.0-beta.0", + "@opentiny/ng-tree": "1.0.0-beta.0", + "@opentiny/ng-treeselect": "1.0.0-beta.0", + "@opentiny/ng-upload": "1.0.0-beta.0", + "@opentiny/ng-utils": "1.0.0-beta.0", + "@opentiny/ng-validation": "1.0.0-beta.0", + "@opentiny/ng-zoom": "1.0.0-beta.0", + "@opentiny/ng-themes": "1.0.0-beta.0", + "@opentiny/ng-guides": "1.0.0-beta.0", + "@opentiny/ng-labeleditor": "1.0.0-beta.0", + "@opentiny/ng-phonenumber": "1.0.0-beta.0", + "@opentiny/ng-selectgroup": "1.0.0-beta.0", + "@opentiny/ng-productpreview": "1.0.0-beta.0", + "@opentiny/ng-buttonselect": "1.0.0-beta.0", + "@opentiny/ng-collapsetext": "1.0.0-beta.0", + "@opentiny/ng-guidesteps": "1.0.0-beta.0", + "@opentiny/ng-path": "1.0.0-beta.0" + } +} diff --git a/src/ng/lib/project.json b/src/ng/lib/project.json new file mode 100644 index 0000000..5486061 --- /dev/null +++ b/src/ng/lib/project.json @@ -0,0 +1,49 @@ +{ + "projectType": "library", + "root": "src/ng/lib", + "sourceRoot": "src/ng/lib", + "targets": { + "build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/ng"], + "options": { + "project": "src/ng/lib/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "dependsOn": [ + { + "target": "pack", + "projects": "dependencies" + } + ], + "options": { + "commands": [ + { + "command": "cd dist/libs/ng && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build ng && ng pack ng && node build/publish.js ng --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/notification/demo/karma.conf.js b/src/notification/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/notification/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/notification/demo/project.json b/src/notification/demo/project.json new file mode 100644 index 0000000..f867611 --- /dev/null +++ b/src/notification/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/notification/demo", + "sourceRoot": "src/notification/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/notification", + "index": "src/notification/demo/src/index.html", + "main": "src/notification/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/notification/demo/tsconfig.app.json", + "assets": ["src/notification/demo/src/favicon.ico", "src/notification/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "notification-demo:build:production" + }, + "development": { + "browserTarget": "notification-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js notification" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/notification/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/notification/demo/tsconfig.spec.json", + "karmaConfig": "src/notification/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/notification/demo/src/app/AppComponent.ts b/src/notification/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/notification/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/notification/demo/src/app/AppModule.ts b/src/notification/demo/src/app/AppModule.ts new file mode 100644 index 0000000..c6f3864 --- /dev/null +++ b/src/notification/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { NotificationTestModule } from './notification/NotificationTestModule'; + +@NgModule({ + imports: [ + NotificationTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/notification/demo/src/app/IndexComponent.ts b/src/notification/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..4372fef --- /dev/null +++ b/src/notification/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { NotificationTestModule } from './notification/NotificationTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = NotificationTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/notification/demo/src/app/app.html b/src/notification/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/notification/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/notification/demo/src/app/notification/NotificationAnimationComponent.ts b/src/notification/demo/src/app/notification/NotificationAnimationComponent.ts new file mode 100644 index 0000000..8854d23 --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationAnimationComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-animation.html' +}) +export class NotificationAnimationComponent { + constructor(private tiNotification: TiNotificationService) {} + + onClickAnimation(): void { + this.tiNotification.simple('TinyNG 为 Web 应用提供了丰富的基础 UI 组件'); + } + + onClickWithoutAnimation(): void { + this.tiNotification.simple('这是一个关闭进场、出场动画的通知', { + animation: false + }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationBasicComponent.ts b/src/notification/demo/src/app/notification/NotificationBasicComponent.ts new file mode 100644 index 0000000..fdef96e --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationBasicComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-basic.html' +}) +export class NotificationBasicComponent { + constructor(private tiNotification: TiNotificationService) {} + + onClickOpen(): void { + this.tiNotification.simple('TinyNG 为 Web 应用提供了丰富的基础 UI 组件'); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationCloseComponent.ts b/src/notification/demo/src/app/notification/NotificationCloseComponent.ts new file mode 100644 index 0000000..72c04d5 --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationCloseComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'notification-close.html' +}) +export class NotificationCloseComponent { + constructor(private tiNotification: TiNotificationService) {} + private static noticName: string = 'notification-close-demo'; + target: any; + noticeCount: number = 0; + + onClickOpen(): void { + this.tiNotification.simple(`${this.noticeCount++}: TinyNG 为 Web 应用提供了丰富的基础 UI 组件`); + } + onClickCloseAll(): void { + this.tiNotification.closeAll(); + } + onClickOpenSingle(): void { + let notice: any = this.tiNotification.simple('NotificationService 返回一个通知实例,通过调用实例的 close 方法来关闭它', { + duration: 0, + name: NotificationCloseComponent.noticName + }); + this.target = notice; + } + onClickCloseSingle(): void { + this.target.close(); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationConfigComponent.ts b/src/notification/demo/src/app/notification/NotificationConfigComponent.ts new file mode 100644 index 0000000..3ef891c --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationConfigComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: './notification-config.html' +}) +export class NotificationConfigComponent { + constructor(private tiNotification: TiNotificationService) { + this.tiNotification.config({ top: '100px' }); + } + + onClickOpen(): void { + this.tiNotification.simple('TinyNG 为 Web 应用提供了丰富的基础 UI 组件', { + duration: 0 + }); + this.tiNotification.simple('TinyNG 为 Web 应用提供了丰富的基础 UI 组件', { + position: 'bottom-right', + duration: 0 + }); + } + onClickChangeTop(): void { + this.tiNotification.config({ top: '200px' }); + } + onClickChangeBottom(): void { + this.tiNotification.config({ bottom: '100px' }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationDurationComponent.ts b/src/notification/demo/src/app/notification/NotificationDurationComponent.ts new file mode 100644 index 0000000..ac78176 --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationDurationComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-duration.html' +}) +export class NotificationDurationComponent { + constructor(private tiNotification: TiNotificationService) {} + + onClickDeafult(): void { + this.tiNotification.simple('通知弹窗默认会在 4.5 秒后自动关闭'); + } + + onClickCustomDuring(): void { + this.tiNotification.simple('将 duration 设置为 6000,通知弹窗将在 6 秒后自动关闭', { duration: 6000 }); + } + + onClickWontAutoClose(): void { + this.tiNotification.simple('如果将 duration 设置为 0,通知弹窗将不自动关闭', { duration: 0 }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationEventsComponent.ts b/src/notification/demo/src/app/notification/NotificationEventsComponent.ts new file mode 100644 index 0000000..245d6a4 --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationEventsComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; + +@Component({ + templateUrl: 'notification-events.html' +}) +export class NotificationEventsComponent { + constructor(private tiNotification: TiNotificationService) {} + + myLogs: Array = []; + + onClickCloseCallback(): void { + this.tiNotification.simple('通过 onClose 方法定义通知关闭时的回调函数,布尔类型的入参表示是否为用户点击关闭按钮关闭', { + onClose: () => { + this.myLogs = [...this.myLogs, `通知弹窗关闭了`]; + } + }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationHoverPauseComponent.ts b/src/notification/demo/src/app/notification/NotificationHoverPauseComponent.ts new file mode 100644 index 0000000..30035c8 --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationHoverPauseComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-hover-pause.html' +}) +export class NotificationHoverPauseComponent { + constructor(private tiNotification: TiNotificationService) {} + + onClickPause(): void { + this.tiNotification.simple('TinyNG 为 Web 应用提供了丰富的基础 UI 组件'); + } + + onClickContinue(): void { + this.tiNotification.simple('TinyNG 为 Web 应用提供了丰富的基础 UI 组件', { + hoverPause: false + }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationNameComponent.ts b/src/notification/demo/src/app/notification/NotificationNameComponent.ts new file mode 100644 index 0000000..d2c458c --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationNameComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-name.html' +}) +export class NotificationNameComponent { + constructor(private tiNotification: TiNotificationService) {} + + private static noticName: string = 'tiny'; + + onClickOpen(): void { + this.tiNotification.simple('我是原来的通知内容', { + name: NotificationNameComponent.noticName, + duration: 0 + }); + } + onClickChange(): void { + this.tiNotification.success('我是被改变后的内容', { + name: NotificationNameComponent.noticName, + duration: 0 + }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationPositionComponent.ts b/src/notification/demo/src/app/notification/NotificationPositionComponent.ts new file mode 100644 index 0000000..30b600d --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationPositionComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-position.html', + styles: [ + '.demo-notify-position-container {width: 320px; display: grid; grid-template-columns: repeat(3, 1fr); grid-gap: 10px;}', + '.demo-notify-position-container > div {width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;}' + ] +}) +export class NotificationPositionComponent { + constructor(private tiNotification: TiNotificationService) {} + private static content: string = 'TinyNG 为 Web 应用提供了丰富的基础 UI 组件'; + + onClickTopLeft(): void { + this.tiNotification.simple(NotificationPositionComponent.content, { + position: 'top-left' + }); + } + + onClickTop(): void { + this.tiNotification.simple(NotificationPositionComponent.content, { + position: 'top' + }); + } + + onClickTopRight(): void { + this.tiNotification.simple(NotificationPositionComponent.content, { + position: 'top-right' + }); + } + + onClickBottomLeft(): void { + this.tiNotification.simple(NotificationPositionComponent.content, { + position: 'bottom-left' + }); + } + + onClickBottom(): void { + this.tiNotification.simple(NotificationPositionComponent.content, { + position: 'bottom' + }); + } + + onClickBottomRight(): void { + this.tiNotification.simple(NotificationPositionComponent.content, { + position: 'bottom-right' + }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationTemplateComponent.ts b/src/notification/demo/src/app/notification/NotificationTemplateComponent.ts new file mode 100644 index 0000000..27e5b1f --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationTemplateComponent.ts @@ -0,0 +1,14 @@ +import { Component, TemplateRef } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-template.html' +}) +export class NotificationTemplateComponent { + constructor(private tiNotification: TiNotificationService) {} + + value: number = 5; + + onClickOpen(template: TemplateRef): void { + this.tiNotification.template(template, { duration: 0 }); + } +} diff --git a/src/notification/demo/src/app/notification/NotificationTestModule.ts b/src/notification/demo/src/app/notification/NotificationTestModule.ts new file mode 100644 index 0000000..4c2f234 --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationTestModule.ts @@ -0,0 +1,99 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; +import { TiButtonModule, TiNotificationModule, TiCardModule, TiRateModule, TiNotificationService } from '@opentiny/ng'; +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { NotificationBasicComponent } from './NotificationBasicComponent'; +import { NotificationCloseComponent } from './NotificationCloseComponent'; +import { NotificationPositionComponent } from './NotificationPositionComponent'; +import { NotificationDurationComponent } from './NotificationDurationComponent'; +import { NotificationNameComponent } from './NotificationNameComponent'; +import { NotificationAnimationComponent } from './NotificationAnimationComponent'; +import { NotificationTemplateComponent } from './NotificationTemplateComponent'; +import { NotificationTypeComponent } from './NotificationTypeComponent'; +import { NotificationEventsComponent } from './NotificationEventsComponent'; +import { NotificationConfigComponent } from './NotificationConfigComponent'; + +@NgModule({ + imports: [ + DemoLogModule, + CommonModule, + FormsModule, + TiNotificationModule, + TiButtonModule, + TiCardModule, + TiRateModule, + RouterModule.forChild(NotificationTestModule.ROUTES) + ], + declarations: [ + NotificationBasicComponent, + NotificationCloseComponent, + NotificationPositionComponent, + NotificationDurationComponent, + NotificationNameComponent, + NotificationAnimationComponent, + NotificationTemplateComponent, + NotificationTypeComponent, + NotificationEventsComponent, + NotificationConfigComponent + ] +}) +export class NotificationTestModule { + constructor(private tiNotification: TiNotificationService) { + this.tiNotification.config({ top: '84px' }); + } + + static readonly ROUTES: Routes = [ + { + path: 'notification/notification-basic', + component: NotificationBasicComponent, + data: { label: 'basic' } + }, + { + path: 'notification/notification-position', + component: NotificationPositionComponent, + data: { label: 'position' } + }, + { + path: 'notification/notification-duration', + component: NotificationDurationComponent, + data: { label: 'duration' } + }, + { + path: 'notification/notification-name', + component: NotificationNameComponent, + data: { label: 'name' } + }, + { + path: 'notification/notification-animation', + component: NotificationAnimationComponent, + data: { label: 'animation' } + }, + { + path: 'notification/notification-template', + component: NotificationTemplateComponent, + data: { label: 'template' } + }, + { + path: 'notification/notification-type', + component: NotificationTypeComponent, + data: { label: 'type' } + }, + { + path: 'notification/notification-events', + component: NotificationEventsComponent, + data: { label: 'events' } + }, + { + path: 'notification/notification-close', + component: NotificationCloseComponent, + data: { label: 'close' } + }, + { + path: 'notification/notification-config', + component: NotificationConfigComponent, + data: { label: 'config' } + } + ]; +} diff --git a/src/notification/demo/src/app/notification/NotificationTypeComponent.ts b/src/notification/demo/src/app/notification/NotificationTypeComponent.ts new file mode 100644 index 0000000..48c5a5c --- /dev/null +++ b/src/notification/demo/src/app/notification/NotificationTypeComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { TiNotificationService } from '@opentiny/ng'; +@Component({ + templateUrl: 'notification-type.html' +}) +export class NotificationTypeComponent { + constructor(private tiNotification: TiNotificationService) {} + private static content: string = 'TinyNG 为 Web 应用提供了丰富的基础 UI 组件'; + + onClickSuccess(): void { + this.tiNotification.success(NotificationTypeComponent.content); + } + onClickPrompt(): void { + this.tiNotification.prompt(NotificationTypeComponent.content); + } + onClickWarn(): void { + this.tiNotification.warn(NotificationTypeComponent.content); + } + onClickError(): void { + this.tiNotification.error(NotificationTypeComponent.content); + } + onClickSimple(): void { + this.tiNotification.simple(NotificationTypeComponent.content); + } +} diff --git a/src/notification/demo/src/app/notification/notification-animation.html b/src/notification/demo/src/app/notification/notification-animation.html new file mode 100644 index 0000000..f39a9a6 --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-animation.html @@ -0,0 +1,2 @@ + + diff --git a/src/notification/demo/src/app/notification/notification-basic.html b/src/notification/demo/src/app/notification/notification-basic.html new file mode 100644 index 0000000..15d5b9b --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-basic.html @@ -0,0 +1 @@ + diff --git a/src/notification/demo/src/app/notification/notification-close.html b/src/notification/demo/src/app/notification/notification-close.html new file mode 100644 index 0000000..dd2a79a --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-close.html @@ -0,0 +1,5 @@ + + +
    + + diff --git a/src/notification/demo/src/app/notification/notification-config.html b/src/notification/demo/src/app/notification/notification-config.html new file mode 100644 index 0000000..d461b0f --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-config.html @@ -0,0 +1,3 @@ + + + diff --git a/src/notification/demo/src/app/notification/notification-duration.html b/src/notification/demo/src/app/notification/notification-duration.html new file mode 100644 index 0000000..553d41a --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-duration.html @@ -0,0 +1,3 @@ + + + diff --git a/src/notification/demo/src/app/notification/notification-events.html b/src/notification/demo/src/app/notification/notification-events.html new file mode 100644 index 0000000..10dd551 --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-events.html @@ -0,0 +1 @@ + diff --git a/src/notification/demo/src/app/notification/notification-hover-pause.html b/src/notification/demo/src/app/notification/notification-hover-pause.html new file mode 100644 index 0000000..184a855 --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-hover-pause.html @@ -0,0 +1,2 @@ + + diff --git a/src/notification/demo/src/app/notification/notification-name.html b/src/notification/demo/src/app/notification/notification-name.html new file mode 100644 index 0000000..481de58 --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-name.html @@ -0,0 +1,2 @@ + + diff --git a/src/notification/demo/src/app/notification/notification-position.html b/src/notification/demo/src/app/notification/notification-position.html new file mode 100644 index 0000000..6853e34 --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-position.html @@ -0,0 +1,8 @@ +
    + + + + + + +
    diff --git a/src/notification/demo/src/app/notification/notification-template.html b/src/notification/demo/src/app/notification/notification-template.html new file mode 100644 index 0000000..73cc364 --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-template.html @@ -0,0 +1,12 @@ + + + + +

    请在下方选择评分

    +
    +
    + +
    + +
    +
    diff --git a/src/notification/demo/src/app/notification/notification-type.html b/src/notification/demo/src/app/notification/notification-type.html new file mode 100644 index 0000000..6d7c0c0 --- /dev/null +++ b/src/notification/demo/src/app/notification/notification-type.html @@ -0,0 +1,5 @@ + + + + + diff --git a/src/notification/demo/src/app/notification/webdoc/notification-demos.js b/src/notification/demo/src/app/notification/webdoc/notification-demos.js new file mode 100644 index 0000000..48e7465 --- /dev/null +++ b/src/notification/demo/src/app/notification/webdoc/notification-demos.js @@ -0,0 +1,146 @@ +export default { + column: '2', + demos: [ + { + demoId: 'notification-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    Notification 组件的最简用法。

    ', + 'en-US': '

    ' + } + }, + { + demoId: 'notification-type', + name: { + 'zh-CN': '通知类型', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': + '

    通过调用TiNotificationService的不同方法来弹出不同类型的通知,包括:successpromptwarnerrorsimple

    ', + 'en-US': '

    ' + }, + apis: [ + 'TiNotificationService.methods.success', + 'TiNotificationService.methods.prompt', + 'TiNotificationService.methods.warn', + 'TiNotificationService.methods.error', + 'TiNotificationService.methods.simple' + ] + }, + { + demoId: 'notification-template', + name: { + 'zh-CN': '自定义内容', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    通过调用TiNotificationServicetemplate方法来自定义通知弹窗内容。

    ', + 'en-US': '

    ' + }, + apis: ['TiNotificationService.methods.template'] + }, + { + demoId: 'notification-duration', + name: { + 'zh-CN': '自动关闭时间', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    通过属性duration配置通知自动关闭的时间,单位为毫秒;注意:当设置为 0 时表示不自动关闭。

    ', + 'en-US': '

    ' + }, + apis: ['TiNoticeConfig.properties.duration'] + }, + { + demoId: 'notification-position', + name: { + 'zh-CN': '弹出位置', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': + '

    通过属性position配置弹出位置,包括top-righttoptop-leftbottom-leftbottombottom-right

    ', + 'en-US': '

    ' + }, + apis: ['TiNoticeConfig.properties.position'] + }, + { + demoId: 'notification-animation', + name: { + 'zh-CN': '禁用动画', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    通过属性animation配置是否使用动画,默认开启动画,当配置为 false 禁用动画。

    ', + 'en-US': '

    ' + }, + apis: ['TiNoticeConfig.properties.animation'] + }, + { + demoId: 'notification-hover-pause', + name: { + 'zh-CN': '鼠标悬停时暂停自动关闭计时', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    通过属性hoverPause配置是否在鼠标悬停在通知弹窗上时暂停自动关闭的计时。

    ', + 'en-US': '

    ' + }, + apis: ['TiNoticeConfig.properties.hoverPause'] + }, + { + demoId: 'notification-name', + name: { + 'zh-CN': '更新消息内容', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    通过属性name配置通知弹窗标识,后续通过唯一的name更新通知弹窗内容。

    ', + 'en-US': '

    ' + }, + apis: ['TiNoticeConfig.properties.name'] + }, + { + demoId: 'notification-events', + name: { + 'zh-CN': '事件', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    通过属性onClose配置通知弹窗关闭时的回调函数。

    ', + 'en-US': '

    ' + }, + apis: ['TiNoticeConfig.properties.onClose'] + }, + { + demoId: 'notification-close', + name: { + 'zh-CN': '关闭通知弹窗', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': + '

    通过属性TiNotificationService的打开通知弹窗方法,会返回一个可关闭对象,调用这个对象的close方法来关闭对应的通知弹窗。同时,你也可以通过TiNotificationService.closeAll()来关闭

    ', + 'en-US': '

    ' + }, + apis: ['TiNotificationService.methods.closeAll'] + }, + { + demoId: 'notification-config', + name: { + 'zh-CN': '配置弹窗距顶底距离', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': + '

    通过调用TiNotificationServiceconfig方法来配置通知弹窗距离顶部或底部的距离,接受配置:topbottom

    ', + 'en-US': '

    ' + }, + apis: ['TiNotificationService.methods.config'] + } + ] +}; diff --git a/src/notification/demo/src/app/notification/webdoc/notification.cn.md b/src/notification/demo/src/app/notification/webdoc/notification.cn.md new file mode 100644 index 0000000..0a66328 --- /dev/null +++ b/src/notification/demo/src/app/notification/webdoc/notification.cn.md @@ -0,0 +1,23 @@ +--- +title: Notification 通知 +--- +# Notification 通知 + +
    + +Notification 是提供通知弹窗的组件。悬浮出现在页面角落,显示全局的通知提醒消息。   + ++ 弹出框组件提供服务方式供业务使用,使用该服务时需要引入模块`TiNotificationModule`,开发者通过调用`TiNotificationService.simple`方法生成弹出框。 + +```typescript +import { TiNotificationModule } from '@opentiny/ng'; +``` + +需要在项目中(建议在根模块)中导入`BrowserAnimationsModule`。 + +```typescript +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +``` +
    + +
    diff --git a/src/notification/demo/src/app/notification/webdoc/notification.en.md b/src/notification/demo/src/app/notification/webdoc/notification.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/notification/demo/src/app/notification/webdoc/notification.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/notification/demo/src/favicon.ico b/src/notification/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/notification/demo/src/index.html b/src/notification/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/notification/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/notification/demo/src/main.ts b/src/notification/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/notification/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/notification/demo/test.ts b/src/notification/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/notification/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/notification/demo/tsconfig.app.json b/src/notification/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/notification/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/notification/demo/tsconfig.spec.json b/src/notification/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/notification/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/notification/lib/index.ts b/src/notification/lib/index.ts new file mode 100644 index 0000000..8efa227 --- /dev/null +++ b/src/notification/lib/index.ts @@ -0,0 +1,5 @@ +export * from './src/TiNotificationComponent'; +export * from './src/TiNotificationModule'; +export * from './src/TiNotificationInterface'; +export * from './src/TiNotificationService'; +export * from './src/TiNotificationContainerComponent'; diff --git a/src/notification/lib/ng-package.json b/src/notification/lib/ng-package.json new file mode 100644 index 0000000..f701fe4 --- /dev/null +++ b/src/notification/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/notification", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/notification/lib/package.json b/src/notification/lib/package.json new file mode 100644 index 0000000..cae90dd --- /dev/null +++ b/src/notification/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "@opentiny/ng-notification", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/animations": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/cdk/overlay": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-alert": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@angular/cdk/portal": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/notification/lib/project.json b/src/notification/lib/project.json new file mode 100644 index 0000000..2498b39 --- /dev/null +++ b/src/notification/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/notification/lib", + "sourceRoot": "src/notification/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/notification"], + "options": { + "project": "src/notification/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/notification"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js notification" + }, + { + "command": "ng default-build notification" + }, + { + "command": "node build/clear-default-theme.js notification" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/notification && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build notification && ng pack notification && node build/publish.js notification --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/notification/lib/src/TiNotificationComponent.html b/src/notification/lib/src/TiNotificationComponent.html new file mode 100644 index 0000000..b27e037 --- /dev/null +++ b/src/notification/lib/src/TiNotificationComponent.html @@ -0,0 +1,19 @@ +
    + {{instance.content}} + + + + + +
    diff --git a/src/notification/lib/src/TiNotificationComponent.ts b/src/notification/lib/src/TiNotificationComponent.ts new file mode 100644 index 0000000..52b81bf --- /dev/null +++ b/src/notification/lib/src/TiNotificationComponent.ts @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { AnimationEvent } from '@angular/animations'; +import { + Component, + ViewEncapsulation, + ChangeDetectorRef, + EventEmitter, + Input, + Output, + OnDestroy, + ChangeDetectionStrategy +} from '@angular/core'; +import { filter, take } from 'rxjs/operators'; + +import { TiNoticeData, TiNoticeConfig, TiTimerType, TiNotificationPosition } from './TiNotificationInterface'; +import { notificationMotion } from './TiNotificationMotion'; + +@Component({ + selector: 'ti-notification', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + styleUrls: ['./notification.less'], + animations: [notificationMotion], + templateUrl: './TiNotificationComponent.html' +}) +export class TiNotificationComponent implements OnDestroy { + config!: Required; + private autoDestroy?: boolean = true; + private timerStamp?: number; + private duration!: number; + private destroyTimer: TiTimerType; + readonly animationEvtEmt: EventEmitter = new EventEmitter(); + + @Input() instance: Required; + @Input() position: TiNotificationPosition; + @Output() readonly destroyed = new EventEmitter<{ id: string }>(); + + constructor(public cdr: ChangeDetectorRef) {} + + private static timerCleaner(targetTimer: TiTimerType): void { + if (targetTimer !== null) { + clearTimeout(targetTimer); + targetTimer = null; + } + } + + ngOnInit(): void { + this.config = this.instance.config as Required; + this.duration = this.config.duration; + if (this.duration === 0) { + this.autoDestroy = false; + } else { + this.timerStamp = Date.now(); + this.startTimer(); + } + if (this.config.animation) { + this.instance.state = 'enter'; + this.animationEvtEmt + .pipe( + filter((evt) => evt.toState === 'leave' && evt.phaseName === 'done'), + take(1) + ) + .subscribe(() => { + this.destroyed.next({ id: this.instance.noticeId }); + }); + } + } + + private startTimer(): void { + if (this.duration > 0) { + TiNotificationComponent.timerCleaner(this.destroyTimer); + this.destroyTimer = setTimeout(() => this.destroy(), this.duration); + this.timerStamp = Date.now(); + } else { + this.destroy(); + } + } + + ngOnDestroy(): void { + if (this.autoDestroy) { + TiNotificationComponent.timerCleaner(this.destroyTimer); + } + this.animationEvtEmt.complete(); + } + + private destroy(): void { + if (this.config.animation) { + this.instance.state = 'leave'; + this.cdr.detectChanges(); + } else { + this.destroyed.next({ id: this.instance.noticeId }); + } + } + + onMouseEnter(): void { + if (this.autoDestroy && this.config.hoverPause) { + TiNotificationComponent.timerCleaner(this.destroyTimer); + this.duration = this.timerStamp + this.duration - Date.now(); + } + } + + onMouseLeave(): void { + if (this.autoDestroy && this.config.hoverPause) { + this.startTimer(); + } + } + + get state(): string | undefined { + if (this.instance.state === 'enter') { + switch (this.position) { + case 'top-right': + case 'bottom-right': + return 'rightEnter'; + case 'top': + return 'topEnter'; + case 'top-left': + case 'bottom-left': + return 'leftEnter'; + case 'bottom': + return 'bottomEnter'; + default: + return 'rightEnter'; + } + } else { + return this.instance.state; + } + } + + alertOpenStateChangeHandle(openState: boolean): void { + if (!openState) { + this.destroy(); + } + } +} diff --git a/src/notification/lib/src/TiNotificationContainerComponent.html b/src/notification/lib/src/TiNotificationContainerComponent.html new file mode 100644 index 0000000..1c493ac --- /dev/null +++ b/src/notification/lib/src/TiNotificationContainerComponent.html @@ -0,0 +1,48 @@ +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    diff --git a/src/notification/lib/src/TiNotificationContainerComponent.ts b/src/notification/lib/src/TiNotificationContainerComponent.ts new file mode 100644 index 0000000..be54ad0 --- /dev/null +++ b/src/notification/lib/src/TiNotificationContainerComponent.ts @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ViewEncapsulation, QueryList, ViewChildren } from '@angular/core'; + +import { TiNoticeBasicConfig, TiNoticeData, TiNotificationRef, TiNotificationConfig } from './TiNotificationInterface'; + +import { TiNotificationComponent } from './TiNotificationComponent'; +import { Util } from '@opentiny/ng-utils'; + +@Component({ + selector: 'ti-notification-container', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + styleUrls: ['./notification.less'], + templateUrl: './TiNotificationContainerComponent.html' +}) +export class TiNotificationContainerComponent { + @ViewChildren(TiNotificationComponent) noticeInstances: QueryList; + + topRightNotices: Array> = []; + topNotices: Array> = []; + topLeftNotices: Array> = []; + bottomLeftNotices: Array> = []; + bottomNotices: Array> = []; + bottomRightNotices: Array> = []; + notices: Array> = []; + notificationConfig: TiNotificationConfig = {}; + consoleHeaderHeight: number = 0; // console头部高度 + + private onHeadChange: (e: CustomEvent) => void; // console头部高度变化时触发 + private consoleDataService: any; + + static TI_NOTIFICATION_DEFAULT_CONFIG: TiNoticeBasicConfig = { + animation: true, + duration: 4500, + hoverPause: true, + position: 'top-right' + }; + + constructor(private cdr: ChangeDetectorRef) {} + + setConsoleHeader(): void { + const consoleContext = (window).getConsoleContext && (window).getConsoleContext(); + this.consoleDataService = consoleContext?.get && consoleContext.get({ name: 'safearea' }); + this.onHeadChange = (e: CustomEvent) => { + this.consoleHeaderHeight = e.detail.top; + this.notificationConfig.top = `calc(var(--ti-common-space-6x) + ${this.consoleHeaderHeight}px)`; + this.reloadNotices(); + }; + // 头部高度变化会触发此事件 + if (this.consoleDataService?.onChange) { + this.consoleDataService.onChange(this.onHeadChange); + } + // console初始化首次进来不会触发consoleDataService.onChange,需要根据getSafeArea()设置一次。 + if (this.consoleDataService?.getSafeArea) { + const safeArea: any = this.consoleDataService.getSafeArea(); + this.consoleHeaderHeight = safeArea.top; + this.notificationConfig.top = `calc(var(--ti-common-space-6x) + ${this.consoleHeaderHeight}px)`; + } + } + + create(userConfig: TiNoticeData): TiNotificationRef { + let targetNotice = this.fixConfig(userConfig); + const existedNotice = this.notices.find((notice) => notice.config.name === targetNotice.config.name); + if (targetNotice.config.name && existedNotice) { + targetNotice = this.updateNotice(targetNotice, existedNotice); + this.markChildChanged(targetNotice.config.name); + } else { + this.bindApis(targetNotice); + this.notices = [...this.notices, targetNotice]; + } + this.reloadNotices(); + const { close } = targetNotice; + return { close } as TiNotificationRef; + } + + updateNotice(targetNotice: TiNoticeData, existedNotice: Required): Required { + existedNotice.content = targetNotice.content; + existedNotice.template = targetNotice.template; + existedNotice.type = targetNotice.type; + existedNotice.config = targetNotice.config; + return existedNotice; + } + + private markChildChanged(name: string): void { + const existedNotice = this.noticeInstances.find((notice) => notice.instance.config.name === name); + if (existedNotice) { + existedNotice.cdr.detectChanges(); + } + } + + private bindApis(targetNotice: Required): void { + targetNotice.close = () => this.close(targetNotice.noticeId); + } + + private reloadNotices(): void { + this.topRightNotices = []; + this.topNotices = []; + this.topLeftNotices = []; + this.bottomLeftNotices = []; + this.bottomNotices = []; + this.bottomRightNotices = []; + this.notices.forEach((notice) => { + const { position } = notice.config; + switch (position) { + case 'top-right': + this.topRightNotices.push(notice); + break; + case 'top': + this.topNotices.push(notice); + break; + case 'top-left': + this.topLeftNotices.push(notice); + break; + case 'bottom-left': + this.bottomLeftNotices.push(notice); + break; + case 'bottom': + this.bottomNotices.push(notice); + break; + case 'bottom-right': + this.bottomRightNotices.push(notice); + break; + default: + this.topRightNotices.push(notice); + } + }); + this.cdr.detectChanges(); + } + + private fixConfig(noticeData: TiNoticeData): Required { + const { animation, duration, hoverPause, position } = TiNotificationContainerComponent.TI_NOTIFICATION_DEFAULT_CONFIG; + noticeData.config = { animation, duration, hoverPause, position, ...noticeData.config }; + return noticeData as Required; + } + + close(id: string): void { + for (let i = 0; i < this.notices.length; i++) { + let target = this.notices[i]; + if (target.noticeId === id) { + this.notices.splice(i, 1); + this.closeHandle(target); + this.reloadNotices(); + break; + } + } + } + + closeAll(): void { + this.notices.forEach((notice) => this.closeHandle(notice)); + this.notices = []; + this.reloadNotices(); + } + + private closeHandle(notice: Required): void { + if (notice.config!.onClose && Util.isFunction(notice.config!.onClose)) { + notice.config.onClose(); + } + } + + config(config: TiNotificationConfig): void { + let confTemp = { ...this.notificationConfig, ...config }; + for (let key in config) { + if (config[key] === 'default') { + delete confTemp[key]; + } + } + this.notificationConfig = confTemp; + this.reloadNotices(); + } +} diff --git a/src/notification/lib/src/TiNotificationInterface.ts b/src/notification/lib/src/TiNotificationInterface.ts new file mode 100644 index 0000000..f644aab --- /dev/null +++ b/src/notification/lib/src/TiNotificationInterface.ts @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TemplateRef } from '@angular/core'; + +/** + * @ignore + */ +export type TiTimerType = ReturnType | number | null; + +/** + * 存储全局唯一容器的 mapper + * @ignore + */ +export interface TiNotificationMapperItem { + target: any; +} + +/** + * 通知弹窗位置 + */ +export type TiNotificationPosition = 'top-right' | 'top' | 'top-left' | 'bottom-left' | 'bottom' | 'bottom-right'; + +/** + * 通知弹窗类型 + */ +export type TiNotificationType = 'success' | 'prompt' | 'warn' | 'error' | 'simple'; + +/** + * 返回的实例对象中提供方法 + */ +export type TiNotificationRef = Pick, 'close'>; + +/** + * @ignore + */ +export type TiNoticeBasicConfig = Omit, 'name' | 'onClose'>; + +/** + * @ignore + * 用户信息配置项 + * 可选传入,暂时不处理左右边距 + */ +export type TiNotificationConfig = Omit; + +/** + * 包装后的 notice 信息 + * @ignore + */ +export interface TiNoticeData { + /** + * @ignore + * 通知弹窗类型,包括 'success' | 'prompt' | 'warn' | 'error' | 'simple' + */ + type?: TiNotificationType; + /** + * 通知弹窗内容,可能为字符串内容或 template + * @ignore + */ + content?: string | TemplateRef; + /** + * @ignore + * 通知弹窗唯一标识 + */ + noticeId?: string; + /** + * 可选配置内容 + */ + config?: TiNoticeConfig; + /** + * @ignore + * 通知弹窗进出场状态 + */ + state?: 'enter' | 'leave'; + /** + * @ignore + * 自定义内容通知弹窗的 TemplateRef + */ + template?: TemplateRef; + /** + * @ignore + * 返回给用户的关闭引用 + */ + close?: () => void; +} + +/** + * @ignore + * 用户输入的配置 + */ +export interface TiNoticeConfig { + /** + * 通知弹窗索引,可通过 name 改变弹窗属性 + */ + name?: string; + /** + * 通知弹窗位置 + * @default 'top-right' + */ + position?: TiNotificationPosition; + /** + * 自动关闭时间,单位毫秒 + * @default 4500 + */ + duration?: number; + /** + * 是否开启动画 + * @default true + */ + animation?: boolean; + /** + * 是否在鼠标悬停时暂停自动关闭的计时 + * @default true + */ + hoverPause?: boolean; + /** + * 通知弹窗关闭后的回调函数 + */ + onClose?: () => void; +} + +/** + * 全局配置 + */ +export interface TiNotificationUserConfig { + top?: string | 0; + left?: string | 0; + bottom?: string | 0; + right?: string | 0; +} diff --git a/src/notification/lib/src/TiNotificationMapper.ts b/src/notification/lib/src/TiNotificationMapper.ts new file mode 100644 index 0000000..3e578a9 --- /dev/null +++ b/src/notification/lib/src/TiNotificationMapper.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Injectable } from '@angular/core'; +import { TiNotificationMapperItem } from './TiNotificationInterface'; + +/** + * 全局容器字典,用来存储外层容器 + */ +@Injectable({ + providedIn: 'root' +}) +export class TiNotificationMapper { + private mapper: Map = new Map(); + + private static readonly KEY: string = 'ti-notification'; + + setItem(target: any): void { + const item: TiNotificationMapperItem = { target }; + this.mapper.set(TiNotificationMapper.KEY, item); + } + + getItem(): T | null { + if (this.mapper.has(TiNotificationMapper.KEY)) { + return this.mapper.get(TiNotificationMapper.KEY)!.target as T; + } + return; + } +} diff --git a/src/notification/lib/src/TiNotificationModule.ts b/src/notification/lib/src/TiNotificationModule.ts new file mode 100644 index 0000000..46ec9a3 --- /dev/null +++ b/src/notification/lib/src/TiNotificationModule.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { OverlayModule } from '@angular/cdk/overlay'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { TiNotificationContainerComponent } from './TiNotificationContainerComponent'; +import { TiNotificationComponent } from './TiNotificationComponent'; + +import { TiAlertModule } from '@opentiny/ng-alert'; +import { TiIconModule } from '@opentiny/ng-icon'; + +@NgModule({ + imports: [OverlayModule, CommonModule, TiAlertModule, TiIconModule], + declarations: [TiNotificationComponent, TiNotificationContainerComponent] +}) +export class TiNotificationModule {} diff --git a/src/notification/lib/src/TiNotificationMotion.ts b/src/notification/lib/src/TiNotificationMotion.ts new file mode 100644 index 0000000..1f150b7 --- /dev/null +++ b/src/notification/lib/src/TiNotificationMotion.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { animate, AnimationTriggerMetadata, state, style, transition, trigger } from '@angular/animations'; + +export const notificationMotion: AnimationTriggerMetadata = trigger('notificationMotion', [ + state('rightEnter', style({ opacity: 1, transform: 'translateX(0)' })), + state('topEnter', style({ opacity: 1, transform: 'translateY(0)' })), + state('leftEnter', style({ opacity: 1, transform: 'translateX(0)' })), + state('bottomEnter', style({ opacity: 1, transform: 'translateY(0)' })), + state('leave', style({ opacity: 0, transform: 'scaleY(0.3)', transformOrigin: '0% 0%' })), + transition('* => rightEnter', [style({ opacity: 0, transform: 'translateX(10%)' }), animate('200ms linear')]), + transition('* => topEnter', [style({ opacity: 0, transform: 'translateY(-10%)' }), animate('200ms linear')]), + transition('* => leftEnter', [style({ opacity: 0, transform: 'translateX(-10%)' }), animate('200ms linear')]), + transition('* => bottomEnter', [style({ opacity: 0, transform: 'translateY(10%)' }), animate('200ms linear')]), + transition('* => leave', [style({ opacity: 1, transform: 'scaleY(1)', transformOrigin: '0% 0%' }), animate('200ms linear')]) +]); diff --git a/src/notification/lib/src/TiNotificationService.ts b/src/notification/lib/src/TiNotificationService.ts new file mode 100644 index 0000000..aa081b5 --- /dev/null +++ b/src/notification/lib/src/TiNotificationService.ts @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Overlay, GlobalPositionStrategy, NoopScrollStrategy, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { ComponentRef, Injectable, Injector, TemplateRef } from '@angular/core'; +import { TiNotificationModule } from './TiNotificationModule'; +import { TiNotificationMapper } from './TiNotificationMapper'; +import { TiNotificationContainerComponent } from './TiNotificationContainerComponent'; +import { TiNoticeConfig, TiNotificationRef, TiNoticeData, TiNotificationType, TiNotificationConfig } from './TiNotificationInterface'; + +@Injectable({ + providedIn: TiNotificationModule +}) +export class TiNotificationService { + private container: TiNotificationContainerComponent; + + private static readonly prefix: string = 'ti-notification-'; + + private static notificationFlag: number = 0; + + constructor(private mapper: TiNotificationMapper, private overlay: Overlay, private injector: Injector) {} + + private static createNoticeId(): string { + return `${this.prefix}${this.notificationFlag++}`; + } + + private createContainer(): TiNotificationContainerComponent { + let container: TiNotificationContainerComponent = this.mapper.getItem(); + if (container) { + return container; + } + + const overlayRef: OverlayRef = this.overlay.create({ + hasBackdrop: false, + disposeOnNavigation: false, + scrollStrategy: new NoopScrollStrategy(), + positionStrategy: new GlobalPositionStrategy() + }); + const componentRef: ComponentRef = overlayRef.attach( + new ComponentPortal(TiNotificationContainerComponent, null, this.injector) + ); + container = componentRef.instance; + this.mapper.setItem(container); + this.container = componentRef.instance; + this.container.setConsoleHeader(); + return container; + } + + /** + * 打开成功通知弹窗 + */ + success(content: string, config?: TiNoticeConfig): TiNotificationRef { + return this.action({ type: 'success', content, config }); + } + + /** + * 打开失败通知弹窗 + */ + error(content: string, config?: TiNoticeConfig): TiNotificationRef { + return this.action({ type: 'error', content, config }); + } + + /** + * 打开提示通知弹窗 + */ + prompt(content: string, config?: TiNoticeConfig): TiNotificationRef { + return this.action({ type: 'prompt', content, config }); + } + + /** + * 打开警告通知弹窗 + */ + warn(content: string, config?: TiNoticeConfig): TiNotificationRef { + return this.action({ type: 'warn', content, config }); + } + + /** + * 打开无图标通知弹窗 + */ + simple(content: string, config?: TiNoticeConfig): TiNotificationRef { + return this.action({ type: 'simple', content, config }); + } + + /** + * 打开通知弹窗方法,已对外提供五个包装方法,此方法暂时对用户隐藏 + * @ignore + * @param type 通知弹窗类型 + * @param content 通知弹窗文案 + * @param config 通知弹窗配置 + * @returns 通知弹窗包装实例,包含一个可关闭对象 + */ + open(type: TiNotificationType, content: string, config?: TiNoticeConfig): TiNotificationRef { + return this.action({ type, content, config }); + } + + /** + * 打开自定义内容通知弹窗 + * @param template 自定义模板 + * @param config 自定义配置 + * @returns 通知弹窗包装实例,包含一个可关闭对象 + */ + template(template: TemplateRef, config?: TiNoticeConfig): TiNotificationRef { + return this.action({ template, config }); + } + + private action(baseData: TiNoticeData): TiNotificationRef { + this.container = this.createContainer(); + + return this.container.create({ + ...baseData, + ...{ + noticeId: TiNotificationService.createNoticeId() + } + }); + } + /** + * 关闭全部已打开的通知弹窗 + */ + closeAll(): void { + this.container.closeAll(); + } + /** + * 设置全局接口 + */ + config(userConfig: TiNotificationConfig): void { + this.container = this.createContainer(); + this.container.config(userConfig); + } +} diff --git a/src/notification/lib/src/notification.less b/src/notification/lib/src/notification.less new file mode 100644 index 0000000..4acdfc7 --- /dev/null +++ b/src/notification/lib/src/notification.less @@ -0,0 +1,34 @@ +@import '../../../themes/basic/base-all.less'; +@import './position.less'; + +:host { + --ti-notification-max-width: 400px; +} + +@ti-notification-prefix: ~'ti3-notification'; + +.@{ti-notification-prefix} { + position: fixed; + z-index: @zindex-notification; + &-notice { + background-color: var(--ti-common-color-bg-white-normal); + margin-bottom: var(--ti-common-space-4x); + max-width: var(--ti-notification-max-width); + position: relative; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + color: var(--ti-common-color-text-primary); + overflow: auto; + &-close { + display: block; + outline: none; + cursor: pointer; + position: absolute; + top: var(--ti-common-space-3x); + right: var(--ti-common-space-4x); + color: var(--ti-common-color-icon-normal); + font-size: var(--ti-common-size-3x); + } + } + .set-position(); +} diff --git a/src/notification/lib/src/position.less b/src/notification/lib/src/position.less new file mode 100644 index 0000000..1563b85 --- /dev/null +++ b/src/notification/lib/src/position.less @@ -0,0 +1,33 @@ +@ti-notification-position-top: ~'&-top-right, &-top, &-top-left'; +@ti-notification-position-bottom: ~'&-bottom-left, &-bottom, &-bottom-right'; +@ti-notification-position-left: ~'&-bottom-left, &-top-left'; +@ti-notification-position-right: ~'&-top-right, &-bottom-right'; +@ti-notification-position-center: ~'&-top, &-bottom'; + +.set-position() { + @{ti-notification-position-top} { + top: var(--ti-common-space-6x); + } + + @{ti-notification-position-bottom} { + bottom: var(--ti-common-space-6x); + } + + @{ti-notification-position-center} { + left: 50%; + transform: translate(-50%); + margin: atuo; + } + + @{ti-notification-position-left} { + left: 0; + margin-right: var(--ti-common-space-0); + margin-left: var(--ti-common-space-6x); + } + + @{ti-notification-position-right} { + right: 0; + margin-left: var(--ti-common-space-0); + margin-right: var(--ti-common-space-6x); + } +} diff --git a/src/outline/lib/index.ts b/src/outline/lib/index.ts new file mode 100644 index 0000000..d7e1022 --- /dev/null +++ b/src/outline/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiOutlineModule'; diff --git a/src/outline/lib/ng-package.json b/src/outline/lib/ng-package.json new file mode 100644 index 0000000..f47b6dd --- /dev/null +++ b/src/outline/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/outline", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/outline/lib/package.json b/src/outline/lib/package.json new file mode 100644 index 0000000..fb63c20 --- /dev/null +++ b/src/outline/lib/package.json @@ -0,0 +1,9 @@ +{ + "name": "@opentiny/ng-outline", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/outline/lib/project.json b/src/outline/lib/project.json new file mode 100644 index 0000000..50de636 --- /dev/null +++ b/src/outline/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/outline/lib", + "sourceRoot": "src/outline/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/outline"], + "options": { + "project": "src/outline/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/outline"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js outline" + }, + { + "command": "ng default-build outline" + }, + { + "command": "node build/clear-default-theme.js outline" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/outline && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build outline && ng pack outline && node build/publish.js outline --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/outline/lib/src/TiOutlineDirective.ts b/src/outline/lib/src/TiOutlineDirective.ts new file mode 100644 index 0000000..ff4c8b3 --- /dev/null +++ b/src/outline/lib/src/TiOutlineDirective.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Renderer2, NgZone, AfterViewInit, OnDestroy, Inject } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +/** + * @ignore + * 点击需要和聚焦区分开。当点击的时候,不需要有outline。 + */ +@Directive({ + selector: '[tiOutline]' +}) +export class TiOutlineDirective implements AfterViewInit { + constructor(private renderer: Renderer2, public hostEle: ElementRef, private zone: NgZone, @Inject(DOCUMENT) private document) {} + private documentVisibilitychangeListener: () => void; + ngAfterViewInit(): void { + this.zone.runOutsideAngular(() => { + let outlineColor: string; + const outlineHostEle: any = this.hostEle.nativeElement; + const transparentColor: string = 'rgba(0, 0, 0, 0)'; // 透明色,跟transparent色值一致 + if (outlineHostEle) { + this.renderer.listen(outlineHostEle, 'mousedown', (): void => { + // 当点击的时候,不需要有outline。 + this.renderer.setStyle(outlineHostEle, 'outlineColor', 'transparent'); + // 注意:仅仅设置outlineColor为透明色,会导致点击后border不可见 + this.renderer.setStyle(outlineHostEle, 'outlineWidth', '0px'); + }); + this.renderer.listen(outlineHostEle, 'blur', (): void => { + // blur时仅能读取到outlineColor,作为窗口切换前的状态标志。 + outlineColor = getComputedStyle(outlineHostEle).outlineColor; + // 恢复outline原生状态 + this.renderer.setStyle(outlineHostEle, 'outline', ''); + }); + } + this.documentVisibilitychangeListener = this.renderer.listen(this.document, 'visibilitychange', (): void => { + if (document.visibilityState === 'visible' && this.document.activeElement === outlineHostEle && outlineColor === transparentColor) { + this.renderer.setStyle(outlineHostEle, 'outlineColor', 'transparent'); + // 解决checkbox组件聚焦状态下页面切换之后,border看不见问题。 + this.renderer.setStyle(outlineHostEle, 'outlineWidth', '0px'); + } + }); + }); + } + + ngOnDestroy(): void { + this.documentVisibilitychangeListener && this.documentVisibilitychangeListener(); + } +} diff --git a/src/outline/lib/src/TiOutlineModule.ts b/src/outline/lib/src/TiOutlineModule.ts new file mode 100644 index 0000000..d9691d5 --- /dev/null +++ b/src/outline/lib/src/TiOutlineModule.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiOutlineDirective } from './TiOutlineDirective'; + +@NgModule({ + imports: [CommonModule], + exports: [TiOutlineDirective], + declarations: [TiOutlineDirective] +}) +export class TiOutlineModule {} +export { TiOutlineDirective } from './TiOutlineDirective'; diff --git a/src/overflow/demo/karma.conf.js b/src/overflow/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/overflow/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/overflow/demo/project.json b/src/overflow/demo/project.json new file mode 100644 index 0000000..d414577 --- /dev/null +++ b/src/overflow/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/overflow/demo", + "sourceRoot": "src/overflow/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/overflow", + "index": "src/overflow/demo/src/index.html", + "main": "src/overflow/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/overflow/demo/tsconfig.app.json", + "assets": ["src/overflow/demo/src/favicon.ico", "src/overflow/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "overflow-demo:build:production" + }, + "development": { + "browserTarget": "overflow-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js overflow" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/overflow/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/overflow/demo/tsconfig.spec.json", + "karmaConfig": "src/overflow/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/overflow/demo/src/app/AppComponent.ts b/src/overflow/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/overflow/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/overflow/demo/src/app/AppModule.ts b/src/overflow/demo/src/app/AppModule.ts new file mode 100644 index 0000000..e303c2e --- /dev/null +++ b/src/overflow/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { OverflowTestModule } from './overflow/OverflowTestModule'; + +@NgModule({ + imports: [ + OverflowTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/overflow/demo/src/app/IndexComponent.ts b/src/overflow/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..3bbe2ec --- /dev/null +++ b/src/overflow/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { OverflowTestModule } from './overflow/OverflowTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = OverflowTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/overflow/demo/src/app/app.html b/src/overflow/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/overflow/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/overflow/demo/src/app/overflow/OverflowDestoryComponent.ts b/src/overflow/demo/src/app/overflow/OverflowDestoryComponent.ts new file mode 100644 index 0000000..10e0b55 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowDestoryComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './overflow-destory.html', + styles: [ + ` + .overflow-cls { + max-width: 200px; + line-height: 30px; + background-color: yellow; + } + ` + ] +}) +export class OverflowDestoryComponent { + textContent: string = '明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。'; + show: boolean = true; + clickFn() { + setTimeout(() => { + this.show = false; + }, 2000); + } +} diff --git a/src/overflow/demo/src/app/overflow/OverflowDirectiveComponent.ts b/src/overflow/demo/src/app/overflow/OverflowDirectiveComponent.ts new file mode 100644 index 0000000..f0710f2 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowDirectiveComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './overflow-directive.html', + styles: [ + ` + .container { + border: 1px solid #000; + width: 70px; + height: 60px; + } + .overflow-cls { + max-width: 100px; + padding: 10px; + background-color: yellow; + } + ` + ] +}) +export class OverflowDirectiveComponent {} diff --git a/src/overflow/demo/src/app/overflow/OverflowMaxlineComponent.ts b/src/overflow/demo/src/app/overflow/OverflowMaxlineComponent.ts new file mode 100644 index 0000000..dc6c716 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowMaxlineComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './overflow-maxline.html' +}) +export class OverflowMaxlineComponent { + textContent: string = '明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。'; +} diff --git a/src/overflow/demo/src/app/overflow/OverflowMaxwidthComponent.ts b/src/overflow/demo/src/app/overflow/OverflowMaxwidthComponent.ts new file mode 100644 index 0000000..c0aa432 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowMaxwidthComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './overflow-maxwidth.html' +}) +export class OverflowMaxwidthComponent { + textContent: string = '明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。'; +} diff --git a/src/overflow/demo/src/app/overflow/OverflowPositionComponent.ts b/src/overflow/demo/src/app/overflow/OverflowPositionComponent.ts new file mode 100644 index 0000000..632bc26 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowPositionComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './overflow-position.html' +}) +export class OverflowPositionComponent { + textContent: string = '明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。'; +} diff --git a/src/overflow/demo/src/app/overflow/OverflowServiceComponent.ts b/src/overflow/demo/src/app/overflow/OverflowServiceComponent.ts new file mode 100644 index 0000000..900b42b --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowServiceComponent.ts @@ -0,0 +1,42 @@ +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { TiOverflowService } from '@opentiny/ng'; + +@Component({ + templateUrl: './overflow-service.html', + styles: [ + ` + .test1 { + max-width: 50px; + } + .container { + border: 1px solid #000; + width: 70px; + height: 60px; + } + .child { + max-width: calc(100% - 20px); + border: 1px solid #000; + box-sizing: border-box; + } + ` + ] +}) +export class OverflowServiceComponent implements OnInit { + @ViewChild('ele1', { static: true }) ele1: ElementRef; + @ViewChild('ele2', { static: true }) ele2: ElementRef; + @ViewChild('ele3', { static: true }) ele3: ElementRef; + @ViewChild('ele4', { static: true }) ele4: ElementRef; + constructor(private overflowService: TiOverflowService) {} + ngOnInit(): void { + this.overflowService.create(this.ele1.nativeElement); + this.overflowService.create(this.ele2.nativeElement, { + tipPosition: 'right', + tipContent: 'my tip define with aa', + tipMaxWidth: '100px' + }); + this.overflowService.create(this.ele3.nativeElement, { + tipElement: this.ele4.nativeElement, + tipPosition: 'right' + }); + } +} diff --git a/src/overflow/demo/src/app/overflow/OverflowTestComponent.ts b/src/overflow/demo/src/app/overflow/OverflowTestComponent.ts new file mode 100644 index 0000000..ea845c7 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowTestComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +@Component({ + templateUrl: './overflow-test.html' +}) +export class OverflowTestComponent { + constructor(private router: Router) {} + goto(): void { + this.router.navigate(['/autocomplete/autocomplete-events']); + } +} diff --git a/src/overflow/demo/src/app/overflow/OverflowTestModule.ts b/src/overflow/demo/src/app/overflow/OverflowTestModule.ts new file mode 100644 index 0000000..dc6bf7f --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowTestModule.ts @@ -0,0 +1,56 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule } from '@angular/common'; + +import { TiIconModule, TiOverflowModule } from '@opentiny/ng'; + +import { OverflowTestComponent } from './OverflowTestComponent'; +import { OverflowMaxlineComponent } from './OverflowMaxlineComponent'; +import { OverflowMaxwidthComponent } from './OverflowMaxwidthComponent'; +import { OverflowTipcontentComponent } from './OverflowTipcontentComponent'; +import { OverflowPositionComponent } from './OverflowPositionComponent'; +import { OverflowDirectiveComponent } from './OverflowDirectiveComponent'; +import { OverflowServiceComponent } from './OverflowServiceComponent'; +import { OverflowDestoryComponent } from './OverflowDestoryComponent'; + +@NgModule({ + imports: [TiIconModule, TiOverflowModule, CommonModule, RouterModule.forChild(OverflowTestModule.ROUTES)], + declarations: [ + OverflowTestComponent, + OverflowServiceComponent, + OverflowMaxlineComponent, + OverflowMaxwidthComponent, + OverflowTipcontentComponent, + OverflowPositionComponent, + OverflowDirectiveComponent, + OverflowDestoryComponent + ] +}) +export class OverflowTestModule { + static readonly LINKS: Array = [{ href: 'directives/TiOverflowDirective.html', label: 'Overflow' }]; + static readonly ROUTES: Routes = [ + { + path: 'overflow/overflow-directive', + component: OverflowDirectiveComponent + }, + { + path: 'overflow/overflow-maxline', + component: OverflowMaxlineComponent + }, + { + path: 'overflow/overflow-maxwidth', + component: OverflowMaxwidthComponent + }, + { + path: 'overflow/overflow-tipcontent', + component: OverflowTipcontentComponent + }, + { + path: 'overflow/overflow-position', + component: OverflowPositionComponent + }, + { path: 'overflow/overflow-service', component: OverflowServiceComponent }, + { path: 'overflow/overflow-test', component: OverflowTestComponent }, + { path: 'overflow/overflow-destory', component: OverflowDestoryComponent } + ]; +} diff --git a/src/overflow/demo/src/app/overflow/OverflowTipcontentComponent.ts b/src/overflow/demo/src/app/overflow/OverflowTipcontentComponent.ts new file mode 100644 index 0000000..3b32174 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/OverflowTipcontentComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './overflow-tipcontent.html' +}) +export class OverflowTipcontentComponent { + textContent: string = '明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。明月几时有,把酒问青天。'; + textContent1: string = '设置为空,文本超长没有提示信息,仅有溢出样式'; +} diff --git a/src/overflow/demo/src/app/overflow/overflow-destory.html b/src/overflow/demo/src/app/overflow/overflow-destory.html new file mode 100644 index 0000000..dcff85e --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-destory.html @@ -0,0 +1,10 @@ +

    描述

    +

    + 1、overflow指令在labelEdit组件中使用,该示例测试销毁场景: + 点击隐藏按钮,宿主元素延时销毁,鼠标hover到宿主元素,tip呈现,等到宿主元素销毁时查看tip是否销毁 +

    +

    示例

    +
    +
    {{textContent}}
    +
    + diff --git a/src/overflow/demo/src/app/overflow/overflow-directive.html b/src/overflow/demo/src/app/overflow/overflow-directive.html new file mode 100644 index 0000000..ea1d264 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-directive.html @@ -0,0 +1,16 @@ +

    导入

    +

    import {{ '{' }} TiOverflowModule {{ '}' }} from '@opentiny/ng';

    + +
    明月几时有,把酒问青天。
    +
    +

    通过tiTipPosition定义tip位置:

    +
    明月几时有,把酒问青天。
    +
    +

    通过tiTipMaxWidth定义Tip最大宽度:

    +
    明月几时有,把酒问青天。
    +
    +

    通过tiTipContent定义Tip内容:

    +
    明月几时有,把酒问青天。
    +
    +

    当通过tiTipContent定义Tip内容为空字符串时,不出tip:

    +
    明月几时有,把酒问青天。
    diff --git a/src/overflow/demo/src/app/overflow/overflow-maxline.html b/src/overflow/demo/src/app/overflow/overflow-maxline.html new file mode 100644 index 0000000..9d4abf7 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-maxline.html @@ -0,0 +1,2 @@ +
    {{textContent}}
    +
    diff --git a/src/overflow/demo/src/app/overflow/overflow-maxwidth.html b/src/overflow/demo/src/app/overflow/overflow-maxwidth.html new file mode 100644 index 0000000..41e8c06 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-maxwidth.html @@ -0,0 +1,4 @@ +
    {{textContent}}
    +
    + 多行:{{textContent}} +
    diff --git a/src/overflow/demo/src/app/overflow/overflow-position.html b/src/overflow/demo/src/app/overflow/overflow-position.html new file mode 100644 index 0000000..699f7fb --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-position.html @@ -0,0 +1,9 @@ +
    +
    {{textContent}}
    +
    {{textContent}}
    +
    {{textContent}}
    +
    {{textContent}}
    +
    + {{textContent}} +
    +
    diff --git a/src/overflow/demo/src/app/overflow/overflow-service.html b/src/overflow/demo/src/app/overflow/overflow-service.html new file mode 100644 index 0000000..a63c243 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-service.html @@ -0,0 +1,9 @@ +
    aaaaaaa
    +
    +

    自定义tip最大宽宽度(100px)、位置(right)、内容:

    +aaaaaaabbbbbbbbbbb +
    +

    自定义tip占位依赖的host元素:

    +
    +
    aaaaaaaaaaaaaa
    +
    diff --git a/src/overflow/demo/src/app/overflow/overflow-test.html b/src/overflow/demo/src/app/overflow/overflow-test.html new file mode 100644 index 0000000..dde4662 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-test.html @@ -0,0 +1,60 @@ +

    1.tiOverflow默认是inline-block,可以设置max-width,min-width,width。width默认值是auto包裹子内容

    +
    binggo
    +
    +
    binggo
    +
    +
    明月几时有,把酒问青天。
    +
    +
    teadmin-<script>alert(1)</script>
    +
    +

    因为overflow:hidden会改变对齐方式。所以后面的span改改对齐方式即可:

    +
    但愿人长久,千里共婵娟。
    +作者:苏轼 +
    +

    文图混排效果:

    +
    + 但愿人长久 + 千里共婵娟。 +
    +

    2.外层设置宽度(随外层),外层必须是block/inline-block,tiOverflow也改为block

    + +
    月有阴晴圆缺,此事古难全。
    +
    + +
    Bingo
    +
    +
    +
    月有阴晴圆缺,此事古难全。
    +
    + +
    月有阴晴圆缺,此事古难全。(此条未截取,因为父类不是block)
    +
    +
    +

    3.外层设置最大宽度(随内层),外层必须是inline-block,tiOverflow也改为block

    + +
    月有阴晴圆缺,此事古难全。
    +
    + +
    bingo
    +
    +
    +
    月有阴晴圆缺,此事古难全。
    +
    + +
    月有阴晴圆缺,此事古难全。(此条未截取,因为父类不是block)
    +
    +

    4.测试跳转到其他路由地址后,tip是否消失

    + 明月几时有,把酒问青天。 + diff --git a/src/overflow/demo/src/app/overflow/overflow-tipcontent.html b/src/overflow/demo/src/app/overflow/overflow-tipcontent.html new file mode 100644 index 0000000..afc6221 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/overflow-tipcontent.html @@ -0,0 +1,7 @@ +
    {{textContent}}
    +
    {{textContent}}
    +
    {{textContent1}}
    +
    + 多行:{{textContent}} +
    +
    多行:设置为空,{{textContent}}
    diff --git a/src/overflow/demo/src/app/overflow/webdoc/overflow-demos.js b/src/overflow/demo/src/app/overflow/webdoc/overflow-demos.js new file mode 100644 index 0000000..9cfabd7 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/webdoc/overflow-demos.js @@ -0,0 +1,71 @@ +export default { + column: '2', + demos: [ + { + demoId: 'overflow-maxline', + name: { + 'zh-CN': '单行、多行溢出', + 'en-US': 'maxline', + }, + desc: { + 'zh-CN': + '

    通过属性maxline配置文本最大行数。注意:多行效果需要设置宿主元素的 line-height

    ', + 'en-US': '

    button color

    ', + }, + apis: [ + 'TiOverflowDirective.properties.tiOverflow', + 'TiOverflowMaxlineDirective.properties.tiOverflow', + 'TiOverflowMaxlineDirective.properties.maxLine', + 'TiOverflowMaxlineDirective.properties.textContent', + ], + }, + { + demoId: 'overflow-maxwidth', + name: { + 'zh-CN': '溢出提示的最大宽度', + 'en-US': 'maxwidth', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipMaxWidth配置文本过长时显示的 tip 的最大宽度。

    ', + 'en-US': '

    maxwidth

    ', + }, + apis: [ + 'TiOverflowDirective.properties.tiTipMaxWidth', + 'TiOverflowMaxlineDirective.properties.tiTipMaxWidth', + ], + }, + { + demoId: 'overflow-tipcontent', + name: { + 'zh-CN': 'tip内容', + 'en-US': 'item', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipContent配置文本过长时显示的 tip 内容,默认为宿主元素文本。

    ', + 'en-US': '

    item

    ', + }, + apis: [ + 'TiOverflowDirective.properties.tiTipContent', + 'TiOverflowMaxlineDirective.properties.tiTipContent', + ], + }, + { + demoId: 'overflow-position', + name: { + 'zh-CN': 'tip方向', + 'en-US': 'item', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipPosition配置文本过长时显示的 tip 位置。

    ', + 'en-US': '

    tipposition

    ', + }, + apis: [ + 'TiOverflowDirective.properties.tiTipPosition', + 'TiOverflowMaxlineDirective.properties.tiTipPosition', + ], + }, + ], +}; diff --git a/src/overflow/demo/src/app/overflow/webdoc/overflow.cn.md b/src/overflow/demo/src/app/overflow/webdoc/overflow.cn.md new file mode 100644 index 0000000..c260225 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/webdoc/overflow.cn.md @@ -0,0 +1,27 @@ +--- +title: Overflow 溢出提示 +--- +# Overflow 溢出提示 + +
    + +内容超长时,超出部分用...代替,并通过 tip 显示全部内容。   + ++ 支持单行、多行。 + +```typescript +import { TiOverflowModule } from '@opentiny/ng'; +``` + +
    + +
    + +内容超长时,超出部分用...代替,并通过 tip 显示全部内容。   + ++ 支持单行 多行。 + +```typescript +import { TiOverflowModule } from '@opentiny/ng'; +``` +
    diff --git a/src/overflow/demo/src/app/overflow/webdoc/overflow.en.md b/src/overflow/demo/src/app/overflow/webdoc/overflow.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/overflow/demo/src/app/overflow/webdoc/overflow.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/overflow/demo/src/favicon.ico b/src/overflow/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/overflow/demo/src/index.html b/src/overflow/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/overflow/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/overflow/demo/src/main.ts b/src/overflow/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/overflow/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/overflow/demo/test.ts b/src/overflow/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/overflow/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/overflow/demo/tsconfig.app.json b/src/overflow/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/overflow/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/overflow/demo/tsconfig.spec.json b/src/overflow/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/overflow/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/overflow/lib/index.ts b/src/overflow/lib/index.ts new file mode 100644 index 0000000..8e37685 --- /dev/null +++ b/src/overflow/lib/index.ts @@ -0,0 +1,3 @@ +export * from './src/TiOverflowModule'; +export * from './src/TiOverflowService'; +export * from './src/TiOverflowServiceModule'; diff --git a/src/overflow/lib/ng-package.json b/src/overflow/lib/ng-package.json new file mode 100644 index 0000000..4117011 --- /dev/null +++ b/src/overflow/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/overflow", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/overflow/lib/package.json b/src/overflow/lib/package.json new file mode 100644 index 0000000..8677ef1 --- /dev/null +++ b/src/overflow/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-overflow", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/overflow/lib/project.json b/src/overflow/lib/project.json new file mode 100644 index 0000000..9a3e6a6 --- /dev/null +++ b/src/overflow/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/overflow/lib", + "sourceRoot": "src/overflow/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/overflow"], + "options": { + "project": "src/overflow/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/overflow"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js overflow" + }, + { + "command": "ng default-build overflow" + }, + { + "command": "node build/clear-default-theme.js overflow" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/overflow && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build overflow && ng pack overflow && node build/publish.js overflow --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/overflow/lib/src/TiOverflowDirective.ts b/src/overflow/lib/src/TiOverflowDirective.ts new file mode 100644 index 0000000..32a62f2 --- /dev/null +++ b/src/overflow/lib/src/TiOverflowDirective.ts @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Input, OnInit } from '@angular/core'; +import { TiOverflowService } from './TiOverflowService'; +import { TiPositionType } from '@opentiny/ng-utils'; + +/** + * 超长情况下文本处理出...并tip提示 + * + */ +@Directive({ + selector: '[tiOverflow]:not([maxLine])' +}) +export class TiOverflowDirective implements OnInit { + /** + * 配置宿主元素支持文本过长提示功能,不需要配置属性值,存在该属性即可 + */ + @Input() tiOverflow: any; + /** + * 配置文本过长时显示的 tip 内容,默认为宿主元素文本 + */ + @Input() tiTipContent: string; + /** + * 配置文本过长时显示的 tip 方向 + */ + @Input() tiTipPosition: TiPositionType; + /** + * 配置文本过长时显示的 tip 的最大宽度 + */ + @Input() tiTipMaxWidth: string = '276px'; + /** + * @ignore + * 决定tip水平方向位置的宿主元素配置 + */ + @Input() tiTipHostEleX: Element; + private overflowRef: any; + constructor(private ele: ElementRef, private overflowService: TiOverflowService) {} + ngOnInit(): void { + this.overflowRef = this.overflowService.create(this.ele.nativeElement, { + tipContent: this.tiTipContent, + tipPosition: this.tiTipPosition, + tipHostEleX: this.tiTipHostEleX, + tipMaxWidth: this.tiTipMaxWidth + }); + } + // 服务方式的overflow无法自销毁,因此此处在组件销毁时销毁服务方式生成的组件实例 + ngOnDestroy(): void { + if (this.overflowRef) { + this.overflowRef.destroy(); + } + } +} diff --git a/src/overflow/lib/src/TiOverflowMaxlineDirective.ts b/src/overflow/lib/src/TiOverflowMaxlineDirective.ts new file mode 100644 index 0000000..6a183f7 --- /dev/null +++ b/src/overflow/lib/src/TiOverflowMaxlineDirective.ts @@ -0,0 +1,290 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + AfterViewInit, + Directive, + ElementRef, + EventEmitter, + Inject, + Input, + NgZone, + OnChanges, + OnDestroy, + Output, + Renderer2, + SimpleChanges, + TemplateRef +} from '@angular/core'; +import { TiTipRef, TiTipService, TiTipShowInfo } from '@opentiny/ng-tip'; +import { Util, TiBrowser, TiPositionType } from '@opentiny/ng-utils'; +import { DOCUMENT } from '@angular/common'; + +/** + * 多行文本超出情况下文本处理出...并tip提示 + * + */ +@Directive({ + selector: '[tiOverflow][maxLine]' +}) +export class TiOverflowMaxlineDirective implements AfterViewInit, OnChanges, OnDestroy { + public nativeElement: any; + constructor( + private hostRef: ElementRef, + private renderer2: Renderer2, + private tipService: TiTipService, + private zone: NgZone, + @Inject(DOCUMENT) private document + ) { + this.nativeElement = this.hostRef.nativeElement; + } + /** + * 文本最大行数 + */ + @Input() maxLine: number = 3; + /** + * 配置文本过长时显示的 tip 的最大宽度 + */ + @Input() tiTipMaxWidth: string = '276px'; + /** + * 配置文本过长时显示的 tip 方向 + */ + @Input() tiTipPosition: TiPositionType; + /** + * 配置文本过长时显示的 tip 内容,默认为宿主元素文本 + */ + @Input() tiTipContent: string; + /** + * @ignore + * 文本被截断之后末尾图标展示 + */ + @Input() iconName: string; + /** + * @ignore + * 图标提示 + */ + @Input() iconTip: string | TemplateRef | any = ''; + /** + * @ignore + * 图标提示内容对应的上下文,tip 内容类型为 templateRef 或 Component 形式时会用到该接口,接口为自定义对象形式 + */ + @Input() iconTipContext: any; + /** + * 宿主文本 + */ + @Input() textContent: string; + /** + * @ignore + * 文本被截断之后的末尾填充符号 + */ + @Input() character: string = '...'; // 暂不开放 + /** + * @ignore + * 图标是否可以聚焦,默认不可聚焦。 + */ + @Input() iconFocusable: boolean = false; + /** + * @ignore + * 图标是否灰化,默认不灰化 + */ + @Input() iconDisabled: boolean = false; + /** + * @ignore + * 文本被截断之后末尾图标点击事件 + */ + @Output() readonly iconClick: EventEmitter = new EventEmitter(); + private text: string; // 宿主元素文本 + private isShave: boolean; // 是否已经截取 + private tipInstance: TiTipRef; // tip实例 + private icontipInstance: TiTipRef; // icontip实例 + + private windowResizeListener: () => void; + private tipContent: string; // 最终展示的tip内容 + private documentVisibilitychangeListener: () => void; + + private shaveTextFn = (): void => { + let fontHtml: string; + if (Util.isEmptyString(this.iconName)) { + fontHtml = ''; + } else { + // 此处添加属性tiOverflowEndicon为了适配在plus3中定义末尾图标的样式 + // 在labelEditor组件中,需要可以聚焦。 + fontHtml = this.iconFocusable + ? `` + : ``; + } + // 修复SSR错误:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle === 'undefined') { + return; + } + const lineHeight: number = parseFloat(getComputedStyle(this.nativeElement).getPropertyValue('line-height')); + const multiLineHeight: number = lineHeight * this.maxLine; + this.nativeElement.textContent = this.text; + // 如果该元素为inline元素时,宽度不生效会导致元素出...样式不生效,因此此处做处理 + if (getComputedStyle(this.nativeElement).display === 'inline') { + this.renderer2.setStyle(this.nativeElement, 'display', 'inline-block'); + } + this.nativeElement.insertAdjacentHTML('beforeend', fontHtml); + if (this.text.length < 2 || this.nativeElement.offsetHeight <= multiLineHeight) { + this.setEvents(); + this.isShave = false; + + return; + } + let charHtml: string = this.character; + charHtml = charHtml.concat(fontHtml); + // 以下使用二分算法计算文本截取位置 + let max: number = this.text.length - 1; + let min: number = 0; + let middle: number; + while (min < max) { + middle = (min + max + 1) / 2; + this.nativeElement.textContent = this.text.slice(0, middle); + this.nativeElement.insertAdjacentHTML('beforeend', charHtml); + if (this.nativeElement.offsetHeight > multiLineHeight + 1) { + max = middle - 1; // 截取的内容少 + } else { + min = middle; // 截取的内容多 + } + } + this.nativeElement.textContent = this.text.slice(0, max); + this.nativeElement.insertAdjacentHTML('beforeend', charHtml); + this.setEvents(); + this.isShave = true; + }; + + ngOnChanges(changes: SimpleChanges): void { + this.text = this.textContent || this.nativeElement.innerHTML; + if ((changes.maxLine && !changes.maxLine.firstChange) || (changes.textContent && !changes.textContent.firstChange)) { + this.shaveTextFn(); + } + + if (this.iconFocusable && changes.iconDisabled && !changes.iconDisabled.firstChange) { + const spanEle: HTMLElement = this.nativeElement.querySelector('span[tiOverflowEndicon]'); + if (spanEle) { + this.renderer2.setAttribute(spanEle, 'tabindex', this.iconDisabled ? '-1' : '0'); + } + } + } + // tip配置 + ngAfterViewInit(): void { + this.text = this.textContent || this.nativeElement.innerHTML; // 视图初始化完成后获取宿主元素文本 + if (TiBrowser.isIE()) { + setTimeout(() => { + this.shaveTextFn(); // IE下需延时处理,否则初始化获取到 offsetHeight 值与谷歌有差异 + }, 0); + } else { + this.shaveTextFn(); + } + this.tipInstance = this.tipService.create(this.nativeElement, { + position: this.tiTipPosition || 'right', + maxWidth: this.tiTipMaxWidth, + trigger: 'mouse', + showFn: (): TiTipShowInfo => { + if (!this.isShave) { + return; + } + this.tipContent = this.tiTipContent !== undefined ? this.tiTipContent : this.text; + + return { content: this.tipContent }; + } + }); + // 修复SSR错误:ERROR ReferenceError: window is not defined + if (typeof window === 'undefined') { + return; + } + this.zone.runOutsideAngular(() => { + this.windowResizeListener = this.renderer2.listen(window, 'resize', this.shaveTextFn); + }); + } + + ngOnDestroy(): void { + if (this.windowResizeListener) { + this.windowResizeListener(); + } + if (this.documentVisibilitychangeListener) { + this.documentVisibilitychangeListener(); + } + // 当组件销毁的时候文本tip也销毁 + // eslint-disable-next-line no-unused-expressions + this.tipInstance?.hide(); + + // 当组件销毁的时候编辑图标tip也销毁 + // eslint-disable-next-line no-unused-expressions + this.icontipInstance?.hide(); + } + + /** + * @ignore + */ + public setEvents(): void { + const clickIconEle: HTMLElement = this.nativeElement.querySelector('span[tiOverflowEndicon]'); + if (!clickIconEle) { + return; + } + + this.renderer2.listen(clickIconEle, 'click', (): void => { + this.zone.run(() => { + if (this.icontipInstance) { + this.icontipInstance.hide(); + } + this.iconClick.emit(); + }); + }); + this.renderer2.listen(clickIconEle, 'mouseenter', (): void => { + if (this.tipInstance) { + this.zone.run(() => { + this.tipInstance.hide(); + }); + } + }); + this.renderer2.listen(clickIconEle, 'mouseleave', (): void => { + if (this.tipInstance && this.isShave) { + this.zone.run(() => { + Util.trigger(this.nativeElement, 'mouseenter'); + }); + } + }); + this.icontipInstance = this.tipService.create(clickIconEle, { + position: 'top', + maxWidth: this.tiTipMaxWidth, + trigger: 'mouse', + showFn: (): TiTipShowInfo => { + return { + content: this.iconTip, + context: this.iconTipContext + }; + } + }); + if (this.iconFocusable) { + this.zone.runOutsideAngular((): void => { + let outlineColor: string; + const transparentColor: string = 'rgba(0, 0, 0, 0)'; // 透明色,跟transparent色值一致 + this.renderer2.listen(clickIconEle, 'mousedown', (): void => { + this.renderer2.setStyle(clickIconEle, 'outlineColor', 'transparent'); + }); + this.renderer2.listen(clickIconEle, 'blur', (): void => { + // blur时仅能读取到outlineColor,作为窗口切换前的状态标志。 + outlineColor = getComputedStyle(clickIconEle).outlineColor; + // 恢复outline原生状态 + this.renderer2.setStyle(clickIconEle, 'outline', ''); + }); + this.documentVisibilitychangeListener = this.renderer2.listen(this.document, 'visibilitychange', (): void => { + if (document.visibilityState === 'visible' && this.document.activeElement === clickIconEle && outlineColor === transparentColor) { + this.renderer2.setStyle(clickIconEle, 'outlineColor', 'transparent'); + } + }); + }); + } + } +} diff --git a/src/overflow/lib/src/TiOverflowModule.ts b/src/overflow/lib/src/TiOverflowModule.ts new file mode 100644 index 0000000..6acd7c6 --- /dev/null +++ b/src/overflow/lib/src/TiOverflowModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiOverflowDirective } from './TiOverflowDirective'; +import { TiOverflowMaxlineDirective } from './TiOverflowMaxlineDirective'; +import { TiOverflowServiceModule } from './TiOverflowServiceModule'; + +@NgModule({ + imports: [TiOverflowServiceModule], + exports: [TiOverflowDirective, TiOverflowMaxlineDirective], + declarations: [TiOverflowDirective, TiOverflowMaxlineDirective] +}) +export class TiOverflowModule {} +export { TiOverflowDirective } from './TiOverflowDirective'; +export { TiOverflowMaxlineDirective } from './TiOverflowMaxlineDirective'; diff --git a/src/overflow/lib/src/TiOverflowService.ts b/src/overflow/lib/src/TiOverflowService.ts new file mode 100644 index 0000000..366f7c6 --- /dev/null +++ b/src/overflow/lib/src/TiOverflowService.ts @@ -0,0 +1,181 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ComponentRef, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiLog, TiPositionType, Util } from '@opentiny/ng-utils'; +import { TiTipRef, TiTipService, TiTipShowInfo } from '@opentiny/ng-tip'; +import { TiOverflowServiceModule } from './TiOverflowServiceModule'; + +/** + * @ignore + * overflow配置项接口,用于[TiOverflowService.create]{@link ../injectables/TiOverflowService.html#create}参数使用 + */ +export interface TiOverflowConfig { + /** + * tip显示时所依据的元素,使用该元素位置显示tip位置,默认为宿主元素,当tip显示所依赖的元素的和宿主元素不一致时才需要定义 + */ + tipElement?: Element | Function; + /** + * tip内容,默认为宿主元素文本 + */ + tipContent?: string; + /** + * tip位置 + */ + tipPosition?: TiPositionType; + /** + * tip最大宽度 + */ + tipMaxWidth?: string; + /** + * @ignore + * 决定定位元素水平方向的元素,用于宿主元素水平方向位置与host元素不一致的场景,暂不对外开放 + */ + tipHostEleX?: Element; +} +/** + * @ignore + * create方法返回值 + */ +export interface TiOverflowRef { + destroy(): void; +} +/** + * @ignore + * 文本过长出...并tip提示配置项,使用该服务时需要引入模块TiOverflowServiceModule,该组件提供了两种使用方式: + * + * 1.服务方式(见如下说明) + * + * 2.指令方式:[TiOverflowDirective]{@link ../directives/TiOverflowDirective.html} + * + */ +@Injectable({ + providedIn: TiOverflowServiceModule +}) +export class TiOverflowService { + private renderer: Renderer2; + constructor(rendererFactory: RendererFactory2, private tiRenderer: TiRenderer, private tipService: TiTipService) { + this.renderer = rendererFactory.createRenderer(null, null); + } + /** + * 生成tip提示方法 + * @param hostElement 文本过长的宿主对象 + * @param config overflow配置项 + * 返回 {destroy(): void} 销毁文本过长的tip提示,使用服务方式时,需要在宿主元素销毁时,通过调用该方法销毁文本过长的tip提示 + */ + create(hostElement: Element, config?: TiOverflowConfig): TiOverflowRef { + this.tiRenderer.setStyles(hostElement, { + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden' + }); + // 非法情况处理 + if (!Util.isElement(hostElement)) { + TiLog.warn('overflow: hostEle type is not element'); + + // 防止外部使用报错,此处做返回值处理 + return { + destroy(): void {} + }; + } + // 如果该元素为inline元素时,宽度不生效会导致元素出...样式不生效,因此此处做处理 + // 修复SSR报错:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle !== 'undefined' && getComputedStyle(hostElement).display === 'inline') { + this.renderer.setStyle(hostElement, 'display', 'inline-block'); + } + const tipInstance: TiTipRef = this.generateTip(hostElement, config || {}); + + return { + destroy(): void { + if (tipInstance) { + tipInstance.hide(); + } + } + }; + } + /** + * @ignore + */ + public isOverflow(element: Element): boolean { + // 复制DOM,并计算元素宽度 + // 此处使用clone方式而不使用scrollWidth方式,是因为目前发现scrollWidth在IE下获取到的值有问题,不可作为文本溢出的判断条件 + const eleStyles: any = getComputedStyle(element); + const cloneEle: any = element.cloneNode(true); + this.tiRenderer.setStyles(cloneEle, { + // 涉及内容字体的相关样式处理 + fontSize: eleStyles.fontSize, + fontWeight: eleStyles.fontWeight, + fontFamily: eleStyles.fontFamily, + padding: eleStyles.padding, + paddingLeft: eleStyles.paddingLeft, // 处理在IE和火狐下获取padding为空问题:在火狐和IE下只能用只能用padding+[方位]的方式来获取元素的padding值 + paddingRight: eleStyles.paddingRight, + border: eleStyles.border, + boxSizing: eleStyles.boxSizing, + height: eleStyles.height, + // 涉及宽度布局的相关样式处理 + maxWidth: 'none', // 清除最大宽度样式,确保内容可显示完全 + width: 'auto', + overflow: 'visible', + display: 'inline-block', // display block的情况下,元素父容器设置margin时,导致导致body变宽,从而与元素本身宽度不匹配,会导致可显示完全但出tip的问题,因此此处改变其display方式 + visibility: 'hidden', // 元素隐藏但做占位处理 + whiteSpace: 'nowrap', + position: 'absolute', // 避免克隆元素影响页面高度,导致出滚动条 + left: '-9999px', + top: '-9999px' + }); + this.renderer.appendChild(document.body, cloneEle); + // 使用getBoundingClientRect而不使用getComputedStyle,是因为getComputedStyle在 + // 各浏览器获取到的宽度不一致(IE下取到的是内容宽度,而在Chrome和FF下取到的是整个元素宽度)。 + // IE下计算精度高(小数点后15位),多数中文和数字或英文混排的文本计算出来的 maxWidth 和 textWidth + // 由于精度高而导致有微小差距,从而影响了 isOverflow 的判断结果,从实际测试得来结论:保留两位小数能够保证判断结果更准确些。 + const maxWidth: number = parseFloat(element.getBoundingClientRect().width.toFixed(2)); + const textWidth: number = parseFloat(cloneEle.getBoundingClientRect().width.toFixed(2)); + // 此处没有使用angular的Renderer2是因为Renderer2.removeChild必须有变化检测才能在dom上生效, + // 此处考虑到性能不触发变化检测,所以选择使用原生的removeChild方法。 + document.body.removeChild(cloneEle); + + return textWidth > maxWidth; + } + private generateTip(element: Element, options: TiOverflowConfig): TiTipRef { + // 文本超长时,显示tip提示: + // tip显示位置元素设置:默认为element + const config: TiOverflowConfig = options || {}; + let hostEleConfig: any = config.tipElement; + if (typeof hostEleConfig === 'function') { + hostEleConfig = hostEleConfig(element); + } + const tipHostEle: Element = hostEleConfig || element; + + return this.tipService.create(tipHostEle, { + hostEleX: config.tipHostEleX, + position: config.tipPosition, + maxWidth: config.tipMaxWidth, + trigger: 'mouse', + showFn: (): TiTipShowInfo => { + // tipContent 为空 或者 未溢出情况下,不显示tip + // tipPosition设置为none是去除Select默认超长tip,改由template内容自定义tip + if (config.tipPosition === 'none' || config.tipContent === '' || !this.isOverflow(element)) { + return; + } + // tip内容设置:默认为元素中的文本内容 + let tipContent: string = config.tipContent; + if (Util.isUndefined(tipContent)) { + // content可能为"",因此此处判断undefined而不使用|| + // 使用element.textContent不能解析html片段 + tipContent = element.innerHTML; + } + + return { content: tipContent }; + } + }); + } +} diff --git a/src/overflow/lib/src/TiOverflowServiceModule.ts b/src/overflow/lib/src/TiOverflowServiceModule.ts new file mode 100644 index 0000000..050cf84 --- /dev/null +++ b/src/overflow/lib/src/TiOverflowServiceModule.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiTipServiceModule } from '@opentiny/ng-tip'; + +@NgModule({ + imports: [TiRendererModule, TiTipServiceModule] +}) +export class TiOverflowServiceModule {} diff --git a/src/pagination/demo/karma.conf.js b/src/pagination/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/pagination/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/pagination/demo/project.json b/src/pagination/demo/project.json new file mode 100644 index 0000000..eaf7808 --- /dev/null +++ b/src/pagination/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/pagination/demo", + "sourceRoot": "src/pagination/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/pagination", + "index": "src/pagination/demo/src/index.html", + "main": "src/pagination/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/pagination/demo/tsconfig.app.json", + "assets": ["src/pagination/demo/src/favicon.ico", "src/pagination/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "pagination-demo:build:production" + }, + "development": { + "browserTarget": "pagination-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js pagination" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/pagination/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/pagination/demo/tsconfig.spec.json", + "karmaConfig": "src/pagination/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/pagination/demo/src/app/AppComponent.ts b/src/pagination/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/pagination/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/pagination/demo/src/app/AppModule.ts b/src/pagination/demo/src/app/AppModule.ts new file mode 100644 index 0000000..25bcec5 --- /dev/null +++ b/src/pagination/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { PaginationTestModule } from './pagination/PaginationTestModule'; + +@NgModule({ + imports: [ + PaginationTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/pagination/demo/src/app/IndexComponent.ts b/src/pagination/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..710c47f --- /dev/null +++ b/src/pagination/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { PaginationTestModule } from './pagination/PaginationTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = PaginationTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/pagination/demo/src/app/app.html b/src/pagination/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/pagination/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/pagination/demo/src/app/pagination/PaginationAutohideComponent.ts b/src/pagination/demo/src/app/pagination/PaginationAutohideComponent.ts new file mode 100644 index 0000000..34fa4b4 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationAutohideComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { TiPageSizeConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './pagination-autohide.html' +}) +export class PaginationAutohideComponent { + currentPage: number = 1; + totalNumber: number = 400; + pageSize: TiPageSizeConfig = { + options: [20, 40, 80], + size: 40 + }; + onClick(): void { + this.totalNumber = this.totalNumber > 20 ? 12 : 400; + } +} diff --git a/src/pagination/demo/src/app/pagination/PaginationDisabledComponent.ts b/src/pagination/demo/src/app/pagination/PaginationDisabledComponent.ts new file mode 100644 index 0000000..dc7060b --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationDisabledComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './pagination-disabled.html' +}) +export class PaginationDisabledComponent { + currentPage: number = 2; + totalNumber: number = 300; + disabled: boolean = true; +} diff --git a/src/pagination/demo/src/app/pagination/PaginationEventComponent.ts b/src/pagination/demo/src/app/pagination/PaginationEventComponent.ts new file mode 100644 index 0000000..626bac4 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationEventComponent.ts @@ -0,0 +1,32 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiPaginationEvent } from '@opentiny/ng'; + +@Component({ + templateUrl: './pagination-event.html', + encapsulation: ViewEncapsulation.None +}) +export class PaginationEventComponent { + myLogs: Array = []; + currentPage: number = 10; + totalNumber: number = 400; + + onPageNumChange(event: TiPaginationEvent): void { + const str: string = `onPageNumChange data=${JSON.stringify(event)}`; + this.myLogs = [...this.myLogs, str]; + } + + onCurrentPageChange(event: number): void { + const str: string = `onCurrentPageChange data=${event}`; + this.myLogs = [...this.myLogs, str]; + } + + onPageUpdate(event: TiPaginationEvent): void { + const str: string = `onPageUpdate data=${JSON.stringify(event)}`; + this.myLogs = [...this.myLogs, str]; + } + + onTotalNumberChange(event: TiPaginationEvent): void { + const str: string = `onTotalNumberChange data=${JSON.stringify(event)}`; + this.myLogs = [...this.myLogs, str]; + } +} diff --git a/src/pagination/demo/src/app/pagination/PaginationFixedComponent.ts b/src/pagination/demo/src/app/pagination/PaginationFixedComponent.ts new file mode 100644 index 0000000..6dd448d --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationFixedComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './pagination-fixed.html' +}) +export class PaginationFixedComponent { + totalNumber: number = 300; + currentPage: number = 10; + isShow: boolean = false; + onClick(): void { + this.isShow = !this.isShow; + } +} diff --git a/src/pagination/demo/src/app/pagination/PaginationLoadingComponent.ts b/src/pagination/demo/src/app/pagination/PaginationLoadingComponent.ts new file mode 100644 index 0000000..38e5d0d --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationLoadingComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { TiPageSizeConfig } from '@opentiny/ng-pagination'; + +@Component({ + templateUrl: './pagination-loading.html', +}) +export class PaginationLoadingComponent { + pageSize: TiPageSizeConfig = { + size: 10, + options: [10, 20, 50], + }; + loading: boolean = false; + + switch(): void { + this.loading = !this.loading; + } +} diff --git a/src/pagination/demo/src/app/pagination/PaginationPageselectwidthComponent.ts b/src/pagination/demo/src/app/pagination/PaginationPageselectwidthComponent.ts new file mode 100644 index 0000000..7bae11f --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationPageselectwidthComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './pagination-pageselectwidth.html' +}) +export class PaginationPageselectwidthComponent { + currentPage: number = 1; + totalNumber: number = 60000; + pageSelectVirtual: boolean = true; +} diff --git a/src/pagination/demo/src/app/pagination/PaginationPagesizeComponent.ts b/src/pagination/demo/src/app/pagination/PaginationPagesizeComponent.ts new file mode 100644 index 0000000..8ca813f --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationPagesizeComponent.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { TiPageSizeConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './pagination-pagesize.html' +}) +export class PaginationPagesizeComponent { + currentPage: number = 2; + totalNumber: number = 300; + currentPage1: number = 3; + totalNumber1: number = 40000; + currentPage2: number = 3; + totalNumber2: number = 400; + currentPage3: number = 2; + totalNumber3: number = 300; + pageSize: TiPageSizeConfig = { + options: [200, 400, 600, 800], + size: 200 + }; + pageSize1: TiPageSizeConfig = { + options: [10, 20, 50], + size: 10, + width: '100px' + }; + pageSize2: TiPageSizeConfig = { + options: [20, 40, 60, 80], + size: 20, + hide: true + }; +} diff --git a/src/pagination/demo/src/app/pagination/PaginationShowgotolinkComponent.ts b/src/pagination/demo/src/app/pagination/PaginationShowgotolinkComponent.ts new file mode 100644 index 0000000..0c20a22 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationShowgotolinkComponent.ts @@ -0,0 +1,10 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './pagination-showgotolink.html', + encapsulation: ViewEncapsulation.None +}) +export class PaginationShowgotolinkComponent { + currentPage: number = 10; + totalNumber: number = 400; +} diff --git a/src/pagination/demo/src/app/pagination/PaginationShowlastpageComponent.ts b/src/pagination/demo/src/app/pagination/PaginationShowlastpageComponent.ts new file mode 100644 index 0000000..82e8ff5 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationShowlastpageComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './pagination-showlastpage.html' +}) +export class PaginationShowlastpageComponent { + currentPage: number = 1; + totalNumber: number = 400; + showLastPage: boolean = false; +} diff --git a/src/pagination/demo/src/app/pagination/PaginationShowtotalnumberComponent.ts b/src/pagination/demo/src/app/pagination/PaginationShowtotalnumberComponent.ts new file mode 100644 index 0000000..aade384 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationShowtotalnumberComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './pagination-showtotalnumber.html' +}) +export class PaginationShowtotalnumberComponent { + currentPage: number = 1; + totalNumber: number = 400; +} diff --git a/src/pagination/demo/src/app/pagination/PaginationTestModule.ts b/src/pagination/demo/src/app/pagination/PaginationTestModule.ts new file mode 100644 index 0000000..f013d81 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationTestModule.ts @@ -0,0 +1,92 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiPaginationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { PaginationDisabledComponent } from './PaginationDisabledComponent'; +import { PaginationShowlastpageComponent } from './PaginationShowlastpageComponent'; +import { PaginationTypeComponent } from './PaginationTypeComponent'; +import { PaginationEventComponent } from './PaginationEventComponent'; +import { PaginationPagesizeComponent } from './PaginationPagesizeComponent'; +import { PaginationShowgotolinkComponent } from './PaginationShowgotolinkComponent'; +import { PaginationPageselectwidthComponent } from './PaginationPageselectwidthComponent'; +import { PaginationShowtotalnumberComponent } from './PaginationShowtotalnumberComponent'; +import { PaginationAutohideComponent } from './PaginationAutohideComponent'; +import { PaginationFixedComponent } from './PaginationFixedComponent'; +import { PaginationLoadingComponent } from './PaginationLoadingComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiPaginationModule, + TiButtonModule, + DemoLogModule, + RouterModule.forChild(PaginationTestModule.ROUTES) + ], + declarations: [ + PaginationDisabledComponent, + PaginationShowlastpageComponent, + PaginationTypeComponent, + PaginationEventComponent, + PaginationPagesizeComponent, + PaginationShowtotalnumberComponent, + PaginationAutohideComponent, + PaginationShowgotolinkComponent, + PaginationPageselectwidthComponent, + PaginationFixedComponent, + PaginationLoadingComponent + ] +}) +export class PaginationTestModule { + static readonly LINKS: Array = [{ href: 'components/TiPaginationComponent.html', label: 'Pagination' }]; + static readonly ROUTES: Routes = [ + { + path: 'pagination/pagination-type', + component: PaginationTypeComponent + }, + { + path: 'pagination/pagination-pagesize', + component: PaginationPagesizeComponent + }, + { + path: 'pagination/pagination-show-lastpage', + component: PaginationShowlastpageComponent + }, + { + path: 'pagination/pagination-disabled', + component: PaginationDisabledComponent + }, + { + path: 'pagination/pagination-event', + component: PaginationEventComponent + }, + { + path: 'pagination/pagination-showtotalnumber', + component: PaginationShowtotalnumberComponent + }, + { + path: 'pagination/pagination-autohide', + component: PaginationAutohideComponent + }, + { + path: 'pagination/pagination-fixed', + component: PaginationFixedComponent + }, + { + path: 'pagination/pagination-showgotolink', + component: PaginationShowgotolinkComponent + }, + { + path: 'pagination/pagination-pageselectwidth', + component: PaginationPageselectwidthComponent + }, + { + path: 'pagination/pagination-loading', + component: PaginationLoadingComponent + } + ]; +} diff --git a/src/pagination/demo/src/app/pagination/PaginationTypeComponent.ts b/src/pagination/demo/src/app/pagination/PaginationTypeComponent.ts new file mode 100644 index 0000000..03c0798 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/PaginationTypeComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './pagination-type.html' +}) +export class PaginationTypeComponent { + currentPage: number = 10; + totalNumber: number = 400; + + typeSimple: string = 'simple'; + currentPageSimple: number = 2; + totalNumberSimple: number = 1600; + + typeMini: string = 'mini'; + currentPageMini: number = 2; + totalNumberMini: number = 300; +} diff --git a/src/pagination/demo/src/app/pagination/pagination-autohide.html b/src/pagination/demo/src/app/pagination/pagination-autohide.html new file mode 100644 index 0000000..14be28a --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-autohide.html @@ -0,0 +1,4 @@ + + +
    + diff --git a/src/pagination/demo/src/app/pagination/pagination-disabled.html b/src/pagination/demo/src/app/pagination/pagination-disabled.html new file mode 100644 index 0000000..b926900 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-disabled.html @@ -0,0 +1 @@ + diff --git a/src/pagination/demo/src/app/pagination/pagination-event.html b/src/pagination/demo/src/app/pagination/pagination-event.html new file mode 100644 index 0000000..bf36502 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-event.html @@ -0,0 +1,10 @@ + + diff --git a/src/pagination/demo/src/app/pagination/pagination-fixed.html b/src/pagination/demo/src/app/pagination/pagination-fixed.html new file mode 100644 index 0000000..73a024a --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-fixed.html @@ -0,0 +1,9 @@ + +
    + diff --git a/src/pagination/demo/src/app/pagination/pagination-loading.html b/src/pagination/demo/src/app/pagination/pagination-loading.html new file mode 100644 index 0000000..e94afb9 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-loading.html @@ -0,0 +1,18 @@ + +
    + + +
    + diff --git a/src/pagination/demo/src/app/pagination/pagination-pageselectwidth.html b/src/pagination/demo/src/app/pagination/pagination-pageselectwidth.html new file mode 100644 index 0000000..70cba2b --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-pageselectwidth.html @@ -0,0 +1,8 @@ + diff --git a/src/pagination/demo/src/app/pagination/pagination-pagesize.html b/src/pagination/demo/src/app/pagination/pagination-pagesize.html new file mode 100644 index 0000000..e33b503 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-pagesize.html @@ -0,0 +1,28 @@ +

    1. options

    + +

    2. width

    + +

    3. hide

    + +

    4. largeData

    + diff --git a/src/pagination/demo/src/app/pagination/pagination-showgotolink.html b/src/pagination/demo/src/app/pagination/pagination-showgotolink.html new file mode 100644 index 0000000..302e638 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-showgotolink.html @@ -0,0 +1,6 @@ + diff --git a/src/pagination/demo/src/app/pagination/pagination-showlastpage.html b/src/pagination/demo/src/app/pagination/pagination-showlastpage.html new file mode 100644 index 0000000..1d4ccd6 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-showlastpage.html @@ -0,0 +1,6 @@ + diff --git a/src/pagination/demo/src/app/pagination/pagination-showtotalnumber.html b/src/pagination/demo/src/app/pagination/pagination-showtotalnumber.html new file mode 100644 index 0000000..290199e --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-showtotalnumber.html @@ -0,0 +1,6 @@ + diff --git a/src/pagination/demo/src/app/pagination/pagination-type.html b/src/pagination/demo/src/app/pagination/pagination-type.html new file mode 100644 index 0000000..5893966 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/pagination-type.html @@ -0,0 +1,16 @@ +

    1. default

    + +

    2. simple

    + +

    3. mini

    + diff --git a/src/pagination/demo/src/app/pagination/webdoc/pagination-demos.js b/src/pagination/demo/src/app/pagination/webdoc/pagination-demos.js new file mode 100644 index 0000000..18bcbef --- /dev/null +++ b/src/pagination/demo/src/app/pagination/webdoc/pagination-demos.js @@ -0,0 +1,161 @@ +export default { + column: '2', + demos: [ + { + demoId: 'pagination-type', + name: { + 'zh-CN': '分页类型', + 'en-US': 'pagination type', + }, + desc: { + 'zh-CN': + '

    通过属性type配置分页组件的类型,包括default(默认)simplemini。', + 'en-US': '

    pagination type

    ', + }, + apis: [ + 'TiPaginationComponent.properties.type', + 'TiPaginationComponent.properties.currentPage', + 'TiPaginationComponent.properties.totalNumber', + ], + }, + { + demoId: 'pagination-pagesize', + name: { + 'zh-CN': '下拉框选项', + 'en-US': 'pagination pagesize', + }, + desc: { + 'zh-CN': + '通过属性pageSize实现分页组件每页显示条数的相关配置,包括每页显示条数选项列表、每页显示条数选项列表宽度、隐藏每页显示条数选项列表、默认每页显示条数选项列表。', + 'en-US': '

    pagination pagesize

    ', + }, + apis: [ + 'TiPaginationComponent.properties.pageSize', + 'TiPaginationComponent.properties.largeData', + 'TiPageSizeConfig', + ], + }, + { + demoId: 'pagination-pageselectwidth', + name: { + 'zh-CN': 'mini 类型大数据场景', + 'en-US': 'pagination pageselectwidth', + }, + desc: { + 'zh-CN': + '通过属性pageSelectWidth配置 mini 类型下拉框宽度;通过属性pageSelectVirtual配置 mini 类型下拉是否开启虚拟滚动。', + 'en-US': '

    pagination pageselectwidth

    ', + }, + apis: [ + 'TiPaginationComponent.properties.pageSelectWidth', + 'TiPaginationComponent.properties.pageSelectVirtual', + ], + }, + { + demoId: 'pagination-showlastpage', + name: { + 'zh-CN': '分页类型', + 'en-US': 'pagination showlastpage', + }, + desc: { + 'zh-CN': + '

    通过属性showLastPage配置是否展示最后一页页码。', + 'en-US': '

    pagination showlastpage

    ', + }, + apis: ['TiPaginationComponent.properties.showLastPage'], + }, + { + demoId: 'pagination-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'pagination disabled', + }, + desc: { + 'zh-CN': '通过属性disabled配置分页是否为禁用状态。', + 'en-US': '

    pagination disabled

    ', + }, + apis: ['TiPaginationComponent.properties.disabled'], + }, + { + demoId: 'pagination-event', + name: { + 'zh-CN': '事件', + 'en-US': 'pagination event', + }, + desc: { + 'zh-CN': + '当操作选择框改变每页显示条数的时候触发pageNumChange事件;当组件内部改变当前页码的时候触发currentPageChange事件;当操作选择框改变每页显示条数或操作页码改变当前页码的时候触发pageUpdate事件;当组件内部改变总条数的时候触发totalNumberChange事件。', + 'en-US': '

    pagination event

    ', + }, + apis: [ + 'TiPaginationComponent.events.currentPageChange', + 'TiPaginationComponent.events.pageNumChange', + 'TiPaginationComponent.events.pageUpdate', + 'TiPaginationComponent.events.totalNumberChange', + 'TiPaginationEvent', + ], + }, + { + demoId: 'pagination-showtotalnumber', + name: { + 'zh-CN': '展示总条数', + 'en-US': 'pagination showtotalnumber', + }, + desc: { + 'zh-CN': '

    通过属性showTotalNumber配置是否展示总条数。', + 'en-US': '

    pagination showtotalnumber

    ', + }, + apis: ['TiPaginationComponent.properties.showTotalNumber'], + }, + { + demoId: 'pagination-autohide', + name: { + 'zh-CN': '自动隐藏', + 'en-US': 'pagination autohide', + }, + desc: { + 'zh-CN': + '通过属性autoHide配置总条数少于每页显示条数选项列表中的最小值时是否隐藏分页。', + 'en-US': '

    pagination autohide

    ', + }, + apis: ['TiPaginationComponent.properties.autoHide'], + }, + { + demoId: 'pagination-showgotolink', + name: { + 'zh-CN': '跳转按钮', + 'en-US': 'pagination showgotolink', + }, + desc: { + 'zh-CN': + '通过属性showGotoLink配置是否显示跳转按钮,仅在default生效。', + 'en-US': '

    pagination showgotolink

    ', + }, + apis: ['TiPaginationComponent.properties.showGotoLink'], + }, + { + demoId: 'pagination-fixed', + name: { + 'zh-CN': '分页吸底', + 'en-US': 'pagination fixed', + }, + desc: { + 'zh-CN': '

    通过属性fixed配置是否开启分页吸底。', + 'en-US': '

    pagination fixed

    ', + }, + apis: ['TiPaginationComponent.properties.fixed'], + }, + { + demoId: 'pagination-loading', + name: { + 'zh-CN': '加载态', + 'en-US': 'pagination loading', + }, + desc: { + 'zh-CN': '

    通过属性loading配置是否开启加载态。', + 'en-US': '

    pagination loading

    ', + }, + apis: ['TiPaginationComponent.properties.loading'], + }, + ], +}; diff --git a/src/pagination/demo/src/app/pagination/webdoc/pagination.cn.md b/src/pagination/demo/src/app/pagination/webdoc/pagination.cn.md new file mode 100644 index 0000000..358d806 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/webdoc/pagination.cn.md @@ -0,0 +1,24 @@ +--- +title: Pagination 分页 +--- +# Pagination 分页 + +
    + +Pagination 是实现对数据分页显示的组件。   + +```typescript +import { TiPaginationModule } from '@opentiny/ng'; +``` + +
    + +
    + +Pagination 是实现对数据分页显示的组件。   + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/pagination/demo/src/app/pagination/webdoc/pagination.en.md b/src/pagination/demo/src/app/pagination/webdoc/pagination.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/pagination/demo/src/app/pagination/webdoc/pagination.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/pagination/demo/src/favicon.ico b/src/pagination/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/pagination/demo/src/index.html b/src/pagination/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/pagination/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/pagination/demo/src/main.ts b/src/pagination/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/pagination/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/pagination/demo/test.ts b/src/pagination/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/pagination/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/pagination/demo/tsconfig.app.json b/src/pagination/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/pagination/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/pagination/demo/tsconfig.spec.json b/src/pagination/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/pagination/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/pagination/lib/index.ts b/src/pagination/lib/index.ts new file mode 100644 index 0000000..caea4ff --- /dev/null +++ b/src/pagination/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiPaginationModule'; diff --git a/src/pagination/lib/ng-package.json b/src/pagination/lib/ng-package.json new file mode 100644 index 0000000..35c4d3b --- /dev/null +++ b/src/pagination/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/pagination", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/pagination/lib/package.json b/src/pagination/lib/package.json new file mode 100644 index 0000000..ef22cf8 --- /dev/null +++ b/src/pagination/lib/package.json @@ -0,0 +1,18 @@ +{ + "name": "@opentiny/ng-pagination", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-table": "~1.0.0-beta.0", + "@opentiny/ng-select": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/pagination/lib/project.json b/src/pagination/lib/project.json new file mode 100644 index 0000000..0299b31 --- /dev/null +++ b/src/pagination/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/pagination/lib", + "sourceRoot": "src/pagination/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/pagination"], + "options": { + "project": "src/pagination/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/pagination"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js pagination" + }, + { + "command": "ng default-build pagination" + }, + { + "command": "node build/clear-default-theme.js pagination" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/pagination && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build pagination && ng pack pagination && node build/publish.js pagination --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/pagination/lib/src/TiPaginationComponent.ts b/src/pagination/lib/src/TiPaginationComponent.ts new file mode 100644 index 0000000..f6fae05 --- /dev/null +++ b/src/pagination/lib/src/TiPaginationComponent.ts @@ -0,0 +1,853 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ElementRef, + EventEmitter, + Input, + Optional, + Output, + Renderer2, + SimpleChange, + SimpleChanges, + TemplateRef, + ViewChild, + ChangeDetectorRef, + ChangeDetectionStrategy, + NgZone +} from '@angular/core'; +import { Util } from '@opentiny/ng-utils'; +import { TiLocale, TiLocaleFormat } from '@opentiny/ng-locale'; // 获取词条 +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiTableComponent } from '@opentiny/ng-table'; +import { TiSelectComponent } from '@opentiny/ng-select'; +import { Subscription } from 'rxjs'; +import packageInfo from '../package.json'; + +/** + * 实现每页显示条数的相关配置的数据类型 + */ +export interface TiPageSizeConfig { + /** + * 每页显示条数选项列表 + */ + options?: Array; + /** + * 默认每页显示条数 + */ + size?: number; + /** + * 每页显示条数选项列表宽度 + */ + width?: string; + /** + * 是否隐藏每页显示条数选项列表。 + */ + hide?: boolean; +} + +/** + * 当分页更新时触发事件通知出去的参数 + * + * 更新场景包括:1.当前页码改变,2.每页显示条数改变,3.总条数改变。 + */ +export interface TiPaginationEvent { + /** + * 当前页码数 + */ + currentPage: number; + /** + * 每页显示条数 + */ + size: number; + /** + * 总条数 + */ + totalNumber: number; +} + +/** + * @ignore + * 页码的数据类型 + */ +export interface TiPageItem { + id: string; + label: any; +} + +/** + * Pagination分页组件 + * + * 分页组件一般情况下与表格组件配合使用,实现对表格数据的分页显示。 + * + */ +@Component({ + selector: 'ti-pagination', + templateUrl: './pagination.html', + styleUrls: ['./pagination.less', '../../../text/lib/src/text.less'], // 引用text的less文件 + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-page-container]': 'true', + '[class.ti3-pag-container-disable]': 'this.disabled', + '[attr.unselectable]': '"on"', + '[class.ti3-page-container-fixed]': 'fixed' + } +}) +export class TiPaginationComponent extends TiFormComponent { + private static readonly MIN_PAGE: number = 1; + /** + * 分页的类型 + */ + @Input() type: 'default' | 'simple' | 'mini' = 'default'; + /** + * 必选,数据总条数 + */ + @Input() totalNumber: number; + /** + * 组件内部改变总条数时触发的回调,参数:总条数。 + */ + @Output() readonly totalNumberChange: EventEmitter = new EventEmitter(); + /** + * 当前页码数 + */ + @Input() currentPage: number = 1; + /** + * 组件内部改变当前页码时触发的回调,参数:当前页码数。 + */ + @Output() readonly currentPageChange: EventEmitter = new EventEmitter(); + /** + * 必选,每页展示条数的配置 + */ + @Input() pageSize: TiPageSizeConfig; + /** + * 是否显示总条数 + */ + @Input() showTotalNumber: boolean = true; + /** + * 是否显示页码跳转按钮 + */ + @Input() showGotoLink: boolean = false; + /** + * 是否开启自动隐藏 + */ + @Input() autoHide: boolean = false; + /** + * 是否禁用 + */ + @Input() disabled: boolean = false; + /** + * 是否显示最后一页 + */ + @Input() showLastPage: boolean = true; + /** + * 每页展示条数是否使用大数据配置:[20, 50, 100];默认每页显示20条 + */ + @Input() largeData: boolean = false; + /** + * 是否开启分页吸底 + */ + @Input() fixed: boolean = false; + /** + * mini类型分页的下拉框宽度 + */ + @Input() pageSelectWidth: string = '70px'; + /** + * mini类型分页的下拉是否开启虚拟滚动 + */ + @Input() pageSelectVirtual: boolean = false; + /** + * 是否处于加载态 + */ + @Input() loading: boolean = false; + /** + * 当操作下拉框改变每页显示条数时触发的回调,参数:当前页码、每页显示条数和总条数。 + */ + @Output() readonly pageNumChange: EventEmitter = new EventEmitter(); + /** + * 当操作下拉框改变每页显示条数或操作页码改变当前页码时触发的回调,参数:当前页码、每页显示条数和总条数。 + */ + @Output() readonly pageUpdate: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + @ViewChild('mini', { static: true }) mini: TemplateRef = undefined; + /** + * @ignore + */ + @ViewChild('input') inputRef: ElementRef; + /** + * @ignore + * 非mini类型:select下拉组件 + */ + public itemsPerPage: any; + /** + * @ignore + * mini类型:select下拉组件 + */ + public itemsMini: any; + /** + * @ignore + * 用户输入即将跳转的页码 + */ + public gotoPage: { page?: any } = {}; + /** + * @ignore + * 页码列表 + */ + public pages: Array<{ key: any; active: boolean }>; + protected versionInfo: string = super.getVersion(packageInfo); + private hidePage: boolean = false; + private totalPageNum: number; + private oldCurrentPage: number; + private oldTotalNumber: number; + private oldSelected: number; + private oldSize: number; + private subscription: Subscription; + private updateFocusableElems: boolean; + private defaultPageSize: TiPageSizeConfig = { + options: [10, 20, 50], // 下拉选项列表 + size: 10, // 默认每页显示10条 + width: '60px', // select宽度 + hide: false // 设置是否在页面上隐藏每页显示条数选项:下拉列表 + }; + private largePageSize: TiPageSizeConfig = { + options: [20, 50, 100], // 下拉选项列表 + size: 20 // 默认每页显示20条 + }; + private unlistenTiReLayoutX: () => void; + constructor( + protected render: Renderer2, + protected hostRef: ElementRef, + protected changeDetectorRef: ChangeDetectorRef, + @Optional() private table: TiTableComponent, + @Optional() private select: TiSelectComponent, + private zone: NgZone + ) { + super(hostRef, render, changeDetectorRef); + } + + /** + * @description 创建每一个页码对应的数据 + * @params key 页码显示,为数字或者是"···" + * @params isActive 是否为当前激活页 + */ + private static makePage(key: any, isActive: boolean): { key: any; active: boolean } { + return { + key, + active: isActive + }; + } + + ngOnInit(): void { + super.ngOnInit(); + // 组件对外各个接口的初始化处理 + this.initData(); + this.renderPage(); + + if (this.isWithTable()) { + this.subscription = this.table.paginationSubject.subscribe((value: { totalNumber?: number; currentPage?: number }) => { + if (!Util.isUndefined(value.totalNumber) && value.totalNumber !== this.totalNumber) { + this.totalNumber = value.totalNumber; + this.verifyCurrentPage(this.itemsPerPage.selected, true); + this.totalNumberChange.emit(this.totalNumber); + } + + if (!Util.isUndefined(value.currentPage) && value.currentPage !== this.currentPage) { + this.currentPage = value.currentPage; + this.currentPageChange.emit(this.currentPage); + } + }); + // 表格记忆 + this.updateFromStorage(); + } + + // select下拉面板(dropsearch)中使用分页 + if (this.select) { + this.select.dropsearchCom.pagination = this; + } + + // 分页吸底 + if (this.fixed && this.isWithTable()) { + this.zone.runOutsideAngular(() => { + this.unlistenTiReLayoutX = this.renderer.listen(document, 'tiReLayoutX', () => { + this.setFixedStyle(); + }); + }); + } + } + + ngOnChanges(changes: SimpleChanges): void { + const currentPageObj: SimpleChange = changes['currentPage']; + const totalNumberObj: SimpleChange = changes['totalNumber']; + const loadingObj: SimpleChange = changes['loading']; + if (loadingObj && !loadingObj.firstChange) { + this.renderPage(); + } + if ((currentPageObj && !currentPageObj.firstChange) || (totalNumberObj && !totalNumberObj.firstChange)) { + this.verifyCurrentPage(this.itemsPerPage.selected, true); + } + } + + ngAfterViewInit(): void { + // 初始化设置 + if (this.fixed && this.isWithTable()) { + this.setFixedStyle(); + } + } + + ngDoCheck(): void { + let isPageSizeChange: boolean = false; + // 监听开发者设置的 pageSize.options 的改变 + if (this.pageSize?.options && this.pageSize.options !== this.itemsPerPage.options) { + this.itemsPerPage.options = this.pageSize.options; + isPageSizeChange = true; + } + // 监听设置的 pageSize.size 的改变 + if (!Util.isUndefined(this.pageSize) && this.pageSize.size !== this.oldSize) { + this.itemsPerPage.selected = this.pageSize.size; + this.oldSize = this.pageSize.size; + isPageSizeChange = true; + } + + if (isPageSizeChange) { + const index: number = this.itemsPerPage.options.indexOf(this.itemsPerPage.selected); + if (index === -1) { + this.itemsPerPage.selected = this.itemsPerPage.options[0]; + this.syncPageSize(this.itemsPerPage.selected); + this.oldSize = this.pageSize.size; + } + this.verifyCurrentPage(this.itemsPerPage.selected, true); + this.changeDetectorRef.markForCheck(); + } + + if ( + this.oldTotalNumber !== this.totalNumber || + this.itemsPerPage.selected !== this.oldSelected || + this.oldCurrentPage !== this.currentPage + ) { + this.renderPage(); + this.oldTotalNumber = this.totalNumber; + this.oldSelected = this.itemsPerPage.selected; + this.oldCurrentPage = this.currentPage; + this.updateFocusableElems = true; // select使用过这个变量后,会重置为false + } + } + ngAfterViewChecked(): void { + // select中使用的分页为default类型且不带左边select,所以此处限定type === 'default' + if (this.type === 'default' && this.updateFocusableElems) { + // 修复SSR报错:ERROR TypeError: this.nativeElement.querySelectorAll(...) is not a function or its return value is not iterable + this.setFocusableElems(Array.prototype.slice.call(this.nativeElement.querySelectorAll('a'))); + } + } + + ngOnDestroy(): void { + if (this.subscription) { + this.subscription.unsubscribe(); + } + + if (this.unlistenTiReLayoutX) { + this.unlistenTiReLayoutX(); + } + } + + // 数据初始化 + private initData(): void { + // 总条数数字国际化处理,拼接成 ---> 总条数: 条数 + this.setTotalNumberFormat(); + + // 初始化获取itemsPerPage:select组件所需的属性 + this.itemsPerPage = this.getFormatedSize(); + + this.verifyCurrentPage(this.itemsPerPage.selected, true); + + this.oldCurrentPage = this.currentPage; + this.oldTotalNumber = this.totalNumber; + this.oldSize = this.itemsPerPage.selected; + this.oldSelected = this.itemsPerPage.selected; + } + + // 是否有表格当前页记忆 + private isStorageCurrentPage(): boolean { + return ( + this.table.storageId && + (this.table.storageConfig.pagination === true || + (this.table.storageConfig.pagination && this.table.storageConfig.pagination['currentPage'] === true)) + ); + } + + // 是否有表格每页个数记忆 + private isStorageItemsPerPage(): boolean { + return ( + this.table.storageId && + (this.table.storageConfig.pagination === true || + (this.table.storageConfig.pagination && this.table.storageConfig.pagination['itemsPerPage'] === true)) + ); + } + // 处理表格记忆 + private updateFromStorage(): void { + // 修复SSR报错:ERROR TypeError: Cannot read property 'currentPage' of undefined + // 当前页 + if (this.isStorageCurrentPage()) { + const currentPage: number = this.table.sessionStorageState && this.table.sessionStorageState.currentPage; + if (currentPage > 0) { + // 此处异步为处理ExpressionChangedAfterItHasBeenCheckedError报错问题,onpush正常default报错; + Promise.resolve().then(() => { + this.currentPage = currentPage; + this.currentPageChange.emit(currentPage); + }); + } + } + // 修复SSR错误:ERROR TypeError: Cannot read property 'itemsPerPage' of undefined + // 每页条数 + if (this.isStorageItemsPerPage()) { + const itemsPerPage: number = this.table.localStorageState && this.table.localStorageState.itemsPerPage; + if (itemsPerPage > 0) { + this.pageSize.size = itemsPerPage; + const pagination: TiPaginationEvent = { + currentPage: this.currentPage, + size: itemsPerPage, + totalNumber: this.totalNumber + }; + this.pageNumChange.emit(pagination); + } + } + } + + /** + * 根据接口配置来渲染页面 + */ + private renderPage(): void { + if (this.type === 'mini') { + this.configMini(); + } else { + // goto页码与当前页码保持一致 + this.gotoPage = { + page: this.currentPage + }; + + // 页码列表 + this.pages = this.getPages(); + } + + // 用户开启自动隐藏功能且无需显示分页(若总条数 =< 最小的每页显示条数)时,则不需要显示分页时,隐藏分页 + // 初始化totalNumber undefined + this.hidePage = + this.autoHide && (Util.isUndefined(this.totalNumber) || Math.min.apply(null, this.itemsPerPage.options) >= this.totalNumber); + if (this.hidePage) { + this.render.setStyle(this.hostRef.nativeElement, 'display', 'none'); + } else { + this.render.setStyle(this.hostRef.nativeElement, 'display', 'inline-block'); + } + + // 若分页与ti-table配合使用,则当分页更新时,触发ti-table的updatePagination + // 进而触发表格更新回调 + if (this.isWithTable()) { + this.table.updatePagination(this.currentPage, this.itemsPerPage.selected); + } + } + + /** + * @ignore + * 对总页数进行数字国际化处理 + */ + public setTotalNumberFormat(): string { + // 数字国际化格式:整数位保留最小位数.小数位保留最小位数-小数位最大保留位置 + const formatTotalNumber: string = Util.isUndefined(this.totalNumber) ? '0' : TiLocaleFormat.formatNumber(this.totalNumber, '1.0-0'); + + return TiLocale.getLocaleWords().tiPagination['totalLabel'] + formatTotalNumber; + } + + private getFormatedSize(): any { + if (!this.pageSize) { + this.pageSize = this.largeData ? this.largePageSize : this.defaultPageSize; + } + const sizeConfig: TiPageSizeConfig = this.largeData + ? { ...this.defaultPageSize, ...this.largePageSize, ...this.pageSize } + : { ...this.defaultPageSize, ...this.pageSize }; + let index: number = sizeConfig.options.indexOf(sizeConfig.size); + if (index === -1) { + index = 0; + this.syncPageSize(sizeConfig.options[index]); + } + + return { + selected: sizeConfig.options[index], // select选中项 + options: sizeConfig.options, // 每页显示条数选项 + hide: sizeConfig.hide, // 是否显示 + style: { + width: sizeConfig.width // 宽度设置 + } + }; + } + + /** + * 同步变化开发者的pageSize数据 + */ + private syncPageSize(pageSizeNum: number): void { + if (this.pageSize instanceof Object) { + this.pageSize.size = pageSizeNum; + } + } + + /** + * @ignore + * 当前页码是否为最小页码 + */ + public noPrevious(): boolean { + return this.currentPage === TiPaginationComponent.MIN_PAGE; + } + + /** + * @ignore + * 当前页码是否为最大页码 + */ + public noNext(): boolean { + return this.currentPage === this.totalPageNum; + } + + /** + * @ignore + * 当前页码是否为'···' 不可被聚焦 + */ + public isEllipse(page: any): boolean { + return page.key === '···'; + } + + /** + * @ignore + * 操作pageSize选择框,每页显示条数改变时触发 + */ + public onSizeChange(pageSizeNum: number): void { + this.syncPageSize(pageSizeNum); + // pageSize变化时,默认跳转到第1页,并触发currentPageChange事件 + if (this.currentPage !== 1) { + this.currentPage = 1; + this.currentPageChange.emit(this.currentPage); + } + const pagination: TiPaginationEvent = { + currentPage: this.currentPage, + size: pageSizeNum, + totalNumber: this.totalNumber + }; + this.pageNumChange.emit(pagination); + this.pageUpdate.emit(pagination); + } + + // 通过操作选择某一页 + private selectPage(page: any, evt?: Event): void { + // tiny2中此处阻止a标签浏览器默认事件 + if (!Util.isUndefined(evt)) { + evt.preventDefault(); + } + + if (this.disabled) { + return; + } + + const selectedPage: number = parseInt(page, 10); + if (selectedPage !== this.currentPage) { + this.currentPage = selectedPage; + this.currentPageChange.emit(this.currentPage); + this.pageUpdate.emit({ + currentPage: this.currentPage, + size: this.itemsPerPage.selected, + totalNumber: this.totalNumber + }); + } + } + + /** + * 计算总页数 + */ + private calculateTotalPages(pageSizeNum: number): number { + const totalPages: number = Math.ceil(this.totalNumber / pageSizeNum); + + return Math.max(totalPages || 0, 1); // 当totalPages是undefined时,取0 + } + + /** + * 组装mini类型时模板需要的数据 + */ + private configMini(): void { + const options: Array = []; + for (let i: number = 1; i <= this.totalPageNum; i++) { + options.push({ + id: `${i}`, + label: `${i}/${this.totalPageNum}` + }); + } + this.itemsMini = { + options, + selected: options[this.currentPage - 1], + change: (option: TiPageItem): void => { + // 选项改变时,修改选中页 + this.selectPage(option.id); + } + }; + } + + /** + * @description 根据每页显示条数及数据总条数获取页码列表 + */ + private getPages(): Array<{ key: any; active: boolean }> { + if (this.loading) { + return this.getLoadingPages(); + } + const startAndEndPage: Array = this.getInterval(); // 获得显示的起始和结束页 + const startPagNum: number = startAndEndPage[0]; + const endPagNum: number = startAndEndPage[1]; + const pages: Array<{ key: any; active: boolean }> = []; + const restPageNum: number = this.totalPageNum - this.currentPage; // 剩余页数 + + // 只有1页的情况 + if (this.totalPageNum === 1) { + pages.push(TiPaginationComponent.makePage(1, true)); + + return pages; + } + + // 当总页数大于8,并且中间连续按钮的起始位置没有和第1页相连时,创建第1页按钮和省略号 + if (startPagNum > 2 && this.totalPageNum > 8) { + pages.push(TiPaginationComponent.makePage(1, this.currentPage === 1)); + pages.push(TiPaginationComponent.makePage('···', false)); + } + + // 创建中间页按钮 + for (let i: number = startPagNum; i <= endPagNum; i++) { + pages.push(TiPaginationComponent.makePage(i, this.currentPage === i)); + } + + // 当总页数大于8,并且中间连续按钮的结束位置没有和最后一页按钮相连时,创建省略号 + // 当总页数大于8,并且最后一页隐藏且不是后四页时,创建省略号 + if (this.totalPageNum > 8 && ((this.showLastPage && endPagNum < this.totalPageNum - 1) || (!this.showLastPage && restPageNum > 3))) { + pages.push(TiPaginationComponent.makePage('···', false)); + } + + // 最后一页隐藏:如果中间连续按钮的结束位置为总页数的前一页且当前页是后四页时,创建最后一页 + // 最后一页不隐藏:如果中间连续按钮的结束位置,不是最后一页按钮时,创建最后一页按钮 + if ( + (!this.showLastPage && endPagNum === this.totalPageNum - 1 && restPageNum <= 3) || + (this.showLastPage && endPagNum < this.totalPageNum) + ) { + pages.push(TiPaginationComponent.makePage(this.totalPageNum, this.currentPage === this.totalPageNum)); + } + + return pages; + } + + /** + * @description 根据当前页码和要显示的数目,计算分页链接的起始页 + */ + private getInterval(): Array { + let start: number; + let end: number; + // 1.根据Agile规范:页数小于等于8,起、始按钮值分别设为1和最大页数:1 - 总数 + if (this.totalPageNum <= 8) { + start = 1; + end = this.totalPageNum; + + return [start, end]; + } + + // 2.总页数大于8,当前项为前三页:1 - 5 / 1 - 6 + if (this.currentPage <= 3) { + start = 1; + end = 5; + if (!this.showLastPage) { + end = 6; + } + + return [start, end]; + } + + // 3.总页数大于8,当前项是后4页:总数-4 - 总数 + if (this.currentPage > this.totalPageNum - 3) { + start = this.totalPageNum - 4; + end = this.totalPageNum; + + return [start, end]; + } + + // 4.总页数大于8,当前项不在前三页,后三页:显示中间4个按钮 + start = this.currentPage - 1; + end = this.currentPage + 2; + + // 一般数据量比较大的场景隐藏最后一页时,显示中间5个按钮 + if (!this.showLastPage) { + end = this.currentPage + 3; + } + + return [start, end]; + } + + /** + * @description 获取加载态页码列表 + */ + private getLoadingPages(): Array<{ key: string; active: boolean }> { + const pages: Array<{ key: string; active: boolean }> = []; + // 总页数小于8 或 当前页小于等于4的情况 + // 1 2 3 4 5 6 7 ... + if (this.totalPageNum < 8 || this.currentPage <= 4) { + // 结尾页取 总页数 和 7 中间的最小值 + const endPage: number = Math.min(this.totalPageNum, 7); + for (let i: number = 1; i <= endPage; i++) { + // 循环生成中间页 + pages.push(TiPaginationComponent.makePage(i, i === this.currentPage)); + } + // 添加结尾...表示加载态 + pages.push(TiPaginationComponent.makePage('···', false)); + } else { + // 当前页大于4且总页数大于等于8的情况 + // 1 ... 5 6 7 8 9 ... + // 添加1和... + pages.push(TiPaginationComponent.makePage(1, false)); + pages.push(TiPaginationComponent.makePage('···', false)); + // 结尾页取 当前页+2 和 总页数 之间的最小值 + const endPage: number = Math.min(this.currentPage + 2, this.totalPageNum); + // 起始页取 结尾页-4 故共渲染5页 + const startPage: number = endPage - 4; + for (let i: number = startPage; i <= endPage; i++) { + // 循环生成中间页 + pages.push(TiPaginationComponent.makePage(i, i === this.currentPage)); + } + // 添加结尾...表示加载态 + pages.push(TiPaginationComponent.makePage('···', false)); + } + // 返回页码列表 + return pages; + } + + /** + * 设置合法的新页码 + */ + private verifyCurrentPage(pageSizeNum: number, triggerPageUpdate: boolean): void { + this.totalPageNum = this.calculateTotalPages(pageSizeNum); + if (this.currentPage > this.totalPageNum || this.currentPage < TiPaginationComponent.MIN_PAGE) { + this.currentPage = this.currentPage > this.totalPageNum ? this.totalPageNum : TiPaginationComponent.MIN_PAGE; + if (triggerPageUpdate) { + this.pageUpdate.emit({ + currentPage: this.currentPage, + size: pageSizeNum, + totalNumber: this.totalNumber + }); + // 此处异步为处理ExpressionChangedAfterItHasBeenCheckedError报错问题,onpush正常default报错; + Promise.resolve().then(() => { + this.currentPageChange.emit(this.currentPage); + }); + } else { + this.currentPageChange.emit(this.currentPage); + } + } + } + + /** + * @ignore + * 输入跳转页码enter快捷键处理 + */ + public enterPageHandler(e: KeyboardEvent): void { + this.gotoPageHandler(e); + } + + /** + * @ignore + * 根据要跳转的页码,同步修改当前页码 + */ + public gotoPageHandler(event: KeyboardEvent): void { + // 当输入值为不合法时处理 + if (this.gotoPage.page < TiPaginationComponent.MIN_PAGE || this.gotoPage.page > this.totalPageNum) { + this.gotoPage.page = this.currentPage; + + return; + } + + if (this.gotoPage.page !== this.currentPage) { + this.currentPage = this.gotoPage.page; + this.currentPageChange.emit(this.currentPage); + this.pageUpdate.emit({ + currentPage: this.currentPage, + size: this.itemsPerPage.selected, + totalNumber: this.totalNumber + }); + } + } + + /** + * @ignore + * 上一页按钮的点击事件 + */ + public onPrevClick(event: Event): any { + return !this.noPrevious() && this.selectPage(this.currentPage - 1, event); + } + + /** + * @ignore + * 页码点击事件 + */ + public onPageClick(page: any, event: Event): any { + return !this.isEllipse(page) && this.selectPage(page.key, event); + } + + /** + * @ignore + * 下一页按钮的点击事件 + */ + public onNextClick(event: Event): any { + return !this.noNext() && this.selectPage(this.currentPage + 1, event); + } + + /** + * @ignore + * input输入框modelchange事件 + */ + public ngModelChange(value: any): void { + if (value === '') { + this.gotoPage.page = ''; + + return; + } + + const newPage: number = parseInt(value, 10); + if (isNaN(newPage)) { + this.render.setProperty(this.inputRef.nativeElement, 'value', this.gotoPage.page); + + return; + } + + this.gotoPage.page = newPage; + this.render.setProperty(this.inputRef.nativeElement, 'value', this.gotoPage.page); + } + + /** + * @ignore + */ + public trackByFn(index: number, item: any): number { + return item.key; + } + + private isWithTable(): boolean { + return this.table && this.render.parentNode(this.nativeElement).tagName === 'TI-TABLE'; + } + + /** + * 设置分页吸底组件样式 + */ + private setFixedStyle(): void { + if (typeof this.table.nativeElement.getBoundingClientRect !== 'function') { + return; + } + + const left: number = this.table.nativeElement.getBoundingClientRect().left; + // 组件要与tp-layout-section左侧对齐,此处需要减去tp-layout-section左侧间距 24px + this.renderer.setStyle(this.nativeElement, 'left', `calc(${left}px - 24px)`); + this.renderer.setStyle(this.nativeElement, 'width', `calc(100% - ${left}px)`); + } +} diff --git a/src/pagination/lib/src/TiPaginationModule.ts b/src/pagination/lib/src/TiPaginationModule.ts new file mode 100644 index 0000000..d7bc36a --- /dev/null +++ b/src/pagination/lib/src/TiPaginationModule.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiPaginationComponent } from './TiPaginationComponent'; +import { FormsModule } from '@angular/forms'; +import { TiSelectModule } from '@opentiny/ng-select'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiLocaleModule } from '@opentiny/ng-locale'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiIconModule, TiSelectModule, TiTextModule, TiLocaleModule, TiOutlineModule], + exports: [TiPaginationComponent], + declarations: [TiPaginationComponent] +}) +export class TiPaginationModule { + constructor() { + TiLocale.setTiWords(locales); + } +} + +export * from './TiPaginationComponent'; +export { TiPaginationComponent, TiPageSizeConfig, TiPaginationEvent } from './TiPaginationComponent'; diff --git a/src/pagination/lib/src/i18n/TiPaginationWords.ts b/src/pagination/lib/src/i18n/TiPaginationWords.ts new file mode 100644 index 0000000..ecaee8f --- /dev/null +++ b/src/pagination/lib/src/i18n/TiPaginationWords.ts @@ -0,0 +1,8 @@ +export interface TiPaginationWords { + tiPagination: { + gotoLabel: string; + prevTitle: string; + nextTitle: string; + totalLabel: string; + }; +} diff --git a/src/pagination/lib/src/i18n/en_US.ts b/src/pagination/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..e286235 --- /dev/null +++ b/src/pagination/lib/src/i18n/en_US.ts @@ -0,0 +1,10 @@ +import { TiPaginationWords } from './TiPaginationWords'; + +export const en_US: TiPaginationWords = { + tiPagination: { + gotoLabel: 'Go', + prevTitle: 'Previous', + nextTitle: 'Next', + totalLabel: 'Total Records: ' + } +}; diff --git a/src/pagination/lib/src/i18n/es_US.ts b/src/pagination/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..635c529 --- /dev/null +++ b/src/pagination/lib/src/i18n/es_US.ts @@ -0,0 +1,10 @@ +import { TiPaginationWords } from './TiPaginationWords'; + +export const es_US: TiPaginationWords = { + tiPagination: { + gotoLabel: 'Ir', + prevTitle: 'Anterior', + nextTitle: 'Siguiente', + totalLabel: 'Cantidad total de registros: ' + } +}; diff --git a/src/pagination/lib/src/i18n/fr_FR.ts b/src/pagination/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..db599d1 --- /dev/null +++ b/src/pagination/lib/src/i18n/fr_FR.ts @@ -0,0 +1,10 @@ +import { TiPaginationWords } from './TiPaginationWords'; + +export const fr_FR: TiPaginationWords = { + tiPagination: { + gotoLabel: 'Aller', + prevTitle: 'Précédent', + nextTitle: 'Prochain', + totalLabel: 'Total des enregistrements: ' + } +}; diff --git a/src/pagination/lib/src/i18n/index.ts b/src/pagination/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/pagination/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/pagination/lib/src/i18n/pt_BR.ts b/src/pagination/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..85336e9 --- /dev/null +++ b/src/pagination/lib/src/i18n/pt_BR.ts @@ -0,0 +1,10 @@ +import { TiPaginationWords } from './TiPaginationWords'; + +export const pt_BR: TiPaginationWords = { + tiPagination: { + gotoLabel: 'Ir', + prevTitle: 'Anterior', + nextTitle: 'Próximo', + totalLabel: 'Total de registros: ' + } +}; diff --git a/src/pagination/lib/src/i18n/zh_CN.ts b/src/pagination/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..f85a5ff --- /dev/null +++ b/src/pagination/lib/src/i18n/zh_CN.ts @@ -0,0 +1,10 @@ +import { TiPaginationWords } from './TiPaginationWords'; + +export const zh_CN: TiPaginationWords = { + tiPagination: { + gotoLabel: '跳转', + prevTitle: '上一页', + nextTitle: '下一页', + totalLabel: '总条数:' + } +}; diff --git a/src/pagination/lib/src/pagination.html b/src/pagination/lib/src/pagination.html new file mode 100644 index 0000000..d74ec84 --- /dev/null +++ b/src/pagination/lib/src/pagination.html @@ -0,0 +1,134 @@ + + + + {{item}} + + + + + + +
    + {{ 'tiPagination.gotoLabel' | tiTranslate }} + + +
    +
    +
    +
    + +
    + + + + + + + + + + + + + +
    +
    diff --git a/src/pagination/lib/src/pagination.less b/src/pagination/lib/src/pagination.less new file mode 100644 index 0000000..18a60a8 --- /dev/null +++ b/src/pagination/lib/src/pagination.less @@ -0,0 +1,231 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-pagination-box-model: border-box; + --ti-pagination-height: var(--ti-common-size-6x); + --ti-pagination-prev-next-width: 22px; + --ti-pagination-goto-input-width: 45px; +} + +/* -----ti-table with ti-pagination----START----*/ +::ng-deep .ti3-table > .ti3-page-container { + margin-top: var(--ti-common-space-10); +} +/* -----ti-table with ti-pagination----END----*/ + +/* 将所有的类名包含在ti-page-container内部的原因是整体提升组件样式的权重 +防止组件样式被框架样式覆盖 */ +:host.ti3-page-container { + display: inline-block; + .ti3-pag-container { + .user-select(none); + display: inline-block; + line-height: normal; + float: left; + } + + .ti3-pag-total-items { + float: left; + margin: var(--ti-common-space-0) var(--ti-common-space-4x); + vertical-align: middle; + line-height: var(--ti-pagination-height); + height: var(--ti-pagination-height); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + color: var(--ti-common-color-text-secondary); + } + .ti3-pag-pages { + float: left; + height: var(--ti-pagination-height); + & > .ti3-pag-prev { + margin-right: var(--ti-common-space-base); + } + } + + .ti3-pag-page, + .ti3-pag-prev, + .ti3-pag-next { + display: inline-block; + cursor: pointer; + text-decoration: none; + height: var(--ti-pagination-height); + line-height: var(--ti-pagination-height); + vertical-align: middle; + text-align: center; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + .box-sizing(var(--ti-pagination-box-model)); + .border-radius(var(--ti-common-border-radius-normal)); + &:hover { + text-decoration: none; + } + } + + .ti3-pag-page { + color: var(--ti-common-color-text-secondary); + margin-right: var(--ti-common-space-base); + padding: var(--ti-common-space-0) var(--ti-common-space-6); + &.active { + background-color: var(--ti-common-color-bg-emphasize); + color: var(--ti-common-color-text-white); + } + &.ellipse { + outline: none; + border: none; + background: transparent; + cursor: default; + } + &:not(.ellipse):hover { + background-color: var(--ti-common-color-bg-light-normal); + color: var(--ti-common-color-icon-hover); + } + &.active, + &.simple { + &:hover { + background-color: var(--ti-common-color-bg-emphasize); + color: var(--ti-common-color-text-white); + } + } + &:not(.ellipse):active { + background-color: var(--ti-common-color-bg-emphasize); + color: var(--ti-common-color-text-white); + } + } + + .ti3-pag-pages-disable { + .ti3-page-disable { + color: var(--ti-common-color-text-disabled); + background-color: var(--ti-common-color-bg-white-normal); + cursor: not-allowed !important; + outline: none; + &:hover { + color: var(--ti-common-color-text-disabled) !important; + background-color: var(--ti-common-color-bg-white-normal); + } + } + } + + .ti3-pag-prev, + .ti3-pag-next { + width: var(--ti-pagination-prev-next-width); + color: var(--ti-common-color-icon-normal); + height: var(--ti-pagination-prev-next-width); + line-height: var(--ti-pagination-prev-next-width); + &.disabled { + cursor: not-allowed; + background-color: var(--ti-common-color-bg-white-normal); + color: var(--ti-common-color-text-disabled); + outline: none; + & > .ti3-icon-angle-left, + & > .ti3-icon-angle-right { + color: var(--ti-common-color-icon-disabled); + } + } + &:not(.disabled):hover { + color: var(--ti-common-color-icon-hover); + background-color: var(--ti-common-color-bg-light-normal); + } + &:not(.disabled):active { + color: var(--ti-common-color-text-white); + background-color: var(--ti-common-color-bg-emphasize); + } + } + + /* Set up special height for select that shows page size. */ + .ti3-page-size-option, + .ti3-page-mini-option { + float: left; + } + + .ti3-pag-mini-pages { + .ti3-pag-prev, + .ti3-pag-next { + float: left; + } + .ti3-page-mini-option { + float: left; + margin: var(--ti-common-space-0) var(--ti-common-space-2x); + } + .ti3-pag-total-items { + margin-left: var(--ti-common-space-0); + } + } + + /* style for disable condition */ + .ti3-pag-container-disable { + cursor: not-allowed; + } + .ti3-pag-total-disable { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + } + + /* For Page Goto */ + .ti3-pag-goto { + float: left; + display: inline-block; + height: var(--ti-pagination-height); + & .ti3-pag-goto-text { + float: left; + color: var(--ti-common-color-text-secondary); + display: inline-block; + height: var(--ti-pagination-height); + line-height: var(--ti-pagination-height); + margin: var(--ti-common-space-0) 15px; + vertical-align: middle; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + } + & .ti3-pag-goto-input { + float: left; + padding: var(--ti-common-space-0) 5px; + width: var(--ti-pagination-goto-input-width); + height: var(--ti-pagination-height); + } + & .ti3-pag-goto-icon-container { + display: inline-block; + margin-left: 15px; + height: var(--ti-pagination-height); + width: var(--ti-pagination-height); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + .border-radius(3px); + background-color: var(--ti-common-color-bg-white-normal); + cursor: pointer; + text-decoration: none; + vertical-align: middle; + .box-sizing(var(--ti-pagination-box-model)); + & .ti3-pag-goto-icon { + .triangle-up(var(--ti-common-size-3x), calc(var(--ti-common-size-3x) / 1.6), var(--ti-common-color-bg-emphasize)); + transform: translateY(-50%) rotate(90deg); + margin-left: calc((var(--ti-pagination-height) - (var(--ti-common-size-3x) / 1.6)) / 2 - 2px); + margin-top: 50%; + } + &:hover, + &:focus, + &:active { + border-color: var(--ti-common-color-line-active); + } + } + } + + .ti3-pag-goto-disable { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + & .ti3-pag-goto-icon { + border-bottom-color: var(--ti-common-color-icon-disabled) !important; + cursor: not-allowed; + } + } +} + +// 组件吸底展示,主要用于表格数据量过多时,方便切换页码 +:host.ti3-page-container-fixed { + position: fixed; + bottom: 0; + width: 100%; + box-shadow: var(--ti-common-shadow-1-up); + background: var(--ti-common-color-bg-white-normal); + box-sizing: border-box; + padding: var(--ti-common-space-5x) var(--ti-common-space-6x); // 左右间距需要和tp-layout-section间距一致 + z-index: 10; +} diff --git a/src/path/demo/project.json b/src/path/demo/project.json new file mode 100644 index 0000000..d5142ed --- /dev/null +++ b/src/path/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/path/demo", + "sourceRoot": "src/path/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/path", + "index": "src/path/demo/src/index.html", + "main": "src/path/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/path/demo/tsconfig.app.json", + "assets": ["src/path/demo/src/favicon.ico", "src/path/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "path-demo:build:production" + }, + "development": { + "browserTarget": "path-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js path" + } + ] + } + } + } +} diff --git a/src/path/demo/src/app/AppComponent.ts b/src/path/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/path/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/path/demo/src/app/AppModule.ts b/src/path/demo/src/app/AppModule.ts new file mode 100644 index 0000000..c3d6d6d --- /dev/null +++ b/src/path/demo/src/app/AppModule.ts @@ -0,0 +1,25 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { PathTestModule } from './path/PathTestModule'; + +@NgModule({ + imports: [ + PathTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + BrowserAnimationsModule, + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/path/demo/src/app/IndexComponent.ts b/src/path/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..ac83ee4 --- /dev/null +++ b/src/path/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { PathTestModule } from './path/PathTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = PathTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/path/demo/src/app/app.html b/src/path/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/path/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/path/demo/src/app/path/PathListComponent.ts b/src/path/demo/src/app/path/PathListComponent.ts new file mode 100644 index 0000000..3742a67 --- /dev/null +++ b/src/path/demo/src/app/path/PathListComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { TiPathListItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './path-list.html' +}) +export class PathListComponent { + items: Array = [ + { + label: 'type = "file"', + type: 'file' + }, + { + label: 'type = "document"', + type: 'document' + } + ]; + items1: Array = [ + { + label: 'clearable-1', + type: 'file' + }, + { + label: 'clearable-2', + type: 'file' + } + ]; + myLogs: Array = []; + clear(item: TiPathListItem): void { + this.myLogs = [...this.myLogs, `${JSON.stringify(item)}`]; + } +} diff --git a/src/path/demo/src/app/path/PathSelectComponent.ts b/src/path/demo/src/app/path/PathSelectComponent.ts new file mode 100644 index 0000000..929d272 --- /dev/null +++ b/src/path/demo/src/app/path/PathSelectComponent.ts @@ -0,0 +1,277 @@ +import { Component } from '@angular/core'; +import { TiModalService, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './path-select.html' +}) +export class PathSelectComponent { + constructor(tiModal: TiModalService) { + this.tiModal = tiModal; + } + private tiModal: TiModalService; + pathValue: string = 'd/code/TinyUI'; + TiPathListItems = []; + TiPathFieldItems = [ + { + label: 'd' + }, + { + label: 'code' + }, + { + label: 'TinyUI' + } + ]; + + // 表格相关数据 + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + checkedList: Array = []; // 默认选中项 + columns: Array = [ + { + title: 'Name', + width: '40%' + }, + { + title: 'Time', + width: '35%' + }, + { + title: 'Size', + width: '25%' + } + ]; + currentPage: number = 0; + pageSize: { options: Array; size: number } = { + options: [10, 20, 50], + size: 10 + }; + noDadaInfo: string = '暂无表格数据'; + + // 模拟路径数据(为演示方便,此处数据仅为示意) + private pathDataMock = [ + { + label: 'd', + type: 'file', + time: '2019-11-22', + size: '5', + children: [ + { + label: 'code', + type: 'file', + time: '2019-08-22', + size: '10', + children: [ + { + label: 'TinyUI', + type: 'file', + time: '2020-11-11', + size: '15', + children: [ + { + label: 'item_1', + type: 'file', + time: '2020-06-18', + size: '20', + children: [ + { + label: 'item_1', + type: 'document', + time: '2020-02-20', + size: '2' + }, + { + label: 'item_2', + type: 'document', + time: '2020-01-03', + size: '20' + }, + { + label: 'item_3', + type: 'document', + time: '2020-06-18', + size: '200' + }, + { + label: 'empty', + type: 'file', + time: '2019-08-23', + size: '0', + children: [] + } + ] + }, + { + label: 'item_2', + type: 'file', + time: '2020-02-20', + size: '2', + children: [ + { + label: 'item_1', + type: 'document', + time: '2020-02-20', + size: '2' + }, + { + label: 'item_2', + type: 'document', + time: '2020-01-03', + size: '20' + }, + { + label: 'empty', + type: 'file', + time: '2019-08-23', + size: '0', + children: [] + } + ] + }, + { + label: 'item_3', + type: 'document', + time: '2020-03-15', + size: '16' + }, + { + label: 'item_4', + type: 'document', + time: '2020-05-01', + size: '28' + }, + { + label: 'item_5', + type: 'document', + time: '2020-11-11', + size: '35' + }, + { + label: 'empty', + type: 'file', + time: '2019-08-23', + size: '0', + children: [] + } + ] + } + ] + }, + { + label: 'empty', + type: 'file', + time: '2019-08-23', + size: '0', + children: [] + } + ] + } + ]; + + ngOnInit(): void { + // 模拟获取当前页面数据 + this.data = this.queryPageItems(); + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了分页特性,且对源数据未进行分页处理,因此tiny会对数据进行分页处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + // 表格选中项 + this.checkedList = [this.data[0], this.data[2]]; + // 路径列表组件同步表格选中项 + this.setPathList(); + } + + // 模拟获取当前页面数据 + private queryPageItems() { + let temp: any = [...this.pathDataMock]; + for (let i = 0; i < this.TiPathFieldItems.length; i++) { + temp = temp.find((item) => { + return item.label === this.TiPathFieldItems[i].label; + })?.children; + } + return temp; + } + + // pathList组件项目清除事件 + onPathListClear(item) { + // 表格选中项同步路径列表组件 + this.checkedList = this.checkedList.filter((_item) => { + return _item.label !== item.label; + }); + } + + // pathField组件路径变化事件 + onPathfieldChange() { + if (this.TiPathFieldItems.length === 0) { + this.srcData.data = this.pathDataMock; + } else { + this.srcData.data = this.queryPageItems(); + } + // 清空表格选中项 + this.checkedList = []; + } + + // 表格项目双击事件 + onTrDblclick(row) { + if (row.type === 'file') { + this.TiPathFieldItems = [...this.TiPathFieldItems, row]; + this.srcData.data = this.queryPageItems(); + this.checkedList = []; + } + } + + // 路径列表组件同步表格选中项 + private setPathList() { + this.TiPathListItems = [...this.checkedList]; + } + + // 改变路径内容 + private setPathValue(): void { + const pathArray: Array = []; + this.TiPathFieldItems.forEach((item, index) => { + pathArray.push(item.label); + }); + this.pathValue = pathArray.join('/'); + } + + // 打开弹窗 + showModal(content: any): void { + // 备份路径及已选项 + const TiPathFieldItemsOld = [...this.TiPathFieldItems]; + const checkedListOld = [...this.checkedList]; + this.tiModal.open(content, { + id: 'myModal', // 定义id防止同一页面出现多个相同弹框 + animation: false, + // 模板上下文:一般通过context定义的是与弹出动作相关的数据,大部分数据还是建议在外部定义 + // 双向绑定的值,建议放在context对象中,每次打开弹窗都重新就行赋值。 + context: { + name: 'select' + }, + close: (): void => { + console.log('on close event'); + this.TiPathListItems = []; + // 改变路径内容 + this.setPathValue(); + // 路径列表组件同步表格选中项 + this.setPathList(); + }, + dismiss: (): void => { + console.log('on dismiss event'); + // 还原所有路径和已选项 + this.TiPathFieldItems = [...TiPathFieldItemsOld]; + this.onPathfieldChange(); + this.checkedList = [...checkedListOld]; + } + }); + } +} diff --git a/src/path/demo/src/app/path/PathTestModule.ts b/src/path/demo/src/app/path/PathTestModule.ts new file mode 100644 index 0000000..8d8df6f --- /dev/null +++ b/src/path/demo/src/app/path/PathTestModule.ts @@ -0,0 +1,56 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; + +import { + TiButtonModule, + TiCheckboxModule, + TiIconModule, + TiModalModule, + TiPaginationModule, + TiPathModule, + TiTableModule +} from '@opentiny/ng'; +import { PathfieldItemsComponent } from './PathfieldItemsComponent'; +import { PathfieldIspanelComponent } from './PathfieldIspanelComponent'; +import { PathfieldPanelwidthComponent } from './PathfieldPanelwidthComponent'; +import { PathfieldEditableComponent } from './PathfieldEditableComponent'; +import { PathfieldEventComponent } from './PathfieldEventComponent'; +import { PathListComponent } from './PathListComponent'; +import { PathSelectComponent } from './PathSelectComponent'; + +@NgModule({ + imports: [ + CommonModule, + TiButtonModule, + TiModalModule, + TiTableModule, + TiCheckboxModule, + TiPaginationModule, + TiIconModule, + TiPathModule, + DemoLogModule, + RouterModule.forChild(PathTestModule.ROUTES) + ], + declarations: [ + PathfieldItemsComponent, + PathfieldIspanelComponent, + PathfieldPanelwidthComponent, + PathfieldEditableComponent, + PathfieldEventComponent, + PathListComponent, + PathSelectComponent + ] +}) +export class PathTestModule { + static readonly ROUTES: Routes = [ + { path: 'path/pathfield-items', component: PathfieldItemsComponent }, + { path: 'path/pathfield-ispanel', component: PathfieldIspanelComponent }, + { path: 'path/pathfield-panelwidth', component: PathfieldPanelwidthComponent }, + { path: 'path/pathfield-editable', component: PathfieldEditableComponent }, + { path: 'path/pathfield-pathchange', component: PathfieldEventComponent }, + { path: 'path/path-list', component: PathListComponent }, + { path: 'path/path-select', component: PathSelectComponent } + ]; +} diff --git a/src/path/demo/src/app/path/PathfieldEditableComponent.ts b/src/path/demo/src/app/path/PathfieldEditableComponent.ts new file mode 100644 index 0000000..c7add5c --- /dev/null +++ b/src/path/demo/src/app/path/PathfieldEditableComponent.ts @@ -0,0 +1,44 @@ +import { Component } from '@angular/core'; +import { TiPathFieldItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './pathfield-editable.html' +}) +export class PathfieldEditableComponent { + editable: boolean = false; + keepEditState: boolean = true; + items: Array = [ + { + label: 'window' + }, + { + label: 'document' + }, + { + label: 'body' + }, + { + label: 'container' + }, + { + label: 'content' + } + ]; + items1: Array = [ + { + label: 'window' + }, + { + label: 'document' + }, + { + label: 'body' + }, + { + label: 'container' + }, + { + label: 'content' + } + ]; +} diff --git a/src/path/demo/src/app/path/PathfieldEventComponent.ts b/src/path/demo/src/app/path/PathfieldEventComponent.ts new file mode 100644 index 0000000..9549587 --- /dev/null +++ b/src/path/demo/src/app/path/PathfieldEventComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiPathChangeEvent, TiPathFieldItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './pathfield-event.html' +}) +export class PathfieldEventComponent { + items: Array = [ + { + label: 'window' + }, + { + label: 'document' + }, + { + label: 'body' + }, + { + label: 'container' + }, + { + label: 'content' + } + ]; + myLogs: Array = []; + pathChange(event: TiPathChangeEvent | Event): void { + this.myLogs = [...this.myLogs, `pathchage => ${JSON.stringify(event)}`]; + } +} diff --git a/src/path/demo/src/app/path/PathfieldIspanelComponent.ts b/src/path/demo/src/app/path/PathfieldIspanelComponent.ts new file mode 100644 index 0000000..2aa79c5 --- /dev/null +++ b/src/path/demo/src/app/path/PathfieldIspanelComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiPathFieldItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './pathfield-ispanel.html' +}) +export class PathfieldIspanelComponent { + isPanel: boolean = false; + items: Array = [ + { + label: 'window' + }, + { + label: 'document' + }, + { + label: 'body' + }, + { + label: 'container' + }, + { + label: 'content' + } + ]; +} diff --git a/src/path/demo/src/app/path/PathfieldItemsComponent.ts b/src/path/demo/src/app/path/PathfieldItemsComponent.ts new file mode 100644 index 0000000..5baa972 --- /dev/null +++ b/src/path/demo/src/app/path/PathfieldItemsComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { TiPathFieldItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './pathfield-items.html' +}) +export class PathfieldItemsComponent { + items: Array = [ + { + label: 'window' + }, + { + label: 'document' + }, + { + label: 'body' + }, + { + label: 'container' + }, + { + label: 'content' + } + ]; +} diff --git a/src/path/demo/src/app/path/PathfieldPanelwidthComponent.ts b/src/path/demo/src/app/path/PathfieldPanelwidthComponent.ts new file mode 100644 index 0000000..aca4f4a --- /dev/null +++ b/src/path/demo/src/app/path/PathfieldPanelwidthComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiPathFieldItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './pathfield-panelwidth.html' +}) +export class PathfieldPanelwidthComponent { + panelWidth: string = '100px'; + items: Array = [ + { + label: 'window' + }, + { + label: 'document' + }, + { + label: 'body' + }, + { + label: 'container' + }, + { + label: 'content' + } + ]; +} diff --git a/src/path/demo/src/app/path/path-list.html b/src/path/demo/src/app/path/path-list.html new file mode 100644 index 0000000..a220a27 --- /dev/null +++ b/src/path/demo/src/app/path/path-list.html @@ -0,0 +1,9 @@ +

    1. default

    +
    + +
    +

    2. clearable

    +
    + +
    + diff --git a/src/path/demo/src/app/path/path-select.html b/src/path/demo/src/app/path/path-select.html new file mode 100644 index 0000000..98b55fb --- /dev/null +++ b/src/path/demo/src/app/path/path-select.html @@ -0,0 +1,111 @@ +

    描述

    +

    路径选择模板页面示例展示:

    +

    该模板页面为服务展示 pathField 和 pathList 组件如何结合使用(该用例仅做简单参考,实际场景需要各服务根据各自业务进行处理)。

    +

    导入

    +

    import {{ '{' }} TpPathModule {{ '}' }} from '@cloud/path';

    +

    import {{ '{' }} TiButtonModule, TiModalModule, TiTableModule, TiCheckgroupModule, TiPaginationModule {{ '}' }} from '@cloud/tiny3';

    +

    import {{ '{' }} TpIconModule {{ '}' }} from '@cloud/tinyplus3';

    +

    示例

    + +
    +
    {{pathValue}}
    + +
    + + + + + + + {{context.name}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + {{column.title}}
    + + + + {{row.label}} + {{row.time}}{{row.size}}
    {{noDadaInfo}}
    + + + +
    +
    + + + + + +
    + + diff --git a/src/path/demo/src/app/path/pathfield-editable.html b/src/path/demo/src/app/path/pathfield-editable.html new file mode 100644 index 0000000..cea9614 --- /dev/null +++ b/src/path/demo/src/app/path/pathfield-editable.html @@ -0,0 +1,9 @@ +

    1.editable = false

    +
    + +
    + +

    2.keepEditState = true

    +
    + +
    diff --git a/src/path/demo/src/app/path/pathfield-event.html b/src/path/demo/src/app/path/pathfield-event.html new file mode 100644 index 0000000..6cdd62b --- /dev/null +++ b/src/path/demo/src/app/path/pathfield-event.html @@ -0,0 +1,4 @@ +
    + +
    + diff --git a/src/path/demo/src/app/path/pathfield-ispanel.html b/src/path/demo/src/app/path/pathfield-ispanel.html new file mode 100644 index 0000000..be4d4f4 --- /dev/null +++ b/src/path/demo/src/app/path/pathfield-ispanel.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/path/demo/src/app/path/pathfield-items.html b/src/path/demo/src/app/path/pathfield-items.html new file mode 100644 index 0000000..fa57392 --- /dev/null +++ b/src/path/demo/src/app/path/pathfield-items.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/path/demo/src/app/path/pathfield-panelwidth.html b/src/path/demo/src/app/path/pathfield-panelwidth.html new file mode 100644 index 0000000..c5f8b7b --- /dev/null +++ b/src/path/demo/src/app/path/pathfield-panelwidth.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/path/demo/src/app/path/webdoc/path-demos.js b/src/path/demo/src/app/path/webdoc/path-demos.js new file mode 100644 index 0000000..679afb2 --- /dev/null +++ b/src/path/demo/src/app/path/webdoc/path-demos.js @@ -0,0 +1,85 @@ +export default { + column: '2', + demos: [ + { + demoId: 'pathfield-items', + name: { + 'zh-CN': '基本用法', + 'en-US': 'pathfield items' + }, + desc: { + 'zh-CN': '

    Path 组件的最简用法;通过属性items配置具体路径。

    ', + 'en-US': 'pathfield items' + }, + apis: ['TiPathFieldComponent.properties.items', 'TiPathFieldItem.properties.label', 'TiPathFieldItem.properties.label'] + }, + { + demoId: 'pathfield-ispanel', + name: { + 'zh-CN': '下拉面板', + 'en-US': 'pathfield ispanel' + }, + desc: { + 'zh-CN': '

    通过属性isPanel配置点击上级按钮是否展开下拉面板。

    ', + 'en-US': 'pathfield ispanel' + }, + apis: ['TiPathFieldComponent.properties.isPanel'] + }, + { + demoId: 'pathfield-panelwidth', + name: { + 'zh-CN': '面板宽度', + 'en-US': 'pathfield panelwidth' + }, + desc: { + 'zh-CN': '

    通过属性panelWidth配置下拉面板宽度。

    ', + 'en-US': 'pathfield panelwidth' + }, + apis: ['TiPathFieldComponent.properties.panelWidth'] + }, + { + demoId: 'pathfield-editable', + name: { + 'zh-CN': '可编辑', + 'en-US': 'pathfield editable' + }, + desc: { + 'zh-CN': + '

    通过属性editable配置是否支持路径编辑功能;通过属性keepEditState配置按下回车键是否保持编辑状态。

    ', + 'en-US': 'pathfield editable' + }, + apis: ['TiPathFieldComponent.properties.editable', 'TiPathFieldComponent.properties.keepEditState'] + }, + { + demoId: 'pathfield-event', + name: { + 'zh-CN': '事件', + 'en-US': 'pathfield event' + }, + desc: { + 'zh-CN': '

    当激活路径发生变化的时候触发pathChange事件。

    ', + 'en-US': 'pathfield event' + }, + apis: ['TiPathFieldComponent.events.pathChange', 'TiPathChangeEvent.properties.type'] + }, + { + demoId: 'path-list', + name: { + 'zh-CN': '路径列表', + 'en-US': 'path list' + }, + desc: { + 'zh-CN': + '

    PathList 组件的使用方法;通过属性items配置路径列表,通过属性clearable配置路径项是否可删除;当路径项删除的时候触发clear事件。

    ', + 'en-US': 'path list' + }, + apis: [ + 'TiPathListComponent.properties.items', + 'TiPathListComponent.properties.clearable', + 'TiPathListComponent.events.clear', + 'TiPathListItem.properties.label', + 'TiPathListItem.properties.type' + ] + } + ] +}; diff --git a/src/path/demo/src/app/path/webdoc/path.cn.md b/src/path/demo/src/app/path/webdoc/path.cn.md new file mode 100644 index 0000000..e703891 --- /dev/null +++ b/src/path/demo/src/app/path/webdoc/path.cn.md @@ -0,0 +1,15 @@ +--- +title: Path 路径 +--- + +# Path 路径 + +
    + +Path 是设置和显示路径的组件。 + +```typescript +import { TiPathModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/path/demo/src/app/path/webdoc/path.en.md b/src/path/demo/src/app/path/webdoc/path.en.md new file mode 100644 index 0000000..4626ccb --- /dev/null +++ b/src/path/demo/src/app/path/webdoc/path.en.md @@ -0,0 +1,13 @@ +--- +title: Path +--- + +# Path + +
    + +```typescript +import { TiPathModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/path/demo/src/favicon.ico b/src/path/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/path/demo/src/index.html b/src/path/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/path/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/path/demo/src/main.ts b/src/path/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/path/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/path/demo/tsconfig.app.json b/src/path/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/path/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/path/lib/index.ts b/src/path/lib/index.ts new file mode 100644 index 0000000..6c85341 --- /dev/null +++ b/src/path/lib/index.ts @@ -0,0 +1,3 @@ +export * from './src/TiPathModule'; +export * from './src/TiPathFieldComponent'; +export * from './src/TiPathListComponent'; diff --git a/src/path/lib/ng-package.json b/src/path/lib/ng-package.json new file mode 100644 index 0000000..86ac57b --- /dev/null +++ b/src/path/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/path", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/path/lib/package.json b/src/path/lib/package.json new file mode 100644 index 0000000..96b7bde --- /dev/null +++ b/src/path/lib/package.json @@ -0,0 +1,16 @@ +{ + "name": "@opentiny/ng-path", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-droplist": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/path/lib/project.json b/src/path/lib/project.json new file mode 100644 index 0000000..2b5569b --- /dev/null +++ b/src/path/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/path/lib", + "sourceRoot": "src/path/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/path"], + "options": { + "project": "src/path/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/path"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js path" + }, + { + "command": "ng default-build path" + }, + { + "command": "node build/clear-default-theme.js path" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/path && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build path && ng pack path && node build/publish.js path --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/path/lib/src/TiPathFieldComponent.ts b/src/path/lib/src/TiPathFieldComponent.ts new file mode 100644 index 0000000..828fa2c --- /dev/null +++ b/src/path/lib/src/TiPathFieldComponent.ts @@ -0,0 +1,393 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ElementRef, + EventEmitter, + Input, + NgZone, + Output, + Renderer2, + ViewChild, + Inject, + OnChanges, + AfterViewInit, + SimpleChanges, + ChangeDetectionStrategy +} from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiDroplistComponent } from '@opentiny/ng-droplist'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiKeymap } from '@opentiny/ng-utils'; + +/** + * pathField组件路径中单项的数据格式 + */ +export interface TiPathFieldItem { + /** + * 路径内容 + */ + label: string; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} + +/** + * pathField组件路径变化时change事件的参数 + */ +export interface TiPathChangeEvent { + type: 'dropdownSelect' | 'goUpperSelect'; +} + +/** + * pathField组件用于显示路径操作 + */ +@Component({ + selector: 'ti-path-field', + templateUrl: 'path-field.html', + styleUrls: ['path-field.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiPathFieldComponent extends TiBaseComponent implements OnChanges, AfterViewInit { + /** + * 路径数据项 + */ + @Input() items: Array; + /** + * 点击返回按钮时是否展开下拉面板 + */ + @Input() isPanel: boolean = true; + /** + * 是否支持编辑路径 + */ + @Input() editable: boolean = true; + /** + * 编辑状态时敲击回车是否保持编辑状态 + */ + @Input() keepEditState: boolean = false; + /** + * 下拉面板的宽度 + */ + @Input() panelWidth: 'auto' | string = 'auto'; + /** + * 路径内容变化时触发的回调 + */ + @Output() readonly pathChange: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + @ViewChild('items', { static: true }) itemsRef: ElementRef; + /** + * @ignore + */ + @ViewChild('backBtn', { static: true }) backBtnRef: ElementRef; + /** + * @ignore + */ + @ViewChild('droplist', { static: true }) dropListComp: TiDroplistComponent; + /** + * @ignore + */ + @ViewChild('input', { static: false }) inputRef: ElementRef; + + /** + * @ignore + * 显示在路径框中的项目 + */ + public showItems: Array = []; + /** + * @ignore + * 隐藏在下拉菜单中的项目 + */ + public menuItems: Array = []; + /** + * @ignore + * 下拉菜单选中项 + */ + public menuSelected: TiPathFieldItem | null = null; + /** + * @ignore + * 路径框内容是否溢出 + */ + public isOverflow: boolean = false; + /** + * @ignore + * 是否处于编辑路径模式 + */ + public isEditState: boolean = false; + /** + * @ignore + * 编辑模式下路径内容 + */ + public pathValue: string = ''; + /** + * 编辑模式下路径内容 + */ + private pathValueOld: string = ''; + // 页面点击事件 + private onDocumentClick: () => void; + + constructor( + private elementRef: ElementRef, + private renderer2: Renderer2, + private zone: NgZone, + private tiRenderer: TiRenderer, + @Inject(DOCUMENT) private document + ) { + super(elementRef, renderer2); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + // items动态变化时,重新设置显示项 + if (changes['items']) { + this.setShowItems(); + } + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + // 设置显示的项目 + this.setShowItems(); + this.zone.runOutsideAngular(() => { + // 点击页面空白处下拉列表关闭 + this.onDocumentClick = this.renderer2.listen(this.document, 'click', () => { + this.dropListComp.hide(); + }); + }); + } + + /** + * @ignore + * 点击路径框 + */ + public onItemsClick(): void { + // 若不可编辑,直接返回 + if (!this.editable) { + return; + } + // 进入编辑模式 + this.isEditState = true; + setTimeout(() => { + this.inputRef.nativeElement.focus(); + }, 0); + } + + /** + * @ignore + * 点击某项 + */ + public onItemClick(item: TiPathFieldItem, event: Event): void { + const index: number = this.items.indexOf(item); + // 若点击的为最后一项时,不做处理 + if (index === this.items.length - 1) { + return; + } + this.items.splice(index + 1); + // 设置显示的项目 + this.setShowItems(); + this.dropListComp.hide(); + // 触发pathChange事件 + this.pathChange.emit(event); + event.stopPropagation(); + } + + /** + * @ignore + * 点击返回按钮 + */ + public onBackBtnClick(event: Event): void { + if (this.isPanel) { + // 展开面板选择路径模式 + this.dropListComp.show(); + } else { + // 直接返回上级模式 + if (this.items.length) { + this.items.pop(); + this.setShowItems(); + const pathChangeEvent: TiPathChangeEvent = { + type: 'goUpperSelect' + }; + // 触发pathChange事件 + this.pathChange.emit(pathChangeEvent); + } + } + event.stopPropagation(); + } + + /** + * @ignore + * 改变下拉列表选中项 + */ + public onDroplistChange(selected: TiPathFieldItem): void { + const index: number = this.menuItems.findIndex((item: TiPathFieldItem) => { + return item === selected; + }); + // 由于menuItems中项目顺序是倒序,故删除items项目时须用长度减除 + this.items.splice(this.menuItems.length - index); + this.setShowItems(); + const pathChangeEvent: TiPathChangeEvent = { + type: 'dropdownSelect' + }; + // 触发pathChange事件 + this.pathChange.emit(pathChangeEvent); + } + + /** + * @ignore + * 编辑模式下input框按键按下 + */ + public onEditorKeydown(event: KeyboardEvent): void { + if (event.keyCode === TiKeymap.KEY_ENTER || event.keyCode === TiKeymap.KEY_NUMPAD_ENTER) { + // 当路径内容发生变化时 + if (this.pathValue !== this.pathValueOld) { + // 设置路径项目 + this.setItems(); + // 触发pathChange事件 + this.pathChange.emit(event); + } + // 路径编辑模式下敲击回车不保持编辑模式 + if (!this.keepEditState) { + // 退出编辑模式 + this.isEditState = false; + } + } + } + + /** + * @ignore + * 编辑模式下input框失焦 + */ + public onEditorBlur(): void { + // 恢复之前备份的pathValueOld路径内容 + this.pathValue = this.pathValueOld; + // 退出编辑模式 + this.isEditState = false; + } + + /** + * 设置路径项目 + */ + private setItems(): void { + // 通过当前pathValue路径值重新设置路径项目 + const array: Array = this.pathValue.split('/'); + // 重置items路径项目 + this.items.splice(0); + array.forEach((value) => { + const item: TiPathFieldItem = { + label: value + }; + this.items.push(item); + }); + // 设置显示的项目 + this.setShowItems(); + } + + /** + * 设置显示的项目 + */ + private setShowItems(): void { + // 生成路径条父容器的临时DOM,将其放入body中 + const eleStyles: CSSStyleDeclaration = getComputedStyle(this.itemsRef.nativeElement); + const itemsNode: HTMLDivElement = document.createElement('div'); + this.renderer.appendChild(document.body, itemsNode); + // 为临时DOM添加必要的宽度相关的样式,便于宽度计算 + this.tiRenderer.setStyles(itemsNode, { + display: 'inline-flex', + padding: eleStyles.padding, + paddingLeft: eleStyles.paddingLeft, // 处理在IE和火狐下获取padding为空问题:在火狐和IE下只能用只能用padding+[方位]的方式来获取元素的padding值 + paddingRight: eleStyles.paddingRight, + visibility: 'hidden', // 元素隐藏但做占位处理 + position: 'absolute', // 避免克隆元素影响页面高度,导致出滚动条 + left: '-9999px', + top: '-9999px' + }); + + // 获取当前路径条父容器的最大宽度 + const maxWidth: number = this.itemsRef.nativeElement.getBoundingClientRect().width; + // 重置是否溢出标志 + this.isOverflow = false; + // 重置路径框数组和下拉数组 + this.showItems = []; + this.menuItems = []; + for (let i: number = this.items.length - 1; i >= 0; i--) { + // 已经超出最大显示宽度了,直接将当前item放入menuItems中 + if (this.isOverflow) { + this.pushMenuItems(i); + continue; + } + + const itemNode: HTMLDivElement = document.createElement('div'); + this.tiRenderer.setStyles(itemNode, { display: 'flex' }); + this.tiRenderer.prepend(itemsNode, itemNode); + + const itemLabelNode: HTMLDivElement = document.createElement('div'); + itemLabelNode.innerText = this.items[i].label; + + const itemDividNode: HTMLDivElement = document.createElement('div'); + itemDividNode.innerText = '/'; + this.tiRenderer.setStyles(itemDividNode, { margin: '0 4px' }); + + this.renderer.appendChild(itemNode, itemLabelNode); + if (i !== this.items.length - 1) { + this.tiRenderer.insertAfter(itemDividNode, itemLabelNode); + this.tiRenderer.setStyles(itemLabelNode, { maxWidth: '160px' }); + } + + // 放入当前item的DOM后,没有超出最大显示宽度时 + if (itemsNode.getBoundingClientRect().width <= maxWidth) { + // 将当前item放入showItems中 + this.showItems.unshift(this.items[i]); + } else { + // 超出最大显示宽度时 + // 若当前为最后一个item时,直接将其放入showItems中,否则将其放入menuItems中 + if (i === this.items.length - 1) { + this.showItems.unshift(this.items[i]); + } else { + this.pushMenuItems(i); + } + + // 将是否溢出标志置为true + this.isOverflow = true; + } + } + // 移除临时DOM + itemsNode.remove(); + // 设置编辑模式下的路径内容 + this.setPathValue(); + } + + /** + * 向menuItems数组中push项目 + */ + private pushMenuItems(index: number) { + this.menuItems.push({ + label: this.items[index].label + }); + } + + /** + * 设置编辑模式下的路径内容 + */ + private setPathValue(): void { + const pathArray: Array = []; + this.items.forEach((item, index) => { + pathArray.push(item.label); + }); + this.pathValue = pathArray.join('/'); + // 备份当前pathValue路径内容 + this.pathValueOld = this.pathValue; + } +} diff --git a/src/path/lib/src/TiPathListComponent.ts b/src/path/lib/src/TiPathListComponent.ts new file mode 100644 index 0000000..d9a1a99 --- /dev/null +++ b/src/path/lib/src/TiPathListComponent.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; + +export interface TiPathListItem { + /** + * 路径项目内容 + */ + label: string; + /** + * 路径项目类型 + */ + type: 'document' | 'file'; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} + +/** + * pathList组件用于显示路径列表 + */ +@Component({ + selector: 'ti-path-list', + templateUrl: 'path-list.html', + styleUrls: ['path-list.less'] +}) +export class TiPathListComponent extends TiBaseComponent { + /** + * 路径列表数据项 + */ + @Input() items: Array; + /** + * 路径项目是否可删除 + */ + @Input() clearable: boolean = false; + /** + * 当路径项目删除时触发的回调 + */ + @Output() readonly clear: EventEmitter = new EventEmitter(); + + /** + * @ignore + * 清除列表项 + */ + public clearItem(item: TiPathListItem): void { + const index = this.items.indexOf(item); + this.items.splice(index, 1); + // 触发路径项目删除 clear 事件 + this.clear.emit(item); + } +} diff --git a/src/path/lib/src/TiPathModule.ts b/src/path/lib/src/TiPathModule.ts new file mode 100644 index 0000000..4894cfc --- /dev/null +++ b/src/path/lib/src/TiPathModule.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiDroplistModule } from '@opentiny/ng-droplist'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiIconModule } from '@opentiny/ng-icon'; + +import { TiPathFieldComponent } from './TiPathFieldComponent'; +import { TiPathListComponent } from './TiPathListComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiDroplistModule, TiIconModule, TiOverflowModule, TiTextModule], + exports: [TiPathFieldComponent, TiPathListComponent], + declarations: [TiPathListComponent, TiPathFieldComponent] +}) +export class TiPathModule {} + +export { TiPathFieldComponent } from './TiPathFieldComponent'; +export { TiPathListComponent } from './TiPathListComponent'; diff --git a/src/path/lib/src/path-field.html b/src/path/lib/src/path-field.html new file mode 100644 index 0000000..0c94832 --- /dev/null +++ b/src/path/lib/src/path-field.html @@ -0,0 +1,29 @@ +
    +
    + +
    +
    +
    +
    {{item.label}}
    +
    /
    +
    +
    + + +
    + +
    +
    diff --git a/src/path/lib/src/path-field.less b/src/path/lib/src/path-field.less new file mode 100644 index 0000000..98a8fbc --- /dev/null +++ b/src/path/lib/src/path-field.less @@ -0,0 +1,78 @@ +::ng-deep :root { + --ti-pathField-height: 26px; + --ti-pathField-back-width: var(--ti-pathField-height); +} + +:host { + display: block; +} + +.ti3-pathField-container { + display: flex; + height: var(--ti-pathField-height); + line-height: var(--ti-pathField-height); + border: 1px solid var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + align-items: center; + position: relative; +} + +.ti3-pathField-backIcon { + display: block; + width: var(--ti-pathField-back-width); + text-align: center; + font-size: 14px; + cursor: pointer; + border-right: 1px solid var(--ti-common-color-line-normal); + box-sizing: border-box; + &:hover { + color: var(--ti-common-color-bg-emphasize); + } +} +.ti3-pathField-items { + display: flex; + flex: 1; + height: var(--ti-pathField-height); + padding: 0 var(--ti-common-space-10); + box-sizing: border-box; + user-select: none; + overflow: hidden; + .ti3-pathField-item { + &:not(:last-child) { + display: flex; + flex-shrink: 0; + align-items: center; + color: var(--ti-common-color-text-weaken); + cursor: pointer; + &:hover .ti3-pathField-item-label { + color: var(--ti-common-color-text-highlight); + } + } + &:last-child { + width: 100%; + font-weight: bold; + .ti3-pathField-item-label { + max-width: none; + } + .ti3-pathField-item-divid { + display: none; + } + } + } +} + +.ti3-pathField-item-label { + max-width: 160px; +} +.ti3-pathField-item-divid { + margin: 0 var(--ti-common-space-base); +} + +.ti3-pathField-Editor { + width: calc(100% + 2px); + position: absolute; + left: -1px; + input { + width: 100%; + } +} diff --git a/src/path/lib/src/path-list.html b/src/path/lib/src/path-list.html new file mode 100644 index 0000000..ff39b6a --- /dev/null +++ b/src/path/lib/src/path-list.html @@ -0,0 +1,8 @@ +
    + {{i + 1}}. + + {{item.label}} +
    + +
    +
    diff --git a/src/path/lib/src/path-list.less b/src/path/lib/src/path-list.less new file mode 100644 index 0000000..a69651c --- /dev/null +++ b/src/path/lib/src/path-list.less @@ -0,0 +1,42 @@ +.ti3-pathList-item { + height: 28px; + line-height: 18px; + padding: 5px 36px 5px var(--ti-common-space-5x); + margin-bottom: var(--ti-common-space-base); + background-color: var(--ti-common-color-bg-white-emphasize); + box-sizing: border-box; + position: relative; + display: flex; + align-items: center; + &:hover { + .ti3-pathList-close { + display: flex; + } + } +} + +.ti3-pathList-index { + width: 16px; + margin-right: var(--ti-common-space-3x); + flex-shrink: 0; +} + +.ti3-pathList-icon { + display: inline-flex; + margin-right: var(--ti-common-space-3x); + font-size: var(--ti-common-font-size-2); +} + +.ti3-pathList-close { + display: none; + width: 30px; + height: 28px; + position: absolute; + right: 0; + justify-content: center; + align-items: center; + cursor: pointer; + &:hover .ti3-pathList-closeIcon { + color: var(--ti-common-color-bg-emphasize); + } +} diff --git a/src/phonenumber/demo/project.json b/src/phonenumber/demo/project.json new file mode 100644 index 0000000..d1d0290 --- /dev/null +++ b/src/phonenumber/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/phonenumber/demo", + "sourceRoot": "src/phonenumber/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/phonenumber", + "index": "src/phonenumber/demo/src/index.html", + "main": "src/phonenumber/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/phonenumber/demo/tsconfig.app.json", + "assets": ["src/phonenumber/demo/src/favicon.ico", "src/phonenumber/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "phonenumber-demo:build:production" + }, + "development": { + "browserTarget": "phonenumber-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js phonenumber" + } + ] + } + } + } +} diff --git a/src/phonenumber/demo/src/app/AppComponent.ts b/src/phonenumber/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/phonenumber/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/phonenumber/demo/src/app/AppModule.ts b/src/phonenumber/demo/src/app/AppModule.ts new file mode 100644 index 0000000..dd33a22 --- /dev/null +++ b/src/phonenumber/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { PhonenumberTestModule } from './phonenumber/PhonenumberTestModule'; + +@NgModule({ + imports: [ + PhonenumberTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/phonenumber/demo/src/app/IndexComponent.ts b/src/phonenumber/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..6c84a9f --- /dev/null +++ b/src/phonenumber/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { PhonenumberTestModule } from './phonenumber/PhonenumberTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = PhonenumberTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/phonenumber/demo/src/app/app.html b/src/phonenumber/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/phonenumber/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/phonenumber/demo/src/app/phonenumber/PhonenumberBasicComponent.ts b/src/phonenumber/demo/src/app/phonenumber/PhonenumberBasicComponent.ts new file mode 100644 index 0000000..1a11810 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/PhonenumberBasicComponent.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { TiCountryCode } from '@opentiny/ng'; +@Component({ + templateUrl: './phonenumber-basic.html' +}) +export class PhonenumberBasicComponent { + value: string; + countryOptions: Array = [ + { + ISO2Code: 'MO', + Name: '中国澳门特别行政区', + CountryCode: '853' + }, + { + ISO2Code: 'CN', + Name: '中国大陆', + CountryCode: '86' + }, + { + ISO2Code: 'TW', + Name: '中国台湾', + CountryCode: '886' + }, + { + ISO2Code: 'HK', + Name: '中国香港特别行政区', + CountryCode: '852' + } + ]; +} diff --git a/src/phonenumber/demo/src/app/phonenumber/PhonenumberCountryComponent.ts b/src/phonenumber/demo/src/app/phonenumber/PhonenumberCountryComponent.ts new file mode 100644 index 0000000..4721022 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/PhonenumberCountryComponent.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { CountryCode } from 'libphonenumber-js/max'; +import { TiCountryCode } from '@opentiny/ng'; + +@Component({ + templateUrl: './phonenumber-country.html' +}) +export class PhonenumberCountryComponent { + value: string; + country: CountryCode = 'MO'; + countryOptions: Array = [ + { + ISO2Code: 'MO', + Name: '中国澳门特别行政区', + CountryCode: '853' + }, + { + ISO2Code: 'CN', + Name: '中国大陆', + CountryCode: '86' + }, + { + ISO2Code: 'TW', + Name: '中国台湾', + CountryCode: '886' + }, + { + ISO2Code: 'HK', + Name: '中国香港特别行政区', + CountryCode: '852' + } + ]; +} diff --git a/src/phonenumber/demo/src/app/phonenumber/PhonenumberDisabledComponent.ts b/src/phonenumber/demo/src/app/phonenumber/PhonenumberDisabledComponent.ts new file mode 100644 index 0000000..e68c50c --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/PhonenumberDisabledComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { TiCountryCode } from '@opentiny/ng'; + +@Component({ + templateUrl: './phonenumber-disabled.html' +}) +export class PhonenumberDisabledComponent { + value: string; + selectDisabled: boolean = true; + countryOptions: Array = [ + { + ISO2Code: 'MO', + Name: '中国澳门特别行政区', + CountryCode: '853' + }, + { + ISO2Code: 'CN', + Name: '中国大陆', + CountryCode: '86' + }, + { + ISO2Code: 'TW', + Name: '中国台湾', + CountryCode: '886' + }, + { + ISO2Code: 'HK', + Name: '中国香港特别行政区', + CountryCode: '852' + } + ]; +} diff --git a/src/phonenumber/demo/src/app/phonenumber/PhonenumberEventComponent.ts b/src/phonenumber/demo/src/app/phonenumber/PhonenumberEventComponent.ts new file mode 100644 index 0000000..a4ad2a0 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/PhonenumberEventComponent.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; +import { CountryCode } from 'libphonenumber-js/max'; +import { TiCountryCode } from '@opentiny/ng'; + +@Component({ + templateUrl: './phonenumber-event.html' +}) +export class PhonenumberEventComponent { + value: string; + countryOptions: Array = [ + { + ISO2Code: 'MO', + Name: '中国澳门特别行政区', + CountryCode: '853' + }, + { + ISO2Code: 'CN', + Name: '中国大陆', + CountryCode: '86' + }, + { + ISO2Code: 'TW', + Name: '中国台湾', + CountryCode: '886' + }, + { + ISO2Code: 'HK', + Name: '中国香港特别行政区', + CountryCode: '852' + } + ]; + myLogs: Array = []; + countrySelect(event: CountryCode): void { + this.myLogs = [...this.myLogs, `countrySelect: ${JSON.stringify(event)}`]; + } + countryChange(event: string): void { + this.myLogs = [...this.myLogs, `countryChange: ${JSON.stringify(event)}`]; + } +} diff --git a/src/phonenumber/demo/src/app/phonenumber/PhonenumberTestModule.ts b/src/phonenumber/demo/src/app/phonenumber/PhonenumberTestModule.ts new file mode 100644 index 0000000..09446f6 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/PhonenumberTestModule.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; + +import { TiPhonenumberModule } from '@opentiny/ng'; +import { PhonenumberBasicComponent } from './PhonenumberBasicComponent'; +import { PhonenumberDisabledComponent } from './PhonenumberDisabledComponent'; +import { PhonenumberEventComponent } from './PhonenumberEventComponent'; +import { PhonenumberValidComponent } from './PhonenumberValidComponent'; +import { PhonenumberCountryComponent } from './PhonenumberCountryComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiPhonenumberModule, DemoLogModule, RouterModule.forChild(PhonenumberTestModule.ROUTES)], + declarations: [ + PhonenumberBasicComponent, + PhonenumberDisabledComponent, + PhonenumberEventComponent, + PhonenumberValidComponent, + PhonenumberCountryComponent + ] +}) +export class PhonenumberTestModule { + static readonly ROUTES: Routes = [ + { path: 'phonenumber/phonenumber-basic', component: PhonenumberBasicComponent }, + { path: 'phonenumber/phonenumber-disabled', component: PhonenumberDisabledComponent }, + { path: 'phonenumber/phonenumber-event', component: PhonenumberEventComponent }, + { path: 'phonenumber/phonenumber-valid', component: PhonenumberValidComponent }, + { path: 'phonenumber/phonenumber-country', component: PhonenumberCountryComponent } + ]; +} diff --git a/src/phonenumber/demo/src/app/phonenumber/PhonenumberValidComponent.ts b/src/phonenumber/demo/src/app/phonenumber/PhonenumberValidComponent.ts new file mode 100644 index 0000000..e5f7533 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/PhonenumberValidComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { TiCountryCode } from '@opentiny/ng'; + +@Component({ + templateUrl: './phonenumber-valid.html' +}) +export class PhonenumberValidComponent { + value: string; + value1: string; + countryOptions: Array = [ + { + ISO2Code: 'MO', + Name: '中国澳门特别行政区', + CountryCode: '853' + }, + { + ISO2Code: 'CN', + Name: '中国大陆', + CountryCode: '86' + }, + { + ISO2Code: 'TW', + Name: '中国台湾', + CountryCode: '886' + }, + { + ISO2Code: 'HK', + Name: '中国香港特别行政区', + CountryCode: '852' + } + ]; +} diff --git a/src/phonenumber/demo/src/app/phonenumber/phonenumber-basic.html b/src/phonenumber/demo/src/app/phonenumber/phonenumber-basic.html new file mode 100644 index 0000000..b6cc9da --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/phonenumber-basic.html @@ -0,0 +1 @@ + diff --git a/src/phonenumber/demo/src/app/phonenumber/phonenumber-country.html b/src/phonenumber/demo/src/app/phonenumber/phonenumber-country.html new file mode 100644 index 0000000..9a07ae1 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/phonenumber-country.html @@ -0,0 +1 @@ + diff --git a/src/phonenumber/demo/src/app/phonenumber/phonenumber-disabled.html b/src/phonenumber/demo/src/app/phonenumber/phonenumber-disabled.html new file mode 100644 index 0000000..aef73a7 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/phonenumber-disabled.html @@ -0,0 +1 @@ + diff --git a/src/phonenumber/demo/src/app/phonenumber/phonenumber-event.html b/src/phonenumber/demo/src/app/phonenumber/phonenumber-event.html new file mode 100644 index 0000000..17b3b26 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/phonenumber-event.html @@ -0,0 +1,8 @@ + + diff --git a/src/phonenumber/demo/src/app/phonenumber/phonenumber-valid.html b/src/phonenumber/demo/src/app/phonenumber/phonenumber-valid.html new file mode 100644 index 0000000..e847ec9 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/phonenumber-valid.html @@ -0,0 +1,4 @@ +

    1.change

    + +

    2.blur

    + diff --git a/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber-demos.js b/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber-demos.js new file mode 100644 index 0000000..c3ab480 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber-demos.js @@ -0,0 +1,75 @@ +export default { + column: '2', + demos: [ + { + demoId: 'phonenumber-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'phonenumber basic' + }, + desc: { + 'zh-CN': '

    通过属性options配置国家/地区码数据集。

    ', + 'en-US': 'phonenumber basic' + }, + apis: ['TiPhonenumberComponent.properties.options'] + }, + { + demoId: 'phonenumber-country', + name: { + 'zh-CN': '当前国家/地区码', + 'en-US': 'phonenumber country' + }, + desc: { + 'zh-CN': '

    通过属性country配置当前国家/地区码。

    ', + 'en-US': 'phonenumber country' + }, + apis: ['TiPhonenumberComponent.properties.country'] + }, + { + demoId: 'phonenumber-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'phonenumber disabled' + }, + desc: { + 'zh-CN': '

    通过属性selectDisabled配置左侧下拉框禁用状态。

    ', + 'en-US': 'phonenumber disabled' + }, + apis: ['TiPhonenumberComponent.properties.selectDisabled'] + }, + { + demoId: 'phonenumber-event', + name: { + 'zh-CN': '事件', + 'en-US': 'phonenumber event' + }, + desc: { + 'zh-CN': + '

    country发生变化时触发countryChange事件;当选中下拉项时触发countrySelect事件。

    ', + 'en-US': 'phonenumber event' + }, + apis: ['TiPhonenumberComponent.events.countryChange', 'TiPhonenumberComponent.events.countrySelect'] + }, + { + demoId: 'phonenumber-valid', + name: { + 'zh-CN': '', + 'en-US': 'phonenumber valid' + }, + desc: { + 'zh-CN': + '

    通过属性validType配置校验类型,包含change(即时校验)blur(失焦检验)两种类型。

    ', + 'en-US': 'phonenumber valid' + }, + apis: ['TiPhonenumberComponent.properties.validType'] + } + ], + ignoreApis: [ + 'TiPhonenumberComponent.properties.disabled', + 'TiPhonenumberComponent.properties.tabindex', + 'TiPhonenumberComponent.events.blur', + 'TiPhonenumberComponent.events.focus', + 'TiPhonenumberComponent.methods.blur', + 'TiPhonenumberComponent.methods.focus' + ] +}; diff --git a/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.cn.md b/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.cn.md new file mode 100644 index 0000000..60161f9 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.cn.md @@ -0,0 +1,15 @@ +--- +title: Phonenumber 电话号码 +--- + +# Phonenumber 电话号码 + +
    + +电话号码是可以根据国家/地区码显示和校验电话号码的组件。 + +```typescript +import { TiPhonenumberModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.en.md b/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.en.md new file mode 100644 index 0000000..b7a9d88 --- /dev/null +++ b/src/phonenumber/demo/src/app/phonenumber/webdoc/phonenumber.en.md @@ -0,0 +1,13 @@ +--- +title: Phonenumber +--- + +# Phonenumber + +
    + +```typescript +import { TiPhonenumberModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/phonenumber/demo/src/favicon.ico b/src/phonenumber/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d7a7af634ee6d643ac81d81feadff569ac25de3d GIT binary patch literal 300852 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 + + + + + + Tiny + + + + + + + + + diff --git a/src/phonenumber/demo/src/main.ts b/src/phonenumber/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/phonenumber/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/phonenumber/demo/tsconfig.app.json b/src/phonenumber/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/phonenumber/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/phonenumber/lib/index.ts b/src/phonenumber/lib/index.ts new file mode 100644 index 0000000..dd18df6 --- /dev/null +++ b/src/phonenumber/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiPhonenumberModule'; +export * from './src/TiPhonenumberComponent'; diff --git a/src/phonenumber/lib/ng-package.json b/src/phonenumber/lib/ng-package.json new file mode 100644 index 0000000..019c0af --- /dev/null +++ b/src/phonenumber/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/phonenumber", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/phonenumber/lib/package.json b/src/phonenumber/lib/package.json new file mode 100644 index 0000000..20b7c77 --- /dev/null +++ b/src/phonenumber/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "@opentiny/ng-phonenumber", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "libphonenumber-js/max": "1.10.7", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-validation": "~1.0.0-beta.0", + "@opentiny/ng-select": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-text": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/phonenumber/lib/project.json b/src/phonenumber/lib/project.json new file mode 100644 index 0000000..47299b2 --- /dev/null +++ b/src/phonenumber/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/phonenumber/lib", + "sourceRoot": "src/phonenumber/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/phonenumber"], + "options": { + "project": "src/phonenumber/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/phonenumber"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js phonenumber" + }, + { + "command": "ng default-build phonenumber" + }, + { + "command": "node build/clear-default-theme.js phonenumber" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/phonenumber && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build phonenumber && ng pack phonenumber && node build/publish.js phonenumber --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/phonenumber/lib/src/TiPhoneValidatorDirective.ts b/src/phonenumber/lib/src/TiPhoneValidatorDirective.ts new file mode 100644 index 0000000..2f2789c --- /dev/null +++ b/src/phonenumber/lib/src/TiPhoneValidatorDirective.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn, Validators } from '@angular/forms'; +import { TiLocale } from '@opentiny/ng-locale'; +import parsePhoneNumber, { CountryCode, PhoneNumber } from 'libphonenumber-js/max'; // max为严格匹配 TODO:都放在大括号中输入一位的时候报错 +/** + * @ignore + */ +@Directive({ + selector: `[tiPhone][formControlName],[tiPhone][formControl],[tiPhone][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, // 该 Token 用于配置自定义验证器 Provider + useExisting: forwardRef(() => TiPhoneValidatorDirective), + multi: true // 可以使用相同的 Token 去注册多个 Provider + } + ] +}) +export class TiPhoneValidatorDirective implements Validator, OnChanges { + private validator: ValidatorFn = Validators.nullValidator; // 校验规则函数 + private onChange?: () => void; // 承接fn的回调函数 + /** + * @ignore + * 电话号码校验参数 + */ + @Input() tiPhone!: CountryCode; + ngOnChanges(changes: SimpleChanges): void { + if ('tiPhone' in changes) { + this._createValidator(); + if (this.onChange) { + this.onChange(); + } + } + } + + validate(control: AbstractControl): ValidationErrors | null { + return this.validator(control); + } + + registerOnValidatorChange(fn: () => void): void { + this.onChange = fn; + } + + private _createValidator(): void { + this.validator = phone(this.tiPhone); + + function phone(code: CountryCode): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const localeWords: any = TiLocale.getLocaleWords().tiPhonenumber; + + // 输入框为空时 + if (control.value === null || control.value === undefined || control.value.length === 0) { + return { phone: { tiErrorMessage: localeWords.not_blank } }; + } + /** + * 电话号码可以包含空格、逗号、括号、数字、连字符、点号,不能包含大小写字母、汉字、其他特殊字符 + * 正则表达式有^,取反集 + */ + const isInValidPhonenumberReg = /[^0-9-.,\s\(\)]/; + if (control.value.match(isInValidPhonenumberReg)) { + return { phone: { tiErrorMessage: localeWords.invalid } }; + } + // 输入框非空时 + const phoneNum: PhoneNumber = parsePhoneNumber(control.value, code); // 输入字母等phoneNum为undefined + + return phoneNum && phoneNum.isValid() ? null : { phone: { tiErrorMessage: localeWords.invalid } }; + }; + } + } +} diff --git a/src/phonenumber/lib/src/TiPhonenumberComponent.ts b/src/phonenumber/lib/src/TiPhonenumberComponent.ts new file mode 100644 index 0000000..028a505 --- /dev/null +++ b/src/phonenumber/lib/src/TiPhonenumberComponent.ts @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef, Renderer2, ChangeDetectorRef } from '@angular/core'; +import { CountryCode } from 'libphonenumber-js/max'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiValidationConfig } from '@opentiny/ng-validation'; +import { TiSelectComponent } from '@opentiny/ng-select'; +import { TiTextComponent } from '@opentiny/ng-text'; + +export interface TiCountryCode { + /** + * 国家/地区码 + */ + ISO2Code: CountryCode; + /** + * 国家 + */ + Name: string; + /** + * 区号 + */ + CountryCode: string; +} +@Component({ + selector: 'ti-phonenumber', + templateUrl: './phonenumber.html', + styleUrls: ['./phonenumber.less'], + providers: [TiFormComponent.getValueAccessor(TiPhonenumberComponent)] +}) +export class TiPhonenumberComponent extends TiFormComponent implements OnInit { + /* + * 下拉框是否灰化 + */ + @Input() selectDisabled = false; + /** + * 校验类型 + */ + @Input() validType: 'blur' | 'change' = 'change'; + /** + * 当前国家/地区码 + */ + @Input() country: CountryCode = 'CN'; // 默认为中国大陆 + /** + * 国家/地区码数据集 + */ + @Input() options: Array; + /** + * 下拉选中项变化时触发的回调,参数:当前国家/地区码 + */ + @Output() readonly countryChange: EventEmitter = new EventEmitter(); + /** + * 下拉项选中时触发的回调,参数:当前国家/地区码 + */ + @Output() readonly countrySelect: EventEmitter = new EventEmitter(); + /** + * @ignore + * 校验配置 + */ + public validation: TiValidationConfig; + /** + * @ignore + * select组件 + */ + @ViewChild('select', { static: true }) selectComp: TiSelectComponent; + /** + * @ignore + */ + @ViewChild('input', { static: true }) inputComp: TiTextComponent; + + constructor(protected hostRef: ElementRef, protected renderer2: Renderer2, cdRef: ChangeDetectorRef) { + super(hostRef, renderer2, cdRef); + } + + ngOnInit() { + super.ngOnInit(); + // 设置组件可聚焦元素 + this.setFocusableElems([this.selectComp.nativeElement].concat(this.inputComp.nativeElement)); + // 校验配置 + this.validation = { + type: this.validType + }; + } + /** + * @ignore + * 下拉组件select事件 + */ + onSelect(event: TiCountryCode): void { + this.countrySelect.emit(event.ISO2Code); + } + /** + * @ignore + * 下拉组件ngModelChange事件 + */ + onNgModelChange(event: CountryCode): void { + this.country = event; + this.countryChange.emit(event); + } +} diff --git a/src/phonenumber/lib/src/TiPhonenumberModule.ts b/src/phonenumber/lib/src/TiPhonenumberModule.ts new file mode 100644 index 0000000..65288df --- /dev/null +++ b/src/phonenumber/lib/src/TiPhonenumberModule.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiPhonenumberComponent } from './TiPhonenumberComponent'; +import { TiPhoneValidatorDirective } from './TiPhoneValidatorDirective'; +import { TiLocale, TiLocaleModule } from '@opentiny/ng-locale'; +import { locales } from './i18n'; +import { FormsModule } from '@angular/forms'; +import { TiSelectModule } from '@opentiny/ng-select'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiValidationModule } from '@opentiny/ng-validation'; + +@NgModule({ + declarations: [TiPhonenumberComponent, TiPhoneValidatorDirective], + imports: [CommonModule, FormsModule, TiLocaleModule, TiSelectModule, TiTextModule, TiValidationModule], + exports: [TiPhonenumberComponent, TiPhoneValidatorDirective] +}) +export class TiPhonenumberModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiPhonenumberComponent, TiCountryCode } from './TiPhonenumberComponent'; +export { TiPhoneValidatorDirective } from './TiPhoneValidatorDirective'; diff --git a/src/phonenumber/lib/src/i18n/TiPhonenumberWords.ts b/src/phonenumber/lib/src/i18n/TiPhonenumberWords.ts new file mode 100644 index 0000000..ba190b6 --- /dev/null +++ b/src/phonenumber/lib/src/i18n/TiPhonenumberWords.ts @@ -0,0 +1,8 @@ +export interface TiPhonenumberWords { + tiPhonenumber: { + placeholder: string; + not_blank: string; + invalid: string; + not_found: string; + }; +} diff --git a/src/phonenumber/lib/src/i18n/en_US.ts b/src/phonenumber/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..69ca9d5 --- /dev/null +++ b/src/phonenumber/lib/src/i18n/en_US.ts @@ -0,0 +1,10 @@ +import { TiPhonenumberWords } from './TiPhonenumberWords'; + +export const en_US: TiPhonenumberWords = { + tiPhonenumber: { + placeholder: 'Enter a mobile number.', + not_blank: 'This field cannot be left blank.', + invalid: 'Invalid mobile number.', + not_found: 'No records found.' + } +}; diff --git a/src/phonenumber/lib/src/i18n/es_US.ts b/src/phonenumber/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..0d521a3 --- /dev/null +++ b/src/phonenumber/lib/src/i18n/es_US.ts @@ -0,0 +1,10 @@ +import { TiPhonenumberWords } from './TiPhonenumberWords'; + +export const es_US: TiPhonenumberWords = { + tiPhonenumber: { + placeholder: 'Ingrese un número de celular', + not_blank: 'Este campo es obligatorio.', + invalid: 'El número de celular no es válido.', + not_found: 'No se encontraron registros.' + } +}; diff --git a/src/phonenumber/lib/src/i18n/fr_FR.ts b/src/phonenumber/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..a363ef2 --- /dev/null +++ b/src/phonenumber/lib/src/i18n/fr_FR.ts @@ -0,0 +1,10 @@ +import { TiPhonenumberWords } from './TiPhonenumberWords'; + +export const fr_FR: TiPhonenumberWords = { + tiPhonenumber: { + placeholder: 'Ingrese un número de celular', + not_blank: 'Este campo es obligatorio.', + invalid: 'El número de celular no es válido.', + not_found: 'No se encontraron registros.' + } +}; diff --git a/src/phonenumber/lib/src/i18n/index.ts b/src/phonenumber/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/phonenumber/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/phonenumber/lib/src/i18n/pt_BR.ts b/src/phonenumber/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..ccfc42a --- /dev/null +++ b/src/phonenumber/lib/src/i18n/pt_BR.ts @@ -0,0 +1,10 @@ +import { TiPhonenumberWords } from './TiPhonenumberWords'; + +export const pt_BR: TiPhonenumberWords = { + tiPhonenumber: { + placeholder: 'Insira um número de celular.', + not_blank: 'Este campo não pode ser deixado em branco.', + invalid: 'Número de celular inválido.', + not_found: 'Nenhum registro encontrado.' + } +}; diff --git a/src/phonenumber/lib/src/i18n/zh_CN.ts b/src/phonenumber/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..b9095b3 --- /dev/null +++ b/src/phonenumber/lib/src/i18n/zh_CN.ts @@ -0,0 +1,10 @@ +import { TiPhonenumberWords } from './TiPhonenumberWords'; + +export const zh_CN: TiPhonenumberWords = { + tiPhonenumber: { + placeholder: '请输入手机号', + not_blank: '手机号不能为空', + invalid: '手机号格式错误', + not_found: '无匹配结果' + } +}; diff --git a/src/phonenumber/lib/src/phonenumber.html b/src/phonenumber/lib/src/phonenumber.html new file mode 100644 index 0000000..4fa1cf9 --- /dev/null +++ b/src/phonenumber/lib/src/phonenumber.html @@ -0,0 +1,34 @@ + +
    + + + {{item.Name}}(+{{item.CountryCode}}) + +{{select.CountryCode}} + +
    diff --git a/src/phonenumber/lib/src/phonenumber.less b/src/phonenumber/lib/src/phonenumber.less new file mode 100644 index 0000000..3007e0a --- /dev/null +++ b/src/phonenumber/lib/src/phonenumber.less @@ -0,0 +1,102 @@ +@import '../../../themes/basic/base-all.less'; +::ng-deep :root { + --ti-phonenumber-width: 300px; // 组件整体宽度 + --ti-phonenumber-input-width: 220px; // 输入框宽度 + --ti-phonenumber-line-height: 14px; // 短竖线高度 + --ti-phonenumber-select-border-right-height: 26px; // select组件右边框竖线高度 +} + +.ti3-phonenumber-input(@border-color) { + width: calc(var(--ti-phonenumber-input-width) + 1px); // 由于要覆盖中间分隔线,左移了1px,故input框总宽+1 + border: 1px solid @border-color; + margin-left: -1px; // 覆盖select框与input框中间的分隔线 +} + +:host { + display: inline-block; + width: var(--ti-phonenumber-width); + height: var(--ti-input-height); +} + +.ti3-phonenumber-container { + position: relative; + display: flex; + width: 100%; + + // select组件样式 + .ti3-phonenumber-select { + width: calc(var(--ti-phonenumber-width) - var(--ti-phonenumber-input-width)); + border-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + position: relative; + &:before { // 定义中间竖线 + content: ""; + position: absolute; + right: 0; + top: 0; + border-left: 1px solid transparent; + height: var(--ti-phonenumber-select-border-right-height); + z-index: 1; // hover或focus时提高层级,覆盖select框与searchbox框中间的透明分隔线 + } + + &:hover:not([disabled]):not([tiFocused]):before { + border-left-color: var(--ti-common-color-line-hover); + } + + &[tiFocused]:before { + border-left-color: var(--ti-common-color-line-active); + } + + // 正常状态时select组件和输入框组件之间短的竖线 + ::ng-deep & .ti3-select-dominator-dropdown-btn:before { + content: ""; + position: absolute; + right: 0; + height: var(--ti-phonenumber-line-height); + top: calc((var(--ti-phonenumber-select-border-right-height) - var(--ti-phonenumber-line-height)) / 2); + border-left: 1px solid var(--ti-common-color-line-dividing); + } + } + + // 输入框组件样式 + input[tiText].ti3-phonenumber-input { + width: var(--ti-phonenumber-input-width); + z-index: 1; // 左边框被select遮挡,故需要提高权重 + border-left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + order: 1; // 父级为弹性布局,order值越大排列越靠后 + // select灰化时输入框的样式 + &.ti3-phonenumber-select-disable { + .ti3-phonenumber-input(var(--ti-common-color-line-normal)); + } + // 输入框hover时的样式 + &:hover { + .ti3-phonenumber-input(var(--ti-common-color-line-hover)); + } + // 输入框聚焦时的样式 + &:focus { + .ti3-phonenumber-input(var(--ti-common-color-line-active)); + } + } +} +// 失焦校验:错误且已输入失焦时显示错误样式,及时校验:错误且已输入时显示错误样式 +@tiny-invalid-class: +&.ng-invalid.ng-touched[tiBlurCheck]:not([tiFocused]), +&.ng-invalid.ng-dirty[tiBlurCheck]:not([tiFocused]), +&.ng-invalid.ng-touched:not([tiBlurCheck]), +&.ng-invalid.ng-dirty:not([tiBlurCheck]); + +// 校验失败样式 +@{tiny-invalid-class}.ti3-phonenumber-input { // input框校验失败时 + + .ti3-phonenumber-select { + &:not([disabled]) { // 非灰化select组件的边框和背景色 + border-color: var(--ti-common-color-error-border); + background-color: var(--ti-common-color-error-bg); + } + &:hover:before, &[tiFocused]:before { // 中间分割线hover和聚焦边框色 + border-left-color: var(--ti-common-color-error-border); + } + } +} diff --git a/src/popconfirm/demo/project.json b/src/popconfirm/demo/project.json new file mode 100644 index 0000000..0a96560 --- /dev/null +++ b/src/popconfirm/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/popconfirm/demo", + "sourceRoot": "src/popconfirm/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/popconfirm", + "index": "src/popconfirm/demo/src/index.html", + "main": "src/popconfirm/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/popconfirm/demo/tsconfig.app.json", + "assets": ["src/popconfirm/demo/src/favicon.ico", "src/popconfirm/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "popconfirm-demo:build:production" + }, + "development": { + "browserTarget": "popconfirm-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js popconfirm" + } + ] + } + } + } +} diff --git a/src/popconfirm/demo/src/app/AppComponent.ts b/src/popconfirm/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/popconfirm/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/popconfirm/demo/src/app/AppModule.ts b/src/popconfirm/demo/src/app/AppModule.ts new file mode 100644 index 0000000..6569ff2 --- /dev/null +++ b/src/popconfirm/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { PopconfirmTestModule } from './popconfirm/PopconfirmTestModule'; + +@NgModule({ + imports: [ + PopconfirmTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/popconfirm/demo/src/app/IndexComponent.ts b/src/popconfirm/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..0138a21 --- /dev/null +++ b/src/popconfirm/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { PopconfirmTestModule } from './popconfirm/PopconfirmTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = PopconfirmTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/popconfirm/demo/src/app/app.html b/src/popconfirm/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/popconfirm/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/popconfirm/demo/src/app/popconfirm/PopconfirmBasicComponent.ts b/src/popconfirm/demo/src/app/popconfirm/PopconfirmBasicComponent.ts new file mode 100644 index 0000000..39371d3 --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/PopconfirmBasicComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { TiPopconfirmConfig } from '@opentiny/ng'; +@Component({ + templateUrl: './popconfirm-basic.html' +}) +export class PopconfirmBasicComponent { + popConfig: TiPopconfirmConfig = { + id: 'delete', + content: '确定要删除该安全组规则吗?' + }; +} diff --git a/src/popconfirm/demo/src/app/popconfirm/PopconfirmDefineComponent.ts b/src/popconfirm/demo/src/app/popconfirm/PopconfirmDefineComponent.ts new file mode 100644 index 0000000..29aab4e --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/PopconfirmDefineComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { TiPopconfirmConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './popconfirm-define.html' +}) +export class PopconfirmDefineComponent { + popConfig1: TiPopconfirmConfig = { + id: 'start', + yesPrimary: false, + content: '确定要启用吗?', + yesText: '确认', + noText: '取消', + position: 'right' + }; +} diff --git a/src/popconfirm/demo/src/app/popconfirm/PopconfirmEventComponent.ts b/src/popconfirm/demo/src/app/popconfirm/PopconfirmEventComponent.ts new file mode 100644 index 0000000..7ece9de --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/PopconfirmEventComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { TiPopconfirmConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './popconfirm-event.html' +}) +export class PopconfirmEventComponent { + myLogs: Array = []; + popConfig: TiPopconfirmConfig = { + id: 'delete', + content: '确定要删除该安全组规则吗?', + close: (data: any): void => { + // 可通过data接口传递参数 + this.myLogs = [...this.myLogs, '确认删除']; + }, + dismiss: (data: any): void => { + this.myLogs = [...this.myLogs, '否认删除']; + } + }; +} diff --git a/src/popconfirm/demo/src/app/popconfirm/PopconfirmTableComponent.ts b/src/popconfirm/demo/src/app/popconfirm/PopconfirmTableComponent.ts new file mode 100644 index 0000000..4a3bb95 --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/PopconfirmTableComponent.ts @@ -0,0 +1,123 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './popconfirm-table.html' +}) +export class PopconfirmTableComponent { + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + myLogs: any = []; + columns: Array = [ + { + label: 'first name', + width: '25%' + }, + { + label: 'last name', + width: '20%' + }, + { + label: 'age', + width: '20%' + }, + { + label: 'state', + width: '15%' + }, + { + label: 'options', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 4; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + onSelect(item: any): void { + this.myLogs = [...this.myLogs, item.label]; + } + dataToItemsFn: (data: any) => Array = (data: any) => { + let items: Array = [ + { + label: '删除', + popConfig: { + content: '确定要删除该项吗?', + yesPrimary: true, + close: (): void => { + const index = this.srcData.data.findIndex((current: TiTableRowData): boolean => { + return current.rowId === data.rowId; + }); + this.srcData.data.splice(index, 1); + this.myLogs = [...this.myLogs, '确认删除']; + }, + dismiss: (): void => { + this.myLogs = [...this.myLogs, '否认删除']; + } + } + }, + { + label: '禁用', + popConfig: { + content: '确定要禁用该项吗?', + close: (): void => { + this.myLogs = [...this.myLogs, '确认禁用']; + }, + dismiss: (): void => { + this.myLogs = [...this.myLogs, '否认禁用']; + } + } + }, + { + label: '制作镜像' + }, + { + label: '这是一个很长的选项' + }, + { + label: '制作镜像2', + disabled: true + } + ]; + if (data.state === '已停止') { + items[0].disabled = false; + items[1].disabled = true; + items = [items[0], items[1]]; + } + return items; + }; + + private createRandomItem(id: number): any { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip']; + const firstName: string = nameList[Math.floor(Math.random() * 4)]; + const lastName: string = familyName[Math.floor(Math.random() * 4)]; + const age: number = Math.floor(Math.random() * 100); + const state: string = ['已启动', '已停止'][Math.floor(Math.random() * 2)]; + const rowId: string = 'row_' + id; + + return { + firstName, + lastName, + age, + state, + rowId + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/popconfirm/demo/src/app/popconfirm/PopconfirmTableDefineComponent.ts b/src/popconfirm/demo/src/app/popconfirm/PopconfirmTableDefineComponent.ts new file mode 100644 index 0000000..d8e6585 --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/PopconfirmTableDefineComponent.ts @@ -0,0 +1,87 @@ +import { Component } from '@angular/core'; +import { TiPopconfirmConfig, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './popconfirm-table-define.html' +}) +export class PopconfirmTableDefineComponent { + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + myLogs: any = []; + columns: Array = [ + { + label: 'first name', + width: '25%' + }, + { + label: 'last name', + width: '20%' + }, + { + label: 'age', + width: '20%' + }, + { + label: 'state', + width: '15%' + }, + { + label: 'option', + width: '20%' + } + ]; + + popConfig: TiPopconfirmConfig = { + id: 'delete', + content: '确定要删除吗?', + yesText: '确认', + noText: '取消', + position: 'right', + close: (data: any): void => { + const index = this.srcData.data.findIndex((current: TiTableRowData): boolean => { + return current.rowId === data.rowId; + }); + this.srcData.data.splice(index, 1); + this.myLogs = [...this.myLogs, '确认删除']; + }, + dismiss: (data: any): void => { + this.myLogs = [...this.myLogs, '取消删除']; + } + }; + + ngOnInit(): void { + for (let j: number = 0; j < 4; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + private createRandomItem(id: number): any { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[Math.floor(Math.random() * 4)]; + const lastName: string = familyName[Math.floor(Math.random() * 4)]; + const age: number = Math.floor(Math.random() * 100); + const state: string = ['已启动', '已停止'][Math.floor(Math.random() * 2)]; + const rowId: string = 'row_' + id; + + return { + firstName, + lastName, + age, + state, + rowId + }; + } + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/popconfirm/demo/src/app/popconfirm/PopconfirmTestModule.ts b/src/popconfirm/demo/src/app/popconfirm/PopconfirmTestModule.ts new file mode 100644 index 0000000..13ad81b --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/PopconfirmTestModule.ts @@ -0,0 +1,56 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiActionmenuModule, TiMenuModule, TiPopconfirmModule, TiTableModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { PopconfirmBasicComponent } from './PopconfirmBasicComponent'; +import { PopconfirmDefineComponent } from './PopconfirmDefineComponent'; +import { PopconfirmEventComponent } from './PopconfirmEventComponent'; +import { PopconfirmTableComponent } from './PopconfirmTableComponent'; +import { PopconfirmTableDefineComponent } from './PopconfirmTableDefineComponent'; + +@NgModule({ + imports: [ + CommonModule, + TiActionmenuModule, + TiPopconfirmModule, + TiTableModule, + TiMenuModule, + DemoLogModule, + RouterModule.forChild(PopconfirmTestModule.ROUTES) + ], + declarations: [ + PopconfirmBasicComponent, + PopconfirmDefineComponent, + PopconfirmEventComponent, + PopconfirmTableComponent, + PopconfirmTableDefineComponent + ] +}) +export class PopconfirmTestModule { + static readonly LINKS: Array = [{ href: 'components/TiPopoverComponent.html', label: 'Popconfirm' }]; + static readonly ROUTES: Routes = [ + { + path: 'popconfirm/popconfirm-basic', + component: PopconfirmBasicComponent + }, + { + path: 'popconfirm/popconfirm-define', + component: PopconfirmDefineComponent + }, + { + path: 'popconfirm/popconfirm-event', + component: PopconfirmEventComponent + }, + { + path: 'popconfirm/popconfirm-table', + component: PopconfirmTableComponent + }, + { + path: 'popconfirm/popconfirm-table-define', + component: PopconfirmTableDefineComponent + } + ]; +} diff --git a/src/popconfirm/demo/src/app/popconfirm/popconfirm-basic.html b/src/popconfirm/demo/src/app/popconfirm/popconfirm-basic.html new file mode 100644 index 0000000..e3d3d6b --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/popconfirm-basic.html @@ -0,0 +1 @@ +删除 diff --git a/src/popconfirm/demo/src/app/popconfirm/popconfirm-define.html b/src/popconfirm/demo/src/app/popconfirm/popconfirm-define.html new file mode 100644 index 0000000..ebd5db7 --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/popconfirm-define.html @@ -0,0 +1 @@ +启用 diff --git a/src/popconfirm/demo/src/app/popconfirm/popconfirm-event.html b/src/popconfirm/demo/src/app/popconfirm/popconfirm-event.html new file mode 100644 index 0000000..1aa2a6c --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/popconfirm-event.html @@ -0,0 +1,3 @@ +删除 +
    + diff --git a/src/popconfirm/demo/src/app/popconfirm/popconfirm-table-define.html b/src/popconfirm/demo/src/app/popconfirm/popconfirm-table-define.html new file mode 100644 index 0000000..5ffd60a --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/popconfirm-table-define.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + +
    {{column.label}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.state}} + 删除 +
    +
    + diff --git a/src/popconfirm/demo/src/app/popconfirm/popconfirm-table.html b/src/popconfirm/demo/src/app/popconfirm/popconfirm-table.html new file mode 100644 index 0000000..b5af44e --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/popconfirm-table.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + +
    {{column.label}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.state}} + +
    +
    + diff --git a/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm-demos.js b/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm-demos.js new file mode 100644 index 0000000..f8f937c --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm-demos.js @@ -0,0 +1,68 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'popconfirm-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': '

    Popconfirm 组件的最简用法。

    ', + 'en-US': '

    basic

    ' + } + }, + { + demoId: 'popconfirm-define', + name: { + 'zh-CN': '自定义使用', + 'en-US': 'define' + }, + desc: { + 'zh-CN': + '

    Popconfirm 组件的自定义用法。通过配置TiPopconfirmConfig接口的内容,如contentyesTextnoText等传递数据。

    ', + 'en-US': '

    define

    ' + }, + apis: ['TiPopconfirmDirective.properties.tiPopconfirm'] + }, + { + demoId: 'popconfirm-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event' + }, + desc: { + 'zh-CN': + '

    当点击弹出确认框中“是”的按钮时触发close事件。当点击弹出确认框中的“否”的按钮时触发dismiss事件。

    ', + 'en-US': '

    event

    ' + }, + apis: ['TiPopconfirmConfig.methods.close', 'TiPopconfirmConfig.methods.dismiss'] + }, + { + demoId: 'popconfirm-table', + name: { + 'zh-CN': '在表格中结合 actionmenu 的用法', + 'en-US': 'PopconfirmInTable' + }, + desc: { + 'zh-CN': + '

    通过配置TiActionmenuItem接口中的内容传递数据。使用 Actionmenu 组件,请导入TiActionmenuModule,具体可参考 Actionmenu 组件的使用说明。

    ', + 'en-US': '

    PopconfirmInTable

    ' + }, + apis: ['TiPopconfirmDirective.properties.data', 'TiActionmenuItem.properties.popConfig'] + }, + { + demoId: 'popconfirm-table-define', + name: { + 'zh-CN': '在表格中自定义使用', + 'en-US': 'PopconfirmInTable' + }, + desc: { + 'zh-CN': '

    通过配置TiPopconfirmConfig接口的内容传递数据。

    ', + 'en-US': '

    PopconfirmInTable

    ' + }, + apis: ['TiPopconfirmDirective.properties.tiPopconfirm'] + } + ] +}; diff --git a/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.cn.md b/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.cn.md new file mode 100644 index 0000000..782e055 --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.cn.md @@ -0,0 +1,23 @@ +--- +title: Popconfirm 气泡确认框 +--- +# Popconfirm 气泡确认框 + +
    + +Popconfirm 是一个文字提示气泡框组件。   + +```typescript +import { TiPopconfirmModule } from '@opentiny/ng'; +``` + +
    + +
    + +Popconfirm 是一个文字提示气泡框组件。   + +```typescript +import { TiPopconfirmModule } from '@opentiny/ng'; +``` +
    diff --git a/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.en.md b/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/popconfirm/demo/src/app/popconfirm/webdoc/popconfirm.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/popconfirm/demo/src/favicon.ico b/src/popconfirm/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/popconfirm/demo/src/index.html b/src/popconfirm/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/popconfirm/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/popconfirm/demo/src/main.ts b/src/popconfirm/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/popconfirm/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/popconfirm/demo/tsconfig.app.json b/src/popconfirm/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/popconfirm/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/popconfirm/lib/index.ts b/src/popconfirm/lib/index.ts new file mode 100644 index 0000000..506e2f7 --- /dev/null +++ b/src/popconfirm/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiPopconfirmModule'; diff --git a/src/popconfirm/lib/ng-package.json b/src/popconfirm/lib/ng-package.json new file mode 100644 index 0000000..8fcc727 --- /dev/null +++ b/src/popconfirm/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/popconfirm", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/popconfirm/lib/package.json b/src/popconfirm/lib/package.json new file mode 100644 index 0000000..2d58f06 --- /dev/null +++ b/src/popconfirm/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "@opentiny/ng-popconfirm", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/popconfirm/lib/project.json b/src/popconfirm/lib/project.json new file mode 100644 index 0000000..f7e0e52 --- /dev/null +++ b/src/popconfirm/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/popconfirm/lib", + "sourceRoot": "src/popconfirm/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/popconfirm"], + "options": { + "project": "src/popconfirm/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/popconfirm"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js popconfirm" + }, + { + "command": "ng default-build popconfirm" + }, + { + "command": "node build/clear-default-theme.js popconfirm" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/popconfirm && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build popconfirm && ng pack popconfirm && node build/publish.js popconfirm --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/popconfirm/lib/src/TiPopconfirmComponent.ts b/src/popconfirm/lib/src/TiPopconfirmComponent.ts new file mode 100644 index 0000000..2157447 --- /dev/null +++ b/src/popconfirm/lib/src/TiPopconfirmComponent.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterViewInit, Component, ElementRef, Input, OnInit, Renderer2, ChangeDetectionStrategy } from '@angular/core'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +@Component({ + selector: 'ti-popconfirm', + templateUrl: 'popconfirm.html', + styleUrls: ['./popconfirm.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-popconfirm-container]': 'true', + '[tabindex]': '0' + } +}) +export class TiPopconfirmComponent extends TiBaseComponent implements OnInit, AfterViewInit { + @Input() config: any; + @Input() destroyPopover: (result?: boolean) => void; + protected versionInfo: string = super.getVersion(packageInfo); + private readonly tiPopconfirm: any = TiLocale.getLocaleWords().tiPopconfirm; + + ngOnInit(): void { + this.config.yesText = this.config.yesText || this.tiPopconfirm.yesLabel; + this.config.noText = this.config.noText || this.tiPopconfirm.noLabel; + } + + ngAfterViewInit(): void { + // 轻量级弹窗阴影定制 + this.renderer.addClass(this.nativeElement.offsetParent, 'ti3-popconfirm-tip'); // 在init中设置会影响button样式 TODO: IE下还存在背景闪烁问题 + } +} diff --git a/src/popconfirm/lib/src/TiPopconfirmDirective.ts b/src/popconfirm/lib/src/TiPopconfirmDirective.ts new file mode 100644 index 0000000..b747e3b --- /dev/null +++ b/src/popconfirm/lib/src/TiPopconfirmDirective.ts @@ -0,0 +1,230 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectorRef, ComponentRef, Directive, ElementRef, Inject, Input, NgZone, OnDestroy, OnInit, Renderer2 } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +import { TiHostLayout, TiPositionType } from '@opentiny/ng-utils'; +import { TiTipRef } from '@opentiny/ng-tip'; +import { TiTipService } from '@opentiny/ng-tip'; +import { TiTipDirective } from '@opentiny/ng-tip'; +import { TiPopconfirmComponent } from './TiPopconfirmComponent'; +import { TiKeymap, Util } from '@opentiny/ng-utils'; + +export interface TiPopconfirmConfig { + /** + * 确认框 id + */ + id?: string; + /** + * 确认框描述信息 + */ + content: string; + /** + * 自定义“是”按钮文本 + */ + yesText?: string; + /** + * 自定义“否”按钮文本 + */ + noText?: string; + /** + * 设置“是”按钮为强调按钮 + */ + yesPrimary?: boolean; + /** + * 确认框弹出方向 + */ + position?: TiPositionType; + /** + * 触发“是”按钮事件 + */ + close?(data?: any): void; + /** + * 触发“否”按钮事件 + */ + dismiss?(data?: any): void; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} + +/** + * TiPopconfirm 气泡确认框指令 + * + * 一般用于操作执行后对用户业务不会有严重影响的轻量级场景。 + * + * + */ +@Directive({ + selector: '[tiPopconfirm]' +}) +export class TiPopconfirmDirective extends TiTipDirective implements OnInit, OnDestroy { + private static readonly Z_INDEX: number = 100; // 默认层级是100,暂不支持可配置 + /** + * 气泡确认框配置对象 + */ + @Input() tiPopconfirm: TiPopconfirmConfig; + /** + * + * 数据接口, 常常绑定表格本行数据 + */ + @Input() data: any; + protected tipInstance: TiTipRef; + private hostElement: any; + private popoverComponentRef: ComponentRef = null; + private tipElement; // popoverComponentRef的原生元素 + private unlistenClick: () => void; + // 可聚焦元素 + private focusableElementsString: string = `a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), + button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), + textarea:not([disabled]):not([tabindex=\'-1\']), + iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]`; + constructor( + private tiTipService: TiTipService, + private hostEleRef: ElementRef, + private zone: NgZone, + private render: Renderer2, + @Inject(DOCUMENT) private document, + private changeDetectorRef: ChangeDetectorRef + ) { + super(tiTipService, hostEleRef); + } + /** + * tip 组件配置 + */ + private tipConfig: any = { + trigger: 'manual', + theme: 'white' + }; + private unListenDocumentKeydown: () => void; + + ngOnInit(): void { + if (!this.tiPopconfirm || !this.tiPopconfirm.content) { + return; + } + this.hostElement = this.hostEleRef.nativeElement; + // 创建实例 + this.tipInstance = this.tiTipService.create(this.hostElement, { + ...this.tipConfig, + position: this.tiPopconfirm.position, + zIndex: TiPopconfirmDirective.Z_INDEX, + registerVisibilityChangeEvent: false + }); + + this.addClickEvent(); + } + + ngOnDestroy(): void { + if (this.tipInstance) { + super.ngOnDestroy(); + } + if (this.unlistenClick) { + this.unlistenClick(); + } + if (this.unListenDocumentKeydown) { + this.unListenDocumentKeydown(); + } + } + + private addClickEvent(): void { + this.zone.runOutsideAngular(() => { + this.unlistenClick = this.render.listen(this.document, 'click', (event: MouseEvent) => { + if (this.hostElement.contains(event.target) && !this.popoverComponentRef) { + this.showPopandFocus(); + + return; + } + if (!(this.tipElement && this.tipElement.contains(event.target))) { + this.zone.run(() => { + this.hide(); + }); + this.popoverComponentRef = null; + } + }); + + this.unListenDocumentKeydown = this.render.listen(document, 'keydown', (event: KeyboardEvent): void => { + switch (event.which) { + case TiKeymap.KEY_TAB: // tab键用于处理在提示框内循环获取焦点 + this.clickTab(event); + break; + case TiKeymap.KEY_ENTER: + if (this.hostElement.contains(event.target) && !this.popoverComponentRef) { + this.showPopandFocus(); + } + break; + default: + break; + } + }); + }); + } + + /** + * @ignore + * + */ + public clickTab(event: KeyboardEvent): void { + const dialogModalEle: HTMLElement = document.querySelector('.ti3-popconfirm-container'); + const focusableElements: NodeList = dialogModalEle?.querySelectorAll(this.focusableElementsString); + Util.focusInDialogOnTabchange(event, focusableElements); + } + /** + * 打开气泡组件,并且把焦点转移到提示框内部,为后续把焦点限制在提示框内做准备。 + * @private + */ + private showPopandFocus(): void { + this.zone.run(() => { + this.popoverComponentRef = this.show(); + const popContainerEle: HTMLElement = document.querySelector('.ti3-popconfirm-container'); + if (popContainerEle) { + popContainerEle.focus(); + } + }); + } + + /** + * 显示气泡确认框 + * @ignore + */ + public show(): ComponentRef { + if (!this.tipInstance) { + return; + } + const destroyPopover: (result?: boolean) => void = (result?: boolean): void => { + this.hide(); + if (result && Util.isFunction(this.tiPopconfirm.close)) { + this.tiPopconfirm.close(this.data); + } else if (!result && Util.isFunction(this.tiPopconfirm.dismiss)) { + this.tiPopconfirm.dismiss(this.data); + } + + // 服务在close和dismiss事件中自行添加逻辑时,视图无法更新需要手动变更刷新 + this.changeDetectorRef.markForCheck(); + }; + const popoverComponentRef: ComponentRef = this.tipInstance.show(TiPopconfirmComponent, { + id: this.tiPopconfirm.id, + config: this.tiPopconfirm, + destroyPopover + }); + this.tipElement = popoverComponentRef.location.nativeElement; + const hostPosition: TiHostLayout = this.tiTipService.positionResult?.hostLayout; + if (typeof getComputedStyle !== 'undefined' && hostPosition?.fixedAncestor) { + const fixedAncestorZindex: number = parseInt(getComputedStyle(hostPosition.fixedAncestor).zIndex, 10); + if (typeof fixedAncestorZindex === 'number' && fixedAncestorZindex > TiPopconfirmDirective.Z_INDEX) { + this.render.setStyle(this.tipElement, 'z-index', fixedAncestorZindex); + } + } + + return popoverComponentRef; + } +} diff --git a/src/popconfirm/lib/src/TiPopconfirmModule.ts b/src/popconfirm/lib/src/TiPopconfirmModule.ts new file mode 100644 index 0000000..0c493c6 --- /dev/null +++ b/src/popconfirm/lib/src/TiPopconfirmModule.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiPopconfirmComponent } from './TiPopconfirmComponent'; +import { TiTipServiceModule } from '@opentiny/ng-tip'; +import { TiLocaleModule, TiLocale } from '@opentiny/ng-locale'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiButtonModule } from '@opentiny/ng-button'; +import { TiPopconfirmDirective } from './TiPopconfirmDirective'; +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, TiTipServiceModule, TiLocaleModule, TiIconModule, TiButtonModule], + exports: [TiPopconfirmDirective], + declarations: [TiPopconfirmComponent, TiPopconfirmDirective], + entryComponents: [TiPopconfirmComponent] +}) +export class TiPopconfirmModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiPopconfirmComponent } from './TiPopconfirmComponent'; +export { TiPopconfirmDirective, TiPopconfirmConfig } from './TiPopconfirmDirective'; diff --git a/src/popconfirm/lib/src/i18n/TiPopconfirmWords.ts b/src/popconfirm/lib/src/i18n/TiPopconfirmWords.ts new file mode 100644 index 0000000..d177154 --- /dev/null +++ b/src/popconfirm/lib/src/i18n/TiPopconfirmWords.ts @@ -0,0 +1,6 @@ +export interface TiPopconfirmWords { + tiPopconfirm: { + yesLabel: string; + noLabel: string; + }; +} diff --git a/src/popconfirm/lib/src/i18n/en_US.ts b/src/popconfirm/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..edc29c3 --- /dev/null +++ b/src/popconfirm/lib/src/i18n/en_US.ts @@ -0,0 +1,8 @@ +import { TiPopconfirmWords } from './TiPopconfirmWords'; + +export const en_US: TiPopconfirmWords = { + tiPopconfirm: { + yesLabel: 'Yes', + noLabel: 'No' + } +}; diff --git a/src/popconfirm/lib/src/i18n/es_US.ts b/src/popconfirm/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..8cec67a --- /dev/null +++ b/src/popconfirm/lib/src/i18n/es_US.ts @@ -0,0 +1,8 @@ +import { TiPopconfirmWords } from './TiPopconfirmWords'; + +export const es_US: TiPopconfirmWords = { + tiPopconfirm: { + yesLabel: 'Sí', + noLabel: 'No' + } +}; diff --git a/src/popconfirm/lib/src/i18n/fr_FR.ts b/src/popconfirm/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..cfc9e90 --- /dev/null +++ b/src/popconfirm/lib/src/i18n/fr_FR.ts @@ -0,0 +1,8 @@ +import { TiPopconfirmWords } from './TiPopconfirmWords'; + +export const fr_FR: TiPopconfirmWords = { + tiPopconfirm: { + yesLabel: 'Oui', + noLabel: 'Non' + } +}; diff --git a/src/popconfirm/lib/src/i18n/index.ts b/src/popconfirm/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/popconfirm/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/popconfirm/lib/src/i18n/pt_BR.ts b/src/popconfirm/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..c65c126 --- /dev/null +++ b/src/popconfirm/lib/src/i18n/pt_BR.ts @@ -0,0 +1,8 @@ +import { TiPopconfirmWords } from './TiPopconfirmWords'; + +export const pt_BR: TiPopconfirmWords = { + tiPopconfirm: { + yesLabel: 'Sim', + noLabel: 'Não' + } +}; diff --git a/src/popconfirm/lib/src/i18n/zh_CN.ts b/src/popconfirm/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..aa26875 --- /dev/null +++ b/src/popconfirm/lib/src/i18n/zh_CN.ts @@ -0,0 +1,8 @@ +import { TiPopconfirmWords } from './TiPopconfirmWords'; + +export const zh_CN: TiPopconfirmWords = { + tiPopconfirm: { + yesLabel: '是', + noLabel: '否' + } +}; diff --git a/src/popconfirm/lib/src/popconfirm.html b/src/popconfirm/lib/src/popconfirm.html new file mode 100644 index 0000000..502243b --- /dev/null +++ b/src/popconfirm/lib/src/popconfirm.html @@ -0,0 +1,20 @@ +
    + + + + +
    + diff --git a/src/popconfirm/lib/src/popconfirm.less b/src/popconfirm/lib/src/popconfirm.less new file mode 100644 index 0000000..0efd193 --- /dev/null +++ b/src/popconfirm/lib/src/popconfirm.less @@ -0,0 +1,49 @@ +:host { + --ti-popconfirm-warn-bg-font-size: var(--ti-common-size-4x); + --ti-popconfirm-warn-bg-line-height: 18px; + --ti-popconfirm-button-margin-left: var(--ti-common-space-2x); + --ti-popconfirm-title-margin-left: var(--ti-common-space-2x); +} + +.ti3-popconfirm-tip { + box-shadow: var(--ti-common-shadow-4-down) !important; +} +:host.ti3-popconfirm-container { + display: inline-block; + outline: none; +} +.ti3-popconfirm-header { + display: flex; + align-content: center; +} + +.ti3-popconfirm-warn-bg { + position: relative; + font-size: var(--ti-popconfirm-warn-bg-font-size); + color: var(--ti-common-color-warn); + line-height: var(--ti-popconfirm-warn-bg-line-height); + align-self: flex-start; // 解决多行文本!图标显示异常问题 +} + +.ti3-popconfirm-warn-icon { + position: absolute; + font-size: calc(var(--ti-popconfirm-warn-bg-font-size) * 0.75); + color: var(--ti-common-color-icon-white); + left: calc((var(--ti-popconfirm-warn-bg-font-size) * 0.25) / 2); + bottom: -1px; +} + +.ti3-popconfirm-title { + color: var(--ti-common-color-text-primary); + font-size: var(--ti-common-font-size-base); + line-height: var(--ti-common-line-height-number); + margin-left: var(--ti-popconfirm-title-margin-left); +} + +.ti3-popconfirm-footer { + margin-top: var(--ti-common-space-3x); + text-align: right; + & button:nth-of-type(2) { + margin-left: var(--ti-popconfirm-button-margin-left); + } +} diff --git a/src/popup/lib/index.ts b/src/popup/lib/index.ts new file mode 100644 index 0000000..73f5082 --- /dev/null +++ b/src/popup/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiPopupService'; +export * from './src/TiPopupModule'; diff --git a/src/popup/lib/ng-package.json b/src/popup/lib/ng-package.json new file mode 100644 index 0000000..731317e --- /dev/null +++ b/src/popup/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/popup", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/popup/lib/package.json b/src/popup/lib/package.json new file mode 100644 index 0000000..78ba411 --- /dev/null +++ b/src/popup/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-popup", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@angular/platform-browser": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/popup/lib/project.json b/src/popup/lib/project.json new file mode 100644 index 0000000..eff9f4c --- /dev/null +++ b/src/popup/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/popup/lib", + "sourceRoot": "src/popup/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/popup"], + "options": { + "project": "src/popup/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/popup"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js popup" + }, + { + "command": "ng default-build popup" + }, + { + "command": "node build/clear-default-theme.js popup" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/popup && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build popup && ng pack popup && node build/publish.js popup --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/popup/lib/src/TiPopupModule.ts b/src/popup/lib/src/TiPopupModule.ts new file mode 100644 index 0000000..9bafcfe --- /dev/null +++ b/src/popup/lib/src/TiPopupModule.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +/** + * @ignore + */ +@NgModule({ + imports: [] +}) +export class TiPopupModule {} diff --git a/src/popup/lib/src/TiPopupService.ts b/src/popup/lib/src/TiPopupService.ts new file mode 100644 index 0000000..1b95196 --- /dev/null +++ b/src/popup/lib/src/TiPopupService.ts @@ -0,0 +1,296 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/* eslint-disable no-param-reassign */ +/** + * 该类提供一个服务,用于管理弹出类组件的创建和销毁 + * 服务中提供三个方法: + * create(componentType) 生成一个popup实例并返回对象, + * componentType:包裹内容的父容器元素类 + * 返回的实例对象中提供方法: + * { + * show({ // 生成元素并在指定容器中显示 + * content:弹出组件内容 + * context:弹出组件上下文 + * container:弹出组件最终放置的容器位置 + * }) : componentRef // 返回生成的组件实例 + * hide():隐藏并销毁元素 + * } + */ +import { + ApplicationRef, + ComponentFactoryResolver, + ComponentRef, + ElementRef, + EmbeddedViewRef, + Injectable, + Injector, + Renderer2, + RendererFactory2, + SecurityContext, + TemplateRef, + ViewRef, + Inject +} from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { DomSanitizer } from '@angular/platform-browser'; + +import { TiPopupModule } from './TiPopupModule'; +import { Util } from '@opentiny/ng-utils'; +/** + * @ignore + * 类型中any代表组件形式 + */ +export type TiContentType = string | TemplateRef | any; +/** + * @ignore + * popup show方法配置 + */ +interface TiPopUpShowConfig { + content?: TiContentType; // 弹出组件内容 + context?: any; // 弹出组件上下文 + contentContext?: any; // 内容部分的组件上下文 + contentParentInjector?: Injector; // 内容部分的组件父级注入器 + container?: any; // 弹出组件最终放置的容器位置 +} +/** + * @ignore + * popup create返回值 + */ +export interface TiPopUpRef { + show(config: TiPopUpShowConfig | {}): ComponentRef; + hide(): void; +} +/** + * @ignore + */ +class ContentRef { + constructor(public nodes: Array, public viewRef?: ViewRef, public componentRef?: ComponentRef) {} +} +/** + * @ignore + */ +@Injectable({ + providedIn: TiPopupModule +}) +export class TiPopupService { + /** + * 由于该服务可能被其他服务使用到,并以服务的形式提供给外部; + * 而Renderer2本身不能脱离于component之外定义或依赖,所以使用RendererFactory2方式实例化进行处理 + * 具体说明见如下: + * https://stackoverflow.com/questions/43070308/using-renderer-in-angular-4 + */ + private renderer: Renderer2; + constructor( + private injector: Injector, + rendererFactory: RendererFactory2, + private componentFactoryResolver: ComponentFactoryResolver, + private applicationRef: ApplicationRef, + private domSanitizer: DomSanitizer, + @Inject(DOCUMENT) private document + ) { + this.renderer = rendererFactory.createRenderer(null, null); + } + + private getParentEle(containerOpt: string | ElementRef): Element { + if (containerOpt instanceof ElementRef) { + // 父元素为正常element元素情况下 + return containerOpt.nativeElement; + } else if (containerOpt === 'body') { + // 父容器为body元素情况下 + return this.document.body; + } + } + + // 销毁组件相关内容 + private static destroyComponentRef(_componentRef: ComponentRef): void { + if (!_componentRef) { + return; + } + // 销毁组件实例 + _componentRef.destroy(); + _componentRef = null; + } + + // 销毁弹出内容相关内容 + private static destroyContentRef(contentRef: ContentRef): void { + // 弹出内容如果有组件实例对象时,销毁组件实例 + if (contentRef.componentRef) { + contentRef.componentRef.destroy(); + } + // 销毁页面视图实例 + if (contentRef.viewRef) { + contentRef.viewRef.destroy(); + } + // 销毁内容实例 + contentRef = null; + } + public create(componentType: any): TiPopUpRef { + let _contentRef: ContentRef; // 弹出内容实例 + let _componentRef: ComponentRef | any; // 生成的组件实例对象 + + return { + show: (options?: TiPopUpShowConfig): ComponentRef => { + // component已生成情况下,不再重复生成 + if (_componentRef) { + return _componentRef; + } + // 获取内容相关信息,包括:组件节点信息、组件实例信息、组件最小视图信息 + _contentRef = this.getContentRef( + options && options.content, + options && options.contentContext, + options && options.contentParentInjector + ); + + // 创建组件实例:为内容本身再包一层父容器组件,用于控制组件(componentType) + _componentRef = this.createCompoentRef({ + componentType, + nodes: _contentRef.nodes, + context: options && options.context + }); + + // 将元素放置在指定容器中 + const parentEle: Element = this.getParentEle(options && options.container); + if (parentEle) { + parentEle.appendChild(_componentRef.location.nativeElement); + } + + // 这时弹出内容已经append,可以对弹出内容进行解析了。 + // 弹出内容中某些元素会在初始化时需要获取自身DOM宽高等,所以要保证弹出内容先append,然后再解析。 + // 解析ng-template形式的弹出内容实例 + if (options.content instanceof TemplateRef) { + // 确保数据变化均可以被检测到 + _contentRef.viewRef.markForCheck(); + // 执行一次变化检测 + _contentRef.viewRef.detectChanges(); + } + + // 使用trycatch是为了组件实例化时产生错误控制台报错)的情况下,后续逻辑能够继续执行(不阻塞)。 + // 主要是为了弹框内容传入的是自定义组件 Component 情况下,当点击按钮打开弹窗,自定义的组件实例化时 + // 产生错误(控制台报错)的情况下,弹窗能正常的打开,且能正常的关闭。 + try { + // 解析component形式的弹出内容实例 + if (typeof options.content === 'function') { + // 在组件的 metadata 中如果设置了OnPush 条件,那么变化检测不会再次执行, + // 但是调用该方法,可以确保数据变化被检测到 + _contentRef.componentRef.changeDetectorRef.markForCheck(); + // 从该组件到其子组件执行一次变化检测 + _contentRef.componentRef.changeDetectorRef.detectChanges(); + } + } catch (error) { + console.error(error); + } + + _componentRef.tiContentRef = _contentRef; + + return _componentRef; + }, + hide: (): void => { + if (!_componentRef) { + return; + } + TiPopupService.destroyComponentRef(_componentRef); + TiPopupService.destroyContentRef(_contentRef); + // 由于该变量在destroyComponentRef函数中赋为null不会改变外部值的改变,导致下次show时判断错误,因此此处需要做处理 + _componentRef = null; + } + }; + } + + private getContentRef = (content: TiContentType, context?: any, contentParentInjector?: Injector): ContentRef => { + // ng-template形式 + if (content instanceof TemplateRef) { + // 将template实例化为内嵌视图,并将其放在可运行环境中进行解析 + const embeddedViewRef: EmbeddedViewRef = content.createEmbeddedView({ + context + }); + this.applicationRef.attachView(embeddedViewRef); // 不做此处处理,ng-template中的标签不会解析 + + return new ContentRef([embeddedViewRef.rootNodes], embeddedViewRef); + } + // 组件形式 + if (typeof content === 'function') { + // 根据传入的component类(即content)创建组件引用 + const componentRef: ComponentRef = this.createCompoentRef({ + componentType: content, + context, + notDetectChanges: true, + parentInjector: contentParentInjector + }); + + return new ContentRef([[componentRef.location.nativeElement]], componentRef.hostView, componentRef); + } + // element Dom形式 + if (content instanceof HTMLElement) { + return new ContentRef([[content]]); + } + + // string形式 或 SafeHtml(object类型) 形式, + if (typeof content === 'string' || typeof content === 'object') { + const wrapEle: Element = this.renderer.createElement('div'); + wrapEle.innerHTML = this.domSanitizer.sanitize(SecurityContext.HTML, content); + + return new ContentRef([wrapEle.childNodes]); + } + + return new ContentRef([]); + }; + /** + * 创建组件实例 + * options { + * componentType: 组件类 + * nodes:组件中的可注入节点 + * context: 组件属性配置,inputs属性均可在此配置 + * } + */ + public createCompoentRef(options: { + componentType?: any; + nodes?: Array; + context?: { outputs?: Object; [propName: string]: any }; + notDetectChanges?: boolean; + parentInjector?: Injector; + }): ComponentRef { + const injector: Injector = Injector.create({ + parent: options.parentInjector || this.injector, + providers: [] + }); + // 1. 根据component类创建组件引用 + const componentRef: ComponentRef = this.componentFactoryResolver + .resolveComponentFactory(options.componentType) + .create(injector, options.nodes); + + // 2. 将组件绑定在ng component树上,不做绑定情况下,内容中的指令无法解析 + this.applicationRef.attachView(componentRef.hostView); + + // 3. 绑定组件上下文定义 + Object.assign(componentRef.instance, options.context); + // outputs事件绑定 + if (options.context && !Util.isUndefined(options.context.outputs)) { + for (const key in options.context.outputs) { + if (Object.prototype.hasOwnProperty.call(options.context.outputs, key)) { + componentRef.instance[key].subscribe(options.context.outputs[key]); + } + } + } + + // 4. 确保组件视图根据数据能实时刷新上。 + // 通过该处理可以确保组件及子组件都完成解析 + if (!options.notDetectChanges) { + // 在组件的 metadata 中如果设置了OnPush 条件,那么变化检测不会再次执行, + // 但是调用该方法,可以确保数据变化被检测到 + componentRef.changeDetectorRef.markForCheck(); + // 从该组件到其子组件执行一次变化检测 + componentRef.changeDetectorRef.detectChanges(); + } + + return componentRef; + } +} diff --git a/src/productpreview/demo/project.json b/src/productpreview/demo/project.json new file mode 100644 index 0000000..6806e8b --- /dev/null +++ b/src/productpreview/demo/project.json @@ -0,0 +1,86 @@ +{ + "projectType": "application", + "root": "src/productpreview/demo", + "sourceRoot": "src/productpreview/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/productpreview", + "index": "src/productpreview/demo/src/index.html", + "main": "src/productpreview/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/productpreview/demo/tsconfig.app.json", + "assets": [ + "src/productpreview/demo/src/favicon.ico", + "src/productpreview/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/image", + "output": "/assets/image/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "productpreview-demo:build:production" + }, + "development": { + "browserTarget": "productpreview-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js productpreview" + } + ] + } + } + } +} diff --git a/src/productpreview/demo/src/app/AppComponent.ts b/src/productpreview/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/productpreview/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/productpreview/demo/src/app/AppModule.ts b/src/productpreview/demo/src/app/AppModule.ts new file mode 100644 index 0000000..ffafda7 --- /dev/null +++ b/src/productpreview/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ProductpreviewTestModule } from './productpreview/ProductpreviewTestModule'; + +@NgModule({ + imports: [ + ProductpreviewTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/productpreview/demo/src/app/IndexComponent.ts b/src/productpreview/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..9e9d368 --- /dev/null +++ b/src/productpreview/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ProductpreviewTestModule } from './productpreview/ProductpreviewTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = ProductpreviewTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/productpreview/demo/src/app/app.html b/src/productpreview/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/productpreview/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/productpreview/demo/src/app/productpreview/ProductpreviewBasicComponent.ts b/src/productpreview/demo/src/app/productpreview/ProductpreviewBasicComponent.ts new file mode 100644 index 0000000..2f03dca --- /dev/null +++ b/src/productpreview/demo/src/app/productpreview/ProductpreviewBasicComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiFilePreviewInfo } from '@opentiny/ng'; + +@Component({ + templateUrl: './productpreview-basic.html' +}) +export class ProductpreviewBasicComponent { + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL']; + fileList: Array = [ + { + previewUrl: `${this.baseUrl}assets/image/1.jpg` + }, + { + previewUrl: `${this.baseUrl}assets/image/2.jpg` + }, + { + previewUrl: `${this.baseUrl}assets/image/3.jpg` + }, + { + previewUrl: `${this.baseUrl}assets/image/1.jpg` + }, + { + previewUrl: `${this.baseUrl}assets/image/2.jpg` + }, + { + previewUrl: `${this.baseUrl}assets/image/3.jpg` + } + ]; +} diff --git a/src/productpreview/demo/src/app/productpreview/ProductpreviewTestModule.ts b/src/productpreview/demo/src/app/productpreview/ProductpreviewTestModule.ts new file mode 100644 index 0000000..2cb4feb --- /dev/null +++ b/src/productpreview/demo/src/app/productpreview/ProductpreviewTestModule.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { TiProductpreviewModule } from '@opentiny/ng'; + +import { ProductpreviewBasicComponent } from './ProductpreviewBasicComponent'; + +@NgModule({ + imports: [CommonModule, TiProductpreviewModule, RouterModule.forChild(ProductpreviewTestModule.ROUTES)], + declarations: [ProductpreviewBasicComponent] +}) +export class ProductpreviewTestModule { + public static readonly ROUTES: Routes = [{ path: 'productpreview/productpreview-basic', component: ProductpreviewBasicComponent }]; +} diff --git a/src/productpreview/demo/src/app/productpreview/productpreview-basic.html b/src/productpreview/demo/src/app/productpreview/productpreview-basic.html new file mode 100644 index 0000000..686e9c5 --- /dev/null +++ b/src/productpreview/demo/src/app/productpreview/productpreview-basic.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/productpreview/demo/src/app/productpreview/webdoc/productpreview-demos.js b/src/productpreview/demo/src/app/productpreview/webdoc/productpreview-demos.js new file mode 100644 index 0000000..1d9c8dc --- /dev/null +++ b/src/productpreview/demo/src/app/productpreview/webdoc/productpreview-demos.js @@ -0,0 +1,17 @@ +export default { + column: '2', + demos: [ + { + demoId: 'productpreview-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': '

    Productproview 组件的最简用法。

    ', + 'en-US': 'Basic usage' + }, + apis: ['TiProductpreviewComponent.properties.files'] + } + ] +}; diff --git a/src/productpreview/demo/src/app/productpreview/webdoc/productpreview.cn.md b/src/productpreview/demo/src/app/productpreview/webdoc/productpreview.cn.md new file mode 100644 index 0000000..b1a40d2 --- /dev/null +++ b/src/productpreview/demo/src/app/productpreview/webdoc/productpreview.cn.md @@ -0,0 +1,32 @@ +--- +title: Productpreview 商品预览 +--- + +# Productpreview 商品预览 + +
    + +Productpreview 用来展示商品的预览内容。   + ++ 商品预览组件,响应式尺寸。 ++ 屏幕分辨率>=1600,尺寸450px。 ++ 1600>屏幕分辨率>=1440,尺寸380px。 ++ 1440>屏幕分辨率>=1280,分辨率>1280,尺寸300px。 + +```typescript +import { TiProductpreviewModule } from '@opentiny/ng'; +``` + +
    + +
    + +Productpreview 商品预览组件。   + +```typescript +import { TiProductpreviewModule } from '@opentiny/ng'; +``` + +
    + + diff --git a/src/productpreview/demo/src/app/productpreview/webdoc/productpreview.en.md b/src/productpreview/demo/src/app/productpreview/webdoc/productpreview.en.md new file mode 100644 index 0000000..5f58dfe --- /dev/null +++ b/src/productpreview/demo/src/app/productpreview/webdoc/productpreview.en.md @@ -0,0 +1,13 @@ +--- +title: Productpreview +--- + +# Productpreview + +
    + +```typescript +import { TiProductpreviewModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/productpreview/demo/src/favicon.ico b/src/productpreview/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/productpreview/demo/src/index.html b/src/productpreview/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/productpreview/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/productpreview/demo/src/main.ts b/src/productpreview/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/productpreview/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/productpreview/demo/tsconfig.app.json b/src/productpreview/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/productpreview/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/productpreview/lib/index.ts b/src/productpreview/lib/index.ts new file mode 100644 index 0000000..d4db45b --- /dev/null +++ b/src/productpreview/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiProductpreviewModule'; +export * from './src/TiProductpreviewComponent'; diff --git a/src/productpreview/lib/ng-package.json b/src/productpreview/lib/ng-package.json new file mode 100644 index 0000000..bd96695 --- /dev/null +++ b/src/productpreview/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/productpreview", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/productpreview/lib/package.json b/src/productpreview/lib/package.json new file mode 100644 index 0000000..bd6771d --- /dev/null +++ b/src/productpreview/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-productpreview", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@opentiny/ng-imagepreview": "~1.0.0-beta.0", + "@opentiny/ng-modal": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-zoom": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/productpreview/lib/project.json b/src/productpreview/lib/project.json new file mode 100644 index 0000000..7d8414b --- /dev/null +++ b/src/productpreview/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/productpreview/lib", + "sourceRoot": "src/productpreview/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/productpreview"], + "options": { + "project": "src/productpreview/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/productpreview"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js productpreview" + }, + { + "command": "ng default-build productpreview" + }, + { + "command": "node build/clear-default-theme.js productpreview" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/productpreview && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build productpreview && ng pack productpreview && node build/publish.js productpreview --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/productpreview/lib/src/TiProductpreviewComponent.ts b/src/productpreview/lib/src/TiProductpreviewComponent.ts new file mode 100644 index 0000000..3917e5a --- /dev/null +++ b/src/productpreview/lib/src/TiProductpreviewComponent.ts @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + OnChanges, + OnInit, + Renderer2, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { TiFilePreviewInfo, TiImagepreviewComponent } from '@opentiny/ng-imagepreview'; +import { TiModalService } from '@opentiny/ng-modal'; +import { TiBaseComponent } from '@opentiny/ng-base'; + +@Component({ + selector: 'ti-productpreview', + templateUrl: './productpreview.html', + styleUrls: ['./productpreview.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiProductpreviewComponent extends TiBaseComponent implements OnInit, OnChanges, AfterViewInit { + /** + * 每页显示的小图个数 + */ + private static readonly ITEM_PER_PAGE: number = 4; + + /** + * 预览文件列表 + */ + @Input() files: Array = []; + + /** + * @ignore + * 缩略图列表,翻页时重新设置 + */ + public thumbList: Array = []; + + /** + * @ignore 记录当前预览的商品索引 + */ + public currentPreviewIndex: number = -1; + + /** + * @ignore 缩略图当前页 + */ + public currentThumbPage: number = 1; + + /** + * @ignore 缩略图总页数 + * + */ + public totalThumbPage: number = 1; + + /** + * @ignore 内部变量 + */ + @ViewChild('productThumb') productThumbEle: ElementRef; + + constructor(private tiModal: TiModalService, private render: Renderer2, private hostEleRef: ElementRef) { + super(hostEleRef, render); + } + + ngAfterViewInit(): void { + this.render.addClass(this.productThumbEle.nativeElement.children[0], 'ti-product-preview-thumb-item-active'); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.files && changes.files.firstChange === false) { + this.files = changes.files.currentValue; + this.initThumb(); + if (this.productThumbEle) { + this.setThumbList(); + } + // 确保files接口变化时,第一个缩略图的选中样式生效 + setTimeout((): void => { + if (this.productThumbEle) { + this.render.addClass(this.productThumbEle.nativeElement.children[0], 'ti-product-preview-thumb-item-active'); + } + }, 0); + } + } + + ngOnInit(): void { + super.ngOnInit(); + this.initThumb(); + } + + /** + * 初始化缩略图相关变量 + */ + private initThumb(): void { + if (this.files && this.files.length > 0) { + this.currentPreviewIndex = 0; + // 计算总页数 + this.totalThumbPage = Math.ceil(this.files.length / TiProductpreviewComponent.ITEM_PER_PAGE); + this.currentThumbPage = 1; + if (this.files.length >= TiProductpreviewComponent.ITEM_PER_PAGE) { + this.thumbList = this.files.slice(0, TiProductpreviewComponent.ITEM_PER_PAGE); + } else { + this.thumbList = this.files; + } + } + } + + /** + * @ignore 主图查看 + * @param event: 事件 + * @param index: 当前选中项在this.thumbList中的索引,this.thumbList中始终只有4项 + */ + onMouseenterThumb(event: any, index: number): void { + this.currentPreviewIndex = (this.currentThumbPage - 1) * TiProductpreviewComponent.ITEM_PER_PAGE + index; + // 计算最后一页的缩略图个数 + const thumbItemNumber: number = this.files.length % TiProductpreviewComponent.ITEM_PER_PAGE; + // 如果是最后一页,且最后一页不满4个 + if (this.totalThumbPage > 1 && this.currentThumbPage === this.totalThumbPage && thumbItemNumber !== 0) { + this.currentPreviewIndex -= TiProductpreviewComponent.ITEM_PER_PAGE - thumbItemNumber; + } + + // 设置选中样式 + const thumbItemEles: any = this.productThumbEle.nativeElement.children; + for (const item of thumbItemEles) { + this.render.removeClass(item, 'ti-product-preview-thumb-item-active'); + } + this.render.addClass(event.target.parentElement, 'ti-product-preview-thumb-item-active'); + } + + /** + * @ignore 弹框中预览大图 + */ + onClickMain(event: any): void { + this.tiModal.open(TiImagepreviewComponent, { + id: this.id + '_productPreviewModal', + modalClass: 'ti-product-preview-modal', + context: { + index: this.currentPreviewIndex, // 当前文件索引 + fileList: this.files, // 预览列表 + id: this.id // 传递id + } + }); + } + + /** + * @ignore 缩略图上一页 + */ + prev(): void { + if (this.currentThumbPage === 1) { + return; + } + this.currentThumbPage--; + this.setThumbList(); + } + + /** + * @ignore 缩略图下一页 + */ + next(): void { + if (this.currentThumbPage === this.totalThumbPage) { + return; + } + this.currentThumbPage++; + this.setThumbList(); + } + + /** + * 设置缩略图列表 + */ + private setThumbList(): void { + // undefined、null、false、string情况下,不处理 + if (!this.files || typeof this.files === 'string' || (this.files && this.files.slice === undefined)) { + return; + } + // tslint:disable-next-line: max-line-length + this.thumbList = this.files.slice( + (this.currentThumbPage - 1) * TiProductpreviewComponent.ITEM_PER_PAGE, + this.currentThumbPage * TiProductpreviewComponent.ITEM_PER_PAGE + ); + + // 计算最后一页的缩略图个数 + const thumbItemNumber: number = this.files.length % TiProductpreviewComponent.ITEM_PER_PAGE; + if (this.totalThumbPage > 1 && this.currentThumbPage === this.totalThumbPage && thumbItemNumber !== 0) { + this.thumbList = this.files.slice(this.files.length - TiProductpreviewComponent.ITEM_PER_PAGE, this.files.length); + } + } +} diff --git a/src/productpreview/lib/src/TiProductpreviewModule.ts b/src/productpreview/lib/src/TiProductpreviewModule.ts new file mode 100644 index 0000000..1726b12 --- /dev/null +++ b/src/productpreview/lib/src/TiProductpreviewModule.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiImagepreviewModule } from '@opentiny/ng-imagepreview'; +import { TiModalModule } from '@opentiny/ng-modal'; +import { TiZoomModule } from '@opentiny/ng-zoom'; +import { TiOutlineModule } from '@opentiny/ng-outline'; + +import { TiProductpreviewComponent } from './TiProductpreviewComponent'; + +@NgModule({ + imports: [CommonModule, TiImagepreviewModule, TiIconModule, TiModalModule, TiZoomModule, TiOutlineModule], + exports: [TiProductpreviewComponent], + declarations: [TiProductpreviewComponent] +}) +export class TiProductpreviewModule {} +export { TiProductpreviewComponent } from './TiProductpreviewComponent'; diff --git a/src/productpreview/lib/src/productpreview.html b/src/productpreview/lib/src/productpreview.html new file mode 100644 index 0000000..9f29f60 --- /dev/null +++ b/src/productpreview/lib/src/productpreview.html @@ -0,0 +1,48 @@ +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    diff --git a/src/productpreview/lib/src/productpreview.less b/src/productpreview/lib/src/productpreview.less new file mode 100644 index 0000000..a6d023d --- /dev/null +++ b/src/productpreview/lib/src/productpreview.less @@ -0,0 +1,157 @@ +@import '../../../themes/basic/base-all.less'; + +::ng-deep :root { + --ti-product-preview-modal-width: 640px; + --ti-product-preview-main-small-size: 300px; + --ti-product-preview-main-middle-size: 380px; + --ti-product-preview-main-large-size: 450px; + --ti-product-preview-thumb-small-size: 49px; + --ti-product-preview-thumb-middle-size: 69px; + --ti-product-preview-thumb-large-size: 86.5px; + --ti-product-preview-thumb-page-size: 24px; + --ti-product-preview-thumb-page-font-size: 24px; + --ti-product-preview-thumb-page-border-radius: 2px; + --ti-product-preview-thumb-page-margin: 16px; + --ti-product-preview-thumb-page-button-size: calc( + var(--ti-product-preview-thumb-page-size) + var(--ti-product-preview-thumb-page-margin) + ); +} + +:host { + display: block; +} +.ti-product-preview-container { + width: 100%; + .ti-product-preview-main { + display: flex; + justify-content: center; + align-items: center; + border: 1px solid var(--ti-common-color-line-dividing); + box-sizing: border-box; + } + .ti-product-preview-thumb-container { + margin-top: var(--ti-common-space-3x); + display: flex; + justify-content: space-between; + align-items: center; + .ti-product-preview-thumb-wapper { + flex: 1; + overflow: hidden; + .ti-product-preview-thumb { + position: relative; + left: 0; + display: flex; + .ti-product-preview-thumb-item { + display: flex; + justify-content: center; + align-items: center; + flex-shrink: 0; + border: 1px solid transparent; + box-sizing: border-box; + img { + width: auto; + height: auto; + } + &:not(:last-child) { + margin-right: var(--ti-common-space-2x); + } + } + .ti-product-preview-thumb-item-active { + border: 1px solid var(--ti-common-color-line-active); + cursor: pointer; + } + } + } + .ti-product-preview-thumb-page { + width: var(--ti-product-preview-thumb-page-size); + height: var(--ti-product-preview-thumb-page-size); + border-radius: var(--ti-product-preview-thumb-page-border-radius); + position: relative; + &:hover:not(.ti-product-preview-thumb-page-disabled) { + cursor: pointer; + color: var(--ti-common-color-icon-hover); + background-color: var(--ti-common-color-bg-light-normal); + } + &:active:not(.ti-product-preview-thumb-page-disabled) { + color: var(--ti-common-bg-minor-active); + background-color: var(--ti-common-color-icon-active); + } + .ti-product-preview-icon { + line-height: var(--ti-product-preview-thumb-page-size); + font-size: var(--ti-product-preview-thumb-page-font-size); + } + } + .ti-product-preview-thumb-page-disabled { + cursor: not-allowed; + outline: none; + color: var(--ti-common-color-icon-disabled); + } + .ti-product-preview-thumb-left { + margin-right: var(--ti-product-preview-thumb-page-margin); + } + .ti-product-preview-thumb-right { + margin-left: var(--ti-product-preview-thumb-page-margin); + } + } +} + +.media(@mainSize, @thumbSize) { + :host { + width: @mainSize; + } + ::ng-deep .ti3-img-zoom-viewer { + width: @mainSize !important; + height: @mainSize !important; + } + .ti-product-preview-container { + .ti-product-preview-main { + height: @mainSize; + img { + max-width: calc(@mainSize - 2px); + max-height: calc(@mainSize - 2px); + } + ::ng-deep .ti3-img-zoom-selector { + width: calc(@mainSize / 2) !important; + height: calc(@mainSize / 2) !important; + } + } + .ti-product-preview-thumb-container { + .ti-product-preview-thumb-wapper { + .ti-product-preview-thumb { + .ti-product-preview-thumb-item { + width: @thumbSize; + height: @thumbSize; + img { + max-width: calc(@thumbSize - 2px); + max-height: calc(@thumbSize - 2px); + } + } + } + } + } + } +} + +// 10.0.4版本新增 +// 分辨率<1440px +@media screen and (max-width: 1440px) { + .media(var(--ti-product-preview-main-small-size), var(--ti-product-preview-thumb-small-size)); +} +// 1440<=分辨率<1600px +@media screen and (min-width: 1440px) and (max-width: 1600px) { + .media(var(--ti-product-preview-main-middle-size), var(--ti-product-preview-thumb-middle-size)); +} +// 分辨率>=1600px +@media screen and (min-width: 1600px) { + .media(var(--ti-product-preview-main-large-size), var(--ti-product-preview-thumb-large-size)); +} + +::ng-deep .ti-product-preview-modal { + width: var(--ti-product-preview-modal-width) !important; + .ti3-modal-close { + top: 0; + } + .ti3-image-preview-container { + margin-top: var(--ti-common-space-2x); + } +} diff --git a/src/progressbar/demo/karma.conf.js b/src/progressbar/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/progressbar/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/progressbar/demo/project.json b/src/progressbar/demo/project.json new file mode 100644 index 0000000..85c627a --- /dev/null +++ b/src/progressbar/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/progressbar/demo", + "sourceRoot": "src/progressbar/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/progressbar", + "index": "src/progressbar/demo/src/index.html", + "main": "src/progressbar/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/progressbar/demo/tsconfig.app.json", + "assets": ["src/progressbar/demo/src/favicon.ico", "src/progressbar/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "progressbar-demo:build:production" + }, + "development": { + "browserTarget": "progressbar-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js progressbar" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/progressbar/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/progressbar/demo/tsconfig.spec.json", + "karmaConfig": "src/progressbar/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/progressbar/demo/src/app/AppComponent.ts b/src/progressbar/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/progressbar/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/progressbar/demo/src/app/AppModule.ts b/src/progressbar/demo/src/app/AppModule.ts new file mode 100644 index 0000000..6c0ba5e --- /dev/null +++ b/src/progressbar/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ProgressbarTestModule } from './progressbar/ProgressbarTestModule'; + +@NgModule({ + imports: [ + ProgressbarTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/progressbar/demo/src/app/IndexComponent.ts b/src/progressbar/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..879ac68 --- /dev/null +++ b/src/progressbar/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ProgressbarTestModule } from './progressbar/ProgressbarTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = ProgressbarTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/progressbar/demo/src/app/app.html b/src/progressbar/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/progressbar/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/progressbar/demo/src/app/progressbar/ProgressbarAnimationComponent.ts b/src/progressbar/demo/src/app/progressbar/ProgressbarAnimationComponent.ts new file mode 100644 index 0000000..0f4fb25 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/ProgressbarAnimationComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +const STEP: number = 10; +@Component({ + templateUrl: 'progressbar-animation.html' +}) +export class ProgressbarAnimationComponent { + value: number = 40; + max: number = 200; + isAnimation: boolean = false; + up(): void { + if (this.value + STEP <= this.max) { + this.value += STEP; + } else { + this.value = this.max; + } + } + down(): void { + if (this.value - STEP > 0) { + this.value -= STEP; + } else { + this.value = 0; + } + } + switchState(): void { + this.isAnimation = !this.isAnimation; + } +} diff --git a/src/progressbar/demo/src/app/progressbar/ProgressbarBasicComponent.ts b/src/progressbar/demo/src/app/progressbar/ProgressbarBasicComponent.ts new file mode 100644 index 0000000..14d8c0d --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/ProgressbarBasicComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: 'progressbar-basic.html' +}) +export class ProgressbarBasicComponent { + value: number = 40; + value1: number = 200; + value2: number = 0; + max: number = 200; +} diff --git a/src/progressbar/demo/src/app/progressbar/ProgressbarClassComponent.ts b/src/progressbar/demo/src/app/progressbar/ProgressbarClassComponent.ts new file mode 100644 index 0000000..fb8c8c3 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/ProgressbarClassComponent.ts @@ -0,0 +1,32 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +const STEP: number = 10; + +@Component({ + templateUrl: './progressbar-class.html', + styleUrls: ['./progressbar-class.less'], + encapsulation: ViewEncapsulation.None +}) +export class ProgressbarClassComponent { + value: number = 80; + value1: number = 50; + max: number = 200; + up(): void { + if (this.value + STEP <= this.max) { + this.value += STEP; + } else { + this.value = this.max; + } + } + down(): void { + if (this.value - STEP > 0) { + this.value -= STEP; + } else { + this.value = 0; + } + } + ngOnInit(): void { + setTimeout(() => { + this.value1 += 100; + }, 500); + } +} diff --git a/src/progressbar/demo/src/app/progressbar/ProgressbarTestModule.ts b/src/progressbar/demo/src/app/progressbar/ProgressbarTestModule.ts new file mode 100644 index 0000000..7a6d19a --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/ProgressbarTestModule.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { TiButtonModule, TiProgressbarModule } from '@opentiny/ng'; +import { ProgressbarBasicComponent } from './ProgressbarBasicComponent'; +import { ProgressbarClassComponent } from './ProgressbarClassComponent'; +import { ProgressbarAnimationComponent } from './ProgressbarAnimationComponent'; + +@NgModule({ + imports: [CommonModule, TiProgressbarModule, TiButtonModule, RouterModule.forChild(ProgressbarTestModule.ROUTES)], + declarations: [ProgressbarBasicComponent, ProgressbarClassComponent, ProgressbarAnimationComponent] +}) +export class ProgressbarTestModule { + static readonly LINKS: Array = [{ href: 'components/TiProgressbarComponent.html', label: 'Progressbar' }]; + static readonly ROUTES: Routes = [ + { + path: 'progressbar/progressbar-basic', + component: ProgressbarBasicComponent, + data: { label: '基本使用' } + }, + { + path: 'progressbar/progressbar-class', + component: ProgressbarClassComponent, + data: { label: '样式设置' } + }, + { + path: 'progressbar/progressbar-animation', + component: ProgressbarAnimationComponent, + data: { label: '动效' } + } + ]; +} diff --git a/src/progressbar/demo/src/app/progressbar/progressbar-animation.html b/src/progressbar/demo/src/app/progressbar/progressbar-animation.html new file mode 100644 index 0000000..a867552 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/progressbar-animation.html @@ -0,0 +1,9 @@ +
    +
    + +
    + {{ 100 * value / max }}% +
    + + + diff --git a/src/progressbar/demo/src/app/progressbar/progressbar-basic.html b/src/progressbar/demo/src/app/progressbar/progressbar-basic.html new file mode 100644 index 0000000..740a101 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/progressbar-basic.html @@ -0,0 +1,18 @@ +
    +
    + +
    + {{ 100 * value / max }}% +
    +
    +
    + +
    + {{ 100 * value2 / max }}% +
    +
    +
    + +
    + {{ 100 * value1 / max }}% +
    diff --git a/src/progressbar/demo/src/app/progressbar/progressbar-class.html b/src/progressbar/demo/src/app/progressbar/progressbar-class.html new file mode 100644 index 0000000..c85d949 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/progressbar-class.html @@ -0,0 +1,16 @@ +
    +
    + + +
    + {{100 * value / max }}% +
    + + +
    +
    + + +
    + {{100 * value1 / max }}% +
    diff --git a/src/progressbar/demo/src/app/progressbar/progressbar-class.less b/src/progressbar/demo/src/app/progressbar/progressbar-class.less new file mode 100644 index 0000000..37302a5 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/progressbar-class.less @@ -0,0 +1,52 @@ +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + + to { + background-position: 0 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + + to { + background-position: 0 0; + } +} + +.container .inline-block .custom-progress-active-warn-bg { + background-image: linear-gradient( + 45deg, + #96adfa 25%, + transparent 25%, + transparent 50%, + #96adfa 50%, + #96adfa 75%, + transparent 75%, + transparent + ); + animation: progress-bar-stripes 1s linear infinite; + background-size: 40px 40px; + background-color: #7693f5; + -webkit-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.container .inline-block .custom-progress-warn-bg { + background-color: #f0ad4e; +} diff --git a/src/progressbar/demo/src/app/progressbar/webdoc/progressbar-demos.js b/src/progressbar/demo/src/app/progressbar/webdoc/progressbar-demos.js new file mode 100644 index 0000000..461a589 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/webdoc/progressbar-demos.js @@ -0,0 +1,48 @@ +export default { + column: '2', + demos: [ + { + demoId: 'progressbar-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    Progressbar 组件的最简用法。通过属性max配置边界值。通过属性value配置当前进度值。

    ', + 'en-US': + '

    Progressbar 组件的最简用法。通过属性max配置边界值。通过属性value配置当前进度值。

    ', + }, + apis: [ + 'TiProgressbarComponent.properties.max', + 'TiProgressbarComponent.properties.value', + ], + }, + { + demoId: 'progressbar-class', + name: { + 'zh-CN': '样式设置', + 'en-US': 'Class', + }, + desc: { + 'zh-CN': '

    通过属性progressClass配置进度条样式。

    ', + 'en-US': '

    通过属性progressClass配置进度条样式。

    ', + }, + apis: ['TiProgressbarComponent.properties.progressClass'], + }, + { + demoId: 'progressbar-animation', + name: { + 'zh-CN': '动画', + 'en-US': 'Animation', + }, + desc: { + 'zh-CN': + '

    通过属性animation配置是否打开动画效果,默认开启。

    ', + 'en-US': + '

    通过属性animation配置是否打开动画效果,默认开启。

    ', + }, + apis: ['TiProgressbarComponent.properties.animation'], + }, + ], +}; diff --git a/src/progressbar/demo/src/app/progressbar/webdoc/progressbar.cn.md b/src/progressbar/demo/src/app/progressbar/webdoc/progressbar.cn.md new file mode 100644 index 0000000..57416b9 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/webdoc/progressbar.cn.md @@ -0,0 +1,24 @@ +--- +title: Progressbar 进度条 +--- +# Progressbar 进度条 + +
    + +Progressbar 是展示当前进度的组件。 + +```typescript +import { TiProgressbarModule } from '@opentiny/ng'; +``` + +
    + +
    + +Progressbar 是展示当前进度的组件。 + +```typescript +import { TiProgressbarModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/progressbar/demo/src/app/progressbar/webdoc/progressbar.en.md b/src/progressbar/demo/src/app/progressbar/webdoc/progressbar.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/progressbar/demo/src/app/progressbar/webdoc/progressbar.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/progressbar/demo/src/favicon.ico b/src/progressbar/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/progressbar/demo/src/index.html b/src/progressbar/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/progressbar/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/progressbar/demo/src/main.ts b/src/progressbar/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/progressbar/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/progressbar/demo/test.ts b/src/progressbar/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/progressbar/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/progressbar/demo/tsconfig.app.json b/src/progressbar/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/progressbar/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/progressbar/demo/tsconfig.spec.json b/src/progressbar/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/progressbar/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/progressbar/lib/index.ts b/src/progressbar/lib/index.ts new file mode 100644 index 0000000..2c297a9 --- /dev/null +++ b/src/progressbar/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiProgressbarModule'; diff --git a/src/progressbar/lib/ng-package.json b/src/progressbar/lib/ng-package.json new file mode 100644 index 0000000..ccd2053 --- /dev/null +++ b/src/progressbar/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/progressbar", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/progressbar/lib/package.json b/src/progressbar/lib/package.json new file mode 100644 index 0000000..fd256b0 --- /dev/null +++ b/src/progressbar/lib/package.json @@ -0,0 +1,10 @@ +{ + "name": "@opentiny/ng-progressbar", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/progressbar/lib/project.json b/src/progressbar/lib/project.json new file mode 100644 index 0000000..be0dfb4 --- /dev/null +++ b/src/progressbar/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/progressbar/lib", + "sourceRoot": "src/progressbar/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/progressbar"], + "options": { + "project": "src/progressbar/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/progressbar"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js progressbar" + }, + { + "command": "ng default-build progressbar" + }, + { + "command": "node build/clear-default-theme.js progressbar" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/progressbar && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build progressbar && ng pack progressbar && node build/publish.js progressbar --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/progressbar/lib/src/TiProgressbarComponent.ts b/src/progressbar/lib/src/TiProgressbarComponent.ts new file mode 100644 index 0000000..0400cb4 --- /dev/null +++ b/src/progressbar/lib/src/TiProgressbarComponent.ts @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, SimpleChanges, ChangeDetectionStrategy } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * 进度条组件 + * + */ +@Component({ + selector: 'ti-progressbar', + templateUrl: './progressbar.html', + styleUrls: ['./progressbar.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-progress]': 'true' + } +}) +export class TiProgressbarComponent extends TiBaseComponent { + /** + * 当前进度值 + */ + @Input() value: number = 0; + /** + * 进度目标值 + */ + @Input() max: number = 100; + /** + * 进度条样式 + */ + @Input() progressClass: string = ''; + /** + * 进度变化时是否开启动画效果过渡 + */ + @Input() animation: boolean = true; + /** + * @ignore + */ + public percent: number; // 当前进度值 + protected versionInfo: string = super.getVersion(packageInfo); + // 设置合法的数值 + private static verifyNum(newVal: any, defaultValue: any): number { + return isNaN(parseFloat(newVal)) ? defaultValue : newVal; + } + ngOnInit(): void { + super.ngOnInit(); + this.max = TiProgressbarComponent.verifyNum(this.max, this.max); + this.value = TiProgressbarComponent.verifyNum(this.value, this.value); + this.calcPercentage(); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + // 当前值更新,重新计算进度 + this.setChanges(changes, 'value'); + // 当最大值更新,重新计算进度 + this.setChanges(changes, 'max'); + } + + /** + * @description 处理用户输入的改变 + * @param: changes 改变的对象 + * @param: changeKey 那个属性改变 + */ + private setChanges(changes: SimpleChanges, changeKey: string): void { + if (changes[changeKey] && !changes[changeKey].isFirstChange()) { + const oldVal: any = changes[changeKey].previousValue; + const newVal: any = changes[changeKey].currentValue; + const _newValue: number = TiProgressbarComponent.verifyNum(newVal, oldVal); + this[changeKey] = _newValue; + this.calcPercentage(); + } + } + + // 计算当前进度百分比 + private calcPercentage(): void { + // 如果开发者设置数据不合理,则进度置0 + if (this.max === 0) { + this.percent = 0; + + return; + } + this.percent = Number(((this.value * 100) / this.max).toFixed(2)); + if (this.percent > 100) { + this.percent = 100; + } + if (this.percent < 0) { + this.percent = 0; + } + } +} diff --git a/src/progressbar/lib/src/TiProgressbarModule.ts b/src/progressbar/lib/src/TiProgressbarModule.ts new file mode 100644 index 0000000..deabed9 --- /dev/null +++ b/src/progressbar/lib/src/TiProgressbarModule.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiProgressbarComponent } from './TiProgressbarComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [TiProgressbarComponent], + declarations: [TiProgressbarComponent] +}) +export class TiProgressbarModule {} +export { TiProgressbarComponent } from './TiProgressbarComponent'; diff --git a/src/progressbar/lib/src/progressbar.html b/src/progressbar/lib/src/progressbar.html new file mode 100644 index 0000000..76de30a --- /dev/null +++ b/src/progressbar/lib/src/progressbar.html @@ -0,0 +1,9 @@ +
    +
    + +
    diff --git a/src/progressbar/lib/src/progressbar.less b/src/progressbar/lib/src/progressbar.less new file mode 100644 index 0000000..ed6f49b --- /dev/null +++ b/src/progressbar/lib/src/progressbar.less @@ -0,0 +1,54 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-progressbar-height: 5px; +} +:host.ti3-progress { + .box-sizing(border-box); + display: inline-block; + position: relative; + background-color: var(--ti-common-color-line-dividing); + width: 100%; + vertical-align: middle; + overflow: visible; + height: var(--ti-progressbar-height); + margin-bottom: 0; +} + +.ti3-progress-bar { + .box-sizing(border-box); + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 0; + background-color: var(--ti-common-color-bg-emphasize); + .transition(width 0.6s ease); +} + +.ti3-progress-label { + display: none; + .box-sizing(border-box); + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 100%; + background: transparent; + white-space: nowrap; + color: var(--ti-common-color-text-white); + text-align: center; + vertical-align: middle; + &:after { + display: inline-block; + width: 0; + height: 100%; + vertical-align: middle; + content: ''; + } +} + +.ti3-progress-bar.ti3-progress-no-animation { + transition: none !important; + animation: none !important; +} diff --git a/src/progresspie/demo/project.json b/src/progresspie/demo/project.json new file mode 100644 index 0000000..eefe26e --- /dev/null +++ b/src/progresspie/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/progresspie/demo", + "sourceRoot": "src/progresspie/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/progresspie", + "index": "src/progresspie/demo/src/index.html", + "main": "src/progresspie/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/progresspie/demo/tsconfig.app.json", + "assets": ["src/progresspie/demo/src/favicon.ico", "src/progresspie/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "progresspie-demo:build:production" + }, + "development": { + "browserTarget": "progresspie-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js progresspie" + } + ] + } + } + } +} diff --git a/src/progresspie/demo/src/app/AppComponent.ts b/src/progresspie/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/progresspie/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/progresspie/demo/src/app/AppModule.ts b/src/progresspie/demo/src/app/AppModule.ts new file mode 100644 index 0000000..aa14b4e --- /dev/null +++ b/src/progresspie/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ProgresspieTestModule } from './progresspie/ProgresspieTestModule'; + +@NgModule({ + imports: [ + ProgresspieTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/progresspie/demo/src/app/IndexComponent.ts b/src/progresspie/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..5471f85 --- /dev/null +++ b/src/progresspie/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ProgresspieTestModule } from './progresspie/ProgresspieTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = ProgresspieTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/progresspie/demo/src/app/app.html b/src/progresspie/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/progresspie/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/progresspie/demo/src/app/progresspie/ProgresspieTestComponent.ts b/src/progresspie/demo/src/app/progresspie/ProgresspieTestComponent.ts new file mode 100644 index 0000000..8fd3618 --- /dev/null +++ b/src/progresspie/demo/src/app/progresspie/ProgresspieTestComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; + +@Component({ + template: ` +

    1. 不设置value和maxValue,均使用默认值

    + +

    2. 不设置maxValue,maxValue使用默认值;value设置为20

    + +

    3. 同时设置value(10)和maxValue(200)

    + + + + + + ` +}) +export class ProgresspieTestComponent { + value1: number; + value2: number = 20; + value3: number = 10; + maxValue3: number = 200; + valueChanged: string; + changeValue(valueChanged: string, valueIndex: string): void { + this[valueIndex] = parseInt(valueChanged, 10); + } +} diff --git a/src/progresspie/demo/src/app/progresspie/ProgresspieTestModule.ts b/src/progresspie/demo/src/app/progresspie/ProgresspieTestModule.ts new file mode 100644 index 0000000..ea0b9cc --- /dev/null +++ b/src/progresspie/demo/src/app/progresspie/ProgresspieTestModule.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiProgresspieModule } from '@opentiny/ng'; + +import { ProgresspieTestComponent } from './ProgresspieTestComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiProgresspieModule, RouterModule.forChild(ProgresspieTestModule.ROUTES)], + declarations: [ProgresspieTestComponent] +}) +export class ProgresspieTestModule { + static readonly LINKS: Array = [{ href: 'components/TiProgresspieComponent.html', label: 'Progresspie' }]; + static readonly ROUTES: Routes = [ + { + path: 'progresspie/progresspie-basic', + component: ProgresspieTestComponent + } + ]; +} diff --git a/src/progresspie/demo/src/favicon.ico b/src/progresspie/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/progresspie/demo/src/index.html b/src/progresspie/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/progresspie/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/progresspie/demo/src/main.ts b/src/progresspie/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/progresspie/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/progresspie/demo/tsconfig.app.json b/src/progresspie/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/progresspie/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/progresspie/lib/index.ts b/src/progresspie/lib/index.ts new file mode 100644 index 0000000..1c04e93 --- /dev/null +++ b/src/progresspie/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiProgresspieModule'; diff --git a/src/progresspie/lib/ng-package.json b/src/progresspie/lib/ng-package.json new file mode 100644 index 0000000..e9da007 --- /dev/null +++ b/src/progresspie/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/progresspie", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/progresspie/lib/package.json b/src/progresspie/lib/package.json new file mode 100644 index 0000000..06e3288 --- /dev/null +++ b/src/progresspie/lib/package.json @@ -0,0 +1,8 @@ +{ + "name": "@opentiny/ng-progresspie", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/progresspie/lib/project.json b/src/progresspie/lib/project.json new file mode 100644 index 0000000..88e228a --- /dev/null +++ b/src/progresspie/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/progresspie/lib", + "sourceRoot": "src/progresspie/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/progresspie"], + "options": { + "project": "src/progresspie/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/progresspie"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js progresspie" + }, + { + "command": "ng default-build progresspie" + }, + { + "command": "node build/clear-default-theme.js progresspie" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/progresspie && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build progresspie && ng pack progresspie && node build/publish.js progresspie --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/progresspie/lib/src/TiProgresspieComponent.ts b/src/progresspie/lib/src/TiProgresspieComponent.ts new file mode 100644 index 0000000..e84203b --- /dev/null +++ b/src/progresspie/lib/src/TiProgresspieComponent.ts @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterViewInit, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild, ChangeDetectionStrategy } from '@angular/core'; +/** + * @ignore + */ +@Component({ + selector: 'ti-progresspie', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiProgresspieComponent implements OnChanges, AfterViewInit { + private defaultConfig: any = { + value: 0, + maxValue: 100, + color: '#3dcca6', + lineWidth: 2 + }; + @Input() value: number = this.defaultConfig.value; + @Input() maxValue: number = this.defaultConfig.maxValue; + private percent: number; // 计算后的百分比 + @ViewChild('canvas', { static: true }) private canvasEle: ElementRef; // canvas元素对应的ElementRef + private canvasElement: any; // canvas元素对应的nativeElement + nativeElement: Element; // 元素本身 + constructor(private hostEle: ElementRef) { + this.nativeElement = this.hostEle.nativeElement; + } + ngOnChanges(changes: SimpleChanges): void { + // value和maxValue的动态修改均需要重绘进度呈现 + if ((changes.value && !changes.value.isFirstChange()) || (changes.maxValue && !changes.maxValue.isFirstChange())) { + this.draw(); + } + } + ngAfterViewInit(): void { + // 初始化走一次圆饼的绘制,在此处处理是因为canvas对象此处是通过ref方式获取canvas的最早时机 + this.canvasElement = this.canvasEle.nativeElement; + // 修复SSR错误:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle === 'undefined') { + return; + } + this.canvasElement.width = parseFloat(getComputedStyle(this.nativeElement).width); + this.canvasElement.height = parseFloat(getComputedStyle(this.nativeElement).height); + this.draw(); + } + private draw(): void { + this.calcPercent(); + this.drawProgressPie(); + } + // 计算百分比,外部需要保证maxVlue和value均为数字类型 + private calcPercent(): void { + // 计算percent值 + if (this.maxValue === 0) { + this.percent = 0; + + return; + } + + let percent: number = this.value / this.maxValue; + if (isNaN(percent)) { + this.percent = 0; + + return; + } + + if (percent > 1) { + percent = 1; + } + + if (percent < 0) { + percent = 0; + } + this.percent = percent; + } + + private drawProgressPie(): void { + const canvas: any = this.canvasElement; + const ctx: any = canvas.getContext('2d'); + + // 清除先前画布内容 + const width: number = canvas.width; + const height: number = canvas.height; + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // 圆半径及起始点计算 + const d: number = Math.min(width, height); + const cx: number = width / 2; + const cy: number = height / 2; + const lineWidth: number = this.defaultConfig.lineWidth; + const r: number = d / 2 - lineWidth; + const startPoint: number = -Math.PI / 2; + // 笔触样式设置 + const color: string = this.defaultConfig.color; + ctx.fillStyle = color; + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + // 画外圆 + ctx.beginPath(); + ctx.arc(cx, cy, r, startPoint, startPoint + Math.PI * 2); + ctx.stroke(); + + // 画扇形 + const endPoint: number = startPoint + Math.PI * this.percent * 2; + ctx.beginPath(); + ctx.moveTo(cx, cy); // 移动至圆心 + ctx.arc(cx, cy, r, startPoint, endPoint); // 从外边上的点画曲线 + ctx.lineTo(cx, cy); // 从圆心画直线到计算好的圆外上的点 + ctx.fill(); + if (this.percent === 0) { + // 为0的情况下,绘制圆心值边缘竖线 + ctx.stroke(); + } + ctx.closePath(); + } +} diff --git a/src/progresspie/lib/src/TiProgresspieModule.ts b/src/progresspie/lib/src/TiProgresspieModule.ts new file mode 100644 index 0000000..5cccbc4 --- /dev/null +++ b/src/progresspie/lib/src/TiProgresspieModule.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiProgresspieComponent } from './TiProgresspieComponent'; +/** + * @ignore + */ +@NgModule({ + exports: [TiProgresspieComponent], + declarations: [TiProgresspieComponent] +}) +export class TiProgresspieModule {} +export { TiProgresspieComponent } from './TiProgresspieComponent'; diff --git a/src/radio/demo/karma.conf.js b/src/radio/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/radio/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/radio/demo/project.json b/src/radio/demo/project.json new file mode 100644 index 0000000..1b66def --- /dev/null +++ b/src/radio/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/radio/demo", + "sourceRoot": "src/radio/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/radio", + "index": "src/radio/demo/src/index.html", + "main": "src/radio/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/radio/demo/tsconfig.app.json", + "assets": ["src/radio/demo/src/favicon.ico", "src/radio/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "radio-demo:build:production" + }, + "development": { + "browserTarget": "radio-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js radio" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/radio/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/radio/demo/tsconfig.spec.json", + "karmaConfig": "src/radio/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/radio/demo/src/app/AppComponent.ts b/src/radio/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/radio/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/radio/demo/src/app/AppModule.ts b/src/radio/demo/src/app/AppModule.ts new file mode 100644 index 0000000..808183a --- /dev/null +++ b/src/radio/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { RadioTestModule } from './radio/RadioTestModule'; + +@NgModule({ + imports: [ + RadioTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/radio/demo/src/app/IndexComponent.ts b/src/radio/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..4d2e98c --- /dev/null +++ b/src/radio/demo/src/app/IndexComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; +import { RadioTestModule } from './radio/RadioTestModule'; + +@Component({ + template: ` + + + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + moudles: Array = [RadioTestModule.ROUTES]; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/radio/demo/src/app/app.html b/src/radio/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/radio/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/radio/demo/src/app/radio/RadioBasicComponent.ts b/src/radio/demo/src/app/radio/RadioBasicComponent.ts new file mode 100644 index 0000000..50815b8 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './radio-basic.html' +}) +export class RadioBasicComponent { + selected: string = 'BuyNow'; +} diff --git a/src/radio/demo/src/app/radio/RadioDarkComponent.ts b/src/radio/demo/src/app/radio/RadioDarkComponent.ts new file mode 100644 index 0000000..1e73111 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioDarkComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './radio-dark.html' +}) +export class RadioDarkComponent { + selected: string = 'option1'; +} diff --git a/src/radio/demo/src/app/radio/RadioDisabledComponent.ts b/src/radio/demo/src/app/radio/RadioDisabledComponent.ts new file mode 100644 index 0000000..de2f8a1 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioDisabledComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-disabled.html' +}) +export class RadioDisabledComponent { + countries: Array<{ key: string; id: string; disable?: boolean }> = [ + { + key: '中国', + id: 'China' + }, + { + key: '美国', + id: 'America', + disable: true + }, + { + key: '英国', + id: 'England', + disable: false + } + ]; + + radioList: Array = [ + { + id: '1', + label: '中国', + value: 'China', + disabled: true + }, + { + id: '2', + label: '美国', + value: 'America', + disabled: false + }, + { + id: '3', + label: '英国', + value: 'Britain' + } + ]; + + selectedGroup: string = 'China'; + disabled: boolean = true; + selected: string = 'beijing'; + selectedCountry: string; +} diff --git a/src/radio/demo/src/app/radio/RadioEventComponent.ts b/src/radio/demo/src/app/radio/RadioEventComponent.ts new file mode 100644 index 0000000..7e63046 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioEventComponent.ts @@ -0,0 +1,61 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-event.html' +}) +export class RadioEventComponent { + myLogs: Array = []; + + season: string = ''; + seasons: Array = [ + { + value: 'spring', + label: '春天' + }, + { + value: 'summer', + label: '夏天' + }, + { + value: 'autumn', + label: '秋天' + }, + { + value: 'winter', + label: '冬天' + } + ]; + + radioList: Array = [ + { + id: '1', + label: '中国', + value: 'China' + }, + { + id: '2', + label: '美国', + value: 'America' + }, + { + id: '3', + label: '英国', + value: 'Britain' + }, + { + id: '4', + label: '加拿大', + value: 'Canada' + } + ]; + selected: string = 'China'; + + onNgModelChange(model: any): void { + this.myLogs = [...this.myLogs, `ngModelChange:${JSON.stringify(model)}`]; + } + + onNgModelChange1(model: any): void { + this.myLogs = [...this.myLogs, `ngModelChange1:${JSON.stringify(model)}`]; + } +} diff --git a/src/radio/demo/src/app/radio/RadioFocusComponent.ts b/src/radio/demo/src/app/radio/RadioFocusComponent.ts new file mode 100644 index 0000000..e5ded08 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioFocusComponent.ts @@ -0,0 +1,34 @@ +import { Component, ViewChild } from '@angular/core'; + +@Component({ + templateUrl: './radio-focus.html' +}) +export class RadioFocusComponent { + selected1: string = 'option1'; + selected2: string = 'option2'; + + myLogs: Array = []; + radioList: Array = [ + { + value: 'option1', + label: '选项1' + }, + { + value: 'option2', + label: '选项2' + }, + { + value: 'option3', + label: '选项3' + }, + { + value: 'option4', + label: '选项4' + } + ]; + @ViewChild('input') input: Element; + + onFocus(index: number): void { + this.myLogs = [...this.myLogs, `focus:第${index + 1}个单选框`]; + } +} diff --git a/src/radio/demo/src/app/radio/RadioGroupComponent.ts b/src/radio/demo/src/app/radio/RadioGroupComponent.ts new file mode 100644 index 0000000..0599de2 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioGroupComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-group.html' +}) +export class RadioGroupComponent { + radioList: Array = [ + { + id: '1', + label: '中国', + value: 'China' + }, + { + id: '2', + label: '美国', + value: 'America' + }, + { + id: '3', + label: '英国', + value: 'Britain' + } + ]; + selected: string = 'China'; +} diff --git a/src/radio/demo/src/app/radio/RadioGroupDirectionComponent.ts b/src/radio/demo/src/app/radio/RadioGroupDirectionComponent.ts new file mode 100644 index 0000000..22f0b0d --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioGroupDirectionComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-group-direction.html' +}) +export class RadioGroupDirectionComponent { + radioList: Array = [ + { + id: '1', + label: '中国', + value: 'China' + }, + { + id: '2', + label: '美国', + value: 'America' + }, + { + id: '3', + label: '英国', + value: 'Britain' + } + ]; + selected: string = 'America'; +} diff --git a/src/radio/demo/src/app/radio/RadioGroupLabelkeyComponent.ts b/src/radio/demo/src/app/radio/RadioGroupLabelkeyComponent.ts new file mode 100644 index 0000000..4c5ede2 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioGroupLabelkeyComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-group-labelkey.html' +}) +export class RadioGroupLabelkeyComponent { + radioList: Array = [ + { + id: '1', + label: '中国', + value: 'China' + }, + { + id: '2', + label: '美国', + value: 'America' + }, + { + id: '3', + label: '英国', + value: 'Britain' + } + ]; + selected: string = 'China'; +} diff --git a/src/radio/demo/src/app/radio/RadioGroupLinewrapComponent.ts b/src/radio/demo/src/app/radio/RadioGroupLinewrapComponent.ts new file mode 100644 index 0000000..b4570f7 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioGroupLinewrapComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-group-linewrap.html' +}) +export class RadioGroupLinewrapComponent { + manyRadios: Array = [ + { id: '1', label: '小猪佩琪' }, + { id: '2', label: '小红帽' }, + { id: '3', label: '花木兰' }, + { id: '4', label: '雅典娜' }, + { id: '5', label: '舒克' }, + { id: '6', label: '机器猫' }, + { id: '7', label: '灰姑娘' }, + { id: '8', label: '天线宝宝' }, + { id: '9', label: '葫芦娃' }, + { id: '10', label: '孙悟空' } + ]; + selected: string = ''; +} diff --git a/src/radio/demo/src/app/radio/RadioGroupValidationComponent.ts b/src/radio/demo/src/app/radio/RadioGroupValidationComponent.ts new file mode 100644 index 0000000..cf7ea94 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioGroupValidationComponent.ts @@ -0,0 +1,72 @@ +import { Component, ElementRef } from '@angular/core'; +import { TiRadioItem, TiValidationConfig, TiValidators } from '@opentiny/ng'; +import { FormBuilder, FormControl, FormGroup, ValidationErrors } from '@angular/forms'; + +@Component({ + templateUrl: './radio-group-validation.html' +}) +export class RadioGroupValidationComponent { + radioList: Array = [ + { + id: '1', + label: '中国', + value: 'China' + }, + { + id: '2', + label: '美国', + value: 'America' + }, + { + id: '3', + label: '英国', + value: 'Britain' + } + ]; + radioList1: Array = [ + { + id: '1', + label: '中国', + value: 'China' + }, + { + id: '2', + label: '美国', + value: 'America' + }, + { + id: '3', + label: '英国', + value: 'Britain' + } + ]; + + selected: string = ''; + validationConfig: TiValidationConfig = { + errorMessage: { + required: '请至少选择一项' + } + }; + validationConfig1: TiValidationConfig = { + errorMessage: { + required: '请至少选择一项' + } + }; + + myFormGroup: FormGroup; + constructor(fb: FormBuilder, private elementRef: ElementRef) { + this.myFormGroup = fb.group({ + formradiogroup: new FormControl(undefined, [TiValidators.required]) + }); + } + + checkgroup(form: FormGroup): void { + const errors: ValidationErrors | null = TiValidators.check(form); + console.log('errors', errors); + } + + checkgroup1(): void { + const errors: ValidationErrors | null = TiValidators.check(this.myFormGroup); + console.log(errors); + } +} diff --git a/src/radio/demo/src/app/radio/RadioGroupValuekeyComponent.ts b/src/radio/demo/src/app/radio/RadioGroupValuekeyComponent.ts new file mode 100644 index 0000000..3acb893 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioGroupValuekeyComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-group-valuekey.html' +}) +export class RadioGroupValuekeyComponent { + radioList: Array = [ + { + id: '1', + label: '中国', + value: 'China' + }, + { + id: '2', + label: '美国', + value: 'America' + }, + { + id: '3', + label: '英国', + value: 'Britain' + } + ]; + selected: string = '中国'; +} diff --git a/src/radio/demo/src/app/radio/RadioLabelComponent.ts b/src/radio/demo/src/app/radio/RadioLabelComponent.ts new file mode 100644 index 0000000..b5d1578 --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioLabelComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiRadioItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './radio-label.html' +}) +export class RadioLabelComponent { + fruit: string = ''; + manyRadios: Array = [ + { id: '1', label: '小猪佩奇' }, + { id: '2', label: '小红帽' }, + { id: '3', label: '花木兰' } + ]; + selected: string = ''; +} diff --git a/src/radio/demo/src/app/radio/RadioTestModule.ts b/src/radio/demo/src/app/radio/RadioTestModule.ts new file mode 100644 index 0000000..fa2631e --- /dev/null +++ b/src/radio/demo/src/app/radio/RadioTestModule.ts @@ -0,0 +1,101 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiButtonModule, TiIconModule, TiRadioModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { RadioBasicComponent } from './RadioBasicComponent'; +import { RadioLabelComponent } from './RadioLabelComponent'; +import { RadioDisabledComponent } from './RadioDisabledComponent'; +import { RadioEventComponent } from './RadioEventComponent'; +import { RadioFocusComponent } from './RadioFocusComponent'; +import { RadioDarkComponent } from './RadioDarkComponent'; +import { RadioGroupComponent } from './RadioGroupComponent'; +import { RadioGroupDirectionComponent } from './RadioGroupDirectionComponent'; +import { RadioGroupLabelkeyComponent } from './RadioGroupLabelkeyComponent'; +import { RadioGroupValuekeyComponent } from './RadioGroupValuekeyComponent'; +import { RadioGroupValidationComponent } from './RadioGroupValidationComponent'; +import { RadioGroupLinewrapComponent } from './RadioGroupLinewrapComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiButtonModule, + TiRadioModule, + TiIconModule, + TiValidationModule, + DemoLogModule, + RouterModule.forChild(RadioTestModule.ROUTES) + ], + declarations: [ + RadioBasicComponent, + RadioDisabledComponent, + RadioEventComponent, + RadioFocusComponent, + RadioDarkComponent, + RadioLabelComponent, + RadioGroupComponent, + RadioGroupDirectionComponent, + RadioGroupLabelkeyComponent, + RadioGroupValuekeyComponent, + RadioGroupValidationComponent, + RadioGroupLinewrapComponent + ] +}) +export class RadioTestModule { + static readonly LINKS: Array = [{ href: 'components/TiRadioComponent.html', label: 'Radio' }]; + static readonly ROUTES: Routes = [ + { + path: 'radio/radio-basic', + component: RadioBasicComponent + }, + { + path: 'radio/radio-disabled', + component: RadioDisabledComponent + }, + { + path: 'radio/radio-label', + component: RadioLabelComponent + }, + { + path: 'radio/radio-event', + component: RadioEventComponent + }, + { + path: 'radio/radio-focus', + component: RadioFocusComponent + }, + { + path: 'radio/radio-dark', + component: RadioDarkComponent + }, + { + path: 'radio/radio-group', + component: RadioGroupComponent + }, + { + path: 'radio/radio-group-direction', + component: RadioGroupDirectionComponent + }, + { + path: 'radio/radio-group-labelkey', + component: RadioGroupLabelkeyComponent + }, + { + path: 'radio/radio-group-valuekey', + component: RadioGroupValuekeyComponent + }, + { + path: 'radio/radio-group-validation', + component: RadioGroupValidationComponent + }, + { + path: 'radio/radio-group-linewrap', + component: RadioGroupLinewrapComponent + } + ]; +} diff --git a/src/radio/demo/src/app/radio/radio-basic.html b/src/radio/demo/src/app/radio/radio-basic.html new file mode 100644 index 0000000..f4097a0 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-basic.html @@ -0,0 +1,10 @@ + + + +
    +
    Current value: {{ selected | json }}
    +
    diff --git a/src/radio/demo/src/app/radio/radio-dark.html b/src/radio/demo/src/app/radio/radio-dark.html new file mode 100644 index 0000000..85bdc65 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-dark.html @@ -0,0 +1,12 @@ +

    1.描述

    +

    深色背景

    +

    2.示例

    +
    +

    +

    +

    + +
    +
    +
    +选中项:{{selected}} diff --git a/src/radio/demo/src/app/radio/radio-disabled.html b/src/radio/demo/src/app/radio/radio-disabled.html new file mode 100644 index 0000000..2821d88 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-disabled.html @@ -0,0 +1,43 @@ +

    1.禁用某个单选框

    + + + + +
    +

    2.在form表单中禁用某个单选框

    +
    + + + +
    +

    3.单选组禁用某个选项

    + diff --git a/src/radio/demo/src/app/radio/radio-event.html b/src/radio/demo/src/app/radio/radio-event.html new file mode 100644 index 0000000..f142e52 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-event.html @@ -0,0 +1,23 @@ + + + +
    + + + diff --git a/src/radio/demo/src/app/radio/radio-focus.html b/src/radio/demo/src/app/radio/radio-focus.html new file mode 100644 index 0000000..7e06577 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-focus.html @@ -0,0 +1,39 @@ +

    1.描述

    +

    单选框焦点功能测试

    +

    2.示例

    +

    2.1 可以通过组件或者原生元素获取焦点

    +

    2.2 tab键可以切换焦点

    +

    单选组1:

    +
    +
    +
    +选中项:{{selected1}}
    +
    +

    单选组2:

    + +tabindex="1" + +tabindex="4" + +tabindex="2" + +tabindex="3"
    +选中项:{{selected2}}
    + +  + +  + +  + +

    事件日志:

    + diff --git a/src/radio/demo/src/app/radio/radio-group-direction.html b/src/radio/demo/src/app/radio/radio-group-direction.html new file mode 100644 index 0000000..7cad6d6 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-group-direction.html @@ -0,0 +1,4 @@ + +
    +
    Current value: {{ selected }}
    +
    diff --git a/src/radio/demo/src/app/radio/radio-group-labelkey.html b/src/radio/demo/src/app/radio/radio-group-labelkey.html new file mode 100644 index 0000000..bc2a2ab --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-group-labelkey.html @@ -0,0 +1,4 @@ + +
    +
    Current value: {{ selected }}
    +
    diff --git a/src/radio/demo/src/app/radio/radio-group-linewrap.html b/src/radio/demo/src/app/radio/radio-group-linewrap.html new file mode 100644 index 0000000..bddb7a8 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-group-linewrap.html @@ -0,0 +1 @@ + diff --git a/src/radio/demo/src/app/radio/radio-group-validation.html b/src/radio/demo/src/app/radio/radio-group-validation.html new file mode 100644 index 0000000..1e4983b --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-group-validation.html @@ -0,0 +1,25 @@ +

    1.模板式表单

    +
    + + +
    + +
    +

    2.响应式表单

    +
    + +
    + +
    diff --git a/src/radio/demo/src/app/radio/radio-group-valuekey.html b/src/radio/demo/src/app/radio/radio-group-valuekey.html new file mode 100644 index 0000000..3c4ab22 --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-group-valuekey.html @@ -0,0 +1,4 @@ + +
    +
    Current value: {{ selected }}
    +
    diff --git a/src/radio/demo/src/app/radio/radio-group.html b/src/radio/demo/src/app/radio/radio-group.html new file mode 100644 index 0000000..3972d7e --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-group.html @@ -0,0 +1,4 @@ + +
    +
    Current value: {{ selected }}
    +
    diff --git a/src/radio/demo/src/app/radio/radio-label.html b/src/radio/demo/src/app/radio/radio-label.html new file mode 100644 index 0000000..a9f330a --- /dev/null +++ b/src/radio/demo/src/app/radio/radio-label.html @@ -0,0 +1,40 @@ + + + + + + +
    + + + + + + + diff --git a/src/radio/demo/src/app/radio/webdoc/radio-demos.js b/src/radio/demo/src/app/radio/webdoc/radio-demos.js new file mode 100644 index 0000000..edf2ff0 --- /dev/null +++ b/src/radio/demo/src/app/radio/webdoc/radio-demos.js @@ -0,0 +1,134 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'radio-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': + '

    Radio 组件的最简用法。一组单选框需要使用相同的name属性且 ngModel 绑定同一变量,才能实现单选效果。value属性表示该选项的值,当 ngModel 和某项的value值相等时,表示该项选中。

    ', + 'en-US': '', + }, + apis: ['TiRadioComponent.properties.label'], + }, + { + demoId: 'radio-group', + name: { + 'zh-CN': '单选组', + 'en-US': 'group', + }, + desc: { + 'zh-CN': '

    通过属性items配置radio-group所有选项的数据集合,各选项互斥。

    ', + 'en-US': '', + }, + apis: [ + 'TiRadioGroupComponent.properties.items', + 'TiRadioItem.properties.value' + ], + }, + { + demoId: 'radio-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置禁用状态;通过属性item.disabled配置radio-group选项禁用状态。

    ', + 'en-US': '', + }, + apis: [ + 'TiRadioComponent.properties.disabled', + ], + }, + { + demoId: 'radio-label', + name: { + 'zh-CN': '自定义文本', + 'en-US': '', + }, + desc: { + 'zh-CN': '通过label标签自定义显示文本;通过#item配置radio-group选项区域的模板。', + 'en-US': '', + }, + apis: ['TiRadioGroupComponent.slots.itemTemplate'], + }, + { + demoId: 'radio-group-direction', + name: { + 'zh-CN': '竖向排列', + 'en-US': 'direction', + }, + desc: { + 'zh-CN': '

    通过属性direction配置radio-group排列的方向,包括verticalhorizontal(默认)两种类型。

    ', + 'en-US': '', + }, + apis: ['TiRadioGroupComponent.properties.direction'], + }, + { + demoId: 'radio-group-labelkey', + name: { + 'zh-CN': '显示字段', + 'en-US': 'labelkey', + }, + desc: { + 'zh-CN': '

    通过属性labelKey配置radio-group组件显示数据的键值。

    ', + 'en-US': '', + }, + apis: ['TiRadioGroupComponent.properties.labelKey'], + }, + { + demoId: 'radio-group-valuekey', + name: { + 'zh-CN': '选中值', + 'en-US': 'valuekey', + }, + desc: { + 'zh-CN': + '

    通过属性valueKey配置radio-group组件选中项数据的键值。

    ', + 'en-US': '', + }, + apis: ['TiRadioGroupComponent.properties.valueKey'], + }, + { + demoId: 'radio-group-validation', + name: { + 'zh-CN': '表单校验', + 'en-US': 'validation', + }, + desc: { + 'zh-CN': + '

    通过指令tiValidation实现校验。

    ', + 'en-US': '', + }, + }, + { + demoId: 'radio-group-linewrap', + name: { + 'zh-CN': '自动换行', + 'en-US': 'linewrap', + }, + desc: { + 'zh-CN': '超过固定宽度会自动换行。', + 'en-US': '', + }, + }, + { + demoId: 'radio-event', + name: { + 'zh-CN': '事件', + 'en-US': 'radio event', + }, + desc: { + 'zh-CN': '当元素的值发生变化时触发ngModelChange事件。', + 'en-US': '', + }, + }, + ], + ignoreApis: [ + 'TiRadioGroupComponent.properties.disabled' + ], +}; diff --git a/src/radio/demo/src/app/radio/webdoc/radio.cn.md b/src/radio/demo/src/app/radio/webdoc/radio.cn.md new file mode 100644 index 0000000..b6e9bda --- /dev/null +++ b/src/radio/demo/src/app/radio/webdoc/radio.cn.md @@ -0,0 +1,29 @@ +--- +title: Radio 单选框 +--- +# Radio 单选框 + +
    + +从一组选项中选择一个。 + +```typescript +import { TiRadioModule } from '@opentiny/ng'; +``` + +如需使用校验功能,请导入。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
    + +
    + +从一组选项中选择一个。 + +```typescript +import { TiRadioModule } from '@opentiny/ng'; +``` +
    diff --git a/src/radio/demo/src/app/radio/webdoc/radio.en.md b/src/radio/demo/src/app/radio/webdoc/radio.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/radio/demo/src/app/radio/webdoc/radio.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/radio/demo/src/favicon.ico b/src/radio/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/radio/demo/src/index.html b/src/radio/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/radio/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/radio/demo/src/main.ts b/src/radio/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/radio/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/radio/demo/test.ts b/src/radio/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/radio/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/radio/demo/tsconfig.app.json b/src/radio/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/radio/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/radio/demo/tsconfig.spec.json b/src/radio/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/radio/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/radio/lib/index.ts b/src/radio/lib/index.ts new file mode 100644 index 0000000..bc21e68 --- /dev/null +++ b/src/radio/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiRadioModule'; diff --git a/src/radio/lib/ng-package.json b/src/radio/lib/ng-package.json new file mode 100644 index 0000000..791d024 --- /dev/null +++ b/src/radio/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/radio", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/radio/lib/package.json b/src/radio/lib/package.json new file mode 100644 index 0000000..333d75c --- /dev/null +++ b/src/radio/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-radio", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-checkbox": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/radio/lib/project.json b/src/radio/lib/project.json new file mode 100644 index 0000000..0cdae96 --- /dev/null +++ b/src/radio/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/radio/lib", + "sourceRoot": "src/radio/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/radio"], + "options": { + "project": "src/radio/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/radio"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js radio" + }, + { + "command": "ng default-build radio" + }, + { + "command": "node build/clear-default-theme.js radio" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/radio && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build radio && ng pack radio && node build/publish.js radio --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/radio/lib/src/TiRadioComponent.ts b/src/radio/lib/src/TiRadioComponent.ts new file mode 100644 index 0000000..0e3fe6e --- /dev/null +++ b/src/radio/lib/src/TiRadioComponent.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core'; +import { TiRadioBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * radio单选框组件 + * + * radio组件完全基于原生input实现,但是需要在input中添加tiRadio属性 + * + */ +@Component({ + selector: '[tiRadio]', // 指定组件名称 + templateUrl: './radio.html', // 指定组件模板 + styleUrls: ['./radio.less'], // 样式路径 + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None // 设置组件的试图包装选项:三个值Emulated(默认值),Native,None +}) +export class TiRadioComponent extends TiRadioBaseComponent { + protected versionInfo: string = super.getVersion(packageInfo); + /** + * @ignore + */ + protected canChange(): boolean { + return !this.nativeElement.checked; + } +} diff --git a/src/radio/lib/src/TiRadioGroupComponent.ts b/src/radio/lib/src/TiRadioGroupComponent.ts new file mode 100644 index 0000000..a578349 --- /dev/null +++ b/src/radio/lib/src/TiRadioGroupComponent.ts @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ContentChild, Input, TemplateRef, ViewEncapsulation } from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiCheckboxItem } from '@opentiny/ng-checkbox'; +import packageInfo from '../package.json'; + +/** + * + * ti-radio-group 组件的数据集格式 + */ +export interface TiRadioItem extends TiCheckboxItem { + /** + * 选中值 + */ + value?: string; +} + +/** + * + * 将多个radio聚合在一起,成为一个组,在表单校验时需要使用。 + */ +@Component({ + selector: 'ti-radio-group', + templateUrl: './radio-group.html', + styleUrls: ['./radiogroup.less'], + host: { + '[class.ti-radiogroup-horizon]': 'direction === "horizontal"', + '[class.ti-radiogroup-vertical]': 'direction === "vertical"', + '[class.ti-radiogroup-defalut-item]': '!itemTemplate' + }, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiRadioGroupComponent)] +}) +export class TiRadioGroupComponent extends TiFormComponent { + private static index: number = 0; + /** + * 所有单选项的数据集合 + */ + @Input() items: Array; + /** + * @ignore + * 组内每个 radio 的 name 属性值,多个 radio 需要共同的 name 属性才能聚合为一组 + */ + @Input() name: string; + /** + * 单选项数据要展示的键值 + */ + @Input() labelKey: string = 'label'; + /** + * 指定选中项数据的键值 + */ + @Input() valueKey: string = 'value'; + /** + * 排列方式 + */ + @Input() direction: 'vertical' | 'horizontal' = 'horizontal'; + /** + * 选项区域的插槽 + */ + @ContentChild('item', { static: false }) itemTemplate: TemplateRef; + /** + * @ignore + * ti-radio-group需要给组内的每个radio唯一的name属性 + */ + public uniqueName: string; + protected versionInfo: string = super.getVersion(packageInfo); + ngOnInit(): void { + super.ngOnInit(); + this.uniqueName = `ti_auto_radiogroup_${TiRadioGroupComponent.index++}`; + } +} diff --git a/src/radio/lib/src/TiRadioModule.ts b/src/radio/lib/src/TiRadioModule.ts new file mode 100644 index 0000000..43d4bb9 --- /dev/null +++ b/src/radio/lib/src/TiRadioModule.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiRadioComponent } from './TiRadioComponent'; +import { TiRadioGroupComponent } from './TiRadioGroupComponent'; +import { FormsModule } from '@angular/forms'; +import { TiOutlineModule } from '@opentiny/ng-outline'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiOutlineModule], + exports: [TiRadioComponent, TiRadioGroupComponent], + declarations: [TiRadioComponent, TiRadioGroupComponent] +}) +export class TiRadioModule {} +export { TiRadioComponent } from './TiRadioComponent'; +export { TiRadioGroupComponent, TiRadioItem } from './TiRadioGroupComponent'; diff --git a/src/radio/lib/src/radio-group.html b/src/radio/lib/src/radio-group.html new file mode 100644 index 0000000..8c15e2e --- /dev/null +++ b/src/radio/lib/src/radio-group.html @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/src/radio/lib/src/radio.html b/src/radio/lib/src/radio.html new file mode 100644 index 0000000..0f92a5a --- /dev/null +++ b/src/radio/lib/src/radio.html @@ -0,0 +1,14 @@ + diff --git a/src/radio/lib/src/radio.less b/src/radio/lib/src/radio.less new file mode 100644 index 0000000..09a5055 --- /dev/null +++ b/src/radio/lib/src/radio.less @@ -0,0 +1,175 @@ +@import '../../../themes/basic/base-all.less'; + +@radio-name: tiRadio; + +[@{radio-name}] + label { + --ti-radio-container-size: var(--ti-common-size-4x); + --ti-radio-mark-size: var(--ti-common-size-2x); + --ti-radio-border-weight: var(--ti-common-border-weight-normal); + --ti-radio-mark-offset: calc((var(--ti-radio-container-size) - var(--ti-radio-mark-size) - var(--ti-radio-border-weight) * 2) / 2); + --ti-radio-dark-checked-active-color: var(--ti-common-color-text-link-darkbg); + --ti-radio-timing-function-default: cubic-bezier(0.25, 0.1, 0.25, 1); + --ti-radio-line-height: 1.5em; +} + +input[type='radio'][@{radio-name}] { + // radio的input框不需要显示:radio的样式使用span/label定制,此处只使用input的点选功能 + // 之前设置为left: -9999px,虽然也能让元素不可见,但是当有横向滚动条时,元素聚焦会导致页面移向最左边 + display: none !important; + // basic + & + .ti3-radio { + .box-sizing(border-box); + display: inline-flex; + flex-shrink: 0; + line-height: var(--ti-radio-line-height); + margin-bottom: 0px; + font-weight: normal; + cursor: pointer; + .user-select(); + .ti3-radio-skin { + .box-sizing(border-box); + flex-shrink: 0; + position: relative; + display: inline-block; + margin-bottom: 0px; + margin-top: calc( + (var(--ti-radio-line-height) - var(--ti-radio-container-size)) / 2 + ); // 为保证按钮与文本对齐,margin-top 的值为(文本的行高 (28px) - 按钮宽高 (16px))/ 2 + height: var(--ti-radio-container-size); + width: var(--ti-radio-container-size); + border: var(--ti-radio-border-weight) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + .border-radius(var(--ti-common-border-radius-3)); + font-weight: normal; + cursor: pointer; + .transition (background-color; 0.15s, border-color; 0.15s); + + &::after { + .box-sizing(border-box); + position: absolute; + content: ''; + left: var(--ti-radio-mark-offset); + top: var(--ti-radio-mark-offset); + height: var(--ti-radio-mark-size); + width: var(--ti-radio-mark-size); + opacity: 0; + .border-radius(var(--ti-common-border-radius-3)); + } + } + .ti3-radio-label { + margin-left: var(--ti-common-space-2x); + cursor: pointer; + color: var(--ti-common-color-text-primary); + margin-bottom: 0px; + font-weight: var(--ti-common-font-weight-4); + font-size: var(--ti-common-font-size-base); + } + } + // unchecked hover + &:not(:disabled):not(:checked) + .ti3-radio .ti3-radio-skin { + border-color: var(--ti-common-color-line-normal); + &::after { + background-color: var(--ti-common-color-bg-emphasize); + // 未选中状态: opacity从1到0,从中心向上移动 + opacity: 0; + transform: translate(0px, -7px) scale(0); + transition: opacity 0.15s ease-out, transform 0.15s; + } + &:focus { + border-color: var(--ti-common-color-line-active); + } + &:hover { + border-color: var(--ti-common-color-line-active); + .transition(border-color; 0.2s; var(--ti-radio-timing-function-default)); + } + &:active { + border-color: var(--ti-common-color-line-active); + } + } + &:not(:disabled):checked + .ti3-radio .ti3-radio-skin { + border-color: var(--ti-common-color-line-active); + &::after { + background-color: var(--ti-common-color-bg-emphasize); + // 选中状态:opacity从0到1,原点位置从上边7px移到中心位置 + opacity: 1; + transform: translate(0px, 0px) scale(1); + transition: opacity 0.2s ease-out, transform 0.2s; + } + &:hover, + &:focus { + border-color: var(--ti-common-color-bg-hover); + &::after { + background-color: var(--ti-common-color-bg-hover); + } + } + &:hover { + // 非禁用,选中时hover动画 + .transition (background-color; 0.2s; var(--ti-radio-timing-function-default), + border-color; 0.2s; var(--ti-radio-timing-function-default), + content; 0.2s; var(--ti-radio-timing-function-default));; + } + &:active { + border-color: var(--ti-common-color-bg-hover); + &::after { + background-color: var(--ti-common-color-bg-hover); + } + } + } + // disable + &:disabled + .ti3-radio { + cursor: not-allowed; + .ti3-radio-skin { + background-color: var(--ti-common-color-bg-disabled); + border-color: var(--ti-common-color-line-disabled); + cursor: not-allowed; + } + .ti3-radio-label { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + } + } + &:disabled:checked + .ti3-radio .ti3-radio-skin::after { + background-color: var(--ti-common-color-bg-secondary); + cursor: not-allowed; + opacity: 1; + } + // hide + &.ng-hide + .ti3-radio { + display: none !important; + } +} +// [dark],适配深色背景,目前是内部组件discount使用,没有提供示例 +input[type='radio'][@{radio-name}][dark] { + & + .ti3-radio .ti3-radio-label { + color: var(--ti-common-color-text-darkbg); + } + + &:checked:not(:disabled) + .ti3-radio .ti3-radio-skin { + border-color: var(--ti-common-color-icon-darkbg-active); + &::after { + background-color: var(--ti-common-color-icon-darkbg-active); + } + &:hover, + &:active, + &:focus { + border-color: var(--ti-radio-dark-checked-active-color); + &::after { + background-color: var(--ti-radio-dark-checked-active-color); + } + } + } + &:not(:checked):not(:disabled) + .ti3-radio .ti3-radio-skin { + border-color: var(--ti-common-color-line-normal); + &:hover, + &:active, + &:focus { + border-color: var(--ti-common-color-icon-darkbg-active); + } + } + &:disabled + .ti3-radio .ti3-radio-skin { + background-color: var(--ti-common-color-icon-disabled); + border-color: var(--ti-common-color-line-disabled); + &::after { + background-color: var(--ti-common-color-bg-dark-disabled); + } + } +} diff --git a/src/radio/lib/src/radiogroup.less b/src/radio/lib/src/radiogroup.less new file mode 100644 index 0000000..1e6a6a3 --- /dev/null +++ b/src/radio/lib/src/radiogroup.less @@ -0,0 +1,23 @@ +ti-radio-group { + display: inline-flex; + flex-wrap: wrap; + line-height: 1.5em; + &.ti-radiogroup-vertical { + flex-direction: column; + input[type='radio'][tiRadio] + .ti3-radio, + .radio-group-item { + line-height: var(--ti-common-size-7x); + & .ti3-radio-skin { + // 为保证按钮与文本对齐,margin-top 的值为(文本的行高 (28px) - 按钮宽高 (16px))/ 2 + margin-top: calc((var(--ti-common-size-7x) - var(--ti-radio-container-size)) / 2); + } + } + } +} +.radio-group-item { + display: inline-flex; +} +// ti-radio-group按规范定义了选项之间的默认间距 +ti-radio-group.ti-radiogroup-horizon.ti-radiogroup-defalut-item label.ti3-radio { + margin-right: var(--ti-common-space-5x); +} diff --git a/src/rate/demo/karma.conf.js b/src/rate/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/rate/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/rate/demo/project.json b/src/rate/demo/project.json new file mode 100644 index 0000000..dc03a50 --- /dev/null +++ b/src/rate/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/rate/demo", + "sourceRoot": "src/rate/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/rate", + "index": "src/rate/demo/src/index.html", + "main": "src/rate/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/rate/demo/tsconfig.app.json", + "assets": ["src/rate/demo/src/favicon.ico", "src/rate/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "rate-demo:build:production" + }, + "development": { + "browserTarget": "rate-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js rate" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/rate/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/rate/demo/tsconfig.spec.json", + "karmaConfig": "src/rate/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/rate/demo/src/app/AppComponent.ts b/src/rate/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/rate/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/rate/demo/src/app/AppModule.ts b/src/rate/demo/src/app/AppModule.ts new file mode 100644 index 0000000..cfd1889 --- /dev/null +++ b/src/rate/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { RateTestModule } from './rate/RateTestModule'; + +@NgModule({ + imports: [ + RateTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/rate/demo/src/app/IndexComponent.ts b/src/rate/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..9e43096 --- /dev/null +++ b/src/rate/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { RateTestModule } from './rate/RateTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = RateTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/rate/demo/src/app/app.html b/src/rate/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/rate/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/rate/demo/src/app/rate/RateBasicComponent.ts b/src/rate/demo/src/app/rate/RateBasicComponent.ts new file mode 100644 index 0000000..690a885 --- /dev/null +++ b/src/rate/demo/src/app/rate/RateBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './rate-basic.html' +}) +export class RateBasicComponent { + value: number = 3; +} diff --git a/src/rate/demo/src/app/rate/RateDisabledComponent.ts b/src/rate/demo/src/app/rate/RateDisabledComponent.ts new file mode 100644 index 0000000..051aca8 --- /dev/null +++ b/src/rate/demo/src/app/rate/RateDisabledComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './rate-disabled.html' +}) +export class RateDisabledComponent { + value: number = 1; + disabled: boolean = true; +} diff --git a/src/rate/demo/src/app/rate/RateEventComponent.ts b/src/rate/demo/src/app/rate/RateEventComponent.ts new file mode 100644 index 0000000..0614179 --- /dev/null +++ b/src/rate/demo/src/app/rate/RateEventComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './rate-event.html' +}) +export class RateEventComponent { + myLogs: Array = []; + value: number = 0; + + onNgModelChange(value: Date): void { + this.myLogs = [...this.myLogs, `onNgModelChange() value = ${value}`]; + } +} diff --git a/src/rate/demo/src/app/rate/RateIdComponent.ts b/src/rate/demo/src/app/rate/RateIdComponent.ts new file mode 100644 index 0000000..8cad473 --- /dev/null +++ b/src/rate/demo/src/app/rate/RateIdComponent.ts @@ -0,0 +1,24 @@ +import { Component, DoCheck } from '@angular/core'; + +@Component({ + templateUrl: './rate-id.html' +}) +export class RateIdComponent implements DoCheck { + value = 2; + rateId = 'rate'; + + public idExistMap: Map = new Map(); + public ids: Array = ['rate_icon_0', 'rate_icon_1', 'rate_icon_2', 'rate_icon_3', 'rate_icon_4']; + public allIdExist = false; + + ngDoCheck(): void { + this.allIdExist = true; + this.ids.forEach((id: string) => { + const idExist: boolean = document.getElementById(id) != undefined; + this.idExistMap.set(id, idExist); + if (!idExist) { + this.allIdExist = false; + } + }); + } +} diff --git a/src/rate/demo/src/app/rate/RateLoadComponent.ts b/src/rate/demo/src/app/rate/RateLoadComponent.ts new file mode 100644 index 0000000..bf7def8 --- /dev/null +++ b/src/rate/demo/src/app/rate/RateLoadComponent.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './rate-load.html' +}) +export class RateLoadComponent { + value: any; + + public myLogs: Array = []; + + onChange(event: any): void { + this.myLogs = [...this.myLogs, `onNgModelChange() value=${event}`]; + } + + changeUndefined(): void { + this.value = undefined; + } + changeNull(): void { + this.value = null; + } + + changeText(): void { + this.value = ''; + } + + changeZero(): void { + this.value = 0; + } + + changeNegativeNumber(): void { + this.value = -10; + } + + changeMoreThanFive(): void { + this.value = 10; + } + + changeFour(): void { + this.value = 4; + } + + changeTwo(): void { + this.value = 2; + } +} diff --git a/src/rate/demo/src/app/rate/RateTestModule.ts b/src/rate/demo/src/app/rate/RateTestModule.ts new file mode 100644 index 0000000..9b992b1 --- /dev/null +++ b/src/rate/demo/src/app/rate/RateTestModule.ts @@ -0,0 +1,37 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiRateModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { RateBasicComponent } from './RateBasicComponent'; +import { RateDisabledComponent } from './RateDisabledComponent'; +import { RateEventComponent } from './RateEventComponent'; +import { RateIdComponent } from './RateIdComponent'; +import { RateLoadComponent } from './RateLoadComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiRateModule, DemoLogModule, RouterModule.forChild(RateTestModule.ROUTES)], + declarations: [RateBasicComponent, RateDisabledComponent, RateEventComponent, RateIdComponent, RateLoadComponent] +}) +export class RateTestModule { + static readonly LINKS: Array = [{ label: 'Rate' }]; + static readonly ROUTES: Routes = [ + { + path: 'rate/rate-basic', + component: RateBasicComponent + }, + { + path: 'rate/rate-disabled', + component: RateDisabledComponent + }, + { + path: 'rate/rate-event', + component: RateEventComponent + }, + { path: 'rate/rate-load', component: RateLoadComponent }, + { path: 'rate/rate-id', component: RateIdComponent } + ]; +} diff --git a/src/rate/demo/src/app/rate/rate-basic.html b/src/rate/demo/src/app/rate/rate-basic.html new file mode 100644 index 0000000..49b378a --- /dev/null +++ b/src/rate/demo/src/app/rate/rate-basic.html @@ -0,0 +1 @@ + diff --git a/src/rate/demo/src/app/rate/rate-disabled.html b/src/rate/demo/src/app/rate/rate-disabled.html new file mode 100644 index 0000000..a2e1049 --- /dev/null +++ b/src/rate/demo/src/app/rate/rate-disabled.html @@ -0,0 +1 @@ + diff --git a/src/rate/demo/src/app/rate/rate-event.html b/src/rate/demo/src/app/rate/rate-event.html new file mode 100644 index 0000000..87ae5a1 --- /dev/null +++ b/src/rate/demo/src/app/rate/rate-event.html @@ -0,0 +1,3 @@ + + + diff --git a/src/rate/demo/src/app/rate/rate-id.html b/src/rate/demo/src/app/rate/rate-id.html new file mode 100644 index 0000000..d68354e --- /dev/null +++ b/src/rate/demo/src/app/rate/rate-id.html @@ -0,0 +1,10 @@ +

    描述

    +

    设置组件id,用于自动化测试

    +
    + +

    示例

    + + +

    id是否存在:

    +

    {{id+':'+idExistMap.get(id)}}

    +

    所有id是否存在:{{allIdExist}}

    diff --git a/src/rate/demo/src/app/rate/rate-load.html b/src/rate/demo/src/app/rate/rate-load.html new file mode 100644 index 0000000..8c146b4 --- /dev/null +++ b/src/rate/demo/src/app/rate/rate-load.html @@ -0,0 +1,24 @@ +

    描述

    +

    Rate评分组件,数据加载。输入异常值时,默认会将value设置为0。

    +
    +

    点击异常值,value会被设置为0,同时会触发ngModelChange事件。设置正常0~5之间的正常值时,不会触发ngModelChange事件。

    +
    + +

    示例

    + +
    + +

    异常值

    +
    +
    +
    +
    +
    +
    + +

    正常值

    +
    + + +

    事件日志:

    + diff --git a/src/rate/demo/src/app/rate/webdoc/rate-demos.js b/src/rate/demo/src/app/rate/webdoc/rate-demos.js new file mode 100644 index 0000000..431901b --- /dev/null +++ b/src/rate/demo/src/app/rate/webdoc/rate-demos.js @@ -0,0 +1,39 @@ +export default { + column: '2', + demos: [ + { + demoId: 'rate-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Rate 组件的最简用法。

    ', + 'en-US': '', + } + }, + { + demoId: 'rate-disabled', + name: { + 'zh-CN': '只读', + 'en-US': 'disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态,即只读模式。

    ', + 'en-US': '', + }, + apis: ['TiRateComponent.properties.disabled'] + }, + { + demoId: 'rate-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event', + }, + desc: { + 'zh-CN': '

    当评分值改变的时候触发ngModelChange事件。

    ', + 'en-US': '', + } + } + ], +}; diff --git a/src/rate/demo/src/app/rate/webdoc/rate.cn.md b/src/rate/demo/src/app/rate/webdoc/rate.cn.md new file mode 100644 index 0000000..af3651d --- /dev/null +++ b/src/rate/demo/src/app/rate/webdoc/rate.cn.md @@ -0,0 +1,23 @@ +--- +title: Rate 评分 +--- +# Rate 评分 + +
    + +用于评分的组件。   + +```typescript +import { TiRateModule } from '@opentiny/ng'; +``` + +
    + +
    + +用于评分的组件。   + +```typescript +import { TiRateModule } from '@opentiny/ng'; +``` +
    diff --git a/src/rate/demo/src/app/rate/webdoc/rate.en.md b/src/rate/demo/src/app/rate/webdoc/rate.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/rate/demo/src/app/rate/webdoc/rate.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/rate/demo/src/favicon.ico b/src/rate/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/rate/demo/src/index.html b/src/rate/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/rate/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/rate/demo/src/main.ts b/src/rate/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/rate/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/rate/demo/test.ts b/src/rate/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/rate/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/rate/demo/tsconfig.app.json b/src/rate/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/rate/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/rate/demo/tsconfig.spec.json b/src/rate/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/rate/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/rate/lib/index.ts b/src/rate/lib/index.ts new file mode 100644 index 0000000..e4f3162 --- /dev/null +++ b/src/rate/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiRateModule'; diff --git a/src/rate/lib/ng-package.json b/src/rate/lib/ng-package.json new file mode 100644 index 0000000..235478f --- /dev/null +++ b/src/rate/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/rate", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/rate/lib/package.json b/src/rate/lib/package.json new file mode 100644 index 0000000..1b38fae --- /dev/null +++ b/src/rate/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-rate", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/rate/lib/project.json b/src/rate/lib/project.json new file mode 100644 index 0000000..3b46d04 --- /dev/null +++ b/src/rate/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/rate/lib", + "sourceRoot": "src/rate/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/rate"], + "options": { + "project": "src/rate/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/rate"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js rate" + }, + { + "command": "ng default-build rate" + }, + { + "command": "node build/clear-default-theme.js rate" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/rate && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build rate && ng pack rate && node build/publish.js rate --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/rate/lib/src/TiRateComponent.ts b/src/rate/lib/src/TiRateComponent.ts new file mode 100644 index 0000000..85a3d32 --- /dev/null +++ b/src/rate/lib/src/TiRateComponent.ts @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component } from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +@Component({ + selector: 'ti-rate', + templateUrl: './rate.html', + styleUrls: ['./rate.less'], + providers: [TiFormComponent.getValueAccessor(TiRateComponent)] +}) +export class TiRateComponent extends TiFormComponent { + /** + * @ignore 数组长度为5 + */ + public items: Array = new Array(5); + + /** + * @ignore 记录鼠标移入时的索引 + */ + public hoverIndex: number = -1; + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + this.setDefalutModel(this.model); + } + + /** + * @ignore 当做钩子函数使用 + */ + ngOnModelChange(model: any): void { + this.setDefalutModel(model); + } + + /** + * @ignore 设置model的默认值,在ngOnInit和ngOnModelChange中被调用 + * @param model 模型 + */ + setDefalutModel(model: any): void { + if (typeof this.model !== 'number' || this.model < 0 || model > this.items.length) { + this.model = 0; + } + } + + /** + * @ignore 鼠标进入时,设置hoverIndex + * @param index 图标索引,取值0 ~ this.items.length-1 + */ + onMouseEnter(index: number): void { + if (this.disabled) { + return; + } + + this.hoverIndex = index; + } + + /** + * @ignore 鼠标离开时,设置hoverIndex为-1 + */ + onMouseLeave(): void { + if (this.disabled) { + return; + } + + this.hoverIndex = -1; + } + + /** + * @ignore 点击图标时,设置model + * @param index 图标索引,取值0 ~ this.items.length-1 + */ + onClick(index: number): void { + if (this.disabled) { + return; + } + + this.model = index + 1; + } + + /** + * @ignore 判断当前图标是否需要点亮 + * @param index 图标索引,取值0 ~ this.items.length-1 + */ + isActive(index: number): boolean { + // 当鼠标移入时(this.hoverIndex > -1),hoverIndex及其坐标的图标需要点亮 + // 当鼠标移走后(this.hoverIndex = -1),当前评分值左边的图标需要点亮 + return this.hoverIndex > -1 ? index <= this.hoverIndex : index < this.model; + } + + /** + * @ignore ngFor遍历的trackBy函数,防止数据更新导致所有DOM重新渲染 + * @param index 图标索引,取值0 ~ this.items.length-1 + * @returns 图标索引 + */ + public trackByFn(index: any): any { + return index; + } +} diff --git a/src/rate/lib/src/TiRateModule.ts b/src/rate/lib/src/TiRateModule.ts new file mode 100644 index 0000000..358b75d --- /dev/null +++ b/src/rate/lib/src/TiRateModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { TiRateComponent } from './TiRateComponent'; + +import { TiIconModule } from '@opentiny/ng-icon'; + +@NgModule({ + imports: [CommonModule, TiIconModule], + declarations: [TiRateComponent], + exports: [TiRateComponent] +}) +export class TiRateModule {} +export { TiRateComponent } from './TiRateComponent'; diff --git a/src/rate/lib/src/rate.html b/src/rate/lib/src/rate.html new file mode 100644 index 0000000..7e356a2 --- /dev/null +++ b/src/rate/lib/src/rate.html @@ -0,0 +1,11 @@ + + diff --git a/src/rate/lib/src/rate.less b/src/rate/lib/src/rate.less new file mode 100644 index 0000000..b5385df --- /dev/null +++ b/src/rate/lib/src/rate.less @@ -0,0 +1,30 @@ +:host { + --ti-rate-star-size: var(--ti-common-size-4x); +} + +:host { + cursor: pointer; + display: inline-block; + &[disabled] { + cursor: not-allowed; + } + .ti3-rate-star { + display: inline-block; + font-size: var(--ti-rate-star-size); + height: var(--ti-rate-star-size); + line-height: var(--ti-rate-star-size); + vertical-align: middle; + padding-right: var(--ti-common-space-2x); + &:last-child { + padding-right: 0; + } + } + + .ti3-rate-star-normal { + color: var(--ti-common-color-bg-light-normal); + } + + .ti3-rate-star-active { + color: var(--ti-common-color-warn-secondary); + } +} diff --git a/src/renderer/lib/index.ts b/src/renderer/lib/index.ts new file mode 100644 index 0000000..f99ea2f --- /dev/null +++ b/src/renderer/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiRenderer'; +export * from './src/TiRendererModule'; diff --git a/src/renderer/lib/ng-package.json b/src/renderer/lib/ng-package.json new file mode 100644 index 0000000..2a54afc --- /dev/null +++ b/src/renderer/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/renderer", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/renderer/lib/package.json b/src/renderer/lib/package.json new file mode 100644 index 0000000..afd4225 --- /dev/null +++ b/src/renderer/lib/package.json @@ -0,0 +1,8 @@ +{ + "name": "@opentiny/ng-renderer", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/renderer/lib/project.json b/src/renderer/lib/project.json new file mode 100644 index 0000000..8ea699a --- /dev/null +++ b/src/renderer/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/renderer/lib", + "sourceRoot": "src/renderer/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/renderer"], + "options": { + "project": "src/renderer/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/renderer"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js renderer" + }, + { + "command": "ng default-build renderer" + }, + { + "command": "node build/clear-default-theme.js renderer" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/renderer && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build renderer && ng pack renderer && node build/publish.js renderer --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/renderer/lib/src/TiRenderer.ts b/src/renderer/lib/src/TiRenderer.ts new file mode 100644 index 0000000..566eaee --- /dev/null +++ b/src/renderer/lib/src/TiRenderer.ts @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Renderer2, Injectable, RendererFactory2 } from '@angular/core'; +import { TiRendererModule } from './TiRendererModule'; + +/** + * @ignore + */ +@Injectable({ + providedIn: TiRendererModule +}) +export class TiRenderer { + private renderer: Renderer2; + constructor(rendererFactory: RendererFactory2) { + this.renderer = rendererFactory.createRenderer(null, null); + } + /** + * @description: 将节点插入某节点元素之后 + * @param: sourceEle 被插入节点 + * @param: targetEle 节点插入位置 + */ + public insertAfter(sourceEle, targetEle) { + const parent = targetEle.parentNode; + // 如果最后的节点是目标元素,则直接添加 + if (parent.lastChild === targetEle) { + this.renderer.appendChild(parent, sourceEle); + } else { + // 如果不是,则插在目标元素的下一个兄弟节点之前 + this.renderer.insertBefore(parent, sourceEle, targetEle.nextSibling); + } + } + /** + * @description: 判读一个元素上是否存在某个样式类名 + * @param: ele 被判断的元素 + * @param: className 样式类名 + */ + public hasClass(element, className): boolean { + const classList = element.classList; + return classList.contains(className); + } + /** + * @description: 给指定元素设置属性 + * @param: element 被设置的元素 + * @param: attr Object 属性对象 + */ + public setAttributes(element, attr: Object) { + for (const key in attr) { + if (Object.prototype.hasOwnProperty.call(attr, key)) { + this.renderer.setAttribute(element, key, String(attr[key])); + } + } + } + + /** + * @description: 为元素添加多个样式 + * @param: ele 元素对象 + * @param: styles Object 样式对象,如:{width: 100, height: 200} + */ + public setStyles(ele, styles) { + for (const key in styles) { + if (Object.prototype.hasOwnProperty.call(styles, key)) { + this.renderer.setStyle(ele, key, styles[key]); + } + } + } + + /** + * @description: 将节点插入某父容器,作为第一个元素 + * @param: parentEle 父节点元素 + * @param: sourceEle 需要插入的节点元素 + */ + public prepend(parentEle, sourceEle) { + if (!parentEle) { + return; + } + this.renderer.insertBefore(parentEle, sourceEle, parentEle.firstElementChild); + } + + /** + * @description: 查找一个元素下有某个样式类的子元素 + * @param: element 被查找的元素 + * @param: className 子元素的样式类名 + */ + public findChildrenByClassName(element, className): Array { + let resultChildren = []; + resultChildren = Array.from(element.children).filter((child: any) => { + return this.hasClass(child, className); + }); + + return resultChildren; + } + /** + * @description: 查找一个元素下有某个样式类的子元素 + * @param: element 被查找的元素 + * @param: className 子元素的样式类名 + */ + public findChildrenByTagName(element, tagName: string): Array { + let resultChildren = []; + resultChildren = Array.from(element.children).filter((child: any) => { + return child.tagName === tagName; + }); + + return resultChildren; + } +} diff --git a/src/renderer/lib/src/TiRendererModule.ts b/src/renderer/lib/src/TiRendererModule.ts new file mode 100644 index 0000000..078f216 --- /dev/null +++ b/src/renderer/lib/src/TiRendererModule.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +/** + * @ignore + */ +@NgModule({ + imports: [] +}) +export class TiRendererModule {} diff --git a/src/rights/demo/project.json b/src/rights/demo/project.json new file mode 100644 index 0000000..8be2121 --- /dev/null +++ b/src/rights/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/rights/demo", + "sourceRoot": "src/rights/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/rights", + "index": "src/rights/demo/src/index.html", + "main": "src/rights/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/rights/demo/tsconfig.app.json", + "assets": ["src/rights/demo/src/favicon.ico", "src/rights/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "rights-demo:build:production" + }, + "development": { + "browserTarget": "rights-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js rights" + } + ] + } + } + } +} diff --git a/src/rights/demo/src/app/AppComponent.ts b/src/rights/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/rights/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/rights/demo/src/app/AppModule.ts b/src/rights/demo/src/app/AppModule.ts new file mode 100644 index 0000000..1955211 --- /dev/null +++ b/src/rights/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { RightsTestModule } from './rights/RightsTestModule'; + +@NgModule({ + imports: [ + RightsTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/rights/demo/src/app/IndexComponent.ts b/src/rights/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..b7c6698 --- /dev/null +++ b/src/rights/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { RightsTestModule } from './rights/RightsTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = RightsTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/rights/demo/src/app/app.html b/src/rights/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/rights/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/rights/demo/src/app/rights/RightsBasicComponent.ts b/src/rights/demo/src/app/rights/RightsBasicComponent.ts new file mode 100644 index 0000000..0be0ccf --- /dev/null +++ b/src/rights/demo/src/app/rights/RightsBasicComponent.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; +import { TiRightsItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './rights-basic.html' +}) +export class RightsBasicComponent { + items: Array = [ + { + label: '灵活调整' + }, + { + label: '默认开启基础监控', + tip: '默认开启基础监控的信息描述', + tipPosition: 'bottom' + }, + { + label: '企业级存储', + tip: '企业级村存储信息描述' + }, + { + label: '智能数据底座', + tip: '智能数据底座信息描述' + }, + { + label: '不支持调整', + tip: '负向权益信息描述', + type: 'noSupport' + } + ]; +} diff --git a/src/rights/demo/src/app/rights/RightsTestModule.ts b/src/rights/demo/src/app/rights/RightsTestModule.ts new file mode 100644 index 0000000..3f121a1 --- /dev/null +++ b/src/rights/demo/src/app/rights/RightsTestModule.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiRightsModule } from '@opentiny/ng'; +import { RightsBasicComponent } from './RightsBasicComponent'; +import { RightsTypeComponent } from './RightsTypeComponent'; + +@NgModule({ + imports: [CommonModule, TiRightsModule, RouterModule.forChild(RightsTestModule.ROUTES)], + declarations: [RightsBasicComponent, RightsTypeComponent] +}) +export class RightsTestModule { + static readonly ROUTES: Routes = [ + { + path: 'rights/rights-basic', + component: RightsBasicComponent + }, + { + path: 'rights/rights-type', + component: RightsTypeComponent + } + ]; +} diff --git a/src/rights/demo/src/app/rights/RightsTypeComponent.ts b/src/rights/demo/src/app/rights/RightsTypeComponent.ts new file mode 100644 index 0000000..92741c4 --- /dev/null +++ b/src/rights/demo/src/app/rights/RightsTypeComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { TiRightsItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './rights-type.html' +}) +export class RightsTypeComponent { + items: Array = [ + { + label: '灵活调整', + // tip: this.tips, + tipPosition: 'bottom' + }, + { + label: '默认开启基础监控', + tip: '默认开启基础监控的信息描述' + }, + { + label: '企业级存储', + tip: '企业级村存储信息描述' + }, + { + label: '智能数据底座', + tip: '智能数据底座信息描述' + }, + { + label: '不支持调整', + tip: '负向权益信息描述', + type: 'noSupport' + } + ]; +} diff --git a/src/rights/demo/src/app/rights/rights-basic.html b/src/rights/demo/src/app/rights/rights-basic.html new file mode 100644 index 0000000..e963012 --- /dev/null +++ b/src/rights/demo/src/app/rights/rights-basic.html @@ -0,0 +1 @@ + diff --git a/src/rights/demo/src/app/rights/rights-type.html b/src/rights/demo/src/app/rights/rights-type.html new file mode 100644 index 0000000..5409ccd --- /dev/null +++ b/src/rights/demo/src/app/rights/rights-type.html @@ -0,0 +1 @@ + diff --git a/src/rights/demo/src/app/rights/webdoc/rights-demos.js b/src/rights/demo/src/app/rights/webdoc/rights-demos.js new file mode 100644 index 0000000..aef1e07 --- /dev/null +++ b/src/rights/demo/src/app/rights/webdoc/rights-demos.js @@ -0,0 +1,29 @@ +export default { + column: '2', + demos: [ + { + demoId: 'rights-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN': 'Rights 组件的最简用法。', + 'en-US': '' + }, + apis: ['TiRightsComponent.properties.items'] + }, + { + demoId: 'rights-type', + name: { + 'zh-CN': '类型', + 'en-US': '' + }, + desc: { + 'zh-CN': '通过type配置权益类型。', + 'en-US': '' + }, + apis: ['TiRightsComponent.properties.type'] + } + ] +}; diff --git a/src/rights/demo/src/app/rights/webdoc/rights.cn.md b/src/rights/demo/src/app/rights/webdoc/rights.cn.md new file mode 100644 index 0000000..61c2c7c --- /dev/null +++ b/src/rights/demo/src/app/rights/webdoc/rights.cn.md @@ -0,0 +1,19 @@ +--- +title: Rights 权益 +--- + +# Rights 权益 + +
    + +Rights 是介绍产品优势等特性信息的组件。 + +- 分为正向权益和负向权益两种。 + +- 可以是针对全局的权益信息,也可以是针对某个参数的权益信息。 + +```typescript +import { TiRightsModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/rights/demo/src/app/rights/webdoc/rights.en.md b/src/rights/demo/src/app/rights/webdoc/rights.en.md new file mode 100644 index 0000000..8801668 --- /dev/null +++ b/src/rights/demo/src/app/rights/webdoc/rights.en.md @@ -0,0 +1,13 @@ +--- +title: Rights +--- + +# Rights + +
    + +```typescript +import { TiRightsModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/rights/demo/src/favicon.ico b/src/rights/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d7a7af634ee6d643ac81d81feadff569ac25de3d GIT binary patch literal 300852 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 + + + + + + Tiny + + + + + + + + + diff --git a/src/rights/demo/src/main.ts b/src/rights/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/rights/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/rights/demo/tsconfig.app.json b/src/rights/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/rights/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/rights/lib/index.ts b/src/rights/lib/index.ts new file mode 100644 index 0000000..4023eb7 --- /dev/null +++ b/src/rights/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiRightsModule'; +export * from './src/TiRightsComponent'; diff --git a/src/rights/lib/ng-package.json b/src/rights/lib/ng-package.json new file mode 100644 index 0000000..d403c5f --- /dev/null +++ b/src/rights/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/rights", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/rights/lib/package.json b/src/rights/lib/package.json new file mode 100644 index 0000000..18fc3bb --- /dev/null +++ b/src/rights/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-rights", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/rights/lib/project.json b/src/rights/lib/project.json new file mode 100644 index 0000000..71fa8d8 --- /dev/null +++ b/src/rights/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/rights/lib", + "sourceRoot": "src/rights/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/rights"], + "options": { + "project": "src/rights/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/rights"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js rights" + }, + { + "command": "ng default-build rights" + }, + { + "command": "node build/clear-default-theme.js rights" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/rights && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build rights && ng pack rights && node build/publish.js rights --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/rights/lib/src/TiRightsComponent.ts b/src/rights/lib/src/TiRightsComponent.ts new file mode 100644 index 0000000..0ece479 --- /dev/null +++ b/src/rights/lib/src/TiRightsComponent.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { Component, Input, TemplateRef } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiPositionType } from '@opentiny/ng-utils'; + +export interface TiRightsItem { + /** + * 显示权益标题 + */ + label: string; + /** + * tip提示的内容 + */ + tip?: string | TemplateRef; + /** + * tip提示方位 + */ + tipPosition?: TiPositionType; + /** + * 权益类型,默认显示正向权益类型,'noSupport'为负向类型 + */ + type?: 'support' | 'noSupport'; + /** + * 允许多余的属性字段 + */ + [propName: string]: any; +} +/** + * 组件描述 + */ +@Component({ + selector: 'ti-rights', + templateUrl: 'rights.html', + styleUrls: ['rights.less', './icon.less'] +}) +export class TiRightsComponent extends TiBaseComponent { + /** + * 权益总数据 + */ + @Input() items: Array; + /** + * 权益级别,分为页面级别(page)和参数级别(param) + */ + @Input() type: 'page' | 'param' = 'param'; + + ngOnInit(): void { + super.ngOnInit(); + // 权益数量不超过6个 + if (this.items.length >= 6) { + this.items = this.items.slice(0, 5); + } + } +} diff --git a/src/rights/lib/src/TiRightsModule.ts b/src/rights/lib/src/TiRightsModule.ts new file mode 100644 index 0000000..864bb29 --- /dev/null +++ b/src/rights/lib/src/TiRightsModule.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiRightsComponent } from './TiRightsComponent'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiIconModule } from '@opentiny/ng-icon'; + +@NgModule({ + imports: [CommonModule, TiTipModule, TiIconModule], + exports: [TiRightsComponent], + declarations: [TiRightsComponent] +}) +export class TiRightsModule {} +export { TiRightsComponent, TiRightsItem } from './TiRightsComponent'; diff --git a/src/rights/lib/src/fonts/rightsFont.svg b/src/rights/lib/src/fonts/rightsFont.svg new file mode 100644 index 0000000..09cd862 --- /dev/null +++ b/src/rights/lib/src/fonts/rightsFont.svg @@ -0,0 +1,28 @@ + + + + + + +{ + "fontFamily": "rightsFont", + "description": "Font generated by IcoMoon.", + "majorVersion": 1, + "minorVersion": 0, + "version": "Version 1.0", + "fontId": "rightsFont", + "psName": "rightsFont", + "subFamily": "Regular", + "fullName": "rightsFont" +} + + + + + + + + + + + \ No newline at end of file diff --git a/src/rights/lib/src/fonts/rightsFont.woff b/src/rights/lib/src/fonts/rightsFont.woff new file mode 100644 index 0000000000000000000000000000000000000000..2bd6faaa49667d37f262db19209fbc54d5c62628 GIT binary patch literal 1832 zcmcIkPly{;82?@-Gn1Julj>v>H=Ar{lQhX%J4rXmW>acfv;>#B6jxaS3YzXtlZ9+j zl9b)`;!!W6w2J7_lUVId5fLo3AVLpnkJVD}VyguUdhp=J-+ME)=_R*)!}s3r`+fgr z-n{Q^ovzi05r&$j3BD>nyE?~@j?*vZ=80GU>}yB{g5osb}4805jQ1Q1n# zVz6->!_2*64{9c4GU}D7J6%<)q&PP`|ND4EFEER)7mDweCPynv@F>_BN4(%g@Gs>)<2 zG9yf4Da=3ghaZw8R3&tP&9K7!LYXZTIl-iFwS-~ndQ=UBV{yCk{z^8RSxU^ti^l_j zneb%PUR>O%9QMRJXub~rVr|xrXQS+rBZWG)^qmZe9o?C=ThZVGNx&X z-RMU~GH4iI%a7^$KAjbULFl{pd>2iG%?2yg>UOz&=x7BsCMTy$7hjk|)!9VMj>kd) zEsjzsn3|ZFN+pjbGMRcVUzpG5>Y429XzkK7&t70wYE`cUIPENhyB2}JS^ij zZhKe(f62o-MNuC&hhGB|g2lx?+=D-OSOWinhyB36d03_}ZFyJ$|AmKjD$-(aquCnt zYu(O(dbB}JYT?B8sfJsjLj%rRO{e4Z8UttDI +
  • + {{item.label}} +
  • + diff --git a/src/rights/lib/src/rights.less b/src/rights/lib/src/rights.less new file mode 100644 index 0000000..11b4f00 --- /dev/null +++ b/src/rights/lib/src/rights.less @@ -0,0 +1,38 @@ +.tp-rights-container { + display: flex; +} + +.tp-rights-item { + color: #575d6c; + margin-right: var(--ti-common-space-2x); + padding: 3px var(--ti-common-space-2x); + cursor: pointer; + + &:last-child { + margin-right: 0; + } + + .tp-rights-item-icon { + font-size: 16px; + vertical-align: -15%; //TODO:在tiny库设置'bottom'属性,图标可对齐,再该项目中无法对齐,暂使用百分比代替 + } +} + +.tp-rights-item-param { + margin-right: var(--ti-common-space-5x); + padding: 3px 0; +} + +.tp-rights-item-support { + background-color: #edfff9; +} + +.tp-rights-item-nosupport { + background-color: #f5f5f6; +} + +.tp-rights-label { + display: inline-block; + line-height: 18px; + margin-left: var(--ti-common-space-base); +} diff --git a/src/score/demo/karma.conf.js b/src/score/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/score/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/score/demo/project.json b/src/score/demo/project.json new file mode 100644 index 0000000..c15582a --- /dev/null +++ b/src/score/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/score/demo", + "sourceRoot": "src/score/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/score", + "index": "src/score/demo/src/index.html", + "main": "src/score/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/score/demo/tsconfig.app.json", + "assets": ["src/score/demo/src/favicon.ico", "src/score/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "score-demo:build:production" + }, + "development": { + "browserTarget": "score-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js score" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/score/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/score/demo/tsconfig.spec.json", + "karmaConfig": "src/score/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/score/demo/src/app/AppComponent.ts b/src/score/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/score/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/score/demo/src/app/AppModule.ts b/src/score/demo/src/app/AppModule.ts new file mode 100644 index 0000000..05ca7af --- /dev/null +++ b/src/score/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ScoreTestModule } from './score/ScoreTestModule'; + +@NgModule({ + imports: [ + ScoreTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/score/demo/src/app/IndexComponent.ts b/src/score/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..cb6063f --- /dev/null +++ b/src/score/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ScoreTestModule } from './score/ScoreTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = ScoreTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/score/demo/src/app/app.html b/src/score/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/score/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/score/demo/src/app/score/ScoreBasicComponent.ts b/src/score/demo/src/app/score/ScoreBasicComponent.ts new file mode 100644 index 0000000..ec40ad3 --- /dev/null +++ b/src/score/demo/src/app/score/ScoreBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './score-basic.html' +}) +export class ScoreBasicComponent { + value: number = 8; +} diff --git a/src/score/demo/src/app/score/ScoreEventsComponent.ts b/src/score/demo/src/app/score/ScoreEventsComponent.ts new file mode 100644 index 0000000..39adba5 --- /dev/null +++ b/src/score/demo/src/app/score/ScoreEventsComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './score-events.html' +}) +export class ScoreEventsComponent { + value: number = 10; + myLogs: Array = []; + onChange(value: number): void { + this.myLogs = [...this.myLogs, `当前评分值:${value}`]; + } +} diff --git a/src/score/demo/src/app/score/ScoreLimittextComponent.ts b/src/score/demo/src/app/score/ScoreLimittextComponent.ts new file mode 100644 index 0000000..94a659f --- /dev/null +++ b/src/score/demo/src/app/score/ScoreLimittextComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './score-limittext.html' +}) +export class ScoreLimittextComponent { + minText: string = '非常不可能'; + maxText: string = '非常可能'; +} diff --git a/src/score/demo/src/app/score/ScorePaddingComponent.ts b/src/score/demo/src/app/score/ScorePaddingComponent.ts new file mode 100644 index 0000000..b3c1cfd --- /dev/null +++ b/src/score/demo/src/app/score/ScorePaddingComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './score-padding.html' +}) +export class ScorePaddingComponent { + value: number = 8; +} diff --git a/src/score/demo/src/app/score/ScoreTestModule.ts b/src/score/demo/src/app/score/ScoreTestModule.ts new file mode 100644 index 0000000..41b8238 --- /dev/null +++ b/src/score/demo/src/app/score/ScoreTestModule.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Routes, RouterModule } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiScoreModule } from '@opentiny/ng'; +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { ScoreBasicComponent } from './ScoreBasicComponent'; +import { ScoreLimittextComponent } from './ScoreLimittextComponent'; +import { ScoreEventsComponent } from './ScoreEventsComponent'; +import { ScorePaddingComponent } from './ScorePaddingComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, DemoLogModule, TiScoreModule, RouterModule.forChild(ScoreTestModule.ROUTES)], + exports: [RouterModule], + declarations: [ScoreBasicComponent, ScoreLimittextComponent, ScorePaddingComponent, ScoreEventsComponent] +}) +export class ScoreTestModule { + static readonly LINKS: Array = [{ href: 'components/TiScoreComponent.html', label: 'Score' }]; + static readonly ROUTES: Routes = [ + { path: 'score/score-basic', component: ScoreBasicComponent }, + { path: 'score/score-limittext', component: ScoreLimittextComponent }, + { path: 'score/score-padding', component: ScorePaddingComponent }, + { path: 'score/score-events', component: ScoreEventsComponent } + ]; +} diff --git a/src/score/demo/src/app/score/score-basic.html b/src/score/demo/src/app/score/score-basic.html new file mode 100644 index 0000000..29659a4 --- /dev/null +++ b/src/score/demo/src/app/score/score-basic.html @@ -0,0 +1 @@ + diff --git a/src/score/demo/src/app/score/score-events.html b/src/score/demo/src/app/score/score-events.html new file mode 100644 index 0000000..ada405b --- /dev/null +++ b/src/score/demo/src/app/score/score-events.html @@ -0,0 +1 @@ + diff --git a/src/score/demo/src/app/score/score-limittext.html b/src/score/demo/src/app/score/score-limittext.html new file mode 100644 index 0000000..732f82c --- /dev/null +++ b/src/score/demo/src/app/score/score-limittext.html @@ -0,0 +1 @@ + diff --git a/src/score/demo/src/app/score/score-padding.html b/src/score/demo/src/app/score/score-padding.html new file mode 100644 index 0000000..8048e56 --- /dev/null +++ b/src/score/demo/src/app/score/score-padding.html @@ -0,0 +1 @@ + diff --git a/src/score/demo/src/app/score/webdoc/score-demos.js b/src/score/demo/src/app/score/webdoc/score-demos.js new file mode 100644 index 0000000..6a4f860 --- /dev/null +++ b/src/score/demo/src/app/score/webdoc/score-demos.js @@ -0,0 +1,32 @@ +export default { + column: '2', + demos: [ + { + demoId: 'score-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Score 组件的最简用法。

    ', + 'en-US': '

    score basic

    ', + }, + }, + { + demoId: 'score-limittext', + name: { + 'zh-CN': '文本', + 'en-US': 'limittext', + }, + desc: { + 'zh-CN': + '

    通过minTextmaxText配置两侧文本。

    ', + 'en-US': '

    score limittext

    ', + }, + apis: [ + 'TiScoreComponent.properties.minText', + 'TiScoreComponent.properties.maxText', + ], + }, + ], +}; diff --git a/src/score/demo/src/app/score/webdoc/score.cn.md b/src/score/demo/src/app/score/webdoc/score.cn.md new file mode 100644 index 0000000..94227ab --- /dev/null +++ b/src/score/demo/src/app/score/webdoc/score.cn.md @@ -0,0 +1,23 @@ +--- +title: Score 评分 +--- +# Score 评分 + +
    + +Score 用户评分的组件。   + +```typescript +import { TiScoreModule } from '@opentiny/ng'; +``` + +
    + +
    + +Score 用户评分的组件。   + +```typescript +import { TiScoreModule } from '@opentiny/ng'; +``` +
    diff --git a/src/score/demo/src/app/score/webdoc/score.en.md b/src/score/demo/src/app/score/webdoc/score.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/score/demo/src/app/score/webdoc/score.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/score/demo/src/favicon.ico b/src/score/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/score/demo/src/index.html b/src/score/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/score/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/score/demo/src/main.ts b/src/score/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/score/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/score/demo/test.ts b/src/score/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/score/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/score/demo/tsconfig.app.json b/src/score/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/score/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/score/demo/tsconfig.spec.json b/src/score/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/score/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/score/lib/index.ts b/src/score/lib/index.ts new file mode 100644 index 0000000..5229ef0 --- /dev/null +++ b/src/score/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiScoreModule'; diff --git a/src/score/lib/ng-package.json b/src/score/lib/ng-package.json new file mode 100644 index 0000000..da4d0a7 --- /dev/null +++ b/src/score/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/score", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/score/lib/package.json b/src/score/lib/package.json new file mode 100644 index 0000000..02de7cb --- /dev/null +++ b/src/score/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-score", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/score/lib/project.json b/src/score/lib/project.json new file mode 100644 index 0000000..da34bcf --- /dev/null +++ b/src/score/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/score/lib", + "sourceRoot": "src/score/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/score"], + "options": { + "project": "src/score/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/score"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js score" + }, + { + "command": "ng default-build score" + }, + { + "command": "node build/clear-default-theme.js score" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/score && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build score && ng pack score && node build/publish.js score --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/score/lib/src/TiScoreComponent.ts b/src/score/lib/src/TiScoreComponent.ts new file mode 100644 index 0000000..2f95cb8 --- /dev/null +++ b/src/score/lib/src/TiScoreComponent.ts @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { Component, ElementRef, Input, Renderer2, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiLocale } from '@opentiny/ng-locale'; + +/** + * NPS评分组件 + */ +@Component({ + selector: 'ti-score', + templateUrl: './score.html', + styleUrls: ['./score.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiScoreComponent)] +}) +export class TiScoreComponent extends TiFormComponent { + /** + * 最小评分值文本 + */ + @Input() minText: string; + /** + * 最大评分值文本 + */ + @Input() maxText: string; + + /** + * @ignore + * 选块内间距,主要用于调查问卷组件完成页场景 + */ + @Input() padding: string; + + /** + * @ignore + * 定义含有11项成员的数组 + */ + public scoreArray: Array = new Array(11); + + constructor(protected hostRef: ElementRef, protected renderer: Renderer2, protected changeDetectorRef: ChangeDetectorRef) { + super(hostRef, renderer, changeDetectorRef); + this.minText = this.minText || TiLocale.getLocaleWords().tiNpsscore.minText; + this.maxText = this.maxText || TiLocale.getLocaleWords().tiNpsscore.maxText; + } + /** + * @ignore + * 选择评分时,触发事件 + */ + public onClick(value: number): void { + this.model = value; + } +} diff --git a/src/score/lib/src/TiScoreModule.ts b/src/score/lib/src/TiScoreModule.ts new file mode 100644 index 0000000..9077a8f --- /dev/null +++ b/src/score/lib/src/TiScoreModule.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiScoreComponent } from './TiScoreComponent'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, TiOutlineModule], + exports: [TiScoreComponent], + declarations: [TiScoreComponent] +}) +export class TiScoreModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiScoreComponent } from './TiScoreComponent'; diff --git a/src/score/lib/src/i18n/TiScoreWords.ts b/src/score/lib/src/i18n/TiScoreWords.ts new file mode 100644 index 0000000..783a174 --- /dev/null +++ b/src/score/lib/src/i18n/TiScoreWords.ts @@ -0,0 +1,6 @@ +export interface TiScoreWords { + tiNpsscore: { + minText: string; + maxText: string; + }; +} diff --git a/src/score/lib/src/i18n/en_US.ts b/src/score/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..ad83641 --- /dev/null +++ b/src/score/lib/src/i18n/en_US.ts @@ -0,0 +1,8 @@ +import { TiScoreWords } from './TiScoreWords'; + +export const en_US: TiScoreWords = { + tiNpsscore: { + minText: 'Very dissatisfied', + maxText: 'Very satisfied' + } +}; diff --git a/src/score/lib/src/i18n/es_US.ts b/src/score/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..ae45e62 --- /dev/null +++ b/src/score/lib/src/i18n/es_US.ts @@ -0,0 +1,8 @@ +import { TiScoreWords } from './TiScoreWords'; + +export const es_US: TiScoreWords = { + tiNpsscore: { + minText: 'Very dissatisfied', + maxText: 'Very satisfied' + } +}; diff --git a/src/score/lib/src/i18n/fr_FR.ts b/src/score/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..a488d48 --- /dev/null +++ b/src/score/lib/src/i18n/fr_FR.ts @@ -0,0 +1,8 @@ +import { TiScoreWords } from './TiScoreWords'; + +export const fr_FR: TiScoreWords = { + tiNpsscore: { + minText: 'Very dissatisfied', + maxText: 'Very satisfied' + } +}; diff --git a/src/score/lib/src/i18n/index.ts b/src/score/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/score/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/score/lib/src/i18n/pt_BR.ts b/src/score/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..2526ef2 --- /dev/null +++ b/src/score/lib/src/i18n/pt_BR.ts @@ -0,0 +1,8 @@ +import { TiScoreWords } from './TiScoreWords'; + +export const pt_BR: TiScoreWords = { + tiNpsscore: { + minText: 'Very dissatisfied', + maxText: 'Very satisfied' + } +}; diff --git a/src/score/lib/src/i18n/zh_CN.ts b/src/score/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..c3a253d --- /dev/null +++ b/src/score/lib/src/i18n/zh_CN.ts @@ -0,0 +1,8 @@ +import { TiScoreWords } from './TiScoreWords'; + +export const zh_CN: TiScoreWords = { + tiNpsscore: { + minText: '非常不满意', + maxText: '非常满意' + } +}; diff --git a/src/score/lib/src/score.html b/src/score/lib/src/score.html new file mode 100644 index 0000000..a6fafd4 --- /dev/null +++ b/src/score/lib/src/score.html @@ -0,0 +1,18 @@ +
    + {{minText}} + {{maxText}} +
    +
    + {{i}} +
    diff --git a/src/score/lib/src/score.less b/src/score/lib/src/score.less new file mode 100644 index 0000000..aef834d --- /dev/null +++ b/src/score/lib/src/score.less @@ -0,0 +1,48 @@ +::ng-deep :root { + --ti3-score-item-height: 28px; +} + +:host { + display: inline-block; + font-weight: normal; + font-size: var(--ti-common-font-size-base); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-primary); +} + +// 每项评分样式 +.ti3-score-item { + display: inline-block; + text-align: center; + cursor: pointer; + height: var(--ti3-score-item-height); + line-height: var(--ti3-score-item-height); + margin-right: 2px; + background-color: var(--ti-common-color-bg-light-normal); + + &:hover { + background-color: var(--ti-common-color-bg-light-emphasize); + } + + &.ti3-score-item-active { + background-color: var(--ti-common-color-bg-emphasize); + color: var(--ti-common-color-text-white); + } + + &:first-child { + border-radius: var(--ti-common-border-radius-normal) 0 0 var(--ti-common-border-radius-normal); + } + + &:last-child { + margin-right: 0; + border-radius: 0 var(--ti-common-border-radius-normal) var(--ti-common-border-radius-normal) 0; + } +} + +// 文本描述,使用flex布局,文本两端对齐 +.ti3-score-text { + display: flex; + justify-content: space-between; + color: var(--ti-common-color-text-weaken); + margin-bottom: var(--ti-common-space-2x); +} diff --git a/src/scroll/lib/index.ts b/src/scroll/lib/index.ts new file mode 100644 index 0000000..8922fe7 --- /dev/null +++ b/src/scroll/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiScrollModule'; diff --git a/src/scroll/lib/ng-package.json b/src/scroll/lib/ng-package.json new file mode 100644 index 0000000..f1d85f4 --- /dev/null +++ b/src/scroll/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/scroll", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/scroll/lib/package.json b/src/scroll/lib/package.json new file mode 100644 index 0000000..1a24b53 --- /dev/null +++ b/src/scroll/lib/package.json @@ -0,0 +1,9 @@ +{ + "name": "@opentiny/ng-scroll", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/scroll/lib/project.json b/src/scroll/lib/project.json new file mode 100644 index 0000000..5913177 --- /dev/null +++ b/src/scroll/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/scroll/lib", + "sourceRoot": "src/scroll/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/scroll"], + "options": { + "project": "src/scroll/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/scroll"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js scroll" + }, + { + "command": "ng default-build scroll" + }, + { + "command": "node build/clear-default-theme.js scroll" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/scroll && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build scroll && ng pack scroll && node build/publish.js scroll --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/scroll/lib/src/TiScrollDirective.ts b/src/scroll/lib/src/TiScrollDirective.ts new file mode 100644 index 0000000..38cf029 --- /dev/null +++ b/src/scroll/lib/src/TiScrollDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Renderer2 } from '@angular/core'; +import { Util } from '@opentiny/ng-utils'; +/** + * tiScroll指令用于触发document上的tiScroll事件。 + * + * + */ +@Directive({ + selector: '[tiScroll]' +}) +export class TiScrollDirective { + private hostEle: Element; + constructor(private hostEleRef: ElementRef, private renderer2: Renderer2) { + this.hostEle = this.hostEleRef.nativeElement; + this.renderer2.listen(this.hostEle, 'scroll', () => { + Util.trigger(document, 'tiScroll'); + }); + } +} diff --git a/src/scroll/lib/src/TiScrollModule.ts b/src/scroll/lib/src/TiScrollModule.ts new file mode 100644 index 0000000..035edf4 --- /dev/null +++ b/src/scroll/lib/src/TiScrollModule.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiScrollDirective } from './TiScrollDirective'; + +@NgModule({ + imports: [], + exports: [TiScrollDirective], + declarations: [TiScrollDirective] +}) +export class TiScrollModule {} + +export { TiScrollDirective } from './TiScrollDirective'; diff --git a/src/searchbox/demo/karma.conf.js b/src/searchbox/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/searchbox/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/searchbox/demo/project.json b/src/searchbox/demo/project.json new file mode 100644 index 0000000..9c2453b --- /dev/null +++ b/src/searchbox/demo/project.json @@ -0,0 +1,98 @@ +{ + "projectType": "application", + "root": "src/searchbox/demo", + "sourceRoot": "src/searchbox/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/searchbox", + "index": "src/searchbox/demo/src/index.html", + "main": "src/searchbox/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/searchbox/demo/tsconfig.app.json", + "assets": [ + "src/searchbox/demo/src/favicon.ico", + "src/searchbox/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/food", + "output": "/assets/food/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "searchbox-demo:build:production" + }, + "development": { + "browserTarget": "searchbox-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js searchbox" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/searchbox/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/searchbox/demo/tsconfig.spec.json", + "karmaConfig": "src/searchbox/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/searchbox/demo/src/app/AppComponent.ts b/src/searchbox/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/searchbox/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/searchbox/demo/src/app/AppModule.ts b/src/searchbox/demo/src/app/AppModule.ts new file mode 100644 index 0000000..e3fb720 --- /dev/null +++ b/src/searchbox/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SearchboxTestModule } from './searchbox/SearchboxTestModule'; + +@NgModule({ + imports: [ + SearchboxTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/searchbox/demo/src/app/IndexComponent.ts b/src/searchbox/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..4a6e56f --- /dev/null +++ b/src/searchbox/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SearchboxTestModule } from './searchbox/SearchboxTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SearchboxTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/searchbox/demo/src/app/app.html b/src/searchbox/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/searchbox/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxAppendtobodyComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxAppendtobodyComponent.ts new file mode 100644 index 0000000..76300db --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxAppendtobodyComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-appendtobody.html' +}) +export class SearchboxAppendtobodyComponent { + value: string = ''; + options: Array = [{ label: '华北' }, { label: '华南' }, { label: '西北' }, { label: '西南' }]; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxBasicComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxBasicComponent.ts new file mode 100644 index 0000000..8e69ba8 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-basic.html' +}) +export class SearchboxBasicComponent { + value: string = ''; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxDisabledComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxDisabledComponent.ts new file mode 100644 index 0000000..3d66391 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxDisabledComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-disabled.html' +}) +export class SearchboxDisabledComponent { + value: string = ''; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxEventComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxEventComponent.ts new file mode 100644 index 0000000..000eb61 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxEventComponent.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-event.html' +}) +export class SearchboxEventComponent { + myLogs: Array = []; + value: string = ''; + options: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; + onFocus(): void { + this.myLogs = [...this.myLogs, 'on focus']; + } + onBlur(): void { + this.myLogs = [...this.myLogs, 'on blur']; + } + onSearch(value: string): void { + this.myLogs = [...this.myLogs, `search value: ${value}`]; + } + onClear(event: MouseEvent): void { + this.myLogs = [...this.myLogs, 'on clear']; + } + onModelChange(value: string): void { + this.myLogs = [...this.myLogs, `modelChange: ${value}`]; + } + onSelect(option: any): void { + this.myLogs = [...this.myLogs, `select: ${JSON.stringify(option)}`]; + } +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxMaxlengthComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxMaxlengthComponent.ts new file mode 100644 index 0000000..529b18e --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxMaxlengthComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-maxlength.html' +}) +export class SearchboxMaxlengthComponent { + value: string = ''; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxNotsearchComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxNotsearchComponent.ts new file mode 100644 index 0000000..03e0c3f --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxNotsearchComponent.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: 'searchbox-notsearch.html' +}) +export class SearchboxNotsearchComponent { + searchWord: string = ''; + myLogs: Array = []; + + modelChange(value: string): void { + this.myLogs = [...this.myLogs, `changed Value: ${value}`]; + } +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxOptionsComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxOptionsComponent.ts new file mode 100644 index 0000000..fcaa06c --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxOptionsComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiSearchboxComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './searchbox-options.html' +}) +export class SearchboxOptionsComponent { + value: string = ''; + options: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxPanelsizeComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxPanelsizeComponent.ts new file mode 100644 index 0000000..5d7e535 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxPanelsizeComponent.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-panelsize.html' +}) +export class SearchboxPanelsizeComponent { + value: string = ''; + options: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + }, + { + label: '东南' + }, + { + label: '东北' + } + ]; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxReactiveComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxReactiveComponent.ts new file mode 100644 index 0000000..08073e2 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxReactiveComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; + +@Component({ + templateUrl: './searchbox-reactive.html' +}) +export class SearchboxReactiveComponent { + placeholder: string = '请输入内容'; + formControl: FormControl = new FormControl(''); +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxSuggestComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxSuggestComponent.ts new file mode 100644 index 0000000..dbf505a --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxSuggestComponent.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; +import { TiSearchboxComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './searchbox-suggest.html' +}) +export class SearchboxSuggestComponent { + value: string = ''; + + onSuggest(searchbox: TiSearchboxComponent): void { + searchbox.setSuggestions(this.getSuggestion(searchbox.model)); + } + + private getSuggestion(value: string): Array { + const options: Array = value + ? [ + { + label: value + '@example.com' + }, + { + label: value + '@example.com' + }, + { + label: value + '@example.com' + } + ] + : []; + + return options; + } +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxTemplateComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxTemplateComponent.ts new file mode 100644 index 0000000..d83c036 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxTemplateComponent.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; +import { TiSearchboxComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './searchbox-template.html' +}) +export class SearchboxTemplateComponent { + value: string = ''; + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL']; + + onSuggest(searchbox: TiSearchboxComponent): void { + searchbox.setSuggestions(this.getSuggestion(searchbox.model)); + } + + private getSuggestion(value: string): Array { + const options: Array = value + ? [ + { + label: value + '@example.com', + url: `${this.baseUrl}assets/food/cake.png` + }, + { + label: value + '@example.com', + url: `${this.baseUrl}assets/food/coffee.png` + }, + { + label: value + '@example.com', + url: `${this.baseUrl}assets/food/cola.png` + } + ] + : []; + + return options; + } +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxTestComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxTestComponent.ts new file mode 100644 index 0000000..f237f23 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxTestComponent.ts @@ -0,0 +1,58 @@ +import { Component } from '@angular/core'; +import { TiSearchboxComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './searchbox-test.html' +}) +export class SearchboxTestComponent { + value: string = ''; + disabled: boolean = true; + value1: string = '2234244'; + placeholderText: string = '请输入关键字'; + maxlength: number = 10; + myLogs: Array = []; + private static getSuggestion(value: string): Array { + const options: Array = value + ? [ + { + label: value + '1' + }, + { + label: value + value + }, + { + label: value + value + value + } + ] + : [ + { + label: 'a' + }, + { + label: 'b' + }, + { + label: 'c' + } + ]; + + return options; + } + changeSearch(): void { + this.value1 = 'aaa'; + } + onFocus(): any { + this.myLogs = [...this.myLogs, 'on focus']; + } + onBlur(): any { + this.myLogs = [...this.myLogs, 'on blur']; + } + search = (value: string): any => { + this.myLogs = [...this.myLogs, `search value: ${value}`]; + }; + onInputChange(searchbox: TiSearchboxComponent): void { + setTimeout(() => { + searchbox.setSuggestions(SearchboxTestComponent.getSuggestion(searchbox.model)); + }, 500); + } +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxTestModule.ts b/src/searchbox/demo/src/app/searchbox/SearchboxTestModule.ts new file mode 100644 index 0000000..dec4b65 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxTestModule.ts @@ -0,0 +1,115 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiButtonModule, TiSearchboxModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { SearchboxTestComponent } from './SearchboxTestComponent'; +import { SearchboxNotsearchComponent } from './SearchboxNotsearchComponent'; +import { SearchboxBasicComponent } from './SearchboxBasicComponent'; +import { SearchboxEventComponent } from './SearchboxEventComponent'; +import { SearchboxPanelsizeComponent } from './SearchboxPanelsizeComponent'; +import { SearchboxTrimmedComponent } from './SearchboxTrimmedComponent'; +import { SearchboxValidComponent } from './SearchboxValidComponent'; +import { SearchboxDisabledComponent } from './SearchboxDisabledComponent'; +import { SearchboxAppendtobodyComponent } from './SearchboxAppendtobodyComponent'; +import { SearchboxReactiveComponent } from './SearchboxReactiveComponent'; +import { SearchboxOptionsComponent } from './SearchboxOptionsComponent'; +import { SearchboxSuggestComponent } from './SearchboxSuggestComponent'; +import { SearchboxMaxlengthComponent } from './SearchboxMaxlengthComponent'; +import { SearchboxTemplateComponent } from './SearchboxTemplateComponent'; +import { SearchboxVirtualscrollComponent } from './SearchboxVirtualscrollComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiButtonModule, + TiSearchboxModule, + TiValidationModule, + DemoLogModule, + RouterModule.forChild(SearchboxTestModule.ROUTES) + ], + declarations: [ + SearchboxBasicComponent, + SearchboxSuggestComponent, + SearchboxPanelsizeComponent, + SearchboxTrimmedComponent, + SearchboxValidComponent, + SearchboxDisabledComponent, + SearchboxEventComponent, + SearchboxNotsearchComponent, + SearchboxAppendtobodyComponent, + SearchboxTestComponent, + SearchboxReactiveComponent, + SearchboxOptionsComponent, + SearchboxMaxlengthComponent, + SearchboxTemplateComponent, + SearchboxVirtualscrollComponent + ] +}) +export class SearchboxTestModule { + static readonly LINKS: Array = [{ href: 'components/TiSearchboxComponent.html', label: 'Searchbox' }]; + static readonly ROUTES: Routes = [ + { + path: 'searchbox/searchbox-basic', + component: SearchboxBasicComponent + }, + { + path: 'searchbox/searchbox-template', + component: SearchboxTemplateComponent + }, + { + path: 'searchbox/searchbox-reactive', + component: SearchboxReactiveComponent + }, + { + path: 'searchbox/searchbox-options', + component: SearchboxOptionsComponent + }, + { + path: 'searchbox/searchbox-suggest', + component: SearchboxSuggestComponent + }, + { + path: 'searchbox/searchbox-maxlength', + component: SearchboxMaxlengthComponent + }, + { + path: 'searchbox/searchbox-panelsize', + component: SearchboxPanelsizeComponent + }, + { + path: 'searchbox/searchbox-trimmed', + component: SearchboxTrimmedComponent + }, + { + path: 'searchbox/searchbox-valid', + component: SearchboxValidComponent + }, + { + path: 'searchbox/searchbox-virtualscroll', + component: SearchboxVirtualscrollComponent + }, + { + path: 'searchbox/searchbox-disabled', + component: SearchboxDisabledComponent + }, + { + path: 'searchbox/searchbox-event', + component: SearchboxEventComponent + }, + { + path: 'searchbox/searchbox-appendtobody', + component: SearchboxAppendtobodyComponent + }, + { + path: 'searchbox/searchbox-notsearch', + component: SearchboxNotsearchComponent + }, + { path: 'searchbox/searchbox-test', component: SearchboxTestComponent } + ]; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxTrimmedComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxTrimmedComponent.ts new file mode 100644 index 0000000..be5bc88 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxTrimmedComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-trimmed.html' +}) +export class SearchboxTrimmedComponent { + value: string; + searchContent: string = ''; + onSearch(value: string): void { + this.searchContent = value; + } + onModelChange(value: string): void { + this.searchContent = value; + } +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxValidComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxValidComponent.ts new file mode 100644 index 0000000..6abe999 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxValidComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './searchbox-valid.html' +}) +export class SearchboxValidComponent { + value: string = ''; +} diff --git a/src/searchbox/demo/src/app/searchbox/SearchboxVirtualscrollComponent.ts b/src/searchbox/demo/src/app/searchbox/SearchboxVirtualscrollComponent.ts new file mode 100644 index 0000000..0bb60c6 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/SearchboxVirtualscrollComponent.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; +import { TiSearchboxComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './searchbox-virtualscroll.html' +}) +export class SearchboxVirtualscrollComponent { + value: string = ''; + suggestions: Array = [ + { + label: '华北' + }, + { + label: '华南' + }, + { + label: '西北' + }, + { + label: '西南' + } + ]; + + onSuggest(searchbox: TiSearchboxComponent): void { + // 模拟后台异步请求 + setTimeout(() => { + searchbox.setSuggestions(this.getSuggestion(searchbox.model)); + }, 200); + } + + private getSuggestion(value: string): Array { + const options: Array = []; + + for (let i: number = 0; i < 10000; i++) { + const item: any = this.suggestions[i % 4]; + options.push({ label: `${item.label}${i}${value}` }); + } + + return options; + } +} diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-appendtobody.html b/src/searchbox/demo/src/app/searchbox/searchbox-appendtobody.html new file mode 100644 index 0000000..bbcb727 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-appendtobody.html @@ -0,0 +1,7 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-basic.html b/src/searchbox/demo/src/app/searchbox/searchbox-basic.html new file mode 100644 index 0000000..8337200 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-basic.html @@ -0,0 +1 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-disabled.html b/src/searchbox/demo/src/app/searchbox/searchbox-disabled.html new file mode 100644 index 0000000..3a4297d --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-disabled.html @@ -0,0 +1 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-event.html b/src/searchbox/demo/src/app/searchbox/searchbox-event.html new file mode 100644 index 0000000..a207a9d --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-event.html @@ -0,0 +1,14 @@ + + + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-maxlength.html b/src/searchbox/demo/src/app/searchbox/searchbox-maxlength.html new file mode 100644 index 0000000..2204d13 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-maxlength.html @@ -0,0 +1 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-notsearch.html b/src/searchbox/demo/src/app/searchbox/searchbox-notsearch.html new file mode 100644 index 0000000..1e6fa98 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-notsearch.html @@ -0,0 +1,7 @@ +

    描述

    +

    下拉组件中的搜索框组件,设置noBorder属性

    + + +
    +

    事件日志:

    + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-options.html b/src/searchbox/demo/src/app/searchbox/searchbox-options.html new file mode 100644 index 0000000..837cda5 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-options.html @@ -0,0 +1,5 @@ + + +
    +
    Current Value: {{ value }}
    +
    diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-panelsize.html b/src/searchbox/demo/src/app/searchbox/searchbox-panelsize.html new file mode 100644 index 0000000..f13f01a --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-panelsize.html @@ -0,0 +1,8 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-reactive.html b/src/searchbox/demo/src/app/searchbox/searchbox-reactive.html new file mode 100644 index 0000000..c26ff07 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-reactive.html @@ -0,0 +1 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-suggest.html b/src/searchbox/demo/src/app/searchbox/searchbox-suggest.html new file mode 100644 index 0000000..6ff5730 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-suggest.html @@ -0,0 +1,5 @@ + + +
    +
    Current Value: {{ value }}
    +
    diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-template.html b/src/searchbox/demo/src/app/searchbox/searchbox-template.html new file mode 100644 index 0000000..a726933 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-template.html @@ -0,0 +1,8 @@ + + + {{i}}{{item.label}} + + +
    +
    Current Value: {{ value }}
    +
    diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-test.html b/src/searchbox/demo/src/app/searchbox/searchbox-test.html new file mode 100644 index 0000000..e024043 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-test.html @@ -0,0 +1,37 @@ +

    描述

    +

    Searchbox组件内部测试示例

    +

    示例

    +
    +

    用在表单中(第一个form)--模板驱动表单

    +
    +
    +
    0. 1)不展示下拉建议项,仅在回车键及点击事件后触发搜索,弹出框展示搜索内容;2)focus/blur时做日志打印; 3) 最大字符长度为10
    +
    + + {{value}}

    + +
    1.根据输入实时搜索内容:
    +
    + + {{value1}} +
    +

    form's touched: {{form.touched | json}}

    +

    form's untouched: {{form.untouched | json}}

    +
    {{form.value | json}}
    + +
    +

    事件日志:

    + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-trimmed.html b/src/searchbox/demo/src/app/searchbox/searchbox-trimmed.html new file mode 100644 index 0000000..4122e63 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-trimmed.html @@ -0,0 +1,13 @@ + + +
    +
    Current Value: {{ searchContent }}
    +
    Value length: {{ searchContent.length }}
    +
    diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-valid.html b/src/searchbox/demo/src/app/searchbox/searchbox-valid.html new file mode 100644 index 0000000..b0dc476 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-valid.html @@ -0,0 +1 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/searchbox-virtualscroll.html b/src/searchbox/demo/src/app/searchbox/searchbox-virtualscroll.html new file mode 100644 index 0000000..6ad67bd --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/searchbox-virtualscroll.html @@ -0,0 +1 @@ + diff --git a/src/searchbox/demo/src/app/searchbox/webdoc/searchbox-demos.js b/src/searchbox/demo/src/app/searchbox/webdoc/searchbox-demos.js new file mode 100644 index 0000000..ae1fc9a --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/webdoc/searchbox-demos.js @@ -0,0 +1,169 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'searchbox-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': '

    Searchbox 组件的最简用法。

    ', + 'en-US': '

    button color

    ' + }, + apis: ['TiSearchboxComponent.properties.placeholder'] + }, + { + demoId: 'searchbox-template', + name: { + 'zh-CN': '下拉建议项模板', + 'en-US': 'template' + }, + desc: { + 'zh-CN': '

    自定义下拉建议项模板。

    ', + 'en-US': '

    button color

    ' + }, + apis: ['TiSearchboxComponent.slots.itemTemplate'] + }, + { + demoId: 'searchbox-trimmed', + name: { + 'zh-CN': '删除搜索内容的首尾空格', + 'en-US': 'trimmed' + }, + desc: { + 'zh-CN': '

    通过trimmed配置是否删除用户搜索内容的首尾空格,不配置时不删除空格。

    ', + 'en-US': '

    trimmed

    ' + }, + apis: ['TiSearchboxComponent.properties.trimmed'] + }, + { + demoId: 'searchbox-options', + name: { + 'zh-CN': '固定下拉建议项', + 'en-US': 'options' + }, + desc: { + 'zh-CN': '

    通过options配置下拉建议项。

    ', + 'en-US': '

    options

    ' + }, + apis: ['TiSearchboxComponent.properties.options'] + }, + { + demoId: 'searchbox-appendtobody', + name: { + 'zh-CN': '附着在body上', + 'en-US': 'appendtobody' + }, + desc: { + 'zh-CN': + '

    通过appendToBody配置下拉面板是否添加在 body 上,在有局部滚动条的场景下,如果配置成 false,下拉面板和选择框不会分离。

    ', + 'en-US': '

    appendtobody

    ' + }, + apis: ['TiSearchboxComponent.properties.appendToBody'] + }, + { + demoId: 'searchbox-suggest', + name: { + 'zh-CN': '动态加载下拉项', + 'en-US': 'item' + }, + desc: { + 'zh-CN': '

    通过suggest事件配置当聚焦或值改变时触发事件,为开发者提供设置建议项的时机。

    ', + 'en-US': '

    item

    ' + }, + apis: ['TiSearchboxComponent.events.suggest', 'TiSearchboxComponent.methods.setSuggestions'] + }, + { + demoId: 'searchbox-maxlength', + name: { + 'zh-CN': '文本最大显示长度', + 'en-US': 'item' + }, + desc: { + 'zh-CN': '

    通过maxlength配置最大文本输入长度。

    ', + 'en-US': '

    item

    ' + }, + apis: ['TiSearchboxComponent.properties.maxlength'] + }, + { + demoId: 'searchbox-panelsize', + name: { + 'zh-CN': '下拉面板大小', + 'en-US': 'item' + }, + desc: { + 'zh-CN': '

    通过panelWidth配置下拉框的宽度。通过panelMaxHeight配置面板最大高度。

    ', + 'en-US': '

    item

    ' + }, + apis: ['TiSearchboxComponent.properties.panelWidth', 'TiSearchboxComponent.properties.panelMaxHeight'] + }, + { + demoId: 'searchbox-reactive', + name: { + 'zh-CN': '响应式表单', + 'en-US': 'item' + }, + desc: { + 'zh-CN': '

    响应式表单的基本用法。

    ', + 'en-US': '

    item

    ' + }, + apis: ['TiSearchboxComponent.properties.placeholder'] + }, + { + demoId: 'searchbox-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'item' + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': '

    item

    ' + }, + apis: ['TiSearchboxComponent.properties.disabled'] + }, + { + demoId: 'searchbox-valid', + name: { + 'zh-CN': '表单校验', + 'en-US': 'item' + }, + desc: { + 'zh-CN': '

    表单校验的用法。

    ', + 'en-US': '

    item

    ' + } + }, + { + demoId: 'searchbox-virtualscroll', + name: { + 'zh-CN': '虚拟滚动', + 'en-US': 'virtualscroll' + }, + desc: { + 'zh-CN': '

    通过属性virtual配置组件是否开启虚拟滚动,一般用于下拉数据量大的场景。

    ', + 'en-US': '' + }, + apis: ['TiSearchboxComponent.properties.virtual'] + }, + { + demoId: 'searchbox-event', + name: { + 'zh-CN': '事件', + 'en-US': 'item' + }, + desc: { + 'zh-CN': + '

    当输入框聚焦按下 enter 键、点击下拉项、点击搜索按钮的时候触发search事件;当点击叉号的时候触发clear事件;当点击下拉项的时候触发select事件;当聚焦的时候触发focus事件;当失焦的时候触发blur事件。

    ', + 'en-US': '

    item

    ' + }, + apis: [ + 'TiSearchboxComponent.events.search', + 'TiSearchboxComponent.events.clear', + 'TiSearchboxComponent.events.select', + 'TiSearchboxComponent.events.blur', + 'TiSearchboxComponent.events.focus' + ] + } + ] +}; diff --git a/src/searchbox/demo/src/app/searchbox/webdoc/searchbox.cn.md b/src/searchbox/demo/src/app/searchbox/webdoc/searchbox.cn.md new file mode 100644 index 0000000..fa15e72 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/webdoc/searchbox.cn.md @@ -0,0 +1,23 @@ +--- +title: Searchbox 搜索框 +--- +# Searchbox 搜索框 + +
    + +Searchbox 是提供搜索数据的组件。   + +```typescript +import { TiSearchboxModule } from '@opentiny/ng'; +``` + +
    + +
    + +Searchbox 是提供搜索数据的组件。   + +```typescript +import { TiSearchboxModule } from '@opentiny/ng'; +``` +
    diff --git a/src/searchbox/demo/src/app/searchbox/webdoc/searchbox.en.md b/src/searchbox/demo/src/app/searchbox/webdoc/searchbox.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/searchbox/demo/src/app/searchbox/webdoc/searchbox.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/searchbox/demo/src/favicon.ico b/src/searchbox/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/searchbox/demo/src/index.html b/src/searchbox/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/searchbox/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/searchbox/demo/src/main.ts b/src/searchbox/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/searchbox/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/searchbox/demo/test.ts b/src/searchbox/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/searchbox/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/searchbox/demo/tsconfig.app.json b/src/searchbox/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/searchbox/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/searchbox/demo/tsconfig.spec.json b/src/searchbox/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/searchbox/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/searchbox/lib/index.ts b/src/searchbox/lib/index.ts new file mode 100644 index 0000000..3db3e3f --- /dev/null +++ b/src/searchbox/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSearchboxModule'; diff --git a/src/searchbox/lib/ng-package.json b/src/searchbox/lib/ng-package.json new file mode 100644 index 0000000..5f866fa --- /dev/null +++ b/src/searchbox/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/searchbox", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/searchbox/lib/package.json b/src/searchbox/lib/package.json new file mode 100644 index 0000000..69404bb --- /dev/null +++ b/src/searchbox/lib/package.json @@ -0,0 +1,18 @@ +{ + "name": "@opentiny/ng-searchbox", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-autocomplete": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-droplist": "~1.0.0-beta.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/searchbox/lib/project.json b/src/searchbox/lib/project.json new file mode 100644 index 0000000..9f7088d --- /dev/null +++ b/src/searchbox/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/searchbox/lib", + "sourceRoot": "src/searchbox/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/searchbox"], + "options": { + "project": "src/searchbox/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/searchbox"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js searchbox" + }, + { + "command": "ng default-build searchbox" + }, + { + "command": "node build/clear-default-theme.js searchbox" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/searchbox && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build searchbox && ng pack searchbox && node build/publish.js searchbox --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/searchbox/lib/src/TiSearchboxComponent.ts b/src/searchbox/lib/src/TiSearchboxComponent.ts new file mode 100644 index 0000000..ac4f18a --- /dev/null +++ b/src/searchbox/lib/src/TiSearchboxComponent.ts @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from '@angular/core'; +import { TiAutocompleteComponent } from '@opentiny/ng-autocomplete'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * 搜索框组件 + * + */ +@Component({ + selector: 'ti-searchbox', + templateUrl: './searchbox.html', + styleUrls: ['./searchbox.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiSearchboxComponent)], + host: { + '[class.ti3-searchbox-container]': 'true', + '(blur)': 'onBlur()', + '(focus)': 'onFocus()' + } +}) +export class TiSearchboxComponent extends TiAutocompleteComponent { + /** + * 是否删除搜索内容的首尾空格 + */ + @Input() trimmed: boolean = false; + /** + * 是否开启虚拟滚动 + */ + @Input() virtual: boolean = false; + /** + * 当选中下拉选项、按 enter 键或者点击搜索图标时触发的回调,参数:搜索内容 + */ + @Output() readonly search: EventEmitter = new EventEmitter(); + + /** + * @ignore + * input ngModel绑定值 + */ + public inputValue: string; + private isDroplistSearch: boolean = false; // 是否是droplist触发搜索 + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + // 默认有清除图标且不可配置:此处先赋值后继承,是由于父类中初始化需要读取clearable的值,设置清除图标 + this.clearable = true; + super.ngOnInit(); + } + + /** + * @ignore + * 组件model更改时,更新input绑定值 + */ + writeValue(model: string) { + super.writeValue(model); + this.inputValue = model; + } + + // 组件交互方法集合--start + + /** + * @ignore + * enter的触发有两种情况: + * 1.在input框中输入值,然后按下enter,此时正常执行search + * 2.在suggestion面板展开的情况下,通过hover选中一项,然后按下enter, + * 此时的search应该在下拉面板的onDroplistChange回调中触发 + */ + public onInputEnter(): void { + if (this.isDroplistSearch) { + // 排除第2中情况 + this.isDroplistSearch = false; + + return; + } + + this.onSearch(); + } + /** + * @ignore + * 搜索事件触发 + */ + public onSearch(): void { + if (this.disabled) { + return; + } + this.search.emit(this.model); + } + /** + * @ignore + * 两种情况下触发 + * 1.在suggestion面板展开的情况下,通过hover选中一项,然后按下enter + * 2.在suggestion面板展开的情况下,通过鼠标点击选中一项 + */ + onDroplistChange(value: { label: string }): void { + if (value) { + this.model = this.inputValue = value[this.labelKey] || value.label; + this.onSearch(); + this.isDroplistSearch = true; + } + } + // 组件交互方法集合--end + + /** + * @ignore + * 输入框中内容改变事件 + */ + public onInputChange(value: string): void { + if (this.disabled || !this.isFocused) { + return; + } + + const searchValue: string = this.trimValue(value); + this.model = searchValue; + this.inputChangeObserve.next(searchValue); + } + + /** + * 控制是否删除搜索内容的首尾空格 + */ + private trimValue(value: string): string { + let searchValue: string = value; + if (this.trimmed && searchValue) { + searchValue = searchValue.trim(); + } + + return searchValue; + } +} diff --git a/src/searchbox/lib/src/TiSearchboxModule.ts b/src/searchbox/lib/src/TiSearchboxModule.ts new file mode 100644 index 0000000..af6a686 --- /dev/null +++ b/src/searchbox/lib/src/TiSearchboxModule.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiSearchboxComponent } from './TiSearchboxComponent'; +import { FormsModule } from '@angular/forms'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiDroplistModule } from '@opentiny/ng-droplist'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiSearchboxNotsearchComponent } from './TiSearchboxNotsearchComponent'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiOverflowModule, TiDroplistModule, TiIconModule, TiTextModule, TiOutlineModule], + exports: [TiSearchboxComponent, TiSearchboxNotsearchComponent], + declarations: [TiSearchboxComponent, TiSearchboxNotsearchComponent] +}) +export class TiSearchboxModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiSearchboxComponent } from './TiSearchboxComponent'; +export { TiSearchboxNotsearchComponent } from './TiSearchboxNotsearchComponent'; diff --git a/src/searchbox/lib/src/TiSearchboxNotsearchComponent.ts b/src/searchbox/lib/src/TiSearchboxNotsearchComponent.ts new file mode 100644 index 0000000..0ad044a --- /dev/null +++ b/src/searchbox/lib/src/TiSearchboxNotsearchComponent.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { TiSearchboxComponent } from './TiSearchboxComponent'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiLocale } from '@opentiny/ng-locale'; + +/** + * @ignore + */ +@Component({ + selector: 'ti-searchbox-notsearch', + templateUrl: './searchbox.html', + styleUrls: ['./searchbox.less', './searchbox-notsearch.less', '../../../text/lib/src/text.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiSearchboxNotsearchComponent)], + host: { + '[class.ti3-searchbox-container]': 'true', + '(blur)': 'onBlur()', + '(focus)': 'onFocus()' + } +}) +export class TiSearchboxNotsearchComponent extends TiSearchboxComponent { + /** + * @ignore + */ + public placeholder: string = TiLocale.getLocaleWords().tiSearchbox.search; + onInputEnter(): void { + event.preventDefault(); // 不做搜索等处理 + } + onSearch(): void { + event.preventDefault(); // 点击搜索图标时,阻止默认行为 + } +} diff --git a/src/searchbox/lib/src/i18n/TiSearchboxWords.ts b/src/searchbox/lib/src/i18n/TiSearchboxWords.ts new file mode 100644 index 0000000..63a1bf1 --- /dev/null +++ b/src/searchbox/lib/src/i18n/TiSearchboxWords.ts @@ -0,0 +1,5 @@ +export interface TiSearchboxWords { + tiSearchbox: { + search: string; + }; +} diff --git a/src/searchbox/lib/src/i18n/en_US.ts b/src/searchbox/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..9f8e65d --- /dev/null +++ b/src/searchbox/lib/src/i18n/en_US.ts @@ -0,0 +1,7 @@ +import { TiSearchboxWords } from './TiSearchboxWords'; + +export const en_US: TiSearchboxWords = { + tiSearchbox: { + search: 'Search' + } +}; diff --git a/src/searchbox/lib/src/i18n/es_US.ts b/src/searchbox/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..b0bd5f5 --- /dev/null +++ b/src/searchbox/lib/src/i18n/es_US.ts @@ -0,0 +1,7 @@ +import { TiSearchboxWords } from './TiSearchboxWords'; + +export const es_US: TiSearchboxWords = { + tiSearchbox: { + search: 'Buscar' + } +}; diff --git a/src/searchbox/lib/src/i18n/fr_FR.ts b/src/searchbox/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..37af243 --- /dev/null +++ b/src/searchbox/lib/src/i18n/fr_FR.ts @@ -0,0 +1,7 @@ +import { TiSearchboxWords } from './TiSearchboxWords'; + +export const fr_FR: TiSearchboxWords = { + tiSearchbox: { + search: 'Recherchez' + } +}; diff --git a/src/searchbox/lib/src/i18n/index.ts b/src/searchbox/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/searchbox/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/searchbox/lib/src/i18n/pt_BR.ts b/src/searchbox/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..bf58af4 --- /dev/null +++ b/src/searchbox/lib/src/i18n/pt_BR.ts @@ -0,0 +1,7 @@ +import { TiSearchboxWords } from './TiSearchboxWords'; + +export const pt_BR: TiSearchboxWords = { + tiSearchbox: { + search: 'Pesquisar' + } +}; diff --git a/src/searchbox/lib/src/i18n/zh_CN.ts b/src/searchbox/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..011fd08 --- /dev/null +++ b/src/searchbox/lib/src/i18n/zh_CN.ts @@ -0,0 +1,7 @@ +import { TiSearchboxWords } from './TiSearchboxWords'; + +export const zh_CN: TiSearchboxWords = { + tiSearchbox: { + search: '搜索' + } +}; diff --git a/src/searchbox/lib/src/searchbox-notsearch.less b/src/searchbox/lib/src/searchbox-notsearch.less new file mode 100644 index 0000000..0b581db --- /dev/null +++ b/src/searchbox/lib/src/searchbox-notsearch.less @@ -0,0 +1,26 @@ +@import '../../../themes/basic/base-all.less'; +// 定义下拉组件中搜索框的样式 +:host.ti3-searchbox-container:not([disabled])[noBorder] { + background-color: var(--ti-common-color-bg-white-emphasize); + border: none; + &:hover { + background-color: var(--ti-common-color-bg-normal); + } + .ti3-searchbox-input { + background-color: transparent; + } +} +:host.ti3-searchbox-container:not([disabled]) .ti3-searchbox-search:hover { + // 覆盖./searchbox.less中的鼠标悬浮效果 + color: var(--ti-common-color-icon-normal); + cursor: auto; +} + +:host ::ng-deep .ti3-searchbox-search[noBorder] { + color: var(--ti-common-color-icon-normal) !important; + &:hover { + color: var(--ti-common-color-icon-hover) !important; + } +} +// CSS标准中 /deep/ >>>刺穿Shadow DOM, 已废弃。所以,这里暂时用::ng-deep angular关键词,以兼容未来可能其他标准。 +// https://blog.csdn.net/sky_sunshine_x/article/details/80622617 diff --git a/src/searchbox/lib/src/searchbox.html b/src/searchbox/lib/src/searchbox.html new file mode 100644 index 0000000..5781f0f --- /dev/null +++ b/src/searchbox/lib/src/searchbox.html @@ -0,0 +1,47 @@ + +
    + + + + + + + {{item[labelKey]}} + + diff --git a/src/searchbox/lib/src/searchbox.less b/src/searchbox/lib/src/searchbox.less new file mode 100644 index 0000000..fcebf7b --- /dev/null +++ b/src/searchbox/lib/src/searchbox.less @@ -0,0 +1,101 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-searchbox-container-height: var(--ti-common-size-7x); + --ti-searchbox-icon-size: 14px; + --ti-searchbox-icon-area-width: 34px; + --ti-searchbox-divider-width: 1px; + --ti-searchbox-divider-height: calc(var(--ti-searchbox-container-height) / 2); +} + +// 1.先定义组件通用样式,与hover、focused、disabled等状态无关; +:host.ti3-searchbox-container { + position: relative; + border-radius: var(--ti-common-border-radius-normal); + width: var(--ti-common-size-50x); + height: var(--ti-searchbox-container-height); + line-height: normal; + display: inline-block; + vertical-align: middle; + text-align: left; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid); + .box-sizing(border-box); + cursor: pointer; + &:after { + content: ''; + display: inline-block; + height: 100%; + line-height: 100%; + vertical-align: middle; + } + & .ti3-searchbox-input { + // 覆盖ti-text组件默认样式 + height: 100%; + width: calc(100% - var(--ti-searchbox-icon-area-width) - var(--ti-searchbox-divider-width)); + box-sizing: border-box; + border: none !important; // 加important是为了避免被ti-text已有的disable状态下的border样式覆盖 + float: left; + padding: 0 0 0 var(--ti-common-space-10); + text-decoration: none; + outline: none; + box-shadow: none; + background-color: transparent; // 添加透明背景是为了覆盖tiText设置的白色背景,使校验结果样式生效 + &[disabled] { + background-color: transparent; // 添加透明背景是为了覆盖tiText设置的禁用背景,避免配置不一致呈现色块 + } + } + & .ti3-searchbox-divider { + position: absolute; + right: var(--ti-searchbox-icon-area-width); + vertical-align: middle; + height: var(--ti-searchbox-divider-height); + top: calc(50% - var(--ti-searchbox-divider-height) / 2); + width: var(--ti-searchbox-divider-width); + } + & .ti3-searchbox-search { + width: var(--ti-searchbox-icon-area-width); + border-radius: 0 3px 3px 0; + height: 100%; + float: right; + text-align: center; + font-size: var(--ti-searchbox-icon-size); + &:before { + position: relative; + top: calc(50% - var(--ti-searchbox-icon-size) / 2); + } + } +} + +// 2.定义enabled状态下的样式,包括hover、focused等状态 +:host.ti3-searchbox-container:not([disabled]) { + background-color: var(--ti-common-color-bg-white-normal); + border-color: var(--ti-common-color-line-normal); + &:hover { + border-color: var(--ti-common-color-line-hover); + } + &[tiFocused] { + border-color: var(--ti-common-color-line-active); + } + & .ti3-searchbox-divider { + background-color: var(--ti-common-color-line-dividing); + } + & .ti3-searchbox-search { + color: var(--ti-common-color-icon-normal); + &:hover { + color: var(--ti-common-color-icon-hover); + } + } +} + +// 3.定义disabled状态下的样式 +:host.ti3-searchbox-container[disabled] { + background-color: var(--ti-common-color-bg-disabled); + border-color: var(--ti-common-color-line-disabled); + cursor: not-allowed; + & .ti3-searchbox-divider { + background-color: var(--ti-common-color-line-disabled); + } + & .ti3-searchbox-search { + color: var(--ti-common-color-icon-disabled); + } +} diff --git a/src/select/demo/karma.conf.js b/src/select/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/select/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/select/demo/project.json b/src/select/demo/project.json new file mode 100644 index 0000000..34bdc8c --- /dev/null +++ b/src/select/demo/project.json @@ -0,0 +1,98 @@ +{ + "projectType": "application", + "root": "src/select/demo", + "sourceRoot": "src/select/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/select", + "index": "src/select/demo/src/index.html", + "main": "src/select/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/select/demo/tsconfig.app.json", + "assets": [ + "src/select/demo/src/favicon.ico", + "src/select/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/food", + "output": "/assets/food/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "select-demo:build:production" + }, + "development": { + "browserTarget": "select-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js select" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/select/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/select/demo/tsconfig.spec.json", + "karmaConfig": "src/select/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/select/demo/src/app/AppComponent.ts b/src/select/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/select/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/select/demo/src/app/AppModule.ts b/src/select/demo/src/app/AppModule.ts new file mode 100644 index 0000000..273c010 --- /dev/null +++ b/src/select/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SelectTestModule } from './select/SelectTestModule'; + +@NgModule({ + imports: [ + SelectTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/select/demo/src/app/IndexComponent.ts b/src/select/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..f24dbd5 --- /dev/null +++ b/src/select/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SelectTestModule } from './select/SelectTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SelectTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/select/demo/src/app/app.html b/src/select/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/select/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/select/demo/src/app/select/NoEmptyPipe.ts b/src/select/demo/src/app/select/NoEmptyPipe.ts new file mode 100644 index 0000000..e4893e6 --- /dev/null +++ b/src/select/demo/src/app/select/NoEmptyPipe.ts @@ -0,0 +1,9 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { Util } from '@opentiny/ng'; + +@Pipe({ name: 'noempty' }) +export class NoEmptyPipe implements PipeTransform { + transform(value: Array): Array { + return value.filter((item: any) => !Util.isEmptyString(item.label)); + } +} diff --git a/src/select/demo/src/app/select/SelectAppendtobodyComponent.ts b/src/select/demo/src/app/select/SelectAppendtobodyComponent.ts new file mode 100644 index 0000000..af76c42 --- /dev/null +++ b/src/select/demo/src/app/select/SelectAppendtobodyComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './select-appendtobody.html' +}) +export class SelectAppendtobodyComponent { + options: Array = [{ label: '美国' }, { label: '巴西' }, { label: '加拿大' }, { label: '中国' }, { label: '法国' }]; + value: any; +} diff --git a/src/select/demo/src/app/select/SelectBasicComponent.ts b/src/select/demo/src/app/select/SelectBasicComponent.ts new file mode 100644 index 0000000..2f8b7ac --- /dev/null +++ b/src/select/demo/src/app/select/SelectBasicComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-basic.html' +}) +export class SelectBasicComponent { + options: Array = [ + { + label: '中国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + } + ]; + + value: any = this.options[0]; +} diff --git a/src/select/demo/src/app/select/SelectBeforesearchComponent.ts b/src/select/demo/src/app/select/SelectBeforesearchComponent.ts new file mode 100644 index 0000000..6aa13d9 --- /dev/null +++ b/src/select/demo/src/app/select/SelectBeforesearchComponent.ts @@ -0,0 +1,60 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiSelectComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './select-beforesearch.html' +}) +export class SelectBeforesearchComponent { + constructor(private changeDetectorRef: ChangeDetectorRef) {} + options: Array = []; + value: any; + baseOptions: Array = [ + { label: '美国' }, + { label: '巴西' }, + { label: '加拿大' }, + { label: '中国' }, + { label: '法国' }, + { label: '德国' }, + { label: '日本' }, + { label: '韩国' }, + { label: '土耳其' }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + ngOnInit(): void { + this.options = [...this.baseOptions].slice(0, 3); + this.value = this.options[2]; + } + + onBeforeSearch(selectComp: TiSelectComponent): void { + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + this.getData(this.baseOptions, searchWord).then((result: any) => { + // 设置搜索结果 + this.options = result.data; + }); + } + + // 模拟异步请求 + private getData(source: Array, searchWord: string): Promise { + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + if (searchWord) { + const filteredResult: Array = source.filter( + (item: any) => item.label.toLowerCase().indexOf(searchWord.toLowerCase()) !== -1 + ); + resolve({ data: filteredResult }); + } else { + resolve({ + data: [...source.slice(0, 3)] + }); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef.markForCheck() + // 默认模式下不需要 + this.changeDetectorRef.markForCheck(); + }, 500); + }); + } +} diff --git a/src/select/demo/src/app/select/SelectBeforesearchTestComponent.ts b/src/select/demo/src/app/select/SelectBeforesearchTestComponent.ts new file mode 100644 index 0000000..6cef607 --- /dev/null +++ b/src/select/demo/src/app/select/SelectBeforesearchTestComponent.ts @@ -0,0 +1,123 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiSelectComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './select-beforesearch.html' +}) +export class SelectBeforesearchTestComponent { + options: Array = []; + myOptions1: Array = []; + value: any; + mySelecteds1: Array; + + baseOptions: Array = [ + { label: '美国' }, + { label: '巴西' }, + { label: '加拿大' }, + { label: '中国' }, + { label: '法国' }, + { label: '德国' }, + { label: '日本' }, + { label: '韩国' }, + { label: '土耳其' }, + { label: '大不列颠和北爱兰联合王国' } + ]; + + baseOptions1: Array = [ + { label: '美国' }, + { + label: '巴西', + disabled: true + }, + { label: '加拿大' }, + { label: '中国' }, + { + label: '法国', + disabled: true + }, + { + label: '德国', + disabled: true + }, + { label: '日本' }, + { label: '韩国' }, + { + label: '土耳其', + disabled: true + }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + changedOptions: Array = [ + { label: 'America' }, + { label: 'Brazil' }, + { label: 'Canada' }, + { label: 'China' }, + { label: 'France' }, + { label: 'Germany' }, + { label: 'Japan' }, + { label: 'South Korea' }, + { label: 'Turkey' }, + { label: 'United Kingdom' } + ]; + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + ngOnInit(): void { + this.options = [...this.baseOptions].slice(0, 3); + this.myOptions1 = [...this.baseOptions1].slice(0, 3); + this.value = this.options[2]; + this.mySelecteds1 = [this.myOptions1[1], this.myOptions1[2]]; + } + + onBeforeSearch(selectComp: TiSelectComponent): void { + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + this.getData(this.baseOptions, searchWord).then((result: any) => { + // 设置搜索结果 + this.options = result.data; + }); + } + onBeforeSearch1(selectComp: TiSelectComponent): void { + const searchWord: string = selectComp.getSearchWord(); + this.getData(this.baseOptions1, searchWord).then((result: any) => { + // 设置搜索结果 + this.myOptions1 = result.data; + }); + } + + changeOptions(): void { + this.baseOptions = this.changedOptions; + this.options = [...this.baseOptions].slice(0, 3); + this.value = undefined; + } + + changeOptions1(): void { + this.baseOptions1 = this.changedOptions; + this.myOptions1 = [...this.baseOptions1].slice(0, 3); + this.mySelecteds1 = undefined; + } + + // 模拟异步请求 + private getData(source: Array, searchWord: string): Promise { + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + if (searchWord) { + const filteredResult: Array = source.filter( + (item: any) => item.label.toLowerCase().indexOf(searchWord.toLowerCase()) !== -1 + ); + resolve({ data: filteredResult }); + } else { + resolve({ + data: [...source.slice(0, 3)] + }); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef.markForCheck() + // 默认模式下不需要 + // 服务可根据自身变化检测策略决定是否使用该方法 + this.changeDetectorRef.markForCheck(); + }, 500); + }); + } +} diff --git a/src/select/demo/src/app/select/SelectChangeSelectallComponent.ts b/src/select/demo/src/app/select/SelectChangeSelectallComponent.ts new file mode 100644 index 0000000..c5fa83e --- /dev/null +++ b/src/select/demo/src/app/select/SelectChangeSelectallComponent.ts @@ -0,0 +1,126 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-change-selectall.html' +}) +export class SelectChangeSelectallComponent { + selectAll: boolean = true; + + myOptions: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + mySelecteds: any = [this.myOptions[3]]; + + myGroupOptions: Array = [ + { + label: '北美洲', + children: [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + } + ] + }, + { + label: '南美洲', + children: [ + { + label: '巴西', + englishname: 'Brazil', + disabled: false + } + ] + }, + { + label: '亚洲', + children: [ + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + } + ] + }, + { + label: '欧洲', + children: [ + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ] + } + ]; + myGroupSelecteds: any = [this.myGroupOptions[0].children[0], this.myGroupOptions[2].children[1]]; + + changeSelectAll() { + this.selectAll = !this.selectAll; + } +} diff --git a/src/select/demo/src/app/select/SelectClearableComponent.ts b/src/select/demo/src/app/select/SelectClearableComponent.ts new file mode 100644 index 0000000..3328a58 --- /dev/null +++ b/src/select/demo/src/app/select/SelectClearableComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-clearable.html' +}) +export class SelectClearableComponent { + options: Array = [ + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + } + ]; + + value: any = this.options[1]; + multiValue: Array = [this.options[1]]; + myLogs: Array = []; + onClear(): void { + this.myLogs = [...this.myLogs, `onClear()`]; + } +} diff --git a/src/select/demo/src/app/select/SelectDisabledComponent.ts b/src/select/demo/src/app/select/SelectDisabledComponent.ts new file mode 100644 index 0000000..f2fe400 --- /dev/null +++ b/src/select/demo/src/app/select/SelectDisabledComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-disabled.html' +}) +export class SelectDisabledComponent { + options: Array = [ + { + label: '巴西' + }, + { + label: '加拿大', + disabled: true + }, + { + label: '中国' + }, + { + label: '法国' + } + ]; + myOptions: Array = JSON.parse(JSON.stringify(this.options)); + value: any; + myValue: any = this.myOptions[2]; +} diff --git a/src/select/demo/src/app/select/SelectDisabledfocusComponent.ts b/src/select/demo/src/app/select/SelectDisabledfocusComponent.ts new file mode 100644 index 0000000..6bf942e --- /dev/null +++ b/src/select/demo/src/app/select/SelectDisabledfocusComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-disabledfocus.html' +}) +export class SelectDisabledfocusComponent { + options: Array = [ + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + } + ]; + + value: any; + myDisabled: boolean = true; + + changeDisabled(): void { + this.myDisabled = !this.myDisabled; + } +} diff --git a/src/select/demo/src/app/select/SelectEnumComponent.ts b/src/select/demo/src/app/select/SelectEnumComponent.ts new file mode 100644 index 0000000..230326d --- /dev/null +++ b/src/select/demo/src/app/select/SelectEnumComponent.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; + +enum EnglishName { + America, + Brazil, + Canada, + China, + France, + Germany, + Japan, + SouthKorea, + Turkey, + UnitedKingdom +} + +@Component({ + templateUrl: './select-enum.html' +}) +export class SelectEnumComponent { + myOptions: Array = [ + { + label: '美国', + englishname: EnglishName.America + }, + { + label: '巴西', + englishname: EnglishName.Brazil + }, + { + label: '加拿大', + englishname: EnglishName.Canada + }, + { + label: '中国', + englishname: EnglishName.China + }, + { + label: '法国', + englishname: EnglishName.France + }, + { + label: '德国', + englishname: EnglishName.Germany + }, + { + label: '日本', + englishname: EnglishName.Japan + }, + { + label: '韩国', + englishname: EnglishName.SouthKorea + }, + { + label: '土耳其', + englishname: EnglishName.Turkey + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: EnglishName.UnitedKingdom + } + ]; + + mySelected1: EnglishName; + mySelecteds: Array; +} diff --git a/src/select/demo/src/app/select/SelectEventComponent.ts b/src/select/demo/src/app/select/SelectEventComponent.ts new file mode 100644 index 0000000..7ed7329 --- /dev/null +++ b/src/select/demo/src/app/select/SelectEventComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-event.html' +}) +export class SelectEventComponent { + options: Array = [ + { label: '美国' }, + { label: '巴西' }, + { label: '加拿大' }, + { label: '中国' }, + { label: '法国' }, + { label: '德国' }, + { label: '日本' }, + { label: '韩国' }, + { label: '土耳其' }, + { label: '大不列颠和北爱兰联合王国' } + ]; + + value: any; + myLogs: Array = []; + + onSelect(option: any): void { + this.myLogs = [...this.myLogs, `onSelect() event=${JSON.stringify(option)}`]; + } +} diff --git a/src/select/demo/src/app/select/SelectFocusComponent.ts b/src/select/demo/src/app/select/SelectFocusComponent.ts new file mode 100644 index 0000000..573f04c --- /dev/null +++ b/src/select/demo/src/app/select/SelectFocusComponent.ts @@ -0,0 +1,53 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-focus.html' +}) +export class SelectFocusComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + mySelected: any; + + myDisabled: boolean = false; +} diff --git a/src/select/demo/src/app/select/SelectGroupComponent.ts b/src/select/demo/src/app/select/SelectGroupComponent.ts new file mode 100644 index 0000000..2a42457 --- /dev/null +++ b/src/select/demo/src/app/select/SelectGroupComponent.ts @@ -0,0 +1,63 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-group.html' +}) +export class SelectGroupComponent { + options: Array = [ + { + label: '北美洲', + children: [ + { + label: '美国', + disabled: true + }, + { + label: '加拿大' + } + ] + }, + { + label: '南美洲', + children: [ + { + label: '巴西' + } + ] + }, + { + label: '亚洲', + children: [ + { + label: '中国' + }, + { + label: '日本' + }, + { + label: '韩国' + } + ] + }, + { + label: '欧洲', + children: [ + { + label: '法国', + disabled: true + }, + { + label: '德国' + }, + { + label: '土耳其' + }, + { + label: '大不列颠和北爱兰联合王国' + } + ] + } + ]; + + value: any; +} diff --git a/src/select/demo/src/app/select/SelectIdComponent.ts b/src/select/demo/src/app/select/SelectIdComponent.ts new file mode 100644 index 0000000..bb49236 --- /dev/null +++ b/src/select/demo/src/app/select/SelectIdComponent.ts @@ -0,0 +1,161 @@ +import { Component, DoCheck, Inject } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +@Component({ + templateUrl: './select-id.html' +}) +export class SelectIdComponent implements DoCheck { + myOptions: Array = [ + { + label: '蛋糕', + englishname: 'Cake', + url: 'assets/food/cake.png' + }, + { + label: '咖啡', + englishname: 'Coffee', + url: 'assets/food/coffee.png' + }, + { + label: '可乐', + englishname: 'Cola', + url: 'assets/food/cola.png' + }, + { + label: '炸鸡', + englishname: 'Fried Chicken', + url: 'assets/food/fried_chicken.png' + }, + { + label: '薯条', + englishname: 'Fries', + url: 'assets/food/fries.png' + }, + { + label: '汉堡', + englishname: 'Hamburger', + url: 'assets/food/hamburger.png' + }, + { + label: '牛奶', + englishname: 'milk', + url: 'assets/food/milk.png' + }, + { + label: '披萨', + englishname: 'pizza', + url: 'assets/food/pizza.png' + } + ]; + + myGroupOptions: Array = [ + { + label: '北美洲', + children: [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + } + ] + }, + { + label: '南美洲', + children: [ + { + label: '巴西', + englishname: 'Brazil', + disabled: false + } + ] + }, + { + label: '亚洲', + children: [ + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + } + ] + }, + { + label: '欧洲', + children: [ + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ] + } + ]; + + mySelected: any = this.myOptions[0]; + myGroupSelected: any = this.myOptions[0]; + mySelecteds: any = [this.myOptions[2], this.myOptions[3]]; + + idExistMap: Map = new Map(); + ids: Array = [ + 'myselect1', + 'myselect1_dominator', + 'myselect1_dominator_input', + 'myselect1_dominator_btn', + 'myselect1_droplist', + 'myselect1_droplist_list', + 'myselect1_droplist_list_0', + 'myselect2_droplist_list_0', + 'myselect3_dominator_tag0', + 'myselect3_dominator_tag0_closeicon', + 'myselect3_droplist_list_3', + 'myselect4_droplist_searchbox', + 'myselect4_droplist_searchbox_input', + 'myselect5_dominator_input', + 'myselect5_droplist_list_0', + 'myselect5_droplist_list_0_0', + 'myselect5_droplist_list_2', + 'myselect5_droplist_list_2_0', + 'myselect5_droplist_list_2_1' + ]; + allIdExist: boolean = false; + + // 修复SSR报错:ERROR ReferenceError: document is not defined + constructor(@Inject(DOCUMENT) private document) {} + + ngDoCheck(): void { + this.allIdExist = true; + this.ids.forEach((id: string) => { + const idExist: boolean = this.document.getElementById(id) != undefined; + this.idExistMap.set(id, idExist); + if (!idExist) { + this.allIdExist = false; + } + }); + } +} diff --git a/src/select/demo/src/app/select/SelectIdkeyComponent.ts b/src/select/demo/src/app/select/SelectIdkeyComponent.ts new file mode 100644 index 0000000..64f441b --- /dev/null +++ b/src/select/demo/src/app/select/SelectIdkeyComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-idkey.html' +}) +export class SelectIdkeyComponent { + options: Array = [ + { value: 0, label: 'America' }, + { value: 1, label: 'Brazil' }, + { value: 2, label: 'Canada' }, + { value: 3, label: 'China' }, + { value: 30, label: 'China' }, + { value: 4, label: 'France' }, + { value: 5, label: 'Germany' }, + { value: 6, label: 'Japan' }, + { value: 7, label: 'South Korea' }, + { value: 8, label: 'Turkey' }, + { value: 9, label: 'United Kingdom' } + ]; + + value: any; +} diff --git a/src/select/demo/src/app/select/SelectInputComponent.ts b/src/select/demo/src/app/select/SelectInputComponent.ts new file mode 100644 index 0000000..4ab9583 --- /dev/null +++ b/src/select/demo/src/app/select/SelectInputComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-input.html' +}) +export class SelectInputComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + mySelected: any; +} diff --git a/src/select/demo/src/app/select/SelectLabelkeyComponent.ts b/src/select/demo/src/app/select/SelectLabelkeyComponent.ts new file mode 100644 index 0000000..3a3bd48 --- /dev/null +++ b/src/select/demo/src/app/select/SelectLabelkeyComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-labelkey.html' +}) +export class SelectLabelkeyComponent { + options: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + value: any; +} diff --git a/src/select/demo/src/app/select/SelectLazyComponent.ts b/src/select/demo/src/app/select/SelectLazyComponent.ts new file mode 100644 index 0000000..e115bce --- /dev/null +++ b/src/select/demo/src/app/select/SelectLazyComponent.ts @@ -0,0 +1,33 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiSelectComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './select-lazy.html' +}) +export class SelectLazyComponent { + constructor(protected changeDetectorRef: ChangeDetectorRef) {} + options: Array = []; + value: any; + onBeforeOpen(selectComp: TiSelectComponent): void { + // 模拟懒加载场景 + setTimeout(() => { + this.options = [ + { label: '美国' }, + { label: '巴西' }, + { label: '加拿大' }, + { label: '中国' }, + { label: '法国' }, + { label: '德国' }, + { label: '日本' }, + { label: '韩国' }, + { label: '土耳其' }, + { label: '大不列颠和北爱兰联合王国' } + ]; + + this.value = this.options[0]; + selectComp.open(); + // 主动触发脏值检测,更新数据 + this.changeDetectorRef.markForCheck(); + }, 300); + } +} diff --git a/src/select/demo/src/app/select/SelectLeakComponent.ts b/src/select/demo/src/app/select/SelectLeakComponent.ts new file mode 100644 index 0000000..5e89633 --- /dev/null +++ b/src/select/demo/src/app/select/SelectLeakComponent.ts @@ -0,0 +1,87 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +import { Component, Inject } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +declare let global: any; + +@Component({ + templateUrl: './select-leak.html' +}) +export class SelectLeakComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '巴西', + englishname: 'Brazil', + disabled: false + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + }, + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + mySelected: any; + + show: boolean = true; + + dropLen: number = 0; + // 修复SSR报错:ERROR ReferenceError: document is not defined + constructor(@Inject(DOCUMENT) private document) { + // 修复SSR问题:打开页面白屏一直转圈 + if (typeof global !== 'undefined') { + return; + } + } + + dropCount(): void { + setTimeout(() => { + this.dropLen = this.document.body.getElementsByTagName('ti-drop').length; + }, 0); + } + + ngOnInit(): void { + this.dropCount(); + } + + toggle(): void { + this.show = !this.show; + this.dropCount(); + } +} diff --git a/src/select/demo/src/app/select/SelectLoadComponent.ts b/src/select/demo/src/app/select/SelectLoadComponent.ts new file mode 100644 index 0000000..4f8be29 --- /dev/null +++ b/src/select/demo/src/app/select/SelectLoadComponent.ts @@ -0,0 +1,108 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-load.html' +}) +export class SelectLoadComponent { + private dataA: Array = [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '巴西', + englishname: 'Brazil', + disabled: false + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + }, + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + private dataB: Array = [ + { label: '水调歌头' }, + { label: '苏轼' }, + { label: '明月几时有?把酒问青天' }, + { label: '不知天上宫阙,今夕是何年。' }, + { label: '我欲乘风归去,又恐琼楼玉宇,高处不胜寒。' }, + { label: '起舞弄清影,何似在人间?' }, + { label: '转朱阁,低绮户,照无眠。' }, + { label: '不应有恨,何事长向别时圆?' }, + { label: '人有悲欢离合,月有阴晴圆缺,此事古难全。' }, + { label: '但愿人长久,千里共婵娟。' } + ]; + + myOptions: Array; + + mySelected: any; + + mySelecteds: any; + + // 每个组件都用下面六个函数,只改变函数内容 + changeUndefined(): void { + this.myOptions = undefined; + } + changeNull(): void { + this.myOptions = null; + } + changeWrongType(): void { + const temp: any = 5; + this.myOptions = temp; + } + changeZeroData(): void { + this.myOptions = []; + this.mySelected = undefined; + } + changeDataA(): void { + this.myOptions = this.dataA; + this.mySelected = this.myOptions[0]; + this.mySelecteds = [this.myOptions[0], this.myOptions[1]]; + } + changeDataB(): void { + this.myOptions = this.dataB; + this.mySelected = this.myOptions[0]; + this.mySelecteds = [this.myOptions[0], this.myOptions[1]]; + } + + // 改变选中项 + changeSelect(): void { + this.mySelected = this.myOptions[1]; + } + changeSelects(): void { + this.mySelecteds = [this.myOptions[2], this.myOptions[3]]; + } +} diff --git a/src/select/demo/src/app/select/SelectManyComponent.ts b/src/select/demo/src/app/select/SelectManyComponent.ts new file mode 100644 index 0000000..1923fac --- /dev/null +++ b/src/select/demo/src/app/select/SelectManyComponent.ts @@ -0,0 +1,95 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-many.html' +}) +export class SelectManyComponent { + private dataA: Array = [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '巴西', + englishname: 'Brazil', + disabled: false + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + }, + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + private dataB: Array = [ + { label: '水调歌头' }, + { label: '苏轼' }, + { label: '明月几时有?把酒问青天' }, + { label: '不知天上宫阙,今夕是何年。' }, + { label: '我欲乘风归去,又恐琼楼玉宇,高处不胜寒。' }, + { label: '起舞弄清影,何似在人间?' }, + { label: '转朱阁,低绮户,照无眠。' }, + { label: '不应有恨,何事长向别时圆?' }, + { label: '人有悲欢离合,月有阴晴圆缺,此事古难全。' }, + { label: '但愿人长久,千里共婵娟。' } + ]; + + myOptions: Array; + + mySelected: any; + + mySelecteds: any; + + constructor() { + for (let i: number = 0; i < 5; i++) { + // 5次为32倍,320条 + this.dataA = this.dataA.concat(this.dataA); + } + for (let i: number = 0; i < 7; i++) { + // 8次为256倍,2560条 + this.dataB = this.dataB.concat(this.dataB); + } + } + changeDataA(): void { + this.myOptions = this.dataA; + this.mySelected = this.myOptions[0]; + this.mySelecteds = [this.myOptions[0], this.myOptions[1]]; + } + changeDataB(): void { + this.myOptions = this.dataB; + this.mySelected = this.myOptions[0]; + this.mySelecteds = [this.myOptions[0], this.myOptions[1]]; + } +} diff --git a/src/select/demo/src/app/select/SelectMaxlineComponent.ts b/src/select/demo/src/app/select/SelectMaxlineComponent.ts new file mode 100644 index 0000000..7e29347 --- /dev/null +++ b/src/select/demo/src/app/select/SelectMaxlineComponent.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-maxline.html' +}) +export class SelectMaxlineComponent { + options: Array = [ + { + label: '美国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + }, + { + label: '法国' + }, + { + label: '德国' + }, + { + label: '日本' + }, + { + label: '韩国' + }, + { + label: '土耳其' + }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + value: any = [this.options[0]]; +} diff --git a/src/select/demo/src/app/select/SelectMuchComponent.ts b/src/select/demo/src/app/select/SelectMuchComponent.ts new file mode 100644 index 0000000..0be2cbc --- /dev/null +++ b/src/select/demo/src/app/select/SelectMuchComponent.ts @@ -0,0 +1,63 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-much.html' +}) +export class SelectMuchComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '巴西', + englishname: 'Brazil', + disabled: false + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + }, + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + items: Array = []; + + ngOnInit(): void { + for (let i: number = 0; i < 300; i++) { + this.items.push({ value: undefined }); + } + } +} diff --git a/src/select/demo/src/app/select/SelectMultiComponent.ts b/src/select/demo/src/app/select/SelectMultiComponent.ts new file mode 100644 index 0000000..b86dc51 --- /dev/null +++ b/src/select/demo/src/app/select/SelectMultiComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-multi.html' +}) +export class SelectMultiComponent { + public options: Array = [ + { + label: '中国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + } + ]; + + public value: any = [this.options[1]]; +} diff --git a/src/select/demo/src/app/select/SelectNoborderComponent.ts b/src/select/demo/src/app/select/SelectNoborderComponent.ts new file mode 100644 index 0000000..0db1496 --- /dev/null +++ b/src/select/demo/src/app/select/SelectNoborderComponent.ts @@ -0,0 +1,53 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-noborder.html' +}) +export class SelectNoborderComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + mySelected: any; + + mySelecteds: any; +} diff --git a/src/select/demo/src/app/select/SelectNodataComponent.ts b/src/select/demo/src/app/select/SelectNodataComponent.ts new file mode 100644 index 0000000..d91d898 --- /dev/null +++ b/src/select/demo/src/app/select/SelectNodataComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-nodata.html' +}) +export class SelectNodataComponent { + options: Array = []; + value: any; +} diff --git a/src/select/demo/src/app/select/SelectNoemptyComponent.ts b/src/select/demo/src/app/select/SelectNoemptyComponent.ts new file mode 100644 index 0000000..3bd3695 --- /dev/null +++ b/src/select/demo/src/app/select/SelectNoemptyComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-noempty.html' +}) +export class SelectNoemptyComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + mySelected: any; +} diff --git a/src/select/demo/src/app/select/SelectNullComponent.ts b/src/select/demo/src/app/select/SelectNullComponent.ts new file mode 100644 index 0000000..a2551c1 --- /dev/null +++ b/src/select/demo/src/app/select/SelectNullComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-null.html' +}) +export class SelectNullComponent {} diff --git a/src/select/demo/src/app/select/SelectPaginBeforesearchComponent.ts b/src/select/demo/src/app/select/SelectPaginBeforesearchComponent.ts new file mode 100644 index 0000000..16bbc51 --- /dev/null +++ b/src/select/demo/src/app/select/SelectPaginBeforesearchComponent.ts @@ -0,0 +1,188 @@ +import { Component, ChangeDetectorRef } from '@angular/core'; +import { TiPageSizeConfig, TiSelectComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './select-pagin-beforesearch.html' +}) +export class SelectPaginBeforesearchComponent { + constructor(private cdRef: ChangeDetectorRef) {} + private database: Array = [ + { label: 'America', disabled: true }, + { label: 'Brazil' }, + { label: 'Canada', disabled: true }, + { label: 'China' }, + { label: 'France', disabled: true }, + { label: 'Germany' }, + { label: 'Japan' }, + { label: 'South Korea' }, + { label: 'Turkey' }, + { label: 'United Kingdom' } + ]; + private data: Array; + myOptions1: Array; + myOptions2: Array; + myOptions3: Array; + myOptions4: Array; + + mySelected1: any; + mySelected2: any; + mySelected3: Array = [{ label: '3China' }]; + mySelected4: Array = [{ label: '3China' }]; + + totalNumber1: number; + totalNumber2: number; + totalNumber3: number; + totalNumber4: number; + + currentPage1: number = 1; + currentPage2: number = 1; + currentPage3: number = 1; + currentPage4: number = 1; + + pageSize: TiPageSizeConfig = { + hide: true, + // 如果想修改size,需要配置pageSize.option数组,size是option数组的一项,具体参考TiPaginationComponent组件API说明。 + size: 5, + options: [5, 10] + }; + + ngOnInit(): void { + this.getdatas(this.currentPage1).then((result) => { + this.myOptions1 = result.data; + this.totalNumber1 = result.totalNumber; + }); + + this.getdatas(this.currentPage3).then((result) => { + this.myOptions3 = result.data; + this.totalNumber3 = result.totalNumber; + }); + } + + onCurrentPageChange1(currentPage: number, selectComp: TiSelectComponent): void { + // 模拟后台分页 + this.getdatas(currentPage, selectComp.getSearchWord()).then((result) => { + this.myOptions1 = result.data; + }); + } + + onBeforeSearch1(selectComp: TiSelectComponent): void { + console.log('onBeforeSearch1'); // 为了展示onBeforeSearch事件触发了几次 + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + + this.currentPage1 = 1; + // 模拟后台搜索 + this.getdatas(this.currentPage1, searchWord).then((result) => { + this.myOptions1 = result.data; + this.totalNumber1 = result.totalNumber; + }); + } + + onBeforeOpen2(selectComp: TiSelectComponent): void { + // this.currentPage2 = 1; 每次重新打开下拉列表,需要显示第几页数据就请求第几页数据,并更新currentPage + this.getdatas(this.currentPage2).then((result) => { + this.myOptions2 = result.data; + this.cdRef.detectChanges(); + this.totalNumber2 = result.totalNumber; + selectComp.open(); + }); + } + + onCurrentPageChange2(currentPage: number, selectComp: TiSelectComponent): void { + // 模拟后台分页 + this.getdatas(currentPage, selectComp.getSearchWord()).then((result) => { + this.myOptions2 = result.data; + }); + } + + onBeforeSearch2(selectComp: TiSelectComponent): void { + console.log('onBeforeSearch2'); // 为了展示onBeforeSearch事件触发了几次 + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + + this.currentPage2 = 1; + // 模拟后台搜索 + this.getdatas(this.currentPage2, searchWord).then((result) => { + this.myOptions2 = result.data; + this.totalNumber2 = result.totalNumber; + }); + } + + onCurrentPageChange3(currentPage: number, selectComp: TiSelectComponent): void { + // 模拟后台分页 + this.getdatas(currentPage, selectComp.getSearchWord()).then((result) => { + this.myOptions3 = result.data; + }); + } + + onBeforeSearch3(selectComp: TiSelectComponent): void { + console.log('onBeforeSearch3'); // 为了展示onBeforeSearch事件触发了几次 + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + + this.currentPage3 = 1; + // 模拟后台搜索 + this.getdatas(this.currentPage3, searchWord).then((result) => { + this.myOptions3 = result.data; + this.totalNumber3 = result.totalNumber; + }); + } + + onBeforeOpen4(selectComp: TiSelectComponent): void { + // this.currentPage4 = 1; 每次重新打开下拉列表,需要显示第几页数据就请求第几页数据,并更新currentPage + this.getdatas(this.currentPage4).then((result) => { + this.myOptions4 = result.data; + this.cdRef.detectChanges(); + this.totalNumber4 = result.totalNumber; + selectComp.open(); + }); + } + + onCurrentPageChange4(currentPage: number, selectComp: TiSelectComponent): void { + // 模拟后台分页 + this.getdatas(currentPage, selectComp.getSearchWord()).then((result) => { + this.myOptions4 = result.data; + }); + } + + onBeforeSearch4(selectComp: TiSelectComponent): void { + console.log('onBeforeSearch4'); // 为了展示onBeforeSearch事件触发了几次 + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + + this.currentPage4 = 1; + // 模拟后台搜索 + this.getdatas(this.currentPage4, searchWord).then((result) => { + this.myOptions4 = result.data; + this.totalNumber4 = result.totalNumber; + }); + } + + private getdatas(pageNumber?: number, searchWord?: string): Promise { + this.data = []; + for (let i: number = 0; i < 136; i++) { + const item: any = this.database[i % 10]; + this.data.push({ ...item, label: i + item.label }); + } + const promise: Promise = new Promise((resolve: any, rejects: any): void => { + setTimeout(() => { + if (pageNumber === undefined && searchWord === undefined) { + rejects(new Error('没有参数')); + } + if (searchWord) { + const filteredResult: Array = this.data.filter((item) => item.label.toLowerCase().includes(searchWord.toLowerCase())); + const slicedResult: Array = filteredResult.slice(this.pageSize.size * (pageNumber - 1), this.pageSize.size * pageNumber); + const totalNumber: number = filteredResult.length; + resolve({ data: slicedResult, totalNumber }); + } else if (pageNumber) { + resolve({ + data: this.data.slice(this.pageSize.size * (pageNumber - 1), this.pageSize.size * pageNumber), + totalNumber: this.data.length + }); + } + }, 300); + }); + + return promise; + } +} diff --git a/src/select/demo/src/app/select/SelectPaginationComponent.ts b/src/select/demo/src/app/select/SelectPaginationComponent.ts new file mode 100644 index 0000000..56cf8b7 --- /dev/null +++ b/src/select/demo/src/app/select/SelectPaginationComponent.ts @@ -0,0 +1,65 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiPageSizeConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './select-pagination.html' +}) +export class SelectPaginationComponent { + data: Array = [ + { label: '美国', englishname: 'America', disabled: true }, + { label: '巴西', englishname: 'Brazil', disabled: false }, + { label: '加拿大', englishname: 'Canada', disabled: true }, + { label: '中国', englishname: 'China', disabled: false }, + { label: '法国', englishname: 'France', disabled: true }, + { label: '德国', englishname: 'Germany', disabled: false }, + { label: '日本', englishname: 'Japan' }, + { label: '韩国', englishname: 'South Korea' }, + { label: '土耳其', englishname: 'Turkey' }, + { label: '大不列颠和北爱兰联合王国', englishname: 'United Kingdom' } + ]; + + options: Array; + value: any; + totalNumber: number; + pageSize: TiPageSizeConfig = { + hide: true, + size: 20 + }; + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + ngOnInit(): void { + // 初始获取 options 下拉数据 + this.getData(1, this.pageSize.size).then((result: any) => { + this.options = result.data; + this.totalNumber = result.totalNumber; + }); + } + + onCurrentPageChange(currentPage: number): void { + this.getData(currentPage, this.pageSize.size).then((result: any) => { + this.options = result.data; + }); + } + + // 模拟异步请求 + private getData(currentPage: number, size: number): Promise { + const startIndex: number = (currentPage - 1) * size; + const database: Array = []; + for (let i: number = 0; i < 500; i++) { + const item: any = this.data[i % 10]; + database.push({ ...item, label: i + item.label }); + } + + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + resolve({ + data: database.slice(startIndex, startIndex + size), + totalNumber: database.length + }); + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef.markForCheck() + // 默认模式下不需要 + this.changeDetectorRef.markForCheck(); + }, 200); + }); + } +} diff --git a/src/select/demo/src/app/select/SelectPanelComponent.ts b/src/select/demo/src/app/select/SelectPanelComponent.ts new file mode 100644 index 0000000..859e9eb --- /dev/null +++ b/src/select/demo/src/app/select/SelectPanelComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-panel.html' +}) +export class SelectPanelComponent { + options: Array = [ + { label: '水调歌头' }, + { label: '苏轼' }, + { label: '明月几时有?把酒问青天' }, + { label: '不知天上宫阙,今夕是何年。' }, + { label: '我欲乘风归去,又恐琼楼玉宇,高处不胜寒。' }, + { label: '起舞弄清影,何似在人间?' }, + { label: '转朱阁,低绮户,照无眠。' }, + { label: '不应有恨,何事长向别时圆?' }, + { label: '人有悲欢离合,月有阴晴圆缺,此事古难全。' }, + { label: '但愿人长久,千里共婵娟。' } + ]; + + value: any; +} diff --git a/src/select/demo/src/app/select/SelectReservesearchwordComponent.ts b/src/select/demo/src/app/select/SelectReservesearchwordComponent.ts new file mode 100644 index 0000000..5d0a41d --- /dev/null +++ b/src/select/demo/src/app/select/SelectReservesearchwordComponent.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-reservesearchword.html' +}) +export class SelectReservesearchwordComponent { + options: Array = [ + { + label: '美国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + }, + { + label: '法国' + }, + { + label: '德国' + }, + { + label: '日本' + }, + { + label: '韩国' + }, + { + label: '土耳其' + }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + value: any = this.options[0]; +} diff --git a/src/select/demo/src/app/select/SelectScrollLoadComponent.ts b/src/select/demo/src/app/select/SelectScrollLoadComponent.ts new file mode 100644 index 0000000..3075442 --- /dev/null +++ b/src/select/demo/src/app/select/SelectScrollLoadComponent.ts @@ -0,0 +1,116 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiSelectComponent, TiListScrollLoad } from '@opentiny/ng'; + +@Component({ + templateUrl: './select-scroll-load.html' +}) +export class SelectScrollLoadComponent { + constructor(private changeDetectorRef: ChangeDetectorRef) {} + optionsTotalNumber: number; + private size: number = 50; + private data: Array = [ + { + label: '美国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + }, + { + label: '法国' + }, + { + label: '德国' + }, + { + label: '日本' + }, + { + label: '韩国' + }, + { + label: '土耳其' + }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + private database: Array = []; + options: Array = []; + value: any; + + beforeOpen(selectComp: TiSelectComponent) { + if (this.options.length === 0) { + // 第一次打开面板需要从后台获取部分数据 + this.getData(0, this.size).then((result: any) => { + this.options = result.data; // 下拉数据赋值 + this.optionsTotalNumber = result.totalNumber; + setTimeout(() => { + // 有搜索时,在beforeOpen中给 options 赋值后需要延迟再打开面板 + selectComp.open(); // 打开面板 + }, 0); + }); + } else { + selectComp.open(); // 非第一次(已经获取到了下拉数据)时直接打开面板 + } + } + + beforeSearch(selectComp: TiSelectComponent): void { + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + this.getData(0, this.size, searchWord).then((result: any) => { + // 设置搜索结果 + this.options = result.data; + this.optionsTotalNumber = result.totalNumber; + }); + } + + loadMore(scrollLoadInfo: TiListScrollLoad, selectComp: TiSelectComponent): void { + const currentOptions: Array = selectComp.getSearchResult(); + if (currentOptions.length >= this.optionsTotalNumber) { + return; + } + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + scrollLoadInfo.loading = true; + this.getData(currentOptions.length, this.size, searchWord).then((result: any) => { + this.options = [...currentOptions, ...result.data]; + scrollLoadInfo.loading = false; + }); + } + + // 模拟异步请求 + private getData(startIndex: number, size: number, searchWord?: string): Promise { + this.database = []; + for (let i: number = 0; i < 10000; i++) { + const item: any = this.data[i % 10]; + this.database.push({ ...item, label: i + item.label }); + } + + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + if (searchWord) { + const filteredResult: Array = this.database.filter((item: any) => item.label.includes(searchWord)); + const slicedResult: Array = filteredResult.slice(startIndex, startIndex + size); + const totalNumber: number = filteredResult.length; + resolve({ data: slicedResult, totalNumber }); + } else { + resolve({ + data: this.database.slice(startIndex, startIndex + size), + totalNumber: this.database.length + }); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef.markForCheck() + // 默认模式下不需要 + // 服务可根据自身变化检测策略决定是否使用该方法 + this.changeDetectorRef.markForCheck(); + }, 300); + }); + } +} diff --git a/src/select/demo/src/app/select/SelectSearchComponent.ts b/src/select/demo/src/app/select/SelectSearchComponent.ts new file mode 100644 index 0000000..d62ca13 --- /dev/null +++ b/src/select/demo/src/app/select/SelectSearchComponent.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-search.html' +}) +export class SelectSearchComponent { + options: Array = [ + { + label: '美国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + }, + { + label: '法国' + }, + { + label: '德国' + }, + { + label: '日本' + }, + { + label: '韩国' + }, + { + label: '土耳其' + }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + value: any = this.options[0]; +} diff --git a/src/select/demo/src/app/select/SelectSearchkeysComponent.ts b/src/select/demo/src/app/select/SelectSearchkeysComponent.ts new file mode 100644 index 0000000..b8ad4aa --- /dev/null +++ b/src/select/demo/src/app/select/SelectSearchkeysComponent.ts @@ -0,0 +1,63 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-searchkeys.html' +}) +export class SelectSearchkeysComponent { + options: Array = [ + { + label: '美国', + yomi: 'meiguo', + initial: 'mg' + }, + { + label: '巴西', + yomi: 'baxi', + initial: 'bx' + }, + { + label: '加拿大', + yomi: 'jianada', + initial: 'jnd', + disabled: true + }, + { + label: '中国', + yomi: 'zhongguo', + initial: 'zg' + }, + { + label: '法国', + yomi: 'faguo', + initial: 'fg' + }, + { + label: '德国', + yomi: 'deguo', + initial: 'dg', + disabled: true + }, + { + label: '日本', + yomi: 'riben', + initial: 'rb' + }, + { + label: '韩国', + yomi: 'hanguo', + initial: 'hg' + }, + { + label: '土耳其', + yomi: 'tuerqi', + initial: 'teq' + }, + { + label: '大不列颠和北爱兰联合王国', + yomi: 'dabuliedianhebeiaierlanlianhewangguo', + initial: 'dbldhbaellhwg' + } + ]; + + value: any; +} diff --git a/src/select/demo/src/app/select/SelectSelectallComponent.ts b/src/select/demo/src/app/select/SelectSelectallComponent.ts new file mode 100644 index 0000000..e22cbea --- /dev/null +++ b/src/select/demo/src/app/select/SelectSelectallComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-selectall.html' +}) +export class SelectSelectallComponent { + options: Array = [ + { + label: '中国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + } + ]; + + value: any = [this.options[1]]; +} diff --git a/src/select/demo/src/app/select/SelectShowselectednumberComponent.ts b/src/select/demo/src/app/select/SelectShowselectednumberComponent.ts new file mode 100644 index 0000000..dd98109 --- /dev/null +++ b/src/select/demo/src/app/select/SelectShowselectednumberComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-showselectednumber.html' +}) +export class SelectShowselectednumberComponent { + options: Array = [ + { label: '美国' }, + { label: '巴西' }, + { label: '加拿大' }, + { label: '中国' }, + { label: '法国' }, + { label: '德国' }, + { label: '日本' }, + { label: '韩国' }, + { label: '土耳其' }, + { label: '大不列颠和北爱兰联合王国' } + ]; + + value: any = [this.options[3]]; +} diff --git a/src/select/demo/src/app/select/SelectSmallComponent.ts b/src/select/demo/src/app/select/SelectSmallComponent.ts new file mode 100644 index 0000000..8c30439 --- /dev/null +++ b/src/select/demo/src/app/select/SelectSmallComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-small.html' +}) +export class SelectSmallComponent { + options: Array = [ + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + } + ]; + + value: any = this.options[1]; +} diff --git a/src/select/demo/src/app/select/SelectTagComponent.ts b/src/select/demo/src/app/select/SelectTagComponent.ts new file mode 100644 index 0000000..b499bd3 --- /dev/null +++ b/src/select/demo/src/app/select/SelectTagComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-tag.html', + styleUrls: ['./select-tag.less'] +}) +export class SelectTagComponent { + myOptions: Array = [ + { + label: '蛋糕', + englishname: 'Cake', + url: 'assets/food/cake.png', + disabled: true, + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + }, + { + label: '咖啡', + englishname: 'Coffee', + url: 'assets/food/coffee.png', + disabled: false, + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + }, + { + label: '可乐', + englishname: 'Cola', + url: 'assets/food/cola.png', + disabled: true, + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + }, + { + label: '炸鸡', + englishname: 'Fried Chicken', + url: 'assets/food/fried_chicken.png', + disabled: false, + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + }, + { + label: '薯条', + englishname: 'Fries', + url: 'assets/food/fries.png', + disabled: true, + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + }, + { + label: '汉堡', + englishname: 'Hamburger', + url: 'assets/food/hamburger.png', + disabled: false, + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + }, + { + label: '牛奶', + englishname: 'milk', + url: 'assets/food/milk.png', + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + }, + { + label: '披萨', + englishname: 'pizza', + url: 'assets/food/pizza.png', + mark: '前标识', + tag: '端口后标识', + tip: '服务自定义tip111111' + } + ]; + + mySelected: any; + // 也是有初值/有禁用项/带模板的一个测试用例, + mySelecteds: any = []; +} diff --git a/src/select/demo/src/app/select/SelectTemplateComponent.ts b/src/select/demo/src/app/select/SelectTemplateComponent.ts new file mode 100644 index 0000000..11edff4 --- /dev/null +++ b/src/select/demo/src/app/select/SelectTemplateComponent.ts @@ -0,0 +1,50 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './select-template.html' +}) +export class SelectTemplateComponent { + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL']; + options: Array = [ + { + label: '蛋糕', + englishname: 'Cake', + url: `${this.baseUrl}assets/food/cake.png` + }, + { + label: '咖啡', + englishname: 'Coffee', + url: `${this.baseUrl}assets/food/coffee.png` + }, + { + label: '可乐', + englishname: 'Cola', + url: `${this.baseUrl}assets/food/cola.png` + }, + { + label: '炸鸡', + englishname: 'Fried Chicken', + url: `${this.baseUrl}assets/food/fried_chicken.png` + }, + { + label: '薯条', + englishname: 'Fries', + url: `${this.baseUrl}assets/food/fries.png` + }, + { + label: '汉堡', + englishname: 'Hamburger', + url: `${this.baseUrl}assets/food/hamburger.png` + }, + { + label: '牛奶', + englishname: 'milk', + url: `${this.baseUrl}assets/food/milk.png` + }, + { + label: '披萨', + englishname: 'pizza', + url: `${this.baseUrl}assets/food/pizza.png` + } + ]; + value: any; +} diff --git a/src/select/demo/src/app/select/SelectTestModule.ts b/src/select/demo/src/app/select/SelectTestModule.ts new file mode 100644 index 0000000..6c3dd2f --- /dev/null +++ b/src/select/demo/src/app/select/SelectTestModule.ts @@ -0,0 +1,293 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { + TiIconModule, + TiPaginationModule, + TiScrollModule, + TiSelectModule, + TiTextModule, + TiTipModule, + TiValidationModule +} from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { SelectBasicComponent } from './SelectBasicComponent'; +import { SelectGroupComponent } from './SelectGroupComponent'; +import { SelectEventComponent } from './SelectEventComponent'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { SelectLeakComponent } from './SelectLeakComponent'; +import { SelectSmallComponent } from './SelectSmallComponent'; +import { SelectDisabledComponent } from './SelectDisabledComponent'; +import { SelectMultiComponent } from './SelectMultiComponent'; +import { SelectSearchComponent } from './SelectSearchComponent'; +import { SelectNoemptyComponent } from './SelectNoemptyComponent'; +import { NoEmptyPipe } from './NoEmptyPipe'; +import { SelectPanelComponent } from './SelectPanelComponent'; +import { SelectTipComponent } from './SelectTipComponent'; +import { SelectValidComponent } from './SelectValidComponent'; +import { SelectTagComponent } from './SelectTagComponent'; +import { SelectTemplateComponent } from './SelectTemplateComponent'; +import { SelectLoadComponent } from './SelectLoadComponent'; +import { SelectFocusComponent } from './SelectFocusComponent'; +import { SelectNoborderComponent } from './SelectNoborderComponent'; +import { SelectNodataComponent } from './SelectNodataComponent'; +import { SelectInputComponent } from './SelectInputComponent'; +import { SelectManyComponent } from './SelectManyComponent'; +import { SelectIdComponent } from './SelectIdComponent'; +import { SelectValidgroupComponent } from './SelectValidgroupComponent'; +import { SelectTiscrollComponent } from './SelectTiscrollComponent'; +import { SelectAppendtobodyComponent } from './SelectAppendtobodyComponent'; +import { SelectClearableComponent } from './SelectClearableComponent'; +import { SelectMuchComponent } from './SelectMuchComponent'; +import { SelectSelectallComponent } from './SelectSelectallComponent'; +import { SelectMaxlineComponent } from './SelectMaxlineComponent'; +import { SelectSearchkeysComponent } from './SelectSearchkeysComponent'; +import { SelectValuekeyComponent } from './SelectValuekeyComponent'; +import { SelectValuekeyTestComponent } from './SelectValuekeyTestComponent'; +import { SelectTworowComponent } from './SelectTworowComponent'; +import { SelectEnumComponent } from './SelectEnumComponent'; +import { SelectBeforesearchComponent } from './SelectBeforesearchComponent'; +import { SelectBeforesearchTestComponent } from './SelectBeforesearchTestComponent'; +import { SelectPaginationComponent } from './SelectPaginationComponent'; +import { SelectPaginBeforesearchComponent } from './SelectPaginBeforesearchComponent'; +import { SelectNullComponent } from './SelectNullComponent'; +import { SelectVirtualscrollComponent } from './SelectVirtualscrollComponent'; +import { SelectVirtualscrollMultiComponent } from './SelectVirtualscrollMultiComponent'; +import { SelectChangeSelectallComponent } from './SelectChangeSelectallComponent'; +import { SelectIdkeyComponent } from './SelectIdkeyComponent'; +import { SelectScrollLoadComponent } from './SelectScrollLoadComponent'; +import { SelectDisabledfocusComponent } from './SelectDisabledfocusComponent'; +import { SelectShowselectednumberComponent } from './SelectShowselectednumberComponent'; +import { SelectLabelkeyComponent } from './SelectLabelkeyComponent'; +import { SelectReservesearchwordComponent } from './SelectReservesearchwordComponent'; +import { SelectLazyComponent } from './SelectLazyComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiIconModule, + TiPaginationModule, + TiScrollModule, + TiSelectModule, + TiTextModule, + TiTipModule, + TiValidationModule, + DemoLogModule, + RouterModule.forChild(SelectTestModule.ROUTES) + ], + declarations: [ + SelectBasicComponent, + SelectTagComponent, + SelectTemplateComponent, + SelectGroupComponent, + SelectEventComponent, + SelectLazyComponent, + SelectLeakComponent, + SelectLeakComponent, + SelectSmallComponent, + SelectDisabledComponent, + SelectFocusComponent, + SelectDisabledComponent, + SelectSearchComponent, + SelectMultiComponent, + SelectMaxlineComponent, + SelectNoborderComponent, + SelectNoemptyComponent, + SelectNodataComponent, + SelectPanelComponent, + SelectTipComponent, + SelectValidComponent, + SelectLoadComponent, + SelectInputComponent, + SelectManyComponent, + SelectIdComponent, + SelectValidgroupComponent, + SelectTiscrollComponent, + SelectValuekeyComponent, + SelectClearableComponent, + SelectMuchComponent, + SelectSelectallComponent, + SelectSearchkeysComponent, + NoEmptyPipe, + SelectTworowComponent, + SelectEnumComponent, + SelectBeforesearchComponent, + SelectBeforesearchTestComponent, + SelectPaginationComponent, + SelectValuekeyTestComponent, + SelectPaginBeforesearchComponent, + SelectNullComponent, + SelectVirtualscrollComponent, + SelectVirtualscrollMultiComponent, + SelectChangeSelectallComponent, + SelectIdkeyComponent, + SelectScrollLoadComponent, + SelectDisabledfocusComponent, + SelectAppendtobodyComponent, + SelectShowselectednumberComponent, + SelectLabelkeyComponent, + SelectReservesearchwordComponent + ] +}) +export class SelectTestModule { + static readonly LINKS: Array = [{ href: 'components/TiSelectComponent.html', label: 'Select' }]; + static readonly ROUTES: Routes = [ + { + path: 'select/select-basic', + component: SelectBasicComponent + }, + { + path: 'select/select-template', + component: SelectTemplateComponent + }, + { + path: 'select/select-labelkey', + component: SelectLabelkeyComponent + }, + { + path: 'select/select-valuekey', + component: SelectValuekeyComponent + }, + { + path: 'select/select-valuekey-test', + component: SelectValuekeyTestComponent + }, + { + path: 'select/select-group', + component: SelectGroupComponent + }, + { + path: 'select/select-event', + component: SelectEventComponent + }, + { + path: 'select/select-lazy', + component: SelectLazyComponent + }, + { + path: 'select/select-disabled', + component: SelectDisabledComponent + }, + { + path: 'select/select-search', + component: SelectSearchComponent + }, + { + path: 'select/select-searchkeys', + component: SelectSearchkeysComponent + }, + { + path: 'select/select-reservesearchword', + component: SelectReservesearchwordComponent + }, + { + path: 'select/select-beforesearch', + component: SelectBeforesearchComponent + }, + { + path: 'select/select-multi', + component: SelectMultiComponent + }, + { + path: 'select/select-maxline', + component: SelectMaxlineComponent + }, + { + path: 'select/select-multi-maxline', + component: SelectShowselectednumberComponent + }, + { + path: 'select/select-panel', + component: SelectPanelComponent + }, + { + path: 'select/select-tip', + component: SelectTipComponent + }, + { + path: 'select/select-focus', + component: SelectFocusComponent + }, + { + path: 'select/select-nodata', + component: SelectNodataComponent + }, + { + path: 'select/select-tiscroll', + component: SelectTiscrollComponent + }, + { + path: 'select/select-appendtobody', + component: SelectAppendtobodyComponent + }, + { + path: 'select/select-clearable', + component: SelectClearableComponent + }, + { + path: 'select/select-selectall', + component: SelectSelectallComponent + }, + { + path: 'select/select-tag', + component: SelectTagComponent + }, + { + path: 'select/select-tworow', + component: SelectTworowComponent + }, + { + path: 'select/select-pagination', + component: SelectPaginationComponent + }, + { + path: 'select/select-pagin-beforesearch', + component: SelectPaginBeforesearchComponent + }, + { + path: 'select/select-virtualscroll', + component: SelectVirtualscrollComponent + }, + { + path: 'select/select-virtualscroll-multi', + component: SelectVirtualscrollMultiComponent + }, + { + path: 'select/select-scroll-load', + component: SelectScrollLoadComponent + }, + { path: 'select/select-small', component: SelectSmallComponent }, + { path: 'select/select-noborder', component: SelectNoborderComponent }, + { path: 'select/select-noempty', component: SelectNoemptyComponent }, + { path: 'select/select-leak', component: SelectLeakComponent }, + { path: 'select/select-valid', component: SelectValidComponent }, + { path: 'select/select-validgroup', component: SelectValidgroupComponent }, + { path: 'select/select-load', component: SelectLoadComponent }, + { path: 'select/select-many', component: SelectManyComponent }, + { path: 'select/select-input', component: SelectInputComponent }, + { path: 'select/select-id', component: SelectIdComponent }, + { path: 'select/select-much', component: SelectMuchComponent }, + { path: 'select/select-enum', component: SelectEnumComponent }, + { path: 'select/select-null', component: SelectNullComponent }, + { + path: 'select/select-beforesearch-test', + component: SelectBeforesearchTestComponent + }, + { + path: 'select/select-change-selectall', + component: SelectChangeSelectallComponent + }, + { + path: 'select/select-disabledfocus', + component: SelectDisabledfocusComponent + }, + { + path: 'select/select-idkey', + component: SelectIdkeyComponent + } + ]; +} diff --git a/src/select/demo/src/app/select/SelectTipComponent.ts b/src/select/demo/src/app/select/SelectTipComponent.ts new file mode 100644 index 0000000..46dc34e --- /dev/null +++ b/src/select/demo/src/app/select/SelectTipComponent.ts @@ -0,0 +1,29 @@ +import { Component, TemplateRef, ViewChild } from '@angular/core'; + +@Component({ + templateUrl: './select-tip.html' +}) +export class SelectTipComponent { + @ViewChild('tip', { static: true }) tip: TemplateRef; + options: Array; + value: any; + + ngOnInit(): void { + this.options = [ + { label: '水调歌头' }, + { label: '苏轼' }, + { label: '(有tip)明月几时有?把酒问青天。', tip: this.tip }, + { label: '不知天上宫阙,今夕是何年。' }, + { + label: '(有tip)我欲乘风归去,又恐琼楼玉宇,高处不胜寒。', + tip: this.tip + }, + { label: '起舞弄清影,何似在人间?' }, + { label: '转朱阁,低绮户,照无眠。' }, + { label: '不应有恨,何事长向别时圆?' }, + { label: '人有悲欢离合,月有阴晴圆缺,此事古难全。' }, + { label: '但愿人长久,千里共婵娟。' } + ]; + this.value = this.options[1]; + } +} diff --git a/src/select/demo/src/app/select/SelectTiscrollComponent.ts b/src/select/demo/src/app/select/SelectTiscrollComponent.ts new file mode 100644 index 0000000..8f2752d --- /dev/null +++ b/src/select/demo/src/app/select/SelectTiscrollComponent.ts @@ -0,0 +1,61 @@ +import { Component, ElementRef, Renderer2, ViewChild } from '@angular/core'; +import { Util } from '@opentiny/ng'; +@Component({ + templateUrl: './select-tiscroll.html', + styles: ['p {margin-bottom: 10px;}'] +}) +export class SelectTiscrollComponent { + @ViewChild('container') private containerRef: ElementRef; + myOptions: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + mySelected: any; + constructor(private renderer2: Renderer2) {} + + ngAfterViewInit(): void { + // 监听容器滚动事件,触发tiScroll收起下拉 + this.renderer2.listen(this.containerRef.nativeElement, 'scroll', () => { + // tiScroll 是自定义的事件,可以触发面板收起 + Util.trigger(document, 'tiScroll'); + }); + } +} diff --git a/src/select/demo/src/app/select/SelectTworowComponent.ts b/src/select/demo/src/app/select/SelectTworowComponent.ts new file mode 100644 index 0000000..21731eb --- /dev/null +++ b/src/select/demo/src/app/select/SelectTworowComponent.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-tworow.html' +}) +export class SelectTworowComponent { + options: Array = [ + { + primary: '子网1 (192.168.0.1/24)', + secondary: '可用IP数278', + disabled: true + }, + { + primary: '子网2 (192.168.0.1/25)', + secondary: '可用IP数312' + }, + { + primary: '子网3 (192.168.0.1/26)', + secondary: '可用IP数225' + }, + { + primary: '子网4 (192.168.0.1/27)', + secondary: '可用IP数300' + } + ]; + + value: any; +} diff --git a/src/select/demo/src/app/select/SelectValidComponent.ts b/src/select/demo/src/app/select/SelectValidComponent.ts new file mode 100644 index 0000000..ce30630 --- /dev/null +++ b/src/select/demo/src/app/select/SelectValidComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './select-valid.html' +}) +export class SelectValidComponent { + options: Array = [ + { + label: '美国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + }, + { + label: '法国' + }, + { + label: '德国' + }, + { + label: '日本' + }, + { + label: '韩国' + }, + { + label: '土耳其' + }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + value: any = this.options[0]; +} diff --git a/src/select/demo/src/app/select/SelectValidGroupComponent.ts b/src/select/demo/src/app/select/SelectValidGroupComponent.ts new file mode 100644 index 0000000..3581c5e --- /dev/null +++ b/src/select/demo/src/app/select/SelectValidGroupComponent.ts @@ -0,0 +1,75 @@ +import { Component } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; + +@Component({ + templateUrl: './select-validgroup.html' +}) +export class SelectValidgroupComponent { + myOptions: Array = [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '巴西', + englishname: 'Brazil', + disabled: false + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + }, + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + mySelected: any; + + mySelecteds: any; + + myinputvalue: string; + + constructor(private fb: FormBuilder) {} + myFormGroup: FormGroup; + ngOnInit(): void { + this.myFormGroup = this.fb.group({ + name: '', + home: '', + age: '' + }); + } + onBlurSelect(): void { + console.log('onBlurSelect'); + } +} diff --git a/src/select/demo/src/app/select/SelectValuekeyComponent.ts b/src/select/demo/src/app/select/SelectValuekeyComponent.ts new file mode 100644 index 0000000..37f91e3 --- /dev/null +++ b/src/select/demo/src/app/select/SelectValuekeyComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-valuekey.html' +}) +export class SelectValuekeyComponent { + options: Array = [ + { + label: '美国', + englishname: 'America' + }, + { + label: '巴西', + englishname: 'Brazil' + }, + { + label: '加拿大', + englishname: 'Canada' + }, + { + label: '中国', + englishname: 'China' + }, + { + label: '法国', + englishname: 'France' + }, + { + label: '德国', + englishname: 'Germany' + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ]; + + value: any; +} diff --git a/src/select/demo/src/app/select/SelectValuekeyTestComponent.ts b/src/select/demo/src/app/select/SelectValuekeyTestComponent.ts new file mode 100644 index 0000000..5e88c47 --- /dev/null +++ b/src/select/demo/src/app/select/SelectValuekeyTestComponent.ts @@ -0,0 +1,111 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-valuekey-test.html' +}) +export class SelectValuekeyTestComponent { + myOptions: Array = [ + { label: '美国', englishname: 'America' }, + { label: '巴西', englishname: 'Brazil' }, + { label: '加拿大', englishname: 'Canada' }, + { label: '中国', englishname: 'China' }, + { label: '法国', englishname: 'France' }, + { label: '德国', englishname: 'Germany' }, + { label: '日本', englishname: 'Japan' }, + { label: '韩国', englishname: 'South Korea' }, + { label: '土耳其', englishname: 'Turkey' }, + { label: '大不列颠和北爱兰联合王国', englishname: 'United Kingdom' } + ]; + myOptions2: Array = [ + { + label: '北美洲', + children: [ + { + label: '美国', + englishname: 'America', + disabled: true + }, + { + label: '加拿大', + englishname: 'Canada', + disabled: true + } + ] + }, + { + label: '南美洲', + children: [ + { + label: '巴西', + englishname: 'Brazil', + disabled: false + } + ] + }, + { + label: '亚洲', + children: [ + { + label: '中国', + englishname: 'China', + disabled: false + }, + { + label: '日本', + englishname: 'Japan' + }, + { + label: '韩国', + englishname: 'South Korea' + } + ] + }, + { + label: '欧洲', + children: [ + { + label: '法国', + englishname: 'France', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + disabled: false + }, + { + label: '土耳其', + englishname: 'Turkey' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom' + } + ] + } + ]; + myOptions3: Array; + mySelected1: string = 'China'; + mySelecteds: any = ['China', 'Japan']; + mySelecteds2: any = ['China', 'Japan']; + + changeOptions(): void { + this.myOptions = [ + { label: '美国', englishname: 'America' }, + { label: '巴西', englishname: 'Brazil' }, + { label: '加拿大', englishname: 'Canada' }, + { label: '中国', englishname: 'China' }, + { label: '法国', englishname: 'France' }, + { label: '德国', englishname: 'Germany' }, + { label: '日本', englishname: 'Japan' }, + { label: '韩国', englishname: 'South Korea' }, + { label: '土耳其', englishname: 'Turkey' }, + { label: '大不列颠和北爱兰联合王国', englishname: 'United Kingdom' }, + { label: '俄罗斯', englishname: 'Russia' } + ]; + } + + addOptions(): void { + this.myOptions3 = this.myOptions2; + } +} diff --git a/src/select/demo/src/app/select/SelectVirtualscrollComponent.ts b/src/select/demo/src/app/select/SelectVirtualscrollComponent.ts new file mode 100644 index 0000000..a9d9611 --- /dev/null +++ b/src/select/demo/src/app/select/SelectVirtualscrollComponent.ts @@ -0,0 +1,51 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-virtualscroll.html' +}) +export class SelectVirtualscrollComponent { + private data: Array = [ + { + label: '美国' + }, + { + label: '巴西' + }, + { + label: '加拿大' + }, + { + label: '中国' + }, + { + label: '法国' + }, + { + label: '德国' + }, + { + label: '日本' + }, + { + label: '韩国' + }, + { + label: '土耳其' + }, + { + label: '大不列颠和北爱兰联合王国' + } + ]; + + private dataA: Array = []; + options: Array = []; + value: any; + + constructor() { + for (let i: number = 0; i < 10000; i++) { + const item: any = this.data[i % 10]; + this.dataA.push({ ...item, label: i + item.label }); + } + this.options = [].concat(this.dataA); + } +} diff --git a/src/select/demo/src/app/select/SelectVirtualscrollMultiComponent.ts b/src/select/demo/src/app/select/SelectVirtualscrollMultiComponent.ts new file mode 100644 index 0000000..9f97d61 --- /dev/null +++ b/src/select/demo/src/app/select/SelectVirtualscrollMultiComponent.ts @@ -0,0 +1,201 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './select-virtualscroll-multi.html' +}) +export class SelectVirtualscrollMultiComponent { + private data: Array = [ + { + label: '美国', + englishname: 'America', + primary: '子网 (192.168.0.1/24)', + secondary: '可用IP数278', + disabled: false + }, + { + label: '巴西', + englishname: 'Brazil', + primary: '子网 (192.168.0.1/25)', + secondary: '可用IP数312', + disabled: false + }, + { + label: '加拿大', + englishname: 'Canada', + primary: '子网 (192.168.0.1/26)', + secondary: '可用IP数225', + disabled: true + }, + { + label: '中国', + englishname: 'China', + primary: '子网 (192.168.0.1/27)', + secondary: '可用IP数300', + disabled: false + }, + { + label: '法国', + englishname: 'France', + primary: '子网 (192.168.0.1/28)', + secondary: '可用IP数278', + disabled: true + }, + { + label: '德国', + englishname: 'Germany', + primary: '子网 (192.168.0.1/29)', + secondary: '可用IP数312', + disabled: false + }, + { + label: '日本', + englishname: 'Japan', + primary: '子网 (192.168.0.1/30)', + secondary: '可用IP数225' + }, + { + label: '韩国', + englishname: 'South Korea', + primary: '子网 (192.168.0.1/31)', + secondary: '可用IP数300' + }, + { + label: '土耳其', + englishname: 'Turkey', + primary: '子网 (192.168.0.1/31)', + secondary: '可用IP数300' + }, + { + label: '大不列颠和北爱兰联合王国', + englishname: 'United Kingdom', + primary: '子网 (192.168.0.1/31)', + secondary: '可用IP数300' + } + ]; + + private dataA: Array = []; + private dataB: Array = []; + myOptions1: Array; + myOptions2: Array; + myOptions3: Array; + myOptions4: Array; + myOptions5: Array; + mySelected1: any; + mySelected2: any; + mySelected3: any; + mySelected4: any; + mySelected5: any; + + constructor() { + for (let i: number = 0; i < 10000; i++) { + const item: any = this.data[i % 10]; + this.dataA.push({ ...item, label: i + item.label }); + } + for (let i: number = 0; i < 5; i++) { + const item: any = this.data[i % 10]; + this.dataB.push({ ...item, label: i + item.label }); + } + + this.myOptions1 = []; + this.myOptions2 = [].concat(this.dataA); + this.myOptions3 = this.dataA.slice(0, 1000); + this.myOptions5 = [].concat(this.dataA); + } + + changeEmpty1(): void { + if (this.mySelected1) { + this.mySelected1 = undefined; + } + this.myOptions1 = []; + } + changeMany1(): void { + if (this.mySelected1) { + this.mySelected1 = undefined; + } + this.myOptions1 = [].concat(this.dataA); + } + changeFew1(): void { + if (this.mySelected1) { + this.mySelected1 = undefined; + } + this.myOptions1 = [].concat(this.dataB); + } + changeEmpty2(): void { + if (this.mySelected2) { + this.mySelected2 = undefined; + } + this.myOptions2 = []; + } + changeMany2(): void { + if (this.mySelected2) { + this.mySelected2 = undefined; + } + this.myOptions2 = [].concat(this.dataA); + } + changeFew2(): void { + if (this.mySelected2) { + this.mySelected2 = undefined; + } + this.myOptions2 = [].concat(this.dataB); + } + + changeEmpty3(): void { + if (this.mySelected3) { + this.mySelected3 = undefined; + } + this.myOptions3 = []; + } + changeMany3(): void { + if (this.mySelected3) { + this.mySelected3 = undefined; + } + this.myOptions3 = this.dataA.slice(0, 1000); + } + changeFew3(): void { + if (this.mySelected3) { + this.mySelected3 = undefined; + } + this.myOptions3 = [].concat(this.dataB); + } + + changeEmpty4(): void { + if (this.mySelected4) { + this.mySelected4 = undefined; + } + this.myOptions4 = []; + } + changeMany4(): void { + if (this.mySelected4) { + this.mySelected4 = undefined; + } + this.myOptions4 = this.dataA.slice(0, 1000); + } + changeFew4(): void { + if (this.mySelected4) { + this.mySelected4 = undefined; + } + this.myOptions4 = [].concat(this.dataB); + } + + changeEmpty5(): void { + if (this.mySelected5) { + this.mySelected5 = undefined; + } + this.myOptions5 = []; + } + changeMany5(): void { + if (this.mySelected5) { + this.mySelected5 = undefined; + } + this.myOptions5 = [].concat(this.dataA); + } + changeFew5(): void { + if (this.mySelected5) { + this.mySelected5 = undefined; + } + this.myOptions5 = [].concat(this.dataB); + } + mousedown(): void { + console.log('footer is clicked'); + } +} diff --git a/src/select/demo/src/app/select/select-appendtobody.html b/src/select/demo/src/app/select/select-appendtobody.html new file mode 100644 index 0000000..6ae71bd --- /dev/null +++ b/src/select/demo/src/app/select/select-appendtobody.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/select-basic.html b/src/select/demo/src/app/select/select-basic.html new file mode 100644 index 0000000..b2f6a2a --- /dev/null +++ b/src/select/demo/src/app/select/select-basic.html @@ -0,0 +1,5 @@ + + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/select/demo/src/app/select/select-beforesearch-test.html b/src/select/demo/src/app/select/select-beforesearch-test.html new file mode 100644 index 0000000..e066b63 --- /dev/null +++ b/src/select/demo/src/app/select/select-beforesearch-test.html @@ -0,0 +1,41 @@ +

    描述

    +

    Select组件提供beforeSearch事件接口,监听搜索框的内容变更,可以获取到搜索框的内容

    +

    beforeSearch事件接口

    +

    示例

    +

    示例中,列表初始化时加载三条数据,搜索时加载全部的匹配的数据,比如在示例1搜索“国”,或者在示例2搜索“国”

    +

    1.单选 + 后台搜索:

    +
    +

    +国家: + + +
    +

    选中项:{{mySelecteds | json}}

    +
    + +

    2.多选 + 全选 + 后台搜索,下拉选项中有禁用的:

    +
    +

    +国家: + + +
    +

    选中项:{{mySelecteds1 | json}}

    +
    diff --git a/src/select/demo/src/app/select/select-beforesearch.html b/src/select/demo/src/app/select/select-beforesearch.html new file mode 100644 index 0000000..f814150 --- /dev/null +++ b/src/select/demo/src/app/select/select-beforesearch.html @@ -0,0 +1,9 @@ + + diff --git a/src/select/demo/src/app/select/select-change-selectall.html b/src/select/demo/src/app/select/select-change-selectall.html new file mode 100644 index 0000000..7a48ec4 --- /dev/null +++ b/src/select/demo/src/app/select/select-change-selectall.html @@ -0,0 +1,34 @@ +

    描述

    +

    测试动态改变selectAll接口

    +

    示例

    + +

    1.基本多选:

    +
    +国家: + + + +

    2.分组多选:

    +
    +国家: + + +
    + + diff --git a/src/select/demo/src/app/select/select-clearable.html b/src/select/demo/src/app/select/select-clearable.html new file mode 100644 index 0000000..af06e3c --- /dev/null +++ b/src/select/demo/src/app/select/select-clearable.html @@ -0,0 +1,24 @@ +

    1.单选

    + + + +

    2.多选

    + + + + diff --git a/src/select/demo/src/app/select/select-disabled.html b/src/select/demo/src/app/select/select-disabled.html new file mode 100644 index 0000000..cfee123 --- /dev/null +++ b/src/select/demo/src/app/select/select-disabled.html @@ -0,0 +1,5 @@ +

    1.整体禁用

    + + +

    2.数据项禁用

    + diff --git a/src/select/demo/src/app/select/select-disabledfocus.html b/src/select/demo/src/app/select/select-disabledfocus.html new file mode 100644 index 0000000..e5e02e9 --- /dev/null +++ b/src/select/demo/src/app/select/select-disabledfocus.html @@ -0,0 +1,10 @@ +
    {{myDisabled}} +
    + + + + + +

    diff --git a/src/select/demo/src/app/select/select-enum.html b/src/select/demo/src/app/select/select-enum.html new file mode 100644 index 0000000..01fed39 --- /dev/null +++ b/src/select/demo/src/app/select/select-enum.html @@ -0,0 +1,46 @@ +

    描述

    +

    Select选择下拉组件, 自定义选中值, 设置valueKey时选中值基于valueKey

    +

    valueKey接口

    +

    示例

    + +

    1.单选,设置valueKey:

    +
    +国家: + + +
    +

    选中项:{{mySelected1 | json}}

    +
    + +

    2.多选,设置valueKey:

    +
    +国家: + + +
    +

    选中项:{{mySelecteds | json}}

    +
    + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-event.html b/src/select/demo/src/app/select/select-event.html new file mode 100644 index 0000000..8b4cc9d --- /dev/null +++ b/src/select/demo/src/app/select/select-event.html @@ -0,0 +1,3 @@ + + + diff --git a/src/select/demo/src/app/select/select-focus.html b/src/select/demo/src/app/select/select-focus.html new file mode 100644 index 0000000..67144f7 --- /dev/null +++ b/src/select/demo/src/app/select/select-focus.html @@ -0,0 +1,38 @@ +

    描述

    +

    Select选择下拉组件, 支持聚焦autofocus/focus()/tabindex演示

    +

    示例

    + +

    1.autofocus:

    +

    + +

    2.focus():

    +
    +
    + +  + +  + +  + +

    + +

    3.tabindex:

    +tabindex="1" +

    +tabindex="3" +

    +tabindex="4" +

    +tabindex="2" + + + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-group.html b/src/select/demo/src/app/select/select-group.html new file mode 100644 index 0000000..b165411 --- /dev/null +++ b/src/select/demo/src/app/select/select-group.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/select-id.html b/src/select/demo/src/app/select/select-id.html new file mode 100644 index 0000000..d45d91b --- /dev/null +++ b/src/select/demo/src/app/select/select-id.html @@ -0,0 +1,53 @@ +

    描述

    +

    Select选择下拉组件,带有搜索框,设置[searchable]="true"

    +

    示例

    + +1.单选: + +
    + +2.模板: + + + + {{i}}  +  {{item.label}} {{item.englishname}} + + + +
    + +3.多选: + + +
    + +4.搜索: + + +
    + +5.分组: + +
    + +

    id是否存在:

    +

    {{id+':'+idExistMap.get(id)}}

    +

    所有id是否存在:{{allIdExist}}

    + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-idkey.html b/src/select/demo/src/app/select/select-idkey.html new file mode 100644 index 0000000..6a7312c --- /dev/null +++ b/src/select/demo/src/app/select/select-idkey.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/select-input.html b/src/select/demo/src/app/select/select-input.html new file mode 100644 index 0000000..2417739 --- /dev/null +++ b/src/select/demo/src/app/select/select-input.html @@ -0,0 +1,29 @@ +

    描述

    +

    Select选择下拉组件, 基础使用:不设置labelKey/设置labelKey/自定义内容模板

    +

    示例

    + +

    1.设置options(不设置labelKey,默认显示字段"label"):

    +
    +国家: + + + + + +
    + + + + + +
    + + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-labelkey.html b/src/select/demo/src/app/select/select-labelkey.html new file mode 100644 index 0000000..2005999 --- /dev/null +++ b/src/select/demo/src/app/select/select-labelkey.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/select-lazy.html b/src/select/demo/src/app/select/select-lazy.html new file mode 100644 index 0000000..e013c9a --- /dev/null +++ b/src/select/demo/src/app/select/select-lazy.html @@ -0,0 +1,2 @@ + + diff --git a/src/select/demo/src/app/select/select-leak.html b/src/select/demo/src/app/select/select-leak.html new file mode 100644 index 0000000..0b2cc36 --- /dev/null +++ b/src/select/demo/src/app/select/select-leak.html @@ -0,0 +1,10 @@ + +
    +
    +
    + 国家: + +
    +
    +

    选中项:{{mySelected | json}}

    +

    ti-drop个数:

    diff --git a/src/select/demo/src/app/select/select-load.html b/src/select/demo/src/app/select/select-load.html new file mode 100644 index 0000000..6ada47c --- /dev/null +++ b/src/select/demo/src/app/select/select-load.html @@ -0,0 +1,35 @@ +

    描述

    +

    Select选择下拉组件,数据加载。

    +

    示例

    +

    使用典型场景:空数据->数据A->数据B:

    +
    +单选: + +
    +

    选中项:{{mySelected | json}}

    + +多选: + + +
    +

    选中项:{{mySelecteds | json}}

    + +

    每个组件改变数据,都用下面六个按钮。不变化

    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-many.html b/src/select/demo/src/app/select/select-many.html new file mode 100644 index 0000000..97c0b9b --- /dev/null +++ b/src/select/demo/src/app/select/select-many.html @@ -0,0 +1,29 @@ +

    描述

    +

    Select选择下拉组件,性能测试。

    +

    示例

    +

    使用典型场景:空数据->大数据A->大数据B:

    +
    +单选: + +
    +

    选中项:{{mySelected | json}}

    +

    总条数:{{myOptions&&myOptions.length}}

    + +多选: + + +
    +

    选中项:{{mySelecteds | json}}

    +

    总条数:{{myOptions&&myOptions.length}}

    + +
    +
    + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-maxline.html b/src/select/demo/src/app/select/select-maxline.html new file mode 100644 index 0000000..8cba2a5 --- /dev/null +++ b/src/select/demo/src/app/select/select-maxline.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/select-much.html b/src/select/demo/src/app/select/select-much.html new file mode 100644 index 0000000..3ac05b8 --- /dev/null +++ b/src/select/demo/src/app/select/select-much.html @@ -0,0 +1,9 @@ +

    描述

    +

    Select选择下拉组件性能测试。

    +

    示例

    +

    使用典型场景:页面中使用大量select组件

    +
    + + +


    +
    diff --git a/src/select/demo/src/app/select/select-multi.html b/src/select/demo/src/app/select/select-multi.html new file mode 100644 index 0000000..8c40071 --- /dev/null +++ b/src/select/demo/src/app/select/select-multi.html @@ -0,0 +1,5 @@ + + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/select/demo/src/app/select/select-noborder.html b/src/select/demo/src/app/select/select-noborder.html new file mode 100644 index 0000000..37f36a7 --- /dev/null +++ b/src/select/demo/src/app/select/select-noborder.html @@ -0,0 +1,53 @@ +

    描述

    +

    + Selec选择下拉组件, 设置是否有边框:noborder属性
    设置dominator 到drop之间的距离:dominatorSpace接口
    设置深色主题:dark + 属性(主题只处理了无边框的场景) +

    +

    示例

    + +无边框: + +
    +
    +无边框有间距: + + +
    +
    +深色无边框有间距: +
    + + +
    +
    +
    +默认: + +
    +
    + + diff --git a/src/select/demo/src/app/select/select-nodata.html b/src/select/demo/src/app/select/select-nodata.html new file mode 100644 index 0000000..c337f87 --- /dev/null +++ b/src/select/demo/src/app/select/select-nodata.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/select-noempty.html b/src/select/demo/src/app/select/select-noempty.html new file mode 100644 index 0000000..997e293 --- /dev/null +++ b/src/select/demo/src/app/select/select-noempty.html @@ -0,0 +1,21 @@ +

    描述

    +

    Select选择下拉组件, 只显示非空选项

    +

    示例

    + +

    1.设置options(不设置labelKey,默认显示字段"label"):

    +
    +国家: + + +
    +

    选中项:{{mySelected | json}}

    +
    + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-null.html b/src/select/demo/src/app/select/select-null.html new file mode 100644 index 0000000..ddd24f2 --- /dev/null +++ b/src/select/demo/src/app/select/select-null.html @@ -0,0 +1,42 @@ +

    描述

    +

    Select选择下拉组件,可变@Input接口异常数据。不测ngModel,不测boolean,不测maxline=3等一旦赋值不再改变的接口。

    +

    + 因为Angular + Html模板,ngFor的items可以抵御undefined/null,但不能抵御类型错误(要求array,传入5)。所以,TS代码也保持同样的容错能力,要求能够抵御undefined/null等 +

    +

    + 这里仅测赋值null等是否正常。@Input数据动态变化,null->数据A->数据B,用代码Review来保证,不要在ngOnInit中处理@Input数据(因为Init只跑一次),而应该在ngOnChange里处理。 +

    +

    导入

    +import {{ '{' }} TiSelectModule {{ '}' }} from '@opentiny/ng'; +

    示例

    +以下示例,控制台是否报错,组件显示交互是否正常。 +

    1.undefined:

    +
    +国家: + + + +

    2.null:

    +
    +国家: + + +

    3.空数据([],'',0):

    +
    +国家: + + +

    4.边界数据(不涉及)

    +
    +

    5.超边界数据(不涉及)

    +
    + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-pagin-beforesearch.html b/src/select/demo/src/app/select/select-pagin-beforesearch.html new file mode 100644 index 0000000..f55fb19 --- /dev/null +++ b/src/select/demo/src/app/select/select-pagin-beforesearch.html @@ -0,0 +1,110 @@ +

    描述

    +

    10.0.3提供该功能

    +

    1.Select的下拉选项如果需要分页显示,需要通过footer将分页添加到下拉面板,如果footer功能异常,参考“模板”示例。

    +

    2.分页结合后台搜索的功能,需要开发者控制分页的 currentPage ,并且将对应页码的options数据传入组件。

    +

    导入

    +import {{ '{' }} TiSelectModule, TiPaginationModule {{ '}' }} from '@opentiny/ng'; +

    示例

    +

    设置options和自定义内容模板:

    + +

    1.单选 + 搜索 + 分页:(10.1.16 版本参考该示例)

    + + + + + +
    +

    选中项:{{mySelected1 | json}}

    + +

    2.单选 + 搜索 + 分页 + beforeOpen:(10.1.10~10.1.15 版本参考该示例)

    + + + + + +
    +

    选中项:{{mySelected2 | json}}

    + +

    3.多选 + 搜索 + 分页:(10.1.16 版本参考该示例)

    +
    + + + + + +
    +

    选中项:{{mySelected3 | json}}

    + +

    4.多选 + 搜索 + 分页 + beforeOpen:(10.1.10~10.1.15 版本参考该示例)

    +
    + + + + + +
    +

    选中项:{{mySelected4 | json}}

    diff --git a/src/select/demo/src/app/select/select-pagination.html b/src/select/demo/src/app/select/select-pagination.html new file mode 100644 index 0000000..5465f6e --- /dev/null +++ b/src/select/demo/src/app/select/select-pagination.html @@ -0,0 +1,11 @@ + + + + + +
    diff --git a/src/select/demo/src/app/select/select-panel.html b/src/select/demo/src/app/select/select-panel.html new file mode 100644 index 0000000..360cbf6 --- /dev/null +++ b/src/select/demo/src/app/select/select-panel.html @@ -0,0 +1,2 @@ + + diff --git a/src/select/demo/src/app/select/select-reservesearchword.html b/src/select/demo/src/app/select/select-reservesearchword.html new file mode 100644 index 0000000..70a81cb --- /dev/null +++ b/src/select/demo/src/app/select/select-reservesearchword.html @@ -0,0 +1,9 @@ + + diff --git a/src/select/demo/src/app/select/select-scroll-load.html b/src/select/demo/src/app/select/select-scroll-load.html new file mode 100644 index 0000000..87b32bf --- /dev/null +++ b/src/select/demo/src/app/select/select-scroll-load.html @@ -0,0 +1,12 @@ + + diff --git a/src/select/demo/src/app/select/select-search.html b/src/select/demo/src/app/select/select-search.html new file mode 100644 index 0000000..611e0f3 --- /dev/null +++ b/src/select/demo/src/app/select/select-search.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/select-searchkeys.html b/src/select/demo/src/app/select/select-searchkeys.html new file mode 100644 index 0000000..fd12ebc --- /dev/null +++ b/src/select/demo/src/app/select/select-searchkeys.html @@ -0,0 +1,9 @@ + + diff --git a/src/select/demo/src/app/select/select-selectall.html b/src/select/demo/src/app/select/select-selectall.html new file mode 100644 index 0000000..f389052 --- /dev/null +++ b/src/select/demo/src/app/select/select-selectall.html @@ -0,0 +1,2 @@ + + diff --git a/src/select/demo/src/app/select/select-showselectednumber.html b/src/select/demo/src/app/select/select-showselectednumber.html new file mode 100644 index 0000000..578e8dd --- /dev/null +++ b/src/select/demo/src/app/select/select-showselectednumber.html @@ -0,0 +1,11 @@ + + diff --git a/src/select/demo/src/app/select/select-small.html b/src/select/demo/src/app/select/select-small.html new file mode 100644 index 0000000..7908936 --- /dev/null +++ b/src/select/demo/src/app/select/select-small.html @@ -0,0 +1 @@ +
    diff --git a/src/select/demo/src/app/select/select-tag.html b/src/select/demo/src/app/select/select-tag.html new file mode 100644 index 0000000..6c25de3 --- /dev/null +++ b/src/select/demo/src/app/select/select-tag.html @@ -0,0 +1,70 @@ +

    描述

    +

    Select选择下拉组件选项添加tag标签

    +

    示例

    +

    设置options和自定义内容模板:

    + +

    1.单选

    + + + + {{i}}  +  {{item.label}} {{item.englishname}} + + + {{item.mark}} + {{item.tag}} + + + + + + {{item.label}} + {{item.englishname}} + {{item.tag}} + + + +
    +

    选中项:{{mySelected | json}}

    + +

    2.多选

    + + + + + {{i}}  +  {{item.label}} {{item.englishname}} + + + {{item.mark}} + {{item.tag}} + + + + + + {{item.label}} + {{item.englishname}} + {{item.tag}} + + + +
    +

    选中项:{{mySelecteds | json}}

    + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-tag.less b/src/select/demo/src/app/select/select-tag.less new file mode 100644 index 0000000..50989dc --- /dev/null +++ b/src/select/demo/src/app/select/select-tag.less @@ -0,0 +1,70 @@ +.select-dropdown-list-tag { + white-space: nowrap; + height: 16px; + line-height: 16px; + background-color: #fff3e8; + color: #fa9841; + display: inline-block; + border-radius: 2px; + margin-left: 10px; + padding: 0 4px; +} +.select-dropdown-list-mark { + background-color: #e9edfa; + color: #575d6c; +} +.select-selfTip { + min-width: 50px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.select-tag-container { + display: flex; + flex-grow: 1; + justify-content: space-between; +} + +.tag-in-dominator.select-dropdown-list-tag { + float: right; + transform: translateY(5px); +} +.tag-in-multDominator.select-dropdown-list-tag { + float: right; + transform: translateY(3px); +} + +.footer { + padding: 10px; + position: relative; + font-size: 12px; +} + +.footer ti-icon[local] { + margin-right: 4px; + font-size: 16px; + vertical-align: bottom; +} + +.footer:after { + position: absolute; + display: inline-block; + top: 0; + left: 10px; + content: ''; + width: calc(100% - 20px); + height: 1px; + background-color: var(--ti-common-color-line-dividing); +} + +.select-dropdown-list-mult { + display: inline-flex; + align-items: center; + width: calc(100% - 26px); +} + +.select-dropdown-list { + display: flex; + align-items: center; +} diff --git a/src/select/demo/src/app/select/select-template.html b/src/select/demo/src/app/select/select-template.html new file mode 100644 index 0000000..a75c7ac --- /dev/null +++ b/src/select/demo/src/app/select/select-template.html @@ -0,0 +1,25 @@ + + + + 请选择餐品 + + + {{ item.label }} + + {{ item.englishname }} + + + + {{ item.label }} + {{ item.englishname }} + + + + + + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/select/demo/src/app/select/select-tip.html b/src/select/demo/src/app/select/select-tip.html new file mode 100644 index 0000000..34e0cc1 --- /dev/null +++ b/src/select/demo/src/app/select/select-tip.html @@ -0,0 +1,12 @@ + + + + 当前提示文本:{{item.label}} diff --git a/src/select/demo/src/app/select/select-tiscroll.html b/src/select/demo/src/app/select/select-tiscroll.html new file mode 100644 index 0000000..ee5b4db --- /dev/null +++ b/src/select/demo/src/app/select/select-tiscroll.html @@ -0,0 +1,56 @@ +

    描述

    +

    tiScroll指令。

    +

    + 在有局部滚动条的容器中使用select,为了防止下拉框在容器滚动时与选择框分离,开发者需要在对应的容器上添加tiScroll指令,使下拉框在容器滚动时消失。
    + 也可以自己监听滚动元素的scroll事件,在回调中触发document下的tiScroll事件,使得容器元素滚动时面板收起 +

    +

    导入

    +import {{ '{' }} TiScrollModule {{ '}' }} from '@opentiny/ng'; +

    示例

    +
    +
    +

    1.没有使用tiScroll

    +
    +

    +

    红豆生南国

    +

    春来发几枝

    +

    愿君多采撷

    +

    此物最相思

    +

    红豆生南国

    +

    春来发几枝

    +

    愿君多采撷

    +

    此物最相思

    +
    +
    +
    +

    2.使用tiScroll指令

    +
    + +

    +

    红豆生南国

    +

    春来发几枝

    +

    愿君多采撷

    +

    此物最相思

    +

    红豆生南国

    +

    春来发几枝

    +

    愿君多采撷

    +

    此物最相思

    +
    +
    +
    +

    3.手动触发tiScroll

    +
    + +

    +

    红豆生南国

    +

    春来发几枝

    +

    愿君多采撷

    +

    此物最相思

    +

    红豆生南国

    +

    春来发几枝

    +

    愿君多采撷

    +

    此物最相思

    +
    +
    +
    diff --git a/src/select/demo/src/app/select/select-tworow.html b/src/select/demo/src/app/select/select-tworow.html new file mode 100644 index 0000000..a777589 --- /dev/null +++ b/src/select/demo/src/app/select/select-tworow.html @@ -0,0 +1,13 @@ + + + {{item.primary}} + + +
    {{i}} {{item.primary}}
    +
    {{item.secondary}}
    +
    +
    + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/select/demo/src/app/select/select-valid.html b/src/select/demo/src/app/select/select-valid.html new file mode 100644 index 0000000..f4864a4 --- /dev/null +++ b/src/select/demo/src/app/select/select-valid.html @@ -0,0 +1,10 @@ + + diff --git a/src/select/demo/src/app/select/select-validgroup.html b/src/select/demo/src/app/select/select-validgroup.html new file mode 100644 index 0000000..30a5e64 --- /dev/null +++ b/src/select/demo/src/app/select/select-validgroup.html @@ -0,0 +1,30 @@ +

    描述

    +

    Select选择下拉组件,校验

    +

    示例

    +
    + + + 单选: + + +
    +

    选中项:{{mySelected | json}}

    +
    + + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-valuekey-test.html b/src/select/demo/src/app/select/select-valuekey-test.html new file mode 100644 index 0000000..5db3f77 --- /dev/null +++ b/src/select/demo/src/app/select/select-valuekey-test.html @@ -0,0 +1,73 @@ +

    描述

    +

    Select选择下拉组件, 自定义选中值, 设置valueKey时选中值基于valueKey

    +

    valueKey接口

    +

    示例

    + +

    1.单选,设置valueKey:

    +
    +国家: + + +
    +

    选中项:{{mySelected1 | json}}

    +
    + +

    2.多选,设置valueKey:

    +
    +国家: + + +
    +

    选中项:{{mySelecteds | json}}

    +
    + +

    + 3.动态修改 + options 接口数据测试:(select组件的选中值高亮不应发生变化) +

    +
    + +
    +
    +

    单选/多选的options:{{myOptions | json}}

    + +

    4.多选+分组,设置valueKey:

    +
    + + +   + +
    +
    +

    选中项:{{mySelecteds2 | json}}

    +
    + +

    原生Select参考

    + diff --git a/src/select/demo/src/app/select/select-valuekey.html b/src/select/demo/src/app/select/select-valuekey.html new file mode 100644 index 0000000..20429bd --- /dev/null +++ b/src/select/demo/src/app/select/select-valuekey.html @@ -0,0 +1,5 @@ + + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/select/demo/src/app/select/select-virtualscroll-multi.html b/src/select/demo/src/app/select/select-virtualscroll-multi.html new file mode 100644 index 0000000..caa55e8 --- /dev/null +++ b/src/select/demo/src/app/select/select-virtualscroll-multi.html @@ -0,0 +1,141 @@ +

    描述

    +

    Select选择下拉组件, 下拉数据量大的时候可以将 virtual 接口设置为 true 来开启虚拟滚动功能。分组下拉不支持虚拟滚动。

    +

    virtual 接口:默认值为 false。

    +

    导入

    +import {{ '{' }} TiSelectModule {{ '}' }} from '@opentiny/ng'; +

    示例

    + +

    1.基础多选:

    +
    +国家: + + +
    +

    选中项:{{mySelected1 | json}}

    +

    总条数:{{myOptions1&&myOptions1.length}}

    + + +
    + +

    2.多选 + 搜索:

    +
    +国家: + + +
    +

    选中项:{{mySelected2 | json}}

    +

    总条数:{{myOptions2&&myOptions2.length}}

    + + +
    + +

    3.多选 + 全选:

    +
    +国家: + + +
    +

    选中项:{{mySelected3 | json}}

    +

    总条数:{{myOptions3&&myOptions3.length}}

    + + +
    + +

    4.多选 + 全选 + 搜索:

    +
    +国家: + + +
    +

    选中项:{{mySelected4 | json}}

    +

    总条数:{{myOptions4&&myOptions4.length}}

    + + +
    + +

    5.多选 + 搜索 + 自定义底部:

    +
    +国家: + + + + + +
    +

    选中项:{{mySelected5 | json}}

    +

    总条数:{{myOptions5&&myOptions5.length}}

    + + +
    + + diff --git a/src/select/demo/src/app/select/select-virtualscroll.html b/src/select/demo/src/app/select/select-virtualscroll.html new file mode 100644 index 0000000..a54fc96 --- /dev/null +++ b/src/select/demo/src/app/select/select-virtualscroll.html @@ -0,0 +1 @@ + diff --git a/src/select/demo/src/app/select/webdoc/select-demos.js b/src/select/demo/src/app/select/webdoc/select-demos.js new file mode 100644 index 0000000..7b8768d --- /dev/null +++ b/src/select/demo/src/app/select/webdoc/select-demos.js @@ -0,0 +1,324 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'select-basic', + name: { + 'zh-CN': '基础', + 'en-US': 'select basic' + }, + desc: { + 'zh-CN': 'select 组件的最简用法。', + 'en-US': '

    select basic

    ' + }, + apis: ['TiSelectComponent.properties.options', 'TiSelectComponent.properties.placeholder'] + }, + { + demoId: 'select-multi', + name: { + 'zh-CN': '多选', + 'en-US': 'select multi' + }, + desc: { + 'zh-CN': '

    通过属性multiple配置多选。', + 'en-US': 'select multi' + }, + apis: ['TiSelectComponent.properties.multiple'] + }, + { + demoId: 'select-selectall', + name: { + 'zh-CN': '全选', + 'en-US': 'select selectall' + }, + desc: { + 'zh-CN': '

    通过属性selectAll配置组件在多选场景下是否显示全选框。', + 'en-US': 'select selectall' + }, + apis: ['TiSelectComponent.properties.selectAll'] + }, + { + demoId: 'select-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'select disabled' + }, + desc: { + 'zh-CN': '

    通过属性disabled配置组件是否禁用。', + 'en-US': 'select disabled' + }, + apis: ['TiSelectComponent.properties.disabled'] + }, + { + demoId: 'select-template', + name: { + 'zh-CN': '模板', + 'en-US': 'select template' + }, + desc: { + 'zh-CN': '自定义模板,组件提供了selecteditemplaceholderfooter四个插槽。', + 'en-US': 'select template' + }, + apis: [ + 'TiSelectComponent.slots.selectedTemplate', + 'TiSelectComponent.slots.itemTemplate', + 'TiSelectComponent.slots.placeholderTemplate', + 'TiSelectComponent.slots.footerTemplate' + ] + }, + { + demoId: 'select-valuekey', + name: { + 'zh-CN': '选中值', + 'en-US': 'select valuekey' + }, + desc: { + 'zh-CN': '

    通过属性valueKey自定义组件选中值。', + 'en-US': 'select valuekey' + }, + apis: ['TiSelectComponent.properties.valueKey'] + }, + { + demoId: 'select-labelkey', + name: { + 'zh-CN': '显示字段', + 'en-US': 'select labelkey' + }, + desc: { + 'zh-CN': '

    通过属性labelKey配置组件下拉面板显示字段。', + 'en-US': 'select labelkey' + }, + apis: ['TiSelectComponent.properties.labelKey'] + }, + { + demoId: 'select-idkey', + name: { + 'zh-CN': '唯一值', + 'en-US': 'select idkey' + }, + desc: { + 'zh-CN': '

    labelkeyvaluekey有重复项时,通过属性idKey设置唯一属性,实现下拉选中。', + 'en-US': 'select idkey' + }, + apis: ['TiSelectComponent.properties.idKey'] + }, + { + demoId: 'select-maxline', + name: { + 'zh-CN': '最大显示行数', + 'en-US': 'select maxline' + }, + desc: { + 'zh-CN': '

    通过属性maxLine配置组件在多选场景下选择框显示的最大行数。', + 'en-US': 'select maxline' + }, + apis: ['TiSelectComponent.properties.maxLine'] + }, + { + demoId: 'select-group', + name: { + 'zh-CN': '分组', + 'en-US': 'select group' + }, + desc: { + 'zh-CN': '

    通过属性options中的children属性设置分组。', + 'en-US': 'select group' + }, + apis: ['TiSelectComponent.properties.options'] + }, + { + demoId: 'select-pagination', + name: { + 'zh-CN': '分页', + 'en-US': 'select pagination' + }, + desc: { + 'zh-CN': '通过footer插槽,实现下拉面板分页场景。', + 'en-US': 'select pagination' + }, + apis: ['TiSelectComponent.slots.footerTemplate'] + }, + { + demoId: 'select-search', + name: { + 'zh-CN': '搜索', + 'en-US': 'select search' + }, + desc: { + 'zh-CN': '

    通过属性searchable配置组件是否显示搜索框。', + 'en-US': 'select search' + }, + apis: ['TiSelectComponent.properties.searchable'] + }, + { + demoId: 'select-searchkeys', + name: { + 'zh-CN': '搜索字段', + 'en-US': 'select searchkeys' + }, + desc: { + 'zh-CN': '

    通过属性searchKeys配置组件搜索字段。', + 'en-US': 'select searchkeys' + }, + apis: ['TiSelectComponent.properties.searchKeys'] + }, + { + demoId: 'select-reservesearchword', + name: { + 'zh-CN': '保留搜索关键词', + 'en-US': 'select reservesearchword' + }, + desc: { + 'zh-CN': '

    搜索场景下,通过属性reserveSearchword配置下拉面板收起后是否保留搜素关键词。', + 'en-US': 'select reservesearchword' + }, + apis: ['TiSelectComponent.properties.reserveSearchword'] + }, + { + demoId: 'select-beforesearch', + name: { + 'zh-CN': '后台搜索', + 'en-US': 'select beforesearch' + }, + desc: { + 'zh-CN': '

    通过事件beforeSearch实现后台搜索。', + 'en-US': 'select beforesearch' + }, + apis: ['TiSelectComponent.events.beforeSearch'] + }, + { + demoId: 'select-lazy', + name: { + 'zh-CN': '懒加载', + 'en-US': 'select lazy' + }, + desc: { + 'zh-CN': '

    通过事件beforeOpen实现懒加载。', + 'en-US': 'select lazy' + }, + apis: ['TiSelectComponent.events.beforeOpen'] + }, + { + demoId: 'select-showselectednumber', + name: { + 'zh-CN': '显示已选项个数', + 'en-US': 'select showSelectedNumber' + }, + desc: { + 'zh-CN': '

    通过属性showSelectedNumber配置组件在多选场景下选择框是否显示已选项的个数。', + 'en-US': 'select showSelectedNumber' + }, + apis: [ + 'TiSelectComponent.properties.showSelectedNumber', + 'TiSelectComponent.properties.showSelectedNumberTip', + 'TiSelectComponent.properties.selectedNumberTipPosition' + ] + }, + { + demoId: 'select-clearable', + name: { + 'zh-CN': '可清除', + 'en-US': 'select clearable' + }, + desc: { + 'zh-CN': '

    通过属性clearable配置组件是否开启清除已选项功能。', + 'en-US': 'select clearable' + }, + apis: ['TiSelectComponent.properties.clearable', 'TiSelectComponent.events.clear'] + }, + { + demoId: 'select-panel', + name: { + 'zh-CN': '面板样式', + 'en-US': 'select panel' + }, + desc: { + 'zh-CN': '

    通过属性panelWidth配置下拉面板宽度,通过属性panelMaxHeight配置下拉面板最大高度。', + 'en-US': 'select panel' + }, + apis: ['TiSelectComponent.properties.panelWidth', 'TiSelectComponent.properties.panelMaxHeight'] + }, + { + demoId: 'select-tip', + name: { + 'zh-CN': '文字提示', + 'en-US': 'select tip' + }, + desc: { + 'zh-CN': '组件的文字提示场景。', + 'en-US': 'select tip' + }, + apis: [ + 'TiSelectComponent.properties.tipMaxWidth', + 'TiSelectComponent.properties.selectedTipPosition', + 'TiSelectComponent.properties.tipPosition' + ] + }, + { + demoId: 'select-nodata', + name: { + 'zh-CN': '空数据', + 'en-US': 'select nodata' + }, + desc: { + 'zh-CN': '

    通过属性noDataText配置组件空数据时显示文本。', + 'en-US': 'select nodata' + }, + apis: ['TiSelectComponent.properties.noDataText'] + }, + { + demoId: 'select-appendtobody', + name: { + 'zh-CN': 'appendToBody', + 'en-US': 'select appendtobody' + }, + desc: { + 'zh-CN': '

    通过属性appendToBody配置下拉面板是否添加在body上。', + 'en-US': 'select appendtobody' + }, + apis: ['TiSelectComponent.properties.appendToBody'] + }, + { + demoId: 'select-virtualscroll', + name: { + 'zh-CN': '虚拟滚动', + 'en-US': 'select virtualscroll' + }, + desc: { + 'zh-CN': '

    通过属性virtual配置组件是否开启虚拟滚动。', + 'en-US': 'select virtualscroll' + }, + apis: ['TiSelectComponent.properties.virtual'] + }, + { + demoId: 'select-scroll-load', + name: { + 'zh-CN': '下拉滚动加载', + 'en-US': 'select scroll-load' + }, + desc: { + 'zh-CN': '

    通过事件scrollToBottom实现下拉滚动加载。', + 'en-US': 'select scroll-load' + }, + apis: [ + 'TiSelectComponent.events.scrollToBottom', + 'TiSelectComponent.methods.getSearchResult', + 'TiSelectComponent.methods.getSearchWord', + 'TiSelectComponent.methods.open' + ] + }, + { + demoId: 'select-event', + name: { + 'zh-CN': '事件', + 'en-US': 'select event' + }, + desc: { + 'zh-CN': '点击当前项时,触发select事件。', + 'en-US': 'select event' + }, + apis: ['TiSelectComponent.events.select'] + } + ] +}; diff --git a/src/select/demo/src/app/select/webdoc/select.cn.md b/src/select/demo/src/app/select/webdoc/select.cn.md new file mode 100644 index 0000000..74891c5 --- /dev/null +++ b/src/select/demo/src/app/select/webdoc/select.cn.md @@ -0,0 +1,28 @@ +--- +title: Select 选择器 +--- +# Select 选择器 + +

    + +Select 组件是提供下拉选项菜单的组件。   + ++ 支持单选、多选、分组、搜索、懒加载等场景。 + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +Select 是提供选项菜单的组件。   + ++ 支持单选、多选、分组、搜索、懒加载等场景。 + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/select/demo/src/app/select/webdoc/select.en.md b/src/select/demo/src/app/select/webdoc/select.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/select/demo/src/app/select/webdoc/select.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git "a/src/select/demo/src/app/select/\346\265\213\350\257\225\347\224\250\344\276\213.txt" "b/src/select/demo/src/app/select/\346\265\213\350\257\225\347\224\250\344\276\213.txt" new file mode 100644 index 0000000..297c5fe --- /dev/null +++ "b/src/select/demo/src/app/select/\346\265\213\350\257\225\347\224\250\344\276\213.txt" @@ -0,0 +1,83 @@ +1select-basic + +selectı򣬿Դ塣 +selectı򣬿Թر塣 +selectǣԴ塣 +selectǣԹر塣 +bodyհ״ߵԪأselectʧʱرա + +ѡеĿѡֵselectı + +2select-disabled + +ѡ +нõѡ޷ѡС +δõѡѡС + +ѡ +нõѡ޷ıѡС +δõѡԸıѡС +ı򣬽õtag޷ȡѡС +ıδõtag޷ȡѡС + +ã +ťúselect޷ + +3select-event +ı䵥ѡ +ѡѡȥѡѡɾѡ + +3select-focus +ı䵥ѡ +ѡѡȥѡѡɾѡ + +4select-focus +ε4ť + +5select-group + +Ч +ѽĿЧ +Ч + +ᱻ +ģ + +6select-id + +7select-lazy +selectһ + +8select-leak +飬selectժûڴй©磺dropȻbodyϣδժ +裺selectdropᱻbodyϡbuttonngIfժselectʱٿdropǷȻbodyϡ +ظһ顣 + +9load +εchangeZeroDatachangeDataAchangeDataBÿεselect鿴 +εchangeSelectchangeSelectsѡݻᷢ仯 + +10multi +ѡѡȥѡѡɾѡ + +11nodata + +12noempty + +13pannel +δ򿪸 + +14search +AnؼʲִСд +֣ + +15small + +16template + +17tip + +18valid + + + diff --git a/src/select/demo/src/favicon.ico b/src/select/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/select/demo/src/index.html b/src/select/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/select/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/select/demo/src/main.ts b/src/select/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/select/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/select/demo/test.ts b/src/select/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/select/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/select/demo/tsconfig.app.json b/src/select/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/select/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/select/demo/tsconfig.spec.json b/src/select/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/select/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/select/lib/index.ts b/src/select/lib/index.ts new file mode 100644 index 0000000..baee9b8 --- /dev/null +++ b/src/select/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSelectModule'; diff --git a/src/select/lib/ng-package.json b/src/select/lib/ng-package.json new file mode 100644 index 0000000..cbc5c84 --- /dev/null +++ b/src/select/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/select", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/select/lib/package.json b/src/select/lib/package.json new file mode 100644 index 0000000..fa4f909 --- /dev/null +++ b/src/select/lib/package.json @@ -0,0 +1,17 @@ +{ + "name": "@opentiny/ng-select", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-dominator": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-dropsearch": "~1.0.0-beta.0", + "@opentiny/ng-list": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/select/lib/project.json b/src/select/lib/project.json new file mode 100644 index 0000000..785d86f --- /dev/null +++ b/src/select/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/select/lib", + "sourceRoot": "src/select/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/select"], + "options": { + "project": "src/select/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/select"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js select" + }, + { + "command": "ng default-build select" + }, + { + "command": "node build/clear-default-theme.js select" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/select && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build select && ng pack select && node build/publish.js select --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/select/lib/src/TiSelectComponent.ts b/src/select/lib/src/TiSelectComponent.ts new file mode 100644 index 0000000..3b03426 --- /dev/null +++ b/src/select/lib/src/TiSelectComponent.ts @@ -0,0 +1,429 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + Component, + ContentChild, + EventEmitter, + HostListener, + Input, + Output, + TemplateRef, + ViewChild +} from '@angular/core'; +import { TiFormComponent, TiWholeComponent } from '@opentiny/ng-base'; +import { TiDominatorComponent } from '@opentiny/ng-dominator'; +import { TiDropComponent } from '@opentiny/ng-drop'; +import { TiDropsearchComponent } from '@opentiny/ng-dropsearch'; +import { TiListComponent, TiListScrollLoad } from '@opentiny/ng-list'; +import { TiKeymap } from '@opentiny/ng-utils'; +import { TiPositionType } from '@opentiny/ng-utils'; +import { TiLocale } from '@opentiny/ng-locale'; +import packageInfo from '../package.json'; + +/** + * Select选择下拉组件 + * + * 支持单选/多选,分组,搜索,懒加载。 + * + * 单选主要功能为从一个数据集合中选择某一条数据,单选与RadioGroup功能相同,只是视觉呈现不同。 + * + * 多选主要功能是从一个数据集合中任意选择多条数据,与checkboxGroup功能相同,只是视觉呈现不同。 + * + */ +@Component({ + selector: 'ti-select', + templateUrl: './select.html', + styleUrls: ['./select.less', './select-small.less'], + providers: [TiFormComponent.getValueAccessor(TiSelectComponent)], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-select-small]': 'size === "small"', + '(blur)': 'onBlur()' + } +}) +export class TiSelectComponent extends TiWholeComponent { + /** + * @ignore + * dominator 到drop的距离 + */ + @Input() dominatorSpace: string = TiDropComponent.DOMINATOR_SPACE + 'px'; + /** + * 下拉面板是否多选 + */ + @Input() multiple: boolean = false; + /** + * 多选场景下,选择框最多显示行数 + */ + @Input() maxLine: number = 2; + /** + * 选择框中选中项的tip提示方向 + */ + @Input() selectedTipPosition: TiPositionType = 'auto'; + /** + * 选择框的占位文本 + */ + @Input() placeholder: string = ''; + // 面板配置 + /** + * 下拉面板的宽度。 + * + * 1."justified": 下拉面板的宽度与选择框宽度保持一致; + * + * 2."auto": 下拉面板的宽度根据下拉项的内容自动撑开; + * + * 3. 固定的下拉框宽度:不小于选择框的宽度,例如:"200px" + */ + @Input() panelWidth: 'justified' | 'auto' | string = 'justified'; // TODO: 确认类型 + /** + * 下拉面板最大高度 + */ + @Input() panelMaxHeight: string; + /** + * 下拉面板无数据时的显示文本 + */ + @Input() noDataText: string = TiLocale.getLocaleWords().tiList.noDataText; + // 列表数据配置 + /** + * 下拉选项的全部数据 + */ + @Input() options: Array; + /** + * 下拉面板要显示的键值 + */ + @Input() labelKey: string = 'label'; + /** + * 下拉面板是否开启搜索功能 + */ + @Input() searchable: boolean = false; + /** + * 指定搜索的字段范围 + */ + @Input() searchKeys: Array; + // 其他配置 + /** + * @ignore + * 选择框大小 + */ + @Input() size: 'default' | 'small' = 'default'; + /** + * 下拉面板选中项的 tip 提示方向 + */ + @Input() tipPosition: TiPositionType = 'right'; + /** + * 选择框是否开启清除已选项功能 + * + * 单选下拉一键清除功能 + * + * 多选下拉一键清除功能 + */ + @Input() clearable: boolean = false; + /** + * 多选场景下是否开启全选功能 + */ + @Input() selectAll: boolean = false; + /** + * 多选场景下选择框是否显示选中项个数 + */ + @Input() showSelectedNumber: boolean = false; + /** + * 是否开启虚拟滚动 + */ + @Input() virtual: boolean = false; + /** + * @ignore + * TODO: 暂不对外开放该接口,后续根据使用场景进行优化 + * 当开启虚拟滚动时,可配置单条选项的高度(单位是px), 默认值30 + */ + @Input() itemSize: number = TiListComponent.OPTION_DEFAULT_HEIGHT; + /** + * tip 提示最大宽度 + */ + @Input() tipMaxWidth: string; + /** + * @ignore + * + * 默认值为 labelKey 的接口值 + * + * idKey指定的属性的值相等时即认为 option 选项是选中的。选中项 ngModel 中的数据跟 options 数据集中的选项(option对象)之间对应相等关系的依据属性。 + * + * 当 modelOption中的 idKey 设置的属性的值 与 option 中的 idKey 设置的属性的值相等时,则认为 modelOption 和 option 是对应的相等关系,即认为 option 选项是选中的。 + * + * 默认当 modelOption === option 或者 modelOption 中的 labelKey 设置的属性的值 与 option 中的 设置的属性的值相等时,则认为 option 选项是选中的。 + * + */ + + /** + * 设置数据唯一标识的键值,默认为 labelKey 的接口值 + */ + @Input() idKey: string; + /** + * 下拉面板是否添加在 body 上 + */ + @Input() appendToBody: boolean = true; + /** + * 多选场景下,选择框是否显示已选个数的tip提示方向 + */ + @Input() showSelectedNumberTip: boolean = false; + /** + * 选择框已选个数的tip提示方向 + */ + @Input() selectedNumberTipPosition: TiPositionType = 'bottom'; + /** + * 搜索场景下,下拉面板收起后是否保留搜索关键词 + */ + @Input() reserveSearchword: boolean = false; + /** + * 打开面板前触发的回调,参数为当前组件实例,一般用于数据懒加载场景 + */ + @Output() readonly beforeOpen: EventEmitter = new EventEmitter(); + /** + * 下拉选项选中或取消选中时触发的回调,参数:当前点击项 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + /** + * 点击清除按钮时触发的回调 + * + * 单选下拉一键清除功能 + * + * 多选下拉一键清除功能 + */ + @Output() readonly clear: EventEmitter = new EventEmitter(); + /** + * 搜索前触发的回调,参数:搜索关键词,一般用于后台搜索 + */ + @Output() readonly beforeSearch: EventEmitter = new EventEmitter(); + /** + * 下拉列表滚动到底部触发的回调,一般用于滚动加载场景 + */ + @Output() readonly scrollToBottom: EventEmitter = new EventEmitter(); + /** + * @ignore + * 用户写的item模板,firstTemplate是第一个或者唯一的template + */ + @ContentChild(TemplateRef, { static: false }) firstTemplate: TemplateRef; + /** + * 下拉选项区域的模板 + */ + @ContentChild('item', { static: false }) itemTemplate: TemplateRef; + /** + * 选择框选中项区域的模板 + */ + @ContentChild('selected', { static: false }) + selectedTemplate: TemplateRef; + /** + * 选择框占位文本区域的模板 + */ + @ContentChild('placeholder', { static: false }) + placeholderTemplate: TemplateRef; + /** + * 下拉面板底部区域的模板 + */ + @ContentChild('footer', { static: false }) footerTemplate: TemplateRef; + /** + * @ignore + * 内部标签donimator组件 + */ + @ViewChild(TiDominatorComponent, { static: true }) + dominatorCom: TiDominatorComponent; + /** + * @ignore + * 内部标签Droplist组件 + */ + @ViewChild(TiDropsearchComponent, { static: true }) + dropsearchCom: TiDropsearchComponent; + protected oldSearchable: boolean = null; + protected versionInfo: string = super.getVersion(packageInfo); + ngOnInit(): void { + super.ngOnInit(); + if (this.beforeSearch.observers.length !== 0) { + this.dropsearchCom.hasBeforeSearchObservers = true; + } + if (this.scrollToBottom.observers.length > 0) { + this.virtual = true; + } + } + + /** + * 兼容旧版: + * 3.1.2版select只能内嵌一个模板,无命名。 + * 新版可以内嵌四个模板,示例书写要求都命名。 + * 但需要兼容旧版无命名测试用例。 + */ + ngAfterContentInit(): void { + super.ngAfterContentInit(); + // 如果item模板为空,那么把第一个出现的无标签(无footer等标签)模板作为item + if (!this.itemTemplate && this.firstTemplate) { + const firstTemplateEle: Element = this.firstTemplate.elementRef.nativeElement; + if ( + firstTemplateEle !== (this.selectedTemplate && this.selectedTemplate.elementRef.nativeElement) && + firstTemplateEle !== (this.placeholderTemplate && this.placeholderTemplate.elementRef.nativeElement) && + firstTemplateEle !== (this.footerTemplate && this.footerTemplate.elementRef.nativeElement) + ) { + this.itemTemplate = this.firstTemplate; + } + } + } + + ngAfterViewChecked(): void { + // 要支持searchable的动态变更,所以ngAfterViewChecked才能获知搜索框节点。 + if (this.searchable !== this.oldSearchable) { + this.oldSearchable = this.searchable; + if (this.searchable) { + this.setFocusableElems(this.dominatorCom.getFocusableElems().concat(this.dropsearchCom.getFocusableElems())); + } else { + this.setFocusableElems(this.dominatorCom.getFocusableElems()); + } + } + if (this.dropsearchCom.isFocusableElemsInPaginationChange) { + this.setFocusableElems(this.dominatorCom.getFocusableElems().concat(this.dropsearchCom.getFocusableElems())); + this.dropsearchCom.isFocusableElemsInPaginationChange = false; + } + // 上面设置好focusElems后,调用父类逻辑afterViewChecked才去设置autofocs + super.ngAfterViewChecked(); + } + + /** + * @ignore + * 切换面板开合状态 + */ + public toggle(): void { + if (!this.dropsearchCom.isShow) { + // 面板关闭时 + this.wantOpen(); + } else { + // 面板开时 + this.close(); + } + } + /** + * 打开面板 + */ + public open(): void { + this.dropsearchCom.show(); + } + /** + * @ignore + * 收起面板 + */ + public close(): void { + this.dropsearchCom.hide(); + } + /** + * @ignore + * 处理点击Dominator事件 + */ + public onClickDominator(): void { + if (this.disabled) { + return; + } + this.toggle(); + } + /** + * @ignore + * ti-select键盘事件处理:回车/空格情况下,展开面板 + * @param event 按键事件 + * @returns void + */ + @HostListener('keydown', ['$event']) public onKeydown(event: KeyboardEvent): void { + if (this.disabled || this.dropsearchCom.isShow) { + return; + } + const enterKeyCodeArr: Array = [TiKeymap.KEY_SPACE, TiKeymap.KEY_ENTER, TiKeymap.KEY_NUMPAD_ENTER]; + if (enterKeyCodeArr.includes(event.keyCode)) { + this.wantOpen(); + // 如果响应了按键,那么不再冒泡 + event.stopPropagation(); + } + } + /** + * @ignore + * 失焦情况下,仅关闭面板,不做聚焦等处理 + */ + public onBlur(): void { + this.dropsearchCom.hideWithoutFocus(); + } + /** + * 点击或者toggle(),尝试打开面板 + */ + protected wantOpen(): void { + if (this.beforeOpen.observers.length === 0) { + // 无beforeOpen懒加载,直接打开 + this.open(); + } else { + // 有beforeOpen懒加载,发出事件 + this.beforeOpen.emit(this); + } + } + /** + * @ignore + * 多选带searchbox场景下,dominator中元素删除时,需要聚焦于searchbox + */ + public onDeleteDominator(): void { + if (this.dropsearchCom.isShow) { + this.dropsearchCom.focus(); + } + } + /** + * @ignore + * 点击下拉选项触发的回调 + */ + public onSelect(option: any): void { + this.select.emit(option); + if (this.multiple) { + return; + } + } + /** + * @ignore + * 单选点击清除按钮时触发clear事件, 如果下拉中有搜索,则需要聚焦于searchbox。 + */ + public onClearDominator(): void { + this.clear.emit(); + if (this.searchable && this.dropsearchCom.isShow) { + this.dropsearchCom.focus(); + } + } + /** + * @ignore + */ + public onBeforeSearch(): void { + this.beforeSearch.emit(this); + } + /** + * 获取搜索关键词 + * @returns 搜索关键词 + */ + public getSearchWord(): string { + return this.dropsearchCom.searchWord; + } + /** + * @ignore + * 设置搜索结果 + * @params 业务设置搜索后的结果,组件不再进行数据处理 + */ + public setSearchResult(searchResult: Array): void { + this.dropsearchCom.setSearchResult(searchResult); + } + /** + * 获取搜索后的下拉列表数据 + */ + public getSearchResult(): Array { + return this.dropsearchCom.searchResult; + } + /** + * @ignore + */ + public onScrollToBottom(scrollLoad: TiListScrollLoad): void { + this.scrollToBottom.emit(scrollLoad); + } +} diff --git a/src/select/lib/src/TiSelectModule.ts b/src/select/lib/src/TiSelectModule.ts new file mode 100644 index 0000000..93a03fa --- /dev/null +++ b/src/select/lib/src/TiSelectModule.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiDropsearchModule } from '@opentiny/ng-dropsearch'; +import { TiDominatorModule } from '@opentiny/ng-dominator'; +import { TiSelectComponent } from './TiSelectComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiDominatorModule, TiDropModule, TiDropsearchModule], + exports: [TiSelectComponent], + declarations: [TiSelectComponent] +}) +export class TiSelectModule {} + +export { TiSelectComponent } from './TiSelectComponent'; diff --git a/src/select/lib/src/select-small.less b/src/select/lib/src/select-small.less new file mode 100644 index 0000000..be47289 --- /dev/null +++ b/src/select/lib/src/select-small.less @@ -0,0 +1,9 @@ +@import '../../../themes/basic/base-all.less'; + +:host.ti3-select-small ti-dominator { + --ti-dominator-container-height: var(--ti-common-size-6x); +} + +// ::ng-deep,表示后面紧跟的样式选择器,不做属性[]包装。 +// CSS标准中 /deep/ >>>刺穿Shadow DOM, 已废弃。所以,这里暂时用::ng-deep angular关键词,以兼容未来可能其他标准。 +// https://blog.csdn.net/sky_sunshine_x/article/details/80622617 diff --git a/src/select/lib/src/select.html b/src/select/lib/src/select.html new file mode 100644 index 0000000..4147532 --- /dev/null +++ b/src/select/lib/src/select.html @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + +{{item[labelKey]}} + + diff --git a/src/select/lib/src/select.less b/src/select/lib/src/select.less new file mode 100644 index 0000000..3992f8f --- /dev/null +++ b/src/select/lib/src/select.less @@ -0,0 +1,37 @@ +@import '../../../themes/basic/base-all.less'; +@import '../../../themes/basic/compnent-container-border.less'; + +:host :extend(.ti3-compnent-container-border all) { + width: var(--ti-common-size-50x); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); +} + +//无边框的样式 +:host[noborder] { + border-color: var(--ti-common-color-bg-white-normal); + &:hover, + &[tiFocused] { + border-color: var(--ti-common-color-bg-white-normal); + } + &[disabled] { + border-color: var(--ti-common-color-bg-disabled); + } +} + +// 深色背景色只处理无边框的场景, disabled /focused 状态没有规范,按透明处理 +:host[dark] { + background-color: transparent; + border-color: transparent; + &:hover, + &[tiFocused], + &[disabled] { + border-color: transparent; + } + ::ng-deep .ti3-select-dominator-text { + color: var(--ti-common-color-text-white); + } + ::ng-deep .ti3-select-dominator-dropdown-btn:after { + border-top-color: var(--ti-common-color-icon-white); + } +} diff --git a/src/selectgroup/demo/project.json b/src/selectgroup/demo/project.json new file mode 100644 index 0000000..538d529 --- /dev/null +++ b/src/selectgroup/demo/project.json @@ -0,0 +1,86 @@ +{ + "projectType": "application", + "root": "src/selectgroup/demo", + "sourceRoot": "src/selectgroup/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/selectgroup", + "index": "src/selectgroup/demo/src/index.html", + "main": "src/selectgroup/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/selectgroup/demo/tsconfig.app.json", + "assets": [ + "src/selectgroup/demo/src/favicon.ico", + "src/selectgroup/demo/src/assets", + { + "glob": "**/*", + "input": "node_modules/ionicons/dist/ionicons/svg", + "output": "/assets/ionicons/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "selectgroup-demo:build:production" + }, + "development": { + "browserTarget": "selectgroup-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js selectgroup" + } + ] + } + } + } +} diff --git a/src/selectgroup/demo/src/app/AppComponent.ts b/src/selectgroup/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/selectgroup/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/selectgroup/demo/src/app/AppModule.ts b/src/selectgroup/demo/src/app/AppModule.ts new file mode 100644 index 0000000..b7baf0b --- /dev/null +++ b/src/selectgroup/demo/src/app/AppModule.ts @@ -0,0 +1,26 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { HttpClientModule } from '@angular/common/http'; +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SelectgroupTestModule } from './selectgroup/SelectgroupTestModule'; + +@NgModule({ + imports: [ + SelectgroupTestModule, + HttpClientModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/selectgroup/demo/src/app/IndexComponent.ts b/src/selectgroup/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..384754d --- /dev/null +++ b/src/selectgroup/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SelectgroupTestModule } from './selectgroup/SelectgroupTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SelectgroupTestModule.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} diff --git a/src/selectgroup/demo/src/app/app.html b/src/selectgroup/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/selectgroup/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/selectgroup/demo/src/app/selectgroup/SelectgroupBasicComponent.ts b/src/selectgroup/demo/src/app/selectgroup/SelectgroupBasicComponent.ts new file mode 100644 index 0000000..3b36299 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/SelectgroupBasicComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { TiSelectgroupItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './selectgroup-basic.html' +}) +export class SelectgroupBasicComponent { + items: Array = [ + { + title: 'Angular', + content: '^15.0.0', + iconName: 'logo-angular' + }, + { + title: 'HTML5', + content: 'html5', + iconName: 'logo-html5', + disabled: true + } + ]; + value: TiSelectgroupItem; +} diff --git a/src/selectgroup/demo/src/app/selectgroup/SelectgroupMultipleComponent.ts b/src/selectgroup/demo/src/app/selectgroup/SelectgroupMultipleComponent.ts new file mode 100644 index 0000000..39c82d8 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/SelectgroupMultipleComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { TiSelectgroupItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './selectgroup-multiple.html' +}) +export class SelectgroupMultipleComponent { + checkPosition: string = 'right-bottom'; + items: Array = [ + { + title: 'Angular', + content: '^15.0.0', + iconName: 'logo-angular' + }, + { + title: 'HTML5', + content: 'html5', + iconName: 'logo-html5' + } + ]; + value: Array; +} diff --git a/src/selectgroup/demo/src/app/selectgroup/SelectgroupSelectComponent.ts b/src/selectgroup/demo/src/app/selectgroup/SelectgroupSelectComponent.ts new file mode 100644 index 0000000..8bf8568 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/SelectgroupSelectComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { TiSelectgroupItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './selectgroup-select.html' +}) +export class SelectgroupSelectComponent { + items: Array = [ + { + title: 'Angular', + options: [{ label: '^13.0.0' }, { label: '^14.0.0' }, { label: '^15.0.0' }], + iconName: 'logo-angular' + }, + { + title: 'HTML5', + options: [{ label: 'html5' }], + iconName: 'logo-html5' + } + ]; + value: TiSelectgroupItem; +} diff --git a/src/selectgroup/demo/src/app/selectgroup/SelectgroupTemplateComponent.ts b/src/selectgroup/demo/src/app/selectgroup/SelectgroupTemplateComponent.ts new file mode 100644 index 0000000..28bf910 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/SelectgroupTemplateComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { TiSelectgroupItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './selectgroup-template.html' +}) +export class SelectgroupTemplateComponent { + items: Array = [ + { + title: 'Angular', + content: '^15.0.0', + iconName: 'logo-angular' + }, + { + title: 'HTML5', + content: 'html5', + iconName: 'logo-html5' + } + ]; + value: TiSelectgroupItem; +} diff --git a/src/selectgroup/demo/src/app/selectgroup/SelectgroupTestModule.ts b/src/selectgroup/demo/src/app/selectgroup/SelectgroupTestModule.ts new file mode 100644 index 0000000..d293f06 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/SelectgroupTestModule.ts @@ -0,0 +1,40 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { SelectgroupBasicComponent } from './SelectgroupBasicComponent'; +import { SelectgroupMultipleComponent } from './SelectgroupMultipleComponent'; +import { SelectgroupValuekeyComponent } from './SelectgroupValuekeyComponent'; +import { SelectgroupTemplateComponent } from './SelectgroupTemplateComponent'; +import { SelectgroupSelectComponent } from './SelectgroupSelectComponent'; +import { TiSelectgroupModule, TiSelectModule, TiIconModule, TiSvgComponent } from '@opentiny/ng'; +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiIconModule, + TiSelectModule, + TiSelectgroupModule, + RouterModule.forChild(SelectgroupTestModule.ROUTES) + ], + declarations: [ + SelectgroupBasicComponent, + SelectgroupMultipleComponent, + SelectgroupValuekeyComponent, + SelectgroupTemplateComponent, + SelectgroupSelectComponent + ] +}) +export class SelectgroupTestModule { + constructor() { + TiSvgComponent.setPath('/assets/ionicons/'); + } + static readonly ROUTES: Routes = [ + { path: 'selectgroup/selectgroup-basic', component: SelectgroupBasicComponent, data: { label: '基础' } }, + { path: 'selectgroup/selectgroup-multiple', component: SelectgroupMultipleComponent, data: { label: '多选' } }, + { path: 'selectgroup/selectgroup-valuekey', component: SelectgroupValuekeyComponent, data: { label: '自定义选中值' } }, + { path: 'selectgroup/selectgroup-template', component: SelectgroupTemplateComponent, data: { label: '自定义模板' } }, + { path: 'selectgroup/selectgroup-select', component: SelectgroupSelectComponent, data: { label: '下拉框' } } + ]; +} diff --git a/src/selectgroup/demo/src/app/selectgroup/SelectgroupValuekeyComponent.ts b/src/selectgroup/demo/src/app/selectgroup/SelectgroupValuekeyComponent.ts new file mode 100644 index 0000000..e71e94a --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/SelectgroupValuekeyComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { TiSelectgroupItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './selectgroup-valuekey.html' +}) +export class SelectgroupValuekeyComponent { + items: Array = [ + { + title: 'Angular', + content: '^15.0.0', + iconName: 'logo-angular' + }, + { + title: 'HTML5', + content: 'html5', + iconName: 'logo-html5' + } + ]; + value: TiSelectgroupItem; +} diff --git a/src/selectgroup/demo/src/app/selectgroup/selectgroup-basic.html b/src/selectgroup/demo/src/app/selectgroup/selectgroup-basic.html new file mode 100644 index 0000000..63a18ff --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/selectgroup-basic.html @@ -0,0 +1,3 @@ + + + diff --git a/src/selectgroup/demo/src/app/selectgroup/selectgroup-multiple.html b/src/selectgroup/demo/src/app/selectgroup/selectgroup-multiple.html new file mode 100644 index 0000000..6174f5a --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/selectgroup-multiple.html @@ -0,0 +1,9 @@ + + + + diff --git a/src/selectgroup/demo/src/app/selectgroup/selectgroup-select.html b/src/selectgroup/demo/src/app/selectgroup/selectgroup-select.html new file mode 100644 index 0000000..e355c75 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/selectgroup-select.html @@ -0,0 +1,10 @@ + + + + + diff --git a/src/selectgroup/demo/src/app/selectgroup/selectgroup-template.html b/src/selectgroup/demo/src/app/selectgroup/selectgroup-template.html new file mode 100644 index 0000000..3fd017b --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/selectgroup-template.html @@ -0,0 +1,28 @@ + + + +
    +
    {{item.title}}
    +
    {{item.content}}
    +
    +
    +
    +
    + + diff --git a/src/selectgroup/demo/src/app/selectgroup/selectgroup-valuekey.html b/src/selectgroup/demo/src/app/selectgroup/selectgroup-valuekey.html new file mode 100644 index 0000000..a51a300 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/selectgroup-valuekey.html @@ -0,0 +1,6 @@ + + + +
    +
    Current Vlaue: {{ value | json }}
    +
    diff --git a/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup-demos.js b/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup-demos.js new file mode 100644 index 0000000..3602b8e --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup-demos.js @@ -0,0 +1,73 @@ +export default { + column: '2', + demos: [ + { + demoId: 'selectgroup-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'selectgroup basic' + }, + desc: { + 'zh-CN': '

    Selectgroup 组件的最简用法。

    ', + 'en-US': 'selectgroup basic' + }, + apis: ['TiSelectitemComponent.properties.item'] + }, + { + demoId: 'selectgroup-multiple', + name: { + 'zh-CN': '多选', + 'en-US': 'selectgroup multiple' + }, + desc: { + 'zh-CN': + '

    通过属性multiple配置是否多选;通过属性checkPosition配置多选角标的位置,包括right-top(默认)、right-bottom两种类型。

    ', + 'en-US': 'selectgroup multiple' + }, + apis: ['TiSelectgroupComponent.properties.multiple', 'TiSelectitemComponent.properties.checkPosition'] + }, + { + demoId: 'selectgroup-valuekey', + name: { + 'zh-CN': '自定义选中值', + 'en-US': 'selectgroup valuekey' + }, + desc: { + 'zh-CN': '

    通过属性valueKey配置选中项数据的键值。

    ', + 'en-US': 'selectgroup valuekey' + }, + apis: ['TiSelectgroupComponent.properties.valueKey'] + }, + { + demoId: 'selectgroup-template', + name: { + 'zh-CN': '模板', + 'en-US': 'selectgroup template' + }, + desc: { + 'zh-CN': '

    通过属性template配置每项卡片展示的模板。

    ', + 'en-US': 'selectgroup template' + }, + apis: ['TiSelectitemComponent.slots.itemTemplate'] + }, + { + demoId: 'selectgroup-select', + name: { + 'zh-CN': '选择下拉', + 'en-US': 'selectgroup select' + }, + desc: { + 'zh-CN': '

    在属性items中配置options可以结合下拉框使用。

    ', + 'en-US': 'selectgroup select' + } + } + ], + ignoreApis: [ + 'TiSelectgroupComponent.properties.disabled', + 'TiSelectgroupComponent.properties.tabindex', + 'TiSelectgroupComponent.events.blur', + 'TiSelectgroupComponent.events.focus', + 'TiSelectgroupComponent.methods.blur', + 'TiSelectgroupComponent.methods.focus' + ] +}; diff --git a/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.cn.md b/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.cn.md new file mode 100644 index 0000000..4a3ef0e --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.cn.md @@ -0,0 +1,15 @@ +--- +title: Selectgroup 选择组 +--- + +# Selectgroup 选择组 + +
    + +选择组是一组选块聚合起来供以选择的组件。 + +```typescript +import { TiSelectgroupModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.en.md b/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.en.md new file mode 100644 index 0000000..5635345 --- /dev/null +++ b/src/selectgroup/demo/src/app/selectgroup/webdoc/selectgroup.en.md @@ -0,0 +1,13 @@ +--- +title: Selectgroup +--- + +# Selectgroup + +
    + +```typescript +import { TiSelectgroupModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/selectgroup/demo/src/favicon.ico b/src/selectgroup/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/selectgroup/demo/src/index.html b/src/selectgroup/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/selectgroup/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/selectgroup/demo/src/main.ts b/src/selectgroup/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/selectgroup/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/selectgroup/demo/tsconfig.app.json b/src/selectgroup/demo/tsconfig.app.json new file mode 100644 index 0000000..36c60b6 --- /dev/null +++ b/src/selectgroup/demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": ["src/main.ts", "../../polyfills.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/src/selectgroup/lib/index.ts b/src/selectgroup/lib/index.ts new file mode 100644 index 0000000..f75fa47 --- /dev/null +++ b/src/selectgroup/lib/index.ts @@ -0,0 +1,2 @@ +export * from './src/TiSelectgroupModule'; +export * from './src/TiSelectgroupComponent'; diff --git a/src/selectgroup/lib/ng-package.json b/src/selectgroup/lib/ng-package.json new file mode 100644 index 0000000..34516a0 --- /dev/null +++ b/src/selectgroup/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/selectgroup", + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/src/selectgroup/lib/package.json b/src/selectgroup/lib/package.json new file mode 100644 index 0000000..26812fd --- /dev/null +++ b/src/selectgroup/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-selectgroup", + "version": "1.0.0-beta.0", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/selectgroup/lib/project.json b/src/selectgroup/lib/project.json new file mode 100644 index 0000000..50567fb --- /dev/null +++ b/src/selectgroup/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/selectgroup/lib", + "sourceRoot": "src/selectgroup/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/selectgroup"], + "options": { + "project": "src/selectgroup/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/selectgroup"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js selectgroup" + }, + { + "command": "ng default-build selectgroup" + }, + { + "command": "node build/clear-default-theme.js selectgroup" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/selectgroup && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build selectgroup && ng pack selectgroup && node build/publish.js selectgroup --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/selectgroup/lib/src/TiSelectgroupComponent.ts b/src/selectgroup/lib/src/TiSelectgroupComponent.ts new file mode 100644 index 0000000..0c1e256 --- /dev/null +++ b/src/selectgroup/lib/src/TiSelectgroupComponent.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +/** + * 选择组 组件 + */ +@Component({ + selector: 'ti-selectgroup', + templateUrl: './selectgroup.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiSelectgroupComponent)] +}) +export class TiSelectgroupComponent extends TiFormComponent { + /** + * 是否多选 + */ + @Input() multiple: boolean; + /** + * 指定选中项数据的键值 + */ + @Input() valueKey: string; +} diff --git a/src/selectgroup/lib/src/TiSelectgroupModule.ts b/src/selectgroup/lib/src/TiSelectgroupModule.ts new file mode 100644 index 0000000..1222807 --- /dev/null +++ b/src/selectgroup/lib/src/TiSelectgroupModule.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiIconModule } from '@opentiny/ng-icon'; + +import { TiSelectgroupComponent } from './TiSelectgroupComponent'; +import { TiSelectitemComponent } from './TiSelectitemComponent'; + +@NgModule({ + imports: [CommonModule, TiOverflowModule, TiIconModule], + exports: [TiSelectgroupComponent, TiSelectitemComponent], + declarations: [TiSelectgroupComponent, TiSelectitemComponent] +}) +export class TiSelectgroupModule {} +export { TiSelectgroupComponent } from './TiSelectgroupComponent'; +export { TiSelectitemComponent, TiSelectgroupItem } from './TiSelectitemComponent'; diff --git a/src/selectgroup/lib/src/TiSelectitemComponent.ts b/src/selectgroup/lib/src/TiSelectitemComponent.ts new file mode 100644 index 0000000..fa3d694 --- /dev/null +++ b/src/selectgroup/lib/src/TiSelectitemComponent.ts @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + Input, + Renderer2, + TemplateRef +} from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; + +import { TiSelectgroupComponent } from './TiSelectgroupComponent'; +export interface TiSelectgroupItem { + /** + * 卡片标题 + */ + title: string; + /** + * 卡片内容 + */ + content?: string; + /** + * 卡片是否禁用 + */ + disabled?: boolean; + /** + * 卡片左侧图标名 + */ + iconName: string; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} +@Component({ + selector: 'ti-selectitem', + templateUrl: './selectitem.html', + styleUrls: ['./selectgroup.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-selectitem-box]': 'true', + '[class.ti3-selectitem-disabled]': 'item.disabled', + '[class.ti3-selectitem-checked]': 'isChecked()', + '[class.ti3-selectitem-template]': 'itemTemplate', + '(click)': 'onClick()' + } +}) +export class TiSelectitemComponent extends TiBaseComponent { + /** + * 数据配置 + */ + @Input() item: TiSelectgroupItem; + /** + * 选中标识位置 + */ + @Input() checkPosition: 'right-top' | 'right-bottom' = 'right-top'; + /** + * 每项模板 + */ + @ContentChild('item', /* TODO: add static flag */ { static: true }) itemTemplate: TemplateRef; + + /** + * @ignore + */ + public selectgroup: TiSelectgroupComponent; + + constructor( + selectgroup: TiSelectgroupComponent, + protected hostRef: ElementRef, + protected renderer: Renderer2, + protected changeDetectorRef: ChangeDetectorRef + ) { + super(hostRef, renderer); + this.selectgroup = selectgroup; + } + + private valueFn: (item: any) => any = (item: any) => { + return item[this.selectgroup.valueKey]; + }; + + /** + * @ignore + */ + public onClick(): void { + if (this.item.disabled) { + return; + } + // valuekey是否存在,存在时做处理 + const itemValue: any = this.selectgroup.valueKey ? this.valueFn(this.item) : this.item; + if (this.selectgroup.multiple) { + // 多选 + this.selectgroup.model = this.selectgroup.model || []; + const index: number = this.selectgroup.model.indexOf(itemValue); + if (index > -1) { + this.selectgroup.model.splice(index, 1); + } else { + this.selectgroup.model.push(itemValue); + } + this.selectgroup.model = this.selectgroup.model.concat(); + } else { + // 单选 + this.selectgroup.model = itemValue; + } + } + /** + * @ignore + * 是否选中 + */ + public isChecked(): boolean { + if (this.selectgroup.model) { + this.changeDetectorRef.markForCheck(); + const itemValue: any = this.selectgroup.valueKey ? this.valueFn(this.item) : this.item; + + return this.selectgroup.multiple ? this.selectgroup.model.includes(itemValue) : this.selectgroup.model === itemValue; + } + } +} diff --git a/src/selectgroup/lib/src/selectgroup.html b/src/selectgroup/lib/src/selectgroup.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/selectgroup/lib/src/selectgroup.html @@ -0,0 +1 @@ + diff --git a/src/selectgroup/lib/src/selectgroup.less b/src/selectgroup/lib/src/selectgroup.less new file mode 100644 index 0000000..2bd0248 --- /dev/null +++ b/src/selectgroup/lib/src/selectgroup.less @@ -0,0 +1,180 @@ +@import '../../../themes/basic/base-all.less'; + +::ng-deep :root { + --ti-selectitem-corner-width: 20px; + --ti-selectitem-border-color-hover: var(--ti-common-color-line-active); // 激活/hover 边框色 +} + +// 选中样式角标公共样式 +.ti3-selectitem-corner { + display: inline-block; + width: 0; + height: 0; + position: absolute; + animation: showCorner 200ms; + right: 0; +} + +// 非灰化状态 +:host.ti3-selectitem-box { + .clearfix(); + display: inline-block; + cursor: pointer; + width: 190px; + height: 54px; + padding: var(--ti-common-space-2x) var(--ti-common-space-2x) var(--ti-common-space-2x) var(--ti-common-space-10); + border-radius: var(--ti-common-border-radius-normal); + background-color: var(--ti-common-color-bg-white-normal); + box-sizing: border-box; + margin-right: var(--ti-common-space-3x); + position: relative; + box-shadow: none; + border: 1px solid var(--ti-common-color-line-normal); + // 选中状态 + &.ti3-selectitem-checked { + background-color: var(--ti-common-color-bg-light-normal); + border: 1px solid var(--ti-selectitem-border-color-hover); + .ti3-selectitem-corner-right-bottom { + &:extend(.ti3-selectitem-corner); + border-left: var(--ti-selectitem-corner-width) solid transparent; + border-bottom: var(--ti-selectitem-corner-width) solid var(--ti-selectitem-border-color-hover); + bottom: 0; + & .ti3-selectitem-checkmark { + position: absolute; + color: var(--ti-common-color-icon-white); + top: calc(var(--ti-selectitem-corner-width) - 12px); + right: 0; + } + } + .ti3-selectitem-corner-right-top { + .ti3-selectitem-corner-right-bottom(); + top: 0; + .rotate(-90deg); + & .ti3-selectitem-checkmark { + .rotate(90deg); + } + } + } + // 自定义模板 + &.ti3-selectitem-template { + width: auto; + height: auto; + padding-left: var(--ti-common-space-2x); + vertical-align: middle; + } + &:hover { + border: 1px solid var(--ti-selectitem-border-color-hover); + } +} + +// 灰化状态 +:host.ti3-selectitem-box.ti3-selectitem-disabled { + background: var(--ti-common-color-bg-disabled); + cursor: not-allowed; + border-color: var(--ti-common-color-line-disabled); + box-shadow: none; + :hover { + border-color: var(--ti-common-color-line-disabled); + } + .ti3-selectitem-lefticon, + .ti3-selectitem-title, + .ti3-selectitem-content { + color: var(--ti-common-color-text-disabled); + } + // 选中状态的角标 + &.ti3-selectitem-checked { + .ti3-selectitem-corner-right-bottom, + .ti3-selectitem-corner-right-top { + border-bottom-color: var(--ti-common-color-line-disabled); + } + } +} +:host .ti3-selectitem-lefticon { + float: left; + margin-top: var(--ti-common-space-base); + margin-right: var(--ti-common-space-2x); + font-size: 28px; + line-height: 100%; // FireFox下,该元素float:left或display:block会导致line-height:1.2,高度增加 +} +// 文本溢出的公共样式 +.ti3-selectitem-text { + width: 100%; + display: block; +} +.ti3-selectitem-right { + float: left; + width: calc(100% - 50px); + .ti3-selectitem-title { + &:extend(.ti3-selectitem-text); + .ellipsis(); + font-size: var(--ti-common-font-size-1); + color: var(--ti-common-color-text-primary); + line-height: 20px; + } + .ti3-selectitem-title-top-space { + margin-top: var(--ti-common-space-2x); + } + .ti3-selectitem-content { + &:extend(.ti3-selectitem-text); + .ellipsis(); + font-size: var(--ti-common-font-size-base); + color: var(--ti-common-color-text-secondary); + line-height: var(--ti-common-line-height-number); + } +} + +// 镜像+下拉组件中:定制select组件的样式 +::ng-deep.ti3-selectitem-box { + ti-select { + position: relative; + left: calc(-1 * var(--ti-common-space-10) - 1px); + bottom: calc(-1 * var(--ti-common-space-2x) - 1px); + &:not([disabled]) { + border-color: transparent; + border-top-color: var(--ti-common-color-line-dividing); + background: transparent; + &:hover, + &[tiFocused] { + border-color: transparent; + border-top-color: var(--ti-common-color-line-dividing); + } + } + } + // 悬停 + &:hover ti-select { + &:not([disabled]) { + border-top-color: var(--ti-selectitem-border-color-hover); + &:hover, + &[tiFocused] { + border-top-color: var(--ti-selectitem-border-color-hover); + } + } + } + // 选中 + &.ti3-selectitem-checked ti-select { + &:not([disabled]) { + border-top-color: var(--ti-selectitem-border-color-hover); + &:hover, + &[tiFocused] { + border-top-color: var(--ti-selectitem-border-color-hover); + } + } + } +} + +@keyframes showCorner { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes showCheckmark { + from { + transform: scale(0); + } + to { + transform: scale(1); + } +} diff --git a/src/selectgroup/lib/src/selectitem.html b/src/selectgroup/lib/src/selectitem.html new file mode 100644 index 0000000..38c052c --- /dev/null +++ b/src/selectgroup/lib/src/selectitem.html @@ -0,0 +1,23 @@ +
    + +
    + + + + + +
    + + {{item.title}} + {{item.content}} + +
    +
    + + + {{item.title}} + diff --git a/src/skeleton/demo/karma.conf.js b/src/skeleton/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/skeleton/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/skeleton/demo/project.json b/src/skeleton/demo/project.json new file mode 100644 index 0000000..0162a46 --- /dev/null +++ b/src/skeleton/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/skeleton/demo", + "sourceRoot": "src/skeleton/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/skeleton", + "index": "src/skeleton/demo/src/index.html", + "main": "src/skeleton/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/skeleton/demo/tsconfig.app.json", + "assets": ["src/skeleton/demo/src/favicon.ico", "src/skeleton/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "skeleton-demo:build:production" + }, + "development": { + "browserTarget": "skeleton-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js skeleton" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/skeleton/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/skeleton/demo/tsconfig.spec.json", + "karmaConfig": "src/skeleton/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/skeleton/demo/src/app/AppComponent.ts b/src/skeleton/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/skeleton/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/skeleton/demo/src/app/AppModule.ts b/src/skeleton/demo/src/app/AppModule.ts new file mode 100644 index 0000000..5b39d38 --- /dev/null +++ b/src/skeleton/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SkeletonTestModule } from './skeleton/SkeletonTestModule'; + +@NgModule({ + imports: [ + SkeletonTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/skeleton/demo/src/app/IndexComponent.ts b/src/skeleton/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..01334c4 --- /dev/null +++ b/src/skeleton/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SkeletonTestModule } from './skeleton/SkeletonTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SkeletonTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/skeleton/demo/src/app/app.html b/src/skeleton/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/skeleton/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/skeleton/demo/src/app/skeleton/SkeletonPageComponent.ts b/src/skeleton/demo/src/app/skeleton/SkeletonPageComponent.ts new file mode 100644 index 0000000..7962ae2 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/SkeletonPageComponent.ts @@ -0,0 +1,8 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './skeleton-page.html', + styleUrls: ['./skeleton-page.less'], + encapsulation: ViewEncapsulation.None +}) +export class SkeletonPageComponent {} diff --git a/src/skeleton/demo/src/app/skeleton/SkeletonTestModule.ts b/src/skeleton/demo/src/app/skeleton/SkeletonTestModule.ts new file mode 100644 index 0000000..41a4334 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/SkeletonTestModule.ts @@ -0,0 +1,31 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiSkeletonModule } from '@opentiny/ng'; + +import { SkeletonPageComponent } from './SkeletonPageComponent'; +import { SkeletonTitleComponent } from './SkeletonTitleComponent'; +import { SkeletonTypeComponent } from './SkeletonTypeComponent'; + +@NgModule({ + imports: [CommonModule, TiSkeletonModule, RouterModule.forChild(SkeletonTestModule.ROUTES)], + declarations: [SkeletonPageComponent, SkeletonTitleComponent, SkeletonTypeComponent] +}) +export class SkeletonTestModule { + static readonly LINKS: Array = [{ href: 'components/TiSkeletonComponent.html', label: 'Skeleton' }]; + static readonly ROUTES: Routes = [ + { + path: 'skeleton/skeleton-type', + component: SkeletonPageComponent + }, + { + path: 'skeleton/skeleton-title', + component: SkeletonTitleComponent + }, + { + path: 'skeleton/skeleton-page', + component: SkeletonTypeComponent + } + ]; +} diff --git a/src/skeleton/demo/src/app/skeleton/SkeletonTitleComponent.ts b/src/skeleton/demo/src/app/skeleton/SkeletonTitleComponent.ts new file mode 100644 index 0000000..4c321e3 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/SkeletonTitleComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './skeleton-title.html', + styleUrls: ['./skeleton.less'] +}) +export class SkeletonTitleComponent {} diff --git a/src/skeleton/demo/src/app/skeleton/SkeletonTypeComponent.ts b/src/skeleton/demo/src/app/skeleton/SkeletonTypeComponent.ts new file mode 100644 index 0000000..6fa4312 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/SkeletonTypeComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './skeleton-type.html', + styleUrls: ['./skeleton.less'] +}) +export class SkeletonTypeComponent {} diff --git a/src/skeleton/demo/src/app/skeleton/skeleton-page.html b/src/skeleton/demo/src/app/skeleton/skeleton-page.html new file mode 100644 index 0000000..b3f3b42 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/skeleton-page.html @@ -0,0 +1,24 @@ +
    + + + +
    + +
    + +
    + + + +
    + +
    + +
    + + + + +
    +
    +
    diff --git a/src/skeleton/demo/src/app/skeleton/skeleton-page.less b/src/skeleton/demo/src/app/skeleton/skeleton-page.less new file mode 100644 index 0000000..57c5ab3 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/skeleton-page.less @@ -0,0 +1,43 @@ +body { + background-color: #eef0f5; +} +.leftmenu-skeleton { + width: 192px; + position: absolute; + height: 750px; +} +.content-skeleton { + margin-left: 192px; + padding: 12px 20px 0 20px; + display: flex; +} +.content-left-skeleton { + width: calc(100% - 183px - 12px); +} +.content-left-skeleton-section1 { + height: 200px; +} +.content-left-skeleton-section2 { + display: flex; + margin: 12px 0; +} +.content-left-skeleton-section2 ti-skeleton { + height: 200px; + width: calc((100% - 12px * 2) / 3); +} +.content-left-skeleton-section2 ti-skeleton:not(:last-child) { + margin-right: 12px; +} +.content-right-skeleton { + width: 183px; + margin-left: 12px; +} +.content-right-skeleton-section1 { + height: 200px; +} +.content-right-skeleton-section2 { + height: 95px; +} +.content-right-skeleton ti-skeleton:not(:last-child) { + margin-bottom: 12px; +} diff --git a/src/skeleton/demo/src/app/skeleton/skeleton-title.html b/src/skeleton/demo/src/app/skeleton/skeleton-title.html new file mode 100644 index 0000000..f8b414b --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/skeleton-title.html @@ -0,0 +1,6 @@ +
    +

    1.不设置 title 时,默认为 true

    + +

    2.设置为 false 时,没有标题

    + +
    diff --git a/src/skeleton/demo/src/app/skeleton/skeleton-type.html b/src/skeleton/demo/src/app/skeleton/skeleton-type.html new file mode 100644 index 0000000..5ddd865 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/skeleton-type.html @@ -0,0 +1,6 @@ +
    +

    1.rows 默认类型

    + +

    2.block 类型

    + +
    diff --git a/src/skeleton/demo/src/app/skeleton/skeleton.less b/src/skeleton/demo/src/app/skeleton/skeleton.less new file mode 100644 index 0000000..535ea83 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/skeleton.less @@ -0,0 +1,8 @@ +.content-background { + background-color: #eef0f5; + padding: 20px; +} +.content-skeleton { + width: 400px; + height: 200px; +} diff --git a/src/skeleton/demo/src/app/skeleton/webdoc/skeleton-demos.js b/src/skeleton/demo/src/app/skeleton/webdoc/skeleton-demos.js new file mode 100644 index 0000000..ab1b60b --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/webdoc/skeleton-demos.js @@ -0,0 +1,43 @@ +export default { + column: '1', + demos: [ + { + demoId: 'skeleton-type', + name: { + 'zh-CN': '类型', + 'en-US': 'skeleton type' + }, + desc: { + 'zh-CN': '

    通过属性type配置骨架屏的类型,包含rowsblock两种类型。', + 'en-US': 'skeleton type' + }, + apis: ['TiSkeletonComponent.properties.type'], + codeFiles: ['skeleton-type.html', 'SkeletonTypeComponent.ts', 'skeleton.less'] + }, + { + demoId: 'skeleton-title', + name: { + 'zh-CN': '标题', + 'en-US': 'skeleton title' + }, + desc: { + 'zh-CN': '

    通过属性title配置是否有标题。', + 'en-US': 'skeleton title' + }, + apis: ['TiSkeletonComponent.properties.title'], + codeFiles: ['skeleton-title.html', 'SkeletonTitleComponent.ts', 'skeleton.less'] + }, + { + demoId: 'skeleton-page', + name: { + 'zh-CN': '在典型页面中使用', + 'en-US': 'skeleton page' + }, + desc: { + 'zh-CN': 'skeleton 组件在典型页面中使用。', + 'en-US': '

    skeleton page

    ' + }, + codeFiles: ['skeleton-page.html', 'SkeletonPageComponent.ts', 'skeleton-page.less'] + } + ] +}; diff --git a/src/skeleton/demo/src/app/skeleton/webdoc/skeleton.cn.md b/src/skeleton/demo/src/app/skeleton/webdoc/skeleton.cn.md new file mode 100644 index 0000000..716b993 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/webdoc/skeleton.cn.md @@ -0,0 +1,26 @@ +--- +title: Skeleton 骨架屏 +--- +# Skeleton 骨架屏 + +
    + +Skeleton 是在需要等待加载内容的位置设置一个占位图形组合的组件。   + +```typescript +import { TiSkeletonModule } from '@opentiny/ng'; +``` + +
    + +
    + +Select 是提供选项菜单的组件。   + ++ 支持单选、多选、分组、搜索、懒加载等场景。 + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/skeleton/demo/src/app/skeleton/webdoc/skeleton.en.md b/src/skeleton/demo/src/app/skeleton/webdoc/skeleton.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/skeleton/demo/src/app/skeleton/webdoc/skeleton.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/skeleton/demo/src/favicon.ico b/src/skeleton/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/skeleton/demo/src/index.html b/src/skeleton/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/skeleton/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/skeleton/demo/src/main.ts b/src/skeleton/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/skeleton/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/skeleton/demo/test.ts b/src/skeleton/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/skeleton/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/skeleton/demo/tsconfig.app.json b/src/skeleton/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/skeleton/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/skeleton/demo/tsconfig.spec.json b/src/skeleton/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/skeleton/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/skeleton/lib/index.ts b/src/skeleton/lib/index.ts new file mode 100644 index 0000000..71cab72 --- /dev/null +++ b/src/skeleton/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSkeletonModule'; diff --git a/src/skeleton/lib/ng-package.json b/src/skeleton/lib/ng-package.json new file mode 100644 index 0000000..0037f39 --- /dev/null +++ b/src/skeleton/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/skeleton", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/skeleton/lib/package.json b/src/skeleton/lib/package.json new file mode 100644 index 0000000..7235b21 --- /dev/null +++ b/src/skeleton/lib/package.json @@ -0,0 +1,9 @@ +{ + "name": "@opentiny/ng-skeleton", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/skeleton/lib/project.json b/src/skeleton/lib/project.json new file mode 100644 index 0000000..fac4d71 --- /dev/null +++ b/src/skeleton/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/skeleton/lib", + "sourceRoot": "src/skeleton/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/skeleton"], + "options": { + "project": "src/skeleton/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/skeleton"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js skeleton" + }, + { + "command": "ng default-build skeleton" + }, + { + "command": "node build/clear-default-theme.js skeleton" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/skeleton && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build skeleton && ng pack skeleton && node build/publish.js skeleton --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/skeleton/lib/src/TiSkeletonComponent.ts b/src/skeleton/lib/src/TiSkeletonComponent.ts new file mode 100644 index 0000000..292d756 --- /dev/null +++ b/src/skeleton/lib/src/TiSkeletonComponent.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input } from '@angular/core'; +/** + * 骨架屏组件 + * + */ +@Component({ + selector: 'ti-skeleton', + templateUrl: 'skeleton.html', + styleUrls: ['skeleton.less'], + host: { + '[class.ti3-skeleton-container]': 'true' + } +}) +export class TiSkeletonComponent { + /** + * 是否显示标题 + */ + @Input() title: boolean = true; + /** + * 类型 + */ + @Input() type: 'rows' | 'block' = 'rows'; +} diff --git a/src/skeleton/lib/src/TiSkeletonModule.ts b/src/skeleton/lib/src/TiSkeletonModule.ts new file mode 100644 index 0000000..7f14ac5 --- /dev/null +++ b/src/skeleton/lib/src/TiSkeletonModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TiSkeletonComponent } from './TiSkeletonComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [TiSkeletonComponent], + declarations: [TiSkeletonComponent] +}) +export class TiSkeletonModule {} + +export { TiSkeletonComponent } from './TiSkeletonComponent'; diff --git a/src/skeleton/lib/src/skeleton.html b/src/skeleton/lib/src/skeleton.html new file mode 100644 index 0000000..c3e6271 --- /dev/null +++ b/src/skeleton/lib/src/skeleton.html @@ -0,0 +1,10 @@ +
    +
      +
    • +
    • +
    +
    diff --git a/src/skeleton/lib/src/skeleton.less b/src/skeleton/lib/src/skeleton.less new file mode 100644 index 0000000..97bf2b3 --- /dev/null +++ b/src/skeleton/lib/src/skeleton.less @@ -0,0 +1,35 @@ +:host { + --ti-skeleton-title-height: var(--ti-common-size-5x); + --ti-skeleton-title-bottom-space: var(--ti-common-space-4x); + --ti-skeleton-row-height: var(--ti-common-size-5x); +} + +:host.ti3-skeleton-container { + display: block; + background-color: var(--ti-common-color-bg-white-normal); + padding: var(--ti-common-space-5x) var(--ti-common-space-6x); + box-shadow: var(--ti-common-shadow-1-down); + box-sizing: border-box; +} +.ti3-skeleton-title { + width: var(--ti-common-size-30x); + height: var(--ti-skeleton-title-height); + background-color: var(--ti-common-color-bg-disabled); + margin-bottom: var(--ti-skeleton-title-bottom-space); +} +.ti3-skeleton-row { + height: var(--ti-skeleton-row-height); + background-color: var(--ti-common-color-bg-disabled); + &:not(:last-child) { + margin-bottom: 12px; + } +} +.ti3-skeleton-block { + background-color: var(--ti-common-color-bg-disabled); +} +.ti3-skeleton-block-with-title { + height: calc(100% - var(--ti-skeleton-title-height) - var(--ti-skeleton-title-bottom-space)); +} +.ti3-skeleton-block-without-title { + height: 100%; +} diff --git a/src/slider/demo/karma.conf.js b/src/slider/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/slider/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/slider/demo/project.json b/src/slider/demo/project.json new file mode 100644 index 0000000..f84bd3d --- /dev/null +++ b/src/slider/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/slider/demo", + "sourceRoot": "src/slider/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/slider", + "index": "src/slider/demo/src/index.html", + "main": "src/slider/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/slider/demo/tsconfig.app.json", + "assets": ["src/slider/demo/src/favicon.ico", "src/slider/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "slider-demo:build:production" + }, + "development": { + "browserTarget": "slider-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js slider" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/slider/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/slider/demo/tsconfig.spec.json", + "karmaConfig": "src/slider/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/slider/demo/src/app/AppComponent.ts b/src/slider/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/slider/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/slider/demo/src/app/AppModule.ts b/src/slider/demo/src/app/AppModule.ts new file mode 100644 index 0000000..a350557 --- /dev/null +++ b/src/slider/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SliderTestModule } from './slider/SliderTestModule'; + +@NgModule({ + imports: [ + SliderTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/slider/demo/src/app/IndexComponent.ts b/src/slider/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..aea6b8b --- /dev/null +++ b/src/slider/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SliderTestModule } from './slider/SliderTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SliderTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/slider/demo/src/app/app.html b/src/slider/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/slider/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/slider/demo/src/app/slider/SliderEventComponent.ts b/src/slider/demo/src/app/slider/SliderEventComponent.ts new file mode 100644 index 0000000..94bd931 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderEventComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './slider-event.html' +}) +export class SliderEventComponent { + myLogs: Array = []; + singleValue: number = 64; + rangeValue: string = '30;85'; + min: number = 0; + max: number = 100; + scales: Array = [0, '', 40, '', 80, 100]; + + changeStop(value: number | string): void { + this.myLogs = [...this.myLogs, `changeStop event = ${value}`]; + } +} diff --git a/src/slider/demo/src/app/slider/SliderFormcontrolComponent.ts b/src/slider/demo/src/app/slider/SliderFormcontrolComponent.ts new file mode 100644 index 0000000..e90bea7 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderFormcontrolComponent.ts @@ -0,0 +1,25 @@ +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms'; + +@Component({ + templateUrl: './slider-formcontrol.html', + encapsulation: ViewEncapsulation.None +}) +export class SliderFormcontrolComponent implements OnInit { + value: number = 6; + value1: number = 2; + value2: number = 10; + singleValue: string = ''; + rangeValue: string = ''; + min: number = 1; + max: number = 12; + scales: Array = ['1个月', '2个月', '3个月', '4个月', '5个月', '6个月', '7个月', '8个月', '9个月', '1年', '2年', '3年']; + sliderForm: FormGroup; + constructor(private fb: FormBuilder) {} + ngOnInit(): void { + this.sliderForm = this.fb.group({ + singleValue: `${this.value}`, + rangeValue: `${this.value1};${this.value2}` + }); + } +} diff --git a/src/slider/demo/src/app/slider/SliderHiddenComponent.ts b/src/slider/demo/src/app/slider/SliderHiddenComponent.ts new file mode 100644 index 0000000..daea4b2 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderHiddenComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './slider-hidden.html' +}) +export class SliderHiddenComponent { + value: number = 64; + min: number = 0; + max: number = 100; + scales: Array = [0, 20, 40, 60, 80, 100]; + hidden: boolean = true; + + number1: number = 300; + number2: number = 650; + value1: string = `${this.number1};${this.number2}`; + min1: number = 100; + max1: number = 2000; + scales1: Array = [100, 300, 650, 1250, 2000]; + ratios1: Array = [0.4, 0.2, 0.2, 0.2]; + tipFormatterFn(value: number): number { + return value; + } +} diff --git a/src/slider/demo/src/app/slider/SliderLimitsComponent.ts b/src/slider/demo/src/app/slider/SliderLimitsComponent.ts new file mode 100644 index 0000000..51af062 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderLimitsComponent.ts @@ -0,0 +1,16 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './slider-limits.html', + encapsulation: ViewEncapsulation.None +}) +export class SliderLimitsComponent { + value: number = 6; + value1: number = 2; + value2: number = 10; + singleValue: string = `${this.value}`; + rangeValue: string = `${this.value1};${this.value2}`; + min: number = 1; + max: number = 12; + scales: Array = ['1个月', '2个月', '3个月', '4个月', '5个月', '6个月', '7个月', '8个月', '9个月', '1年', '2年', '3年']; +} diff --git a/src/slider/demo/src/app/slider/SliderRatiosComponent.ts b/src/slider/demo/src/app/slider/SliderRatiosComponent.ts new file mode 100644 index 0000000..8bcbdd2 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderRatiosComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './slider-ratios.html' +}) +export class SliderRatiosComponent { + singleValue1: number = 300; + singleValue2: number = 650; + singleValue: number = 1250; + rangeValue: string = `${this.singleValue1};${this.singleValue2}`; + min: number = 100; + max: number = 2000; + scales: Array = [100, 300, 650, 1250, 2000]; + ratios: Array = [0.4, 0.2, 0.2, 0.2]; +} diff --git a/src/slider/demo/src/app/slider/SliderScalesComponent.ts b/src/slider/demo/src/app/slider/SliderScalesComponent.ts new file mode 100644 index 0000000..80989d8 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderScalesComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './slider-scales.html' +}) +export class SliderScalesComponent { + singleValue1: number = 200; + max1: number = 1000; + min1: number = 100; + scales1: Array = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]; + + rangeValue: string = '200;600'; + max2: number = 1000; + min2: number = 100; + scales2: Array = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]; + + singleValue: string = '4;8'; + min: number = 1; + max: number = 11; + + scales(value: number, min: number, max: number): string { + if (value === min || value === max) { + return undefined; // 为undefined或null情况下,不显示断点且不显示文本 + } else if ((value - min) % 2 === 0) { + return `${value}个月`; + } else { + return ''; // 为""情况下,显示断点但不显示文本 + } + } +} diff --git a/src/slider/demo/src/app/slider/SliderTemplateComponent.ts b/src/slider/demo/src/app/slider/SliderTemplateComponent.ts new file mode 100644 index 0000000..1238c08 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderTemplateComponent.ts @@ -0,0 +1,54 @@ +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './slider-template.html', + encapsulation: ViewEncapsulation.None +}) +export class SliderTemplateComponent { + value: string = '2;11'; + min: number = 1; + max: number = 12; + scales: Array = [ + { + label: '1个月' + }, + { + label: '2个月' + }, + { + label: '3个月', + iconName: 'discount-sup' + }, + { + label: '4个月' + }, + { + label: '5个月' + }, + { + label: '6个月', + iconName: 'discount-sup' + }, + { + label: '7个月' + }, + { + label: '8个月' + }, + { + label: '9个月' + }, + { + label: '1年', + iconName: 'discount' + }, + { + label: '2年', + iconName: 'discount' + }, + { + label: '3年', + iconName: 'discount' + } + ]; +} diff --git a/src/slider/demo/src/app/slider/SliderTestModule.ts b/src/slider/demo/src/app/slider/SliderTestModule.ts new file mode 100644 index 0000000..28e1164 --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderTestModule.ts @@ -0,0 +1,72 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiIconModule, TiSliderModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { SliderLimitsComponent } from './SliderLimitsComponent'; +import { SliderFormcontrolComponent } from './SliderFormcontrolComponent'; +import { SliderScalesComponent } from './SliderScalesComponent'; +import { SliderTemplateComponent } from './SliderTemplateComponent'; +import { SliderRatiosComponent } from './SliderRatiosComponent'; +import { SliderTipComponent } from './SliderTipComponent'; +import { SliderEventComponent } from './SliderEventComponent'; +import { SliderHiddenComponent } from './SliderHiddenComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiSliderModule, + TiIconModule, + DemoLogModule, + RouterModule.forChild(SliderTestModule.ROUTES) + ], + declarations: [ + SliderLimitsComponent, + SliderFormcontrolComponent, + SliderScalesComponent, + SliderTemplateComponent, + SliderRatiosComponent, + SliderTipComponent, + SliderEventComponent, + SliderHiddenComponent + ] +}) +export class SliderTestModule { + static readonly LINKS: Array = [{ href: 'components/TiSliderComponent.html', label: 'Slider' }]; + static readonly ROUTES: Routes = [ + { + path: 'slider/slider-limits', + component: SliderLimitsComponent + }, + { + path: 'slider/slider-fromcontrol', + component: SliderFormcontrolComponent + }, + { + path: 'slider/slider-scales', + component: SliderScalesComponent + }, + { + path: 'slider/slider-template', + component: SliderTemplateComponent + }, + { + path: 'slider/slider-ratios', + component: SliderRatiosComponent + }, + { + path: 'slider/slider-tip', + component: SliderTipComponent + }, + { + path: 'slider/slider-event', + component: SliderEventComponent + }, + { path: 'slider/slider-hidden', component: SliderHiddenComponent } + ]; +} diff --git a/src/slider/demo/src/app/slider/SliderTipComponent.ts b/src/slider/demo/src/app/slider/SliderTipComponent.ts new file mode 100644 index 0000000..df599ba --- /dev/null +++ b/src/slider/demo/src/app/slider/SliderTipComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './slider-tip.html' +}) +export class SliderTipComponent { + value1: number = 8; + value2: number = 12; + value3: number = 2; + value4: number = 12; + rangeValue: string = `${this.value1};${this.value2}`; + rangeValue1: string = `${this.value3};${this.value4}`; + min: number = 1; + max: number = 12; + scales: Array = ['1个月', '2个月', '3个月', '4个月', '5个月', '6个月', '7个月', '8个月', '9个月', '1年', '2年', '3年']; + tipMode: string = 'always'; + tipFormatterFn(value: number): string { + if (value === 10) { + return '付10个月费用,享1年优惠'; + } else if (value === 11) { + return '2年'; + } else if (value === 12) { + return '3年'; + } else { + return `${value}个月`; + } + } +} diff --git a/src/slider/demo/src/app/slider/slider-event.html b/src/slider/demo/src/app/slider/slider-event.html new file mode 100644 index 0000000..d351b83 --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-event.html @@ -0,0 +1,5 @@ + + + + + diff --git a/src/slider/demo/src/app/slider/slider-formcontrol.html b/src/slider/demo/src/app/slider/slider-formcontrol.html new file mode 100644 index 0000000..02d11f7 --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-formcontrol.html @@ -0,0 +1,4 @@ +
    + + +
    diff --git a/src/slider/demo/src/app/slider/slider-hidden.html b/src/slider/demo/src/app/slider/slider-hidden.html new file mode 100644 index 0000000..d2a2bdd --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-hidden.html @@ -0,0 +1,20 @@ +

    1.描述

    +

    Slider组件,组件从隐藏到显示

    +

    2.示例

    +
    +

    单滑块

    + +

    双滑块

    + + +
    + diff --git a/src/slider/demo/src/app/slider/slider-limits.html b/src/slider/demo/src/app/slider/slider-limits.html new file mode 100644 index 0000000..3fa869a --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-limits.html @@ -0,0 +1,2 @@ + + diff --git a/src/slider/demo/src/app/slider/slider-ratios.html b/src/slider/demo/src/app/slider/slider-ratios.html new file mode 100644 index 0000000..2a3fb69 --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-ratios.html @@ -0,0 +1,2 @@ + + diff --git a/src/slider/demo/src/app/slider/slider-scales.html b/src/slider/demo/src/app/slider/slider-scales.html new file mode 100644 index 0000000..d243128 --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-scales.html @@ -0,0 +1,6 @@ +

    1. Array方式双滑块

    + +

    2. Array方式双滑块

    + +

    3. Function方式双滑块

    + diff --git a/src/slider/demo/src/app/slider/slider-template.html b/src/slider/demo/src/app/slider/slider-template.html new file mode 100644 index 0000000..2184c8a --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-template.html @@ -0,0 +1,17 @@ + + + {{scale.label}} + + + + + diff --git a/src/slider/demo/src/app/slider/slider-tip.html b/src/slider/demo/src/app/slider/slider-tip.html new file mode 100644 index 0000000..1487ad2 --- /dev/null +++ b/src/slider/demo/src/app/slider/slider-tip.html @@ -0,0 +1,11 @@ + + + diff --git a/src/slider/demo/src/app/slider/webdoc/slider-demos.js b/src/slider/demo/src/app/slider/webdoc/slider-demos.js new file mode 100644 index 0000000..fbab6b8 --- /dev/null +++ b/src/slider/demo/src/app/slider/webdoc/slider-demos.js @@ -0,0 +1,100 @@ +export default { + column: '1', + + demos: [ + { + demoId: 'slider-limits', + name: { + 'zh-CN': '基本使用', + 'en-US': 'limits', + }, + desc: { + 'zh-CN': + '

    通过属性minmax配置滑动条的范围;通过属性step配置滑动条拖动时的最小间隔。

    ', + 'en-US': '

    limits

    ', + }, + apis: [ + 'TiSliderComponent.properties.max', + 'TiSliderComponent.properties.min', + 'TiSliderComponent.properties.step', + ], + }, + { + demoId: 'slider-formcontrol', + name: { + 'zh-CN': '响应式表单', + 'en-US': 'formcontrol', + }, + desc: { + 'zh-CN': '

    响应式表单的用法。

    ', + 'en-US': '

    formcontrol

    ', + }, + }, + { + demoId: 'slider-scales', + name: { + 'zh-CN': '显示文本', + 'en-US': 'scales', + }, + desc: { + 'zh-CN': + '

    通过属性scales配置滑动条刻度的显示文本;可使用 Array 和 Function 两种方式。

    ', + 'en-US': '

    scales

    ', + }, + apis: ['TiSliderComponent.properties.scales'], + }, + { + demoId: 'slider-template', + name: { + 'zh-CN': '自定义文本', + 'en-US': 'template', + }, + desc: { + 'zh-CN': '

    通过模板scale配置刻度显示文本。

    ', + 'en-US': '

    template

    ', + }, + apis: ['TiSliderComponent.slots.labelTemplate'], + }, + { + demoId: 'slider-ratios', + name: { + 'zh-CN': '不均匀分配的滑动条', + 'en-US': 'ratios', + }, + desc: { + 'zh-CN': '

    通过属性ratios配置滑动条每段所占长度比。

    ', + 'en-US': '

    ratios

    ', + }, + apis: ['TiSliderComponent.properties.ratios'], + }, + { + demoId: 'slider-tip', + name: { + 'zh-CN': '提示', + 'en-US': 'tip', + }, + desc: { + 'zh-CN': + '

    通过属性tipMode配置 tip 的显示方式,包括autoalways两种类型;通过属性tipFormatterFn配置 tip 的显示文本。

    ', + 'en-US': '

    tip

    ', + }, + apis: [ + 'TiSliderComponent.properties.tipMode', + 'TiSliderComponent.properties.tipFormatterFn', + ], + }, + { + demoId: 'slider-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event', + }, + desc: { + 'zh-CN': + '

    当绑定的值变化停止的时候触发changeStop事件,传递出去的参数为:停止时的值。

    ', + 'en-US': '

    event

    ', + }, + apis: ['TiSliderComponent.events.changeStop'], + }, + ], +}; diff --git a/src/slider/demo/src/app/slider/webdoc/slider.cn.md b/src/slider/demo/src/app/slider/webdoc/slider.cn.md new file mode 100644 index 0000000..dc168bf --- /dev/null +++ b/src/slider/demo/src/app/slider/webdoc/slider.cn.md @@ -0,0 +1,24 @@ +--- +title: Slider 滑块 +--- +# Slider 滑块 + +
    + +Slider 滑块组件,显示当前值和选择范围。   + +```typescript +import { TiSliderModule } from '@opentiny/ng'; +``` + +
    + +
    + +Slider 滑动条组件,显示当前值和选择范围。   + +```typescript +import { TiSliderModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/slider/demo/src/app/slider/webdoc/slider.en.md b/src/slider/demo/src/app/slider/webdoc/slider.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/slider/demo/src/app/slider/webdoc/slider.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/slider/demo/src/favicon.ico b/src/slider/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/slider/demo/src/index.html b/src/slider/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/slider/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/slider/demo/src/main.ts b/src/slider/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/slider/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/slider/demo/test.ts b/src/slider/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/slider/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/slider/demo/tsconfig.app.json b/src/slider/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/slider/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/slider/demo/tsconfig.spec.json b/src/slider/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/slider/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/slider/lib/index.ts b/src/slider/lib/index.ts new file mode 100644 index 0000000..0920512 --- /dev/null +++ b/src/slider/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSliderModule'; diff --git a/src/slider/lib/ng-package.json b/src/slider/lib/ng-package.json new file mode 100644 index 0000000..6d1f158 --- /dev/null +++ b/src/slider/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/slider", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/slider/lib/package.json b/src/slider/lib/package.json new file mode 100644 index 0000000..7410874 --- /dev/null +++ b/src/slider/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "@opentiny/ng-slider", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/platform-browser": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-drag": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/slider/lib/project.json b/src/slider/lib/project.json new file mode 100644 index 0000000..f341207 --- /dev/null +++ b/src/slider/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/slider/lib", + "sourceRoot": "src/slider/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/slider"], + "options": { + "project": "src/slider/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/slider"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js slider" + }, + { + "command": "ng default-build slider" + }, + { + "command": "node build/clear-default-theme.js slider" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/slider && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build slider && ng pack slider && node build/publish.js slider --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/slider/lib/src/TiSliderComponent.ts b/src/slider/lib/src/TiSliderComponent.ts new file mode 100644 index 0000000..406f414 --- /dev/null +++ b/src/slider/lib/src/TiSliderComponent.ts @@ -0,0 +1,1027 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + HostListener, + Input, + IterableChanges, + IterableDiffer, + IterableDiffers, + Output, + Renderer2, + SecurityContext, + SimpleChanges, + TemplateRef, + ViewChild, + ChangeDetectionStrategy +} from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { Util } from '@opentiny/ng-utils'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * @ignore + */ +export interface TiDragConfig { + helper: any; + position: { + left: number; + top: number; + }; +} + +/** + * Slider滑块组件 + * + * 滑块组件,通过操作组件选择指示范围 + * + */ +@Component({ + selector: 'ti-slider', + templateUrl: './slider.html', + styleUrls: ['./slider.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-slider-container]': 'true', + '[class.ti3-slider-disable]': 'disabled', + '(mousedown)': 'hostMousedownEvent($event)' + }, + providers: [TiFormComponent.getValueAccessor(TiSliderComponent)] +}) +export class TiSliderComponent extends TiFormComponent { + /** + * 最小范围值 + */ + @Input() min: number = 0; + /** + * 最大范围值 + */ + @Input() max: number = 10; + + /** + * 步长 + */ + @Input() step: number = 1; + /** + * 刻度标记 + * + * 1.当刻度标记为数组时,数组中各元素依次对应各刻度显示值;当刻度标记为 Function 时,返回刻度显示值; + * 参数:value (当前刻度)、max (最大值)、min (最小值)。 + * + * 2.当刻度标记为空字符串,只有刻度没有标记;当刻度标记为 undefined / null,没有刻度标记;其他情况,有刻度且有标记。 + */ + @Input() scales: Array | Function; + /** + * 滑块 tip 提示的显示方式 + */ + @Input() tipMode: 'auto' | 'always' = 'auto'; + /** + * tip 提示内容的函数,返回值是 tip 中显示的文本 + */ + @Input() tipFormatterFn: (value: any) => string; + /** + * 刻度比,依次设置 scales 中两个相邻刻度间的长度占比,总和为 1 + */ + @Input() ratios: Array; + /** + * 滑动停止并且值发生改变时触发的回调 + */ + @Output() readonly changeStop: EventEmitter = new EventEmitter(); + /** + * 获取到用户自定义的刻度 + */ + @ContentChild(TemplateRef, { static: true }) + public labelTemplate: TemplateRef; + /** + * @ignore + * 存放用户传入的刻度值 + */ + public ticks: Array; + /** + * @ignore + * 对于双滑块,左滑块是否处于变化状态 + */ + public isMinPointerActive: boolean; + /** + * @ignore + * 拖动配置参数 + */ + public dragOptions: any; + protected versionInfo: string = super.getVersion(packageInfo); + private ticksArr: Array; // 存放合法的刻度值 + private isDouble: boolean = false; // 判断是否是双滑块,默认是单滑块 false + private isTipAutoShow: boolean = true; // 判断tip提示是否是自动显示(鼠标操作才显示),默认true + private decimalDigit: number; // ratios中最大的小数位数 + private scalesDiffer: IterableDiffer; // scales 变化检查 + private dragStartModel: any; + private pointerMaxEleWidth: number; // 滑块的宽度 + + // 获取模板上DOM变量 + @ViewChild('trackEle', { static: true }) private trackRef: ElementRef; + @ViewChild('selection', { static: true }) private selectionRef: ElementRef; + @ViewChild('pointerMin', { static: true }) private pointerMinRef: ElementRef; + @ViewChild('pointerMax', { static: true }) private pointerMaxRef: ElementRef; + @ViewChild('tipMin', { static: true }) private tipMinRef: ElementRef; + @ViewChild('tipMax', { static: true }) private tipMaxRef: ElementRef; + // 定义DOM变量 + private trackELe: any; + private selectionEle: any; + private pointerMinEle: any; + private pointerMaxEle: any; + private tipMinEle: any; + private tipMaxEle: any; + private isVisibleInit: boolean; // 标识初始化时组件是否可见 + + /** + * @description: 将value值处理成数组 + * @param: value 需要切割的数组 + */ + private static splitValueToArray(value: string): { + valueMin: number; + valueMax: number; + } { + const arr: Array = `${value}`.split(';'); // value为number时强转为字符串 + + return { + valueMin: parseFloat(arr[0]), + valueMax: arr[1] && parseFloat(arr[1]) + }; + } + + /** + * @description: value值是否超限制 + * @param: value: 判断的value值 + * @param: minValue: 最大值 + * @param: maxValue: 最小值 + */ + private static isLimitExceed(value: number, minValue: number, maxValue: number): boolean { + return value < minValue || value > maxValue; + } + + /** + * @description: 当this.scales为函数情况下,转换为ticks数组 + * @param: scaleFormat 被转换的函数 + * @param: minValue 刻度限制的最大值 + * @param: maxValue 刻度限制的最小值 + * @param: step 刻度步长 + */ + private static translateScales(scaleFormat: Function, minValue: number, maxValue: number, step: number): Array { + const valueLen: number = (maxValue - minValue) / step; + const tickArray: Array = []; + for (let i: number = 0; i <= valueLen; i++) { + const stepNValue: number = step * i + minValue; + const formatRet: any = scaleFormat(stepNValue, minValue, maxValue); + if (Util.isUndefined(formatRet) || Util.isNull(formatRet)) { + // 为undefined或null情况下,不打点不显示label + tickArray.push(null); + } else { + // 包含为""(打点不显示label)和非""情况(打点且显示label) + tickArray.push(formatRet); + } + } + + return tickArray; + } + + /** + * @description: 在范围内限制value数值 + * @param: value 校验的value值 + * @param: min 最小值 + * @param: max 最大值 + * @return: 有效value值 + */ + private static limitValue(value: number, min: number, max: number): number { + // 小于min取min,大于max取max,其他不变。 + const res: number = value < min ? min : value > max ? max : value; + + return res; + } + + /** + * @description: 将样式数值转化为calc形式的css样式(设置为百分比形式,确保缩放的支持) + * @param: percent calc百分比 + * @param: subValue calc减去值 + */ + private static parseToCalcStyle(percent: number, subValue?: number): string { + if (Number.isNaN(subValue) || Util.isUndefined(subValue)) { + return `calc(${percent * 100}%)`; + } + + return `calc(${percent * 100}% - ${subValue}px)`; // calc中运算符合2边一定要空格 + } + + constructor( + protected hostRef: ElementRef, + protected renderer2: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + private tiRenderer: TiRenderer, + private iterableDiffers: IterableDiffers, + private domSanitizer: DomSanitizer + ) { + super(hostRef, renderer2, changeDetectorRef); + } + + /** + * @ignore + */ + @HostListener('window:resize') onResize(): void { + this.updateValuePosition(this.model); + } + + // 组件声明周期钩子--start + ngOnInit(): void { + super.ngOnInit(); + this.initDom(); + this.initVars(); + } + /** + * @ignore + */ + public initDom(): void { + this.trackELe = this.trackRef.nativeElement; + this.selectionEle = this.selectionRef.nativeElement; + this.pointerMinEle = this.pointerMinRef.nativeElement; + this.pointerMaxEle = this.pointerMaxRef.nativeElement; + this.tipMinEle = this.tipMinRef.nativeElement; + this.tipMaxEle = this.tipMaxRef.nativeElement; + } + /** + * @ignore + */ + public initVars(): void { + this.step = Number.isNaN(parseFloat(`${this.step}`)) ? 1 : parseFloat(`${this.step}`); + this.setMinMax(); + // ratios中最大的小数位数 + this.decimalDigit = this.getDecimalDigit(); + + // 处理用戶未设置scales或者scales為function情況 + if (!this.scales || !Array.isArray(this.scales)) { + this.setScales(); + } + + this.setTipConfig(); + // 给tip添加事件(tipMode是 'auto'的场景) + this.addTipEvent(); + + this.dragOptions = { + axis: 'x', + start: this.dragStartHandle, + drag: this.dragHandle, + stop: this.dragStopHandle + }; + } + ngOnChanges(changes: SimpleChanges): void { + if (changes['scales'] && !changes['scales'].firstChange && !Array.isArray(this.scales)) { + this.setScales(); + this.updateValuePosition(this.model); + } + + // 监听最大值最小值变化 + if ((changes['min'] && !changes['min'].firstChange) || (changes['max'] && !changes['max'].firstChange)) { + this.setMinMax(); + } + } + ngAfterViewInit(): void { + this.pointerMaxEleWidth = this.pointerMaxEle.offsetWidth; + this.isVisibleInit = this.pointerMaxEleWidth !== 0; + this.changeDetectorRef.detectChanges(); + } + + ngAfterViewChecked(): void { + // 监听滑块的宽度:处理组件从隐藏到显示定位问题 + if (!this.isVisibleInit) { + const pointerMaxWidth: number = this.pointerMaxEle.offsetWidth; + if (pointerMaxWidth !== 0) { + this.isVisibleInit = true; + this.pointerMaxEleWidth = pointerMaxWidth; + this.updateValuePosition(this.model); + this.changeDetectorRef.detectChanges(); + } + } + } + ngDoCheck(): void { + if (!Array.isArray(this.scales)) { + return; + } + if (!this.scalesDiffer) { + this.scalesDiffer = this.iterableDiffers.find(this.scales).create(); + } + + const scalesDiffer: IterableChanges = this.scalesDiffer.diff(this.scales); + if (scalesDiffer) { + this.setScales(); + // 初始化的updateValuePosition在writeValue中执行(writeValue迟于docheck执行) + if (!Util.isUndefined(this.model)) { + this.updateValuePosition(this.model); + } + } + } + // 组件声明周期钩子--end + + // 实现ControlValueAccessor接口 + /** + * @ignore + */ + writeValue(value: any): void { + // TODO: 接口设计重新调整 + // ngModel => value: string | number; // 滑块对应的value值 exp: 单滑块: 15 OR '15' 双滑块 '15;34'; + super.writeValue(value); + if (!Util.isNull(value) && !Util.isUndefined(value)) { + // 1.判断是否是双滑块 + this.isDouble = !Util.isUndefined(TiSliderComponent.splitValueToArray(value).valueMax); // 根据value形式确定单滑块/双滑块 + if (Util.isUndefined(this.onModelChange)) { + // 在reactive-form中使用,初始化赋值调用writeValue时, + // 此时registerOnChange还未被调用,onChangeFn还未被赋值, + // 所以要使用promise(异步)等onChangeFn被赋值后再调用 + Promise.resolve(undefined).then(() => { + this.writeValueHandle(value); + }); + } else { + this.writeValueHandle(value); + } + } + } + private writeValueHandle(value: any): void { + if (this.isDouble) { + this.show(this.pointerMinEle); + } else { + this.hide(this.pointerMinEle); + } + // 赋值操作 && 更新位置 + this.updateValuePosition(value); + } + // 实现ControlValueAccessor接口--end + + // 组件交互方法集合--start + /** + * @ignore + * 不存在下面的问题,用click和mousedown事件都可以(统一使用mousedown事件) + * 使用mousedown替代click,用于防止拖拽过程中触发click事件导致的滑块滑动 + * 问题场景:min滑块拖动与max滑块重合后,鼠标继续向右移动,此时鼠标抬起,触发click事件,导致max发生移动 + */ + public hostMousedownEvent = (event: MouseEvent): void => { + // 灰化状态或点击pointer时不做处理 + if ( + this.disabled || + this.tiRenderer.hasClass(event.target, 'ti3-slider-pointer') || + this.tiRenderer.hasClass(event.target, 'ti3-slider-tip') + ) { + return; + } + this.stepSliderFromClick(event); + }; + + /** + * @description: 添加tip的事件监听 + */ + private addTipEvent(): void { + if (this.isTipAutoShow) { + this.renderer2.listen(this.pointerMaxEle, 'mouseover', this.tipMouseoverHandle); + this.renderer2.listen(this.pointerMinEle, 'mouseover', this.tipMouseoverHandle); + this.renderer2.listen(this.pointerMaxEle, 'mouseleave', this.tipMouseleaveHandle); + this.renderer2.listen(this.pointerMinEle, 'mouseleave', this.tipMouseleaveHandle); + } + } + + /** + * @description: 滑块元素的mouseover事件处理函数 + */ + private tipMouseoverHandle = (event: any): void => { + if (!this.disabled) { + const sliderTip: Element = event.target.querySelector('.ti3-slider-tip'); + if (sliderTip) { + this.show(sliderTip); + } + } + }; + + /** + * @description: 滑块元素的mouseleave事件处理函数 + */ + private tipMouseleaveHandle = (event: any): void => { + if (this.disabled) { + return; + } + this.hide(event.target.querySelector('.ti3-slider-tip')); + }; + + /** + * @description: 宿主元素的mousedown事件处理函数 + */ + private stepSliderFromClick = (event: MouseEvent): void => { + const firstTickX: number = this.trackELe.getBoundingClientRect().left + this.pointerMaxEleWidth / 2; + const oldModel: any = this.model; + let pointerX: number = event.clientX - firstTickX; + pointerX = TiSliderComponent.limitValue(pointerX, 0, this.getBarWidth()); + let value: number = this.positionToValue(pointerX); // 获取value绝对比例值,用于确定滑块移动位置 + // 确定滑动哪个滑块 + let pointer: any = this.pointerMaxEle; + if (this.isDouble) { + const valueMin: number = TiSliderComponent.splitValueToArray(this.model).valueMin; + const valueMax: number = TiSliderComponent.splitValueToArray(this.model).valueMax; + // 以value中间值为界,在最小值及中间值之间=》左滑块移动;否则=》右滑块移动 + const midValue: number = (valueMax + valueMin) / 2; + if (value >= this.min && value < midValue) { + pointer = this.pointerMinEle; + this.isMinPointerActive = true; + } else { + this.isMinPointerActive = false; + } + } + value = this.getStepValue(value); + this.setValue(value); // 向外部通知value值 + this.valueToPosition(value, pointer); // 跳至value对应的坐标位置(与step对应) + if (this.model !== oldModel) { + this.changeStop.emit(this.model); + } + }; + + /** + * @description 拖拽开始执行的事件 + * @param: ui 拖拽助手 {helper:表示被拖拽的助手(helper), position: 助手(helper)的当前 CSS 位置} + */ + private dragStartHandle = (ui: TiDragConfig): void => { + this.dragStartModel = this.model; + if (this.isDouble) { + this.isMinPointerActive = !this.tiRenderer.hasClass(ui.helper, 'ti3-slider-pointer-max'); + } + }; + + /** + * @description 根据拖拽位置更新value值并改变位置呈现 + * @param: ui 拖拽助手 {helper:表示被拖拽的助手(helper), position: 助手(helper)的当前 CSS 位置} + */ + private dragHandle = (ui: TiDragConfig): void => { + // 设置value值 + const value: number = this.dragCommonHandle(ui); + this.setValue(value); // 向外部通知value值 + // 设置tip提示 + if (this.isTipAutoShow) { + // 拖拽过程中显示tip提示 + this.show(ui.helper.querySelector('.ti3-slider-tip')); + } + this.setTip(ui.helper.querySelector('.ti3-slider-tip'), value); // 设置tip提示值及位置 + }; + + /** + * @description: 鼠标弹起之后:根据拖拽位置跳至step对应的值并设置slider + * @param: ui 拖拽助手 {helper:表示被拖拽的助手(helper), position: 助手(helper)的当前 CSS 位置} + */ + private dragStopHandle = (ui: TiDragConfig): void => { + // 设置value值 + const value: number = this.dragCommonHandle(ui); + + if (this.isTipAutoShow) { + // 拖拽停止隐藏tip提示 + this.hide(ui.helper.querySelector('.ti3-slider-tip')); + } + this.valueToPosition(value, ui.helper); // 跳至value对应的坐标位置 + if (this.model !== this.dragStartModel) { + this.changeStop.emit(this.model); + } + }; + + // 组件交互方法集合--end + + // 内部公共方法集合--start + /** + * @description: 拖拽公共处理 + * @param: ui 拖拽助手 {helper:表示被拖拽的助手(helper), position: 助手(helper)的当前 CSS 位置} + */ + private dragCommonHandle(ui: TiDragConfig): number { + const pointerX: number = this.limitDragPosition(ui); // 限制指针在合理的位置范围内 + const value: number = this.positionToStepValue(pointerX); // 获取当前位置对应的value值 + + return value; + } + + /** + * @description: 限制滑块移动位置并设置对应的选择区域宽度 + * @param: pointerX 鼠标点击位置(x方向) + */ + private limitDragPosition(ui: TiDragConfig): number { + let pointerX: number = ui.position.left; // 当前滑块中心距离tick第一个坐标位置 + const barWidth: number = this.getBarWidth(); + let pointerWidthHalf: number = this.pointerMaxEleWidth / 2; + let pointerMinLeft: number; + let pointerMaxLeft: number; + if (this.isDouble) { + if (!this.isMinPointerActive) { + // max对应滑块情况的处理 + pointerMinLeft = parseFloat(getComputedStyle(this.pointerMinEle).left); + pointerX = TiSliderComponent.limitValue(pointerX, pointerMinLeft, barWidth); // 限制滑块在最小和右边界之间 + pointerMaxLeft = pointerX; + } else { + pointerMaxLeft = parseFloat(getComputedStyle(this.pointerMaxEle).left); + pointerX = TiSliderComponent.limitValue(pointerX, 0, pointerMaxLeft); // 限制滑块在左边界和最大之间 + pointerMinLeft = pointerX; + } + const styles: { width: string; left: string } = { + width: `${pointerMaxLeft - pointerMinLeft}px`, + left: `${pointerMinLeft + pointerWidthHalf}px` + }; + this.tiRenderer.setStyles(this.selectionEle, styles); + } else { + pointerX = TiSliderComponent.limitValue(pointerX, 0, barWidth); + this.renderer2.setStyle(this.selectionEle, 'width', `${pointerX + pointerWidthHalf}px`); + } + + ui.position.left = pointerX; // 限制滑块的位置显示 + + return pointerX; + } + + /** + * @description: 根据滑块位置获取value值(和step对应) (拖拽公共函数中用) + * @param: pointerX 鼠标点击位置(x方向) + */ + private positionToStepValue(pointerX: number): number { + const value: number = this.positionToValue(pointerX); // 获取指针对应的value绝对比例值(和step无关) + + return this.getStepValue(value); // 转化value为与step对应的值; + } + + /** + * @description: 获取位置对应的value值 + * @param: pointerX 鼠标点击位置(x方向) + */ + private positionToValue(pointerX: number): number { + const barWidth: number = this.getBarWidth(); + const percent: number = pointerX / barWidth; + if (!Util.isUndefined(this.ratios)) { + return this.unequalPositionToValue(pointerX, barWidth, percent); + } + + return this.min + percent * (this.max - this.min); + } + + /** + * @description: 设置组件的model值,用于向外通知 + * @param: value 当前指针的值 + */ + private setValue(value: number): void { + if (this.isDouble) { + const valueMin: number = TiSliderComponent.splitValueToArray(this.model).valueMin; + const valueMax: number = TiSliderComponent.splitValueToArray(this.model).valueMax; + if (!this.isMinPointerActive && value !== valueMax) { + this.model = `${valueMin};${value}`; + } else if (this.isMinPointerActive && value !== valueMin) { + this.model = `${value};${valueMax}`; + } + } else { + if (value !== this.model) { + this.model = value; + } + } + } + + /** + * @description: 获取对应的step值(根据step修正value值) + * @param: value 当前点击的绝对值 + */ + private getStepValue(value: number): number { + const min: number = this.min; + const step: number = this.step; + let stepValue: number; + const stepN: number = Math.round((value - min) / step); // value值变化几个步长 + if (step.toString().indexOf('.') !== -1) { + const n: number = step.toString().split('.').length; + stepValue = parseFloat((min + stepN * step).toFixed(n)); + } else { + stepValue = min + stepN * step; + } + + return TiSliderComponent.limitValue(stepValue, min, this.max); + } + + /** + * @description: 在不等分条件下获取位置对应的value值 + * @param: pointerX 鼠标点击位置(x方向) + * @param: barWidth 指针宽度 + * @param: percent pointerX占滑动轴百分比 + */ + private unequalPositionToValue(pointerX: number, barWidth: number, percent: number): number { + let limitWidth: number = 0; // 每一段的宽度 + let selectLength: number = 0; // 已被选中区域段的宽度 + let ratiosWidth: number = 0; // 已被选中区域段长度和占总长的比例 + const c: number = this.decimalDigit; + const ticksLen: number = this.ticksArr.length; + for (let i: number = 0; i < ticksLen - 1; i++) { + limitWidth = barWidth * this.ratios[i]; + if (i === 0) { + selectLength = 0; + ratiosWidth = 0; + } else { + ratiosWidth = Number((ratiosWidth + this.ratios[i - 1]).toFixed(c)); + selectLength = barWidth * ratiosWidth; + } + const pointerWidth: number = pointerX - selectLength; + if (percent <= Number((ratiosWidth + this.ratios[i]).toFixed(c)) && percent >= ratiosWidth) { + return this.ticksArr[i] + (pointerWidth / limitWidth) * (this.ticksArr[i + 1] - this.ticksArr[i]); + } + } + } + + /** + * @description: 当前组件写入新值 || min || max || scales发生变化 时更新value对应的位置 + * @param: newValue 需要处理的value的值 + */ + private updateValuePosition(newValue: string): void { + let valueTmp: string; + if (this.isDouble) { + valueTmp = this.restrictDoubleValue(newValue); + const valueMax: number = TiSliderComponent.splitValueToArray(valueTmp).valueMax; + const valueMin: number = TiSliderComponent.splitValueToArray(valueTmp).valueMin; + // 当设置value均为最大值情况下,设置最小滑块层级,确保滑块可被拖动 + // 最小值的情况左右滑块zIndex相等,但是右滑块DOM在左滑块之后,因此根据zIndex规则,右滑块可以覆盖左滑块 + if (valueMin === valueMax && valueMin === this.max) { + this.isMinPointerActive = true; + } + // 设置滑块的位置 + this.valueToPosition(valueMin, this.pointerMinEle); + this.valueToPosition(valueMax, this.pointerMaxEle); + } else { + valueTmp = this.restrictSingleValue(newValue); + this.valueToPosition(valueTmp, this.pointerMaxEle); + } + + if (newValue !== valueTmp) { + this.model = valueTmp; + } + } + + /** + * @description: 限定双滑块value的值 + * @param: value 需要处理的value的值 + */ + private restrictDoubleValue(value: string): string { + const valueMin: number = TiSliderComponent.splitValueToArray(value).valueMin; // 双滑块的小值 + const valueMax: number = TiSliderComponent.splitValueToArray(value).valueMax; // 双滑块的大值 + // 非法情况:非数字,超过限制,value大小顺序不正确情况 + const isInvalidValueMin: boolean = + isNaN(valueMin) || TiSliderComponent.isLimitExceed(valueMin, this.min, this.max) || valueMin > valueMax; + // 非法情况:非数字,超过限制,value大小顺序不正确情况 + const isInvalidValueMax: boolean = + isNaN(valueMax) || TiSliderComponent.isLimitExceed(valueMax, this.min, this.max) || valueMin > valueMax; + + return `${isInvalidValueMin ? this.min : valueMin};${isInvalidValueMax ? this.max : valueMax}`; + } + + /** + * @description: 限定单滑块value的值 + * @param: value 需要处理的value的值 + */ + private restrictSingleValue(value: any): any { + if (isNaN(parseFloat(value)) || value < this.min) { + // value不是数字或者小于最小值point定位到最小值 + return this.min; + } + if (value > this.max) { + // value大于最大值point定位到最大值 + return this.max; + } + + return value; + } + + /** + * @description: 处理tip + */ + private setTipConfig(): void { + const defaultTipShow: 'auto' | 'always' = 'auto'; + const defaultTipFormatter: (value: any) => string = (value: any): any => { + return value; + }; + + this.tipMode = Util.isUndefined(this.tipMode) ? defaultTipShow : this.tipMode; + this.tipFormatterFn = Util.isUndefined(this.tipFormatterFn) ? defaultTipFormatter : this.tipFormatterFn; + this.isTipAutoShow = this.tipMode !== 'always'; + if (this.isTipAutoShow) { + this.hide(this.tipMaxEle); + this.hide(this.tipMinEle); + } else { + this.show(this.tipMaxEle); + this.show(this.tipMinEle); + } + } + + /** + * @description: 处理限制 + */ + private setMinMax(): void { + const defaultMin: number = 0; + const defaultMax: number = 10; + const min: number = parseFloat(`${this.min}`); + const max: number = parseFloat(`${this.max}`); + this.min = isNaN(min) || min >= max ? defaultMin : min; + this.max = isNaN(max) || min >= max ? defaultMax : max; + } + + /** + * @description: 处理刻度 + */ + private setScales(): void { + const step: number = this.step; + const min: number = this.min; + const max: number = this.max; + let ticks: Array = []; + if (typeof this.scales === 'function') { + this.ticks = TiSliderComponent.translateScales(this.scales, min, max, step); + } else if (Array.isArray(this.scales)) { + // 为Array情况下,直接使用数值 + this.ticks = this.scales.concat(); + } else { + // 未传scale(包含非法)的情况下,只显示最小和最大值 + const ticksLen: number = (max - min) / step; + ticks.push(min); + for (let i: number = 0; i < ticksLen - 1; i++) { + // 除最小最大值外,其余只打点不显示label + ticks.push(''); + } + ticks.push(max); + this.ticks = ticks; + } + if (!Util.isUndefined(this.ratios)) { + let ratiosSum: number = 0; // 将ratios中每一项与之前的求和 + const sumArray: Array = []; // 由ratiosSum组成的数组 + const ratiosLen: number = this.ratios.length; + for (let i: number = 0; i < ratiosLen; i++) { + ratiosSum = Number((ratiosSum + Number(this.ratios[i])).toFixed(this.decimalDigit)); + sumArray.push(ratiosSum); + } + ticks = this.getTicks(sumArray, this.decimalDigit); + this.ticks = ticks; + } + } + + /** + * @description: 非均匀情况下确定打点的位置 + * @param: arr 由ratiosSum组成的数组 + * @param: num ratios中最大的小数位数 + */ + private getTicks(arr: Array, num: number): Array { + const minScale: number = Number(Math.pow(0.1, num).toFixed(num)); // 判断是否打点的基数 + let k: number = 1; + let p: number = 0; + const ticks: Array = [this.ticks[0]]; + const length: number = parseInt(`${1 / minScale}`, 10); + for (let i: number = 0; i < length; i++) { + p = Number((p + minScale).toFixed(num)); + if (arr.indexOf(p) === -1) { + // 判断每次相加基数后是否在数组中,不在不打点不显示label + ticks.push(null); + } else { + // 否则打点显示label + ticks.push(this.ticks[k]); + k++; + } + } + + return ticks; + } + + /** + * @description: 显示隐藏的元素 + * @param: ele: 要显示的DOM对象 + */ + private show(ele: any): void { + // 注:此处未使用ng-show方式控制滑块显示是因为有延迟,造成后续方法获取pointer宽度为0,显示错乱 + this.renderer2.setStyle(ele, 'display', 'block'); + } + + /** + * @description: 隐藏显示的元素 + * @param: ele: 要隐藏的DOM对象 + */ + private hide(ele: any): void { + // 注:此处未使用ng-show方式控制滑块显示是因为有延迟,造成后续方法获取pointer宽度为0,显示错乱 + this.renderer2.setStyle(ele, 'display', 'none'); + } + + /** + * @ignore + * @description: 获取ticks的最大显示宽度,一行显示不下情况下,换行显示 + * @param: index 刻度下标 + */ + public calcTickMaxWidth(index: number): string { + const ticksLen: number = this.ticks.length; + const spacePercent: number = 1 / (ticksLen - 1); // 两个刻度点间隔宽度百分百(小数) + if (index === 0 || index === ticksLen - 1) { + // 第一个和最后一个刻度 + return TiSliderComponent.parseToCalcStyle(spacePercent, this.pointerMaxEleWidth / 2); + } + + return TiSliderComponent.parseToCalcStyle(spacePercent); + } + + /** + * @ignore + * @description: 获取 ticks 的left位置 + * @param: index 刻度下标 + */ + public calcTickLeftPosition(index: number): string { + return `${(index / (this.ticks.length - 1)) * 100}%`; + } + + // TODO: 看这个方法在模板上调用添加的样式类没有什么实际作用,是不是可以删除? + /** + * @ignore + * @description: 确定滑动轴打点是否为选中点,根据函数返回值设置选中样式 (是否是selection区域的点) + * @param: index 刻度下标 + */ + public isSelectTick(index: number): boolean { + const ticksLen: number = this.ticks.length; + // 双滑块情况下,位于最大最小值之间为选中状态 + if (this.isDouble) { + const value: Array = this.model.split(';'); + const isLargeThanValMin: boolean = index / ticksLen >= (parseFloat(value[0]) - this.min) / (this.max - this.min); + const isSmallThanValMax: boolean = index / ticksLen <= (parseFloat(value[1]) - this.min) / (this.max - this.min); + + return isLargeThanValMin && isSmallThanValMax; + } + + // 单滑块情况下,小于选中值为选中状态 + return index / ticksLen <= parseFloat(this.model) - this.min; + } + + /** + * @description: 获取滑动轴的宽度(涉及到屏幕缩放,所以需要实时获取) + */ + private getBarWidth(): number { + return this.trackELe.getBoundingClientRect().width - this.pointerMaxEleWidth; + } + + /** + * @description: 获取ratios中最大的小数位数 + */ + private getDecimalDigit(): number { + const decimalArr: Array = []; + if (Array.isArray(this.ratios)) { + this.ratios.forEach((item: number) => { + const decimal: string = item.toString().split('.')[1]; + if (decimal) { + decimalArr.push(decimal.length); + } + }); + } + + return Math.max.apply(null, decimalArr); + } + + /** + * @description: 获取当前value所在的区域以及当前value对应的长度占总长的百分比(小数) + * @param: value 指针对应的value值 + * @param: paragraph 当前打点的段数 + */ + private getValuePercent(value: number, paragraph: number): number { + let ratiosSum: number = 0; // 当前打点的段数下ratios的总和 + for (let i: number = 0; i < paragraph; i++) { + ratiosSum = Number((ratiosSum + this.ratios[i]).toFixed(this.decimalDigit)); + } + + return ( + ratiosSum + ((value - this.ticksArr[paragraph]) / (this.ticksArr[paragraph + 1] - this.ticksArr[paragraph])) * this.ratios[paragraph] + ); + } + + /** + * @description: 设置pointerDOM位置 + * @param: value 指针对应的value值 + * @param: pointer 被设置的DOM对象 + */ + private setPointerPos(value: number, pointer: any): void { + let valuePercent: number; + if (!Util.isUndefined(this.ratios)) { + if (Util.isUndefined(this.ticks)) { + return; + } + this.ticksArr = []; + this.ticks.forEach((tick: string) => { + if (!Util.isNull(tick)) { + this.ticksArr.push(tick); + } + }); + const ticksArrLen: number = this.ticksArr.length; + for (let i: number = 0; i < ticksArrLen - 1; i++) { + if (value >= this.ticksArr[i] && value <= this.ticksArr[i + 1]) { + valuePercent = this.getValuePercent(value, i); + } + } + } else { + valuePercent = (value - this.min) / (this.max - this.min); + } + // 计算value对应的百分比位置,并以tick中心点居中显示 + const pointerLeft: string = TiSliderComponent.parseToCalcStyle(valuePercent); + this.renderer2.setStyle(pointer, 'left', pointerLeft); + } + + /** + * @description: 改变某一指针value值对应的指示位置 + * @param: value 指针对应的value值 + * @param: pointer 被设置的DOM对象 + */ + private valueToPosition(value: any, pointer: any): void { + this.setPointerPos(value, pointer); // 设置pointer位置 + let styles: object; // 滑动轴有效区域背景 样式对象 + // 修复SSR报错:ERROR { Error: Uncaught (in promise): TypeError: this.pointerMaxEle.getBoundingClientRect is not a function + if (typeof this.pointerMaxEle.getBoundingClientRect !== 'function') { + return; + } + // 设置滑动轴有效区域背景 + const pointerMaxLeft: number = this.pointerMaxEle.getBoundingClientRect().left; + const pointerMinLeft: number = this.pointerMinEle.getBoundingClientRect().left; + const barWidth: number = this.getBarWidth(); + const barLeft: number = this.trackELe.getBoundingClientRect().left; + const pointerWidthHalf: number = this.pointerMaxEleWidth / 2; + if (this.isDouble) { + styles = { + width: `${((pointerMaxLeft - pointerMinLeft) * 100) / barWidth}%`, + left: `${((pointerMinLeft - barLeft + pointerWidthHalf) * 100) / barWidth}%` + }; + } else { + styles = { + width: `${((pointerMaxLeft - barLeft + pointerWidthHalf) * 100) / barWidth}%`, + left: 0 + }; + } + this.tiRenderer.setStyles(this.selectionEle, styles); + this.setTip(pointer.querySelector('.ti3-slider-tip'), value); + } + + /** + * @description:设置Tip提示内容及位置 + * @param: curTipEle 要设置tip的元素 + * @param: value 设置tip内容对应的value值 + */ + private setTip(curTipEle: any, value: number): void { + // 设置tip内容 + const tipContent: string = this.tipFormatterFn(value); + curTipEle.innerHTML = this.domSanitizer.sanitize(SecurityContext.HTML, tipContent); + + // 设置当前tip位置 + const tipDisplay: string = getComputedStyle(curTipEle).display; + this.show(curTipEle); // 先把tip显示出来 才能获取宽度 + const tipContentWidth: number = curTipEle.getBoundingClientRect().width; + const styles: { left: string; display: string } = { + left: `${-(tipContentWidth - this.pointerMaxEleWidth) / 2 - 1}px`, // 定位受border的影响,需要减去border的宽度 1px + display: tipDisplay // 设置回原来的display + }; + this.tiRenderer.setStyles(curTipEle, styles); + // tip重合情况处理 (双滑块并且tip是一直显示出来那种场景) + if (this.isDouble && !this.isTipAutoShow) { + /** + * @description: 渲染当前的tip样式 (移除一个class并且添加一个class) + * @param: tipEle 当前tipDOM + * @param: removeClass 要移除的class名字 + * @param: addClass 要添加的class名字 + */ + const renderTipStyle: (tipEle: any, removeClass: string, addClass: string) => void = ( + tipEle: any, + removeClass: string, + addClass: string + ): void => { + this.renderer2.removeClass(tipEle, removeClass); + this.renderer2.addClass(tipEle, addClass); + }; + // 重置tip样式,防止之前样式设置造成的影响 ti3-slider-tip-top + renderTipStyle(this.tipMaxEle, 'ti3-slider-tip-bottom', 'ti3-slider-tip-top'); + renderTipStyle(this.tipMinEle, 'ti3-slider-tip-bottom', 'ti3-slider-tip-top'); + this.show(this.tipMaxEle); + this.show(this.tipMinEle); + + // 最大最小值重合情况下,只显示当前tip + const selectionWidth: number = this.selectionEle.getBoundingClientRect().width; + if (selectionWidth === 0) { + if (this.tiRenderer.hasClass(curTipEle, 'ti3-slider-tip-max')) { + // 当前tip是最大值的 隐藏最小值tip + this.hide(this.tipMinEle); + } else { + // 当前tip是最小值的 隐藏最大值tip + this.hide(this.tipMaxEle); + } + + return; + } + // tip重合情况下,当前tip向下显示 + const tipMaxWidth: number = this.tipMaxEle.getBoundingClientRect().width; + const tipMinWidth: number = this.tipMinEle.getBoundingClientRect().width; + if ((tipMaxWidth + tipMinWidth) / 2 >= selectionWidth) { + renderTipStyle(curTipEle, 'ti3-slider-tip-top', 'ti3-slider-tip-bottom'); + } + } + } +} diff --git a/src/slider/lib/src/TiSliderModule.ts b/src/slider/lib/src/TiSliderModule.ts new file mode 100644 index 0000000..0368fff --- /dev/null +++ b/src/slider/lib/src/TiSliderModule.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiDraggableModule } from '@opentiny/ng-drag'; +import { TiSliderComponent } from './TiSliderComponent'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiIconModule } from '@opentiny/ng-icon'; + +@NgModule({ + imports: [CommonModule, TiDraggableModule, TiIconModule, TiRendererModule], + exports: [TiSliderComponent], + declarations: [TiSliderComponent] +}) +export class TiSliderModule {} +export { TiSliderComponent } from './TiSliderComponent'; diff --git a/src/slider/lib/src/slider.html b/src/slider/lib/src/slider.html new file mode 100644 index 0000000..931611f --- /dev/null +++ b/src/slider/lib/src/slider.html @@ -0,0 +1,43 @@ +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
      +
    • + +
      +
      + +
      + + + + +
      +
    • +
    +
    diff --git a/src/slider/lib/src/slider.less b/src/slider/lib/src/slider.less new file mode 100644 index 0000000..46b2120 --- /dev/null +++ b/src/slider/lib/src/slider.less @@ -0,0 +1,179 @@ +@import "../../../themes/basic/base-all.less"; + +:host { + --ti-slider-track-height: var(--ti-common-size-2x); + --ti-slider-pointer-width: var(--ti-common-size-5x); + --ti-slider-pointer-height: var(--ti-common-size-7x); + --ti-slider-bar-selection-height: var(--ti-common-size-4x); + --ti-slider-tip-triangle-width: 10px; + --ti-slider-tip-triangle-height: 6px; + --ti-slider-border-radius: var(--ti-common-border-radius-normal); + --ti-slider-tick-top: var(--ti-slider-track-height); + --ti-slider-tip-vertical-space: calc(var(--ti-slider-tip-triangle-height) + 5px); + --ti-slider-tick-margin-left: calc(var(--ti-slider-pointer-width)/2); +} + +:host.ti3-slider-container{ + display: block; + margin: var(--ti-common-space-9x) var(--ti-common-space-0); + cursor: pointer; + .clearfix(); + + & .ti3-slider-track-container { + width: 100%; + height: var(--ti-slider-track-height); + position: relative; + background-color: var(--ti-common-color-bg-light-normal); + border-radius: var(--ti-slider-border-radius); + & .ti3-slider-track-content { + width: calc(100% - var(--ti-slider-pointer-width)); + height: var(--ti-slider-track-height); + position: relative; + & .ti3-slider-bar-selection { + height: var(--ti-slider-bar-selection-height); + background-color: var(--ti-common-color-bg-emphasize); + position: absolute; + z-index: 1; + top: calc((var(--ti-slider-track-height) - var(--ti-slider-bar-selection-height)) / 2); + border-radius: var(--ti-slider-border-radius) 0 0 var(--ti-slider-border-radius); + } + & .ti3-slider-pointer-min, + & .ti3-slider-pointer-max { + position: absolute; + width: var(--ti-slider-pointer-width); + height: var(--ti-slider-pointer-height); + line-height: calc(var(--ti-slider-pointer-height) - 2px); + background-color: var(--ti-common-color-bg-white-normal); + top: calc(-1 * (var(--ti-slider-pointer-height) - var(--ti-slider-track-height))/2); + z-index: 1; + .box-sizing(border-box); + border-radius: calc(var(--ti-slider-border-radius) * 5); + &.ti3-icon-slider-point:hover { + color: var(--ti-common-color-bg-hover); + border-color: var(--ti-common-color-bg-hover); + } + &.ti3-icon-slider-point { + position: absolute; + color: var(--ti-common-color-icon-active); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-active); + border-radius: calc(var(--ti-slider-border-radius) * 5); + text-align: center; + .box-shadow(var(--ti-common-shadow-1-down)); + background: linear-gradient(153deg, var(--ti-common-color-bg-white-normal), var(--ti-common-color-bg-light-normal) 99%); + } + & .ti3-slider-tip{ + border-radius: var(--ti-common-border-radius-1); + position: absolute; + background-color: var(--ti-common-color-bg-dark-deep); + color: var(--ti-common-color-text-gray); + padding: var(--ti-common-space-3x) var(--ti-common-space-4x); + word-wrap: break-word; + white-space: pre; + line-height: var(--ti-common-line-height-number); + cursor: default; + font-size: var(--ti-common-font-size-base); + font-family: var(--ti-common-color-text-primary); // 复写字体原因:此处继承父级字体ti3Font + .box-shadow(var(--ti-common-shadow-3-down)); + &.ti3-slider-tip-top{ + bottom: calc(100% + var(--ti-slider-tip-vertical-space)); + &:before{ + .triangle-down(var(--ti-slider-tip-triangle-width); var(--ti-slider-tip-triangle-height); var(--ti-common-color-bg-dark-deep)); + content: " "; + position: absolute; + pointer-events: none; + margin-left: calc(-1 * var(--ti-slider-tip-triangle-width)/2); + z-index: 3; + top: 100%; + left: 50%; + } + } + &.ti3-slider-tip-bottom{ + top: calc(100% + var(--ti-slider-tip-vertical-space)); + &:before{ + .triangle-up(var(--ti-slider-tip-triangle-width); var(--ti-slider-tip-triangle-height); var(--ti-common-color-bg-dark-deep)); + content: " "; + position: absolute; + pointer-events: none; + margin-left: calc(-1 * var(--ti-slider-tip-triangle-width)/2); + z-index: 3; + bottom: 100%; + left: 50%; + } + } + } + } + & .ti3-slider-pointer-min { + display: none; + } + } + + } + + & .ti3-slider-ticks { + position: relative; + width: calc(100% - var(--ti-slider-pointer-width)); + & .ti3-slider-tick { + position: absolute; + height: var(--ti-slider-track-height); + margin-left: var(--ti-slider-tick-margin-left); + top: calc(-1* var(--ti-slider-tick-top)); + & .ti3-slider-tick-dot { + width: 2px; + height: var(--ti-slider-track-height); + background-color: var(--ti-common-color-bg-white-normal); + position: relative; + &.ti3-slider-selection-tick-dot { + background-color: var(--ti-common-color-bg-white-normal); + } + } + &:first-child, &:last-child{ + .ti3-slider-tick-dot { + width: 0; + } + } + &:first-child { + .ti3-slider-tick-value{ + left: calc(-1 * var(--ti-slider-tick-margin-left)); + } + } + &:last-child { + .ti3-slider-tick-value { + white-space: nowrap; + left: calc(-100% + var(--ti-slider-tick-margin-left)); + } + } + & .ti3-slider-tick-value { + position: relative; + top: var(--ti-slider-tick-top); + left: -50%; + color: var(--ti-common-color-text-secondary); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + } + } + } + + &.ti3-slider-disable{ + &.ti3-slider-container { + cursor: not-allowed; + & .ti3-slider-track-container { + background-color: var(--ti-common-color-bg-disabled); + .ti3-slider-bar-selection{ + background-color: var(--ti-common-color-text-disabled); + } + .ti3-slider-pointer-min, .ti3-slider-pointer-max { + &.ti3-icon-slider-point { + color: var(--ti-common-color-icon-disabled); + border-color: var(--ti-common-color-line-disabled); + .box-shadow(none); + background: var(--ti-common-color-bg-disabled); + } + &.ti3-icon-slider-point:hover { + color: var(--ti-common-color-icon-disabled); + border-color: var(--ti-common-color-line-disabled); + } + } + } + } + } +} diff --git a/src/spinner/demo/karma.conf.js b/src/spinner/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/spinner/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/spinner/demo/project.json b/src/spinner/demo/project.json new file mode 100644 index 0000000..5479491 --- /dev/null +++ b/src/spinner/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/spinner/demo", + "sourceRoot": "src/spinner/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/spinner", + "index": "src/spinner/demo/src/index.html", + "main": "src/spinner/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/spinner/demo/tsconfig.app.json", + "assets": ["src/spinner/demo/src/favicon.ico", "src/spinner/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "spinner-demo:build:production" + }, + "development": { + "browserTarget": "spinner-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js spinner" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/spinner/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/spinner/demo/tsconfig.spec.json", + "karmaConfig": "src/spinner/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/spinner/demo/src/app/AppComponent.ts b/src/spinner/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/spinner/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/spinner/demo/src/app/AppModule.ts b/src/spinner/demo/src/app/AppModule.ts new file mode 100644 index 0000000..4f45e4f --- /dev/null +++ b/src/spinner/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SpinnerTestModule } from './spinner/SpinnerTestModule'; + +@NgModule({ + imports: [ + SpinnerTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/spinner/demo/src/app/IndexComponent.ts b/src/spinner/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..00d230d --- /dev/null +++ b/src/spinner/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SpinnerTestModule } from './spinner/SpinnerTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SpinnerTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/spinner/demo/src/app/app.html b/src/spinner/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/spinner/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/spinner/demo/src/app/spinner/SpinnerBasicComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerBasicComponent.ts new file mode 100644 index 0000000..7575e5b --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerBasicComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-basic.html' +}) +export class SpinnerBasicComponent { + max: number = 2000; + min: number = -20; + spinnerValue: number = undefined; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerBasicTestComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerBasicTestComponent.ts new file mode 100644 index 0000000..9a9ecc5 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerBasicTestComponent.ts @@ -0,0 +1,119 @@ +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms'; +export interface SpinnerModel { + min?: number; + max?: number; + spinnerValue?: any; + spinnerValue1?: any; + spinnerValue2?: any; + spinnerValue3?: any; + step?: number; + format?: string; + disable?: any; + placeholder?: any; + blur?: any; + change?: any; +} +@Component({ + templateUrl: './spinner-basic-test.html', + styleUrls: ['./spinnerTest.less'] +}) +export class SpinnerBasicTestComponent implements OnInit { + max: number = 10000; + min: number = -400; + spinnerModel: SpinnerModel = { + max: this.max, + min: this.min, + spinnerValue: 8, + spinnerValue1: '', + spinnerValue2: 10, + spinnerValue3: 10, + step: 1, + format: 'N2', + disable: false, + placeholder: `请输入${this.min}到${this.max}的数值`, + change: (value: number): void => { + console.log('change evt:' + value); + } + }; + @ViewChild('form', { static: true }) form: any; + @ViewChild('spinner1', { static: true }) spinner1: any; + ngForm: any; + spinnerValue: number = 11; + format: string = 'N4'; + spinnerForm: FormGroup; + constructor(private fb: FormBuilder) {} + ngOnInit(): void { + const baseValue: number = 1200; + this.spinnerForm = this.fb.group({ + spinnerValue: baseValue, // 设置初始值 + text1Value: 14, // 设置初始值 + text2Value: baseValue // 设置初始值 + }); + // 订阅响应式表单的值的改变和状态改变 + this.reactiveFormSpinnerValueChange(); + this.reactiveFormValueChanges(); + this.reactiveFormStatusChanges(); + } + // ngModel形式(模板驱动表单)对value的监控 + changeFn(value: number): void { + console.log('change evt:' + value); + } + // 响应式表单 对value的监控 + reactiveFormSpinnerValueChange(): void { + const spinnerValueControl: AbstractControl = this.spinnerForm.get('spinnerValue'); + + spinnerValueControl.valueChanges.subscribe((value: number) => { + console.log(value); + this.spinnerForm.patchValue({ + text2Value: value + }); + }); + } + // 整个响应式表单监控 + reactiveFormValueChanges(): void { + this.spinnerForm.valueChanges.subscribe((data: number) => { + console.log(data); + }); + } + reactiveFormStatusChanges(): void { + this.spinnerForm.statusChanges.subscribe((states: any) => { + console.log(states); + }); + } + focusFn($event: any): void { + console.log('focusEvent'); + } + blurFn($event: any): void { + console.log('blurEvent'); + } + changeValue(): void { + this.spinnerModel.spinnerValue = -401; + console.log(this.spinnerModel.spinnerValue); + } + changeReactiveValue(): void { + this.spinnerForm.patchValue({ + spinnerValue: 1300 // 设置初始值 + }); + } + changeMax(): void { + this.spinnerModel.max = 0; + } + changeMin(): void { + this.spinnerModel.min = 20; + } + changeDisable(): void { + this.spinnerModel.disable = !this.spinnerModel.disable; + } + changePlaceholder(): void { + this.spinnerModel.placeholder = 'Please input the number from -100 to 100.'; + } + focusSpinner1(): void { + const spinner1: any = this.spinner1; + console.log(spinner1); + spinner1.focus(); + setTimeout(() => { + spinner1.blur(); + }, 1000); + } +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerCorrectableComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerCorrectableComponent.ts new file mode 100644 index 0000000..e32683f --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerCorrectableComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-correctable.html' + // styleUrls: ['./spinnerTest.less'] +}) +export class SpinnerCorrectableComponent { + max: number = 200; + min: number = -20; + spinnerValue: number = 23837.4545; + format: string = 'N2'; + correctable: boolean = true; + + changeCorrect(): void { + this.spinnerValue = 23837.4545; + this.correctable = false; + } +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerDisabledComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerDisabledComponent.ts new file mode 100644 index 0000000..1d0251e --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerDisabledComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-disabled.html' +}) +export class SpinnerDisabledComponent { + spinnerValue: number = 1500.3624; + max: number = 2000; + min: number = -20; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerEventComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerEventComponent.ts new file mode 100644 index 0000000..bf5f82e --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerEventComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-event.html' +}) +export class SpinnerEventComponent { + myLogs: Array = []; + max: number = 2000; + min: number = -20; + spinnerValue: number = 1500.3624; + + onModelChange(value: any): void { + this.myLogs = [...this.myLogs, `onModelChange value=${value} type=${typeof value}`]; + } +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerFormatComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerFormatComponent.ts new file mode 100644 index 0000000..050e7ae --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerFormatComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-format.html' +}) +export class SpinnerFormatComponent { + max: number = 2000; + min: number = -20; + spinnerValue: number = 1500.3624; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerIdComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerIdComponent.ts new file mode 100644 index 0000000..9be7f85 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerIdComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + template: ` ` +}) +export class SpinnerIdComponent { + elementId: string = 'spinner'; + pinnerValue: number = 12; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerLoadComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerLoadComponent.ts new file mode 100644 index 0000000..e716f17 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerLoadComponent.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-load.html' +}) +export class SpinnerLoadComponent { + max: number = 10000; + min: number = -400; + spinnerValue: any; + max1: number = 100; + min1: number = 0; + spinnerValue1: number = 40; + max2: number = 100; + min2: number = 0; + spinnerValue2: number = 40; + myLogs: Array = []; + changeLegal(): void { + this.spinnerValue = 1234.66666; + } + changeIllegal(): void { + this.spinnerValue = 'werty3452'; + } + changeMax(): void { + this.spinnerValue1 = 120; + } + changeMin(): void { + this.spinnerValue1 = -100; + } + changeMiddle(): void { + this.spinnerValue1 = 50; + } + onModelChange(value: any): void { + this.myLogs = [...this.myLogs, `onModelChange value=${value}`]; + } + + onModelChange1(value: any): void { + this.myLogs = [...this.myLogs, `onModelChange1 value=${value}`]; + } +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerLocaleableComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerLocaleableComponent.ts new file mode 100644 index 0000000..5f1100d --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerLocaleableComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-localeable.html' +}) +export class SpinnerLocaleableComponent { + max: number = 2000; + min: number = -20; + spinnerValue: number = 1500.3624; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerMaxMinComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerMaxMinComponent.ts new file mode 100644 index 0000000..1c3e501 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerMaxMinComponent.ts @@ -0,0 +1,38 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-max-min.html', + styleUrls: ['./spinnerTest.less'] +}) +export class SpinnerMaxMinComponent { + max: any = 20; + min: number = 0; + spinnerValue: number = 30; + + max1: any = 20; + min1: number = 0; + spinnerValue1: number = 30; + + max2: any = 20; + min2: any = 10; + spinnerValue2: number = 8; + + changeMax(): void { + this.max = 40; + } + changeInvalid(): void { + this.max = 'werq'; + } + changeMax1(): void { + this.max1 = 15; + } + changeInvalid1(): void { + this.max1 = 'wewe'; + } + changeMin2(): void { + this.min2 = 12; + } + changeInvalid2(): void { + this.min2 = 'werq'; + } +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerMaxlengthComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerMaxlengthComponent.ts new file mode 100644 index 0000000..d5e786e --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerMaxlengthComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-maxlength.html' +}) +export class SpinnerMaxlengthComponent { + max: number = 2000; + min: number = -20; + spinnerValue: number = 1500.3624; + maxlength: number = 10; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerStepComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerStepComponent.ts new file mode 100644 index 0000000..69ae5e5 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerStepComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-step.html', + styleUrls: ['./spinnerTest.less'] +}) +export class SpinnerStepComponent { + max: number = 2000; + min: number = -20; + spinnerValue: number = 1500.3624; + step: number = 2; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerStepfnComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerStepfnComponent.ts new file mode 100644 index 0000000..a3e7e5e --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerStepfnComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-stepfn.html' +}) +export class SpinnerStepfnComponent { + myLogs: Array = []; + max: number = 2000; + min: number = -20; + spinnerValue: number = 1500.3624; + + stepFn: (value: number, isAdd: boolean) => number = (value: number, isAdd: boolean) => { + let step: number = 1; + if (value < 10) { + step = isAdd ? 2 : 1; + } else if (value >= 10 && value <= 100) { + step = isAdd ? 10 : 5; + } else { + step = 100; + } + + this.myLogs = [...this.myLogs, `stepFn isAdd=${isAdd} value=${value} step=${step}`]; + + return step; + }; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerTestModule.ts b/src/spinner/demo/src/app/spinner/SpinnerTestModule.ts new file mode 100644 index 0000000..f63d61c --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerTestModule.ts @@ -0,0 +1,118 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiButtonModule, TiSpinnerModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { SpinnerBasicTestComponent } from './SpinnerBasicTestComponent'; +import { SpinnerIdComponent } from './SpinnerIdComponent'; +import { SpinnerValidationTestComponent } from './SpinnerValidationTestComponent'; +import { SpinnerBasicComponent } from './SpinnerBasicComponent'; +import { SpinnerFormatComponent } from './SpinnerFormatComponent'; +import { SpinnerStepComponent } from './SpinnerStepComponent'; +import { SpinnerValidationComponent } from './SpinnerValidationComponent'; +import { SpinnerCorrectableComponent } from './SpinnerCorrectableComponent'; +import { SpinnerLocaleableComponent } from './SpinnerLocaleableComponent'; +import { SpinnerMaxlengthComponent } from './SpinnerMaxlengthComponent'; +import { SpinnerEventComponent } from './SpinnerEventComponent'; +import { SpinnerLoadComponent } from './SpinnerLoadComponent'; +import { SpinnerMaxMinComponent } from './SpinnerMaxMinComponent'; +import { SpinnerDisabledComponent } from './SpinnerDisabledComponent'; +import { SpinnerStepfnComponent } from './SpinnerStepfnComponent'; +import { SpinnerTipComponent } from './SpinnerTipComponent'; +import { SpinnerTipTestComponent } from './SpinnerTipTestComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiValidationModule, + TiButtonModule, + TiSpinnerModule, + DemoLogModule, + RouterModule.forChild(SpinnerTestModule.ROUTES) + ], + declarations: [ + SpinnerBasicTestComponent, + SpinnerIdComponent, + SpinnerBasicComponent, + SpinnerStepComponent, + SpinnerValidationComponent, + SpinnerFormatComponent, + SpinnerValidationTestComponent, + SpinnerCorrectableComponent, + SpinnerLocaleableComponent, + SpinnerMaxlengthComponent, + SpinnerEventComponent, + SpinnerLoadComponent, + SpinnerMaxMinComponent, + SpinnerDisabledComponent, + SpinnerStepfnComponent, + SpinnerTipComponent, + SpinnerTipTestComponent + ] +}) +export class SpinnerTestModule { + static readonly LINKS: Array = [{ href: 'components/TiSpinnerComponent.html', label: 'Spinner' }]; + static readonly ROUTES: Routes = [ + { + path: 'spinner/spinner-basic', + component: SpinnerBasicComponent + }, + { + path: 'spinner/spinner-format', + component: SpinnerFormatComponent + }, + { + path: 'spinner/spinner-step', + component: SpinnerStepComponent + }, + { + path: 'spinner/spinner-validation', + component: SpinnerValidationComponent + }, + { + path: 'spinner/spinner-basic-test', + component: SpinnerBasicTestComponent + }, + { path: 'spinner/id', component: SpinnerIdComponent }, + { + path: 'spinner/spinner-validation-test', + component: SpinnerValidationTestComponent + }, + { + path: 'spinner/spinner-correctable', + component: SpinnerCorrectableComponent + }, + { + path: 'spinner/spinner-localeable', + component: SpinnerLocaleableComponent + }, + { path: 'spinner/spinner-maxlength', component: SpinnerMaxlengthComponent }, + { + path: 'spinner/spinner-disabled', + component: SpinnerDisabledComponent + }, + { + path: 'spinner/spinner-event', + component: SpinnerEventComponent + }, + { + path: 'spinner/spinner-stepfn', + component: SpinnerStepfnComponent + }, + { + path: 'spinner/spinner-tip', + component: SpinnerTipComponent + }, + { + path: 'spinner/spinner-tip-test', + component: SpinnerTipTestComponent + }, + { path: 'spinner/spinner-load', component: SpinnerLoadComponent }, + { path: 'spinner/spinner-max-min', component: SpinnerMaxMinComponent } + ]; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerTipComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerTipComponent.ts new file mode 100644 index 0000000..6a77e73 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerTipComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-tip.html' +}) +export class SpinnerTipComponent { + max: number = 2000; + min: number = -20; + spinnerValue: number = 1500.3624; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerTipTestComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerTipTestComponent.ts new file mode 100644 index 0000000..d6a3d1e --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerTipTestComponent.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-tip-test.html' +}) +export class SpinnerTipTestComponent { + max: number = 10000; + min: number = -400; + spinnerValue: number; + spinnerValue1: number = 180; + spinnerValue2: number = 900; + spinnerValue3: number = 900; + spinnerValue4: number = 900; + spinnerValue5: number = 800; + + changeRange(): void { + this.max = 200; + this.min = 100; + } + + changeMaxValue(): void { + this.max = 800; + } + + changeMinValue(): void { + this.min = 200; + } +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerValidationComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerValidationComponent.ts new file mode 100644 index 0000000..95c1081 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerValidationComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './spinner-validation.html' +}) +export class SpinnerValidationComponent { + spinnerValue: number = undefined; +} diff --git a/src/spinner/demo/src/app/spinner/SpinnerValidationTestComponent.ts b/src/spinner/demo/src/app/spinner/SpinnerValidationTestComponent.ts new file mode 100644 index 0000000..cba8ee6 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/SpinnerValidationTestComponent.ts @@ -0,0 +1,32 @@ +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiValidationConfig, TiValidators } from '@opentiny/ng'; +@Component({ + templateUrl: `./spinner-validation-test.html`, + styleUrls: ['./spinnerTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class SpinnerValidationTestComponent { + elementId: string = 'spinner'; + form: FormGroup; + spinnerValue: number = undefined; + placeholder: string = '-90~90'; + placeholder1: string = '-10~100'; + validationObj: TiValidationConfig = { + type: 'blur' + }; + validationObj1: TiValidationConfig = { + type: 'change' + }; + constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + mySpinner: new FormControl(1, [TiValidators.required, TiValidators.rangeValue(-10, 100)]) + }); + } + onBlur(): void { + console.log('on blur'); + } + onFocus(): void { + console.log('on focus'); + } +} diff --git a/src/spinner/demo/src/app/spinner/spinner-basic-test.html b/src/spinner/demo/src/app/spinner/spinner-basic-test.html new file mode 100644 index 0000000..03be7fa --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-basic-test.html @@ -0,0 +1,129 @@ +

    Spinner组件默认样式展示

    +
    +
    +

    用在表单中(第一个form)--模板驱动表单

    +
    + + + +
    +

    form's touched: {{form.touched | json}}

    +

    form's untouched: {{form.untouched | json}}

    +
    {{form.value | json}}
    +

    用在表单外

    + + +
    +
    +

    用在表单中(第二个form)--响应式表单

    +
    + + + + +
    + +
    +
    + + + + + + + +
    +
    +
    + +

    表格单元格设置white-space: nowrap;时,spinner布局问题,方案:spinner外框设置white-space: normal;

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    1234
    + + + 456dfdfdfdfdf
    dfdfdhgderjhedtkirkl8756856
    6856895695685632fgjfrkjwtyp;yul
    dujrtikdfgdfhjdfkjfrtkidjft
    +
    +
    diff --git a/src/spinner/demo/src/app/spinner/spinner-basic.html b/src/spinner/demo/src/app/spinner/spinner-basic.html new file mode 100644 index 0000000..8a9a1f6 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-basic.html @@ -0,0 +1 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinner-correctable.html b/src/spinner/demo/src/app/spinner/spinner-correctable.html new file mode 100644 index 0000000..ac3e893 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-correctable.html @@ -0,0 +1,17 @@ +

    描述

    +

    自10.0.1版本起去掉屏蔽该接口,不想强转时不要设置最大最小值就行。

    +

    失去焦点,是否强制转换。默认值为true,支持强制转换。当配置为false时,不支持强制转换!

    +

    示例

    + +

    + diff --git a/src/spinner/demo/src/app/spinner/spinner-disabled.html b/src/spinner/demo/src/app/spinner/spinner-disabled.html new file mode 100644 index 0000000..82e6aa4 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-disabled.html @@ -0,0 +1 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinner-event.html b/src/spinner/demo/src/app/spinner/spinner-event.html new file mode 100644 index 0000000..f328336 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-event.html @@ -0,0 +1,9 @@ + + diff --git a/src/spinner/demo/src/app/spinner/spinner-format.html b/src/spinner/demo/src/app/spinner/spinner-format.html new file mode 100644 index 0000000..c55efed --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-format.html @@ -0,0 +1 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinner-load.html b/src/spinner/demo/src/app/spinner/spinner-load.html new file mode 100644 index 0000000..eb73e0a --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-load.html @@ -0,0 +1,21 @@ +

    描述

    +

    spinner组件,数据加载

    +

    示例

    +

    1. 典型应用场景:非法数据-->数据A--->数据B

    + +

    +     +   

    +

    2. 典型应用场景:数据-->大于最大值--->小于最小值

    + +

    +         +    +

    值改变日志:

    + diff --git a/src/spinner/demo/src/app/spinner/spinner-localeable.html b/src/spinner/demo/src/app/spinner/spinner-localeable.html new file mode 100644 index 0000000..a082e41 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-localeable.html @@ -0,0 +1,8 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinner-max-min.html b/src/spinner/demo/src/app/spinner/spinner-max-min.html new file mode 100644 index 0000000..23c2abc --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-max-min.html @@ -0,0 +1,18 @@ +

    描述

    +

    最大值、最小值设置

    +

    示例

    +

    1.不设置最大最小值时,默认最大值是 2^53, 最小值是 -2^53,当前值为30

    + +

    + +

    2.修改最大值。最小最大值[0,20] 当前值为30

    + +

    +    +   

    + +

    3.修改最小值。最小最大值[10,20] 当前值为8

    + +

    +    +    diff --git a/src/spinner/demo/src/app/spinner/spinner-maxlength.html b/src/spinner/demo/src/app/spinner/spinner-maxlength.html new file mode 100644 index 0000000..e43df09 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-maxlength.html @@ -0,0 +1,8 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinner-step.html b/src/spinner/demo/src/app/spinner/spinner-step.html new file mode 100644 index 0000000..fad6e70 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-step.html @@ -0,0 +1 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinner-stepfn.html b/src/spinner/demo/src/app/spinner/spinner-stepfn.html new file mode 100644 index 0000000..967340c --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-stepfn.html @@ -0,0 +1,2 @@ + + diff --git a/src/spinner/demo/src/app/spinner/spinner-tip-test.html b/src/spinner/demo/src/app/spinner/spinner-tip-test.html new file mode 100644 index 0000000..4444e57 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-tip-test.html @@ -0,0 +1,32 @@ +

    描述

    +

    设置最大最小值时,鼠标移入时显示输入范围提示

    +

    示例

    +

    1.不设置最大最小值

    + +

    +

    2.设置最大最小值默认配置

    + +

    +

    3.只设置最小值

    + +

    +

    4.只设置最大值

    + +

    +

    5.设置最大最小值自定义提示内容

    + +

    +
    6.设置最大最小值时,若不想显示默认提示文本,可自定义提示内容为空字符串''
    + +

    + + + diff --git a/src/spinner/demo/src/app/spinner/spinner-tip.html b/src/spinner/demo/src/app/spinner/spinner-tip.html new file mode 100644 index 0000000..5278d0d --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-tip.html @@ -0,0 +1,9 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinner-validation-test.html b/src/spinner/demo/src/app/spinner/spinner-validation-test.html new file mode 100644 index 0000000..93f8701 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-validation-test.html @@ -0,0 +1,18 @@ +

    Spinner组件 校验展示

    +
    +
    + + +
    +
    +
    + +
    diff --git a/src/spinner/demo/src/app/spinner/spinner-validation.html b/src/spinner/demo/src/app/spinner/spinner-validation.html new file mode 100644 index 0000000..44a5c79 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinner-validation.html @@ -0,0 +1,9 @@ + diff --git a/src/spinner/demo/src/app/spinner/spinnerTest.less b/src/spinner/demo/src/app/spinner/spinnerTest.less new file mode 100644 index 0000000..f06233c --- /dev/null +++ b/src/spinner/demo/src/app/spinner/spinnerTest.less @@ -0,0 +1,22 @@ +.container { + padding-left: 10px; + padding-top: 10px; + a { + text-decoration: none; + background-color: #8bc34a; + color: white; + padding: 5px 10px; + display: inline-block; + border: 1px solid #00bcd4; + margin-right: 10px; + &:last-child { + margin-right: 0; + } + &:hover { + background-color: #ff9800; + } + } + .wrapper { + margin-top: 10px; + } +} diff --git a/src/spinner/demo/src/app/spinner/webdoc/spinner-demos.js b/src/spinner/demo/src/app/spinner/webdoc/spinner-demos.js new file mode 100644 index 0000000..26e0d07 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/webdoc/spinner-demos.js @@ -0,0 +1,137 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'spinner-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Spinner 组件的最简用法。

    ', + 'en-US': '', + }, + apis: [ + 'TiSpinnerComponent.properties.min', + 'TiSpinnerComponent.properties.max', + 'TiSpinnerComponent.properties.placeholder', + ], + }, + { + demoId: 'spinner-format', + name: { + 'zh-CN': '数字精度', + 'en-US': 'format', + }, + desc: { + 'zh-CN': + '

    通过属性format配置小数点位数,使用“n(N)+数字”方式设置,数字代表保留几位小数。

    ', + 'en-US': '', + }, + apis: ['TiSpinnerComponent.properties.format'], + }, + { + demoId: 'spinner-localeable', + name: { + 'zh-CN': '国际化', + 'en-US': 'localeable', + }, + desc: { + 'zh-CN': '

    通过属性localeable配置是否开启国际化。

    ', + 'en-US': '', + }, + apis: ['TiSpinnerComponent.properties.localeable'], + }, + { + demoId: 'spinner-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': '', + }, + apis: ['TiSpinnerComponent.properties.disabled'], + }, + { + demoId: 'spinner-step', + name: { + 'zh-CN': '步长', + 'en-US': 'step', + }, + desc: { + 'zh-CN': '

    通过属性step配置微调器的步长。

    ', + 'en-US': '', + }, + apis: ['TiSpinnerComponent.properties.step'], + }, + { + demoId: 'spinner-stepfn', + name: { + 'zh-CN': '动态步长', + 'en-US': 'stepfn', + }, + desc: { + 'zh-CN': + '

    通过属性stepFn动态配置步长。传递出来的参数为当前值、当前是否点击了加号按钮。

    ', + 'en-US': '', + }, + apis: ['TiSpinnerComponent.properties.stepFn'], + }, + { + demoId: 'spinner-maxlength', + name: { + 'zh-CN': '允许的最大字符数', + 'en-US': 'maxlength', + }, + desc: { + 'zh-CN': + '

    通过属性maxlength配置输入框中允许的最大字符数。

    ', + 'en-US': '', + }, + apis: ['TiSpinnerComponent.properties.maxlength'], + }, + { + demoId: 'spinner-tip', + name: { + 'zh-CN': 'tip提示', + 'en-US': 'tip', + }, + desc: { + 'zh-CN': + '

    通过属性tipContent配置提示内容。通过属性tipPosition配置提示方向。

    ', + 'en-US': '', + }, + apis: [ + 'TiSpinnerComponent.properties.tipContent', + 'TiSpinnerComponent.properties.tipPosition', + ], + }, + { + demoId: 'spinner-validation', + name: { + 'zh-CN': '校验', + 'en-US': 'validation', + }, + desc: { + 'zh-CN': + '

    通过指令tiValidation实现校验。如果要对最大最小值进行校验就不要给 ti-spinner 设置minmax

    ', + 'en-US': '', + }, + }, + { + demoId: 'spinner-event', + name: { + 'zh-CN': '事件', + 'en-US': 'events', + }, + desc: { + 'zh-CN': + '

    当元素内容发生变化的时候触发ngModelChange事件。

    ', + 'en-US': 'spinner event description', + }, + }, + ], +}; diff --git a/src/spinner/demo/src/app/spinner/webdoc/spinner.cn.md b/src/spinner/demo/src/app/spinner/webdoc/spinner.cn.md new file mode 100644 index 0000000..1831536 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/webdoc/spinner.cn.md @@ -0,0 +1,36 @@ +--- +title: Spinner 数字微调 +--- +# Spinner 数字微调 + +
    + +Spinner 是数字微调组件。   + +```typescript +import { TiSpinnerModule } from '@opentiny/ng'; +``` + +如需使用校验功能,请导入。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
    + +
    + +Spinner 是数字微调组件。   + +```typescript +import { TiSpinnerModule } from '@opentiny/ng'; +``` + +如需使用校验功能,请导入。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/spinner/demo/src/app/spinner/webdoc/spinner.en.md b/src/spinner/demo/src/app/spinner/webdoc/spinner.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/spinner/demo/src/app/spinner/webdoc/spinner.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/spinner/demo/src/favicon.ico b/src/spinner/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/spinner/demo/src/index.html b/src/spinner/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/spinner/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/spinner/demo/src/main.ts b/src/spinner/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/spinner/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/spinner/demo/test.ts b/src/spinner/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/spinner/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/spinner/demo/tsconfig.app.json b/src/spinner/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/spinner/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/spinner/demo/tsconfig.spec.json b/src/spinner/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/spinner/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/spinner/lib/index.ts b/src/spinner/lib/index.ts new file mode 100644 index 0000000..4f33553 --- /dev/null +++ b/src/spinner/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSpinnerModule'; diff --git a/src/spinner/lib/ng-package.json b/src/spinner/lib/ng-package.json new file mode 100644 index 0000000..5ce85f5 --- /dev/null +++ b/src/spinner/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/spinner", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/spinner/lib/package.json b/src/spinner/lib/package.json new file mode 100644 index 0000000..e35a8a1 --- /dev/null +++ b/src/spinner/lib/package.json @@ -0,0 +1,17 @@ +{ + "name": "@opentiny/ng-spinner", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-inputnumber": "~1.0.0-beta.0", + "@angular/forms": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-tip": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/spinner/lib/project.json b/src/spinner/lib/project.json new file mode 100644 index 0000000..f49d9c2 --- /dev/null +++ b/src/spinner/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/spinner/lib", + "sourceRoot": "src/spinner/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/spinner"], + "options": { + "project": "src/spinner/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/spinner"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js spinner" + }, + { + "command": "ng default-build spinner" + }, + { + "command": "node build/clear-default-theme.js spinner" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/spinner && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build spinner && ng pack spinner && node build/publish.js spinner --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/spinner/lib/src/TiSpinnerComponent.ts b/src/spinner/lib/src/TiSpinnerComponent.ts new file mode 100644 index 0000000..6db4f00 --- /dev/null +++ b/src/spinner/lib/src/TiSpinnerComponent.ts @@ -0,0 +1,484 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Injector, + Input, + Output, + Renderer2, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { TiLocale, TiLocaleFormat } from '@opentiny/ng-locale'; +import { TiKeymap, TiPositionType, Util } from '@opentiny/ng-utils'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +import { NgControl, NgModel } from '@angular/forms'; +/** + * 数字微调组件 + * + */ +@Component({ + selector: 'ti-spinner', + templateUrl: './spinner.html', + styleUrls: ['./spinner.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiSpinnerComponent)] +}) +export class TiSpinnerComponent extends TiFormComponent { + /** + * @ignore + */ + public method: any = { + METHOD_ADD: 'add', + METHOD_SUB: 'sub' + }; + /** + * 小数保留位数。使用 n + '数字' 形式,例如:'n4',代表保留4位小数。 + * + * 不设置时,10.0.1 版本后小数保留位数最少 0 位,最多 3 位;10.0.0 版本前小数保留位数为 3 位 + */ + @Input() format: string; + /** + * 最大值,支持整数和小数 + */ + @Input() max: number = Math.pow(2, 53); + /** + * 最小值,支持整数和小数 + */ + @Input() min: number = -Math.pow(2, 53); + /** + * 步长,按键盘上、下键或点击 + 、- 按钮增减的数值 + */ + @Input() step: number = 1; + /** + * 输入框的占位文本 + */ + @Input() placeholder: string = ''; + /** + * @ignore + * + * 从 10.0.1 版本开始该接口不再开放。如果不想要组件根据最大最小值进行强制转换,那么不要设置最大最小值即可。 + * + * 失去焦点是否支持根据最大最小值进行强制转换(默认值为 true, 支持强转; 当用户配置为 false 时,不支持强制转换) + */ + @Input() correctable: boolean = true; + /** + * 是否开启国际化 + */ + @Input() localeable: boolean = true; + /** + * 输入框允许的最大字符数 + */ + @Input() maxlength: number = 20; + /** + * @ignore + * 10.1.11版本之前服务使用tiTip指令自行实现提示 + * + * 此处做兼容性处理,添加该接口判断服务是否使用指令实现 + * + */ + @Input() tiTip: string; + /** + * tip 提示内容,当值为空字符串,则不显示 tip。 + * + * 同时设置最大、最小值时,默认提示文本是:输入值必须在 { 0 } 到 { 1 } 之间。; + * 只设置最大值,默认提示文本是:输入值不能超过 { 0 }。 + * 只设置最小值,默认提示文本是:输入值不能小于 { 0 } + */ + @Input() tipContent: string; + /** + * tip 提示方向 + */ + @Input() tipPosition: TiPositionType = 'top'; + /** + * 动态改变步长,参数为当前输入框值、是否点击加号或者键盘上键,返回值:新步长 + */ + @Input() stepFn: (value: number, isAdd: boolean) => number; + /** + * @ignore + * 当数据发生改变时,触发change事件 + */ + @Output() readonly stepChange: EventEmitter = new EventEmitter(); + @ViewChild('input', { static: true }) private inputEle: ElementRef; + + private numberFormat: string = '1.0-3'; // 整数位保留最小位数.小数位保留最小位数-小数位最大保留位置 + /** + * @ignore + */ + public inputValue: number; + /** + * @ignore + */ + public inputTip: string; + protected versionInfo: string = super.getVersion(packageInfo); + private spinnerWords = TiLocale.getLocaleWords().tiSpinner; + /** + * 对最大最小值进行合法性校验 + */ + private static validateMaxAndMin(key: string, value: any): number { + if (!Number.isNaN(parseFloat(value))) { + return value; + } + + if (key === 'max') { + return Math.pow(2, 53); + } + + if (key === 'min') { + return -Math.pow(2, 53); + } + } + + constructor( + protected hostRef: ElementRef, + protected renderer2: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + private injector: Injector + ) { + super(hostRef, renderer2, changeDetectorRef); + } + + // 组件声明周期钩子--start ↓ + ngOnInit(): void { + super.ngOnInit(); + this.setFocusableElems([this.inputEle.nativeElement]); + this.init(); + } + + private init(): void { + if (!TiLocaleFormat.isInvalidFormat(this.format)) { + const precision: number = parseInt(this.format.slice(1), 10); + this.numberFormat = '1.' + precision + '-' + precision; + } + + this.max = TiSpinnerComponent.validateMaxAndMin('max', this.max); + this.min = TiSpinnerComponent.validateMaxAndMin('min', this.min); + this.setInputTip(); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + this.setChanges(changes, 'max'); + this.setChanges(changes, 'min'); + } + + /** + * @ignore + */ + setDisabledState(isDisabled: boolean): void { + super.setDisabledState(isDisabled); + this.setInputTip(); + // 响应式表单场景,onpush策略下需要更新视图 + this.changeDetectorRef.markForCheck(); + } + + // 输入属性发生改变 + private setChanges(changes: SimpleChanges, key: string): void { + if (changes[key] && !changes[key].isFirstChange()) { + this[key] = parseFloat(changes[key].currentValue); + if (Number.isNaN(this[key]) || this.min > this.max) { + this[key] = TiSpinnerComponent.validateMaxAndMin(key, changes[key].previousValue); + } + if (this.correctable && this.model !== undefined) { + // 这里如果去除了setTimeout,在OnPush环境并不会报错。但是在default环境不能去除setTimeout。综合是不去除 + // error: Expression has changed after it was checked. + setTimeout(() => { + const model: number = this.getModelByMinMax(this.inputValue); + if (model !== this.model) { + this.inputValue = model; + this.model = model; + // onpush策略 响应式表单中使用组件值更改后视图不更新,需要手动标记 + this.changeDetectorRef.markForCheck(); + } + }, 0); + } + + this.setInputTip(); + } + } + + // 组件声明周期钩子--end + // 实现ControlValueAccessor接口 + /** + * @ignore + * model --> view + */ + writeValue(value: any): void { + if (value === null) { + return; + } + + if (!Util.isUndefined(value)) { + if (Number.isNaN(parseFloat(value))) { + super.writeValue(value); + this.model = this.inputValue; + + return; + } + + if (this.correctable) { + // 此处添加延时是由于动态修改value的同时修改max或min时,响应式表单场景输入框值显示异常 + // 此处判断是否是模板式表单,决定是否延时处理 + const spinnerControl: any = this.injector.get(NgControl); + const changeModel: () => void = (): void => { + super.writeValue(value); + const val: any = this.getModelByMinMax(value); + this.model = val; + this.inputValue = val; + }; + if (spinnerControl instanceof NgModel) { + changeModel(); + + return; + } + setTimeout(() => { + changeModel(); + this.changeDetectorRef.markForCheck(); + }, 0); + + return; + } + } + + super.writeValue(value); + this.inputValue = value; + } + // 实现ControlValueAccessor接口--end + /** + * @ignore + */ + public blurFn(): void { + let correctValue: number = this.inputValue; + if (this.correctable && this.inputValue !== undefined) { + correctValue = this.getModelByMinMax(this.inputValue); + } + const parseFormatValue: number = correctValue === undefined ? correctValue : parseFloat(this.formatValue(correctValue)); + if (parseFormatValue !== this.model) { + this.model = parseFormatValue; + } + this.inputValue = this.model; + this.onModelTouched(); // 校验 初次聚焦 后续聚焦失焦 + } + /** + * @ignore + * description: Event emitter for producting the `ngModelChange` event after the view model updates. + * ngModelChange 是ngModel指令的@Output. + * 它在viewToModelUpdate函数中触发 + * + */ + public inputChange(value: number): void { + if (value !== undefined && (value < this.min || value > this.max)) { + return; + } + const parseFormatValue: number = value === undefined ? value : parseFloat(this.formatValue(value)); + if (parseFormatValue !== this.model) { + this.model = parseFormatValue; + } + } + /** + * 键盘上下键操作 + * @ignore + */ + public keydownFn(event: any): void { + if (this.disabled) { + return; + } + if (event.keyCode === TiKeymap.KEY_ARROW_UP) { + // 阻止input默认事件,防止按上键时光标移动到内容前面 + event.preventDefault(); + this.stepNumber(this.method.METHOD_ADD); + } else if (event.keyCode === TiKeymap.KEY_ARROW_DOWN) { + this.stepNumber(this.method.METHOD_SUB); + } + } + /** + * @ignore + */ + public stepNumberMousedown(e: any, method: string): void { + if (e.button === TiKeymap.MOUSE_MIDDLE_BUTTON || e.button === TiKeymap.MOUSE_RIGHT_BUTTON || this.disabled) { + return; + } + // 如果是鼠标按下向下btn,输入框需要做获得光标的处理 + this.inputEle.nativeElement.focus(); + e.preventDefault(); // 目的是防止input失去焦点 + this.stepNumber(method); + } + + private stepNumber(method: string): void { + if ( + (this.inputValue >= this.max && method === this.method.METHOD_ADD) || + (this.inputValue <= this.min && method === this.method.METHOD_SUB) + ) { + return; + } + this.stepChange.emit(method); + + if (this.stepFn && Util.isFunction(this.stepFn)) { + const isAdd: boolean = method === 'add' ? true : false; + this.step = this.stepFn(this.inputValue, isAdd); + } + + // 当输入框中的值为空时,点击+,-,显示最小值。 + if (this.inputValue === undefined) { + this.model = this.min; + this.inputValue = this.model; + + return; + } + if (method === this.method.METHOD_ADD) { + if (this.inputValue > this.max - this.step) { + this.model = this.max; + this.inputValue = this.model; + + return; + } + if (this.inputValue < this.min) { + this.model = this.min; + this.inputValue = this.model; + + return; + } + } else if (method === this.method.METHOD_SUB) { + if (this.inputValue < this.min + this.step) { + this.model = this.min; + this.inputValue = this.model; + + return; + } + if (this.inputValue > this.max) { + this.model = this.max; + this.inputValue = this.model; + + return; + } + } + this.model = this.accOperate(this.inputValue, this.step, method); + this.inputValue = this.model; + } + // 组件交互方法集合--end + + // 根据最大最小值得到model值 + private getModelByMinMax(curValue: number): number { + if (curValue < this.min) { + return this.min; + } + + if (curValue > this.max) { + return this.max; + } + + return curValue; + } + + private formatValue(value: number): string { + if (value === undefined) { + return; + } + // https://angular.cn/api/common/DecimalPipe + // digitsInfo: {minIntegerDigits}.{minFractionDigits}-{maxFractionDigits} + // minIntegerDigits: 小数点前最小位数 默认为1 + // minFractionDigits: 小数点后最小位数 默认0 + // maxFractionDigits: 小数点后最大位数 默认3 + const localeValue: string = TiLocaleFormat.formatNumber(value, this.numberFormat); + + return TiLocaleFormat.parseNumber(localeValue).toString(); + } + + // 根据步长、+/-进行数值计算 + private accOperate(value: number, step: number, method: string): number { + let r1: number; + let r2: number; + let c: number; + let m: number; + let _value: number; + let _step: number; + // 计算 val 小数点后数字的位数 + const getLength: (value: string | number) => number = (val: string): number => { + return String(val).split('.')[1] ? String(val).split('.')[1].length : 0; + }; + // 去除 val 中的 ‘.’ + const replacePeriod: (value: string | number) => number = (val: string): number => { + return Number(String(val).replace('.', '')); + }; + r1 = getLength(value); + r2 = getLength(step); + c = Math.abs(r1 - r2); + m = Math.pow(10, Math.max(r1, r2)); + + if (c > 0) { + const cm: number = Math.pow(10, c); + if (r1 > r2) { + _value = replacePeriod(value); + _step = replacePeriod(step) * cm; + } else { + _value = replacePeriod(value) * cm; + _step = replacePeriod(step); + } + } else { + _value = replacePeriod(value); + _step = replacePeriod(step); + } + + if (method === this.method.METHOD_ADD) { + return (_value + _step) / m; + } + + if (method === this.method.METHOD_SUB) { + return (_value - _step) / m; + } + + return undefined; + } + + /** + * 设置tip提示内容 + */ + private setInputTip(): void { + // 兼容使用tiTip指令实现的tip提示 + if (this.disabled || this.tiTip) { + this.inputTip = ''; + + return; + } + + if (Util.isString(this.tipContent)) { + this.inputTip = this.tipContent; + + return; + } + + if (this.max !== Math.pow(2, 53) && this.min !== -Math.pow(2, 53)) { + this.inputTip = Util.formatEntry(this.spinnerWords.rangeValue, [this.min, this.max]); + + return; + } + + if (this.min !== -Math.pow(2, 53)) { + this.inputTip = Util.formatEntry(this.spinnerWords.minValue, [this.min]); + + return; + } + + if (this.max !== Math.pow(2, 53)) { + this.inputTip = Util.formatEntry(this.spinnerWords.maxValue, [this.max]); + + return; + } + } + + // 内部公共方法集合--end +} diff --git a/src/spinner/lib/src/TiSpinnerModule.ts b/src/spinner/lib/src/TiSpinnerModule.ts new file mode 100644 index 0000000..17f0fc6 --- /dev/null +++ b/src/spinner/lib/src/TiSpinnerModule.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiInputNumberModule } from '@opentiny/ng-inputnumber'; +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiSpinnerComponent } from './TiSpinnerComponent'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; +@NgModule({ + imports: [CommonModule, FormsModule, TiIconModule, TiTextModule, TiInputNumberModule, TiTipModule], + exports: [TiSpinnerComponent], + declarations: [TiSpinnerComponent] +}) +export class TiSpinnerModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiSpinnerComponent } from './TiSpinnerComponent'; diff --git a/src/spinner/lib/src/i18n/TiSpinnerWords.ts b/src/spinner/lib/src/i18n/TiSpinnerWords.ts new file mode 100644 index 0000000..5f223d9 --- /dev/null +++ b/src/spinner/lib/src/i18n/TiSpinnerWords.ts @@ -0,0 +1,7 @@ +export interface TiSpinnerWords { + tiSpinner: { + maxValue: string; + minValue: string; + rangeValue: string; + }; +} diff --git a/src/spinner/lib/src/i18n/en_US.ts b/src/spinner/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..194d4b7 --- /dev/null +++ b/src/spinner/lib/src/i18n/en_US.ts @@ -0,0 +1,9 @@ +import { TiSpinnerWords } from './TiSpinnerWords'; + +export const en_US: TiSpinnerWords = { + tiSpinner: { + maxValue: 'Enter a value less than or equal to {0}.', + minValue: 'Enter a value greater than or equal to {0}.', + rangeValue: 'Enter a value from {0} to {1}.' + } +}; diff --git a/src/spinner/lib/src/i18n/es_US.ts b/src/spinner/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..3f325e8 --- /dev/null +++ b/src/spinner/lib/src/i18n/es_US.ts @@ -0,0 +1,9 @@ +import { TiSpinnerWords } from './TiSpinnerWords'; + +export const es_US: TiSpinnerWords = { + tiSpinner: { + maxValue: 'Ingrese un valor inferior o igual a {0}.', + minValue: 'Ingrese un valor superior o igual a {0}.', + rangeValue: 'Ingrese un valor entre {0} y {1}.' + } +}; diff --git a/src/spinner/lib/src/i18n/fr_FR.ts b/src/spinner/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..2cdfa0a --- /dev/null +++ b/src/spinner/lib/src/i18n/fr_FR.ts @@ -0,0 +1,9 @@ +import { TiSpinnerWords } from './TiSpinnerWords'; + +export const fr_FR: TiSpinnerWords = { + tiSpinner: { + maxValue: 'Saisissez une valeur inférieure ou égale à {0}.', + minValue: 'Saisissez une valeur supérieure ou égale à {0}.', + rangeValue: 'Saisissez une valeur comprise entre {0} et {1}.' + } +}; diff --git a/src/spinner/lib/src/i18n/index.ts b/src/spinner/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/spinner/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/spinner/lib/src/i18n/pt_BR.ts b/src/spinner/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..c41d053 --- /dev/null +++ b/src/spinner/lib/src/i18n/pt_BR.ts @@ -0,0 +1,9 @@ +import { TiSpinnerWords } from './TiSpinnerWords'; + +export const pt_BR: TiSpinnerWords = { + tiSpinner: { + maxValue: 'Insira um valor inferior ou igual a {0}.', + minValue: 'Insira um valor superior ou igual a {0}.', + rangeValue: 'Insira um valor de {0} a {1}.' + } +}; diff --git a/src/spinner/lib/src/i18n/zh_CN.ts b/src/spinner/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..a0901d8 --- /dev/null +++ b/src/spinner/lib/src/i18n/zh_CN.ts @@ -0,0 +1,9 @@ +import { TiSpinnerWords } from './TiSpinnerWords'; + +export const zh_CN: TiSpinnerWords = { + tiSpinner: { + maxValue: '输入值不能超过{0}。', + minValue: '输入值不能小于{0}。', + rangeValue: '输入值必须在{0}到{1}之间。' + } +}; diff --git a/src/spinner/lib/src/spinner.html b/src/spinner/lib/src/spinner.html new file mode 100644 index 0000000..efdda9d --- /dev/null +++ b/src/spinner/lib/src/spinner.html @@ -0,0 +1,32 @@ +
    +
    + +
    +
    diff --git a/src/spinner/lib/src/spinner.less b/src/spinner/lib/src/spinner.less new file mode 100644 index 0000000..4f98ec2 --- /dev/null +++ b/src/spinner/lib/src/spinner.less @@ -0,0 +1,109 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-spinner-container-height: var(--ti-common-size-7x); + --ti-spinner-inner-height: calc(var(--ti-spinner-container-height) - var(--ti-common-border-weight-normal) * 2); +} + +:host { + display: inline-block; + position: relative; + vertical-align: middle; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + background-color: var(--ti-common-color-bg-white-normal); + line-height: normal; + width: var(--ti-common-size-30x); + height: var(--ti-spinner-container-height) !important; + white-space: normal; // 解决在表格中的使用问题 + .box-sizing(border-box); + .form-border-animat-init(); + &:active { + border-color: var(--ti-common-color-line-active); + cursor: pointer; + } + &:hover { + border-color: var(--ti-common-color-line-hover); + //默认状态下的hover动画 + .form-border-animat-enter(); + } + &[tiFocused] { + border-color: var(--ti-common-color-line-active); + } + // 错误场景下中间input背景色 + @{tiny-invalid-class} { + .ti3-spinner-upicon, + .ti3-spinner-downicon { + background-color: var(--ti-common-color-error-bg); + } + } +} + +.ti3-spinner-input-box { + position: absolute; + left: var(--ti-spinner-inner-height); + top: 0; + width: calc(100% - var(--ti-spinner-inner-height) * 2); + // 解决浏览器缩放问题:缩放时,输入框底部边框消失 + // 原因:输入框设置固定高度,但其父元素没有高度,随浏览器缩放,输入框固高大于父元素高度,导致底部边框被遮挡 + height: 100%; + .box-sizing(border-box); + + text-align: center; + ime-mode: disabled; + .ti3-spinner-input[tiText] { + text-align: center; + height: 100%; + width: 100%; + line-height: var(--ti-spinner-inner-height); + background-color: var(--ti-common-color-transparent); // 设置text透明色,避免覆盖组件样式 + } +} + +.ti3-spinner-upicon, +.ti3-spinner-downicon { + .box-sizing(border-box); + position: absolute; + text-align: center; + top: 0; + color: var(--ti-common-color-icon-normal); + width: var(--ti-spinner-inner-height); + // 解决浏览器缩放问题:缩放时,按钮禁用时底部边框消失 + height: 100%; + line-height: var(--ti-spinner-inner-height); + &:not(.ti3-spinner-icon-disabled):hover { + color: var(--ti-common-color-icon-hover); + cursor: pointer; + } + &.ti3-spinner-icon-disabled { + color: var(--ti-common-color-icon-disabled); + background-color: var(--ti-common-color-bg-disabled); + cursor: not-allowed; + } +} +.ti3-spinner-upicon { + border-left: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + right: 0; +} + +.ti3-spinner-downicon { + border-right: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + left: 0; +} + +:host[disabled] { + background-color: var(--ti-common-color-bg-disabled); + border-color: var(--ti-common-color-line-disabled); + cursor: not-allowed; + + .ti3-spinner-upicon, + .ti3-spinner-downicon { + // 禁用样式要优先于悬浮样式,增加样式权重 + &:not(.ti3-spinner-icon-disabled) { + color: var(--ti-common-color-icon-disabled); + background-color: var(--ti-common-color-bg-disabled); + cursor: not-allowed; + } + border-color: var(--ti-common-color-line-disabled); + } +} diff --git a/src/steps/demo/karma.conf.js b/src/steps/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/steps/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/steps/demo/project.json b/src/steps/demo/project.json new file mode 100644 index 0000000..6b6cce9 --- /dev/null +++ b/src/steps/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/steps/demo", + "sourceRoot": "src/steps/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/steps", + "index": "src/steps/demo/src/index.html", + "main": "src/steps/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/steps/demo/tsconfig.app.json", + "assets": ["src/steps/demo/src/favicon.ico", "src/steps/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "steps-demo:build:production" + }, + "development": { + "browserTarget": "steps-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js steps" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/steps/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/steps/demo/tsconfig.spec.json", + "karmaConfig": "src/steps/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/steps/demo/src/app/AppComponent.ts b/src/steps/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/steps/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/steps/demo/src/app/AppModule.ts b/src/steps/demo/src/app/AppModule.ts new file mode 100644 index 0000000..23634cf --- /dev/null +++ b/src/steps/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { StepsTestModule } from './steps/StepsTestModule'; + +@NgModule({ + imports: [ + StepsTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/steps/demo/src/app/IndexComponent.ts b/src/steps/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..989a5fb --- /dev/null +++ b/src/steps/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { StepsTestModule } from './steps/StepsTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = StepsTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/steps/demo/src/app/app.html b/src/steps/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/steps/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/steps/demo/src/app/steps/StepsActiveComponent.ts b/src/steps/demo/src/app/steps/StepsActiveComponent.ts new file mode 100644 index 0000000..c177923 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsActiveComponent.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-active.html' // 指定组件模板 +}) +export class StepsActiveComponent { + steps: Array = [ + { + id: 'id1', + label: 'General' + }, + { + id: 'id2', + label: 'Host Group' + }, + { + id: 'id3', + label: 'Policy' + }, + { + id: 'id4', + label: 'Names' + } + ]; + activeStep: TiStepItem = this.steps[2]; + clickable: boolean = true; + labelKey: string = 'id'; + + jump(): void { + this.activeStep = this.steps[1]; + } +} diff --git a/src/steps/demo/src/app/steps/StepsAdaptiveComponent.ts b/src/steps/demo/src/app/steps/StepsAdaptiveComponent.ts new file mode 100644 index 0000000..8e4399a --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsAdaptiveComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-adaptive.html' +}) +export class StepsAdaptiveComponent { + steps: Array = [ + { + label: '购买专业服务' + }, + { + label: '服务受理' + }, + { + label: '查看进展' + }, + { + label: '验收完成' + } + ]; + activeStep: number = 1; +} diff --git a/src/steps/demo/src/app/steps/StepsAdaptiveTestComponent.ts b/src/steps/demo/src/app/steps/StepsAdaptiveTestComponent.ts new file mode 100644 index 0000000..46b03aa --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsAdaptiveTestComponent.ts @@ -0,0 +1,69 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-adaptive-test.html' +}) +export class StepsAdaptiveTestComponent { + steps1: Array = [ + { + label: '基础配置' + }, + { + label: '网络系统配置' + }, + { + label: '其他各种配置' + }, + { + label: '确认配置' + } + ]; + steps2: Array = [ + { + label: 'API设计' + }, + { + label: 'API代理' + }, + { + label: 'API产品' + }, + { + label: '资产中心' + } + ]; + activeStep1: TiStepItem = this.steps1[2]; + activeStep2: TiStepItem = this.steps2[1]; + changeLabel1(): void { + this.steps1[0].label = 'General General General General General General General'; + } + changeSteps1(): void { + this.steps1 = [ + { + label: 'dfhaashdfa dhajdh dhfahdj sd' + }, + { + label: 'fajksdj djfa fjasj fjakd' + }, + { + label: '其他各种系统配置' + }, + { + label: '江山如此多娇,引无数英雄竞折腰' + }, + { + label: '快乐旋转' + } + ]; + } + changeContainerWidht1(container: any): void { + container.style.width = '1200px'; + } + changeLabel2(): void { + this.steps2[2].label = 'General General General General General General General'; + } + changeContainerWidht2(container: any): void { + container.style.marginLeft = '0px'; + } +} diff --git a/src/steps/demo/src/app/steps/StepsBaseComponent.ts b/src/steps/demo/src/app/steps/StepsBaseComponent.ts new file mode 100644 index 0000000..3a24027 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsBaseComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-base.html' +}) +export class StepsBaseComponent { + steps: Array = [ + { + label: '购买专业服务' + }, + { + label: '服务受理' + }, + { + label: '查看进展' + }, + { + label: '验收完成' + } + ]; + activeStep: number = 1; +} diff --git a/src/steps/demo/src/app/steps/StepsBeforeComponent.ts b/src/steps/demo/src/app/steps/StepsBeforeComponent.ts new file mode 100644 index 0000000..d9052f3 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsBeforeComponent.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-before.html' +}) +export class StepsBeforeComponent { + myLogs: Array = []; + steps: Array = [ + { + label: '购买专业服务' + }, + { + label: '服务受理' + }, + { + label: '查看进展(必选项)', + require: true + }, + { + label: '验收完成' + } + ]; + activeStep: number = 1; + beforeStep(step: TiStepItem): void { + let requireIndex: number; + const index: number = this.steps.indexOf(step); + for (let i: number = 0; i < this.steps.length; i++) { + if (this.steps[i].require) { + requireIndex = i; + } + } + + if (index < requireIndex) { + this.activeStep = index; + } else { + this.myLogs = [...this.myLogs, '有必选项未完成,点击项不可直接跳转,需完成未完成项。']; + } + } +} diff --git a/src/steps/demo/src/app/steps/StepsClickableComponent.ts b/src/steps/demo/src/app/steps/StepsClickableComponent.ts new file mode 100644 index 0000000..6fac903 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsClickableComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-clickable.html' +}) +export class StepsClickableComponent { + steps: Array = [ + { + label: '购买专业服务' + }, + { + label: '服务受理' + }, + { + label: '查看进展', + error: true + }, + { + label: '验收完成(禁用)', + disabled: true + } + ]; + + activeStep: number = 1; +} diff --git a/src/steps/demo/src/app/steps/StepsEventsComponent.ts b/src/steps/demo/src/app/steps/StepsEventsComponent.ts new file mode 100644 index 0000000..efed272 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsEventsComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-events.html' +}) +export class StepsEventsComponent { + myLogs: Array = []; + steps: Array = [ + { + label: '购买专业服务' + }, + { + label: '服务受理' + }, + { + label: '查看进展' + }, + { + label: '验收完成' + } + ]; + activeStep: number = 1; + onActiveStepChange(activeStep: number): void { + this.myLogs = [...this.myLogs, `activeStepChange() event => ${JSON.stringify(this.steps[activeStep])}`]; + } +} diff --git a/src/steps/demo/src/app/steps/StepsLabelComponent.ts b/src/steps/demo/src/app/steps/StepsLabelComponent.ts new file mode 100644 index 0000000..d14f363 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsLabelComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-label.html' +}) +export class StepsLabelComponent { + steps: Array = [ + { + label: '购买专业服务', + english: 'Purchase' + }, + { + label: '服务受理', + english: 'Processing' + }, + { + label: '查看进展', + english: 'Progress' + }, + { + label: '验收完成', + english: 'Acceptance' + } + ]; + activeStep: number = 1; +} diff --git a/src/steps/demo/src/app/steps/StepsMaxwidthComponent.ts b/src/steps/demo/src/app/steps/StepsMaxwidthComponent.ts new file mode 100644 index 0000000..debf422 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsMaxwidthComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-maxwidth.html' +}) +export class StepsMaxwidthComponent { + steps: Array = [ + { + label: '咨询购买专业服务,并支付' + }, + { + label: '服务受理' + }, + { + label: '查看进展' + }, + { + label: '验收完成' + } + ]; + activeStep: number = 1; +} diff --git a/src/steps/demo/src/app/steps/StepsTemplateComponent.ts b/src/steps/demo/src/app/steps/StepsTemplateComponent.ts new file mode 100644 index 0000000..8c824a7 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsTemplateComponent.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core'; +import { TiStepItem } from '@opentiny/ng-steps'; + +@Component({ + templateUrl: './steps-template.html' +}) +export class StepsTemplateComponent { + steps: Array = [ + { + label: '购买专业服务' + }, + { + label: '服务受理' + }, + { + label: '查看进展' + }, + { + label: '验收完成' + } + ]; + activeStep: number = 1; + ngOnInit(): void { + this.getIcon(this.activeStep); + } + + activeStepChange(activeStep: number): void { + this.getIcon(activeStep); + } + + getIcon(activeStep: number): void { + this.steps.forEach((step: TiStepItem, index: number) => { + step.icon = index > activeStep ? 'exclamation-circle' : 'add1'; + }); + } +} diff --git a/src/steps/demo/src/app/steps/StepsTestModule.ts b/src/steps/demo/src/app/steps/StepsTestModule.ts new file mode 100644 index 0000000..4f943d1 --- /dev/null +++ b/src/steps/demo/src/app/steps/StepsTestModule.ts @@ -0,0 +1,78 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiIconModule, TiStepsModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { StepsActiveComponent } from './StepsActiveComponent'; +import { StepsClickableComponent } from './StepsClickableComponent'; +import { StepsBeforeComponent } from './StepsBeforeComponent'; +import { StepsBaseComponent } from './StepsBaseComponent'; +import { StepsMaxwidthComponent } from './StepsMaxwidthComponent'; +import { StepsLabelComponent } from './StepsLabelComponent'; +import { StepsAdaptiveComponent } from './StepsAdaptiveComponent'; +import { StepsAdaptiveTestComponent } from './StepsAdaptiveTestComponent'; +import { StepsTemplateComponent } from './StepsTemplateComponent'; +import { StepsEventsComponent } from './StepsEventsComponent'; + +@NgModule({ + imports: [CommonModule, TiStepsModule, TiIconModule, DemoLogModule, RouterModule.forChild(StepsTestModule.ROUTES)], + declarations: [ + StepsBaseComponent, + StepsActiveComponent, + StepsClickableComponent, + StepsBeforeComponent, + StepsMaxwidthComponent, + StepsAdaptiveComponent, + StepsLabelComponent, + StepsAdaptiveTestComponent, + StepsTemplateComponent, + StepsEventsComponent + ] +}) +export class StepsTestModule { + static readonly LINKS: Array = [{ href: 'components/TiStepsComponent.html', label: 'Steps' }]; + static readonly ROUTES: Routes = [ + { + path: 'steps/steps-base', + component: StepsBaseComponent + }, + { + path: 'steps/steps-clickable', + component: StepsClickableComponent + }, + { + path: 'steps/steps-active', + component: StepsActiveComponent + }, + { + path: 'steps/steps-before', + component: StepsBeforeComponent + }, + { + path: 'steps/steps-maxwidth', + component: StepsMaxwidthComponent + }, + { + path: 'steps/steps-label', + component: StepsLabelComponent + }, + { + path: 'steps/steps-adaptive', + component: StepsAdaptiveComponent + }, + { + path: 'steps/steps-events', + component: StepsEventsComponent + }, + { + path: 'steps/steps-template', + component: StepsTemplateComponent + }, + { + path: 'steps/steps-adaptive-test', + component: StepsAdaptiveTestComponent + } + ]; +} diff --git a/src/steps/demo/src/app/steps/steps-active.html b/src/steps/demo/src/app/steps/steps-active.html new file mode 100644 index 0000000..61646a1 --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-active.html @@ -0,0 +1,9 @@ +

    1.描述

    +

    activeStep接口测试

    +

    2.示例

    +

    (2.1)activeStep动态变更

    + +

    当前选中项:{{activeStep.label}}

    +
    + + diff --git a/src/steps/demo/src/app/steps/steps-adaptive-test.html b/src/steps/demo/src/app/steps/steps-adaptive-test.html new file mode 100644 index 0000000..482fbab --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-adaptive-test.html @@ -0,0 +1,29 @@ +

    描述

    +

    + adaptive接口场景测试。设置为adaptive为true时,整体宽度会自适应撑满父容器,父容器宽度改变的时候steps也会调整(这个是10.1.2版本才会支持), + label改变时steps也会调整。 +

    +

    示例

    + +

    1.容器固定值宽度,改变容器宽度或改变label,steps都会自适应

    +
    +
    + +
    +
    +

    当前选中项:{{activeStep1.label}}

    +
    + + + + +

    2.容器宽度自适应时,容器宽度变化或改变label,steps都会自适应

    +
    +
    + +
    +
    +

    当前选中项:{{activeStep2.label}}

    +
    + + diff --git a/src/steps/demo/src/app/steps/steps-adaptive.html b/src/steps/demo/src/app/steps/steps-adaptive.html new file mode 100644 index 0000000..b93c24e --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-adaptive.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/steps/demo/src/app/steps/steps-base.html b/src/steps/demo/src/app/steps/steps-base.html new file mode 100644 index 0000000..8640525 --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-base.html @@ -0,0 +1 @@ + diff --git a/src/steps/demo/src/app/steps/steps-before.html b/src/steps/demo/src/app/steps/steps-before.html new file mode 100644 index 0000000..913f6ff --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-before.html @@ -0,0 +1,2 @@ + + diff --git a/src/steps/demo/src/app/steps/steps-clickable.html b/src/steps/demo/src/app/steps/steps-clickable.html new file mode 100644 index 0000000..7a615e5 --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-clickable.html @@ -0,0 +1,4 @@ + +
    +
    Current value: {{ steps[activeStep].label }}
    +
    diff --git a/src/steps/demo/src/app/steps/steps-events.html b/src/steps/demo/src/app/steps/steps-events.html new file mode 100644 index 0000000..731e2f0 --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-events.html @@ -0,0 +1,8 @@ + + diff --git a/src/steps/demo/src/app/steps/steps-label.html b/src/steps/demo/src/app/steps/steps-label.html new file mode 100644 index 0000000..742f141 --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-label.html @@ -0,0 +1 @@ + diff --git a/src/steps/demo/src/app/steps/steps-maxwidth.html b/src/steps/demo/src/app/steps/steps-maxwidth.html new file mode 100644 index 0000000..baff5fd --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-maxwidth.html @@ -0,0 +1 @@ + diff --git a/src/steps/demo/src/app/steps/steps-template.html b/src/steps/demo/src/app/steps/steps-template.html new file mode 100644 index 0000000..2c0c7d3 --- /dev/null +++ b/src/steps/demo/src/app/steps/steps-template.html @@ -0,0 +1,11 @@ + + + + + + + +
    第{{ i }}步
    + {{ step.label }} +
    +
    diff --git a/src/steps/demo/src/app/steps/webdoc/steps-demos.js b/src/steps/demo/src/app/steps/webdoc/steps-demos.js new file mode 100644 index 0000000..ca8f8d6 --- /dev/null +++ b/src/steps/demo/src/app/steps/webdoc/steps-demos.js @@ -0,0 +1,107 @@ +export default { + column: '2', + demos: [ + { + demoId: 'steps-base', + name: { + 'zh-CN': '基本使用', + 'en-US': 'base', + }, + desc: { + 'zh-CN': '

    Steps 组件的最简用法。

    ', + 'en-US': '', + }, + apis: [ + 'TiStepsComponent.properties.steps', + 'TiStepsComponent.properties.activeStep', + 'TiStepsComponent.properties.disabled', + ], + }, + { + demoId: 'steps-clickable', + name: { + 'zh-CN': '是否可点击跳转', + 'en-US': 'clickable', + }, + desc: { + 'zh-CN': + '

    通过属性clickable配置步骤是否可被点击跳转。

    ', + 'en-US': '', + }, + apis: ['TiStepsComponent.properties.clickable'], + }, + { + demoId: 'steps-label', + name: { + 'zh-CN': '显示文本', + 'en-US': 'labelkey', + }, + desc: { + 'zh-CN': '

    通过属性labelKey配置显示文本的键值。

    ', + 'en-US': '', + }, + apis: ['TiStepsComponent.properties.labelKey'], + }, + { + demoId: 'steps-maxwidth', + name: { + 'zh-CN': '文本最大宽度', + 'en-US': 'maxwidth', + }, + desc: { + 'zh-CN': '

    通过属性maxWidth配置文本最大宽度。

    ', + 'en-US': '', + }, + apis: ['TiStepsComponent.properties.maxWidth'], + }, + { + demoId: 'steps-adaptive', + name: { + 'zh-CN': '宽度自适应', + 'en-US': 'adaptive', + }, + desc: { + 'zh-CN': '

    通过属性adaptive配置宽度自适应。

    ', + 'en-US': '', + }, + apis: ['TiStepsComponent.properties.adaptive'], + }, + { + demoId: 'steps-before', + name: { + 'zh-CN': '必选项', + 'en-US': 'require', + }, + desc: { + 'zh-CN': + '

    通过属性require配置某一步骤为必选项。当点击必选步骤的时候触发beforeStep事件。

    ', + 'en-US': '', + }, + apis: ['TiStepsComponent.events.beforeStep'], + }, + { + demoId: 'steps-events', + name: { + 'zh-CN': '事件', + 'en-US': 'events', + }, + desc: { + 'zh-CN': + '

    当步骤改变的时候触发activeStepChange事件。

    ', + 'en-US': '', + }, + apis: ['TiStepsComponent.events.activeStepChange'], + }, + { + demoId: 'steps-template', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'custom template', + }, + desc: { + 'zh-CN': '

    通过#icon配置图标区域的模板,通过#step配置文本区域的模板。

    ', + 'en-US': '', + }, + }, + ], +}; diff --git a/src/steps/demo/src/app/steps/webdoc/steps.cn.md b/src/steps/demo/src/app/steps/webdoc/steps.cn.md new file mode 100644 index 0000000..19a6ef5 --- /dev/null +++ b/src/steps/demo/src/app/steps/webdoc/steps.cn.md @@ -0,0 +1,23 @@ +--- +title: Steps 步骤导航 +--- +# Steps 步骤导航 + +
    + +Steps 是引导用户按照流程完成任务的导航条。   + +```typescript +import { TiStepsModule } from '@opentiny/ng'; +``` + +
    + +
    + +Steps 是引导用户按照流程完成任务的导航条。   + +```typescript +import { TiStepsModule } from '@opentiny/ng'; +``` +
    diff --git a/src/steps/demo/src/app/steps/webdoc/steps.en.md b/src/steps/demo/src/app/steps/webdoc/steps.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/steps/demo/src/app/steps/webdoc/steps.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/steps/demo/src/favicon.ico b/src/steps/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/steps/demo/src/index.html b/src/steps/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/steps/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/steps/demo/src/main.ts b/src/steps/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/steps/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/steps/demo/test.ts b/src/steps/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/steps/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/steps/demo/tsconfig.app.json b/src/steps/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/steps/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/steps/demo/tsconfig.spec.json b/src/steps/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/steps/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/steps/lib/index.ts b/src/steps/lib/index.ts new file mode 100644 index 0000000..5b924e3 --- /dev/null +++ b/src/steps/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiStepsModule'; diff --git a/src/steps/lib/ng-package.json b/src/steps/lib/ng-package.json new file mode 100644 index 0000000..ad914a3 --- /dev/null +++ b/src/steps/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/steps", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/steps/lib/package.json b/src/steps/lib/package.json new file mode 100644 index 0000000..ded6800 --- /dev/null +++ b/src/steps/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-steps", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/steps/lib/project.json b/src/steps/lib/project.json new file mode 100644 index 0000000..065c173 --- /dev/null +++ b/src/steps/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/steps/lib", + "sourceRoot": "src/steps/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/steps"], + "options": { + "project": "src/steps/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/steps"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js steps" + }, + { + "command": "ng default-build steps" + }, + { + "command": "node build/clear-default-theme.js steps" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/steps && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build steps && ng pack steps && node build/publish.js steps --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/steps/lib/src/TiStepsComponent.ts b/src/steps/lib/src/TiStepsComponent.ts new file mode 100644 index 0000000..6cceb02 --- /dev/null +++ b/src/steps/lib/src/TiStepsComponent.ts @@ -0,0 +1,283 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + Input, + IterableDiffers, + NgZone, + Output, + QueryList, + Renderer2, + TemplateRef, + ViewChild, + ViewChildren +} from '@angular/core'; +import { Util } from '@opentiny/ng-utils'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +/** + * 步骤项 + */ +export interface TiStepItem { + /** + * 默认标题,通过配置 labelKey 可以指定其他属性作为标题 + */ + label?: string; + /** + * 是否禁用 + */ + disabled?: boolean; + /** + * 是否错误 + */ + error?: boolean; + /** + * 配置其他自定义属性 + */ + [key: string]: any; +} + +/** + * steps步骤组件 + * + * 点击可跳转和不可跳转两种方式(默认点击不跳转) + * + */ +@Component({ + selector: 'ti-steps', + templateUrl: './steps.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./steps.less'] +}) +export class TiStepsComponent extends TiBaseComponent { + /** + * 必选,步骤导航数据项。 + */ + @Input() steps: Array; + /** + * 是否支持点击跳转功能,当为 false 时,视觉呈现和禁用一致 + */ + @Input() clickable: boolean = false; + /** + * 步骤标题文本最大宽度 + */ + @Input() maxWidth: string; + /** + * 步骤标题要显示的字段。 和 select 保持一致 + */ + @Input() labelKey: string = 'label'; + /** + * 必选,当前激活步骤项,10.1.19 版本支持传入激活项的下标 + */ + @Input() activeStep: any; + /** + * 宽度是否自动撑满父容器。 + * + * 设置为 true 整体宽度会撑满父容器,常用在弹窗场景 + */ + @Input() adaptive: boolean = false; + /** + * 步骤激活项改变时触发的回调 + */ + @Output() readonly activeStepChange: EventEmitter = new EventEmitter(); + /** + * 步骤激活项改变前触发的回调 + */ + @Output() readonly beforeStep: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + @ViewChild('stepRef') stepsRef: ElementRef; + /** + * @ignore + */ + @ViewChildren('line') lineRef: QueryList; + /** + * @ignore + */ + @ViewChildren('explain') explainRef: QueryList; + /** + * @ignore + * 获取到用户自定义的模板 + */ + @ContentChild(TemplateRef, { static: true }) itemTemplate: TemplateRef; + /** + * 文本区域的模板 + */ + @ContentChild('step', { static: true }) stepTemplate: TemplateRef; + /** + * icon 区域的模板 + */ + @ContentChild('icon', { static: true }) iconTemplate: TemplateRef; + protected versionInfo: string = super.getVersion(packageInfo); + private labelChange: boolean; // 标志文本长度是否变化,从而重新计算线长 + private stepsDiffer: any; + private isInitLabelChange: boolean = true; + private explainTotalWidth: number = 0; + private windowResizeListener: () => void; + constructor( + protected elementRef: ElementRef, + protected renderer2: Renderer2, + private iterableDiffers: IterableDiffers, + private zone: NgZone, + private changeDetectorRef: ChangeDetectorRef + ) { + super(elementRef, renderer2); + } + + ngOnInit(): void { + super.ngOnInit(); + if (!this.adaptive) { + return; + } + // this.trackByLabelFn监听对象:可以深度监听 + this.stepsDiffer = this.iterableDiffers.find(this.steps).create(this.trackByLabelFn); + // 修复错误:ERROR ReferenceError: window is not defined + if (typeof window === 'undefined') { + return; + } + this.zone.runOutsideAngular(() => { + this.windowResizeListener = this.renderer2.listen(window, 'resize', this.setLineWidth); + }); + } + + ngDoCheck(): void { + if (this.adaptive) { + // 处理增删步骤 + const stepsDiffer: any = this.stepsDiffer.diff(this.steps); + if (stepsDiffer) { + this.labelChange = true; + } + } + + // 增删步骤,改变属性时指引未发生变化,onpush模式下不会触发变更,故手动触发 + this.changeDetectorRef.markForCheck(); + } + + ngAfterContentInit(): void { + super.ngAfterContentInit(); + /** + * 兼容旧版无命名模板: + * 10.1.16 版本到 10.1.22 之前只能内嵌一个模板,无命名。 + * 10.1.22 版本之后可以内嵌两个模板,#step #icon + */ + if ( + !this.stepTemplate && + this.itemTemplate && + this.itemTemplate.elementRef.nativeElement !== (this.iconTemplate && this.iconTemplate.elementRef.nativeElement) + ) { + this.stepTemplate = this.itemTemplate; + } + } + + ngAfterViewChecked(): void { + // 需要判断当label变化时才计算线长 + if (this.adaptive && this.labelChange) { + this.labelChange = false; + this.setLineWidth(); + // 初始时在前几次的 ngAfterViewChecked 中计算文本宽度,生产环境清缓存场景下计算有点不准确(时机有点早),但在初始时的后几次ngAfterViewChecked中计算的文本宽度是准确的。 + // TODO: 是否有更合适的时机计算(已验证使用MutationObserver也解决不了,在ngAfterViewChecked中加setTimeout(0)也解决不了) + if (this.isInitLabelChange) { + setTimeout(() => { + const explainTotalWidth: number = this.getExplainTotalWidth(); + if (explainTotalWidth !== this.explainTotalWidth) { + this.setLineWidth(explainTotalWidth); + } + }, 50); // 50ms是经验值 + this.isInitLabelChange = false; + } + } + } + + ngOnDestroy(): void { + if (this.windowResizeListener) { + this.windowResizeListener(); + } + } + + /** + * @ignore + * 每步的点击事件处理 + */ + public onClick(index: number): void { + const step: any = this.steps[index]; + // 不支持点击跳转、当前项灰化或者点击当前激活想项时,不处理 + if (!this.clickable || step.disabled || this.activeStep === step || this.activeStep === index) { + return; + } + + // 未定义beforeStep事件,直接跳转,定义beforeStep事件将当前点击项索引传出 + if (this.beforeStep.observers.length === 0) { + this.activeStep = Util.isNumber(this.activeStep) ? index : step; + this.activeStepChange.emit(this.activeStep); + } else { + this.beforeStep.emit(step); + } + } + + // 设置每个步骤之间的横线的宽度 + public setLineWidth = (width?: number): void => { + this.explainTotalWidth = Util.isUndefined(width) ? this.getExplainTotalWidth() : width; + this.lineRef.forEach((stepLine: ElementRef) => { + this.renderer2.setStyle(stepLine.nativeElement, 'width', `calc((100% - ${this.explainTotalWidth}px) / ${this.steps.length - 1})`); + }); + }; + + private getExplainTotalWidth(): number { + let explainTotalWidth: number = 0; + if (!Util.isUndefined(this.explainRef)) { + this.explainRef.forEach((step: ElementRef) => { + // 计算每步选择框和文字的总长度 + explainTotalWidth += step.nativeElement.offsetWidth; + }); + } + + return explainTotalWidth; + } + + /** + * @ignore + * Diff监听steps中label值的改变 + */ + public trackByLabelFn(index: number, item: any): string { + return item.label; + } + + /** + * @ignore + * ngFor 使用 + */ + public trackByIndexFn(index: number): number { + return index; + } + + /** + * @ignore + * 判断当前项的状态 + */ + public getStepState(index: number): string { + const activedIndex: number = Util.isNumber(this.activeStep) ? this.activeStep : this.steps.indexOf(this.activeStep); + if (index < activedIndex) { + return this.steps[index].error ? 'error' : 'complete'; + } else if (index === activedIndex) { + return this.steps[index].error ? 'error' : 'active'; + } else { + return 'uncomplete'; + } + } +} diff --git a/src/steps/lib/src/TiStepsModule.ts b/src/steps/lib/src/TiStepsModule.ts new file mode 100644 index 0000000..812a9ec --- /dev/null +++ b/src/steps/lib/src/TiStepsModule.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiStepsComponent } from './TiStepsComponent'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; + +@NgModule({ + imports: [TiOverflowModule, TiIconModule, CommonModule, TiOutlineModule], + exports: [TiStepsComponent], + declarations: [TiStepsComponent] +}) +export class TiStepsModule {} +export { TiStepsComponent, TiStepItem } from './TiStepsComponent'; diff --git a/src/steps/lib/src/steps.html b/src/steps/lib/src/steps.html new file mode 100644 index 0000000..66ee1fb --- /dev/null +++ b/src/steps/lib/src/steps.html @@ -0,0 +1,51 @@ +
      + +
    • + +
    • +
      +
      + +
      + +
      +
      + + + {{i + 1}} +
      +
      +
      +
      + + + +
      + + + +
      +
    • +
      +
    diff --git a/src/steps/lib/src/steps.less b/src/steps/lib/src/steps.less new file mode 100644 index 0000000..e80b9d5 --- /dev/null +++ b/src/steps/lib/src/steps.less @@ -0,0 +1,227 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-steps-transition-time: 300ms; + --ti-steps-wire-height: 1px; + --ti-steps-max-width: 150px; + --ti-steps-box-size: var(--ti-common-size-5x); +} + +:host { + display: block; +} + +.ti3-steps { + padding: var(--ti-common-space-3x) 0px; + white-space: nowrap; + & .ti3-steps-explain { + vertical-align: middle; + display: inline-block; + .ti3-steps-text-container { + display: flex; + align-items: center; + } + .ti3-steps-icon { + font-size: var(--ti-common-font-size-4); + margin-left: var(--ti-common-space-2x); + } + + .ti3-steps-box { + display: flex; + align-items: center; + justify-content: center; + width: var(--ti-steps-box-size); + height: var(--ti-steps-box-size); + .box-sizing(border-box); + font-size: var(--ti-common-font-size-1); + border: var(--ti-common-border-weight-normal) solid; + margin-left: var(--ti-common-space-2x); + position: relative; + .border-radius(var(--ti-common-border-radius-3)); + &:before { + content: ''; + display: inline-block; + width: var(--ti-steps-box-size); + height: var(--ti-steps-box-size); + .border-radius(var(--ti-common-border-radius-3)); + background-color: var(--ti-common-color-bg-emphasize); + position: absolute; + opacity: 0; + } + & .ti3-steps-box-number { + position: absolute; + width: calc(var(--ti-steps-box-size) * 7 / 10); + height: calc(var(--ti-steps-box-size) * 7 / 10); + line-height: calc(var(--ti-steps-box-size) * 7 / 10); + text-align: center; + ti-icon { + line-height: unset; + } + } + } + + .ti3-steps-text { + display: inline-block; + line-height: var(--ti-steps-box-size); + margin: 0 var(--ti-common-space-2x) 0 var(--ti-common-space-base); + max-width: var(--ti-steps-max-width); + vertical-align: top; + word-break: break-word; + white-space: normal; + } + } + & li { + &:first-child.ti3-steps-explain .ti3-steps-box { + margin-left: 0px; + } + + &:last-child.ti3-steps-explain .ti3-steps-text { + margin-right: 0px; + } + } + + & .ti3-steps-explain-clickable { + cursor: pointer; + } + & .ti3-steps-line { + width: var(--ti-common-size-15x); + height: var(--ti-steps-wire-height); + min-width: var(--ti-common-size-5x); + vertical-align: middle; + display: inline-block; + line-height: 0; // 当父节点有line-height属性并大于3时,子节点继承该属性,此时两条线会错位 + background-color: var(--ti-common-color-line-normal); + font-size: 0; + &:before { + content: ' '; + display: inline-block; + width: 0%; + height: 100%; + background-color: var(--ti-common-color-line-active); + } + } +} + +// 未完成效果 +.ti3-steps .ti3-steps-uncomplete { + &.ti3-steps-line::before { + .steps-line-animation(0%); + } + &.ti3-steps-explain { + .ti3-steps-box { + .steps-box(var(--ti-common-color-text-primary), var(--ti-common-color-text-primary), 0); + } + & .ti3-steps-text { + .steps-text-animation(var(--ti-common-color-text-secondary), 0%); + &:hover { + .steps-text-animation(var(--ti-common-color-prompt), 100%); + } + } + .ti3-steps-icon { + color: var(--ti-common-color-text-secondary); + } + } +} + +// 完成状态 +.ti3-steps .ti3-steps-complete { + &.ti3-steps-line::before { + .steps-line-animation(100%); + } + &.ti3-steps-explain { + .ti3-steps-box { + .steps-box(var(--ti-common-color-icon-active), var(--ti-common-color-icon-active), 0); + } + & .ti3-steps-text { + .steps-text-animation(var(--ti-common-color-text-primary), 0%); + &:hover { + .steps-text-animation(var(--ti-common-color-prompt), 100%); + } + } + .ti3-steps-icon { + color: var(--ti-common-color-text-highlight); + } + } +} + +// 选中状态 +.ti3-steps .ti3-steps-active { + &.ti3-steps-line::before { + .steps-line-animation(100%); + } + &.ti3-steps-explain { + .ti3-steps-box { + .steps-box(var(--ti-common-color-icon-active), var(--ti-common-color-text-white), 1); + } + & .ti3-steps-text { + .steps-text-animation(var(--ti-common-color-text-highlight), 100%); + } + .ti3-steps-icon { + color: var(--ti-common-color-text-highlight); + } + } +} + +// 灰化状态 +.ti3-steps .ti3-steps-disabled { + &.ti3-steps-explain { + .ti3-steps-box { + border-color: var(--ti-common-color-text-disabled); + color: var(--ti-common-color-text-disabled); + &:before { + display: none !important; + } + } + & .ti3-steps-text { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + } + .ti3-steps-icon { + color: var(--ti-common-color-text-disabled); + } + } +} + +// 错误状态 +.ti3-steps .ti3-steps-error { + &.ti3-steps-line::before { + .steps-line-animation(100%); + } + &.ti3-steps-explain { + .ti3-steps-box { + .steps-box(var(--ti-common-color-error), var(--ti-common-color-error), 0); + } + & .ti3-steps-text { + .steps-text-animation(var(--ti-common-color-error), 0%); + } + .ti3-steps-icon { + color: var(--ti-common-color-error); + } + } +} + +.steps-box(@border-color, @color, @number) { + border-color: @border-color; + color: @color; + &:before { + transform: scale(@number); + opacity: @number; + transition: transform var(--ti-steps-transition-time), opacity var(--ti-steps-transition-time); + } +} + +// 不支持IE +.steps-text-animation(@color, @background-size-width) { + color: @color; + background-size: @background-size-width 100%; + background-repeat: no-repeat; + -webkit-background-clip: text; + background-image: linear-gradient(var(--ti-common-color-text-highlight) 0%, var(--ti-common-color-text-highlight) 100%); + background-image: -ms-linear-gradient(top, transparent, transparent); + transition: background-size var(--ti-steps-transition-time), color var(--ti-steps-transition-time); +} + +.steps-line-animation(@width) { + width: @width; + transition: width var(--ti-steps-transition-time); +} diff --git a/src/subtitle/demo/karma.conf.js b/src/subtitle/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/subtitle/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/subtitle/demo/project.json b/src/subtitle/demo/project.json new file mode 100644 index 0000000..4526b4b --- /dev/null +++ b/src/subtitle/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/subtitle/demo", + "sourceRoot": "src/subtitle/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/subtitle", + "index": "src/subtitle/demo/src/index.html", + "main": "src/subtitle/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/subtitle/demo/tsconfig.app.json", + "assets": ["src/subtitle/demo/src/favicon.ico", "src/subtitle/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "subtitle-demo:build:production" + }, + "development": { + "browserTarget": "subtitle-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js subtitle" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/subtitle/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/subtitle/demo/tsconfig.spec.json", + "karmaConfig": "src/subtitle/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/subtitle/demo/src/app/AppComponent.ts b/src/subtitle/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/subtitle/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/subtitle/demo/src/app/AppModule.ts b/src/subtitle/demo/src/app/AppModule.ts new file mode 100644 index 0000000..99b43bf --- /dev/null +++ b/src/subtitle/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SubtitleTestModule } from './subtitle/SubtitleTestModule'; + +@NgModule({ + imports: [ + SubtitleTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/subtitle/demo/src/app/IndexComponent.ts b/src/subtitle/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..f5aac77 --- /dev/null +++ b/src/subtitle/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SubtitleTestModule } from './subtitle/SubtitleTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SubtitleTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/subtitle/demo/src/app/app.html b/src/subtitle/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/subtitle/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleBasicComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleBasicComponent.ts new file mode 100644 index 0000000..c0f878e --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleBasicComponent.ts @@ -0,0 +1,34 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-basic.html' +}) +export class SubtitleBasicComponent { + items1: Array = [ + { + id: '1', + label: 'item1 test 测试长标题 测试长标题 测试长标题' + } + ]; + + items: Array = [ + { + id: '1', + label: 'item test 测试长标题 测试长标题 测试长标题' + }, + { + id: '2', + label: '弹性云服务器' + }, + { + id: '3', + label: '3' + } + ]; + + clickFn(): void { + this.items[0].label = '变更后的标题'; + } +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleBeforeSearchComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleBeforeSearchComponent.ts new file mode 100644 index 0000000..f5bc131 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleBeforeSearchComponent.ts @@ -0,0 +1,85 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiSelectComponent, TiSubtitleItem, TiSubtitleListScrollLoad } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-before-search.html' +}) +export class SubtitleBeforeSearchComponent { + private size: number = 50; + private data: Array = [ + { label: '购买弹性云服务器购买弹性云服务器购买弹性云服务器' }, + { label: '美国' }, + { label: '巴西' }, + { label: '加拿大' }, + { label: '中国' }, + { label: '法国' }, + { label: '德国' }, + { label: '韩国' }, + { label: '土耳其' }, + { label: '大不列颠和北爱兰联合王国' } + ]; + private database: Array; + totalNumber: number; + items: Array = []; + selected: TiSubtitleItem = { label: '69' + this.data[9].label }; + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + ngOnInit(): void { + this.getData(0, this.size).then((result: any) => { + this.items = result.data; + this.totalNumber = result.totalNumber; + }); + } + + beforeSearch(selectComp: TiSelectComponent): void { + // 获取搜索内容 + const searchWord: string = selectComp.getSearchWord(); + this.getData(0, this.size, searchWord).then((result: any) => { + this.items = result.data; + this.totalNumber = result.totalNumber; + }); + } + + loadMore(scrollLoadInfo: TiSubtitleListScrollLoad): void { + const currentOptions: Array = scrollLoadInfo.selectInstance.getSearchResult(); + if (currentOptions.length >= this.totalNumber) { + return; + } + // 获取搜索内容 + const searchWord: string = scrollLoadInfo.selectInstance.getSearchWord(); + scrollLoadInfo.loading = true; + this.getData(currentOptions.length, this.size, searchWord).then((result: any) => { + this.items = [...currentOptions, ...result.data]; + scrollLoadInfo.loading = false; + }); + } + + // 模拟异步请求 + private getData(startIndex: number, size: number, searchWord?: string): Promise { + this.database = []; + for (let i: number = 0; i < 10000; i++) { + const item: any = this.data[i % 10]; + this.database.push({ ...item, label: i + item.label }); + } + + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + if (searchWord) { + const filteredResult: Array = this.database.filter((item: any) => item.label.includes(searchWord)); + const slicedResult: Array = filteredResult.slice(startIndex, startIndex + size); + const totalNumber: number = filteredResult.length; + resolve({ data: slicedResult, totalNumber }); + } else { + resolve({ + data: this.database.slice(startIndex, startIndex + size), + totalNumber: this.database.length + }); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef.markForCheck() + // 默认模式下不需要 + // 服务可根据自身变化检测策略决定是否使用该方法 + this.changeDetectorRef.markForCheck(); + }, 1000); + }); + } +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleDarkComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleDarkComponent.ts new file mode 100644 index 0000000..1a0af36 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleDarkComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-dark.html' +}) +export class SubtitleDarkComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: '弹性云服务器' + }, + { + id: '3', + label: '云服务器' + } + ]; + + toggleItems(): void { + this.items.length = 1; + } +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleEventComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleEventComponent.ts new file mode 100644 index 0000000..1d9e649 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleEventComponent.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-event.html' +}) +export class SubtitleEventComponent { + myLogs: Array = []; + + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: 'ECS-name-1' + }, + { + id: '3', + label: '云服务器' + } + ]; + + selected: TiSubtitleItem = this.items[1]; + + onBack($event: Event): void { + $event.preventDefault(); + this.myLogs = [...this.myLogs, `onBack() event`]; + } + onChange(item: TiSubtitleItem): void { + this.myLogs = [...this.myLogs, `selected change event: ${JSON.stringify(item)}`]; + } +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleIdkeyComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleIdkeyComponent.ts new file mode 100644 index 0000000..2a13942 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleIdkeyComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-idkey.html' +}) +export class SubtitleIdkeyComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: '弹性云服务器' + }, + { + id: '3', + label: '云服务器' + }, + { + id: '4', + label: '弹性云服务器' + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleItemsComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleItemsComponent.ts new file mode 100644 index 0000000..2b99210 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleItemsComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-items.html' +}) +export class SubtitleItemsComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + } + ]; + items1: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: 'ECS-name-1' + }, + { + id: '3', + label: '云服务器' + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleMaxwidthComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleMaxwidthComponent.ts new file mode 100644 index 0000000..d1c3392 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleMaxwidthComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-maxwidth.html' +}) +export class SubtitleMaxwidthComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + } + ]; + items1: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: 'ECS-name-1' + }, + { + id: '3', + label: '云服务器' + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitlePanelwidthComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitlePanelwidthComponent.ts new file mode 100644 index 0000000..00d15c6 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitlePanelwidthComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-panelwidth.html' +}) +export class SubtitlePanelwidthComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: 'ECS-name-1' + }, + { + id: '3', + label: '云服务器' + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleRouteComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleRouteComponent.ts new file mode 100644 index 0000000..a8abdd3 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleRouteComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-route.html' +}) +export class SubtitleRouteComponent { + public items1: Array = [ + { + id: '1', + label: '弹性云服务器' + } + ]; + + routerLink = ['./../subtitle-event']; + // 参数传递 + queryParams = { name: 'route' }; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleScrollLoadComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleScrollLoadComponent.ts new file mode 100644 index 0000000..d5615e8 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleScrollLoadComponent.ts @@ -0,0 +1,74 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiSubtitleItem, TiSubtitleListScrollLoad } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-scroll-load.html' +}) +export class SubtitleScrollLoadComponent { + private size: number = 20; + private data: Array = [ + { label: '购买弹性云服务器购买弹性云服务器购买弹性云服务器' }, + { label: '美国' }, + { label: '巴西' }, + { label: '加拿大' }, + { label: '中国' }, + { label: '法国' }, + { label: '德国' }, + { label: '韩国' }, + { label: '土耳其' }, + { label: '大不列颠和北爱兰联合王国' } + ]; + private database: Array; + totalNumber: number; + items: Array = []; + selected: TiSubtitleItem = { label: '0' + this.data[0].label }; + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + ngOnInit(): void { + this.getData(0, this.size).then((result: any) => { + this.items = result.data; + this.totalNumber = result.totalNumber; + }); + } + + loadMore(scrollLoadInfo: TiSubtitleListScrollLoad): void { + if (this.items.length >= this.totalNumber) { + return; + } + + scrollLoadInfo.loading = true; + this.getData(this.items.length, this.size).then((result: any) => { + this.items = [...this.items, ...result.data]; + scrollLoadInfo.loading = false; + }); + } + + // 模拟异步请求 + private getData(startIndex: number, size: number, searchWord?: string): Promise { + this.database = []; + for (let i: number = 0; i < 10000; i++) { + const item: any = this.data[i % 10]; + this.database.push({ ...item, label: i + item.label }); + } + + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + if (searchWord) { + const filteredResult: Array = this.database.filter((item: any) => item.label.includes(searchWord)); + const slicedResult: Array = filteredResult.slice(startIndex, startIndex + size); + const totalNumber: number = filteredResult.length; + resolve({ data: slicedResult, totalNumber }); + } else { + resolve({ + data: this.database.slice(startIndex, startIndex + size), + totalNumber: this.database.length + }); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef.markForCheck() + // 默认模式下不需要 + // 服务可根据自身变化检测策略决定是否使用该方法 + this.changeDetectorRef.markForCheck(); + }, 1000); + }); + } +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleSearchableComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleSearchableComponent.ts new file mode 100644 index 0000000..b44a91e --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleSearchableComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-searchable.html' +}) +export class SubtitleSearchableComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: 'ECS-name-1' + }, + { + id: '3', + label: '云服务器' + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleTargetComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleTargetComponent.ts new file mode 100644 index 0000000..c327a96 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleTargetComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-target.html' +}) +export class SubtitleTargetComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器' + } + ]; + items1: Array = [ + { + id: '1', + label: '购买弹性云服务器' + }, + { + id: '2', + label: 'ECS-name-1' + }, + { + id: '3', + label: '云服务器' + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleTestModule.ts b/src/subtitle/demo/src/app/subtitle/SubtitleTestModule.ts new file mode 100644 index 0000000..e666272 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleTestModule.ts @@ -0,0 +1,85 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiSubtitleModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { SubtitleBasicComponent } from './SubtitleBasicComponent'; +import { SubtitleDarkComponent } from './SubtitleDarkComponent'; +import { SubtitleEventComponent } from './SubtitleEventComponent'; +import { SubtitleRouteComponent } from './SubtitleRouteComponent'; +import { SubtitleScrollLoadComponent } from './SubtitleScrollLoadComponent'; +import { SubtitleItemsComponent } from './SubtitleItemsComponent'; +import { SubtitleIdkeyComponent } from './SubtitleIdkeyComponent'; +import { SubtitleTargetComponent } from './SubtitleTargetComponent'; +import { SubtitleMaxwidthComponent } from './SubtitleMaxwidthComponent'; +import { SubtitlePanelwidthComponent } from './SubtitlePanelwidthComponent'; +import { SubtitleSearchableComponent } from './SubtitleSearchableComponent'; +import { SubtitleTipPositionComponent } from './SubtitleTipPositionComponent'; +import { SubtitleBeforeSearchComponent } from './SubtitleBeforeSearchComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiButtonModule, TiSubtitleModule, DemoLogModule, RouterModule.forChild(SubtitleTestModule.ROUTES)], + declarations: [ + SubtitleBasicComponent, + SubtitleDarkComponent, + SubtitleEventComponent, + SubtitleRouteComponent, + SubtitleScrollLoadComponent, + SubtitleItemsComponent, + SubtitleIdkeyComponent, + SubtitleTargetComponent, + SubtitleMaxwidthComponent, + SubtitlePanelwidthComponent, + SubtitleSearchableComponent, + SubtitleTipPositionComponent, + SubtitleBeforeSearchComponent + ] +}) +export class SubtitleTestModule { + static readonly LINKS: Array = [{ href: 'components/TiSubtitleComponent.html', label: 'Subtitle' }]; + public static readonly ROUTES: Routes = [ + { path: 'subtitle/subtitle-basic', component: SubtitleBasicComponent }, + { path: 'subtitle/subtitle-event', component: SubtitleEventComponent }, + { + path: 'subtitle/subtitle-dark', + component: SubtitleDarkComponent + }, + { + path: 'subtitle/subtitle-idkey', + component: SubtitleIdkeyComponent + }, + { + path: 'subtitle/subtitle-route', + component: SubtitleRouteComponent + }, + { + path: 'subtitle/subtitle-scroll-load', + component: SubtitleScrollLoadComponent + }, + { path: 'subtitle/subtitle-target', component: SubtitleTargetComponent }, + { path: 'subtitle/subtitle-items', component: SubtitleItemsComponent }, + { + path: 'subtitle/subtitle-maxwidth', + component: SubtitleMaxwidthComponent + }, + { + path: 'subtitle/subtitle-panelwidth', + component: SubtitlePanelwidthComponent + }, + { + path: 'subtitle/subtitle-searchable', + component: SubtitleSearchableComponent + }, + { + path: 'subtitle/subtitle-tip-position', + component: SubtitleTipPositionComponent + }, + { + path: 'subtitle/subtitle-before-search', + component: SubtitleBeforeSearchComponent + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/SubtitleTipPositionComponent.ts b/src/subtitle/demo/src/app/subtitle/SubtitleTipPositionComponent.ts new file mode 100644 index 0000000..17c2568 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/SubtitleTipPositionComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; + +import { TiSubtitleItem } from '@opentiny/ng'; + +@Component({ + templateUrl: './subtitle-tip-position.html' +}) +export class SubtitleTipPositionComponent { + items: Array = [ + { + id: '1', + label: '购买弹性云服务器购买弹性云服务器购买弹性云服务器' + } + ]; + items1: Array = [ + { + id: '1', + label: '购买弹性云服务器购买弹性云服务器购买弹性云服务器' + }, + { + id: '2', + label: 'ECS-name-1' + }, + { + id: '3', + label: '云服务器' + } + ]; +} diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-basic.html b/src/subtitle/demo/src/app/subtitle/subtitle-basic.html new file mode 100644 index 0000000..a219f03 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-basic.html @@ -0,0 +1,40 @@ +

    1 描述

    +

    基本展示,可配置为文本类型和下拉框类型

    +

    导入

    +

    import {{ '{' }} TiSubtitleModule {{ '}' }} from '@opentiny/ng';

    +

    2 示例

    +
    +

    2.1 基础使用(文本类型的宽度可配置maxWidth接口,设置组件最大宽度自适应显示)

    +

    items 只有1项

    +

    2.1.1未配置maxWidth,默认为250px

    + +
    +

    2.1.2配置maxWidth为300px

    + +
    +
    +

    2.2 可以选择

    +

    items 有多项

    + +
    +
    +

    2.3 searchable 接口

    + +
    +
    +

    2.4 panelWidth 接口(设置下拉面板宽度)

    + +
    +
    +

    2.5 tipPosition 接口(设置标题溢出提示的方向)

    +
    +

    2.5.1 items仅一项

    + +
    +

    2.5.2 items有多项

    + +
    +

    2.6 配置maxWidth为300px

    + + + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-before-search.html b/src/subtitle/demo/src/app/subtitle/subtitle-before-search.html new file mode 100644 index 0000000..620caa7 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-before-search.html @@ -0,0 +1,8 @@ + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-dark.html b/src/subtitle/demo/src/app/subtitle/subtitle-dark.html new file mode 100644 index 0000000..9e73762 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-dark.html @@ -0,0 +1,10 @@ +
    + +
    + + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-event.html b/src/subtitle/demo/src/app/subtitle/subtitle-event.html new file mode 100644 index 0000000..7817e93 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-event.html @@ -0,0 +1,3 @@ + + + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-idkey.html b/src/subtitle/demo/src/app/subtitle/subtitle-idkey.html new file mode 100644 index 0000000..1c16ef6 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-idkey.html @@ -0,0 +1 @@ + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-items.html b/src/subtitle/demo/src/app/subtitle/subtitle-items.html new file mode 100644 index 0000000..b33e1c5 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-items.html @@ -0,0 +1,3 @@ + +
    + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-maxwidth.html b/src/subtitle/demo/src/app/subtitle/subtitle-maxwidth.html new file mode 100644 index 0000000..ad0528f --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-maxwidth.html @@ -0,0 +1,3 @@ + +
    + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-panelwidth.html b/src/subtitle/demo/src/app/subtitle/subtitle-panelwidth.html new file mode 100644 index 0000000..cb396eb --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-panelwidth.html @@ -0,0 +1 @@ + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-route.html b/src/subtitle/demo/src/app/subtitle/subtitle-route.html new file mode 100644 index 0000000..351421f --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-route.html @@ -0,0 +1,14 @@ +

    1 描述

    +

    backRouterLink:设置图标点击后跳转路由,用于应用内跳转。如果配置了该参数,href和targe参数失效。

    +

    queryParams: 设置跳转路由参数。

    +

    10.1.2版本接口routerLink变更为backRouterLink

    + +

    2 示例

    +
    +

    2.1 基础使用

    +

    + +
    +
    +

    2.2 设置routerLink后href是否有效

    + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-scroll-load.html b/src/subtitle/demo/src/app/subtitle/subtitle-scroll-load.html new file mode 100644 index 0000000..fd1ec57 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-scroll-load.html @@ -0,0 +1 @@ + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-searchable.html b/src/subtitle/demo/src/app/subtitle/subtitle-searchable.html new file mode 100644 index 0000000..065c920 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-searchable.html @@ -0,0 +1 @@ + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-target.html b/src/subtitle/demo/src/app/subtitle/subtitle-target.html new file mode 100644 index 0000000..6f17f64 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-target.html @@ -0,0 +1,3 @@ + +
    + diff --git a/src/subtitle/demo/src/app/subtitle/subtitle-tip-position.html b/src/subtitle/demo/src/app/subtitle/subtitle-tip-position.html new file mode 100644 index 0000000..34d63fc --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/subtitle-tip-position.html @@ -0,0 +1,3 @@ + +
    + diff --git a/src/subtitle/demo/src/app/subtitle/webdoc/subtitle-demos.js b/src/subtitle/demo/src/app/subtitle/webdoc/subtitle-demos.js new file mode 100644 index 0000000..64e1cf0 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/webdoc/subtitle-demos.js @@ -0,0 +1,147 @@ +export default { + column: '2', + demos: [ + { + demoId: 'subtitle-items', + name: { + 'zh-CN': '基本使用', + 'en-US': 'base', + }, + desc: { + 'zh-CN': '

    标题数据集,如果只有一个选项则标题以文本形式呈现,如果选项大于1个,标题以下拉的形式呈现。

    ', + 'en-US': '', + }, + apis: [ + 'TiSubtitleComponent.properties.href', + 'TiSubtitleComponent.properties.items' + ] + }, + { + demoId: 'subtitle-target', + name: { + 'zh-CN': '跳转路径', + 'en-US': 'dark', + }, + desc: { + 'zh-CN': '

    通过属性target配置在何处打开链接。

    ', + 'en-US': '', + }, + apis: [ + 'TiSubtitleComponent.properties.target' + ] + }, + { + demoId: 'subtitle-tip-position', + name: { + 'zh-CN': 'tip方向', + 'en-US': 'dark', + }, + desc: { + 'zh-CN': '

    通过属性tipPosition配置标题溢出提示的方向。

    ', + 'en-US': '', + }, + apis: [ + 'TiSubtitleComponent.properties.tipPosition' + ] + }, + { + demoId: 'subtitle-dark', + name: { + 'zh-CN': '暗色背景', + 'en-US': 'dark', + }, + desc: { + 'zh-CN': '

    通过属性dark配置组件在暗色场景呈现。

    ', + 'en-US': '', + } + }, + { + demoId: 'subtitle-maxwidth', + name: { + 'zh-CN': '最大宽度', + 'en-US': 'maxWidth', + }, + desc: { + 'zh-CN': '

    通过属性maxWidth配置组件最大宽度。

    ', + 'en-US': '', + }, + apis: ['TiSubtitleComponent.properties.maxWidth'] + }, + { + demoId: 'subtitle-panelwidth', + name: { + 'zh-CN': '面板最大宽度', + 'en-US': 'panelwidth' + }, + desc: { + 'zh-CN': '

    通过属性panelWidth配置下拉面板宽度。

    ', + 'en-US': '' + }, + apis: ['TiSubtitleComponent.properties.panelWidth'] + }, + { + demoId: 'subtitle-searchable', + name: { + 'zh-CN': '搜索', + 'en-US': 'searchable' + }, + desc: { + 'zh-CN': '

    通过属性searchable配置items大于1项时,展开面板中是否有搜索。

    ', + 'en-US': '' + }, + apis: ['TiSubtitleComponent.properties.searchable'] + }, + { + demoId: 'subtitle-idkey', + name: { + 'zh-CN': '唯一值', + 'en-US': 'idKey' + }, + desc: { + 'zh-CN': '

    通过属性idKey设置唯一属性,实现下拉选中。

    ', + 'en-US': '' + }, + apis: ['TiSubtitleComponent.properties.idKey'] + }, + { + demoId: 'subtitle-scroll-load', + name: { + 'zh-CN': '下拉加载', + 'en-US': 'scrollToBottom' + }, + desc: { + 'zh-CN': '

    当下拉列表数据量大时,先请求部分数据,等滚动条滚动到底部时利用scrollToBottom事件接口提供的时机再一次次去加载后面的数据。

    ', + 'en-US': '' + }, + apis: ['TiSubtitleComponent.events.scrollToBottom'] + }, + { + demoId: 'subtitle-before-search', + name: { + 'zh-CN': '后台搜索', + 'en-US': 'beforeSearch', + }, + desc: { + 'zh-CN': '

    必须搭配scrollToBottom事件接口使用,后台搜索传出下拉搜索框的值,使用了该事件接口,组件内部将不再进行搜索,搜索需由业务在该事件回调中进行(后台搜索),将搜索后的数据传给items接口。

    ', + 'en-US': '', + }, + apis: ['TiSubtitleComponent.events.beforeSearch'] + }, + { + demoId: 'subtitle-event', + name: { + 'zh-CN': '事件', + 'en-US': 'events', + }, + desc: { + 'zh-CN': '

    当点击后退按钮的时候触发back事件,当选中项变化的时候触发selectedChange事件。

    ', + 'en-US': '', + }, + apis: [ + 'TiSubtitleComponent.properties.selected', + 'TiSubtitleComponent.events.back', + 'TiSubtitleComponent.events.selectedChange' + ] + }, + ] +} \ No newline at end of file diff --git a/src/subtitle/demo/src/app/subtitle/webdoc/subtitle.cn.md b/src/subtitle/demo/src/app/subtitle/webdoc/subtitle.cn.md new file mode 100644 index 0000000..b194151 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/webdoc/subtitle.cn.md @@ -0,0 +1,23 @@ +--- +title: Subtitle 返回标题 +--- +# Subtitle 返回标题 + +
    + +Subtitle 用于显示当前页面在系统层级结构中的位置,并能向上返回。   + +```typescript +import { TiSubtitleModule } from '@opentiny/ng'; +``` + +
    + +
    + +Subtitle 用于显示当前页面在系统层级结构中的位置,并能向上返回。   + +```typescript +import { TiSubtitleModule } from '@opentiny/ng'; +``` +
    diff --git a/src/subtitle/demo/src/app/subtitle/webdoc/subtitle.en.md b/src/subtitle/demo/src/app/subtitle/webdoc/subtitle.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/subtitle/demo/src/app/subtitle/webdoc/subtitle.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/subtitle/demo/src/favicon.ico b/src/subtitle/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/subtitle/demo/src/index.html b/src/subtitle/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/subtitle/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/subtitle/demo/src/main.ts b/src/subtitle/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/subtitle/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/subtitle/demo/test.ts b/src/subtitle/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/subtitle/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/subtitle/demo/tsconfig.app.json b/src/subtitle/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/subtitle/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/subtitle/demo/tsconfig.spec.json b/src/subtitle/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/subtitle/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/subtitle/lib/index.ts b/src/subtitle/lib/index.ts new file mode 100644 index 0000000..be9900a --- /dev/null +++ b/src/subtitle/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSubtitleModule'; diff --git a/src/subtitle/lib/ng-package.json b/src/subtitle/lib/ng-package.json new file mode 100644 index 0000000..ab082c8 --- /dev/null +++ b/src/subtitle/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/subtitle", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/subtitle/lib/package.json b/src/subtitle/lib/package.json new file mode 100644 index 0000000..c526a41 --- /dev/null +++ b/src/subtitle/lib/package.json @@ -0,0 +1,18 @@ +{ + "name": "@opentiny/ng-subtitle", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/router": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-list": "~1.0.0-beta.0", + "@opentiny/ng-select": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-tip": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/subtitle/lib/project.json b/src/subtitle/lib/project.json new file mode 100644 index 0000000..7da3405 --- /dev/null +++ b/src/subtitle/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/subtitle/lib", + "sourceRoot": "src/subtitle/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/subtitle"], + "options": { + "project": "src/subtitle/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/subtitle"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js subtitle" + }, + { + "command": "ng default-build subtitle" + }, + { + "command": "node build/clear-default-theme.js subtitle" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/subtitle && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build subtitle && ng pack subtitle && node build/publish.js subtitle --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/subtitle/lib/src/TiSubtitleComponent.ts b/src/subtitle/lib/src/TiSubtitleComponent.ts new file mode 100644 index 0000000..3f7cdc2 --- /dev/null +++ b/src/subtitle/lib/src/TiSubtitleComponent.ts @@ -0,0 +1,233 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Input, + Output, + Renderer2, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { Params } from '@angular/router'; + +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiListScrollLoad } from '@opentiny/ng-list'; +import { TiSelectComponent } from '@opentiny/ng-select'; +import { TiPositionType } from '@opentiny/ng-utils'; +import { Util } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; +/** + * items接口item项的数据接口 + */ +export interface TiSubtitleItem { + label: string; + [propName: string]: any; +} + +/** + * scrollToBottom 事件回调参数 + * + */ +export interface TiSubtitleListScrollLoad extends TiListScrollLoad { + /** + * subtile 中 下拉选择部分 select 组件实例 + */ + selectInstance?: TiSelectComponent; +} + +@Component({ + selector: 'ti-subtitle', + templateUrl: './subtitle.html', + styleUrls: ['./subtitle.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) + +/** + * 返回标题组件 + * + */ +export class TiSubtitleComponent extends TiBaseComponent { + /** + * 后退按钮链接 + */ + @Input() href: string = ''; + /** + * 标题数据集,如果选项大于 1 个,标题是下拉的形式 + */ + @Input() items: Array = []; + /** + * 下拉面板中是否有搜索框 + */ + @Input() searchable: boolean = false; + /** + * 指定在何处打开链接, 即 a 标签的 target 属性 + */ + @Input() target: '_blank' | '_self' | '_parent' | '_top' = '_blank'; + // select组件选中项模板引用名为#selected, 两个名字冲突,所以使用了别名 + /** + * 选中项,items 的条数大于 1 时才生效 + */ + @Input('selected') selectedItem: TiSubtitleItem; + /** + * 后退按钮点击后的跳转路由, 设置后 href 和 target 失效 + */ + @Input() backRouterLink: string | Array; + /** + * 后退按钮点击后的跳转路由参数 + */ + @Input() queryParams: Params; + /** + * 下拉面板宽度 + */ + @Input() panelWidth: string; + /** + * 最大宽度 + */ + @Input() maxWidth: string = '250px'; + /** + * 标题溢出提示的方向 + */ + @Input() tipPosition: TiPositionType = 'top'; + /** + * @ignore + * + * idKey指定的属性的值相等时即认为select的 option 选项是选中的。选中项 ngModel 中的数据(modelOption对象)跟 options 数据集中的选项(option对象)之间对应相等关系的依据属性。当 + * modelOption中的 idKey 设置的属性的值 与 option 中的 idKey 设置的属性的值相等时,则认为 modelOption 和 option 是对应的相等关系,即认为 option 选项是选中的。 + * + * 默认当 modelOption === option 或者 modelOption 中的 labelKey 设置的属性的值 与 option 中的 labelKey 设置的属性的值相等时,则认为 option 选项是选中的。 + */ + + /** + * 设置数据唯一标识的键值,默认为 labelKey 的接口值 + */ + @Input() idKey: string; + /** + * 选中项变更时触发的回调,items 的条数大于 1 时才生效 + */ + @Output() readonly selectedChange: EventEmitter = new EventEmitter(); + /** + * 点击后退按钮时触发的回调 + */ + @Output() readonly back: EventEmitter = new EventEmitter(); + /** + * + * 必须搭配 scrollToBottom 事件接口使用。后台搜索,传出下拉中搜索框的值。 + * + * 一旦使用该事件接口,组件内部将不再进行搜索,搜索需由业务在该事件时回调中进行(后台搜索),将搜索后的数据传给 items 接口。 + */ + @Output() readonly beforeSearch: EventEmitter = new EventEmitter(); + /** + * 下拉列表滚动到底部的回调 + */ + @Output() readonly scrollToBottom: EventEmitter = new EventEmitter(); + + /** + * @ignore + */ + @ViewChild(TiSelectComponent, { static: false }) + selectComRef: TiSelectComponent; + private hasSelectCom: boolean = false; + /** + * @ignore + */ + public originalItems: Array; // 后台搜索时需要该变量 + /** + * @ignore + * + * items最大宽度 + */ + public itemsMaxWidth: string = ''; + protected versionInfo: string = super.getVersion(packageInfo); + constructor(private element: ElementRef, private renderer2: Renderer2, private changeDetectorRef: ChangeDetectorRef) { + super(element, renderer2); + } + ngOnInit(): void { + super.ngOnInit(); + // 左侧图标的宽度 + const iconWidth: string = getComputedStyle(this.nativeElement).getPropertyValue('--ti3-subtitle-icon-width'); + // 右侧items的最大宽度 + this.itemsMaxWidth = `calc(${this.maxWidth} - ${iconWidth})`; + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + // 如果没有传入选中项,且items 条目数大于1 则默认选中第一项 + if (changes['items'] && Util.isUndefined(this.selectedItem) && this.items?.length > 1) { + this.selectedItem = this.items[0]; + } + + if (this.searchable && this.beforeSearch.observers.length > 0 && !this.originalItems && this.items?.length > 1) { + this.originalItems = this.items; + } + } + + ngDoCheck(): void { + super.ngDoCheck(); + this.changeDetectorRef.markForCheck(); + } + + /** + * @ignore + * 触发back 事件 + */ + public onClick($event: Event): void { + this.back.emit($event); + } + /** + * @ignore + * select选中项改变,触发subtitle selectedChange事件 + */ + public onSelectedOptionChange(option: any): void { + this.selectedChange.emit(option as TiSubtitleItem); + } + /** + * @ignore + */ + public isMoreThanOneItem(): boolean { + let result: boolean = false; + // 后台搜索时,业务会将后台搜索结果赋值给items,如果搜索结果数据条数小于1条,不能认为是实际原有的数据总数就小于1条 + if ((this.searchable && this.beforeSearch.observers.length > 0 && this.originalItems?.length > 1) || this.items?.length > 1) { + result = true; + if (!this.hasSelectCom) { + setTimeout(() => { + // 暗色背景,给ti-select 添加dark属性 + if (this.selectComRef?.nativeElement && !Util.isUndefined(this.hostRef?.nativeElement?.attributes?.['dark'])) { + this.renderer.setAttribute(this.selectComRef.nativeElement, 'dark', ''); + } + }, 0); + } + this.hasSelectCom = true; + } else { + this.hasSelectCom = false; + } + + return result; + } + /** + * @ignore + */ + public onBeforeSearch(event: TiSelectComponent): void { + this.beforeSearch.emit(event); + } + /** + * @ignore + * select下拉列表滚动条滚动到底部时,触发 subtitle 的 scrollToBottom 事件 + */ + public onScrollToBottom(info: TiListScrollLoad, selectCom: TiSelectComponent): void { + info['selectInstance'] = selectCom; + this.scrollToBottom.emit(info); + } +} diff --git a/src/subtitle/lib/src/TiSubtitleModule.ts b/src/subtitle/lib/src/TiSubtitleModule.ts new file mode 100644 index 0000000..46cbe48 --- /dev/null +++ b/src/subtitle/lib/src/TiSubtitleModule.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; + +import { TiSubtitleComponent, TiSubtitleListScrollLoad } from './TiSubtitleComponent'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiSelectModule } from '@opentiny/ng-select'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiTipModule } from '@opentiny/ng-tip'; + +@NgModule({ + imports: [CommonModule, FormsModule, RouterModule, TiOverflowModule, TiSelectModule, TiTipModule, TiIconModule], + exports: [TiSubtitleComponent], + declarations: [TiSubtitleComponent] +}) +export class TiSubtitleModule {} +export { TiSubtitleComponent, TiSubtitleItem, TiSubtitleListScrollLoad } from './TiSubtitleComponent'; diff --git a/src/subtitle/lib/src/subtitle.html b/src/subtitle/lib/src/subtitle.html new file mode 100644 index 0000000..877450c --- /dev/null +++ b/src/subtitle/lib/src/subtitle.html @@ -0,0 +1,73 @@ +
    + + + + + +
    + {{items[0]?.label}} +
    + + + {{item?.label}} + + + {{item?.label}} + + +
    +
    diff --git a/src/subtitle/lib/src/subtitle.less b/src/subtitle/lib/src/subtitle.less new file mode 100644 index 0000000..ed3c706 --- /dev/null +++ b/src/subtitle/lib/src/subtitle.less @@ -0,0 +1,106 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-subtitle-height: var(--ti-common-size-6x); + --ti-subtitle-font-weight: var(--ti-common-font-weight-7); + --ti-subtitle-icon-size: var(--ti-common-size-4x); + --ti-subtitle-icon-padding-left: var(--ti-common-space-6); + --ti-subtitle-icon-padding-right: var(--ti-common-space-2x); + // 左侧按钮区域宽度 = 按钮尺寸 + 左边距 + 右边距。注意:该变量在ts中使用到,用于设置最大宽度 + --ti-subtitle-icon-width: calc( + var(--ti-subtitle-icon-size) + var(--ti-subtitle-icon-padding-left) + var(--ti-subtitle-icon-padding-right) + ); +} + +:host { + display: inline-block; + height: var(--ti-subtitle-height); + line-height: var(--ti-subtitle-height); +} +.ti3-subtitle-container { + display: flex; + align-items: center; +} +//后退按钮 +.ti3-subtitle-icon-back { + position: relative; + display: inline-block; + font-size: var(--ti-subtitle-icon-size); + padding: 0 var(--ti-subtitle-icon-padding-right) 0 var(--ti-subtitle-icon-padding-left); + color: var(--ti-common-color-icon-normal); + font-weight: var(--ti-subtitle-font-weight); + vertical-align: top; + text-decoration: none; + cursor: pointer; + .box-sizing(border-box); + &:hover { + color: var(--ti-common-color-icon-hover); + } + &:after { + content: ''; + position: absolute; + height: var(--ti-subtitle-icon-size); + right: 0; + top: calc((var(--ti-subtitle-height) - var(--ti-subtitle-icon-size)) / 2); + border-right: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + } +} + +// 单文本标题 +.ti3-subtitle-text { + padding-left: var(--ti-common-space-10); + font-size: var(--ti-common-font-size-2); + color: var(--ti-common-color-text-primary); + font-weight: var(--ti-subtitle-font-weight); + .box-sizing(border-box); + cursor: default; +} + +// 覆盖select样式适配subtitle +:host ::ng-deep ti-select { + height: var(--ti-subtitle-height); + line-height: var(--ti-subtitle-height); + width: 100%; + .ti3-overflow-padding { + padding-right: 0; + } + & ::ng-deep ti-dominator.ti3-select-dominator-container { + vertical-align: top; + height: var(--ti-subtitle-height); + line-height: var(--ti-subtitle-height); + --ti-dominator-container-height: var(--ti-subtitle-height); + } +} +:host .ti3-subtitle-selected-text { + width: 100%; + font-size: var(--ti-common-font-size-2); + font-weight: var(--ti-subtitle-font-weight); +} + +// 暗色主题 +:host[dark] { + .ti3-subtitle-icon-back { + color: var(--ti-common-color-icon-white); + &:after { + opacity: 0.2; + } + &:hover { + color: var(--ti-common-color-icon-hover); + } + } + .ti3-subtitle-text { + color: var(--ti-common-color-text-white); + } + + & ::ng-deep ti-select { + background-color: transparent; + border-color: transparent; + &:hover, + &[tiFocused] { + border-color: transparent; + } + } + .ti3-subtitle-selected-text { + color: var(--ti-common-color-text-white); + } +} diff --git a/src/swiper/demo/karma.conf.js b/src/swiper/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/swiper/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/swiper/demo/project.json b/src/swiper/demo/project.json new file mode 100644 index 0000000..d8f29a0 --- /dev/null +++ b/src/swiper/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/swiper/demo", + "sourceRoot": "src/swiper/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/swiper", + "index": "src/swiper/demo/src/index.html", + "main": "src/swiper/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/swiper/demo/tsconfig.app.json", + "assets": ["src/swiper/demo/src/favicon.ico", "src/swiper/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "swiper-demo:build:production" + }, + "development": { + "browserTarget": "swiper-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js swiper" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/swiper/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/swiper/demo/tsconfig.spec.json", + "karmaConfig": "src/swiper/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/swiper/demo/src/app/AppComponent.ts b/src/swiper/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/swiper/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/swiper/demo/src/app/AppModule.ts b/src/swiper/demo/src/app/AppModule.ts new file mode 100644 index 0000000..f8a4314 --- /dev/null +++ b/src/swiper/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SwiperTestModule } from './swiper/SwiperTestModule'; + +@NgModule({ + imports: [ + SwiperTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/swiper/demo/src/app/IndexComponent.ts b/src/swiper/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..f61f319 --- /dev/null +++ b/src/swiper/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SwiperTestModule } from './swiper/SwiperTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SwiperTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/swiper/demo/src/app/app.html b/src/swiper/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/swiper/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/swiper/demo/src/app/swiper/SwiperActiveindexComponent.ts b/src/swiper/demo/src/app/swiper/SwiperActiveindexComponent.ts new file mode 100644 index 0000000..bcb0463 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperActiveindexComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-activeindex.html', + styleUrls: ['./swiper.less'] +}) +export class SwiperActiveindexComponent { + items = ['First', 'Second', 'Third', 'Fourth']; + activeIndex: number = 2; +} diff --git a/src/swiper/demo/src/app/swiper/SwiperAutoplayComponent.ts b/src/swiper/demo/src/app/swiper/SwiperAutoplayComponent.ts new file mode 100644 index 0000000..3a4312d --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperAutoplayComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-autoplay.html', + styleUrls: ['./swiper.less'] +}) +export class SwiperAutoplayComponent { + items = ['First', 'Second', 'Third', 'Fourth']; +} diff --git a/src/swiper/demo/src/app/swiper/SwiperBasicComponent.ts b/src/swiper/demo/src/app/swiper/SwiperBasicComponent.ts new file mode 100644 index 0000000..2a072c0 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperBasicComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-basic.html', + styleUrls: ['./swiper.less'] +}) +export class SwiperBasicComponent { + items = ['First', 'Second', 'Third', 'Fourth']; +} diff --git a/src/swiper/demo/src/app/swiper/SwiperEventsComponent.ts b/src/swiper/demo/src/app/swiper/SwiperEventsComponent.ts new file mode 100644 index 0000000..b921387 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperEventsComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-events.html', + styleUrls: ['./swiper.less'] +}) +export class SwiperEventsComponent { + activeIndex: number = 1; + myLogs: Array = []; + items: Array = ['First', 'Second', 'Third', 'Fourth']; + activeIndexChange = (currentPage: number): void => { + this.myLogs = [...this.myLogs, `activeIndex: ${currentPage}`]; + }; +} diff --git a/src/swiper/demo/src/app/swiper/SwiperIndicatorpositionComponent.ts b/src/swiper/demo/src/app/swiper/SwiperIndicatorpositionComponent.ts new file mode 100644 index 0000000..01f097a --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperIndicatorpositionComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-indicatorposition.html', + styleUrls: ['./swiper.less'] +}) +export class SwiperIndicatorpositionComponent { + items = ['First', 'Second', 'Third', 'Fourth']; +} diff --git a/src/swiper/demo/src/app/swiper/SwiperLoopComponent.ts b/src/swiper/demo/src/app/swiper/SwiperLoopComponent.ts new file mode 100644 index 0000000..9237b52 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperLoopComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-loop.html', + styleUrls: ['./swiper.less'] +}) +export class SwiperLoopComponent { + items = ['First', 'Second', 'Third', 'Fourth']; +} diff --git a/src/swiper/demo/src/app/swiper/SwiperShowcardnumComponent.ts b/src/swiper/demo/src/app/swiper/SwiperShowcardnumComponent.ts new file mode 100644 index 0000000..585443d --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperShowcardnumComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-showcardnum.html', + styleUrls: ['./swiper.less'] +}) +export class SwiperShowcardnumComponent { + cards: Array = [ + { text: 'card-0' }, + { text: 'card-1' }, + { text: 'card-2' }, + { text: 'card-3' }, + { text: 'card-4' }, + { text: 'card-5' }, + { text: 'card-6' }, + { text: 'card-7' } + ]; +} diff --git a/src/swiper/demo/src/app/swiper/SwiperShowcardnumTestComponent.ts b/src/swiper/demo/src/app/swiper/SwiperShowcardnumTestComponent.ts new file mode 100644 index 0000000..321da35 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperShowcardnumTestComponent.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './swiper-showcardnum-test.html' +}) +export class SwiperShowcardnumTestComponent { + public newCards: Array = [{ text: 'card-0' }, { text: 'card-1' }, { text: 'card-2' }, { text: 'card-3' }]; + public newCards1: Array = [ + { text: 'card-0' }, + { text: 'card-1' }, + { text: 'card-2' }, + { text: 'card-3' }, + { text: 'card-4' }, + { text: 'card-5' }, + { text: 'card-6' }, + { text: 'card-7' }, + { text: 'card-8' } + ]; + + // 当前激活卡片索引 + public currentIndex: number = 1; + public currentIndex1: number = 2; + + // 激活卡片 + public activeCard(index: number): void { + this.currentIndex = index; + } + + public activeCard1(index: number): void { + this.currentIndex1 = index; + } +} diff --git a/src/swiper/demo/src/app/swiper/SwiperTestModule.ts b/src/swiper/demo/src/app/swiper/SwiperTestModule.ts new file mode 100644 index 0000000..53cbe64 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/SwiperTestModule.ts @@ -0,0 +1,76 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiCardModule, TiCheckboxModule, TiModalModule, TiSwiperModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { SwiperBasicComponent } from './SwiperBasicComponent'; +import { SwiperShowcardnumComponent } from './SwiperShowcardnumComponent'; +import { SwiperActiveindexComponent } from './SwiperActiveindexComponent'; +import { SwiperEventsComponent } from './SwiperEventsComponent'; +import { SwiperAutoplayComponent } from './SwiperAutoplayComponent'; +import { SwiperLoopComponent } from './SwiperLoopComponent'; +import { SwiperIndicatorpositionComponent } from './SwiperIndicatorpositionComponent'; +import { SwiperShowcardnumTestComponent } from './SwiperShowcardnumTestComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiSwiperModule, + TiButtonModule, + TiModalModule, + TiCardModule, + TiCheckboxModule, + DemoLogModule, + RouterModule.forChild(SwiperTestModule.ROUTES) + ], + declarations: [ + SwiperBasicComponent, + SwiperShowcardnumComponent, + SwiperActiveindexComponent, + SwiperEventsComponent, + SwiperAutoplayComponent, + SwiperLoopComponent, + SwiperIndicatorpositionComponent, + SwiperShowcardnumTestComponent + ] +}) +export class SwiperTestModule { + public static readonly ROUTES: Routes = [ + { + path: 'swiper/swiper-basic', + component: SwiperBasicComponent + }, + { + path: 'swiper/swiper-showcardnum', + component: SwiperShowcardnumComponent + }, + { + path: 'swiper/swiper-activeindex', + component: SwiperActiveindexComponent + }, + { + path: 'swiper/swiper-events', + component: SwiperEventsComponent + }, + { + path: 'swiper/swiper-autoplay', + component: SwiperAutoplayComponent + }, + { + path: 'swiper/swiper-loop', + component: SwiperLoopComponent + }, + { + path: 'swiper/swiper-showcardnum-test', + component: SwiperShowcardnumTestComponent + }, + { + path: 'swiper/swiper-indicatorposition', + component: SwiperIndicatorpositionComponent + } + ]; +} diff --git a/src/swiper/demo/src/app/swiper/swiper-activeindex.html b/src/swiper/demo/src/app/swiper/swiper-activeindex.html new file mode 100644 index 0000000..95e196f --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-activeindex.html @@ -0,0 +1,10 @@ +
    +
    Current activeIndex: {{ activeIndex | json }}
    +
    + + +
    + {{item}} +
    +
    +
    diff --git a/src/swiper/demo/src/app/swiper/swiper-autoplay.html b/src/swiper/demo/src/app/swiper/swiper-autoplay.html new file mode 100644 index 0000000..fed8fd7 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-autoplay.html @@ -0,0 +1,7 @@ + + +
    + {{item}} +
    +
    +
    diff --git a/src/swiper/demo/src/app/swiper/swiper-basic.html b/src/swiper/demo/src/app/swiper/swiper-basic.html new file mode 100644 index 0000000..eb28418 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-basic.html @@ -0,0 +1,7 @@ + + +
    + {{item}} +
    +
    +
    diff --git a/src/swiper/demo/src/app/swiper/swiper-events.html b/src/swiper/demo/src/app/swiper/swiper-events.html new file mode 100644 index 0000000..e9d9288 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-events.html @@ -0,0 +1,8 @@ + + +
    + {{item}} +
    +
    +
    + diff --git a/src/swiper/demo/src/app/swiper/swiper-indicatorposition.html b/src/swiper/demo/src/app/swiper/swiper-indicatorposition.html new file mode 100644 index 0000000..8ef002e --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-indicatorposition.html @@ -0,0 +1,27 @@ +

    1.默认值为‘below’,指示器在容器外部

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

    2.设置值为‘bottom’,指示器在容器底部

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

    3.设置值为‘none’,不显示指示器

    +
    + + +
    + {{item}} +
    +
    +
    diff --git a/src/swiper/demo/src/app/swiper/swiper-loop.html b/src/swiper/demo/src/app/swiper/swiper-loop.html new file mode 100644 index 0000000..f73e018 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-loop.html @@ -0,0 +1,7 @@ + + +
    + {{item}} +
    +
    +
    diff --git a/src/swiper/demo/src/app/swiper/swiper-showcardnum-test.html b/src/swiper/demo/src/app/swiper/swiper-showcardnum-test.html new file mode 100644 index 0000000..a52f310 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-showcardnum-test.html @@ -0,0 +1,36 @@ +

    1.描述

    +

    showCardNum接口设置每页展示卡片数量

    +

    2.示例,查看最后一页是否存在显示不全,或者两个卡片没有间隙的情况

    +

    1.共4张卡片,每页展示3张卡片

    +
    +
    + + + +

    {{card.text}}

    +
    +
    +
    +
    +

    +

    2.共9张卡片,每页展示5张卡片

    +
    +
    + + + +

    {{card.text}}

    +
    +
    +
    +
    + diff --git a/src/swiper/demo/src/app/swiper/swiper-showcardnum.html b/src/swiper/demo/src/app/swiper/swiper-showcardnum.html new file mode 100644 index 0000000..bc3fabf --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper-showcardnum.html @@ -0,0 +1,7 @@ + + +
    +

    {{card.text}}

    +
    +
    +
    diff --git a/src/swiper/demo/src/app/swiper/swiper.less b/src/swiper/demo/src/app/swiper/swiper.less new file mode 100644 index 0000000..5cc4d96 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/swiper.less @@ -0,0 +1,13 @@ +ti-swipercard:nth-child(odd) div { + background-color: #beccfa; +} +ti-swipercard:nth-child(even) div { + background-color: #dfe1e6; +} +ti-swipercard div { + height: 180px; + line-height: 180px; + font-size: 20px; + text-align: center; + border-radius: 2px; +} diff --git a/src/swiper/demo/src/app/swiper/webdoc/swiper-demos.js b/src/swiper/demo/src/app/swiper/webdoc/swiper-demos.js new file mode 100644 index 0000000..c3b13bb --- /dev/null +++ b/src/swiper/demo/src/app/swiper/webdoc/swiper-demos.js @@ -0,0 +1,96 @@ +export default { + column: '2', + demos: [ + { + demoId: 'swiper-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'swiper basic' + }, + desc: { + 'zh-CN': '

    Swiper 组件的最简用法。

    ', + 'en-US': 'swiper basic' + }, + codeFiles: ['swiper-basic.html', 'SwiperBasicComponent.ts', 'swiper.less'] + }, + { + demoId: 'swiper-loop', + name: { + 'zh-CN': '禁止无限循环', + 'en-US': 'swiper loop' + }, + desc: { + 'zh-CN': '

    通过属性loop配置是否无限循环。

    ', + 'en-US': 'swiper loop' + }, + codeFiles: ['swiper-loop.html', 'SwiperLoopComponent.ts', 'swiper.less'], + apis: ['TiSwiperComponent.properties.loop'] + }, + { + demoId: 'swiper-autoplay', + name: { + 'zh-CN': '自动播放', + 'en-US': 'swiper autoplay' + }, + desc: { + 'zh-CN': '<>通过属性autoplay配置是否自动播放。

    ', + 'en-US': 'swiper autoplay' + }, + codeFiles: ['swiper-autoplay.html', 'SwiperAutoplayComponent.ts', 'swiper.less'], + apis: ['TiSwiperComponent.properties.autoplay', 'TiSwiperComponent.properties.autoplaySpeed'] + }, + { + demoId: 'swiper-activeindex', + name: { + 'zh-CN': '指定当前展示项', + 'en-US': 'swiper activeindex' + }, + desc: { + 'zh-CN': '

    通过属性activeindex配置当前展示项。

    ', + 'en-US': 'swiper activeindex' + }, + codeFiles: ['swiper-activeindex.html', 'SwiperActiveindexComponent.ts', 'swiper.less'], + + apis: ['TiSwiperComponent.properties.activeIndex'] + }, + { + demoId: 'swiper-showcardnum', + name: { + 'zh-CN': '每页展示卡片数', + 'en-US': 'swiper showcardnum' + }, + desc: { + 'zh-CN': '

    通过属性showcardnum配置每页展示卡片数。

    ', + 'en-US': 'swiper showcardnum' + }, + codeFiles: ['swiper-showcardnum.html', 'SwiperShowcardnumComponent.ts', 'swiper.less'], + apis: ['TiSwiperComponent.properties.showCardNum', 'TiSwiperComponent.properties.cardGap'] + }, + { + demoId: 'swiper-indicatorposition', + name: { + 'zh-CN': '指示器位置', + 'en-US': 'swiper indicatorposition' + }, + desc: { + 'zh-CN': '

    通过属性indicatorposition配置指示器位置。

    ', + 'en-US': 'swiper indicatorposition' + }, + codeFiles: ['swiper-indicatorposition.html', 'SwiperIndicatorpositionComponent.ts', 'swiper.less'], + apis: ['TiSwiperComponent.properties.indicatorPosition'] + }, + { + demoId: 'swiper-events', + name: { + 'zh-CN': '事件', + 'en-US': 'swiper events' + }, + desc: { + 'zh-CN': '

    activeIndex改变时触发的回调,参数为改变后的activeIndex

    ', + 'en-US': 'swiper events' + }, + codeFiles: ['swiper-events.html', 'SwiperEventsComponent.ts', 'swiper.less'], + apis: ['TiSwiperComponent.properties.'] + } + ] +}; diff --git a/src/swiper/demo/src/app/swiper/webdoc/swiper.cn.md b/src/swiper/demo/src/app/swiper/webdoc/swiper.cn.md new file mode 100644 index 0000000..7229b04 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/webdoc/swiper.cn.md @@ -0,0 +1,31 @@ +--- +title: Swiper 轮播 +--- +# Swiper 轮播 + +
    + +播放一组内容。 + ++ ``是轮播的容器;``是每个轮播内容的容器。 ++ 一组轮播由一个``嵌套多个``组成。 + +```typescript +import { TiSwiperModule } from '@opentiny/ng'; +``` + +
    + +
    +div class="used-tiny"> + +播放一组内容。 + ++ ``是轮播的容器;``是每个轮播内容的容器。 ++ 一组轮播由一个``嵌套多个``组成。 + +```typescript +import { TiSwiperModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/swiper/demo/src/app/swiper/webdoc/swiper.en.md b/src/swiper/demo/src/app/swiper/webdoc/swiper.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/swiper/demo/src/app/swiper/webdoc/swiper.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/swiper/demo/src/favicon.ico b/src/swiper/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/swiper/demo/src/index.html b/src/swiper/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/swiper/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/swiper/demo/src/main.ts b/src/swiper/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/swiper/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/swiper/demo/test.ts b/src/swiper/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/swiper/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/swiper/demo/tsconfig.app.json b/src/swiper/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/swiper/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/swiper/demo/tsconfig.spec.json b/src/swiper/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/swiper/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/swiper/lib/index.ts b/src/swiper/lib/index.ts new file mode 100644 index 0000000..057ff47 --- /dev/null +++ b/src/swiper/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSwiperModule'; diff --git a/src/swiper/lib/ng-package.json b/src/swiper/lib/ng-package.json new file mode 100644 index 0000000..723dd15 --- /dev/null +++ b/src/swiper/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/swiper", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/swiper/lib/package.json b/src/swiper/lib/package.json new file mode 100644 index 0000000..6c8cda2 --- /dev/null +++ b/src/swiper/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-swiper", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/swiper/lib/project.json b/src/swiper/lib/project.json new file mode 100644 index 0000000..4742d17 --- /dev/null +++ b/src/swiper/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/swiper/lib", + "sourceRoot": "src/swiper/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/swiper"], + "options": { + "project": "src/swiper/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/swiper"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js swiper" + }, + { + "command": "ng default-build swiper" + }, + { + "command": "node build/clear-default-theme.js swiper" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/swiper && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build swiper && ng pack swiper && node build/publish.js swiper --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/swiper/lib/src/TiSwiperComponent.ts b/src/swiper/lib/src/TiSwiperComponent.ts new file mode 100644 index 0000000..c894a7f --- /dev/null +++ b/src/swiper/lib/src/TiSwiperComponent.ts @@ -0,0 +1,397 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChildren, + ElementRef, + EventEmitter, + Input, + NgZone, + Output, + QueryList, + Renderer2, + SimpleChange, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiSwipercardComponent } from './TiSwipercardComponent'; +import packageInfo from '../package.json'; +/** + * swiper组件每个展示块为一个card,点击左右按钮可以实现card切换功能 + * + */ +@Component({ + selector: 'ti-swiper', + templateUrl: './swiper.html', + styleUrls: ['./swiper.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-swiper-multiple]': 'showCardNum > 1' + } +}) +export class TiSwiperComponent extends TiBaseComponent { + constructor(private elementRef: ElementRef, private renderer2: Renderer2, private zone: NgZone, private cdRef: ChangeDetectorRef) { + super(elementRef, renderer2); + } + /** + * 指定当前显示项 + */ + @Input() activeIndex: number = 0; + /** + * 是否自动轮播,自动轮播时,鼠标移入元素,轮播暂停;移出元素,轮播继续。 + */ + @Input() autoplay: boolean = false; + /** + * 自动轮播时的播放速度,单位为毫秒 + */ + @Input() autoplaySpeed: number = 3000; + /** + * 每屏展示的项目数量 + */ + @Input() showCardNum: number = 1; + /** + * 多图轮播卡片间距,8-12px,默认为8px + */ + @Input() cardGap: string = '8px'; + /** + * 指示器位置,三个属性值,“below”容器外部,‘bottom’容器内部底部,‘none’不显示 + */ + @Input() indicatorPosition: 'bottom' | 'below' | 'none' = 'below'; + /** + * 是否无限循环 + */ + @Input() loop: boolean = true; + /** + * 切换触发的事件 + */ + @Output() readonly activeIndexChange: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + @ViewChild('cards', { static: true }) cards: ElementRef; + /** + * @ignore + */ + @ContentChildren(TiSwipercardComponent) + swipercardComs: QueryList; + /** + * @ignore + */ + @ViewChild('wrapper', { static: true }) wrapperEle: ElementRef; + /** + * @ignore + * 卡片总页数 + */ + public totalPage: number = 0; + /** + * @ignore + * 当前页, 按照索引值从0开始 + */ + public currentPage: number = 0; + /** + * @ignore + * 面板指示点的集合 + */ + public swiperBullets: Array = []; + protected versionInfo: string = super.getVersion(packageInfo); + // 卡片切换动画间隔,单位ms + private transitionSpeed: number = 600; + // 定时器任务 + private autoplayId: any; + // 卡片总数量 + private cardNum: number = 0; + private pageWidth: number; + private swiperCardWidth: number; + private cardSpace: number; + private totalWidth: number; + private unlistenResize: () => void; + + ngOnInit(): void { + super.ngOnInit(); + // 一屏显示卡片的数目会随窗口大小而变化,所以监听窗口大小变化重新计算分页, + this.zone.runOutsideAngular(() => { + this.unlistenResize = this.renderer2.listen(window, 'resize', () => { + this.setCardsWidth(); + this.setLeftPosition(this.currentPage); + }); + }); + } + + ngAfterContentChecked(): void { + super.ngAfterContentChecked(); + // 当卡片数量变化时,计算总数量 + if (this.cards.nativeElement.children.length !== this.cardNum) { + this.setCardNum(); + this.setCardsWidth(); + this.cdRef.markForCheck(); + } + } + + ngAfterViewInit(): void { + this.setCardsWidth(); + this.setLeftPosition(this.currentPage); + this.renderer.setStyle(this.cards.nativeElement, 'transition', `left ${this.transitionSpeed}ms`); + + // 只有一页时,不自动轮播 + if (this.autoplay && this.autoplaySpeed && this.totalPage > 1) { + this.startAutoplay(); + + this.renderer2.listen(this.wrapperEle.nativeElement, 'mouseenter', () => { + this.stopAutoplay(); + }); + + this.renderer2.listen(this.wrapperEle.nativeElement, 'mouseleave', () => { + this.startAutoplay(); + }); + } + } + ngAfterViewChecked(): void { + // 外层容器宽度有变化时,重新计算宽度及位置 + if (this.wrapperEle && this.wrapperEle.nativeElement.getBoundingClientRect().width !== this.pageWidth) { + this.setCardsWidth(); + this.setLeftPosition(this.currentPage); + } + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + const indexObj: SimpleChange = changes['activeIndex']; + if (indexObj && !indexObj.firstChange) { + this.calculateCurrentPage(indexObj.currentValue); + this.setLeftPosition(this.currentPage); + } + } + + ngOnDestroy(): void { + this.stopAutoplay(); + + if (this.unlistenResize) { + this.unlistenResize(); + } + } + + /** + * @ignore + * 点击左侧按钮触发事件 + */ + public onClickPrev(): void { + this.activeCard(this.currentPage - 1); + } + + /** + * @ignore + * 点击右侧按钮触发事件 + */ + public onClickNext(): void { + this.activeCard(this.currentPage + 1); + } + + /** + * @ignore + * 点击指示器触发事件 + */ + public onClickDot(index: number): void { + this.activeCard(index); + } + /** + * @ignore + * 点击左右侧按钮、面板指示板触发事件 + */ + private activeCard(index: number): void { + if (this.totalPage === 1) { + return; + } + + this.renderer.setStyle(this.cards.nativeElement, 'transition', `left ${this.transitionSpeed}ms`); + if (index === this.totalPage && this.loop) { + this.currentPage = 0; + const first: Element = this.swipercardComs.first.nativeElement; + this.setPosition(first, true); + this.setLeftPosition(this.totalPage); + this.adjustPosition(first); + } else if (index === -1 && this.loop) { + this.currentPage = this.totalPage - 1; + const last: Element = this.swipercardComs.last.nativeElement; + this.setPosition(last, false); + this.setLeftPosition(-1); + this.adjustPosition(last); + } else { + this.currentPage = index; + if (index <= -1) { + this.currentPage = 0; + } + if (index >= this.totalPage) { + this.currentPage = this.totalPage - 1; + } + this.setLeftPosition(this.currentPage); + this.activeIndexChange.emit(this.currentPage); + } + } + + // 计算卡片总数及总页数 + private setCardNum(): void { + this.cardNum = this.cards && this.cards.nativeElement.children && this.cards.nativeElement.children.length; + this.calculatpaging(); + this.swiperBullets.length = this.totalPage; + } + + // 计算卡片容器总宽度 + private setCardsWidth(): void { + this.calculatecardWidth(); + this.renderer.setStyle(this.cards.nativeElement, 'width', `${this.totalWidth}px`); + } + + // 当点击左右按钮时,设置激活卡片相对位置 + private setLeftPosition(index: number): void { + if (this.totalPage === 1) { + return; + } + + // 最后一页未占完 + if (index === this.totalPage - 1 && this.cardNum % this.showCardNum !== 0) { + // 最后一页呈现需要偏移的卡片数 + const swiperCardNum: number = this.cardNum - this.showCardNum; + this.renderer.setStyle( + this.cards.nativeElement, + 'left', + `${-swiperCardNum * this.swiperCardWidth - swiperCardNum * this.cardSpace}px` + ); + + return; + } + + // 无限循环 + if (index === this.totalPage && this.loop) { + this.renderer.setStyle(this.cards.nativeElement, 'left', `${-this.totalWidth}px`); + + return; + } + if (index === -1 && this.loop) { + this.renderer.setStyle( + this.cards.nativeElement, + 'left', + `${this.swiperCardWidth * this.showCardNum + (this.showCardNum - 1) * this.cardSpace}px` + ); + + return; + } + + // left偏移量 + const leftVal: number = -index * this.showCardNum * (this.swiperCardWidth + this.cardSpace); + this.renderer.setStyle(this.cards.nativeElement, 'left', `${leftVal}px`); + } + + // 调整第一页或者最后一页的位置,为动画做准备 + private setPosition(targetEle: Element, lastToFirst: boolean): void { + this.renderer.setStyle(targetEle, 'transform', `translateX(${lastToFirst ? this.totalWidth : this.totalWidth * -1}px)`); + } + + private adjustPosition(targetEle?: Element): void { + setTimeout((): void => { + this.renderer.removeStyle(targetEle, 'transform'); + this.renderer.removeStyle(this.cards.nativeElement, 'transition'); + this.setLeftPosition(this.currentPage); + this.activeIndexChange.emit(this.currentPage); + }, this.transitionSpeed); + } + + // 计算每页显示卡片数,总页数及当前页 + private calculatpaging(): void { + this.calculateTotalPage(); + this.calculateCurrentPage(this.activeIndex); + } + + /** + * 计算卡片宽度 + */ + private calculatecardWidth(): void { + // 初始化时,多图且左右翻页按钮还没有显示,容器元素的可用宽度需要减去左右按钮的宽度之和32+32=64 + if (this.totalPage === 0 && this.showCardNum > 1) { + this.pageWidth = this.wrapperEle.nativeElement.getBoundingClientRect().width - 64; + } else { + this.pageWidth = this.wrapperEle.nativeElement.getBoundingClientRect().width; + } + + // 每页卡片总间距 + this.cardSpace = this.showCardNum > 1 ? parseInt(this.cardGap, 10) : 0; + const cardsSpace: number = (this.showCardNum - 1) * parseInt(this.cardGap, 10); + this.swiperCardWidth = (this.pageWidth - cardsSpace) / this.showCardNum; + this.totalWidth = this.swiperCardWidth * this.cardNum + cardsSpace * this.totalPage; + + this.swipercardComs.toArray().forEach((item: TiSwipercardComponent, index: number): void => { + // 设置卡片宽度 + this.renderer2.setStyle(item.nativeElement, 'width', this.swiperCardWidth + 'px'); + // 设置卡片间距,最后一个卡片不设置 + if (index !== this.cardNum - 1) { + this.renderer2.setStyle(item.nativeElement, 'margin-right', this.cardSpace + 'px'); + } + }); + } + + /** + * 计算总页数 + */ + private calculateTotalPage(): void { + this.totalPage = Math.ceil(this.cardNum / this.showCardNum); + if (this.currentPage > this.totalPage) { + this.activeCard(this.totalPage); + } + } + + /** + * 计算当前页 + */ + private calculateCurrentPage(index: number): void { + // 当前索引值不正确,不处理 + if (index < 0 || index > this.cardNum - 1) { + return; + } + + if (this.showCardNum === 1) { + this.currentPage = index; + + return; + } + + // 当前一页卡片的最大最小索引 + const maxIndex: number = (this.currentPage + 1) * this.showCardNum - 1; + const minIndex: number = this.currentPage > 0 ? this.currentPage * this.showCardNum - 1 : 0; + // 当前索卡片是否在当前页面 + if (index < minIndex || index > maxIndex) { + this.currentPage = Math.floor((index + 1) / this.showCardNum); + } + } + + /** + * 暂停自动播放,清除定时任务 + */ + private stopAutoplay(): void { + if (this.autoplayId) { + clearInterval(this.autoplayId); + this.autoplayId = undefined; + } + } + /** + * 开始自动播放,设置定时任务 + */ + private startAutoplay(): void { + if (!this.autoplayId) { + this.autoplayId = setInterval((): void => { + this.onClickNext(); + this.cdRef.markForCheck(); + }, this.autoplaySpeed); + } + } +} diff --git a/src/swiper/lib/src/TiSwiperModule.ts b/src/swiper/lib/src/TiSwiperModule.ts new file mode 100644 index 0000000..c59ce3a --- /dev/null +++ b/src/swiper/lib/src/TiSwiperModule.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOutlineModule } from '@opentiny/ng-outline'; + +import { TiSwiperComponent } from './TiSwiperComponent'; +import { TiSwipercardComponent } from './TiSwipercardComponent'; + +@NgModule({ + imports: [CommonModule, TiIconModule, TiOutlineModule], + exports: [TiSwiperComponent, TiSwipercardComponent], + declarations: [TiSwiperComponent, TiSwipercardComponent] +}) +export class TiSwiperModule {} +export { TiSwiperComponent } from './TiSwiperComponent'; +export { TiSwipercardComponent } from './TiSwipercardComponent'; diff --git a/src/swiper/lib/src/TiSwipercardComponent.ts b/src/swiper/lib/src/TiSwipercardComponent.ts new file mode 100644 index 0000000..55ec012 --- /dev/null +++ b/src/swiper/lib/src/TiSwipercardComponent.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * TiSwiperCard 是单个卡片组件 + */ +@Component({ + selector: 'ti-swipercard', + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./swipercard.less'] +}) +export class TiSwipercardComponent extends TiBaseComponent { + protected versionInfo: string = super.getVersion(packageInfo); +} diff --git a/src/swiper/lib/src/swiper.html b/src/swiper/lib/src/swiper.html new file mode 100644 index 0000000..9fbbc05 --- /dev/null +++ b/src/swiper/lib/src/swiper.html @@ -0,0 +1,51 @@ +
    + +
    + +
    + + +
    +
    + +
    +
    + + +
    + +
    +
    + + +
      +
    • +
    diff --git a/src/swiper/lib/src/swiper.less b/src/swiper/lib/src/swiper.less new file mode 100644 index 0000000..10dea45 --- /dev/null +++ b/src/swiper/lib/src/swiper.less @@ -0,0 +1,155 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-swiper-icon-height: var(--ti-common-size-12x); + --ti-swiper-icon-color: var(--ti-common-color-icon-white); + --ti-swiper-icon-color-hover: var(--ti-common-color-icon-white); + --ti-swiper-icon-bg-color: rgba(0, 0, 0, 0.2); + --ti-swiper-icon-bg-color-hover: rgba(0, 0, 0, 0.3); + --ti-swiper-finite-icon-color: rgba(255, 255, 255, 0.4); + --ti-swiper-pagination-space: var(--ti-common-space-7x); // 指示器距离容器元素间距 + --ti-swiper-pagination-bullets-space: var(--ti-common-space-6); + --ti-swiper-multiple-icon-bg-color: var(--ti-common-color-transparent); + --ti-swiper-multiple-icon-bg-color-hover: var(--ti-common-color-transparent); + --ti-swiper-bullet-border: 2.5px; +} + +:host { + display: block; + position: relative; // 相对于自己定位,在正常文档流中占位 +} + +.ti3-swiper-container { + .flex-container(row, center, center); + position: relative; +} + +// 左右侧按钮公共样式 +.ti3-swiper-arrow { + flex-shrink: 0; + position: absolute; + display: flex; + align-items: center; + justify-content: center; + top: calc((100% - var(--ti-swiper-icon-height)) / 2); // IE下需要设置top定位,否则会居下显示 + z-index: 1; + font-size: var(--ti-common-size-4x); + width: var(--ti-common-size-6x); + height: var(--ti-swiper-icon-height); + background: var(--ti-swiper-icon-bg-color); + text-align: center; + cursor: pointer; + color: var(--ti-swiper-icon-color); + border-radius: var(--ti-common-border-radius-normal); + + &:not(.ti3-swiper-finite-arrow):hover, + &:not(.ti3-swiper-finite-arrow):focus { + color: var(--ti-swiper-icon-color-hover); + background: var(--ti-swiper-icon-bg-color-hover); + } +} + +.ti3-swiper-finite-arrow { + cursor: not-allowed; + + ti-icon[local] { + color: var(--ti-swiper-finite-icon-color); + } +} + +// 左侧按钮 +.ti3-swiper-prev { + left: var(--ti-common-space-0); +} + +// 右侧按钮 +.ti3-swiper-next { + right: var(--ti-common-space-0); +} + +// 多图轮播左右按钮样式 +:host.ti3-swiper-multiple { + .ti3-swiper-wrapper { + margin: 0 var(--ti-common-space-10); + } + + .ti3-swiper-arrow { + position: static; + background: var(--ti-swiper-multiple-icon-bg-color); + color: var(--ti-common-color-icon-normal); + + &:not(.ti3-swiper-finite-arrow):hover, + &:not(.ti3-swiper-finite-arrow):focus { + background: var(--ti-swiper-multiple-icon-bg-color-hover); + color: var(--ti-common-color-icon-hover); + } + } +} + +// 卡片容器 +.ti3-swiper-wrapper { + position: relative; // 让卡片相对于该元素定位 + overflow-x: hidden; + width: 100%; + + .ti3-swiper-cards-container { + position: relative; + display: flex; + } +} + +// 面板指示点样式 +.ti3-swiper-pagination-bullets { + text-align: center; + margin-top: var(--ti-swiper-pagination-space); + + .ti3-swiper-pagination-bullet { + display: inline-block; + .box-sizing(border-box); + border: var(--ti-swiper-bullet-border) solid var(--ti-common-color-line-normal); + background-color: var(--ti-common-color-line-normal); + border-radius: var(--ti-swiper-bullet-border); + margin: 0 0 0 var(--ti-swiper-pagination-bullets-space); + cursor: pointer; + + &:first-child { + margin: 0; + } + + // 激活后当前bullet样式 + &.ti3-swiper-pagination-bullet-active { + width: var(--ti-common-size-5x); + border-color: var(--ti-common-color-line-hover); + background-color: var(--ti-common-color-line-hover); + } + } +} + +// 指示器在底部样式 +.ti3-swiper-pagination-bullets-bottom { + position: absolute; + width: 100%; + bottom: var(--ti-swiper-pagination-space); + margin-top: 0; +} + +// 指示器在右侧样式,预留样式 +.ti3-swiper-pagination-bullets-right { + position: absolute; + width: 5px; + right: var(--ti-swiper-pagination-space); + top: 50%; + transform: translateY(-50%); + margin-top: 0; + + .ti3-swiper-pagination-bullet { + margin: 0 0 var(--ti-swiper-pagination-bullets-space) 0; + + &.ti3-swiper-pagination-bullet-active { + width: 5px; + height: 20px; + border-color: var(--ti-common-color-line-hover); + background-color: var(--ti-common-color-line-hover); + } + } +} diff --git a/src/swiper/lib/src/swipercard.less b/src/swiper/lib/src/swipercard.less new file mode 100644 index 0000000..5d2e8e3 --- /dev/null +++ b/src/swiper/lib/src/swipercard.less @@ -0,0 +1,4 @@ +:host { + box-sizing: border-box; + flex-shrink: 0; // 宽度不够时不压缩 +} diff --git a/src/switch/demo/karma.conf.js b/src/switch/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/switch/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/switch/demo/project.json b/src/switch/demo/project.json new file mode 100644 index 0000000..2c4b9eb --- /dev/null +++ b/src/switch/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/switch/demo", + "sourceRoot": "src/switch/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/switch", + "index": "src/switch/demo/src/index.html", + "main": "src/switch/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/switch/demo/tsconfig.app.json", + "assets": ["src/switch/demo/src/favicon.ico", "src/switch/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "switch-demo:build:production" + }, + "development": { + "browserTarget": "switch-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js switch" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/switch/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/switch/demo/tsconfig.spec.json", + "karmaConfig": "src/switch/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/switch/demo/src/app/AppComponent.ts b/src/switch/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/switch/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/switch/demo/src/app/AppModule.ts b/src/switch/demo/src/app/AppModule.ts new file mode 100644 index 0000000..c3aa27c --- /dev/null +++ b/src/switch/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { SwitchTestModule } from './switch/SwitchTestModule'; + +@NgModule({ + imports: [ + SwitchTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/switch/demo/src/app/IndexComponent.ts b/src/switch/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..9fc0be6 --- /dev/null +++ b/src/switch/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { SwitchTestModule } from './switch/SwitchTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = SwitchTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/switch/demo/src/app/app.html b/src/switch/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/switch/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/switch/demo/src/app/switch/SwitchBasicComponent.ts b/src/switch/demo/src/app/switch/SwitchBasicComponent.ts new file mode 100644 index 0000000..707414c --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-basic.html' +}) +export class SwitchBasicComponent { + switchState: boolean = true; +} diff --git a/src/switch/demo/src/app/switch/SwitchBeforeComponent.ts b/src/switch/demo/src/app/switch/SwitchBeforeComponent.ts new file mode 100644 index 0000000..35468a3 --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchBeforeComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { TiSwitchComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: './switch-before.html' +}) +export class SwitchBeforeComponent { + myLogs: Array = []; + switchState: boolean = true; + + onBeforeChange(component: TiSwitchComponent): void { + if (window.confirm('您确定切换状态吗?')) { + this.switchState = !this.switchState; + this.myLogs = [...this.myLogs, `BeforeChange event = 确认切换状态`]; + } + } +} diff --git a/src/switch/demo/src/app/switch/SwitchDisabledComponent.ts b/src/switch/demo/src/app/switch/SwitchDisabledComponent.ts new file mode 100644 index 0000000..2f7bbcc --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchDisabledComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-disabled.html' +}) +export class SwitchDisabledComponent { + switchState: boolean = true; +} diff --git a/src/switch/demo/src/app/switch/SwitchEventComponent.ts b/src/switch/demo/src/app/switch/SwitchEventComponent.ts new file mode 100644 index 0000000..1612756 --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchEventComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-event.html' +}) +export class SwitchEventComponent { + switchState: boolean = true; + myLogs: Array = []; + onNgModelChange(state: boolean): void { + this.myLogs = [...this.myLogs, `onNgModelChange,state= ${state}`]; + } + onFocus(event: FocusEvent): void { + this.myLogs = [...this.myLogs, 'onFocus']; + } + onBlur(event: FocusEvent): void { + this.myLogs = [...this.myLogs, 'onBlur']; + } + onChange(state: boolean): void { + this.myLogs = [...this.myLogs, `onChange,state= ${state}`]; + } +} diff --git a/src/switch/demo/src/app/switch/SwitchExplanationComponent.ts b/src/switch/demo/src/app/switch/SwitchExplanationComponent.ts new file mode 100644 index 0000000..0575290 --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchExplanationComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-explanation.html' +}) +export class SwitchExplanationComponent { + switchState: boolean = true; + textOn: string = '编辑'; + textOff: string = '查看'; +} diff --git a/src/switch/demo/src/app/switch/SwitchFocusComponent.ts b/src/switch/demo/src/app/switch/SwitchFocusComponent.ts new file mode 100644 index 0000000..04cd013 --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchFocusComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-focus.html' +}) +export class SwitchFocusComponent { + switchState1: boolean = true; + switchState2: boolean = true; + switchState3: boolean = true; + switchState4: boolean = true; + switchState5: boolean = true; + switchState6: boolean = true; + myDisabled: boolean = false; +} diff --git a/src/switch/demo/src/app/switch/SwitchIdComponent.ts b/src/switch/demo/src/app/switch/SwitchIdComponent.ts new file mode 100644 index 0000000..30778f0 --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchIdComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-id.html' +}) +export class SwitchIdComponent { + switchState: boolean = true; +} diff --git a/src/switch/demo/src/app/switch/SwitchLoadComponent.ts b/src/switch/demo/src/app/switch/SwitchLoadComponent.ts new file mode 100644 index 0000000..951772d --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchLoadComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-load.html' +}) +export class SwitchLoadComponent { + switchState: boolean = false; + + // 每个组件都用下面六个函数,只改变函数内容 + changeUndefined(): void { + this.switchState = undefined; + } + changeNull(): void { + this.switchState = null; + } + changeWrongType(): void { + const temp: any = 5; + this.switchState = temp; + } + changeZeroData(): void { + this.switchState = false; + } + changeDataA(): void { + this.switchState = false; + } + changeDataB(): void { + this.switchState = true; + } +} diff --git a/src/switch/demo/src/app/switch/SwitchTemplateComponent.ts b/src/switch/demo/src/app/switch/SwitchTemplateComponent.ts new file mode 100644 index 0000000..2b4d1e2 --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchTemplateComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './switch-template.html' +}) +export class SwitchTemplateComponent { + switchState: boolean = true; +} diff --git a/src/switch/demo/src/app/switch/SwitchTestModule.ts b/src/switch/demo/src/app/switch/SwitchTestModule.ts new file mode 100644 index 0000000..69cdde1 --- /dev/null +++ b/src/switch/demo/src/app/switch/SwitchTestModule.ts @@ -0,0 +1,69 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiIconModule, TiSwitchModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { SwitchDisabledComponent } from './SwitchDisabledComponent'; +import { SwitchBasicComponent } from './SwitchBasicComponent'; +import { SwitchExplanationComponent } from './SwitchExplanationComponent'; +import { SwitchBeforeComponent } from './SwitchBeforeComponent'; +import { SwitchEventComponent } from './SwitchEventComponent'; +import { SwitchFocusComponent } from './SwitchFocusComponent'; +import { SwitchIdComponent } from './SwitchIdComponent'; +import { SwitchLoadComponent } from './SwitchLoadComponent'; +import { SwitchTemplateComponent } from './SwitchTemplateComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiButtonModule, + TiIconModule, + TiSwitchModule, + DemoLogModule, + RouterModule.forChild(SwitchTestModule.ROUTES) + ], + declarations: [ + SwitchBasicComponent, + SwitchDisabledComponent, + SwitchExplanationComponent, + SwitchTemplateComponent, + SwitchBeforeComponent, + SwitchEventComponent, + SwitchFocusComponent, + SwitchIdComponent, + SwitchLoadComponent + ] +}) +export class SwitchTestModule { + static readonly LINKS: Array = [{ href: 'components/TiSwitchComponent.html', label: 'Switch' }]; + static readonly ROUTES: Routes = [ + { + path: 'switch/switch-basic', + component: SwitchBasicComponent + }, + { + path: 'switch/switch-disabled', + component: SwitchDisabledComponent + }, + { + path: 'switch/switch-explanation', + component: SwitchExplanationComponent + }, + { + path: 'switch/switch-template', + component: SwitchTemplateComponent + }, + { + path: 'switch/switch-before', + component: SwitchBeforeComponent + }, + { path: 'switch/switch-focus', component: SwitchFocusComponent }, + { path: 'switch/switch-id', component: SwitchIdComponent }, + { path: 'switch/switch-load', component: SwitchLoadComponent }, + { path: 'switch/switch-event', component: SwitchEventComponent } + ]; +} diff --git a/src/switch/demo/src/app/switch/switch-basic.html b/src/switch/demo/src/app/switch/switch-basic.html new file mode 100644 index 0000000..3b2d640 --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-basic.html @@ -0,0 +1 @@ + diff --git a/src/switch/demo/src/app/switch/switch-before.html b/src/switch/demo/src/app/switch/switch-before.html new file mode 100644 index 0000000..c664508 --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-before.html @@ -0,0 +1,2 @@ + + diff --git a/src/switch/demo/src/app/switch/switch-disabled.html b/src/switch/demo/src/app/switch/switch-disabled.html new file mode 100644 index 0000000..ca60a88 --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-disabled.html @@ -0,0 +1 @@ + diff --git a/src/switch/demo/src/app/switch/switch-event.html b/src/switch/demo/src/app/switch/switch-event.html new file mode 100644 index 0000000..213ca84 --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-event.html @@ -0,0 +1,19 @@ +

    描述

    +

    Switch开关组件,事件用法

    +

    注意:(ngModelChange)="onNgModelChange($event)" 监听绑定值的改变

    +

    示例

    +

    1.[(ngModel)]双向绑定开关状态

    +开关: + + +
    +switchState:{{switchState}} + +

    事件日志:

    + diff --git a/src/switch/demo/src/app/switch/switch-explanation.html b/src/switch/demo/src/app/switch/switch-explanation.html new file mode 100644 index 0000000..f7a4cfc --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-explanation.html @@ -0,0 +1 @@ + diff --git a/src/switch/demo/src/app/switch/switch-focus.html b/src/switch/demo/src/app/switch/switch-focus.html new file mode 100644 index 0000000..7cc2698 --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-focus.html @@ -0,0 +1,29 @@ +

    描述

    +

    Switch开关组件,焦点用法

    +

    示例

    + +

    1.autofocus:

    +

    + +

    2.focus():

    +
    +
    + +  + +  + +  + +

    + +

    3.tabindex:

    +tabindex="1" +

    +tabindex="3" +

    +tabindex="4" +

    +tabindex="2" + + diff --git a/src/switch/demo/src/app/switch/switch-id.html b/src/switch/demo/src/app/switch/switch-id.html new file mode 100644 index 0000000..dca01d0 --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-id.html @@ -0,0 +1,8 @@ +

    描述

    +

    Switch开关组件,id

    +

    示例

    +

    1.[(ngModel)]双向绑定开关状态

    +开关: + +
    +switchState:{{switchState}} diff --git a/src/switch/demo/src/app/switch/switch-load.html b/src/switch/demo/src/app/switch/switch-load.html new file mode 100644 index 0000000..9c98732 --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-load.html @@ -0,0 +1,15 @@ +

    描述

    +

    Switch开关组件,加载数据

    +

    示例

    +

    1.[(ngModel)]双向绑定开关状态

    +开关: + +
    +switchState:{{switchState}} +

    每个组件改变数据,都用下面六个按钮。不变化

    +
    +
    +
    +
    +
    +
    diff --git a/src/switch/demo/src/app/switch/switch-template.html b/src/switch/demo/src/app/switch/switch-template.html new file mode 100644 index 0000000..ad3227f --- /dev/null +++ b/src/switch/demo/src/app/switch/switch-template.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/switch/demo/src/app/switch/webdoc/switch-demos.js b/src/switch/demo/src/app/switch/webdoc/switch-demos.js new file mode 100644 index 0000000..825ea5b --- /dev/null +++ b/src/switch/demo/src/app/switch/webdoc/switch-demos.js @@ -0,0 +1,74 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'switch-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Switch 组件的基本用法。

    ', + 'en-US': '

    basic

    ', + }, + }, + { + demoId: 'switch-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': '

    disabled

    ', + }, + apis: ['TiSwitchComponent.properties.disabled'], + }, + { + demoId: 'switch-before', + name: { + 'zh-CN': '开关状态拦截', + 'en-US': 'beforeChange', + }, + desc: { + 'zh-CN': + '

    当开关状态将要发生变化的时候触发beforeChange事件。

    ', + 'en-US': '

    beforeChange

    ', + }, + apis: ['TiSwitchComponent.events.beforeChange'], + }, + { + demoId: 'switch-explanation', + name: { + 'zh-CN': '自定义文本', + 'en-US': 'explanation', + }, + desc: { + 'zh-CN': + '

    通过属性onTextoffText配置开关时显示的文本。

    ', + 'en-US': '

    explanation

    ', + }, + apis: [ + 'TiSwitchComponent.properties.onText', + 'TiSwitchComponent.properties.offText', + ], + }, + { + demoId: 'switch-template', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'template', + }, + desc: { + 'zh-CN': + '

    通过模板onoff配置开关时显示的自定义内容。

    ', + 'en-US': '

    template

    ', + }, + apis: [ + 'TiSwitchComponent.slots.onTemplate', + 'TiSwitchComponent.slots.offTemplate', + ], + }, + ], +}; diff --git a/src/switch/demo/src/app/switch/webdoc/switch.cn.md b/src/switch/demo/src/app/switch/webdoc/switch.cn.md new file mode 100644 index 0000000..c9075d1 --- /dev/null +++ b/src/switch/demo/src/app/switch/webdoc/switch.cn.md @@ -0,0 +1,23 @@ +--- +title: Switch 开关 +--- +# Switch 开关 + +
    + +Switch 是提供开关状态或两种状态切换的组件。   + +```typescript +import { TiSwitchModule } from '@opentiny/ng'; +``` + +
    + +
    + +Switch 是提供开关状态或两种状态切换的组件。   + +```typescript +import { TiSwitchModule } from '@opentiny/ng'; +``` +
    diff --git a/src/switch/demo/src/app/switch/webdoc/switch.en.md b/src/switch/demo/src/app/switch/webdoc/switch.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/switch/demo/src/app/switch/webdoc/switch.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/switch/demo/src/favicon.ico b/src/switch/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/switch/demo/src/index.html b/src/switch/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/switch/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/switch/demo/src/main.ts b/src/switch/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/switch/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/switch/demo/test.ts b/src/switch/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/switch/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/switch/demo/tsconfig.app.json b/src/switch/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/switch/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/switch/demo/tsconfig.spec.json b/src/switch/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/switch/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/switch/lib/index.ts b/src/switch/lib/index.ts new file mode 100644 index 0000000..6053d89 --- /dev/null +++ b/src/switch/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiSwitchModule'; diff --git a/src/switch/lib/ng-package.json b/src/switch/lib/ng-package.json new file mode 100644 index 0000000..66797dd --- /dev/null +++ b/src/switch/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/switch", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/switch/lib/package.json b/src/switch/lib/package.json new file mode 100644 index 0000000..ccbe22f --- /dev/null +++ b/src/switch/lib/package.json @@ -0,0 +1,10 @@ +{ + "name": "@opentiny/ng-switch", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/switch/lib/project.json b/src/switch/lib/project.json new file mode 100644 index 0000000..eb9a670 --- /dev/null +++ b/src/switch/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/switch/lib", + "sourceRoot": "src/switch/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/switch"], + "options": { + "project": "src/switch/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/switch"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js switch" + }, + { + "command": "ng default-build switch" + }, + { + "command": "node build/clear-default-theme.js switch" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/switch && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build switch && ng pack switch && node build/publish.js switch --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/switch/lib/src/TiSwitchComponent.ts b/src/switch/lib/src/TiSwitchComponent.ts new file mode 100644 index 0000000..e28340a --- /dev/null +++ b/src/switch/lib/src/TiSwitchComponent.ts @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + Component, + ContentChild, + ElementRef, + EventEmitter, + Input, + Output, + TemplateRef, + ViewChild +} from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * Switch开关组件 + * + * 用于实现页面中的开关操作。 + * + */ +@Component({ + selector: 'ti-switch', + templateUrl: './switch.html', + styleUrls: ['./switch.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-switch-container]': 'true' + }, + providers: [TiFormComponent.getValueAccessor(TiSwitchComponent)] +}) +export class TiSwitchComponent extends TiFormComponent { + /** + * 打开状态的显示文本 + */ + @Input() onText: string = ''; + /** + * 关闭状态的显示文本 + */ + @Input() offText: string = ''; + /** + * 开关切换前触发的回调,参数:组件实例 + */ + @Output() readonly beforeChange: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + @ViewChild('a', { static: true }) aRef: ElementRef; + /** + * 打开状态的自定义模板 + */ + @ContentChild('on', { static: true }) onTemplate: TemplateRef; + /** + * 关闭状态的自定义模板 + */ + @ContentChild('off', { static: true }) offTemplate: TemplateRef; + protected versionInfo: string = super.getVersion(packageInfo); + private initialized: boolean = false; + private hasAnimation: boolean = false; + + ngOnInit(): void { + // 基类中做了设置宿主id的操作 + super.ngOnInit(); + this.setFocusableElems([this.aRef.nativeElement]); + } + /** + * @ignore + */ + ngOnModelChange(value: boolean): void { + super.ngOnModelChange(value); + if (value === null) { + // 以 ngModel 的形式传入值时, writeValue首次传入null + return; + } + + if (this.initialized && !this.hasAnimation) { + // 保证初始时没有动画 + this.renderer.addClass(this.aRef.nativeElement, 'ti3-switch-animation'); + this.hasAnimation = true; + } + this.initialized = true; + } + + /** + * 切换开关状态 + */ + private toggle(): void { + if (this.beforeChange.observers.length === 0) { + // 用户未定义beforeChange + this.model = !this.model; + } else { + this.beforeChange.emit(this); + } + } + /** + * @ignore + * 点击事件 + */ + public onClick(): void { + if (!this.disabled) { + this.toggle(); + } + } +} diff --git a/src/switch/lib/src/TiSwitchModule.ts b/src/switch/lib/src/TiSwitchModule.ts new file mode 100644 index 0000000..c73609c --- /dev/null +++ b/src/switch/lib/src/TiSwitchModule.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiSwitchComponent } from './TiSwitchComponent'; + +@NgModule({ + imports: [CommonModule], + exports: [TiSwitchComponent], + declarations: [TiSwitchComponent] +}) +export class TiSwitchModule {} +export { TiSwitchComponent } from './TiSwitchComponent'; diff --git a/src/switch/lib/src/switch.html b/src/switch/lib/src/switch.html new file mode 100644 index 0000000..cc623ef --- /dev/null +++ b/src/switch/lib/src/switch.html @@ -0,0 +1,30 @@ + +
    + + +
    + {{onText}} +
    +
    + +
    + {{offText}} +
    +
    +
    +
    + +
    diff --git a/src/switch/lib/src/switch.less b/src/switch/lib/src/switch.less new file mode 100644 index 0000000..69e03e0 --- /dev/null +++ b/src/switch/lib/src/switch.less @@ -0,0 +1,125 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-switch-height: var(--ti-common-size-5x); + --ti-switch-width: 38px; + --ti-switch-pointer-width: var(--ti-common-size-4x); + --ti-switch-pointer-space: 2px; + --ti-switch-pointer-explanation-space: var(--ti-common-space-6); +} + +:host.ti3-switch-container { + display: inline-block; + height: var(--ti-switch-height); +} + +// 1.组件通用样式:与hover、focused、disabled等状态无关 +.ti3-switch { + width: var(--ti-switch-width); + height: var(--ti-switch-height); + display: inline-block; + position: relative; + cursor: pointer; + .user-select(none); + + .ti3-switch-track { + position: relative; + width: 100%; + height: 100%; + z-index: 0; + border-radius: var(--ti-switch-height); + .box-sizing (border-box); + } + + .ti3-switch-pointer { + display: inline-block; + position: absolute; + top: calc((var(--ti-switch-height) - var(--ti-switch-pointer-width)) / 2); + z-index: 8; + width: var(--ti-switch-pointer-width); + height: var(--ti-switch-pointer-width); + background: var(--ti-common-color-bg-white-normal); + border-radius: var(--ti-common-border-radius-3); + .box-sizing (border-box); + } + + &.ti3-switch-off { + & .ti3-switch-track { + background: var(--ti-common-color-bg-secondary); + } + + & .ti3-switch-pointer { + left: var(--ti-switch-pointer-space); + } + } + + &.ti3-switch-on { + & .ti3-switch-track { + background: var(--ti-common-color-bg-emphasize); + } + + & .ti3-switch-pointer { + left: calc(100% - var(--ti-switch-pointer-space) - var(--ti-switch-pointer-width)); + } + } + + &.ti3-switch-with-explanation { + width: auto; + + .ti3-switch-track { + width: auto; + } + + .ti3-switch-explanation-container { + display: inline-block; + line-height: var(--ti-switch-height); + color: var(--ti-common-color-text-white); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + } + + &.ti3-switch-off { + .ti3-switch-explanation-container { + padding-left: calc(var(--ti-switch-pointer-explanation-space) + var(--ti-switch-pointer-width) + var(--ti-switch-pointer-space)); + padding-right: var(--ti-common-space-10); + } + } + + &.ti3-switch-on { + .ti3-switch-explanation-container { + padding-left: var(--ti-common-space-10); + padding-right: calc(var(--ti-switch-pointer-explanation-space) + var(--ti-switch-pointer-width) + var(--ti-switch-pointer-space)); + } + } + } +} + +// 2.灰化状态下的样式 +.ti3-switch[disabled] { + cursor: not-allowed; + + & .ti3-switch-pointer { + background: var(--ti-common-color-bg-disabled); + } + + &.ti3-switch-on { + & .ti3-switch-track { + background-color: var(--ti-common-color-bg-light-emphasize); + } + } + + &.ti3-switch-off { + & .ti3-switch-track { + background-color: var(--ti-common-color-bg-dark-disabled); + } + } +} + +/************************************动效************************************************************************/ +.ti3-switch.ti3-switch-animation { + .ti3-switch-track, + .ti3-switch-pointer { + transition: all 0.25s linear; + } +} +/************************************动效************************************************************************/ diff --git a/src/tab/demo/karma.conf.js b/src/tab/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/tab/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/tab/demo/project.json b/src/tab/demo/project.json new file mode 100644 index 0000000..45d2968 --- /dev/null +++ b/src/tab/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/tab/demo", + "sourceRoot": "src/tab/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/tab", + "index": "src/tab/demo/src/index.html", + "main": "src/tab/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tab/demo/tsconfig.app.json", + "assets": ["src/tab/demo/src/favicon.ico", "src/tab/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "tab-demo:build:production" + }, + "development": { + "browserTarget": "tab-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js tab" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/tab/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tab/demo/tsconfig.spec.json", + "karmaConfig": "src/tab/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/tab/demo/src/app/AppComponent.ts b/src/tab/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/tab/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/tab/demo/src/app/AppModule.ts b/src/tab/demo/src/app/AppModule.ts new file mode 100644 index 0000000..b2b2c82 --- /dev/null +++ b/src/tab/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TabTestModule } from './tab/TabTestModule'; + +@NgModule({ + imports: [ + TabTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/tab/demo/src/app/IndexComponent.ts b/src/tab/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..910f9fe --- /dev/null +++ b/src/tab/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TabTestModule } from './tab/TabTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TabTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/tab/demo/src/app/app.html b/src/tab/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/tab/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/tab/demo/src/app/tab/TabBasicComponent.ts b/src/tab/demo/src/app/tab/TabBasicComponent.ts new file mode 100644 index 0000000..ae8f648 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabBasicComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './tab-basic.html' +}) +export class TabBasicComponent { + tabs: any = [ + { + title: 'Tab1', + active: true + }, + { + title: 'Tab2', + disabled: true + }, + { + title: 'Tab3', + active: false + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabBeforeactivechangeComponent.ts b/src/tab/demo/src/app/tab/TabBeforeactivechangeComponent.ts new file mode 100644 index 0000000..964831a --- /dev/null +++ b/src/tab/demo/src/app/tab/TabBeforeactivechangeComponent.ts @@ -0,0 +1,34 @@ +import { Component } from '@angular/core'; +import { TiTabsComponent } from '@opentiny/ng'; +@Component({ + templateUrl: './tab-beforeactivechange.html' +}) +export class TabBeforeactivechangeComponent { + myLogs: Array = []; + tabs: any = [ + { + title: 'Tab1', + active: true, + onActiveChange: (isActive: boolean): void => { + if (isActive) { + this.myLogs = [...this.myLogs, 'tab1: not active => active']; + } else { + this.myLogs = [...this.myLogs, 'tab1: active => not active']; + } + } + }, + { + title: 'Tab2' + }, + { + title: 'Tab3', + beforeActiveChange: (tabs: TiTabsComponent): void => { + this.myLogs = [...this.myLogs, 'tab3: beforeActiveChange trigger']; + setTimeout(() => { + tabs.changeActive(tabs.clickTab); + this.myLogs = [...this.myLogs, 'tab3: changeActive trigger at 1000ms']; + }, 1000); + } + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabContentCompComponent.ts b/src/tab/demo/src/app/tab/TabContentCompComponent.ts new file mode 100644 index 0000000..f82248c --- /dev/null +++ b/src/tab/demo/src/app/tab/TabContentCompComponent.ts @@ -0,0 +1,100 @@ +import { Component, Injector, Input, Injectable } from '@angular/core'; + +@Component({ + templateUrl: './tab-content-comp.html' +}) +export class TabContentCompComponent { + title1: string = 'Libary'; + title2: string = 'Company'; + title3: string = 'Concert'; + active: boolean = true; + type: string = 'pop song'; + + tabs: Array = [ + { + title: 'Libary', + component: LibaryComponent + }, + { + title: 'Company', + active: true, + component: CompanyComponent + }, + { + title: 'Concert1', + component: Concert1Component, + token: Greeter, + data: { suffix: 'hippop', name: 'violet' } // 给组件传参 + } + ]; + + constructor(private injector: Injector) {} + + getInjector(tab: any): any { + if (tab.token && tab.data) { + return Injector.create([{ provide: tab.token, useValue: tab.data }], this.injector); + } + } +} + +@Component({ + selector: 'app-libary', + template: `

    + I'm the content component of {{ name }}. +

    ` +}) +export class LibaryComponent { + name: string = 'Libary'; +} + +@Component({ + selector: 'app-company', + template: `

    + I'm the content component of {{ place }}. +

    ` +}) +export class CompanyComponent { + place: string = 'Company'; +} + +@Component({ + selector: 'app-concert', + template: `

    + I'm the content component of {{ text }}. +

    +

    Params: {{ type }}

    +

    ` +}) +export class ConcertComponent { + text: string = 'Concert'; + @Input() type: string; +} + +@Injectable() +export class Greeter { + suffix: string; + name: string; +} + +@Component({ + selector: 'app-concert1', + template: `

    + I'm the content component of {{ text }}. +

    +

    + Params: {{ concert1.suffix }} +

    +

    +

    + Params: {{ concert1.name }} +

    +

    ` +}) +export class Concert1Component { + text: string = 'Concert1'; + constructor(public concert1: Greeter) {} +} diff --git a/src/tab/demo/src/app/tab/TabCustomHeadComponent.ts b/src/tab/demo/src/app/tab/TabCustomHeadComponent.ts new file mode 100644 index 0000000..d7c49e1 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabCustomHeadComponent.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './tab-custom-head.html', + styles: [ + ` + .sum-color { + color: red; + } + ` + ] +}) +export class TabCustomHeadComponent { + tabs: any = [ + { + title: 'Tab1', + active: false + }, + { + title: 'Tab2', + active: true + }, + { + title: 'Tab3', + active: false + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabDarkComponent.ts b/src/tab/demo/src/app/tab/TabDarkComponent.ts new file mode 100644 index 0000000..9633518 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabDarkComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './tab-dark.html' +}) +export class TabDarkComponent { + tabs: any = [ + { + title: 'Tab1', + active: true + }, + { + title: 'Tab2' + }, + { + title: 'Tab3' + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabDefaultTestComponent.ts b/src/tab/demo/src/app/tab/TabDefaultTestComponent.ts new file mode 100644 index 0000000..7a8a334 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabDefaultTestComponent.ts @@ -0,0 +1,89 @@ +import { Component, OnInit } from '@angular/core'; +@Component({ + templateUrl: './tab-default-test.html', + styles: [ + ` + .sum-color { + color: red; + } + ` + ] +}) +export class TabDefaultTestComponent implements OnInit { + hiden: boolean = false; + tabsId: string = 'tabs_test'; + notShow: boolean = true; + tab1: any = { + id: 'tab1', + sum: 26, + active: false, + title: '大法师', + show: false + }; + otherTabs: Array = [ + { + title: 'Profile', + id: 'tab2', + disabled: true, + active: false, + content: "Raw denim you probably haven't heard of them jean shorts Austin. ", + child: [{ title: 'daxiao' }, { title: 'hello' }] + }, + { + title: 'school', + id: 'tab3', + content: 'show me the school', + active: false, + child: [{ title: 'happy' }, { title: 'marray' }] + }, + { + title: 'About', + id: 'tab4', + content: 'Dynamic content 2', + disabled: false, + active: true, + child: [{ title: 'tinger' }] + } + ]; + + ngOnInit(): void { + setTimeout(() => { + this.tab1.show = true; + this.notShow = false; + }, 3000); + } + + onActiveChange(isActive: boolean, tabId: string): void { + if (isActive) { + console.log(tabId, 'not active => active'); + } else { + console.log(tabId, 'active => not active'); + } + } + + showTabs(): void { + this.hiden = !this.hiden; + } + + activeTab3(): void { + this.otherTabs[1]['active'] = true; + } + + remove(): void { + this.otherTabs.splice(1, 1); + } + + add(): void { + this.otherTabs.push({ + title: '添加', + content: '添加的', + active: true, + child: [{ title: 'freedom' }] + }); + } + + changeHeader(): void { + this.otherTabs[1]['title'] = 'dfasl dajdjfaksj'; + this.tab1.title = 'Our Good and peaceful'; + } +} diff --git a/src/tab/demo/src/app/tab/TabLazyLoadComponent.ts b/src/tab/demo/src/app/tab/TabLazyLoadComponent.ts new file mode 100644 index 0000000..0a1b9f8 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabLazyLoadComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tab-lazy-load.html' +}) +export class TabLazyLoadComponent { + tabs: any = [ + { + title: 'Tab1', + active: true + }, + { + title: 'Tab2' + }, + { + title: 'Tab3' + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabLevel2Component.ts b/src/tab/demo/src/app/tab/TabLevel2Component.ts new file mode 100644 index 0000000..a3d38b9 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabLevel2Component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './tab-level2.html' +}) +export class TabLevel2Component { + tabs: any = [ + { + title: 'Tab1', + active: true + }, + { + title: 'Tab2' + }, + { + title: 'Tab3' + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabLevel2TestComponent.ts b/src/tab/demo/src/app/tab/TabLevel2TestComponent.ts new file mode 100644 index 0000000..97b9287 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabLevel2TestComponent.ts @@ -0,0 +1,50 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tab-level2-test.html' +}) +export class TabLevel2TestComponent { + hiden: boolean = false; + tabsId: string = 'tabs_test'; + tab1: any = { + title: 'Home', + id: 'tab1', + active: false + }; + otherTabs: Array = [ + { + title: 'Profile', + id: 'tab2', + active: true, + disabled: false, + content: `Raw denim you probably haven't heard of them jean shorts Austin. ` + }, + { + title: 'school', + id: 'tab3', + content: 'show me the school' + }, + { + title: 'About', + id: 'tab4', + content: 'Dynamic content 2', + disabled: true + } + ]; + + onActiveChange(isActive: boolean): void { + if (isActive) { + console.log('not active => active'); + } else { + console.log('active => not active'); + } + } + + showTabs = (): void => { + this.hiden = !this.hiden; + }; + + activeTab3 = (): void => { + this.otherTabs[1]['active'] = true; + }; +} diff --git a/src/tab/demo/src/app/tab/TabOverflowComponent.ts b/src/tab/demo/src/app/tab/TabOverflowComponent.ts new file mode 100644 index 0000000..a7bb661 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabOverflowComponent.ts @@ -0,0 +1,46 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tab-overflow.html' +}) +export class TabOverflowComponent { + tabs: Array = [ + { + title: 'Tab1', + active: true + }, + { + title: 'Tab2' + }, + { + title: 'Tab3' + }, + { + title: 'Tab4' + }, + { + title: 'Tab5' + }, + { + title: 'Tab6' + }, + { + title: 'Tab7' + }, + { + title: 'Tab8' + }, + { + title: 'Tab9' + }, + { + title: 'Tab10' + }, + { + title: 'Tab11' + }, + { + title: 'Tab12' + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabRouteComponent.ts b/src/tab/demo/src/app/tab/TabRouteComponent.ts new file mode 100644 index 0000000..9cec9ca --- /dev/null +++ b/src/tab/demo/src/app/tab/TabRouteComponent.ts @@ -0,0 +1,66 @@ +import { Component } from '@angular/core'; +import { ActivatedRoute, NavigationEnd, Router, UrlTree } from '@angular/router'; + +@Component({ + template: `
    Welcome to School!
    ` +}) +export class SchoolComponent {} + +@Component({ + template: `
    Welcome to home!
    ` +}) +export class HomeComponent {} + +@Component({ + templateUrl: './tab-route.html' +}) +export class TabRouteComponent { + tabs: any = [ + { + title: 'home', + active: false, + onActiveChange: (isActive: boolean, title: string): void => { + if (isActive) { + this.router.navigate([title], { relativeTo: this.activeRoute }); + } + } + }, + { + title: 'school', + active: true, + onActiveChange: (isActive: boolean, title: string): void => { + if (isActive) { + this.router.navigate([title], { relativeTo: this.activeRoute }); + } + } + } + ]; + + private urlArray: Array = this.getUrlTree(this.router); + constructor(private router: Router, private activeRoute: ActivatedRoute) { + // 页面回退时,监听当前路由设置激活项 + router.events.forEach((event: any) => { + if (event instanceof NavigationEnd) { + const index: number = this.urlArray.findIndex((url: UrlTree) => { + return this.router.isActive(url, false); + }); + + if (this.tabs[index]) { + this.tabs[index].active = true; + } + } + }); + } + + private getUrlTree(router: Router): Array { + const urlArry: Array = []; + for (const tab of this.tabs) { + const url: UrlTree = router.createUrlTree([tab.title], { + relativeTo: this.activeRoute + }); + urlArry.push(url); + } + + return urlArry; + } +} diff --git a/src/tab/demo/src/app/tab/TabScrollComponent.ts b/src/tab/demo/src/app/tab/TabScrollComponent.ts new file mode 100644 index 0000000..66e535d --- /dev/null +++ b/src/tab/demo/src/app/tab/TabScrollComponent.ts @@ -0,0 +1,31 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + templateUrl: './tab-scroll.html' +}) +export class TabScrollComponent implements OnInit { + items: Array = []; + + tabs: Array = [ + { + title: 'Libary' + }, + { + title: 'Company', + active: true + }, + { + title: 'Concert1' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 15; j++) { + this.items.push((j * 113) % 29); + } + } + + addItems(): void { + this.items.push(333); + } +} diff --git a/src/tab/demo/src/app/tab/TabSmallComponent.ts b/src/tab/demo/src/app/tab/TabSmallComponent.ts new file mode 100644 index 0000000..c1f8205 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabSmallComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './tab-small.html' +}) +export class TabSmallComponent { + tabs: any = [ + { + title: 'Tab1', + active: true + }, + { + title: 'Tab2' + }, + { + title: 'Tab3' + } + ]; +} diff --git a/src/tab/demo/src/app/tab/TabTestModule.ts b/src/tab/demo/src/app/tab/TabTestModule.ts new file mode 100644 index 0000000..fcc46a2 --- /dev/null +++ b/src/tab/demo/src/app/tab/TabTestModule.ts @@ -0,0 +1,102 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiFormfieldModule, TiTabModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TabBasicComponent } from './TabBasicComponent'; +import { TabLevel2Component } from './TabLevel2Component'; +import { TabCustomHeadComponent } from './TabCustomHeadComponent'; +import { TabLazyLoadComponent } from './TabLazyLoadComponent'; +import { HomeComponent, SchoolComponent, TabRouteComponent } from './TabRouteComponent'; +import { TabDefaultTestComponent } from './TabDefaultTestComponent'; +import { TabLevel2TestComponent } from './TabLevel2TestComponent'; +import { TabOverflowComponent } from './TabOverflowComponent'; +import { TabSmallComponent } from './TabSmallComponent'; +import { TabBeforeactivechangeComponent } from './TabBeforeactivechangeComponent'; +import { TabDarkComponent } from './TabDarkComponent'; +import { CompanyComponent, Concert1Component, ConcertComponent, LibaryComponent, TabContentCompComponent } from './TabContentCompComponent'; +import { TabScrollComponent } from './TabScrollComponent'; + +@NgModule({ + imports: [CommonModule, RouterModule, TiTabModule, TiFormfieldModule, DemoLogModule, RouterModule.forChild(TabTestModule.ROUTES)], + declarations: [ + TabBasicComponent, + TabBeforeactivechangeComponent, + TabLevel2Component, + TabCustomHeadComponent, + TabLazyLoadComponent, + TabRouteComponent, + TabOverflowComponent, + TabSmallComponent, + HomeComponent, + SchoolComponent, + TabDefaultTestComponent, + TabLevel2TestComponent, + TabContentCompComponent, + CompanyComponent, + ConcertComponent, + Concert1Component, + LibaryComponent, + TabScrollComponent, + TabDarkComponent + ] +}) +export class TabTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiTabsComponent.html', label: 'Tabs' }, + { href: 'components/TiTabComponent.html', label: 'Tab' }, + { href: 'components/TiTabHeaderComponent.html', label: 'TabHeader' } + ]; + static readonly ROUTES: Routes = [ + { + path: 'tab/tab-basic', + component: TabBasicComponent + }, + { + path: 'tab/tab-beforeactivechange', + component: TabBeforeactivechangeComponent + }, + { + path: 'tab/tab-dark', + component: TabDarkComponent + }, + { + path: 'tab/tab-level2', + component: TabLevel2Component + }, + { + path: 'tab/tab-custom-head', + component: TabCustomHeadComponent + }, + { + path: 'tab/tab-lazy-load', + component: TabLazyLoadComponent + }, + { + path: 'tab/tab-overflow', + component: TabOverflowComponent + }, + { + path: 'tab/tab-route', + component: TabRouteComponent, + children: [ + { path: 'home', component: HomeComponent }, + { path: 'school', component: SchoolComponent }, + { path: '', redirectTo: 'school', pathMatch: 'full' } + ] + }, + { + path: 'tab/tab-content-comp', + component: TabContentCompComponent + }, + { + path: 'tab/tab-small', + component: TabSmallComponent + }, + { path: 'tab/tab-default-test', component: TabDefaultTestComponent }, + { path: 'tab/tab-level2-test', component: TabLevel2TestComponent }, + { path: 'tab/tab-scroll', component: TabScrollComponent } + ]; +} diff --git a/src/tab/demo/src/app/tab/tab-basic.html b/src/tab/demo/src/app/tab/tab-basic.html new file mode 100644 index 0000000..4cb5ffb --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-basic.html @@ -0,0 +1,11 @@ + + +
    Content of Tab Pane 1
    +
    + +
    Content of Tab Pane 2
    +
    + +
    Content of Tab Pane 3
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-beforeactivechange.html b/src/tab/demo/src/app/tab/tab-beforeactivechange.html new file mode 100644 index 0000000..3512a74 --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-beforeactivechange.html @@ -0,0 +1,23 @@ + + +
    Content of Tab Pane 1
    +
    + +
    Content of Tab Pane 2
    +
    + +
    Content of Tab Pane 3
    +
    +
    + + diff --git a/src/tab/demo/src/app/tab/tab-content-comp.html b/src/tab/demo/src/app/tab/tab-content-comp.html new file mode 100644 index 0000000..f562e50 --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-content-comp.html @@ -0,0 +1,34 @@ +

    描述

    +

    tab页签组件内容区为组件的示例

    +

    示例

    + +

    1.方式一(在内容区直接以标签的形式使用组件):

    +
    +
    + + + + {{title1}} + + + + + + + + + +
    +

    +

    2.方式二(ngFor循环时,在内容区可借助Angular提供的ngComponentOutlet指令插入组件):

    +
    +
    + + + + + + +
    diff --git a/src/tab/demo/src/app/tab/tab-custom-head.html b/src/tab/demo/src/app/tab/tab-custom-head.html new file mode 100644 index 0000000..8a83a14 --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-custom-head.html @@ -0,0 +1,17 @@ + + + {{tabs[0].title}} +
    Content of Tab Pane 1
    +
    + + + {{tabs[1].title}} + (26) + +
    Content of Tab Pane 2
    +
    + + {{tabs[2].title}} +
    Content of Tab Pane 3
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-dark.html b/src/tab/demo/src/app/tab/tab-dark.html new file mode 100644 index 0000000..b6956cb --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-dark.html @@ -0,0 +1,11 @@ + + +
    Content of Tab Pane 1
    +
    + +
    Content of Tab Pane 2
    +
    + +
    Content of Tab Pane 3
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-default-test.html b/src/tab/demo/src/app/tab/tab-default-test.html new file mode 100644 index 0000000..7419cf8 --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-default-test.html @@ -0,0 +1,51 @@ +

    一级页签测试用例

    + +

    场景1:

    +
    + + + + {{tab1.title}} + ({{tab1.sum}}) + + If the expression evaluates to a string, the string should be one or more space-delimited class names.
    + If the expression evaluates to an object. +
    + +
    + {{tab.title}} +
    {{tab.content}}
    +
    {{item.title}}
    +
    +
    +
    +
    +

    + +

    + +

    + +

    + +

    + +

    + +

    场景2:

    +
    + + +
    +
    {{tab.content}}
    +
    {{item.title}}
    +
    +
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-lazy-load.html b/src/tab/demo/src/app/tab/tab-lazy-load.html new file mode 100644 index 0000000..ff42810 --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-lazy-load.html @@ -0,0 +1,18 @@ + + + +
    Content of Tab Pane 1
    +
    +
    + + +
    Content of Tab Pane 2
    +
    {{ item }}
    +
    +
    + + +
    Content of Tab Pane 3
    +
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-level2-test.html b/src/tab/demo/src/app/tab/tab-level2-test.html new file mode 100644 index 0000000..b74f1c5 --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-level2-test.html @@ -0,0 +1,24 @@ +

    二级页签示例

    +
    +
    + + + If the expression evaluates to a string, the string should be one or more space-delimited class names.
    + If the expression evaluates to an object. +
    + + {{tab.title}} +
    {{tab.content}}
    +
    +
    +
    +

    + +
    + diff --git a/src/tab/demo/src/app/tab/tab-level2.html b/src/tab/demo/src/app/tab/tab-level2.html new file mode 100644 index 0000000..95109ea --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-level2.html @@ -0,0 +1,11 @@ + + +
    Content of Tab Pane 1
    +
    + +
    Content of Tab Pane 2
    +
    + +
    Content of Tab Pane 3
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-overflow.html b/src/tab/demo/src/app/tab/tab-overflow.html new file mode 100644 index 0000000..91cfb57 --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-overflow.html @@ -0,0 +1,7 @@ +
    + + +
    Content of Tab Pane {{ i + 1 }}
    +
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-route.html b/src/tab/demo/src/app/tab/tab-route.html new file mode 100644 index 0000000..58a651d --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-route.html @@ -0,0 +1,8 @@ + + + + + +
    + +
    diff --git a/src/tab/demo/src/app/tab/tab-scroll.html b/src/tab/demo/src/app/tab/tab-scroll.html new file mode 100644 index 0000000..d84939f --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-scroll.html @@ -0,0 +1,40 @@ +

    描述

    +

    本测试用例主要测试tab是否影响页面触发滚动条。

    +

    + 问题 1:在有tab的页面,且tab的祖先级级元素display:none, 页面有滚动条时,将页面滚动条 + 拖至formfiled的头部不在视野范围内时,给formfield中添加数据后页面滚动条会异常跳动到顶部,正常滚动条应该不跳动。该问题在10.1.9版本修复 +

    +

    + 问题 2:在有tab的页面,且使用了 ti-tab-header 标签, 页面有滚动条时,将页面滚动条 + 拖至formfiled的头部不在视野范围内时,给formfield中添加数据后页面滚动条会异常跳动到顶部,正常滚动条应该不跳动。该问题在10.1.9版本修复 +

    +

    示例

    +
    + + +
    {{item}}
    +
    +
    + +
    +
    + +
    + + + +
    + +
    + + + + + title + (59) + + If the expression evaluates to a string, the string should be one or more space-delimited class names.
    + If the expression evaluates to an object. +
    +
    +
    diff --git a/src/tab/demo/src/app/tab/tab-small.html b/src/tab/demo/src/app/tab/tab-small.html new file mode 100644 index 0000000..083863a --- /dev/null +++ b/src/tab/demo/src/app/tab/tab-small.html @@ -0,0 +1,11 @@ + + +
    Content of Tab Pane 1
    +
    + +
    Content of Tab Pane 2
    +
    + +
    Content of Tab Pane 3
    +
    +
    diff --git a/src/tab/demo/src/app/tab/webdoc/tab-demos.js b/src/tab/demo/src/app/tab/webdoc/tab-demos.js new file mode 100644 index 0000000..3bf344e --- /dev/null +++ b/src/tab/demo/src/app/tab/webdoc/tab-demos.js @@ -0,0 +1,126 @@ +export default { + column: '2', + demos: [ + { + demoId: 'tab-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Tabs 组件的最简用法。

    ', + 'en-US': '', + }, + apis: [ + 'TiTabComponent.properties.disabled', + 'TiTabComponent.properties.active', + ], + }, + { + demoId: 'tab-dark', + name: { + 'zh-CN': '深色背景', + 'en-US': 'dark', + }, + desc: { + 'zh-CN': '

    通过属性dark配置深色背景页签。

    ', + 'en-US': '', + }, + apis: ['TiTabComponent.properties.dark'], + }, + { + demoId: 'tab-small', + name: { + 'zh-CN': '小尺寸页签', + 'en-US': 'small', + }, + desc: { + 'zh-CN': '

    通过属性small配置小尺寸页签。

    ', + 'en-US': '', + }, + apis: ['TiTabComponent.properties.small'], + }, + { + demoId: 'tab-level2', + name: { + 'zh-CN': '二级页签', + 'en-US': 'level2', + }, + desc: { + 'zh-CN': '

    通过属性level2配置二级页签。

    ', + 'en-US': '', + }, + apis: ['TiTabComponent.properties.level2'], + }, + { + demoId: 'tab-custom-head', + name: { + 'zh-CN': '自定义页签头部', + 'en-US': 'custom head', + }, + desc: { + 'zh-CN': + '

    通过ti-tab-header标签自定义当前页签头部。

    ', + 'en-US': '', + }, + apis: ['TiTabComponent.properties.header'], + }, + { + demoId: 'tab-overflow', + name: { + 'zh-CN': '页签超长下拉展示', + 'en-US': 'overflow', + }, + desc: { + 'zh-CN': + '

    使用大量选项卡切换的场景。通过属性panelWidth配置下拉面板的宽度,包含autojustifiedstring三种类型。通过属性panelMaxHeight配置下拉面板的最大高度。通过panelAlign配置面板对齐方式。

    ', + 'en-US': '', + }, + apis: [ + 'TiTabsComponent.properties.panelWidth', + 'TiTabsComponent.properties.panelMaxHeight', + 'TiTabsComponent.properties.panelAlign', + ], + }, + { + demoId: 'tab-lazy-load', + name: { + 'zh-CN': '懒加载', + 'en-US': 'lazyload', + }, + desc: { + 'zh-CN': + '

    通过 ng-template 标签包裹懒加载的内容区,并且加上#tiTabContent标识。当页签初次激活时,才会加载对应的内容。

    ', + 'en-US': '', + }, + }, + { + demoId: 'tab-route', + name: { + 'zh-CN': '路由', + 'en-US': 'route', + }, + desc: { + 'zh-CN': '

    Tabs 组件只用作头部导航展示,内容区由路由控制。

    ', + 'en-US': '', + }, + }, + { + demoId: 'tab-beforeactivechange', + name: { + 'zh-CN': '事件', + 'en-US': 'events', + }, + desc: { + 'zh-CN': + '

    切换页签之前触发beforeActiveChange事件。当切换页签时触发activeChange事件。

    ', + 'en-US': '', + }, + apis: [ + 'TiTabComponent.events.activeChange', + 'TiTabComponent.events.beforeActiveChange', + 'TiTabsComponent.methods.changeActive', + ], + }, + ], +}; diff --git a/src/tab/demo/src/app/tab/webdoc/tab.cn.md b/src/tab/demo/src/app/tab/webdoc/tab.cn.md new file mode 100644 index 0000000..3bd0519 --- /dev/null +++ b/src/tab/demo/src/app/tab/webdoc/tab.cn.md @@ -0,0 +1,23 @@ +--- +title: Tabs 页签 +--- +# Tabs 页签 + +
    + +Tabs 是选项卡切换组件。   + +```typescript +import { TiTabModule } from '@opentiny/ng'; +``` + +
    + +
    + +Tabs 是选项卡切换组件。   + +```typescript +import { TiTabModule } from '@cloud/tiny-config'; +``` +
    diff --git a/src/tab/demo/src/app/tab/webdoc/tab.en.md b/src/tab/demo/src/app/tab/webdoc/tab.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/tab/demo/src/app/tab/webdoc/tab.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/tab/demo/src/favicon.ico b/src/tab/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/tab/demo/src/index.html b/src/tab/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/tab/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/tab/demo/src/main.ts b/src/tab/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/tab/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/tab/demo/test.ts b/src/tab/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/tab/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/tab/demo/tsconfig.app.json b/src/tab/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/tab/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/tab/demo/tsconfig.spec.json b/src/tab/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/tab/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/tab/lib/index.ts b/src/tab/lib/index.ts new file mode 100644 index 0000000..ae118a4 --- /dev/null +++ b/src/tab/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTabModule'; diff --git a/src/tab/lib/ng-package.json b/src/tab/lib/ng-package.json new file mode 100644 index 0000000..5116880 --- /dev/null +++ b/src/tab/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/tab", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/tab/lib/package.json b/src/tab/lib/package.json new file mode 100644 index 0000000..5d8d430 --- /dev/null +++ b/src/tab/lib/package.json @@ -0,0 +1,16 @@ +{ + "name": "@opentiny/ng-tab", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-include": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-droplist": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/tab/lib/project.json b/src/tab/lib/project.json new file mode 100644 index 0000000..fa1fa59 --- /dev/null +++ b/src/tab/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/tab/lib", + "sourceRoot": "src/tab/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/tab"], + "options": { + "project": "src/tab/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/tab"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js tab" + }, + { + "command": "ng default-build tab" + }, + { + "command": "node build/clear-default-theme.js tab" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/tab && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build tab && ng pack tab && node build/publish.js tab --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/tab/lib/src/TiTabComponent.ts b/src/tab/lib/src/TiTabComponent.ts new file mode 100644 index 0000000..4600054 --- /dev/null +++ b/src/tab/lib/src/TiTabComponent.ts @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + HostBinding, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + Renderer2, + SimpleChange, + SimpleChanges, + TemplateRef +} from '@angular/core'; +import { TiTabsComponent } from './TiTabsComponent'; +import { Util } from '@opentiny/ng-utils'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * TiTabComponent 是单个页签组件,包含了页签标题头指令TiTabHeader 和 该页签对应的内容部分 + * + */ +@Component({ + selector: 'ti-tab', + templateUrl: './tab.html', + styleUrls: ['./tab.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-tab-pane]': 'true' + } +}) +export class TiTabComponent extends TiBaseComponent implements OnInit, OnDestroy, OnChanges { + /** + * 页签标题 + */ + @Input() header: string; + /** + * 是否禁用 + */ + @Input() disabled: boolean; + /** + * 当前页签是否激活,该接口是双向绑定的 + */ + @Input() @HostBinding('class.ti3-tab-active') active: boolean; + /** + * 页签激活项发生改变时触发的回调,参数为当前页签是否为激活状态 + */ + @Output() readonly activeChange: EventEmitter = new EventEmitter(); + /** + * 页签激活项发生改变前触发的回调 + */ + @Output() readonly beforeActiveChange: EventEmitter = new EventEmitter(); + + /** + * @ignore + * 获取到用户自定义的模板 + */ + @ContentChild('tiTabContent', { static: true }) + public contentTemplate: TemplateRef; + /** + * @ignore + * 当前页签标题头显示的node节点 + */ + public headNode: any; + /** + * @ignore + * 记录此页签的内容区是否已经加载过 + */ + public loaded: boolean = false; + protected versionInfo: string = super.getVersion(packageInfo); + + constructor( + private elementRef: ElementRef, + private renderer2: Renderer2, + public tabset: TiTabsComponent, + public changeDetectorRef: ChangeDetectorRef + ) { + super(elementRef, renderer2); + } + + ngOnInit(): void { + super.ngOnInit(); + this.createHeadNode(); + this.tabset.addTab(this); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + const activeObj: SimpleChange = changes['active']; + if (activeObj && activeObj.currentValue && this.tabset.selectedTab !== this && !this.disabled) { + this.tabset.activeTab(this, !activeObj.firstChange); + } + + const disabledChange: SimpleChange = changes['disabled']; + if (disabledChange && !disabledChange.firstChange) { + this.tabset.changeDetectorRef.markForCheck(); + } + + const headerChange: SimpleChange = changes['header']; + if (headerChange && !headerChange.firstChange) { + this.createHeadNode(); + this.tabset.changeDetectorRef.markForCheck(); + setTimeout(() => { + this.tabset.setTabStyle(true); + }, 0); + } + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + // 父容器TiTabs的OnDestroy执行晚于TiTab的OnDestroy,所以此处需要setTimeout来延时 + setTimeout(() => { + if (this.tabset.destroyed) { + return; + } + this.tabset.removeTab(this); + // tabs动态增删,onpush模式视图未更新 + this.tabset.changeDetectorRef.markForCheck(); + // 如果没有tab,那么下划线隐藏 + if (this.tabset.tabs.length === 0) { + this.tabset.tiRenderer.setStyles(this.tabset.slider.nativeElement, { + width: 0, + left: 0 + }); + } + }, 0); + } + + private createHeadNode(): void { + if (Util.isString(this.header)) { + this.headNode = this.renderer2.createText(this.header); + } + } +} diff --git a/src/tab/lib/src/TiTabHeaderComponent.ts b/src/tab/lib/src/TiTabHeaderComponent.ts new file mode 100644 index 0000000..e277882 --- /dev/null +++ b/src/tab/lib/src/TiTabHeaderComponent.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterContentInit, AfterViewChecked, ChangeDetectionStrategy, Component, ElementRef } from '@angular/core'; +import { TiTabComponent } from './TiTabComponent'; +/** + * TiTabHeaderComponent 是单个页签的标题头组件,其包裹的元素会作为当前页签的标题头 + * + */ +@Component({ + selector: 'ti-tab-header', + templateUrl: './tab-head.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-tab-head-no-outline]': 'true' + } +}) +export class TiTabHeaderComponent implements AfterContentInit, AfterViewChecked { + private oldHeaderText: string; + constructor(private tab: TiTabComponent, private elementRef: ElementRef) {} + + ngAfterContentInit(): void { + // 由于此处的AfterContentInit会比TiTabComponent组件的OnInit + // 执行得晚,所以ti-tab-header中的内容会覆盖header接口传入的字符串值 + this.tab.headNode = this.elementRef.nativeElement; + if (!this.tab.header) { + // 处理自定义dom没有header接口时对应下拉面板展示 + this.tab.header = this.elementRef.nativeElement.textContent; + } + this.oldHeaderText = this.tab.header; + } + + ngAfterViewChecked(): void { + // 既没有滑动条也没有溢出 + if (!this.tab.tabset.hasSlider && !this.tab.tabset.overflow) { + return; + } + + // 获取当前文本 + const headerText: string = this.elementRef.nativeElement.textContent; + + if (headerText === this.oldHeaderText) { + return; + } + + // 有滑动条slider场景 + if (this.tab.tabset.hasSlider) { + this.tab.tabset.headerWidthChange = true; + } + + // 溢出时:更新header + if (this.tab.tabset.overflow) { + this.tab.header = headerText; + } + + this.oldHeaderText = headerText; + } +} diff --git a/src/tab/lib/src/TiTabModule.ts b/src/tab/lib/src/TiTabModule.ts new file mode 100644 index 0000000..e23454d --- /dev/null +++ b/src/tab/lib/src/TiTabModule.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiTabsComponent } from './TiTabsComponent'; +import { TiTabComponent } from './TiTabComponent'; +import { TiTabHeaderComponent } from './TiTabHeaderComponent'; +import { TiIncludeModule } from '@opentiny/ng-include'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiDroplistModule } from '@opentiny/ng-droplist'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiDroplistModule, TiIconModule, TiIncludeModule, TiRendererModule], + exports: [TiTabsComponent, TiTabComponent, TiTabHeaderComponent], + declarations: [TiTabsComponent, TiTabComponent, TiTabHeaderComponent] +}) +export class TiTabModule {} +export { TiTabComponent } from './TiTabComponent'; +export { TiTabsComponent } from './TiTabsComponent'; +export { TiTabHeaderComponent } from './TiTabHeaderComponent'; diff --git a/src/tab/lib/src/TiTabsComponent.ts b/src/tab/lib/src/TiTabsComponent.ts new file mode 100644 index 0000000..34fe43e --- /dev/null +++ b/src/tab/lib/src/TiTabsComponent.ts @@ -0,0 +1,507 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + AfterViewChecked, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Inject, + Input, + NgZone, + OnDestroy, + OnInit, + Renderer2, + ViewChild +} from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +import { TiTabComponent } from './TiTabComponent'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiDroplistComponent } from '@opentiny/ng-droplist'; +import { TiPositionType, Util } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; +/** + * TiTabs组件属于布局组件,通过该组件产品可以实现不同视图的切换功能。 + * + * 其内部包含2个组件 TiTab 和 TiTabHeader + * + */ +@Component({ + selector: 'ti-tabs', + templateUrl: './tabs.html', + styleUrls: ['./tabs.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-tabs-container]': 'true' // 给host添加类ti-tabs-container替代之前的组件模板内部的最外层div + } +}) +export class TiTabsComponent extends TiBaseComponent implements AfterViewInit, AfterViewChecked, OnInit, OnDestroy { + /** + * 下拉面板的宽度。 + * + * 1."justified": 下拉面板宽度与页签项宽度保持一致; + * + * 2."auto": 下拉面板宽度根据下拉项的内容自动撑开; + * + * 3.固定的下拉面板宽度: 不小于页签项的宽度,例如:"200px"; + * + */ + @Input() panelWidth: string = '100px'; + /** + * 下拉面板对齐方式 + * + */ + @Input() panelAlign: 'left' | 'right' = 'right'; + /** + * 下拉面板最大高度 + * + */ + @Input() panelMaxHeight: string; + /** + * 下拉项超长文本提示方向 + * + * @ignore + */ + @Input() tipPosition: TiPositionType = 'left'; + /** + * @ignore + */ + @ViewChild('slider', { static: true }) slider: ElementRef; + /** + * @ignore + */ + @ViewChild('tabContentContainer', { static: true }) + tabContentContainer: ElementRef; + /** + * @ignore + */ + @ViewChild('more', { static: true }) moreRef: ElementRef; + /** + * @ignore + */ + @ViewChild('moreIcon', { static: true }) moreIconRef: ElementRef; + /** + * @ignore + */ + @ViewChild('tabsList', { static: true }) tabsListRef: ElementRef; + /** + * @ignore + */ + @ViewChild('tabsHeader', { static: true }) tabsHeaderRef: ElementRef; + /** + * @ignore + */ + @ViewChild('tabsContainer', { static: true }) tabsContainerRef: ElementRef; + /** + * @ignore + */ + @ViewChild('droplist', { static: true }) dropListComp: TiDroplistComponent; + /** + * @ignore + */ + public labelKey: string = 'header'; + /** + * @ignore + * 下拉项 + */ + public options: Array = []; + /** + * 被点击tab项 + */ + public clickTab: TiTabComponent; + /** + * @ignore + */ + public onlistenClick: () => void; + /** + * @ignore + */ + public windowResizeListener: () => void; + /** + * @ignore + * TiTabs中包裹的所有的 TiTabComponent 集合 + */ + public tabs: Array = []; + /** + * @ignore + */ + public selectedTab: TiTabComponent; // 当前选中的tab + private isNotVisibleInit: boolean = false; // 标志初始时是否可见 + /** + * @ignore + * TiTabComponent.ts中使用 + */ + public destroyed: boolean = false; + /** + * @ignore + * 是否显示滑动条slider(一级页签显示,二级/暗色页签不显示) + * TiTabHeaderComponent.ts中使用 + */ + public hasSlider: boolean = false; + /** + * @ignore + * 是否溢出 + * TiTabHeaderComponent.ts中使用 + */ + public overflow: boolean = false; + /** + * @ignore + * TiTabHeaderComponent.ts中使用 + */ + public headerWidthChange: boolean = false; + protected versionInfo: string = super.getVersion(packageInfo); + private resolvedPromise: Promise = Promise.resolve(null); + private tabsResizeObserver: any; + /** + * @ignore + * 下拉选中项变更时触发 + */ + public onDroplistChange($event: any): void { + this.tabs.forEach((item: any) => { + if (item.id === this.selectedTab.id) { + if (item.beforeActiveChange.observers.length === 0) { + this.changeActive(item); + } else { + this.clickTab = item; + item.beforeActiveChange.emit(this); + } + } + }); + } + /** + * @ignore + * 点击下拉按钮打开下拉面板 + */ + public showMorePanel(event: MouseEvent): void { + if (this.dropListComp.isShow) { + this.dropListComp.hide(); + + return; + } + this.dropListComp.show(); + event.stopPropagation(); + } + constructor( + private elementRef: ElementRef, + private renderer2: Renderer2, + public tiRenderer: TiRenderer, + private zone: NgZone, + @Inject(DOCUMENT) private document, + public changeDetectorRef: ChangeDetectorRef + ) { + super(elementRef, renderer2); + } + + ngOnInit(): void { + super.ngOnInit(); + // 初始化是否显示滑动条slider(一级页签显示,二级/暗色页签不显示) + this.hasSlider = !this.nativeElement.hasAttribute('level2') && !this.nativeElement.hasAttribute('dark'); + } + + ngAfterViewInit(): void { + super.ngAfterContentInit(); + if (this.hasSlider) { + // 如果祖先元素中display为none,获取到的组件的宽度为0,元素不可见 + this.isNotVisibleInit = this.nativeElement.clientWidth === 0; + } + this.zone.runOutsideAngular(() => { + // 点击documnet优惠信息窗口关闭 + this.onlistenClick = this.renderer2.listen(this.document, 'click', () => this.dropListComp.hide()); + if (typeof window === 'undefined') { + return; + } + this.windowResizeListener = this.renderer2.listen(window, 'resize', () => { + this.setMoreStyle(); + this.setTabStyle(true); + }); + + if (this.isNotVisibleInit && (window as any).ResizeObserver) { + // 利用 ResizeObserver 来监听tabs的尺寸发生改变的时机。IE不支持 ResizeObserver。 + this.tabsResizeObserver = new (window as any).ResizeObserver((entries: any): void => { + // 初始祖先元素中display为none,之后第一次由none变成可见时 + if (entries[0] && entries[0].contentRect.width !== 0) { + this.setTabStyle(false); + this.tabsResizeObserver.disconnect(); + } + }); + this.tabsResizeObserver.observe(this.nativeElement); + } + }); + } + + ngAfterViewChecked(): void { + super.ngAfterViewChecked(); + // 此处执行晚于TiTabHeaderComponent的ngAfterViewChecked + if (this.headerWidthChange) { + this.setTabStyle(false); + this.headerWidthChange = false; + this.setMoreStyle(); + } + } + + ngOnDestroy(): void { + this.destroyed = true; + if (this.windowResizeListener) { + this.windowResizeListener(); + } + if (this.onlistenClick) { + this.onlistenClick(); + } + if (this.tabsResizeObserver) { + this.tabsResizeObserver.disconnect(); + } + } + /** + * @ignore + */ + public setMoreStyle(): void { + if (this.isOverflow()) { + this.renderer2.setStyle(this.moreRef.nativeElement, 'display', 'block'); + } else { + this.renderer2.setStyle(this.moreRef.nativeElement, 'display', 'none'); + } + } + + /** + * @ignore + * TiTabComponent.ts 中调用 + */ + public addTab(tab: TiTabComponent): void { + if (tab.active) { + this.tabs.forEach((item: TiTabComponent) => { + if (item.active) { + this.setActiveValue(item, false); + } + }); + } + this.tabs.push(tab); + const tabElements: Array = Array.from(this.tabContentContainer.nativeElement.children); + this.tabs.sort((a: TiTabComponent, b: TiTabComponent): number => { + return tabElements.indexOf(a.nativeElement) - tabElements.indexOf(b.nativeElement); + }); + if (!tab.active && this.selectedTab && tabElements.indexOf(tab.nativeElement) < tabElements.indexOf(this.selectedTab.nativeElement)) { + setTimeout(() => { + this.setTabStyle(true); + }, 0); + } + this.options = this.tabs; + setTimeout(() => { + this.setMoreStyle(); + }, 0); + // tabs动态增删,onpush模式视图未更新 + this.changeDetectorRef.markForCheck(); + } + /** + * @ignore + * TiTabComponent.ts 中调用 + */ + public removeTab(tab: TiTabComponent): void { + const index: number = this.tabs.indexOf(tab); + const length: number = this.tabs.length; + this.tabs.splice(index, 1); + + setTimeout(() => { + this.setMoreStyle(); + }, 0); + + // 删除当前激活状态的tab + if (tab.active && length > 1) { + const newActiveIndex: number = this.getNewActiveIndex(index, length); + if (newActiveIndex !== -1) { + this.changeActive(this.tabs[newActiveIndex]); + } + + return; + } + + // 删除的是非激活状态的tab,那么就在删除后的tabs数组中查找剩下的哪一个tab是激活状态,再设置激活状态; + if (!tab.active) { + setTimeout(() => { + this.setTabStyle(true); + }, 0); + } + } + /** + * @ignore + * TiTabComponent.ts 中调用 + */ + public activeTab(selectedTab: TiTabComponent, enableAnimate: boolean): void { + this.deActiveOthers(selectedTab); + + selectedTab.loaded = true; + + this.selectedTab = selectedTab; + // onpush模式下,懒加载页签内容不刷新 + this.selectedTab.changeDetectorRef.markForCheck(); + // 只有不是灰化状态时,才触发切换动效 + if (!selectedTab.disabled) { + // 添加定时器,处理ngFor未渲染完毕,获取到得silder的位置不准确 + setTimeout(() => { + // 初始化激活时没有动效 + this.setTabStyle(enableAnimate); + }, 0); + } + } + + private deActiveOthers(selectTab: TiTabComponent): void { + this.tabs.forEach((tab: TiTabComponent) => { + if (tab.active && tab !== selectTab) { + // 处理有路由存在情况下,点击浏览器后退按钮,上一次选中项处于聚焦状态,导致两个页签高亮 #2136 + if (tab.headNode && tab.headNode.parentNode) { + tab.headNode.parentNode.blur(); + } + + this.setActiveValue(tab, false); + } + }); + // 通过其他方式(非直接点击页签)修改tab.active,页面激活效果未更新(onPush问题) + this.changeDetectorRef.markForCheck(); + } + + /** + * @ignore + */ + public click(tab: TiTabComponent): void { + if (tab.disabled || tab.active) { + return; + } + if (tab.beforeActiveChange.observers.length === 0) { + this.changeActive(tab); + } else { + this.clickTab = tab; + tab.beforeActiveChange.emit(this); + } + } + /** + * 页签激活状态变更后执行 + */ + public changeActive(tab: TiTabComponent): void { + this.setActiveValue(tab, true); + this.activeTab(tab, true); + } + + // 删除当前激活状态的tab后,需要激活下一个active tab + private getNewActiveIndex(index: number, length: number): number { + // 如果删除的当前激活状态的tab是最后一个, + // 那么下一个激活的tab就是从右到左第一个不是禁用的tab + if (index === length - 1) { + for (let i: number = index - 1; i >= 0; i--) { + if (!this.tabs[i].disabled) { + return i; + } + } + // 如果删除的当前激活状态的tab不是最后一个, + // 那么下一个激活的tab就是从左到右第一个不是禁用的tab + } else { + for (let i: number = index; i < this.tabs.length; i++) { + if (!this.tabs[i].disabled) { + return i; + } + } + } + + return -1; + } + + /** + * @ignore + * 设置页签样式 + */ + public setTabStyle(enableAnimate: boolean): void { + const targetTab: any = this.selectedTab && this.selectedTab.headNode.parentNode.parentNode; + // 容错处理:没有获取到DOM时直接return; + if (Util.isUndefined(targetTab) || !targetTab.classList.contains('ti3-tab-li')) { + return; + } + // 获取该页签的偏移量 + const tagetLeft: number = targetTab.offsetLeft; + // 需要显示滑动条时设置滑动条的样式及动效 + if (this.hasSlider) { + // 修复SSR错误:ERROR TypeError: targetTab.getBoundingClientRect is not a function + const targetWidth: number = + typeof targetTab.getBoundingClientRect === 'function' ? parseFloat(targetTab.getBoundingClientRect().width.toFixed(2)) : 0; + const transitionSet: string = enableAnimate ? 'left 0.1s ease-in-out' : 'none'; + this.tiRenderer.setStyles(this.slider.nativeElement, { + transition: transitionSet, + width: `${targetWidth}px`, + left: `${tagetLeft}px` + }); + } + this.setTabScroll(targetTab, tagetLeft); + } + // 设置页签超长滑动动效 + private setTabScroll(target: any, distance: number): void { + const moreWidth: number = this.moreRef && this.moreRef.nativeElement.offsetWidth; + const tabsHeaderWidth: number = this.tabsHeaderRef && this.tabsHeaderRef.nativeElement.offsetWidth; + const listWidth: number = this.tabsListRef && this.tabsListRef.nativeElement.offsetWidth; + // 修复SSR错误:ERROR TypeError: this.tabsContainerRef.nativeElement.getBoundingClientRect is not a function + if (typeof this.tabsContainerRef.nativeElement.getBoundingClientRect !== 'function') { + return; + } + const tabsContainerClientRect: any = this.tabsContainerRef && this.tabsContainerRef.nativeElement.getBoundingClientRect(); + const targetClientRect: any = target.getBoundingClientRect(); + // 1、条件1和2:激活项能在当前屏范围内完全显示时 + // 2、尾部页签紧贴右侧时(页签动态删减,导致尾部页签没有紧贴右侧 #15412) + // 同时满足,tabs无需滑动 + if ( + targetClientRect.x >= tabsContainerClientRect.x && + targetClientRect.x <= tabsContainerClientRect.x + tabsContainerClientRect.width - moreWidth - targetClientRect.width && + tabsHeaderWidth <= listWidth + moreWidth + ) { + return; + } + + let marginLeftDistance: number = distance; + const maxDistance: number = this.tabsListRef.nativeElement.scrollWidth - tabsContainerClientRect.width + moreWidth; + if (maxDistance > 0) { + if (distance >= maxDistance) { + marginLeftDistance = maxDistance; + } + } else { + marginLeftDistance = 0; + } + this.renderer2.setStyle(this.tabsHeaderRef.nativeElement, 'transition', 'margin-left 0.1s ease-in-out'); + this.renderer2.setStyle(this.tabsHeaderRef.nativeElement, 'margin-left', -marginLeftDistance + 'px'); + } + + private setActiveValue(tab: TiTabComponent, value: boolean): void { + // 双向数据绑定时,初始传入的值不合法立即修改并传出时会报错。 + // 此处参考ngModel源码setValue的处理,使用promise延后执行时序 + this.resolvedPromise.then(() => { + tab.active = value; + tab.activeChange.emit(value); + // tab快捷键操作时,active状态未及时更新 + this.changeDetectorRef.markForCheck(); + }); + } + + /** + * @ignore + */ + public trackByFn(index: number, item: any): Node { + return item.headNode; + } + + // 空间不足时,出现下拉面板 + private isOverflow(): boolean { + const headerWidth: number = this.tabsContainerRef.nativeElement.offsetWidth; + const listWidth: number = this.tabsListRef.nativeElement.scrollWidth; + const moreWidth: number = this.moreRef.nativeElement.offsetWidth; + + this.overflow = headerWidth - moreWidth < listWidth; + + return this.overflow; + } +} diff --git a/src/tab/lib/src/tab-head.html b/src/tab/lib/src/tab-head.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/tab/lib/src/tab-head.html @@ -0,0 +1 @@ + diff --git a/src/tab/lib/src/tab.html b/src/tab/lib/src/tab.html new file mode 100644 index 0000000..54f71b7 --- /dev/null +++ b/src/tab/lib/src/tab.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/tab/lib/src/tab.less b/src/tab/lib/src/tab.less new file mode 100644 index 0000000..a778f40 --- /dev/null +++ b/src/tab/lib/src/tab.less @@ -0,0 +1,19 @@ +@import '../../../themes/basic/base-all.less'; + +// Hide tabbable panes to start, show them when `.active` +:host.ti3-tab-pane { + display: none; + padding-top: var(--ti-common-space-3x); + animation-duration: 0.3s; + animation-timing-function: ease; + animation-fill-mode: backwards; + &.ti3-tab-active { + display: block; + } + &.active-remove { + display: none !important; + } + &.active-add { + animation-name: tiFadeIn; + } +} diff --git a/src/tab/lib/src/tabs.html b/src/tab/lib/src/tabs.html new file mode 100644 index 0000000..c7fb054 --- /dev/null +++ b/src/tab/lib/src/tabs.html @@ -0,0 +1,44 @@ +
    +
    +
      +
    • + +
    • +
    + + + +
    +
    +
    +
    +
    +
    + +
    + + diff --git a/src/tab/lib/src/tabs.less b/src/tab/lib/src/tabs.less new file mode 100644 index 0000000..a9acc08 --- /dev/null +++ b/src/tab/lib/src/tabs.less @@ -0,0 +1,265 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-tab-second-level-header-height: var(--ti-common-size-7x); + --ti-tab-second-level-header-line-height: calc(var(--ti-tab-second-level-header-height) - 2px); + --ti-tab-dark-container-height: var(--ti-common-size-10x); + --ti-tab-dark-li-height: var(--ti-common-size-9x); + --ti-tab-slider-height: var(--ti-common-border-weight-2); +} + +:host.ti3-tabs-container { + display: block; +} + +.ti3-tab-content { + border-top: 1px solid var(--ti-common-color-line-dividing); +} + +.ti3-tabs { + .box-sizing(); + display: flex; + white-space: nowrap; + flex-shrink: 1; + overflow: hidden; + position: relative; + padding-left: 0; + margin-bottom: 0; + list-style: none; + font-size: 0; + height: var(--ti-common-size-9x); + &::before { + display: table; + content: ' '; + .box-sizing(); + } + &::after { + display: table; + content: ' '; + clear: both; + .box-sizing(); + } + & > li { + .box-sizing(); + margin: 0 var(--ti-common-space-5x); + position: relative; + display: inline-block; + cursor: pointer; + line-height: var(--ti-common-line-height-number); + height: 100%; + font-size: var(--ti-common-font-size-1); + &:first-child { + margin-left: 0; + } + & > a { + .box-sizing(); + position: relative; + display: block; + // 修改a标签的下padding + padding: var(--ti-common-space-6) 0 var(--ti-common-space-2x); + text-decoration: none; + //此处line-height值与height值不一致是为了使文字距蓝色下边线的距离为10px + line-height: var(--ti-common-line-height-number); + height: 100%; + color: var(--ti-common-color-text-secondary); + .transition (color 150ms); + } + &:not(.disabled) > a:hover, + &:not(.disabled) > a:focus { + border-bottom-color: transparent; + color: var(--ti-common-color-text-highlight); + //添加此属性用来兼容ie和火狐下a标签会产生外围的虚线框 + outline: none; + .transition (color 200ms); + } + &.ti3-tab-active:not(.disabled) > a { + cursor: default; + color: var(--ti-common-color-text-highlight); + font-weight: normal; + } + &.disabled > a { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + background-color: transparent; + &:focus { + outline: none !important; + } + } + } +} +.ti3-tab-slider-container { + border-bottom: var(--ti-tab-slider-height) solid transparent; + position: absolute; + left: 0; + right: 0; + bottom: 0; +} +.ti3-tab-slider { + .box-sizing(); + border-bottom: var(--ti-tab-slider-height) solid var(--ti-common-color-bg-emphasize); + width: 0px; + position: absolute; + left: -50px; +} + +:host[level2] .ti3-tabs-container1 { + position: relative; + top: 1px; +} + +:host[level2] .ti3-tabs { + //二级页签的时候,滑块儿不显示 + & + .ti3-tab-more + .ti3-tab-slider-container .ti3-tab-slider { + display: none; + border-bottom: none; + } + height: var(--ti-tab-second-level-header-height); + & > li { + font-size: var(--ti-common-font-size-base); + height: var(--ti-tab-second-level-header-height); + line-height: var(--ti-tab-second-level-header-line-height); + margin: 0; + & > a { + .box-sizing(); + height: var(--ti-tab-second-level-header-height); + line-height: var(--ti-tab-second-level-header-line-height); + color: var(--ti-common-color-text-secondary); + padding: 0 var(--ti-common-space-3x); + border: 1px solid transparent; + } + &:not(.disabled) > a:hover, + &:not(.disabled) > a:focus { + color: var(--ti-common-color-text-highlight); + //添加此属性用来兼容ie和火狐下a标签会产生外围的虚线框 + outline: none; + } + &.ti3-tab-active:not(.disabled) > a { + border-color: var(--ti-common-color-line-dividing); + border-bottom-color: var(--ti-common-color-bg-white-normal); + color: var(--ti-common-color-text-highlight); + padding: 0 var(--ti-common-space-3x); + } + &.ti3-tab-active:not(.disabled), + &:last-child { + margin-right: 0; + } + &.disabled > a { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + background-color: transparent; + } + } +} +:host[dark] > .ti3-tabs-container1 .ti3-tabs-header { + background-color: var(--ti-common-color-bg-dark-normal); +} + +:host[dark] > .ti3-tabs-container1 .ti3-tabs { + height: var(--ti-tab-dark-container-height); + padding-top: calc(var(--ti-tab-dark-container-height) - var(--ti-tab-dark-li-height)); + & + .ti3-tab-more { + padding-right: var(--ti-common-space-5x); + color: var(--ti-common-color-text-darkbg); + &:hover { + color: var(--ti-common-color-bg-emphasize); + } + } + //暗色页签的时候,滑块儿不显示 + & + .ti3-tab-more + .ti3-tab-slider-container .ti3-tab-slider { + display: none; + border-bottom: none; + } + & > li { + margin-left: 0; + margin-right: var(--ti-common-space-base); + & > a { + .box-sizing(); + height: var(--ti-tab-dark-li-height); + line-height: var(--ti-tab-dark-li-height); + color: var(--ti-common-color-text-darkbg); + padding: 0 var(--ti-common-space-6x); + border-radius: var(--ti-common-border-radius-normal) var(--ti-common-border-radius-normal) 0 0; + .transition (background 150ms ease-in); + } + &:not(.disabled) > a:hover, + &:not(.disabled) > a:focus { + background-color: var(--ti-common-color-bg-dark-emphasize); + color: var(--ti-common-color-text-white); + outline: none; + .transition (background 200ms); + } + &.ti3-tab-active:not(.disabled) > a { + background-color: var(--ti-common-color-bg-normal); + color: var(--ti-common-color-text-primary); + .transition (background 250ms); + } + &.disabled > a { + color: var(--ti-common-color-text-gray-disabled); + cursor: not-allowed; + background-color: transparent; + } + &:first-child { + margin-left: var(--ti-common-space-5x); + } + &:last-child { + margin-right: 0; + } + } +} + +// 去掉下边框 +:host[dark].ti3-tabs-container { + & > .ti3-tab-content { + border: none; + } +} + +// 小尺寸页签 +:host[small] .ti3-tab-li { + font-size: var(--ti-common-font-size-base); + margin: 0 var(--ti-common-space-10); +} + +@-webkit-keyframes tiFadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +@keyframes tiFadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.ti3-tab-more { + cursor: pointer; + display: none; + padding-left: var(--ti-common-space-10); + &:hover { + color: var(--ti-common-color-text-highlight); + } + & ti-icon[local] { + display: block; + } +} + +.ti3-tabs-header { + display: flex; + justify-content: space-between; + align-items: center; + position: relative; +} + +.ti3-tabs-container1 { + overflow: hidden; +} + +.ti3-tab-head-no-outline { + outline: none; +} diff --git a/src/table/demo/karma.conf.js b/src/table/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/table/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/table/demo/project.json b/src/table/demo/project.json new file mode 100644 index 0000000..ccf4644 --- /dev/null +++ b/src/table/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/table/demo", + "sourceRoot": "src/table/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/table", + "index": "src/table/demo/src/index.html", + "main": "src/table/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/table/demo/tsconfig.app.json", + "assets": ["src/table/demo/src/favicon.ico", "src/table/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "table-demo:build:production" + }, + "development": { + "browserTarget": "table-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js table" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/table/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/table/demo/tsconfig.spec.json", + "karmaConfig": "src/table/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/table/demo/src/app/AppComponent.ts b/src/table/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/table/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/table/demo/src/app/AppModule.ts b/src/table/demo/src/app/AppModule.ts new file mode 100644 index 0000000..c41135b --- /dev/null +++ b/src/table/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TableTestModule } from './table/TableTestModule'; + +@NgModule({ + imports: [ + TableTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/table/demo/src/app/IndexComponent.ts b/src/table/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..ed5e866 --- /dev/null +++ b/src/table/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TableTestModule } from './table/TableTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TableTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/table/demo/src/app/app.html b/src/table/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/table/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/table/demo/src/app/table/TableActionmenuComponent.ts b/src/table/demo/src/app/table/TableActionmenuComponent.ts new file mode 100644 index 0000000..fdf8575 --- /dev/null +++ b/src/table/demo/src/app/table/TableActionmenuComponent.ts @@ -0,0 +1,81 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-actionmenu.html' +}) +export class TableActionmenuComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + name: 's2.small.2', + createTime: new Date(2003, 2, 6), + operator: 'Pierre Dupont', + state: 'on' + }, + { + name: 's2.xlarge.2', + createTime: new Date(2013, 9, 1), + operator: 'Jacques Germain', + state: 'off' + }, + { + name: 's2.medium.1', + createTime: new Date(2015, 1, 1), + operator: 'Robert Delcourt', + state: 'disabled' + }, + { + name: 's2.large.1', + createTime: new Date(2008, 5, 12), + operator: 'Elisa Menez', + state: 'stop' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'create Time' + }, + { + title: 'Operator' + }, + { + title: 'State' + }, + { + title: 'Action' + } + ]; + + onSelect(item: TiActionmenuItem, row: TiTableRowData): void { + row.state = item.label; + } + + dataToItemsFn: (data: any) => Array = (data: any) => { + let items: Array = [ + { + label: 'on' + }, + { + label: 'stop' + }, + { + label: 'off' + }, + { + label: 'disabled' + } + ]; + if (data.operator === 'Robert Delcourt') { + items = [{ ...items[0], disabled: true }, items[3]]; + } + + return items; + }; +} diff --git a/src/table/demo/src/app/table/TableBasicComponent.ts b/src/table/demo/src/app/table/TableBasicComponent.ts new file mode 100644 index 0000000..084dfd5 --- /dev/null +++ b/src/table/demo/src/app/table/TableBasicComponent.ts @@ -0,0 +1,52 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-basic.html' +}) +export class TableBasicComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], // 源数据 + state: undefined // 源数据分页、排序、搜索状态 + }; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableBasicTestComponent.ts b/src/table/demo/src/app/table/TableBasicTestComponent.ts new file mode 100644 index 0000000..d0ca795 --- /dev/null +++ b/src/table/demo/src/app/table/TableBasicTestComponent.ts @@ -0,0 +1,100 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-basic-test.html' +}) +export class TableBasicTestComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + changeData(): void { + const temp: TiTableRowData = { + firstName: 'slkdfjsadf', + lastName: 'weiorui', + age: 23, + email: 'sdlfk@example.com', + balance: (this.data.length * 761) % 10000, + id: 'weoiriow' + }; + // 推荐 + this.srcData.data = this.srcData.data.concat(temp); + } + changeData2(): void { + // 测试删除 + this.srcData.data.splice(0, 1); + } + + changeData3(): void { + // 测试直接改变一行的数据是否生效 + this.srcData.data[0].firstName = '新的名字'; + this.srcData.data[1].age = 100; + } +} diff --git a/src/table/demo/src/app/table/TableCellTipComponent.ts b/src/table/demo/src/app/table/TableCellTipComponent.ts new file mode 100644 index 0000000..3fd99d7 --- /dev/null +++ b/src/table/demo/src/app/table/TableCellTipComponent.ts @@ -0,0 +1,56 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-cell-tip.html' +}) +export class TableCellTipComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + name: 'Pierre Dupont', + age: 20, + email: 'PierreLong@example.com', + address: 'New York No. 1 Lake Park', + tipContent: 'address: New York No. 1 Lake Park' + }, + { + name: 'Jacques Germain', + age: 42, + email: 'JacquesLong@example.com', + address: 'Sidney No. 1 Lake Park', + tipContent: 'address: Sidney No. 1 Lake Park' + }, + { + name: 'Robert Delcourt', + age: 15, + email: 'RobertLong@example.com', + address: 'London No. 1 Lake Park', + tipContent: 'address: London No. 1 Lake Park' + }, + { + name: 'Elisa Menez', + age: 36, + email: 'ElisaLong@example.com', + address: 'Los Angeles No. 1 Lake Park', + tipContent: 'address: Los Angeles No. 1 Lake Park' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'Age' + }, + { + title: 'Long Email Address' + }, + { + title: 'Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableCelliconsColsresizableComponent.ts b/src/table/demo/src/app/table/TableCelliconsColsresizableComponent.ts new file mode 100644 index 0000000..37ff325 --- /dev/null +++ b/src/table/demo/src/app/table/TableCelliconsColsresizableComponent.ts @@ -0,0 +1,77 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-cellicons-colsresizable.html' +}) +export class TableCelliconsColsresizableComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + sortKey: 'firstName' + }, + { + title: 'last name', + width: '20%', + sortKey: 'lastName' + }, + { + title: 'birth date', + width: '10%', + sortKey: 'age' + }, + { + title: 'balance', + width: '30%', + sortKey: 'balance' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableCheckboxComponent.ts b/src/table/demo/src/app/table/TableCheckboxComponent.ts new file mode 100644 index 0000000..918040d --- /dev/null +++ b/src/table/demo/src/app/table/TableCheckboxComponent.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-checkbox.html' +}) +export class TableCheckboxComponent { + checkedList: Array = []; + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com', + disabled: true + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: '' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; + + ngOnInit(): void { + // this.checkedList.push(this.srcData.data[6]); // 设置初始选中项 + } + + trackByFn(index: number, item: any): number { + return item.firstName; + } +} diff --git a/src/table/demo/src/app/table/TableCheckboxPaginationComponent.ts b/src/table/demo/src/app/table/TableCheckboxPaginationComponent.ts new file mode 100644 index 0000000..5151747 --- /dev/null +++ b/src/table/demo/src/app/table/TableCheckboxPaginationComponent.ts @@ -0,0 +1,146 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-checkbox-pagination.html' +}) +export class TableCheckboxPaginationComponent implements OnInit { + constructor(private ref: ChangeDetectorRef) {} + + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + checkedList: Array = []; // 默认选中项 + currentPage: number = 1; + totalNumber: number = 56; + show: boolean = false; + pageSize: { options: Array; size: number } = { + options: [10, 20, 50, 100], + size: 10 + }; + columns: Array = [ + { + title: '' + }, + { + title: 'last name' + }, + { + title: 'birth date' + }, + { + title: 'balance' + }, + { + title: 'email' + } + ]; + + displayed1: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData1: TiTableSrcData; + private data1: Array = []; + checkedList1: Array = []; // 默认选中项 + currentPage1: number = 1; + totalNumber1: number = 0; + pageSize1: { options: Array; size: number } = { + options: [5, 10, 20, 50], + size: 10 + }; + columns1: Array = [ + { + title: '' + }, + { + title: 'last name' + }, + { + title: 'birth date' + }, + { + title: 'balance' + }, + { + title: 'email' + } + ]; + show1: boolean = false; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + + this.srcData1 = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: true // 源数据已进行分页处理 + } + }; + // 初始时向后台发送请求获取数据 + this.getCurrentPageData(this.currentPage1, this.pageSize1.size).then((data: Array) => { + this.srcData1.data = data; + this.totalNumber1 = 256; + }); + } + + // 模拟后台异步请求返回的数据 + private getCurrentPageData(currentPage: number, itemsPerPage: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + const data: Array = []; + for (let j: number = (currentPage - 1) * itemsPerPage; j < currentPage * itemsPerPage; j++) { + data.push(this.createRandomItem(j)); + } + resolve(data); + // 在onpush模式下,异步场景需要手动触发变更,default模式下不用写该行代码 + this.ref.markForCheck(); + }, 500); + }); + } + + // 表格的数据状态(分页,排序,搜索)发生改变时,此方法会被触发; + // 多用于后台分页、排序和搜索 + stateUpdate(tiTable: TiTableComponent): void { + const dataState: TiTableDataState = tiTable.getDataState(); + this.getCurrentPageData(dataState.pagination.currentPage, dataState.pagination.itemsPerPage).then((data: Array) => { + this.srcData1.data = data; + }); + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const disabled: boolean = id % 8 === 0; + + return { + firstName, + lastName, + age, + email, + balance, + disabled, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableCheckboxPaginationHeadmenuComponent.ts b/src/table/demo/src/app/table/TableCheckboxPaginationHeadmenuComponent.ts new file mode 100644 index 0000000..6c8cf7f --- /dev/null +++ b/src/table/demo/src/app/table/TableCheckboxPaginationHeadmenuComponent.ts @@ -0,0 +1,133 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-checkbox-pagination-headmenu.html' +}) +export class TableCheckboxPaginationHeadmenuComponent implements OnInit { + default: any = { + checkedList: [], + displayedData: [], + srcData: { + data: [], + state: undefined + }, + currentPage: 1, + totalNumber: 56 + }; + + custom: any = { + checkedList: [], + displayedData: [], + srcData: { + data: [], + state: undefined + }, + currentPage: 1, + totalNumber: 68, + options: [ + { + key: 'all', + label: '选择所有数据' + }, + { + key: 'age', + label: '选择所有年龄不小于60岁的' + }, + { + key: 'clear', + label: '清空' + } + ] + }; + columns: Array = [ + { + title: '' + }, + { + title: 'Id' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; + pageSize: { options: Array; size: number } = { + options: [5, 10, 50], + size: 5 + }; + + ngOnInit(): void { + this.default.srcData.data = this.getData(this.default.totalNumber); + this.custom.srcData.data = this.getData(this.custom.totalNumber); + } + + onDefaultHeadmenuSelect(option: any): void { + if (option.key === 'checkAll') { + this.default.checkedList = this.default.srcData.data.filter((row: TiTableRowData) => { + return !row.disabled || this.default.checkedList.includes(row); + }); + } else { + this.default.checkedList = this.default.srcData.data.filter((row: TiTableRowData) => { + return row.disabled && this.default.checkedList.includes(row); + }); + } + } + + onCustomHeadmenuSelect(option: any): void { + if (option.key === 'all') { + this.custom.checkedList = this.custom.srcData.data.filter((row: TiTableRowData) => { + return !row.disabled || this.custom.checkedList.includes(row); + }); + } else if (option.key === 'age') { + this.custom.checkedList = this.custom.srcData.data.filter((row: TiTableRowData, index: number) => { + const isMatched: boolean = row.age >= 60; + return (isMatched && !row.disabled) || (row.disabled && this.custom.checkedList.includes(row)); + }); + } else { + this.custom.checkedList = this.custom.srcData.data.filter((row: TiTableRowData) => { + return row.disabled && this.custom.checkedList.includes(row); + }); + } + } + + trackByFn(index: number, item: TiTableRowData): number { + return item.id; + } + + private getData(totalNumber: number): Array { + const data: Array = []; + for (let j: number = 0; j < totalNumber; j++) { + data.push(this.createRandomItem(j)); + } + + return data; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const disabled: boolean = id % 5 === 1; + + return { + firstName, + lastName, + age, + email, + disabled, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColAlignComponent.ts b/src/table/demo/src/app/table/TableColAlignComponent.ts new file mode 100644 index 0000000..2e258cd --- /dev/null +++ b/src/table/demo/src/app/table/TableColAlignComponent.ts @@ -0,0 +1,52 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-col-align.html' +}) +export class TableColAlignComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableColalignSortResizableTestComponent.ts b/src/table/demo/src/app/table/TableColalignSortResizableTestComponent.ts new file mode 100644 index 0000000..1c5f562 --- /dev/null +++ b/src/table/demo/src/app/table/TableColalignSortResizableTestComponent.ts @@ -0,0 +1,72 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-colalign-sort-resizable-test.html' +}) +export class TableColalignSortResizableTestComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + sortKey: 'firstName' + }, + { + title: 'last name', + sortKey: 'lastName' + }, + { + title: 'birth date', + sortKey: 'age' + }, + { + title: 'balance', + sortKey: 'balance' + }, + { + title: 'email' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColsResizableComponent.ts b/src/table/demo/src/app/table/TableColsResizableComponent.ts new file mode 100644 index 0000000..7eada46 --- /dev/null +++ b/src/table/demo/src/app/table/TableColsResizableComponent.ts @@ -0,0 +1,59 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-cols-resizable.html' +}) +export class TableColsResizableComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com', + address: 'New York No. 1 Lake Park' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com', + address: 'Sidney No. 1 Lake Park' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com', + address: 'London No. 1 Lake Park' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com', + address: 'Los Angeles No. 1 Lake Park' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Address' + }, + { + title: 'Email Address' + }, + { + title: 'Age' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableColsToggleComponent.ts b/src/table/demo/src/app/table/TableColsToggleComponent.ts new file mode 100644 index 0000000..56ab827 --- /dev/null +++ b/src/table/demo/src/app/table/TableColsToggleComponent.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-cols-toggle.html' +}) +export class TableColsToggleComponent { + displayedData: Array = []; + searchable: boolean = true; + selectAll: boolean = true; + srcData: TiTableSrcData = { + data: [ + { + id: '1', + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + id: '2', + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + id: '3', + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + id: '4', + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'Id' + }, + { + title: 'First Name', + show: true + }, + { + title: 'Last Name', + show: false + }, + { + title: 'Age', + show: true + }, + { + title: 'Email Address', + show: true + } + ]; +} diff --git a/src/table/demo/src/app/table/TableColsToggleDetailsComponent.ts b/src/table/demo/src/app/table/TableColsToggleDetailsComponent.ts new file mode 100644 index 0000000..7e0de43 --- /dev/null +++ b/src/table/demo/src/app/table/TableColsToggleDetailsComponent.ts @@ -0,0 +1,81 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-cols-toggle-details.html' +}) +export class TableColsToggleDetailsComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: '' // title为空字符串时,该列信息不在控制列隐藏/显示的下拉中出现,对于操作列适用于改场景 + }, + { + title: 'first name', + width: '20%', + show: true + }, + { + title: 'last name', + width: '20%', + show: false + }, + { + title: 'birth date', + width: '10%', + show: true + }, + { + title: 'balance' + }, + { + title: 'email', + width: '20%', + show: false + } + ]; + + panelWidth: string = '200px'; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColsToggleTestComponent.ts b/src/table/demo/src/app/table/TableColsToggleTestComponent.ts new file mode 100644 index 0000000..71e04d3 --- /dev/null +++ b/src/table/demo/src/app/table/TableColsToggleTestComponent.ts @@ -0,0 +1,103 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-cols-toggle-test.html' +}) +export class TableColsToggleTestComponent implements OnInit { + disabled: boolean = false; + selectAll: boolean = true; + panelWidth: string = '250px'; + searchable: boolean = true; // 可切换测试 + tipContent: string = '控制列隐藏/显示'; // 默认提示文本为'自定义列表项' + tipPosition: string = 'left'; // 默认提示文本方向为'top' + noDataText: string = '无数据'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name 我需要出省略号和tip提示aaaa ', + show: false + }, + { + title: 'last name', + show: true + }, + { + title: 'birth date', + show: true + }, + { + title: 'balance', + show: undefined // undefined表示该列不参与动态显示/隐藏 + }, + { + title: 'email', + show: true + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + // + onClick(): void { + this.disabled = !this.disabled; + this.tipContent = this.disabled ? '禁用状态说明' : '控制列隐藏/显示'; + } + onClick2(): void { + this.selectAll = !this.selectAll; + } + onClick3(): void { + this.searchable = !this.searchable; + } + + onBlur(): void { + console.log('blur', this.columns); + } + + onSelect(item: TiTableColumns): void { + console.log('select', item); + const selectedColumns: Array = this.columns.filter((column: { show?: boolean }) => { + return column.show === true || column.show === undefined; + }); + console.log('显示的列', selectedColumns); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColsresizableBasicComponent.ts b/src/table/demo/src/app/table/TableColsresizableBasicComponent.ts new file mode 100644 index 0000000..b5957ea --- /dev/null +++ b/src/table/demo/src/app/table/TableColsresizableBasicComponent.ts @@ -0,0 +1,72 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-colsresizable-basic.html' +}) +export class TableColsresizableBasicComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '15%' + }, + { + title: 'last name', + width: '25%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColsresizableColstoggleComponent.ts b/src/table/demo/src/app/table/TableColsresizableColstoggleComponent.ts new file mode 100644 index 0000000..e8aca79 --- /dev/null +++ b/src/table/demo/src/app/table/TableColsresizableColstoggleComponent.ts @@ -0,0 +1,79 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-colsresizable-colstoggle.html' +}) +export class TableColsresizableColstoggleComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%', + show: true + }, + { + title: 'birth date', + width: '10%', + show: false + }, + { + title: 'balance', + width: '30%', + show: true + }, + { + title: 'email', + width: '20%', + show: true + } + ]; + panelWidth: number = 200; + searchable: boolean = false; // 可切换测试 + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColsresizableColstoggleFixedheadComponent.ts b/src/table/demo/src/app/table/TableColsresizableColstoggleFixedheadComponent.ts new file mode 100644 index 0000000..9a7014c --- /dev/null +++ b/src/table/demo/src/app/table/TableColsresizableColstoggleFixedheadComponent.ts @@ -0,0 +1,79 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-colsresizable-colstoggle-fixedhead.html' +}) +export class TableColsresizableColstoggleFixedheadComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + show: true + }, + { + title: 'last name', + width: '20%', + show: true + }, + { + title: 'birth date', + width: '10%', + show: true + }, + { + title: 'balance', + width: '30%', + show: undefined + }, + { + title: 'email', + width: '20%', + show: true + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 5; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColsresizableLoadfailComponent.ts b/src/table/demo/src/app/table/TableColsresizableLoadfailComponent.ts new file mode 100644 index 0000000..044c924 --- /dev/null +++ b/src/table/demo/src/app/table/TableColsresizableLoadfailComponent.ts @@ -0,0 +1,56 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-colsresizable-loadfail.html' +}) +export class TableColsresizableLoadfailComponent implements OnInit { + status: number = 404; // 404应该是http请求返回的状态码,此处只是用此变量做模拟 + failLoadInfo: string = '加载失败,请'; + reloadInfo: string = '重新加载'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + reloadFn(): void { + console.log('重新加载'); + } +} diff --git a/src/table/demo/src/app/table/TableColsresizableSortComponent.ts b/src/table/demo/src/app/table/TableColsresizableSortComponent.ts new file mode 100644 index 0000000..e6aea4f --- /dev/null +++ b/src/table/demo/src/app/table/TableColsresizableSortComponent.ts @@ -0,0 +1,77 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-colsresizable-sort.html' +}) +export class TableColsresizableSortComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + sortKey: 'firstName' + }, + { + title: 'last name', + width: '20%', + sortKey: 'lastName' + }, + { + title: 'birth date', + width: '10%', + sortKey: 'age', + asc: false // 默认排序,且为降序 + }, + { + title: 'balance', + sortKey: 'balance' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了排序特性,且对源数据未进行排序处理,因此tiny会对数据进行排序处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColsresizableSortHeadfilterComponent.ts b/src/table/demo/src/app/table/TableColsresizableSortHeadfilterComponent.ts new file mode 100644 index 0000000..ec4d24f --- /dev/null +++ b/src/table/demo/src/app/table/TableColsresizableSortHeadfilterComponent.ts @@ -0,0 +1,120 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-colsresizable-sort-headfilter.html' +}) +export class TableColsresizableSortHeadfilterComponent implements OnInit { + // 初始化时,searchWords 的长度和searchKeys相同,无搜索时设置为空字符串 + searchWords: Array = ['', '']; + searchKeys: Array = ['firstName', 'lastName']; // 设置过滤字段 + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + key: 'firstName', + selected: null, + options: [ + { + label: 'all' + }, + { + label: 'Pierre' + }, + { + label: 'Jacques' + }, + { + label: 'Robert' + } + ] + }, + { + title: 'last name', + width: '10%', + key: 'lastName', + selected: null, + options: [ + { + label: 'all' + }, + { + label: 'Dupont' + }, + { + label: 'Germain' + }, + { + label: 'Delcourt' + }, + { + label: 'bjip' + } + ] + }, + { + title: 'birth date', + width: '10%', + key: 'age', + asc: false // 默认排序,且为降序 + }, + { + title: 'balance', + key: 'balance' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了排序特性,且对源数据未进行排序处理,因此tiny会对数据进行排序处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + // 使 headfilter 和表格搜索联动。根据 headfilter 选中项给表格的搜索接口传入对应的搜索值,进行表格数据搜索。 + // 此示例是利用表格提供的搜索功能来搜索,也可在此事件中来自己实现搜索。 + onSelect(item: any, column: TiTableColumns): void { + const index: number = this.searchKeys.indexOf(column.key); + this.searchWords[index] = item.label === 'all' ? '' : item.label; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColumnFixedComponent.ts b/src/table/demo/src/app/table/TableColumnFixedComponent.ts new file mode 100644 index 0000000..c7b15ac --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnFixedComponent.ts @@ -0,0 +1,80 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-column-fixed.html' +}) +export class TableColumnFixedComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + id: '1', + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com', + address: 'New York No. 1 Lake Park' + }, + { + id: '2', + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com', + address: 'Sidney No. 1 Lake Park' + }, + { + id: '3', + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com', + address: 'London No. 1 Lake Park' + }, + { + id: '4', + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com', + address: 'Los Angeles No. 1 Lake Park' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'Id', + fixed: 'left', + width: '100px' + }, + { + title: 'First Name', + fixed: 'left', + width: '200px' + }, + { + title: 'Last Name', + fixed: 'left', + width: '200px' + }, + { + title: 'Age', + width: '250px' + }, + { + title: 'Email Address', + width: '400px' + }, + { + title: 'Address', + width: '400px' + }, + { + title: 'Action', + fixed: 'right', + width: '150px' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableColumnfixedCheckboxComponent.ts b/src/table/demo/src/app/table/TableColumnfixedCheckboxComponent.ts new file mode 100644 index 0000000..21b2bcd --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedCheckboxComponent.ts @@ -0,0 +1,110 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-checkbox.html' +}) +export class TableColumnfixedCheckboxComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + checkedList: Array = []; // 默认选中项 + columns: Array = [ + { + title: '', + fixed: 'left' + }, + { + title: 'first name', + width: '150px', + fixed: 'left' + }, + { + title: 'last name', + width: '200px', + fixed: 'left' + }, + { + title: 'birth date', + width: '280px', + fixed: 'left' + }, + { + title: 'balance', + width: '340px' + }, + { + title: 'email', + width: '310px' + }, + { + title: 'address', + width: '340px' + }, + { + title: 'phone number', + width: '210px' + }, + { + title: 'parents', + width: '340px' + }, + { + title: 'school', + width: '190px', + fixed: 'right' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了分页特性,且源数据未进行分页处理,因此tiny会对数据进行分页处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + this.checkedList.push(this.data[6]); // 初始选中项 + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const address: string = "Xi'an"; + const phone: string = '123 4567 8900'; + const parents: string = 'father and mother'; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + address, + phone, + parents, + school, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColumnfixedColstoggleComponent.ts b/src/table/demo/src/app/table/TableColumnfixedColstoggleComponent.ts new file mode 100644 index 0000000..01f2b70 --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedColstoggleComponent.ts @@ -0,0 +1,125 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-colstoggle.html' +}) +export class TableColumnfixedColstoggleComponent implements OnInit { + disabled: boolean = false; + selectAll: boolean = true; + panelWidth: string = '250px'; + searchable: boolean = true; // 可切换测试 + tipContent: string = '控制列隐藏/显示'; // 默认提示文本为'自定义列表项' + tipPosition: string = 'left'; // 默认提示文本方向为'top' + noDataText: string = '无数据'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '150px', + fixed: 'left', + show: true + }, + { + title: 'last name', + width: '200px', + fixed: 'left', + show: true + }, + { + title: 'birth date', + width: '180px', + show: true + }, + { + title: 'balance', + width: '340px', + show: undefined + }, + { + title: 'email', + width: '310px', + show: false + }, + { + title: 'address', + width: '340px', + show: true + }, + { + title: 'phone number', + width: '210px', + show: true + }, + { + title: 'parents', + width: '340px', + show: true + }, + { + title: 'school', + width: '190px', + fixed: 'right', + show: true + } + ]; + currentPage: number = 1; + totalNumber: number = 10; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50], + size: 5 + }; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了分页特性,且对源数据未进行分页处理,因此tiny会对数据进行分页处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const address: string = "Xi'an"; + const phone: string = '123 4567 8900'; + const parents: string = 'father and mother'; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + address, + phone, + parents, + school, + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/table/demo/src/app/table/TableColumnfixedEditrowComponent.ts b/src/table/demo/src/app/table/TableColumnfixedEditrowComponent.ts new file mode 100644 index 0000000..f9da46b --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedEditrowComponent.ts @@ -0,0 +1,188 @@ +import { Component, OnInit } from '@angular/core'; +import { TiActionmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-editrow.html' +}) +export class TableColumnfixedEditrowComponent implements OnInit { + // 表格数据 + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + + columns: Array = [ + { + title: '名称/ID', + width: '15%', + fixed: 'left' + }, + { + title: '客户等级', + width: '15%', + fixed: 'left' + }, + { + title: '信用账户金额', + width: '15%' + }, + { + title: '退订次数', + width: '15%' + }, + { + title: '创建时间', + width: '15%' + }, + { + title: '操作员', + width: '15%' + }, + { + title: '操作', + width: '10%', + fixed: 'right' + } + ]; + + // 正在编辑行 + editingRow: TiTableRowData; + // 新添加一行 + newRow: TiTableRowData; + // 展示行actionmenu 选项 + items: Array = [ + { + label: '编辑' + }, + { + label: '删除' + } + ]; + // 编辑行actionmenu 选项 + editingItems: Array = [ + { + label: '保存' + }, + { + label: '取消' + } + ]; + + // 新增行actionmenu + addItems: Array = [ + { + label: '添加' + }, + { + label: '取消' + } + ]; + + // 客户等级备选项 + levelOptions: Array = [ + { + label: '一级' + }, + { + label: '二级' + }, + { + label: '三级' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + // 正常行按钮处理-编辑、删除 + onSelect(item: any, row: TiTableRowData): void { + if (item.label === '编辑') { + this.editingRow = { ...row }; + this.editableRows(false); + } else { + // 删除 + this.srcData.data = this.srcData.data.filter((current: TiTableRowData): boolean => { + return current.id !== row.id; + }); + } + } + + // 正在编辑行按钮处理-保存、取消 + onSelectEditing(item: any, row: TiTableRowData): void { + if (item.label === '保存') { + this.srcData.data = this.srcData.data.map((current: TiTableRowData) => { + if (current.id === row.id) { + current = this.editingRow; + } + + return current; + }); + } + + this.editableRows(true); + this.editingRow = undefined; + } + + // 添加按钮处理 + onSelectAdd(item: any): void { + if (item.label === '添加') { + const newRow: TiTableRowData = { ...this.newRow }; + newRow.id = 'row_' + this.srcData.data.length * 911; + this.srcData.data = [newRow].concat(this.srcData.data); + } + + this.editableRows(true); + + this.newRow = undefined; + } + // 添加行 + addRow(): void { + this.newRow = {}; + this.editingRow = undefined; + this.editableRows(false); + } + + private editableRows(editable: boolean): void { + this.items = this.items.map((item: TiActionmenuItem): TiActionmenuItem => { + return { + ...item, + disabled: !editable, + tip: editable ? '' : '请先保存处于编辑状态的行' + }; + }); + } + + // 模拟数据 + private createRandomItem(id: number): TiTableRowData { + const size: Array = ['small', 'medium', 'medium', 'xlarge']; + const sourceName: string = `s2.${size[(id * 29) % 4]}.2`; + const level: string = '一级'; + const balance: number = ((id + 5) * 1000 * 7) % 10000; + const unsubscribe: number = (id * 17) % 5; + const createTime: Date = new Date(1598872204569 - ((id * 100) % 23) * 3600000 * 24 * 30); + const operator: string = 'Operator001'; + const editable: boolean = true; + + return { + sourceName, + level, + balance, + unsubscribe, + createTime, + operator, + editable, + id: 'row_' + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColumnfixedFixedheadColsresizablePaginationComponent.ts b/src/table/demo/src/app/table/TableColumnfixedFixedheadColsresizablePaginationComponent.ts new file mode 100644 index 0000000..e56ea65 --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedFixedheadColsresizablePaginationComponent.ts @@ -0,0 +1,99 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-fixedhead-colsresizable-pagination.html' +}) +export class TableColumnfixedFixedheadColsresizablePaginationComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + currentPage: number = 1; + totalNumber: number = 112; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50, 100], + size: 10 + }; + columns: Array = [ + { + title: 'first name', + fixed: 'left' + }, + { + title: 'last name', + fixed: 'left' + }, + { + title: 'birth date', + fixed: 'left' + }, + { + title: 'balance' + }, + { + title: 'email' + }, + { + title: 'address' + }, + { + title: 'phone number' + }, + { + title: 'parents' + }, + { + title: 'school', + fixed: 'right' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const address: string = "Xi'an"; + const phone: string = '123 4567 8900'; + const parents: string = 'father and mother'; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + address, + phone, + parents, + school, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableColumnfixedHeadfixedComponent.ts b/src/table/demo/src/app/table/TableColumnfixedHeadfixedComponent.ts new file mode 100644 index 0000000..a0b252e --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedHeadfixedComponent.ts @@ -0,0 +1,112 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-columnfixed-headfixed.html' +}) +export class TableColumnfixedHeadfixedComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + id: '1', + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com', + address: 'New York No. 1 Lake Park' + }, + { + id: '2', + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com', + address: 'Sidney No. 1 Lake Park' + }, + { + id: '3', + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com', + address: 'London No. 1 Lake Park' + }, + { + id: '4', + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com', + address: 'Los Angeles No. 1 Lake Park' + }, + { + id: '5', + firstName: 'Pol', + lastName: 'Dupont', + age: 22, + email: 'Pol@example.com', + address: 'Los Angeles No. 1 Lake Park' + }, + { + id: '6', + firstName: 'Adelina', + lastName: 'Germain', + age: 30, + email: 'Adelina@example.com', + address: 'London No. 1 Lake Park' + }, + { + id: '7', + firstName: 'Abner', + lastName: 'Delcourt', + age: 29, + email: 'Abner@example.com', + address: 'Los Angeles No. 1 Lake Park' + }, + { + id: '8', + firstName: 'Emma', + lastName: 'Menez', + age: 48, + email: 'Emma@example.com', + address: 'Sidney No. 1 Lake Park' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'Id', + fixed: 'left', + width: '100px' + }, + { + title: 'First Name', + fixed: 'left', + width: '200px' + }, + { + title: 'Last Name', + fixed: 'left', + width: '200px' + }, + { + title: 'Age', + width: '250px' + }, + { + title: 'Email Address', + width: '400px' + }, + { + title: 'Address', + width: '400px' + }, + { + title: 'Action', + fixed: 'right', + width: '150px' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableColumnfixedLeftmenuComponent.ts b/src/table/demo/src/app/table/TableColumnfixedLeftmenuComponent.ts new file mode 100644 index 0000000..d55b693 --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedLeftmenuComponent.ts @@ -0,0 +1,119 @@ +import { Component, OnInit } from '@angular/core'; +import { TiLeftmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-leftmenu.html' +}) +export class TableColumnfixedLeftmenuComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '200px', + fixed: 'left' + }, + { + title: 'last name', + width: '160px', + fixed: 'left' + }, + { + title: 'birth date', + width: '240px', + fixed: 'left' + }, + { + title: 'balance', + width: '200px' + }, + { + title: 'email', + width: '120px' + }, + { + title: 'address', + width: '280px' + }, + { + title: 'phone number', + width: '100px' + }, + { + title: 'parents', + width: '300px' + }, + { + title: 'school', + width: '200px', + fixed: 'right' + } + ]; + headLabel: string = '头部区域(可定制)'; + marginLeft: string = '192px'; + collapsed: boolean = false; // 默认展开,当设置为true时会收起 + reloadState: boolean = true; // 初始设置为true + items: Array = [ + { + label: '一级菜单' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const address: string = "Xi'an"; + const phone: string = '123 4567 8900'; + const parents: string = 'father and mother'; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + address, + phone, + parents, + school, + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + toggleClick(isHide: boolean): void { + // 需要业务侧在菜单收起\展开时,控制右侧内容的位置 + this.marginLeft = isHide ? '0' : '192px'; + } +} diff --git a/src/table/demo/src/app/table/TableColumnfixedNodataComponent.ts b/src/table/demo/src/app/table/TableColumnfixedNodataComponent.ts new file mode 100644 index 0000000..3116771 --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedNodataComponent.ts @@ -0,0 +1,71 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-nodata.html' +}) +export class TableColumnfixedNodataComponent implements OnInit { + noDadaInfo: string = '暂无表格数据'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '150px', + fixed: 'left' + }, + { + title: 'last name', + width: '200px', + show: true, + fixed: 'left' + }, + { + title: 'birth date', + width: '380px', + fixed: 'left' + }, + { + title: 'balance', + width: '340px' + }, + { + title: 'email', + width: '310px' + }, + { + title: 'address', + width: '340px' + }, + { + title: 'phone number', + width: '210px' + }, + { + title: 'parents', + width: '340px' + }, + { + title: 'school', + width: '190px', + fixed: 'right' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } +} diff --git a/src/table/demo/src/app/table/TableColumnfixedPaginationComponent.ts b/src/table/demo/src/app/table/TableColumnfixedPaginationComponent.ts new file mode 100644 index 0000000..30b7f09 --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedPaginationComponent.ts @@ -0,0 +1,109 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-pagination.html' +}) +export class TableColumnfixedPaginationComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '150px', + fixed: 'left' + }, + { + title: 'last name', + width: '200px', + fixed: 'left' + }, + { + title: 'birth date', + width: '180px' + }, + { + title: 'balance', + width: '340px' + }, + { + title: 'email', + width: '310px' + }, + { + title: 'address', + width: '340px' + }, + { + title: 'phone number', + width: '210px' + }, + { + title: 'parents', + width: '340px' + }, + { + title: 'school', + width: '190px', + fixed: 'right' + } + ]; + currentPage: number = 1; + totalNumber: number = 32; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50], + size: 5 + }; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了分页特性,且对源数据未进行分页处理,因此tiny会对数据进行分页处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const address: string = "Xi'an"; + const phone: string = '123 4567 8900'; + const parents: string = 'father and mother'; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + address, + phone, + parents, + school, + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/table/demo/src/app/table/TableColumnfixedResizableComponent.ts b/src/table/demo/src/app/table/TableColumnfixedResizableComponent.ts new file mode 100644 index 0000000..6c2c391 --- /dev/null +++ b/src/table/demo/src/app/table/TableColumnfixedResizableComponent.ts @@ -0,0 +1,85 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-columnfixed-resizable.html' +}) +export class TableColumnfixedResizableComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '120px', + fixed: 'left' + }, + { + title: 'last name', + width: '120px', + fixed: 'left' + }, + { + title: 'birth date', + width: '80px', + fixed: 'left' + }, + { + title: 'balance' + }, + { + title: 'email' + }, + { + title: 'school', + width: '150px', + fixed: 'right' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + school, + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/table/demo/src/app/table/TableComprehensiveComponent.ts b/src/table/demo/src/app/table/TableComprehensiveComponent.ts new file mode 100644 index 0000000..173c13b --- /dev/null +++ b/src/table/demo/src/app/table/TableComprehensiveComponent.ts @@ -0,0 +1,121 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-comprehensive.html' +}) +export class TableComprehensiveComponent implements OnInit { + disabled: boolean = false; + selectAll: boolean = true; + panelWidth: string = '250px'; + searchable: boolean = true; // 可切换测试 + tipContent: string = '控制列隐藏/显示'; // 默认提示文本为'自定义列表项' + tipPosition: string = 'left'; // 默认提示文本方向为'top' + noDataText: string = '无数据'; + checkedList: Array = []; // 默认选中项 + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + currentPage: number = 1; + totalNumber: number = 112; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50, 100], + size: 10 + }; + columns: Array = [ + { + title: '', + fixed: 'left' + }, + { + title: 'first name', + fixed: 'left', + show: true + }, + { + title: 'last name', + fixed: 'left', + show: true + }, + { + title: 'birth date', + fixed: 'left', + show: true + }, + { + title: 'balance', + show: true + }, + { + title: 'email', + show: undefined + }, + { + title: 'address', + show: true + }, + { + title: 'phone number', + show: true + }, + { + title: 'parents', + show: true + }, + { + title: 'school', + fixed: 'right', + show: true + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const address: string = "Xi'an"; + const phone: string = '123 4567 8900'; + const parents: string = 'father and mother'; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + address, + phone, + parents, + school, + id + }; + } + + onCheckedsChange(event: any): void {} +} diff --git a/src/table/demo/src/app/table/TableDetailsCloseotherdetailsComponent.ts b/src/table/demo/src/app/table/TableDetailsCloseotherdetailsComponent.ts new file mode 100644 index 0000000..054db89 --- /dev/null +++ b/src/table/demo/src/app/table/TableDetailsCloseotherdetailsComponent.ts @@ -0,0 +1,77 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-details-closeotherdetails.html' +}) +export class TableDetailsCloseotherdetailsComponent implements OnInit { + closeOtherDetails: boolean = true; // 设置为true时,则点击展开某一行的详情时,收起其它行的详情;默认不收起其他行 + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: '' + }, + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableDetailsComponent.ts b/src/table/demo/src/app/table/TableDetailsComponent.ts new file mode 100644 index 0000000..89f05a7 --- /dev/null +++ b/src/table/demo/src/app/table/TableDetailsComponent.ts @@ -0,0 +1,55 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-details.html' +}) +export class TableDetailsComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: '' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableDetailsNesttableComponent.ts b/src/table/demo/src/app/table/TableDetailsNesttableComponent.ts new file mode 100644 index 0000000..56c4dac --- /dev/null +++ b/src/table/demo/src/app/table/TableDetailsNesttableComponent.ts @@ -0,0 +1,129 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-details-nesttable.html' +}) +export class TableDetailsNesttableComponent { + displayedData: Array = []; + columns: Array = [ + { + title: '' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com', + data: [ + { + friend: 'Jack', + birthday: new Date(2000, 5, 6), + address: 'London, Park Lane no. 1' + }, + { + friend: 'Edward', + birthday: new Date(2001, 9, 6), + address: 'London, Park Lane no. 0' + } + ] + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com', + data: [ + { + friend: 'Joe Black', + birthday: new Date(1976, 5, 6), + address: 'New York No. 1 Lake Park' + }, + { + friend: 'John Brown', + birthday: new Date(1975, 9, 6), + address: 'Sidney No. 1 Lake Park' + } + ] + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com', + data: [ + { + friend: 'Elisa', + birthday: new Date(2004, 11, 6), + address: 'New York No. 1 Lake Park' + }, + { + friend: 'Jim Red', + birthday: new Date(2005, 6, 16), + address: 'London No. 1 Lake Park' + } + ] + } + ], + state: undefined + }; +} + +@Component({ + selector: 'nested-table', + styleUrls: ['./tableTest.less'], + template: ` + + + + + + + + + + + + + +
    {{ column.title }}
    {{ row.friend }}{{ row.birthday | date }}{{ row.address }}
    +
    ` +}) +export class NestedTableComponent implements OnInit { + @Input() data: Array; + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [], + state: undefined + }; + columns: Array = [ + { + title: 'Friend Name' + }, + { + title: 'Friend Age' + }, + { + title: 'Friend Address' + } + ]; + + ngOnInit(): void { + this.srcData.data = this.data; + } +} diff --git a/src/table/demo/src/app/table/TableDetailsPaginationComponent.ts b/src/table/demo/src/app/table/TableDetailsPaginationComponent.ts new file mode 100644 index 0000000..2941c89 --- /dev/null +++ b/src/table/demo/src/app/table/TableDetailsPaginationComponent.ts @@ -0,0 +1,82 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-details-pagination.html' +}) +export class TableDetailsPaginationComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + currentPage: number = 1; + totalNumber: number = 67; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50], + size: 5 + }; + columns: Array = [ + { + title: '' + }, + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableDynamicDetailsComponent.ts b/src/table/demo/src/app/table/TableDynamicDetailsComponent.ts new file mode 100644 index 0000000..e9d17bb --- /dev/null +++ b/src/table/demo/src/app/table/TableDynamicDetailsComponent.ts @@ -0,0 +1,66 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-dynamic-details.html' +}) +export class TableDynamicDetailsComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: '' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; + + beforeToggle(row: TiTableRowData): void { + if (row.showDetails) { + row.showDetails = false; + } else { + setTimeout(() => { + row.info = `My name is ${row.firstName} ${row.lastName}. I am ${row.age} years old. My email address is ${row.email}.`; + row.showDetails = true; + }, 800); + } + } +} diff --git a/src/table/demo/src/app/table/TableEditallComponent.ts b/src/table/demo/src/app/table/TableEditallComponent.ts new file mode 100644 index 0000000..fc42286 --- /dev/null +++ b/src/table/demo/src/app/table/TableEditallComponent.ts @@ -0,0 +1,80 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-editall.html' +}) +export class TableEditallComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + id: 'row_1', + sourceName: 's2.small.2', + level: '一级', + unsubscribe: '3', + createTime: new Date(2019, 11, 5) + }, + { + id: 'row_2', + sourceName: 's2.medium.2', + level: '三级', + unsubscribe: '1', + createTime: new Date(2022, 1, 15) + }, + { + id: 'row_3', + sourceName: 's2.xlarge.2', + level: '二级', + unsubscribe: '0', + createTime: new Date(2021, 5, 25) + } + ], + state: undefined + }; + columns: Array = [ + { + title: '名称/ID' + }, + { + title: '客户等级' + }, + { + title: '退订次数' + }, + { + title: '创建时间' + }, + { + title: '操作' + } + ]; + + levelOptions: Array = [ + { + label: '一级' + }, + { + label: '二级' + }, + { + label: '三级' + } + ]; + + items: Array = [ + { + label: '删除' + } + ]; + + onDetele(item: any, row: TiTableRowData): void { + this.srcData.data = this.srcData.data.filter((current: TiTableRowData): boolean => { + return current.id !== row.id; + }); + } + + trackByFn(index: number, item: any): string { + return item.id; + } +} diff --git a/src/table/demo/src/app/table/TableEditallTestComponent.ts b/src/table/demo/src/app/table/TableEditallTestComponent.ts new file mode 100644 index 0000000..1b8f91f --- /dev/null +++ b/src/table/demo/src/app/table/TableEditallTestComponent.ts @@ -0,0 +1,168 @@ +import { Component, OnInit } from '@angular/core'; +import { TiActionmenuItem, TiMessageService, TiModalRef, TiTableColumns, TiTableRowData, TiTableSrcData, TiValidators } from '@opentiny/ng'; +import { AbstractControl, FormArray, FormBuilder, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-editall-test.html' +}) +export class TableEditallTestComponent implements OnInit { + // 表格数据 + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: '名称/ID' + }, + { + title: '客户等级' + }, + { + title: '信用账户金额' + }, + { + title: '退订次数' + }, + { + title: '创建时间' + }, + { + title: '操作员' + }, + { + title: '操作' + } + ]; + + // 等级 + levelOptions = [ + { + label: '一级' + }, + { + label: '二级' + }, + { + label: '三级' + } + ]; + + // 表单所有元素 + allFormControl: FormArray = new FormArray([], atLeastOneValidator); + + // 正常行actionmenu选项 + items: Array = [ + { + label: '删除' + } + ]; + + constructor(private fb: FormBuilder, private tiMessage: TiMessageService) {} + + ngOnInit(): void { + for (let j: number = 0; j < 5; j++) { + const item = this.createRandomItem(j); + this.data.push(item); + // 每一行是一个FormGroup, 每一个可输入单元格是一个FormControl + // 所有行组成一个FormArray + this.allFormControl.push(item.formgroupCtrl); + } + this.srcData = { + data: this.data, + + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + // 模拟提交 + onClickSubmit(): void { + this.srcData.data.forEach((item) => { + console.log(item.formgroupCtrl.status); + }); + + this.tiMessage.open({ + content: '提交成功', + close(messageRef: TiModalRef): void { + console.log('on close', messageRef); + }, + dismiss(messageRef: TiModalRef): void { + console.log('on dismiss', messageRef); + } + }); + } + + // 删除 + onSelect(item: any, row: TiTableRowData): void { + if (item.label === '删除') { + const index = this.srcData.data.findIndex((current: TiTableRowData): boolean => { + return current.id === row.id; + }); + this.allFormControl.removeAt(index); + this.srcData.data.splice(index, 1); + } + } + + // 新增一行 + addRow(): void { + const size: Array = ['small', 'medium', 'medium', 'xlargr']; + const sourceName = 's2.' + size[(this.srcData.data.length * 27) % 4] + '.2'; + const id = 'row_' + this.srcData.data.length; + const newRow = { + sourceName, + id, + formgroupCtrl: this.fb.group({ + level: [null, TiValidators.required], + balance: [null, TiValidators.rangeValue(50, 1000)], + unsubscribe: [null, TiValidators.required], + createTime: [null, TiValidators.required], + operator: [null, TiValidators.required] + }) + }; + + this.allFormControl.push(newRow.formgroupCtrl); + + this.srcData.data = [...this.srcData.data, newRow]; + } + + private createRandomItem(id: number): TiTableRowData { + const size: Array = ['small', 'medium', 'medium', 'xlargr']; + const sourceName = 's2.' + size[(id * 27) % 4] + '.2'; + return { + sourceName, + formgroupCtrl: this.fb.group({ + level: [undefined, TiValidators.required], + balance: [null, TiValidators.rangeValue(100, 1000)], + unsubscribe: [null, TiValidators.required], + createTime: [null, TiValidators.required], + operator: [null, TiValidators.required] + }), + id: 'row_' + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} + +// 自定义校验器 +export const atLeastOneValidator: ValidatorFn = (control: FormArray): ValidationErrors | null => { + let validNum = 0; + control.controls.forEach((ctrl: AbstractControl) => { + if (ctrl.status === 'VALID') { + validNum++; + } + console.log(ctrl.status); + }); + + return validNum > 0 + ? null + : { + message: '表格至少填写一行有效数据' + }; +}; diff --git a/src/table/demo/src/app/table/TableEditrowComponent.ts b/src/table/demo/src/app/table/TableEditrowComponent.ts new file mode 100644 index 0000000..5c334e7 --- /dev/null +++ b/src/table/demo/src/app/table/TableEditrowComponent.ts @@ -0,0 +1,123 @@ +import { Component } from '@angular/core'; +import { TiActionmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-editrow.html' +}) +export class TableEditrowComponent { + editingRow: TiTableRowData; + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + id: 'row_1', + sourceName: 's2.small.2', + level: '一级', + unsubscribe: '3', + createTime: new Date(2019, 11, 5) + }, + { + id: 'row_2', + sourceName: 's2.medium.2', + level: '三级', + unsubscribe: '1', + createTime: new Date(2022, 1, 15) + }, + { + id: 'row_3', + sourceName: 's2.xlarge.2', + level: '二级', + unsubscribe: '0', + createTime: new Date(2021, 5, 25) + } + ], + state: undefined + }; + columns: Array = [ + { + title: '名称/ID' + }, + { + title: '客户等级' + }, + { + title: '退订次数' + }, + { + title: '创建时间' + }, + { + title: '操作' + } + ]; + + items: Array = [ + { + label: '编辑' + }, + { + label: '删除' + } + ]; + + editingItems: Array = [ + { + label: '保存' + }, + { + label: '取消' + } + ]; + + levelOptions: Array = [ + { + label: '一级' + }, + { + label: '二级' + }, + { + label: '三级' + } + ]; + + onSelect(item: any, row: TiTableRowData): void { + if (item.label === '编辑') { + this.editingRow = { ...row }; + this.editableRows(false); + } else { + this.srcData.data = this.srcData.data.filter((current: TiTableRowData): boolean => { + return current.id !== row.id; + }); + } + } + + onSelectEditing(item: any, row: TiTableRowData): void { + if (item.label === '保存') { + this.srcData.data = this.srcData.data.map((current: TiTableRowData) => { + if (current.id === row.id) { + current = this.editingRow; + } + + return current; + }); + } + + this.editingRow = undefined; + this.editableRows(true); + } + + trackByFn(index: number, item: any): string { + return item.id; + } + + private editableRows(editable: boolean): void { + this.items = this.items.map((item: TiActionmenuItem): TiActionmenuItem => { + return { + ...item, + disabled: !editable, + tip: editable ? '' : '请先保存处于编辑状态的行' + }; + }); + } +} diff --git a/src/table/demo/src/app/table/TableEditrowTestComponent.ts b/src/table/demo/src/app/table/TableEditrowTestComponent.ts new file mode 100644 index 0000000..b79ae12 --- /dev/null +++ b/src/table/demo/src/app/table/TableEditrowTestComponent.ts @@ -0,0 +1,184 @@ +import { Component, OnInit } from '@angular/core'; +import { TiActionmenuItem, TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-editrow-test.html' +}) +export class TableEditrowTestComponent implements OnInit { + // 表格数据 + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: '名称/ID', + width: '15%' + }, + { + title: '客户等级', + width: '15%' + }, + { + title: '信用账户金额', + width: '15%' + }, + { + title: '退订次数', + width: '15%' + }, + { + title: '创建时间', + width: '15%' + }, + { + title: '操作员', + width: '15%' + }, + { + title: '操作', + width: '10%' + } + ]; + + // 正在编辑行 + editingRow: TiTableRowData; + // 新添加一行 + newRow: TiTableRowData; + // 展示行actionmenu 选项 + items: Array = [ + { + label: '编辑' + }, + { + label: '删除' + } + ]; + // 编辑行actionmenu 选项 + editingItems: Array = [ + { + label: '保存' + }, + { + label: '取消' + } + ]; + + // 新增行actionmenu + addItems: Array = [ + { + label: '添加' + }, + { + label: '取消' + } + ]; + + // 客户等级备选项 + levelOptions: Array = [ + { + label: '一级' + }, + { + label: '二级' + }, + { + label: '三级' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + // 正常行按钮处理-编辑、删除 + onSelect(item: any, row: TiTableRowData): void { + if (item.label === '编辑') { + this.editingRow = { ...row }; + this.editableRows(false); + } else { + // 删除 + this.srcData.data = this.srcData.data.filter((current: TiTableRowData): boolean => { + return current.id !== row.id; + }); + } + } + + // 正在编辑行按钮处理-保存、取消 + onSelectEditing(item: any, row: TiTableRowData): void { + if (item.label === '保存') { + this.srcData.data = this.srcData.data.map((current: TiTableRowData) => { + if (current.id === row.id) { + current = this.editingRow; + } + + return current; + }); + } + + this.editableRows(true); + this.editingRow = undefined; + } + + // 添加按钮处理 + onSelectAdd(item: any): void { + if (item.label === '添加') { + const newRow: TiTableRowData = { ...this.newRow }; + newRow.id = 'row_' + this.srcData.data.length * 911; + this.srcData.data = [newRow].concat(this.srcData.data); + } + + this.editableRows(true); + + this.newRow = undefined; + } + // 添加行 + addRow(): void { + this.newRow = {}; + this.editingRow = undefined; + this.editableRows(false); + } + + private editableRows(editable: boolean): void { + this.items = this.items.map((item: TiActionmenuItem): TiActionmenuItem => { + return { + ...item, + disabled: !editable, + tip: editable ? '' : '请先保存处于编辑状态的行' + }; + }); + } + + // 模拟数据 + private createRandomItem(id: number): TiTableRowData { + const size: Array = ['small', 'medium', 'medium', 'xlarge']; + const sourceName: string = `s2.${size[(id * 29) % 4]}.2`; + const level: string = '一级'; + const balance: number = ((id + 5) * 1000 * 7) % 10000; + const unsubscribe: number = (id * 17) % 5; + const createTime: Date = new Date(1598872204569 - ((id * 100) % 23) * 3600000 * 24 * 30); + const operator: string = 'Operator001'; + const editable: boolean = true; + + return { + sourceName, + level, + balance, + unsubscribe, + createTime, + operator, + editable, + id: 'row_' + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableFilterComponent.ts b/src/table/demo/src/app/table/TableFilterComponent.ts new file mode 100644 index 0000000..243ca2c --- /dev/null +++ b/src/table/demo/src/app/table/TableFilterComponent.ts @@ -0,0 +1,138 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableRowData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-filter.html' +}) +export class TableFilterComponent implements OnInit { + inputValue1: string = 'p'; + inputValue21: string = ''; + inputValue22: string = ''; + oneWordSearch: any; + moreThanOneWordSearch: any; + data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + searchedData: Array = []; + @ViewChild('table1', { static: true }) table1: TiTableComponent; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.oneWordSearch = { + displayed: [], // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了搜索特性,且对源数据未进行搜索处理,因此tiny会对数据进行搜索处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }, + searchWords: [this.inputValue1], + searchKeys: ['firstName', 'lastName'] + }; + + this.moreThanOneWordSearch = { + displayed: [], // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }, + // 多列多条件搜索时, searchWords,searchKeys 长度相同, searchWords无搜索时设置为空字符串如['', ''] + searchWords: [this.inputValue21, this.inputValue22], + searchKeys: ['firstName', 'age'] + }; + } + + setOneWordSearch(value: string): void { + this.oneWordSearch.searchWords[0] = value; + } + + setMoreThanOneWordSearch(value: string, index: number): void { + this.moreThanOneWordSearch.searchWords[index] = value; + } + + getSearchedResult(): void { + // TiTableComponent提供了getSearchedResult方法,可通过此方法获取到搜索的数据结果。 + this.searchedData = this.table1.getSearchedResult(); + console.log('searchedData', this.searchedData); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id, + test: { + a: 'hello', + b: 'welcome' + } + }; + } + + changeData(): void { + const temp: TiTableRowData = { + firstName: 'slkdfjsadf', + lastName: 'weiorui', + age: 23, + email: 'sdlfk@example.com', + balance: (this.data.length * 761) % 10000, + id: 'weoiriow' + }; + this.oneWordSearch['srcData']['data'].push(temp); + this.moreThanOneWordSearch['srcData']['data'].push(temp); + } + + changeSearchKeys(): void { + this.oneWordSearch['searchKeys'] = []; + } +} diff --git a/src/table/demo/src/app/table/TableFilterStrictComponent.ts b/src/table/demo/src/app/table/TableFilterStrictComponent.ts new file mode 100644 index 0000000..716827e --- /dev/null +++ b/src/table/demo/src/app/table/TableFilterStrictComponent.ts @@ -0,0 +1,140 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableRowData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-filter-strict.html' +}) +export class TableFilterStrictComponent implements OnInit { + inputValue1: string = 'p'; + inputValue21: string = ''; + inputValue22: string = ''; + oneWordSearch: any; + moreThanOneWordSearch: any; + data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + searchedData: Array = []; + @ViewChild('table1', { static: true }) table1: TiTableComponent; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.oneWordSearch = { + displayed: [], // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了搜索特性,且对源数据未进行搜索处理,因此tiny会对数据进行搜索处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }, + searchWords: [this.inputValue1], + searchKeys: ['firstName', 'lastName', 'age'], + searchStrictKeys: ['firstName'] // 指定精确匹配的字段 + }; + + this.moreThanOneWordSearch = { + displayed: [], // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }, + // 多列多条件搜索时, searchWords,searchKeys 长度相同, searchWords无搜索时设置为空字符串如['', ''] + searchWords: [this.inputValue21, this.inputValue22], + searchKeys: ['firstName', 'age'], + searchStrictKeys: ['age'] // 指定精确匹配的字段 + }; + } + + setOneWordSearch(value: string): void { + this.oneWordSearch.searchWords[0] = value; + } + + setMoreThanOneWordSearch(value: string, index: number): void { + this.moreThanOneWordSearch.searchWords[index] = value; + } + + getSearchedResult(): void { + // TiTableComponent提供了getSearchedResult方法,可通过此方法获取到搜索的数据结果。 + this.searchedData = this.table1.getSearchedResult(); + console.log('searchedData', this.searchedData); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'pol', 'Jacques', 'Robert', 'Elisa', 'bjip']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'Bjipuie', 'Polst', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 6]; + const lastName: string = familyName[((id + 3) * 29) % 6]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id, + test: { + a: 'hello', + b: 'welcome' + } + }; + } + + changeData(): void { + const temp: TiTableRowData = { + firstName: 'slkdfjsadf', + lastName: 'weiorui', + age: 23, + email: 'sdlfk@example.com', + balance: (this.data.length * 761) % 10000, + id: 'weoiriow' + }; + this.oneWordSearch['srcData']['data'].push(temp); + this.moreThanOneWordSearch['srcData']['data'].push(temp); + } + + changeSearchKeys(): void { + this.oneWordSearch['searchKeys'] = []; + } +} diff --git a/src/table/demo/src/app/table/TableFixedHeadColsResizableComponent.ts b/src/table/demo/src/app/table/TableFixedHeadColsResizableComponent.ts new file mode 100644 index 0000000..ba71a7a --- /dev/null +++ b/src/table/demo/src/app/table/TableFixedHeadColsResizableComponent.ts @@ -0,0 +1,74 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-fixed-head-cols-resizable.html' +}) +export class TableFixedHeadColsResizableComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 5; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableFixedHeadComponent.ts b/src/table/demo/src/app/table/TableFixedHeadComponent.ts new file mode 100644 index 0000000..09bc42c --- /dev/null +++ b/src/table/demo/src/app/table/TableFixedHeadComponent.ts @@ -0,0 +1,76 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-fixed-head.html' +}) +export class TableFixedHeadComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + }, + { + firstName: 'Pol', + lastName: 'Dupont', + age: 22, + email: 'Pol@example.com' + }, + { + firstName: 'Adelina', + lastName: 'Germain', + age: 30, + email: 'Adelina@example.com' + }, + { + firstName: 'Abner', + lastName: 'Delcourt', + age: 29, + email: 'Abner@example.com' + }, + { + firstName: 'Emma', + lastName: 'Menez', + age: 48, + email: 'Emma@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableFixedHeadInAccordionComponent.ts b/src/table/demo/src/app/table/TableFixedHeadInAccordionComponent.ts new file mode 100644 index 0000000..e3e5b04 --- /dev/null +++ b/src/table/demo/src/app/table/TableFixedHeadInAccordionComponent.ts @@ -0,0 +1,76 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-fixed-head-in-accordion.html' +}) +export class TableFixedHeadInAccordionComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + headClass: string = 'headCls'; + bodyClass: string = 'bodyCls'; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 18; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableFixedHeadNodataComponent.ts b/src/table/demo/src/app/table/TableFixedHeadNodataComponent.ts new file mode 100644 index 0000000..c2a5b77 --- /dev/null +++ b/src/table/demo/src/app/table/TableFixedHeadNodataComponent.ts @@ -0,0 +1,58 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-fixed-head-nodata.html' +}) +export class TableFixedHeadNodataComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + noDadaInfo: string = '暂无表格数据'; + status: number = 404; // 404应该是http请求返回的状态码,此处只是用此变量做模拟 + failLoadInfo: string = '加载失败,请'; + reloadInfo: string = '重新加载'; + noGoodsInfo: string = '您还没有购买任何商品,去'; + goShopInfo: string = '购买商品'; + + columns: TiTableColumns = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } +} diff --git a/src/table/demo/src/app/table/TableFixedHeadPaginationDetailsComponent.ts b/src/table/demo/src/app/table/TableFixedHeadPaginationDetailsComponent.ts new file mode 100644 index 0000000..205f8da --- /dev/null +++ b/src/table/demo/src/app/table/TableFixedHeadPaginationDetailsComponent.ts @@ -0,0 +1,83 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-fixed-head-pagination-details.html' +}) +export class TableFixedHeadPaginationDetailsComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + currentPage: number = 10; + totalNumber: number = 23; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50, 100], + size: 10 + }; + columns: TiTableColumns = [ + { + title: '' + }, + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableFixedheadColsresizablePaginationDetailsComponent.ts b/src/table/demo/src/app/table/TableFixedheadColsresizablePaginationDetailsComponent.ts new file mode 100644 index 0000000..06c70b8 --- /dev/null +++ b/src/table/demo/src/app/table/TableFixedheadColsresizablePaginationDetailsComponent.ts @@ -0,0 +1,83 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-fixedhead-colsresizable-pagination-details.html' +}) +export class TableFixedheadColsresizablePaginationDetailsComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + currentPage: number = 10; + totalNumber: number = 23; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50, 100], + size: 10 + }; + columns: Array = [ + { + title: '' + }, + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableFixheadScrollComponent.ts b/src/table/demo/src/app/table/TableFixheadScrollComponent.ts new file mode 100644 index 0000000..eac0aa1 --- /dev/null +++ b/src/table/demo/src/app/table/TableFixheadScrollComponent.ts @@ -0,0 +1,81 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +declare let global: any; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-fixhead-scroll.html' +}) +export class TableFixheadScrollComponent implements OnInit { + // 列表数据 + items: Array = []; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + noDadaInfo: string = '暂无表格数据'; + + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + this.items.push((j * 113) % 29); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + addItems(): void { + this.items.push(333); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 37) % 4]; + const lastName: string = familyName[((id + 3) * 17) % 4]; + const age: number = Math.floor(((id + 3) * 73) % 100); + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 100) * 181) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableGroupComponent.ts b/src/table/demo/src/app/table/TableGroupComponent.ts new file mode 100644 index 0000000..9587a12 --- /dev/null +++ b/src/table/demo/src/app/table/TableGroupComponent.ts @@ -0,0 +1,151 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-group.html' +}) +export class TableGroupComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'Size' + }, + { + title: 'Type' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [ + { + name: 'Group1: ti-table group test', + type: 'Folder', + size: '--', + showSub: true, + subData: [ + { + name: 'Work', + type: 'Folder', + size: '2.08MB', + showSub: true + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + } + ] + }, + { + name: 'Group2: ti-table group test', + type: 'Folder', + size: '--', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + } + ] + }, + { + name: 'Group3: ti-table group test', + type: 'Folder', + size: '--', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + } + ] + }, + { + name: 'Group4: ti-table group test', + type: 'Folder', + size: '--', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + } + ] + }, + { + name: 'Group5: ti-table group test', + type: 'Folder', + size: '--', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + }, + { + name: 'Work', + type: 'Folder', + size: '2.08MB' + } + ] + } + ], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } +} diff --git a/src/table/demo/src/app/table/TableGuideComponent.ts b/src/table/demo/src/app/table/TableGuideComponent.ts new file mode 100644 index 0000000..562308f --- /dev/null +++ b/src/table/demo/src/app/table/TableGuideComponent.ts @@ -0,0 +1,55 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-guide.html' +}) +export class TableGuideComponent implements OnInit { + noGoodsInfo: string = '您还没有购买商品,去'; + goShopInfo: string = '购买商品'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + goShopFn(): void { + console.log('去购物'); + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterComponent.ts b/src/table/demo/src/app/table/TableHeadFilterComponent.ts new file mode 100644 index 0000000..9c83b09 --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterComponent.ts @@ -0,0 +1,183 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-head-filter.html' +}) +export class TableHeadFilterComponent { + displayedData: Array = []; + noDadaInfo: string = '暂无数据'; + baseData: Array = [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + gender: 'male', + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + gender: 'female', + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + gender: 'male', + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + gender: 'female', + email: 'Elisa@example.com' + } + ]; + srcData: TiTableSrcData = { + data: this.baseData, + state: undefined + }; + columns: Array = [ + { + title: 'First Name', + key: 'firstName', + selected: null, + labelKey: 'label', // 默认值 + panelAlign: 'left', // 默认值 + panelWidth: 'auto', // 默认值 + options: [ + { + label: 'all' + }, + { + label: 'Pierre' + }, + { + label: 'Jacques' + }, + { + label: 'Robert' + }, + { + label: 'Elisa' + } + ] + }, + { + title: 'Last Name', + key: 'lastName', + selected: null, + multiple: true, + searchable: true, + labelKey: 'label', + panelAlign: 'left', + panelWidth: 'auto', + options: [ + { + label: 'Dupont' + }, + { + label: 'Germain' + }, + { + label: 'Delcourt' + }, + { + label: 'Menez' + } + ] + }, + { + title: 'Age', + key: 'age', + labelKey: 'label', + panelAlign: 'left', + panelWidth: '100px', + virtual: true, + searchable: true, + options: this.getAgeOptions() + }, + { + title: 'Gender', + key: 'gender', + labelKey: 'type', + multiple: true, + selectAll: true, + panelAlign: 'left', + panelWidth: 'auto', + options: [ + { + type: 'female' + }, + { + type: 'male' + } + ] + }, + { + title: 'Email Address', + key: 'email', + panelAlign: 'right', + options: [ + { + label: 'all' + }, + { + label: '@example.com' + }, + { + label: '@example.com' + }, + { + label: '@example.com' + }, + { + label: '@example.com' + } + ] + } + ]; + + onSelect(option: any): void { + this.srcData.data = this.baseData.filter((rowData: TiTableRowData) => { + for (const column of this.columns) { + const labelKey: string = column.labelKey || 'label'; + if (!column.multiple && column.selected) { + if (column.selected[labelKey] === 'all') { + continue; + } + const isMatched: boolean = + column.key === 'email' + ? rowData[column.key].indexOf(column.selected[labelKey]) >= 0 + : column.selected[labelKey] === rowData[column.key]; + if (!isMatched) { + return false; + } + } + if (column.multiple && column.selected && column.selected.length > 0) { + const index: number = column.selected.findIndex((item: any) => { + return item[labelKey] === rowData[column.key]; + }); + if (index < 0) { + return false; + } + } + } + + return true; + }); + } + + private getAgeOptions(): Array<{ label: any }> { + const options: Array<{ label: any }> = [{ label: 'all' }]; + for (let i = 12; i <= 90; i++) { + options.push({ label: i }); + } + + return options; + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterDatetimeComponent.ts b/src/table/demo/src/app/table/TableHeadFilterDatetimeComponent.ts new file mode 100644 index 0000000..1e66da5 --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterDatetimeComponent.ts @@ -0,0 +1,124 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-head-filter-datetime.html' +}) +export class TableHeadFilterDatetimeComponent { + displayedData: Array = []; + noDadaInfo: string = '暂无数据'; + baseData: Array = [ + { + name: 'Pierre Dupont', + birthday: '1993-07-22', + hireDate: '2015-04-28', + start: '2018-07-21 00:00:00', + expired: '2022-07-21 23:59:59' + }, + { + name: 'Jacques Germain', + birthday: '1996-09-02', + hireDate: '2018-12-08', + start: '2019-07-03 08:00:00', + expired: '2028-07-21 22:00:00' + }, + { + name: 'Robert Delcourt', + birthday: '1989-06-20', + hireDate: '2014-05-30', + start: '2015-08-11 06:00:12', + expired: '2019-07-11 21:12:59' + }, + { + name: 'Elisa Menez', + birthday: '2000-06-16', + hireDate: '2022-02-20', + start: '2022-11-01 13:15:00', + expired: '2024-10-01 18:32:59' + } + ]; + srcData: TiTableSrcData = { + data: this.baseData, + state: undefined + }; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'Birthday', + key: 'birthday', + selected: null, + panelAlign: 'left', + isDatetime: true, + datetimeConfig: { + onlyDate: true, + max: new Date(2000, 11, 1), + min: new Date(1989, 5, 1) + } + }, + { + title: 'Hire Date', + key: 'hireDate', + panelAlign: 'left', + isDatetime: true, + datetimeConfig: { + onlyDate: true, + format: 'yyyy-MM-dd' + } + }, + { + title: 'start', + key: 'start', + panelAlign: 'left', + isDatetime: true, + datetimeConfig: { + format: { + date: 'yyyy-MM-dd', + time: 'HH:mm:ss' + } + } + }, + { + title: 'Expired', + key: 'expired', + panelAlign: 'right', + isDatetime: true, + datetimeConfig: { + max: new Date(2026, 12, 25, 21, 59, 59), + min: new Date(2019, 6, 1, 0, 0, 0) + } + } + ]; + + onSelect(selected: { start: Date; end: Date; type: string }): void { + this.srcData.data = this.baseData.filter((rowData: TiTableRowData) => { + for (const column of this.columns) { + if (column.isDatetime && column.selected) { + const startTimestamp: number = column.selected.start && Date.parse(column.selected.start); + const endTimestamp: number = column.selected.end && Date.parse(column.selected.end); + const dataTimestamp: number = rowData[column.key] && Date.parse(rowData[column.key]); + let isMatched: boolean = true; + + if (startTimestamp && endTimestamp) { + isMatched = startTimestamp <= dataTimestamp && dataTimestamp <= endTimestamp; + } + + if (startTimestamp && !endTimestamp) { + isMatched = startTimestamp <= dataTimestamp; + } + + if (!startTimestamp && endTimestamp) { + isMatched = dataTimestamp <= endTimestamp; + } + + if (!isMatched) { + return false; + } + } + } + + return true; + }); + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterDatetimeTestComponent.ts b/src/table/demo/src/app/table/TableHeadFilterDatetimeTestComponent.ts new file mode 100644 index 0000000..5732c99 --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterDatetimeTestComponent.ts @@ -0,0 +1,213 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-head-filter-datetime-test.html' +}) +export class TableHeadFilterDatetimeTestComponent implements OnInit { + public displayed: Array = []; + public srcData: TiTableSrcData; + private data: Array = []; + public columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'birthday', + width: '20%', + key: 'birthday', + selected: undefined, + panelAlign: 'left', + isDatetime: true, + datetimeConfig: { + // 此项为可选项,此处是为了展示如何使用 + format: { + date: 'yyyy-MM-dd', + time: 'HH:mm:ss' + } + } + }, + { + title: 'hireDate', + width: '20%', + key: 'hireDate', + selected: undefined, + panelAlign: 'left', + isDatetime: true, + datetimeConfig: { + // 此项为可选项,此处为了展示如何使用 + max: new Date(2024, 9, 13, 21, 6, 59), + min: new Date(2020, 8, 1, 8, 30, 0) + } + }, + { + title: 'start', + width: '20%', + key: 'start', + selected: undefined, + panelAlign: 'left', + isDatetime: true, + datetimeConfig: { + onlyDate: true, + format: 'yyyy-MM-dd' // 此项为可选项,此处为了展示如何使用 + } + }, + { + title: 'expired', + width: '20%', + key: 'expired', + selected: undefined, + isDatetime: true, + panelAlign: 'right', + datetimeConfig: { + onlyDate: true, + max: new Date(2024, 10, 31), // 此项为可选项,此处为了展示如何使用 + min: new Date(2020, 9, 1) // 此项为可选项,此处为了展示如何使用 + } + } + ]; + + ngOnInit(): void { + // 随机生成10条数据 + for (let j: number = 0; j < 10; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + // 设置初始化第二列的 headfilter 的选中项,start表示开始时间,end表示结束时间,start,end需传入Date类型 + this.columns[1].selected = { + start: new Date(2021, 6, 21, 40, 23, 45), + end: new Date(2021, 6, 22, 40, 23, 45) + }; + // 根据初始化第二列的 headfilter 的选中项对表格筛选 + this.onSelect(this.columns[1].selected, this.columns[1]); + + // 设置初始化第四列的 headfilter 的选中项,start表示开始时间,end表示结束时间,start,end需传入Date类型 + this.columns[3].selected = { + start: new Date(2021, 6, 21), + end: new Date(2021, 6, 22, 23, 59, 59) + }; + // 根据初始化第四列的 headfilter 的选中项对表格筛选 + this.onSelect(this.columns[3].selected, this.columns[3]); + } + + /* 表格自带搜索功能不满足此种场景,此示例是在 onselect 事件中通过对表格的 srcData 进行处理实现过滤。 + * 通过select事件,获取时间日期下拉中的选中项,时间日期的选中项包括: + { + start:开始时间 + end: 结束时间 + type: 'datetime' 当前选中项的面板类型,方便用户进行选中面板的辨别 + } + **/ + + /** + * @param items 当前选中项 + * @param column 表格中配置的每个列的表头信息 + */ + public onSelect(items: any, column: TiTableColumns): void { + // 从每一行进行过滤筛选 + this.srcData.data = this.data.filter((rowData: TiTableRowData) => { + // 遍历所有列 + for (const columnData of this.columns) { + // 只有筛选列有选中项时进行筛选,如果某一筛选列选中项不包含当前行数据,则跳出循环 + if (columnData.isDatetime && columnData.selected) { + // 将时间日期转为时间戳,通过时间戳进行日期大小的比较 + const start: number = this.isValidDate(columnData.selected.start) ? Date.parse(columnData.selected.start) : undefined; + const currentTime: number = this.isValidDate(new Date(rowData[columnData.key])) ? Date.parse(rowData[columnData.key]) : undefined; + const end: number = this.isValidDate(columnData.selected.end) ? Date.parse(columnData.selected.end) : undefined; + let exit: boolean = true; + + // 如果当时值不存在或者不是有效的时间日期格式,则怕排查此列,跳出循环 + if (!currentTime) { + return false; + } + + // 开始结束时间都存在 + if (start && end) { + exit = start <= currentTime && currentTime <= end; + } + + // 开始时间存在,结束时间不存在, + if (start && !end) { + exit = start <= currentTime; + } + + // 结束时间存在,开始时间不存在 + if (!start && end) { + exit = currentTime <= end; + } + + if (exit === false) { + return false; + } + } + } + + return true; + }); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const firstName: string = nameList[((id + 3) * 19) % 5]; + // 时间日期格式列 + const birthday: string = this.timestampToTime(new Date(2021, 6, 21, 40, 23, 45).getTime() + id * 1000 * 60 * 60 * 24); + // 日期格式列,测试时间日期面板是否可以过滤日期格式 + const hireDate: string = this.timestampToTime(new Date(2021, 6, 21).getTime() + id * 1000 * 60 * 60 * 24 * 100); + const expired: string = this.timestampToTime(new Date(2021, 6, 21).getTime() + id * 1000 * 60 * 60 * 24 * 100, true); + const start: string = this.timestampToTime(new Date(2021, 6, 21).getTime() + id * 1000 * 60 * 60 * 24 * 50, true); + + return { + start, + birthday, + hireDate, + expired, + firstName, + id + }; + } + + // 将时间戳转换为时间格式,此处的函数是为了创建表格中的随机时间,服务可自行设置自己的时间 + public timestampToTime(timestamp: number, isDateFormat?: boolean): string { + const date: Date = new Date(timestamp); // 时间戳为10位需*1000,时间戳为13位的话不需乘1000 + + const Y: string = date.getFullYear() + '-'; + + const M: string = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'; + + const D: string = (date.getDate() < 10 ? '0' + date.getDate() : date.getDate()) + ' '; + + const h: string = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours()) + ':'; + + const m: string = (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()) + ':'; + + const s: string | number = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds(); + + // 返回日期格式 + if (isDateFormat) { + return Y + M + D; + } + + // 返回时间日期格式 + return Y + M + D + h + m + s; + } + + // 判断当前值是否日期格式 + public isValidDate(dateTemp: Date | string): boolean { + let date: Date | string = dateTemp; + if (Object.prototype.toString.call(date) === '[object String]') { + // 转为时间格式 + date = new Date(dateTemp); + } + + return Object.prototype.toString.call(date) === '[object Date]' && String(date) !== 'Invalid Date'; + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterMultiComponent.ts b/src/table/demo/src/app/table/TableHeadFilterMultiComponent.ts new file mode 100644 index 0000000..15398dd --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterMultiComponent.ts @@ -0,0 +1,152 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-head-filter-multi.html' +}) +export class TableHeadFilterMultiComponent implements OnInit { + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + key: 'firstName', // 该列的 headfilter 要过滤的字段 + selected: null, + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'Pierre' + }, + { + label: 'Jacques' + }, + { + label: 'Robert' + }, + { + label: 'Pol' + }, + { + label: 'Elisa' + } + ], + multiple: true + }, + { + title: 'last name', + width: '20%', + key: 'lastName', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + searchable: true, // 该列的 headfilter 的下拉中是否开启搜索功能 + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'Dupont' + }, + { + label: 'Germain' + }, + { + label: 'Delcourt' + }, + { + label: 'bjip' + }, + { + label: 'Menez' + }, + { + label: 'Henry' + }, + { + label: 'Jeff' + }, + { + label: 'John' + }, + { + label: 'Elizabeth' + } + ], + multiple: true, + selectAll: true + }, + { + title: 'birth date', + width: '20%' + }, + { + title: 'balance', + width: '20%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + // 随机生成10条数据 + for (let j: number = 0; j < 10; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + // 设置初始化第一列 headfilter 的选中项 + this.columns[0].selected = [this.columns[0].options[0], this.columns[0].options[1]]; + // 根据初始化第一列 headfilter 的选中项对表格筛选 + this.onSelect(this.columns[0].selected, this.columns[0]); + } + + // 表格自带搜索功能不满足此种场景,此示例是在 onselect 事件中通过对表格的 srcData 进行处理实现过滤。 + onSelect(items: any, column: TiTableColumns): void { + console.log('select', items); + console.log('column', column); + + // 从每一行进行过滤筛选 + this.srcData.data = this.data.filter((rowData: TiTableRowData) => { + // 遍历所有列 + for (const columnData of this.columns) { + // 只有筛选列有选中项时进行筛选,如果某一筛选列选中项不包含当前行数据,则跳出循环 + if (columnData.selected && columnData.selected.length) { + const index: number = columnData.selected.findIndex((item: any) => { + return item.label === rowData[columnData.key]; + }); + if (index < 0) { + return false; + } + } + } + + return true; + }); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez', 'Henry', 'Jeff', 'John', 'Elizabeth']; + const firstName: string = nameList[((id + 3) * 19) % 5]; + const lastName: string = familyName[((id + 3) * 19) % 9]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterMultiValuekeyComponent.ts b/src/table/demo/src/app/table/TableHeadFilterMultiValuekeyComponent.ts new file mode 100644 index 0000000..0854236 --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterMultiValuekeyComponent.ts @@ -0,0 +1,222 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-head-filter-multi-valuekey.html' +}) +export class TableHeadFilterMultiValuekeyComponent implements OnInit { + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + key: 'firstName', // 该列的 headfilter 要过滤的字段 + selected: null, + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'Pol', + value: '玫瑰' + }, + { + label: 'Pierre', + value: '皮特' + }, + { + label: 'Jacques', + value: '杰克' + }, + { + label: 'Robert', + value: '罗伯特' + } + ], + multiple: true + }, + { + title: 'last name', + width: '20%', + key: 'lastName', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + searchable: true, // 该列的 headfilter 的下拉中是否开启搜索功能 + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'Dupont', + value: '李白' + }, + { + label: 'Germain', + value: '杜甫' + }, + { + label: 'Delcourt', + value: '杜牧' + }, + { + label: 'bjip', + value: '王维' + }, + { + label: 'Menez', + value: '李清照' + }, + { + label: 'Henry', + value: '白居易' + }, + { + label: 'Jeff', + value: '范仲淹' + }, + { + label: 'John', + value: '陆游' + }, + { + label: 'Elizabeth', + value: '孟浩然' + } + ], + multiple: true, + selectAll: true + }, + { + title: 'birth date', + width: '20%' + }, + { + title: 'balance', + width: '20%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + // 随机生成10条数据 + for (let j: number = 0; j < 10; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + // 设置初始化第一列 headfilter 的选中项,此处的选中项根基于valueKey进行设置 + this.columns[0].selected = [this.columns[0].options[0].value, this.columns[0].options[1].value]; + const items: Array = [this.columns[0].options[0], this.columns[0].options[1]]; + // 根据初始化第一列 headfilter 的选中项对表格筛选 + this.onSelect(items, this.columns[0]); + } + + // 表格自带搜索功能不满足此种场景,此示例是在 onselect 事件中通过对表格的 srcData 进行处理实现过滤。 + /** + * @param items 当前的选中项 + * @param column 当前列的数据 column.selected的值是通过keyValue获得的当前选中项 + */ + onSelect(items: any, column: TiTableColumns): void { + // 从每一行进行过滤筛选 + this.srcData.data = this.data.filter((rowData: TiTableRowData) => { + // 遍历所有列 + for (const columnData of this.columns) { + // 只有筛选列有选中项时进行筛选,如果某一筛选列选中项不包含当前行数据,则跳出循环 + if (columnData.selected && columnData.selected.length) { + const index: number = columnData.selected.findIndex((selectedItem: any) => { + return selectedItem === rowData[columnData.key].value; + }); + if (index < 0) { + return false; + } + } + } + + return true; + }); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = [ + { + label: 'Pierre', + value: '皮特' + }, + { + label: 'Jacques', + value: '杰克' + }, + { + label: 'Robert', + value: '罗伯特' + }, + { + label: 'Pol', + value: '玫瑰' + }, + { + label: 'Elisa', + value: '安蕾斯' + } + ]; + const familyName: Array = [ + { + label: 'Dupont', + value: '李白' + }, + { + label: 'Germain', + value: '杜甫' + }, + { + label: 'Delcourt', + value: '杜牧' + }, + { + label: 'bjip', + value: '王维' + }, + { + label: 'Menez', + value: '李清照' + }, + { + label: 'Henry', + value: '白居易' + }, + { + label: 'Jeff', + value: '范仲淹' + }, + { + label: 'John', + value: '陆游' + }, + { + label: 'Elizabeth', + value: '孟浩然' + } + ]; + const firstName: object = nameList[((id + 3) * 19) % 5]; + const lastName: object = familyName[((id + 3) * 19) % 9]; + const age: number = ((id + 3) * 13) % 100; + const balance: number = ((id + 3) * 761) % 10000; + const email: string = `${age}${balance}@whatever.com`; + + return { + firstName, + lastName, + age, + balance, + email, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterTestComponent.ts b/src/table/demo/src/app/table/TableHeadFilterTestComponent.ts new file mode 100644 index 0000000..802a65d --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterTestComponent.ts @@ -0,0 +1,184 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-head-filter-test.html' +}) +export class TableHeadFilterTestComponent implements OnInit { + searchWords: Array = ['', '', '', '']; + searchKeys: Array = ['firstName', 'lastName', 'gender', 'email']; // 设置过滤字段 + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + key: 'firstName', // 该列的 headfilter 要过滤的字段 + selected: null, + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'all' + }, + { + label: 'Pierre' + }, + { + label: 'Jacques' + }, + { + label: 'Robert' + } + ] + }, + { + title: 'last name', + width: '8%', + key: 'lastName', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + searchable: true, // 该列的 headfilter 的下拉中是否开启搜索功能 + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'all' + }, + { + label: 'Dupont' + }, + { + label: 'Germain' + }, + { + label: 'Delcourt' + }, + { + label: 'bjip' + }, + { + label: 'Menez' + }, + { + label: 'Henry' + }, + { + label: 'Jeff' + }, + { + label: 'John' + }, + { + label: 'Elizabeth' + } + ] + }, + { + title: 'birth date', + width: '20%' + }, + { + title: 'balance', + width: '15%' + }, + { + title: 'email', + searchable: true, + key: 'email', // 该列的 headfilter 要过滤的字段 + labelKey: 'label', // 该列的 headfilter 下拉中显示的字段 + panelWidth: '120px', // 该列的 headfilter 的下拉框宽度 + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'all' + }, + { + label: 'Pierre' + }, + { + label: 'Pol' + }, + { + label: 'Jacques' + }, + { + label: 'Robert' + }, + { + label: 'Elisa' + } + ] + }, + { + title: 'classification', + key: 'gender', // 该列的 headfilter 要过滤的字段 + selected: null, + searchable: false, + labelKey: 'type', // 该列的 headfilter 下拉中显示的字段 + panelWidth: '120px', // 该列的 headfilter 的下拉框宽度 + panelAlign: 'right', // 该列的 headfilter 下拉面板对齐方式,默认左对齐 + options: [ + { + // 该列的 headfilter 下拉选择项 + type: 'all' + }, + { + type: 'girl' + }, + { + type: 'boy' + } + ] + } + ]; + + ngOnInit(): void { + // 设置初始化第一列 headfilter 的选中项 + this.columns[0]['selected'] = this.columns[0].options[1]; + // 根据 headfilter 选中项给表格的搜索接口传入对应的搜索值,进行表格搜索 + const index: number = this.searchKeys.indexOf(this.columns[0].key); + this.searchWords[index] = this.columns[0].options[1].label; + + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + // 使 headfilter 和表格搜索联动。根据 headfilter 选中项给表格的搜索接口传入对应的搜索值,进行表格数据搜索。 + // 此示例是利用表格提供的搜索功能来搜索,也可在此事件中来自己实现搜索。 + onSelect(item: any, column: TiTableColumns): void { + const index: number = this.searchKeys.indexOf(column.key); + const labelKey: string = column.labelKey || 'label'; + this.searchWords[index] = item[labelKey] === 'all' ? '' : item[labelKey]; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez', 'Henry', 'Jeff', 'John', 'Elizabeth']; + const firstName: string = nameList[((id + 3) * 19) % 5]; + const lastName: string = familyName[((id + 3) * 19) % 9]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const gender: string = ['girl', 'boy'][((id + 3) * 19) % 2]; + + return { + firstName, + lastName, + age, + email, + balance, + gender, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterValuekeyComponent.ts b/src/table/demo/src/app/table/TableHeadFilterValuekeyComponent.ts new file mode 100644 index 0000000..9ffb044 --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterValuekeyComponent.ts @@ -0,0 +1,226 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-head-filter-valuekey.html' +}) +export class TableHeadFilterValuekeyComponent implements OnInit { + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%', + key: 'firstName', // 该列的 headfilter 要过滤的字段 + selected: null, + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'all', + value: '' + }, + { + label: 'Pierre', + value: '皮特' + }, + { + label: 'Jacques', + value: '杰克' + }, + { + label: 'Robert', + value: '罗伯特' + } + ] + }, + { + title: 'last name', + width: '20%', + key: 'lastName', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + searchable: true, // 该列的 headfilter 的下拉中是否开启搜索功能 + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'all', + value: '' + }, + { + label: 'Dupont', + value: '李白' + }, + { + label: 'Germain', + value: '杜甫' + }, + { + label: 'Delcourt', + value: '杜牧' + }, + { + label: 'bjip', + value: '王维' + }, + { + label: 'Menez', + value: '李清照' + }, + { + label: 'Henry', + value: '白居易' + }, + { + label: 'Jeff', + value: '范仲淹' + }, + { + label: 'John', + value: '陆游' + }, + { + label: 'Elizabeth', + value: '孟浩然' + } + ] + }, + { + title: 'birth date', + width: '20%' + }, + { + title: 'balance', + width: '20%' + }, + { + title: 'email', + width: '20%' + }, + { + title: 'classification', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + // 设置初始化第一列 headfilter 的选中项,此处的选中项根基于valueKey进行设置 + this.columns[0].selected = this.columns[0].options[1].value; + const items: Array = this.columns[0].options[0]; + // 根据初始化第一列 headfilter 的选中项对表格筛选 + this.onSelect(items, this.columns[0]); + } + + // 表格自带搜索功能不满足此种场景,此示例是在 onselect 事件中通过对表格的 srcData 进行处理实现过滤。 + /** + * @param items 当前的选中项 + * @param column 当前列的数据 column.selected的值是通过keyValue获得的当前选中项 + */ + onSelect(item: any, column: TiTableColumns): void { + this.srcData.data = this.data.filter((rowData: TiTableRowData) => { + // 遍历所有列 + for (const columnData of this.columns) { + // 只有筛选列有选中项时进行筛选,如果某一筛选列选中项不包含当前行数据,则跳出循环 + if (columnData.selected) { + const isExit: boolean = columnData.selected === rowData[columnData.key].value; + if (!isExit) { + return false; + } + } + } + + return true; + }); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = [ + { + label: 'Pierre', + value: '皮特' + }, + { + label: 'Jacques', + value: '杰克' + }, + { + label: 'Robert', + value: '罗伯特' + }, + { + label: 'Pol', + value: '玫瑰' + }, + { + label: 'Elisa', + value: '安蕾斯' + } + ]; + const familyName: Array = [ + { + label: 'Dupont', + value: '李白' + }, + { + label: 'Germain', + value: '杜甫' + }, + { + label: 'Delcourt', + value: '杜牧' + }, + { + label: 'bjip', + value: '王维' + }, + { + label: 'Menez', + value: '李清照' + }, + { + label: 'Henry', + value: '白居易' + }, + { + label: 'Jeff', + value: '范仲淹' + }, + { + label: 'John', + value: '陆游' + }, + { + label: 'Elizabeth', + value: '孟浩然' + } + ]; + const firstName: string = nameList[((id + 3) * 19) % 5]; + const lastName: string = familyName[((id + 3) * 19) % 9]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const gender: string = ['girl', 'boy'][((id + 3) * 19) % 2]; + + return { + firstName, + lastName, + age, + email, + balance, + gender, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableHeadFilterVirtualscrollComponent.ts b/src/table/demo/src/app/table/TableHeadFilterVirtualscrollComponent.ts new file mode 100644 index 0000000..63cb6e0 --- /dev/null +++ b/src/table/demo/src/app/table/TableHeadFilterVirtualscrollComponent.ts @@ -0,0 +1,178 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-head-filter-virtualscroll.html' +}) +export class TableHeadFilterVirtualscrollComponent implements OnInit { + displayed: Array = []; + srcData: TiTableSrcData; + private data: Array = []; + currentPage: number = 1; + totalNumber: number = 30000; + columns: Array = [ + { + title: 'first name', + width: '20%', + key: 'firstName', // 该列的 headfilter 要过滤的字段 + selected: null, + options: [ + { + // 该列的 headfilter 下拉选择项 + label: 'all' + }, + { + label: 'Pierre' + }, + { + label: 'Jacques' + }, + { + label: 'Robert' + } + ] + }, + { + title: 'last name', + width: '8%', + key: 'lastName', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + multiple: true, + options: [ + { + label: 'Dupont' + }, + { + label: 'Germain' + }, + { + label: 'Delcourt' + }, + { + label: 'bjip' + }, + { + label: 'Menez' + }, + { + label: 'Henry' + }, + { + label: 'Jeff' + }, + { + label: 'John' + }, + { + label: 'Elizabeth' + } + ] + }, + { + title: 'total', + width: '20%', + key: 'total', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + searchable: true, + virtual: true, // 该列的 headfilter 的下拉中开启虚拟滚动功能 + options: [] + }, + { + title: 'balance', + width: '30%', + multiple: true, + key: 'balance', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + searchable: true, + virtual: true, // 该列的 headfilter 的下拉中开启虚拟滚动功能 + options: [] + }, + { + title: 'email', + multiple: true, + key: 'email', // 该列的 headfilter 要过滤的字段 + selected: null, // 该列的 headfilter 下拉选中项 + searchable: true, + selectAll: true, + virtual: true, // 该列的 headfilter 的下拉中开启虚拟滚动功能 + options: [] + } + ]; + + ngOnInit(): void { + this.columns[2].options.push({ label: 'all' }); + for (let j: number = 0; j < 10000; j++) { + this.columns[2].options.push({ label: j + ' total' }); + } + + for (let j: number = 0; j < 10000; j++) { + this.columns[3].options.push({ label: j + ' balance' }); + } + + for (let j: number = 0; j < 10000; j++) { + this.columns[4].options.push({ label: j + '@whatever.com' }); + } + + for (let j: number = 0; j < 30000; j++) { + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + } + + // 使 headfilter 和表格搜索联动。根据 headfilter 选中项给表格的搜索接口传入对应的搜索值,进行表格数据搜索。 + // 此示例是利用表格提供的搜索功能来搜索,也可在此事件中来自己实现搜索。 + onSelect(item: any, column: TiTableColumns): void { + // 从每一行进行过滤筛选 + this.srcData.data = this.data.filter((rowData: TiTableRowData) => { + // 遍历所有列 + for (const columnData of this.columns) { + // 只有筛选列有选中项时进行筛选,如果某一筛选列选中项不包含当前行数据,则跳出循环 + if (columnData.selected) { + // 单选过滤 + if (!Array.isArray(columnData.selected) && columnData.selected.label !== 'all') { + if (columnData.selected.label !== rowData[columnData.key]) { + return false; + } + // 多选过滤 + } else if (columnData.selected.length > 0) { + const index: number = columnData.selected.findIndex((item: any) => { + return item.label === rowData[columnData.key]; + }); + if (index < 0) { + return false; + } + } + } + } + + return true; + }); + this.totalNumber = this.srcData.data.length; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez', 'Henry', 'Jeff', 'John', 'Elizabeth']; + const firstName: string = nameList[((id + 3) * 19) % 5]; + const lastName: string = familyName[((id + 3) * 19) % 9]; + const total: string = (((id + 3) * 13) % 10000) + ' total'; + const email: string = (((id + 2) * 19) % 10000) + '@whatever.com'; + const balance: string = (((id + 4) * 761) % 10000) + ' balance'; + return { + firstName, + lastName, + total, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableLoadFailComponent.ts b/src/table/demo/src/app/table/TableLoadFailComponent.ts new file mode 100644 index 0000000..23148d4 --- /dev/null +++ b/src/table/demo/src/app/table/TableLoadFailComponent.ts @@ -0,0 +1,56 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-load-fail.html' +}) +export class TableLoadFailComponent implements OnInit { + status: number = 404; // 404应该是http请求返回的状态码,此处只是用此变量做模拟 + failLoadInfo: string = '加载失败,请'; + reloadInfo: string = '重新加载'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + reloadFn(): void { + console.log('重新加载'); + } +} diff --git a/src/table/demo/src/app/table/TableNodataComponent.ts b/src/table/demo/src/app/table/TableNodataComponent.ts new file mode 100644 index 0000000..4bcbd9f --- /dev/null +++ b/src/table/demo/src/app/table/TableNodataComponent.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-nodata.html' +}) +export class TableNodataComponent { + noDataInfo: string = '暂无表格数据'; + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [], + state: undefined + }; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableNodataSimpleComponent.ts b/src/table/demo/src/app/table/TableNodataSimpleComponent.ts new file mode 100644 index 0000000..1c79ea5 --- /dev/null +++ b/src/table/demo/src/app/table/TableNodataSimpleComponent.ts @@ -0,0 +1,50 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-nodata-simple.html' +}) +export class TableNodataSimpleComponent implements OnInit { + noDadaInfo: string = '暂无表格数据'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } +} diff --git a/src/table/demo/src/app/table/TableNodataTestComponent.ts b/src/table/demo/src/app/table/TableNodataTestComponent.ts new file mode 100644 index 0000000..0ccb72d --- /dev/null +++ b/src/table/demo/src/app/table/TableNodataTestComponent.ts @@ -0,0 +1,50 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-nodata-test.html' +}) +export class TableNodataTestComponent implements OnInit { + noDadaInfo: string = '暂无表格数据'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } +} diff --git a/src/table/demo/src/app/table/TableOverflowLinkComponent.ts b/src/table/demo/src/app/table/TableOverflowLinkComponent.ts new file mode 100644 index 0000000..8238cc7 --- /dev/null +++ b/src/table/demo/src/app/table/TableOverflowLinkComponent.ts @@ -0,0 +1,90 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-overflow-link.html' +}) +export class TableOverflowLinkComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name(默认设置)', + width: '20%' + }, + { + title: 'last name(解决方案一)', + width: '20%' + }, + { + title: 'email(解决方案二)', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = [ + '明月几时有,把酒问青天,不知天上宫阙,今夕是何年', + '我欲乘风归去,又恐琼楼玉宇,高处不胜寒。', + '起舞弄清影,何似在人间。', + '转朱阁,低绮户,照无眠。不应有恨,何事长向别时圆,', + '人有悲欢离合,月有阴晴圆缺,此事古难全。但愿人长久,千里共婵娟。' + ]; + + const familyName: Array = [ + '君不见黄河之水天上来,奔流到海不复回。君不见高堂明镜悲白发,朝如青丝暮成雪。', + '人生得意须尽欢,莫使金樽空对月。天生我材必有用,千金散尽还复来。', + '烹羊宰牛且为乐,会须一饮三百杯。岑夫子,丹丘生,将进酒,杯莫停。', + '与君歌一曲,请君为我倾耳听。钟鼓馔玉不足贵⑮,但愿长醉不复醒。', + '古来圣贤皆寂寞,惟有饮者留其名。陈王昔时宴平乐,斗酒十千恣欢谑。' + ]; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/table/demo/src/app/table/TablePagiWithFilterComponent.ts b/src/table/demo/src/app/table/TablePagiWithFilterComponent.ts new file mode 100644 index 0000000..1613b9b --- /dev/null +++ b/src/table/demo/src/app/table/TablePagiWithFilterComponent.ts @@ -0,0 +1,95 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-pagi-with-filter.html' +}) +export class TablePagiWithFilterComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + inputValue: string = 'p'; + searchWords: Array = [this.inputValue]; + searchKeys: Array = ['firstName', 'lastName', 'age', 'balance', 'email']; + currentPage: number = 1; + totalNumber: number = 62; + pageSize: { options: Array; size: number } = { + options: [10, 20, 50, 100], + size: 10 + }; + columns: TiTableColumns = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + searchedData: Array = []; + @ViewChild('table', { static: true }) table: TiTableComponent; + + ngOnInit(): void { + for (let j: number = 0; j < 62; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + setSearchWords(value: string): void { + this.searchWords[0] = value; + } + + getSearchedResult(): void { + // TiTableComponent提供了getSearchedResult方法,可通过此方法获取到搜索的数据结果。 + this.searchedData = this.table.getSearchedResult(); + console.log('searchedData', this.searchedData); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TablePaginationComponent.ts b/src/table/demo/src/app/table/TablePaginationComponent.ts new file mode 100644 index 0000000..d8f5b29 --- /dev/null +++ b/src/table/demo/src/app/table/TablePaginationComponent.ts @@ -0,0 +1,70 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-pagination.html' +}) +export class TablePaginationComponent implements OnInit { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [], + state: undefined + }; + columns: Array = [ + { + title: 'Id', + width: '10%' + }, + { + title: 'First Name', + width: '20%' + }, + { + title: 'Last Name', + width: '20%' + }, + { + title: 'Age', + width: '10%' + }, + { + title: 'Email Address', + width: '40%' + } + ]; + currentPage: number = 2; + totalNumber: number = 312; + pageSize: { options: Array; size: number } = { + options: [10, 20, 50, 100], + size: 10 + }; + + ngOnInit(): void { + const data: Array = []; + for (let i: number = 0; i < this.totalNumber; i++) { + data.push(this.createRandomItem(i)); + } + this.srcData.data = data; + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'Bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + + return { + firstName, + lastName, + age, + email, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableRadioComponent.ts b/src/table/demo/src/app/table/TableRadioComponent.ts new file mode 100644 index 0000000..c9c0256 --- /dev/null +++ b/src/table/demo/src/app/table/TableRadioComponent.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-radio.html' +}) +export class TableRadioComponent { + displayedData: Array = []; + selectedValue: string; + srcData: TiTableSrcData = { + data: [ + { + id: '1', + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + id: '2', + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com', + disabled: true + }, + { + id: '3', + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + id: '4', + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: '' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; + + trackByFn(index: number, item: any): number { + return item.id; + } +} diff --git a/src/table/demo/src/app/table/TableRadioTestComponent.ts b/src/table/demo/src/app/table/TableRadioTestComponent.ts new file mode 100644 index 0000000..391ae97 --- /dev/null +++ b/src/table/demo/src/app/table/TableRadioTestComponent.ts @@ -0,0 +1,108 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-radio-test.html' +}) +export class TableRadioTestComponent implements OnInit { + currentPage: number = 1; + totalNumber: number = 256; + pageSize: { options: Array; size: number } = { + options: [10, 20, 50, 100], + size: 10 + }; + selectedValue: number; // 选中项 + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: '' + }, + { + title: 'last name' + }, + { + title: 'birth date' + }, + { + title: 'balance' + }, + { + title: 'email' + } + ]; + constructor(private ref: ChangeDetectorRef) {} + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,模拟了后台分页场景 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: true // 源数据已进行分页处理 + } + }; + + // 初始时向后台发送请求获取数据 + this.getCurrentPageData(this.currentPage, this.pageSize.size).then((data: Array) => { + this.srcData.data = data; + this.totalNumber = 256; + this.selectedValue = data[2].id; + }); + } + + // 表格的数据状态(分页,排序,搜索)发生改变时,此方法会被触发; + // 多用于后台分页、排序和搜索 + stateUpdate(tiTable: TiTableComponent): void { + const dataState: TiTableDataState = tiTable.getDataState(); + this.getCurrentPageData(dataState.pagination.currentPage, dataState.pagination.itemsPerPage).then((data: Array) => { + this.srcData.data = data; + }); + } + + // 模拟后台异步请求返回的数据 + private getCurrentPageData(currentPage: number, itemsPerPage: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + const data: Array = []; + for (let j: number = (currentPage - 1) * itemsPerPage; j < currentPage * itemsPerPage; j++) { + data.push(this.createRandomItem(j)); + } + resolve(data); + // 在onpush模式下,异步场景需要手动触发变更,default模式下不用写该行代码 + this.ref.markForCheck(); + }, 1000); + }); + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const disabled: boolean = id % 4 === 0; + + return { + firstName, + lastName, + age, + email, + balance, + disabled, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableRowDrag2Component.ts b/src/table/demo/src/app/table/TableRowDrag2Component.ts new file mode 100644 index 0000000..b64927a --- /dev/null +++ b/src/table/demo/src/app/table/TableRowDrag2Component.ts @@ -0,0 +1,77 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-row-drag2.html' +}) +export class TableRowDrag2Component { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + id: '1', + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + id: '2', + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + id: '3', + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + id: '4', + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'Id' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; + private startIndex: number = 0; + + trackByFn(index: number, item: any): string { + return item.id; + } + + dragstart(index: number): void { + this.startIndex = index; + } + + dragover(event: any): void { + event.preventDefault(); + } + + drop(endIndex: number): void { + const current: TiTableRowData = this.displayedData.splice(this.startIndex, 1); + this.displayedData.splice(endIndex, 0, current[0]); + } +} diff --git a/src/table/demo/src/app/table/TableRowspanComponent.ts b/src/table/demo/src/app/table/TableRowspanComponent.ts new file mode 100644 index 0000000..6269274 --- /dev/null +++ b/src/table/demo/src/app/table/TableRowspanComponent.ts @@ -0,0 +1,58 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-rowspan.html' +}) +export class TableRowspanComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com', + rowSpan: 3, + colSpan: 1 + }, + { + firstName: 'Jacques', + age: 42, + email: 'Jacques@example.com', + rowHide: true, + colSpan: 1 + }, + { + firstName: 'Robert', + age: 15, + email: 'Robert@example.com', + rowHide: true, + colSpan: 1 + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 'no data', + rowSpan: 1, + colSpan: 2, + colHide: true + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableSearchComponent.ts b/src/table/demo/src/app/table/TableSearchComponent.ts new file mode 100644 index 0000000..9cb99e6 --- /dev/null +++ b/src/table/demo/src/app/table/TableSearchComponent.ts @@ -0,0 +1,103 @@ +import { Component } from '@angular/core'; +import { TiTableColumns } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-search.html' +}) +export class TableSearchComponent { + oneWordSearchValue: string = 'p'; + moreThanOneWordSearchValue1: string = ''; + moreThanOneWordSearchValue2: string = ''; + noDadaInfo: string = '暂无数据'; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; + oneWordSearch: any = { + displayedData: [], + srcData: { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }, + searchWords: [this.oneWordSearchValue], + searchKeys: ['firstName', 'lastName', 'age'], + searchStrictKeys: ['age'] + }; + moreThanOneWordSearch: any = { + displayedData: [], + srcData: { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }, + searchWords: [this.moreThanOneWordSearchValue1, this.moreThanOneWordSearchValue2], + searchKeys: ['firstName', 'age'] + }; + + setOneWordSearch(value: string): void { + this.oneWordSearch.searchWords[0] = value; + } + + setMoreThanOneWordSearch(value: string, index: number): void { + this.moreThanOneWordSearch.searchWords[index] = value; + } +} diff --git a/src/table/demo/src/app/table/TableServerPagiComponent.ts b/src/table/demo/src/app/table/TableServerPagiComponent.ts new file mode 100644 index 0000000..efbf93d --- /dev/null +++ b/src/table/demo/src/app/table/TableServerPagiComponent.ts @@ -0,0 +1,111 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-server-pagi.html' +}) +export class TableServerPagiComponent implements OnInit { + myLogs: Array = []; + + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + currentPage: number = 2; + totalNumber: number; + pageSize: { options: Array; size: number } = { + options: [10, 20, 50, 100], + size: 10 + }; + constructor(private ref: ChangeDetectorRef) {} + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了分页特性,且对源数据已进行了分页处理,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: true // 后台分页,源数据已进行了分页处理 + } + }; + + // 初始时向后台发送请求获取数据 + this.getCurrentPageData(this.currentPage, this.pageSize.size).then((data: Array) => { + this.myLogs = [...this.myLogs, '获取数据成功!']; + this.srcData.data = data; + this.totalNumber = 256; + }); + } + + // 表格的数据状态(分页,排序,搜索)发生改变时,此方法会被触发; + // 多用于后台分页、排序和搜索 + stateUpdate(tiTable: TiTableComponent): void { + this.myLogs = [...this.myLogs, `dataState=> ${JSON.stringify(tiTable.getDataState())}`]; + this.myLogs = [...this.myLogs, `triggerEvent=> ${tiTable.getTriggerEvent()}`]; + const dataState: TiTableDataState = tiTable.getDataState(); + this.getCurrentPageData(dataState.pagination.currentPage, dataState.pagination.itemsPerPage).then((data: Array) => { + this.myLogs = [...this.myLogs, '获取数据成功!']; + this.srcData.data = data; + }); + } + + // 模拟后台异步请求返回的数据 + private getCurrentPageData(currentPage: number, itemsPerPage: number): Promise { + this.myLogs = [...this.myLogs, '模拟向后台发送请求']; + return new Promise((resolve, reject) => { + setTimeout(() => { + const data: Array = []; + for (let j: number = (currentPage - 1) * itemsPerPage; j < currentPage * itemsPerPage; j++) { + data.push(this.createRandomItem(j)); + } + resolve(data); + // 在onpush模式下,异步场景需要手动触发变更,default模式下不用写该行代码 + this.ref.markForCheck(); + }, 1000); + }); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableServerPagiSearchSortComponent.ts b/src/table/demo/src/app/table/TableServerPagiSearchSortComponent.ts new file mode 100644 index 0000000..6941148 --- /dev/null +++ b/src/table/demo/src/app/table/TableServerPagiSearchSortComponent.ts @@ -0,0 +1,152 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-server-pagi-search-sort.html' +}) +export class TableServerPagiSearchSortComponent implements OnInit { + value: string = ''; + loading: boolean = true; + displayedData: Array = []; + searchWords: Array = [this.value]; + searchKeys: Array = ['firstName', 'lastName', 'email']; + currentPage: number = 1; + totalNumber: number = 0; + pageSize: { options: Array; size: number } = { + options: [10, 20, 50, 100], + size: 10 + }; + srcData: TiTableSrcData = { + data: [], + state: { + searched: true, // 源数据已进行过搜索处理 + sorted: true, // 源数据已进行过排序处理 + paginated: true // 源数据已进行过分页处理 + } + }; + columns: TiTableColumns = [ + { + title: 'Id' + }, + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age', + sortKey: 'age' + }, + { + title: 'Email Address' + } + ]; + private baseData: Array = []; + + ngOnInit(): void { + for (let j: number = 0; j < 226; j++) { + this.baseData.push(this.createRandomItem(j)); + } + + this.getCurrentPageData(this.currentPage, this.pageSize.size, this.searchWords, this.searchKeys, this.columns[2].sortKey, null).then( + (response: any) => { + this.loading = false; + this.srcData.data = response.data; + this.totalNumber = response.totalNumber; + } + ); + } + + // 点击搜索时,将搜索的值通过 tiTable 提供的 searchWords 接口传进表格, + // 然后表格的搜索数据状态就会改变,从而触发 stateUpdate的 事件的执行。 + onSearch(value: string): void { + this.searchWords[0] = value; + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + // 表格的数据状态(分页,排序,搜索)发生改变时,此方法会被触发 + onStateUpdate(tiTable: TiTableComponent): void { + const dataState: TiTableDataState = tiTable.getDataState(); + this.loading = true; + this.getCurrentPageData( + dataState.pagination.currentPage, + dataState.pagination.itemsPerPage, + dataState.search.searchWords, + dataState.search.searchKeys, + dataState.sort.sortKey, + dataState.sort.asc + ).then((response: any) => { + this.loading = false; + this.srcData.data = response.data; + this.totalNumber = response.totalNumber; + }); + } + + // 模拟异步远程请求返回的数据 + private getCurrentPageData( + currentPage: number, + itemsPerPage: number, + searchWords: Array, + searchKeys: Array, + sortKey: string, + asc: boolean + ): Promise { + let output: Array = [].concat(this.baseData); + + output = this.search(output, searchWords, searchKeys); + const totalNumber: number = output.length; + + if (sortKey && asc !== null) { + output.sort((a: any, b: any): number => { + return (a[sortKey] - b[sortKey]) * (asc ? 1 : -1); + }); + } + + const start: number = (currentPage - 1) * itemsPerPage; + output = output.slice(start, start + parseInt(itemsPerPage.toString(), 10)); + + return new Promise((resolve, reject) => { + setTimeout(() => { + resolve({ data: output, totalNumber }); + }, 1000); + }); + } + + private search(src: Array, searchWords: Array, searchKeys: Array): Array { + const output: Array = []; + src.forEach((item: TiTableRowData) => { + let isMatched: boolean = false; + for (let i: number = 0; i < searchKeys.length; i++) { + if (item[searchKeys[i]].toLowerCase().indexOf(searchWords[0].toLowerCase()) > -1) { + isMatched = true; + break; + } + } + if (isMatched) { + output.push(item); + } + }); + + return output; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + return { + firstName, + lastName, + age, + email, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableSmallComponent.ts b/src/table/demo/src/app/table/TableSmallComponent.ts new file mode 100644 index 0000000..ca93eb0 --- /dev/null +++ b/src/table/demo/src/app/table/TableSmallComponent.ts @@ -0,0 +1,52 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-small.html' +}) +export class TableSmallComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'First Name' + }, + { + title: 'Last Name' + }, + { + title: 'Age' + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableSoldoutComponent.ts b/src/table/demo/src/app/table/TableSoldoutComponent.ts new file mode 100644 index 0000000..766ed44 --- /dev/null +++ b/src/table/demo/src/app/table/TableSoldoutComponent.ts @@ -0,0 +1,91 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-soldout.html' +}) +export class TableSoldoutComponent implements OnInit { + checkedList: Array = []; // 默认选中项 + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 21; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + this.checkedList.push(this.data[2]); + this.checkedList.push(this.data[6]); // 初始选中项 + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const messageArr: Array = [ + { text: '', class: 'ti3-table-row-message-soldout', iconName: '' }, + { text: 'New', class: 'ti3-table-row-message-new', iconName: '' }, + { + text: '', + class: 'ti3-table-row-message-recommended', + iconName: 'discount-sup' + } + ]; + const message: any = messageArr[((id + 3) * 19) % 3]; + + return { + firstName, + lastName, + age, + email, + balance, + message, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableSortBasicComponent.ts b/src/table/demo/src/app/table/TableSortBasicComponent.ts new file mode 100644 index 0000000..61b75a2 --- /dev/null +++ b/src/table/demo/src/app/table/TableSortBasicComponent.ts @@ -0,0 +1,93 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-sort-basic.html' +}) +export class TableSortBasicComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + sortKey: 'firstName', // 设置排序时按照源数据中的哪一个属性进行排序, + width: '10%' + }, + { + title: 'last name', + sortKey: 'lastName', + width: '25%' + }, + { + title: 'birthday', + sortKey: 'birthday', + asc: false, // 默认排序,且为降序 + width: '20%' + }, + { + title: 'balance', + sortKey: 'balance', + width: '30%' + }, + { + title: 'email' + } + ]; + @ViewChild(TiTableComponent, { static: true }) tableCom: TiTableComponent; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了排序特性,且对源数据未进行排序处理,因此tiny会对数据进行排序处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 1) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const birthday: string = new Date(1600657756626 - age * 3600 * 24 * 365 * 1000).toISOString().substring(0, 10); + + return { + firstName, + lastName, + age, + birthday, + email, + balance, + id + }; + } + + clearSort(): void { + const dataState: TiTableDataState = this.tableCom.getDataState(); + dataState.sort.sortKey = undefined; + dataState.sort.asc = null; + } + + // 10.1.13 版本支持手动修改表格排序状态 + changeSort(): void { + const dataState: TiTableDataState = this.tableCom.getDataState(); + dataState.sort.sortKey = 'firstName'; + dataState.sort.asc = false; + } +} diff --git a/src/table/demo/src/app/table/TableSortComparefnComponent.ts b/src/table/demo/src/app/table/TableSortComparefnComponent.ts new file mode 100644 index 0000000..351f2ef --- /dev/null +++ b/src/table/demo/src/app/table/TableSortComparefnComponent.ts @@ -0,0 +1,85 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-sort-comparefn.html' +}) +export class TableSortComparefnComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + sortKey: 'firstName', // 设置排序时按照源数据中的哪一个属性进行排序, + compareFn: (a: object, b: object, predicate: string): number => { + console.log('firstName compareFn', this); + console.log('Params', a, b, predicate); + + return a[predicate].length - b[predicate].length; + } + }, + { + title: 'last name', + sortKey: 'lastName' + }, + { + title: 'birth date', + sortKey: 'age', + asc: true, // 默认排序,且为升序 + compareFn: (a: Object, b: Object, predicate: string): number => { + console.log('age compareFn', this); + console.log('Params', a, b, predicate); + + return a[predicate] - b[predicate]; + } + }, + { + title: 'balance', + sortKey: 'balance' + }, + { + title: 'email' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了排序特性,且对源数据未进行排序处理,因此tiny会对数据进行排序处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableSortComparefnLocaleComponent.ts b/src/table/demo/src/app/table/TableSortComparefnLocaleComponent.ts new file mode 100644 index 0000000..6f162a5 --- /dev/null +++ b/src/table/demo/src/app/table/TableSortComparefnLocaleComponent.ts @@ -0,0 +1,82 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-sort-comparefn-locale.html' +}) +export class TableSortComparefnLocaleComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + sortKey: 'firstName', // 设置排序时按照源数据中的哪一个属性进行排序, + asc: true // 默认排序,且为升序 + }, + { + title: 'last name', + sortKey: 'lastName' + }, + { + title: 'birth date', + sortKey: 'age' + }, + { + title: 'balance', + sortKey: 'balance' + }, + { + title: 'email' + } + ]; + + // 表格组件提供的排序中字符串比较是使用基于标准字典的 Unicode 值来进行比较的。如果开发者需要本地化的排序, + // 可使用tiHeadSort组件的compareFn接口来自定义所在列的本地化排序规则。本地化排序规则可利用 localeCompare 方法。 + compareFn = (a: TiTableRowData, b: TiTableRowData, sortKey: string): number => { + console.log('Params', a, b, sortKey); + const language: string = 'zh-CN'; // 根据实际情况设置当前语言种类 + + return a[sortKey].localeCompare(b[sortKey], language); // localeCompare方法还有更多配置,可在网上查阅。 + }; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了排序特性,且对源数据未进行排序处理,因此tiny会对数据进行排序处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['内存状态', '网口状态', '文件状态', '磁盘寿命', 'CPU状态', 'I/O状态']; + const familyName: Array = ['主机信息统计', '网络统计', '文件系统统计', 'CPU统计', 'I/O统计']; + const firstName: string = nameList[((id + 3) * 19) % 6]; + const lastName: string = familyName[((id + 3) * 29) % 5]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableSortComponent.ts b/src/table/demo/src/app/table/TableSortComponent.ts new file mode 100644 index 0000000..38e622a --- /dev/null +++ b/src/table/demo/src/app/table/TableSortComponent.ts @@ -0,0 +1,72 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-sort.html' +}) +export class TableSortComponent { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com', + poetry: '但愿人长久,千里共婵娟。' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com', + poetry: '天生我材必有用,千金散尽还复来。' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com', + poetry: '人生得意须尽欢,莫使金樽空对月。' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com', + poetry: '明月几时有,把酒问青天,不知天上宫阙,今夕是何年。' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'First Name', + sortKey: 'firstName', + asc: true + }, + { + title: 'Last Name', + sortKey: 'lastName', + compareFn: (a: TiTableRowData, b: TiTableRowData, sortKey: string): number => { + return a[sortKey].length - b[sortKey].length; + } + }, + { + title: 'Age', + sortKey: 'age' + }, + { + title: 'Favorite Chinese Poetry', + sortKey: 'poetry', + compareFn: (a: TiTableRowData, b: TiTableRowData, sortKey: string): number => { + const language: string = 'zh-CN'; + + return a[sortKey].localeCompare(b[sortKey], language); + } + }, + { + title: 'Email Address' + } + ]; +} diff --git a/src/table/demo/src/app/table/TableSortDetailsComponent.ts b/src/table/demo/src/app/table/TableSortDetailsComponent.ts new file mode 100644 index 0000000..6bd74bb --- /dev/null +++ b/src/table/demo/src/app/table/TableSortDetailsComponent.ts @@ -0,0 +1,89 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-sort-details.html' +}) +export class TableSortDetailsComponent implements OnInit { + elementId: string = 'table_test'; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + columns: TiTableColumns = [ + { + title: '' + }, + { + title: 'first name', + sortKey: 'firstName' // 设置排序时按照源数据中的哪一个属性进行排序, + }, + { + title: 'last name', + sortKey: 'lastName' + }, + { + title: 'birth date', + sortKey: 'age', + asc: true // 默认排序,且为升序 + }, + { + title: 'balance', + sortKey: 'balance' + }, + { + title: 'email' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } + + changeData(): void { + const temp: TiTableRowData = { + firstName: 'Rose', + lastName: 'Jack', + age: 23, + email: 'roseJack@example.com', + balance: (this.data.length * 761) % 10000, + id: this.data.length + }; + this.srcData['data'].push(temp); + } +} diff --git a/src/table/demo/src/app/table/TableSortResetComponent.ts b/src/table/demo/src/app/table/TableSortResetComponent.ts new file mode 100644 index 0000000..a39e48f --- /dev/null +++ b/src/table/demo/src/app/table/TableSortResetComponent.ts @@ -0,0 +1,71 @@ +import { Component, ViewChild } from '@angular/core'; +import { TiTableComponent, TiTableColumns, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-sort-reset.html' +}) +export class TableSortResetComponent { + @ViewChild(TiTableComponent, { static: true }) tableInstance: TiTableComponent; + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [ + { + firstName: 'Pierre', + lastName: 'Dupont', + age: 20, + email: 'Pierre@example.com' + }, + { + firstName: 'Jacques', + lastName: 'Germain', + age: 42, + email: 'Jacques@example.com' + }, + { + firstName: 'Robert', + lastName: 'Delcourt', + age: 15, + email: 'Robert@example.com' + }, + { + firstName: 'Elisa', + lastName: 'Menez', + age: 36, + email: 'Elisa@example.com' + } + ], + state: undefined + }; + columns: Array = [ + { + title: 'First Name', + sortKey: 'firstName' + }, + { + title: 'Last Name', + sortKey: 'lastName', + compareFn: (a: TiTableRowData, b: TiTableRowData, sortKey: string): number => { + return a[sortKey].length - b[sortKey].length; + } + }, + { + title: 'Age', + sortKey: 'age' + }, + { + title: 'Email Address' + } + ]; + + sortByAge(): void { + const dataState: TiTableDataState = this.tableInstance.getDataState(); + dataState.sort.sortKey = 'age'; + dataState.sort.asc = false; + } + + clearSorters(): void { + const dataState: TiTableDataState = this.tableInstance.getDataState(); + dataState.sort.sortKey = undefined; + dataState.sort.asc = null; + } +} diff --git a/src/table/demo/src/app/table/TableSortTestComponent.ts b/src/table/demo/src/app/table/TableSortTestComponent.ts new file mode 100644 index 0000000..f39d238 --- /dev/null +++ b/src/table/demo/src/app/table/TableSortTestComponent.ts @@ -0,0 +1,93 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-sort-test.html' +}) +export class TableSortTestComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + sortKey: 'firstName', // 设置排序时按照源数据中的哪一个属性进行排序, + width: '10%' + }, + { + title: 'last name', + sortKey: 'lastName', + width: '25%' + }, + { + title: 'birthday', + sortKey: 'birthday', + asc: false, // 默认排序,且为降序 + width: '20%' + }, + { + title: 'balance', + sortKey: 'balance', + width: '30%' + }, + { + title: 'email' + } + ]; + @ViewChild(TiTableComponent, { static: true }) tableCom: TiTableComponent; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者设置了排序特性,且对源数据未进行排序处理,因此tiny会对数据进行排序处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 1) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const birthday: string = new Date(1600657756626 - age * 3600 * 24 * 365 * 1000).toISOString().substring(0, 10); + + return { + firstName, + lastName, + age, + birthday, + email, + balance, + id + }; + } + + clearSort(): void { + const dataState: TiTableDataState = this.tableCom.getDataState(); + dataState.sort.sortKey = undefined; + dataState.sort.asc = null; + } + + // 10.1.13 版本支持手动修改表格排序状态 + changeSort(): void { + const dataState: TiTableDataState = this.tableCom.getDataState(); + dataState.sort.sortKey = 'firstName'; + dataState.sort.asc = false; + } +} diff --git a/src/table/demo/src/app/table/TableStorageComponent.ts b/src/table/demo/src/app/table/TableStorageComponent.ts new file mode 100644 index 0000000..afc10f2 --- /dev/null +++ b/src/table/demo/src/app/table/TableStorageComponent.ts @@ -0,0 +1,69 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-storage.html' +}) +export class TableStorageComponent implements OnInit { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [], + state: undefined + }; + currentPage: number = 1; + totalNumber: number = 126; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 40, 60], + size: 10 + }; + columns: Array = [ + { + title: 'Id', + width: '10%' + }, + { + title: 'First Name', + width: '25%' + }, + { + title: 'Last Name', + field: 'lastName', + sortKey: 'lastName', + width: '25%' + }, + { + title: 'Age', + sortKey: 'age', + width: '10%' + }, + { + title: 'email', + width: '30%' + } + ]; + + ngOnInit(): void { + const data: Array = []; + for (let j: number = 0; j < this.totalNumber; j++) { + data.push(this.createRandomItem(j)); + } + this.srcData.data = data; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 1) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + + return { + firstName, + lastName, + age, + email, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableStorageConfigComponent.ts b/src/table/demo/src/app/table/TableStorageConfigComponent.ts new file mode 100644 index 0000000..a3600d4 --- /dev/null +++ b/src/table/demo/src/app/table/TableStorageConfigComponent.ts @@ -0,0 +1,101 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData, TiTableStorageConfig } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-storage-config.html' +}) +export class TableStorageConfigComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + private data: Array = []; + columns: Array = [ + { + title: 'first name', + field: 'firstName', + width: '10%' + }, + { + title: 'last name', + field: 'lastName', + show: true, + width: '25%' + }, + { + title: 'birthday', + sortKey: 'birthday', + field: 'birthday', + show: true, + asc: false, + width: '20%' + }, + { + title: 'balance', + field: 'balance', + show: true, + sortKey: 'balance', + width: '30%' + }, + { + title: 'email', + field: 'email', + show: true + } + ]; + @ViewChild(TiTableComponent, { static: true }) tableCom: TiTableComponent; + + currentPage: number = 1; + totalNumber: number = 100; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 40, 60], + size: 5 + }; + + storageConfig: TiTableStorageConfig = { + sort: false, + // pagination设置为false,表示当前页和每页个数都不记忆 + // pagination设置为true,表示当前页和每页个数都要记忆 + // 10.1.2版本支持只记忆当前页或只记忆每页个数 + pagination: { + currentPage: false, + itemsPerPage: true + }, + colsWidth: false + }; + + ngOnInit(): void { + for (let j: number = 0; j < 100; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 1) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const birthday: string = new Date(1600657756626 - age * 3600 * 24 * 365 * 1000).toISOString().substring(0, 10); + + return { + firstName, + lastName, + age, + birthday, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableStorageFilterComponent.ts b/src/table/demo/src/app/table/TableStorageFilterComponent.ts new file mode 100644 index 0000000..8f4f41c --- /dev/null +++ b/src/table/demo/src/app/table/TableStorageFilterComponent.ts @@ -0,0 +1,123 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-storage-filter.html' +}) +export class TableStorageFilterComponent implements OnInit { + storageId: string = 'storage-filter'; + searchWords: Array = ['', '', '']; + searchKeys: Array = ['firstName', 'lastName', 'age']; // 设置过滤字段 + displayed: Array = []; + srcData: TiTableSrcData; + data: Array = []; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%', + key: 'lastName', + options: [ + { + label: 'all' + }, + { + label: 'Dupont' + }, + { + label: 'Germain' + }, + { + label: 'Delcourt' + }, + { + label: 'bjip' + }, + { + label: 'Menez' + }, + { + label: 'Henry' + }, + { + label: 'Jeff' + }, + { + label: 'John' + }, + { + label: 'Elizabeth' + } + ] + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < 10; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + data: this.data, + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + // 修复SSR报错:ERROR ReferenceError: window is not defined + if (typeof window === 'undefined') { + return; + } + // 检查缓存 + let storageSearchWords = window.sessionStorage.getItem(this.storageId); + if (storageSearchWords) { + let { searchWords } = JSON.parse(storageSearchWords); + this.searchWords = [...searchWords]; + // 表格过滤选中项 + this.columns[1].selected = this.columns[1].options.find((option) => { + return option.label === searchWords[1]; + }); + } + } + + onSelect(item: any, column: TiTableColumns): void { + const index: number = this.searchKeys.indexOf(column.key); + this.searchWords[index] = item['label'] === 'all' ? '' : item['label']; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableStorageServeComponent.ts b/src/table/demo/src/app/table/TableStorageServeComponent.ts new file mode 100644 index 0000000..6d2b125 --- /dev/null +++ b/src/table/demo/src/app/table/TableStorageServeComponent.ts @@ -0,0 +1,115 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableComponent, TiTableDataState, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-storage-serve.html' +}) +export class TableStorageServeComponent implements OnInit { + myLogs: Array = []; + storageId: string = 'table-storage-pagation-serve'; + storageConfig = { + sort: false, + pagination: true, + colsWidth: false + }; + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + currentPage: number = 1; + totalNumber: number = 222; + pageSize: { options: Array; size: number } = { + options: [5, 10, 20, 50, 100], + size: 5 + }; + constructor(private ref: ChangeDetectorRef) {} + ngOnInit(): void { + this.srcData = { + data: [], + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: true // 后台分页,源数据已进行了分页处理 + } + }; + // 修复SSR报错:ERROR ReferenceError: window is not defined + // 处理用户第一次访问页面,没有记忆状态,不会触发stateUpdate + let storageState = typeof window !== 'undefined' ? window.localStorage.getItem('this.storageId') : undefined; + if (!storageState) { + this.getCurrentPageData(this.currentPage, this.pageSize.size).then((data: Array) => { + this.myLogs = [...this.myLogs, '初始化化成功']; + this.srcData.data = data; + this.totalNumber = 256; + }); + } + } + + // 表格的数据状态(分页,排序,搜索)发生改变时,此方法会被触发; + // 多用于后台分页、排序和搜索 + stateUpdate(tiTable: TiTableComponent): void { + this.myLogs = [...this.myLogs, `dataState=> ${JSON.stringify(tiTable.getDataState())}`]; + this.myLogs = [...this.myLogs, `triggerEvent=> ${tiTable.getTriggerEvent()}`]; + const dataState: TiTableDataState = tiTable.getDataState(); + this.getCurrentPageData(dataState.pagination.currentPage, dataState.pagination.itemsPerPage).then((data: Array) => { + this.myLogs = [...this.myLogs, '获取数据成功!']; + this.srcData.data = data; + this.totalNumber = 256; + }); + } + + // 模拟后台异步请求返回的数据 + private getCurrentPageData(currentPage: number, itemsPerPage: number): Promise { + this.myLogs = [...this.myLogs, '模拟向后台发送请求']; + return new Promise((resolve, reject) => { + setTimeout(() => { + const data: Array = []; + for (let j: number = (currentPage - 1) * itemsPerPage; j < currentPage * itemsPerPage; j++) { + data.push(this.createRandomItem(j)); + } + resolve(data); + // 在onpush模式下,异步场景需要手动触发变更,default模式下不用写该行代码 + this.ref.markForCheck(); + }, 1000); + }); + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableTestModule.ts b/src/table/demo/src/app/table/TableTestModule.ts new file mode 100644 index 0000000..c3fa926 --- /dev/null +++ b/src/table/demo/src/app/table/TableTestModule.ts @@ -0,0 +1,642 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ScrollingModule } from '@angular/cdk/scrolling'; + +import { + TiAccordionModule, + TiActionmenuModule, + TiButtonModule, + TiCheckboxModule, + TiDateModule, + TiFormfieldModule, + TiIconModule, + TiLeftmenuModule, + TiLoadingModule, + TiMessageModule, + TiOverflowModule, + TiPaginationModule, + TiRadioModule, + TiSearchboxModule, + TiSelectModule, + TiTableModule, + TiTextModule, + TiTipModule, + TiValidationModule +} from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TableBasicComponent } from './TableBasicComponent'; +import { TableBasicTestComponent } from './TableBasicTestComponent'; +import { TableFilterStrictComponent } from './TableFilterStrictComponent'; +import { TableFilterComponent } from './TableFilterComponent'; +import { TablePaginationComponent } from './TablePaginationComponent'; +import { TableServerPagiComponent } from './TableServerPagiComponent'; +import { TablePagiWithFilterComponent } from './TablePagiWithFilterComponent'; +import { TableDetailsComponent } from './TableDetailsComponent'; +import { TableDetailsCloseotherdetailsComponent } from './TableDetailsCloseotherdetailsComponent'; +import { TableDynamicDetailsComponent } from './TableDynamicDetailsComponent'; +import { TableSortBasicComponent } from './TableSortBasicComponent'; +import { TableSortComparefnComponent } from './TableSortComparefnComponent'; +import { TableSortDetailsComponent } from './TableSortDetailsComponent'; +import { TableSortComparefnLocaleComponent } from './TableSortComparefnLocaleComponent'; +import { TableSortTestComponent } from './TableSortTestComponent'; +import { TableColsToggleComponent } from './TableColsToggleComponent'; +import { TableColsToggleDetailsComponent } from './TableColsToggleDetailsComponent'; +import { NestedTableComponent, TableDetailsNesttableComponent } from './TableDetailsNesttableComponent'; +import { TableDetailsPaginationComponent } from './TableDetailsPaginationComponent'; +import { TableColsResizableComponent } from './TableColsResizableComponent'; +import { TableColsresizableSortComponent } from './TableColsresizableSortComponent'; +import { TableColsresizableColstoggleComponent } from './TableColsresizableColstoggleComponent'; +import { TableColsresizableLoadfailComponent } from './TableColsresizableLoadfailComponent'; +import { TableCellTipComponent } from './TableCellTipComponent'; +import { TableCelliconsColsresizableComponent } from './TableCelliconsColsresizableComponent'; +import { TableNodataComponent } from './TableNodataComponent'; +import { TableNodataSimpleComponent } from './TableNodataSimpleComponent'; +import { TableLoadFailComponent } from './TableLoadFailComponent'; +import { TableGuideComponent } from './TableGuideComponent'; +import { TableColAlignComponent } from './TableColAlignComponent'; +import { TableColalignSortResizableTestComponent } from './TableColalignSortResizableTestComponent'; +import { TableGroupComponent } from './TableGroupComponent'; +import { TableCheckboxComponent } from './TableCheckboxComponent'; +import { TableRadioComponent } from './TableRadioComponent'; +import { TableRowspanComponent } from './TableRowspanComponent'; +import { TableSoldoutComponent } from './TableSoldoutComponent'; +import { TableTreeComponent } from './TableTreeComponent'; +import { TableTreeUnknowdeepthComponent } from './TableTreeUnknowdeepthComponent'; +import { TableTreeMulitiselectComponent } from './TableTreeMulitiselectComponent'; +import { TableSmallComponent } from './TableSmallComponent'; +import { TableFixedHeadComponent } from './TableFixedHeadComponent'; +import { TableFixedHeadPaginationDetailsComponent } from './TableFixedHeadPaginationDetailsComponent'; +import { TableFixedHeadInAccordionComponent } from './TableFixedHeadInAccordionComponent'; +import { TableFixedHeadNodataComponent } from './TableFixedHeadNodataComponent'; +import { TableFixedHeadColsResizableComponent } from './TableFixedHeadColsResizableComponent'; +import { TableFixedheadColsresizablePaginationDetailsComponent } from './TableFixedheadColsresizablePaginationDetailsComponent'; +import { TableActionmenuComponent } from './TableActionmenuComponent'; +import { TableHeadFilterTestComponent } from './TableHeadFilterTestComponent'; +import { TableHeadFilterValuekeyComponent } from './TableHeadFilterValuekeyComponent'; +import { TableHeadFilterMultiValuekeyComponent } from './TableHeadFilterMultiValuekeyComponent'; +import { TableHeadFilterMultiComponent } from './TableHeadFilterMultiComponent'; +import { TableHeadFilterDatetimeComponent } from './TableHeadFilterDatetimeComponent'; +import { TableHeadFilterVirtualscrollComponent } from './TableHeadFilterVirtualscrollComponent'; +import { TableColsresizableSortHeadfilterComponent } from './TableColsresizableSortHeadfilterComponent'; +import { TableServerPagiSearchSortComponent } from './TableServerPagiSearchSortComponent'; +import { TableColsresizableColstoggleFixedheadComponent } from './TableColsresizableColstoggleFixedheadComponent'; +import { TableColumnFixedComponent } from './TableColumnFixedComponent'; +import { TableColumnfixedResizableComponent } from './TableColumnfixedResizableComponent'; +import { TableColumnfixedHeadfixedComponent } from './TableColumnfixedHeadfixedComponent'; +import { TableColumnfixedEditrowComponent } from './TableColumnfixedEditrowComponent'; +import { TableColumnfixedNodataComponent } from './TableColumnfixedNodataComponent'; +import { TableColumnfixedCheckboxComponent } from './TableColumnfixedCheckboxComponent'; +import { TableColumnfixedColstoggleComponent } from './TableColumnfixedColstoggleComponent'; +import { TableColumnfixedPaginationComponent } from './TableColumnfixedPaginationComponent'; +import { TableColumnfixedFixedheadColsresizablePaginationComponent } from './TableColumnfixedFixedheadColsresizablePaginationComponent'; +import { TableColumnfixedLeftmenuComponent } from './TableColumnfixedLeftmenuComponent'; +import { TableEditrowComponent } from './TableEditrowComponent'; +import { TableEditallComponent } from './TableEditallComponent'; +import { TableOverflowLinkComponent } from './TableOverflowLinkComponent'; +import { TableFixheadScrollComponent } from './TableFixheadScrollComponent'; +import { TableRowDrag2Component } from './TableRowDrag2Component'; +import { TableStorageFilterComponent } from './TableStorageFilterComponent'; +import { TableStorageConfigComponent } from './TableStorageConfigComponent'; +import { TableStorageServeComponent } from './TableStorageServeComponent'; +import { TableStorageComponent } from './TableStorageComponent'; +import { TableCheckboxPaginationComponent } from './TableCheckboxPaginationComponent'; +import { TableComprehensiveComponent } from './TableComprehensiveComponent'; +import { TableVirtualscrollBasicComponent } from './TableVirtualscrollBasicComponent'; +import { TableVirtualscrollSizesComponent } from './TableVirtualscrollSizesComponent'; +import { TableVirtualscrollComprehensiveComponent } from './TableVirtualscrollComprehensiveComponent'; +import { TableVirtualscrollTreeComponent } from './TableVirtualscrollTreeComponent'; +import { TableCheckboxPaginationHeadmenuComponent } from './TableCheckboxPaginationHeadmenuComponent'; +import { TableNodataTestComponent } from './TableNodataTestComponent'; +import { TableSortComponent } from './TableSortComponent'; +import { TableSortResetComponent } from './TableSortResetComponent'; +import { TableSearchComponent } from './TableSearchComponent'; +import { TableHeadFilterComponent } from './TableHeadFilterComponent'; +import { TableHeadFilterDatetimeTestComponent } from './TableHeadFilterDatetimeTestComponent'; +import { TableRadioTestComponent } from './TableRadioTestComponent'; +import { TableColsToggleTestComponent } from './TableColsToggleTestComponent'; +import { TableColsresizableBasicComponent } from './TableColsresizableBasicComponent'; +import { TableEditrowTestComponent } from './TableEditrowTestComponent'; +import { TableEditallTestComponent } from './TableEditallTestComponent'; +import { TableVirtualscrollComponent } from './TableVirtualscrollComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiActionmenuModule, + TiPaginationModule, + TiTipModule, + TiOverflowModule, + TiCheckboxModule, + TiRadioModule, + TiAccordionModule, + TiTextModule, + TiSearchboxModule, + TiTableModule, + TiIconModule, + TiSelectModule, + TiDateModule, + TiValidationModule, + TiLeftmenuModule, + TiMessageModule, + ScrollingModule, + TiFormfieldModule, + TiButtonModule, + TiLoadingModule, + DemoLogModule, + RouterModule.forChild(TableTestModule.ROUTES) + ], + declarations: [ + TableBasicComponent, + TableFilterComponent, + TableFilterStrictComponent, + TablePaginationComponent, + TableServerPagiComponent, + TablePagiWithFilterComponent, + TableSortBasicComponent, + TableDetailsComponent, + TableDetailsCloseotherdetailsComponent, + TableDynamicDetailsComponent, + TableSortComparefnComponent, + TableSortComparefnLocaleComponent, + TableSortTestComponent, + TableSortDetailsComponent, + TableColsToggleComponent, + TableColsToggleDetailsComponent, + TableDetailsNesttableComponent, + TableDetailsPaginationComponent, + NestedTableComponent, + TableColsResizableComponent, + TableColsresizableSortComponent, + TableColsresizableColstoggleComponent, + TableColsresizableLoadfailComponent, + TableCellTipComponent, + TableCelliconsColsresizableComponent, + TableNodataComponent, + TableNodataSimpleComponent, + TableLoadFailComponent, + TableGuideComponent, + TableColAlignComponent, + TableColalignSortResizableTestComponent, + TableGroupComponent, + TableCheckboxComponent, + TableRadioComponent, + TableRowspanComponent, + TableSoldoutComponent, + TableTreeComponent, + TableTreeUnknowdeepthComponent, + TableTreeMulitiselectComponent, + TableSmallComponent, + TableFixedHeadComponent, + TableFixedHeadPaginationDetailsComponent, + TableFixedHeadInAccordionComponent, + TableFixedHeadNodataComponent, + TableFixedHeadColsResizableComponent, + TableFixedheadColsresizablePaginationDetailsComponent, + TableActionmenuComponent, + TableHeadFilterTestComponent, + TableHeadFilterValuekeyComponent, + TableHeadFilterMultiValuekeyComponent, + TableHeadFilterMultiComponent, + TableHeadFilterDatetimeComponent, + TableHeadFilterVirtualscrollComponent, + TableColsresizableSortHeadfilterComponent, + TableServerPagiSearchSortComponent, + TableColsresizableColstoggleFixedheadComponent, + TableColumnFixedComponent, + TableColumnfixedResizableComponent, + TableColumnfixedHeadfixedComponent, + TableColumnfixedNodataComponent, + TableColumnfixedCheckboxComponent, + TableColumnfixedColstoggleComponent, + TableColumnfixedPaginationComponent, + TableColumnfixedFixedheadColsresizablePaginationComponent, + TableEditrowComponent, + TableEditallComponent, + TableFixheadScrollComponent, + TableColumnfixedLeftmenuComponent, + TableColumnfixedEditrowComponent, + TableStorageComponent, + TableStorageFilterComponent, + TableStorageConfigComponent, + TableStorageServeComponent, + TableCheckboxPaginationComponent, + TableCheckboxPaginationHeadmenuComponent, + TableRowDrag2Component, + TableComprehensiveComponent, + TableVirtualscrollBasicComponent, + TableVirtualscrollSizesComponent, + TableVirtualscrollComprehensiveComponent, + TableVirtualscrollTreeComponent, + TableOverflowLinkComponent, + TableBasicTestComponent, + TableNodataTestComponent, + TableSortComponent, + TableSortResetComponent, + TableSearchComponent, + TableHeadFilterComponent, + TableHeadFilterDatetimeTestComponent, + TableRadioTestComponent, + TableColsToggleTestComponent, + TableColsresizableBasicComponent, + TableEditrowTestComponent, + TableEditallTestComponent, + TableVirtualscrollComponent + ] +}) +export class TableTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiTableComponent.html', label: 'TiTable' }, + { href: 'components/TiColsToggleComponent.html', label: 'ColsToggle' }, + { href: 'components/TiCellTextComponent.html', label: 'CellText' }, + { href: 'components/TiCellIconsComponent.html', label: 'CellIcons' }, + { href: 'components/TiHeadSortComponent.html', label: 'HeadSort' }, + { + href: 'directives/TiColsResizableDirective.html', + label: 'ColsResizable' + }, + { href: 'components/TiDetailsIconComponent.html', label: 'DetailsIcon' }, + { href: 'directives/TiDetailsTrDirective.html', label: 'DetailsTr' }, + { href: 'directives/TiColspanDirective.html', label: 'Colspan' } + ]; + static readonly ROUTES: Routes = [ + { + path: 'table/table-basic', + component: TableBasicComponent + }, + { + path: 'table/table-small', + component: TableSmallComponent + }, + { + path: 'table/table-pagination', + component: TablePaginationComponent + }, + { + path: 'table/table-server-pagi', + component: TableServerPagiComponent + }, + { + path: 'table/table-checkbox', + component: TableCheckboxComponent + }, + { + path: 'table/table-checkbox-pagination', + component: TableCheckboxPaginationComponent + }, + { + path: 'table/table-checkbox-pagination-headmenu', + component: TableCheckboxPaginationHeadmenuComponent + }, + { + path: 'table/table-radio-test', + component: TableRadioTestComponent + }, + { + path: 'table/table-filter', + component: TableFilterComponent + }, + { + path: 'table/table-filter-strict', + component: TableFilterStrictComponent + }, + { + path: 'table/table-sort-basic', + component: TableSortBasicComponent + }, + { + path: 'table/table-sort-comparefn', + component: TableSortComparefnComponent + }, + { + path: 'table/table-sort-comparefn-locale', + component: TableSortComparefnLocaleComponent + }, + { + path: 'table/table-details', + component: TableDetailsComponent + }, + { + path: 'table/table-details-closeotherdetails', + component: TableDetailsCloseotherdetailsComponent + }, + { + path: 'table/table-details-nesttable', + component: TableDetailsNesttableComponent + }, + { + path: 'table/table-dynamic-details', + component: TableDynamicDetailsComponent + }, + { + path: 'table/table-cols-toggle-test', + component: TableColsToggleTestComponent + }, + { + path: 'table/table-colsresizable-basic', + component: TableColsresizableBasicComponent + }, + { + path: 'table/table-colsresizable-sort', + component: TableColsresizableSortComponent + }, + { + path: 'table/table-cellicons-colsresizable', + component: TableCelliconsColsresizableComponent + }, + { + path: 'table/table-nodata', + component: TableNodataComponent + }, + { + path: 'table/table-nodata-simple', + component: TableNodataSimpleComponent + }, + { + path: 'table/table-load-fail', + component: TableLoadFailComponent + }, + { + path: 'table/table-guide', + component: TableGuideComponent + }, + { + path: 'table/table-soldout', + component: TableSoldoutComponent + }, + { + path: 'table/table-cell-tip', + component: TableCellTipComponent + }, + { + path: 'table/table-col-align', + component: TableColAlignComponent + }, + { + path: 'table/table-group', + component: TableGroupComponent + }, + { + path: 'table/table-rowspan', + component: TableRowspanComponent + }, + { + path: 'table/table-tree', + component: TableTreeComponent + }, + { + path: 'table/table-tree-unknowdeepth', + component: TableTreeUnknowdeepthComponent + }, + { + path: 'table/table-tree-mulitiselect', + component: TableTreeMulitiselectComponent + }, + { + path: 'table/table-fixed-head', + component: TableFixedHeadComponent + }, + { + path: 'table/table-actionmenu', + component: TableActionmenuComponent + }, + { + path: 'table/table-head-filter-test', + component: TableHeadFilterTestComponent + }, + { + path: 'table/table-head-filter-valuekey', + component: TableHeadFilterValuekeyComponent + }, + { + path: 'table/table-head-filter-multi-valuekey', + component: TableHeadFilterMultiValuekeyComponent + }, + { + path: 'table/table-head-filter-multi', + component: TableHeadFilterMultiComponent + }, + { + path: 'table/table-head-filter-datetime-test', + component: TableHeadFilterDatetimeTestComponent + }, + { + path: 'table/table-head-filter-virtualscroll', + component: TableHeadFilterVirtualscrollComponent + }, + { + path: 'table/table-server-pagi-search-sort', + component: TableServerPagiSearchSortComponent + }, + { + path: 'table/table-colsresizable-colstoggle-fixedhead', + component: TableColsresizableColstoggleFixedheadComponent + }, + { + path: 'table/table-column-fixed', + component: TableColumnFixedComponent + }, + { + path: 'table/table-columnfixed-resizable', + component: TableColumnfixedResizableComponent + }, + { + path: 'table/table-columnfixed-headfixed', + component: TableColumnfixedHeadfixedComponent + }, + { + path: 'table/table-columnfixed-checkbox', + component: TableColumnfixedCheckboxComponent + }, + { + path: 'table/table-columnfixed-colstoggle', + component: TableColumnfixedColstoggleComponent + }, + { + path: 'table/table-editrow-test', + component: TableEditrowTestComponent + }, + { + path: 'table/table-editall-test', + component: TableEditallTestComponent + }, + { + path: 'table/table-row-drag2', + component: TableRowDrag2Component + }, + { + path: 'table/table-storage', + component: TableStorageComponent + }, + { + path: 'table/table-storage-filter', + component: TableStorageFilterComponent + }, + { + path: 'table/table-storage-config', + component: TableStorageConfigComponent + }, + { + path: 'table/table-storage-serve', + component: TableStorageServeComponent + }, + { + path: 'table/table-virtualscroll-basic', + component: TableVirtualscrollBasicComponent + }, + { + path: 'table/table-virtualscroll-comprehensive', + component: TableVirtualscrollComprehensiveComponent + }, + { + path: 'table/table-virtualscroll-tree', + component: TableVirtualscrollTreeComponent + }, + // 用作内部测试用例 + { + path: 'table/table-pagi-with-filter', + component: TablePagiWithFilterComponent + }, + { + path: 'table/table-details-pagination', + component: TableDetailsPaginationComponent + }, + { path: 'table/table-sort-details', component: TableSortDetailsComponent }, + { + path: 'table/table-cols-toggle-details', + component: TableColsToggleDetailsComponent + }, + { + path: 'table/table-colsresizable-toggle', + component: TableColsresizableColstoggleComponent + }, + { + path: 'table/table-colsresizable-loadfail', + component: TableColsresizableLoadfailComponent + }, + { + path: 'table/table-colsresizable-sort-headfilter', + component: TableColsresizableSortHeadfilterComponent + }, + { + path: 'table/table-col-align-sort-resizable', + component: TableColalignSortResizableTestComponent + }, + { + path: 'table/table-fixed-head-pagination-details', + component: TableFixedHeadPaginationDetailsComponent + }, + { + path: 'table/table-fixed-head-accordion', + component: TableFixedHeadInAccordionComponent + }, + { + path: 'table/table-fixed-head-nodata', + component: TableFixedHeadNodataComponent + }, + { + path: 'table/table-fixed-head-cols-resizable', + component: TableFixedHeadColsResizableComponent + }, + { + path: 'table/table-fixed-head/cols-resizable-pagination-details', + component: TableFixedheadColsresizablePaginationDetailsComponent + }, + { + path: 'table/table-columnfixed-nodata', + component: TableColumnfixedNodataComponent + }, + { + path: 'table/table-columnfixed-pagination', + component: TableColumnfixedPaginationComponent + }, + { + path: 'table/table-columnfixed-fixedhead-colsresizable-pagination', + component: TableColumnfixedFixedheadColsresizablePaginationComponent + }, + { + path: 'table/table-columnfixed-leftmenu', + component: TableColumnfixedLeftmenuComponent + }, + { + path: 'table/table-columnfixed-editrow', + component: TableColumnfixedEditrowComponent + }, + { + path: 'table/table-fixhead-scroll', + component: TableFixheadScrollComponent + }, + { + path: 'table/table-comprehensive', + component: TableComprehensiveComponent + }, + { + path: 'table/table-virtualscroll-sizes', + component: TableVirtualscrollSizesComponent + }, + { + path: 'table/table-overflow-link', + component: TableOverflowLinkComponent + }, + { + path: 'table/table-sort-test', + component: TableSortTestComponent + }, + { + path: 'table/table-basic-test', + component: TableBasicTestComponent + }, + { + path: 'table/table-nodata-test', + component: TableNodataTestComponent + }, + { + path: 'table/table-sort', + component: TableSortComponent + }, + { + path: 'table/table-sort-reset', + component: TableSortResetComponent + }, + { + path: 'table/table-search', + component: TableSearchComponent + }, + { + path: 'table/table-head-filter', + component: TableHeadFilterComponent + }, + { + path: 'table/table-head-filter-datetime', + component: TableHeadFilterDatetimeComponent + }, + { + path: 'table/table-radio', + component: TableRadioComponent + }, + { + path: 'table/table-cols-toggle', + component: TableColsToggleComponent + }, + { + path: 'table/table-cols-resizable', + component: TableColsResizableComponent + }, + { + path: 'table/table-editrow', + component: TableEditrowComponent + }, + { + path: 'table/table-editall', + component: TableEditallComponent + }, + { + path: 'table/table-virtualscroll', + component: TableVirtualscrollComponent + } + ]; +} diff --git a/src/table/demo/src/app/table/TableTreeComponent.ts b/src/table/demo/src/app/table/TableTreeComponent.ts new file mode 100644 index 0000000..8452b6b --- /dev/null +++ b/src/table/demo/src/app/table/TableTreeComponent.ts @@ -0,0 +1,310 @@ +import { Component } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-tree.html' +}) +export class TableTreeComponent { + displayedData: Array = []; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'Size' + }, + { + title: 'Type' + } + ]; + srcData: TiTableSrcData = { + data: [ + { + name: 'Documents', + type: 'Folder', + size: '-', + showSub: true, + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + showSub: true, + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB', + show: false + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB', + show: false + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB', + show: false + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-' + } + ] + }, + { + name: 'Documents', + type: 'Folder', + size: '-' + }, + { + name: 'Documents', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + } + ] + }, + { + name: 'Documents', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + } + ] + }, + { + name: 'Documents', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + } + ] + } + ], + state: undefined + }; +} diff --git a/src/table/demo/src/app/table/TableTreeMulitiselectComponent.ts b/src/table/demo/src/app/table/TableTreeMulitiselectComponent.ts new file mode 100644 index 0000000..e68c8bc --- /dev/null +++ b/src/table/demo/src/app/table/TableTreeMulitiselectComponent.ts @@ -0,0 +1,356 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData, Util } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-tree-mulitiselect.html' +}) +export class TableTreeMulitiselectComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + checkedList: Array = []; // 默认选中项 + youCheckedList: Array = []; // 默认选中项 + sonCheckedList: Array = []; // 默认选中项 + + data: Array = [ + { + name: 'Documents', + type: 'Folder', + size: '-', + showSub: true, + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + showSub: true, + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB', + show: false + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB', + show: false + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB', + show: false + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-' + } + ] + }, + { + name: 'Documents', + type: 'Folder', + size: '-' + }, + { + name: 'Documents', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + } + ] + }, + { + name: 'Documents', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + } + ] + }, + { + name: 'Documents', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + }, + { + name: 'Work', + type: 'Folder', + size: '-', + subData: [ + { + name: 'Expenses.doc', + type: 'Word Document', + size: '30 KB' + }, + { + name: 'Resume.doc', + type: 'Word Document', + size: '10 KB' + }, + { + name: 'weike.doc', + type: 'Word Document', + size: '3 KB' + } + ] + } + ] + } + ]; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'Size' + }, + { + title: 'Type' + } + ]; + + // 设置各层级复选组绑定items数据 + private static getCheckgroupItems(treeData: Array, items: Array, parentData?: Array): any { + if (treeData) { + for (const data of treeData) { + if (data.subData) { + this.getCheckgroupItems(data.subData, items); + } else { + items.push(data); + } + } + } else { + items.push(parentData); + } + + return items; + } + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + onMyChange(checkedList: Array): void { + console.log(checkedList); + } + + // 获取数据 + getItems(data: Array, parentData?: Array): Array { + const items: Array = []; + + return TableTreeMulitiselectComponent.getCheckgroupItems(data, items, parentData); + } +} diff --git a/src/table/demo/src/app/table/TableTreeUnknowdeepthComponent.ts b/src/table/demo/src/app/table/TableTreeUnknowdeepthComponent.ts new file mode 100644 index 0000000..4259f94 --- /dev/null +++ b/src/table/demo/src/app/table/TableTreeUnknowdeepthComponent.ts @@ -0,0 +1,151 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-tree-unknowdeepth.html' +}) +export class TableTreeUnknowdeepthComponent implements OnInit { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [], + state: undefined + }; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'Size' + }, + { + title: 'Type' + } + ]; + treeData: Array = [ + { + name: 'tree_0', + size: 'tree_0_size', + type: 'tree_0_type', + expand: true, + children: [ + { + name: 'tree_0_0', + size: 'tree_0_0_size', + type: 'tree_0_0_type', + expand: true, + children: [ + { + name: 'tree_0_0_0', + size: 'tree_0_0_0_size', + type: 'tree_0_0_0_type' + }, + { + name: 'tree_0_0_1', + size: 'tree_0_0_1_size', + type: 'tree_0_0_1_type', + children: [ + { + name: 'tree_0_0_1_0', + size: 'tree_0_0_1_0_size', + type: 'tree_0_0_1_0_type' + }, + { + name: 'tree_0_0_1_1', + size: 'tree_0_0_1_1_size', + type: 'tree_0_0_1_1_type' + } + ] + } + ] + }, + { + name: 'tree_0_1', + size: 'tree_0_1_size', + type: 'tree_0_1_type', + expand: true, + children: [ + { + name: 'tree_0_1_0', + size: 'tree_0_1_0_size', + type: 'tree_0_1_0_type' + } + ] + }, + { + name: 'tree_0_2', + size: 'tree_0_2_size', + type: 'tree_0_2_type' + } + ] + }, + { + name: 'tree_1', + size: 'tree_1_size', + type: 'tree_1_type' + }, + { + name: 'tree_2', + size: 'tree_2_size', + type: 'tree_2_type' + }, + { + name: 'tree_3', + size: 'tree_3_size', + type: 'tree_3_type', + children: [ + { + name: 'tree_3_0', + size: 'tree_3_0_size', + type: 'tree_3_0_type' + }, + { + name: 'tree_3_1', + size: 'tree_3_1_size', + type: 'tree_3_1_type' + } + ] + }, + { + name: 'tree_4', + size: 'tree_4_size', + type: 'tree_4_type' + } + ]; + + ngOnInit(): void { + this.srcData.data = this.getFlatData(this.treeData); + } + + toggle(node: TiTableRowData): void { + node.expand = !node.expand; + this.srcData.data = this.getFlatData(this.treeData); + } + + getLevelStyle(node: TiTableRowData): { 'padding-left': string } { + return { + 'padding-left': `${node.level * 24 + 10}px` // 图标16px + 间距8px = 24px + }; + } + + /** + * 对树形结构数据做扁平化处理 + * @param nodes 同层级且隶属同一父节点的节点集合 + * @param level 节点层级,根节点层级为0,往下依次类推 + */ + private getFlatData(nodes: Array, level?: number): Array { + let result: Array = []; + if (!nodes) { + return result; + } + nodes.forEach((item: TiTableRowData, index: number): void => { + item.level = level ? level : 0; + item.hasChildren = item.children && item.children.length > 0; + result.push(item); + if (item.expand && item.hasChildren) { + result = result.concat(this.getFlatData(item.children, item.level + 1)); + } + }); + + return result; + } +} diff --git a/src/table/demo/src/app/table/TableVirtualscrollBasicComponent.ts b/src/table/demo/src/app/table/TableVirtualscrollBasicComponent.ts new file mode 100644 index 0000000..2816b74 --- /dev/null +++ b/src/table/demo/src/app/table/TableVirtualscrollBasicComponent.ts @@ -0,0 +1,88 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-virtualscroll-basic.html' +}) +export class TableVirtualscrollBasicComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + noDadaInfo: string = '暂无表格数据'; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.getData(20000), // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + changeEmpty(): void { + this.srcData.data = []; + } + changeMany(): void { + this.srcData.data = this.getData(20000); + } + changeFew(): void { + this.srcData.data = this.getData(5); + } + + private getData(total: number): Array { + const data: Array = []; + for (let j: number = 0; j < total; j++) { + data.push(this.createRandomItem(j)); + } + + return data; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 5]; + const lastName: string = familyName[((id + 3) * 29) % 5]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableVirtualscrollComponent.ts b/src/table/demo/src/app/table/TableVirtualscrollComponent.ts new file mode 100644 index 0000000..6891d06 --- /dev/null +++ b/src/table/demo/src/app/table/TableVirtualscrollComponent.ts @@ -0,0 +1,64 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + templateUrl: './table-virtualscroll.html' +}) +export class TableVirtualscrollComponent implements OnInit { + displayedData: Array = []; + srcData: TiTableSrcData = { + data: [], + state: undefined + }; + columns: Array = [ + { + title: 'Id', + width: '10%' + }, + { + title: 'First Name', + width: '20%' + }, + { + title: 'Last Name', + width: '20%' + }, + { + title: 'Age', + width: '10%' + }, + { + title: 'Email Address', + width: '40%' + } + ]; + + ngOnInit(): void { + const data: Array = []; + for (let i: number = 0; i < 1000; i++) { + data.push(this.createRandomItem(i)); + } + this.srcData.data = data; + } + + trackByFn(index: number, item: any): number { + return item.id; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'Bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + + return { + firstName, + lastName, + age, + email, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableVirtualscrollComprehensiveComponent.ts b/src/table/demo/src/app/table/TableVirtualscrollComprehensiveComponent.ts new file mode 100644 index 0000000..6682d0d --- /dev/null +++ b/src/table/demo/src/app/table/TableVirtualscrollComprehensiveComponent.ts @@ -0,0 +1,139 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-virtualscroll-comprehensive.html' +}) +export class TableVirtualscrollComprehensiveComponent implements OnInit { + selectAll: boolean = true; + panelWidth: string = '250px'; + searchable: boolean = true; // 可切换测试 + noDataText: string = '无数据'; + checkedList: Array = []; // 默认选中项 + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + data: Array = []; + currentPage: number = 1; + totalNumber: number = 16112; + pageSize: { options: Array; size: number } = { + options: [50, 120, 260, 500, 1000], + size: 120 + }; + searchWords: Array = ['']; + searchKeys: Array = ['lastName']; // 设置过滤字段 + columns: Array = [ + { + title: 'first name', + fixed: 'left', + sortKey: 'firstName' + }, + { + title: 'last name', + fixed: 'left', + key: 'lastName', + selected: null, + options: [ + { + label: 'all' + }, + { + label: 'Dupont' + }, + { + label: 'Germain' + }, + { + label: 'Delcourt' + }, + { + label: 'bjip' + }, + { + label: 'Menez' + } + ] + }, + { + title: 'birth date', + show: true + }, + { + title: 'balance', + show: true + }, + { + title: 'email', + show: false + }, + { + title: 'address', + show: true + }, + { + title: 'phone number', + show: true + }, + { + title: 'parents', + show: true + }, + { + title: 'school', + fixed: 'right' + } + ]; + + ngOnInit(): void { + for (let j: number = 0; j < this.totalNumber; j++) { + // 生成10条数据 + this.data.push(this.createRandomItem(j)); + } + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: this.data, // 源数据 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + onSelect(item: any, column: TiTableColumns): void { + const index: number = this.searchKeys.indexOf(column.key); + const labelKey: string = column.labelKey || 'label'; + this.searchWords[index] = item[labelKey] === 'all' ? '' : item[labelKey]; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 5]; + const lastName: string = familyName[((id + 3) * 29) % 5]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + const address: string = "Xi'an"; + const phone: string = '123 4567 8900'; + const parents: string = 'father and mother'; + const school: string = 'high school'; + + return { + firstName, + lastName, + age, + email, + balance, + address, + phone, + parents, + school, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableVirtualscrollSizesComponent.ts b/src/table/demo/src/app/table/TableVirtualscrollSizesComponent.ts new file mode 100644 index 0000000..2cad7c3 --- /dev/null +++ b/src/table/demo/src/app/table/TableVirtualscrollSizesComponent.ts @@ -0,0 +1,88 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-virtualscroll-sizes.html' +}) +export class TableVirtualscrollSizesComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + noDadaInfo: string = '暂无表格数据'; + columns: Array = [ + { + title: 'first name', + width: '20%' + }, + { + title: 'last name', + width: '20%' + }, + { + title: 'birth date', + width: '10%' + }, + { + title: 'balance', + width: '30%' + }, + { + title: 'email', + width: '20%' + } + ]; + + ngOnInit(): void { + this.srcData = { + // 表格源数据,开发者对表格的数据设置请在这里进行 + data: [], // 源数据,此处测试初始化无数据+虚拟滚动 + // 用来标识传进来的源数据是否已经进行过排序、搜索、分页操作, + // 已经做过的,tiny就不再做了 + // 如果没做,tiny会对传入的源数据做进一步处理(前提是开发者设置了相关特性,比如分页),然后作为displayedData显示出来 + // 本示例中,开发者没有设置分页、搜索和排序这些特性,因此tiny不会对数据进行进一步的处理 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + changeEmpty(): void { + this.srcData.data = []; + } + changeMany(): void { + this.srcData.data = this.getData(20000); + } + changeFew(): void { + this.srcData.data = this.getData(3); + } + + private getData(total: number): Array { + const data: Array = []; + for (let j: number = 0; j < total; j++) { + data.push(this.createRandomItem(j)); + } + + return data; + } + + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[((id + 3) * 19) % 4]; + const lastName: string = familyName[((id + 3) * 29) % 4]; + const age: number = ((id + 3) * 13) % 100; + const email: string = `${firstName}${lastName}@whatever.com`; + const balance: number = ((id + 3) * 761) % 10000; + + return { + firstName, + lastName, + age, + email, + balance, + id + }; + } +} diff --git a/src/table/demo/src/app/table/TableVirtualscrollTreeComponent.ts b/src/table/demo/src/app/table/TableVirtualscrollTreeComponent.ts new file mode 100644 index 0000000..70dcffe --- /dev/null +++ b/src/table/demo/src/app/table/TableVirtualscrollTreeComponent.ts @@ -0,0 +1,308 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTableColumns, TiTableRowData, TiTableSrcData } from '@opentiny/ng'; + +@Component({ + styleUrls: ['./tableTest.less'], + templateUrl: './table-virtualscroll-tree.html' +}) +export class TableVirtualscrollTreeComponent implements OnInit { + displayed: Array = []; // 表示表格实际呈现的数据(开发者默认设置为[]即可) + srcData: TiTableSrcData; + noDadaInfo: string = '暂无表格数据'; + columns: Array = [ + { + title: 'Name' + }, + { + title: 'Size' + }, + { + title: 'Type' + } + ]; + treeData: Array = [ + { + name: 'tree_0', + size: 'tree_0_size', + type: 'tree_0_type', + expand: true, + children: [ + { + name: 'tree_0_0', + size: 'tree_0_0_size', + type: 'tree_0_0_type', + expand: true, + children: [ + { + name: 'tree_0_0_0', + size: 'tree_0_0_0_size', + type: 'tree_0_0_0_type' + }, + { + name: 'tree_0_0_1', + size: 'tree_0_0_1_size', + type: 'tree_0_0_1_type' + } + ] + }, + { + name: 'tree_0_1', + size: 'tree_0_1_size', + type: 'tree_0_1_type', + expand: true, + children: [ + { + name: 'tree_0_1_0', + size: 'tree_0_1_0_size', + type: 'tree_0_1_0_type' + } + ] + }, + { + name: 'tree_0_2', + size: 'tree_0_2_size', + type: 'tree_0_2_type' + } + ] + }, + { + name: 'tree_1', + size: 'tree_1_size', + type: 'tree_1_type' + }, + { + name: 'tree_2', + size: 'tree_2_size', + type: 'tree_2_type' + }, + { + name: 'tree_3', + size: 'tree_3_size', + type: 'tree_3_type', + expand: true, + children: [ + { + name: 'tree_3_0', + size: 'tree_3_0_size', + type: 'tree_3_0_type' + }, + { + name: 'tree_3_1', + size: 'tree_3_1_size', + type: 'tree_3_1_type' + } + ] + }, + { + name: 'tree_4', + size: 'tree_4_size', + type: 'tree_4_type' + }, + { + name: 'tree_5', + size: 'tree_5_size', + type: 'tree_5_type', + expand: true, + children: [ + { + name: 'tree_5_0', + size: 'tree_5_0_size', + type: 'tree_5_0_type', + expand: true, + children: [ + { + name: 'tree_5_0_0', + size: 'tree_5_0_0_size', + type: 'tree_5_0_0_type' + }, + { + name: 'tree_5_0_1', + size: 'tree_5_0_1_size', + type: 'tree_5_0_1_type' + } + ] + }, + { + name: 'tree_5_1', + size: 'tree_5_1_size', + type: 'tree_5_1_type', + expand: true, + children: [ + { + name: 'tree_5_1_0', + size: 'tree_5_1_0_size', + type: 'tree_5_1_0_type' + } + ] + }, + { + name: 'tree_5_2', + size: 'tree_5_2_size', + type: 'tree_5_2_type' + } + ] + }, + { + name: 'tree_6', + size: 'tree_6_size', + type: 'tree_6_type' + }, + { + name: 'tree_7', + size: 'tree_7_size', + type: 'tree_7_type' + }, + { + name: 'tree_8', + size: 'tree_8_size', + type: 'tree_8_type', + expand: true, + children: [ + { + name: 'tree_8_0', + size: 'tree_8_0_size', + type: 'tree_8_0_type' + }, + { + name: 'tree_8_1', + size: 'tree_8_1_size', + type: 'tree_8_1_type' + } + ] + }, + { + name: 'tree_9', + size: 'tree_9_size', + type: 'tree_9_type' + }, + { + name: 'tree_10', + size: 'tree_10_size', + type: 'tree_10_type', + expand: true, + children: [ + { + name: 'tree_10_0', + size: 'tree_10_0_size', + type: 'tree_10_0_type', + expand: true, + children: [ + { + name: 'tree_10_0_0', + size: 'tree_10_0_0_size', + type: 'tree_10_0_0_type' + }, + { + name: 'tree_10_0_1', + size: 'tree_10_0_1_size', + type: 'tree_10_0_1_type' + } + ] + }, + { + name: 'tree_10_1', + size: 'tree_10_1_size', + type: 'tree_10_1_type', + expand: true, + children: [ + { + name: 'tree_10_1_0', + size: 'tree_10_1_0_size', + type: 'tree_10_1_0_type' + } + ] + }, + { + name: 'tree_10_2', + size: 'tree_10_2_size', + type: 'tree_10_2_type' + } + ] + }, + { + name: 'tree_11', + size: 'tree_11_size', + type: 'tree_11_type' + }, + { + name: 'tree_12', + size: 'tree_12_size', + type: 'tree_12_type' + }, + { + name: 'tree_13', + size: 'tree_13_size', + type: 'tree_13_type', + expand: true, + children: [ + { + name: 'tree_13_0', + size: 'tree_13_0_size', + type: 'tree_13_0_type' + }, + { + name: 'tree_13_1', + size: 'tree_13_1_size', + type: 'tree_13_1_type' + } + ] + }, + { + name: 'tree_14', + size: 'tree_14_size', + type: 'tree_14_type' + } + ]; + + ngOnInit(): void { + this.srcData = { + data: this.getFlatData(this.treeData), // 源数据 + state: { + searched: false, // 源数据未进行搜索处理 + sorted: false, // 源数据未进行排序处理 + paginated: false // 源数据未进行分页处理 + } + }; + } + + toggle(node: TiTableRowData): void { + node.expand = !node.expand; + this.srcData.data = this.getFlatData(this.treeData); + } + + getLevelStyle(node: TiTableRowData): { 'padding-left': string } { + return { + 'padding-left': `${node.level * 24 + 10}px` // 图标16px + 间距8px = 24px + }; + } + + changeEmpty(): void { + this.srcData.data = []; + } + + changeMany(): void { + this.srcData.data = this.getFlatData(this.treeData); + } + + /** + * 对树形结构数据做扁平化处理 + * @param nodes 同层级且隶属同一父节点的节点集合 + * @param level 节点层级,根节点层级为0,往下依次类推 + */ + private getFlatData(nodes: Array, level?: number): Array { + let result: Array = []; + if (!nodes) { + return result; + } + nodes.forEach((item: TiTableRowData, index: number): void => { + item.level = level ? level : 0; + item.hasChildren = item.children && item.children.length > 0; + result.push(item); + if (item.expand && item.hasChildren) { + result = result.concat(this.getFlatData(item.children, item.level + 1)); + } + }); + + return result; + } +} diff --git a/src/table/demo/src/app/table/table-actionmenu.html b/src/table/demo/src/app/table/table-actionmenu.html new file mode 100644 index 0000000..5a3b840 --- /dev/null +++ b/src/table/demo/src/app/table/table-actionmenu.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.name}}{{row.createTime | date: 'yyyy-MM-dd'}}{{row.operator}}{{row.state}} + +
    +
    diff --git a/src/table/demo/src/app/table/table-basic-test.html b/src/table/demo/src/app/table/table-basic-test.html new file mode 100644 index 0000000..cac0ebd --- /dev/null +++ b/src/table/demo/src/app/table/table-basic-test.html @@ -0,0 +1,43 @@ +
    +

    描述

    +

    + 表格的基本使用
    + 使用前先了解一下表格的主要功能和数据流。表格组件的功能是把传入的srcData 通过一系列的处理,
    + 输出到displayedData中,再用displayedData的数据显示出来。
    + 表格的数据流是 srcData =>(根据srcData.state状态转化)=>displayedData,
    + 一系列的转化包括分页、排序、搜索。所以srcData.data 中的数据变化, 以及srcData.state 中的分页、排序、搜索状态变化,都会触发一次 + srcData.data =>(srcData.state)=>displayedData的转化。
    + 用户只需输入源数据srcData.data 以及初始化状态srcData.state, 使用输出数据displayedData 展示成表格。 +

    + +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    导入

    +

    import {{ '{' }} TiTableModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-basic.html b/src/table/demo/src/app/table/table-basic.html new file mode 100644 index 0000000..25343d2 --- /dev/null +++ b/src/table/demo/src/app/table/table-basic.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-cell-tip.html b/src/table/demo/src/app/table/table-cell-tip.html new file mode 100644 index 0000000..d66501a --- /dev/null +++ b/src/table/demo/src/app/table/table-cell-tip.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}
    {{row.name}}{{row.age}}{{row.email}}{{row.address}}
    +
    diff --git a/src/table/demo/src/app/table/table-cellicons-colsresizable.html b/src/table/demo/src/app/table/table-cellicons-colsresizable.html new file mode 100644 index 0000000..2a761d2 --- /dev/null +++ b/src/table/demo/src/app/table/table-cellicons-colsresizable.html @@ -0,0 +1,62 @@ +
    +

    描述

    +

    当表格有列拖动功能且单元格文本后有可操作图标时,在拖动列宽变小内容显示不下时,需要使文本出'...', 而图标显示完全。

    +

    需要使用tiCellText组件包裹单元格中的文本,使用tiCellIcons组件包裹图标

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    +

    表格单元格中有图标 + 列拖动

    + +
    + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} + + + + + + {{columns[1].title}} + + + + + + {{columns[2].title}} + + + {{columns[3].title}} + + {{columns[4].title}}
    + {{row.firstName}} + + + + {{row.lastName}}{{row.age}} + {{row.balance}} + + + + {{row.email}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-checkbox-pagination-headmenu.html b/src/table/demo/src/app/table/table-checkbox-pagination-headmenu.html new file mode 100644 index 0000000..17d5e3e --- /dev/null +++ b/src/table/demo/src/app/table/table-checkbox-pagination-headmenu.html @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + +
    + + + {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}{{columns[5].title}}
    + + {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + + + {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}{{columns[5].title}}
    + + {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    + +
    diff --git a/src/table/demo/src/app/table/table-checkbox-pagination.html b/src/table/demo/src/app/table/table-checkbox-pagination.html new file mode 100644 index 0000000..0d3c25e --- /dev/null +++ b/src/table/demo/src/app/table/table-checkbox-pagination.html @@ -0,0 +1,87 @@ +

    描述

    +

    表格复选 + 分页

    +

    1. 表格复选功能需要配合tiCheckgroup组件来实现,具体可参考tiCheckgroup组件的使用说明。

    +

    2. 按UI规范,全选复选框控制选中/取消选中当前页。

    +

    3. 某页选中一些行,跳转到其他页,再次返回到之前页,之前选中的行仍然是选中的。

    +

    导入

    +import {{ '{' }} TiTableModule, TiCheckboxModule, TiOverflowModule {{ '}' }} from '@opentiny/ng'; +

    示例

    + +

    1.复选 + 前台分页

    +
    + +
    checkedList:{{checkedList|json}}
    + + + + + + + + + + + + + + + + + + + + + + + +
    + + + {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + {{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    + +
    +
    +

    2.复选 + 后台分页

    +
    +

    注意 tiCheckgroup 上 valueKey 接口的使用

    + +
    checkedList1:{{checkedList1 | json}}
    +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + {{columns1[1].title}}{{columns1[2].title}}{{columns1[3].title}}{{columns1[4].title}}
    + + {{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    + +
    diff --git a/src/table/demo/src/app/table/table-checkbox.html b/src/table/demo/src/app/table/table-checkbox.html new file mode 100644 index 0000000..2771977 --- /dev/null +++ b/src/table/demo/src/app/table/table-checkbox.html @@ -0,0 +1,27 @@ +

    Selected {{ checkedList.length }} rows.

    + + + + + + + + + + + + + + + + + + + + +
    + + {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-col-align.html b/src/table/demo/src/app/table/table-col-align.html new file mode 100644 index 0000000..5aadf10 --- /dev/null +++ b/src/table/demo/src/app/table/table-col-align.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-colalign-sort-resizable-test.html b/src/table/demo/src/app/table/table-colalign-sort-resizable-test.html new file mode 100644 index 0000000..4b9d2d7 --- /dev/null +++ b/src/table/demo/src/app/table/table-colalign-sort-resizable-test.html @@ -0,0 +1,46 @@ +
    +

    描述

    +

    表格列对齐方式、排序、列拖动

    +

    示例

    + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} + + + {{columns[1].title}} + + + {{columns[2].title}} + + + {{columns[3].title}} + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-cols-resizable.html b/src/table/demo/src/app/table/table-cols-resizable.html new file mode 100644 index 0000000..ad7f57d --- /dev/null +++ b/src/table/demo/src/app/table/table-cols-resizable.html @@ -0,0 +1,34 @@ + +
    + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}} + {{columns[3].title}} + + + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.address}} + {{row.email}} + + + + {{row.age}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-cols-toggle-details.html b/src/table/demo/src/app/table/table-cols-toggle-details.html new file mode 100644 index 0000000..58f257f --- /dev/null +++ b/src/table/demo/src/app/table/table-cols-toggle-details.html @@ -0,0 +1,52 @@ +
    +

    描述

    +

    表格详情/列隐藏显示 + 详情展开

    +

    示例

    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}{{columns[5].title}}
    + + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    +
    +
    详情展示:{{row.firstName}}
    +
    详情展示:{{row.balance}}
    +
    详情展示:{{row.lastName}}
    +
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-cols-toggle-test.html b/src/table/demo/src/app/table/table-cols-toggle-test.html new file mode 100644 index 0000000..61c3a95 --- /dev/null +++ b/src/table/demo/src/app/table/table-cols-toggle-test.html @@ -0,0 +1,55 @@ +

    描述

    +

    表格列动态显示/隐藏功能需要配合tiColsToggle组件来实现,具体可参考tiColsToggle组件的使用说明。

    +

    + tiColsToggle组件需要放在tiTable标签内部,具体布局位置由开发者来控制;此功能与columns的中的show属性 + 设置密切相关,需要使用ngIf利用show的值来控制需要动态显示/隐藏的列;columns的相关配置可参考tiTable组 件的columns接口说明。 +

    +

    + 另外:selectAll- 设置是否有全选项,默认false
    + disabled- 设置禁用
    + searchable-设置是否有搜索,默认true +

    +

    列隐藏/显示按钮悬浮时有默认提示文本和方向,服务也可自行配置。

    +

    注意:使用 tiTip 组件,请导入 TiTipModule;使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + + + +
    + + + +
    + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-cols-toggle.html b/src/table/demo/src/app/table/table-cols-toggle.html new file mode 100644 index 0000000..31bd3af --- /dev/null +++ b/src/table/demo/src/app/table/table-cols-toggle.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-colsresizable-basic.html b/src/table/demo/src/app/table/table-colsresizable-basic.html new file mode 100644 index 0000000..6cfdda0 --- /dev/null +++ b/src/table/demo/src/app/table/table-colsresizable-basic.html @@ -0,0 +1,36 @@ +

    描述

    +

    + 要开启列拖动功能时,需要在ti-table标签上使用tiColsResizable指令,并且需要给table一个带有 + ti3-resize-wrapper样式类的父容器。如果要禁止某一列的拖动,则可以给当前列表头的th标签上添加 not-resizable 属性 +

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-colsresizable-colstoggle-fixedhead.html b/src/table/demo/src/app/table/table-colsresizable-colstoggle-fixedhead.html new file mode 100644 index 0000000..e38f5e5 --- /dev/null +++ b/src/table/demo/src/app/table/table-colsresizable-colstoggle-fixedhead.html @@ -0,0 +1,52 @@ +
    +

    描述

    +

    列拖动特性 + 列动态显示/隐藏特性 + 表头锁定特性

    +

    示例

    + +
    + + +
    + + + +
    + + + + + + + + + +
    {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-colsresizable-colstoggle.html b/src/table/demo/src/app/table/table-colsresizable-colstoggle.html new file mode 100644 index 0000000..ca33e7c --- /dev/null +++ b/src/table/demo/src/app/table/table-colsresizable-colstoggle.html @@ -0,0 +1,36 @@ +
    +

    描述

    +

    表格列拖动、列隐藏/显示

    +

    示例

    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-colsresizable-loadfail.html b/src/table/demo/src/app/table/table-colsresizable-loadfail.html new file mode 100644 index 0000000..b577e3d --- /dev/null +++ b/src/table/demo/src/app/table/table-colsresizable-loadfail.html @@ -0,0 +1,34 @@ +
    +

    描述

    +

    表格列拖动特性 + 数据加载失败特性

    +

    示例

    + +
    + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + {{failLoadInfo}} + {{reloadInfo}} +
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-colsresizable-sort-headfilter.html b/src/table/demo/src/app/table/table-colsresizable-sort-headfilter.html new file mode 100644 index 0000000..1ac5972 --- /dev/null +++ b/src/table/demo/src/app/table/table-colsresizable-sort-headfilter.html @@ -0,0 +1,67 @@ +
    +

    描述

    +

    表格列拖动特性 + 排序特性 + 表头过滤

    +

    用户可通过panelAlign接口控制表头过滤下拉面板展开方向,详见用例

    +

    示例

    + + +
    + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} + + + + + {{columns[1].title}} + + + + + {{columns[2].title}} + + + {{columns[3].title}} + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-colsresizable-sort.html b/src/table/demo/src/app/table/table-colsresizable-sort.html new file mode 100644 index 0000000..2ec6978 --- /dev/null +++ b/src/table/demo/src/app/table/table-colsresizable-sort.html @@ -0,0 +1,43 @@ +

    描述

    +

    表格列拖动特性 + 排序特性

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + +
    + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} + + + {{columns[1].title}} + + + {{columns[2].title}} + + + {{columns[3].title}} + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-column-fixed.html b/src/table/demo/src/app/table/table-column-fixed.html new file mode 100644 index 0000000..35d6d34 --- /dev/null +++ b/src/table/demo/src/app/table/table-column-fixed.html @@ -0,0 +1,22 @@ + +
    + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}{{row.address}}action
    +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-checkbox.html b/src/table/demo/src/app/table/table-columnfixed-checkbox.html new file mode 100644 index 0000000..f4a32f3 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-checkbox.html @@ -0,0 +1,41 @@ +
    +

    描述

    +

    表格复选列固定

    +

    示例

    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + {{column.title}}
    + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.address}}{{row.phone}}{{row.parents}}{{row.school}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-colstoggle.html b/src/table/demo/src/app/table/table-columnfixed-colstoggle.html new file mode 100644 index 0000000..8dee616 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-colstoggle.html @@ -0,0 +1,49 @@ +
    +

    描述

    +

    列固定 + 列动态显示/隐藏

    + +
    + + + +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    + {{column.title}} +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.address}}{{row.phone}}{{row.parents}}{{row.school}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-editrow.html b/src/table/demo/src/app/table/table-columnfixed-editrow.html new file mode 100644 index 0000000..9c40b71 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-editrow.html @@ -0,0 +1,95 @@ +

    描述

    +

    表格列固定+行编辑示例场景

    + +

    示例

    + + +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    + + + + + + + + + + + + + + +
    {{row.sourceName}}{{row.level}}{{row.balance}}{{row.unsubscribe}}{{row.createTime| date: "yyyy-MM-dd"}}{{row.operator}} + +
    {{row.sourceName}} + + + + + + + + + + + + +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-fixedhead-colsresizable-pagination.html b/src/table/demo/src/app/table/table-columnfixed-fixedhead-colsresizable-pagination.html new file mode 100644 index 0000000..e682af9 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-fixedhead-colsresizable-pagination.html @@ -0,0 +1,44 @@ +
    +

    描述

    +

    表格列固定 + 表头锁定特性 + 列拖动特性 + 分页特性

    +

    注意:使用 pagination 组件,请导入 TiPaginationModule

    + + + +
    + + + + + + +
    {{column.title}}
    +
    + + + +
    + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.address}}{{row.phone}}{{row.parents}}{{row.school}}
    +
    + +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-headfixed.html b/src/table/demo/src/app/table/table-columnfixed-headfixed.html new file mode 100644 index 0000000..62ddf13 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-headfixed.html @@ -0,0 +1,31 @@ + +
    + + + + + + +
    {{column.title}}
    +
    +
    + + + + + + + + + + + + + + + + + +
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}{{row.address}}action
    +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-leftmenu.html b/src/table/demo/src/app/table/table-columnfixed-leftmenu.html new file mode 100644 index 0000000..f57f9d1 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-leftmenu.html @@ -0,0 +1,54 @@ +

    描述

    +

    测试固定列和leftmenu

    +

    示例

    + + + +
    {{headLabel}}
    +
    + + + +
    {{m1.label}}
    +
    + + {{m2.label}} + +
    +
    + + +
    +

    描述

    +

    表格的列固定 + leftmenu, 主要测试leftmenu向左展开/收起时, 表格列固定是否正常

    +

    示例

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.address}}{{row.phone}}{{row.parents}}{{row.school}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-nodata.html b/src/table/demo/src/app/table/table-columnfixed-nodata.html new file mode 100644 index 0000000..9ad668a --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-nodata.html @@ -0,0 +1,35 @@ +
    +

    描述

    +

    表格无数据 + 列固定

    +

    示例

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.address}}{{row.phone}}{{row.parents}}{{row.email}}{{row.school}}
    {{noDadaInfo}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-pagination.html b/src/table/demo/src/app/table/table-columnfixed-pagination.html new file mode 100644 index 0000000..9de0480 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-pagination.html @@ -0,0 +1,33 @@ +
    +

    描述

    +

    表格的列固定 + 分页, 主要测试操作分页表格数据变动时, 表格列固定是否正常

    +

    示例

    + + +
    + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.address}}{{row.phone}}{{row.parents}}{{row.school}}
    +
    + + + +
    +
    diff --git a/src/table/demo/src/app/table/table-columnfixed-resizable.html b/src/table/demo/src/app/table/table-columnfixed-resizable.html new file mode 100644 index 0000000..d51d189 --- /dev/null +++ b/src/table/demo/src/app/table/table-columnfixed-resizable.html @@ -0,0 +1,37 @@ +
    +

    描述

    +

    表格列固定 + 列拖动特性

    +

    表格的列固定功能。使用 tiColumnFixed 指令和带有ti3-table-container 这个样式类的容器配合实现。

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + + +
    + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.school}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-comprehensive.html b/src/table/demo/src/app/table/table-comprehensive.html new file mode 100644 index 0000000..01c0080 --- /dev/null +++ b/src/table/demo/src/app/table/table-comprehensive.html @@ -0,0 +1,82 @@ +
    +

    描述

    +

    综合示例,表格复选 + 列动态显示/隐藏 + 列固定 + 表头锁定特性 + 列拖动特性 + 分页特性

    +

    注意:使用 pagination 组件,请导入 TiPaginationModule

    + + +
    + + + +
    + + +
    + + + + + + + + + +
    + + + + + {{column.title}} +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.address}}{{row.phone}}{{row.parents}}{{row.school}}
    +
    + +
    +
    diff --git a/src/table/demo/src/app/table/table-details-closeotherdetails.html b/src/table/demo/src/app/table/table-details-closeotherdetails.html new file mode 100644 index 0000000..a9ad345 --- /dev/null +++ b/src/table/demo/src/app/table/table-details-closeotherdetails.html @@ -0,0 +1,51 @@ +
    +

    描述

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    表格点击展开某一行的详情,需要收起其它行的详情时,需要将closeOtherDetails接口设为true,该接口默认为false。

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}{{columns[5].title}}
    + + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    +
    +
    详情展示:{{row.firstName}}
    +
    详情展示:{{row.balance}}
    +
    详情展示:{{row.lastName}}
    +
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-details-nesttable.html b/src/table/demo/src/app/table/table-details-nesttable.html new file mode 100644 index 0000000..3875fea --- /dev/null +++ b/src/table/demo/src/app/table/table-details-nesttable.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    + +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-details-pagination.html b/src/table/demo/src/app/table/table-details-pagination.html new file mode 100644 index 0000000..c6ade71 --- /dev/null +++ b/src/table/demo/src/app/table/table-details-pagination.html @@ -0,0 +1,52 @@ +
    +

    描述

    +

    表格详情展开特性 + 前台分页特性

    +

    示例

    +

    注意:使用 pagination 组件,请导入 TiPaginationModule

    +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}{{columns[5].title}}
    + + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    + + +
    +
    详情展示:{{row.firstName}}
    +
    详情展示:{{row.balance}}
    +
    详情展示:{{row.lastName}}
    +
    +
    + +
    +
    diff --git a/src/table/demo/src/app/table/table-details.html b/src/table/demo/src/app/table/table-details.html new file mode 100644 index 0000000..21dc5c4 --- /dev/null +++ b/src/table/demo/src/app/table/table-details.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    +
    Name: {{row.firstName}} {{row.lastName}}
    +
    Email: {{row.email}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-dynamic-details.html b/src/table/demo/src/app/table/table-dynamic-details.html new file mode 100644 index 0000000..bced8a3 --- /dev/null +++ b/src/table/demo/src/app/table/table-dynamic-details.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    +

    {{row.info}}

    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-editall-test.html b/src/table/demo/src/app/table/table-editall-test.html new file mode 100644 index 0000000..8bf36ac --- /dev/null +++ b/src/table/demo/src/app/table/table-editall-test.html @@ -0,0 +1,60 @@ +

    描述

    +

    + 表格整体编辑场景示例 +
    +

    +
    +

    示例

    +
    + +
    +
    + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.sourceName}} + + + + + + + + + + + + +
    +
    +
    + + + + +

    {{allFormControl.errors.message}}

    + +
    + +
    + + diff --git a/src/table/demo/src/app/table/table-editall.html b/src/table/demo/src/app/table/table-editall.html new file mode 100644 index 0000000..cc9edee --- /dev/null +++ b/src/table/demo/src/app/table/table-editall.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    + + + + + + + + + +
    +
    + +
    +
    Current Table Data: {{ srcData.data | json }}
    +
    diff --git a/src/table/demo/src/app/table/table-editrow-test.html b/src/table/demo/src/app/table/table-editrow-test.html new file mode 100644 index 0000000..ca5d16c --- /dev/null +++ b/src/table/demo/src/app/table/table-editrow-test.html @@ -0,0 +1,87 @@ +

    描述

    +

    表格行编辑场景示例

    + +

    示例

    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    + + + + + + + + + + + + + + +
    {{row.sourceName}}{{row.level}}{{row.balance}}{{row.unsubscribe}}{{row.createTime| date: "yyyy-MM-dd"}}{{row.operator}} + +
    {{row.sourceName}} + + + + + + + + + + + + +
    +
    diff --git a/src/table/demo/src/app/table/table-editrow.html b/src/table/demo/src/app/table/table-editrow.html new file mode 100644 index 0000000..c67e217 --- /dev/null +++ b/src/table/demo/src/app/table/table-editrow.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.sourceName}}{{row.level}}{{row.unsubscribe}}{{row.createTime| date: "yyyy-MM-dd"}} + +
    {{row.sourceName}} + + + + + + + +
    +
    diff --git a/src/table/demo/src/app/table/table-filter-strict.html b/src/table/demo/src/app/table/table-filter-strict.html new file mode 100644 index 0000000..7d15ae2 --- /dev/null +++ b/src/table/demo/src/app/table/table-filter-strict.html @@ -0,0 +1,93 @@ +
    +

    描述

    +

    表格搜索利用tiTable组件的searchKeys和searchWords接口来实现。

    +

    默认为模糊匹配,可设置 searchStrictKeys 接口 来指定其中哪些字段是精确匹配的。

    +

    表格组件提供了简单的搜索功能,如果搜索条件复杂可自行处理搜索逻辑,将搜索后的数据直接传给 srcData.data 源数据接口。

    +

    示例

    +

    + 1.针对源数据在searchKeys指定的字段下搜索searchWords[0]指定的字符串, + 在指定字段中的任一字段搜索到时即满足条件(并集)。默认为模糊匹配,哪些字段是精确匹配取决于searchStrictKeys。 如:searchWords: ['po'], + searchKeys: ['firstName', 'lastName', 'age'], searchStrictKeys: ['firstName'], + 则或在firstName字段中等于(精确匹配)‘po’,或在lastName字段包含(模糊匹配)‘po’, 或在age字段包含(模糊匹配)‘po’时本行数据即满足搜索条件。 +

    + + + +
    +

    searchWords: {{oneWordSearch.searchWords[0]}}

    +

    searchKeys: {{oneWordSearch.searchKeys}}

    +

    searchStrictKeys: {{oneWordSearch.searchStrictKeys}}

    +
    + +

    搜索结果个数: {{ searchedData.length }}

    +
    + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    + +

    + 2.针对源数据在searchKeys指定的字段下搜索searchWords对应(按顺序)元素指定的字符串, + 在指定字段中的所有字段搜索到对应值时才满足条件(交集)。默认为模糊匹配,哪些字段是精确匹配取决于searchStrictKeys。 如:searchWords: ['b', + '39'], searchKeys: ['firstName', 'age'], searchStrictKeys: ['age'], 则在firstName字段包含(模糊匹配)‘b’且 + 在age字段等于(精确匹配)‘39’时本行数据才满足搜索条件。 +

    + + +
    +

    searchWords: {{moreThanOneWordSearch.searchWords[0]}},{{moreThanOneWordSearch.searchWords[1]}}

    +

    searchKeys: {{moreThanOneWordSearch.searchKeys}}

    +

    searchKeys: {{moreThanOneWordSearch.searchStrictKeys}}

    +
    + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-filter.html b/src/table/demo/src/app/table/table-filter.html new file mode 100644 index 0000000..aa56e34 --- /dev/null +++ b/src/table/demo/src/app/table/table-filter.html @@ -0,0 +1,85 @@ +
    +

    描述

    +

    表格搜索利用tiTable组件的searchKeys和searchWords接口来实现。

    +

    表格组件提供了简单的搜索功能,如果搜索条件复杂可自行处理搜索逻辑,将搜索后的数据直接传给srcData.data源数据接口。

    +

    示例

    +

    + 1.针对源数据在searchKeys指定的字段下搜索searchWords[0]指定的字符串, 在指定字段中的任一字段搜索到时即满足条件(并集)。如:searchWords: + ['b'], searchKeys: ['firstName', 'lastName'],则在firstName字段包含‘b’, 或在lastName字段包含‘b’时本行数据即满足搜索条件 +

    + + + +
    +

    searchWords: {{oneWordSearch.searchWords[0]}}

    +

    searchKeys: {{oneWordSearch.searchKeys}}

    +
    + +

    搜索结果个数: {{ searchedData.length }}

    +
    + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    + +

    + 2.针对源数据在searchKeys指定的字段下搜索searchWords对应(按顺序)元素指定的字符串, + 在指定字段中的所有字段搜索到对应值时才满足条件(交集)。如:searchWords: ['b', '18'], searchKeys: ['firstName', + 'age'],则在firstName字段包含‘b’且 在age字段包含‘18’时本行数据才满足搜索条件 +

    + + +
    +

    searchWords: {{moreThanOneWordSearch.searchWords[0]}},{{moreThanOneWordSearch.searchWords[1]}}

    +

    searchKeys: {{moreThanOneWordSearch.searchKeys}}

    +
    + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-fixed-head-cols-resizable.html b/src/table/demo/src/app/table/table-fixed-head-cols-resizable.html new file mode 100644 index 0000000..8bd1dac --- /dev/null +++ b/src/table/demo/src/app/table/table-fixed-head-cols-resizable.html @@ -0,0 +1,41 @@ +
    +

    描述

    +

    表格表头锁定特性 + 列拖动特性

    +

    示例

    + + + +
    + + + + + + + +
    {{column.title}}
    +
    +
    dasdf
    + + + +
    + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-fixed-head-in-accordion.html b/src/table/demo/src/app/table/table-fixed-head-in-accordion.html new file mode 100644 index 0000000..7565c3b --- /dev/null +++ b/src/table/demo/src/app/table/table-fixed-head-in-accordion.html @@ -0,0 +1,49 @@ +
    +

    描述

    +

    放在手风琴中的表格表头锁定特性

    +

    注意:使用 accordion 组件,请导入 TiAccordionModule

    +

    +

    示例

    + + + + Fixed head table in accordion + + + +
    + + + + + + + +
    {{column.title}}
    +
    +
    dasdf
    + + + +
    + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-fixed-head-nodata.html b/src/table/demo/src/app/table/table-fixed-head-nodata.html new file mode 100644 index 0000000..540a158 --- /dev/null +++ b/src/table/demo/src/app/table/table-fixed-head-nodata.html @@ -0,0 +1,187 @@ +
    +

    描述

    +

    表头固定和无数各种场景测试

    +

    示例

    +

    无数据(ti3-table-nodata)

    +

    + + + +
    + + + + + + + +
    {{column.title}}
    +
    +
    dasdf
    + + + +
    + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    +
    +
    +

    无数据("ti3-table-nodata-guide)

    +

    + + + +
    + + + + + + + +
    {{column.title}}
    +
    +
    dasdf
    + + + +
    + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + {{noGoodsInfo}} + {{goShopInfo}} +
    +
    +
    + +

    加载失败(ti3-table-loadfail)

    +

    + + + +
    + + + + + + + +
    {{column.title}}
    +
    +
    dasdf
    + + + +
    + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + {{failLoadInfo}} + {{reloadInfo}} +
    +
    +
    +
    +
    +

    无数据简单类型(ti3-table-nodata-simple)

    +

    + + + +
    + + + + + + + +
    {{column.title}}
    +
    +
    dasdf
    + + + +
    + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-fixed-head-pagination-details.html b/src/table/demo/src/app/table/table-fixed-head-pagination-details.html new file mode 100644 index 0000000..d3910e6 --- /dev/null +++ b/src/table/demo/src/app/table/table-fixed-head-pagination-details.html @@ -0,0 +1,72 @@ +
    +

    描述

    +

    表格表头锁定特性

    +

    注意:使用 pagination 组件,请导入 TiPaginationModule

    +

    示例

    + + + +
    + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}{{columns[5].title}}
    +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    + + +
    +
    详情展示:{{row.firstName}}
    +
    详情展示:{{row.balance}}
    +
    详情展示:{{row.lastName}}
    +
    +
    +
    + +
    +
    diff --git a/src/table/demo/src/app/table/table-fixed-head.html b/src/table/demo/src/app/table/table-fixed-head.html new file mode 100644 index 0000000..98bf043 --- /dev/null +++ b/src/table/demo/src/app/table/table-fixed-head.html @@ -0,0 +1,28 @@ + +
    + + + + + + +
    {{column.title}}
    +
    +
    + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-fixedhead-colsresizable-pagination-details.html b/src/table/demo/src/app/table/table-fixedhead-colsresizable-pagination-details.html new file mode 100644 index 0000000..8a2e4ec --- /dev/null +++ b/src/table/demo/src/app/table/table-fixedhead-colsresizable-pagination-details.html @@ -0,0 +1,72 @@ +
    +

    描述

    +

    表格表头锁定特性 + 列拖动特性 + 分页特性 + 详情展开特性

    +

    注意:使用 pagination 组件,请导入 TiPaginationModule

    +

    示例

    + + + +
    + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}{{columns[5].title}}
    +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    + + +
    +
    详情展示:{{row.firstName}}
    +
    详情展示:{{row.balance}}
    +
    详情展示:{{row.lastName}}
    +
    +
    +
    + +
    +
    diff --git a/src/table/demo/src/app/table/table-fixhead-scroll.html b/src/table/demo/src/app/table/table-fixhead-scroll.html new file mode 100644 index 0000000..30d586e --- /dev/null +++ b/src/table/demo/src/app/table/table-fixhead-scroll.html @@ -0,0 +1,51 @@ +

    描述

    +

    本测试用例主要测试表头固定是否影响页面触发滚动条。

    +

    + 在有表格表头固定的页面,且页面有滚动条,将页面滚动条 + 拖至formfiled的头部不在视野范围内时,给formfield中添加数据后页面滚动条会异常跳动到顶部,正常滚动条应该不跳动。该问题在10.1.8版本修复 +

    +

    示例

    +
    + + +
    {{item}}
    +
    +
    + +
    +
    + +
    + + + + + + +
    {{column.title}}
    +
    + +
    + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-group.html b/src/table/demo/src/app/table/table-group.html new file mode 100644 index 0000000..5f7a8b8 --- /dev/null +++ b/src/table/demo/src/app/table/table-group.html @@ -0,0 +1,42 @@ +
    +

    描述

    +

    开发者可结合数据利用表格dom结构来实现简单的表格分组

    +

    示例

    + + +
    + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    + + + + {{you.name}} +
    {{son.name}}{{son.size}}{{son.type}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-guide.html b/src/table/demo/src/app/table/table-guide.html new file mode 100644 index 0000000..67227e4 --- /dev/null +++ b/src/table/demo/src/app/table/table-guide.html @@ -0,0 +1,65 @@ +
    +

    描述

    +

    表格无数据时导引,结合tiColspan指令和ti3-table-nodata-guide样式类实现。

    +

    示例

    + +

    大表格(默认表格)

    +

    + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + {{noGoodsInfo}} + {{goShopInfo}} +
    +
    +



    +

    小表格

    +

    + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + {{noGoodsInfo}} + {{goShopInfo}} +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter-datetime-test.html b/src/table/demo/src/app/table/table-head-filter-datetime-test.html new file mode 100644 index 0000000..9033b4d --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter-datetime-test.html @@ -0,0 +1,65 @@ +
    +

    描述

    +

    + 表格表头过滤要结合 tiCellText 组件和 tiHeadFilter 组件来实现。ti-cell-text包裹的是表头单元格 + 文本,ti-head-filter为表头过滤的漏斗图标,tiHeadFilter的接口说明请查询 TiHeadFilterComponent。 +

    +

    表头过滤-时间日期通过配置 isDatetimedatetimeConfig 等接口来配置表头过滤下拉面板。

    +

    isDatetime 接口控制表头过滤是否设置为时间日期面板。

    +

    datetimeConfig 接口设置表头过滤时间日期面板中的配置信息:

    +

    datetimeConfig.format 可选,设置日期时间显示格式。

    +

    datetimeConfig.min 可选,设置最小值。

    +

    datetimeConfig.max 可选,设置最大值。

    +

    datetimeConfig.onlyDate 可选,设置是否仅是日期的选择(没有时间的选择),不设置时默认是日期时间的选择。

    +

    panelAlign 接口控制表头过滤下拉面板展开方向,默认左对齐

    +

    panelWidth 接口控制表头过滤下拉框的宽度,默认为'auto',搜索场景下默认值修改为“180px”

    +

    备注:

    +

    + searchable,labelKey + 两个接口,因时间日期和下拉是两个相互独立的面板,且面板中不包含搜索框和下拉options的匹配,此处不涉及这两个接口的使用。 + 所以在这两个接口在时间日期面板中使用无效 +

    +

    + 因时间日期面板涉及的场景较多,需求不一,表格只提供接口及过滤时机,具体搜索实现方案,用户根据实际场景进行自定义,此处提供简单示例供参考 +

    +

    示例

    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    {{column.title}} + + {{column.title}} + + + +
    {{row.firstName}}{{row.birthday}}{{row.hireDate}}{{row.start}}{{row.expired}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter-datetime.html b/src/table/demo/src/app/table/table-head-filter-datetime.html new file mode 100644 index 0000000..dacf551 --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter-datetime.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}} + {{column.title}} + + +
    {{row.name}}{{row.birthday}}{{row.hireDate}}{{row.start}}{{row.expired}}
    {{noDadaInfo}}
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter-multi-valuekey.html b/src/table/demo/src/app/table/table-head-filter-multi-valuekey.html new file mode 100644 index 0000000..83580f8 --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter-multi-valuekey.html @@ -0,0 +1,59 @@ +
    +

    描述

    +

    + 表格表头过滤要结合 tiCellText 组件和 tiHeadFilter 组件来实现。ti-cell-text包裹的是表头单元格 + 文本,ti-head-filter为表头过滤的漏斗图标,tiHeadFilter的接口说明请查询 TiHeadFilterComponent。 +

    +

    + 表头过滤-多选通过配置 multipleselectAll + 等接口来配置表头过滤下拉面板。 +

    +

    multiple 接口控制表头过滤是否为多选,默认为单选。

    +

    selectAll 接口控制表头过滤下拉多选是否开启全选功能,默认不开启增。

    +

    panelAlign 接口控制表头过滤下拉面板展开方向,默认左对齐

    +

    searchable 接口控制表头过滤下拉是否开启搜索功能,默认不开启

    +

    labelKey 接口控制表头过滤下拉中显示的字段,默认为'label'

    +

    panelWidth 接口控制表头过滤下拉框的宽度,默认为'auto',搜索场景下默认值修改为“180px”

    +

    valuekey 自定义选中值, 设置valueKey时选中值基于valueKey,单选或者多选的选中值结构都和select组件保持一致

    +

    示例

    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    + + {{column.title}} + + + {{column.title}}
    {{row.firstName.label}}{{row.lastName.label}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter-multi.html b/src/table/demo/src/app/table/table-head-filter-multi.html new file mode 100644 index 0000000..42840f3 --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter-multi.html @@ -0,0 +1,57 @@ +
    +

    描述

    +

    + 表格表头过滤要结合 tiCellText 组件和 tiHeadFilter 组件来实现。ti-cell-text包裹的是表头单元格 + 文本,ti-head-filter为表头过滤的漏斗图标,tiHeadFilter的接口说明请查询 TiHeadFilterComponent。 +

    +

    + 表头过滤-多选通过配置 multipleselectAll + 等接口来配置表头过滤下拉面板。 +

    +

    multiple 接口控制表头过滤是否为多选,默认为单选。

    +

    selectAll 接口控制表头过滤下拉多选是否开启全选功能,默认不开启增。

    +

    panelAlign 接口控制表头过滤下拉面板展开方向,默认左对齐

    +

    searchable 接口控制表头过滤下拉是否开启搜索功能,默认不开启

    +

    labelKey 接口控制表头过滤下拉中显示的字段,默认为'label'

    +

    panelWidth 接口控制表头过滤下拉框的宽度,默认为'auto',搜索场景下默认值修改为“180px”

    +

    示例

    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    + + {{column.title}} + + + {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter-test.html b/src/table/demo/src/app/table/table-head-filter-test.html new file mode 100644 index 0000000..b55b929 --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter-test.html @@ -0,0 +1,84 @@ +
    +

    描述

    +

    + 表格表头过滤要结合 tiCellText 组件和 tiHeadFilter 组件来实现。ti-cell-text包裹的是表头单元格 + 文本,ti-head-filter为表头过滤的漏斗图标,tiHeadFilter的接口说明请查询 TiHeadFilterComponent。 +

    +

    panelAlign 接口控制表头过滤下拉面板展开方向,默认左对齐

    +

    searchable 接口控制表头过滤下拉是否开启搜索功能,默认不开启

    +

    labelKey 接口控制表头过滤下拉中显示的字段,默认为'label'

    +

    panelWidth 接口控制表头过滤下拉框的宽度,默认为'auto',搜索场景下默认值修改为“180px”

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    +

    + + + + + + + + + + + + + + + + + + + + + + + +
    + + {{column.title}} + + + {{column.title}} + + {{column.title}} + + + + + {{column.title}} + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.gender}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter-valuekey.html b/src/table/demo/src/app/table/table-head-filter-valuekey.html new file mode 100644 index 0000000..04c3bc6 --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter-valuekey.html @@ -0,0 +1,52 @@ +
    +

    描述

    +

    + 表格表头过滤要结合 tiCellText 组件和 tiHeadFilter 组件来实现。ti-cell-text包裹的是表头单元格 + 文本,ti-head-filter为表头过滤的漏斗图标,tiHeadFilter的接口说明请查询 TiHeadFilterComponent。 +

    +

    panelAlign 接口控制表头过滤下拉面板展开方向,默认左对齐

    +

    searchable 接口控制表头过滤下拉是否开启搜索功能,默认不开启

    +

    labelKey 接口控制表头过滤下拉中显示的字段,默认为'label'

    +

    panelWidth 接口控制表头过滤下拉框的宽度,默认为'auto',搜索场景下默认值修改为“180px”

    +

    valuekey 自定义选中值, 设置valueKey时选中值基于valueKey,单选或者多选的选中值结构都和select组件保持一致

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    +

    + + + + + + + + + + + + + + + + + + + + + +
    + + {{column.title}} + + + {{column.title}}
    {{row.firstName.label}}{{row.lastName.label}}{{row.age}}{{row.balance}}{{row.email}}{{row.gender}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter-virtualscroll.html b/src/table/demo/src/app/table/table-head-filter-virtualscroll.html new file mode 100644 index 0000000..5ed475b --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter-virtualscroll.html @@ -0,0 +1,55 @@ +
    +

    描述

    +

    表格表头过滤下拉面板如果数据量很大,可通过 virtual 接口开启下拉面板虚拟滚动功能,默认不开启

    +

    + 表格表头过滤要结合 tiCellText 组件和 tiHeadFilter 组件来实现。ti-cell-text包裹的是表头单元格 + 文本,ti-head-filter为表头过滤的漏斗图标,tiHeadFilter的接口说明请查询 TiHeadFilterComponent。 +

    +

    multiple 接口控制表头过滤是否为多选,默认为单选。

    +

    selectAll 接口控制表头过滤下拉多选是否开启全选功能,默认不开启增。

    +

    searchable 接口控制表头过滤下拉是否开启搜索功能,默认不开启

    +

    virtual 接口控制表头过滤下拉是否开启下拉面板虚拟滚动功能,默认不开启

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    +

    + + + + + + + + + + + + + + + + + + + +
    + + {{column.title}} + + +
    {{row.firstName}}{{row.lastName}}{{row.total}}{{row.balance}}{{row.email}}
    + + + +
    +
    diff --git a/src/table/demo/src/app/table/table-head-filter.html b/src/table/demo/src/app/table/table-head-filter.html new file mode 100644 index 0000000..59104fc --- /dev/null +++ b/src/table/demo/src/app/table/table-head-filter.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
    + {{column.title}} + + + + {{column.title}} + + + 邮箱后缀:{{item.label}} + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.gender}}{{row.email}}
    {{noDadaInfo}}
    +
    diff --git a/src/table/demo/src/app/table/table-load-fail.html b/src/table/demo/src/app/table/table-load-fail.html new file mode 100644 index 0000000..65fa73b --- /dev/null +++ b/src/table/demo/src/app/table/table-load-fail.html @@ -0,0 +1,65 @@ +
    +

    描述

    +

    表格加载数据失败,结合tiColspan指令和ti3-table-loadfail样式类实现。

    +

    示例

    + +

    大表格(默认表格)

    +

    + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + {{failLoadInfo}} + {{reloadInfo}} +
    +
    +



    +

    小表格

    +

    + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + {{failLoadInfo}} + {{reloadInfo}} +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-nodata-simple.html b/src/table/demo/src/app/table/table-nodata-simple.html new file mode 100644 index 0000000..c5d2ff0 --- /dev/null +++ b/src/table/demo/src/app/table/table-nodata-simple.html @@ -0,0 +1,28 @@ +

    描述

    +

    表格无数据简单类型,结合tiColspan指令和ti3-table-nodata-simple样式类实现。

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    diff --git a/src/table/demo/src/app/table/table-nodata-test.html b/src/table/demo/src/app/table/table-nodata-test.html new file mode 100644 index 0000000..355e900 --- /dev/null +++ b/src/table/demo/src/app/table/table-nodata-test.html @@ -0,0 +1,57 @@ +

    描述

    +

    表格无数据,结合tiColspan指令和ti3-table-nodata样式类实现。

    +

    示例

    + +

    大表格(默认表格)

    +

    + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    +



    +

    小表格

    +

    + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    diff --git a/src/table/demo/src/app/table/table-nodata.html b/src/table/demo/src/app/table/table-nodata.html new file mode 100644 index 0000000..b717136 --- /dev/null +++ b/src/table/demo/src/app/table/table-nodata.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    {{noDataInfo}}
    +
    diff --git a/src/table/demo/src/app/table/table-overflow-link.html b/src/table/demo/src/app/table/table-overflow-link.html new file mode 100644 index 0000000..160671b --- /dev/null +++ b/src/table/demo/src/app/table/table-overflow-link.html @@ -0,0 +1,49 @@ +
    +

    描述

    +

    + 表格中有链接,使用tiOverflow的场景
    + 问题:表格中使用A标签,使用tiOverflow后,tip显示的内容,会有A标签的样式
    + 原因:tiOverflow获取的是元素的innerHTML,将tiOverflow写在td上面,会获取到td中的所有内容,从而显示在tip里面。
    + 原本tip中获取的是元素的textContent,因为textContent只能获取文本,如果想要显示图片,则无法获取到。
    + 针对以上原因,目前提供的方案有两种:
    + 解决方案一:将tiOverflow写在A标签上,设置A标签的宽度
    + 解决方案二:通过使用tiTipContent接口,将tip中的内容重新设置即可 +

    + +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    导入

    +

    import {{ '{' }} TiTableModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    + {{row.firstName}} + + {{row.lastName}} + + {{row.email}} + {{row.age}}{{row.balance}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-pagi-with-filter.html b/src/table/demo/src/app/table/table-pagi-with-filter.html new file mode 100644 index 0000000..b07b2e2 --- /dev/null +++ b/src/table/demo/src/app/table/table-pagi-with-filter.html @@ -0,0 +1,36 @@ +

    描述

    +

    表格前台分页 + 前台过滤特性

    +

    注意:使用 pagination 组件,请导入 TiPaginationModule。

    +

    示例

    + + +

    搜索结果个数: {{ searchedData.length }}

    + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + +
    diff --git a/src/table/demo/src/app/table/table-pagination.html b/src/table/demo/src/app/table/table-pagination.html new file mode 100644 index 0000000..c3fae36 --- /dev/null +++ b/src/table/demo/src/app/table/table-pagination.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    + +
    diff --git a/src/table/demo/src/app/table/table-radio-test.html b/src/table/demo/src/app/table/table-radio-test.html new file mode 100644 index 0000000..c0f37dc --- /dev/null +++ b/src/table/demo/src/app/table/table-radio-test.html @@ -0,0 +1,46 @@ +

    描述

    +

    表格单选功能需要配合tiRadio组件来实现,具体可参考tiRadio组件的使用说明。使用单选时, 需要给单选框对应列(th,td)加 radio-column 属性。

    +

    注意:使用 tiRadio 组件,请导入 TiRadioModule;使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + + + {{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    + +
    diff --git a/src/table/demo/src/app/table/table-radio.html b/src/table/demo/src/app/table/table-radio.html new file mode 100644 index 0000000..40647bd --- /dev/null +++ b/src/table/demo/src/app/table/table-radio.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + +
    {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-row-drag2.html b/src/table/demo/src/app/table/table-row-drag2.html new file mode 100644 index 0000000..f1baadf --- /dev/null +++ b/src/table/demo/src/app/table/table-row-drag2.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-rowspan.html b/src/table/demo/src/app/table/table-rowspan.html new file mode 100644 index 0000000..9c7bd9b --- /dev/null +++ b/src/table/demo/src/app/table/table-rowspan.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-search.html b/src/table/demo/src/app/table/table-search.html new file mode 100644 index 0000000..149e9d6 --- /dev/null +++ b/src/table/demo/src/app/table/table-search.html @@ -0,0 +1,70 @@ +firstName,lastName,or age: + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    {{noDadaInfo}}
    +
    +


    + +firstName: + +age: + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    {{noDadaInfo}}
    +
    diff --git a/src/table/demo/src/app/table/table-server-pagi-search-sort.html b/src/table/demo/src/app/table/table-server-pagi-search-sort.html new file mode 100644 index 0000000..c6e39ad --- /dev/null +++ b/src/table/demo/src/app/table/table-server-pagi-search-sort.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}}{{columns[1].title}}{{columns[2].title}} + {{columns[3].title}} + + {{columns[4].title}}
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    + +
    暂无数据
    + +
    diff --git a/src/table/demo/src/app/table/table-server-pagi.html b/src/table/demo/src/app/table/table-server-pagi.html new file mode 100644 index 0000000..4c7564e --- /dev/null +++ b/src/table/demo/src/app/table/table-server-pagi.html @@ -0,0 +1,32 @@ +
    +

    描述

    +

    表格分页要结合tiPagination分页组件使用,后台分页时需要将srcData的state.paginated 设置为true(表示传入组件的数据已经进行过分页处理)。

    +

    注意:使用 tiPagination 组件,请导入 TiPaginationModule;使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + +

    事件日志:

    + +
    + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + + +
    +
    diff --git a/src/table/demo/src/app/table/table-small.html b/src/table/demo/src/app/table/table-small.html new file mode 100644 index 0000000..da9c802 --- /dev/null +++ b/src/table/demo/src/app/table/table-small.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-soldout.html b/src/table/demo/src/app/table/table-soldout.html new file mode 100644 index 0000000..2864da9 --- /dev/null +++ b/src/table/demo/src/app/table/table-soldout.html @@ -0,0 +1,46 @@ +

    描述

    +

    开发者可参考该示例实现表格售罄场景

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + {{columns[1].title}}{{columns[2].title}}{{columns[3].title}}{{columns[4].title}}
    + + + + {{row.lastName}} + {{row.message.text}} + {{row.age}}{{row.balance | currency}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-sort-basic.html b/src/table/demo/src/app/table/table-sort-basic.html new file mode 100644 index 0000000..a5235cb --- /dev/null +++ b/src/table/demo/src/app/table/table-sort-basic.html @@ -0,0 +1,51 @@ +
    +

    描述

    +

    表格排序要结合tiCellText组件和tiHeadSort组件来实现。ti-cell-text包裹的是表头单元格 文本,ti-head-sort为点击的排序图标。

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + + +

    + + + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} + + + + {{columns[1].title}} + + + {{columns[2].title}} + + + + {{columns[3].title}} + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.birthday}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-sort-comparefn-locale.html b/src/table/demo/src/app/table/table-sort-comparefn-locale.html new file mode 100644 index 0000000..afd214c --- /dev/null +++ b/src/table/demo/src/app/table/table-sort-comparefn-locale.html @@ -0,0 +1,50 @@ +
    +

    描述

    +

    + 表格组件提供的排序中字符串比较是使用基于标准字典的 Unicode 值来进行比较的。如果开发真需要本地化的排序, + 可使用tiHeadSort组件的compareFn接口来自定义所在列的本地化排序规则。本地化排序规则可利用 localeCompare 方法。 +

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} (此列用了自定义排序规则) + + + + + + {{columns[1].title}} (此列用了自定义排序规则) + + + {{columns[2].title}} + + + {{columns[3].title}} + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-sort-comparefn.html b/src/table/demo/src/app/table/table-sort-comparefn.html new file mode 100644 index 0000000..1bcec9d --- /dev/null +++ b/src/table/demo/src/app/table/table-sort-comparefn.html @@ -0,0 +1,47 @@ +
    +

    描述

    +

    可使用tiHeadSort组件的compareFn接口来自定义所在列的排序规则。

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} (此列用了自定义排序规则) + + + + + {{columns[1].title}} + + + {{columns[2].title}} (此列用了自定义排序规则) + + + + {{columns[3].title}} + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-sort-details.html b/src/table/demo/src/app/table/table-sort-details.html new file mode 100644 index 0000000..58ce830 --- /dev/null +++ b/src/table/demo/src/app/table/table-sort-details.html @@ -0,0 +1,67 @@ +
    +

    描述

    +

    表格详情

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{columns[0].title}} + {{columns[1].title}} + + + + {{columns[2].title}} + + + {{columns[3].title}} + + + + {{columns[4].title}} + + {{columns[5].title}}
    + + + {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance | currency}}{{row.email}}
    + + +
    +
    详情展示:{{row.firstName}}
    +
    详情展示:{{row.balance}}
    +
    详情展示:{{row.lastName}}
    +
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-sort-reset.html b/src/table/demo/src/app/table/table-sort-reset.html new file mode 100644 index 0000000..e3446bc --- /dev/null +++ b/src/table/demo/src/app/table/table-sort-reset.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} + + + {{columns[1].title}} + + + {{columns[2].title}} + + {{columns[3].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-sort-test.html b/src/table/demo/src/app/table/table-sort-test.html new file mode 100644 index 0000000..73fce59 --- /dev/null +++ b/src/table/demo/src/app/table/table-sort-test.html @@ -0,0 +1,37 @@ +
    +

    描述

    +

    表格排序要结合tiCellText组件和tiHeadSort组件来实现。ti-cell-text包裹的是表头单元格 文本,ti-head-sort为点击的排序图标。

    +

    10.1.14 版本 ti-head-sort 组件初始无序时(默认为null)兼容传入 undefined。

    +

    注意:使用 tiOverflow 组件,请导入 TiOverflowModule。

    +

    示例

    + + + + +

    + + + + + + + + + + + + + + + + + + +
    + {{column.title}} + + +
    {{row.firstName}}{{row.lastName}}{{row.birthday}}{{row.balance}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/table-sort.html b/src/table/demo/src/app/table/table-sort.html new file mode 100644 index 0000000..ba41fc3 --- /dev/null +++ b/src/table/demo/src/app/table/table-sort.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + +
    + {{columns[0].title}} + + + {{columns[1].title}} + + + {{columns[2].title}} + + + {{columns[3].title}} + + {{columns[4].title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.poetry}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-storage-config.html b/src/table/demo/src/app/table/table-storage-config.html new file mode 100644 index 0000000..21e8761 --- /dev/null +++ b/src/table/demo/src/app/table/table-storage-config.html @@ -0,0 +1,49 @@ +

    描述

    +

    + storageConfig 接口设置表格记忆项排序、分页、列宽 的开关 +
    + 测试:排序、列宽和分页都是设置了false,如果没有记忆功能,测试用过。
    + 只记忆当前页或只记忆每页个数 +

    +

    示例

    +

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + {{column.title}} + + {{column.title}}
    {{row[column.field]}}
    +
    + +
    diff --git a/src/table/demo/src/app/table/table-storage-filter.html b/src/table/demo/src/app/table/table-storage-filter.html new file mode 100644 index 0000000..4e349e1 --- /dev/null +++ b/src/table/demo/src/app/table/table-storage-filter.html @@ -0,0 +1,49 @@ +

    描述

    +

    表格记忆--过滤搜索

    +

    示例

    + +

    + first name: + + age : +

    +
    +

    searchWords: {{searchWords|json}}

    +

    searchKeys: {{searchKeys|json}}

    +
    + + + + + + + + + + + + + + + + + + + +
    + {{column.title}} + + {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    +
    diff --git a/src/table/demo/src/app/table/table-storage-serve.html b/src/table/demo/src/app/table/table-storage-serve.html new file mode 100644 index 0000000..d2d33a5 --- /dev/null +++ b/src/table/demo/src/app/table/table-storage-serve.html @@ -0,0 +1,34 @@ +

    描述

    +

    表格记忆--后台分页

    +

    示例

    + +

    事件日志:

    + +
    + + + + + + + + + + + + + + + + + +
    {{column.title}}
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    + +
    diff --git a/src/table/demo/src/app/table/table-storage.html b/src/table/demo/src/app/table/table-storage.html new file mode 100644 index 0000000..29bf157 --- /dev/null +++ b/src/table/demo/src/app/table/table-storage.html @@ -0,0 +1,29 @@ + +
    + + + + + + + + + + + + + + + + + + + + +
    + {{column.title}} + + {{column.title}}
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    + +
    diff --git a/src/table/demo/src/app/table/table-tree-mulitiselect.html b/src/table/demo/src/app/table/table-tree-mulitiselect.html new file mode 100644 index 0000000..55dddd2 --- /dev/null +++ b/src/table/demo/src/app/table/table-tree-mulitiselect.html @@ -0,0 +1,76 @@ +

    描述

    +

    开发者可结合数据利用表格dom结构和ticheckgroup组件来实现的复选树表,具体可参考tiCheckgroup组件的使用说明。

    +

    不建议分页,父子关系会有影响

    +

    注意:使用 TiIcon 组件,请导入 TiIconModule;使用 tiCheckgroup 组件,请导入 TiCheckboxModule

    +

    示例

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + {{column.title}}
    + + + + + + {{you.name}} + {{you.size}}{{you.type}}
    + + + + + + {{son.name}} + {{son.size}}{{son.type}}
    + + + {{grandson.name}} + {{grandson.size}}{{grandson.type}}
    +
    diff --git a/src/table/demo/src/app/table/table-tree-unknowdeepth.html b/src/table/demo/src/app/table/table-tree-unknowdeepth.html new file mode 100644 index 0000000..b47128c --- /dev/null +++ b/src/table/demo/src/app/table/table-tree-unknowdeepth.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + +
    {{column.title}}
    + + + + {{node.name}} + {{node.size}}{{node.type}}
    +
    diff --git a/src/table/demo/src/app/table/table-tree.html b/src/table/demo/src/app/table/table-tree.html new file mode 100644 index 0000000..9b69297 --- /dev/null +++ b/src/table/demo/src/app/table/table-tree.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{column.title}}
    + + + + {{you.name}} + {{you.size}}{{you.type}}
    + + + + {{son.name}} + {{son.size}}{{son.type}}
    + {{grandson.name}} + {{grandson.size}}{{grandson.type}}
    +
    diff --git a/src/table/demo/src/app/table/table-virtualscroll-basic.html b/src/table/demo/src/app/table/table-virtualscroll-basic.html new file mode 100644 index 0000000..0e5ae0a --- /dev/null +++ b/src/table/demo/src/app/table/table-virtualscroll-basic.html @@ -0,0 +1,65 @@ +
    +

    描述

    +

    表格虚拟滚动。

    +

    虚拟滚动是和表头锁定功能搭配使用的。

    +

    虚拟滚动不支持详情展开、数据分组。

    +

    + 表格表头锁定功能:使用双表来实现的,样式类ti3-table-fixed-head包裹的是表头(第一个表格),样式类ti3-table-container包裹的是表体(第二个表格)。 +

    +

    + 表格虚拟滚动功能:在有样式类 ti3-table-container 的元素(第二个表格的父容器)上使用 itemSize + 接口设置每条数据占据的高度,并且给其设置最大高度或高度,然后在 div.ti3-table-container > table > tbody > tr 上用 cdkVirtualFor + 替换原有的 ngFor。 +

    +

    导入

    +

    import {{ '{' }} TiTableModule, TiOverflowModule {{ '}' }} from '@opentiny/ng';

    +

    使用虚拟滚动需要引入 ScrollingModule 模块, 前提是安装了 @angular/cdk 三方库。

    +

    import {{ '{' }} ScrollingModule {{ '}' }} from '@angular/cdk/scrolling';

    +

    示例

    + + +
    + + + + +
    + + + + + + + +
    {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-virtualscroll-comprehensive.html b/src/table/demo/src/app/table/table-virtualscroll-comprehensive.html new file mode 100644 index 0000000..ff72808 --- /dev/null +++ b/src/table/demo/src/app/table/table-virtualscroll-comprehensive.html @@ -0,0 +1,106 @@ +
    +

    描述

    +

    虚拟滚动是和表头锁定功能搭配使用的。

    +

    虚拟滚动不支持详情展开、数据分组。

    +

    虚拟滚动综合示例,虚拟滚动 + 表头锁定特性 + 列动态显示/隐藏 + 列固定 + 列拖动特性 + 分页特性 + 排序特性 + 表头搜索特性

    +

    + 表格虚拟滚动功能:在有样式类 ti3-table-container 的元素(第二个表格的父容器)上使用 itemSize + 接口设置每条数据占据的高度,并且给其设置最大高度或高度,然后在 div.ti3-table-container > table > tbody > tr 上用 cdkVirtualFor + 替换原有的 ngFor。 +

    +

    导入

    +

    import {{ '{' }} TiTableModule, TiOverflowModule, TiPaginationModule {{ '}' }} from '@opentiny/ng';

    +

    使用虚拟滚动需要引入 ScrollingModule 模块, 前提是安装了 @angular/cdk 三方库。

    +

    import {{ '{' }} ScrollingModule {{ '}' }} from '@angular/cdk/scrolling;

    +

    示例

    + + + +
    + + + +
    + + +
    + + + + + + + + + + + + + +
    + {{column.title}} + + + + {{column.title}} + + + {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + {{row.id + '-' + row.firstName}} + {{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}{{row.address}}{{row.phone}}{{row.parents}}{{row.id + '-' + row.school}}
    +
    + + +
    +
    diff --git a/src/table/demo/src/app/table/table-virtualscroll-sizes.html b/src/table/demo/src/app/table/table-virtualscroll-sizes.html new file mode 100644 index 0000000..b7a0203 --- /dev/null +++ b/src/table/demo/src/app/table/table-virtualscroll-sizes.html @@ -0,0 +1,204 @@ +
    +

    描述

    +

    虚拟滚动是和表头锁定功能搭配使用的。

    +

    虚拟滚动不支持详情展开、数据分组。

    +

    + 表格表头锁定功能:使用双表来实现的,样式类ti3-table-fixed-head包裹的是表头(第一个表格),样式类ti3-table-container包裹的是表体(第二个表格)。 +

    +

    + 表格虚拟滚动功能:在有样式类 ti3-table-container 的元素(第二个表格的父容器)上使用 itemSize + 接口设置每条数据占据的高度,并且给其设置最大高度或高度,然后在 div.ti3-table-container > table > tbody > tr 上用 cdkVirtualFor + 替换原有的 ngFor。 +

    +

    在使用虚拟滚动时可根据具体表格数据每行的高度来设置 itemSize 接口,详见下面个示例。

    +

    导入

    +

    import {{ '{' }} TiTableModule, TiOverflowModule {{ '}' }} from '@opentiny/ng';

    +

    使用虚拟滚动需要引入 ScrollingModule 模块, 前提是安装了 @angular/cdk 三方库。

    +

    import {{ '{' }} ScrollingModule {{ '}' }} from '@angular/cdk/scrolling;

    +

    示例

    + + +
    + +

    1.表格-单行:

    +
    + + + +
    + + + + + + + +
    {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +


    + +

    2.表格-多行:

    +
    + + + +
    + + + + + + + +
    {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    {{row.firstName}}
    +
    {{row.id + row.lastName}}
    +
    {{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +


    + +

    3.小表格-单行:

    +
    + + + +
    + + + + + + + +
    {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + +
    {{row.firstName}}{{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +


    + +

    4.小表格-多行:

    +
    + + + +
    + + + + + + + +
    {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    {{row.firstName}}
    +
    {{row.id + row.lastName}}
    +
    {{row.lastName}}{{row.age}}{{row.balance}}{{row.email}}
    {{noDadaInfo}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-virtualscroll-tree.html b/src/table/demo/src/app/table/table-virtualscroll-tree.html new file mode 100644 index 0000000..ec9c0bd --- /dev/null +++ b/src/table/demo/src/app/table/table-virtualscroll-tree.html @@ -0,0 +1,67 @@ +
    +

    描述

    +

    表格虚拟滚动

    +

    虚拟滚动是和表头锁定功能搭配使用的。

    +

    虚拟滚动不支持详情展开、数据分组。

    +

    + 表格表头锁定功能:使用双表来实现的,样式类ti3-table-fixed-head包裹的是表头(第一个表格),样式类ti3-table-container包裹的是表体(第二个表格)。 +

    +

    + 表格虚拟滚动功能:在有样式类 ti3-table-container 的元素(第二个表格的父容器)上使用 itemSize + 接口设置每条数据占据的高度,并且给其设置最大高度或高度,然后在 div.ti3-table-container > table > tbody > tr 上用 cdkVirtualFor + 替换原有的 ngFor。 +

    +

    导入

    +

    import {{ '{' }} TiTableModule {{ '}' }} from '@opentiny/ng';

    +

    使用虚拟滚动需要引入 ScrollingModule 模块, 前提是安装了 @angular/cdk 三方库。

    +

    import {{ '{' }} ScrollingModule {{ '}' }} from '@angular/cdk/scrolling';

    +

    示例

    + + + + + + +
    + + + + + + + +
    {{column.title}}
    +
    + + + + +
    + + + + + + + + + + + + + + + + + + + +
    + + + + {{node.name}} + {{node.size}}{{node.type}}
    {{noDadaInfo}}
    +
    +
    +
    diff --git a/src/table/demo/src/app/table/table-virtualscroll.html b/src/table/demo/src/app/table/table-virtualscroll.html new file mode 100644 index 0000000..18504cb --- /dev/null +++ b/src/table/demo/src/app/table/table-virtualscroll.html @@ -0,0 +1,29 @@ + +
    + + + + + + +
    {{column.title}}
    +
    +
    + + + + + + + + + + + + + + + +
    {{row.id}}{{row.firstName}}{{row.lastName}}{{row.age}}{{row.email}}
    +
    +
    diff --git a/src/table/demo/src/app/table/tableTest.less b/src/table/demo/src/app/table/tableTest.less new file mode 100644 index 0000000..09b9530 --- /dev/null +++ b/src/table/demo/src/app/table/tableTest.less @@ -0,0 +1,19 @@ +.containerStyle { + margin-left: auto; + margin-right: auto; + margin-top: 20px; +} +.containerTipStyle { + width: 35%; + margin-left: auto; + margin-right: auto; + margin-top: 30px; +} +.x { + max-width: calc(100% - 8px - 15px); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + display: inline-block; + vertical-align: top; +} diff --git a/src/table/demo/src/app/table/webdoc/table-demos.js b/src/table/demo/src/app/table/webdoc/table-demos.js new file mode 100644 index 0000000..163c01b --- /dev/null +++ b/src/table/demo/src/app/table/webdoc/table-demos.js @@ -0,0 +1,504 @@ +export default { + column: '1', + demos: [ + { + demoId: 'table-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': '

    Table 组件的最简用法。

    ', + 'en-US': '' + }, + apis: ['TiTableComponent.properties.srcData', 'TiTableComponent.properties.displayedData'] + }, + { + demoId: 'table-nodata', + name: { + 'zh-CN': '空表格', + 'en-US': 'nodata' + }, + desc: { + 'zh-CN': + '

    通过给tr标签上添加ti3-table-nodata样式类,在td标签上使用tiColspan属性指令配置表格数据为空时的状态。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-small', + name: { + 'zh-CN': '紧凑型', + 'en-US': 'samll' + }, + desc: { + 'zh-CN': '

    通过给ti-table标签上添加ti3-table-small样式类配置为紧凑型表格。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-cell-tip', + name: { + 'zh-CN': '带提示的省略', + 'en-US': 'cell ellipsis' + }, + desc: { + 'zh-CN': `

    通过使用tiOverflow属性指令使文本内容根据单元格宽度自动省略显示,并在鼠标悬浮时弹出完整内容提示,具体可查看 Overflow 属性指令示例及 + API。
    需导入:import { TiOverflowModule } from '@opentiny/ng';

    `, + 'en-US': '' + } + }, + { + demoId: 'table-pagination', + name: { + 'zh-CN': '分页', + 'en-US': 'pagination' + }, + desc: { + 'zh-CN': `

    通过ti-pagination组件实现分页功能,具体可查看 Pagination 组件示例及 API。
    需导入: + import { TiPaginationModule } from '@opentiny/ng';

    `, + 'en-US': '' + } + }, + { + demoId: 'table-sort', + name: { + 'zh-CN': '排序', + 'en-US': 'sort' + }, + desc: { + 'zh-CN': `

    通过ti-head-sort组件(可点击的排序图标)和ti-cell-text组件(表头单元格文本)实现对某一列数据的排序功能。通过ti-head-sort组件的 + 属性sortKey配置该列排序时依据的数据属性,属性asc配置初始默认是否为升序,属性compareFn配置自定义排序规则函数。

    `, + 'en-US': '' + }, + apis: ['TiHeadSortComponent.properties.sortKey', 'TiHeadSortComponent.properties.asc', 'TiHeadSortComponent.properties.compareFn'] + }, + { + demoId: 'table-sort-reset', + name: { + 'zh-CN': '可控的排序', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    通过ti-table组件实例上getDataState方法的返回值对象中的sort属性控制当前的排序状态,其中sort.sortKey配置当前排序 + 的列,sort.asc配置是否为升序。

    `, + 'en-US': '' + }, + apis: ['TiHeadSortComponent.properties.sortKey', 'TiHeadSortComponent.properties.asc', 'TiHeadSortComponent.properties.compareFn'] + }, + { + demoId: 'table-search', + name: { + 'zh-CN': '搜索', + 'en-US': 'search' + }, + desc: { + 'zh-CN': `

    通过属性searchWords配置需被检索的字符串的集合;通过属性searchKeys配置搜索的字段范围;通过属性searchStrictKeys配置其 + 中哪些字段是精确匹配(等于)的。

    `, + 'en-US': '' + }, + apis: [ + 'TiTableComponent.properties.searchWords', + 'TiTableComponent.properties.searchKeys', + 'TiTableComponent.properties.searchStrictKeys' + ] + }, + { + demoId: 'table-server-pagi-search-sort', + name: { + 'zh-CN': '远程加载数据', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    使用Promise简单模拟从服务端请求异步数据,具有分页、排序和搜索功能以及数据加载 loading 效果,开发者可接入实际从服务端获取数据的方式。设置属性 + srcData.state.paginated为 true 来指定由服务端分页;设置属性srcData.state.sorted为 true 来指定由服务端排序;设置属性 + srcData.state.searched为 true 来指定由服务端搜索。分页、排序和搜索状态改变时会触发stateUpdate回调,在该时机通过其参数的 + getDataState方法获取服务端需要的参数后,向服务端发送请求获取数据。
    使用分页功能需导入:import { TiPaginationModule } from '@opentiny/ng'; +

    `, + 'en-US': '' + }, + apis: ['TiTableComponent.properties.srcData', 'TiTableComponent.events.stateUpdate', 'TiTableComponent.methods.getDataState'] + }, + { + demoId: 'table-head-filter', + name: { + 'zh-CN': '筛选-单选/多选菜单', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    通过ti-head-filter组件(筛选的漏斗图标)和ti-cell-text组件(表头单元格文本)来实现表头筛选功能。ti-head-filter组件提供筛选 + 条件的选择能力和筛选的时机,筛选的数据处理逻辑需自行控制。

    `, + 'en-US': '' + }, + apis: [ + 'TiHeadFilterComponent.properties.multiple', + 'TiHeadFilterComponent.properties.selectAll', + 'TiHeadFilterComponent.properties.labelKey', + 'TiHeadFilterComponent.properties.panelWidth', + 'TiHeadFilterComponent.properties.options', + 'TiHeadFilterComponent.properties.searchable', + 'TiHeadFilterComponent.properties.panelAlign', + 'TiHeadFilterComponent.properties.virtual', + 'TiHeadFilterComponent.events.select', + 'TiHeadFilterComponent.slots.itemTemplate' + ] + }, + { + demoId: 'table-head-filter-datetime', + name: { + 'zh-CN': '筛选-时间日期菜单', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    ti-head-filter组件提供筛选条件的选择能力和筛选的时机,筛选的数据处理逻辑需自行控制。通过属性isDatetime配置为时间日期菜单;通过属性 + datetimeConfig设置时间日期菜单中的配置信息。

    `, + 'en-US': '' + }, + apis: [ + 'TiHeadFilterComponent.properties.isDatetime', + 'TiHeadFilterComponent.properties.datetimeConfig', + 'TiHeadFilterComponent.properties.panelAlign', + 'TiHeadFilterComponent.events.select' + ] + }, + { + demoId: 'table-radio', + name: { + 'zh-CN': '单行选择', + 'en-US': 'radio' + }, + desc: { + 'zh-CN': `

    通过tiRadio实现单行选择的功能,具体可查看 Radio 组件示例及 API。另外,需要在选择列的thtd + 上添加radio-column属性,在tr上使用ti3-selected-tr样式类,选择列的td上使用ti3-disabled-cell样式类。 +
    需导入:import { TiRadioModule } from '@opentiny/ng';

    `, + 'en-US': '' + } + }, + { + demoId: 'table-checkbox', + name: { + 'zh-CN': '多行选择', + 'en-US': 'checkbox' + }, + desc: { + 'zh-CN': `

    通过tiCheckgroup和tiCheckitem实现多行选择的功能,具体可查看 Checkgroup 组件示例及 API。另外,需要在选择列的 + thtd上添加checkbox-column属性,在tr上使用ti3-selected-tr样式类,选择列的td上使用 + ti3-disabled-cell样式类。
    需导入:import { TiCheckboxModule } from '@opentiny/ng';

    `, + 'en-US': '' + } + }, + { + demoId: 'table-checkbox-pagination-headmenu', + name: { + 'zh-CN': '扩展多行选择全选菜单', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    表头的全选复选框默认选中/清空当前页数据,可通过ti-head-menu组件扩展多行选择全选菜单,添加选择所有数据和清空所有数据的菜单项。ti-head-menu + 组件默认有 '选择所有' 和 '清空所有' 这两个菜单项,可以通过属性options自定义菜单项。
    使用分页功能需导入:import { TiPaginationModule } from '@opentiny/ng'; +
    使用多行选择功能需导入:import { TiCheckboxModule } from '@opentiny/ng';

    `, + 'en-US': '' + }, + apis: ['TiHeadMenuComponent.properties.options', 'TiHeadMenuComponent.events.select'] + }, + { + demoId: 'table-details', + name: { + 'zh-CN': '可展开', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    通过ti-details-icon组件、tiDetailsTr结构指令和tiColspan属性指令实现展开功能。另外,需要在展开图标列的thtd上 + 添加details-icon-column属性,在展开的行里的元素使用ti3-table-detail-container样式类。

    `, + 'en-US': '' + }, + apis: [ + 'TiDetailsIconComponent.properties.row', + 'TiDetailsIconComponent.properties.index', + 'TiDetailsTrDirective.properties.tiDetailsTr' + ] + }, + { + demoId: 'table-dynamic-details', + name: { + 'zh-CN': '可控的展开', + 'en-US': '' + }, + desc: { + 'zh-CN': + '

    当展开或收起时会触发 ti-details-icon 组件的beforeToggle事件,开发者可在该时机使用行数据的showDetails属性来控制是否展开及展开的时间点。

    ', + 'en-US': '' + }, + apis: [ + 'TiDetailsIconComponent.properties.row', + 'TiDetailsIconComponent.properties.index', + 'TiDetailsTrDirective.properties.tiDetailsTr', + 'TiDetailsIconComponent.events.beforeToggle' + ] + }, + { + demoId: 'table-details-nesttable', + name: { + 'zh-CN': '嵌套子表格', + 'en-US': '' + }, + desc: { + 'zh-CN': + '

    在展开的行里嵌入表格,注意嵌入的表格组件需自己定义,可以给嵌入的表格组件里的ti-table标签上添加ti3-table-nest样式类。

    ', + 'en-US': '' + }, + apis: [ + 'TiTableComponent.properties.closeOtherDetails', + 'TiDetailsIconComponent.properties.row', + 'TiDetailsIconComponent.properties.index', + 'TiDetailsTrDirective.properties.tiDetailsTr' + ] + }, + { + demoId: 'table-cols-toggle', + name: { + 'zh-CN': '列设置', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    通过ti-cols-toggle组件和ti-table组件上的属性columns实现列设置功能。通过属性columns配置所有列数据, + 在thtd上利用每列数据中的show属性值设置ngIf来控制列的显示/隐藏,ti-cols-toggle组件需 + 要放在ti-table元素内部,具体呈现位置布局由开发者自行控制。

    `, + 'en-US': '' + }, + apis: [ + 'TiTableComponent.properties.columns', + 'TiColsToggleComponent.properties.searchable', + 'TiColsToggleComponent.properties.selectAll' + ] + }, + { + demoId: 'table-cols-resizable', + name: { + 'zh-CN': '拖拽调整列宽', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    通过在ti-table组件上使用tiColsResizable属性指令和属性columns实现拖拽调整列宽的功能。另外,table元素需要一个 + 带有ti3-resize-wrapper样式类的父容器。如果要使某一列不能拖拽调整列宽,可以给该列表头的th标签上添加not-resizable属性。

    `, + 'en-US': '' + } + }, + { + demoId: 'table-fixed-head', + name: { + 'zh-CN': '固定表头', + 'en-US': '' + }, + desc: { + 'zh-CN': + '

    通过双表实现固定表头功能,实际显示的表头(第一个表格)在带有样式类ti3-table-fixed-head的容器里,表体(第二个表格)在带有样式类ti3-table-container的容器里。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-column-fixed', + name: { + 'zh-CN': '固定列', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    通过在th标签上使用tiColumnFixed属性指令实现固定前后列的功能。通过属性tiColumnFixed配置是固定的左右位置。另外,table + 元素需要一个带有ti3-table-container的父容器。

    `, + 'en-US': '' + }, + apis: ['TiColumnFixedDirective.properties.tiColumnFixed'] + }, + { + demoId: 'table-columnfixed-headfixed', + name: { + 'zh-CN': '固定头和列', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    同时固定头和列,适合同时有大量列和数据的展示。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-col-align', + name: { + 'zh-CN': '对齐方式', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    单元格内容默认左对齐,可直接在单元格中使用text-align样式设置居中或右对齐。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-rowspan', + name: { + 'zh-CN': '行/列合并', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    W3C标准table一样,使用colspanrowspan合并行/列。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-virtualscroll', + name: { + 'zh-CN': '虚拟滚动', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    结合@angular/cdk/scrolling实现虚拟滚动。虚拟滚动是和固定表头功能搭配使用的,在有ti3-table-container样式类的元素标签(第二个表格的父容器) + 上使用属性itemSize设置每行数据占据的高度,并且给其设置最大高度或高度,另外在div.ti3-table-container > table > tbody > tr上用 + cdkVirtualFor结构指令替换原有的ngFor
    需导入:import { ScrollingModule } from '@angular/cdk/scrolling';

    `, + 'en-US': '' + } + }, + { + demoId: 'table-actionmenu', + name: { + 'zh-CN': '操作列', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    通过ti-actionmenu组件实现最后一列操作列,具体可查看 Actionmenu 组件示例及 API。
    需导入: + import { TiActionmenuModule } from '@opentiny/ng';

    `, + 'en-US': '' + } + }, + { + demoId: 'table-editrow', + name: { + 'zh-CN': '可编辑行', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    自由编辑行内容,开发者可以参照该示例根据自己需求自由定制表格的行编辑功能。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-editall', + name: { + 'zh-CN': '可编辑表格', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    自由编辑表格全部内容,开发者可以参照该示例根据自己需求自由定制表格的编辑功能。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-row-drag2', + name: { + 'zh-CN': '行拖拽排序', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    使用H5的拖拽能力。在dragstart事件中获取当前拖拽元素起始下标,dragover事件中获取拖拽终止下标,drop + 事件中根据拖拽起始和终止下标操作移动数据。

    `, + 'en-US': '' + } + }, + { + demoId: 'table-tree', + name: { + 'zh-CN': '深度确定的树形数据展示', + 'en-US': '' + }, + desc: { + 'zh-CN': '

    深度确定的树形数据可利用树形结构的HTML展示,开发者可以参照该示例根据自己需求自由展示树形数据。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-tree-unknowdeepth', + name: { + 'zh-CN': '深度不确定的树形数据展示', + 'en-US': '' + }, + desc: { + 'zh-CN': + '

    深度不确定的树形数据,将其转换成扁平化数据之后再展示,本示例中提供了树形结构与扁平化数据之间的转换函数,实际业务中请根据需求修改。

    ', + 'en-US': '' + } + }, + { + demoId: 'table-storage', + name: { + 'zh-CN': '排序、分页和列宽状态缓存', + 'en-US': '' + }, + desc: { + 'zh-CN': `

    排序和分页的当前页码是SessionStorage缓存,分页每页显示几条和拖拽调整后的列宽是LocalStorage缓存。通过属性storageId配置 + 缓存表格状态的唯一标志值,一旦配置了storageId,那么表格各状态就会缓存,如果想使某一状态不缓存,可使用属性storageConfig进行配置。 +
    使用分页功能需导入:import { TiPaginationModule } from '@opentiny/ng';

    `, + 'en-US': '' + }, + apis: ['TiTableComponent.properties.storageId', 'TiTableComponent.properties.storageConfig'] + } + ], + ignoreApis: [ + 'TiHeadFilterComponent.properties.appendToBody', + 'TiHeadFilterComponent.properties.clearable', + 'TiHeadFilterComponent.properties.maxLine', + 'TiHeadFilterComponent.properties.placeholder', + 'TiHeadFilterComponent.properties.reserveSearchword', + 'TiHeadFilterComponent.properties.searchKeys', + 'TiHeadFilterComponent.properties.selectedNumberTipPosition', + 'TiHeadFilterComponent.properties.selectedTipPosition', + 'TiHeadFilterComponent.properties.showSelectedNumber', + 'TiHeadFilterComponent.properties.showSelectedNumberTip', + 'TiHeadFilterComponent.properties.tipMaxWidth', + 'TiHeadFilterComponent.properties.tipPosition', + 'TiHeadFilterComponent.properties.disabled', + 'TiHeadFilterComponent.properties.tabindex', + 'TiHeadFilterComponent.events.beforeOpen', + 'TiHeadFilterComponent.events.beforeSearch', + 'TiHeadFilterComponent.events.clear', + 'TiHeadFilterComponent.events.scrollToBottom', + 'TiHeadFilterComponent.events.blur', + 'TiHeadFilterComponent.events.focus', + 'TiHeadFilterComponent.slots.footerTemplate', + 'TiHeadFilterComponent.slots.placeholderTemplate', + 'TiHeadFilterComponent.slots.selectedTemplate', + 'TiHeadFilterComponent.methods.getSearchResult', + 'TiHeadFilterComponent.methods.getSearchWord', + 'TiHeadFilterComponent.methods.open', + 'TiHeadFilterComponent.methods.blur', + 'TiHeadFilterComponent.methods.focus', + 'TiColsToggleComponent.properties.appendToBody', + 'TiColsToggleComponent.properties.clearable', + 'TiColsToggleComponent.properties.labelKey', + 'TiColsToggleComponent.properties.maxLine', + 'TiColsToggleComponent.properties.multiple', + 'TiColsToggleComponent.properties.options', + 'TiColsToggleComponent.properties.panelMaxHeight', + 'TiColsToggleComponent.properties.placeholder', + 'TiColsToggleComponent.properties.reserveSearchword', + 'TiColsToggleComponent.properties.searchKeys', + 'TiColsToggleComponent.properties.selectedNumberTipPosition', + 'TiColsToggleComponent.properties.selectedTipPosition', + 'TiColsToggleComponent.properties.showSelectedNumber', + 'TiColsToggleComponent.properties.showSelectedNumberTip', + 'TiColsToggleComponent.properties.tipMaxWidth', + 'TiColsToggleComponent.properties.virtual', + 'TiColsToggleComponent.properties.valueKey', + 'TiColsToggleComponent.properties.tabindex', + 'TiColsToggleComponent.properties.idKey', + 'TiColsToggleComponent.events.beforeOpen', + 'TiColsToggleComponent.events.beforeSearch', + 'TiColsToggleComponent.events.clear', + 'TiColsToggleComponent.events.scrollToBottom', + 'TiColsToggleComponent.slots.footerTemplate', + 'TiColsToggleComponent.slots.placeholderTemplate', + 'TiColsToggleComponent.slots.selectedTemplate', + 'TiColsToggleComponent.slots.itemTemplate', + 'TiColsToggleComponent.methods.getSearchResult', + 'TiColsToggleComponent.methods.getSearchWord', + 'TiColsToggleComponent.methods.open', + 'TiColsToggleComponent.methods.blur', + 'TiColsToggleComponent.methods.focus' + ] +}; diff --git a/src/table/demo/src/app/table/webdoc/table.cn.md b/src/table/demo/src/app/table/webdoc/table.cn.md new file mode 100644 index 0000000..3d7927c --- /dev/null +++ b/src/table/demo/src/app/table/webdoc/table.cn.md @@ -0,0 +1,44 @@ +--- +title: Table 表格 +--- +# Table 表格 + +
    + +展示行列数据。   + ++ 当有大量结构化的数据需要展现时; + ++ 当需要对数据进行排序、过滤、分页、自定义操作等复杂行为时。 + +```typescript +import { TiTableModule } from '@opentiny/ng'; +``` + +如需使用单元格带提示的省略功能,请导入: + +```typescript +import { TiOverflowModule } from '@opentiny/ng'; +``` + +
    + +
    + +展示行列数据。   + ++ 当有大量结构化的数据需要展现时; + ++ 当需要对数据进行排序、过滤、分页、自定义操作等复杂行为时。 + +```typescript +import { TiTableModule } from '@opentiny/ng'; +``` + +如需使用单元格带提示的省略功能,请导入: + +```typescript +import { TiOverflowModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/table/demo/src/app/table/webdoc/table.en.md b/src/table/demo/src/app/table/webdoc/table.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/table/demo/src/app/table/webdoc/table.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/table/demo/src/favicon.ico b/src/table/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/table/demo/src/index.html b/src/table/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/table/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/table/demo/src/main.ts b/src/table/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/table/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/table/demo/test.ts b/src/table/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/table/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/table/demo/tsconfig.app.json b/src/table/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/table/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/table/demo/tsconfig.spec.json b/src/table/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/table/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/table/lib/index.ts b/src/table/lib/index.ts new file mode 100644 index 0000000..ee258f5 --- /dev/null +++ b/src/table/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTableModule'; diff --git a/src/table/lib/ng-package.json b/src/table/lib/ng-package.json new file mode 100644 index 0000000..973474d --- /dev/null +++ b/src/table/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/table", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/table/lib/package.json b/src/table/lib/package.json new file mode 100644 index 0000000..007c783 --- /dev/null +++ b/src/table/lib/package.json @@ -0,0 +1,28 @@ +{ + "name": "@opentiny/ng-table", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-select": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-dropsearch": "~1.0.0-beta.0", + "@opentiny/ng-list": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-datebase": "~1.0.0-beta.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@angular/cdk/scrolling": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-date": "~1.0.0-beta.0", + "@opentiny/ng-datetime": "~1.0.0-beta.0", + "@opentiny/ng-searchbox": "~1.0.0-beta.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/table/lib/project.json b/src/table/lib/project.json new file mode 100644 index 0000000..76cc3d8 --- /dev/null +++ b/src/table/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/table/lib", + "sourceRoot": "src/table/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/table"], + "options": { + "project": "src/table/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/table"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js table" + }, + { + "command": "ng default-build table" + }, + { + "command": "node build/clear-default-theme.js table" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/table && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build table && ng pack table && node build/publish.js table --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/table/lib/src/TiCellIconsComponent.ts b/src/table/lib/src/TiCellIconsComponent.ts new file mode 100644 index 0000000..0800c61 --- /dev/null +++ b/src/table/lib/src/TiCellIconsComponent.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +/** + * TiCellIcons 表格单元格中放置图标的容器组件 + * + * 配合TiCellText组件使用可使表格单元格中内容显示不下时,文本出省略号..., + * 而文本后的放在TiCellIcons图标始终可以显示出来 + * + */ +@Component({ + selector: 'ti-cell-icons', + templateUrl: './cell-icons.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-cell-icons-container]': 'true' + } +}) +export class TiCellIconsComponent {} diff --git a/src/table/lib/src/TiCellTextComponent.ts b/src/table/lib/src/TiCellTextComponent.ts new file mode 100644 index 0000000..db44514 --- /dev/null +++ b/src/table/lib/src/TiCellTextComponent.ts @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterViewInit, Component, ElementRef, Input, OnInit, Renderer2, ViewChild, ChangeDetectionStrategy } from '@angular/core'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiPositionType } from '@opentiny/ng-utils'; + +/** + * TiCellText 表格单元格中放置文本的容器组件 + * + * 提供文本溢出时出省略号...,且hover时出tip的功能。 + * + * 配合TiCellText组件使用可使表格单元格中内容显示不下时,文本出省略号..., + * 而文本后的放在TiCellIcons图标始终可以显示出来 + * + */ +@Component({ + selector: 'ti-cell-text', + templateUrl: './cell-text.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-cell-text-container]': 'true' + } +}) +export class TiCellTextComponent implements AfterViewInit { + /** + * 文本超长显示时 tip 内容 + */ + @Input() tipContent: string; + /** + * 文本超长显示时 tip 位置 + */ + @Input() tipPosition: TiPositionType; + @ViewChild('text', { static: true }) private cellTextEle: ElementRef; + constructor(private renderer: Renderer2, private elementRef: ElementRef, private tiRenderer: TiRenderer) {} + + ngAfterViewInit(): void { + // 保证有图标时,图标渲染完再计算宽度:可看该测试用例table-cellicons-colsresizable + setTimeout(() => { + this.setMaxWidth(); + }, 0); + } + + private setMaxWidth(): void { + let otherWidth: number = 0; + const parent: Element = this.renderer.parentNode(this.elementRef.nativeElement); + const iconsContainer: Element = this.tiRenderer.findChildrenByClassName(parent, 'ti3-cell-icons-container')[0]; + const sortContainer: Element = this.tiRenderer.findChildrenByClassName(parent, 'ti3-sort-container')[0]; + const headFilterContainer: Element = this.tiRenderer.findChildrenByClassName(parent, 'ti3-head-filter-container')[0]; + const renameContainer: Element = this.tiRenderer.findChildrenByTagName(parent, 'TP-RENAME')[0]; + + // 修复SSR报错:ERROR TypeError: sortContainer.getBoundingClientRect is not a function + if (typeof parent.getBoundingClientRect !== 'function') { + return; + } + // 修复SSR报错:ERROR TypeError: Cannot read property 'getBoundingClientRect' of undefined + if (iconsContainer) { + const iconsContainerWidth: number = parseFloat(iconsContainer.getBoundingClientRect().width.toFixed(1)); + otherWidth += iconsContainerWidth; + } + + if (sortContainer) { + const sortContainerWidth: number = parseFloat(sortContainer.getBoundingClientRect().width.toFixed(1)); + otherWidth += sortContainerWidth; + } + + if (headFilterContainer) { + const headFilterContainerWidth: number = parseFloat(headFilterContainer.getBoundingClientRect().width.toFixed(1)); + otherWidth += headFilterContainerWidth; + } + + if (renameContainer) { + const renameContainerWidth: number = parseFloat(renameContainer.getBoundingClientRect().width.toFixed(1)); + otherWidth += renameContainerWidth; + } + + this.renderer.setStyle(this.cellTextEle.nativeElement, 'max-width', `calc(100% - ${otherWidth}px)`); + } +} diff --git a/src/table/lib/src/TiColClickDirective.ts b/src/table/lib/src/TiColClickDirective.ts new file mode 100644 index 0000000..405a2f1 --- /dev/null +++ b/src/table/lib/src/TiColClickDirective.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterViewInit, Directive, ElementRef, Renderer2 } from '@angular/core'; +/** + * 根据规范:“单/复选框”关联表格使用时,为了提升选中效率,点击热区为“单/复选框”按钮所在单元格,点击即可选中。 + */ +@Directive({ + selector: 'td[checkbox-column], th[checkbox-column], td[radio-column]' +}) +export class TiColClickDirective implements AfterViewInit { + constructor(private renderer: Renderer2, private elementRef: ElementRef) {} + ngAfterViewInit(): void { + this.renderer.listen(this.elementRef.nativeElement, 'click', (event: MouseEvent): void => { + // 全选复选框右侧有下拉操作图标时,不能做热区放大 + if (this.elementRef.nativeElement.tagName === 'TH' && this.elementRef.nativeElement.querySelector('ti-head-menu')) { + return; + } + const inputEle: any = this.elementRef.nativeElement.querySelector('input'); + const checkboxLabelEle: any = this.elementRef.nativeElement.querySelector('.ti3-checkbox'); + const radioLabelEle: any = this.elementRef.nativeElement.querySelector('.ti3-radio'); + // 点击单/复选空白区域时,触发input点击事件 + // 表格和单/复选联用时,checkboxLabelEle和radioLabelEle不会同时出现,所以需要分别进行判断 + // inputEle和checkboxLabelEle/radioLabelEle是兄弟元素 + // event.target !== inputEle阻止在firefox下因为inputEle.click()产生的冒泡循环 + if ( + event.target !== inputEle && + ((checkboxLabelEle && !(event.target === checkboxLabelEle || checkboxLabelEle.contains(event.target))) || + (radioLabelEle && !(event.target === radioLabelEle || radioLabelEle.contains(event.target)))) + ) { + inputEle.click(); + } + }); + } +} diff --git a/src/table/lib/src/TiColsResizableDirective.ts b/src/table/lib/src/TiColsResizableDirective.ts new file mode 100644 index 0000000..f90f588 --- /dev/null +++ b/src/table/lib/src/TiColsResizableDirective.ts @@ -0,0 +1,604 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Directive, + DoCheck, + ElementRef, + IterableChanges, + IterableDiffer, + IterableDiffers, + NgZone, + OnDestroy, + OnInit, + Renderer2, + Inject, + Input +} from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Subscription } from 'rxjs'; + +import { TiTableColumns, TiTableComponent } from './TiTableComponent'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { Util } from '@opentiny/ng-utils'; + +import { TiTableFixedHeadService } from './TiTableFixedHeadService'; + +/** + * @ignore + */ +export interface ResizableOpts { + table: any; + ths: Array; + wrap: any; // 包裹表格的父容器,拖动表格超过父容器时出滚动条 + mouseXPosition: number; // 光标位置,列拖动时使用 + target: any; // 拖动的当前列 + storedSizes: Array; // 用来实时保存各列宽度 + storedSortEleSizes: Array; // 用来保存各列sort元素的宽度, + storeTableWidthChange: number; + secondTable?: any; + secondThs?: Array; + xScrollState?: boolean; + yScrollState?: boolean; + isYOverflowedWithX?: boolean; // 表头锁定时,列拖动过程中横向滚动条的出现导致纵向滚动条出现的标志位 +} +/** + * TiColsResizable 表格列拖动指令 + * + * 支持拖动表格列来调整各列宽度。 + * + * 在ti-table标签上加了tiColsResizable属性,则表格自动具有列拖动功能; + * 同时还需要对table元素包裹父容器div,并对父容器设置 ti3-resize-wrapper样式类。 + */ +@Directive({ + selector: 'ti-table[tiColsResizable]' +}) +export class TiColsResizableDirective implements OnInit, DoCheck, OnDestroy { + /** + * @ignore + */ + public static readonly UNSELECTABLE_CLASS: string = 'ti3-unselectable'; + /** + * @ignore + * 移到表头能够显示拖动线的感应范围 + */ + public static readonly COL_BORDER_RANGE: number = 10; + /** + * @ignore + */ + public readonly notResizableAttr: string = 'not-resizable'; + /** + * 是否开启列拖动功能 + */ + @Input('tiColsResizable') isColsResizable: boolean | ''; + /** + * @ignore + */ + public resizableOpts: ResizableOpts = { + table: null, + ths: null, + wrap: null, + mouseXPosition: 0, + target: null, + storedSizes: [], + storedSortEleSizes: [], + storeTableWidthChange: 0, + isYOverflowedWithX: false + }; // 初始化列拖动属性的配置项 + private hostEle: Element; + private documentMouseMoveListener: () => void; + private documentMouseUpListener: () => void; + private windowResizeListener: () => void; + private isDragStart: boolean = false; + private columsDiffer: IterableDiffer; + private subscription: Subscription; + private isColumnsInit: boolean = true; + /** + * @ignore + */ + public isfirstYScrollStateChange: boolean = true; + /** + * @ignore + */ + public tableCom: TiTableComponent; + constructor( + tableCom: TiTableComponent, + private renderer: Renderer2, + private elementRef: ElementRef, + private zone: NgZone, + private tiRenderer: TiRenderer, + private iterableDiffers: IterableDiffers, + private fixedHeadService: TiTableFixedHeadService, + @Inject(DOCUMENT) private document + ) { + this.hostEle = this.elementRef.nativeElement; + this.tableCom = tableCom; + } + + private static getThs(table: Element): Array { + return Array.from(table.children[0].children[0].children); + } + + private static getWidth(element: any): number { + return parseFloat(element.getBoundingClientRect().width.toFixed(1)); + } + + private static trackByFn(index: number, item: { show?: boolean }): boolean { + return item.show; + } + + ngOnInit(): void { + if (this.isColsResizable === false) { + return; + } + this.subscription = this.tableCom.viewInitSubject.subscribe(() => { + this.processColsResizable(); + this.isColumnsInit = false; // 主要处理 columns 初始为空,异步的场景 + // 表格记忆 + // 版本更新后用户第一次访问,localStorageState['colsWidth']不存在 + // 修复SSR错误:ERROR TypeError: Cannot read property 'colsWidth' of undefined + if ( + this.tableCom.storageId && + this.tableCom.storageConfig && + this.tableCom.storageConfig.colsWidth && + this.tableCom.localStorageState && + this.tableCom.localStorageState['colsWidth'] + ) { + this.initStorageColsWidth(); + } + }); + + this.columsDiffer = this.iterableDiffers.find(this.tableCom.columns).create<{ show?: boolean }>(TiColsResizableDirective.trackByFn); + } + + ngDoCheck(): void { + if (this.isColsResizable === false) { + return; + } + const columsDiffer: IterableChanges = this.columsDiffer.diff(this.tableCom.columns); + if (columsDiffer) { + if (this.isColumnsInit) { + this.isColumnsInit = false; + + return; + } + this.zone.runOutsideAngular(() => { + setTimeout(() => { + this.setDefaultWidth(); + }, 0); + }); + } + } + + ngOnDestroy(): void { + if (this.subscription) { + this.subscription.unsubscribe(); + } + if (this.documentMouseMoveListener) { + this.documentMouseMoveListener(); + } + if (this.documentMouseUpListener) { + this.documentMouseUpListener(); + } + if (this.windowResizeListener) { + this.windowResizeListener(); + } + } + + private processColsResizable(): void { + this.setTableWidthChange(); + const table: any = (this.resizableOpts.table = this.getTable()); + const thead: any = table.children[0]; + if (this.tableCom.isFixedHead) { + this.resizableOpts.secondTable = this.getSecondTable(); + this.resizableOpts.xScrollState = this.overflowedX(); + this.resizableOpts.yScrollState = this.overflowedY(); + this.resizableOpts.ths = TiColsResizableDirective.getThs(table); + this.resizableOpts.secondThs = TiColsResizableDirective.getThs(this.resizableOpts.secondTable); + this.tableCom.processYScrollStateChangeWithColsResizable = (): void => { + this.fixedHeadService.processYScrollStateChange(this); + }; + } + this.zone.runOutsideAngular(() => { + // 避免不停触发变化检测 + this.renderer.listen(thead, 'mousemove', (event: MouseEvent) => { + const currentTh: any = event.target; + + if (currentTh['nodeName'] !== 'TH') { + return; + } + + // 获取当前th距页面文档的left值 + const left: number = currentTh['getBoundingClientRect']().left + this.document.documentElement.scrollLeft; + const thWidth: number = currentTh['getBoundingClientRect']().width; + // 判断光标是否落在th的右边缘 + const isOnColBorder: boolean = Math.abs(event.pageX - Math.round(left + thWidth)) <= TiColsResizableDirective.COL_BORDER_RANGE; + + if (isOnColBorder) { + if (event.target['hasAttribute'](this.notResizableAttr) || this.isLastColumn(currentTh)) { + return; + } + this.createDividingLine(currentTh); + } else { + this.removeDividingLine(); + } + }); + this.renderer.listen(thead, 'mouseleave', () => { + this.removeDividingLine(); + }); + this.renderer.listen(thead, 'mousedown', (event: MouseEvent) => { + const currentTh: any = event.target; + if (this.tiRenderer.hasClass(currentTh, 'col-resize-active')) { + this.isDragStart = true; + // 鼠标点击的当前列 + this.resizableOpts.target = currentTh; + // 更新光标位置 + this.resizableOpts.mouseXPosition = event.pageX; + // 在拖动前获取当前表格最新的列信息、宽度,很关键,使得设置宽度与样式实际宽度一直 + this.updateTableInfo(); + // 给页面设置不可选样式,避免拖动时页面或表格内部出现被选中的蓝色区域 + this.toggleTextSelection(true); + } + + if (this.tableCom.isFixedHead) { + this.resizableOpts.xScrollState = this.overflowedX(); + this.resizableOpts.yScrollState = this.overflowedY(); + } + }); + + const tableContainer: any = this.tiRenderer.findChildrenByClassName(this.hostEle, 'ti3-resize-wrapper')[0]; + if (!this.tiRenderer.hasClass(tableContainer, 'ti3-table-container')) { + this.renderer.listen(tableContainer, 'scroll', () => { + Util.trigger(this.document, 'tiScroll'); + }); + } + + this.documentMouseMoveListener = this.renderer.listen(this.document, 'mousemove', (event: MouseEvent) => { + // 列拖动的动作应该是先mousedown,然后mousemove,因此先判断是否已经触发了mousedown + if (!this.isDragStart || this.resizableOpts.mouseXPosition === 0 || !this.resizableOpts.target) { + return; + } + + this.mouseMove(event); + }); + this.documentMouseUpListener = this.renderer.listen(this.document, 'mouseup', (event: MouseEvent) => { + if (!this.isDragStart) { + return; + } + + this.toggleTextSelection(false); // 恢复页面可选样式 + this.stopResize(); // 保存最新宽度到浏览器中 + this.isDragStart = false; + }); + + // 修复SSR报错:ERROR ReferenceError: window is not defined + if (typeof window === 'undefined') { + return; + } + this.windowResizeListener = this.renderer.listen(window, 'resize', () => { + this.setTableWidthChange(); + }); + }); + } + + private mouseMove(event: MouseEvent): void { + const options: ResizableOpts = this.resizableOpts; + const lastColIndex: number = options.ths.length - 1; + const curColIndex: number = parseInt(options.target.getAttribute('ti-visible-index'), 10); + const colWidth: number = options.storedSizes[curColIndex]; + const leftEdge: number = parseFloat((event.pageX - options.mouseXPosition).toFixed(1)); + const minWidth: number = this.getColMinWidth(options.target); + + // 当拖拽方向为列宽度减小的方向且列宽已达到文本区最小宽度时阻止拖动 + if (leftEdge >= 0 || colWidth + leftEdge > Math.ceil(minWidth)) { + // 更新拖动列宽度 + options.storedSizes[curColIndex] += leftEdge; + this.setWidth(options.target, options.storedSizes[curColIndex]); + + // 对最后一列的列宽的整体处理方案: + // 如果更新拖动列宽度后的表格的总宽度小于表格初始宽度,则将宽度差补偿到最后一列; + // 如果更新拖动列宽度后的表格的总宽度大于表格初始宽度,且之前最后一列列宽有补偿时, + // 将最后一列的补偿先抵消掉,然后有超出时再出滚动条 + + // s: storeTableWidthChange(此次拖动前各列拖动值的累计和) + // b: 此次拖动前给末列的补偿值 + // l: leftEdge(此次拖动的距离) + // s l s + l b 此次拖动后对末列宽的设置 + // -5 -1 -6 < 0 +5 +1(-leftEdge) + // -5 +3 -2 < 0 +5 -3(-leftEdge) + // +5 -8 -3 < 0 0 +3(-(storeColsWidthChange+leftEdge)) + // -5 +8 3 > 0 +5 -5(+storeColsWidthChange) + // +5 +3 8 > 0 0 +0(不做处理) + // +5 -3 2 > 0 0 +0(不做处理) + if (leftEdge + options.storeTableWidthChange < 0) { + if (options.storeTableWidthChange < 0) { + options.storedSizes[lastColIndex] -= leftEdge; + } else { + options.storedSizes[lastColIndex] -= leftEdge + options.storeTableWidthChange; + } + } else { + if (options.storeTableWidthChange < 0) { + options.storedSizes[lastColIndex] += options.storeTableWidthChange; + } + } + + this.setWidth(options.ths[lastColIndex], options.storedSizes[lastColIndex]); + + // 更新各列拖动相对值的累计和 + options.storeTableWidthChange += leftEdge; + + // 表头锁定 + if (this.tableCom.isFixedHead) { + // 更新第二个表格(bodyTable)中的当前列和最后一列的宽度 + this.setWidth(options.secondThs[curColIndex], options.storedSizes[curColIndex]); + this.setWidth(options.secondThs[options.secondThs.length - 1], options.storedSizes[lastColIndex]); + + this.fixedHeadService.handleYNotOverflowedWithX(this.resizableOpts, this.tableCom.tbodyContainer); + + // 当横向滚动条出现导致纵向滚动条出现时对账本做处理 + this.fixedHeadService.handleYOverflowedWithX(this.resizableOpts, this.tableCom.tbodyContainer); + + // 纵向滚动条状态变化时对固定的表头右边填充块的处理 + this.fixedHeadService.processOverflowY(this.tableCom.theadContainer, this.tableCom.tbodyContainer, this.tableCom); + } + + if (this.tableCom.fixedColumnInfo.hasFixedColumn) { + this.tableCom.thResizeSubject.next({ + th: options.target, + leftEdge, + resizableOpts: this.resizableOpts + }); + const container: any = this.tableCom.fixedColumnInfo.container; + if (container) { + const scrollLeft: number = container.scrollLeft; + const isRightColumnFloat: boolean = scrollLeft + container.clientWidth < container.scrollWidth; + this.tableCom.containerScrollXChangeSubject.next({ + scrollLeft, + isRightColumnFloat + }); + } + } + } + + // 更新光标位置 + options.mouseXPosition = event.pageX; + } + + private getColMinWidth(thElement: any): number { + // 此处的值是文本区所占最小宽度;当容器宽度小于此值时,文本会完全显示,但超出部分会直接被截断。 + // Chrome 和 IE下省略号'...'所占宽度大概为10px; + // Firefox 下出现省略号'...'所占宽度大概为49px(中文字符); + // 此处为保证三个浏览器都表现正常,并根据规范4.0取值50(49 + 1, 1为矫正偏差值)。 + let minWidth: number = 50; + + minWidth += parseFloat(window.getComputedStyle(thElement).paddingLeft) + parseFloat(window.getComputedStyle(thElement).paddingRight); + const sortEle: any = this.tiRenderer.findChildrenByClassName(thElement, 'ti3-sort-container')[0]; + const iconsEle: any = this.tiRenderer.findChildrenByClassName(thElement, 'ti3-cell-icons-container')[0]; + const headFilterEle: any = this.tiRenderer.findChildrenByClassName(thElement, 'ti3-head-filter-container')[0]; + + if (sortEle) { + const sortWidth: number = parseFloat(sortEle.getBoundingClientRect().width.toFixed(1)); + minWidth += sortWidth; + } + + if (iconsEle) { + const filterWidth: number = parseFloat(iconsEle.getBoundingClientRect().width.toFixed(1)); + minWidth += filterWidth; + } + + if (headFilterEle) { + const headFilterWidth: number = parseFloat(headFilterEle.getBoundingClientRect().width.toFixed(1)); + minWidth += headFilterWidth; + } + + return minWidth; + } + + private setTableWidthChange(): void { + const tableContainer: any = this.tiRenderer.findChildrenByClassName(this.hostEle, 'ti3-resize-wrapper')[0]; + // 表格容器自身宽度(不包括Y方向滚动条的宽度) + const clientWidth: number = tableContainer ? tableContainer['clientWidth'] : 0; + // 表格容器实际内容区域宽度 + const scrollWidth: number = tableContainer ? tableContainer['scrollWidth'] : 0; + + // resizableOpts.storeColsWidthChange为各列拖动相对值的累计和 + // 将表格内容溢出部分的宽度赋给各列拖动相对值的累计和 + this.resizableOpts.storeTableWidthChange = scrollWidth - clientWidth; + } + + private stopResize(): void { + this.updateStoredSizes(); + this.resizableOpts.mouseXPosition = 0; + this.resizableOpts.target = null; + if (this.tableCom.isFixedHead) { + this.resizableOpts.xScrollState = this.overflowedX(); + this.resizableOpts.yScrollState = this.overflowedY(); + } + } + + private getTable(): any { + // 用户给table元素加父容器,通过父容器设置overflow属性 + const tableContainerClassName: string = this.tableCom.isFixedHead ? 'ti3-table-fixed-head' : 'ti3-resize-wrapper'; + const tableContainer: any = this.tiRenderer.findChildrenByClassName(this.hostEle, tableContainerClassName)[0]; + const table: any = tableContainer.children[0].nodeName === 'TABLE' ? tableContainer.children[0] : null; + + return table; + } + + /** + * @ignore + */ + public isLastColumn(th: any): boolean { + const parentElement: any = th.parentElement; + if (!parentElement) { + return true; + } + const ths: any = th.parentElement.children; + const index: number = Array.from(ths).indexOf(th); + + return index === ths.length - 1; + } + + // 当光标移到感应区时,出现拖动分割线 + private createDividingLine(th: any): void { + this.renderer.addClass(th, 'col-resize-active'); + + const index: number = Array.from(th.parentElement.children).indexOf(th); + const bodyTable: any = this.tableCom.isFixedHead ? this.resizableOpts.secondTable : this.resizableOpts.table; + // 防止不存在tbody的场景报错 + if (!bodyTable.children[1]) { + return; + } + const trs: Array = Array.from(bodyTable.children[1].children).filter((tr: any) => { + return this.needDividingLine(tr); + }); + trs.forEach((tr: any) => { + // 分组场景index 大于tr.children.length + if (!tr.children[index]) { + return; + } + this.renderer.addClass(tr.children[index], 'col-resize-active'); + }); + } + + // 当光标离开感应区时,拖动分割线消失 + private removeDividingLine(): void { + const bodyTable: any = this.tableCom.isFixedHead ? this.resizableOpts.secondTable : this.resizableOpts.table; + const ths: Array = Array.from(this.resizableOpts.table.children[0].children[0].children); + + ths.forEach((th: Element) => { + this.renderer.removeClass(th, 'col-resize-active'); + }); + + // 防止不存在tbody的场景报错 + if (!bodyTable.children[1]) { + return; + } + const trs: Array = Array.from(bodyTable.children[1].children); + trs.forEach((tr: Element) => { + if (!this.needDividingLine(tr)) { + return; + } + Array.from(tr.children).forEach((td: any) => { + this.renderer.removeClass(td, 'col-resize-active'); + }); + }); + } + + private updateTableInfo(): void { + this.resizableOpts.ths = TiColsResizableDirective.getThs(this.resizableOpts.table); + this.updateStoredSizes(); // 保存最新宽度 + this.setComputedWidth(); // 设置最新宽度 + } + + private initStorageColsWidth(): void { + this.resizableOpts.ths = TiColsResizableDirective.getThs(this.resizableOpts.table); + this.resizableOpts.storedSizes = this.tableCom.localStorageState['colsWidth']; + this.setComputedWidth(); + // 各列宽度设置后,可能出现滚动条,所以要计算一次 storeTableWidthChange的值 + this.setTableWidthChange(); + } + + /** + * @ignore + * 将当前表格各列的宽度更新到到resizableOpts.storedSizes中 + */ + public updateStoredSizes = (): void => { + this.resizableOpts.storedSizes = []; + this.resizableOpts.ths.forEach((th: any, index: number) => { + this.tiRenderer.setAttributes(th, { 'ti-visible-index': index }); + this.resizableOpts.storedSizes[index] = TiColsResizableDirective.getWidth(th); + }); + // 更新表格记忆 + if (this.tableCom.storageId && this.tableCom.storageConfig.colsWidth) { + this.tableCom.localStorageState['colsWidth'] = this.resizableOpts.storedSizes; + } + }; + + private setComputedWidth(): void { + if (this.resizableOpts.table && this.resizableOpts.ths && this.resizableOpts.ths.length) { + this.resizableOpts.ths.forEach((th: any, index: number) => { + this.setWidth(th, this.resizableOpts.storedSizes[index]); + // 表头锁定 + if (this.tableCom.isFixedHead) { + this.resizableOpts.secondThs = TiColsResizableDirective.getThs(this.resizableOpts.secondTable); + this.setWidth(this.resizableOpts.secondThs[index], this.resizableOpts.storedSizes[index]); + } + }); + } + } + + /** + * @ignore + */ + public setWidth = (element: any, width: number): void => { + this.renderer.setStyle(element, 'width', width + 'px'); + }; + + // 将表格宽度设置为默认宽度 + private setDefaultWidth(): void { + const ths: Array = (this.resizableOpts.ths = TiColsResizableDirective.getThs(this.resizableOpts.table)); + // 列隐藏或显示后,将各列宽设置为初始用户设置的列宽 + ths.forEach((th: any) => { + const width: string = th.getAttribute('width') ? th.getAttribute('width') : ''; + // 注意:设置列宽的方式有多种,此处统一默认以width属性设置的宽为准 + this.renderer.setStyle(th, 'width', width); + }); + + // 表头锁定 + if (this.tableCom.isFixedHead) { + const secondTable: any = this.getSecondTable(); + const secondThs: Array = TiColsResizableDirective.getThs(secondTable); + this.resizableOpts.secondThs = secondThs; + this.resizableOpts.xScrollState = this.overflowedX(); + this.resizableOpts.yScrollState = this.overflowedY(); + + secondThs.forEach((th: any) => { + const width: string = th.getAttribute('width') ? th.getAttribute('width') : ''; + // 注意:设置列宽的方式有多种,此处统一默认以width属性设置的宽为准 + this.renderer.setStyle(th, 'width', width); + }); + this.fixedHeadService.processYScrollStateChange(this); + } + this.setTableWidthChange(); + } + + // 当列拖动进行时去掉文字可选样式(user-select: none) + private toggleTextSelection(toggle: boolean): void { + const body: any = this.document.body; + if (toggle) { + this.renderer.addClass(body, 'ti3-unselectable'); + this.renderer.setAttribute(body, 'unselectable', 'on'); + } else { + this.renderer.removeClass(body, 'ti3-unselectable'); + body.removeAttribute('unselectable'); + } + } + + private needDividingLine(tr: any): boolean { + return this.tableCom.needTr(tr); + } + + // 供表头锁定使用 + private getSecondTable(): Element { + const secondTableContainer: any = this.tiRenderer.findChildrenByClassName(this.hostEle, 'ti3-resize-wrapper')[0]; + + return secondTableContainer ? secondTableContainer.children[0] : null; + } + + private overflowedX(isNum?: boolean): any { + return this.fixedHeadService.overflowedResult(this.tableCom.tbodyContainer, 'X', isNum); + } + + private overflowedY(isNum?: boolean): any { + return this.fixedHeadService.overflowedResult(this.tableCom.tbodyContainer, 'Y', isNum); + } +} diff --git a/src/table/lib/src/TiColsToggleComponent.ts b/src/table/lib/src/TiColsToggleComponent.ts new file mode 100644 index 0000000..27b8e79 --- /dev/null +++ b/src/table/lib/src/TiColsToggleComponent.ts @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + ElementRef, + Input, + Renderer2, + SimpleChanges, + ViewChild, + ChangeDetectorRef, + ChangeDetectionStrategy +} from '@angular/core'; +import { TiTableComponent } from './TiTableComponent'; +import { TiColsToggleDropComponent } from './TiColsToggleDropComponent'; +import { TiSelectComponent } from '@opentiny/ng-select'; +import { TiKeymap, TiPositionType } from '@opentiny/ng-utils'; +import { TiLocale } from '@opentiny/ng-locale'; +import packageInfo from '../package.json'; +// 下面注释,可以避免编译lib时正则报错。原理未知,副作用未知。 +// @dynamic +/** + * TiColsToggle 控制列动态隐藏/显示的组件 + * + * ti-cols-toggle 用来显示列操作按钮,点击该按钮可在打开的下拉中设置各列的隐藏/显示; + * 在 ti-table 标签内,开发者可灵活设置其位置。 + * + * TiTable 上的 columns 接口传入值各项(列)的 show 和 title 属性值影响着各列的隐藏/显示状 + * 态,具体可参考 TiTableComponent columns 输入接口说明。 + * + * 开发者必须给需要动态显示/隐藏的列(th,td)使用 ngIf, 利用 ngIf 和 tiTable 上的 columns + * 接口值中每列的show的值来控制当前列的显示或隐藏。 + * + * ### 接口说明 + * **Inputs:** + * + * | 名称 | 类型 | 默认值 | 功能描述 | + * | -------- | :----- | :---- | :---- | + * | disabled | boolean | false | 设置列操作按钮是否禁用 | + * | searchable | boolean | false | 是否显示搜索框 | + * | selectAll | boolean | false | 是否开启全选功能| + * | panelWidth | string | 'justified' | 下拉面板的宽度,可选值为'justified'(默认), 'auto'或自定义宽度,但宽度不能小于select面板的宽度 | + * | noDataText | string | '暂无数据' | 无数据时的显示文本。默认值改为国际化词条 | + * | select | EventEmitter | 无 | 选中/取消选中事件,向外通知当前项数据,需要自行判断当前项是否选中| + * | focus | HTML事件 | 无 | 聚焦事件 | + * | blur | HTML事件 | 无 | 失焦事件 | + * + *

    以下说明不可用,不做参考。该组件只有以上disabled、searchable、panelWidth、selectAll 这四个Input接口 + * 和foucs、blur、select 这三个事件接口可用,其余方法、Input、输出等(继承于TiSelectComponent,是供select的内部使用的)都不可用

    + */ +@Component({ + selector: 'ti-cols-toggle', + templateUrl: './cols-toggle.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiColsToggleComponent extends TiSelectComponent { + private static readonly DEFAULT_PANEL_WIDTH: number = 250; + private static readonly MAX_PANEL_WIDTH: number = 300; + private static readonly MIN_PANEL_WIDTH: number = 200; + /** + * @ignore + * 10.0.3 版本之前服务使用 tiTip 指令自行实现提示,为做兼容性处理,添加该接口判断服务是否使用指令实现 + */ + @Input() tiTip: string; + /** + * 设置按钮 tip 提示内容 + */ + @Input() tipContent: string = TiLocale.getLocaleWords().tiTable.colsToggleTip; + /** + * 设置按钮 tip 提示方向 + */ + @Input() tipPosition: TiPositionType = 'top'; + /** + * @ignore + */ + @ViewChild('toggleMenu', { static: true }) toggleMenuEleRef: ElementRef; + /** + * @ignore + */ + @ViewChild(TiColsToggleDropComponent, { static: true }) dropsearchCom: TiColsToggleDropComponent; + /** + * @ignore + */ + public table: TiTableComponent; + protected versionInfo: string = super.getVersion(packageInfo); + // 标记是否需要更新聚焦元素 + private shouldUpdateFocusableElems: boolean = false; + constructor( + protected elementRef: ElementRef, + protected renderer2: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + table: TiTableComponent + ) { + super(elementRef, renderer2, changeDetectorRef); + this.table = table; + } + + ngOnInit(): void { + // 兼容使用tiTip指令实现的tip提示 + this.tipContent = this.tiTip ? '' : this.tipContent; + this.tipPosition = this.tiTip ? undefined : this.tipPosition; + this.setDropPanelWidth(); + super.ngOnInit(); + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + this.updateFocusableElems(); + // TODO:严格来说,不应该继承自Select,Select中有对dominator的处理。看有没有更好实现方式。 + } + + ngAfterViewChecked(): void { + // 这里不能调用父类方法,因为Select父类对dominator处理,空指针报错。 + // TODO: 看是否能够不继承select + + if (this.shouldUpdateFocusableElems) { + this.shouldUpdateFocusableElems = false; + this.updateFocusableElems(); + } + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['searchable'] && !changes['searchable'].firstChange) { + // searchable 参数变更后,dropSearch 组件中会重新获取搜索输入框元素,this.dropsearchCom.getFocusableElems() 方法获取的搜索框元素失效。 + // 由于dropSearch在ngAfterViewChecked中变更聚焦元素,时机比ngOnChanges晚,但比组件的ngAfterViewChecked早, + // 所以用shouldUpdateFocusableElems变量标记是否需要更新聚焦元素,之后在ngAfterViewChecked中处理 + this.shouldUpdateFocusableElems = true; + } + } + /** + * @ignore + * 在失焦时,通知更新了表格的列数据,用于表格记忆上传列显示数据 + * + * 继承了TiSelectComponent,TiSelectComponent 中在 @Component 元数据 host 配置中调用了该方法 + */ + public onBlur(): void { + super.onBlur(); + // 表格记忆通知列切换变化 + if (this.table.storageId) { + this.table.updateColumnsSubject.next(null); + } + } + /** + * @ignore + * 阻止 button 聚焦时点击空格回车触发 click 事件导致面板展开关闭异常 + */ + public preventKeydownDefault(event: KeyboardEvent): void { + const enterKeyCodeArr: Array = [TiKeymap.KEY_SPACE, TiKeymap.KEY_ENTER, TiKeymap.KEY_NUMPAD_ENTER]; + if (enterKeyCodeArr.includes(event.keyCode)) { + event.preventDefault(); + } + } + // 更新可聚焦元素 + private updateFocusableElems(): void { + if (this.searchable) { + // 推荐在onInit()时调用setFocusableElems(), 但是ngFor/ngIf中的元素在ngAfterViewInit()才能获取到 + this.setFocusableElems([this.toggleMenuEleRef.nativeElement].concat(this.dropsearchCom.getFocusableElems())); + } else { + this.setFocusableElems([this.toggleMenuEleRef.nativeElement]); + } + } + + private setDropPanelWidth(): void { + // TODO: + const panelWidthNum: number = parseInt(this.panelWidth, 10); + + if (panelWidthNum > TiColsToggleComponent.MAX_PANEL_WIDTH) { + this.panelWidth = `${TiColsToggleComponent.MAX_PANEL_WIDTH}px`; + + return; + } + + if (panelWidthNum < TiColsToggleComponent.MIN_PANEL_WIDTH) { + this.panelWidth = `${TiColsToggleComponent.MIN_PANEL_WIDTH}px`; + } + } +} diff --git a/src/table/lib/src/TiColsToggleDropComponent.ts b/src/table/lib/src/TiColsToggleDropComponent.ts new file mode 100644 index 0000000..f70ec2d --- /dev/null +++ b/src/table/lib/src/TiColsToggleDropComponent.ts @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectorRef, + ChangeDetectionStrategy, + Component, + ElementRef, + IterableChanges, + IterableDiffer, + IterableDiffers, + NgZone, + Renderer2, + ViewChild +} from '@angular/core'; +import { TiDropsearchComponent } from '@opentiny/ng-dropsearch'; +import { TiListComponent } from '@opentiny/ng-list'; +import { TiTableColumns, TiTableComponent } from './TiTableComponent'; +import packageInfo from '../package.json'; +/** + * @ignore + * TiColsToggleDrop 控制列动态隐藏/显示的下拉组件 + * + */ +@Component({ + selector: 'ti-cols-toggle-drop', + templateUrl: './cols-toggle-drop.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-cols-toggle-drop-container]': 'true' + } +}) +export class TiColsToggleDropComponent extends TiDropsearchComponent { + public static readonly DOMINATOR_SPACE: number = 4; + public static readonly DEFAULT_LIST_MAX_HEIGHT: number = 30 * 8 + 8; + public static readonly LIST_WITH_SEARCHBOX_MAX_HEIGHT: number = 30 * 7; + public static readonly SEARCHBOX_AREA_HEIGHT: number = 28 + 6 + 4; // 搜索框所占区域高度 + public static readonly DROP_VERTICAL_PADDING: number = 4 + 4; + public dominatorSpace: string = TiColsToggleDropComponent.DOMINATOR_SPACE + 'px'; + public columns: Array = []; + public selectedColumns: Array; // TODO: 可以不定义此变量,用this.model来代替 + public readonly listLabelKey: string = 'title'; + protected versionInfo: string = super.getVersion(packageInfo); + private columnsDiffer: IterableDiffer; + private optionsChangeInner: boolean = false; + /** + * @ignore + * 覆写 TiDroplistComponent 中的 listInited 值 + */ + public listInited: boolean = true; + @ViewChild(TiListComponent, { static: true }) listCom: TiListComponent; + constructor( + protected elementRef: ElementRef, + protected renderer2: Renderer2, + public changeDetectorRef: ChangeDetectorRef, + protected zone: NgZone, + private iterableDiffers: IterableDiffers, + private table: TiTableComponent + ) { + super(elementRef, renderer2, changeDetectorRef, zone); + } + + private static trackByFn(index: number, item: any): string { + // 表格记忆show属性也需要跟踪 + return item.title + item.show; + } + + ngOnInit(): void { + super.ngOnInit(); + this.multiple = true; // 设置为多选。 + this.heightExcludeList = this.searchable + ? TiColsToggleDropComponent.DROP_VERTICAL_PADDING + TiColsToggleDropComponent.SEARCHBOX_AREA_HEIGHT + : TiColsToggleDropComponent.DROP_VERTICAL_PADDING; + this.defaultListMaxHeight = this.searchable + ? TiColsToggleDropComponent.LIST_WITH_SEARCHBOX_MAX_HEIGHT + : TiColsToggleDropComponent.DEFAULT_LIST_MAX_HEIGHT; + this.searchKeys = ['title']; + this.columnsDiffer = this.iterableDiffers.find(this.options).create(TiColsToggleDropComponent.trackByFn); + } + + ngDoCheck(): void { + // 动态监听columns的变化(主要是引用不变,内容变化colums.push等), 从而更新经过 tiColumns 管道的数据(数据引用变了才会进pipe管道) + super.ngDoCheck(); + const columnsDiffer: IterableChanges = this.columnsDiffer.diff(this.options); + if (columnsDiffer) { + if (this.optionsChangeInner) { + this.optionsChangeInner = false; + } else { + this.selectedColumns = this.options.filter((column: { show?: boolean }) => { + return column.show === true || column.show === undefined; + }); + this.columns = this.options.concat(); // 强制变化,不然colums.push的列不能出现在列表中,与在html中使用的 PIPE 有关 + if (this.searchable && this.searchResult === this.options) { + // 如果 options 由于push等方法导致的改变,这里将 searchResult 引用更新一下,这样在 dropsearch 中打开面板时 + // 把 options 的值赋给 searchResult(searchResult = options)时,searchResult新数据(引用改变了)才能进 tiColumns 管道, 从而渲染出来。 + this.searchResult = this.searchResult.concat(); + } + } + + // columns变化需要处理列固定 + if (this.table.fixedColumnInfo.hasFixedColumn) { + // 需要延迟处理,columns变化时,组件渲染还未完成。 + // 但如果在ngAfterViewChecked处理,太频繁,影响性能。 + setTimeout((): void => { + // 处理左侧列固定 + this.table.updateFixedThLeftSubject.next(null); + this.table.updateFixedTdLeftSubject.next(null); + // 处理右侧列固定 + const scrollLeft: number = this.table.fixedColumnInfo.container.scrollLeft; + const isRightColumnFloat: boolean = + scrollLeft + this.table.fixedColumnInfo.container.clientWidth < this.table.fixedColumnInfo.container.scrollWidth; + this.table.containerScrollXChangeSubject.next({ + scrollLeft, + isRightColumnFloat + }); + }, 0); + } + } + } + + public onSelect(option: any): void { + if (option === this.listCom.optionSelectAll) { + this.options.forEach((item: any) => { + if (!this.isDisabledFn(item)) { + item.show = this.selectedColumns.includes(item); + } + }); + } else { + option.show = this.selectedColumns.includes(option); + } + this.optionsChangeInner = true; + // 需要在父类select.emit之前,更改option内容 + super.onSelect(option); + // TODO: 没有处理,用户主动改变绑定变量selectedColumns。应该在docheck里监听selectedColumns(this.model) + } + + public isDisabledFn = (item: { disabled?: boolean; show?: boolean }): boolean => { + return item.disabled === true || (item.show === undefined && item !== this.listCom.optionSelectAll); + }; + + // 鼠标点击到搜索框外围的空白,会失焦导致面板关闭 + // 此处做特殊处理,当点击空白时通过阻止默认事件的方式处理 + public onMousedownSearchBoxOuter(event: MouseEvent): void { + if ((event.target as any).tagName === 'INPUT') { + return; + } + event.preventDefault(); + } +} diff --git a/src/table/lib/src/TiColspanDirective.ts b/src/table/lib/src/TiColspanDirective.ts new file mode 100644 index 0000000..4da0916 --- /dev/null +++ b/src/table/lib/src/TiColspanDirective.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterViewInit, Directive, ElementRef, OnDestroy, Renderer2 } from '@angular/core'; +import { TiTableComponent } from './TiTableComponent'; +/** + * TiColspan 表格跨列数colspan 计算, + * + * 根据表头第一行tr中th的数目,同步colspan数。 + * + * 主要配合表格详情展开功能使用,用于表格详情展示行的的列合并。 + * + */ +@Directive({ + selector: 'td[tiColspan]' +}) +export class TiColspanDirective implements AfterViewInit, OnDestroy { + private firstTrObserver: MutationObserver; + constructor(private table: TiTableComponent, private renderer: Renderer2, private elementRef: ElementRef) {} + ngAfterViewInit(): void { + // 获取thead中第一行tr + const tableFirstTr = (this.table.nativeElement as HTMLElement).querySelector('table>thead>tr'); + if (!tableFirstTr) { + return; + } + // 初始化设置一次colspan + this.renderer.setAttribute(this.elementRef.nativeElement, 'colspan', String(tableFirstTr.children.length)); + // 修复SSR报错:ERROR ReferenceError: MutationObserver is not defined + if (typeof MutationObserver === 'undefined') { + return; + } + this.firstTrObserver = new MutationObserver((mutationsList) => { + for (let mutation of mutationsList) { + if (mutation.type === 'childList') { + this.renderer.setAttribute(this.elementRef.nativeElement, 'colspan', String(tableFirstTr.children.length)); + } + } + }); + // 只需要处理tr中th的增删,所以只配置 childList为true, + this.firstTrObserver.observe(tableFirstTr, { childList: true }); + } + ngOnDestroy(): void { + if (this.firstTrObserver) { + this.firstTrObserver.disconnect(); + } + } +} diff --git a/src/table/lib/src/TiColumnFixedDirective.ts b/src/table/lib/src/TiColumnFixedDirective.ts new file mode 100644 index 0000000..f5427f0 --- /dev/null +++ b/src/table/lib/src/TiColumnFixedDirective.ts @@ -0,0 +1,266 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { AfterViewChecked, Directive, ElementRef, NgZone, OnInit, OnDestroy, Renderer2, Input } from '@angular/core'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiTableComponent } from './TiTableComponent'; +import { TiBrowser } from '@opentiny/ng-utils'; +import { Subscription } from 'rxjs'; +/** + * tiColumnFixed 表格列固定(IE和Edge浏览器不支持) + * + * 适用场景:对于列数很多的表格,可以固定前后列,横向滚动查看其它数据。 + * + * 规范:左侧支持最多3列固定(3列不包含单选/复选框所在列),右侧支持最多1列固定。横向滚动条贯穿左右,与表格最左和最右对齐。 + * 表格下展场景不需要支持列固定。 + * + */ +@Directive({ + selector: 'th[tiColumnFixed], td[tiColumnFixed]', + host: { + '[class.ti3-table-column-fixed-right]': "!notSupportBrowser && type === 'right'", + '[class.ti3-table-column-fixed-left]': "!notSupportBrowser && type === 'left'" + } +}) +export class TiColumnFixedDirective implements OnInit, AfterViewChecked, OnDestroy { + /** + * 固定的左右位置,左侧可连续多列固定,且必须从第一列开始固定,右侧只能固定一列,且必须是最后一列 + */ + @Input('tiColumnFixed') type: 'right' | 'left'; + private tagName: 'TH' | 'TD'; + private tdLeft: number; + private element: any; + private columnFixedLeftLast: boolean = false; + private floatingFixedColumn: boolean = false; + private firstAfterViewChecked: boolean = true; + private containerScrollXChangeSubscription: Subscription; + private thResizeSubscription: Subscription; + private updateFixedThLeftSubscription: Subscription; + private updateFixedTdLeftSubscription: Subscription; + /** + * @ignore + */ + public notSupportBrowser: boolean; + constructor( + private renderer: Renderer2, + elementRef: ElementRef, + private tiRenderer: TiRenderer, + private tableCom: TiTableComponent, + private zone: NgZone + ) { + this.element = elementRef.nativeElement; + } + + ngOnInit(): void { + // IE 不支持粘性定位position: sticky,使用其他定位方式有闪动,目前无法解决; EDGE 固定列边框不显示, 表头固定时拖动横向滚动条表头横向平移不及时 + this.notSupportBrowser = TiBrowser.isIE() || TiBrowser.isEdge(); + if (this.notSupportBrowser || !this.type) { + return; + } + + if (!this.tableCom.fixedColumnInfo.container) { + const container: any = this.tiRenderer.findChildrenByClassName(this.tableCom.hostEle, 'ti3-table-container')[0]; + if (!container) { + return; + } + this.tableCom.fixedColumnInfo.container = container; + } + + this.tableCom.fixedColumnInfo.hasFixedColumn = true; + + this.tagName = this.element.tagName; + + if (this.type === 'right' && this.tagName === 'TH') { + // 表头固定需要此参数 + this.tableCom.fixedColumnInfo.thFixedRight = this.element; + } + + this.addBehavior(); + } + + ngAfterViewChecked(): void { + if (this.notSupportBrowser || !this.type || !this.firstAfterViewChecked) { + return; + } + + this.firstAfterViewChecked = false; + this.init(); + } + + ngOnDestroy(): void { + if (this.containerScrollXChangeSubscription) { + this.containerScrollXChangeSubscription.unsubscribe(); + } + if (this.thResizeSubscription) { + this.thResizeSubscription.unsubscribe(); + } + if (this.updateFixedThLeftSubscription) { + this.updateFixedThLeftSubscription.unsubscribe(); + } + if (this.updateFixedTdLeftSubscription) { + this.updateFixedTdLeftSubscription.unsubscribe(); + } + } + + private init(): void { + if (this.type === 'left') { + if (this.tagName === 'TH') { + this.processTh(); + } else { + this.processTd(); + } + } + + if (this.tagName === 'TH') { + this.zone.runOutsideAngular(() => { + // 表头初始时容器的滚动状态还未初始完成 + setTimeout(() => { + const scrollLeft: number = this.tableCom.fixedColumnInfo.container.scrollLeft; + const isRightColumnFloat: boolean = + scrollLeft + this.tableCom.fixedColumnInfo.container.clientWidth < this.tableCom.fixedColumnInfo.container.scrollWidth; + this.tableCom.containerScrollXChangeSubject.next({ + scrollLeft, + isRightColumnFloat + }); + }, 0); + }); + } else { + const scrollLeft: number = this.tableCom.fixedColumnInfo.container.scrollLeft; + const isRightColumnFloat: boolean = + scrollLeft + this.tableCom.fixedColumnInfo.container.clientWidth < this.tableCom.fixedColumnInfo.container.scrollWidth; + this.tableCom.containerScrollXChangeSubject.next({ + scrollLeft, + isRightColumnFloat + }); + } + } + + private addBehavior(): void { + // 处理左右固定的最后一列是否有阴影(看起来有浮动效果) + this.containerScrollXChangeSubscription = this.tableCom.containerScrollXChangeSubject.subscribe((scrollInfo: any) => { + if (this.type === 'right' && scrollInfo.isRightColumnFloat !== this.floatingFixedColumn) { + this.processLastFixedColumn(this.element, scrollInfo.isRightColumnFloat); + } + if (this.type === 'left' && this.columnFixedLeftLast && scrollInfo.scrollLeft > 0 !== this.floatingFixedColumn) { + this.processLastFixedColumn(this.element, scrollInfo.scrollLeft > 0); + } + }); + + // 处理左侧固定的列的left值(左侧可多列固定) + if (this.type === 'left' && this.tagName === 'TH') { + this.thResizeSubscription = this.tableCom.thResizeSubject.subscribe((thResizeInfo: any) => { + const th: any = thResizeInfo.th; + if (th !== this.element) { + return; + } + + const nextSibling: any = th.nextElementSibling; + if (nextSibling && this.tiRenderer.hasClass(nextSibling, 'ti3-table-column-fixed-left')) { + const siblings: any = th.parentElement.children; + const index: number = Array.from(siblings).indexOf(th); + const changeColumnsIndex: Array = []; + for (let i: number = index + 1; i < siblings.length; i++) { + if (!siblings[i] || !this.tiRenderer.hasClass(siblings[i], 'ti3-table-column-fixed-left')) { + break; + } + changeColumnsIndex.push(i); + this.tableCom.fixedColumnInfo.fixedColumnLeftValues[i] += thResizeInfo.leftEdge; + this.renderer.setStyle(siblings[i], 'left', `${this.tableCom.fixedColumnInfo.fixedColumnLeftValues[i]}px`); + } + const bodyTable: any = this.tableCom.isFixedHead ? thResizeInfo.resizableOpts.secondTable : thResizeInfo.resizableOpts.table; + const trs: Array = Array.from(bodyTable.children[1].children).filter((tr: any) => { + return this.needFixedColumnTr(tr); + }); + trs.forEach((tr: any) => { + changeColumnsIndex.forEach((columnIndex: number) => { + this.renderer.setStyle( + tr.children[columnIndex], + 'left', + `${this.tableCom.fixedColumnInfo.fixedColumnLeftValues[columnIndex]}px` + ); + }); + }); + } + }); + + this.updateFixedThLeftSubscription = this.tableCom.updateFixedThLeftSubject.subscribe(() => { + this.processTh(); + }); + } + + if (this.type === 'left' && this.tagName === 'TD') { + this.updateFixedTdLeftSubscription = this.tableCom.updateFixedTdLeftSubject.subscribe(() => { + this.processTd(); + }); + } + } + + private processTd(): void { + const siblings: any = this.element.parentElement.children; + const index: number = Array.from(siblings).indexOf(this.element); + // 进行判空处理,因为有可能是td元素先出现。 + // TODO: 考虑下processTd和processTd是否可以使用相同的处理方式。 + if ( + this.tableCom.fixedColumnInfo.fixedColumnLeftValues && + this.tableCom.fixedColumnInfo.fixedColumnLeftValues.length && + this.tableCom.fixedColumnInfo.fixedColumnLeftValues[index] !== this.tdLeft + ) { + this.tdLeft = this.tableCom.fixedColumnInfo.fixedColumnLeftValues[index]; + this.renderer.setStyle(this.element, 'left', `${this.tdLeft}px`); + } + + if (index === this.tableCom.fixedColumnInfo.columnFixedLeftLastIndex) { + this.columnFixedLeftLast = true; + } + } + + private processTh(): void { + const siblings: any = this.element.parentElement.children; + let left: number = 0; + this.tableCom.fixedColumnInfo.fixedColumnLeftValues = []; + + for (let i: number = 0; i < siblings.length; i++) { + const current: any = siblings[i]; + if (this.tableCom.fixedColumnInfo.fixedColumnLeftValues[i] !== left) { + this.tableCom.fixedColumnInfo.fixedColumnLeftValues[i] = left; + this.renderer.setStyle(current, 'left', `${left}px`); + } + // offsetWidth 包括边框 + left += current.offsetWidth; + const nextSibling: any = siblings[i + 1]; + if (nextSibling && !this.tiRenderer.hasClass(nextSibling, 'ti3-table-column-fixed-left')) { + this.tableCom.fixedColumnInfo.columnFixedLeftLastIndex = i; + if (current === this.element) { + this.columnFixedLeftLast = true; + } + break; + } else { + if (current === this.element) { + this.columnFixedLeftLast = false; + } + } + } + } + + private processLastFixedColumn(ele: any, add: boolean): void { + if (add) { + this.floatingFixedColumn = true; + this.renderer.addClass(ele, 'ti3-table-floating-fixed-column'); + } else { + this.floatingFixedColumn = false; + this.renderer.removeClass(ele, 'ti3-table-floating-fixed-column'); + } + } + + private needFixedColumnTr(tr: any): boolean { + return this.tableCom.needTr(tr); + } +} diff --git a/src/table/lib/src/TiColumnsPipe.ts b/src/table/lib/src/TiColumnsPipe.ts new file mode 100644 index 0000000..d6ab387 --- /dev/null +++ b/src/table/lib/src/TiColumnsPipe.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Pipe, PipeTransform } from '@angular/core'; +import { Util } from '@opentiny/ng-utils'; +/** + * @ignore + * TiColumnsPipe 过滤掉columns中的title为空的数据项 + * + */ +@Pipe({ name: 'tiColumns' }) +export class TiColumnsPipe implements PipeTransform { + transform(value: Array): Array { + return value.filter((item: any) => !Util.isEmptyString(item.title)); + } +} diff --git a/src/table/lib/src/TiDetailsIconComponent.ts b/src/table/lib/src/TiDetailsIconComponent.ts new file mode 100644 index 0000000..b4aa81f --- /dev/null +++ b/src/table/lib/src/TiDetailsIconComponent.ts @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + Component, + DoCheck, + ElementRef, + EventEmitter, + Input, + OnInit, + Output, + Renderer2, + ChangeDetectionStrategy, + ChangeDetectorRef +} from '@angular/core'; +import { TiBrowser, Util } from '@opentiny/ng-utils'; +import { TiTableComponent, TiTableRowData } from './TiTableComponent'; +/** + * TiDetailsIcon 详情展开图标组件 + * + * ti-details-icon 用来显示详情展开图标,其嵌在详情展开列的 td 标签中; + * 点击该图标,对应详情行在展开/收起两个状态之间切换。 + */ +@Component({ + selector: 'ti-details-icon', + templateUrl: './details-icon.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '(click)': 'onClick()' + } +}) +export class TiDetailsIconComponent implements OnInit, DoCheck { + /** + * @ignore + * 表格详情中,非详情行标记样式类 + */ + public static TABLE_ClASS_DETAIL_BASE: string = 'ti3-table-detail-icon-tr'; + /** + * 当前行数据 + */ + @Input() row: TiTableRowData; + /** + * 当前行索引值,即 ngFor 中对应的 index + */ + @Input() index: number; + /** + * 点击详情展开图标时触发的回调,参数为当前行数据,一旦使用此接口,就由开发者决定是否要展开/收起, + * 可通过行数据中的 showDetails 属性控制是否及何时展开/收起 + */ + @Output() readonly beforeToggle: EventEmitter = new EventEmitter(); + private oldShowDetails: boolean = false; + private element: any; + constructor( + private table: TiTableComponent, + private elementRef: ElementRef, + private renderer: Renderer2, + private changeRef: ChangeDetectorRef + ) { + this.element = this.elementRef.nativeElement; + } + + ngOnInit(): void { + this.row = Util.isUndefined(this.row) ? {} : this.row; + this.row.showDetails = Util.isUndefined(this.row.showDetails) ? false : this.row.showDetails; + // 给当前行添加样式类标记 + const parentTr: any = this.renderer.parentNode(this.renderer.parentNode(this.element)); + this.renderer.addClass(parentTr, TiDetailsIconComponent.TABLE_ClASS_DETAIL_BASE); + } + + ngDoCheck(): void { + // 如果需要关闭其它行的详情(即只能展开一行的详情)时,需要监听showDetails + if (this.table.closeOtherDetails && this.row.showDetails !== this.oldShowDetails) { + this.oldShowDetails = this.row.showDetails; + if (this.row.showDetails) { + // 当打开当前行的详情时,关闭其他行的详情 + this.table.closeOtherDetailsFn(this.row); + } + } + // 使用closeOtherDetails和beforeToggle接口时,保证图标收起 + this.changeRef.markForCheck(); + } + + /** + * @ignore + */ + public onClick(): void { + if (this.beforeToggle.observers.length > 0) { + this.beforeToggle.emit(this.row); + } else { + this.row.showDetails = !this.row.showDetails; + } + + // 为了解决 codeclub #1720 + // 在chrome浏览器下,滚动条滚到底部后,这时页面内容高度变小,滚动条保留在底部(正常的现象), + // 之后页面内容高度再变大时,滚动条依然保留在底部。 + // 这导致了当页面滚动条拖动到底部后,将表格的pagesize由大变小(如20条修改为10条),再去点击某一行详情展开时, + // 视觉上详情行是向上展开的,其原因是滚动条一直停留在底部;这时,只要稍微手动触发滚动条滚动,详情正常展开 + // 注意:此问题只修复了在body上出滚动条的表格的详情展开问题。 + if (TiBrowser.isChrome()) { + const scrollTop: number = Math.ceil(document.documentElement.scrollTop); + // 窗口可视区域高度 + const height: number = document.documentElement.clientHeight; + const scrollHeight: number = document.body.scrollHeight; + if (scrollTop > 0 && scrollTop + height >= scrollHeight) { + document.body.scrollTop = scrollTop - 0.5; + document.body.scrollTop = scrollTop + 0.5; + } + } + } +} diff --git a/src/table/lib/src/TiDetailsTrDirective.ts b/src/table/lib/src/TiDetailsTrDirective.ts new file mode 100644 index 0000000..fbdc7a4 --- /dev/null +++ b/src/table/lib/src/TiDetailsTrDirective.ts @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, DoCheck, Input, Renderer2, TemplateRef, ViewContainerRef } from '@angular/core'; +import { TiTableComponent, TiTableRowData } from './TiTableComponent'; +import { TiDetailsIconComponent } from './TiDetailsIconComponent'; +/** + * TiDetailsTr 详情行结构指令 + * + * 使用时需要在其前面加 * 语法糖,传入当前行数据; + * 其内部根据row.showDetails的值来控制详情行是否显示,功能类似于ngIf。 + */ +@Directive({ + selector: '[tiDetailsTr]' +}) +export class TiDetailsTrDirective implements DoCheck { + /** + * 当前行数据 + */ + @Input('tiDetailsTr') row: TiTableRowData; + private oldShowDetails: boolean; + constructor( + private templateRef: TemplateRef, + private viewContainerRef: ViewContainerRef, + private renderer: Renderer2, + private table: TiTableComponent + ) {} + + ngDoCheck(): void { + if (this.row.showDetails !== this.oldShowDetails) { + this.updateView(); + this.oldShowDetails = this.row.showDetails; + } + } + private updateView(): void { + if (this.row.showDetails) { + // 上下文参数是否需要传递 + this.viewContainerRef.createEmbeddedView(this.templateRef); + // 结构指令的宿主元素是一个 comment(注释) 的Node节点, + // 生成节点元素插入DOM中的位置跟angularCompilerOptions.enableIvy 配置有关,true时在宿主元素之前,false时在宿主元素之后, + // 而 ng9开始angularCompilerOptions.enableIvy 默认为true,但一些项目为兼容性,强制设置为了false,所以要做一下兼容处理 + let detailTr: HTMLElement; + const nativeElement = this.templateRef.elementRef.nativeElement; + if ( + nativeElement.previousSibling && + nativeElement.previousSibling.classList && + nativeElement.previousSibling.classList.contains(TiDetailsIconComponent.TABLE_ClASS_DETAIL_BASE) + ) { + detailTr = nativeElement.nextSibling; + } else { + detailTr = nativeElement.previousSibling; + } + + this.renderer.addClass(detailTr, 'ti3-details-tr'); + } else { + this.viewContainerRef.clear(); + // TODO: 方案是否可优化 + // 表头锁定时,展开时可在table的AfterViewChecked中获取到表格变化后的高度, + // 但是收起时,在table的AfterViewChecked中获取不到表格变化后的高度,不能及时处理滚动条的变化 + // 所以此处使用setTimeout使其延时触发table的AfterViewChecked,使其能够处理由高度变化引起的滚动条状态的改变 + // 注:使用ngIf指令也是如此效果 + if (this.table.isFixedHead) { + setTimeout(() => {}, 0); + } + } + } +} diff --git a/src/table/lib/src/TiHeadFilterComponent.ts b/src/table/lib/src/TiHeadFilterComponent.ts new file mode 100644 index 0000000..f2f5443 --- /dev/null +++ b/src/table/lib/src/TiHeadFilterComponent.ts @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ElementRef, Input, ViewChild } from '@angular/core'; +import { TiSelectComponent } from '@opentiny/ng-select'; +import { TiHeadFilterDatetimeConfig, TiHeadFilterDropComponent } from './TiHeadFilterDropComponent'; +import { TiFormComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +// 下面注释,可以避免编译lib时正则报错。原理未知,副作用未知。 +// @dynamic +/** + * TiHeadFilter 表头过滤(漏斗)组件, 嵌在表头的 th 中使用。 + * + * ti-head-filter 用来显示过滤操作漏斗图标,点击其可打开下拉框,选中下拉框中的选项时, + * 可在提供的 select 事件中做对应的表格数据过滤处理。 + * + * ### 接口说明 + * **Inputs:** + * + * | 名称 | 类型 | 默认值 | 功能描述 | + * | -------- | :----- | :---- | :---- | + * | options | array | [] | 所有过滤项 | + * | ngModel | object | false | 可设置和获取 headfilter下拉选中值 | + * | labelKey | string | 'label' | 下拉面板中要显示的字段| + * | valueKey | string | '' | 当存在valueKey时,选中值基于valueKey| + * | idKey | string | '' | idKey指定的属性的值相等时即认为 option 选项是选中的 | + * | panelMaxHeight | string | 最大显示8条数据 | 下下拉面板的最大显示高度,溢出时则出滚动条。 | + * | id | string | undefined | HTML属性id,自动化测试要求必须给id赋值 | + * | select | function | 无 | 下拉面板选中事件,向外通知选中数据 | + * | multiple | boolean | false | 下拉面板是否为多选 | + * | selectAll | boolean | false | 下拉面板是否显示全选框 | + * | panelAlign | string | 'left' | 下拉面板展开对齐方式| + * | searchable | boolean | false | 是否开启搜索功能| + * | panelWidth | 'auto' 或 string | 'auto' | 设置下拉面板的宽度,"auto"表示下拉框的宽度根据下拉选项的内容自动撑开,也可设置固定的下拉框宽度(不小于过滤图标的宽度),例如:"200px"| + * | noDataText | string | '暂无数据' | 无数据时的显示文本。默认值改为国际化词条 | + * | isDatetime | boolean | false | 下拉面板是否为时间日期格式 | + * | datetimeConfig | object | {} | 时间日期中的基本信息配置 | + * + *

    以下说明不可用,不做参考。该组件只有以上15个Input接口和1个Output接口 + * 可用,其余方法、Input、输出等(继承于TiSelectComponent,是供select的内部使用的)都不可用

    + */ +@Component({ + selector: 'ti-head-filter', + templateUrl: './head-filter.html', + styleUrls: ['./head-filter.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiHeadFilterComponent)], + host: { + '[class.ti3-head-filter-container]': 'true', + '(blur)': 'onBlur()' + } +}) +export class TiHeadFilterComponent extends TiSelectComponent { + /** + * 面板对齐方式 + */ + @Input() panelAlign: 'left' | 'right' = 'left'; + // 由于headfilter与select中的panelWidth的默认值和类型不一样,所以这里重定义panelWidth接口 + /** + * 下拉面板的最大宽度,'auto' 表示下拉框的宽度根据下拉选项的内容自动撑开,string 是可以设置具体的宽度值,比如 '200px' + */ + @Input() panelWidth: 'auto' | string = undefined; // 设置为undefined是为了覆盖父类的panelWidth值 + + /** + * 是否为时间日期菜单 + */ + @Input() isDatetime: boolean = false; + /** + * 时间日期的相关配置 + */ + @Input() datetimeConfig: TiHeadFilterDatetimeConfig = {}; + /** + * @ignore + */ + @ViewChild('filterIcon', { static: true }) filterIconEleRef: ElementRef; + /** + * @ignore + */ + @ViewChild(TiHeadFilterDropComponent, { static: true }) + dropsearchCom: TiHeadFilterDropComponent; + protected versionInfo: string = super.getVersion(packageInfo); + /** + * @ignore + * 在初始化DOM渲染完成之时,将下拉面板中的所有选中项放在一起,使可聚焦元素都放在组件内部,从而在点击面板进行相关操作时, + * 组件不会触发blur事件,保证面板内操作的正常执行,在点击组件外的空白处时,保证blur事件的正常执行 + */ + ngAfterViewInit(): void { + if (this.isDatetime) { + const elemsNodeList: Array = [this.filterIconEleRef.nativeElement].concat( + this.dropsearchCom.dropCom.nativeElement, + this.dropsearchCom.datetimeStartCom.getFocusableElems(), + this.dropsearchCom.datetimeEndCom.getFocusableElems(), + this.dropsearchCom.datetimeOkCom.nativeElement, + this.dropsearchCom.datetimeCancelCom.nativeElement + ); + this.setFocusableElems(elemsNodeList); + } + } + + ngAfterViewChecked(): void { + // 这里不能调用父类方法,因为Select父类对dominator处理,空指针报错。 + // TODO: 看是否能够不继承select + if (!this.isDatetime && this.searchable !== this.oldSearchable) { + this.oldSearchable = this.searchable; + if (this.searchable) { + this.setFocusableElems([this.filterIconEleRef.nativeElement].concat(this.dropsearchCom.getFocusableElems())); + } else { + this.setFocusableElems([this.filterIconEleRef.nativeElement]); + } + } + } + + /** + * @ignore + * 失焦情况下,仅关闭面板,不做聚焦等处理 + */ + public onBlur(): void { + super.onBlur(); + if (this.searchable) { + if (getComputedStyle(this.filterIconEleRef.nativeElement).outlineStyle === 'none') { + this.renderer.setStyle(this.filterIconEleRef.nativeElement, 'outlineStyle', ''); + } + } + } + + /** + * @ignore + * seachbox:180 + * datetime:310 面板的宽度需要固定值 + * 其他场景如果不设置,则自动撑开 + */ + public getDropWidth(): string { + let panelWidthTemp: string = 'auto'; + if (!this.isDatetime && this.searchable) { + panelWidthTemp = '180px'; + } + if (this.isDatetime) { + panelWidthTemp = this.datetimeConfig?.onlyDate ? '270px' : '310px'; + } + + panelWidthTemp = this.panelWidth ? this.panelWidth : panelWidthTemp; + + return panelWidthTemp; + } +} diff --git a/src/table/lib/src/TiHeadFilterDropComponent.ts b/src/table/lib/src/TiHeadFilterDropComponent.ts new file mode 100644 index 0000000..75beca2 --- /dev/null +++ b/src/table/lib/src/TiHeadFilterDropComponent.ts @@ -0,0 +1,270 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input, SimpleChanges, ViewChild } from '@angular/core'; +import { TiDropsearchComponent } from '@opentiny/ng-dropsearch'; +import { TiDateBaseComponent, TiDatetimeFormat } from '@opentiny/ng-datebase'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiBrowser } from '@opentiny/ng-utils'; +import { TiButtonComponent } from '@opentiny/ng-button'; +import packageInfo from '../package.json'; + +/** + * datetime 接口相应的匹配值 + */ +export interface TiHeadFilterDatetimeConfig { + /** + * datetime 类型日期显示格式 + */ + format?: string | TiDatetimeFormat; + /** + * datetime可选最小值 + */ + min?: Date; + /** + * datetime可选最大值 + */ + max?: Date; + /** + * 是否仅是日期的选择(没有时间的选择)。不设置时默认是日期时间的选择 + */ + onlyDate?: boolean; + + [propName: string]: any; +} + +/** + * @ignore + * TiHeadFilterDrop 表头过滤漏斗组件的下拉框选择组件 + */ +@Component({ + selector: 'ti-head-filter-drop', + templateUrl: './head-filter-drop.html', + styleUrls: ['./head-filter-drop.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiHeadFilterDropComponent)] +}) +export class TiHeadFilterDropComponent extends TiDropsearchComponent { + public static readonly DOMINATOR_SPACE: number = 6; + public dominatorSpace: string = TiHeadFilterDropComponent.DOMINATOR_SPACE + 'px'; + // 选中项 + public selected: Array; + // 时间日期面板选中项 + public datetimeSelected: TiHeadFilterDatetimeConfig = {}; + // 时间日期值范围设置 + public datetimeLimit: TiHeadFilterDatetimeConfig = {}; + protected heightExcludeContent: number = 10; // 上下边框为2,上下padding为8 + protected buttonHeight: number = 45; // 确定、取消按钮高度 + protected versionInfo: string = super.getVersion(packageInfo); + /** + * 是否开启时间日期下拉 + */ + @Input() isDatetime: boolean = false; + /** + * 时间日期相关的配置 + */ + @Input() datetimeConfig: TiHeadFilterDatetimeConfig = {}; + /** + * @ignore + * 时间日期范围开始 + */ + @ViewChild('datetimeStart', { static: false }) + datetimeStartCom: TiDateBaseComponent; + /** + * @ignore + * 时间日期范围结束 + */ + @ViewChild('datetimeEnd', { static: false }) + datetimeEndCom: TiDateBaseComponent; + @ViewChild('datetimeOk', { static: false }) datetimeOkCom: TiButtonComponent; + @ViewChild('datetimeCancel', { static: false }) + datetimeCancelCom: TiButtonComponent; + + ngOnInit(): void { + super.ngOnInit(); + this.initDatetime(); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + + // 重新设置headfilterdrop的heightExcludeList + if (changes['searchable']) { + if (changes['searchable'].currentValue) { + this.heightExcludeList = this.multiple + ? this.heightExcludeContent + this.buttonHeight + TiDropsearchComponent.SEARCHBOX_EXCLUDE_HEIGHT + : this.heightExcludeContent + TiDropsearchComponent.SEARCHBOX_EXCLUDE_HEIGHT; + } else { + this.heightExcludeList = this.multiple ? this.heightExcludeContent + this.buttonHeight : this.heightExcludeContent; + } + } + } + + // 初始化时间日期面板信息 + public initDatetime(): void { + if (this.isDatetime) { + // 初始化 this.datetimeConfig如果用户传入undefined,组件会报错,此处做下初始化容错处理。 + this.datetimeConfig = this.datetimeConfig ? this.datetimeConfig : {}; + // 开始日期的max是结束日期的选中值,如果结束日期没有选择,使用item中设置的max + this.datetimeLimit.startMax = this.datetimeSelected?.end ? this.datetimeSelected.end : this.datetimeConfig.max; + // 结束日期的min是开始日期的选中值,如果开始日期没有选择,使用中设置的min + this.datetimeLimit.endMin = this.datetimeSelected?.start ? this.datetimeSelected.start : this.datetimeConfig.min; + } + } + + writeValue(model: any): void { + super.writeValue(model); + // 初始化selected选中项 + if (this.multiple) { + this.selected = [...(model ? model : [])]; + } else if (this.isDatetime) { + this.datetimeSelected = { + start: this.isValidDate(model?.start) ? model.start : undefined, + end: this.isValidDate(model?.end) ? model.end : undefined + }; + this.dateStartChange(this.datetimeSelected.start); + this.dateEndChange(this.datetimeSelected.end); + } else { + this.selected = model ? model : ''; + } + } + + onSelect(option: any): void { + // 多选时不处理onSelect事件(在点击确定按钮时触发) + if (this.multiple) { + return; + } + this.model = option; + super.onSelect(option); + } + + private restoreSelected(): void { + // 还原selected选中项 + if (this.multiple) { + this.selected = [...(this.model ? this.model : [])]; + } + + // 还原时间日期的选中项 + if (this.isDatetime) { + this.datetimeSelected = { + start: this.isValidDate(this.model?.start) ? this.model.start : undefined, + end: this.isValidDate(this.model?.end) ? this.model.end : undefined + }; + } + } + + public hide(): void { + // 处理问题:搜索框时,在下拉中选择完收起后漏斗图标由于聚焦而有了虚线框 + if (this.searchable) { + this.renderer.setStyle(this.dominatorElem, 'outlineStyle', 'none'); + super.hide(); + } else { + super.hide(); + } + this.restoreSelected(); + } + + hideWithoutFocus(): void { + super.hideWithoutFocus(); + this.restoreSelected(); + } + + // 按钮面板鼠标按下事件(解决因点击按钮面板导致整个下拉面板隐藏的问题) + btnContainerMousedown(event: any): void { + event.preventDefault(); + } + + // 确定按钮 + okClick(): void { + // 选中值发生改变时对model值做处理 + if (JSON.stringify(this.selected) !== JSON.stringify(this.model ? this.model : [])) { + this.model = [...this.selected]; + } + // 触发onSelect事件 + this.select.emit(this.model); + this.hide(); + } + + // 取消按钮 + cancelClick(): void { + // 还原selected选中项 + this.selected = [...(this.model ? this.model : [])]; + this.hide(); + } + + // 时间面板确定按钮 + onClickDatetimeOK(): void { + // 进行时间戳对比,在选中项发生改变时再进行model的赋值。直接给model赋值,在开始和结束时间都没有改变的情况下,也会触发ngmodelchange事件 + if ( + Date.parse(this.datetimeSelected?.start) !== Date.parse(this.model?.start) || + Date.parse(this.datetimeSelected?.end) !== Date.parse(this.model?.end) + ) { + this.model = { + start: this.datetimeSelected.start, + end: + this.datetimeSelected.end && this.datetimeConfig?.onlyDate + ? new Date( + this.datetimeSelected.end.getFullYear(), + this.datetimeSelected.end.getMonth(), + this.datetimeSelected.end.getDate(), + 23, + 59, + 59 + ) + : this.datetimeSelected.end, // 如果只是日期的选择,那么结束日期的时间应该是 23:59:59 + type: 'datetime' + }; + } + // 触发onSelect事件 + this.select.emit(this.model); + this.hide(); + } + + // 时间面板取消按钮 + onClickDatetimeCancel(): void { + this.hide(); + } + + // Invalid Date判断 + private isValidDate(dateTemp: Date | string): boolean { + let date: Date | string = dateTemp; + if (Object.prototype.toString.call(date) === '[object String]') { + // 转为时间格式 + date = new Date(dateTemp); + } + + return Object.prototype.toString.call(date) === '[object Date]' && String(date) !== 'Invalid Date'; + } + + // 监听开始时间值的变化,开始时间的最大值不大于结束选中时间 + dateStartChange(value: Date): void { + if (this.isValidDate(value)) { + this.datetimeLimit.endMin = value; + } else if (this.isValidDate(this.datetimeConfig?.min as Date)) { + this.datetimeLimit.endMin = this.datetimeConfig?.min; + } else { + // 没有设置min,开始日期选中又清除后,需要设置一个min的缺省值 + this.datetimeLimit.endMin = undefined; + } + } + + // 监听结束时间值的变化,结束时间的最最小值不小于开始选中时间 + dateEndChange(value: any): void { + if (this.isValidDate(value)) { + this.datetimeLimit.startMax = value; + } else if (this.isValidDate(this.datetimeConfig?.max as Date)) { + this.datetimeLimit.startMax = this.datetimeConfig?.max; + } else { + // 没有设置 max, 结束日期选中又清除后,需要设置一个max的缺省值 + this.datetimeLimit.startMax = undefined; + } + } +} diff --git a/src/table/lib/src/TiHeadMenuComponent.ts b/src/table/lib/src/TiHeadMenuComponent.ts new file mode 100644 index 0000000..501b912 --- /dev/null +++ b/src/table/lib/src/TiHeadMenuComponent.ts @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewChild, ViewEncapsulation } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiDropComponent } from '@opentiny/ng-drop'; +import { TiLocale } from '@opentiny/ng-locale'; +import packageInfo from '../package.json'; +/** + * TiHeadMenu 表头下拉菜单组件,搭配表头复选 tiCheckGroup 组件使用 + * + * + * 嵌在表头的th中使用,点击该组件时打开下拉操作菜单项面板 + * + * 组件有默认下拉菜单项,也可支持自定义下拉菜单项 + */ +@Component({ + selector: 'ti-head-menu', + templateUrl: './head-menu.html', + styleUrls: ['./head-menu.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None +}) +export class TiHeadMenuComponent extends TiBaseComponent { + /** + * 下拉菜单项的数据集合 + */ + @Input() options: Array; + /** + * 选中菜单项时触发的回调,参数为该菜单项数据 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + @ViewChild(TiDropComponent, { static: true }) dropCom: TiDropComponent; + /** + * @ignore + */ + public dominatorSpace: string = '8px'; + /** + * @ignore + */ + public selected: any; + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + super.ngOnInit(); + if (!this.options) { + // 默认菜单项 + this.options = [ + { + key: 'checkAll', + label: TiLocale.getLocaleWords().tiTable.headMenuSelectAll + }, + { + key: 'uncheckAll', + label: TiLocale.getLocaleWords().tiTable.headMenuClearAll + } + ]; + } + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + const parent: any = this.nativeElement.parentElement; // 表头单元格th + if (parent?.hasAttribute('checkbox-column')) { + this.renderer.addClass(parent, 'ti3-head-menu-cell'); + } + } + + /** + * @ignore + * 触发select事件 + */ + public onSelect(option: any): void { + this.dropCom.hide(); + this.select.emit(option); + setTimeout(() => { + this.selected = undefined; + }, 0); + } + + /** + * @ignore + */ + onBlur(): void { + this.dropCom.hide(); + } +} diff --git a/src/table/lib/src/TiHeadSortComponent.ts b/src/table/lib/src/TiHeadSortComponent.ts new file mode 100644 index 0000000..1f2b483 --- /dev/null +++ b/src/table/lib/src/TiHeadSortComponent.ts @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, DoCheck, ElementRef, Input, OnInit, Renderer2, ChangeDetectionStrategy } from '@angular/core'; +import { TiTableComponent } from './TiTableComponent'; +import { Util } from '@opentiny/ng-utils'; +/** + * TiHeadSort 表头排序(上下箭头)组件 + * + * 嵌在表头的th中使用,点击该组件时表格数据按该列的排序规则进行升序/降序排序 + * + * 组件有默认的排序规则,也可支持自定义排序规则 + */ +@Component({ + selector: 'ti-head-sort', + templateUrl: './head-sort.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-sort-container]': 'true', + '(click)': 'onClick()' + } +}) +export class TiHeadSortComponent implements OnInit, DoCheck { + /** + * 必选,排序时依据的数据属性 + * + */ + @Input() sortKey: string; + /** + * 初始默认是否为升序,默认值 null 表示不排序 + */ + @Input() asc: boolean = null; + /** + * 排序函数(参考 Array.sort 的 compareFunction) + */ + @Input() compareFn: (a: any, b: any, sortKey?: string) => number; + /** + * @ignore + * 排序状态,也是图标名 + */ + public sortState: string = 'sort'; + + private hostElement: HTMLElement; + /** + * @ignore + */ + public table: TiTableComponent; + constructor(private elementRef: ElementRef, table: TiTableComponent) { + this.hostElement = this.elementRef.nativeElement; + this.table = table; + } + ngOnInit(): void { + // 更新TiTableComponent的enableSort,表示启用了排序。 + this.table.enableSort = true; + // 修复SSR报错:ERROR TypeError: Cannot read property 'sort' of undefined + // 表格记忆 + if (this.table?.storageId && this.table?.storageConfig?.sort && this.table?.sessionStorageState?.sort) { + this.asc = this.sortKey === this.table.sessionStorageState.sort.sortKey ? this.table.sessionStorageState.sort.asc : null; + } + if (Util.isUndefined(this.asc)) { + // 初始无序时(默认为null)兼容传入 undefined + this.asc = null; + } + if (this.asc !== null) { + // 如果有默认排序,即排序的初始化,将排序初始化的各个值赋给 tiTable 的 tableState 对应的 sort 属性 + this.sort(true); + } + } + + ngDoCheck(): void { + this.asc = this.table.dataState.sort.sortKey === this.sortKey ? this.table.dataState.sort.asc : null; + if (this.sortKey === this.table.dataState.sort.sortKey && this.asc === true) { + this.sortState = 'sort-ascent'; + } else if (this.sortKey === this.table.dataState.sort.sortKey && this.asc === false) { + this.sortState = 'sort-descent'; + } else { + this.sortState = 'sort'; + } + } + + /** + * @ignore + */ + public onClick(): void { + if (!this.sortKey) { + return; + } + // =》 无序 =》 升序 =》 降序 =》 无序 + if (this.asc === null) { + this.asc = true; + } else if (this.asc) { + this.asc = false; + } else { + this.asc = null; + } + this.sort(false); + } + + private sort(isDefaultSort: boolean): void { + this.table.updateSort(this.sortKey, this.asc, isDefaultSort, this.compareFn); + } +} diff --git a/src/table/lib/src/TiTableComponent.ts b/src/table/lib/src/TiTableComponent.ts new file mode 100644 index 0000000..e72ff3c --- /dev/null +++ b/src/table/lib/src/TiTableComponent.ts @@ -0,0 +1,1088 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + AfterViewChecked, + AfterViewInit, + Component, + ElementRef, + EventEmitter, + Input, + IterableChanges, + IterableDiffer, + IterableDiffers, + NgZone, + OnDestroy, + OnInit, + Output, + Renderer2, + ViewEncapsulation, + ChangeDetectionStrategy, + ChangeDetectorRef +} from '@angular/core'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { Util } from '@opentiny/ng-utils'; +import { Subject } from 'rxjs'; +import { TiTableFixedHeadService } from './TiTableFixedHeadService'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; + +/** + * TiTable 组件上 srcData 输入接口中 state 属性值的类型接口 + */ +export interface TiTableSrcState { + /** + * 传给组件的表格源数据是否已经过搜索处理 + */ + searched: boolean; + /** + * 传给组件的表格源数据是否已经过排序处理 + */ + sorted: boolean; + /** + * 传给组件的表格源数据是否已经过分页处理 + */ + paginated: boolean; +} +/** + * 表格当前数据的搜索、排序、分页状态值接口 + */ +export interface TiTableDataState { + /** + * 搜索状态,其包含两个属性: + * + * searchWords:类型为Array,搜索字符串集合 + * + * searchKeys:类型为Array,搜索指定的字段范围 + * + * searchStrictKeys: 类型为Array,搜索指定进行严格搜索的字段范围。 + * + */ + search: { + searchWords: Array; + searchKeys?: Array; + searchStrictKeys?: Array; + }; + /** + * 排序状态,其包含两个属性: + * + * sortKey:类型为 String,进行排序的数据属性 + * + * asc:类型为 boolean,是否为升序 + * + */ + sort: { sortKey: string; asc: boolean }; + /** + * 分页状态,其包含两个属性: + * + * currentPage:类型Number,表示当前页 + * + * itemsPerPage:类型为Number,每页显示条数 + * + */ + pagination: { currentPage: number; itemsPerPage: number }; +} + +/** + * 表格行数据类型接口 + */ +export interface TiTableRowData { + /** + * 控制当前详情行是否展开 + * + */ + showDetails?: boolean; + /** + * 允许有多余的属性字段 + * + */ + [propName: string]: any; +} +/** + * TiTable 组件上 srcData 输入接口的数据类型接口 + */ +export interface TiTableSrcData { + /** + * 源数据 + */ + data: Array; + /** + * 源数据状态(是否已经过排序 sorted、过滤 searched、分页 paginated 处理)。比如,远程加载数据的分页场景下,从远程获取传入给 data 的数据为当前页数据, + * 即源数据已经进行了分页,因此应将 state.paginated 设置为true + * + */ + state: TiTableSrcState; +} + +/** + * 表格的各列信息 + * + */ +export interface TiTableColumns { + /** + * 表头文本内容,当某一列的 title 为空字符串时,当前列一直在表格中显示,但是不出现在控制列动态 + * 隐藏/显示 设置面板中,例如单选列、多选列、详情展开图标列 + * + */ + title?: string; + /** + * 设置列宽,支持百分比和px值; + */ + width?: string; + /** + * 此属性只有在表格具有列设置功能时才需要设置,show 可以分别设置如下值: + * + * true: 表示该列默认显示,用户可以通过列设置下拉面板操作切换其显示/隐藏状态。 + * + * false : 表示该列默认隐藏,用户可以通过列设置下拉面板操作切换其显示/隐藏状态。 + * + * undefined : 表示该列不具备动态显示/隐藏功能,固定显示。 + * + * 注意:设置show属性值时,title为空字符串时,该列固定显示,但不出现在列操作面板中。 + */ + show?: boolean; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} + +/** + * 表格当前页、每页个数记忆开关 + */ +export interface TiPaginationStorageConfig { + currentPage?: boolean; + itemsPerPage?: boolean; +} +/** + * 表格记忆各项开关 + */ +export interface TiTableStorageConfig { + /** + * 排序 + */ + sort: boolean; + /** + * 分页 + */ + pagination: boolean | TiPaginationStorageConfig; + /** + * 列宽 + */ + colsWidth: boolean; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} +/** + * TiTable 表格组件 + * + * 支持以表格的形式展示多条数据 + * + * 支持前后台分页、搜索、排序、详情展开、列拖动(调整列宽)、控制列动态隐藏/显示、行复选、行单选、 + * 表格单元格(th, td)智能tip、树表、列文本对齐方式、表头固定等功能 + * + */ +@Component({ + selector: 'ti-table', + templateUrl: './table.html', + styleUrls: ['./table.less', './table-nodata-small-nest-resize.less', './table-toggle-sort-details.less', './table-tree-fix.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + host: { + '[class.ti3-table]': 'true' + } +}) +export class TiTableComponent extends TiBaseComponent implements OnInit, AfterViewChecked, AfterViewInit, OnDestroy { + private static DEFAULT_SRC_DATA: TiTableSrcData = { + data: [], + state: { + searched: false, + sorted: false, + paginated: false + } + }; + /** + * 必选,源数据及其状态,srcData.data 是源数据,srcData.state 是数据状态(是否已经过排序 sorted、过滤 searched、分页 paginated 处理) + */ + @Input() srcData: TiTableSrcData = TiTableComponent.DEFAULT_SRC_DATA; + /** + * 必选,表格呈现的数据, 只需在初始化时将其设置为空数组即可,组件内部会对其赋值和更新 + * + */ + @Input() displayedData: Array; // 表格中实时展示的数据集 + /** + * 缓存表格状态的唯一标志值 + */ + @Input() storageId: string; + /** + * 配置表格各状态是否进行缓存 + */ + @Input() storageConfig: TiTableStorageConfig = { + sort: true, + pagination: true, + colsWidth: true + }; + /** + * 表格呈现的数据改变时触发的回调, 参数为呈现的数据集合 + */ + @Output() readonly displayedDataChange: EventEmitter> = new EventEmitter(); + /** + * 表格各列信息 + */ + @Input() columns: Array = []; + /** + * 被搜索的字符串的集合 + * + * 当 searchWords 长度为 1 时,在 searchKeys 指定的字段下搜索 searchWords[0] 指定的字符串, 在指定字段中的任一字段中搜索到时即满足条件(并集)。如:searchWords: ['b'], + * searchKeys: ['firstName', 'lastName'], 则在 firstName 字段包含 'b',或在 lastName 字段包含 'b' 时本行数据即满足搜索条件。 + * + * 当 searchWords 长度大于 1 且 searchKeys 与 searchWords 长度相等时,在 searchKeys 指定的字段下搜索 searchWords 对应(按索引顺序)指定的字符串,在指定字段中的所有字段 + * 搜索到对应字符串时才满足条件(交集)。如:searchWords: ['b', '18'], searchKeys: ['firstName', 'age'], 则在 firstName 字段包含 'b' 且在 age 字段包含 '18' 时本行数据才满足搜索条件。 + */ + @Input() searchWords: Array = null; + /** + * 搜索的字段范围 + */ + @Input() searchKeys: Array = null; // 是否使用null + /** + * 搜索时精确匹配(等于)的字段范围, 其值必须是属性 searchKeys 指定字段的范围的子集,不设置时默认按 searchKeys 指定字段范围进行模糊搜索 + * + */ + @Input() searchStrictKeys: Array = null; + /** + * 是否在展开当前行时关闭其他行 + */ + @Input() closeOtherDetails: boolean = false; + /** + * 分页、排序和搜索状态改变时触发的回调,参数为 ti-table 组件实例,一般用于远程加载数据 + * + */ + @Output() readonly stateUpdate: EventEmitter = new EventEmitter(); + /** + * @ignore + */ + public hostEle: Element; + protected versionInfo: string = super.getVersion(packageInfo); + private srcDataDiffer: IterableDiffer; + private data: Array; + private promise: Promise = Promise.resolve(null); + /** + * @ignore 判断是否使用了ti-head-sort组件 + */ + public enableSort: boolean = false; + /** + * @ignore + */ + public paginationSubject: Subject = new Subject(); + /** + * @ignore + */ + public viewInitSubject: Subject = new Subject(); + /** + * @ignore + */ + public containerScrollXChangeSubject: Subject = new Subject(); + /** + * @ignore + */ + public thResizeSubject: Subject = new Subject(); + /** + * @ignore + */ + public updateFixedThLeftSubject: Subject = new Subject(); + /** + * @ignore + */ + public updateFixedTdLeftSubject: Subject = new Subject(); + /** + * @ignore + */ + public updateColumnsSubject: Subject> = new Subject>(); + /** + * @ignore + * 其中包含了sort、search、pagination三个属性,每个属性都描述了当前表格数据的参数状态: + * + * sort:object类型,包含了sortKey(类型为string,进行排序的数据属性)、asc(类型为boolean,是否为升序)属性; + * + * search: object类型,包含了searchWords(类型为array,搜索字符串集合)、searchKeys(类型为Array,搜索指定的字段范围)属性; + * + * pagination: object类型,包含了currentPage(类型number,表示当前页)、itemsPerPage(类型为Number,每页显示条数)属性。 + */ + public dataState: TiTableDataState = { + search: { searchWords: null, searchKeys: null, searchStrictKeys: null }, + sort: { sortKey: '', asc: null }, + pagination: { currentPage: -1, itemsPerPage: -1 } + }; + /** + * @ignore 子元素需要访问,所以public + */ + public localStorageState: { [propName: string]: any }; + /** + * @ignore 子元素需要访问,所以public + */ + public sessionStorageState: { [propName: string]: any }; + private isDataStateChange: boolean = false; + private oldSearchWords: Array = null; + private oldSearchKeys: Array = null; + private oldPagination: { currentPage: number; itemsPerPage: number } = { + currentPage: -1, + itemsPerPage: -1 + }; + private oldSort: { sortKey: string; asc: boolean } = { + sortKey: '', + asc: null + }; + /** + * 触发 stateUpdate 的事件(即引起表格数据状态改变的起因事件),其有"search", "sort", "pagination"这三个值 + */ + private triggerEvent: 'search' | 'sort' | 'pagination'; + private searchedResult: Array = []; + private customCompareFn: (a: TiTableRowData, b: TiTableRowData, sortKey?: string) => number; + /** + * @ignore + * 表格是否为表头锁定 + */ + public isFixedHead: boolean = false; + /** + * @ignore + */ + public theadContainer: Element; + /** + * @ignore + */ + public tbodyContainer: Element; + private oldTbodyHeight: number = 0; + private tbodyResizeObserver: any; + private unlistenFixedHeadWindowResize: () => void; + private unlistenFixedColumnWindowResize: () => void; + private unlistenWindowBeforeunload: () => void; + /** + * @ignore + */ + public processYScrollStateChangeWithColsResizable: () => void; + /** + * @ignore + */ + public fixedColumnInfo: { + hasFixedColumn?: boolean; + container?: any; + thFixedRight?: any; + containerWidth?: number; + fixedColumnLeftValues?: Array; + columnFixedLeftLastIndex?: number; + } = {}; + + constructor( + private iterableDiffers: IterableDiffers, + elementRef: ElementRef, + private fixedHeadService: TiTableFixedHeadService, + renderer: Renderer2, + private zone: NgZone, + private tiRenderer: TiRenderer, + private changeRef: ChangeDetectorRef + ) { + super(elementRef, renderer); + this.hostEle = elementRef.nativeElement; + } + + /** + * 测试某一字串(testStr)中是否存在子串(key) + * @param testStr 待检索的字符串或数值 + * @param key 检索的子串 + */ + private static isMatched(testStr: any, keywords: string, isStrict: boolean): boolean { + if (!Util.isString(testStr) && !Util.isNumber(testStr)) { + return false; + } + + if (isStrict) { + return keywords === '' || String(testStr) === keywords; + } else { + return String(testStr).toLowerCase().indexOf(keywords.toLowerCase()) >= 0; + } + } + + private static searchByOneWord( + src: Array, + searchWords: Array, + searchKeys: Array, + searchStrictKeys: Array + ): Array { + const output: Array = []; + src.forEach((item: TiTableRowData) => { + if (searchKeys && searchKeys.length > 0) { + for (const key of searchKeys) { + if (TiTableComponent.isMatched(item[key], searchWords[0], searchStrictKeys?.includes(key))) { + output.push(item); + break; + } + } + } else { + for (const key in item) { + if (TiTableComponent.isMatched(item[key], searchWords[0], searchStrictKeys?.includes(key))) { + output.push(item); + break; + } + } + } + }); + + return output; + } + + private static searchByMoreThanOneWord( + src: Array, + searchWords: Array, + searchKeys: Array, + searchStrictKeys: Array + ): Array { + const output: Array = []; + src.forEach((item: TiTableRowData) => { + let isMatched: boolean = true; + for (let i: number = 0; i < searchKeys.length; i++) { + if (!TiTableComponent.isMatched(item[searchKeys[i]], searchWords[i], searchStrictKeys?.includes(searchKeys[i]))) { + isMatched = false; + break; + } + } + if (isMatched) { + output.push(item); + } + }); + + return output; + } + + private static getSearchedData( + inputData: Array, + searchWords: Array, + searchKeys: Array, + searchStrictKeys: Array + ): Array { + let outData: Array = []; + + if (searchWords && searchWords.length === 1) { + outData = TiTableComponent.searchByOneWord(inputData, searchWords, searchKeys, searchStrictKeys); + } else if (searchWords && searchWords.length > 1 && searchKeys && searchKeys.length === searchWords.length) { + outData = TiTableComponent.searchByMoreThanOneWord(inputData, searchWords, searchKeys, searchStrictKeys); + } + + return outData; + } + + private static doCompare(a: any, b: any): number { + let result: number = 0; + if (a.type === b.type) { + if (a.value !== b.value) { + result = a.value < b.value ? -1 : 1; + } + } else { + result = a.type < b.type ? -1 : 1; + } + + return result; + } + + private static getPredicateValue(value: TiTableRowData, predicate: string): any { + if (!Util.isEmptyString(predicate) && Object.prototype.hasOwnProperty.call(value, predicate)) { + return value[predicate]; + } + + return value; + } + + private static getPredicateValueObj(value: any, index: number): { value: any; type: string } { + let resultVal: any = value; + if (value === null) { + resultVal = 'null'; + } else if (Util.isString(value)) { + resultVal = value.toLowerCase(); + } else if (Util.isArray(value)) { + resultVal = value.toString(); + } else if (typeof value === 'object') { + resultVal = index; + } else if (Util.isFunction(value)) { + const resultValue: any = value(); + + return TiTableComponent.getPredicateValueObj(resultValue, index); + } + const type: string = typeof resultVal; + + return { value: resultVal, type }; + } + + private static safeCopy(data: Array): Array { + return data ? [].concat(data) : []; + } + + private static isValueEqual(n: any, o: any): boolean { + if (!(n instanceof Object) || !(o instanceof Object)) { + return n === o; + } + + if (Util.isArray(n) && Util.isArray(o)) { + if (n.length !== o.length) { + return false; + } else { + for (let i: number = 0; i < o.length; i++) { + if (n[i] !== o[i]) { + return false; + } + } + } + + return true; + } + + if (Object.keys(n).length !== Object.keys(o).length) { + return false; + } else { + for (const key in n) { + if (!Object.prototype.hasOwnProperty.call(o, key) || n[key] !== o[key]) { + return false; + } + } + } + + return true; + } + + ngOnInit(): void { + super.ngOnInit(); + if (!this.srcData) { + this.srcData = TiTableComponent.DEFAULT_SRC_DATA; + } + if (!this.srcData.data) { + this.srcData.data = []; + } + this.srcDataDiffer = this.iterableDiffers.find(this.srcData.data).create(); + + if (this.searchWords !== null) { + this.oldSearchWords = TiTableComponent.safeCopy(this.searchWords); + this.dataState.search.searchWords = this.searchWords; + } + + if (this.searchKeys !== null) { + this.oldSearchKeys = TiTableComponent.safeCopy(this.searchKeys); + this.dataState.search.searchKeys = this.searchKeys; + } + if (this.searchStrictKeys !== null) { + this.dataState.search.searchStrictKeys = this.searchStrictKeys; + } + // 表格记忆 + if (this.storageId) { + this.initStorageDataState(); + } + } + + ngAfterViewInit(): void { + this.initFixedHead(); + if (this.fixedColumnInfo.hasFixedColumn) { + this.addFixedColumnBehavior(); + } + this.viewInitSubject.next(null); + if (this.storageId) { + // 在ngOnDestroy 无法保存,所有使用beforeunload + // 为什么不用unload? beforeunload 兼容比unload效果好 + // 参考:https://sinaad.github.io/xfe/2016/06/29/beforeunlod-vs-unload/ + // 修复SSR报错:ERROR ReferenceError: window is not defined + if (typeof window === 'undefined') { + return; + } + this.unlistenWindowBeforeunload = this.renderer.listen(window, 'beforeunload', () => { + this.updateStorageDataState(); + }); + } + } + + // 为了确保表格内嵌组件(比如tiHeadSort,tiPagination等)的数据变化在表格组件中能监听到,排序、搜索、分页 + // 的相关数据监听需要放在表格组件的AfterViewChecked周期中。因为表格组件的AfterViewChecked生命周期晚于 + // 内嵌组件(比如tiHeadSort,tiPagination等)的各个checked生命周期。 + // eslint-disable-next-line complexity + ngAfterViewChecked(): void { + // 监听srcData.data的变化 + const dataArr: Array = this.srcData && this.srcData.data ? this.srcData.data : []; + const srcDataDiffer: IterableChanges = this.srcDataDiffer.diff(dataArr); + if (srcDataDiffer) { + this.updateSafeCopy(); + this.data2displayed(); + } + + // 监听search相关接口值变化 + if (this.searchWords !== null && !TiTableComponent.isValueEqual(this.searchWords, this.oldSearchWords)) { + this.oldSearchWords = TiTableComponent.safeCopy(this.searchWords); + this.isDataStateChange = true; + this.triggerEvent = 'search'; + this.dataState.search.searchWords = this.searchWords; + // 搜索时,如果使用分页时,把用于表格内部计算分页的当前页码置为1 + if (this.dataState.pagination.itemsPerPage !== -1 && this.dataState.pagination.currentPage !== 1) { + this.dataState.pagination.currentPage = 1; + // 使用Promise是为了解决使用stateUpdate接口做后台分页+后台搜索时, + // 分页组件ExpressionChangedAfterItHasBeenCheckedError报错问题。 + // 此处不能使用setTimeout来解决,使用setTimeout会导致stateUpdate多次执行。 + this.promise.then(() => { + this.paginationSubject.next({ currentPage: 1 }); + }); + } + } + + if (this.searchKeys !== null && this.searchWords.length > 0 && !TiTableComponent.isValueEqual(this.searchKeys, this.oldSearchKeys)) { + this.oldSearchKeys = TiTableComponent.safeCopy(this.searchKeys); + this.isDataStateChange = true; + this.triggerEvent = 'search'; + this.dataState.search.searchKeys = this.searchKeys; + // 搜索时,如果使用分页时,把用于表格内部计算分页的当前页码置为1 + if (this.dataState.pagination.itemsPerPage !== -1 && this.dataState.pagination.currentPage !== 1) { + this.dataState.pagination.currentPage = 1; + this.promise.then(() => { + this.paginationSubject.next({ currentPage: 1 }); + }); + } + } + + if (!TiTableComponent.isValueEqual(this.dataState.sort, this.oldSort)) { + this.oldSort = { ...this.dataState.sort }; + this.isDataStateChange = true; + this.triggerEvent = 'sort'; + } + + if ( + this.dataState.pagination.currentPage !== -1 && + this.dataState.pagination.itemsPerPage !== -1 && + !TiTableComponent.isValueEqual(this.dataState.pagination, this.oldPagination) + ) { + this.oldPagination = { ...this.dataState.pagination }; + this.isDataStateChange = true; + this.triggerEvent = 'pagination'; + } + + if (this.isDataStateChange) { + this.updateTable(); + this.isDataStateChange = false; + } + + // 表格容器宽度改变时,列宽和横向滚动条状态都可能发生改变,这时需要处理表格列固定 + if (this.fixedColumnInfo.hasFixedColumn) { + const containerWidth: number = this.fixedColumnInfo.container.clientWidth; + if (containerWidth !== this.fixedColumnInfo.containerWidth) { + this.fixedColumnInfo.containerWidth = containerWidth; + this.processFixedColumn(); + } + } + } + + ngOnDestroy(): void { + if (this.isFixedHead && this.unlistenFixedHeadWindowResize) { + this.unlistenFixedHeadWindowResize(); + } + + if (this.unlistenFixedColumnWindowResize) { + this.unlistenFixedColumnWindowResize(); + } + + if (this.tbodyResizeObserver) { + this.tbodyResizeObserver.disconnect(); + } + + if (this.storageId) { + // 路由跳转时,页面没有刷新,beforeunload监听不生效,所以要更新一次记忆数据。 + this.updateStorageDataState(); + // 修复SSR错误:TypeError: this.unlistenWindowBeforeunload is not a function + typeof this.unlistenWindowBeforeunload === 'function' && this.unlistenWindowBeforeunload(); + } + } + // 初始化表格记忆 + private initStorageDataState(): void { + // 修复SSR报错:ERROR ReferenceError: window is not defined + if (typeof window === 'undefined') { + return; + } + // 缓存在sessionStorage中的数据 + let sessionStorageState = window.sessionStorage.getItem(this.storageId); + // 缓存在localStorage 中的数据 + let localStorageState = window.localStorage.getItem(this.storageId); + + // 版本发布后,用户第一次访问时,sessionStorageState、localStorageState是null + this.sessionStorageState = sessionStorageState ? JSON.parse(sessionStorageState) : {}; + this.localStorageState = localStorageState ? JSON.parse(localStorageState) : {}; + } + // 更新本地存储中的 + private updateStorageDataState() { + const localStorageState = { ...this.localStorageState }; + const sessionStorageState = { ...this.sessionStorageState }; + + // 排序 + if (this.storageConfig.sort && this.dataState.sort.sortKey) { + sessionStorageState['sort'] = this.dataState.sort; + } + + // 表格分页 + if ( + (this.storageConfig.pagination === true || + (this.storageConfig.pagination && this.storageConfig.pagination['itemsPerPage'] === true)) && + this.dataState.pagination.itemsPerPage !== -1 + ) { + // 每页条数 + localStorageState['itemsPerPage'] = this.dataState.pagination.itemsPerPage; + } + // 表格分页 + if ( + (this.storageConfig.pagination === true || + (this.storageConfig.pagination && this.storageConfig.pagination['currentPage'] === true)) && + this.dataState.pagination.currentPage !== -1 + ) { + // 当前页 + sessionStorageState['currentPage'] = this.dataState.pagination.currentPage; + } + + // 表格搜索 + if (this.dataState.search.searchKeys) { + sessionStorageState['searchKeys'] = this.dataState.search.searchKeys; + } + if (this.dataState.search.searchWords) { + sessionStorageState['searchWords'] = this.dataState.search.searchWords; + } + if (this.dataState.search.searchStrictKeys) { + sessionStorageState['searchStrictKeys'] = this.dataState.search.searchStrictKeys; + } + + if (Object.keys(sessionStorageState).length > 0) { + // 修复:SSR报错:ReferenceError: window is not defined + typeof window !== 'undefined' && window.sessionStorage.setItem(this.storageId, JSON.stringify(sessionStorageState)); + } + if (Object.keys(localStorageState).length > 0) { + typeof window !== 'undefined' && window.localStorage.setItem(this.storageId, JSON.stringify(localStorageState)); + } + } + + /** + * @ignore + */ + public updatePagination(currentPage: number, itemsPerPage: number): void { + if (this.oldPagination.currentPage === -1 && this.oldPagination.itemsPerPage === -1) { + this.oldPagination = { + currentPage, + itemsPerPage + }; + } + this.dataState.pagination.currentPage = currentPage; + this.dataState.pagination.itemsPerPage = itemsPerPage; + } + + /** + * @ignore + * 排序状态更新及处理 + * + * @param sortKey - 进行排序的数据属性(如对数据的name属性值进行排序,则传入"name") + * @param asc - 是否为升序,true表示升序,false表示降序,null表示无序 + * @param isDefaultSort - 是否为默认排序 + */ + public updateSort( + sortKey: string, + asc: boolean, + isDefaultSort: boolean, + compareFn: (a: TiTableRowData, b: TiTableRowData, sortKey?: string) => number + ): void { + if (isDefaultSort) { + this.oldSort = { + sortKey, + asc + }; + } + this.dataState.sort.sortKey = sortKey; + this.dataState.sort.asc = asc; + this.customCompareFn = compareFn; + } + /** + * 获取表格当前的数据状态(sort、search、pagination) + * @return TiTableDataState + */ + public getDataState(): TiTableDataState { + return this.dataState; + } + + /** + * 获取当前触发 stateUpdate 回调的操作(即引起表格数据状态改变的起因操作),有 "search", "sort", "pagination" 这三个值 + * @return 有"search", "sort", "pagination"这三个值 + */ + public getTriggerEvent(): string { + return this.triggerEvent; + } + + /** + * 使用搜索功能时,获取搜索的结果数据 + * @return 搜索到的数据集合 + * + */ + public getSearchedResult(): Array { + return this.searchedResult; + } + + private updateTable(): void { + if (this.isFrontPagination() || this.isFrontSort() || this.isFrontSearch()) { + this.data2displayed(); + } + // 延时的目的是防止stateUpdate 回调中同步修改数据时报错。 + this.promise.then(() => { + this.stateUpdate.emit(this); + }); + } + + /** + * 根据数据状态(search,sort,pagination)处理数据 + */ + private dataProcessor(data: Array): Array { + let output = data; + // 如果存在过滤条件且开发者传递的源数据尚未进行过滤处理,则进行前台过滤处理 + if (this.isFrontSearch()) { + output = TiTableComponent.getSearchedData(output, this.searchWords, this.searchKeys, this.searchStrictKeys); + this.searchedResult = [].concat(output); + const totalNumber: number = output.length; + // 如果存在分页且开发者传递的源数据尚未进行分页处理,即进行前台分页, + if (this.isFrontPagination()) { + this.promise.then(() => { + this.paginationSubject.next({ totalNumber }); + }); + } + } + // 如果存在排序且开发者传递的源数据尚未进行排序处理,则进行前台排序处理(字典排序) + if (this.isFrontSort()) { + output = this.getSortedData(output, this.dataState.sort.sortKey, this.dataState.sort.asc); + } + + // 存在分页且开发者传递的源数据尚未进行分页处理,则进行前台分页 + if (this.isFrontPagination()) { + const pagination: { currentPage: number; itemsPerPage: number } = this.dataState.pagination; + const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage; + output = output.slice(start, start + parseInt(pagination.itemsPerPage.toString(), 10)); + } + + return output; + } + /** + * data => displayedData,处理分3步: + * 1. 复制data数据 + * 2. 根据 分页、过滤、排序 处理数据 + * 3. 通过displayedDataChange 事件更新到displayedData + */ + private data2displayed() { + // 复制源数据 + let output: Array = TiTableComponent.safeCopy(this.data); + + // 处理数据 + output = this.dataProcessor(output); + + // 更新到displayedData + this.promise.then(() => { + // 同步修改内部数据 + this.displayedData = output; + this.displayedDataChange.emit(output); + }); + } + private isFrontSearch(): boolean { + const srcState: TiTableSrcState = this.getSrcState(); + return this.searchWords && this.searchWords.length > 0 && (!srcState || !srcState.searched); + } + private isFrontSort(): boolean { + const srcState: TiTableSrcState = this.getSrcState(); + return this.enableSort && (!srcState || !srcState.sorted); + } + private isFrontPagination(): boolean { + const srcState: TiTableSrcState = this.getSrcState(); + const pagination: { currentPage: number; itemsPerPage: number } = this.dataState.pagination; + return pagination.currentPage !== -1 && pagination.itemsPerPage !== -1 && (!srcState || !srcState.paginated); + } + + private getSortedData(data: Array, predicate: string, asc: boolean): Array { + let resultArr: Array = []; + if (!Util.isArray(data)) { + return resultArr; + } + + // 无序状态 + if (!predicate || asc === null) { + return data; + } + + const compareArr: Array<{ + value: TiTableRowData; + index: number; + predicateValueObj: { value: any; type: string }; + }> = data.map((value: TiTableRowData, index: number) => { + const predicateValue: any = TiTableComponent.getPredicateValue(value, predicate); + + return { + value, + index, + predicateValueObj: TiTableComponent.getPredicateValueObj(predicateValue, index) + }; + }); + compareArr.sort(this.compareTo); + resultArr = compareArr.map((item: { value: TiTableRowData; index: number; predicateValueObj: { value: any; type: string } }) => { + return item.value; + }); + if (!asc) { + resultArr.reverse(); + } + + return resultArr; + } + + private compareTo = ( + v1: { + value: TiTableRowData; + index: number; + predicateValueObj: { value: any; type: string }; + }, + v2: { + value: TiTableRowData; + index: number; + predicateValueObj: { value: any; type: string }; + } + ): number => { + let result: number = 0; + if (Util.isFunction(this.customCompareFn)) { + result = this.customCompareFn(v1.value, v2.value, this.dataState.sort.sortKey); + } else { + result = TiTableComponent.doCompare(v1.predicateValueObj, v2.predicateValueObj); + if (result === 0) { + result = v1.index - v2.index; + } + } + + return result; + }; + + private updateSafeCopy(): void { + this.data = TiTableComponent.safeCopy(this.srcData && this.srcData.data ? this.srcData.data : []); + } + + private getSrcState(): TiTableSrcState { + return this.srcData && this.srcData.state ? this.srcData.state : undefined; + } + + /** + * @ignore + */ + public closeOtherDetailsFn(currentRow: TiTableRowData): void { + this.displayedData.forEach((row: TiTableRowData) => { + if (row !== currentRow && row['showDetails']) { + // 保证其他详情展开能收起 + this.promise.then(() => { + row['showDetails'] = false; + this.changeRef.markForCheck(); // onpush模式下手动触发,详情收起 + }); + } + }); + } + + private initFixedHead(): void { + this.theadContainer = this.fixedHeadService.getTheadContainer(this.hostEle); + if (!this.theadContainer) { + return; + } + this.tbodyContainer = this.fixedHeadService.getTbodyContainer(this.theadContainer); + this.isFixedHead = !Util.isUndefined(this.theadContainer) && !Util.isUndefined(this.tbodyContainer); + + if (this.isFixedHead) { + this.renderer.listen(this.tbodyContainer, 'scroll', () => { + const scrollLeft: number = this.tbodyContainer.scrollLeft; + this.renderer.setStyle(this.theadContainer.children[0], 'marginLeft', -scrollLeft + 'px'); + Util.trigger(document, 'tiScroll'); + }); + + this.unlistenFixedHeadWindowResize = this.renderer.listen('window', 'resize', () => { + this.fixedHeadService.processOverflowY(this.theadContainer, this.tbodyContainer, this); + }); + + if ((window as any).ResizeObserver && this.tbodyContainer.children[0]) { + // 利用 ResizeObserver 来监听表体的尺寸发生改变的时机。IE不支持 ResizeObserver。 + this.tbodyResizeObserver = new (window as any).ResizeObserver((entries: any): void => { + if (entries[0] && entries[0].contentRect.height !== this.oldTbodyHeight) { + // 如果表头锁定+列拖动;在 TiColsResizableDirective 的 oninit 时会对该方法赋值;否则为 undefined + if (this.processYScrollStateChangeWithColsResizable) { + this.processYScrollStateChangeWithColsResizable(); + } else { + this.fixedHeadService.processOverflowY(this.theadContainer, this.tbodyContainer, this); + } + this.fixedHeadService.removeTbodyContainerBorderBottom(this.tbodyContainer, this.displayedData); + this.oldTbodyHeight = entries[0].contentRect.height; + } + }); + this.tbodyResizeObserver.observe(this.tbodyContainer.children[0]); + } + } + } + private addFixedColumnBehavior(): void { + const container: any = this.tiRenderer.findChildrenByClassName(this.hostEle, 'ti3-table-container')[0]; + if (!container) { + return; + } + this.zone.runOutsideAngular(() => { + this.renderer.listen(container, 'scroll', () => { + const scrollLeft: number = container.scrollLeft; + const isRightColumnFloat: boolean = scrollLeft + container.clientWidth < container.scrollWidth; + this.containerScrollXChangeSubject.next({ + scrollLeft, + isRightColumnFloat + }); + }); + + this.unlistenFixedColumnWindowResize = this.renderer.listen('window', 'resize', () => { + const containerWidth: number = this.fixedColumnInfo.container.clientWidth; + if (containerWidth !== this.fixedColumnInfo.containerWidth) { + this.fixedColumnInfo.containerWidth = containerWidth; + this.processFixedColumn(); + } + }); + }); + } + + /** + * @ignore + */ + public processFixedColumn(): void { + const scrollLeft: number = this.fixedColumnInfo.container.scrollLeft; + const isRightColumnFloat: boolean = + scrollLeft + this.fixedColumnInfo.container.clientWidth < this.fixedColumnInfo.container.scrollWidth; + this.containerScrollXChangeSubject.next({ + scrollLeft, + isRightColumnFloat + }); + this.updateFixedThLeftSubject.next(null); + this.updateFixedTdLeftSubject.next(null); + } + + /** + * @ignore + */ + public needTr(tr: any): boolean { + if (!tr) { + return false; + } + const classes: Array = [ + 'ti3-details-tr', + 'ti3-table-nodata', + 'ti3-table-loadfail', + 'ti3-table-nodata-guide', + 'ti3-table-nodata-simple' + ]; + for (const className of classes) { + if (this.tiRenderer.hasClass(tr, className)) { + return false; + } + } + + return true; + } +} diff --git a/src/table/lib/src/TiTableFixedHeadService.ts b/src/table/lib/src/TiTableFixedHeadService.ts new file mode 100644 index 0000000..c787192 --- /dev/null +++ b/src/table/lib/src/TiTableFixedHeadService.ts @@ -0,0 +1,290 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiTableFixedHeadServiceModule } from './TiTableFixedHeadServiceModule'; +import { ResizableOpts, TiColsResizableDirective } from './TiColsResizableDirective'; +import { TiTableComponent } from './TiTableComponent'; +/** + * @ignore + * 提供表头锁定功能 + */ +@Injectable({ + providedIn: TiTableFixedHeadServiceModule +}) +export class TiTableFixedHeadService { + private renderer: Renderer2; + constructor(rendererFactory: RendererFactory2, private tiRenderer: TiRenderer) { + this.renderer = rendererFactory.createRenderer(null, null); + } + + public getTheadContainer(hostEle: any): any { + return this.tiRenderer.findChildrenByClassName(hostEle, 'ti3-table-fixed-head')[0]; + } + + public getTbodyContainer(theadContainer: any): any { + const tbodyContainer: any = theadContainer.nextElementSibling; + if (tbodyContainer && this.tiRenderer.hasClass(tbodyContainer, 'ti3-table-container')) { + return tbodyContainer; + } + + return; + } + + public processOverflowY(theadContainer: any, tbodyContainer: any, tableCom: TiTableComponent): void { + const headTable: any = theadContainer.children[0]; + const scrollBarWidth: number = this.getScrollBarWidth('Y'); + const isOverflowedY: boolean = this.overflowedResult(tbodyContainer, 'Y'); + const isOverflowedX: boolean = this.overflowedResult(tbodyContainer, 'X'); + // 根据表体的宽度来设置表头的宽度 + if (isOverflowedY && !isOverflowedX) { + this.renderer.setStyle(headTable, 'width', `calc(100% - ${scrollBarWidth}px)`); + } else { + this.renderer.setStyle(headTable, 'width', '100%'); + } + + const fixheadFiller: any = this.tiRenderer.findChildrenByClassName(theadContainer, 'ti3-table-fixed-head-filler')[0]; + // 1px为经验值,避免计算的误差导致填充块和表头之间有一点空白间隔 + const fillerWidth: number = scrollBarWidth + 1; + // 根据Y轴是否有滚动条来控制表头右侧的填充块的有无 + if (isOverflowedY) { + if (tableCom.fixedColumnInfo.thFixedRight) { + this.renderer.setStyle(tableCom.fixedColumnInfo.thFixedRight, 'right', `${scrollBarWidth}px`); + } + + if (fixheadFiller) { + this.renderer.setStyle(fixheadFiller, 'width', fillerWidth + 'px'); + + return; + } + + const headerFiller: any = document.createElement('div'); + this.renderer.addClass(headerFiller, 'ti3-table-fixed-head-filler'); + this.renderer.setStyle(headerFiller, 'width', fillerWidth + 'px'); + theadContainer.appendChild(headerFiller); + } else if (fixheadFiller) { + this.renderer.removeChild(theadContainer, fixheadFiller); + if (tableCom.fixedColumnInfo.thFixedRight) { + this.renderer.setStyle(tableCom.fixedColumnInfo.thFixedRight, 'right', 0); + } + } + } + + public removeTbodyContainerBorderBottom(tbodyContainer: any, displayedData: Array): void { + const tbody: any = tbodyContainer.children[0].children[1]; + const hasNodataTbody: any = this.tiRenderer.findChildrenByClassName(tbody, 'ti3-table-nodata').length > 0; + const hasNodataGuideTbody: any = this.tiRenderer.findChildrenByClassName(tbody, 'ti3-table-nodata-guide').length > 0; + const hasLoadfailTbody: any = this.tiRenderer.findChildrenByClassName(tbody, 'ti3-table-loadfail').length > 0; + const hasNodataSimpleTbody: any = this.tiRenderer.findChildrenByClassName(tbody, 'ti3-table-nodata-simple').length > 0; + // 判断是否包含表格无数据,有图片是大间距,其他场景全部为单行文字样式,包括以下四种情况; + // 1、表格无数据:ti-table-nodata(有图片); + // 2、查询内容为空,提示用户'购买商品':ti-table-nodata-guide; + // 3、加载不成功,提示'重新加载':ti-table-loadfail。 + // 4. 表格无数据:ti-table-nodata-simple,只有文字提示(无图片) + const noBorderTerm: any = hasNodataTbody || hasNodataGuideTbody || hasLoadfailTbody || hasNodataSimpleTbody; + + if (displayedData.length === 0 && noBorderTerm) { + this.renderer.setStyle(tbodyContainer, 'border-bottom', 'none'); + } else { + this.renderer.setStyle(tbodyContainer, 'border-bottom', ''); + } + } + + // 由于用scrollHeight计算在IE和火狐下偶尔有1px的误差, + // 所以此处直接获取表格更准确的高度和容器的clientHeight作对比 + public overflowedResult(tbodyContainer: any, direction: string, isNum?: boolean): any { + let containerSize: number = 0; + let tbodySize: number = 0; + const tbodyTable: any = tbodyContainer.children[0]; + if (direction === 'X') { + containerSize = tbodyContainer.clientWidth; + // 修复SSR错误:ERROR TypeError: tbodyTable.getBoundingClientRect is not a function + if (typeof tbodyTable.getBoundingClientRect !== 'function') { + return; + } + tbodySize = this.numRound(tbodyTable.getBoundingClientRect().width, 0); + } else { + containerSize = tbodyContainer.clientHeight; + // 修复SSR错误:ERROR TypeError: tbodyTable.getBoundingClientRect is not a function + if (typeof tbodyTable.getBoundingClientRect !== 'function') { + return; + } + tbodySize = this.numRound(tbodyTable.getBoundingClientRect().height, 0); + } + + // 若isNum为true,返回差值,否则返回布尔值 + return isNum ? tbodySize - containerSize : tbodySize > containerSize; + } + + public processYScrollStateChange(colsResizableDire: TiColsResizableDirective): void { + const options: ResizableOpts = colsResizableDire.resizableOpts; + // 主要处理 columns 初始异步的场景 + if (!options.ths || options.ths.length === 0) { + return; + } + const tbodyContainer: any = colsResizableDire.tableCom.tbodyContainer; + const theadContainer: any = colsResizableDire.tableCom.theadContainer; + const newYScrollState: boolean = this.overflowedResult(tbodyContainer, 'Y'); + if (newYScrollState === options.yScrollState) { + this.processOverflowY(theadContainer, tbodyContainer, colsResizableDire.tableCom); + + return; + } + + const newXScrollState: boolean = this.overflowedResult(tbodyContainer, 'X'); + let lastColIndex: number = options.ths.length - 1; + let lastTh: any = options.ths[lastColIndex]; + let lastThWidth: number = options.storedSizes[lastColIndex]; + const scrollBarWidth: number = this.getScrollBarWidth('Y'); + // 纵向滚动条由无到有 + if (newYScrollState) { + if (newXScrollState && newXScrollState === options.xScrollState) { + options.storeTableWidthChange += scrollBarWidth; + } else { + // 第一次纵向滚动条状态发生变化要减小表格宽度时,去减小非特殊列中宽度最大的一列的宽度 + // 这样处理是为了避免最后一列的宽度变小 + if (colsResizableDire.isfirstYScrollStateChange) { + lastTh = this.getMaxWidthTh(colsResizableDire); + if (lastTh) { + lastColIndex = parseInt(lastTh.getAttribute('ti-visible-index'), 10); + lastThWidth = options.storedSizes[lastColIndex]; + } + } + options.storedSizes[lastColIndex] = lastThWidth - scrollBarWidth; + } + + // 纵向滚动条由有到无 + } else { + if (newXScrollState === options.xScrollState) { + if (newXScrollState) { + options.storeTableWidthChange -= scrollBarWidth; + } else { + options.storedSizes[lastColIndex] = lastThWidth + scrollBarWidth; + } + } else if (!newXScrollState) { + options.storedSizes[lastColIndex] = lastThWidth + (scrollBarWidth - options.storeTableWidthChange); + options.storeTableWidthChange = 0; + } + } + if (lastTh) { + colsResizableDire.setWidth(lastTh, options.storedSizes[lastColIndex]); + colsResizableDire.setWidth(options.secondThs[lastColIndex], options.storedSizes[lastColIndex]); + } + + this.processOverflowY(theadContainer, tbodyContainer, colsResizableDire.tableCom); + colsResizableDire.isfirstYScrollStateChange = false; + options.xScrollState = this.overflowedResult(tbodyContainer, 'X'); + options.yScrollState = this.overflowedResult(tbodyContainer, 'Y'); + + // 纵向滚动条状态改变时可能会引起列宽改变和横向滚动条状态改变,所以要处理列固定 + if (colsResizableDire.tableCom.fixedColumnInfo.hasFixedColumn) { + colsResizableDire.tableCom.processFixedColumn(); + } + } + + private getMaxWidthTh(colsResizableDire: TiColsResizableDirective): any { + const options: ResizableOpts = colsResizableDire.resizableOpts; + colsResizableDire.updateStoredSizes(); + + const ths: Array = options.ths.filter((th: any) => { + // 排除最后一列和不可拖动的列 + return !th.hasAttribute(colsResizableDire.notResizableAttr) && !colsResizableDire.isLastColumn(th); + }); + + // 按列宽从小到大排序 + const sortedThs: Array = ths.sort((a: any, b: any) => { + const indexA: number = parseInt(a.getAttribute('ti-visible-index'), 10); + const indexB: number = parseInt(b.getAttribute('ti-visible-index'), 10); + + return options.storedSizes[indexA] - options.storedSizes[indexB]; + }); + + // 返回宽度最大的列 + return sortedThs[sortedThs.length - 1]; + } + + // 处理在列拖动过程中横向滚动条的出现引起纵向滚动条出现时的账本(storeColsWidthChange)的变化 + public handleYOverflowedWithX(resizableOpts: ResizableOpts, tbodyContainer: any): void { + const newYScrollState: boolean = this.overflowedResult(tbodyContainer, 'Y'); + + if (newYScrollState === resizableOpts.yScrollState) { + return; + } + + const scrollBarWidth: number = this.getScrollBarWidth('Y'); + const newXScrollState: boolean = this.overflowedResult(tbodyContainer, 'X'); + if (newYScrollState) { + resizableOpts.storeTableWidthChange += scrollBarWidth; + resizableOpts.isYOverflowedWithX = true; + } + + resizableOpts.xScrollState = newXScrollState; + resizableOpts.yScrollState = newYScrollState; + } + + public handleYNotOverflowedWithX(resizableOpts: ResizableOpts, tbodyContainer: any): void { + const scrollBarWidthX: number = this.getScrollBarWidth('X'); + const scrollBarWidthY: number = this.getScrollBarWidth('Y'); + const overflowedHeight: number = this.overflowedResult(tbodyContainer, 'Y', true); + const overflowedWidth: number = this.overflowedResult(tbodyContainer, 'X', true); + // 当列拖动横向滚动条出现导致纵向滚动条出现后,再缩小列宽时,由于无法准确获得横向滚动条消失的时刻去对表格 + // 宽度做补偿,所以当表格列宽到达容器边界附近时,用临时将overflow-x设为hidden的方式使横向滚动条消失。 + if ( + resizableOpts.isYOverflowedWithX && + overflowedHeight > 0 && + overflowedHeight <= scrollBarWidthX && + overflowedWidth > 0 && + overflowedWidth <= scrollBarWidthY + ) { + this.renderer.setStyle(tbodyContainer, 'overflow-x', 'hidden'); + // 对账本(storeTableWidthChange)清零 + resizableOpts.storeTableWidthChange = 0; + resizableOpts.isYOverflowedWithX = false; + + setTimeout(() => { + this.renderer.setStyle(tbodyContainer, 'overflow-x', 'auto'); + }, 0); + } + } + + // 获取滚动条宽度 + public getScrollBarWidth(direction: string): number { + const temporaryDiv: any = document.createElement('div'); + this.tiRenderer.setStyles(temporaryDiv, { + position: 'absolute', + top: '-9999px', + left: '-9999px', + width: '100px', + height: '100px', + overflow: 'hidden' + }); + const nestDiv: any = document.createElement('div'); + this.tiRenderer.setStyles(nestDiv, { width: '100%', height: '100%' }); + temporaryDiv.appendChild(nestDiv); + document.body.appendChild(temporaryDiv); + const noScrollWidth: number = nestDiv.getBoundingClientRect().width; + const noScrollHeight: number = nestDiv.getBoundingClientRect().height; + this.renderer.setStyle(temporaryDiv, 'overflow', 'scroll'); + const scrollWidth: number = nestDiv.getBoundingClientRect().width; + const scrollHeight: number = nestDiv.getBoundingClientRect().height; + this.renderer.removeChild(document.body, temporaryDiv); + const scrollBarWidthX: number = this.numRound(noScrollHeight - scrollHeight, 1); + const scrollBarWidthY: number = this.numRound(noScrollWidth - scrollWidth, 1); + + return direction === 'X' ? scrollBarWidthX : scrollBarWidthY; + } + + // 对浮点数四舍五入保留指定小数位数的数字 + public numRound(num: number, decimal: number): number { + return parseFloat(num.toFixed(decimal)); + } +} diff --git a/src/table/lib/src/TiTableFixedHeadServiceModule.ts b/src/table/lib/src/TiTableFixedHeadServiceModule.ts new file mode 100644 index 0000000..55180e5 --- /dev/null +++ b/src/table/lib/src/TiTableFixedHeadServiceModule.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +/** + * @ignore + */ +@NgModule({ + imports: [] +}) +export class TiTableFixedHeadServiceModule {} diff --git a/src/table/lib/src/TiTableFixedSizeVirtualScrollDirective.ts b/src/table/lib/src/TiTableFixedSizeVirtualScrollDirective.ts new file mode 100644 index 0000000..72f6bd9 --- /dev/null +++ b/src/table/lib/src/TiTableFixedSizeVirtualScrollDirective.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, forwardRef } from '@angular/core'; +import { CdkFixedSizeVirtualScroll, FixedSizeVirtualScrollStrategy, VIRTUAL_SCROLL_STRATEGY } from '@angular/cdk/scrolling'; + +/** + * @ignore + */ +export function fixedSizeVirtualScrollStrategyFactory( + fixedSizeDir: TiTableFixedSizeVirtualScrollDirective +): FixedSizeVirtualScrollStrategy { + return fixedSizeDir._scrollStrategy; +} + +/** + * @ignore + * 配合表格的表体虚拟滚动容器组件 TiTableVirtualScrollViewportComponent 来使用的指令。 + * + * 主要是重新定义了指令选择器selector。 + * + * + * 相关代码参考 @angular/cdk/scrolling 提供的 CdkFixedSizeVirtualScroll 指令。 + * + */ +@Directive({ + selector: '.ti3-table-container[itemSize]', + providers: [ + { + provide: VIRTUAL_SCROLL_STRATEGY, + useFactory: fixedSizeVirtualScrollStrategyFactory, + deps: [forwardRef(() => TiTableFixedSizeVirtualScrollDirective)] + } + ] +}) +export class TiTableFixedSizeVirtualScrollDirective extends CdkFixedSizeVirtualScroll {} diff --git a/src/table/lib/src/TiTableModule.ts b/src/table/lib/src/TiTableModule.ts new file mode 100644 index 0000000..d5050f8 --- /dev/null +++ b/src/table/lib/src/TiTableModule.ts @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ScrollingModule } from '@angular/cdk/scrolling'; +import { TiDateModule } from '@opentiny/ng-date'; +import { TiDatetimeModule } from '@opentiny/ng-datetime'; +import { TiListModule } from '@opentiny/ng-list'; +import { TiSearchboxModule } from '@opentiny/ng-searchbox'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiTableFixedHeadServiceModule } from './TiTableFixedHeadServiceModule'; +import { TiTableComponent } from './TiTableComponent'; +import { TiHeadFilterComponent } from './TiHeadFilterComponent'; +import { TiHeadFilterDropComponent } from './TiHeadFilterDropComponent'; +import { TiHeadSortComponent } from './TiHeadSortComponent'; +import { TiHeadMenuComponent } from './TiHeadMenuComponent'; +import { TiDetailsIconComponent } from './TiDetailsIconComponent'; +import { TiDetailsTrDirective } from './TiDetailsTrDirective'; +import { TiColspanDirective } from './TiColspanDirective'; +import { TiColsToggleComponent } from './TiColsToggleComponent'; +import { TiColsToggleDropComponent } from './TiColsToggleDropComponent'; +import { TiColsResizableDirective } from './TiColsResizableDirective'; +import { TiCellTextComponent } from './TiCellTextComponent'; +import { TiCellIconsComponent } from './TiCellIconsComponent'; +import { TiColumnsPipe } from './TiColumnsPipe'; +import { TiColumnFixedDirective } from './TiColumnFixedDirective'; +import { TiColClickDirective } from './TiColClickDirective'; +import { TiTableVirtualScrollViewportComponent } from './TiTableVirtualScrollViewportComponent'; +import { TiTableFixedSizeVirtualScrollDirective } from './TiTableFixedSizeVirtualScrollDirective'; +import { TiLocaleModule, TiLocale } from '@opentiny/ng-locale'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiButtonModule } from '@opentiny/ng-button'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { locales } from './i18n'; +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiButtonModule, + TiIconModule, + TiListModule, + TiSearchboxModule, + TiDropModule, + TiOverflowModule, + TiTableFixedHeadServiceModule, + TiLocaleModule, + TiRendererModule, + TiTipModule, + TiOutlineModule, + ScrollingModule, + TiDateModule, + TiDatetimeModule + ], + exports: [ + TiTableComponent, + TiHeadFilterComponent, + TiHeadSortComponent, + TiHeadMenuComponent, + TiDetailsIconComponent, + TiDetailsTrDirective, + TiColspanDirective, + TiColsToggleComponent, + TiColsResizableDirective, + TiCellTextComponent, + TiCellIconsComponent, + TiColumnFixedDirective, + TiColClickDirective, + TiTableVirtualScrollViewportComponent, + TiTableFixedSizeVirtualScrollDirective + ], + declarations: [ + TiTableComponent, + TiHeadFilterComponent, + TiHeadFilterDropComponent, + TiHeadSortComponent, + TiHeadMenuComponent, + TiDetailsIconComponent, + TiDetailsTrDirective, + TiColspanDirective, + TiColsToggleComponent, + TiColsToggleDropComponent, + TiColsResizableDirective, + TiCellTextComponent, + TiCellIconsComponent, + TiColumnsPipe, + TiColumnFixedDirective, + TiColClickDirective, + TiTableVirtualScrollViewportComponent, + TiTableFixedSizeVirtualScrollDirective + ] +}) +export class TiTableModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { + TiTableComponent, + TiTableSrcState, + TiTableDataState, + TiTableRowData, + TiTableSrcData, + TiTableColumns, + TiTableStorageConfig, + TiPaginationStorageConfig +} from './TiTableComponent'; +export { TiHeadFilterComponent } from './TiHeadFilterComponent'; +export { TiHeadSortComponent } from './TiHeadSortComponent'; +export { TiHeadMenuComponent } from './TiHeadMenuComponent'; +export { TiDetailsIconComponent } from './TiDetailsIconComponent'; +export { TiDetailsTrDirective } from './TiDetailsTrDirective'; +export { TiColspanDirective } from './TiColspanDirective'; +export { TiColsToggleComponent } from './TiColsToggleComponent'; +export { TiColsResizableDirective } from './TiColsResizableDirective'; +export { TiCellTextComponent } from './TiCellTextComponent'; +export { TiCellIconsComponent } from './TiCellIconsComponent'; +export { TiColumnFixedDirective } from './TiColumnFixedDirective'; +export { TiColClickDirective } from './TiColClickDirective'; +export { TiTableVirtualScrollViewportComponent } from './TiTableVirtualScrollViewportComponent'; +export { fixedSizeVirtualScrollStrategyFactory, TiTableFixedSizeVirtualScrollDirective } from './TiTableFixedSizeVirtualScrollDirective'; diff --git a/src/table/lib/src/TiTableVirtualScrollViewportComponent.ts b/src/table/lib/src/TiTableVirtualScrollViewportComponent.ts new file mode 100644 index 0000000..7c67b11 --- /dev/null +++ b/src/table/lib/src/TiTableVirtualScrollViewportComponent.ts @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewEncapsulation } from '@angular/core'; +import { CdkVirtualForOf, CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { Subject, Subscription } from 'rxjs'; + +/** + * 表格的表体虚拟滚动容器组件 + * + * + * 为了在原有表格表头锁定的基础上简化使用虚拟滚动且不增加dom层级,这里并没有让业务直接使用 @angular/cdk/scrolling 提供的 CdkVirtualScrollViewport() 组件, + * 而是继承 CdkVirtualScrollViewport 来适配 TiTable。 + * + * 相关代码参考 @angular/cdk/scrolling 提供的 CdkVirtualScrollViewport 组件。 + */ +@Component({ + selector: '.ti3-table-container[itemSize]', + templateUrl: './table-virtual-scroll-viewport.html', + styleUrls: ['./table-virtual-scroll-viewport.less'], + host: { + '[class.ti3-table-virtual-scroll-viewport]': 'true', + // _totalContentHeight 该变量虽然以下划线开头,但在 CdkVirtualScrollViewport 类中是 public 的,且在 CdkVirtualScrollViewport 组件的模板中也有使用 + '[style.height]': 'getTotalContentHeight()', + // 初始化无数据时_totalContentHeight为空字符串,需要兼容这种情况 + '[class.ti3-table-virtual-scroll-viewport-nodata]': `getTotalContentHeight() === '0px'` + }, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + // 参考 CdkVirtualScrollViewport 组件的元数据配置 + provide: CdkVirtualScrollViewport, + useExisting: TiTableVirtualScrollViewportComponent + } + ] +}) +export class TiTableVirtualScrollViewportComponent extends CdkVirtualScrollViewport implements OnInit { + private dataStreamSubscription: Subscription; + /** + * + * 虚拟滚动容器中是否有 cdkVirtualFor 指令 + * + * 使用表格时经常会在无数据或数据加载中时将 cdkVirtualFor 指令那块dom销毁隐藏掉(ngIf=false), 而在有数据时才会使用 cdkVirtualFor 指令渲染数据。 + * + * hasCdkVirtualFor 为 true 时表示有 cdkVirtualFor 指令存在, 为 false 时表示此时 cdkVirtualFor 指令不存在。 + */ + private hasCdkVirtualFor: boolean = false; + + ngOnInit(): void { + // 获取 .ti3-table-container 容器的子元素: table 元素 + const tableEle: any = this.elementRef.nativeElement.children[0]; + // 将子元素 table 作为 virtual-scroll-content-wrapper,类比 CdkVirtualScrollViewport 组件中的 .cdk-virtual-scroll-content-wrapper + // 元素,从而做到不增加dom层级 + if (tableEle) { + tableEle.className += 'ti3-table-virtual-scroll-content-wrapper'; + // 覆写 CdkVirtualScrollViewport 类中的 _contentWrapper 变量(该变量在CdkVirtualScrollViewport中是public) + this._contentWrapper = new ElementRef(tableEle); + super.ngOnInit(); + } + } + + /** + * @ignore + * + * cdkVirtualFor 指令在初始化时会调用 CdkVirtualScrollViewport 虚拟滚动容器中的 attach 方法 + * + * 覆写 CdkVirtualScrollViewport 类中的 attach 方法。 + * + * @param forOf cdkVirtualFor结构指令实例 + */ + public attach(forOf: CdkVirtualForOf): void { + super.attach(forOf); + + // 此时 cdkVirtualFor 指令初始化,即存在 dkVirtualFor 指令。 + this.hasCdkVirtualFor = true; + + // 这里可以获取到forOf: CdkVirtualForOf,在forOf.dataStream中可以获取数据改变(数据搜索过滤,排序,切换分页等)的时机, + // 需要在这个时机将虚拟滚动容器滚动条置回顶部。 + this.ngZone.runOutsideAngular(() => { + this.dataStreamSubscription = forOf.dataStream.subscribe((data: any): void => { + this.elementRef.nativeElement.scrollTop = 0; + }); + }); + + // 使用表格时经常会是 CdkVirtualScrollViewport 虚拟滚动容器(即该组件)一直存在,而在无数据或数据加载中时将 cdkVirtualFor 指令 + // 那块dom销毁隐藏掉(ngIf=false), 有数据时才会使用 cdkVirtualFor 指令渲染数据。在 cdkVirtualFor 指令部分动态切换(生成或销毁)过程中, + // 销毁后再生成时,需要手动触发渲染一次数据,不然出现空白数据渲染不出来。 + setTimeout(() => { + // 需延时等 cdkVirtualFor 指令拿到数据集 + // 研究源码发现此方法可触发使 cdkVirtualFor 指令进行计算渲染数据 + (this.renderedRangeStream as Subject).next(this.getRenderedRange()); + }, 0); + } + + /** + * @ignore + * + * cdkVirtualFor 指令在 OnDestroy 时会调用 CdkVirtualScrollViewport 虚拟滚动容器中的 detach 方法。 + * + * 覆写 CdkVirtualScrollViewport 类中的 detach 方法。 + */ + public detach(): void { + super.detach(); + + // 此时 cdkVirtualFor 指令销毁,即不存在 dkVirtualFor 指令。 + this.hasCdkVirtualFor = false; + + // 取消在 attach 方法中的订阅。 + if (this.dataStreamSubscription) { + this.dataStreamSubscription.unsubscribe(); + } + } + + /** + * @ignore + * + * 由于数据量可能有多有少,所以业务不能直接给表体容器设置高度而是要设置最大高度。 + * + * CdkVirtualScrollViewport 组件中通过 getViewportSize 方法获取虚拟容器高度来计算实际应该渲染的数据,而这个方法底层是直接是读取滚动容器的 clientHeight, + * 那么给容器设置最大高度时这个 clientHeight 获取的就是不准确的, 所以这里要 覆写 CdkVirtualScrollViewport 类中的 getViewportSize 方法。 + */ + public getViewportSize(): number { + const maxHeight: number = parseInt(getComputedStyle(this.elementRef.nativeElement).maxHeight, 10); + if (maxHeight) { + return maxHeight; + } + const height: number = parseInt(getComputedStyle(this.elementRef.nativeElement).height, 10); + if (height) { + return height; + } + + return 0; + } + + /** + * @ignore + * + * 元数据 host 中使用 + */ + public getTotalContentHeight(): string { + if (!this.hasCdkVirtualFor || !this._totalContentHeight) { + return '0px'; + } + // _totalContentHeight 该变量虽然以下划线开头,但在 CdkVirtualScrollViewport 类中是 public 的,且在 CdkVirtualScrollViewport 组件的模板中也有使用 + const totalContentHeight: number = parseInt(this._totalContentHeight, 10); + + // 减去 1px 是因为表格最后一条行是没有border-bottom的 + return totalContentHeight > 0 ? `${totalContentHeight - 1}px` : this._totalContentHeight; + } +} diff --git a/src/table/lib/src/cell-icons.html b/src/table/lib/src/cell-icons.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/table/lib/src/cell-icons.html @@ -0,0 +1 @@ + diff --git a/src/table/lib/src/cell-text.html b/src/table/lib/src/cell-text.html new file mode 100644 index 0000000..c319eb7 --- /dev/null +++ b/src/table/lib/src/cell-text.html @@ -0,0 +1,3 @@ + + + diff --git a/src/table/lib/src/cols-toggle-drop.html b/src/table/lib/src/cols-toggle-drop.html new file mode 100644 index 0000000..647ec52 --- /dev/null +++ b/src/table/lib/src/cols-toggle-drop.html @@ -0,0 +1,31 @@ + +
    + +
    + + +
    diff --git a/src/table/lib/src/cols-toggle.html b/src/table/lib/src/cols-toggle.html new file mode 100644 index 0000000..80d9e7a --- /dev/null +++ b/src/table/lib/src/cols-toggle.html @@ -0,0 +1,22 @@ + + diff --git a/src/table/lib/src/details-icon.html b/src/table/lib/src/details-icon.html new file mode 100644 index 0000000..d26f703 --- /dev/null +++ b/src/table/lib/src/details-icon.html @@ -0,0 +1,7 @@ + + + diff --git a/src/table/lib/src/head-filter-drop.html b/src/table/lib/src/head-filter-drop.html new file mode 100644 index 0000000..4324719 --- /dev/null +++ b/src/table/lib/src/head-filter-drop.html @@ -0,0 +1,138 @@ + + + +
    + + +
    + + + + + + {{item[labelKey]}} + + +
    +
    + + +
    +
    +
    + + + +
    + + + +
    +
    {{ 'tiTable.headFilterDatetimeTitle' | tiTranslate }}
    +
    + + + + + + + +
    +
    + + + + + + + +
    +
    + + +
    +
    +
    diff --git a/src/table/lib/src/head-filter-drop.less b/src/table/lib/src/head-filter-drop.less new file mode 100644 index 0000000..5b654f1 --- /dev/null +++ b/src/table/lib/src/head-filter-drop.less @@ -0,0 +1,40 @@ +.ti3-head-filter-drop ti-searchbox-notsearch { + width: 100%; +} +.ti3-head-filter-drop-button-container { + padding: var(--ti-common-space-10) var(--ti-common-space-0); + margin: var(--ti-common-space-0) var(--ti-common-space-10); + border-top: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solids) var(--ti-common-color-line-dividing); +} + +.ti3-head-filter-drop-button-ok { + margin-right: var(--ti-common-space-2x); +} + +.ti3-head-filter-drop-datetime { + padding: var(--ti-common-space-10); + .ti3-head-filter-drop-datetime-tip { + line-height: var(--ti-common-line-height-number); + padding-bottom: var(--ti-common-space-10); + color: var(--ti-common-color-text-weaken); + } + + .ti3-head-filter-drop-datetime-field { + height: var(--ti-common-size-7x); + min-width: 250px; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--ti-common-space-10); + } + + .ti3-head-filter-drop-datetime-buttons { + border-top: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + padding-top: var(--ti-common-space-10); + display: flex; + justify-content: flex-end; + button:first-child { + margin-right: var(--ti-common-space-10); + } + } +} diff --git a/src/table/lib/src/head-filter.html b/src/table/lib/src/head-filter.html new file mode 100644 index 0000000..5bf35e5 --- /dev/null +++ b/src/table/lib/src/head-filter.html @@ -0,0 +1,27 @@ + + + + + + +{{item[labelKey]}} diff --git a/src/table/lib/src/head-filter.less b/src/table/lib/src/head-filter.less new file mode 100644 index 0000000..cf74bb0 --- /dev/null +++ b/src/table/lib/src/head-filter.less @@ -0,0 +1,20 @@ +:host.ti3-head-filter-container { + display: inline-block; + vertical-align: middle; + padding-left: var(--ti-common-space-2x); + line-height: var(--ti-common-size-4x); + width: var(--ti-common-size-4x); + .ti3-head-filter-icon { + font-size: var(--ti-common-size-4x); + display: block; + cursor: pointer; + color: var(--ti-common-color-icon-normal); + &:hover { + color: var(--ti-common-color-icon-hover); + } + } + + &.ti3-head-filtered .ti3-head-filter-icon { + color: var(--ti-common-color-icon-active); + } +} diff --git a/src/table/lib/src/head-menu.html b/src/table/lib/src/head-menu.html new file mode 100644 index 0000000..d56b9dd --- /dev/null +++ b/src/table/lib/src/head-menu.html @@ -0,0 +1,15 @@ + + + + + diff --git a/src/table/lib/src/head-menu.less b/src/table/lib/src/head-menu.less new file mode 100644 index 0000000..32eed50 --- /dev/null +++ b/src/table/lib/src/head-menu.less @@ -0,0 +1,26 @@ +ti-head-menu { + --ti-head-menu-angle-size: var(--ti-common-size-3x); +} + +.ti3-head-menu-angle { + font-size: var(--ti-head-menu-angle-size); + width: var(--ti-head-menu-angle-size); + line-height: var(--ti-head-menu-angle-size); + display: inline-block; + vertical-align: middle; + margin-left: -4px; + cursor: pointer; + &:hover { + color: var(--ti-common-color-icon-hover); + } +} +.ti3-head-menu-angle-down { + transform: rotate(90deg); +} +.ti3-head-menu-angle-up { + transform: rotate(-90deg); +} +.ti3-table > table > thead > tr > th:first-child[checkbox-column].ti3-head-menu-cell { + width: 52px !important; + cursor: auto; +} diff --git a/src/table/lib/src/head-sort.html b/src/table/lib/src/head-sort.html new file mode 100644 index 0000000..5f254bc --- /dev/null +++ b/src/table/lib/src/head-sort.html @@ -0,0 +1,3 @@ + + + diff --git a/src/table/lib/src/i18n/TiTableWords.ts b/src/table/lib/src/i18n/TiTableWords.ts new file mode 100644 index 0000000..a806cbc --- /dev/null +++ b/src/table/lib/src/i18n/TiTableWords.ts @@ -0,0 +1,8 @@ +export interface TiTableWords { + tiTable: { + colsToggleTip: string; + headFilterDatetimeTitle: string; + headMenuSelectAll: string; + headMenuClearAll: string; + }; +} diff --git a/src/table/lib/src/i18n/en_US.ts b/src/table/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..70ea7d8 --- /dev/null +++ b/src/table/lib/src/i18n/en_US.ts @@ -0,0 +1,10 @@ +import { TiTableWords } from './TiTableWords'; + +export const en_US: TiTableWords = { + tiTable: { + colsToggleTip: 'Customize Column', + headFilterDatetimeTitle: 'Enter at least one date.', + headMenuSelectAll: 'Select All', + headMenuClearAll: 'Clear All' + } +}; diff --git a/src/table/lib/src/i18n/es_US.ts b/src/table/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..86219bd --- /dev/null +++ b/src/table/lib/src/i18n/es_US.ts @@ -0,0 +1,10 @@ +import { TiTableWords } from './TiTableWords'; + +export const es_US: TiTableWords = { + tiTable: { + colsToggleTip: 'Personalizar columna', + headFilterDatetimeTitle: 'Ingrese al menos una fecha.', + headMenuSelectAll: 'Seleccionar todo', + headMenuClearAll: 'Deseleccionar todo' + } +}; diff --git a/src/table/lib/src/i18n/fr_FR.ts b/src/table/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..17190b0 --- /dev/null +++ b/src/table/lib/src/i18n/fr_FR.ts @@ -0,0 +1,10 @@ +import { TiTableWords } from './TiTableWords'; + +export const fr_FR: TiTableWords = { + tiTable: { + colsToggleTip: 'Personnaliser la colonne', + headFilterDatetimeTitle: 'Saisissez au moins une date.', + headMenuSelectAll: 'Sélectionner tout', + headMenuClearAll: 'Effacer tout' + } +}; diff --git a/src/table/lib/src/i18n/index.ts b/src/table/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/table/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/table/lib/src/i18n/pt_BR.ts b/src/table/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..27f0ae0 --- /dev/null +++ b/src/table/lib/src/i18n/pt_BR.ts @@ -0,0 +1,10 @@ +import { TiTableWords } from './TiTableWords'; + +export const pt_BR: TiTableWords = { + tiTable: { + colsToggleTip: 'Personalizar coluna', + headFilterDatetimeTitle: 'Insira pelo menos uma data.', + headMenuSelectAll: 'Selecionar tudo', + headMenuClearAll: 'Limpar tudo' + } +}; diff --git a/src/table/lib/src/i18n/zh_CN.ts b/src/table/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..85415bd --- /dev/null +++ b/src/table/lib/src/i18n/zh_CN.ts @@ -0,0 +1,10 @@ +import { TiTableWords } from './TiTableWords'; + +export const zh_CN: TiTableWords = { + tiTable: { + colsToggleTip: '自定义列表项', + headFilterDatetimeTitle: '请至少输入一个日期', + headMenuSelectAll: '选择所有', + headMenuClearAll: '清空所有' + } +}; diff --git a/src/table/lib/src/table-nodata-small-nest-resize.less b/src/table/lib/src/table-nodata-small-nest-resize.less new file mode 100644 index 0000000..16b6684 --- /dev/null +++ b/src/table/lib/src/table-nodata-small-nest-resize.less @@ -0,0 +1,198 @@ +@import '../../../themes/basic/base-all.less'; + +/* ---------------table无数据基础样式----START-----------------------------------------*/ +.table-nodata (@bg) { + background-color: var(--ti-common-color-bg-white-normal); + & > td { + position: relative; + overflow: visible !important; + background: @bg; + font-size: var(--ti-common-font-size-1); + color: var(--ti-common-color-text-secondary); + text-align: center; + line-height: 20px; + vertical-align: top; + border-bottom: none; + } +} +.ti3-table > table, +.ti3-table > .ti3-resize-wrapper > table, +.ti3-table > .ti3-table-fixed-head > table, +.ti3-table > .ti3-table-container > table { + & > tbody > tr { + &.ti3-table-nodata { + .table-nodata(var(--ti-table-nodata-td-bg-img-url) 50% var(--ti-table-nodata-lead-icon-padding) no-repeat); + & > td { + height: var(--ti-table-nodata-height); + padding: calc(var(--ti-table-nodata-lead-icon-height) + var(--ti-table-nodata-lead-icon-padding)) 0 0 0; + } + } + + &.ti3-table-loadfail, + &.ti3-table-nodata-guide { + .table-nodata(none); + & > td { + padding: var(--ti-common-space-5x) 0; + .table-nodata-a; + } + } + &.ti3-table-nodata-simple { + .table-nodata(none); + & > td { + padding: var(--ti-common-space-5x) 0; + } + } + } +} +/* ---------------table无数据基础样式----END-----------------------------------------*/ + +/* ---------------table窄行基础样式----START-----------------------------------------*/ +.ti3-table-small > table, +.ti3-table-small > .ti3-resize-wrapper > table, +.ti3-table-small > .ti3-table-fixed-head > table, +.ti3-table-small > .ti3-table-container > table { + & > thead > tr > th { + height: var(--ti-table-small-th-height); + line-height: var(--ti-table-small-th-height); + } + + & > tbody > tr > td { + padding: var(--ti-common-space-2x) var(--ti-table-small-td-horizontal-padding); + line-height: var(--ti-common-line-height-number); + } + + & > tbody > tr.ti3-details-tr > td { + padding: var(--ti-common-space-5x); + } + + .small-table-nodata (@bg) { + & > td { + vertical-align: top; + background: @bg; + } + } + + & > tbody > tr { + &.ti3-table-nodata { + .small-table-nodata(var(--ti-table-nodata-td-bg-img-url) 50% var(--ti-table-nodata-lead-icon-padding) no-repeat); + & > td { + height: var(--ti-table-small-nodata-height); + padding: calc(var(--ti-table-nodata-lead-icon-height) + var(--ti-table-nodata-lead-icon-padding)) 0 0 0; + } + } + + &.ti3-table-loadfail, + &.ti3-table-nodata-guide { + .small-table-nodata(none); + & > td { + .table-nodata-a; + } + } + } +} +/* ---------------table窄行基础样式----END-----------------------------------------*/ + +/* --------------- table嵌套样式 ----start------------------*/ +.table-nodata-a { + a { + color: var(--ti-common-color-text-link); + text-decoration: none; + &:hover { + text-decoration: underline; + color: var(--ti-common-color-text-link-hover); + cursor: pointer; + } + } +} +.ti3-table-nest > table, +.ti3-table-nest > .ti3-resize-wrapper > table, +.ti3-table-nest > .ti3-table-fixed-head > table, +.ti3-table-nest > .ti3-table-container > table { + & > thead > tr > th { + height: var(--ti-table-nest-th-height); + line-height: var(--ti-table-nest-th-height); + } + + .nest-table-nodata (@bg) { + & > td { + vertical-align: top; + background: @bg; + } + } + + & > tbody > tr { + &.ti3-table-nodata { + .nest-table-nodata(var(--ti-table-nodata-td-bg-img-url) 50% var(--ti-table-nodata-lead-icon-padding) no-repeat); + & > td { + height: var(--ti-table-small-nodata-height); + padding: calc(var(--ti-table-nodata-lead-icon-height) + var(--ti-table-nodata-lead-icon-padding)) 0 0 0; + } + } + + &.ti3-table-loadfail &.ti3-table-nodata-guide { + .nest-table-nodata(none); + & > td { + .table-nodata-a; + } + } + } + + & > tbody > tr > td { + padding: var(--ti-common-space-2x) var(--ti-table-small-td-horizontal-padding); + line-height: var(--ti-common-line-height-number); + border-bottom: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + } + + & > tbody > tr.ti3-details-tr > td { + padding: var(--ti-common-space-5x); + } +} + +/* --------------- table嵌套样式 ----end--------------------*/ + +/* ---------------table列拖动样式(colsResizable)----START----------------------------------------------*/ +// 列拖动父容器区域 +.ti3-resize-wrapper { + overflow-x: auto; + min-height: ~'0%'; // 解决:IE浏览器下,将表格列拖动出现横向滚动条时,分页会与表体出现空白,且hover到表格时,空白范围在不断改变; #7091 +} +// 页面文字不可选,避免出现蓝色选择区域(对于列拖动) +.ti3-unselectable { + .user-select(none); +} +.ti3-table > table, +.ti3-table > .ti3-resize-wrapper > table, +.ti3-table > .ti3-table-fixed-head > table, +.ti3-table > .ti3-table-container > table { + & > thead > tr > th.col-resize-active { + // 因为IE下给伪元素设置cursor是不生效的,所以cursor没有加在th.col-resize-active:after上 + cursor: col-resize !important; // 加important是为了避免被排序的手型鼠标样式覆盖掉 + } + & > thead > tr > th.col-resize-active:after { + // 列拖动激活状态时th样式 + content: ''; + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 2px; + height: 100%; + border-right: var(--ti-common-border-weight-normal) var(--ti-common-border-style-dashed) var(--ti-common-color-line-dividing); + background: transparent; + } + & > tbody > tr > td.col-resize-active { + position: relative; + } + & > tbody > tr:not(.ti3-table-nodata) > td.col-resize-active:after { + // 列拖动激活状态时td样式 + content: ''; + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 2px; + height: 100%; + border-right: var(--ti-common-border-weight-normal) var(--ti-common-border-style-dashed) var(--ti-common-color-line-dividing); + } +} +/* ---------------table列拖动样式(colsResizable)----END----------------------------------------------*/ diff --git a/src/table/lib/src/table-toggle-sort-details.less b/src/table/lib/src/table-toggle-sort-details.less new file mode 100644 index 0000000..185e3d9 --- /dev/null +++ b/src/table/lib/src/table-toggle-sort-details.less @@ -0,0 +1,124 @@ +@import '../../../themes/basic/base-all.less'; +ti-drop.ti3-cols-toggle-drop { + --ti-table-cols-toggle-horizontal-padding: var(--ti-common-space-10); +} +/* ---------------table列动态隐藏和显示(colsToggle)----START----------------------------------------------*/ +.ti3-cols-toggle-drop { + .border-radius(var(--ti-common-border-radius-normal)); + .box-shadow(var(--ti-common-shadow-2-down)); + min-width: 200px !important; + max-width: 300px !important; + & > .ti3-cols-toggle-searchbox-wrap { + padding: var(--ti-common-space-10) var(--ti-table-cols-toggle-horizontal-padding) var(--ti-common-space-base) !important; + .box-sizing(border-box); + .ti3-cols-toggle-searchbox { + width: 100% !important; + } + } + .ti3-overflow-padding { + padding: 0 var(--ti-table-cols-toggle-horizontal-padding) !important; + } + .ti3-dropdown-no-data { + padding-left: var(--ti-table-cols-toggle-horizontal-padding) !important; + } + .ti3-dropdown-option-hover { + color: var(--ti-common-color-text-primary); + } +} + +/* ---------------table列动态隐藏和显示(colsToggle)----END----------------------------------------------*/ + +/* ---------------table列排序(sort)----START----------------------------------------------*/ +.ti3-table > table, +.ti3-table > .ti3-resize-wrapper > table, +.ti3-table > .ti3-table-fixed-head > table, +.ti3-table > .ti3-table-container > table { + & > thead > tr > th { + & > .ti3-sort-container { + display: inline-block; + cursor: pointer; // 鼠标的形态与是否触发排序功能保持一致,避免在整个th区域表现为手型 + outline-offset: -1px; // 防止tab聚焦的时候,上下outline被遮挡。 + // padding-right由原来的30px改为0.6em是为了解决 #7611 + // 原因:当父元素设置内容溢出时出省略号的功能时将内部元素的padding和margin也算作内容 + & .ti3-sort { + cursor: pointer; + width: var(--ti-common-size-4x); + height: var(--ti-common-size-4x); + font-size: var(--ti-common-size-4x); + display: inline-block; + margin-left: var(--ti-common-space-2x); + vertical-align: middle; + line-height: var(--ti-common-size-4x); + &:hover { + color: var(--ti-common-color-icon-hover); + } + & .ti3-headsort-icon-active { + color: var(--ti-common-color-icon-active); + } + } + } + } +} + +/* ---------------table列排序(sort)----END----------------------------------------------*/ + +/* ---------------table表头有排序图标或漏斗图标或其他图标时文本样式和图标容器样式----START----------------------------------------------*/ +.ti3-table > table, +.ti3-table > .ti3-resize-wrapper > table, +.ti3-table > .ti3-table-fixed-head > table, +.ti3-table > .ti3-table-container > table { + & > thead > tr > th, + & > tbody > tr > td { + & > .ti3-cell-text-container { + & > .ti3-cell-text { + display: inline-block; + vertical-align: top; + } + } + & > .ti3-cell-icons-container { + display: inline-block; + } + } +} +/* ---------------table列排序(sort)----END----------------------------------------------*/ + +/* ---------------table详情展开(details)----START----------------------------------------------*/ + +// 由于每次详情展开时,需要给当前行的下展详情底部与下一行表格数据之间增加下边框,因此增加border-bottom属性 +.ti3-table > table > tbody, +.ti3-table > .ti3-resize-wrapper > table > tbody, +.ti3-table > .ti3-table-container > table > tbody { + & > .ti3-details-tr { + background: var(--ti-common-color-bg-white-emphasize); + & > td { + padding: var(--ti-common-space-0); + border-bottom: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + // 隔离作用,防止td的white-space:nowrap 属性继承下去,影响详情行样式 + white-space: normal; + & > .ti3-table-detail-container { + padding: var(--ti-common-space-5x); + background-color: var(--ti-common-color-bg-white-normal); + } + } + } + & > tr > td .ti3-toggle-details { + cursor: pointer; + color: var(--ti-common-color-icon-normal); + & .ti3-icon { + vertical-align: middle; + font-size: 15px; + &:hover { + color: var(--ti-common-color-icon-hover); + } + } + } +} +.ti3-icon-angle-transform-up-thin { + display: inline-block; + .rotate(-90deg); +} +.ti3-icon-angle-transform-down-thin { + display: inline-block; + .rotate(90deg); +} +/* ---------------table详情展开(details)----END----------------------------------------------*/ diff --git a/src/table/lib/src/table-tree-fix.less b/src/table/lib/src/table-tree-fix.less new file mode 100644 index 0000000..4d640bb --- /dev/null +++ b/src/table/lib/src/table-tree-fix.less @@ -0,0 +1,145 @@ +@import '../../../themes/basic/base-all.less'; +/* ---------------table树表特性(tree)----START----------------------------------------------*/ +.ti3-table > table > tbody, +.ti3-table > .ti3-resize-wrapper > table > tbody, +.ti3-table > .ti3-table-container > table > tbody { + & > tr > td .ti3-table-tree { + cursor: pointer; + .ti3-icon-minus-square, + .ti3-icon-plus-square { + font-size: var(--ti-table-tree-square-icon-font-size); + margin-right: var(--ti-common-space-2x); + line-height: 18px; + vertical-align: bottom; + } + .ti3-icon-minus-square { + color: var(--ti-common-color-icon-active); + } + .ti3-icon-plus-square { + color: var(--ti-common-color-icon-normal); + } + .ti3-icon-minus-square:hover, + .ti3-icon-plus-square:hover { + color: var(--ti-common-color-icon-hover); + } + .ti3-icon-minus-square:active, + .ti3-icon-plus-square:active { + color: var(--ti-common-color-icon-active); + background: var(--ti-common-color-bg-white-normal); + } + } + // 各层级内容对齐 + & > tr > td .ti3-table-tree-no-leaf { + margin-left: calc(var(--ti-table-tree-square-icon-font-size) + var(--ti-common-space-2x)); + } +} + +/* ---------------table树表特性(tree)----END----------------------------------------------*/ + +/* ---------------table设置tip时克隆DOM的样式----START----------------------------------------------*/ +.ti3-table-cell-clone { + overflow: visible !important; + position: absolute !important; + visibility: hidden !important; + .box-sizing(var(--ti-table-box-model)); + width: auto !important; +} +/* ---------------table设置tip时克隆DOM的样式----END----------------------------------------------*/ + +/* ---------------table表头锁定(双表)----START----------------------------------------------*/ +.ti3-table { + & > .ti3-table-fixed-head { + overflow: hidden; + position: relative; + & > table { + margin-left: 0; + } + & > .ti3-table-fixed-head-filler { + position: absolute; + height: 100%; + right: 0; + background-color: var(--ti-common-color-bg-white-emphasize); + top: 0; + bottom: 0; + } + & + .ti3-table-container { + // 表头锁定时,表格容器底部有边框 + border-bottom: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + & > table > thead > tr > th { + height: 0; + border-bottom: none; + } + & > table > tbody > tr { + &:last-child > td { + border-bottom: none !important; + } + } + } + } + & > .ti3-table-container { + overflow: auto; + } +} +/* ---------------table表头锁定(双表头)----END----------------------------------------------*/ + +/* ---------------table列固定----START----------------------------------------------*/ +.ti3-table-column-fixed-left { + position: sticky !important; + left: 0; + z-index: 1; + background-color: inherit; + &.ti3-table-floating-fixed-column { + box-shadow: 5px 0 5px 0 rgba(0, 0, 0, 0.1); + } +} +.ti3-table-column-fixed-right { + position: sticky !important; + right: 0; + z-index: 1; + background-color: inherit; + &.ti3-table-floating-fixed-column { + box-shadow: -5px 0 5px 0 rgba(0, 0, 0, 0.1); + } +} +/* ---------------table列固定----END----------------------------------------------*/ +.ti3-table-row-disable { + color: #999; +} + +.ti3-table-soldout { + &:extend(.ti3-icon all); + &:extend(.ti3-icon-sold-out all); + width: 16px; + display: inline-block; + height: 16px; + background: #999; + text-align: center; + line-height: 16px; + color: #fff; + font-size: 12px; +} + +.ti3-table-row-message { + display: inline-block; + padding: 0 4px; + height: 16px; + line-height: 16px; + font-size: 12px; + color: #fff; + text-align: center; + margin-left: 5px; +} +.ti3-table-row-message-soldout { + &:extend(.ti3-table-row-message all); + background: #999; +} + +.ti3-table-row-message-new { + &:extend(.ti3-table-row-message all); + background: #e41f2b; +} + +.ti3-table-row-message-recommended { + &:extend(.ti3-table-row-message all); + background: #f57f06; +} diff --git a/src/table/lib/src/table-virtual-scroll-viewport.html b/src/table/lib/src/table-virtual-scroll-viewport.html new file mode 100644 index 0000000..4efb2fd --- /dev/null +++ b/src/table/lib/src/table-virtual-scroll-viewport.html @@ -0,0 +1,3 @@ + + +
    diff --git a/src/table/lib/src/table-virtual-scroll-viewport.less b/src/table/lib/src/table-virtual-scroll-viewport.less new file mode 100644 index 0000000..6aef48a --- /dev/null +++ b/src/table/lib/src/table-virtual-scroll-viewport.less @@ -0,0 +1,34 @@ +.ti3-table-virtual-scroll-viewport { + display: block; + position: relative; + overflow: auto; + contain: strict; + transform: translateZ(0); + will-change: scroll-position; + -webkit-overflow-scrolling: touch; + + &.ti3-table-virtual-scroll-viewport-nodata { + // 以下样式都是为了让在无数据时无数据的dom能够显示出来(撑起 ti3-table-virtual-scroll-viewport 的高度) + height: auto !important; // 覆盖 style.height: 0 的设置; + contain: none; + & > .ti3-table-virtual-scroll-content-wrapper { + position: static; + } + } +} + +.ti3-table-virtual-scroll-content-wrapper { + position: absolute; + top: 0; + left: 0; + contain: content; +} + +.ti3-table-virtual-scroll-spacer { + position: absolute; + top: 0; + left: 0; + height: inherit; + width: 1px; + transform-origin: 0 0; +} diff --git a/src/table/lib/src/table.html b/src/table/lib/src/table.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/table/lib/src/table.html @@ -0,0 +1 @@ + diff --git a/src/table/lib/src/table.less b/src/table/lib/src/table.less new file mode 100644 index 0000000..e7a2d51 --- /dev/null +++ b/src/table/lib/src/table.less @@ -0,0 +1,187 @@ +@import '../../../themes/basic/base-all.less'; + +ti-table { + --ti-table-box-model: border-box; + --ti-table-th-horizontal-padding: var(--ti-common-space-10); + --ti-table-nodata-lead-icon-padding: var(--ti-common-space-5x); + --ti-table-nodata-td-bg-img-url: data-uri('../../../themes/basic/img/table-nodata-bg.png'); + --ti-table-small-td-horizontal-padding: var(--ti-common-space-10); + --ti-table-nodata-height: 210px; + --ti-table-small-nodata-height: 190px; + --ti-table-nodata-lead-icon-height: var(--ti-common-size-20x); + --ti-table-cols-toggle-menu-size: var(--ti-common-size-7x); + --ti-table-tree-square-icon-font-size: var(--ti-common-font-size-2); + --ti-table-th-height: var(--ti-common-size-7x); + --ti-table-small-th-height: var(--ti-common-size-7x); + --ti-table-nest-th-height: var(--ti-common-size-7x); + --ti-table-column-select-icon-width: var(--ti-common-size-8x); + --ti-table-column-icon-width: 42px; + --ti-table-timing-function-default: cubic-bezier(0.25, 0.1, 0.25, 1); +} + +/* ---------------table基础样式----START----------------------------------------------*/ +.ti3-table { + display: block; +} +.table-nodata-a { + a { + color: var(--ti-common-color-text-link); + text-decoration: none; + &:hover { + text-decoration: underline; + color: var(--ti-common-color-text-link-hover); + cursor: pointer; + } + } +} + +.ti3-table > table, +.ti3-table > .ti3-resize-wrapper > table, +.ti3-table > .ti3-table-fixed-head > table, +.ti3-table > .ti3-table-container > table { + .box-sizing(var(--ti-table-box-model)); + border-width: 0; + border-collapse: separate; + table-layout: fixed; + border-spacing: 0; + empty-cells: show; + margin-bottom: 0; + width: 100%; + & > thead > tr > th { + .box-sizing(var(--ti-table-box-model)); + position: relative; + cursor: default; + background-color: var(--ti-common-color-bg-white-emphasize); + outline: 0; + height: var(--ti-table-th-height); + line-height: calc(var(--ti-table-th-height) - 1px); + color: var(--ti-common-color-text-secondary); + font-weight: var(--ti-common-font-weight-6); + border-left: none; + padding: 0; + padding-left: var(--ti-table-th-horizontal-padding); + padding-right: var(--ti-table-th-horizontal-padding); + text-align: left; + vertical-align: middle; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border-bottom: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + &:not(:last-child)::after { + width: 1px; + background: var(--ti-common-color-text-white); + color: transparent; + height: 100%; + position: absolute; + right: 0; + top: 0; + content: ''; + } + &[checkbox-column], + &[radio-column] { + width: var(--ti-common-size-8x) !important; + overflow: visible; + padding-right: var(--ti-common-space-6); + font-size: 0; // 解决图标溢出问题 + &::after { + width: 0; + } + } + // 单选时,th不需要radio按钮 + &[checkbox-column] { + cursor: pointer; + } + &.ti3-disabled-cell { + cursor: not-allowed; + } + &[details-icon-column] { + width: var(--ti-table-column-icon-width) !important; + overflow: visible; + padding-right: var(--ti-common-space-6); + &::after { + width: 0; + } + } + &:first-child { + &[checkbox-column], + &[radio-column] { + width: var(--ti-table-column-icon-width) !important; + padding-left: var(--ti-common-space-5x) !important; + } + } + } + & > tbody > tr { + border-spacing: 0; + background-color: var(--ti-common-color-bg-white-normal); + .transition (background-color 150ms var(--ti-table-timing-function-default));; + //行hover时显示当前行下的cti-rename组件图标 + &:hover { + .cti-rename-edit { + visibility: visible; + } + } + &.ti3-selected-tr { + background-color: var(--ti-common-color-bg-white-normal); + &:hover { + background-color: var(--ti-common-color-bg-white-normal); + } + } + + &.ti3-disabled-tr { + background-color: var(--ti-common-color-bg-disabled); + & > td { + color: var(--ti-common-color-text-disabled); + &[checkbox-column], + &[radio-column] { + cursor: not-allowed; + } + } + } + + &:not(.ti3-details-tr):not(.ti3-disabled-tr):not(.ti3-table-nodata):not(.ti3-table-nodata-simple):not(.ti3-table-loadfail):not( + .ti3-table-nodata-guide + ):not(.ti3-selected-tr) { + &:hover { + background-color: var(--ti-common-color-bg-white-emphasize); + .transition (background-color 200ms var(--ti-table-timing-function-default));; + } + } + + & > td { + .box-sizing(var(--ti-table-box-model)); + border-left: none; + border-top: none; + padding: var(--ti-common-space-3x) var(--ti-common-space-10); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: left; + vertical-align: middle; + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-primary); + border-bottom: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); + &[checkbox-column], + &[radio-column] { + overflow: visible; + padding-right: var(--ti-common-space-6); + font-size: 0; // 解决图标溢出问题 + cursor: pointer; + } + &.ti3-disabled-cell { + cursor: not-allowed; + } + &[details-icon-column] { + overflow: visible; + padding-left: var(--ti-common-space-5x); + padding-right: var(--ti-common-space-6); + } + &:first-child { + &[checkbox-column], + &[radio-column] { + padding-left: var(--ti-common-space-5x); + } + } + } + } +} +/* ---------------table基础样式----End----------------------------------------------*/ diff --git a/src/tag/demo/karma.conf.js b/src/tag/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/tag/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/tag/demo/project.json b/src/tag/demo/project.json new file mode 100644 index 0000000..42bfae3 --- /dev/null +++ b/src/tag/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/tag/demo", + "sourceRoot": "src/tag/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/tag", + "index": "src/tag/demo/src/index.html", + "main": "src/tag/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tag/demo/tsconfig.app.json", + "assets": ["src/tag/demo/src/favicon.ico", "src/tag/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "tag-demo:build:production" + }, + "development": { + "browserTarget": "tag-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js tag" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/tag/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tag/demo/tsconfig.spec.json", + "karmaConfig": "src/tag/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/tag/demo/src/app/AppComponent.ts b/src/tag/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/tag/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/tag/demo/src/app/AppModule.ts b/src/tag/demo/src/app/AppModule.ts new file mode 100644 index 0000000..caee982 --- /dev/null +++ b/src/tag/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TagTestModule } from './tag/TagTestModule'; + +@NgModule({ + imports: [ + TagTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/tag/demo/src/app/IndexComponent.ts b/src/tag/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..aa79cac --- /dev/null +++ b/src/tag/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TagTestModule } from './tag/TagTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TagTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/tag/demo/src/app/app.html b/src/tag/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/tag/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/tag/demo/src/app/tag/TagBasicComponent.ts b/src/tag/demo/src/app/tag/TagBasicComponent.ts new file mode 100644 index 0000000..db76caa --- /dev/null +++ b/src/tag/demo/src/app/tag/TagBasicComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tag-basic.html' +}) +export class TagBasicComponent { + myLogs: Array = []; + + onDelete(): void { + this.myLogs = [...this.myLogs, `onDelete() delete`]; + } +} diff --git a/src/tag/demo/src/app/tag/TagDefaultComponent.ts b/src/tag/demo/src/app/tag/TagDefaultComponent.ts new file mode 100644 index 0000000..9732c98 --- /dev/null +++ b/src/tag/demo/src/app/tag/TagDefaultComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tag-default.html' +}) +export class TagDefaultComponent { + disabled: boolean = false; + student1: any = { + id: 1, + name: '苏轼', + age: 36 + }; + onDelete(data: any): void { + console.log('onDelete:' + data.name); + } + onClick(event: MouseEvent): void { + console.log('onClick:原生事件'); + console.log(event); + } + changeDisabled(): void { + this.disabled = !this.disabled; + } +} diff --git a/src/tag/demo/src/app/tag/TagDisabledComponent.ts b/src/tag/demo/src/app/tag/TagDisabledComponent.ts new file mode 100644 index 0000000..c68ed38 --- /dev/null +++ b/src/tag/demo/src/app/tag/TagDisabledComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tag-disabled.html' +}) +export class TagDisabledComponent {} diff --git a/src/tag/demo/src/app/tag/TagEditComponent.ts b/src/tag/demo/src/app/tag/TagEditComponent.ts new file mode 100644 index 0000000..db10048 --- /dev/null +++ b/src/tag/demo/src/app/tag/TagEditComponent.ts @@ -0,0 +1,62 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tag-edit.html' +}) +export class TagEditComponent { + myLogs: Array = []; + inputValue: string = ''; + showInput: boolean = false; + + items: any = [ + { + label: 'Tag1' + }, + { + label: 'Tag2', + disabled: true + }, + { + label: 'Tag3' + } + ]; + onDelete(item: any): void { + if (item.disabled === true) { + return; + } + // 删除选中 + const index: number = this.items.indexOf(item); + if (index !== -1) { + this.items.splice(index, 1); + } + + this.myLogs = [...this.myLogs, `delete ${item.label}`]; + } + + onClick(): void { + this.showInput = true; + } + + onInputKeyup(event: KeyboardEvent): void { + // 获取输入框的值 + const value: string = this.inputValue; + + this.inputValue = ''; + this.showInput = false; + + // 输入框为空时或者已经生成当前标签时 + if (value.trim() === '' || this.findFirstIndex(this.items, 'label', value) !== -1) { + return; + } + + this.items.push({ label: value }); + } + + private findFirstIndex(arr: any, key: string, value: string): number { + if (!(arr instanceof Array)) { + return -1; + } + + return arr.findIndex((i: any) => i[key] === value); + } +} diff --git a/src/tag/demo/src/app/tag/TagTestModule.ts b/src/tag/demo/src/app/tag/TagTestModule.ts new file mode 100644 index 0000000..68bde13 --- /dev/null +++ b/src/tag/demo/src/app/tag/TagTestModule.ts @@ -0,0 +1,48 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiIconModule, TiTagModule, TiTextModule } from '@opentiny/ng'; +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; + +import { TagBasicComponent } from './TagBasicComponent'; +import { TagDisabledComponent } from './TagDisabledComponent'; +import { TagEditComponent } from './TagEditComponent'; +import { TagDefaultComponent } from './TagDefaultComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiIconModule, + TiTagModule, + TiButtonModule, + TiIconModule, + TiTextModule, + DemoLogModule, + RouterModule.forChild(TagTestModule.ROUTES) + ], + declarations: [TagBasicComponent, TagDisabledComponent, TagEditComponent, TagDefaultComponent] +}) +export class TagTestModule { + static readonly LINKS: Array = [{ href: 'components/TiTagComponent.html', label: 'Tag' }]; + static readonly ROUTES: Routes = [ + { + path: 'tag/tag-basic', + component: TagBasicComponent + }, + { + path: 'tag/tag-disabled', + component: TagDisabledComponent + }, + { + path: 'tag/tag-edit', + component: TagEditComponent + }, + { + path: 'tag/tag-default', + component: TagDefaultComponent + } + ]; +} diff --git a/src/tag/demo/src/app/tag/tag-basic.html b/src/tag/demo/src/app/tag/tag-basic.html new file mode 100644 index 0000000..6647258 --- /dev/null +++ b/src/tag/demo/src/app/tag/tag-basic.html @@ -0,0 +1 @@ +Default diff --git a/src/tag/demo/src/app/tag/tag-default.html b/src/tag/demo/src/app/tag/tag-default.html new file mode 100644 index 0000000..2df70b8 --- /dev/null +++ b/src/tag/demo/src/app/tag/tag-default.html @@ -0,0 +1,25 @@ +

    描述

    +

    Tag标签组件,基础用法

    +

    导入

    +

    import {{ '{' }} TiTagModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    +

    1.ti-tag默认是inline-block,max-width: 100%;

    +
    + go + binggo + 明月几时有,把酒问青天。不知天上宫阙,今夕是何年 + + 但愿人长久,千里共婵娟。 + + 但愿人长久,千里共婵娟。 +
    +

    2.设置可以设置max-width,min-width,width。width默认值是

    +明月几时有,把酒问青天。不知天上宫阙,今夕是何年 +明月几时有,把酒问青天。不知天上宫阙,今夕是何年 +

    +

    3.事件和方法

    +{{student1.name}} +明月几时有,把酒问青天。不知天上宫阙,今夕是何年 +
    +
    + diff --git a/src/tag/demo/src/app/tag/tag-disabled.html b/src/tag/demo/src/app/tag/tag-disabled.html new file mode 100644 index 0000000..17d3017 --- /dev/null +++ b/src/tag/demo/src/app/tag/tag-disabled.html @@ -0,0 +1 @@ +Disabled diff --git a/src/tag/demo/src/app/tag/tag-edit.html b/src/tag/demo/src/app/tag/tag-edit.html new file mode 100644 index 0000000..8098603 --- /dev/null +++ b/src/tag/demo/src/app/tag/tag-edit.html @@ -0,0 +1,9 @@ +{{item.label}} + + + + + + + diff --git a/src/tag/demo/src/app/tag/webdoc/tag-demos.js b/src/tag/demo/src/app/tag/webdoc/tag-demos.js new file mode 100644 index 0000000..5406d94 --- /dev/null +++ b/src/tag/demo/src/app/tag/webdoc/tag-demos.js @@ -0,0 +1,40 @@ +export default { + column: '2', + demos: [ + { + demoId: 'tag-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Tag 组件的最简用法。

    ', + 'en-US': '', + } + }, + { + demoId: 'tag-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': '', + }, + apis: ['TiTagComponent.properties.disabled'], + }, + { + demoId: 'tag-events', + name: { + 'zh-CN': '事件', + 'en-US': 'events', + }, + desc: { + 'zh-CN':'

    当点击删除按钮的时候触发delete事件。

    ', + 'en-US': '', + }, + apis: ['TiTagComponent.events.delete'], + } + ] +}; diff --git a/src/tag/demo/src/app/tag/webdoc/tag.cn.md b/src/tag/demo/src/app/tag/webdoc/tag.cn.md new file mode 100644 index 0000000..1edda3d --- /dev/null +++ b/src/tag/demo/src/app/tag/webdoc/tag.cn.md @@ -0,0 +1,23 @@ +--- +title: Tag 标签 +--- +# Tag 标签 + +
    + +Tag 是标签组件。   + +```typescript +import { TiTagModule } from '@opentiny/ng'; +``` + +
    + +
    + +Tag 是标签组件。   + +```typescript +import { TiTagModule } from '@cloud/tiny-config'; +``` +
    diff --git a/src/tag/demo/src/app/tag/webdoc/tag.en.md b/src/tag/demo/src/app/tag/webdoc/tag.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/tag/demo/src/app/tag/webdoc/tag.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/tag/demo/src/favicon.ico b/src/tag/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/tag/demo/src/index.html b/src/tag/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/tag/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/tag/demo/src/main.ts b/src/tag/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/tag/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/tag/demo/test.ts b/src/tag/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/tag/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/tag/demo/tsconfig.app.json b/src/tag/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/tag/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/tag/demo/tsconfig.spec.json b/src/tag/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/tag/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/tag/lib/index.ts b/src/tag/lib/index.ts new file mode 100644 index 0000000..6172381 --- /dev/null +++ b/src/tag/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTagModule'; diff --git a/src/tag/lib/ng-package.json b/src/tag/lib/ng-package.json new file mode 100644 index 0000000..6c45b6c --- /dev/null +++ b/src/tag/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/tag", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/tag/lib/package.json b/src/tag/lib/package.json new file mode 100644 index 0000000..5b31f1a --- /dev/null +++ b/src/tag/lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "@opentiny/ng-tag", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/tag/lib/project.json b/src/tag/lib/project.json new file mode 100644 index 0000000..cc0dd91 --- /dev/null +++ b/src/tag/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/tag/lib", + "sourceRoot": "src/tag/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/tag"], + "options": { + "project": "src/tag/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/tag"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js tag" + }, + { + "command": "ng default-build tag" + }, + { + "command": "node build/clear-default-theme.js tag" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/tag && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build tag && ng pack tag && node build/publish.js tag --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/tag/lib/src/TiTagComponent.ts b/src/tag/lib/src/TiTagComponent.ts new file mode 100644 index 0000000..660cdda --- /dev/null +++ b/src/tag/lib/src/TiTagComponent.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiPositionType } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; +// TODO: 等待规范统一,可能只保留方形tag +// TODO: hover时是否需要高亮,选中是否高亮。增加selectable接口,表示是否可以选中。 +// TODO: 禁用时是否显示叉号?规范和tiny2不一致。 +// TODO: 禁用时,不发出原生click事件。 +/** + * Tag标签组件,支持显示自定义内容,和删除事件通知。 + * + */ +@Component({ + selector: 'ti-tag', + templateUrl: './tag.html', + styleUrls: ['./tag-rect.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-multiselect-box-cell]': 'true', + '[class.ti3-multiselect-option-disabled]': 'disabled', + '[style.maxWidth]': 'maxWidth' + } +}) +export class TiTagComponent extends TiBaseComponent { + /** + * 是否禁用 + */ + @Input() disabled: boolean = false; + /** + * @ignore + * 选中项文本超出时tip展开方向 + */ + @Input() selectedTipPosition: TiPositionType = 'auto'; + /** + * @ignore + * 最大宽度,TpSearchbox 使用 + */ + @Input() maxWidth: string; + /** + * 点击删除按钮时触发的回调 + */ + @Output() readonly delete: EventEmitter = new EventEmitter(); + protected versionInfo: string = super.getVersion(packageInfo); + /** + * @ignore + * @param $event + */ + public onClickDelete($event: MouseEvent): void { + if (this.disabled) { + return; + } + $event.stopPropagation(); // 阻止冒泡,阻止了Tag整体的onClick事件,也阻止Tag之外接收到Click。 + this.delete.emit(); + } + /** + * 删除 Tag 标签元素 + */ + public remove(): void { + this.renderer.removeChild(this.renderer.parentNode(this.nativeElement), this.nativeElement); + } +} diff --git a/src/tag/lib/src/TiTagModule.ts b/src/tag/lib/src/TiTagModule.ts new file mode 100644 index 0000000..ec03c3a --- /dev/null +++ b/src/tag/lib/src/TiTagModule.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiTagComponent } from './TiTagComponent'; +import { CommonModule } from '@angular/common'; +import { TiIconModule } from '@opentiny/ng-icon'; + +@NgModule({ + imports: [CommonModule, TiIconModule], + exports: [TiTagComponent], + declarations: [TiTagComponent] +}) +export class TiTagModule {} +export { TiTagComponent } from './TiTagComponent'; diff --git a/src/tag/lib/src/tag-arrow.less b/src/tag/lib/src/tag-arrow.less new file mode 100644 index 0000000..b528508 --- /dev/null +++ b/src/tag/lib/src/tag-arrow.less @@ -0,0 +1,44 @@ +@import '../../../themes/basic/base-all.less'; +:host.ti3-tag-container { + display: inline-block; + max-width: 221px; + height: 28px; + line-height: 28px; + margin-left: 24px; + padding-right: 28px; + font-size: 14px; + background-color: var(--ti-common-color-bg-normal); + color: var(--ti-common-color-text-primary); + cursor: pointer; + box-sizing: border-box; + position: relative; + + &:hover { + .ti3-tag-clear { + color: var(--ti-common-color-icon-hover); + } + } + + .ti3-tag-selected-triangle { + position: absolute; + top: 0; + left: -14px; + width: 0; + height: 0; + border-top: 14px solid transparent; + border-right: 14px solid var(--ti-common-color-bg-normal); + border-bottom: 14px solid transparent; + } + + .ti3-tag-clear { + position: absolute; + right: 7px; + top: 0; + background-color: transparent; + color: #999; + font-size: 14px; + text-align: center; + vertical-align: middle; + cursor: pointer; + } +} diff --git a/src/tag/lib/src/tag-rect.less b/src/tag/lib/src/tag-rect.less new file mode 100644 index 0000000..54fafb0 --- /dev/null +++ b/src/tag/lib/src/tag-rect.less @@ -0,0 +1,66 @@ +//新插入的盒子的样式.ti3-multiselect-box-cell 默认样式 +:host { + --ti-tag-container-height: var(--ti-common-size-5x); + --ti-tag-clear-icon-size: var(--ti-common-size-3x); + --ti-tag-border-weight: var(--ti-common-border-weight-normal); + --ti-tag-icon-area-width: calc(var(--ti-tag-clear-icon-size) + var(--ti-common-space-10) * 2); // 图标大小12px + 左右间距各10px +} +:host.ti3-multiselect-box-cell { + display: inline-flex; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + + &::after { + content: ''; + overflow: hidden; + display: block; + } + + width: auto; + max-width: calc(100% - var(--ti-tag-border-weight) * 2); + border-radius: var(--ti-common-border-radius-normal); + height: var(--ti-tag-container-height); + line-height: var(--ti-tag-container-height); + border: var(--ti-tag-border-weight) var(--ti-common-border-style-solid) var(--ti-common-color-transparent); + overflow: hidden; + color: var(--ti-common-color-text-secondary); + background-color: var(--ti-common-color-bg-normal); + box-sizing: content-box; + + .ti3-box-cell-key { + box-sizing: border-box; + margin-left: var(--ti-common-space-10); + margin-right: var(--ti-common-space-6); + width: calc(100% - var(--ti-tag-icon-area-width) - var(--ti-common-space-6) - var(--ti-common-space-10)); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + &:hover { + color: var(--ti-common-color-text-highlight); + } + } + & .ti3-icon-close-staic { + box-sizing: border-box; + color: var(--ti-common-color-icon-graybg-normal); + width: var(--ti-tag-icon-area-width); + height: 100%; + text-align: center; + font-size: var(--ti-tag-clear-icon-size); + &:hover { + color: var(--ti-common-color-icon-graybg-hover); + } + } + + &.ti3-multiselect-option-disabled { + color: var(--ti-common-color-text-disabled); + background-color: var(--ti-common-color-bg-disabled); + border-color: var(--ti-common-color-bg-disabled); + cursor: not-allowed; + & .ti3-box-cell-key { + color: var(--ti-common-color-text-disabled); + } + & .ti3-icon-close-staic { + color: var(--ti-common-color-icon-graybg-disabled); + } + } +} diff --git a/src/tag/lib/src/tag.html b/src/tag/lib/src/tag.html new file mode 100644 index 0000000..4cb351b --- /dev/null +++ b/src/tag/lib/src/tag.html @@ -0,0 +1,2 @@ +
    +
    diff --git a/src/tagsinput/demo/karma.conf.js b/src/tagsinput/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/tagsinput/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/tagsinput/demo/project.json b/src/tagsinput/demo/project.json new file mode 100644 index 0000000..d37143f --- /dev/null +++ b/src/tagsinput/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/tagsinput/demo", + "sourceRoot": "src/tagsinput/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/tagsinput", + "index": "src/tagsinput/demo/src/index.html", + "main": "src/tagsinput/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tagsinput/demo/tsconfig.app.json", + "assets": ["src/tagsinput/demo/src/favicon.ico", "src/tagsinput/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "tagsinput-demo:build:production" + }, + "development": { + "browserTarget": "tagsinput-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js tagsinput" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/tagsinput/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tagsinput/demo/tsconfig.spec.json", + "karmaConfig": "src/tagsinput/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/tagsinput/demo/src/app/AppComponent.ts b/src/tagsinput/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/tagsinput/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/tagsinput/demo/src/app/AppModule.ts b/src/tagsinput/demo/src/app/AppModule.ts new file mode 100644 index 0000000..ff6ffb1 --- /dev/null +++ b/src/tagsinput/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TagsInputTestModule } from './tagsinput/TagsInputTestModule'; + +@NgModule({ + imports: [ + TagsInputTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/tagsinput/demo/src/app/IndexComponent.ts b/src/tagsinput/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..2a3852c --- /dev/null +++ b/src/tagsinput/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TagsInputTestModule } from './tagsinput/TagsInputTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TagsInputTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/tagsinput/demo/src/app/app.html b/src/tagsinput/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/tagsinput/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsInputTestModule.ts b/src/tagsinput/demo/src/app/tagsinput/TagsInputTestModule.ts new file mode 100644 index 0000000..445f4c0 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsInputTestModule.ts @@ -0,0 +1,105 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiTagsInputModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TagsinputDisabledComponent } from './TagsinputDisabledComponent'; +import { TagsinputSuggestionComponent } from './TagsinputSuggestionComponent'; +import { TagsinputBasicComponent } from './TagsinputBasicComponent'; +import { TagsinputPanelwidthComponent } from './TagsinputPanelwidthComponent'; +import { TagsinputValidComponent } from './TagsinputValidComponent'; +import { TagsinputLabelkeyComponent } from './TagsinputLabelkeyComponent'; +import { TagsinputEventsComponent } from './TagsinputEventsComponent'; +import { TagsinputValuekeyComponent } from './TagsinputValuekeyComponent'; +import { TagsinputNullComponent } from './TagsinputNullComponent'; +import { TagsinputReactiveComponent } from './TagsinputReactiveComponent'; +import { TagsinputTemplateComponent } from './TagsinputTemplateComponent'; +import { TagsinputMaxlengthComponent } from './TagsinputMaxlengthComponent'; +import { TagsinputSeparatorsComponent } from './TagsinputSeparatorsComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiTagsInputModule, + TiValidationModule, + ReactiveFormsModule, + DemoLogModule, + RouterModule.forChild(TagsInputTestModule.ROUTES) + ], + declarations: [ + TagsinputDisabledComponent, + TagsinputSuggestionComponent, + TagsinputBasicComponent, + TagsinputPanelwidthComponent, + TagsinputValidComponent, + TagsinputLabelkeyComponent, + TagsinputEventsComponent, + TagsinputValuekeyComponent, + TagsinputNullComponent, + TagsinputReactiveComponent, + TagsinputTemplateComponent, + TagsinputMaxlengthComponent, + TagsinputSeparatorsComponent + ] +}) +export class TagsInputTestModule { + static readonly LINKS: Array = [{ href: 'components/TiTagsInputComponent.html', label: 'TagsInput' }]; + static readonly ROUTES: Routes = [ + { + path: 'tagsinput/tagsinput-basic', + component: TagsinputBasicComponent + }, + { + path: 'tagsinput/tagsinput-suggestion', + component: TagsinputSuggestionComponent + }, + { + path: 'tagsinput/tagsinput-panelwidth', + component: TagsinputPanelwidthComponent + }, + { + path: 'tagsinput/tagsinput-labelkey', + component: TagsinputLabelkeyComponent + }, + { + path: 'tagsinput/tagsinput-valid', + component: TagsinputValidComponent + }, + { + path: 'tagsinput/tagsinput-disabled', + component: TagsinputDisabledComponent + }, + { + path: 'tagsinput/tagsinput-events', + component: TagsinputEventsComponent + }, + { + path: 'tagsinput/tagsinput-valuekey', + component: TagsinputValuekeyComponent + }, + { + path: 'tagsinput/tagsinput-reactive', + component: TagsinputReactiveComponent + }, + { + path: 'tagsinput/tagsinput-template', + component: TagsinputTemplateComponent + }, + { + path: 'tagsinput/tagsinput-null', + component: TagsinputNullComponent + }, + { + path: 'tagsinput/tagsinput-maxlength', + component: TagsinputMaxlengthComponent + }, + { + path: 'tagsinput/tagsinput-separators', + component: TagsinputSeparatorsComponent + } + ]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputBasicComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputBasicComponent.ts new file mode 100644 index 0000000..d32eaab --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputBasicComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsinputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-basic.html' +}) +export class TagsinputBasicComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + placeholder: string = '当前无选中项'; + selected: Array = []; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputDisabledComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputDisabledComponent.ts new file mode 100644 index 0000000..619aa57 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputDisabledComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsinputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-disabled.html' +}) +export class TagsinputDisabledComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + selected: Array = [this.suggestions[0]]; + disabled: boolean = true; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputEventsComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputEventsComponent.ts new file mode 100644 index 0000000..ea9a34c --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputEventsComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsinputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-events.html' +}) +export class TagsinputEventsComponent { + myLogs: Array = []; + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + selected: Array = [this.suggestions[0]]; + + onModelchange(selected: Array): void { + this.selected = selected; + this.myLogs = [...this.myLogs, `onModelchange() event=${JSON.stringify(selected)}`]; + } +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputLabelkeyComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputLabelkeyComponent.ts new file mode 100644 index 0000000..1c784ed --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputLabelkeyComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tagsinput-labelkey.html' +}) +export class TagsinputLabelkeyComponent { + suggestions: Array = [ + { id: '1', label: '中文', english: 'Chinese' }, + { id: '2', label: '英文', english: 'English' }, + { id: '3', label: '拉美西语', english: 'Latin American' }, + { id: '4', label: '欧洲西语', english: 'European Spanish' }, + { id: '5', label: '法语', english: 'French' }, + { id: '6', label: '葡萄牙语', english: 'Portuguese' } + ]; + selected: Array = [this.suggestions[0]]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputMaxlengthComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputMaxlengthComponent.ts new file mode 100644 index 0000000..789ee54 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputMaxlengthComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tagsinput-maxlength.html' +}) +export class TagsinputMaxlengthComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + selected: Array = []; + placeholder: string = '当前无选中项'; + maxlength: number = 10; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputNullComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputNullComponent.ts new file mode 100644 index 0000000..89f8649 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputNullComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsinputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-null.html' +}) +export class TagsinputNullComponent { + id: string = 'tag'; + selected: Array = []; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputPanelwidthComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputPanelwidthComponent.ts new file mode 100644 index 0000000..325a17f --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputPanelwidthComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +export interface SuggestionItem { + id?: string; + label?: string; +} + +@Component({ + templateUrl: './tagsinput-panelwidth.html' +}) +export class TagsinputPanelwidthComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + selected: Array = [this.suggestions[0]]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputReactiveComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputReactiveComponent.ts new file mode 100644 index 0000000..1483594 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputReactiveComponent.ts @@ -0,0 +1,25 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { SuggestionItem } from './TagsInputPanelwidthComponent'; +@Component({ + templateUrl: './tagsinput-reactive.html' +}) +export class TagsinputReactiveComponent implements OnInit { + form: FormGroup; + constructor(private fb: FormBuilder) {} + + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + + ngOnInit(): void { + this.form = this.fb.group({ + mytagsInput: [[this.suggestions[0]]] // 设置初始值 + }); + } +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputSeparatorsComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputSeparatorsComponent.ts new file mode 100644 index 0000000..bcdc450 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputSeparatorsComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsInputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-separators.html' +}) +export class TagsinputSeparatorsComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + + selected: Array = [this.suggestions[2]]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputSuggestionComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputSuggestionComponent.ts new file mode 100644 index 0000000..dfdcd25 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputSuggestionComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsinputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-suggestion.html' +}) +export class TagsinputSuggestionComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + + selected: Array = [this.suggestions[0]]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputTemplateComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputTemplateComponent.ts new file mode 100644 index 0000000..91824b6 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputTemplateComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsinputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-template.html' +}) +export class TagsinputTemplateComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + + selected: Array = [this.suggestions[0]]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputValidComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputValidComponent.ts new file mode 100644 index 0000000..5c456e0 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputValidComponent.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { SuggestionItem } from './TagsinputPanelwidthComponent'; + +@Component({ + templateUrl: './tagsinput-valid.html' +}) +export class TagsinputValidComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + + selected: Array = [this.suggestions[0]]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/TagsinputValuekeyComponent.ts b/src/tagsinput/demo/src/app/tagsinput/TagsinputValuekeyComponent.ts new file mode 100644 index 0000000..e878e54 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/TagsinputValuekeyComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tagsinput-valuekey.html' +}) +export class TagsinputValuekeyComponent { + suggestions: Array = [ + { id: '1', label: '中文 Chinese' }, + { id: '2', label: '英文 English' }, + { id: '3', label: '拉美西语 Latin American' }, + { id: '4', label: '欧洲西语 European Spanish' }, + { id: '5', label: '法语 French' }, + { id: '6', label: '葡萄牙语 Portuguese' } + ]; + selected: Array = [this.suggestions[0].id]; +} diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-basic.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-basic.html new file mode 100644 index 0000000..83f7e27 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-basic.html @@ -0,0 +1,10 @@ + +
    +
    Current value: {{ selected | json }}
    +
    diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-disabled.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-disabled.html new file mode 100644 index 0000000..155fa78 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-disabled.html @@ -0,0 +1,7 @@ + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-events.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-events.html new file mode 100644 index 0000000..70c4c63 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-events.html @@ -0,0 +1,8 @@ + + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-labelkey.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-labelkey.html new file mode 100644 index 0000000..fbd6c10 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-labelkey.html @@ -0,0 +1 @@ + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-maxlength.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-maxlength.html new file mode 100644 index 0000000..ca47254 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-maxlength.html @@ -0,0 +1,11 @@ + +
    +
    Current value: {{ selected | json }}
    +
    diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-null.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-null.html new file mode 100644 index 0000000..6a6c88d --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-null.html @@ -0,0 +1,17 @@ +

    描述

    +

    测试suggestion接口为null、undefined、空数据时,组件功能是否正常

    +

    示例

    +

    1.undefined:

    +
    +

    选中项: {{selected | json}}

    + +

    +

    2.null:

    +
    +

    选中项: {{selected | json}}

    + +

    +

    3.空数据[]:

    +
    +

    选中项: {{selected | json}}

    + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-panelwidth.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-panelwidth.html new file mode 100644 index 0000000..2dc84bf --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-panelwidth.html @@ -0,0 +1 @@ + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-reactive.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-reactive.html new file mode 100644 index 0000000..6982540 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-reactive.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-separators.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-separators.html new file mode 100644 index 0000000..7dba87e --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-separators.html @@ -0,0 +1,10 @@ + +
    +
    Current value: {{ selected | json }}
    +
    diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-suggestion.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-suggestion.html new file mode 100644 index 0000000..7e06bb5 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-suggestion.html @@ -0,0 +1 @@ + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-template.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-template.html new file mode 100644 index 0000000..c4dda27 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-template.html @@ -0,0 +1,5 @@ + + + {{i}} {{item.label}} + + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-valid.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-valid.html new file mode 100644 index 0000000..8e10751 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-valid.html @@ -0,0 +1 @@ + diff --git a/src/tagsinput/demo/src/app/tagsinput/tagsinput-valuekey.html b/src/tagsinput/demo/src/app/tagsinput/tagsinput-valuekey.html new file mode 100644 index 0000000..ba95bf2 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/tagsinput-valuekey.html @@ -0,0 +1,4 @@ + +
    +
    Current value: {{ selected | json }}
    +
    diff --git a/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput-demos.js b/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput-demos.js new file mode 100644 index 0000000..1acfd27 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput-demos.js @@ -0,0 +1,135 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'tagsinput-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic' + }, + desc: { + 'zh-CN': '

    TagsInput 组件的最简用法。

    ', + 'en-US': '' + }, + apis: ['TiTagsInputComponent.properties.suggestions', 'TiTagsInputComponent.properties.placeholder'] + }, + { + demoId: 'tagsinput-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled' + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': '' + }, + apis: ['TiTagsInputComponent.properties.disabled'] + }, + { + demoId: 'tagsinput-events', + name: { + 'zh-CN': '事件', + 'en-US': 'events' + }, + desc: { + 'zh-CN': '

    当元素内容发生变化的时候触发ngModelChange事件。

    ', + 'en-US': 'tagsinput events description' + } + }, + { + demoId: 'tagsinput-labelkey', + name: { + 'zh-CN': '显示文本', + 'en-US': 'labelkey' + }, + desc: { + 'zh-CN': '

    通过属性labelKey配置显示文本的键值。

    ', + 'en-US': '' + }, + apis: ['TiTagsInputComponent.properties.labelKey'] + }, + { + demoId: 'tagsinput-panelwidth', + name: { + 'zh-CN': '下拉面板宽度', + 'en-US': 'panelwidth' + }, + desc: { + 'zh-CN': + '

    通过属性panelWidth配置下拉面板宽度,包含autojustifiedstring三种类型。

    ', + 'en-US': '' + }, + apis: ['TiTagsInputComponent.properties.panelWidth'] + }, + { + demoId: 'tagsinput-valid', + name: { + 'zh-CN': '校验', + 'en-US': 'tagsinput validation' + }, + desc: { + 'zh-CN': '

    通过指令tiValidation实现校验。

    ', + 'en-US': '' + } + }, + { + demoId: 'tagsinput-valuekey', + name: { + 'zh-CN': '绑定值为基础类型', + 'en-US': 'valuekey' + }, + desc: { + 'zh-CN': '

    通过属性valueKey配置绑定值为基础类型的值。

    ', + 'en-US': '' + }, + apis: ['TiTagsInputComponent.properties.valueKey'] + }, + { + demoId: 'tagsinput-separators', + name: { + 'zh-CN': '自动分词', + 'en-US': 'separators' + }, + desc: { + 'zh-CN': '

    通过属性separators配置自动分词的分隔符,试着粘贴下中文 Chinese,法语 French|泰文。', + 'en-US': '' + }, + apis: ['TiTagsInputComponent.properties.separators'] + }, + { + demoId: 'tagsinput-reactive', + name: { + 'zh-CN': '响应式表单', + 'en-US': 'reactive-form' + }, + desc: { + 'zh-CN': '

    响应式表单的基本用法。

    ', + 'en-US': '' + } + }, + { + demoId: 'tagsinput-template', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'template' + }, + desc: { + 'zh-CN': '

    通过#item配置下拉面板中选项的模板。

    ', + 'en-US': '' + } + }, + { + demoId: 'tagsinput-maxlength', + name: { + 'zh-CN': '允许的最大字符数', + 'en-US': 'maxlength' + }, + desc: { + 'zh-CN': '

    通过属性maxlength配置输入框中允许的最大字符数。

    ', + 'en-US': '' + }, + apis: ['TiTagsInputComponent.properties.maxlength'] + } + ] +}; diff --git a/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.cn.md b/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.cn.md new file mode 100644 index 0000000..b49358e --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.cn.md @@ -0,0 +1,40 @@ +--- +title: TagsInput 标签输入 +--- +# TagsInput 标签输入 + +
    + +TagsInput 是标签输入框组件。   + ++ 支持输入框输入标签、可以联想选择标签等场景。 + +```typescript +import { TiTagsInputModule } from '@opentiny/ng'; +``` + ++ 如需使用校验功能,请导入。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
    + +
    + +TagsInput 是标签输入框组件。   + ++ 支持输入框输入标签、可以联想选择标签等场景。 + +```typescript +import { TiTagsInputModule } from '@cloud/tiny-config'; +``` + ++ 如需使用校验功能,请导入。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +
    diff --git a/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.en.md b/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/tagsinput/demo/src/app/tagsinput/webdoc/tagsinput.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/tagsinput/demo/src/favicon.ico b/src/tagsinput/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/tagsinput/demo/src/index.html b/src/tagsinput/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/tagsinput/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/tagsinput/demo/src/main.ts b/src/tagsinput/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/tagsinput/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/tagsinput/demo/test.ts b/src/tagsinput/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/tagsinput/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/tagsinput/demo/tsconfig.app.json b/src/tagsinput/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/tagsinput/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/tagsinput/demo/tsconfig.spec.json b/src/tagsinput/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/tagsinput/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/tagsinput/lib/index.ts b/src/tagsinput/lib/index.ts new file mode 100644 index 0000000..64bf38f --- /dev/null +++ b/src/tagsinput/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTagsInputModule'; diff --git a/src/tagsinput/lib/ng-package.json b/src/tagsinput/lib/ng-package.json new file mode 100644 index 0000000..8fed06b --- /dev/null +++ b/src/tagsinput/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/tagsinput", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/tagsinput/lib/package.json b/src/tagsinput/lib/package.json new file mode 100644 index 0000000..aa2b6c7 --- /dev/null +++ b/src/tagsinput/lib/package.json @@ -0,0 +1,15 @@ +{ + "name": "@opentiny/ng-tagsinput", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-dominator": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-dropsearch": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/tagsinput/lib/project.json b/src/tagsinput/lib/project.json new file mode 100644 index 0000000..0947c6e --- /dev/null +++ b/src/tagsinput/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/tagsinput/lib", + "sourceRoot": "src/tagsinput/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/tagsinput"], + "options": { + "project": "src/tagsinput/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/tagsinput"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js tagsinput" + }, + { + "command": "ng default-build tagsinput" + }, + { + "command": "node build/clear-default-theme.js tagsinput" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/tagsinput && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build tagsinput && ng pack tagsinput && node build/publish.js tagsinput --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/tagsinput/lib/src/TiTagsInputComponent.ts b/src/tagsinput/lib/src/TiTagsInputComponent.ts new file mode 100644 index 0000000..2ceaa31 --- /dev/null +++ b/src/tagsinput/lib/src/TiTagsInputComponent.ts @@ -0,0 +1,479 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ContentChild, ElementRef, Input, TemplateRef, ViewChild } from '@angular/core'; +import { TiDominatorComponent } from '@opentiny/ng-dominator'; +import { TiFormComponent, TiWholeComponent } from '@opentiny/ng-base'; +import { TiDropsearchComponent } from '@opentiny/ng-dropsearch'; +import { TiKeymap } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; +/** + * TagsInput标签输入组件 + * + * TagsInput组件主要实现了一个可以输入标签、可以联想选择标签的功能组件。 + * + */ +@Component({ + selector: 'ti-tags-input', + templateUrl: './tagsinput.html', + styleUrls: ['./tagsinput.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiTagsInputComponent)], + host: { + '(blur)': 'onBlur()' + } +}) +export class TiTagsInputComponent extends TiWholeComponent { + /** + * 必选,下拉建议项 + */ + @Input() suggestions: Array = []; + /** + * 输入框的占位文本 + */ + @Input() placeholder: string = ''; + /** + * 下拉面板的宽度。 + * + * 1."justified": 下拉面板宽度与输入框宽度保持一致; + * + * 2."auto": 下拉面板宽度根据下拉选项的内容自动撑开; + * + * 3.固定的下拉面板宽度: 不小于输入框组件的宽度,例如:"200px" + */ + @Input() panelWidth: string = 'justified'; + /** + * 下拉建议项要展示的键值 + */ + @Input() labelKey: string = 'label'; + /** + * 输入框允许的最大字符数 + */ + @Input() maxlength: number; + /** + * 自动分词的分隔符,12.1.28 新增 + */ + @Input() separators: Array = []; + /** + * @ignore + * 内部的input标签 + */ + @ViewChild('input', { static: true }) public inputRef: ElementRef; + /** + * @ignore + * 用户写的item模板 + */ + @ContentChild(TemplateRef, { static: true }) itemTemplate: TemplateRef; + /** + * @ignore + * 内部标签donimator组件 + */ + @ViewChild(TiDominatorComponent, { static: true }) + dominatorCom: TiDominatorComponent; + /** + * @ignore + * 内部标签selectDrop组件 + */ + @ViewChild(TiDropsearchComponent, { static: true }) + selectDrop: TiDropsearchComponent; + /** + * @ignore + * input输入框的值 + */ + public inputValue: string = ''; // 输入框内容 + /** + * @ignore + * 兼容TiWholeComponent,该组件仅多选 + */ + public multiple: boolean = true; + /** + * 自动分词的分隔符正则 + * + * @private + */ + private separatorsReg: RegExp; + /** + * 记录粘贴的文本 + * + * @private + */ + private pasteValue: string; + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + super.ngOnInit(); + if (this.separators.length !== 0) { + const separatorsStr: string = this.separators.join(''); + this.separatorsReg = new RegExp(`[${separatorsStr}]`); + } + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + // 推荐在onInit()时调用setFocusableElems(), 但是ngFor/ngIf中的元素在ngAfterViewInit()才能获取到 + this.setFocusableElems([this.dominatorCom.nativeElement, this.inputRef.nativeElement]); + } + + /** + * @ignore + * 处理select事件 + */ + onSelect(event: any): void { + // 当前内容对应的tag不在选中列表中或者不是输入生成的tag + + if (this.findFirstIndex(this.modelWhole, this.labelKey, event[this.labelKey]) === -1 || !event.isInput) { + this.modelWhole.push(event); + this.modelWhole = this.modelWhole.concat(); + } + + this.selectDrop.searchResult = this.getSuggestions(this.suggestions, this.modelWhole); + // 去除选中项样式 + this.selectDrop.listCom.model = undefined; + + // 清空输入框 + this.inputValue = ''; + // 选中项由一行变为两行时,需要重定位 + setTimeout(() => { + this.selectDrop.rePosition(); + }, 0); + } + + /** + * @ignore + * 处理点击Dominator事件 + */ + onClickDominator(): void { + if (this.disabled) { + return; + } + + this.inputRef.nativeElement.focus(); + } + + /** + * @ignore + * 点击选中项的叉号:从选中项中移除当前选中项 + */ + onDelete(event: { item: any; model: any }): void { + this.inputRef.nativeElement.focus(); + // 选中项由两行变为一行时,需要重定位 + // 在app onpush和app default两种模式测试,确实需要settimeout,但不需要markfor + setTimeout(() => { + this.selectDrop.rePosition(); + }, 0); + this.selectDrop.searchResult = this.getSuggestions(this.suggestions, event.model); + } + + /** + * @ignore + * 组件整体失焦时,面板收起 + */ + onBlur(): void { + this.selectDrop.hide(); + } + + /** + * @ignore + * 输入框失焦时,面板隐藏,根据文本框内容处理是否添加到选中项 + */ + onInputBlur(): void { + const value: string = this.inputValue; + + // 清空输入框 + this.inputValue = ''; + + // 输入框为空时或者当前输入框内容对应的标签已经在选中列表中了 + if (value.trim() === '' || this.findFirstIndex(this.modelWhole, this.labelKey, value) !== -1) { + return; + } + // 将输入框内容添加到选中标签中 + this.addTagToSelected(value); + } + + /** + * @ignore + * 处理input框聚焦事件 + */ + onInputFocus(event: FocusEvent): void { + this.selectDrop.searchResult = this.getSuggestions(this.suggestions, this.modelWhole); + // 视图绘制早于变量更新,变量更新后下拉定位不正确, 需要延时处理 + setTimeout(() => { + this.showSelectDrop(); + }, 0); + } + + /** + * @ignore + * 处理input输入框keyup快捷键功能 + */ + onInputKeyup(event: KeyboardEvent): void { + if (event.keyCode === TiKeymap.KEY_ENTER || event.keyCode === TiKeymap.KEY_NUMPAD_ENTER) { + this.responseEnter(event); + } + } + + /** + * @ignore + * 处理input输入框keydown快捷键功能 + * 回删键只能在keydown中处理:因为回删时,在keydown时输入框为当前值,keyup时为删除后的值 + * 比如输入一个字符,回删,获取的应该是回删前的一个字符,而不是没有删除后的 + */ + onInputKeydown(event: KeyboardEvent): void { + if (event.keyCode === TiKeymap.KEY_BACKSPACE) { + this.responseBackspace(event); + } + } + + /** + * @ignore + * 获取粘贴时的文本 + * @param event + */ + onInputPaste(event: any): void { + this.pasteValue = event.clipboardData.getData('Text'); + } + + /** + * @ignore + * 输入框内容变化时的处理 + * @param inputValue + */ + onInputChange(inputValue: string): void { + let value: string = inputValue; + + // 此处在粘贴事件中获取值的原因:当粘贴值包含换行符时,输入框会将换行符处理为空格 + if (this.pasteValue) { + value = this.pasteValue; + this.pasteValue = ''; + } + + // 输入值匹配到分隔符时 + if (this.separatorsReg?.test(value)) { + this.tokenSeparate(value); + + return; + } + + // 记录上次匹配项长度 + const oldSearchResultLength: number = this.selectDrop.searchResult?.length; + // 过滤出匹配项 + this.selectDrop.searchWordChange(inputValue); + + // 由于dropsearch中searchWordChange调用setSearchResult方法,当匹配项长度变化时才会去重定位 + // tagsinput组件 匹配项长度相同选中项行数变化时,也需要重定位 + if (this.selectDrop.searchResult?.length === oldSearchResultLength) { + setTimeout(() => { + this.selectDrop.rePosition(); + }, 0); + } + + // 从匹配项中移除已选中项 + this.selectDrop.searchResult = this.getSuggestions(this.selectDrop.searchResult, this.modelWhole); + + // 输入框为空时或者当前输入框内容对应的标签已经在选中列表中了,无需将输入框值添加至建议项中 + if (inputValue.trim() === '' || this.findFirstIndex(this.selectDrop.searchResult, this.labelKey, inputValue) !== -1) { + return; + } + + // 有labelkey和valueKey时,需要设置对应键值 + if (this.valueKey) { + this.selectDrop.searchResult.unshift({ + id: inputValue, + [this.labelKey]: inputValue, + [this.valueKey]: inputValue, + isInput: true + }); + } else { + this.selectDrop.searchResult.unshift({ + id: inputValue, + [this.labelKey]: inputValue, + isInput: true + }); + } + } + + /** + * 处理input输入框enter键功能 + * @param event + */ + private responseEnter(event: KeyboardEvent): void { + // 获取输入框的值 + const value: string = this.inputValue; + + // 当有建议项存在时,按回车键会执行下拉建议项select事件逻辑,此处无需再处理 + if (this.suggestions && this.suggestions.length !== 0) { + return; + } + + // 清空input框的内容 + this.inputValue = ''; + + this.addTag(value); + } + + /** + * 处理input输入框回删键功能 + * @param event + */ + private responseBackspace(event: KeyboardEvent): void { + this.selectDrop.hide(); + // 面板关闭时 + if (!this.selectDrop.isShow) { + this.showSelectDrop(); + // 选中项由两行变为一行时,需要重定位 + setTimeout(() => { + this.selectDrop.rePosition(); + }, 0); + } + const value: string = this.inputRef.nativeElement.value; + // 输入框值不为空字符串时 + if (value !== '') { + return; + } + + // 获取当前选中项的长度 + const modelLength: number = this.modelWhole.length; + + // 当前已经没有选中项时,不做处理 + if (modelLength === 0) { + return; + } + + const lastSelected: any = this.modelWhole[modelLength - 1]; + const index: number = this.suggestions?.indexOf(lastSelected); + // 从选中项中删除 + this.modelWhole.pop(); + this.modelWhole = this.modelWhole.concat(); + + // 将要从选中项删除的最后一项是下拉选项中的一项时,将其添加到现有的下拉选项中; + // 如果是用户自己创建的,则不放入下拉项中 + if (index !== -1) { + this.selectDrop.searchResult = this.getSuggestions(this.suggestions, this.modelWhole); + } + } + + /** + * 根据数组中属性找是否有匹配项 + * @param arr 需要匹配的数组 + * @param key 需要查找的属性 + * @param value 属性值 + * @returns + */ + private findFirstIndex(arr: any, key: string, value: string): number { + if (!(arr instanceof Array)) { + return -1; + } + + return arr.findIndex((i: any) => i[key] === value); + } + + /** + * 将输入框内容添加到选中标签中 + * @param inputValue 输入框的内容 + */ + private addTagToSelected(inputValue: string): void { + const index: number = this.findFirstIndex(this.suggestions, this.labelKey, inputValue); + // 有labelkey和valueKey时,需要设置对应键值 + const newTag: any = + index === -1 + ? this.valueKey + ? { + id: inputValue, + [this.labelKey]: inputValue, + [this.valueKey]: inputValue + } + : { id: inputValue, [this.labelKey]: inputValue } + : this.suggestions[index]; + this.modelWhole.push(newTag); + this.modelWhole = this.modelWhole.concat(); + } + + /** + * 从suggestions中删除选中项 + */ + private getSuggestions(suggestions: any, selected: any): Array { + const newSuggestions: any = !(suggestions instanceof Array) ? [] : suggestions.concat(); // 使用数组的concat方法,实现深拷贝 + + let index: number; + for (const select of selected) { + index = newSuggestions.indexOf(select); + + if (index !== -1) { + // 选中项如果在下拉列表中存在,则删除 + newSuggestions.splice(index, 1); + } + } + + // 下拉建议项数据更新时,需要手动触发变化检测 + if (newSuggestions !== this.selectDrop.searchResult) { + this.selectDrop.changeDetectorRef.markForCheck(); + } + + return newSuggestions; + } + + /** + * @ignore + * 展开面板时设置下拉选中项和hoverOption + */ + public showSelectDrop(): void { + if (!this.suggestions || (this.suggestions && this.suggestions.length === 0)) { + return; + } + // 显示下拉面板 + this.selectDrop.show(); + + if (!this.selectDrop.listCom) { + return; + } + // 设置selectDrop的选中项 + this.selectDrop.listCom.model = undefined; + } + + /** + * 自动分词时,将输入框的值分隔生成 tag 并处理建议项数据 + * + * @private + * @param value + */ + private tokenSeparate(value: string): void { + const valueArr: Array = value.split(this.separatorsReg); + + // 清空输入框的值 + this.inputRef.nativeElement.value = ''; + this.inputValue = ''; + + valueArr.forEach((item: string) => { + // 过滤出匹配项 + this.selectDrop.searchWordChange(item); + this.addTag(item); + }); + + // 从匹配项中移除已选中项 + this.selectDrop.searchResult = this.getSuggestions(this.suggestions, this.modelWhole); + } + + /** + * 生成 tag 标签 + * + * @private + * @param value + * @returns + */ + private addTag(value: string): void { + // 输入框为空时或者当前输入框内容对应的标签已经在选中列表中了 + if (value.trim() === '' || this.findFirstIndex(this.modelWhole, this.labelKey, value) !== -1) { + return; + } + + this.addTagToSelected(value); + } +} diff --git a/src/tagsinput/lib/src/TiTagsInputModule.ts b/src/tagsinput/lib/src/TiTagsInputModule.ts new file mode 100644 index 0000000..30830b5 --- /dev/null +++ b/src/tagsinput/lib/src/TiTagsInputModule.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TiTagsInputComponent } from './TiTagsInputComponent'; +import { TiDropsearchModule } from '@opentiny/ng-dropsearch'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiDominatorModule } from '@opentiny/ng-dominator'; +/** + * + */ +@NgModule({ + imports: [CommonModule, FormsModule, TiDominatorModule, TiDropsearchModule, TiTextModule], + exports: [TiTagsInputComponent], + declarations: [TiTagsInputComponent] +}) +export class TiTagsInputModule {} +export { TiTagsInputComponent } from './TiTagsInputComponent'; diff --git a/src/tagsinput/lib/src/tagsinput.html b/src/tagsinput/lib/src/tagsinput.html new file mode 100644 index 0000000..8a3c759 --- /dev/null +++ b/src/tagsinput/lib/src/tagsinput.html @@ -0,0 +1,63 @@ + + + + +
    + + + {{inputValue}} +
    +
    + + + + + + + + + + + {{item[labelKey]}} + diff --git a/src/tagsinput/lib/src/tagsinput.less b/src/tagsinput/lib/src/tagsinput.less new file mode 100644 index 0000000..1118ac1 --- /dev/null +++ b/src/tagsinput/lib/src/tagsinput.less @@ -0,0 +1,37 @@ +@import '../../../themes/basic/base-all.less'; +@import '../../../themes/basic/compnent-container-border.less'; + +:host { + &:extend(.ti3-compnent-container-border all); +} + +.ti3-dominator-tagsinput { + input[tiText] { + position: absolute; + left: 0; + padding: var(--ti-common-space-0) var(--ti-common-space-10); + width: 100%; + height: 100%; + .box-sizing(border-box); + background-color: transparent; // 添加透明背景是为了覆盖tiText设置的白色背景,使校验结果样式生效 + } + .ti3-dominator-input-container { + position: relative; + display: inline-block; + vertical-align: middle; + overflow: hidden; + max-width: 100%; + height: calc( + var(--ti-common-size-7x) - var(--ti-common-border-weight-normal) * 2 - var(--ti-common-space-1) * 2 + ); // tagsinput上下边框2px + dominator内边距2px = 4px + padding: var(--ti-common-space-0) var(--ti-common-space-10); + .box-sizing(border-box); + & > span { + position: relative; + display: block; + visibility: hidden; + max-width: 100%; + height: 100%; + } + } +} diff --git a/src/text/demo/karma.conf.js b/src/text/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/text/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/text/demo/project.json b/src/text/demo/project.json new file mode 100644 index 0000000..b5edca5 --- /dev/null +++ b/src/text/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/text/demo", + "sourceRoot": "src/text/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/text", + "index": "src/text/demo/src/index.html", + "main": "src/text/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/text/demo/tsconfig.app.json", + "assets": ["src/text/demo/src/favicon.ico", "src/text/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "text-demo:build:production" + }, + "development": { + "browserTarget": "text-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js text" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/text/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/text/demo/tsconfig.spec.json", + "karmaConfig": "src/text/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/text/demo/src/app/AppComponent.ts b/src/text/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/text/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/text/demo/src/app/AppModule.ts b/src/text/demo/src/app/AppModule.ts new file mode 100644 index 0000000..491ba9d --- /dev/null +++ b/src/text/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TextTestModule } from './text/TextTestModule'; + +@NgModule({ + imports: [ + TextTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/text/demo/src/app/IndexComponent.ts b/src/text/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..73138ad --- /dev/null +++ b/src/text/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TextTestModule } from './text/TextTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TextTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/text/demo/src/app/app.html b/src/text/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/text/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/text/demo/src/app/text/TextBasicComponent.ts b/src/text/demo/src/app/text/TextBasicComponent.ts new file mode 100644 index 0000000..d23424f --- /dev/null +++ b/src/text/demo/src/app/text/TextBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + templateUrl: './text-basic.html' +}) +export class TextBasicComponent { + value: string = '长期艰苦奋斗'; +} diff --git a/src/text/demo/src/app/text/TextClearComponent.ts b/src/text/demo/src/app/text/TextClearComponent.ts new file mode 100644 index 0000000..fe3e56b --- /dev/null +++ b/src/text/demo/src/app/text/TextClearComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './text-clear.html' +}) +export class TextClearComponent { + value: string = '长期艰苦奋斗'; +} diff --git a/src/text/demo/src/app/text/TextDisabledComponent.ts b/src/text/demo/src/app/text/TextDisabledComponent.ts new file mode 100644 index 0000000..6768cd9 --- /dev/null +++ b/src/text/demo/src/app/text/TextDisabledComponent.ts @@ -0,0 +1,9 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + templateUrl: './text-disabled.html' +}) +export class TextDisabledComponent { + value: string = '长期艰苦奋斗'; + disabled: boolean = true; +} diff --git a/src/text/demo/src/app/text/TextEventsComponent.ts b/src/text/demo/src/app/text/TextEventsComponent.ts new file mode 100644 index 0000000..005b72f --- /dev/null +++ b/src/text/demo/src/app/text/TextEventsComponent.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './text-events.html' +}) +export class TextEventsComponent { + myLogs: Array = []; + value: string = '长期艰苦奋斗'; + + onClear(value: string): void { + this.myLogs = [...this.myLogs, `clear:${value}`]; + } + + onFocus(value: string): void { + this.myLogs = [...this.myLogs, `focus:${value}`]; + } + + onBlur(value: string): void { + this.myLogs = [...this.myLogs, `blur:${value}`]; + } + + onNgModelChange(value: string): void { + this.myLogs = [...this.myLogs, `modelChange:${value}`]; + } +} diff --git a/src/text/demo/src/app/text/TextFocusComponent.ts b/src/text/demo/src/app/text/TextFocusComponent.ts new file mode 100644 index 0000000..f589d7d --- /dev/null +++ b/src/text/demo/src/app/text/TextFocusComponent.ts @@ -0,0 +1,8 @@ +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; + +@Component({ + templateUrl: './text-focus.html' +}) +export class TextFocusComponent { + value: string = '长期艰苦奋斗'; +} diff --git a/src/text/demo/src/app/text/TextMaskinputComponent.ts b/src/text/demo/src/app/text/TextMaskinputComponent.ts new file mode 100644 index 0000000..0881902 --- /dev/null +++ b/src/text/demo/src/app/text/TextMaskinputComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './text-maskinput.html' +}) +export class TextMaskinputComponent { + mask: string = '000-0000-0000'; + value: string = 'dfasd13af464sd4fasdfas7d8asdf4as5df4'; +} diff --git a/src/text/demo/src/app/text/TextNoborderTestComponent.ts b/src/text/demo/src/app/text/TextNoborderTestComponent.ts new file mode 100644 index 0000000..8d04c49 --- /dev/null +++ b/src/text/demo/src/app/text/TextNoborderTestComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './text-noborder-test.html' +}) +export class TextNoborderTestComponent { + noborder: boolean = true; + value: string = '长期艰苦奋斗'; +} diff --git a/src/text/demo/src/app/text/TextPasswordComponent.ts b/src/text/demo/src/app/text/TextPasswordComponent.ts new file mode 100644 index 0000000..776828c --- /dev/null +++ b/src/text/demo/src/app/text/TextPasswordComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './text-password.html' +}) +export class TextPasswordComponent { + passwordValue: string = '123456'; +} diff --git a/src/text/demo/src/app/text/TextPasswordVisibleComponent.ts b/src/text/demo/src/app/text/TextPasswordVisibleComponent.ts new file mode 100644 index 0000000..b2908e7 --- /dev/null +++ b/src/text/demo/src/app/text/TextPasswordVisibleComponent.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './text-password-visible.html' +}) +export class TextPasswordVisibleComponent { + myLogs: Array = []; + + passwordValue: string = '123456'; + isVisible: boolean = false; + + visibleChange(passwordVisible: boolean): void { + this.isVisible = passwordVisible; + this.myLogs = [...this.myLogs, `passwordVisibleState:${passwordVisible}`]; + } + + changeVisible(): void { + this.isVisible = !this.isVisible; + this.myLogs = [...this.myLogs, `passwordVisibleState:${this.isVisible}`]; + } +} diff --git a/src/text/demo/src/app/text/TextReactiveComponent.ts b/src/text/demo/src/app/text/TextReactiveComponent.ts new file mode 100644 index 0000000..bb1fa8f --- /dev/null +++ b/src/text/demo/src/app/text/TextReactiveComponent.ts @@ -0,0 +1,16 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; + +@Component({ + templateUrl: './text-reactive.html' +}) +export class TextReactiveComponent implements OnInit { + form: FormGroup; + constructor(private fb: FormBuilder) {} + + ngOnInit(): void { + this.form = this.fb.group({ + myInput: ['长期艰苦奋斗'] + }); + } +} diff --git a/src/text/demo/src/app/text/TextReadonlyComponent.ts b/src/text/demo/src/app/text/TextReadonlyComponent.ts new file mode 100644 index 0000000..c2ad8de --- /dev/null +++ b/src/text/demo/src/app/text/TextReadonlyComponent.ts @@ -0,0 +1,8 @@ +import { Component, ElementRef, Renderer2, ViewChild } from '@angular/core'; + +@Component({ + templateUrl: './text-readonly.html' +}) +export class TextReadonlyComponent { + value: string = '长期艰苦奋斗'; +} diff --git a/src/text/demo/src/app/text/TextTestModule.ts b/src/text/demo/src/app/text/TextTestModule.ts new file mode 100644 index 0000000..91af0c1 --- /dev/null +++ b/src/text/demo/src/app/text/TextTestModule.ts @@ -0,0 +1,90 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiTextModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TextBasicComponent } from './TextBasicComponent'; +import { TextClearComponent } from './TextClearComponent'; +import { TextPasswordComponent } from './TextPasswordComponent'; +import { TextReadonlyComponent } from './TextReadonlyComponent'; +import { TextFocusComponent } from './TextFocusComponent'; +import { TextDisabledComponent } from './TextDisabledComponent'; +import { TextEventsComponent } from './TextEventsComponent'; +import { TextNoborderTestComponent } from './TextNoborderTestComponent'; +import { TextReactiveComponent } from './TextReactiveComponent'; +import { TextPasswordVisibleComponent } from './TextPasswordVisibleComponent'; +import { TextMaskinputComponent } from './TextMaskinputComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiButtonModule, + TiTextModule, + DemoLogModule, + RouterModule.forChild(TextTestModule.ROUTES) + ], + declarations: [ + TextBasicComponent, + TextClearComponent, + TextPasswordComponent, + TextReadonlyComponent, + TextFocusComponent, + TextDisabledComponent, + TextEventsComponent, + TextNoborderTestComponent, + TextReactiveComponent, + TextPasswordVisibleComponent, + TextMaskinputComponent + ] +}) +export class TextTestModule { + static readonly LINKS: Array = [{ href: 'components/TiTextComponent.html', label: 'Text' }]; + static readonly ROUTES: Routes = [ + { + path: 'text/text-basic', + component: TextBasicComponent + }, + { + path: 'text/text-clear', + component: TextClearComponent + }, + { + path: 'text/text-password', + component: TextPasswordComponent + }, + { + path: 'text/text-password-visible', + component: TextPasswordVisibleComponent + }, + { + path: 'text/text-readonly', + component: TextReadonlyComponent + }, + { + path: 'text/text-focus', + component: TextFocusComponent + }, + { + path: 'text/text-disabled', + component: TextDisabledComponent + }, + { + path: 'text/text-events', + component: TextEventsComponent + }, + { + path: 'text/text-reactive', + component: TextReactiveComponent + }, + { path: 'text/text-noborder-test', component: TextNoborderTestComponent }, + { + path: 'text/text-maskinput', + component: TextMaskinputComponent + } + ]; +} diff --git a/src/text/demo/src/app/text/text-basic.html b/src/text/demo/src/app/text/text-basic.html new file mode 100644 index 0000000..2a70c3f --- /dev/null +++ b/src/text/demo/src/app/text/text-basic.html @@ -0,0 +1,4 @@ + +
    +
    Current value: {{ value }}
    +
    diff --git a/src/text/demo/src/app/text/text-clear.html b/src/text/demo/src/app/text/text-clear.html new file mode 100644 index 0000000..40073f0 --- /dev/null +++ b/src/text/demo/src/app/text/text-clear.html @@ -0,0 +1 @@ + diff --git a/src/text/demo/src/app/text/text-disabled.html b/src/text/demo/src/app/text/text-disabled.html new file mode 100644 index 0000000..a1dc978 --- /dev/null +++ b/src/text/demo/src/app/text/text-disabled.html @@ -0,0 +1 @@ + diff --git a/src/text/demo/src/app/text/text-events.html b/src/text/demo/src/app/text/text-events.html new file mode 100644 index 0000000..83c0dea --- /dev/null +++ b/src/text/demo/src/app/text/text-events.html @@ -0,0 +1,13 @@ + +
    + diff --git a/src/text/demo/src/app/text/text-focus.html b/src/text/demo/src/app/text/text-focus.html new file mode 100644 index 0000000..10266c9 --- /dev/null +++ b/src/text/demo/src/app/text/text-focus.html @@ -0,0 +1 @@ + diff --git a/src/text/demo/src/app/text/text-maskinput.html b/src/text/demo/src/app/text/text-maskinput.html new file mode 100644 index 0000000..7059dfb --- /dev/null +++ b/src/text/demo/src/app/text/text-maskinput.html @@ -0,0 +1,4 @@ + +
    +
    Current Value: {{ value }}
    +
    diff --git a/src/text/demo/src/app/text/text-noborder-test.html b/src/text/demo/src/app/text/text-noborder-test.html new file mode 100644 index 0000000..0039ff1 --- /dev/null +++ b/src/text/demo/src/app/text/text-noborder-test.html @@ -0,0 +1,2 @@ +

    1.noborder接口测试:

    + diff --git a/src/text/demo/src/app/text/text-password-visible.html b/src/text/demo/src/app/text/text-password-visible.html new file mode 100644 index 0000000..0af42ac --- /dev/null +++ b/src/text/demo/src/app/text/text-password-visible.html @@ -0,0 +1,12 @@ + +
    +
    + + diff --git a/src/text/demo/src/app/text/text-password.html b/src/text/demo/src/app/text/text-password.html new file mode 100644 index 0000000..e5d3e7d --- /dev/null +++ b/src/text/demo/src/app/text/text-password.html @@ -0,0 +1,5 @@ + + +
    +
    + diff --git a/src/text/demo/src/app/text/text-reactive.html b/src/text/demo/src/app/text/text-reactive.html new file mode 100644 index 0000000..0280fa1 --- /dev/null +++ b/src/text/demo/src/app/text/text-reactive.html @@ -0,0 +1,6 @@ +
    + +
    +
    +
    Current value: {{ form.value.myInput }}
    +
    diff --git a/src/text/demo/src/app/text/text-readonly.html b/src/text/demo/src/app/text/text-readonly.html new file mode 100644 index 0000000..3705577 --- /dev/null +++ b/src/text/demo/src/app/text/text-readonly.html @@ -0,0 +1 @@ + diff --git a/src/text/demo/src/app/text/text.spec.ts b/src/text/demo/src/app/text/text.spec.ts new file mode 100644 index 0000000..61d0583 --- /dev/null +++ b/src/text/demo/src/app/text/text.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; + +import { TiTextModule, TiTextComponent } from '../../../../lib/src/TiTextModule'; +import { TextBasicComponent } from './TextBasicComponent'; +NamedNodeMap; +describe('text', () => { + let testComponent: TextBasicComponent; + let fixture: ComponentFixture; + let testDebugEl: DebugElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TiTextModule, FormsModule], + declarations: [TiTextComponent, TextBasicComponent] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TextBasicComponent); + testComponent = fixture.componentInstance; + testDebugEl = fixture.debugElement.query(By.directive(TiTextComponent)); + }); + + describe('text basic', () => { + it('should component defined', () => { + fixture.detectChanges(); + expect(testComponent).toBeDefined(); + }); + + it('should id correct', () => { + fixture.detectChanges(); + expect(testDebugEl.nativeElement!.getAttribute('id')).toEqual('text-basic'); + }); + + it('should ngModel work', fakeAsync(() => { + const testNativeEl = testDebugEl.nativeElement; + fixture.detectChanges(); + tick(); + expect(testNativeEl.value).toEqual('长期艰苦奋斗'); + testNativeEl.value = 'My string'; + var event = new Event('input', { + bubbles: true, + cancelable: true + }); + testNativeEl.dispatchEvent(event); + fixture.detectChanges(); + tick(); + expect(testNativeEl.value).toEqual('My string'); + })); + }); +}); diff --git a/src/text/demo/src/app/text/webdoc/text-demos.js b/src/text/demo/src/app/text/webdoc/text-demos.js new file mode 100644 index 0000000..f3d46cc --- /dev/null +++ b/src/text/demo/src/app/text/webdoc/text-demos.js @@ -0,0 +1,131 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'text-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Text 组件的最简用法。

    ', + 'en-US': '

    basic

    ', + }, + }, + { + demoId: 'text-clear', + name: { + 'zh-CN': '清除功能', + 'en-US': 'clear', + }, + desc: { + 'zh-CN': '

    通过属性clearable配置一键清除功能。

    ', + 'en-US': '

    clear

    ', + }, + apis: ['TiTextComponent.properties.clearable'], + }, + { + demoId: 'text-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'text disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': '

    disabled

    ', + }, + }, + { + demoId: 'text-focus', + name: { + 'zh-CN': '聚焦', + 'en-US': 'focus', + }, + desc: { + 'zh-CN': + '

    通过属性autofocus配置页面重新加载时是否自动聚焦。

    ', + 'en-US': '

    focus

    ', + }, + }, + { + demoId: 'text-password', + name: { + 'zh-CN': '密码框', + 'en-US': 'password', + }, + desc: { + 'zh-CN': + '

    通过type = "password"配置为密码框。通过noeye属性配置为密码不显示。

    ', + 'en-US': '

    password

    ', + }, + apis: [ + 'TiTextComponent.properties.type', + 'TiTextComponent.properties.noeye', + ], + }, + { + demoId: 'text-password-visible', + name: { + 'zh-CN': '密码是否可见', + 'en-US': '', + }, + desc: { + 'zh-CN': + '

    通过passwordVisible属性配置密码是否可见,默认不可见。当密码可见状态改变的时候触发passwordVisibleChange事件。

    ', + 'en-US': '

    reactive

    ', + }, + apis: [ + 'TiTextComponent.properties.passwordVisible', + 'TiTextComponent.events.passwordVisibleChange', + ], + }, + { + demoId: 'text-maskinput', + name: { + 'zh-CN': '格式化输入', + 'en-US': 'maskinput', + }, + desc: { + 'zh-CN': '

    通过属性tiMask配置其输入框数字的格式;输入框中呈现的是格式化后的数字,但是通过ngModel取得的值为纯数字。', + 'en-US': '', + }, + apis: ['TiMaskDirective.properties.tiMask'], + }, + { + demoId: 'text-readonly', + name: { + 'zh-CN': '只读状态', + 'en-US': 'readonly', + }, + desc: { + 'zh-CN': '

    通过readonly属性配置只读状态。

    ', + 'en-US': '

    readonly

    ', + }, + }, + { + demoId: 'text-events', + name: { + 'zh-CN': '事件', + 'en-US': 'events', + }, + desc: { + 'zh-CN': + '

    当元素聚焦的时候触发focus事件。当元素失焦的时候触发blur事件。当元素内容发生变化的时候的时候触发ngModelChange事件。当点击清除图标的时候触发clear事件。

    ', + 'en-US': '

    events', + }, + apis: ['TiTextComponent.events.clear'], + }, + { + demoId: 'text-reactive', + name: { + 'zh-CN': '响应式表单', + 'en-US': 'reactive-form', + }, + desc: { + 'zh-CN': '

    响应式表单的基本用法

    ', + 'en-US': '

    reactive

    ', + }, + }, + ], +}; diff --git a/src/text/demo/src/app/text/webdoc/text.cn.md b/src/text/demo/src/app/text/webdoc/text.cn.md new file mode 100644 index 0000000..9035273 --- /dev/null +++ b/src/text/demo/src/app/text/webdoc/text.cn.md @@ -0,0 +1,29 @@ +--- +title: Text 文本框 +--- +# Text 文本框 + +
    + +文本输入框。   + ++ 支持禁用、密码显示、完全清除等场景。 + ++ 支持通过`tiMask`属性限制用户只能输入数字及设置数字格式,常用于限定身份证、手机号码等格式的输入。 + +```typescript +import { TiTextModule } from '@opentiny/ng'; +``` + +
    + +
    + +文本输入框。   + ++ 支持禁用、密码显示、完全清除等场景。 + +```typescript +import { TiTextModule } from '@opentiny/ng'; +``` +
    diff --git a/src/text/demo/src/app/text/webdoc/text.en.md b/src/text/demo/src/app/text/webdoc/text.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/text/demo/src/app/text/webdoc/text.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/text/demo/src/favicon.ico b/src/text/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/text/demo/src/index.html b/src/text/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/text/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/text/demo/src/main.ts b/src/text/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/text/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/text/demo/test.ts b/src/text/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/text/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/text/demo/tsconfig.app.json b/src/text/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/text/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/text/demo/tsconfig.spec.json b/src/text/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/text/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/text/lib/index.ts b/src/text/lib/index.ts new file mode 100644 index 0000000..9c46f7b --- /dev/null +++ b/src/text/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTextModule'; diff --git a/src/text/lib/ng-package.json b/src/text/lib/ng-package.json new file mode 100644 index 0000000..3b60e57 --- /dev/null +++ b/src/text/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/text", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/text/lib/package.json b/src/text/lib/package.json new file mode 100644 index 0000000..0dc62ce --- /dev/null +++ b/src/text/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-text", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0" + } +} \ No newline at end of file diff --git a/src/text/lib/project.json b/src/text/lib/project.json new file mode 100644 index 0000000..cdf1fff --- /dev/null +++ b/src/text/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/text/lib", + "sourceRoot": "src/text/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/text"], + "options": { + "project": "src/text/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/text"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js text" + }, + { + "command": "ng default-build text" + }, + { + "command": "node build/clear-default-theme.js text" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/text && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build text && ng pack text && node build/publish.js text --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/text/lib/src/TiMaskDirective.ts b/src/text/lib/src/TiMaskDirective.ts new file mode 100644 index 0000000..083bac3 --- /dev/null +++ b/src/text/lib/src/TiMaskDirective.ts @@ -0,0 +1,283 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, forwardRef, Input, Renderer2 } from '@angular/core'; +import { DefaultValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { TiBrowser, Util } from '@opentiny/ng-utils'; +/** + * TiMask 格式化数字指令 + * + * 该指令主要用于输入框中,限制用户只能输入数字,可以通过设置tiMask属性接口设置其数字的格式:身份证,手机号码等形式输入。 + * + * 输入框中呈现的是格式化后的数字,但是通过ngModel取得的值为纯数字的值,如输入框中呈现的值为'123 456 789',通过ngModel取得的值为'123456789' + * + */ +@Directive({ + selector: '[tiMask]', + providers: [ + { + // MASK_INPUT_VALUE_ACCESSOR, 原本是外部变量或者static变量,lib编译不通过 + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TiMaskDirective), + multi: true + } + ], + host: { + '(input)': 'handleInput($event.target.value)', + '(compositionstart)': 'handleCompositionStart()', + '(compositionend)': 'handleCompositionEnd($event.target.value)', + '(blur)': 'blur()' + } +}) +export class TiMaskDirective extends DefaultValueAccessor { + private static readonly NUM_SIGN: string = '0'; + /** + * 设置数字格式 + */ + @Input('tiMask') format: string = '000 0000 0000'; + private element: HTMLInputElement; + private ctxPos: any; + private valueCharPosOffset: any; + private composing: boolean = false; // 是否正在拼写拼音 + private onChangeFn: Function; // 需要在registerOnChange中注册,用于改变模型值时调用 + private modelValue: any; + constructor(private renderer: Renderer2, private elementRef: ElementRef) { + // super中第三个参数是DefaultValueAccessor中对文本段落(例如中文)输入的处理的一个标志位, + // 表示是否为非安卓系统,在PC端此参数为true。后续如果要处理移动端,则可能需要动态根据此参数对 + // 文本段落(例如中文)输入做处理。 + super(renderer, elementRef, true); + this.element = this.elementRef.nativeElement; + } + + // 获取在format范围内的有效字符串 + private static getClearValue(value: any, format: string): void { + const formatNumLen: number = format.replace(/[^0]/g, '').length; + let clearValue: any = value.replace(/\D/g, ''); + clearValue = clearValue.slice(0, formatNumLen); + + return clearValue; + } + + private static formatValue(value: any, format: string): any { + if (!value) { + return ''; + } + const clearValue: any = TiMaskDirective.getClearValue(value, format); + const clearValueLen: number = clearValue.length; // value长度 + let valueCharPosOffset: number = 0; // value字符位置增量 + let valueCharPos: number; // 循环中的value字符位置 + let newValue: any = ''; + let formatChar: string; + for (let i: number = 0, len: number = format.length; i < len; i++) { + valueCharPos = i - valueCharPosOffset; + if (valueCharPos >= clearValueLen) { + // value内容循环完成情况下,不进行处理 + break; + } + formatChar = format[i]; + if (formatChar === TiMaskDirective.NUM_SIGN) { + // format字符为数字情况下的处理 + newValue += clearValue[valueCharPos]; + } else { + // format字符非数字情况下的处理,此时value字段中增加format字符 + valueCharPosOffset++; + newValue += formatChar; + } + } + + return newValue; + } + + // model => view + /** + * @ignore + */ + writeValue(value: any): void { + // 使用ngModel时,初始赋值第一次传入的value为null + if (value === null) { + return; + } + // format modelValue + const formatValue: any = TiMaskDirective.formatValue(value, this.format); + // Write formatted modelValue to view + super.writeValue(formatValue); + // 格式化后也需要通知修改模型值 + this.modelValue = this.getAntiFormatValue(formatValue); + if (this.modelValue !== value) { + if (Util.isUndefined(this.onChangeFn)) { + // 在reactive-form中使用,初始化赋值调用writeValue时, + // 此时registerOnChange还未被调用,onChangeFn还未被赋值, + // 所以要使用setTimeout等onChangeFn被赋值后再调用 + setTimeout(() => { + this.onChangeFn(this.modelValue); + }, 0); + } else { + this.onChangeFn(this.modelValue); + } + } + } + /** + * @ignore + */ + registerOnChange(fn: (value: any) => void): void { + this.onChangeFn = fn; + } + + /** + * @ignore + * view => model + */ + public handleInput(value: any): void { + if (!this.composing) { + // 在parser()中对view值进行格式化处理,再重写view的值; + this.parser(value); + } + } + + /** + * @ignore + * 中文输入之前(在输入拼音前) + */ + public handleCompositionStart(): void { + // 在IE和FF下中文输入法下,输入拼音时不会触发input事件, + // 但是在Chrome下,输入拼音时会触发input事件,所以针对Chrome要做特殊处理, + // 使其在输入拼音时不做格式化处理 + if (TiBrowser.isChrome()) { + this.composing = true; + } + } + + /** + * @ignore + * 文本段完成输入或取消输入 + */ + public handleCompositionEnd(value: any): void { + if (TiBrowser.isChrome()) { + this.composing = false; + // 在Chrome下compositionend比input执行滞后, + this.parser(value); + } + } + + /** + * @ignore + */ + public blur(): void { + // 此处的onTouched继承于DefaultValueAccessor + this.onTouched(); + } + + private parser(value: any): void { + const formattedValue: any = TiMaskDirective.formatValue(value, this.format); + this.setCtxPos(value, this.format); + const ctxPos: number = this.ctxPos; + + // 设置viewValue及光标位置 + if (value !== formattedValue) { + this.renderer.setProperty(this.element, 'value', formattedValue); + // 设置光标位置:value非法及输入数字后需要变换位置情况下,需要设置光标位置 + if (this.element === document.activeElement) { + this.element.setSelectionRange(ctxPos, ctxPos); + } + } + const modelValue: any = this.getAntiFormatValue(formattedValue); + if (modelValue !== this.modelValue) { + this.onChangeFn(modelValue); + this.modelValue = modelValue; + } + } + + // 获取反格式化后的value + private getAntiFormatValue(formattedValue: any): any { + return formattedValue.replace(/\D/g, ''); + } + + private setCtxPos(value: any, format: string): void { + // 元素有光标情况下,设置元素光标位置 + this.initCtxPos(); + + if (!value) { + return; + } + + // value非空情况下的处理 + this.valueCharPosOffset = 0; // value字符位置增量 + const ctxPos: number = this.ctxPos; // 初始化ctxPos,后续循环中会以此为光标位置作为对比值 + for (let i: number = 0, len: number = format.length; i < len; i++) { + if (i - this.valueCharPosOffset >= value.length) { + // value字符循环完成情况下,不进行后续处理 + break; + } + this.setCharPos(i, value, format, ctxPos); + } + } + + private setCharPos(pos: number, value: any, format: string, ctxPos: number): void { + const valueCharPos: number = pos - this.valueCharPosOffset; // 循环中的value字符位置 + const valueChar: any = value[valueCharPos]; // 当前value字符 + const formatChar: string = format[pos]; // 当前format字符 + if (formatChar === TiMaskDirective.NUM_SIGN) { + // format为数字情况下的处理 + this.setPosWithNum(value, valueChar, valueCharPos, ctxPos); + } else { + // format非数字情况处理 + this.setPosWithoutNum(valueChar, valueCharPos, formatChar, ctxPos); + } + } + + private setPosWithNum(value: any, valueChar: any, valueCharPos: number, ctxPos: number): void { + if (valueChar.match(/\d/)) { + // value字符相匹配情况下,不做处理 + return; + } + + // value字符不匹配情况下,光标前移,直到找到下一个数字为止 + let valueCharNew: any; + for (let j: number = valueCharPos, valueLen: number = value.length; j < valueLen; j++) { + valueCharNew = value[j]; + if (valueCharNew.match(/\d/)) { + // 找到value字符为数字的情况下,结束循环 + return; + } + + // 找到value字符非数字情况下,过滤掉该字符,光标前移,value偏移量后移,下次循环跳过该字符的检索 + if (ctxPos && ctxPos >= j + 1) { + this.ctxPos--; + } + this.valueCharPosOffset--; + } + } + + private setPosWithoutNum(valueChar: any, valueCharPos: number, formatChar: string, ctxPos: number): void { + // value为数字情况下,value偏移量前移,确保下次循环依然为该value字符 + if (valueChar.match(/\d/)) { + this.valueCharPosOffset++; + if (ctxPos && ctxPos >= valueCharPos + 1) { + this.ctxPos++; + } + + return; + } + + // value非数字且不匹配format字符情况下,过滤掉该字符,下次继续循环下个value字符 + if (valueChar !== formatChar && ctxPos && ctxPos >= valueCharPos + 1) { + this.ctxPos--; + } + } + + private initCtxPos(): void { + let ctxPositon: number; + if (this.element === document.activeElement) { + ctxPositon = this.element.selectionStart; + } + this.ctxPos = ctxPositon; + } +} diff --git a/src/text/lib/src/TiTextComponent.ts b/src/text/lib/src/TiTextComponent.ts new file mode 100644 index 0000000..e175358 --- /dev/null +++ b/src/text/lib/src/TiTextComponent.ts @@ -0,0 +1,389 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DoCheck, + ElementRef, + EventEmitter, + NgZone, + OnDestroy, + OnInit, + Output, + Optional, + Renderer2, + Input, + SimpleChanges +} from '@angular/core'; +import { NgControl } from '@angular/forms'; +import { TiBrowser, Util } from '@opentiny/ng-utils'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiAutofocusComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * 本组件基于原生input标签进行扩展,原生input加 tiText 属性即为text组件, + * 原生input的所有属性以及 Angular 的各种属性指令都可以使用。 + * + */ +@Component({ + selector: '[tiText]', + template: '', + styleUrls: ['text.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-text-input-show-icon]': 'isShowClear || showEye', + '[class.ti3-text-input-show-clear]': 'isShowClear', + '[class.ti3-text-input-show-password]': 'showEye', + '[class.ti3-text-input-noborder]': 'isNoBorder', + '(focus)': 'handleFocus()' + } +}) +export class TiTextComponent extends TiAutofocusComponent implements OnInit, DoCheck, OnDestroy { + private static readonly CLEAR_WIDTH: number = 26; // 常量,清除按钮区域宽度 + private static readonly PASSWORD_WIDTH: number = 36; // 常量,眼睛按钮区域宽度 + /** + * + * 密码是否可见 + * + */ + @Input() passwordVisible: boolean = false; + /** + * 密码可见/不可见状态改变时触发的回调 + * + */ + @Output() readonly passwordVisibleChange: EventEmitter = new EventEmitter(); + /** + * 点击清除按钮触发的回调,一般用于文本清除场景 + */ + @Output() readonly clear: EventEmitter = new EventEmitter(); + /** + * @ignore + * + * autoComplete 也使用了该变量 + */ + public isClearActive: boolean; + /** + * @ignore + * + * autoComplete 也使用了该变量 + */ + public isShowClear: boolean; + /** + * @ignore + */ + public isNoBorder: boolean; + /** + * @ignore + * + * 密码类型是否显示眼睛 + */ + public showEye: boolean = false; + protected versionInfo: string = super.getVersion(packageInfo); + private isFirstFocus: boolean = true; // 是否为第一次聚焦 + private hasClear: boolean = false; // 是否有清除功能 + private pwdHackEle: HTMLInputElement; // 当类型为password时,动态生成放在password文本框前的input元素 + private lastIsHover: boolean = undefined; // 最后一次绘制叉号的状态。 + private lastDisabled: boolean = undefined; // 最后一次叉号是否禁用。 + constructor( + elementRef: ElementRef, + renderer: Renderer2, + private tiRenderer: TiRenderer, + @Optional() private formControl: NgControl, + private zone: NgZone, + private changeDetectorRef: ChangeDetectorRef + ) { + super(elementRef, renderer); + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + if (changes['passwordVisible'] && !changes['passwordVisible'].firstChange) { + if (this.showEye && !this.isClearActive) { + const isHover: boolean = this.nativeElement.disabled ? null : false; + this.lastIsHover = undefined; + this.controlEyeShow(isHover); + } + } + } + + ngOnInit(): void { + super.ngOnInit(); + this.showEye = this.nativeElement.type === 'password' && !this.nativeElement.hasAttribute('noeye'); + this.hasClear = this.nativeElement.hasAttribute('clearable') && !this.showEye; + this.isNoBorder = this.nativeElement.hasAttribute('noborder'); + + // 初始化注册事件 + this.initFn(); + this.handlePassword(); + + // Chrome下输入框有时出现自动联想(该问题出现无规律可寻,比较奇怪), + // 将其autocomplete置为off可规避此问题 + // 之后firefox及IE11也出现类似问题,所以不区分浏览器差异,统一添加这层处理 + if (this.nativeElement.type === 'text' && this.nativeElement.autocomplete !== 'on') { + this.renderer.setAttribute(this.nativeElement, 'autocomplete', 'off'); + } + + // 由于在valueChanges中监听不到动态表单中文本框的初始值 + // 所以此处对初始值要做单独处理 + this.controlClearShow(this.nativeElement.value); + + // 文本框的值发生变化时触发 + if (this.formControl) { + this.formControl.valueChanges.subscribe((value: any): void => { + this.controlClearShow(value); + // onpush策略 需要更新视图 + this.changeDetectorRef.markForCheck(); + }); + } + + // 初始设置eye图标 + this.controlEyeShow(); + } + ngDoCheck(): void { + const disabled: boolean = this.nativeElement.disabled; + // 与上次disabled状态相同,则返回 + if (this.lastDisabled === disabled) { + return; + } + // 叉号特殊处理,2)禁用 + if (this.isShowClear || this.showEye) { + if (disabled) { + this.setClearBackgroud(null); + this.lastDisabled = true; + } else { + // 从禁用改为非禁用 + this.setClearBackgroud(false); + this.lastDisabled = false; + } + } + } + + ngOnDestroy(): void { + // 销毁组件时需要将内部生成dom也移除 + if (this.pwdHackEle) { + this.renderer.removeChild(this.renderer.parentNode(this.pwdHackEle), this.pwdHackEle); + } + } + + /** + * @ignore + */ + public handleFocus(): void { + // IE和火狐浏览器在首次autofoucus聚焦时会将光标移到文字起始位置,用户体验不好 + // 需要手动设置光标位置到行尾 + if (this.isFirstFocus && this.nativeElement.autofocus && (TiBrowser.isFirefox() || TiBrowser.isIE())) { + this.nativeElement.setSelectionRange(this.nativeElement.value.length, this.nativeElement.value.length); + } + this.isFirstFocus = false; + } + + private initFn(): void { + this.zone.runOutsideAngular(() => { + // 避免不停触发变化检测 + this.renderer.listen(this.nativeElement, 'mousemove', this.handleMousemove); + this.renderer.listen(this.nativeElement, 'mouseout', this.handleMouseout); + this.renderer.listen(this.nativeElement, 'mousedown', this.handleMousedown); + this.renderer.listen(this.nativeElement, 'click', this.handleClick); + }); + } + + /** + * @ignore + * 鼠标移入清除按钮区域时,给宿主元素添加ti-text-clear-active样式 + */ + public handleMousemove = (event: MouseEvent): void => { + if (this.nativeElement.disabled || (!this.isShowClear && !this.showEye)) { + return; + } + + // 鼠标移入清除按钮区域时,给宿主元素添加ti-text-clear-active样式; + // 否则去掉ti-text-clear-active样式 + this.isClearActive = this.isIconField(event); + + this.isClearActive + ? this.renderer.addClass(this.nativeElement, 'ti3-text-clear-active') + : this.renderer.removeClass(this.nativeElement, 'ti3-text-clear-active'); + + // 密码类型只需要设置悬浮时手型样式 + if (this.showEye) { + return; + } + + // 叉号特殊处理,3)hover + this.isClearActive ? this.setClearBackgroud(true) : this.setClearBackgroud(false); + }; + /** + * @ignore + */ + public handleMouseout = (event: MouseEvent) => { + if (this.nativeElement.disabled || !this.isShowClear) { + this.isClearActive = false; + + return; + } + // 叉号特殊处理,3)hover + this.setClearBackgroud(false); + }; + + /** + * @ignore + * 切换密码可见不可见时,输入框需处于失焦状态 + */ + public handleMousedown = (event: MouseEvent) => { + if (this.nativeElement.disabled || !this.showEye || !this.isIconField(event)) { + return; + } + + event.preventDefault(); + + // 聚焦到可输入区域,再点击眼睛图标时,输入框应失焦 + if (document.activeElement === this.nativeElement) { + this.nativeElement.blur(); + } + }; + /** + * @ignore + * @param isHover true标示悬浮,false标示非悬浮,null标示禁用,undefinded标示不画叉号。 + * @returns + */ + setClearBackgroud(isHover: boolean): void { + // 如果跟上次一样,则返回。 + if (this.lastIsHover === isHover) { + return; + } else { + this.lastIsHover = isHover; + } + + let colorStr: string = ''; + if (Util.supportsCssVars()) { + // Chrome等支持CSSVar的浏览器,正常操作, 从CSSVar变量中取出颜色。 + let varName: string = ''; + if (isHover === false) { + varName = '--ti-common-color-icon-normal'; + } else if (isHover === true) { + varName = '--ti-common-color-icon-hover'; + } else if (isHover === null) { + varName = '--ti-common-color-icon-disabled'; + } + // 修复SSR报错:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle !== 'undefined') { + colorStr = getComputedStyle(document.documentElement).getPropertyValue(varName); + } + } else { + // IE和低版Edge不支持CSSVar的浏览器,从一个CSS样式中取出颜色。 + let styleName: string = ''; + if (isHover === false) { + styleName = 'flood-color'; + } else if (isHover === true) { + styleName = 'lighting-color'; + } else if (isHover === null) { + styleName = 'stop-color'; + } + // 修复SSR报错:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle !== 'undefined') { + colorStr = getComputedStyle(this.nativeElement).getPropertyValue(styleName); + } + } + colorStr = colorStr.trim().replace('#', '%23'); // URL编码 + let urlStr: string; + if (this.showEye) { + urlStr = this.passwordVisible + ? `url(data:image/svg+xml,%3Csvg%20version=%221.1%22%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%2232%22%20height=%2232%22%20viewBox=%220%200%2032%2032%22%3E%3Cpath%20fill%3D%22${colorStr}%22%20d=%22M16%206.4c9.4%200%2016%206.4%2016%209.6%200%203-6.6%209.6-16%209.6-9.2%200-16-6.4-16-9.6s6.8-9.6%2016-9.6zM16%208.2c-4.2%200-7.8%203.6-7.8%207.8%200%204.4%203.4%207.8%207.8%207.8%204.2%200%207.8-3.6%207.8-7.8%200-4.4-3.6-7.8-7.8-7.8zM16%2011c2.8%200%205%202.2%205%205s-2.2%205-5%205-5-2.2-5-5c0-2.8%202.2-5%205-5z%22%3E%3C/path%3E%3C/svg%3E%0A)` + : `url(data:image/svg+xml,%3Csvg%20version=%221.1%22%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%2232%22%20height=%2232%22%3E%3Cpath%20fill%3D%22${colorStr}%22%20d=%22M1.6%202.925c0.6-0.6%201.4-0.6%201.8%200v0l25.6%2024.2c0.2%200.2%200.4%200.6%200.4%201s-0.2%200.6-0.4%201c-0.2%200.2-0.6%200.4-1%200.4s-0.8-0.2-1-0.4v0l-5-5.2c-2%200.6-4%201-6%201-9.2%200-16-6.6-16-9.8%200-1.8%202-4.6%205.4-6.6v0l-3.6-3.8c-0.6-0.4-0.6-1.2-0.2-1.8zM9%2011.725c-0.4%201-0.6%202.2-0.6%203.4%200%204.4%203.4%208%207.8%208%201.4%200%202.8-0.4%203.8-1v0l-2.4-2.4c-0.4%200.2-1%200.4-1.6%200.4-2.8%200-5-2.2-5-5%200-0.4%200-0.8%200.2-1.2v0l-2.2-2.2zM16%205.325c9.4%200%2016%206.6%2016%209.8%200%201.8-2%204.6-5.6%206.8v0l-3.4-3.2c0.6-1%200.8-2.2%200.8-3.4%200-4.4-3.4-8-7.8-8-1.4%200-2.8%200.4-4%201.2v0%200l-2-2c2-1%204-1.2%206-1.2zM16%209.925c2.8%200%205%202.2%205%205%200%200.4%200%201-0.2%201.4v0l-6.6-6c0.6-0.2%201.2-0.4%201.8-0.4z%22%3E%3C/path%3E%3C/svg%3E)`; + } else { + urlStr = `url(data:image/svg+xml,%3Csvg%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216px%22%20height%3D%2216px%22%3E%3Cpath%20fill%3D%22${colorStr}%22%20d%3D%22M12.529%2C11.469L9.061%2C8l3.469-3.469c0.294-0.294%2C0.295-0.768%2C0.001-1.062c-0.293-0.293-0.769-0.293-1.061%2C0L8%2C6.938L4.53%2C3.469c-0.292-0.293-0.767-0.293-1.061%2C0C3.177%2C3.762%2C3.177%2C4.238%2C3.471%2C4.532L6.939%2C8l-3.469%2C3.468c-0.294%2C0.293-0.294%2C0.768-0.001%2C1.062C3.616%2C12.678%2C3.81%2C12.75%2C4%2C12.75s0.384-0.072%2C0.53-0.219L8%2C9.062l3.47%2C3.469c0.147%2C0.146%2C0.339%2C0.219%2C0.53%2C0.219c0.192%2C0%2C0.384-0.072%2C0.53-0.219C12.823%2C12.236%2C12.824%2C11.762%2C12.529%2C11.469z%22%2F%3E%3C%2Fsvg%3E)`; + } + this.renderer.setStyle(this.nativeElement, 'backgroundImage', urlStr); + } + + /** + * @ignore + * 当在清除按钮区域点击时,清空输入框内容 + */ + public handleClick = (event: MouseEvent) => { + if (this.nativeElement.disabled || (!this.isShowClear && !this.showEye) || !this.isIconField(event)) { + return; + } + + if (this.isShowClear) { + this.renderer.setProperty(this.nativeElement, 'value', ''); + Util.trigger(this.nativeElement, 'input'); + this.zone.run(() => { + this.clear.emit(event); + }); + + this.nativeElement.focus(); + } + + if (this.showEye) { + this.passwordVisible = !this.passwordVisible; + this.lastIsHover = undefined; + this.controlEyeShow(); + this.zone.run(() => { + this.passwordVisibleChange.emit(this.passwordVisible); + }); + } + }; + + private handlePassword(): void { + // 密码框情况下,默认关闭autocomplete,避免浏览器弹出记住密码提示框引起的安全问题 + if (TiBrowser.isChrome() && this.nativeElement.type === 'password' && this.nativeElement.autocomplete !== 'on') { + this.pwdHackEle = this.renderer.createElement('input'); + this.tiRenderer.setAttributes(this.pwdHackEle, { + type: 'text', + tabindex: -1, + autocomplete: 'off' + }); + this.renderer.addClass(this.pwdHackEle, 'ti3-password-hack-input'); + // 插入一个input元素,阻止密码框前面的input联想用户名 + this.renderer.insertBefore(this.nativeElement.parentElement, this.pwdHackEle, this.nativeElement); + } + } + + // 控制clear是否显示,文本框为空时不显示,有内容时才显示 + private controlClearShow(value: any): void { + if (!this.hasClear) { + return; + } + const lastIsShowClear: boolean = this.isShowClear; + this.isShowClear = Util.isString(value) && value !== ''; + // 与上次状态相同,则返回 + if (this.isShowClear === lastIsShowClear) { + return; + } + + // 叉号特殊处理,1)初始化和监听是否显示 + if (this.isShowClear) { + this.setClearBackgroud(false); + } else { + this.renderer.removeStyle(this.nativeElement, 'background'); + this.lastIsHover = undefined; + } + } + + // 控制eye是否显示, + private controlEyeShow(isHover: boolean = false): void { + if (!this.showEye) { + return; + } + this.setClearBackgroud(isHover); + this.nativeElement.type = this.passwordVisible ? 'text' : 'password'; + } + + // 判断事件是否在清除按钮区域 + private isIconField(event: MouseEvent): boolean { + const iconWidth: number = this.showEye ? TiTextComponent.PASSWORD_WIDTH : TiTextComponent.CLEAR_WIDTH; + + return event.clientX - this.nativeElement.getBoundingClientRect().left > this.nativeElement.getBoundingClientRect().width - iconWidth; + } +} diff --git a/src/text/lib/src/TiTextModule.ts b/src/text/lib/src/TiTextModule.ts new file mode 100644 index 0000000..cf11ca2 --- /dev/null +++ b/src/text/lib/src/TiTextModule.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiTextComponent } from './TiTextComponent'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiMaskDirective } from './TiMaskDirective'; + +@NgModule({ + imports: [CommonModule, TiRendererModule], + exports: [TiTextComponent, TiMaskDirective], + declarations: [TiTextComponent, TiMaskDirective] +}) +export class TiTextModule {} +export { TiTextComponent } from './TiTextComponent'; +export { TiMaskDirective } from './TiMaskDirective'; diff --git a/src/text/lib/src/clear.svg b/src/text/lib/src/clear.svg new file mode 100644 index 0000000..d74f9da --- /dev/null +++ b/src/text/lib/src/clear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/text/lib/src/invisible.svg b/src/text/lib/src/invisible.svg new file mode 100644 index 0000000..5fe8764 --- /dev/null +++ b/src/text/lib/src/invisible.svg @@ -0,0 +1 @@ + diff --git a/src/text/lib/src/text.less b/src/text/lib/src/text.less new file mode 100644 index 0000000..fe78873 --- /dev/null +++ b/src/text/lib/src/text.less @@ -0,0 +1,101 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-text-clear-width: 26px; // 在ts中判断是否在清除按钮区域时需要此参数 + --ti-text-password-width: 36px; // 在ts中判断是否在眼睛按钮区域时需要此参数 +} + +// 注意:backgroud url里的svg fill填充颜色使用CSS var不生效。所以改用js监听hover和禁用,设置backgroud样式 +:host[tiText] { + border-radius: var(--ti-common-border-radius-normal); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + height: var(--ti-common-size-7x); + line-height: normal; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + color: var(--ti-common-color-text-primary); + background-color: var(--ti-common-color-bg-white-normal); + padding: 0 var(--ti-common-space-10); + .box-sizing(border-box); + outline: none; + vertical-align: middle; + /*IE下js暂时无法读取css var变量的值。所以,把这三个var存放在不用的css style里。需要这3个变量ie、edge下可读*/ + flood-color: var(--ti-common-color-icon-normal); + lighting-color: var(--ti-common-color-icon-hover); + stop-color: var(--ti-common-color-icon-disabled); + + &.ti3-text-input-show-icon { + background-repeat: no-repeat; + background-size: var(--ti-common-size-4x); + } + + &.ti3-text-input-show-clear { + padding-right: var(--ti-text-clear-width) !important; + background-position: center right 5px; + } + + &.ti3-text-input-show-password { + padding-right: var(--ti-text-password-width) !important; + background-position: center right 10px; + } + + &.ti3-text-input-noborder { + outline: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} + +:host[tiText]:not([disabled]):not([noborder]) { + &.ti3-text-input-show-icon { + &.ti3-text-clear-active:hover { + cursor: pointer; + } + } + + &:hover { + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-hover); + } + + &:focus { + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-active); + } +} + +:host[tiText][disabled] { + &:disabled { + background-color: var(--ti-common-color-bg-disabled); + color: var(--ti-common-color-text-disabled); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-disabled); //FireFox浏览器,初始设置focused为true时,disable状态有蓝框 + cursor: not-allowed !important; + &.ti3-text-input-noborder { + border: 0; + } + } +} + +.ti3-password-hack-input { + width: 0; + height: 0; + position: absolute; + top: -9999px; + left: -9999px; +} + +/***********************************动效************************************/ +:host[tiText] { + .form-border-animat-init(); + &.ti3-text-input-show-icon { + &:focus { + // 添加focus状态下的动画 + transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1), font-size 0.15s cubic-bezier(0.4, 0, 0.2, 1); + } + } + + //添加hover状态下的动画效果 + &:hover:not(:focus) { + //默认状态下的hover动画 + .form-border-animat-enter(); + } +} diff --git a/src/text/lib/src/visible.svg b/src/text/lib/src/visible.svg new file mode 100644 index 0000000..a7af23c --- /dev/null +++ b/src/text/lib/src/visible.svg @@ -0,0 +1 @@ + diff --git a/src/textarea/demo/karma.conf.js b/src/textarea/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/textarea/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/textarea/demo/project.json b/src/textarea/demo/project.json new file mode 100644 index 0000000..001c1ce --- /dev/null +++ b/src/textarea/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/textarea/demo", + "sourceRoot": "src/textarea/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/textarea", + "index": "src/textarea/demo/src/index.html", + "main": "src/textarea/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/textarea/demo/tsconfig.app.json", + "assets": ["src/textarea/demo/src/favicon.ico", "src/textarea/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "textarea-demo:build:production" + }, + "development": { + "browserTarget": "textarea-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js textarea" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/textarea/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/textarea/demo/tsconfig.spec.json", + "karmaConfig": "src/textarea/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/textarea/demo/src/app/AppComponent.ts b/src/textarea/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/textarea/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/textarea/demo/src/app/AppModule.ts b/src/textarea/demo/src/app/AppModule.ts new file mode 100644 index 0000000..50549f7 --- /dev/null +++ b/src/textarea/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TextareaTestModule } from './textarea/TextareaTestModule'; + +@NgModule({ + imports: [ + TextareaTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/textarea/demo/src/app/IndexComponent.ts b/src/textarea/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..738bd16 --- /dev/null +++ b/src/textarea/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TextareaTestModule } from './textarea/TextareaTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TextareaTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/textarea/demo/src/app/app.html b/src/textarea/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/textarea/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/textarea/demo/src/app/textarea/TextareaAutofocusComponent.ts b/src/textarea/demo/src/app/textarea/TextareaAutofocusComponent.ts new file mode 100644 index 0000000..c2964c9 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaAutofocusComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './textarea-autofocus.html' +}) +export class TextareaAutofocusComponent { + value: string = '解落三秋叶,能开二月花。'; + placeholder: string = '欢迎使用Tiny UI'; +} diff --git a/src/textarea/demo/src/app/textarea/TextareaDisabledComponent.ts b/src/textarea/demo/src/app/textarea/TextareaDisabledComponent.ts new file mode 100644 index 0000000..389dd8d --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaDisabledComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './textarea-disabled.html' +}) +export class TextareaDisabledComponent { + value: string = '解落三秋叶,能开二月花。'; + disabled: boolean = true; +} diff --git a/src/textarea/demo/src/app/textarea/TextareaMaxlengthComponent.ts b/src/textarea/demo/src/app/textarea/TextareaMaxlengthComponent.ts new file mode 100644 index 0000000..4637788 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaMaxlengthComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; +@Component({ + templateUrl: './textarea-maxlength.html' +}) +export class TextareaMaxlengthComponent { + value: string = '解落三秋叶'; + placeholder: string = '欢迎使用Tiny UI'; + aaControl: any = new FormControl(''); +} diff --git a/src/textarea/demo/src/app/textarea/TextareaNoneComponent.ts b/src/textarea/demo/src/app/textarea/TextareaNoneComponent.ts new file mode 100644 index 0000000..550f08e --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaNoneComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './textarea-none.html' +}) +export class TextareaNoneComponent { + value: string = '123'; + show: boolean = false; + changeShow = (): void => { + this.show = !this.show; + }; +} diff --git a/src/textarea/demo/src/app/textarea/TextareaResizeComponent.ts b/src/textarea/demo/src/app/textarea/TextareaResizeComponent.ts new file mode 100644 index 0000000..3b55491 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaResizeComponent.ts @@ -0,0 +1,11 @@ +import { Component, ViewEncapsulation } from '@angular/core'; + +@Component({ + templateUrl: './textarea-resize.html', // 指定组件模板 + encapsulation: ViewEncapsulation.None +}) +export class TextareaResizeComponent { + value: string = ''; + value1: string = ''; + value2: string = ''; +} diff --git a/src/textarea/demo/src/app/textarea/TextareaScrollComponent.ts b/src/textarea/demo/src/app/textarea/TextareaScrollComponent.ts new file mode 100644 index 0000000..f127cfa --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaScrollComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './textarea-scroll.html' +}) +export class TextareaScrollComponent { + items: Array = new Array(20); + enableWhiteList: boolean = false; + item1: any = { + show: true, + label: '姓名:', + required: true, + value: '' + }; + item2: any = { + label: '输入框:', + required: true, + value: '' + }; +} diff --git a/src/textarea/demo/src/app/textarea/TextareaTestModule.ts b/src/textarea/demo/src/app/textarea/TextareaTestModule.ts new file mode 100644 index 0000000..d451011 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaTestModule.ts @@ -0,0 +1,63 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiFormfieldModule, TiSwitchModule, TiTextareaModule, TiValidationModule } from '@opentiny/ng'; + +import { TextareaResizeComponent } from './TextareaResizeComponent'; +import { TextareaMaxlengthComponent } from './TextareaMaxlengthComponent'; +import { TextareaAutofocusComponent } from './TextareaAutofocusComponent'; +import { TextareaDisabledComponent } from './TextareaDisabledComponent'; +import { TextareaValidComponent } from './TextareaValidComponent'; +import { TextareaNoneComponent } from './TextareaNoneComponent'; +import { TextareaWidthComponent } from './TextareaWidthComponent'; +import { TextareaScrollComponent } from './TextareaScrollComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiTextareaModule, + TiValidationModule, + TiFormfieldModule, + TiSwitchModule, + RouterModule.forChild(TextareaTestModule.ROUTES) + ], + declarations: [ + TextareaResizeComponent, + TextareaMaxlengthComponent, + TextareaAutofocusComponent, + TextareaDisabledComponent, + TextareaValidComponent, + TextareaWidthComponent, + TextareaNoneComponent, + TextareaScrollComponent + ] +}) +export class TextareaTestModule { + static readonly LINKS: Array = [{ href: 'components/TiTextareaComponent.html', label: 'Textarea' }]; + static readonly ROUTES: Routes = [ + { + path: 'textarea/textarea-resize', + component: TextareaResizeComponent + }, + { + path: 'textarea/textarea-maxlength', + component: TextareaMaxlengthComponent + }, + { + path: 'textarea/textarea-autofocus', + component: TextareaAutofocusComponent + }, + { + path: 'textarea/textarea-disabled', + component: TextareaDisabledComponent + }, + { path: 'textarea/textarea-width', component: TextareaWidthComponent }, + { path: 'textarea/textarea-valid', component: TextareaValidComponent }, + { path: 'textarea/textarea-none', component: TextareaNoneComponent }, + { path: 'textarea/textarea-scroll', component: TextareaScrollComponent } + ]; +} diff --git a/src/textarea/demo/src/app/textarea/TextareaValidComponent.ts b/src/textarea/demo/src/app/textarea/TextareaValidComponent.ts new file mode 100644 index 0000000..ae67eaa --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaValidComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './textarea-valid.html' +}) +export class TextareaValidComponent { + value: string = ''; + value1: string = '123'; + placeholder: string = '欢迎使用Tiny UI'; + // 使用TiValidation定义接口类型 + validation: TiValidationConfig = { + type: 'blur' + }; +} diff --git a/src/textarea/demo/src/app/textarea/TextareaWidthComponent.ts b/src/textarea/demo/src/app/textarea/TextareaWidthComponent.ts new file mode 100644 index 0000000..1f1306f --- /dev/null +++ b/src/textarea/demo/src/app/textarea/TextareaWidthComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './textarea-width.html' +}) +export class TextareaWidthComponent { + value: string = ''; + placeholder: string = '欢迎使用Tiny UI'; +} diff --git a/src/textarea/demo/src/app/textarea/textarea-autofocus.html b/src/textarea/demo/src/app/textarea/textarea-autofocus.html new file mode 100644 index 0000000..daea912 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-autofocus.html @@ -0,0 +1,11 @@ + +
    +
    Current value: {{ value }}
    +
    diff --git a/src/textarea/demo/src/app/textarea/textarea-disabled.html b/src/textarea/demo/src/app/textarea/textarea-disabled.html new file mode 100644 index 0000000..847b301 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-disabled.html @@ -0,0 +1,7 @@ + diff --git a/src/textarea/demo/src/app/textarea/textarea-maxlength.html b/src/textarea/demo/src/app/textarea/textarea-maxlength.html new file mode 100644 index 0000000..fa91014 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-maxlength.html @@ -0,0 +1,8 @@ + diff --git a/src/textarea/demo/src/app/textarea/textarea-none.html b/src/textarea/demo/src/app/textarea/textarea-none.html new file mode 100644 index 0000000..4d4198e --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-none.html @@ -0,0 +1,18 @@ +

    1.描述

    +

    场景测试:祖先元素初始display为none时,组件设置class样式类不生效问题

    +

    2.示例

    +

    1.通过class设置样式

    +
    + +
    +

    2.通过style设置样式

    +
    + +
    + + diff --git a/src/textarea/demo/src/app/textarea/textarea-resize.html b/src/textarea/demo/src/app/textarea/textarea-resize.html new file mode 100644 index 0000000..ed7cc57 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-resize.html @@ -0,0 +1,5 @@ + +
    + +
    + diff --git a/src/textarea/demo/src/app/textarea/textarea-scroll.html b/src/textarea/demo/src/app/textarea/textarea-scroll.html new file mode 100644 index 0000000..f438f3e --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-scroll.html @@ -0,0 +1,19 @@ +

    1.描述

    +

    + 场景测试:在有textarea的页面,且textarea的宿主元素初始display:none, + 页面有滚动条时,将页面滚动条拖至formfiled的头部不在视野范围内时,textarea动态显示时页面滚动条会异常跳动到顶部。 +

    +

    2.示例

    + + + item1 + + + ewdfwew + + + + wqwqw + + + diff --git a/src/textarea/demo/src/app/textarea/textarea-valid.html b/src/textarea/demo/src/app/textarea/textarea-valid.html new file mode 100644 index 0000000..ba2ee64 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-valid.html @@ -0,0 +1,8 @@ +

    1.描述

    +

    校验

    +

    2.示例

    +

    (2.1)blur校验

    + + +

    (2.2)及时校验

    + diff --git a/src/textarea/demo/src/app/textarea/textarea-width.html b/src/textarea/demo/src/app/textarea/textarea-width.html new file mode 100644 index 0000000..255e13e --- /dev/null +++ b/src/textarea/demo/src/app/textarea/textarea-width.html @@ -0,0 +1,7 @@ +

    设置宽度自适应

    + +

    + + +

    + diff --git a/src/textarea/demo/src/app/textarea/webdoc/textarea-demos.js b/src/textarea/demo/src/app/textarea/webdoc/textarea-demos.js new file mode 100644 index 0000000..e333a11 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/webdoc/textarea-demos.js @@ -0,0 +1,54 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'textarea-autofocus', + name: { + 'zh-CN': '自动聚焦', + 'en-US': 'autofocus', + }, + desc: { + 'zh-CN': + '

    通过属性autofocus配置页面重新加载时是否自动聚焦。

    ', + 'en-US': '

    autofocus

    ', + }, + }, + { + demoId: 'textarea-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'textarea disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': '

    disabled

    ', + }, + }, + { + demoId: 'textarea-maxlength', + name: { + 'zh-CN': '限制可输入字数', + 'en-US': 'maxlength', + }, + desc: { + 'zh-CN': + '

    通过属性maxlength配置文本框可输入字符的最大长度。

    ', + 'en-US': '

    maxlength

    ', + }, + }, + { + demoId: 'textarea-resize', + name: { + 'zh-CN': '调整文本框大小', + 'en-US': 'resize', + }, + desc: { + 'zh-CN': + '

    通过属性resize配置文本框调整的方向,包含none、vertical、horizontal三种类型。

    ', + 'en-US': '

    resize

    ', + }, + apis: ['TiTextareaComponent.properties.resize'], + }, + ], +}; diff --git a/src/textarea/demo/src/app/textarea/webdoc/textarea.cn.md b/src/textarea/demo/src/app/textarea/webdoc/textarea.cn.md new file mode 100644 index 0000000..83db92c --- /dev/null +++ b/src/textarea/demo/src/app/textarea/webdoc/textarea.cn.md @@ -0,0 +1,27 @@ +--- +title: Textarea 多行文本框 +--- +# Textarea 多行文本框 + +
    + +文本框组件基于原生 textarea 标签进行扩展,原生 textarea 加 tiTextarea 属性指令即为 textarea 组件。   + ++ 支持调整大小、限制输入字数、自动聚焦等场景。 + +```typescript +import { TiTextareaModule } from '@opentiny/ng'; +``` + +
    + +
    + +文本框组件基于原生 textarea 标签进行扩展,原生 textarea 加 tiTextarea 属性指令即为 textarea 组件。   + ++ 支持调整大小、限制输入字数、自动聚焦等场景。 + +```typescript +import { TiTextareaModule } from '@opentiny/ng'; +``` +
    diff --git a/src/textarea/demo/src/app/textarea/webdoc/textarea.en.md b/src/textarea/demo/src/app/textarea/webdoc/textarea.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/textarea/demo/src/app/textarea/webdoc/textarea.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/textarea/demo/src/favicon.ico b/src/textarea/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/textarea/demo/src/index.html b/src/textarea/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/textarea/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/textarea/demo/src/main.ts b/src/textarea/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/textarea/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/textarea/demo/test.ts b/src/textarea/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/textarea/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/textarea/demo/tsconfig.app.json b/src/textarea/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/textarea/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/textarea/demo/tsconfig.spec.json b/src/textarea/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/textarea/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/textarea/lib/index.ts b/src/textarea/lib/index.ts new file mode 100644 index 0000000..1b3814a --- /dev/null +++ b/src/textarea/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTextareaModule'; diff --git a/src/textarea/lib/ng-package.json b/src/textarea/lib/ng-package.json new file mode 100644 index 0000000..4aad4e8 --- /dev/null +++ b/src/textarea/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/textarea", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/textarea/lib/package.json b/src/textarea/lib/package.json new file mode 100644 index 0000000..3e89886 --- /dev/null +++ b/src/textarea/lib/package.json @@ -0,0 +1,14 @@ +{ + "name": "@opentiny/ng-textarea", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/textarea/lib/project.json b/src/textarea/lib/project.json new file mode 100644 index 0000000..78e16a6 --- /dev/null +++ b/src/textarea/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/textarea/lib", + "sourceRoot": "src/textarea/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/textarea"], + "options": { + "project": "src/textarea/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/textarea"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js textarea" + }, + { + "command": "ng default-build textarea" + }, + { + "command": "node build/clear-default-theme.js textarea" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/textarea && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build textarea && ng pack textarea && node build/publish.js textarea --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/textarea/lib/src/TiFormatNumPipe.ts b/src/textarea/lib/src/TiFormatNumPipe.ts new file mode 100644 index 0000000..78fa2ed --- /dev/null +++ b/src/textarea/lib/src/TiFormatNumPipe.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Pipe, PipeTransform } from '@angular/core'; +import { TiLocaleFormat } from '@opentiny/ng-locale'; + +/** + * @ignore + * TiFormatNumPipe 对textarea字数统计的数字进行国际化处理 + * + */ +@Pipe({ name: 'tiFormatNum' }) +export class TiFormatNumPipe implements PipeTransform { + private numberFormat: string = '1.0-0'; // 整数位保留最小位数.小数位保留最小位数-小数位最大保留位置 + transform(value: number): string { + return TiLocaleFormat.formatNumber(value, this.numberFormat); + } +} diff --git a/src/textarea/lib/src/TiTextareaComponent.ts b/src/textarea/lib/src/TiTextareaComponent.ts new file mode 100644 index 0000000..9393fc9 --- /dev/null +++ b/src/textarea/lib/src/TiTextareaComponent.ts @@ -0,0 +1,456 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + AfterViewChecked, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DoCheck, + ElementRef, + Inject, + Input, + NgZone, + OnDestroy, + OnInit, + Optional, + Renderer2, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { TiBrowser, Util } from '@opentiny/ng-utils'; +import { TiAutofocusComponent } from '@opentiny/ng-base'; +import { NgControl } from '@angular/forms'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import packageInfo from '../package.json'; +/** + * Textarea多行文本框组件 + * + * 文本框组件基于原生textarea标签进行扩展,原生textarea加tiTextarea属性指令即为textarea组件。 + * + */ +@Component({ + selector: '[tiTextarea]', // 指定组件名称 + templateUrl: './textarea.html', // 指定组件模板 + styleUrls: ['./textarea.less'], // 样式路径 + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '(focus)': 'onFocus()' + }, + encapsulation: ViewEncapsulation.None +}) +export class TiTextareaComponent extends TiAutofocusComponent implements OnInit, AfterViewChecked, AfterViewInit, DoCheck, OnDestroy { + /** + * 文本框大小,该属性提供了四个属性值: vertical(仅可调整垂直方向的大小,即调整组件的高度)、 + * horizontal(仅可调节水平方向的大小,即调整组件的宽度)、 + * both(水平和垂直方向均可调节,宽高都可调节)、 + * none(不可调整组件大小) + */ + @Input() resize: 'none' | 'vertical' | 'horizontal' | 'both' = 'both'; + /** + * @ignore + */ + @ViewChild('resize') private resizeIconRef: ElementRef; + /** + * @ignore + */ + @ViewChild('maxlength') private eleCharacterRef: ElementRef; + /** + * @ignore + */ + @ViewChild('charactersCount') private charactersCountRef: ElementRef; + /** + * @ignore + * 是否设置maxlength属性 + */ + public hasMaxlength: boolean; + /** + * @ignore + * 用户设置的最大值 + */ + public maxLength: number; + /** + * @ignore + * 当前输入内容的长度 + */ + public countLength: number; + protected versionInfo: string = super.getVersion(packageInfo); + // 用作拖动文本框大小 + private options: any = { + $target: undefined, + storeWidth: 0, + storeHeight: 0, + mouseXPosition: 0, + mouseYPosition: 0 + }; + // 默认最大最小宽高 + private defaultStyle: any = { + minWidth: 60, + minHeight: 64, + maxWidth: 1280, + maxHeight: 9999 + }; + private element: HTMLTextAreaElement; // 宿主元素 + private attrs: NamedNodeMap; // 宿主元素的属性 + private container: HTMLDivElement; // 创建div标签 + private textAreaMinWidth: number; // 最小宽度 + private textAreaMinHeight: number; // 最小高度 + private textAreaMaxWidth: number; // 最大宽度 + private textAreaMaxHeight: number; // 最大高度 + private isVisibleInit: boolean; // 标志初始时是否可见 + private isFirstFocus: boolean = true; // 是否为第一次聚焦 + private documentMouseMoveListener: () => void; + private documentMouseUpListener: () => void; + private textareaResizeObserver: any; + + constructor( + elementRef: ElementRef, + renderer: Renderer2, + private tiRenderer: TiRenderer, + private zone: NgZone, + @Optional() private formControl: NgControl, + @Inject(DOCUMENT) private document, + private changeDetectorRef: ChangeDetectorRef + ) { + super(elementRef, renderer); + this.element = elementRef.nativeElement; // 获取宿主元素 + this.attrs = this.element.attributes; // 可以根据属性名获取各个属性 + this.container = this.renderer.createElement('div'); // 创建div标签 + this.renderer.addClass(this.container, 'ti3-textarea-container'); + } + + ngOnInit(): void { + super.ngOnInit(); + // 初始化maxlength属性 + this.hasMaxlength = Util.isUndefined(this.attrs['maxlength']) ? false : true; + // 原生不设置maxlength属性时该值为-1 + this.maxLength = this.hasMaxlength ? this.element.maxLength : -1; + // 文本框的值发生变化时触发 + // 使用ngModel初始赋值时,触发valueChanges。 + // 使用formControl初始赋值时,不会触发valueChanges。 + if (this.formControl) { + this.formControl.valueChanges.subscribe((value: any) => { + this.setCountLength(); + // onpush策略,需要手动触发变更检测 + this.changeDetectorRef.markForCheck(); + }); + } + + // 使用formControl初始赋值时,初始化不会触发valueChanges,所以此处额外执行一次。 + // 使用ngModel初始赋值时,此处通过this.element.value拿到的值为空字符串,无法正确显示当前输入内容的长度 + this.setCountLength(); + } + + ngAfterViewInit(): void { + // 如果祖先元素中display为none,获取到的组件的宽度为0,元素不可见 + this.isVisibleInit = this.element.offsetWidth !== 0; + if (this.isVisibleInit) { + this.setContainerstyle(); + } + + // maxLength接口添加样式 + this.setMaxLengthFn(); + + // 处理resize功能 + this.resizeFn(); + // 上面执行完初始化dom后,调用父类的ngAfterViewInit去设置autofocus + super.ngAfterViewInit(); + if (!this.isVisibleInit && (window as any).ResizeObserver) { + // 利用 ResizeObserver 来监听宿主元素的尺寸发生改变的时机。IE不支持 ResizeObserver。 + this.textareaResizeObserver = new (window as any).ResizeObserver((entries: any): void => { + // 初始祖先元素中display为none,之后第一次由none变成可见时 + if (entries[0] && entries[0].contentRect.width !== 0) { + this.setContainerstyle(); + this.textareaResizeObserver.disconnect(); + } + }); + this.textareaResizeObserver.observe(this.element); + } + } + + ngAfterViewChecked(): void { + // 修复SSR报错:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle === 'undefined') { + return; + } + // 处理通过设置display的方式隐藏组件:内部生成的dom也需要隐藏 + if (getComputedStyle(this.element).display === 'none') { + this.renderer.setStyle(this.container, 'display', 'none'); + } else { + this.renderer.setStyle(this.container, 'display', 'block'); + } + } + // maxlength通过接口传值需触发重新判断,因为init函数中执行较早且只执行一次。 + ngDoCheck(): void { + this.hasMaxlength = Util.isUndefined(this.attrs['maxlength']) ? false : true; + this.maxLength = this.hasMaxlength ? this.element.maxLength : -1; + } + + // 销毁组件时需要将内部生成dom也移除 + ngOnDestroy(): void { + this.renderer.removeChild(this.renderer.parentNode(this.container), this.container); + if (this.documentMouseMoveListener) { + this.documentMouseMoveListener(); + } + if (this.documentMouseUpListener) { + this.documentMouseUpListener(); + } + + if (this.textareaResizeObserver) { + this.textareaResizeObserver.disconnect(); + } + } + + /** + * @ignore + */ + public onFocus(): void { + // IE和火狐浏览器在首次autofoucus聚焦时会将光标移到文字起始位置,用户体验不好 + // 需要手动设置光标位置到行尾 + if (this.isFirstFocus && (this.element as any).autofocus && (TiBrowser.isFirefox() || TiBrowser.isIE())) { + this.element.setSelectionRange(this.element.value.length, this.element.value.length); + } + this.isFirstFocus = false; + } + + private setContainerstyle(): void { + // 设置container的样式 + this.setContainerSize(); + // 将宿主元素的样式类移到container上 + this.setContainerClass(); + // 初始化创建dom + this.initFn(); + } + + private setContainerSize(): void { + // 修正SSR错误:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle === 'undefined') { + return; + } + + // 判断用户设置的width是否为百分比 + const PERCENT_REGEXP: RegExp = /^(100|[1-9]?\d(.\d\d?\d?)?)%$|0$/; + const width: string = this.element.style.width; + const isPercent: boolean = PERCENT_REGEXP.test(width); + + this.tiRenderer.setStyles(this.container, { + height: getComputedStyle(this.element).height, + width: isPercent ? width : getComputedStyle(this.element).width, + // 若服务在textarea上面设置margin外边距,会导致textarea所占的页面空间大于外框的宽高,所以要将textarea上设置的margin转移到外边框 + margin: this.element.style.margin + }); + // 取掉宿主元素的margin属性: + this.element.style.margin = ''; + } + + private setContainerClass(): void { + // 获取未经过angular解析后的属性需要在onint中处理:若服务在textarea添加class,则把其加到父容器上 + const cNames: string = this.element.className; + if (cNames) { + const cNameArr: Array = this.element.className.split(' '); // 存在多个class + for (const cName of cNameArr) { + // 跟校验有关的样式是以"ng-"开头,如果转移到外部容器上会影响校验样式 + if (!/^ng-/.test(cName)) { + this.renderer.removeClass(this.element, cName); + this.renderer.addClass(this.container, cName); + } + } + } + } + + // 初始化创建dom + private initFn(): void { + const pNode: any = this.renderer.parentNode(this.element); // 找到宿主元素父元素 + const nextNode: any = this.renderer.nextSibling(this.element); // 找到宿主元素的兄弟元素 + + // 将宿主元素包裹在this.container里边: + this.renderer.removeChild(pNode, this.element); // 从父元素中移除宿主元素 + this.renderer.appendChild(this.container, this.element); // 将宿主元素放入创建的div标签中 + this.renderer.insertBefore(pNode, this.container, nextNode); + + if (this.resize !== 'none') { + this.moveNode(this.resizeIconRef.nativeElement); + } + + if (this.hasMaxlength) { + this.moveNode(this.eleCharacterRef.nativeElement); + } + } + + private moveNode(node: Element): void { + this.renderer.removeChild(this.element, node); + this.renderer.appendChild(this.container, node); + } + + // maxLength接口 + private setMaxLengthFn(): void { + // 添加字数限制与当前输入字数显示功能 + if (this.attrs['maxlength'] && !isNaN(this.maxLength)) { + // 有maxlength限制时给外框div加上标志类 ti3-textarea-container-counter + this.renderer.addClass(this.container, 'ti3-textarea-container-counter'); + } else { + this.renderer.removeClass(this.container, 'ti3-textarea-container-counter'); + } + } + + // resize接口 + private resizeFn(): void { + if (this.resize === 'none') { + return; + } + + this.textAreaMinWidth = this.getSizeNumber('minWidth'); + this.textAreaMinHeight = this.getSizeNumber('minHeight'); + this.textAreaMaxWidth = this.getSizeNumber('maxWidth'); + this.textAreaMaxHeight = this.getSizeNumber('maxHeight'); + + // 清除textarea上的样式 + this.tiRenderer.setStyles(this.element, { + minWidth: '', + minHeight: '', + maxWidth: '', + maxHeight: '' + }); + + this.zone.runOutsideAngular(() => { + // 避免不停触发变化检测 + // resizeIconRef模板的mousedown事件 + this.renderer.listen(this.resizeIconRef.nativeElement, 'mousedown', ($event: MouseEvent) => { + this.renderer.addClass(this.element, 'ti3-textarea-resize-border'); // 拖拽时边框颜色为聚焦时 + this.options.$target = $event.target; + this.options.mouseXPosition = $event.pageX; + this.options.mouseYPosition = $event.pageY; + // 在拖动前获取当前文本框最新的高度、宽度 + this.updateTextAreaSize(); + // 给页面设置不可选样式,避免拖动时页面出现被选中的蓝色区域 + this.toggleTextSelection(true); + }); + + // resizeIconRef模板的mouseup事件 + this.renderer.listen(this.resizeIconRef.nativeElement, 'mouseup', ($event: MouseEvent) => { + this.renderer.removeClass(this.element, 'ti3-textarea-resize-border'); + }); + + this.documentMouseMoveListener = this.renderer.listen(this.document, 'mousemove', this.mouseMoveHandlerFn); + this.documentMouseUpListener = this.renderer.listen(this.document, 'mouseup', this.mouseUpHandlerFn); + }); + } + + private setCountLength(): void { + if (this.hasMaxlength) { + this.countLength = this.element.value.length; + // safari浏览器回车键是按两个字计算 + if (TiBrowser.isSafari()) { + this.countLength = this.countLength + this.element.value.split('\n').length - 1; + } + } + } + + // 拖动函数 + private mouseMove(event: MouseEvent): void { + // 阻止调整文本框大小时文本框聚焦 + event.preventDefault(); + const width: number = this.options.storeWidth; + const height: number = this.options.storeHeight; + const horizonWidth: number = event.pageX - this.options.mouseXPosition; + const verticalHeight: number = event.pageY - this.options.mouseYPosition; + + if (this.resize !== 'vertical' && width + horizonWidth >= this.textAreaMinWidth && width + horizonWidth < this.textAreaMaxWidth) { + // 更新textarea宽度 + this.options.storeWidth += horizonWidth; + // 调整width + this.renderer.setStyle(this.container, 'width', `${this.options.storeWidth}px`); + } + + if ( + this.resize !== 'horizontal' && + height + verticalHeight >= this.textAreaMinHeight && + height + verticalHeight < this.textAreaMaxHeight + ) { + // 更新textarea高度 + this.options.storeHeight += verticalHeight; + this.renderer.setStyle(this.container, 'height', `${this.options.storeHeight}px`); + } + + // 更新光标位置 + this.options.mouseXPosition = event.pageX; + this.options.mouseYPosition = event.pageY; + } + + private stopResize(): void { + this.options.mouseXPosition = 0; + this.options.mouseYPosition = 0; + this.updateTextAreaSize(); + this.options.$target = null; + } + + // 获取min-width、min-height等的样式值 + private getSizeNumber(value: string): number { + const val: string = this.element.style[value]; // 可实现获取样式 + + if (val !== 'none' && val !== '') { + return parseFloat(val.replace(/px/, '')); + } + + return this.defaultStyle[value]; + } + + // 在拖动前获取当前文本框最新的高度、宽度 + private updateTextAreaSize(): void { + this.options.storeWidth = this.container.clientWidth; + this.options.storeHeight = this.container.clientHeight; + } + + // 给页面设置不可选样式,避免拖动时页面出现被选中的蓝色区域 + private toggleTextSelection(toggle: boolean): void { + const body: any = this.document.getElementsByTagName('body'); + if (toggle) { + this.renderer.addClass(body[0], 'ti3-unselectable'); + this.renderer.setAttribute(body[0], 'unselectable', 'on'); + } else { + this.renderer.removeClass(body[0], 'ti3-unselectable'); + this.renderer.removeAttribute(body[0], 'unselectable'); + } + } + + /** + * @ignore + * 绑定在document上的mouseMove事件 + */ + public mouseMoveHandlerFn = ($event: MouseEvent): void => { + // 拖动的动作应该是先mousedown,然后mousemove,因此先判断是否已经触发了mousedown + if ((this.options.mouseXPosition === 0 && this.options.mouseYPosition === 0) || !this.options.$target) { + return; + } + this.mouseMove($event); + }; + /** + * @ignore + * 绑定在document上的mouseUp事件 + */ + public mouseUpHandlerFn = (): void => { + // 解决问题:当拖动鼠标时,鼠标不在小三角上时,鼠标放开不会移除样式 + // 为确保鼠标弹起时, textarea的边框样式恢复默认: + if (this.isResizing()) { + this.renderer.removeClass(this.element, 'ti3-textarea-resize-border'); + } + try { + this.stopResize(); + this.toggleTextSelection(false); + } catch (e) {} + }; + + private isResizing(): boolean { + return this.element.className.indexOf('ti3-textarea-resize-border') > -1; + } +} diff --git a/src/textarea/lib/src/TiTextareaModule.ts b/src/textarea/lib/src/TiTextareaModule.ts new file mode 100644 index 0000000..e31949a --- /dev/null +++ b/src/textarea/lib/src/TiTextareaModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiTextareaComponent } from './TiTextareaComponent'; +import { TiFormatNumPipe } from './TiFormatNumPipe'; +import { TiRendererModule } from '@opentiny/ng-renderer'; + +@NgModule({ + imports: [CommonModule, TiRendererModule], + exports: [TiTextareaComponent], + declarations: [TiTextareaComponent, TiFormatNumPipe] +}) +export class TiTextareaModule {} +export { TiTextareaComponent } from './TiTextareaComponent'; diff --git a/src/textarea/lib/src/textarea.html b/src/textarea/lib/src/textarea.html new file mode 100644 index 0000000..698ad92 --- /dev/null +++ b/src/textarea/lib/src/textarea.html @@ -0,0 +1,5 @@ + + + {{countLength | tiFormatNum}}/{{maxLength | + tiFormatNum}} diff --git a/src/textarea/lib/src/textarea.less b/src/textarea/lib/src/textarea.less new file mode 100644 index 0000000..0c4f6ea --- /dev/null +++ b/src/textarea/lib/src/textarea.less @@ -0,0 +1,144 @@ +@import "../../../themes/basic/base-all.less"; + +.ti3-textarea-container { + --ti-textarea-counter-height: var(--ti-common-size-4x); + --ti-textarea-counter-margin-top: 2px; +} + +@textarea-name: tiTextarea; + +.textarea-resize-slash(@color, @size, @position) { + content: ""; + width: @size; + height: @size; + display: block; + border-bottom: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) @color; + transform: rotate(135deg); + bottom: @position; + right: @position; + position: absolute; +} +.textarea-resize-color(@color) { + &:before{ + border-bottom-color: @color; + } + &:after { + border-bottom-color: @color; + } +} +.textarea-color(@color) { + border-color: @color; + & ~ .ti3-textarea-resize-icon { + .textarea-resize-color(@color); + } +} + +//处理校验失败时textarea右下角三角颜色问题 +textarea{ + @{tiny-invalid-class}{ + &~ .ti3-textarea-resize-icon{ + &:before{ + border-bottom-color: var(--ti-common-color-error-border) !important; + } + &:after { + border-bottom-color: var(--ti-common-color-error-border) !important; + } + } + } +} + +.ti3-textarea-container { + min-height: var(--ti-common-size-16x); + position: relative; + display: inline-block; + overflow: hidden; + vertical-align: middle; + line-height: normal; + + & > textarea[@{textarea-name}] { + .form-border-animat-init(); + overflow-x: hidden; + width: 100% !important; + height: 100% !important; + resize: none !important; + border-radius: var(--ti-common-border-radius-normal); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + color: var(--ti-common-color-text-primary); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + background-color: var(--ti-common-color-bg-white-normal); + padding: var(--ti-common-space-6) var(--ti-common-space-10); + .box-sizing(border-box); + outline: none; + vertical-align: middle; + &:disabled { + background-color: var(--ti-common-color-bg-disabled); + color: var(--ti-common-color-text-disabled); + border-color: var(--ti-common-color-line-disabled); + cursor: not-allowed !important; + } + + &:not(:disabled) { + &:hover { + .textarea-color(var(--ti-common-color-line-hover)); + .form-border-animat-enter(); + } + &:focus{ + .textarea-color(var(--ti-common-color-line-active)); + .form-border-animat-enter(); + } + &.ti3-textarea-resize-border {// textareaa拉伸时,边框和小三角颜色的设置 + .textarea-color(var(--ti-common-color-line-active)); + .form-border-animat-enter(); + } + } + } + + & > .ti3-textarea-resize-icon { + position: absolute; + bottom: 1px; + right: 1px; + width: 10px; + height: 10px; + cursor: nw-resize; + &:before { + .textarea-resize-slash(var(--ti-common-color-line-normal), 8.4px, -2px); + } + &:after { + .textarea-resize-slash(var(--ti-common-color-line-normal), 4.2px, 0px); + } + } + + & > .ti3-textarea-counter { + height: var(--ti-textarea-counter-height); + line-height: var(--ti-textarea-counter-height); + display: block; + .box-sizing(border-box); + position: absolute; + right: 0; + margin-top: var(--ti-textarea-counter-margin-top); + text-align: right; + color: var(--ti-common-color-text-weaken); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + background-color: var(--ti-common-color-bg-white-normal); + & > .ti3-textarea-counter-count { + color:var(--ti-common-color-text-primary); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + } + } + + &.ti3-textarea-container-counter { + & > textarea[@{textarea-name}] { + height: calc(100% - var(--ti-textarea-counter-height) - var(--ti-textarea-counter-margin-top)) !important; + } + & > .ti3-textarea-resize-icon { + bottom: calc(var(--ti-textarea-counter-height) + var(--ti-textarea-counter-margin-top)); + } + } +} + +.ti3-unselectable { + .user-select(none); +} \ No newline at end of file diff --git a/src/themes/README.md b/src/themes/README.md new file mode 100644 index 0000000..a019c0b --- /dev/null +++ b/src/themes/README.md @@ -0,0 +1,30 @@ +### 样式开发规范: + +1. 变量开发部分 + +【使用变量目的】 + +为了方便组件整体换肤和扩展,使用CSS变量进行了公共变量提取 + +【开发说明】 + +**基础变量开发部分:** + +基础变量开发包含以下两个文件 + +* `themes/basic/basic-var.less`:文件中定义了所有可变的视觉变量 + + **注意点:** + + 1)颜色变量定义部分,区分了基础色和场景色 + + 基础色:用于描述用到的所有颜色变量,以 ti-base- 开头命名,该变量仅在场景色中使用,组件中不可使用 + + 场景色:用于描述组件使用场景中使用到的颜色变量,以 ti-common- 开头命名,具体组件中使用 + + 2)除非组件特殊宽高、组件交互样式属性外,组件中使用到的所有变量均需引用该文件中的变量 + +**样式开发部分:** + +1. 字体图标中的图标样式类应该只包含原始的图标编码,不要加其他样式属性 + \ No newline at end of file diff --git a/src/themes/basic/base-all.less b/src/themes/basic/base-all.less new file mode 100644 index 0000000..14d614f --- /dev/null +++ b/src/themes/basic/base-all.less @@ -0,0 +1,6 @@ +// 供组件引用的基础less变量和mixin函数。经过less编译后,零冗余。 + +// 公共样式 +@import 'common.less'; +// 混合 +@import 'mixins.less'; diff --git a/src/themes/basic/basic-var.css b/src/themes/basic/basic-var.css new file mode 100644 index 0000000..44b9f7c --- /dev/null +++ b/src/themes/basic/basic-var.css @@ -0,0 +1,346 @@ +/*基础变量定义*/ +:root { + /* 一.颜色*/ + /* 1.基础色:此处的颜色变量为所有组件应用到的颜色,此处颜色仅在公共色中使用,具体组件不可使用*/ + --ti-base-color-white: #FFFFFF; /* 纯白*/ + + /* 1.1品牌色*/ + /* 品牌主色*/ + --ti-base-color-brand-6: #5E7CE0; /* 主色蓝*/ + /* 主色衍生色*/ + --ti-base-color-brand-8: #344899; /* 品牌色-8*/ + --ti-base-color-brand-7: #526ECC; /* 品牌色-7*/ + --ti-base-color-brand-5: #7693F5; /* 品牌色-5*/ + --ti-base-color-brand-4: #96ADFA; /* 品牌色-4*/ + --ti-base-color-brand-3: #BECCFA; /* 品牌色-3*/ + --ti-base-color-brand-2: #E9EDFA; /* 品牌色-2*/ + --ti-base-color-brand-1: #F2F5FC; /* 品牌色-1*/ + + /* 1.2中立色*/ + /* 公用灰色系,用于文本、图标、线条、背景色*/ + --ti-base-color-common-9: #181818; /* 中立色-9*/ + --ti-base-color-common-8: #282B33; /* 中立色-8*/ + --ti-base-color-common-7: #252B3A; /* 中立色-7*/ + --ti-base-color-common-6: #464C59; /* 中立色-6*/ + --ti-base-color-common-5: #575D6C; /* 中立色-5*/ + --ti-base-color-common-4: #5C6173; /* 中立色-4*/ + --ti-base-color-common-3: #8A8E99; /* 中立色-3*/ + --ti-base-color-common-2: #ADB0B8; /* 中立色-2*/ + --ti-base-color-common-1: #DFE1E6; /* 中立色-1*/ + + /* 1.3背景色*/ + --ti-base-color-bg-9: #B12220; /* 背景-9*/ + --ti-base-color-bg-8: #C7000B; /* 背景-8*/ + --ti-base-color-bg-7: #D64A52; /* 背景-7*/ + --ti-base-color-bg-6: #EEF0F5; /* 背景-6*/ + --ti-base-color-bg-5: #F5F5F6; /* 背景-5*/ + --ti-base-color-bg-4: #FAFAFA; /* 背景-4*/ + --ti-base-color-bg-3: #FFFFFF; /* 背景-3*/ + --ti-base-color-bg-2: #FFFFFF; /* 背景-2*/ + --ti-base-color-bg-1: #FFFFFF; /* 背景-1*/ + + /* 1.4功能色*/ + --ti-base-color-error-4: #DE504E; /* 错误-4*/ + --ti-base-color-error-3: #F66F6A; /* 错误-3*/ + --ti-base-color-error-2: #FFBCBA; /* 错误-2*/ + --ti-base-color-error-1:#FFEEED; /* 错误-1*/ + + --ti-base-color-success-4: #3AC295; /* 成功-4*/ + --ti-base-color-success-3: #50D4AB; /* 成功-3*/ + --ti-base-color-success-2:#ACF2DC; /* 成功-2*/ + --ti-base-color-success-1:#EDFFF9; /* 成功-1*/ + + --ti-base-color-warn-5: #E37D29; /* 告警-5*/ + --ti-base-color-warn-4: #FA9841; /* 告警-4*/ + --ti-base-color-warn-3:#FAC20A; /* 告警-3*/ + --ti-base-color-warn-2: #FFD0A6; /* 告警-2*/ + --ti-base-color-warn-1:#FFF3E8; /* 告警-1*/ + + --ti-base-color-prompt-4: var(--ti-base-color-brand-7); /* 提示-4*/ + --ti-base-color-prompt-3: var(--ti-base-color-brand-6); /* 提示-3*/ + --ti-base-color-prompt-2: var(--ti-base-color-brand-3); /* 提示-2*/ + --ti-base-color-prompt-1: #EBF6FF; /* 提示-1*/ + + --ti-base-color-prompt-icon-from: #7769E8; /* 渐变图标-提示-起始色*/ + --ti-base-color-prompt-icon-to: #58BBFF; /* 渐变图标-提示-终止色*/ + + /* 状态图标色*/ + --ti-base-color-icon-info:#6CBFFF; /* 状态图标-常规*/ + + /* 图表色*/ + --ti-base-color-data-3:#A6DD82; /* 图表数据色-3*/ + --ti-base-color-data-4:#F3689A; /* 图表数据色-4*/ + --ti-base-color-data-5:#A97AF8; /* 图表数据色-5*/ + + /* 透明色*/ + --ti-base-color-transparent: transparent; + + /* 2.公共色:此处颜色为组件场景色,根据使用场景分为以下几大类,具体组件引用以下颜色,如在使用过程中有问题,请自行按类别添加*/ + /* 2.1提示类型颜色,用于alert组件、涉及功能提示的背景、文字、图标等的颜色使用*/ + --ti-common-color-success: var(--ti-base-color-success-3); /* 成功-图标色/状态图标-成功*/ + --ti-common-color-text-success: var(--ti-base-color-success-4); /* 成功-文字色*/ + --ti-common-color-success-bg: var(--ti-base-color-success-1); /* 成功-背景色*/ + --ti-common-color-success-border: var(--ti-base-color-success-2); /* 成功-边框色*/ + + --ti-common-color-error: var(--ti-base-color-error-3); /* 错误-图标色/状态图标-危险、错误、失败/深色Tip中的价格文本*/ + --ti-common-color-error-text: var(--ti-base-color-error-4); /* 错误-文字色*/ + --ti-common-color-error-bg: var(--ti-base-color-error-1); /* 错误-背景色/校验背景色*/ + --ti-common-color-error-border: var(--ti-base-color-error-3); /* 错误-校验边框色*/ + --ti-common-color-error-border-secondary: var(--ti-base-color-error-2); /* 错误-alert边框色*/ + + --ti-common-color-warn: var(--ti-base-color-warn-4); /* 告警-图标色/状态图标-警告*/ + --ti-common-color-warn-text: var(--ti-base-color-warn-5); /* 告警-文字色*/ + --ti-common-color-warn-bg: var(--ti-base-color-warn-1); /* 告警-背景色*/ + --ti-common-color-warn-border: var(--ti-base-color-warn-2); /* 告警-边框色*/ + --ti-common-color-warn-secondary: var(--ti-base-color-warn-3); /* 次要告警-图标色/状态图标-异常*/ + + --ti-common-color-prompt: var(--ti-base-color-prompt-3); /* 提示-图标色*/ + --ti-common-color-prompt-text: var(--ti-base-color-prompt-4); /* 提示-图标色*/ + --ti-common-color-prompt-bg: var(--ti-base-color-prompt-1); /* 提示-背景色*/ + --ti-common-color-prompt-border: var(--ti-base-color-prompt-2); /* 提示-边框色*/ + --ti-common-color-prompt-icon-from:var(--ti-base-color-prompt-icon-from); /* 渐变图标-提示-起始色*/ + --ti-common-color-prompt-icon-to:var(--ti-base-color-prompt-icon-to); /* 渐变图标-提示-终止色*/ + + /* 2.2文本色*/ + --ti-common-color-text-primary: var(--ti-base-color-common-7); /* 一级文本色-重要信息/标题颜色/输入类文本颜色*/ + --ti-common-color-text-secondary: var(--ti-base-color-common-5); /* 二级文本色-次要信息*/ + --ti-common-color-text-weaken: var(--ti-base-color-common-3); /* 三级文本色-弱化信息/说明文字*/ + --ti-common-color-text-disabled: var(--ti-base-color-common-2); /* 文本禁用信息*/ + --ti-common-color-text-darkbg: var(--ti-base-color-common-2); /* 深色背景下文本信息*/ + --ti-common-color-text-darkbg-disabled: var(--ti-base-color-common-5); /* 深色背景下文本信息禁用色*/ + --ti-common-color-text-link: var(--ti-base-color-brand-7); /* 链接色*/ + --ti-common-color-text-link-hover: var(--ti-base-color-brand-8); /* 链接悬浮色*/ + --ti-common-color-text-link-darkbg: var(--ti-base-color-brand-4); /* 深色背景链接色*/ + --ti-common-color-text-link-darkbg-hover: var(--ti-base-color-brand-3); /* 深色背景链接悬浮色*/ + --ti-common-color-text-highlight: var(--ti-base-color-brand-7); /* 文本高亮色*/ + --ti-common-color-text-white: var(--ti-base-color-white); /* 深色背景或图标上文字色*/ + --ti-common-color-text-gray: var(--ti-base-color-white); /* 深色背景下的文本色,用于tip*/ + --ti-common-color-text-gray-disabled: var(--ti-base-color-common-4); /* 深色背景下的灰色文本禁用色,用于tab页签中*/ + --ti-common-color-text-important: var(--ti-base-color-error-4); /* 文本_金额*/ + + /* 2.3图标色*/ + /* 浅底背景图标色*/ + --ti-common-color-icon-normal: var(--ti-base-color-common-5); + --ti-common-color-icon-hover: var(--ti-base-color-brand-6); + --ti-common-color-icon-active: var(--ti-base-color-brand-6); + --ti-common-color-icon-disabled: var(--ti-base-color-common-2); /* 图标禁用色/状态图标-禁用、停止*/ + --ti-common-color-icon-white: var(--ti-base-color-white); + + /* 灰色背景下图标色*/ + --ti-common-color-icon-graybg-normal: var(--ti-base-color-common-2); + --ti-common-color-icon-graybg-hover: var(--ti-base-color-brand-6); + --ti-common-color-icon-graybg-active: var(--ti-base-color-brand-6); + --ti-common-color-icon-graybg-disabled: var(--ti-base-color-common-1); + + /* 深底背景图标色*/ + --ti-common-color-icon-darkbg-normal: var(--ti-base-color-common-2); + --ti-common-color-icon-darkbg-hover: var(--ti-base-color-brand-5); + --ti-common-color-icon-darkbg-active: var(--ti-base-color-brand-5); + --ti-common-color-icon-darkbg-disabled: var(--ti-base-color-common-5); + + /* 状态图标背景色*/ + --ti-common-color-icon-info: var(--ti-base-color-icon-info); /* 状态图标-常规、信息提示*/ + + /* 2.4线颜色,用于边框,线条等的颜色使用*/ + --ti-common-color-line-normal: var(--ti-base-color-common-2); + --ti-common-color-line-hover: var(--ti-base-color-common-5); + --ti-common-color-line-active: var(--ti-base-color-brand-6); + --ti-common-color-line-disabled: var(--ti-base-color-common-1); + /* 分割线颜色*/ + --ti-common-color-line-dividing: var(--ti-base-color-common-1); + /* 虚线*/ + --ti-common-color-dash-line-normal: var(--ti-base-color-common-5); + --ti-common-color-dash-line-hover: var(--ti-base-color-brand-7); + + /* 2.5背景色*/ + /* 背景基础色各状态色*/ + --ti-common-color-bg-normal: var(--ti-base-color-bg-6); /* 通用背景-页面背景色/下拉搜索框背景色/标签背景色*/ + --ti-common-color-bg-emphasize: var(--ti-base-color-brand-6); /* 背景高亮色*/ + --ti-common-color-bg-disabled: var(--ti-base-color-bg-5); /* 禁用背景色*/ + --ti-common-color-bg-hover: var(--ti-base-color-brand-8); /* 主色背景悬浮色*/ + --ti-common-color-bg-gray: var(--ti-base-color-bg-4); /* 新区域组件-悬浮背景色*/ + --ti-common-color-bg-secondary: var(--ti-base-color-common-2); /* 开关组件-关闭状态-背景色*/ + + /* 重要背景色,主要用于重要按钮场景*/ + --ti-common-bg-primary: var(--ti-base-color-bg-8); /* 重要按钮背景色*/ + --ti-common-bg-primary-hover: var(--ti-base-color-bg-7); /* 重要按钮背景悬浮、focus色*/ + --ti-common-bg-primary-active: var(--ti-base-color-bg-9); /* 重要按钮背景色按下色*/ + + /* 次要背景色,主要用于次要按钮场景*/ + --ti-common-bg-minor: var(--ti-base-color-bg-2); /* 次要按钮背景色*/ + --ti-common-bg-minor-hover: var(--ti-base-color-bg-1); /* 次要按钮背景悬浮、focus色*/ + --ti-common-bg-minor-active: var(--ti-base-color-bg-3); /* 次要按钮背景色按下色*/ + + /* 白底背景状态色*/ + --ti-common-color-bg-white-normal: var(--ti-base-color-white); /* 白色背景,用于输入框背景*/ + --ti-common-color-bg-white-emphasize: var(--ti-base-color-brand-1); /* 白色hover或强调色,如表头背景、表格悬浮、下拉选项悬浮背景*/ + + /* 浅底背景状态色*/ + --ti-common-color-bg-light-normal: var(--ti-base-color-brand-2); /* 滑块slider-背景色/多选快buttongroup-默认背景色/树组件选中背景色*/ + --ti-common-color-bg-light-emphasize: var(--ti-base-color-brand-3); /* 浅背景hover或强调色,开关组件“开”禁用背景色*/ + + /* 深色底背景状态色*/ + --ti-common-color-bg-dark-normal: var(--ti-base-color-common-6); /* 一级tab页签背景色*/ + --ti-common-color-bg-dark-emphasize: var(--ti-base-color-common-4); /* 一级tab页签背景-悬浮色*/ + --ti-common-color-bg-dark-active: var(--ti-common-color-bg-normal); /* 一级tab页签背景-激活/focus状态背景色*/ + --ti-common-color-bg-dark-deep: var(--ti-base-color-common-6); /* tip、alert提示背景色*/ + --ti-common-color-bg-dark-disabled: var(--ti-base-color-common-1); /* 深色背景禁用色,开关组件“关”禁用背景色*/ + + /* 顶部导航*/ + --ti-common-color-bg-navigation: var(--ti-base-color-common-8); /* 顶部导航背景色/顶部导航下拉悬浮背景色*/ + --ti-common-color-bg-dark-select: var(--ti-base-color-common-9); /* 顶部导航下拉背景色 */ + + /* 2.6 图表色*/ + --ti-common-color-data-1: var(--ti-base-color-success-3); /* 图表数据色-1*/ + --ti-common-color-data-2: var(--ti-base-color-icon-info); /* 图表数据色-2*/ + --ti-common-color-data-3: var(--ti-base-color-data-3); /* 图表数据色-3*/ + --ti-common-color-data-4: var(--ti-base-color-data-4); /* 图表数据色-4*/ + --ti-common-color-data-5: var(--ti-base-color-data-5); /* 图表数据色-5*/ + --ti-common-color-data-6: var(--ti-base-color-warn-3); /* 图表数据色-6*/ + --ti-common-color-data-7: var(--ti-base-color-warn-4); /* 图表数据色-7*/ + --ti-common-color-data-8: var(--ti-base-color-error-3); /* 图表数据色-8*/ + + /* 2.7 透明色*/ + --ti-common-color-transparent: var(--ti-base-color-transparent); + + /* 二.其他变量*/ + /* 边框圆角*/ + --ti-common-border-radius-normal: 2px; /* 常规*/ + --ti-common-border-radius-0: 0px; /* 直角*/ + --ti-common-border-radius-1: 4px; /* 圆角-1*/ + --ti-common-border-radius-2: 8px; /* 圆角-2*/ + --ti-common-border-radius-3: 50%; /* 圆形*/ + + /* 字号*/ + --ti-common-font-size-base: 12px; /* 正文-常规*/ + --ti-common-font-size-1: 14px; /* 标题-小*/ + --ti-common-font-size-2: 16px; /* 标题-中*/ + --ti-common-font-size-3: 18px; /* 标题-大*/ + --ti-common-font-size-4: 20px; /* 字号-4*/ + --ti-common-font-size-5: 24px; /* 字号-5*/ + --ti-common-font-size-6: 32px; /* 字号-6*/ + --ti-common-font-size-7: 36px; /* 字号-7*/ + + /* 行高*/ + --ti-common-line-height-number: 1.5; /* 文字行高倍数,建议组件中设置行高使用该变量,如有特殊情况,请自行定义*/ + + /* 间距:适用于组件中的margin、padding*/ + --ti-common-space-base: 4px; /* 基础间距*/ + --ti-common-space-2x: calc(var(--ti-common-space-base) * 2); /* 间距-2*/ + --ti-common-space-3x: calc(var(--ti-common-space-base) * 3); /* 间距-3*/ + --ti-common-space-4x: calc(var(--ti-common-space-base) * 4); /* 间距-4*/ + --ti-common-space-5x: calc(var(--ti-common-space-base) * 5); /* 间距-5*/ + --ti-common-space-6x: calc(var(--ti-common-space-base) * 6); /* 间距-6*/ + --ti-common-space-7x: calc(var(--ti-common-space-base) * 7); /* 间距-7*/ + --ti-common-space-8x: calc(var(--ti-common-space-base) * 8); /* 间距-8*/ + --ti-common-space-9x: calc(var(--ti-common-space-base) * 9); /* 间距-9*/ + --ti-common-space-10x: calc(var(--ti-common-space-base) * 10); /* 间距-10*/ + /* 其他间距*/ + --ti-common-space-0: 0px; /* 其他间距-1*/ + --ti-common-space-1: 1px; /* 其他间距-2*/ + --ti-common-space-6: 6px; /* 其他间距-3*/ + --ti-common-space-10: 10px; /* 其他间距-4*/ + + /* 阴影*/ + --ti-common-shadow-1-up: 0 -1px 4px 0 rgba(0,0,0,0.1); /* 阴影-1 上*/ + --ti-common-shadow-1-down: 0 1px 4px 0 rgba(0,0,0,0.1); /* 阴影-1 下*/ + --ti-common-shadow-1-left: -1px 0px 4px 0 rgba(0,0,0,0.1); /* 阴影-1 左*/ + --ti-common-shadow-1-right: 1px 0px 4px 0 rgba(0,0,0,0.1); /* 阴影-1 右*/ + --ti-common-shadow-2-up: 0 -2px 8px 0 rgba(0,0,0,0.2); /* 阴影-2 上*/ + --ti-common-shadow-2-down: 0 2px 8px 0 rgba(0,0,0,0.2); /* 阴影-2 下*/ + --ti-common-shadow-2-left: -2px 0 8px 0 rgba(238, 10, 10, 0.2); /* 阴影-2 左*/ + --ti-common-shadow-2-right: 2px 0 8px 0 rgba(252, 5, 5, 0.2); /* 阴影-2 右*/ + --ti-common-shadow-3-up: 0 -4px 16px 0 rgba(0,0,0,0.2); /* 阴影-3 上*/ + --ti-common-shadow-3-down: 0 4px 16px 0 rgba(0,0,0,0.2); /* 阴影-3 下*/ + --ti-common-shadow-3-left: -4px 0 16px 0 rgba(0,0,0,0.2); /* 阴影-3 左*/ + --ti-common-shadow-3-right: 4px 0 16px 0 rgba(0,0,0,0.2); /* 阴影-3 右*/ + --ti-common-shadow-4-up: 0 -8px 40px 0 rgba(0,0,0,0.2); /* 阴影-4 上*/ + --ti-common-shadow-4-down: 0 8px 40px 0 rgba(0,0,0,0.2); /* 阴影-4 下*/ + --ti-common-shadow-4-left: -8px 0 40px 0 rgba(0,0,0,0.2); /* 阴影-4 左*/ + --ti-common-shadow-4-right: 8px 0 40px 0 rgba(0,0,0,0.2); /* 阴影-4 右*/ + + /* 提示类阴影*/ + --ti-common-shadow-error: 0 1px 3px 0 rgba(199,54,54,0.25); /* 错误*/ + --ti-common-shadow-warn: 0 1px 3px 0 rgba(204,100,20,0.25); /* 告警*/ + --ti-common-shadow-prompt: 0 1px 3px 0 rgba(70,94,184,0.25); /* 提示*/ + --ti-common-shadow-success: 0 1px 3px 0 rgba(39,176,128,0.25); /* 成功*/ + + /* 字体*/ + --ti-common-font-family: "Helvetica", "Arial", "PingFangSC-Regular", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", "Microsoft JhengHei"; + + /* 字重*/ + --ti-common-font-weight-1: 100; /* 极细*/ + --ti-common-font-weight-2: 200; /* 纤细*/ + --ti-common-font-weight-3: 300; /* 细体*/ + --ti-common-font-weight-4: normal; /* 常规*/ + --ti-common-font-weight-5: 500; /* 中等*/ + --ti-common-font-weight-6: 600; /* 半粗*/ + --ti-common-font-weight-7: bold; /* 粗体*/ + --ti-common-font-weight-8: 800; /* 中黑*/ + --ti-common-font-weight-9: 900; /* 黑体*/ + + /* 边框粗细*/ + --ti-common-border-weight-normal: 1px; /* 常规*/ + --ti-common-border-weight-1: 2px; /* 较粗*/ + --ti-common-border-weight-2: 3px; /* 粗*/ + + /* 边框样式*/ + --ti-common-border-style-dashed: dashed; /* 虚线*/ + --ti-common-border-style-dotted: dotted; /* 点线*/ + --ti-common-border-style-solid: solid; /* 实线*/ + + /* 尺寸*/ + --ti-common-size-base: 4px; /* 基础尺寸*/ + --ti-common-size-2x: calc(var(--ti-common-size-base) * 2); /* 尺寸-2*/ + --ti-common-size-3x: calc(var(--ti-common-size-base) * 3); /* 尺寸-3*/ + --ti-common-size-4x: calc(var(--ti-common-size-base) * 4); /* 尺寸-4*/ + --ti-common-size-5x: calc(var(--ti-common-size-base) * 5); /* 尺寸-5*/ + --ti-common-size-6x: calc(var(--ti-common-size-base) * 6); /* 尺寸-6*/ + --ti-common-size-7x: calc(var(--ti-common-size-base) * 7); /* 尺寸-7*/ + --ti-common-size-8x: calc(var(--ti-common-size-base) * 8); /* 尺寸-8*/ + --ti-common-size-9x: calc(var(--ti-common-size-base) * 9); /* 尺寸-9*/ + --ti-common-size-10x: calc(var(--ti-common-size-base) * 10); /* 尺寸-10*/ + --ti-common-size-11x: calc(var(--ti-common-size-base) * 11); /* 尺寸-11*/ + --ti-common-size-12x: calc(var(--ti-common-size-base) * 12); /* 尺寸-12*/ + --ti-common-size-13x: calc(var(--ti-common-size-base) * 13); /* 尺寸-13*/ + --ti-common-size-14x: calc(var(--ti-common-size-base) * 14); /* 尺寸-14*/ + --ti-common-size-15x: calc(var(--ti-common-size-base) * 15); /* 尺寸-15*/ + --ti-common-size-16x: calc(var(--ti-common-size-base) * 16); /* 尺寸-16*/ + --ti-common-size-17x: calc(var(--ti-common-size-base) * 17); /* 尺寸-17*/ + --ti-common-size-18x: calc(var(--ti-common-size-base) * 18); /* 尺寸-18*/ + --ti-common-size-19x: calc(var(--ti-common-size-base) * 19); /* 尺寸-19*/ + --ti-common-size-20x: calc(var(--ti-common-size-base) * 20); /* 尺寸-20*/ + --ti-common-size-21x: calc(var(--ti-common-size-base) * 21); /* 尺寸-21*/ + --ti-common-size-22x: calc(var(--ti-common-size-base) * 22); /* 尺寸-22*/ + --ti-common-size-23x: calc(var(--ti-common-size-base) * 23); /* 尺寸-23*/ + --ti-common-size-24x: calc(var(--ti-common-size-base) * 24); /* 尺寸-24*/ + --ti-common-size-25x: calc(var(--ti-common-size-base) * 25); /* 尺寸-25*/ + --ti-common-size-26x: calc(var(--ti-common-size-base) * 26); /* 尺寸-26*/ + --ti-common-size-27x: calc(var(--ti-common-size-base) * 27); /* 尺寸-27*/ + --ti-common-size-28x: calc(var(--ti-common-size-base) * 28); /* 尺寸-28*/ + --ti-common-size-29x: calc(var(--ti-common-size-base) * 29); /* 尺寸-29*/ + --ti-common-size-30x: calc(var(--ti-common-size-base) * 30); /* 尺寸-30*/ + --ti-common-size-31x: calc(var(--ti-common-size-base) * 31); /* 尺寸-31*/ + --ti-common-size-32x: calc(var(--ti-common-size-base) * 32); /* 尺寸-32*/ + --ti-common-size-33x: calc(var(--ti-common-size-base) * 33); /* 尺寸-33*/ + --ti-common-size-34x: calc(var(--ti-common-size-base) * 34); /* 尺寸-34*/ + --ti-common-size-35x: calc(var(--ti-common-size-base) * 35); /* 尺寸-35*/ + --ti-common-size-36x: calc(var(--ti-common-size-base) * 36); /* 尺寸-36*/ + --ti-common-size-37x: calc(var(--ti-common-size-base) * 37); /* 尺寸-37*/ + --ti-common-size-38x: calc(var(--ti-common-size-base) * 38); /* 尺寸-38*/ + --ti-common-size-39x: calc(var(--ti-common-size-base) * 39); /* 尺寸-39*/ + --ti-common-size-40x: calc(var(--ti-common-size-base) * 40); /* 尺寸-40*/ + --ti-common-size-41x: calc(var(--ti-common-size-base) * 41); /* 尺寸-41*/ + --ti-common-size-42x: calc(var(--ti-common-size-base) * 42); /* 尺寸-42*/ + --ti-common-size-43x: calc(var(--ti-common-size-base) * 43); /* 尺寸-43*/ + --ti-common-size-44x: calc(var(--ti-common-size-base) * 44); /* 尺寸-44*/ + --ti-common-size-45x: calc(var(--ti-common-size-base) * 45); /* 尺寸-45*/ + --ti-common-size-46x: calc(var(--ti-common-size-base) * 46); /* 尺寸-46*/ + --ti-common-size-47x: calc(var(--ti-common-size-base) * 47); /* 尺寸-47*/ + --ti-common-size-48x: calc(var(--ti-common-size-base) * 48); /* 尺寸-48*/ + --ti-common-size-49x: calc(var(--ti-common-size-base) * 49); /* 尺寸-49*/ + --ti-common-size-50x: calc(var(--ti-common-size-base) * 50); /* 尺寸-50*/ + /* 其他尺寸*/ + --ti-common-size-0: 0px; /* 其他尺寸-1*/ + --ti-common-size-auto: auto; /* 其他尺寸-2*/ +} \ No newline at end of file diff --git a/src/themes/basic/build.less b/src/themes/basic/build.less new file mode 100644 index 0000000..bda0bfd --- /dev/null +++ b/src/themes/basic/build.less @@ -0,0 +1,4 @@ +// 编译后是给用户提供的style.css,所有组件公用部分。目前,仅normalize.less + +// 规范化 +@import 'normalize.less'; diff --git a/src/themes/basic/common.less b/src/themes/basic/common.less new file mode 100644 index 0000000..71d78a7 --- /dev/null +++ b/src/themes/basic/common.less @@ -0,0 +1,21 @@ +/** + * @description + * 该文件下定义组件less变量 + */ + +//-- Z-index master list +// +// Warning: Avoid customizing these values. They're used for a bird's eye view +// of components dependent on the z-axis and are designed to all work together. +// @zindex-navbar: 1000; +// @zindex-dropdown: 1000; +// @zindex-popover: 1060; +// @zindex-tooltip: 1070; +// @zindex-navbar-fixed: 1030; +// 如果确认其他index都不用了,那么以下这两个变量,移入modal +@zindex-modal-background: 1200; +@zindex-modal: 1300; +@zindex-notification: 6000; +// 失焦校验:错误且已输入失焦时显示错误样式,及时校验:错误且已输入时显示错误样式 +@tiny-invalid-class: &.ng-invalid.ng-touched[tiBlurCheck]:not([tiFocused]), &.ng-invalid.ng-dirty[tiBlurCheck]:not([tiFocused]), + &.ng-invalid.ng-touched:not([tiBlurCheck]):not([tiRadiobaseCheck]), &.ng-invalid.ng-dirty:not([tiBlurCheck]):not([tiRadiobaseCheck]); diff --git a/src/themes/basic/compnent-container-border.less b/src/themes/basic/compnent-container-border.less new file mode 100644 index 0000000..1cc7602 --- /dev/null +++ b/src/themes/basic/compnent-container-border.less @@ -0,0 +1,33 @@ +// @import "mixins.less"; // 一般组件,都会先引入mixins.less,所以这里就不必引入 + +// 组件边框样式:ip,select,spinner,tagsinput,date,datetime,daterange,datetimerange等组件 +// 组件皮儿已定义background-color,所以内部元素可以不定义background-color +// 组件皮儿已定义border,所以dominator边框如果和皮儿边框重合,那么dominator边框为透明。 +// 因为dominator和组件皮儿一样大,所以是dominator定义的cursor: not-allowed !important; 生效 +.ti3-compnent-container-border { + // 1.组件边框,通用尺寸样式,与disabled、hover、focused等状态无关; + display: inline-block; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid); + border-radius: var(--ti-common-border-radius-normal); + .box-sizing(border-box); + + // 2.组件边框,非禁用颜色样式,包括hover和focused等状态 + &:not([disabled]) { + background-color: var(--ti-common-color-bg-white-normal); + border-color: var(--ti-common-color-line-normal); + &:hover { + border-color: var(--ti-common-color-line-hover); + } + &[tiFocused] { + border-color: var(--ti-common-color-line-active); + } + // 是否存在hover focused同时出现的样式? + } + + // 3.组件边框,禁用颜色样式,包括hover和focused等状态 + &[disabled] { + background-color: var(--ti-common-color-bg-disabled); + border-color: var(--ti-common-color-line-disabled); + cursor: not-allowed !important; + } +} diff --git a/src/themes/basic/img/table-loadfail-bg.png b/src/themes/basic/img/table-loadfail-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..894d3daeb7fa5e14444b60035fe3de4944172baa GIT binary patch literal 3437 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 P00000NkvXXu0mjfD~)mj literal 0 HcmV?d00001 diff --git a/src/themes/basic/img/table-nodata-bg.png b/src/themes/basic/img/table-nodata-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..2180606f605ba69ed3d0d114ee012495e514c2a4 GIT binary patch literal 3430 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 P00000NkvXXu0mjfsB8wT literal 0 HcmV?d00001 diff --git a/src/themes/basic/img/upload-image-preview.png b/src/themes/basic/img/upload-image-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..e7cc4a76fed4d75d2759fc15f03f62889fa65e4b GIT binary patch literal 230 zcmVouE~7={4)_Fk4EZw0c_ua zNdz{M_BHSn;3#M%Eo#K!%_pD*=D=Cfx(1R0cEPso6R-k~lC~vCHbmP8U?yq#6QCqY z@Bdfhx2d)-z%7?am4lecrfb(haNF1TPZ$gcwt*(X9+IYm g9pu&=v_cN>1Q_Z$5!!^i0000007*qoM6N<$f?j1`r~m)} literal 0 HcmV?d00001 diff --git a/src/themes/basic/link-no-decoration.less b/src/themes/basic/link-no-decoration.less new file mode 100644 index 0000000..e06c42c --- /dev/null +++ b/src/themes/basic/link-no-decoration.less @@ -0,0 +1,10 @@ +.ti-link-no-decoration { + color: var(--ti-common-color-text-link); + text-decoration: none; + cursor: pointer; + + &:hover { + color: var(--ti-common-color-text-link-hover); + text-decoration: underline; + } +} diff --git a/src/themes/basic/mixins.less b/src/themes/basic/mixins.less new file mode 100644 index 0000000..9853564 --- /dev/null +++ b/src/themes/basic/mixins.less @@ -0,0 +1,242 @@ +// 阴影 +// @x 横轴偏移 +// @y 纵轴偏移 +// @blur 阴影偏移 +// @color 阴影颜色 +.box-shadow (...) { + -moz-box-shadow: @arguments; + -webkit-box-shadow: @arguments; + box-shadow: @arguments; +} + +// 盒模型计算模式 +// @type border-box | content-box +.box-sizing (@type:border-box) { + -webkit-box-sizing: @type; + -moz-box-sizing: @type; + -ms-box-sizing: @type; + box-sizing: @type; +} + +// 变换 +.transition (...) { + -moz-transition: @arguments; + -webkit-transition: @arguments; + transition: @arguments; +} + +.transition-transform(@transition) { + -webkit-transition: -webkit-transform @transition; + -moz-transition: -moz-transform @transition; + -o-transition: -o-transform @transition; + transition: transform @transition; +} +// 圆角 +.border-radius (...) { + -moz-border-radius: @arguments; + -webkit-border-radius: @arguments; + border-radius: @arguments; +} + +// 文字选择 +.user-select (@type:none) { + // 火狐 + -moz-user-select: @type; + // webkit浏览器 + -webkit-user-select: @type; + // IE10 + -ms-user-select: @type; + // 早期浏览器 + -khtml-user-select: @type; + user-select: @type; +} + +.triangle-up(@triangle-width; @triangle-height; @triangle-color) { + width: 0; + height: 0; + border-left: calc(@triangle-width / 2) solid transparent; + border-right: calc(@triangle-width / 2) solid transparent; + border-bottom: @triangle-height solid @triangle-color; +} + +.triangle-down(@triangle-width; @triangle-height; @triangle-color) { + width: 0; + height: 0; + border-left: calc(@triangle-width / 2) solid transparent; + border-right: calc(@triangle-width / 2) solid transparent; + border-top: @triangle-height solid @triangle-color; +} + +.translate(@x; @y) { + -webkit-transform: translate(@x, @y); + -ms-transform: translate(@x, @y); // IE9 only + -o-transform: translate(@x, @y); + transform: translate(@x, @y); +} + +// 旋转 +.rotate(@x) { + -webkit-transform: rotate(@x); + -ms-transform: rotate(@x); // IE9 only + -moz-transform: rotate(@x); + -o-transform: rotate(@x); + transform: rotate(@x); +} + +.clearfix() { + &:after { + display: table; + content: ''; + clear: both; + } + &:before { + display: table; + content: ''; + } +} +.background-linear-vertical(@start-color, @end-color...) { + & when (length(@arguments) > 1) { + background: mix(extract(@arguments, 1), extract(@arguments, 2), 50%); + background: -webkit-linear-gradient(top, @arguments); + background: -moz-linear-gradient(top, @arguments); + background: -o-linear-gradient(top, @arguments); + background: linear-gradient(top, @arguments); + } + + & when(length(@arguments) = 1) { + background: @arguments; + } +} + +.ellipsis() { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.flex-container(@direction: row, @justify-content: flex-start, @align-items: stretch) { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: @direction; + -ms-flex-direction: @direction; + -moz-flex-direction: @direction; + flex-direction: @direction; + -webkit-justify-content: @justify-content; + -moz-justify-content: @justify-content; + -ms-justify-content: @justify-content; + justify-content: @justify-content; + -webkit-align-items: @align-items; + -moz-align-items: @align-items; + -ms-align-items: @align-items; + align-items: @align-items; +} + +.flex-item(@grow: 0) { + -webkit-flex-grow: @grow; + -ms-flex-grow: @grow; + -moz-flex-grow: @grow; + flex-grow: @grow; +} + +/*------------------------------------------------动效------------------------------------------------*/ +//动效公共的less方法 +.animation(@animate-name, @time, @timing-function: cubic-bezier(0.4, 0, 0.2, 1), @delay:0s, @count:1,@wards:forwards) { + animation: @animate-name @time @timing-function @delay @count @wards; +} + +//.common-animation用来定义动画的函数,如果不想使用该函数中的某个属性,直接使用默认值:false +.common-animation(@common-name, + @from-border-color:false,@to-border-color:false, + @from-background-color:false,@to-background-color:false, + @from-color:false,@to-color:false) { + @keyframes @common-name { + from { + border: 1px solid @from-border-color; + background-color: @from-background-color; + color: @from-color; + } + to { + border: 1px solid @to-border-color; + background-color: @to-background-color; + color: @to-color; + } + } +} + +//定义一个移动动画函数 +.translate-animation(@name,@from-y:0px,@to-y:0px,@from-x:0px,@to-x:0px) { + @keyframes @name { + from { + transform: translate(@from-x, @from-y); + } + to { + transform: translate(@to-x, @to-y); + } + } +} + +//定义一个缩放函数 +.scale-animation(@scale-name,@from-scale,@to-scale) { + @keyframes @scale-name { + from { + transform: scale(@from-scale); + } + to { + transform: scale(@to-scale); + } + } +} + +//定义一个淡入淡出函数:使用的透明度参数必须是0--100,不能是0-1 +.fade-animation(@fade-name,@from-opacity,@to-opacity) { + @keyframes @fade-name { + from { + opacity: @from-opacity; + } + to { + opacity: @to-opacity; + } + } +} + +// 定义一个Y轴方向上缩放的动画函数 +.scaleY-animation(@scaleY-name,@scaleY-origin,@from-scaleY,@to-scaleY) { + @keyframes @scaleY-name { + from { + transform-origin: @scaleY-origin; + transform: scaleY(@from-scaleY); + } + to { + transform-origin: @scaleY-origin; + transform: scaleY(@to-scaleY); + } + } +} + +//定义一个旋转函数 +.rotate-aniamtion(@rotate-name,@from-rotate,@to-rotate) { + @keyframes @rotate-name { + from { + transform: rotate(@from-rotate); + -moz-transform: rotate(@from-rotate); + -ms-transform: rotate(@from-rotate); + -webkit-transform: rotate(@from-rotate); + } + to { + transform: rotate(@to-rotate); + -moz-transform: rotate(@to-rotate); + -ms-transform: rotate(@to-rotate); + -webkit-transform: rotate(@to-rotate); + } + } +} + +// 表单边框变化移入动画,在边框颜色变化过程中处理 +.form-border-animat-enter() { + .transition (border-color 0.2s); +} + +// 表单边框变化移出动画,在边框颜色初始定义时处理 +.form-border-animat-init() { + .transition (border-color 0.15s); +} diff --git a/src/themes/basic/normalize.less b/src/themes/basic/normalize.less new file mode 100644 index 0000000..a9be017 --- /dev/null +++ b/src/themes/basic/normalize.less @@ -0,0 +1,201 @@ +@import 'common.less'; // 此文件用到了@tiny-invalid-class +/* 基础样式 */ +body { + margin: 0; + padding: 0; + font-size: var(--ti-common-font-size-base); + font-family: var(--ti-common-font-family); + color: var(--ti-common-color-text-primary); + background-color: var(--ti-common-color-bg-white-normal); +} + +button, +blockquote, /* 结构类 */ +div, +section, +h1,h2,h3,h4,h5,h6, /* 标题类 */ +ul,ol,dl,dt,dd,li, /* 列表类 */ +pre, /* 文本格式元素 */ +code, +form,fieldset,legend,input,textarea, /* 表单类 */ +th,td, /* 表格类 */ +p, +select { + margin: 0; + padding: 0; +} + +/* 表单部分 */ +input { + vertical-align: middle; +} + +fieldset, +img { + border: 0; +} +// 校正不同浏览器下字体,字号及边距差异化 +button, +input, +textarea { + font-family: inherit; + font-size: 100%; + margin: 0; // 为了兼容Firefox 4+、Safari 5、Chrome +} +// IE下line-height属性默认是inherit,因此需要显示设置 +button { + line-height: normal; +} + +textarea { + overflow: auto; // 移除默认的纵向滚动条(IE 8/9) + vertical-align: top; // 统一各浏览器的对齐格式。 + resize: vertical; +} + +// 移除内部的padding、border值。为了兼容 Firefox 4+ +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} +// 特殊标签差异化处理 +address, +caption, +cite, +code, +dfn, +em, +var { + font-style: normal; + font-weight: 400; +} + +// 去除列表标记 +ol, +ul { + list-style: none; +} +// 标记引用 +q:before { + content: ''; +} +q:after { + content: ''; +} + +// 缩写类标签 +abbr, +acronym { + border: 0; + font-variant: normal; +} + +// 为了兼容Firefox 4+, Safari 5, and Chrome +b, +strong { + font-weight: 700; +} + +// 防止在所有浏览器中的“ sub”和“ sup”影响行高。 +sub { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; + bottom: -0.25em; +} +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; + top: -0.5em; +} +// 在IE10下,移除链接中的灰色背景色。 +a { + background-color: transparent; +} + +// 设置默认样式,避免受到bootstrap等第三方样式库的影响 +label { + margin-bottom: 0; + font-weight: 400; +} + +/* 消除表格单元格直接空隙 */ + +table { + border-spacing: 0 0; + border-collapse: collapse; + margin-bottom: 0; +} + +/* H5新标签 */ +figcaption, // 定义figure元素的标题 +figure,hgroup, // 用于对元素进行组合 +article,footer,header,nav,section,aside, // 结构布局类标签 +menu, // 定义菜单列表 +main, // 规定文档的主要内容 +details,summary { + // 定义标题详情 + display: block; +} + +button::-moz-focus-inner { + padding: 0; + border: none; +} + +audio, +canvas, +progress, +video { + display: inline-block; // 兼容 IE 8/9 + vertical-align: baseline; // 兼容Chrome, Firefox, and Opera +} + +// IE10+下隐藏的clear和password按钮 +input::-ms-clear { + width: 0; + height: 0; +} +input[type='password']::-ms-reveal { + width: 0; + height: 0; +} + +// placeholder 相关样式 适用于 Chrome/Opera/Safari +::-webkit-input-placeholder { + color: var(--ti-common-color-text-disabled); + text-overflow: initial !important; +} + +// placeholder 相关样式 适用于Firefox 19+ +::-moz-placeholder { + color: var(--ti-common-color-text-disabled); + opacity: 1; + text-overflow: initial; +} +// placeholder 相关样式 适用于Firefox 18- +:-moz-placeholder { + color: var(--ti-common-color-text-disabled); + opacity: 1; +} + +// 处理IE下placeholder文本颜色显示问题 +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + color: var(--ti-common-color-text-disabled) !important; +} + +// 一些Angular和组件相关的样式 +// 表单元素校验样式,归属于表单的样式定义,校验中没有合适的文件定义和加载,因此需要在公共处定义样式 +input, +textarea, +[tiForm] { + @{tiny-invalid-class} { + border-color: var(--ti-common-color-error-border) !important; + background-color: var(--ti-common-color-error-bg); + } +} diff --git a/src/themes/package.json b/src/themes/package.json new file mode 100644 index 0000000..9c815b1 --- /dev/null +++ b/src/themes/package.json @@ -0,0 +1,5 @@ +{ + "name": "@opentiny/ng-themes", + "version": "1.0.0-beta.0", + "license": "MIT" +} \ No newline at end of file diff --git a/src/themes/project.json b/src/themes/project.json new file mode 100644 index 0000000..f1b81dd --- /dev/null +++ b/src/themes/project.json @@ -0,0 +1,38 @@ +{ + "projectType": "library", + "root": "src/themes", + "sourceRoot": "src/themes", + "targets": { + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/themes"], + "options": { + "commands": [ + { + "command": "node build/buildThemes.js" + } + ] + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/themes && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build themes && ng pack themes && node build/publish.js themes --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/themes/theme-blue/basic-var.less b/src/themes/theme-blue/basic-var.less new file mode 100644 index 0000000..d0c81bc --- /dev/null +++ b/src/themes/theme-blue/basic-var.less @@ -0,0 +1,16 @@ +/*基础变量定义*/ +:root { + /* 一.颜色*/ + /* 1.基础色:此处的颜色变量为所有组件应用到的颜色,此处颜色仅在公共色中使用,具体组件不可使用*/ + /* 1.1品牌色*/ + /* 品牌主色*/ + --ti-base-color-brand-6: #6cbfff; /* 主色蓝*/ + /* 主色衍生色*/ + --ti-base-color-brand-8: #0f6999; /* 品牌色-8*/ + --ti-base-color-brand-7: #4ea6e6; /* 品牌色-7*/ + --ti-base-color-brand-5: #85caff; /* 品牌色-5*/ + --ti-base-color-brand-4: #9ed5ff; /* 品牌色-4*/ + --ti-base-color-brand-3: #b8e0ff; /* 品牌色-3*/ + --ti-base-color-brand-2: #d1ebff; /* 品牌色-2*/ + --ti-base-color-brand-1: #ebf6ff; /* 品牌色-1*/ +} diff --git a/src/themes/theme-blue/build.less b/src/themes/theme-blue/build.less new file mode 100644 index 0000000..c343975 --- /dev/null +++ b/src/themes/theme-blue/build.less @@ -0,0 +1,5 @@ +/*注意:css var变量,以最后一次出现的生效。*/ +// 基础变量 +@import (less) '../basic/basic-var.css'; +// 基础变量 +@import 'basic-var.less'; diff --git a/src/themes/theme-default/build.less b/src/themes/theme-default/build.less new file mode 100644 index 0000000..7bad1b0 --- /dev/null +++ b/src/themes/theme-default/build.less @@ -0,0 +1,3 @@ +/*注意:css var变量,以最后一次出现的生效。*/ +// 基础变量 +@import (less) '../basic/basic-var.css'; diff --git a/src/themes/theme-green/basic-var.less b/src/themes/theme-green/basic-var.less new file mode 100644 index 0000000..6902b99 --- /dev/null +++ b/src/themes/theme-green/basic-var.less @@ -0,0 +1,16 @@ +/*基础变量定义*/ +:root { + /* 一.颜色*/ + /* 1.基础色:此处的颜色变量为所有组件应用到的颜色,此处颜色仅在公共色中使用,具体组件不可使用*/ + /* 1.1品牌色*/ + /* 品牌主色*/ + --ti-base-color-brand-6: #a6dd82; /* 主色绿*/ + /* 主色衍生色*/ + --ti-base-color-brand-8: #5e9629; /* 品牌色-8*/ + --ti-base-color-brand-7: #92cc68; /* 品牌色-7*/ + --ti-base-color-brand-5: #b3e890; /* 品牌色-5*/ + --ti-base-color-brand-4: #c5f2a7; /* 品牌色-4*/ + --ti-base-color-brand-3: #d8fcc0; /* 品牌色-3*/ + --ti-base-color-brand-2: #e5ffd4; /* 品牌色-2*/ + --ti-base-color-brand-1: #f0ffe6; /* 品牌色-1*/ +} diff --git a/src/themes/theme-green/build.less b/src/themes/theme-green/build.less new file mode 100644 index 0000000..c343975 --- /dev/null +++ b/src/themes/theme-green/build.less @@ -0,0 +1,5 @@ +/*注意:css var变量,以最后一次出现的生效。*/ +// 基础变量 +@import (less) '../basic/basic-var.css'; +// 基础变量 +@import 'basic-var.less'; diff --git a/src/themes/theme-purple/basic-var.less b/src/themes/theme-purple/basic-var.less new file mode 100644 index 0000000..454e8ed --- /dev/null +++ b/src/themes/theme-purple/basic-var.less @@ -0,0 +1,16 @@ +/*基础变量定义*/ +:root { + /* 一.颜色*/ + /* 1.基础色:此处的颜色变量为所有组件应用到的颜色,此处颜色仅在公共色中使用,具体组件不可使用*/ + /* 1.1品牌色*/ + /* 品牌主色*/ + --ti-base-color-brand-6: #a97af8; /* 主色紫*/ + /* 主色衍生色*/ + --ti-base-color-brand-8: #3f1a9c; /* 品牌色-8*/ + --ti-base-color-brand-7: #8a5ce0; /* 品牌色-7*/ + --ti-base-color-brand-5: #bc94ff; /* 品牌色-5*/ + --ti-base-color-brand-4: #caabff; /* 品牌色-4*/ + --ti-base-color-brand-3: #d8c2ff; /* 品牌色-3*/ + --ti-base-color-brand-2: #e7d9ff; /* 品牌色-2*/ + --ti-base-color-brand-1: #f5f0ff; /* 品牌色-1*/ +} diff --git a/src/themes/theme-purple/build.less b/src/themes/theme-purple/build.less new file mode 100644 index 0000000..c343975 --- /dev/null +++ b/src/themes/theme-purple/build.less @@ -0,0 +1,5 @@ +/*注意:css var变量,以最后一次出现的生效。*/ +// 基础变量 +@import (less) '../basic/basic-var.css'; +// 基础变量 +@import 'basic-var.less'; diff --git a/src/themes/theme-red/basic-var.less b/src/themes/theme-red/basic-var.less new file mode 100644 index 0000000..9dc0087 --- /dev/null +++ b/src/themes/theme-red/basic-var.less @@ -0,0 +1,16 @@ +/*基础变量定义*/ +:root { + /* 一.颜色*/ + /* 1.基础色:此处的颜色变量为所有组件应用到的颜色,此处颜色仅在公共色中使用,具体组件不可使用*/ + /* 1.1品牌色*/ + /* 品牌主色*/ + --ti-base-color-brand-6: #f3689a; /* 主色红*/ + /* 主色衍生色*/ + --ti-base-color-brand-8: #96114d; /* 品牌色-8*/ + --ti-base-color-brand-7: #db4d83; /* 品牌色-7*/ + --ti-base-color-brand-5: #fc86b0; /* 品牌色-5*/ + --ti-base-color-brand-4: #ffa1c2; /* 品牌色-4*/ + --ti-base-color-brand-3: #ffbad2; /* 品牌色-3*/ + --ti-base-color-brand-2: #ffd4e3; /* 品牌色-2*/ + --ti-base-color-brand-1: #ffedf3; /* 品牌色-1*/ +} diff --git a/src/themes/theme-red/build.less b/src/themes/theme-red/build.less new file mode 100644 index 0000000..c343975 --- /dev/null +++ b/src/themes/theme-red/build.less @@ -0,0 +1,5 @@ +/*注意:css var变量,以最后一次出现的生效。*/ +// 基础变量 +@import (less) '../basic/basic-var.css'; +// 基础变量 +@import 'basic-var.less'; diff --git a/src/time/demo/karma.conf.js b/src/time/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/time/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/time/demo/project.json b/src/time/demo/project.json new file mode 100644 index 0000000..2ea0648 --- /dev/null +++ b/src/time/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/time/demo", + "sourceRoot": "src/time/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/time", + "index": "src/time/demo/src/index.html", + "main": "src/time/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/time/demo/tsconfig.app.json", + "assets": ["src/time/demo/src/favicon.ico", "src/time/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "time-demo:build:production" + }, + "development": { + "browserTarget": "time-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js time" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/time/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/time/demo/tsconfig.spec.json", + "karmaConfig": "src/time/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/time/demo/src/app/AppComponent.ts b/src/time/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/time/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/time/demo/src/app/AppModule.ts b/src/time/demo/src/app/AppModule.ts new file mode 100644 index 0000000..1771188 --- /dev/null +++ b/src/time/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TimeTestModule } from './time/TimeTestModule'; + +@NgModule({ + imports: [ + TimeTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/time/demo/src/app/IndexComponent.ts b/src/time/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..7c186a4 --- /dev/null +++ b/src/time/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TimeTestModule } from './time/TimeTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TimeTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/time/demo/src/app/app.html b/src/time/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/time/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/time/demo/src/app/time/TimeCleariconComponent.ts b/src/time/demo/src/app/time/TimeCleariconComponent.ts new file mode 100644 index 0000000..08f04ac --- /dev/null +++ b/src/time/demo/src/app/time/TimeCleariconComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './time-clearicon.html' +}) +export class TimeCleariconComponent { + timeValue: string = '10:10:10'; +} diff --git a/src/time/demo/src/app/time/TimeDisabledComponent.ts b/src/time/demo/src/app/time/TimeDisabledComponent.ts new file mode 100644 index 0000000..02a3e3e --- /dev/null +++ b/src/time/demo/src/app/time/TimeDisabledComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './time-disabled.html' +}) +export class TimeDisabledComponent { + timeValue: string = '8:23:27'; +} diff --git a/src/time/demo/src/app/time/TimeEventComponent.ts b/src/time/demo/src/app/time/TimeEventComponent.ts new file mode 100644 index 0000000..5feb8b1 --- /dev/null +++ b/src/time/demo/src/app/time/TimeEventComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './time-event.html' +}) +export class TimeEventComponent { + myLogs: Array = []; + timeValue: string = ''; + + ngModelChangeFn($event: any): void { + this.myLogs = [...this.myLogs, `ngModelChange model=${$event}`]; + } +} diff --git a/src/time/demo/src/app/time/TimeFormatComponent.ts b/src/time/demo/src/app/time/TimeFormatComponent.ts new file mode 100644 index 0000000..5ffd6e7 --- /dev/null +++ b/src/time/demo/src/app/time/TimeFormatComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './time-format.html' +}) +export class TimeFormatComponent { + timeValue: string = '8:23:27'; + timeValue1: string = '8:23:27'; + timeValue2: string = '8:23:27'; + timeValue3: string = '8:23:27'; + format: string = 'HH:mm:ss'; + format1: string = 'HH:mm'; + format2: string = 'HH'; + format3: string = 'mm:ss'; +} diff --git a/src/time/demo/src/app/time/TimeMaxComponent.ts b/src/time/demo/src/app/time/TimeMaxComponent.ts new file mode 100644 index 0000000..3195290 --- /dev/null +++ b/src/time/demo/src/app/time/TimeMaxComponent.ts @@ -0,0 +1,40 @@ +import { Component, OnInit } from '@angular/core'; +import { AbstractControl, FormBuilder, FormControl, FormGroup } from '@angular/forms'; +@Component({ + templateUrl: './time-max.html' +}) +export class TimeMaxComponent implements OnInit { + timeValue: string = 'sss'; + disabled: boolean = false; + timeId: string = 'timeId'; + min: string = '8:23:27'; + max: string = '21:45:47'; + format: string = 'HH:mm:ss'; + timeForm: FormGroup; + timeBaseValue: string = '18:22:00'; + constructor(private fb: FormBuilder) {} + ngOnInit(): void { + this.timeForm = this.fb.group({ + timeValue: { value: this.timeBaseValue, disabled: this.disabled }, + text1Value: new FormControl(this.timeBaseValue), + text2Value: '1111' + }); + this.reactiveFormTimeValueChange(); + } + changeMaxValue(): void { + this.max = '9:10:21'; + } + changeDisable(): void { + this.disabled = !this.disabled; + } + // 响应式表单 对value的监控 + reactiveFormTimeValueChange(): void { + const spinnerValueControl: AbstractControl = this.timeForm.get('timeValue'); + spinnerValueControl.valueChanges.subscribe((value: string) => { + console.log(value); + this.timeForm.patchValue({ + text1Value: value + }); + }); + } +} diff --git a/src/time/demo/src/app/time/TimeMaxminComponent.ts b/src/time/demo/src/app/time/TimeMaxminComponent.ts new file mode 100644 index 0000000..cae6832 --- /dev/null +++ b/src/time/demo/src/app/time/TimeMaxminComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './time-maxmin.html' +}) +export class TimeMaxminComponent { + timeValue: string; + timeValue1: string; + max: string = '21:45:00'; + min: string = '21:45:00'; + format: string = 'HH:mm:ss'; +} diff --git a/src/time/demo/src/app/time/TimeMinComponent.ts b/src/time/demo/src/app/time/TimeMinComponent.ts new file mode 100644 index 0000000..ab55a77 --- /dev/null +++ b/src/time/demo/src/app/time/TimeMinComponent.ts @@ -0,0 +1,43 @@ +import { Component, OnInit } from '@angular/core'; +import { AbstractControl, FormBuilder, FormControl, FormGroup } from '@angular/forms'; +@Component({ + templateUrl: './time-min.html' +}) +export class TimeMinComponent implements OnInit { + timeValue: string = '9:12:45'; + min: string = '8:23:27'; + max: string = '21:45:47'; + format: string = 'HH:mm:ss'; + disabled: boolean = false; + timeForm: FormGroup; + timeBaseValue: string = '18:22:00'; + constructor(private fb: FormBuilder) {} + ngOnInit(): void { + this.timeForm = this.fb.group({ + timeValue: { value: this.timeBaseValue, disabled: this.disabled }, + text1Value: new FormControl(this.timeBaseValue), + text2Value: '1111' + }); + this.reactiveFormTimeValueChange(); + } + changeMinValue(): void { + this.min = '6:12:13'; + } + // 响应式表单 对value的监控 + reactiveFormTimeValueChange(): void { + const spinnerValueControl: AbstractControl = this.timeForm.get('timeValue'); + spinnerValueControl.valueChanges.subscribe((value: string) => { + console.log(value); + this.timeForm.patchValue({ + text1Value: value + }); + }); + } + + // 改变time禁用装态(动态表单形式) + changeFormGroupDisable(): void { + this.disabled = !this.disabled; + this.timeForm.controls['timeValue'].disable({ onlySelf: this.disabled }); + this.timeForm.controls['text1Value'].disable(); + } +} diff --git a/src/time/demo/src/app/time/TimeOptionDisabledComponent.ts b/src/time/demo/src/app/time/TimeOptionDisabledComponent.ts new file mode 100644 index 0000000..2d2c8d6 --- /dev/null +++ b/src/time/demo/src/app/time/TimeOptionDisabledComponent.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './time-option-disabled.html' +}) +export class TimeOptionDisabledComponent { + timeValue: string = ''; + timeValue1: string = ''; + timeValue2: string = ''; + timeValue3: string = ''; + timeValue4: string = ''; + timeValue5: string = ''; + min: string = '8:23:27'; + max: string = '21:56:47'; + min1: string = '8:23:27'; + max1: string = '8:56:47'; + min2: string = '8:23:27'; + max2: string = '8:23:47'; + + changeMaxMinValue(): void { + this.max2 = '23:59:59'; + this.min2 = '00:00:00'; + } +} diff --git a/src/time/demo/src/app/time/TimePanelalignComponent.ts b/src/time/demo/src/app/time/TimePanelalignComponent.ts new file mode 100644 index 0000000..b5a83e9 --- /dev/null +++ b/src/time/demo/src/app/time/TimePanelalignComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './time-panelalign.html' +}) +export class TimePanelalignComponent { + timeValue: string = '10:10:10'; +} diff --git a/src/time/demo/src/app/time/TimeReactiveComponent.ts b/src/time/demo/src/app/time/TimeReactiveComponent.ts new file mode 100644 index 0000000..67dcf23 --- /dev/null +++ b/src/time/demo/src/app/time/TimeReactiveComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; + +@Component({ + templateUrl: './time-reactive.html' +}) +export class TimeReactiveComponent { + timeValue: string = ''; + timeValue1: string = ''; + format: string = 'HH:mm:ss'; + format1: string = 'HH:mm'; + timeGroup = new FormGroup({ + timeForm: new FormControl('timeValue'), + timeForm1: new FormControl('timeValue1') + }); +} diff --git a/src/time/demo/src/app/time/TimeTestModule.ts b/src/time/demo/src/app/time/TimeTestModule.ts new file mode 100644 index 0000000..2164380 --- /dev/null +++ b/src/time/demo/src/app/time/TimeTestModule.ts @@ -0,0 +1,94 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TiButtonModule, TiTimeModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TimeValidationComponent } from './TimeValidationComponent'; +import { TimeFormatComponent } from './TimeFormatComponent'; +import { TimeMaxminComponent } from './TimeMaxminComponent'; +import { TimeEventComponent } from './TimeEventComponent'; +import { TimeCleariconComponent } from './TimeCleariconComponent'; +import { TimeOptionDisabledComponent } from './TimeOptionDisabledComponent'; +import { TimePanelalignComponent } from './TimePanelalignComponent'; +import { TimeReactiveComponent } from './TimeReactiveComponent'; +import { TimeDisabledComponent } from './TimeDisabledComponent'; +import { TimeMaxComponent } from './TimeMaxComponent'; +import { TimeMinComponent } from './TimeMinComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TiValidationModule, + TiButtonModule, + TiTimeModule, + DemoLogModule, + RouterModule.forChild(TimeTestModule.ROUTES) + ], + declarations: [ + TimeFormatComponent, + TimeMaxminComponent, + TimeValidationComponent, + TimeEventComponent, + TimeCleariconComponent, + TimeOptionDisabledComponent, + TimePanelalignComponent, + TimeReactiveComponent, + TimeDisabledComponent, + TimeMaxComponent, + TimeMinComponent + ] +}) +export class TimeTestModule { + static readonly LINKS: Array = [{ href: 'components/TiTimeComponent.html', label: 'Time' }]; + static readonly ROUTES: Routes = [ + { + path: 'time/time-format', + component: TimeFormatComponent + }, + { + path: 'time/time-maxmin', + component: TimeMaxminComponent + }, + { + path: 'time/time-validation', + component: TimeValidationComponent + }, + { + path: 'time/time-event', + component: TimeEventComponent + }, + { + path: 'time/time-disabled', + component: TimeDisabledComponent + }, + { + path: 'time/time-clearicon', + component: TimeCleariconComponent + }, + { + path: 'time/time-panelalign', + component: TimePanelalignComponent + }, + { + path: 'time/time-option-disabled', + component: TimeOptionDisabledComponent + }, + { + path: 'time/time-reactive', + component: TimeReactiveComponent + }, + { + path: 'time/time-max', + component: TimeMaxComponent + }, + { + path: 'time/time-min', + component: TimeMinComponent + } + ]; +} diff --git a/src/time/demo/src/app/time/TimeValidationComponent.ts b/src/time/demo/src/app/time/TimeValidationComponent.ts new file mode 100644 index 0000000..a2c8661 --- /dev/null +++ b/src/time/demo/src/app/time/TimeValidationComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './time-validation.html' +}) +export class TimeValidationComponent { + timeValue: string = ''; +} diff --git a/src/time/demo/src/app/time/time-clearicon.html b/src/time/demo/src/app/time/time-clearicon.html new file mode 100644 index 0000000..d604fb1 --- /dev/null +++ b/src/time/demo/src/app/time/time-clearicon.html @@ -0,0 +1 @@ + diff --git a/src/time/demo/src/app/time/time-disabled.html b/src/time/demo/src/app/time/time-disabled.html new file mode 100644 index 0000000..0f6963a --- /dev/null +++ b/src/time/demo/src/app/time/time-disabled.html @@ -0,0 +1 @@ + diff --git a/src/time/demo/src/app/time/time-event.html b/src/time/demo/src/app/time/time-event.html new file mode 100644 index 0000000..ae4e380 --- /dev/null +++ b/src/time/demo/src/app/time/time-event.html @@ -0,0 +1,3 @@ + +
    + diff --git a/src/time/demo/src/app/time/time-format.html b/src/time/demo/src/app/time/time-format.html new file mode 100644 index 0000000..eda7cd3 --- /dev/null +++ b/src/time/demo/src/app/time/time-format.html @@ -0,0 +1,10 @@ + +
    +
    + +
    +
    + +
    +
    + diff --git a/src/time/demo/src/app/time/time-max.html b/src/time/demo/src/app/time/time-max.html new file mode 100644 index 0000000..34015aa --- /dev/null +++ b/src/time/demo/src/app/time/time-max.html @@ -0,0 +1,16 @@ +

    1.描述

    +

    max接口测试

    +

    2.示例

    +

    (2.1)max设置为合法时间时,以该值为最大值

    +
    +
    + +
    + +

    (2.2)动态变更

    +
    + +
    +
    + + diff --git a/src/time/demo/src/app/time/time-maxmin.html b/src/time/demo/src/app/time/time-maxmin.html new file mode 100644 index 0000000..fe6c860 --- /dev/null +++ b/src/time/demo/src/app/time/time-maxmin.html @@ -0,0 +1,4 @@ + +
    +
    + diff --git a/src/time/demo/src/app/time/time-min.html b/src/time/demo/src/app/time/time-min.html new file mode 100644 index 0000000..8821088 --- /dev/null +++ b/src/time/demo/src/app/time/time-min.html @@ -0,0 +1,17 @@ +

    1.描述

    +

    min接口测试

    +

    2.示例

    +

    (2.1)min设置为合法时间时,以该值为最小值

    +
    + +
    +
    +
    +
    +

    (2.2)动态变更

    +
    + +
    +
    + + diff --git a/src/time/demo/src/app/time/time-option-disabled.html b/src/time/demo/src/app/time/time-option-disabled.html new file mode 100644 index 0000000..946fcd4 --- /dev/null +++ b/src/time/demo/src/app/time/time-option-disabled.html @@ -0,0 +1,21 @@ +

    描述

    +

    时间面板时分秒禁用场景测试

    +

    示例

    +

    1.不设置最大最小值:

    + +

    +

    2.只设置最大值:

    + +

    +

    3.只设置最小值:

    + +

    +

    4.最大最小值都设置:

    + +

    +

    5.最大时与最小时相同:

    + +

    +

    6.最大时分与最小时分相同:

    + + diff --git a/src/time/demo/src/app/time/time-panelalign.html b/src/time/demo/src/app/time/time-panelalign.html new file mode 100644 index 0000000..f8bf9c0 --- /dev/null +++ b/src/time/demo/src/app/time/time-panelalign.html @@ -0,0 +1 @@ + diff --git a/src/time/demo/src/app/time/time-reactive.html b/src/time/demo/src/app/time/time-reactive.html new file mode 100644 index 0000000..dc2873e --- /dev/null +++ b/src/time/demo/src/app/time/time-reactive.html @@ -0,0 +1,6 @@ +
    + +
    +
    + +
    diff --git a/src/time/demo/src/app/time/time-validation.html b/src/time/demo/src/app/time/time-validation.html new file mode 100644 index 0000000..8447182 --- /dev/null +++ b/src/time/demo/src/app/time/time-validation.html @@ -0,0 +1 @@ + diff --git a/src/time/demo/src/app/time/webdoc/time-demos.js b/src/time/demo/src/app/time/webdoc/time-demos.js new file mode 100644 index 0000000..21bf4f3 --- /dev/null +++ b/src/time/demo/src/app/time/webdoc/time-demos.js @@ -0,0 +1,100 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'time-format', + name: { + 'zh-CN': '显示格式', + 'en-US': 'format' + }, + desc: { + 'zh-CN': '

    通过属性format配置时间显示的格式。包括 HH:mm:ss、HH:mm、HH、mm:ss 四种格式。

    ', + 'en-US': '

    format

    ' + }, + apis: ['TiTimeComponent.properties.format'] + }, + { + demoId: 'time-maxmin', + name: { + 'zh-CN': '预设范围', + 'en-US': 'maxmin' + }, + desc: { + 'zh-CN': + '

    通过属性max配置可以选择或输入的合法的最大时间。通过属性min配置可以选择或输入的合法的最小时间。

    ', + 'en-US': '

    maxmin

    ' + }, + apis: ['TiTimeComponent.properties.max', 'TiTimeComponent.properties.min'] + }, + { + demoId: 'time-clearicon', + name: { + 'zh-CN': '清除功能', + 'en-US': 'clear' + }, + desc: { + 'zh-CN': '

    通过属性clearIcon配置是否存在清除功能。

    ', + 'en-US': '

    clear

    ' + }, + apis: ['TiTimeComponent.properties.clearIcon'] + }, + { + demoId: 'time-panelalign', + name: { + 'zh-CN': '面板对齐方式', + 'en-US': 'panelalign' + }, + desc: { + 'zh-CN': + '

    通过属性panelAlign配置时间下拉面板与时间框的对齐方式,包括left(默认)、right

    ', + 'en-US': '

    panelalign

    ' + }, + apis: ['TiTimeComponent.properties.panelAlign'] + }, + { + demoId: 'time-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event' + }, + desc: { + 'zh-CN': '

    当时间框内值改变的时候触发ngModelChange事件。

    ', + 'en-US': '

    event

    ' + } + }, + { + demoId: 'time-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled' + }, + desc: { + 'zh-CN': '

    通过属性disabled配置是否为禁用状态。

    ', + 'en-US': 'disabled' + } + }, + { + demoId: 'time-reactive', + name: { + 'zh-CN': '响应式表单', + 'en-US': 'reactive' + }, + desc: { + 'zh-CN': '

    响应式表单中时间选择器的最简用法。

    ', + 'en-US': '

    time-reactive

    ' + } + }, + { + demoId: 'time-validation', + name: { + 'zh-CN': '校验', + 'en-US': 'validation' + }, + desc: { + 'zh-CN': '

    通过指令tiValidation实现校验。

    ', + 'en-US': '

    validation

    ' + } + } + ] +}; diff --git a/src/time/demo/src/app/time/webdoc/time.cn.md b/src/time/demo/src/app/time/webdoc/time.cn.md new file mode 100644 index 0000000..5013e73 --- /dev/null +++ b/src/time/demo/src/app/time/webdoc/time.cn.md @@ -0,0 +1,23 @@ +--- +title: Time 时间 +--- +# Time 时间 + +
    + +Time 是输入或者选择时间的控件。   + +```typescript +import { TiTimeModule } from '@opentiny/ng'; +``` + +
    + +
    + +Time 是输入或者选择时间的控件。   + +```typescript +import { TiTimeModule } from '@opentiny/ng'; +``` +
    \ No newline at end of file diff --git a/src/time/demo/src/app/time/webdoc/time.en.md b/src/time/demo/src/app/time/webdoc/time.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/time/demo/src/app/time/webdoc/time.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/time/demo/src/favicon.ico b/src/time/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/time/demo/src/index.html b/src/time/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/time/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/time/demo/src/main.ts b/src/time/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/time/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/time/demo/test.ts b/src/time/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/time/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/time/demo/tsconfig.app.json b/src/time/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/time/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/time/demo/tsconfig.spec.json b/src/time/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/time/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/time/lib/index.ts b/src/time/lib/index.ts new file mode 100644 index 0000000..7f54dcb --- /dev/null +++ b/src/time/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTimeModule'; diff --git a/src/time/lib/ng-package.json b/src/time/lib/ng-package.json new file mode 100644 index 0000000..5802f0c --- /dev/null +++ b/src/time/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/time", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/time/lib/package.json b/src/time/lib/package.json new file mode 100644 index 0000000..4bc7df6 --- /dev/null +++ b/src/time/lib/package.json @@ -0,0 +1,18 @@ +{ + "name": "@opentiny/ng-time", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-datedominator": "~1.0.0-beta.0", + "@opentiny/ng-list": "~1.0.0-beta.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-button": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/time/lib/project.json b/src/time/lib/project.json new file mode 100644 index 0000000..7c99349 --- /dev/null +++ b/src/time/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/time/lib", + "sourceRoot": "src/time/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/time"], + "options": { + "project": "src/time/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/time"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js time" + }, + { + "command": "ng default-build time" + }, + { + "command": "node build/clear-default-theme.js time" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/time && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build time && ng pack time && node build/publish.js time --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/time/lib/src/TiTimeComponent.ts b/src/time/lib/src/TiTimeComponent.ts new file mode 100644 index 0000000..9051d82 --- /dev/null +++ b/src/time/lib/src/TiTimeComponent.ts @@ -0,0 +1,943 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + HostListener, + Inject, + Input, + NgZone, + Renderer2, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { TiDateUtil, TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiLocaleFormat } from '@opentiny/ng-locale'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiDropComponent } from '@opentiny/ng-drop'; +import { DOCUMENT } from '@angular/common'; +import { TiListComponent } from '@opentiny/ng-list'; +import packageInfo from '../package.json'; +/** + * @ignore + */ +export interface TimeField { + hour: string; + minute: string; + second: string; +} + +/** + * @ignore + */ +export interface TiComputingParams { + max: number; + min: number; + needAddZero: boolean; +} + +/** + * Time时间组件 + * + * Time组件提供了一种方便的显示和设置时间的方式。 + * + */ +@Component({ + selector: 'ti-time', + templateUrl: './time.html', + styleUrls: ['./time.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiTimeComponent)], + host: { + '[class.ti3-time-container]': 'true', + '(blur)': 'onBlur()' + } +}) +export class TiTimeComponent extends TiFormComponent { + /** + * 时间显示格式。各个时间段格式规则; 默认值:'HH:mm:ss' + * + * 1.小时可以设置为: + * + * HH —— 24 小时制,两位数字表示小时(00-23) + * + * H —— 24 小时制,开头不补零数字表示小时(0-23) + * + * 2.分钟可以设置为: + * + * mm —— 两位数字表示分钟值(00-59) + * + * m —— 开头不补零数字表示分钟值(0-59) + * + * 3.秒可以设置为: + * + * ss —— 两位数字表示秒值(00-59) + * + * s —— 开头不补零数字表示秒值(0-59) + * + * 说明:开头补零是指当前时间是个位数字时,前边补零,始终保持两位数字 + */ + @Input() format: string; + /** + * 时间最大值,默认值:'23:59:59' + */ + @Input() max: string; + /** + * 时间最小值,默认值:'00:00:00' + */ + @Input() min: string; + /** + * 是否展示清除时间图标(默认显示) + */ + @Input() clearIcon: boolean = true; + /** + * 面板对齐方式 + */ + @Input() panelAlign: 'left' | 'right' = 'left'; + /** + * @ignore + */ + @ViewChild('dominator', { static: true }) private containerRef: ElementRef; + /** + * @ignore + */ + @ViewChild(TiDropComponent, { static: true }) dropCom: TiDropComponent; + /** + * @ignore + */ + @ViewChild('text', { static: true }) textCom: ElementRef; + /** + * @ignore + */ + @ViewChild('input', { static: true }) inputEle: ElementRef; + /** + * @ignore + */ + @ViewChild('button', { static: true }) buttonEle: ElementRef; + /** + * @ignore + * time编辑框内部value值 + */ + public inputValue: string = ''; + /** + * @ignore + * dominator最终显示时间值 + */ + public timeValue: string = ''; + /** + * @ignore + */ + public dominatorCom: ElementRef; + /** + * @ignore + * 时间下拉面板宽度,根据时间格式宽度各异 + */ + public timePanelWidth: number = 284; + /** + * @ignore + * 底部确认按钮是否禁用 + */ + public buttonDisabled: boolean = true; + /** + * @ignore + * placeholder提示文本 + */ + public placeholder: string; + /** + * @ignore + */ + public hourOptions: Array; + /** + * @ignore + */ + public minuteOptions: Array; + /** + * @ignore + */ + public secondOptions: Array; + /** + * @ignore + */ + public selectedHour: any; + /** + * @ignore + */ + public selectedMinute: any; + /** + * @ignore + */ + public selectedSecond: any; + /** + * @ignore + */ + public hourScroll: number; + /** + * @ignore + */ + public minuteScroll: number; + /** + * @ignore + */ + public secondScroll: number; + /** + * @ignore + * 只展示小时段 + */ + public onlyHour: boolean; + /** + * @ignore + * 只展示时分段 + */ + public onlyHourMinute: boolean; + /** + * @ignore + * 只展示分秒段 + */ + public onlyMinuteSecond: boolean; + /** + * @ignore + */ + public oldInputValue: string = ''; + /** + * @ignore + * 面板与dominator的距离 + */ + public dominatorSpace: string = '-30px'; + /** + * @ignore + * 是否清除 + */ + public isClearClick: boolean; + protected versionInfo: string = super.getVersion(packageInfo); + // 默认最大最小值 + private config: { min: string; max: string } = { + min: '00:00:00', + max: '23:59:59' + }; + private documentKeydownListener: () => void; + + constructor( + protected hostRef: ElementRef, + protected renderer2: Renderer2, + protected changeDetectorRef: ChangeDetectorRef, + private zone: NgZone, + @Inject(DOCUMENT) private document + ) { + super(hostRef, renderer2, changeDetectorRef); + } + + /** + * @description: 若时间字符串没有一个冒号时处理成合法的事件字符串(加冒号) + * @param value 时间字符串 + */ + private static addColon(value: any): string { + // new Date()时,时间中没有一个冒号得到的是非法时间 + if (value.match(/:/)) { + return value; + } + let ampm: string = value.match(/am|AM|pm|PM/); + ampm = ampm || ''; + + return `${parseInt(value, 10)}: ${ampm}`; + } + + /** + * @description 判读字符串是否是合法的时间 + * @param: time: 校验的时间 + */ + private static isValidTime(time: string): boolean { + // value非字符串或者为空字符串时,为非法时间 + if (!Util.isString(time) || time.trim() === '') { + return false; + } + + const date: any = new Date(`2018/5/15 ${TiTimeComponent.addColon(time)}`); + + // any类型是为了防止编译报错 Date类型不能和string类型比较 + return !(String(date) === 'Invalid Date'); + } + + /** + * @description: hh/HH/mm/ss 时间格式时,显示的值处理成2位,不足2位补0 + * @param: num : 各个时间显示框的数值 + * @param: length : 需要处理之后的长度 + */ + private static addZero(num: number, length: number): string { + const zeroNum: string = `00${num}`; + + return zeroNum.substr(zeroNum.length - length, length); + } + /** + * @description newVal参数为合法值时返回newVal,否则返回defaultValue参数 + * @param: newVal:新值 + * @param: defaultValue:默认值 + */ + private static verifyTime(newVal: string, defaultValue: string): string { + return TiTimeComponent.isValidTime(newVal) ? newVal : defaultValue; + } + /** + * @description 比较value1和value2两个时间值大小.value1 < value2时,返回true;否则返回false ; + * @param: value1:比较的前一个值 + * @param: value2:比较的后一个值 + */ + private static isSmaller(value1: string, value2: string): boolean { + return Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value1)}`) < Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value2)}`); + } + + /** + * @description 比较value1和value2两个时间值大小.value1 > value2时,返回true;否则返回false ; + * @param: value1:比较的前一个值 + * @param: value2:比较的后一个值 + */ + private static isBigger(value1: string, value2: string): boolean { + return Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value1)}`) > Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value2)}`); + } + + /** + * @description 比较value1和value2两个时间值大小.value1 <= value2时,返回true;否则返回false ; + * @param: value1:比较的前一个值 + * @param: value2:比较的后一个值 + */ + private static isSmallerOrEqual(value1: string, value2: string): boolean { + return Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value1)}`) <= Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value2)}`); + } + + /** + * @description 比较value1和value2两个时间值大小.value1 >= value2时,返回true;否则返回false ; + * @param: value1:比较的前一个值 + * @param: value2:比较的后一个值 + */ + private static isBiggerOrEqual(value1: string, value2: string): boolean { + return Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value1)}`) >= Date.parse(`2018/5/15 ${TiTimeComponent.addColon(value2)}`); + } + + // 组件声明周期钩子--start + ngOnInit(): void { + super.ngOnInit(); + // 初始化变量 + this.initVariable(); + + this.showTimeWithFormat(); + + // 最大最小值校验 + this.validateMaxAndMin(); + + this.setTimeOptions(); + + // 时间框提示文本按照小写显示 + this.placeholder = this.format.toLowerCase(); + + this.zone.runOutsideAngular(() => { + // 避免不停触发变化检测 + // document上的Ecs快捷键功能 + this.documentKeydownListener = this.renderer2.listen(this.document, 'keydown', this.keydownHandlerFn); + }); + } + + private setTimeOptions(): void { + this.hourOptions = this.setOptions(24, 'hour'); + this.minuteOptions = this.setOptions(60, 'minute'); + this.secondOptions = this.setOptions(60, 'second'); + } + // 根据格式format判断面板展示情况 + private showTimeWithFormat(): void { + // 纯小时 + if (this.format.indexOf('m') === -1) { + this.onlyHour = true; + } else if (this.format.indexOf('s') === -1) { + // 仅时分 + this.onlyHourMinute = true; + } else if (this.format.indexOf('H') === -1) { + // 仅分秒 + this.onlyMinuteSecond = true; + } + } + + private initVariable(): void { + // 1. 时间格式校验 + this.format = Util.isString(this.format) ? this.format : TiDateUtil.DEFAULT_TIME_FORMAT; + } + + private validateMaxAndMin(): void { + this.config.max = this.formatTime(this.config.max); + this.config.min = this.formatTime(this.config.min); + // 最大值合法性校验 + this.max = TiTimeComponent.verifyTime(this.formatTime(this.max), this.config.max); + // 最小值合法性校验 + this.min = TiTimeComponent.verifyTime(this.formatTime(this.min), this.config.min); + // 最大最小值矛盾时,设置为默认值 + if (TiTimeComponent.isSmaller(this.max, this.min)) { + this.min = this.config.min; + this.max = this.config.max; + } + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + // 1.0 minValue监控 + if (changes['min'] && !changes['min'].isFirstChange()) { + this.min = this.formatTime(changes['min'].currentValue); + // 新minValue值非法时,恢复到之前值 + if (!this.isValidMinValue(this.min)) { + this.min = changes['min'].previousValue; + + return; + } + // 对value值进行最小值校验 + // setTimeout不能去掉,在onpush环境没问题,但是在default环境会报错 + // (ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked) + setTimeout(() => { + const model: string = this.addHour(this.model); + if (TiTimeComponent.isValidTime(model) && TiTimeComponent.isSmaller(model, this.min)) { + this.model = this.min; + this.formatValue(); + } + this.setDisableData(true); + this.changeDetectorRef.markForCheck(); + }, 0); + } + // 2.0 maxValue监控 + if (changes['max'] && !changes['max'].isFirstChange()) { + this.max = this.formatTime(changes['max'].currentValue); + // 新maxValue值非法时,恢复到之前值 + if (!this.isValidMaxValue(this.max)) { + this.max = changes['max'].previousValue; + + return; + } + // 对value值进行最小值校验 + // setTimeout不能去掉,在onpush环境没问题,但是在default环境会报错 + // (ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked) + setTimeout(() => { + const model: string = this.addHour(this.model); + if (TiTimeComponent.isValidTime(model) && TiTimeComponent.isBigger(model, this.max)) { + this.model = this.max; + this.formatValue(); + } + this.setDisableData(true); + this.changeDetectorRef.markForCheck(); + }, 0); + } + // 3.0 format监控 + if (changes['format'] && !changes['format'].isFirstChange()) { + // 新format值非法时,恢复到之前值 + if (!Util.isString(changes['format'].currentValue)) { + this.format = changes['format'].previousValue; + + return; + } + this.showTimeWithFormat(); + if (TiTimeComponent.isValidTime(this.addHour(this.model))) { + this.formatValue(); + } + } + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + this.dominatorCom = this.containerRef.nativeElement; + this.setFocusableElems( + [this.dominatorCom] + .concat(this.dropCom.nativeElement) + .concat(this.textCom.nativeElement) + .concat(this.inputEle.nativeElement) + .concat(this.buttonEle.nativeElement) + ); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + if (this.documentKeydownListener) { + this.documentKeydownListener(); + } + } + // 组件声明周期钩子--end + + // 实现ControlValueAccessor接口 + /** + * @ignore + */ + writeValue(value: any): void { + super.writeValue(value); + if (value === '' || !this.isValidValue(value)) { + this.model = ''; + this.buttonDisabled = true; + if (this.timeValue) { + this.timeValue = ''; + this.inputValue = ''; + this.oldInputValue = ''; + this.setSelectVal(); + } + + this.setDisableData(); + + return; + } + this.buttonDisabled = false; + this.model = value; + this.formatValue(); + this.setDisableData(); + } + // 实现ControlValueAccessor接口--end + /** + * @ignore + * @description: 组件整体失焦之后处理函数 + */ + public onBlur(): void { + // 失焦后下拉关闭 + this.hidePanel(); + this.inputValue = this.model; + this.setDisableData(); + this.setSelectVal(); + this.getScrollData(); + this.oldInputValue = this.inputValue; + } + // 面板延迟200ms关闭,用户输入新值之后,直接点击确认按钮关闭面板时, + // 面板内部选中项先刷新 再关闭 200ms的延迟 用户可以看到这样一个视觉的过程 + // 200ms通过本地用例,跟规范侧共同决定的一个延时数值 + private hidePanel(): void { + setTimeout(() => { + this.dropCom.hide(); + // 初始化数据,保证有时间值时,初次打开数据定位在顶部 + this.hourScroll = -1; + this.minuteScroll = -1; + this.secondScroll = -1; + }, 200); + } + /** + * @ignore + * 点击dominator打开面板 + */ + public onShowClick(): void { + if (this.disabled) { + return; + } + if (this.isClearClick) { + this.isClearClick = false; + return; + } + this.getScrollData(); + this.dropCom.show(); + this.textCom.nativeElement.focus(); + this.buttonDisabled = !this.model; + } + /** + * @ignore + * 点击确认按钮关闭面板 + */ + public okClickFn(): void { + this.timeValue = this.inputValue; + this.model = this.inputValue; + // 记录旧值,当输入非法时,返回之前合法值 + this.oldInputValue = this.inputValue; + this.containerRef.nativeElement.focus(); + this.hidePanel(); + } + /** + * @ignore + * 时间面板点击选择事件 + * @param val + * @param title + */ + public onSelect(val: any, title: string): void { + // 禁用状态 + if (val.disabled) { + return; + } + let arr: Array = []; + if (this.onlyHour) { + arr = ['00']; + } else if (this.onlyHourMinute) { + arr = ['00', '00']; + } else { + arr = ['00', '00', '00']; + } + const timeArr: Array = this.inputValue === '' ? arr : this.addHour(this.inputValue).split(':'); + switch (title) { + case 'hour': + timeArr[0] = val.label; + timeArr[1] = timeArr[1] ? timeArr[1] : '00'; + timeArr[2] = timeArr[2] ? timeArr[2] : '00'; + this.hourScroll = val.label * 30; + break; + case 'minute': + timeArr[1] = val.label; + timeArr[2] = timeArr[2] ? timeArr[2] : '00'; + this.minuteScroll = val.label * 30; + break; + case 'second': + timeArr[2] = val.label; + this.secondScroll = val.label * 30; + break; + default: + break; + } + + // 仅有分秒时,去除时 + if (this.onlyMinuteSecond) { + timeArr.shift(); + } + + this.inputValue = timeArr.join(':'); + this.setDisableData(); + this.setSelectVal(); + this.oldInputValue = this.inputValue; + this.buttonDisabled = false; + } + + private setSelectVal(): void { + let value: string = this.addHour(this.inputValue); + + if (!TiTimeComponent.isValidTime(value)) { + this.selectedHour = null; + this.selectedMinute = null; + this.selectedSecond = null; + return; + } + const newtimeArr: Array = this.validateValue(value).split(':'); + const hourVal: string = TiTimeComponent.addZero(Number(newtimeArr[0]), 2); + const minuteVal: string = TiTimeComponent.addZero(Number(newtimeArr[1]), 2); + const secondVal: string = TiTimeComponent.addZero(Number(newtimeArr[2]), 2); + this.selectedHour = this.getSelectedVal(this.hourOptions, hourVal); + this.selectedMinute = this.getSelectedVal(this.minuteOptions, minuteVal); + this.selectedSecond = this.getSelectedVal(this.secondOptions, secondVal); + // 失焦之后,个位的数字需补零 + if (this.onlyHour) { + this.inputValue = hourVal; + } else if (this.onlyHourMinute) { + this.inputValue = hourVal + ':' + minuteVal; + } else if (this.onlyMinuteSecond) { + this.inputValue = minuteVal + ':' + secondVal; + } else { + this.inputValue = hourVal + ':' + minuteVal + ':' + secondVal; + } + this.getScrollData(); + } + + private keydownHandlerFn = (event: KeyboardEvent): void => { + if (event.keyCode === TiKeymap.KEY_ESCAPE) { + this.hidePanel(); + } + }; + /** + * @ignore + * 组件快捷键处理tab键 enter键 + */ + @HostListener('keydown', ['$event']) public onKeydown(event: KeyboardEvent): void { + switch (event.keyCode) { + case TiKeymap.KEY_ENTER: // ENTER键(大键盘) + case TiKeymap.KEY_NUMPAD_ENTER: // ENTER键(数字小键盘) + this.responseEnter(); + break; + default: + break; + } + } + /** + * @ignore + * enter键的功能:如果面板展开不处理,面板收起则展开,设置datePanel指令的接口值 + */ + public responseEnter(): void { + if (this.dropCom.isShow) { + return; + } + this.getScrollData(); + // 时间面板展开 + this.dropCom.show(); + this.textCom.nativeElement.focus(); + } + /** + * @ignore + * 输入为中文冒号时,自动转换为英文冒号 + * @param val + */ + public onInputChangeFn(val: any): void { + if (!Util.isEmptyString(val)) { + let value: string = val; + value = value.replace(':', ':'); + this.inputValue = value; + } else { + this.buttonDisabled = true; + } + } + + /** + * @ignore + * 时间框enter事件 + */ + public timeKeydownFn(event: KeyboardEvent, val: string): void { + if (event.keyCode === TiKeymap.KEY_ENTER || event.keyCode === TiKeymap.KEY_NUMPAD_ENTER) { + this.timeBlur(val); + } + } + + /** + * @ignore + * + */ + public timeBlur(val: string): void { + if (val === '') { + this.buttonDisabled = true; + this.inputValue = ''; + this.oldInputValue = this.inputValue; + this.getScrollData(); + + return; + } + + const timeArr: Array = val.split(TiDateUtil.COLON_REGEXP); + const formatArr: Array = this.format.split(':'); + const datetime: Date = new Date(`2022/01/28 ${TiDateUtil.addColon(this.addHour(val))}`); + // 用户输入与format不匹配或当前时间是非法值时,都按照非法字符处理 + if (timeArr.length !== formatArr.length || String(datetime) === 'Invalid Date') { + this.inputValue = this.oldInputValue; + this.buttonDisabled = Util.isEmptyString(this.inputValue); + + return; + } + + // 时分秒是负数或不是数字时 + timeArr.forEach((item: any) => { + if (item < 0 || isNaN(Number(item))) { + this.inputValue = this.oldInputValue; + this.buttonDisabled = Util.isEmptyString(this.inputValue); + + return; + } + }); + this.setDisableData(); + this.setSelectVal(); + this.getScrollData(); + this.oldInputValue = this.inputValue; + this.buttonDisabled = false; + } + // 获取滚动值 + private getScrollData(): void { + if (Util.isEmptyString(this.inputValue)) { + this.hourScroll = 0; + this.minuteScroll = 0; + this.secondScroll = 0; + } else { + const timeArr: Array = this.addHour(this.inputValue).split(TiDateUtil.COLON_REGEXP); + this.hourScroll = timeArr[0] * 30; + this.minuteScroll = timeArr[1] * 30; + this.secondScroll = timeArr[2] * 30; + } + } + /** + * @ignore + * 清除 + */ + public onIconClearClick(): void { + if (this.disabled || !this.clearIcon) { + return; + } + this.inputValue = ''; + this.oldInputValue = ''; + this.timeValue = ''; + this.model = ''; + this.selectedHour = null; + this.selectedMinute = null; + this.selectedSecond = null; + this.buttonDisabled = true; + this.isClearClick = true; + this.setDisableData(); + } + + private getSelectedVal(options: Array, val: any): Function { + return options.find((item: any) => { + if (!item.disabled && item.label === val) { + return item; + } else { + return null; + } + }); + } + private setOptions(num: number, labelKey?: string): Array { + const options: Array = []; + for (let i: number = 0; i < num; i++) { + options[i] = { + label: TiTimeComponent.addZero(i, 2), + disabled: false + }; + } + + return options; + } + + private setDisableData(isChange?: boolean): boolean { + if (this.max === this.config.max && this.min === this.config.min && !isChange) { + return false; + } + const maxArr: Array = this.max.split(':'); + const minArr: Array = this.min.split(':'); + const value: string = this.addHour(this.inputValue); + const timeArr: Array = this.validateValue(value).split(':'); + // 根据最大最小值判定小时禁用项 + this.hourOptions.forEach((item: any) => { + item.disabled = Number(item.label) < Number(minArr[0]) || Number(item.label) > Number(maxArr[0]); + }); + + if (Util.isNumber(Number(timeArr[0])) && Number(minArr[0]) === Number(maxArr[0])) { + // 最大小时最小小时相等情况,需考虑分钟禁用 + this.minuteOptions.forEach((minuteItem: any) => { + minuteItem.disabled = Number(minuteItem.label) < Number(minArr[1]) || Number(minuteItem.label) > Number(maxArr[1]); + }); + } else if (Number(timeArr[0]) === Number(minArr[0])) { + // 最小小时情况,需考虑分钟禁用 + this.minuteOptions.forEach((minuteItem: any) => { + minuteItem.disabled = Number(minuteItem.label) < Number(minArr[1]); + }); + } else if (Number(timeArr[0]) === Number(maxArr[0])) { + // 最大小时情况,需考虑分钟禁用 + this.minuteOptions.forEach((minuteItem: any) => { + minuteItem.disabled = Number(minuteItem.label) > Number(maxArr[1]); + }); + } else { + this.minuteOptions.forEach((minuteItem: any) => { + minuteItem.disabled = false; + }); + } + + if (Util.isNumber(Number(timeArr[1])) && Number(minArr[0]) === Number(maxArr[0]) && Number(minArr[1]) === Number(maxArr[1])) { + // 最大小时分最小小时分相等情况,需考虑分钟禁用 + this.secondOptions.forEach((secondItem: any) => { + secondItem.disabled = Number(secondItem.label) < Number(minArr[2]) || Number(secondItem.label) > Number(maxArr[2]); + }); + } else if (Number(timeArr[0]) === Number(minArr[0]) && Number(timeArr[1]) === Number(minArr[1])) { + // 最小时分情况,需考虑秒数禁用 + this.secondOptions.forEach((secondItem: any) => { + secondItem.disabled = Number(secondItem.label) < Number(minArr[2]); + }); + } else if (Number(timeArr[0]) === Number(maxArr[0]) && Number(timeArr[1]) === Number(maxArr[1])) { + // 最大时分情况,需考虑秒数禁用 + this.secondOptions.forEach((secondItem: any) => { + secondItem.disabled = Number(secondItem.label) > Number(maxArr[2]); + }); + } else { + this.secondOptions.forEach((secondItem: any) => { + secondItem.disabled = false; + }); + } + + // 需要修改配置对象的引用,实现下拉禁用状态及时刷新 + this.hourOptions = [...this.hourOptions]; + this.minuteOptions = [...this.minuteOptions]; + this.secondOptions = [...this.secondOptions]; + } + + /** + * @ignore + * 时间选择框部分鼠标移出时去除hover样式 + */ + public onMouseleave(listCom: TiListComponent): void { + listCom.hoverOption = null; + } + // 组件交互方法集合--end + + // 内部公共方法集合--start + /** + * 校正当前时间为最大值或最小值 + * @param value 时间值 + * @returns + */ + private validateValue(value: string): string { + let timeValue: string = value; + + if (TiTimeComponent.isSmaller(timeValue, this.min)) { + timeValue = this.min; + } + + if (TiTimeComponent.isBigger(timeValue, this.max)) { + timeValue = this.max; + } + + return timeValue; + } + + /** + * @description: 时间格式化及时间各个框显示值设置 + */ + private formatValue(): void { + // 格式化value + const date: Date = new Date(`2018/5/15 ${TiTimeComponent.addColon(this.model)}`); + const formatStr: string = TiLocaleFormat.formatTime(date, this.format); + if (this.model !== formatStr) { + this.model = formatStr; + } + this.timeValue = this.model; + this.inputValue = this.model; + this.oldInputValue = this.inputValue; + const timeArr: Array = this.addHour(this.inputValue).split(':'); + this.selectedHour = this.getSelectedVal(this.hourOptions, TiTimeComponent.addZero(Number(timeArr[0]), 2)); + this.selectedMinute = this.getSelectedVal(this.minuteOptions, TiTimeComponent.addZero(Number(timeArr[1]), 2)); + this.selectedSecond = this.getSelectedVal(this.secondOptions, TiTimeComponent.addZero(Number(timeArr[2]), 2)); + } + /** + * @description: 校验动态更新的minValue是否是合法值 + * @param: minValue:动态更新的传入的minValue + */ + private isValidMinValue(minValue: string): boolean { + return TiTimeComponent.isValidTime(minValue) && TiTimeComponent.isSmallerOrEqual(minValue, this.max); + } + + /** + * @description: 校验动态更新的maxValue是否是合法值 + * @param: maxValue:动态更新的传入的maxValue + */ + private isValidMaxValue(maxValue: string): boolean { + return TiTimeComponent.isValidTime(maxValue) && TiTimeComponent.isBiggerOrEqual(maxValue, this.min); + } + + /** + * @description: 判断动态更新写入组件的value值是否是一个合法值 + * @param: value 动态更新写入组件的value值 + */ + private isValidValue(value: string): boolean { + const time: string = this.formatTime(value); + + return ( + TiTimeComponent.isValidTime(time) && + TiTimeComponent.isBiggerOrEqual(time, this.min) && + TiTimeComponent.isSmallerOrEqual(time, this.max) + ); + } + + /** + * @description: 格式化时间值 + * @param: value 值 + */ + private formatTime(value: string): string { + if (!value) { + return value; + } + const date: Date = new Date(`2018/5/15 ${TiTimeComponent.addColon(value)}`); + + if (String(date) === 'Invalid Date') { + return value; + } + + return this.addHour(TiLocaleFormat.formatTime(date, this.format)); + } + + /** + * 若当前格式是 mm:ss,补充小时位 + * @param value 时间值 + * @returns + */ + private addHour(value: string): string { + return this.onlyMinuteSecond && value ? `00:${value}` : value; + } + // 内部公共方法集合--end +} diff --git a/src/time/lib/src/TiTimeModule.ts b/src/time/lib/src/TiTimeModule.ts new file mode 100644 index 0000000..d2a31c6 --- /dev/null +++ b/src/time/lib/src/TiTimeModule.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { FormsModule } from '@angular/forms'; +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiTimeComponent } from './TiTimeComponent'; +import { TiDateDominatorModule } from '@opentiny/ng-datedominator'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiListModule } from '@opentiny/ng-list'; +import { TiLocaleModule, TiLocale } from '@opentiny/ng-locale'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiButtonModule } from '@opentiny/ng-button'; +import { locales } from './i18n'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiButtonModule, TiDropModule, TiListModule, TiDateDominatorModule, TiLocaleModule, TiTextModule], + exports: [TiTimeComponent], + declarations: [TiTimeComponent] +}) +export class TiTimeModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiTimeComponent } from './TiTimeComponent'; diff --git a/src/time/lib/src/i18n/TiTimeWords.ts b/src/time/lib/src/i18n/TiTimeWords.ts new file mode 100644 index 0000000..17984e1 --- /dev/null +++ b/src/time/lib/src/i18n/TiTimeWords.ts @@ -0,0 +1,7 @@ +export interface TiTimeWords { + tiTime: { + hour: string; + minute: string; + second: string; + }; +} diff --git a/src/time/lib/src/i18n/en_US.ts b/src/time/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..dfa9417 --- /dev/null +++ b/src/time/lib/src/i18n/en_US.ts @@ -0,0 +1,9 @@ +import { TiTimeWords } from './TiTimeWords'; + +export const en_US: TiTimeWords = { + tiTime: { + hour: 'Hour', + minute: 'Minute', + second: 'Second' + } +}; diff --git a/src/time/lib/src/i18n/es_US.ts b/src/time/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..a1e0eec --- /dev/null +++ b/src/time/lib/src/i18n/es_US.ts @@ -0,0 +1,9 @@ +import { TiTimeWords } from './TiTimeWords'; + +export const es_US: TiTimeWords = { + tiTime: { + hour: 'Hora', + minute: 'Minuto', + second: 'Segundo' + } +}; diff --git a/src/time/lib/src/i18n/fr_FR.ts b/src/time/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..095a6ac --- /dev/null +++ b/src/time/lib/src/i18n/fr_FR.ts @@ -0,0 +1,9 @@ +import { TiTimeWords } from './TiTimeWords'; + +export const fr_FR: TiTimeWords = { + tiTime: { + hour: 'Heure', + minute: 'Minute', + second: 'Second' + } +}; diff --git a/src/time/lib/src/i18n/index.ts b/src/time/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/time/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/time/lib/src/i18n/pt_BR.ts b/src/time/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..79f83cb --- /dev/null +++ b/src/time/lib/src/i18n/pt_BR.ts @@ -0,0 +1,9 @@ +import { TiTimeWords } from './TiTimeWords'; + +export const pt_BR: TiTimeWords = { + tiTime: { + hour: 'Hora', + minute: 'Minuto', + second: 'Segundo' + } +}; diff --git a/src/time/lib/src/i18n/zh_CN.ts b/src/time/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..52372bf --- /dev/null +++ b/src/time/lib/src/i18n/zh_CN.ts @@ -0,0 +1,9 @@ +import { TiTimeWords } from './TiTimeWords'; + +export const zh_CN: TiTimeWords = { + tiTime: { + hour: '时', + minute: '分', + second: '秒' + } +}; diff --git a/src/time/lib/src/time.html b/src/time/lib/src/time.html new file mode 100644 index 0000000..f007346 --- /dev/null +++ b/src/time/lib/src/time.html @@ -0,0 +1,93 @@ +{{ placeholder }} + + + + + +
    + +
    + {{ 'tiTime.hour' | tiTranslate }} + +
    + +
    + {{ 'tiTime.minute' | tiTranslate }} + +
    + +
    + {{ 'tiTime.second' | tiTranslate }} + +
    +
    +
    + +
    +
    diff --git a/src/time/lib/src/time.less b/src/time/lib/src/time.less new file mode 100644 index 0000000..57840b2 --- /dev/null +++ b/src/time/lib/src/time.less @@ -0,0 +1,89 @@ +@import '../../../themes/basic/base-all.less'; +@import '../../../themes/basic/compnent-container-border.less'; + +ti-drop { + --ti-time-select-title-height: 30px; +} + +:host.ti3-time-container :extend(.ti3-compnent-container-border all) { + width: var(--ti-common-size-30x); +} + +::ng-deep :root .ti3-time-drop-container { + &:focus { + outline: 0px; + } +} +:host.ti3-time-container { + display: inline-flex; + align-items: center; + font-size: var(--ti-common-font-size-base); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + height: var(--ti-common-size-7x); + line-height: normal; + background-color: var(--ti-common-color-bg-white-normal); + .box-sizing(border-box); + .user-select(); +} +.ti3-time-select-container { + display: flex; + justify-content: space-around; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); +} +.ti3-time-select-item-container { + width: 100%; + border-right: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-dividing); +} +.ti3-time-select-item-container:last-child { + border-right: none; +} +::ng-deep :root .ti3-time-select-list { + height: 230px; + width: 100%; + overflow-y: auto; + & li { + text-align: center; + margin-top: 0; + } +} +.ti3-dropdown-container.ti3-time-drop-container { + height: 365px; + padding: var(--ti-common-space-3x) var(--ti-common-space-4x); + z-index: 10002; +} +.ti3-time-edit { + display: block; + margin-bottom: var(--ti-common-space-3x); + width: 100%; +} + +::ng-deep .ti3-time-edit { + border-color: var(--ti-common-color-line-active) !important; +} + +.ti3-time-select-title { + width: 100%; + display: block; + margin-left: -8px; // ���������ȵ�һ�� + height: var(--ti-time-select-title-height); + line-height: var(--ti-time-select-title-height); + text-align: center; +} +.ti3-time-button-confirm { + margin-top: var(--ti-common-space-3x); + float: right; +} +::ng-deep .ti3-time-edit-with-button-disabled { + opacity: 0; +} +::ng-deep .ti3-time-edit-with-button-undisabled { + display: none; +} +.ti3-time-tab-input { + width: 0; + height: 0; + position: absolute; + top: -9999px; + left: -9999px; +} diff --git a/src/timeline/demo/karma.conf.js b/src/timeline/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/timeline/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/timeline/demo/project.json b/src/timeline/demo/project.json new file mode 100644 index 0000000..6ad9698 --- /dev/null +++ b/src/timeline/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/timeline/demo", + "sourceRoot": "src/timeline/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/timeline", + "index": "src/timeline/demo/src/index.html", + "main": "src/timeline/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/timeline/demo/tsconfig.app.json", + "assets": ["src/timeline/demo/src/favicon.ico", "src/timeline/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "timeline-demo:build:production" + }, + "development": { + "browserTarget": "timeline-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js timeline" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/timeline/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/timeline/demo/tsconfig.spec.json", + "karmaConfig": "src/timeline/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/timeline/demo/src/app/AppComponent.ts b/src/timeline/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/timeline/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/timeline/demo/src/app/AppModule.ts b/src/timeline/demo/src/app/AppModule.ts new file mode 100644 index 0000000..bb67fa8 --- /dev/null +++ b/src/timeline/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TimelineTestModule } from './timeline/TimelineTestModule'; + +@NgModule({ + imports: [ + TimelineTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/timeline/demo/src/app/IndexComponent.ts b/src/timeline/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..ad93cee --- /dev/null +++ b/src/timeline/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TimelineTestModule } from './timeline/TimelineTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TimelineTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/timeline/demo/src/app/app.html b/src/timeline/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/timeline/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/timeline/demo/src/app/timeline/TimelineBasicComponent.ts b/src/timeline/demo/src/app/timeline/TimelineBasicComponent.ts new file mode 100644 index 0000000..8cdff37 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineBasicComponent.ts @@ -0,0 +1,25 @@ +import { TiTimelineOption } from '@opentiny/ng'; +import { Component } from '@angular/core'; +@Component({ + templateUrl: './timeline-basic.html' +}) +export class TimelineBasicComponent { + options: Array = [ + { + label: '部署准备', + time: '2021年3月19日 11:30:26' + }, + { + label: '服务器部署', + time: '2021年3月19日 11:35:26' + }, + { + label: '网络配置', + time: '2021年3月19日 11:48:26' + }, + { + label: '实例发放', + time: '2021年3月19日 11:50:26' + } + ]; +} diff --git a/src/timeline/demo/src/app/timeline/TimelineDarkComponent.ts b/src/timeline/demo/src/app/timeline/TimelineDarkComponent.ts new file mode 100644 index 0000000..c945761 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineDarkComponent.ts @@ -0,0 +1,56 @@ +import { TiTimelineOption } from '@opentiny/ng'; +import { Component } from '@angular/core'; +@Component({ + templateUrl: './timeline-dark.html' +}) +export class TimelineDarkComponent { + activeIndex: number = 7; + options: Array = [ + { + label: '部署准备', + isTitle: true + }, + { + label: '下发部署任务', + time: '2021年3月19日 11:30:26' + }, + { + label: '服务器部署', + isTitle: true + }, + { + label: '分配服务器', + time: '2021年3月19日 11:35:26' + }, + { + label: '部署服务器', + time: '2021年3月19日 11:36:26' + }, + { + label: '网络配置', + isTitle: true + }, + { + label: '绑定EIP', + time: '2021年3月19日 11:40:26' + }, + { + label: '绑定安全组', + time: '2021年3月19日 11:45:26', + errorMessage: `失败原因:链接超时查看解决方案`, + type: 'danger' + }, + { + label: '绑定设备IP', + time: '2021年3月19日 11:48:26' + }, + { + label: '实例发放', + isTitle: true + }, + { + label: '开始发放实例', + time: '2021年3月19日 11:50:26' + } + ]; +} diff --git a/src/timeline/demo/src/app/timeline/TimelineHelptipComponent.ts b/src/timeline/demo/src/app/timeline/TimelineHelptipComponent.ts new file mode 100644 index 0000000..a549fd7 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineHelptipComponent.ts @@ -0,0 +1,58 @@ +import { TiTimelineOption } from '@opentiny/ng'; +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './timeline-helptip.html' +}) +export class TimelineHelptipComponent { + options: Array = [ + { + label: '部署准备', + isTitle: true + }, + { + label: '下发部署任务', + time: '2021年3月19日 11:30:26' + }, + { + label: '服务器部署', + iconTip: '服务器部署', + isTitle: true + }, + { + label: '分配服务器', + time: '2021年3月19日 11:35:26' + }, + { + label: '部署服务器', + time: '2021年3月19日 11:36:26' + }, + { + label: '网络配置', + isTitle: true + }, + { + label: '绑定EIP', + time: '2021年3月19日 11:40:26', + iconTip: 'EIP指的是可以独立购买和持有的公网IP地址资源。', + iconTipPosition: 'bottom-left', + iconTipMaxWidth: '200px' + }, + { + label: '绑定安全组', + time: '2021年3月19日 11:45:26' + }, + { + label: '绑定设备IP', + time: '2021年3月19日 11:48:26' + }, + { + label: '实例发放', + isTitle: true + }, + { + label: '开始发放实例', + time: '2021年3月19日 11:50:26' + } + ]; +} diff --git a/src/timeline/demo/src/app/timeline/TimelineMultiComponent.ts b/src/timeline/demo/src/app/timeline/TimelineMultiComponent.ts new file mode 100644 index 0000000..ea1d6cc --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineMultiComponent.ts @@ -0,0 +1,56 @@ +import { TiTimelineOption } from '@opentiny/ng'; +import { Component } from '@angular/core'; +@Component({ + templateUrl: './timeline-multi.html' +}) +export class TimelineMultiComponent { + activeIndex: number = 7; + options: Array = [ + { + label: '部署准备', + isTitle: true + }, + { + label: '下发部署任务', + time: '2021年3月19日 11:30:26' + }, + { + label: '服务器部署', + isTitle: true + }, + { + label: '分配服务器', + time: '2021年3月19日 11:35:26' + }, + { + label: '部署服务器', + time: '2021年3月19日 11:36:26' + }, + { + label: '网络配置', + isTitle: true + }, + { + label: '绑定EIP', + time: '2021年3月19日 11:40:26' + }, + { + label: '绑定安全组', + time: '2021年3月19日 11:45:26', + errorMessage: `失败原因:链接超时查看解决方案`, + type: 'danger' + }, + { + label: '绑定设备IP', + time: '2021年3月19日 11:48:26' + }, + { + label: '实例发放', + isTitle: true + }, + { + label: '开始发放实例', + time: '2021年3月19日 11:50:26' + } + ]; +} diff --git a/src/timeline/demo/src/app/timeline/TimelineTempleteComponent.ts b/src/timeline/demo/src/app/timeline/TimelineTempleteComponent.ts new file mode 100644 index 0000000..e35a01a --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineTempleteComponent.ts @@ -0,0 +1,34 @@ +import { TiTimelineOption } from '@opentiny/ng'; +import { Component } from '@angular/core'; +@Component({ + templateUrl: './timeline-templete.html' +}) +export class TimelineTempleteComponent { + options: Array = [ + { + label: 'primary', + time: '2015年4月28日 11:30:26', + type: 'primary' + }, + { + label: 'info', + time: '2015年4月28日 11:30:26', + type: 'info' + }, + { + label: 'success', + time: '2015年4月28日 11:30:26', + type: 'success' + }, + { + label: 'warning', + time: '2015年4月28日 11:30:26', + type: 'warning' + }, + { + label: 'danger', + time: '2015年4月28日 11:30:26', + type: 'danger' + } + ]; +} diff --git a/src/timeline/demo/src/app/timeline/TimelineTestComponent.ts b/src/timeline/demo/src/app/timeline/TimelineTestComponent.ts new file mode 100644 index 0000000..d2ba583 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineTestComponent.ts @@ -0,0 +1,113 @@ +import { TiTimelineOption } from '@opentiny/ng'; +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './timeline-test.html' +}) +export class TimelineTestComponent { + activeIndex: number = 4; + activeIndex1: number = 0; + options: Array = [ + { + label: '部署准备', + isTitle: true + }, + { + label: '下发部署任务', + time: '2021年3月19日 11:30:26' + }, + { + label: '服务器部署', + iconTip: 'tip', + isTitle: true + }, + { + label: '分配服务器', + time: '2021年3月19日 11:35:26' + }, + { + label: '部署服务器', + time: '2021年3月19日 11:36:26' + }, + { + label: '网络配置', + isTitle: true + }, + { + label: '绑定EIP', + time: '2021年3月19日 11:40:26' + }, + { + label: '绑定安全组', + time: '2021年3月19日 11:45:26', + errorMessage: `失败原因:链接超时查看解决方案` + }, + { + label: '绑定设备IP', + time: '2021年3月19日 11:48:26' + }, + { + label: '实例发放', + isTitle: true + }, + { + label: '开始发放实例', + time: '2021年3月19日 11:50:26' + } + ]; + options1: Array = [ + { + label: '部署准备', + isTitle: true + }, + { + label: '下发部署任务', + time: '2021年3月19日 11:30:26' + }, + { + label: '服务器部署', + iconTip: 'tip', + isTitle: true + }, + { + label: '分配服务器', + time: '2021年3月19日 11:35:26' + }, + { + label: '部署服务器', + time: '2021年3月19日 11:36:26' + }, + { + label: '网络配置', + isTitle: true + }, + { + label: '绑定EIP', + time: '2021年3月19日 11:40:26' + }, + { + label: '绑定安全组', + time: '2021年3月19日 11:45:26', + errorMessage: `失败原因:链接超时查看解决方案` + }, + { + label: '绑定设备IP', + time: '2021年3月19日 11:48:26' + }, + { + label: '实例发放', + isTitle: true + }, + { + label: '开始发放实例', + time: '2021年3月19日 11:50:26' + } + ]; + changeActiveStep(): void { + this.activeIndex = 7; + this.activeIndex1 = 7; + } + changeDanger(): void { + this.options[7].type = 'danger'; + } +} diff --git a/src/timeline/demo/src/app/timeline/TimelineTestModule.ts b/src/timeline/demo/src/app/timeline/TimelineTestModule.ts new file mode 100644 index 0000000..3344b68 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineTestModule.ts @@ -0,0 +1,63 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiTimelineModule, TiTipModule } from '@opentiny/ng'; + +import { TimelineMultiComponent } from './TimelineMultiComponent'; +import { TimelineDarkComponent } from './TimelineDarkComponent'; +import { TimelineBasicComponent } from './TimelineBasicComponent'; +import { TimelineTempleteComponent } from './TimelineTempleteComponent'; +import { TimelineTypeComponent } from './TimelineTypeComponent'; +import { TimelineHelptipComponent } from './TimelineHelptipComponent'; +import { TimelineTestComponent } from './TimelineTestComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiTimelineModule, TiTipModule, RouterModule.forChild(TimelineTestModule.ROUTES)], + declarations: [ + TimelineBasicComponent, + TimelineDarkComponent, + TimelineMultiComponent, + TimelineTempleteComponent, + TimelineTypeComponent, + TimelineHelptipComponent, + TimelineTestComponent + ] +}) +export class TimelineTestModule { + static readonly LINKS: Array = [{ label: 'Timeline' }]; + static readonly ROUTES: Routes = [ + { + path: 'timeline/timeline-basic', + component: TimelineBasicComponent, + data: { label: '基础' } + }, + { + path: 'timeline/timeline-multi', + component: TimelineMultiComponent, + data: { label: '多级展示' } + }, + { + path: 'timeline/timeline-dark', + component: TimelineDarkComponent, + data: { label: '深色背景展示' } + }, + { + path: 'timeline/timeline-templete', + component: TimelineTempleteComponent + }, + { + path: 'timeline/timeline-type', + component: TimelineTypeComponent + }, + { + path: 'timeline/timeline-helptip', + component: TimelineHelptipComponent + }, + { + path: 'timeline/timeline-test', + component: TimelineTestComponent + } + ]; +} diff --git a/src/timeline/demo/src/app/timeline/TimelineTypeComponent.ts b/src/timeline/demo/src/app/timeline/TimelineTypeComponent.ts new file mode 100644 index 0000000..7af4eb2 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/TimelineTypeComponent.ts @@ -0,0 +1,34 @@ +import { TiTimelineOption } from '@opentiny/ng'; +import { Component } from '@angular/core'; +@Component({ + templateUrl: './timeline-type.html' +}) +export class TimelineTypeComponent { + options: Array = [ + { + label: 'primary', + time: '2015年4月28日 11:30:26', + type: 'primary' + }, + { + label: 'info', + time: '2015年4月28日 11:30:26', + type: 'info' + }, + { + label: 'success', + time: '2015年4月28日 11:30:26', + type: 'success' + }, + { + label: 'warning', + time: '2015年4月28日 11:30:26', + type: 'warning' + }, + { + label: 'danger', + time: '2015年4月28日 11:30:26', + type: 'danger' + } + ]; +} diff --git a/src/timeline/demo/src/app/timeline/timeline-basic.html b/src/timeline/demo/src/app/timeline/timeline-basic.html new file mode 100644 index 0000000..a66c621 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/timeline-basic.html @@ -0,0 +1 @@ + diff --git a/src/timeline/demo/src/app/timeline/timeline-dark.html b/src/timeline/demo/src/app/timeline/timeline-dark.html new file mode 100644 index 0000000..054a6f7 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/timeline-dark.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/timeline/demo/src/app/timeline/timeline-helptip.html b/src/timeline/demo/src/app/timeline/timeline-helptip.html new file mode 100644 index 0000000..864aae8 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/timeline-helptip.html @@ -0,0 +1 @@ + diff --git a/src/timeline/demo/src/app/timeline/timeline-multi.html b/src/timeline/demo/src/app/timeline/timeline-multi.html new file mode 100644 index 0000000..74d25fa --- /dev/null +++ b/src/timeline/demo/src/app/timeline/timeline-multi.html @@ -0,0 +1 @@ + diff --git a/src/timeline/demo/src/app/timeline/timeline-templete.html b/src/timeline/demo/src/app/timeline/timeline-templete.html new file mode 100644 index 0000000..202f76d --- /dev/null +++ b/src/timeline/demo/src/app/timeline/timeline-templete.html @@ -0,0 +1,6 @@ + + + {{item.label}} + {{item.time}} + + diff --git a/src/timeline/demo/src/app/timeline/timeline-test.html b/src/timeline/demo/src/app/timeline/timeline-test.html new file mode 100644 index 0000000..144707c --- /dev/null +++ b/src/timeline/demo/src/app/timeline/timeline-test.html @@ -0,0 +1,23 @@ +

    1、描述

    +

    时间线组件,呈现为一组时间序列的文本展示

    +

    导入

    +

    import {{ '{' }} TpTimelineModule {{ '}' }} from '@opentiny/ng';

    +

    基本展示

    +
    + +

    + + +
    +

    tip场景

    +鼠标移入出tip使用场景 + + + + diff --git a/src/timeline/demo/src/app/timeline/timeline-type.html b/src/timeline/demo/src/app/timeline/timeline-type.html new file mode 100644 index 0000000..3ad10ab --- /dev/null +++ b/src/timeline/demo/src/app/timeline/timeline-type.html @@ -0,0 +1 @@ + diff --git a/src/timeline/demo/src/app/timeline/webdoc/timeline-demos.js b/src/timeline/demo/src/app/timeline/webdoc/timeline-demos.js new file mode 100644 index 0000000..27348e7 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/webdoc/timeline-demos.js @@ -0,0 +1,89 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'timeline-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'basic', + }, + desc: { + 'zh-CN': '

    Timeline 组件的最简用法。

    ', + 'en-US': '', + }, + apis: [ + 'TiTimelineComponent.properties.options', + 'TiTimelineOption.properties.label', + 'TiTimelineOption.properties.time' + ], + }, + { + demoId: 'timeline-type', + name: { + 'zh-CN': '类型', + 'en-US': 'type', + }, + desc: { + 'zh-CN': '

    通过属性option.type配置 timeline 节点类型,包括infosuccessdangerprimarywarning

    ', + 'en-US': '', + }, + apis: ['TiTimelineOption.properties.type'], + }, + { + demoId: 'timeline-multi', + name: { + 'zh-CN': '多级', + 'en-US': 'multi', + }, + desc: { + 'zh-CN': '

    通过属性activeIndex配置当前激活项;通过属性option.isTitle配置是否为一级节点标题;通过属性option.errorMessage配置节点执行失败的错误信息。

    ', + 'en-US': '', + }, + apis: [ + 'TiTimelineComponent.properties.activeIndex', + 'TiTimelineOption.properties.isTitle', + 'TiTimelineOption.properties.errorMessage' + ], + }, + { + demoId: 'timeline-dark', + name: { + 'zh-CN': '深色背景', + 'en-US': 'dark', + }, + desc: { + 'zh-CN': '

    通过属性dark配置深色背景时间线。

    ', + 'en-US': '', + } + }, + { + demoId: 'timeline-helptip', + name: { + 'zh-CN': '提示信息', + 'en-US': 'helptip', + }, + desc: { + 'zh-CN': '

    通过属性option.iconTip配置帮助图标提示内容;通过属性option.iconTipPosition配置提示方向;通过属性option.iconTipMaxWidth配置提示信息最大宽度。

    ', + 'en-US': '', + }, + apis: [ + 'TiTimelineOption.properties.iconTip', + 'TiTimelineOption.properties.iconTipPosition', + 'TiTimelineOption.properties.iconTipMaxWidth' + ], + }, + { + demoId: 'timeline-templete', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'templete', + }, + desc: { + 'zh-CN': '

    通过属性#item配置时间线节点内容的模板。

    ', + 'en-US': '', + }, + apis: ['TiTimelineComponent.slots.itemTemplate'], + } + ] +} diff --git a/src/timeline/demo/src/app/timeline/webdoc/timeline.cn.md b/src/timeline/demo/src/app/timeline/webdoc/timeline.cn.md new file mode 100644 index 0000000..c805522 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/webdoc/timeline.cn.md @@ -0,0 +1,23 @@ +--- +title: Timeline 时间线 +--- +# Timeline 时间线 + +
    + +垂直展示时间流信息的组件。    + +```typescript +import { TiTimelineModule } from '@opentiny/ng'; +``` + +
    + +
    + +垂直展示时间流信息的组件。   + +```typescript +import { TiTimelineModule } from '@opentiny/ng'; +``` +
    \ No newline at end of file diff --git a/src/timeline/demo/src/app/timeline/webdoc/timeline.en.md b/src/timeline/demo/src/app/timeline/webdoc/timeline.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/timeline/demo/src/app/timeline/webdoc/timeline.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/timeline/demo/src/favicon.ico b/src/timeline/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/timeline/demo/src/index.html b/src/timeline/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/timeline/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/timeline/demo/src/main.ts b/src/timeline/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/timeline/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/timeline/demo/test.ts b/src/timeline/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/timeline/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/timeline/demo/tsconfig.app.json b/src/timeline/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/timeline/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/timeline/demo/tsconfig.spec.json b/src/timeline/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/timeline/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/timeline/lib/index.ts b/src/timeline/lib/index.ts new file mode 100644 index 0000000..8a52840 --- /dev/null +++ b/src/timeline/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTimelineModule'; diff --git a/src/timeline/lib/ng-package.json b/src/timeline/lib/ng-package.json new file mode 100644 index 0000000..bfd1328 --- /dev/null +++ b/src/timeline/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/timeline", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/timeline/lib/package.json b/src/timeline/lib/package.json new file mode 100644 index 0000000..046eab5 --- /dev/null +++ b/src/timeline/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-timeline", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/timeline/lib/project.json b/src/timeline/lib/project.json new file mode 100644 index 0000000..8f6d71c --- /dev/null +++ b/src/timeline/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/timeline/lib", + "sourceRoot": "src/timeline/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/timeline"], + "options": { + "project": "src/timeline/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/timeline"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js timeline" + }, + { + "command": "ng default-build timeline" + }, + { + "command": "node build/clear-default-theme.js timeline" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/timeline && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build timeline && ng pack timeline && node build/publish.js timeline --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/timeline/lib/src/TiTimelineComponent.ts b/src/timeline/lib/src/TiTimelineComponent.ts new file mode 100644 index 0000000..a109b8e --- /dev/null +++ b/src/timeline/lib/src/TiTimelineComponent.ts @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + Input, + Renderer2, + SimpleChanges, + TemplateRef +} from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { Util } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; +export interface TiTimelineOption { + /** + * 节点类型 + */ + type?: 'info' | 'success' | 'danger' | 'primary' | 'warning'; + /** + * 必选,节点文本 + */ + label: string; + /** + * 节点时间 + */ + time?: string; + /** + * 节点是否为一级标题 + */ + isTitle?: boolean; + /** + * 节点执行失败错误信息 + */ + errorMessage?: string; + /** + * 帮助提示图标 tip 提示内容 + */ + iconTip?: string | TemplateRef | any; + /** + * 帮助提示图标 tip 提示方向 + */ + iconTipPosition?: string; + /** + * 帮助提示图标 tip 最大宽度 + */ + iconTipMaxWidth?: string; +} +/** + * 时间线timeline组件 + */ +@Component({ + selector: 'ti-timeline', + templateUrl: './timeline.html', + styleUrls: ['./timeline.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiTimelineComponent extends TiBaseComponent { + /** + * 必选,时间线节点数据集 + */ + @Input() options: Array; + /** + * 当前激活项 + */ + @Input() activeIndex: number = -1; + /** + * 时间线节点内容区域的模板 + */ + @ContentChild('item', { static: true }) itemTemplate: TemplateRef; + /** + * @ignore + * 时间轴一级信息集合 + */ + public titleOptions: Array = []; + protected versionInfo: string = super.getVersion(packageInfo); + + constructor(protected hostRef: ElementRef, protected renderer: Renderer2, protected changeDetectorRef: ChangeDetectorRef) { + super(hostRef, renderer); + } + + ngOnInit(): void { + super.ngOnInit(); + this.titleOptions = this.getTitleOptions(); + this.changeToSuccess(); + } + ngOnChanges(changes: SimpleChanges): void { + if (changes['activeIndex'] && !changes['activeIndex'].firstChange) { + this.changeToSuccess(); + } + if (changes['options'] && !changes['options'].firstChange) { + this.titleOptions = this.getTitleOptions(); + } + } + + ngDoCheck(): void { + super.ngDoCheck(); + // 改为onPush模式后,复杂数据类型接口的变更必须要修改引用地址,才能出发变更检测。此处添加标记向前兼容。 + this.changeDetectorRef.markForCheck(); + } + + /** + * @ignore + */ + public findItemIndex(item: TiTimelineOption) { + return this.titleOptions.findIndex((ele: TiTimelineOption) => ele === item); + } + /** + * @ignore + * 获取时间轴一级(标题)信息集合 + */ + public getTitleOptions(): Array { + return this.options.filter((option: TiTimelineOption) => option.isTitle); + } + /** + * @ignore + * activeIndex之前的项全部变更为success状态 + */ + public changeToSuccess(): void { + if (this.activeIndex === -1 || Util.isUndefined(this.activeIndex) || !Util.isNumber(this.activeIndex)) { + return; + } + this.options.forEach((option: TiTimelineOption, index: number) => { + option.type = index < this.activeIndex ? 'success' : option.type || 'primary'; + }); + } + /** + * @ignore + * 判断当前一级时间轴状态 + */ + public isSuccess(option: TiTimelineOption): boolean { + if (!option.isTitle) { + return; + } + let level2Options: Array = []; + const index: number = this.options.findIndex((item: TiTimelineOption) => item === option); + let isSucess: boolean = true; + for (let i = index + 1; i <= this.activeIndex; i++) { + if (this.options[i] && this.options[i].isTitle) { + break; + } + level2Options.push(this.options[i]); + } + if (level2Options.length > 0) { + level2Options.forEach((option: TiTimelineOption) => { + if (option.type && option.type !== 'success') { + isSucess = false; + } + }); + } + + return isSucess; + } +} diff --git a/src/timeline/lib/src/TiTimelineModule.ts b/src/timeline/lib/src/TiTimelineModule.ts new file mode 100644 index 0000000..ea0b3b3 --- /dev/null +++ b/src/timeline/lib/src/TiTimelineModule.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiTimelineComponent } from './TiTimelineComponent'; + +@NgModule({ + imports: [CommonModule, TiIconModule, TiTipModule], + exports: [TiTimelineComponent], + declarations: [TiTimelineComponent] +}) +export class TiTimelineModule {} +export { TiTimelineComponent, TiTimelineOption } from './TiTimelineComponent'; diff --git a/src/timeline/lib/src/timeline.html b/src/timeline/lib/src/timeline.html new file mode 100644 index 0000000..8b99f3d --- /dev/null +++ b/src/timeline/lib/src/timeline.html @@ -0,0 +1,60 @@ +
      +
    • + + + {{findItemIndex(item) + 1}} + + + + +
    • +
    + + +
    + {{item.label}} + + + {{item.time}} + +
    + {{item.time}} +
    + +
    +
    + + + + diff --git a/src/timeline/lib/src/timeline.less b/src/timeline/lib/src/timeline.less new file mode 100644 index 0000000..9589c8c --- /dev/null +++ b/src/timeline/lib/src/timeline.less @@ -0,0 +1,220 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-timeline-badge-width: var(--ti-common-size-2x); + --ti-timeline-line-width: 1px; + --ti-timeline-label-line-height: calc(var(--ti-common-font-size-base) * var(--ti-common-line-height-number)); + --ti-timeline-level1-width: 14px; + --ti-timeline-level1-border-weight: var(--ti-common-border-weight-normal); + --ti-timeline-level1-line-height: calc(var(--ti-common-font-size-1) * var(--ti-common-line-height-number)); + --ti-timeline-current-border-weight: var(--ti-common-border-weight-1); +} + +.ti3-timeline-container { + position: relative; + padding-bottom: calc(var(--ti-common-space-5x)); + list-style: none; + font-weight: var(--ti-common-font-weight-4); + &:after { + content: ''; + left: calc(-1 * var(--ti-timeline-line-width) / 2); + position: absolute; + top: calc((var(--ti-timeline-label-line-height) + var(--ti-timeline-badge-width)) / 2); + width: var(--ti-timeline-line-width); + height: calc(100% - var(--ti-timeline-badge-width)); + background-color: var(--ti-common-color-line-dividing); + } + &:last-child { + padding-bottom: 0; + &:after { + display: none; + } + } +} +// 只有多级信息场景下,时间轴线条才会变色 +.ti3-timeline-multistage .ti3-timeline-container.ti3-timeline-completed { + &:after { + background-color: var(--ti-common-color-success); + } +} +.ti3-timeline-label { + font-size: var(--ti-common-font-size-base); + display: block; + padding-left: var(--ti-common-space-5x); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-primary); + position: relative; + word-wrap: break-word; + &:before { + content: ''; + position: absolute; + left: calc(-1 * var(--ti-timeline-badge-width) / 2); + top: calc((var(--ti-timeline-label-line-height) - var(--ti-timeline-badge-width)) / 2); + width: var(--ti-timeline-badge-width); + height: var(--ti-timeline-badge-width); + background-color: var(--ti-common-color-line-dividing); + border-radius: var(--ti-common-border-radius-3); + z-index: 1; + } +} +.ti3-timeline-multistage { + padding-left: var(--ti-common-space-2x); + .ti3-timeline-label { + color: var(--ti-common-color-text-weaken); + } + .ti3-timeline-success { + color: var(--ti-common-color-text-primary); + } + .ti3-timeline-time-level2 { + display: inline-block; + padding-left: var(--ti-common-space-3x); + } +} +// 当前进行中的步骤,水波纹样式 +.ti3-timeline-currentStep { + &:before { + left: calc(-1 * var(--ti-timeline-badge-width) / 2 - var(--ti-timeline-current-border-weight)); + top: calc((var(--ti-timeline-label-line-height) - var(--ti-timeline-badge-width)) / 2 - var(--ti-timeline-current-border-weight)); + background-color: var(--ti-common-color-success); + background-clip: content-box; + border: var(--ti-timeline-current-border-weight) var(--ti-common-border-style-solid) rgba(80, 212, 171, 0.2); + } +} +.ti3-timeline-multistage .ti3-timeline-danger { + &:before { + background-color: var(--ti-common-color-error-text); + border: var(--ti-timeline-current-border-weight) var(--ti-common-border-style-solid) rgba(222, 80, 78, 0.2); + } +} +.changecolor(@color) { + background-color: @color; + border-color: @color; +} +.ti3-timeline-info:before { + .changecolor(var(--ti-common-color-prompt)); +} +.ti3-timeline-success:before { + .changecolor(var(--ti-common-color-success)); +} +.ti3-timeline-warning:before { + .changecolor(var(--ti-common-color-warn)); +} +.ti3-timeline-danger:before { + .changecolor(var(--ti-common-color-error)); +} +.ti3-timeline-time { + font-size: var(--ti-common-font-size-base); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-weaken); + display: block; + white-space: pre; +} + +.ti3-timeline-active { + color: var(--ti-common-color-text-success); +} +.ti3-timeline-active-danger { + color: var(--ti-common-color-error-text); +} + +.ti3-timeline-level1 { + width: var(--ti-timeline-level1-width); + height: var(--ti-timeline-level1-width); + text-align: center; + line-height: calc(var(--ti-timeline-level1-width) + var(--ti-timeline-level1-border-weight)); + display: inline-block; + border: var(--ti-timeline-level1-border-weight) var(--ti-common-border-style-solid) var(--ti-common-color-text-weaken); + border-radius: var(--ti-common-border-radius-3); + color: var(--ti-common-color-text-weaken); + position: absolute; + left: calc(-1 * calc(var(--ti-timeline-level1-width) + var(--ti-timeline-level1-border-weight) * 2) / 2); + top: calc((var(--ti-timeline-level1-line-height) - var(--ti-timeline-level1-width) - var(--ti-timeline-level1-border-weight) * 2) / 2); + background-color: var(--ti-common-color-bg-white-normal); + z-index: 1; + .box-sizing(content-box); +} + +.ti3-timeline-level1-success { + color: var(--ti-common-color-success); + border-color: var(--ti-common-color-success); +} + +.ti3-timeline-processing { + background: var(--ti-common-color-success); + color: var(--ti-common-color-text-white); +} + +.ti3-timeline-errorMessagg { + color: var(--ti-common-color-error-text); + padding-top: var(--ti-common-space-base); +} + +.ti3-timeline-label-level1 { + font-size: var(--ti-common-font-size-1); +} + +.ti3-timeline-label-container { + display: flex; + align-items: center; +} +.ti3-timeline-icontip { + font-size: var(--ti-common-font-size-2); + padding-left: var(--ti-common-space-2x); + color: var(--ti-common-color-icon-normal); + vertical-align: bottom; +} + +:host[dark] { + .ti3-timeline-container { + &:after { + background: var(--ti-common-color-line-normal); + } + } + .ti3-timeline-label { + color: var(--ti-common-color-text-darkbg); + &:before { + background: var(--ti-common-color-line-normal); + } + } + .ti3-timeline-level1 { + color: var(--ti-common-color-text-darkbg); + border-color: var(--ti-common-color-line-normal); + background-color: var(--ti-common-color-bg-dark-normal); + } + .ti3-timeline-level1.ti3-timeline-level1-success { + color: var(--ti-common-color-success); + border-color: var(--ti-common-color-success); + } + .ti3-timeline-success, + .ti3-timeline-currentStep { + &:before { + background: var(--ti-common-color-success); + background-clip: content-box; + } + } + .ti3-timeline-currentStep.ti3-timeline-danger { + &:before { + background: var(--ti-common-color-error); + background-clip: content-box; + border: var(--ti-timeline-current-border-weight) var(--ti-common-border-style-solid) rgba(246, 111, 106, 0.2); + } + } + .ti3-timeline-active-danger, + .ti3-timeline-errorMessagg { + color: var(--ti-common-color-error); + } + .ti3-timeline-time { + color: var(--ti-common-color-text-darkbg); + } + .ti3-timeline-success { + color: var(--ti-common-color-text-gray); + } + .ti3-timeline-level1.ti3-timeline-processing { + background-color: var(--ti-common-color-success); + color: var(--ti-common-color-text-white); + } + // 目前深色背景只有tip场景,故深色背景下不会再有问号tip + .ti3-timeline-icontip { + display: none; + } +} diff --git a/src/tip/demo/karma.conf.js b/src/tip/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/tip/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/tip/demo/project.json b/src/tip/demo/project.json new file mode 100644 index 0000000..90d845a --- /dev/null +++ b/src/tip/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/tip/demo", + "sourceRoot": "src/tip/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/tip", + "index": "src/tip/demo/src/index.html", + "main": "src/tip/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tip/demo/tsconfig.app.json", + "assets": ["src/tip/demo/src/favicon.ico", "src/tip/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "tip-demo:build:production" + }, + "development": { + "browserTarget": "tip-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js tip" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/tip/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tip/demo/tsconfig.spec.json", + "karmaConfig": "src/tip/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/tip/demo/src/app/AppComponent.ts b/src/tip/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/tip/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/tip/demo/src/app/AppModule.ts b/src/tip/demo/src/app/AppModule.ts new file mode 100644 index 0000000..fe87613 --- /dev/null +++ b/src/tip/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TipTestModule } from './tip/TipTestModule'; + +@NgModule({ + imports: [ + TipTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/tip/demo/src/app/IndexComponent.ts b/src/tip/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..43a4c0a --- /dev/null +++ b/src/tip/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TipTestModule } from './tip/TipTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TipTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/tip/demo/src/app/app.html b/src/tip/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/tip/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/tip/demo/src/app/tip/TipBasicComponent.ts b/src/tip/demo/src/app/tip/TipBasicComponent.ts new file mode 100644 index 0000000..f1c3820 --- /dev/null +++ b/src/tip/demo/src/app/tip/TipBasicComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-basic.html' +}) +export class TipBasicComponent { + content: string = 'mouseover to show tip'; +} diff --git a/src/tip/demo/src/app/tip/TipContentCompComponent.ts b/src/tip/demo/src/app/tip/TipContentCompComponent.ts new file mode 100644 index 0000000..a364bde --- /dev/null +++ b/src/tip/demo/src/app/tip/TipContentCompComponent.ts @@ -0,0 +1,51 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + templateUrl: './tip-content-comp.html' +}) +export class TipContentCompComponent { + myLogs: Array = []; + // 定义组件类型 + // eslint-disable-next-line no-use-before-define + component: any = TipDemoComponent; + + // 定义tip显示内容的上下文 + tipContext: any = { + label: 'Are you sure delete this task?', // 该属性与TipDemoComponent组件中的@Input属性定义对应 + outputs: { + // 定义在outputs对象中的属性(例如 ok cancel)与TipDemoComponent组件中的@Output属性定义对应 + ok: ($event: string): void => { + this.myLogs = [...this.myLogs, `ok() event = ${$event}`]; + }, + cancel: ($event: string): void => { + this.myLogs = [...this.myLogs, `cancel() event = ${$event}`]; + } + } + }; +} + +// 自定义组件,此处为了方便demo展示与tip生成组件写在同一文件,项目中请将组件单独写在一个文件中 +@Component({ + template: ` + + {{ label }} +
    + + +
    + ` +}) +export class TipDemoComponent { + // 定义元素属性,该属性可作为Tip上下文使用 + @Input() label: string; + @Output() readonly ok: EventEmitter = new EventEmitter(); + @Output() readonly cancel: EventEmitter = new EventEmitter(); + + onOk(): void { + this.ok.emit('ok event'); + } + + onCancel(): void { + this.cancel.emit('cancel event'); + } +} diff --git a/src/tip/demo/src/app/tip/TipContentTemplateComponent.ts b/src/tip/demo/src/app/tip/TipContentTemplateComponent.ts new file mode 100644 index 0000000..784213f --- /dev/null +++ b/src/tip/demo/src/app/tip/TipContentTemplateComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-content-template.html' +}) +export class TipContentTemplateComponent { + tip1Context: string = 'this is a tip'; + tip2Context: string = 'this is another tip'; +} diff --git a/src/tip/demo/src/app/tip/TipEmptyComponent.ts b/src/tip/demo/src/app/tip/TipEmptyComponent.ts new file mode 100644 index 0000000..2ffc45a --- /dev/null +++ b/src/tip/demo/src/app/tip/TipEmptyComponent.ts @@ -0,0 +1,59 @@ +import { AfterViewInit, ChangeDetectorRef, Component, TemplateRef, ViewChild } from '@angular/core'; +import { TipDemoComponent } from './TipContentCompComponent'; + +@Component({ + templateUrl: './tip-empty.html', + styleUrls: ['./tipTest.less'] +}) +export class TipEmptyComponent implements AfterViewInit { + // tip内容为string, tip内容为null + tipStr: string = null; + + // tip内容为组件 + component: any = TipDemoComponent; + + // 获取tip内容模板,实际上获取不到模板时,tipTemplate为undefined + @ViewChild('tipTempContent') tipTemplate: TemplateRef; + + // 当tip内容是模板时,控制tip是否显示 + showTipTemplate: boolean = true; + + tipTempContext: any = { + label: 'this is a tip of template.' + }; + + tipCompContext: any = { + label: 'name:', // 该属性与TipDemoComponent组件中的@Input属性定义对应 + value: 'this is a tip of component.', // 该属性与TipDemoComponent组件中的@Input属性定义对应 + outputs: { + // 定义在outputs对象中的属性(例如 ok cancel)与TipDemoComponent组件中的@Output属性定义对应 + ok: ($event: string): void => {}, + cancel: ($event: string): void => {} + } + }; + + constructor(private cd: ChangeDetectorRef) {} + + ngAfterViewInit(): void { + // 处理ExpressionChangedAfterItHasBeenCheckedError报错 + this.cd.detectChanges(); + console.log(this.tipTemplate); + } + + onClickTemp(event: any): void { + this.showTipTemplate = !this.showTipTemplate; + // 处理ExpressionChangedAfterItHasBeenCheckedError报错 + this.cd.detectChanges(); + console.log(this.tipTemplate); + } + + onClickComp(event: any): void { + this.component ? (this.component = null) : (this.component = TipDemoComponent); + console.log(this.component); + } + + onClickStr(event: any): void { + this.tipStr ? (this.tipStr = null) : (this.tipStr = 'tip内容'); + console.log(this.tipStr); + } +} diff --git a/src/tip/demo/src/app/tip/TipHasArrowComponent.ts b/src/tip/demo/src/app/tip/TipHasArrowComponent.ts new file mode 100644 index 0000000..f71db1b --- /dev/null +++ b/src/tip/demo/src/app/tip/TipHasArrowComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-has-arrow.html' +}) +export class TipHasArrowComponent {} diff --git a/src/tip/demo/src/app/tip/TipLongTextPositionComponent.ts b/src/tip/demo/src/app/tip/TipLongTextPositionComponent.ts new file mode 100644 index 0000000..8b013b9 --- /dev/null +++ b/src/tip/demo/src/app/tip/TipLongTextPositionComponent.ts @@ -0,0 +1,63 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-long-text-position.html', + styleUrls: ['./tipTest.less'] +}) +export class TipLongTextPositionComponent { + tipStr: string = `苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。 + 苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。`; +} diff --git a/src/tip/demo/src/app/tip/TipMaxWidthComponent.ts b/src/tip/demo/src/app/tip/TipMaxWidthComponent.ts new file mode 100644 index 0000000..fac3621 --- /dev/null +++ b/src/tip/demo/src/app/tip/TipMaxWidthComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-max-width.html' +}) +export class TipMaxWidthComponent { + tipStr: string = 'this is my tip, it is a long tip, its maxWidth is defined'; +} diff --git a/src/tip/demo/src/app/tip/TipPositionComponent.ts b/src/tip/demo/src/app/tip/TipPositionComponent.ts new file mode 100644 index 0000000..fd8d7cd --- /dev/null +++ b/src/tip/demo/src/app/tip/TipPositionComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-position.html', + styles: [ + ` + .center { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + margin: 0; + } + ` + ] +}) +export class TipPositionComponent { + tipStr: string = 'myTip'; +} diff --git a/src/tip/demo/src/app/tip/TipPositionTestComponent.ts b/src/tip/demo/src/app/tip/TipPositionTestComponent.ts new file mode 100644 index 0000000..c9a6dab --- /dev/null +++ b/src/tip/demo/src/app/tip/TipPositionTestComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-position-test.html' +}) +export class TipPositionTestComponent { + tipStr: string = 'myTip'; +} diff --git a/src/tip/demo/src/app/tip/TipServiceComponent.ts b/src/tip/demo/src/app/tip/TipServiceComponent.ts new file mode 100644 index 0000000..f7fdc13 --- /dev/null +++ b/src/tip/demo/src/app/tip/TipServiceComponent.ts @@ -0,0 +1,42 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { TiTipRef, TiTipService, TiTipShowInfo } from '@opentiny/ng'; + +@Component({ + templateUrl: './tip-service.html' +}) +export class TipServiceComponent { + private tipInstance: TiTipRef; // tip组件实例 + private tipShowState: boolean = false; // tip显示状态标志位 + @ViewChild('button') buttonRef: ElementRef; + constructor(private tipService: TiTipService) {} + ngAfterViewInit(): void { + this.tipService.create(this.buttonRef.nativeElement, { + position: 'right', + trigger: 'mouse', // 需要与 showFn 结合使用;指定触发 tip 显示的方式 + showFn: (): TiTipShowInfo => { + // 设置为 'mouse' 时的显示函数,返回值类型为 TiTipShowInfo + return { content: 'tip content', context: {} }; + } + }); + } + onClick($event: any): void { + if (!this.tipInstance) { + // 生成tip实例:通过调用tipService的create方法生成 + this.tipInstance = this.tipService.create($event.target, { + position: 'right' + }); + } + + // tip实例已生成情况下,切换tip提示的显示状态 + this.toggleTip(); + } + toggleTip(): void { + if (!this.tipShowState) { + this.tipInstance.show('tip content'); // 显示tip提示,并定义其内容为'tip content' + this.tipShowState = true; // 设置tip显示状态标志位 + } else { + this.tipInstance.hide(); // 隐藏tip提示 + this.tipShowState = false; // 重置tip显示状态标志位 + } + } +} diff --git a/src/tip/demo/src/app/tip/TipServiceDestroyComponent.ts b/src/tip/demo/src/app/tip/TipServiceDestroyComponent.ts new file mode 100644 index 0000000..a81597f --- /dev/null +++ b/src/tip/demo/src/app/tip/TipServiceDestroyComponent.ts @@ -0,0 +1,32 @@ +import { Component, ElementRef } from '@angular/core'; +import { TiTipRef, TiTipService, TiTipShowInfo } from '@opentiny/ng'; + +@Component({ + templateUrl: './tip-service-destroy.html' +}) +export class TipServiceDestroyComponent { + private tipInstance: TiTipRef; // tip组件实例 + constructor(private tipService: TiTipService) {} + + addTip($event: any, target: ElementRef): void { + if (this.tipInstance) { + return; + } + // 生成tip实例:通过调用tipService的create方法生成 + this.tipInstance = this.tipService.create(target.nativeElement, { + position: 'right', + trigger: 'mouse', + showFn: (): TiTipShowInfo => { + return { + content: '自定义tip内容' + }; + } + }); + } + removeTip($event: any): void { + if (this.tipInstance) { + this.tipInstance.destroy(); + this.tipInstance = null; + } + } +} diff --git a/src/tip/demo/src/app/tip/TipTestModule.ts b/src/tip/demo/src/app/tip/TipTestModule.ts new file mode 100644 index 0000000..272aeb2 --- /dev/null +++ b/src/tip/demo/src/app/tip/TipTestModule.ts @@ -0,0 +1,111 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiIconModule, TiTipModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TipBasicComponent } from './TipBasicComponent'; +import { TipPositionComponent } from './TipPositionComponent'; +import { TipContentTemplateComponent } from './TipContentTemplateComponent'; +import { TipContentCompComponent, TipDemoComponent } from './TipContentCompComponent'; +import { TipHasArrowComponent } from './TipHasArrowComponent'; +import { TipMaxWidthComponent } from './TipMaxWidthComponent'; +import { TipTriggerComponent } from './TipTriggerComponent'; +import { TipServiceComponent } from './TipServiceComponent'; +import { TipZindexComponent } from './TipZindexComponent'; +import { TipPositionTestComponent } from './TipPositionTestComponent'; +import { TipServiceDestroyComponent } from './TipServiceDestroyComponent'; +import { TipValidPositionTestComponent } from './TipValidPositionTestComponent'; +import { TipEmptyComponent } from './TipEmptyComponent'; +import { TipLongTextPositionComponent } from './TipLongTextPositionComponent'; + +@NgModule({ + imports: [ + CommonModule, + TiTipModule, + TiButtonModule, + TiIconModule, + FormsModule, + ReactiveFormsModule, + DemoLogModule, + RouterModule.forChild(TipTestModule.ROUTES) + ], + declarations: [ + TipBasicComponent, + TipContentTemplateComponent, + TipContentCompComponent, + TipPositionComponent, + TipHasArrowComponent, + TipMaxWidthComponent, + TipServiceComponent, + TipServiceDestroyComponent, + TipTriggerComponent, + TipPositionTestComponent, + TipValidPositionTestComponent, + TipDemoComponent, + TipEmptyComponent, + TipLongTextPositionComponent, + TipZindexComponent + ], + entryComponents: [TipDemoComponent] +}) +export class TipTestModule { + static readonly LINKS: Array = [ + { href: 'directives/TiTipDirective.html', label: 'Tip' }, + { href: 'injectables/TiTipService.html', label: 'TiTipService' } + ]; + static readonly ROUTES: Routes = [ + { + path: 'tip/tip-basic', + component: TipBasicComponent + }, + { + path: 'tip/tip-trigger', + component: TipTriggerComponent + }, + { + path: 'tip/tip-content-template', + component: TipContentTemplateComponent + }, + { + path: 'tip/tip-content-comp', + component: TipContentCompComponent + }, + { + path: 'tip/tip-position', + component: TipPositionComponent + }, + { + path: 'tip/tip-has-arrow', + component: TipHasArrowComponent + }, + { + path: 'tip/tip-max-width', + component: TipMaxWidthComponent + }, + { + path: 'tip/tip-service', + component: TipServiceComponent + }, + { + path: 'tip/tip-zindex', + component: TipZindexComponent + }, + { path: 'tip/tip-service-destroy', component: TipServiceDestroyComponent }, + { path: 'tip/tip-position-test', component: TipPositionTestComponent }, + { + path: 'tip/tip-valid-position-test', + component: TipValidPositionTestComponent + }, + { + path: 'tip/tip-empty', + component: TipEmptyComponent + }, + { + path: 'tip/tip-long-text-position', + component: TipLongTextPositionComponent + } + ]; +} diff --git a/src/tip/demo/src/app/tip/TipTriggerComponent.ts b/src/tip/demo/src/app/tip/TipTriggerComponent.ts new file mode 100644 index 0000000..483f1b8 --- /dev/null +++ b/src/tip/demo/src/app/tip/TipTriggerComponent.ts @@ -0,0 +1,15 @@ +import { Component, ViewChild } from '@angular/core'; +import { TiTipDirective } from '@opentiny/ng'; + +@Component({ + templateUrl: './tip-trigger.html' +}) +export class TipTriggerComponent { + @ViewChild('tip', { static: true }) tipDirective: TiTipDirective; + tipContent: string = 'click to show tip'; + show: boolean = false; + onClick(): void { + this.show = !this.show; + this.show ? this.tipDirective.hide() : this.tipDirective.show(); + } +} diff --git a/src/tip/demo/src/app/tip/TipValidPositionTestComponent.ts b/src/tip/demo/src/app/tip/TipValidPositionTestComponent.ts new file mode 100644 index 0000000..ede4479 --- /dev/null +++ b/src/tip/demo/src/app/tip/TipValidPositionTestComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-valid-position-test.html', + styleUrls: ['./tipTest.less'] +}) +export class TipValidPositionTestComponent { + tipStr11: string = '寻寻觅觅,冷冷清清,凄凄惨惨戚戚。乍'; + tipStr12: string = + '苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。' + + '园中菊花堆积满地,都已经憔悴不堪,如今还有谁来采摘?冷清清地守着窗子,独自一个人怎么熬到天黑?梧桐叶上细雨淋漓,到黄昏时分,还是点点滴滴。这般情景,怎么能用一个愁字了结!'; + tipStr13: string = + '苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。'; + tipStr21: string = + '苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。'; + tipStr22: string = + '苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。'; + tipStr31: string = '寻寻觅觅,冷冷清清,凄凄惨惨戚戚。乍'; + tipStr32: string = + '苦苦地寻寻觅觅,却只见冷冷清清,怎不让人凄惨悲戚。乍暖还寒的时节,最难保养休息。喝三杯两杯淡酒,怎么能抵得住早晨的寒风急袭?一行大雁从眼前飞过,更让人伤心,因为都是旧日的相识。'; + tipStr33: string = '寻寻觅觅,冷冷清清,凄凄惨惨戚戚。乍'; +} diff --git a/src/tip/demo/src/app/tip/TipZindexComponent.ts b/src/tip/demo/src/app/tip/TipZindexComponent.ts new file mode 100644 index 0000000..714bb7a --- /dev/null +++ b/src/tip/demo/src/app/tip/TipZindexComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './tip-zindex.html', + styles: [ + ` + .zIndex { + z-index: 1000; + } + ` + ] +}) +export class TipZindexComponent { + tipStr: string = 'this is my tip dfa'; +} diff --git a/src/tip/demo/src/app/tip/tip-basic.html b/src/tip/demo/src/app/tip/tip-basic.html new file mode 100644 index 0000000..86ae6a2 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-basic.html @@ -0,0 +1 @@ + diff --git a/src/tip/demo/src/app/tip/tip-content-comp.html b/src/tip/demo/src/app/tip/tip-content-comp.html new file mode 100644 index 0000000..3f56a1f --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-content-comp.html @@ -0,0 +1,2 @@ + + diff --git a/src/tip/demo/src/app/tip/tip-content-template.html b/src/tip/demo/src/app/tip/tip-content-template.html new file mode 100644 index 0000000..b169b94 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-content-template.html @@ -0,0 +1,8 @@ + + + + + + + {{context}} + diff --git a/src/tip/demo/src/app/tip/tip-empty.html b/src/tip/demo/src/app/tip/tip-empty.html new file mode 100644 index 0000000..5ca6579 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-empty.html @@ -0,0 +1,21 @@ +

    描述

    +

    当组件内容为空(null,undefined、空字符串)时,tip自动隐藏。

    +

    示例

    +

    1、提示内容的数据类型为字符串,将空字符串赋值给内容变量,不显示tip

    +
    mouseover to show tip
    + +

    tipStr:{{tipStr}}

    + +

    2、提示内容为模板,模板为undefined时,不显示tip

    +
    mouseover to show tip
    + +

    showTipTemplate:{{showTipTemplate}}

    + + + {{context.label}} + + +

    3、提示内容为组件,设置组件为null时,不显示tip

    +
    mouseover to show tip
    + +

    组件:{{component}}

    diff --git a/src/tip/demo/src/app/tip/tip-has-arrow.html b/src/tip/demo/src/app/tip/tip-has-arrow.html new file mode 100644 index 0000000..cef1a7e --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-has-arrow.html @@ -0,0 +1 @@ + diff --git a/src/tip/demo/src/app/tip/tip-long-text-position.html b/src/tip/demo/src/app/tip/tip-long-text-position.html new file mode 100644 index 0000000..719ee87 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-long-text-position.html @@ -0,0 +1,17 @@ +

    本测试用例主要测试tip中的文字超长,在屏幕显示不下时,出滚动条,在各个方向的展示情况

    +
    +
    +
    top
    +
    top
    +
    top-left
    +
    +
    +
    left
    +
    right
    +
    +
    +
    bottom-right
    +
    bottom
    +
    bottom-left
    +
    +
    diff --git a/src/tip/demo/src/app/tip/tip-max-width.html b/src/tip/demo/src/app/tip/tip-max-width.html new file mode 100644 index 0000000..c5bf0fc --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-max-width.html @@ -0,0 +1 @@ + diff --git a/src/tip/demo/src/app/tip/tip-position-test.html b/src/tip/demo/src/app/tip/tip-position-test.html new file mode 100644 index 0000000..718766c --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-position-test.html @@ -0,0 +1,18 @@ +

    本测试用例除了测位置以外,也测试了有滚动条情况下的tip行为

    +
    +
    top
    +
    top-left
    +
    top-right
    +
    bottom
    +
    bottom-left
    +
    bottom-right
    +
    left
    +
    left-top
    +
    left-bottom
    +
    right
    +
    right-top
    +
    right-bottom
    +
    center
    + +
    auto
    +
    diff --git a/src/tip/demo/src/app/tip/tip-position.html b/src/tip/demo/src/app/tip/tip-position.html new file mode 100644 index 0000000..efd7a5d --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-position.html @@ -0,0 +1,23 @@ +
    +
    + + + +
    +
    + + + +
    + +
    + + + +
    +
    + + + +
    +
    diff --git a/src/tip/demo/src/app/tip/tip-service-destroy.html b/src/tip/demo/src/app/tip/tip-service-destroy.html new file mode 100644 index 0000000..bcf4af4 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-service-destroy.html @@ -0,0 +1,9 @@ +

    1 描述

    +

    服务生成方式生成的实例tipInstance 可以通过tipInstance.destroy()销毁

    +

    2 示例

    +

    2.1 tip实例 destroy()方法

    +
    +
    + + + diff --git a/src/tip/demo/src/app/tip/tip-service.html b/src/tip/demo/src/app/tip/tip-service.html new file mode 100644 index 0000000..13c7182 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-service.html @@ -0,0 +1,2 @@ +
    + diff --git a/src/tip/demo/src/app/tip/tip-trigger.html b/src/tip/demo/src/app/tip/tip-trigger.html new file mode 100644 index 0000000..0f0d092 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-trigger.html @@ -0,0 +1 @@ + diff --git a/src/tip/demo/src/app/tip/tip-valid-position-test.html b/src/tip/demo/src/app/tip/tip-valid-position-test.html new file mode 100644 index 0000000..1ae74f8 --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-valid-position-test.html @@ -0,0 +1,17 @@ +

    本测试用例主要测试tip在开发者指定的位置显示不下时,能够自适应在其能显示的位置显示

    +
    +
    +
    top
    +
    top
    +
    top-left
    +
    +
    +
    left
    +
    right
    +
    +
    +
    bottom-right
    +
    bottom
    +
    bottom-left
    +
    +
    diff --git a/src/tip/demo/src/app/tip/tip-zindex.html b/src/tip/demo/src/app/tip/tip-zindex.html new file mode 100644 index 0000000..1ea9dbc --- /dev/null +++ b/src/tip/demo/src/app/tip/tip-zindex.html @@ -0,0 +1,6 @@ +
    +
    z-index: 1000
    + +
    + +
    diff --git a/src/tip/demo/src/app/tip/tipTest.less b/src/tip/demo/src/app/tip/tipTest.less new file mode 100644 index 0000000..845d19e --- /dev/null +++ b/src/tip/demo/src/app/tip/tipTest.less @@ -0,0 +1,38 @@ +.blockCls { + border: 1px solid #ccc; + width: 100px; + height: 100px; + margin: 10px 0; +} +.demo { + border: 1px solid #ccc; + width: 80px; + margin: 10px; + padding: 10px; + display: inline-block; + text-align: center; +} +.demo-tiny { + border: 1px solid #ccc; + margin: 5px 10px; + padding: 2px; + display: inline-block; + cursor: default; +} +body { + margin: 100px; + position: relative; +} +.test-tip { + border: 1px solid #ccc; + width: 200px; + height: 100px; + display: inline-block; +} +.center { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + margin: 0; +} diff --git a/src/tip/demo/src/app/tip/webdoc/tip-demos.js b/src/tip/demo/src/app/tip/webdoc/tip-demos.js new file mode 100644 index 0000000..eb95d75 --- /dev/null +++ b/src/tip/demo/src/app/tip/webdoc/tip-demos.js @@ -0,0 +1,123 @@ +export default { + column: '2', + demos: [ + { + demoId: 'tip-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'tiTip', + }, + desc: { + 'zh-CN': '

    通过属性tiTip配置气泡提示内容。

    ', + 'en-US': '

    tiTip

    ', + }, + apis: ['TiTipDirective.properties.tiTip'], + }, + { + demoId: 'tip-content-template', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'tiTipContext', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipContext配置显示内容对应的上下文。

    ', + 'en-US': '

    button color

    ', + }, + apis: ['TiTipDirective.properties.tiTipContext'], + }, + { + demoId: 'tip-content-comp', + name: { + 'zh-CN': '内容为组件', + 'en-US': 'component', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipContext配置显示内容对应的上下文,适用于 tip 内容较复杂且多个页面中共用的场景。

    ', + 'en-US': '

    item

    ', + }, + apis: ['TiTipDirective.properties.tiTipContext'], + }, + { + demoId: 'tip-has-arrow', + name: { + 'zh-CN': '气泡提示不带箭头', + 'en-US': 'hasArrow', + }, + desc: { + 'zh-CN': '

    通过属性tiTipHasArrow配置是否带箭头。

    ', + 'en-US': '

    item

    ', + }, + apis: ['TiTipDirective.properties.tiTipHasArrow'], + }, + { + demoId: 'tip-position', + name: { + 'zh-CN': '位置', + 'en-US': 'position', + }, + desc: { + 'zh-CN': '

    通过属性tiTipPosition配置 tip 显示位置。

    ', + 'en-US': '

    item

    ', + }, + apis: ['TiTipDirective.properties.tiTipPosition', 'TiPositionType'], + }, + { + demoId: 'tip-max-width', + name: { + 'zh-CN': '最大宽度', + 'en-US': 'position', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipMaxWidth配置 tip 的最大宽度。

    ', + 'en-US': '

    item

    ', + }, + apis: ['TiTipDirective.properties.tiTipMaxWidth'], + }, + { + demoId: 'tip-service', + name: { + 'zh-CN': '服务生成', + 'en-US': 'position', + }, + desc: { + 'zh-CN': '

    通过服务的方式生成 tip。

    ', + 'en-US': '

    item

    ', + }, + apis: [ + 'TiTipService.methods.create', + 'TiTipDirective.methods.hide', + 'TiTipDirective.methods.show', + 'TiTipShowInfo', + ], + }, + { + demoId: 'tip-zindex', + name: { + 'zh-CN': '层级', + 'en-US': 'position', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipZIndex配置 tip 的 z-index 属性值。

    ', + 'en-US': '

    item

    ', + }, + apis: ['TiTipDirective.properties.tiTipZIndex'], + }, + { + demoId: 'tip-trigger', + name: { + 'zh-CN': '点击触发', + 'en-US': 'trigger', + }, + desc: { + 'zh-CN': + '

    通过属性tiTipTrigger配置 tip 的生成方式。

    ', + 'en-US': '

    item

    ', + }, + apis: ['TiTipDirective.properties.tiTipTrigger'], + }, + ], +}; diff --git a/src/tip/demo/src/app/tip/webdoc/tip.cn.md b/src/tip/demo/src/app/tip/webdoc/tip.cn.md new file mode 100644 index 0000000..985a460 --- /dev/null +++ b/src/tip/demo/src/app/tip/webdoc/tip.cn.md @@ -0,0 +1,23 @@ +--- +title: Tip 气泡提示 +--- +# Tip 气泡提示 + +
    + +Tip 是提供文本气泡提示的组件,可以通过指令方式、服务方式生成。 + +```typescript +import { TiTipModule } from '@opentiny/ng'; +``` + +
    + +
    + +Tip 是提供文本气泡提示的组件,可以通过指令方式、服务方式生成。 + +```typescript +import { TiTipModule } from '@opentiny/ng'; +``` +
    diff --git a/src/tip/demo/src/app/tip/webdoc/tip.en.md b/src/tip/demo/src/app/tip/webdoc/tip.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/tip/demo/src/app/tip/webdoc/tip.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/tip/demo/src/favicon.ico b/src/tip/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/tip/demo/src/index.html b/src/tip/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/tip/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/tip/demo/src/main.ts b/src/tip/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/tip/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/tip/demo/test.ts b/src/tip/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/tip/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/tip/demo/tsconfig.app.json b/src/tip/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/tip/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/tip/demo/tsconfig.spec.json b/src/tip/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/tip/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/tip/lib/index.ts b/src/tip/lib/index.ts new file mode 100644 index 0000000..b76064a --- /dev/null +++ b/src/tip/lib/index.ts @@ -0,0 +1,4 @@ +export * from './src/TiTipModule'; +export * from './src/TiTipServiceModule'; +export * from './src/TiTipService'; +export * from './src/TiTipInterface'; diff --git a/src/tip/lib/ng-package.json b/src/tip/lib/ng-package.json new file mode 100644 index 0000000..371691b --- /dev/null +++ b/src/tip/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/tip", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/tip/lib/package.json b/src/tip/lib/package.json new file mode 100644 index 0000000..9baf2b3 --- /dev/null +++ b/src/tip/lib/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opentiny/ng-tip", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/router": ">=13.0.0", + "@opentiny/ng-popup": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/tip/lib/project.json b/src/tip/lib/project.json new file mode 100644 index 0000000..18e5ef4 --- /dev/null +++ b/src/tip/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/tip/lib", + "sourceRoot": "src/tip/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/tip"], + "options": { + "project": "src/tip/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/tip"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js tip" + }, + { + "command": "ng default-build tip" + }, + { + "command": "node build/clear-default-theme.js tip" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/tip && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build tip && ng pack tip && node build/publish.js tip --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/tip/lib/src/TiTipContainerComponent.ts b/src/tip/lib/src/TiTipContainerComponent.ts new file mode 100644 index 0000000..2763f80 --- /dev/null +++ b/src/tip/lib/src/TiTipContainerComponent.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +/** + * @ignore + */ +@Component({ + selector: 'ti-tip-container', + template: `
    +
    `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-tooltip]': 'true', + '[style.z-index]': 'zIndex' + }, + styleUrls: ['./tip.less'] +}) +export class TiTipContainerComponent { + @Input() zIndex: number; +} diff --git a/src/tip/lib/src/TiTipDirective.ts b/src/tip/lib/src/TiTipDirective.ts new file mode 100644 index 0000000..1cdad46 --- /dev/null +++ b/src/tip/lib/src/TiTipDirective.ts @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ComponentRef, Directive, ElementRef, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; +import { TiPositionType, Util } from '@opentiny/ng-utils'; +import { TiTipRef, TiTipShowInfo } from './TiTipInterface'; +import { TiTipService } from './TiTipService'; + +// TODO:exportAs: 'tiTip' 可去除? +/** + * tip提供了两种使用方式: + * 1.服务方式:[TiTipService]{@link ../injectables/TiTipService.html} + * + * 2.指令方式(见如下说明) + * + */ +@Directive({ + selector: '[tiTip]', + exportAs: 'tiTip' +}) +export class TiTipDirective implements OnInit, OnDestroy { + /** + * tip 提示方向 + */ + @Input() tiTipPosition?: TiPositionType = 'auto'; + /** + * tip 最大宽度 + */ + @Input() tiTipMaxWidth: string = '276px'; + /** + * 是否带箭头 + */ + @Input() tiTipHasArrow?: boolean = true; + /** + * tip 显示内容对应的上下文,tip内容类型为 templateRef 或 Component 形式时会用到该参数,参数为自定义对象形式 + * + * 注意:指令形式时才会使用到该参数 + */ + @Input() tiTipContext?: any; + /** + * tip 触发行为,默认支持鼠标移入时显示 + * + * 注意:指令形式时才会使用到该参数 + */ + @Input() tiTipTrigger?: 'mouse' | 'manual' = 'mouse'; + /** + * @ignore + * 决定tip水平方向位置的宿主元素配置 + */ + @Input() tiTipHostEleX: Element; + /** + * z-index 属性值 + */ + @Input() tiTipZIndex: number; + protected tipInstance: TiTipRef; + private hostEle: Element; + private _tiTip: string | TemplateRef | any; + constructor(private tipService: TiTipService, hostEleRef: ElementRef) { + this.hostEle = hostEleRef.nativeElement; + } + + private static isInValidValue(value: string | TemplateRef | any): boolean { + return Util.isUndefined(value) || Util.isNull(value) || value === ''; + } + ngOnInit(): void { + // 初始创建tip实例 + this.tipInstance = this.tipService.create(this.hostEle, { + position: this.tiTipPosition, + maxWidth: this.tiTipMaxWidth, + hasArrow: this.tiTipHasArrow, + hostEleX: this.tiTipHostEleX, + zIndex: this.tiTipZIndex, + trigger: this.tiTipTrigger || 'mouse', // 指令方式,默认为'mouse' + showFn: (): TiTipShowInfo => { + if (!this.tipInstance || TiTipDirective.isInValidValue(this._tiTip)) { + return; + } + + return { content: this._tiTip, context: this.tiTipContext }; + } + }); + } + /** + * tip显示内容配置 + * + * 类型:string | TemplateRef | any + * + * 传入string类型时,在v3.0.6及之前的版本存在XSS攻击风险;v3.0.7做了安全处理后不存在XSS风险了,把传入的字符串当做纯文本解析,传入什么显示什么; + * v10.1.4版本支持html字符串片段,也已做安全处理。 + */ + @Input() + // 监听tip值的变化,当tip为''或undefined情况下,直接隐藏tip组件 + /** + * @ignore + */ + set tiTip(value: string | TemplateRef | any) { + this._tiTip = value; + if (TiTipDirective.isInValidValue(value)) { + this.hide(); + } + } + /** + * @ignore + */ + get tiTip(): string | TemplateRef | any { + return this._tiTip; + } + /** + * 显示 tip 的方法 + */ + public show(): ComponentRef { + if (!this.tipInstance || TiTipDirective.isInValidValue(this._tiTip)) { + return; + } + + return this.tipInstance.show(this._tiTip, this.tiTipContext); + } + /** + * 隐藏 tip 的方法 + */ + public hide(): void { + if (this.tipInstance) { + this.tipInstance.hide(); + } + } + // 宿主销毁,tip连带销毁 + ngOnDestroy(): void { + if (this.tipInstance) { + this.tipInstance.hide(); + this.tipInstance = null; + } + } +} diff --git a/src/tip/lib/src/TiTipInterface.ts b/src/tip/lib/src/TiTipInterface.ts new file mode 100644 index 0000000..49c1a8e --- /dev/null +++ b/src/tip/lib/src/TiTipInterface.ts @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ComponentRef, TemplateRef } from '@angular/core'; +import { TiPositionEventType, TiPositionType } from '@opentiny/ng-utils'; +/** + * showFn 函数返回值类型 + * + */ +export interface TiTipShowInfo { + /** + * tip 显示内容,可为字符串/ng-template/component + */ + content: string | TemplateRef | any; + /** + * tip 显示内容对应的上下文,content类型为templateRef或Component形式时会用到该参数 + */ + context?: any; +} +/** + * tip配置项, + * 作为[TiTipService.create]{@link ../injectables/TiTipService.html#create}方法中的参数类型使用 + */ +export interface TiTipConfig { + /** + * 位置 + * + * @default 'auto' + */ + position?: TiPositionType; + /** + * 最大宽度 + * + * @default '276px' + */ + maxWidth?: string; + /** + * 是否带箭头 + * + * @default true + */ + hasArrow?: boolean; + /** + * tip色系 'white'(tip背景为浅色)/'dark'(tip背景为深色), 不设置时默认为 'dark' + */ + theme?: 'dark' | 'white'; + /** + * tip 的触发方式,设置为 'manual' 时手动触发,设置为 'mouse' 时应和 showFn 结合使用鼠标在宿主悬浮时触发 + */ + trigger?: 'mouse' | 'manual'; + /** + * @ignore + * + * 页面不可见时,是否让tip消失,默认消失,intro组件不消失 + */ + registerVisibilityChangeEvent?: boolean; + /** + * + * tip的z-index属性值 + */ + zIndex?: number; + /** + * @ignore + */ + positionEventTypes?: Array; + /** + * trigger 设置为 'mouse' 时的显示函数,返回值类型为 TiTipShowInfo + */ + showFn?(): TiTipShowInfo; + /** + * @ignore + * 决定定位元素水平方向的元素,用于宿主元素水平方向位置与host元素不一致的场景,暂不对外开放 + */ + hostEleX?: Element; +} +/** + * tip生成实例对象 + * 作为[TiTipService.create]{@link ../injectables/TiTipService.html#create}方法的返回值使用 + */ +export interface TiTipRef { + /** + * @ignore + * 内部变量,用于标识mouse事件触发情况下,鼠标是否在tip元素上 + */ + isInsideTip?: boolean; + /** + * 显示tip方法 + * + * **函数类型:**(content: string | TemplateRef | any, context?: any) => void; + * + * **参数类型:** + * + * 1. content tip显示内容,可为字符串/ng-template/component,具体用法见示例 + * + * 2. context tip显示内容对应的上下文,content类型为templateRef或Component形式时会用到该参数 + * + */ + show(content: string | TemplateRef | any, context?: any): ComponentRef; + /** + * 隐藏tip方法 + * + * **函数类型:** () => void; + */ + hide(): void; + /** + * @ignore + * 销毁tip实例方法,如下场景需要此方法: + * 一定条件下,希望通过TiTpService给目标元素添加tip,并使用mouseenter/mouseleave 触发tip, 当条件改变后需要去除目标元素上的tip。 + * 目前 TpFrozen 在使用此接口 + * **函数类型:** () => void; + */ + destroy(): void; +} diff --git a/src/tip/lib/src/TiTipModule.ts b/src/tip/lib/src/TiTipModule.ts new file mode 100644 index 0000000..5bf0ca9 --- /dev/null +++ b/src/tip/lib/src/TiTipModule.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiTipDirective } from './TiTipDirective'; +import { TiTipServiceModule } from './TiTipServiceModule'; +// 引用TiTipModule,可以使用Tip指令和Tip服务 +@NgModule({ + imports: [CommonModule, TiTipServiceModule], + exports: [TiTipDirective], + declarations: [TiTipDirective] +}) +export class TiTipModule {} +export { TiTipDirective } from './TiTipDirective'; diff --git a/src/tip/lib/src/TiTipService.ts b/src/tip/lib/src/TiTipService.ts new file mode 100644 index 0000000..b8b4b19 --- /dev/null +++ b/src/tip/lib/src/TiTipService.ts @@ -0,0 +1,343 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/* eslint-disable no-eq-null */ +/* eslint-disable eqeqeq */ +/** + * 该类提供服务,用于管理Tip组件的创建和销毁 + * 服务中提供三个方法: + * create(hostEle, config) 生成一个tip实例并返回对象, + * hostEle:宿主元素 + * config:{ + * position:tip元素位置 + * maxWidth:最大宽度 + * hasArrow:是否带箭头 + * theme:tip色系 'white'/'dark' + * } + * 返回的实例对象中提供方法: + * { + * show({ // 显示Tip组件 + * content:弹出组件内容 + * context:弹出组件上下文 + * }) + * hide():隐藏并销毁Tip + * } + */ +import { ComponentRef, Inject, Injectable, NgZone, Optional, Renderer2, RendererFactory2 } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { NavigationEnd, Router, RouterEvent } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; + +import { TiContentType, TiPopUpRef, TiPopupService } from '@opentiny/ng-popup'; +import { Position, TiPositionResult, TiPositionType, Util } from '@opentiny/ng-utils'; +import { TiRenderer } from '@opentiny/ng-renderer'; + +import { TiTipContainerComponent } from './TiTipContainerComponent'; +import { TiTipServiceModule } from './TiTipServiceModule'; +import { TiTipConfig, TiTipRef, TiTipShowInfo } from './TiTipInterface'; +/** + * @ignore + */ +export interface TiTipShowConfig { + popInstance: TiPopUpRef; + hostEle: Element; + content: TiContentType; + context: any; + config: TiTipConfig; +} + +/** + * @ignore + */ +interface ShareValues { + mouseenterTimer: any; +} +/** + * tip提供了两种使用方式: + * + * 1.服务方式(见如下说明),使用该服务时需要引入模块TiTipServiceModule + * + * 2.指令方式:[TiTipDirective]{@link ../directives/TiTipDirective.html} + * + */ +@Injectable({ + providedIn: TiTipServiceModule +}) +export class TiTipService { + private static readonly DEFAULT_WIDTH: number = 276; // tip换行宽度 + private static readonly SPACE: number = 6.5 + 5; // tip框与元素本身的距离 = 三角宽高 + tip三角到触发tip的内容区域的距离 + private static readonly MOUSE_ENTER_DELAY: number = 500; // 防止每次鼠标不小心经过目标元素就会显示出tip的内容,所以增加适当的延迟 + private render: Renderer2; + /** + * @ignore + */ + public positionResult: TiPositionResult; // Position.setPosition()的返回值; + constructor( + private popService: TiPopupService, + rendererFactory: RendererFactory2, + private tiRenderer: TiRenderer, + private zone: NgZone, + @Inject(DOCUMENT) private document, + @Optional() private router: Router + ) { + this.render = rendererFactory.createRenderer(null, null); + } + /** + * 页面激活窗口改变事件处理,此处获取事件名称,该事件后续会在tip显示时注册,隐藏时销毁 + * 添加该事件用于解决的问题现象是:tip带链接,并且点击链接跳转至新开页面,因此,当返回先前页面时,tip不消失(因为未触发任何tip消失的事件), + * 且移入其他出tip的元素,会出现页面有多个tip的现象 + */ + private getVisibleChangeEventName(): string { + const hiddenProperty: string = + 'hidden' in this.document + ? 'hidden' + : 'webkitHidden' in this.document + ? 'webkitHidden' + : 'mozHidden' in this.document + ? 'mozHidden' + : ''; + + return hiddenProperty.replace(/hidden/i, 'visibilitychange'); + } + /** + * 创建 tip 实例 + */ + public create(hostEle: Element, config?: TiTipConfig): TiTipRef { + // 宿主元素不存在情况下,不做处理 + if (!Util.isElement(hostEle)) { + return; + } + const shareValues: ShareValues = { mouseenterTimer: undefined }; + const tipConfig: TiTipConfig = config ? config : {}; + const tipInstance: TiTipRef = this.createTip(hostEle, shareValues, tipConfig); + this.addTriggerEvent(hostEle, tipConfig, tipInstance, shareValues); + + return tipInstance; + } + /** + * 创建Tip实例 + * + * @param hostEle tip生成所依附的宿主元素 + * + * @param config tip属性配置 + * + * @returns 生成的Tip实例对象如下: + * + * ``` + * { + * // 显示tip方法 + * // @param content {string | TemplateRef | any} tip显示内容,可为字符串/ng-template/component,具体用法见示例 + * // @param context {any} tip显示内容对应的上下文,content类型为templateRef或Component形式时会用到该参数 + * show: (content: string | TemplateRef | any, context?: any) => void; + * + * // 隐藏tip方法 + * hide: () => void; + * } + * ``` + */ + private createTip(hostEle: Element, shareValues: ShareValues, config?: TiTipConfig): TiTipRef { + const popInstance: TiPopUpRef = this.popService.create(TiTipContainerComponent); // tip弹出服务实例,可通过调用show/hide方法切换组件的显示状态 + let tipComponentRef: any; // 生成好的 tip 组件实例对象componentRef + const visibilityChangeEvent: string = this.getVisibleChangeEventName(); + let visibleChangeEvtHandle: () => void; + let routerChangeSub: Subscription; + + // 组件添加全局事件销毁相关处理 + let eventHandles: Array<() => void> = []; // 用于存储事件句柄,事件句柄在事件取消时需要用到 + + // 隐藏处理函数 + const hideFn: () => void = (): void => { + if (shareValues?.mouseenterTimer !== undefined) { + clearTimeout(shareValues.mouseenterTimer); + } + if (tipComponentRef != null) { + // 销毁弹出元素 + popInstance.hide(); + // 通过执行事件返回句柄方法解绑事件 + Position.removePosChangeEvts(eventHandles); + if (config.registerVisibilityChangeEvent !== false) { + visibleChangeEvtHandle(); + } + routerChangeSub?.unsubscribe(); + tipComponentRef = null; + } + }; + + return { + show: (content: string, context?: any): ComponentRef | undefined => { + if (Util.isUndefined(content) || content === '') { + return; + } + // 显示Tip元素 + tipComponentRef = this.showTip({ + popInstance, + hostEle, + content, + context, + config + }); + // 添加全局事件,用于控制特殊情况下宿主位置改变时Tip的隐藏 + eventHandles = Position.addPosChangeEvts(hideFn, this.render, config.positionEventTypes); + if (config.registerVisibilityChangeEvent !== false) { + visibleChangeEvtHandle = this.render.listen(this.document, visibilityChangeEvent, hideFn); + } + // 部分服务使用了路由复用策略重写了 angular/router 的 RouteReuseStrategy 接口 + // 使用 hashchange 监听不到通过 routerLink/navigate 路由跳转的场景 + // 此时宿主元素的 ngOnDestroy 不会触发不会销毁 tip,因此监听路由变化的方式 + routerChangeSub = this.router?.events.pipe(filter((event: RouterEvent) => event instanceof NavigationEnd)).subscribe(() => { + hideFn(); + }); + + return tipComponentRef; + }, + hide: (): void => { + hideFn(); + }, + // 销毁tip + destroy: (): void => { + // 销毁tip前先隐藏 + hideFn(); + } + }; + } + + // 根据trigger配置为宿主元素添加事件,该事件用于控制tip的显示/隐藏 + private addTriggerEvent(hostEle: Element, config: TiTipConfig, tipInstance: TiTipRef, shareValues: ShareValues): void { + // 非mouse情况下,不用做事件处理 + if (config.trigger !== 'mouse') { + return; + } + let tipComponentRef: ComponentRef = null; + // 默认情况下,使用mouse进行tip显示和隐藏控制(只对指令形式有效) + this.zone.runOutsideAngular(() => { + const unlistenMouseenterFn: () => void = this.render.listen(hostEle, 'mouseenter', () => { + if (typeof config.showFn !== 'function') { + return; + } + shareValues.mouseenterTimer = setTimeout(() => { + this.zone.run(() => { + const showInfo: TiTipShowInfo = config.showFn(); + if (!showInfo) { + return; + } + tipComponentRef = tipInstance.show(showInfo.content, showInfo.context); + if (!tipComponentRef) { + return; + } + // 根据trigger配置添加tip元素本身事件,此处事件用于支持移出tip元素时tip消失 + const targetEle: Element = tipComponentRef.location.nativeElement; + // eslint-disable-next-line max-nested-callbacks + this.render.listen(targetEle, 'mouseleave', (event: MouseEvent) => { + /** + * 此处处理是为了解决Chrome高版本下,连续点击tip区域情况下,导致tip消失的问题 + * 【问题原因】chrome高版本(chrome60以上版本)下,连续的click事件会触发tipEle的mouseleave事件, + * 从而导致tip消失 + * 【解决方案】如mouseleve事件是由tip元素本身点击触发的,则event.relatedTarget为null,则通过该 + * 方式进行特殊情况排除 + */ + if (event.relatedTarget === null) { + return; + } + tipInstance.hide(); + tipComponentRef = null; + }); + }); + }, TiTipService.MOUSE_ENTER_DELAY); + }); + const unlistenMouseleaveFn: () => void = this.render.listen(hostEle, 'mouseleave', (event: MouseEvent) => { + if (shareValues?.mouseenterTimer !== undefined) { + clearTimeout(shareValues?.mouseenterTimer); + } + // 鼠标移入tip时,tip不消失 + if (tipComponentRef && !tipComponentRef.location.nativeElement.contains(event.relatedTarget)) { + this.zone.run(() => { + tipInstance.hide(); + tipComponentRef = null; + }); + } + }); + // 给实例添加销毁方法 + tipInstance.destroy = (): void => { + // 先隐藏tip示例再取消监听事件 + tipInstance.hide(); + unlistenMouseenterFn(); + unlistenMouseleaveFn(); + }; + }); + } + + private showTip(options: TiTipShowConfig): ComponentRef { + const tipComponentRef: ComponentRef = options.popInstance.show({ + content: options.content, + contentContext: options.context, + context: { zIndex: options.config.zIndex }, + container: 'body' + }); + const targetEle: Element = tipComponentRef.location.nativeElement; + this.tiRenderer.setStyles(targetEle, { + left: '-9999px', + top: '-9999px' + }); + this.setTipTheme(targetEle, options.config); + // 计算元素宽高时,需要确保元素已生成 + this.setTipWidth(targetEle, options.config); + this.setPosition(options.hostEle, targetEle, options.config); + + return tipComponentRef; + } + + private setTipTheme(ele: Element, config: TiTipConfig): void { + if (!config || !config.theme) { + return; + } + if (config.theme === 'white') { + this.render.addClass(ele, 'ti3-tip-white-theme'); + } + } + + private setTipWidth(ele: Element, config: TiTipConfig): void { + const maxWidth: number = parseInt((config && config.maxWidth) || (TiTipService.DEFAULT_WIDTH as any), 10); + // 修复SSR错误:ERROR TypeError: ele.getBoundingClientRect is not a function + if (typeof ele.getBoundingClientRect !== 'function') { + return; + } + const targetWidth: number = ele.getBoundingClientRect().width; + if (targetWidth > maxWidth) { + // 如果宽度超过最大值,重新设置当前tip的内容宽度 + this.render.setStyle(ele, 'width', `${maxWidth}px`); + } + } + + private setPosition(hostEle: Element, targetEle: Element, config: TiTipConfig): void { + this.positionResult = Position.setPosition({ + targetEle, + hostEle, + hostEleX: config.hostEleX, + position: (config && config.position) || 'auto', + hostSpace: TiTipService.SPACE, + fixMaxHeight: true, + hasOffsetFix: true + }); + const position: TiPositionType = this.positionResult.position; + if (!config || config.hasArrow !== false) { + // 未定义或定义为false情况下都需要加该样式类控制三角样式 + this.render.addClass(targetEle, `ti3-tooltip-${position}`); + } + // 设置该样式类是为了支持鼠标移入Tip不消失:使用该样式类用于确保tip和宿主元素连接处的DOM在tip范围内 + this.render.addClass(targetEle.querySelector('.ti3-tooltip-sqr'), `ti3-tooltip-${position}-sqr`); + // 设置当前tip的内容最大高度,超出显示滚动条 + this.tiRenderer.setStyles(targetEle.querySelector('.ti3-tooltip-content'), { + // targetEle的最大高度 = 可视区域高度avilableHeight - 11 * 2 (tip的横向padding) - 1 * 2(border) + maxHeight: `${this.positionResult.avilableHeight - 22 - 2}px` + }); + } +} diff --git a/src/tip/lib/src/TiTipServiceModule.ts b/src/tip/lib/src/TiTipServiceModule.ts new file mode 100644 index 0000000..69ad2d7 --- /dev/null +++ b/src/tip/lib/src/TiTipServiceModule.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { TiTipContainerComponent } from './TiTipContainerComponent'; +import { TiPopupModule } from '@opentiny/ng-popup'; +import { TiRendererModule } from '@opentiny/ng-renderer'; + +/** + * @ignore + */ +@NgModule({ + imports: [TiPopupModule, TiRendererModule], + exports: [], + declarations: [TiTipContainerComponent], + entryComponents: [TiTipContainerComponent] +}) +export class TiTipServiceModule {} diff --git a/src/tip/lib/src/tip.less b/src/tip/lib/src/tip.less new file mode 100644 index 0000000..bcf1f01 --- /dev/null +++ b/src/tip/lib/src/tip.less @@ -0,0 +1,343 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-tip-before-square-size: 10px; + --ti-tip-square-distance: 5px; // tip小三角的尖 到 宿主元素的距离 + --ti-tip-before-square-height: 9px; // (10*cos45=7)+1px边框 + 1px调整保证移动之后无缝隙 + --ti-tip-sqr-height: calc(var(--ti-tip-square-distance) + var(--ti-tip-before-square-height)); // sqr元素的宽、高 + // 4.0规范 tip都是深色,根据intro组件需求,添加white主题 + --ti-tip-white-theme-bg: var(--ti-common-color-bg-white-normal); + --ti-tip-z-index: 5000; + --ti-tip-offset: 9px; // 非居中对齐时,小箭头的偏移量 + --ti-tip-box-shadow-color: rgba(0, 0, 0, 0.2); +} + +:host.ti3-tooltip { + display: block; + position: absolute; + background-color: var(--ti-common-color-bg-dark-deep); + color: var(--ti-common-color-text-gray); + padding: calc(var(--ti-common-space-3x) - 1px) calc(var(--ti-common-space-4x) - 1px); //tip的内容区域的padding:1px,此处减去1px与规范保持一致 + border: 1px solid var(--ti-common-color-bg-dark-deep); + box-shadow: var(--ti-common-shadow-3-down); + border-radius: var(--ti-common-border-radius-1); + .box-sizing(border-box); + z-index: var(--ti-tip-z-index); + white-space: normal; // 自动换行属性,normal为默认值,此处处理为了防止外部样式影响 + word-wrap: break-word; // 换行方式:单词整体换行 + line-height: var(--ti-common-line-height-number); + &.ti3-tip-white-theme { + background-color: var(--ti-tip-white-theme-bg); + color: var(--ti-common-color-text-secondary); + border-color: var(--ti-common-color-bg-white-normal); + &::after, + &::before { + background: var(--ti-tip-white-theme-bg); + } + &.ti3-tooltip-top, + &.ti3-tooltip-top-left, + &.ti3-tooltip-top-right, + &.ti3-tooltip-bottom, + &.ti3-tooltip-bottom-right, + &.ti3-tooltip-bottom-left, + &.ti3-tooltip-left, + &.ti3-tooltip-left-top, + &.ti3-tooltip-left-bottom, + &.ti3-tooltip-right, + &.ti3-tooltip-right-top, + &.ti3-tooltip-right-bottom { + &::before { + border-left-color: var(--ti-common-color-bg-white-normal); + border-top-color: var(--ti-common-color-bg-white-normal); + } + } + & .ti3-tooltip-content { + .box-sizing(border-box); + overflow-y: auto; + padding: 1px; + } + } +} + +/** +* 此处设置padding:1px解决的问题是: +* 【问题现象】:ie浏览器下,汉字且字数较少时,未达到最大高度出现滚动条,主要是字体为汉字时会出现这个现象 +* 【出现该问题的原因】:属浏览器对字体解析的问题,具体原因不明 +* 【解决办法】:将内容区域的padding增加1px,修正因ie浏览器字体解析产生的偏差 +*/ + +.ti3-tooltip-content { + .box-sizing(border-box); + overflow-y: auto; + padding: 1px; + font-weight: var(--ti-common-font-weight-4); + font-size: var(--ti-common-font-size-base); + ::ng-deep a { + color: var(--ti-common-color-text-link-darkbg); + text-decoration: none; + &:hover { + color: var(--ti-common-color-text-link-darkbg-hover); + text-decoration: underline; + } + } +} + +//**-sqr元素为触发tooltip的内容区域与tooltip之间的连接的矩形过度区域 +// 提取-sqr的公共样式变量 +.ti3-tooltip-position-sqr(@width, @height) { + position: absolute; + background-color: transparent; + box-sizing: border-box; + width: @width; + height: @height; +} + +// 提取before的公共样式 +.ti3-tooltip-position-before { + display: block; + position: absolute; + border-left: 1px solid var(--ti-common-color-bg-dark-deep); + border-top: v1px solid var(--ti-common-color-bg-dark-deep); + content: ''; + width: var(--ti-tip-before-square-size); + height: var(--ti-tip-before-square-size); + overflow: hidden; + .rotate(45deg); + background: var(--ti-common-color-bg-dark-deep); +} +// 提取after的公共样式 +.ti3-tooltip-position-after(@width, @height) { + display: block; + position: absolute; + content: ''; + overflow: hidden; + background: var(--ti-common-color-bg-dark-deep); + width: @width; + height: @height; +} +.ti3-tooltip-bottom-sqr, +.ti3-tooltip-bottom-left-sqr, +.ti3-tooltip-bottom-right-sqr { + .ti3-tooltip-position-sqr(100%, var(--ti-tip-sqr-height)); + left: 0; + bottom: 100%; +} + +//before元素为旋转形成三角形的正方形,after元素为遮挡before元素另一半的三角的长方形 + +:host.ti3-tooltip-bottom::after { + .ti3-tooltip-position-after(20px, 10px); + left: 50%; + margin-left: -10px; + top: 0; +} +:host.ti3-tooltip-bottom::before { + .ti3-tooltip-position-before(); + left: 50%; + margin-left: calc(-1 * var(--ti-tip-before-square-size) / 2); + top: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px -1px 5px var(--ti-tip-box-shadow-color); +} +// position: bottom-left +:host.ti3-tooltip-bottom-left::after { + .ti3-tooltip-position-after(20px, 10px); + left: calc(var(--ti-tip-offset) - 3px); + top: 0; +} + +:host.ti3-tooltip-bottom-left::before { + .ti3-tooltip-position-before(); + left: var(--ti-tip-offset); + top: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: bottom-right +:host.ti3-tooltip-bottom-right::after { + .ti3-tooltip-position-after(20px, 10px); + right: calc(var(--ti-tip-offset) - 3px); + top: 0; +} + +:host.ti3-tooltip-bottom-right::before { + .ti3-tooltip-position-before(); + right: var(--ti-tip-offset); + top: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: top +.ti3-tooltip-top-sqr, +.ti3-tooltip-top-left-sqr, +.ti3-tooltip-top-right-sqr { + .ti3-tooltip-position-sqr(100%, var(--ti-tip-sqr-height)); + left: 0; + top: 100%; +} + +:host.ti3-tooltip-top::after { + .ti3-tooltip-position-after(20px, 10px); + left: 50%; + margin-left: -10px; + bottom: 0; +} + +:host.ti3-tooltip-top::before { + .ti3-tooltip-position-before(); + transform: rotate(-135deg); + left: 50%; + margin-left: calc(-1 * var(--ti-tip-before-square-size) / 2); + bottom: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: top-left +:host.ti3-tooltip-top-left::after { + .ti3-tooltip-position-after(20px, 10px); + left: calc(var(--ti-tip-offset) - 3px); + bottom: 0; +} + +:host.ti3-tooltip-top-left::before { + .ti3-tooltip-position-before(); + transform: rotate(-135deg); + left: var(--ti-tip-offset); + bottom: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: top-right +:host.ti3-tooltip-top-right::after { + .ti3-tooltip-position-after(20px, 10px); + right: calc(var(--ti-tip-offset) - 3px); + bottom: 0; +} + +:host.ti3-tooltip-top-right::before { + .ti3-tooltip-position-before(); + transform: rotate(-135deg); + right: var(--ti-tip-offset); + bottom: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: left +.ti3-tooltip-left-sqr, +.ti3-tooltip-left-top-sqr, +.ti3-tooltip-left-bottom-sqr { + .ti3-tooltip-position-sqr(var(--ti-tip-sqr-height), 100%); + top: 0; + left: 100%; +} + +:host.ti3-tooltip-left::after { + .ti3-tooltip-position-after(10px, 20px); + top: 50%; + margin-top: -10px; + right: 0; +} + +:host.ti3-tooltip-left::before { + .ti3-tooltip-position-before(); + transform: rotate(135deg); + top: 50%; + margin-top: calc(-1 * var(--ti-tip-before-square-size) / 2); + right: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: 1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: left-top +:host.ti3-tooltip-left-top::after { + .ti3-tooltip-position-after(10px, 20px); + top: calc(var(--ti-tip-offset) - 3px); + right: 0; +} + +:host.ti3-tooltip-left-top::before { + .ti3-tooltip-position-before(); + transform: rotate(135deg); + content: ''; + top: var(--ti-tip-offset); + right: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: 1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: left-bottom +:host.ti3-tooltip-left-bottom::after { + .ti3-tooltip-position-after(10px, 20px); + bottom: calc(var(--ti-tip-offset) - 3px); + right: 0; +} + +:host.ti3-tooltip-left-bottom::before { + .ti3-tooltip-position-before(); + transform: rotate(135deg); + bottom: var(--ti-tip-offset); + right: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: 1px -1px 5px var(--ti-tip-box-shadow-color); +} + +// position: right +.ti3-tooltip-right-sqr, +.ti3-tooltip-right-top-sqr, +.ti3-tooltip-right-bottom-sqr { + .ti3-tooltip-position-sqr(var(--ti-tip-sqr-height), 100%); + top: 0; + right: 100%; +} + +:host.ti3-tooltip-right::after { + .ti3-tooltip-position-after(10px, 20px); + top: 50%; + margin-top: -10px; + left: 0; +} + +:host.ti3-tooltip-right::before { + .ti3-tooltip-position-before(); + transform: rotate(-45deg); + top: 50%; + margin-top: calc(-1 * var(--ti-tip-before-square-size) / 2); + left: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px 1px 5px var(--ti-tip-box-shadow-color); +} + +// position: right-top +:host.ti3-tooltip-right-top::after { + .ti3-tooltip-position-after(10px, 20px); + top: calc(var(--ti-tip-offset) - 3px); + left: 0; +} + +:host.ti3-tooltip-right-top::before { + .ti3-tooltip-position-before(); + transform: rotate(-45deg); + top: var(--ti-tip-offset); + left: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px 1px 5px var(--ti-tip-box-shadow-color); +} + +// position: right-bottom +:host.ti3-tooltip-right-bottom::after { + .ti3-tooltip-position-after(10px, 20px); + bottom: calc(var(--ti-tip-offset) - 3px); + left: 0; +} + +:host.ti3-tooltip-right-bottom::before { + .ti3-tooltip-position-before(); + transform: rotate(-45deg); + bottom: var(--ti-tip-offset); + left: calc(-1 * var(--ti-tip-before-square-size) / 2); + box-shadow: -1px 1px 5px var(--ti-tip-box-shadow-color); +} + +/* ---------------设置tip克隆DOM的样式----START----------------------------------------------*/ +.ti3-tooltip-clone { + overflow: visible !important; + position: absolute !important; + visibility: hidden !important; + .box-sizing(border-box); + width: auto !important; +} +/* ---------------设置tip克隆DOM的样式----END----------------------------------------------*/ diff --git a/src/transfer/demo/karma.conf.js b/src/transfer/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/transfer/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/transfer/demo/project.json b/src/transfer/demo/project.json new file mode 100644 index 0000000..6bba26c --- /dev/null +++ b/src/transfer/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/transfer/demo", + "sourceRoot": "src/transfer/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/transfer", + "index": "src/transfer/demo/src/index.html", + "main": "src/transfer/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/transfer/demo/tsconfig.app.json", + "assets": ["src/transfer/demo/src/favicon.ico", "src/transfer/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "transfer-demo:build:production" + }, + "development": { + "browserTarget": "transfer-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js transfer" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/transfer/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/transfer/demo/tsconfig.spec.json", + "karmaConfig": "src/transfer/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/transfer/demo/src/app/AppComponent.ts b/src/transfer/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/transfer/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/transfer/demo/src/app/AppModule.ts b/src/transfer/demo/src/app/AppModule.ts new file mode 100644 index 0000000..3d8b709 --- /dev/null +++ b/src/transfer/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TransferTestModule } from './transfer/TransferTestModule'; + +@NgModule({ + imports: [ + TransferTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/transfer/demo/src/app/IndexComponent.ts b/src/transfer/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..fb2707c --- /dev/null +++ b/src/transfer/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TransferTestModule } from './transfer/TransferTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TransferTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/transfer/demo/src/app/app.html b/src/transfer/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/transfer/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/transfer/demo/src/app/transfer/TransferBasicComponent.ts b/src/transfer/demo/src/app/transfer/TransferBasicComponent.ts new file mode 100644 index 0000000..8b896a2 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferBasicComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { myOptions, mySelecteds } from './data.js'; +@Component({ + templateUrl: './transfer-basic.html' +}) +export class TransferBasicComponent { + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + mySelecteds: Array = JSON.parse(JSON.stringify(mySelecteds)); +} diff --git a/src/transfer/demo/src/app/transfer/TransferDisabledComponent.ts b/src/transfer/demo/src/app/transfer/TransferDisabledComponent.ts new file mode 100644 index 0000000..cce71ae --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferDisabledComponent.ts @@ -0,0 +1,44 @@ +import { Component, ChangeDetectorRef } from '@angular/core'; +import { mySelecteds } from './data.js'; +@Component({ + templateUrl: './transfer-disabled.html' +}) +export class TransferDisabledComponent { + constructor(protected changeDetectorRef: ChangeDetectorRef) {} + + myOptions: Array = [ + { + label: '一帆风顺' + }, + { + label: '二龙戏珠', + disabled: true + }, + { + label: '三阳开泰', + disabled: false + }, + { + label: '四季发财' + }, + { + label: '五福临门' + }, + { + label: '六六大顺' + }, + { + label: '七星高照' + }, + { + label: '八面来风' + }, + { + label: '九九归一' + }, + { + label: '十全十美' + } + ]; + mySelecteds: Array = JSON.parse(JSON.stringify(mySelecteds)); +} diff --git a/src/transfer/demo/src/app/transfer/TransferEventComponent.ts b/src/transfer/demo/src/app/transfer/TransferEventComponent.ts new file mode 100644 index 0000000..a97ce82 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferEventComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { myOptions } from './data.js'; +@Component({ + templateUrl: './transfer-event.html' +}) +export class TransferEventComponent { + myLogs: Array = []; + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + mySelecteds: Array = []; + + transferToLeft(event: object): void { + this.myLogs = [...this.myLogs, `transferToLeft event = ${JSON.stringify(event)}}`]; + } + + transferToRight(event: object): void { + this.myLogs = [...this.myLogs, `transferToRight event = ${JSON.stringify(event)}}`]; + } + + onNgModelChange(event: object): void { + this.myLogs = [...this.myLogs, `onNgModelChange event = ${JSON.stringify(event)}}`]; + } +} diff --git a/src/transfer/demo/src/app/transfer/TransferIdComponent.ts b/src/transfer/demo/src/app/transfer/TransferIdComponent.ts new file mode 100644 index 0000000..bbd06a7 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferIdComponent.ts @@ -0,0 +1,38 @@ +import { Component, Inject } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { myOptions, mySelecteds } from './data.js'; +@Component({ + templateUrl: './transfer-id.html' +}) +export class TransferIdComponent { + // 设置穿梭框源数据 + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + // 设置穿梭框初始选中项,选中项会移动到右侧面板 + mySelecteds: Array = JSON.parse(JSON.stringify(mySelecteds)); + ids: Array = [ + 'myTransfer', + 'myTransfer_left_list_list_list', + 'myTransfer_left_list_list_0', + 'myTransfer_left_list_select_all', + 'myTransfer_to_left_button', + 'myTransfer_right_list_list_list', + 'myTransfer_right_list_list_0', + 'myTransfer_right_list_select_all', + 'myTransfer_to_right_button' + ]; + + idExistMap: Map = new Map(); + allIdExist: boolean = false; + // 修复SSR报错:ERROR ReferenceError: document is not defined + constructor(@Inject(DOCUMENT) private document) {} + ngDoCheck(): void { + this.allIdExist = true; + this.ids.forEach((id: string) => { + const idExist: boolean = this.document.getElementById(id) !== undefined; + this.idExistMap.set(id, idExist); + if (!idExist) { + this.allIdExist = false; + } + }); + } +} diff --git a/src/transfer/demo/src/app/transfer/TransferIdkeyComponent.ts b/src/transfer/demo/src/app/transfer/TransferIdkeyComponent.ts new file mode 100644 index 0000000..f0b8757 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferIdkeyComponent.ts @@ -0,0 +1,55 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './transfer-idkey.html' +}) +export class TransferIdkeyComponent { + // 设置穿梭框源数据 + myOptions: Array = [ + { + id: 1, + label: '一帆风顺' + }, + { + id: 2, + label: '一帆风顺' + }, + { + id: 3, + label: '一帆风顺' + }, + { + id: 4, + label: '四季发财' + }, + { + id: 5, + label: '五福临门' + }, + { + id: 6, + label: '六六大顺' + }, + { + id: 7, + label: '七星高照' + }, + { + id: 8, + label: '八面来风' + }, + { + id: 9, + label: '九九归一' + }, + { + id: 10, + label: '十全十美' + } + ]; + mySelecteds: Array = [ + { + id: 4, + label: '四季发财' + } + ]; +} diff --git a/src/transfer/demo/src/app/transfer/TransferLabelkeyComponent.ts b/src/transfer/demo/src/app/transfer/TransferLabelkeyComponent.ts new file mode 100644 index 0000000..e8f2000 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferLabelkeyComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { valueOptions } from './data.js'; +@Component({ + templateUrl: './transfer-labelkey.html' +}) +export class TransferLabelkeyComponent { + myOptions: Array = JSON.parse(JSON.stringify(valueOptions)); + mySelecteds: Array = []; + labelKey: string = 'label'; +} diff --git a/src/transfer/demo/src/app/transfer/TransferLazyComponent.ts b/src/transfer/demo/src/app/transfer/TransferLazyComponent.ts new file mode 100644 index 0000000..5078ec7 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferLazyComponent.ts @@ -0,0 +1,21 @@ +import { Component, ChangeDetectorRef } from '@angular/core'; +import { myOptions, mySelecteds } from './data.js'; +@Component({ + templateUrl: './transfer-lazy.html' +}) +export class TransferLazyComponent { + constructor(protected changeDetectorRef: ChangeDetectorRef) {} + // 设置穿梭框源数据 + myOptions: Array = []; + // 设置穿梭框初始选中项,选中项会移动到右侧面板 + mySelecteds: Array = []; + + ngOnInit(): void { + setTimeout(() => { + this.myOptions = JSON.parse(JSON.stringify(myOptions)); + this.mySelecteds = JSON.parse(JSON.stringify(mySelecteds)); + // OnPush模式下,需要手动更新数据 + this.changeDetectorRef.markForCheck(); + }, 1000); + } +} diff --git a/src/transfer/demo/src/app/transfer/TransferLoadComponent.ts b/src/transfer/demo/src/app/transfer/TransferLoadComponent.ts new file mode 100644 index 0000000..5303fcb --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferLoadComponent.ts @@ -0,0 +1,85 @@ +import { Component } from '@angular/core'; +import { myOptions } from './data.js'; +@Component({ + templateUrl: './transfer-load.html', + styleUrls: ['./transfer.less'] +}) +export class TransferLoadComponent { + private dataA: Array = JSON.parse(JSON.stringify(myOptions)); + + private dataB: Array = [ + { label: '水调歌头' }, + { label: '苏轼' }, + { label: '明月几时有?把酒问青天' }, + { label: '不知天上宫阙,今夕是何年。' }, + { label: '我欲乘风归去,又恐琼楼玉宇,高处不胜寒。' }, + { label: '起舞弄清影,何似在人间?' }, + { label: '转朱阁,低绮户,照无眠。' }, + { label: '不应有恨,何事长向别时圆?' }, + { label: '人有悲欢离合,月有阴晴圆缺,此事古难全。' }, + { label: '但愿人长久,千里共婵娟。' } + ]; + + private dataC: Array = [ + { + label: '三阳开泰' + }, + { + label: '四季发财' + }, + { + label: '五福临门' + }, + { + label: '六六大顺' + }, + { + label: '七星高照' + }, + { + label: '八面来风' + }, + { + label: '九九归一' + }, + { + label: '十全十美' + } + ]; + + // 设置穿梭框源数据 + myOptions: Array; + // 设置穿梭框初始选中项,选中项会移动到右侧面板 + mySelecteds: Array; + + // 每个组件都用下面六个函数,只改变函数内容 + changeUndefined(): void { + this.myOptions = undefined; + } + changeNull(): void { + this.myOptions = null; + } + changeWrongType(): void { + const temp: any = 5; + this.myOptions = temp; + } + changeZeroData(): void { + this.myOptions = []; + } + changeDataA(): void { + this.myOptions = this.dataA; + this.mySelecteds = [this.myOptions[0], this.myOptions[1]]; + } + changeDataB(): void { + this.myOptions = this.dataB; + this.mySelecteds = [this.myOptions[0], this.myOptions[1]]; + } + changeDataC(): void { + this.myOptions = this.dataC; + } + + // 改变选中项 + changeSelects(): void { + this.mySelecteds = [this.myOptions[2], this.myOptions[3]]; + } +} diff --git a/src/transfer/demo/src/app/transfer/TransferNodatatextComponent.ts b/src/transfer/demo/src/app/transfer/TransferNodatatextComponent.ts new file mode 100644 index 0000000..7cc6dcd --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferNodatatextComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { myOptions } from './data.js'; +@Component({ + templateUrl: './transfer-nodatatext.html' +}) +export class TransferNodatatextComponent { + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + mySelecteds: Array = []; + noDataText: string = '暂无列表数据'; +} diff --git a/src/transfer/demo/src/app/transfer/TransferPaginationComponent.ts b/src/transfer/demo/src/app/transfer/TransferPaginationComponent.ts new file mode 100644 index 0000000..f8e251c --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferPaginationComponent.ts @@ -0,0 +1,34 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './transfer-pagination.html' +}) +export class TransferPaginationComponent { + myOptions: Array; + mySelecteds: Array; + myOptionsSearchable: Array; + mySelectedsSearchable: Array; + searchable: boolean = true; + // 设置搜索字段, 该数组中设置多个字段,就会根据该数组中的任意一个字段进行搜索匹配 + searchKeys: Array = ['label']; + private database: Array = [ + { label: 'America', disabled: true }, + { label: 'Brazil' }, + { label: 'Canada', disabled: true }, + { label: 'China' }, + { label: 'France', disabled: true }, + { label: 'Germany' }, + { label: 'Japan' }, + { label: 'South Korea' }, + { label: 'Turkey' }, + { label: 'United Kingdom' } + ]; + ngOnInit(): void { + const data: Array = []; + for (let i: number = 0; i < 136; i++) { + const item: any = this.database[i % 10]; + data.push({ ...item, label: i + item.label }); + } + this.myOptions = data; + this.myOptionsSearchable = [...data]; + } +} diff --git a/src/transfer/demo/src/app/transfer/TransferPlaceholderComponent.ts b/src/transfer/demo/src/app/transfer/TransferPlaceholderComponent.ts new file mode 100644 index 0000000..a2229b7 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferPlaceholderComponent.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { myOptions, mySelecteds } from './data.js'; +@Component({ + templateUrl: './transfer-placeholder.html' +}) +export class TransferPlaceholderComponent { + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + mySelecteds: Array = JSON.parse(JSON.stringify(mySelecteds)); + searchable: boolean = true; + placeholder: string = '请搜索'; + + // 改变placeholder的内容 + changePlaceholder(): void { + this.placeholder = '请搜索关键字'; + } +} diff --git a/src/transfer/demo/src/app/transfer/TransferSearchableComponent.ts b/src/transfer/demo/src/app/transfer/TransferSearchableComponent.ts new file mode 100644 index 0000000..0d95667 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferSearchableComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { myOptions, mySelecteds } from './data.js'; +@Component({ + templateUrl: './transfer-searchable.html' +}) +export class TransferSearchableComponent { + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + mySelecteds: Array = JSON.parse(JSON.stringify(mySelecteds)); + searchable: boolean = true; +} diff --git a/src/transfer/demo/src/app/transfer/TransferSearchkeysComponent.ts b/src/transfer/demo/src/app/transfer/TransferSearchkeysComponent.ts new file mode 100644 index 0000000..2d8369f --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferSearchkeysComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { valueOptions } from './data.js'; +@Component({ + templateUrl: './transfer-searchkeys.html' +}) +export class TransferSearchkeysComponent { + // 设置穿梭框源数据 + myOptions: Array = JSON.parse(JSON.stringify(valueOptions)); + // 设置穿梭框初始选中项,选中项会移动到右侧面板 + mySelecteds: Array = [ + { + label: '四季发财', + name: '心远地自偏' + } + ]; + searchable: boolean = true; + // 设置搜索字段, 该数组中设置多个字段,就会根据该数组中的任意一个字段进行搜索匹配 + searchKeys: Array = ['label', 'name']; +} diff --git a/src/transfer/demo/src/app/transfer/TransferSizeComponent.ts b/src/transfer/demo/src/app/transfer/TransferSizeComponent.ts new file mode 100644 index 0000000..48091ce --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferSizeComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { myOptions, mySelecteds } from './data.js'; +@Component({ + templateUrl: './transfer-size.html' +}) +export class TransferSizeComponent { + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + mySelecteds: Array = JSON.parse(JSON.stringify(mySelecteds)); +} diff --git a/src/transfer/demo/src/app/transfer/TransferTableComponent.ts b/src/transfer/demo/src/app/transfer/TransferTableComponent.ts new file mode 100644 index 0000000..a1b7e11 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferTableComponent.ts @@ -0,0 +1,56 @@ +import { Component } from '@angular/core'; +import { TiTableRowData, TiTransferColumn } from '@opentiny/ng'; +@Component({ + templateUrl: './transfer-table.html' +}) +export class TransferTableComponent { + myOptions: Array; + mySelecteds: Array; + myOptions1: Array; + mySelecteds1: Array; + myOptions2: Array; + mySelecteds2: Array; + searchable: boolean = true; + searchKeys: Array = ['firstName', 'lastName']; + columns: Array = [ + { + title: 'First Name', // 表头列文本内容 + field: 'firstName', // 不自定义行展示模板时,表格每列按照源数据哪一项属性展示数据的标识值 + width: '40%' + }, + { + title: 'Last Name', + field: 'lastName', + width: '30%' + }, + { + title: 'Age', + field: 'age', + width: '30%' + } + ]; + ngOnInit(): void { + const data: Array = []; + for (let j: number = 0; j < 100; j++) { + data.push(this.createRandomItem(j)); + } + this.myOptions = data; + this.myOptions1 = JSON.parse(JSON.stringify(this.myOptions)); + this.myOptions2 = JSON.parse(JSON.stringify(this.myOptions)); + } + private createRandomItem(id: number): TiTableRowData { + const nameList: Array = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa']; + const familyName: Array = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez']; + const firstName: string = nameList[Math.floor(Math.random() * 5)]; + const lastName: string = familyName[Math.floor(Math.random() * 5)]; + const age: number = ((id + 3) * 13) % 100; + const disabled: boolean = Math.random() > 0.7 ? true : false; + + return { + firstName, + lastName, + age, + disabled + }; + } +} diff --git a/src/transfer/demo/src/app/transfer/TransferTestModule.ts b/src/transfer/demo/src/app/transfer/TransferTestModule.ts new file mode 100644 index 0000000..b8d72b6 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferTestModule.ts @@ -0,0 +1,122 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiButtonModule, TiIconModule, TiOverflowModule, TiPaginationModule, TiTransferModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TransferBasicComponent } from './TransferBasicComponent'; +import { TransferLazyComponent } from './TransferLazyComponent'; +import { TransferSizeComponent } from './TransferSizeComponent'; +import { TransferLabelkeyComponent } from './TransferLabelkeyComponent'; +import { TransferNodatatextComponent } from './TransferNodatatextComponent'; +import { TransferEventComponent } from './TransferEventComponent'; +import { TransferTitlesComponent } from './TransferTitlesComponent'; +import { TransferDisabledComponent } from './TransferDisabledComponent'; +import { TransferLoadComponent } from './TransferLoadComponent'; +import { TransferSearchableComponent } from './TransferSearchableComponent'; +import { TransferSearchkeysComponent } from './TransferSearchkeysComponent'; +import { TransferPlaceholderComponent } from './TransferPlaceholderComponent'; +import { TransferIdkeyComponent } from './TransferIdkeyComponent'; +import { TransferPaginationComponent } from './TransferPaginationComponent'; +import { TransferIdComponent } from './TransferIdComponent'; +import { TransferTableComponent } from './TransferTableComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiButtonModule, + TiPaginationModule, + TiIconModule, + TiOverflowModule, + TiTransferModule, + DemoLogModule, + RouterModule.forChild(TransferTestModule.ROUTES) + ], + declarations: [ + TransferBasicComponent, + TransferLazyComponent, + TransferSizeComponent, + TransferLabelkeyComponent, + TransferNodatatextComponent, + TransferEventComponent, + TransferTitlesComponent, + TransferDisabledComponent, + TransferLoadComponent, + TransferSearchableComponent, + TransferSearchkeysComponent, + TransferPlaceholderComponent, + TransferIdkeyComponent, + TransferPaginationComponent, + TransferIdComponent, + TransferTableComponent + ] +}) +export class TransferTestModule { + static readonly LINKS: Array = [{ href: 'components/TiTransferComponent.html', label: 'Transfer' }]; + static readonly ROUTES: Routes = [ + { + path: 'transfer/transfer-basic', + component: TransferBasicComponent + }, + { + path: 'transfer/transfer-lazy', + component: TransferLazyComponent + }, + { + path: 'transfer/transfer-size', + component: TransferSizeComponent + }, + { + path: 'transfer/transfer-labelkey', + component: TransferLabelkeyComponent + }, + { + path: 'transfer/transfer-nodatatext', + component: TransferNodatatextComponent + }, + { + path: 'transfer/transfer-titles', + component: TransferTitlesComponent + }, + { + path: 'transfer/transfer-event', + component: TransferEventComponent + }, + { + path: 'transfer/transfer-disabled', + component: TransferDisabledComponent + }, + { + path: 'transfer/transfer-load', + component: TransferLoadComponent + }, + { + path: 'transfer/transfer-searchable', + component: TransferSearchableComponent + }, + { + path: 'transfer/transfer-searchkeys', + component: TransferSearchkeysComponent + }, + { + path: 'transfer/transfer-placeholder', + component: TransferPlaceholderComponent + }, + { + path: 'transfer/transfer-idkey', + component: TransferIdkeyComponent + }, + { + path: 'transfer/transfer-pagination', + component: TransferPaginationComponent + }, + { + path: 'transfer/transfer-table', + component: TransferTableComponent + }, + { path: 'transfer/transfer-id', component: TransferIdComponent } + ]; +} diff --git a/src/transfer/demo/src/app/transfer/TransferTitlesComponent.ts b/src/transfer/demo/src/app/transfer/TransferTitlesComponent.ts new file mode 100644 index 0000000..82c02a4 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/TransferTitlesComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { myOptions } from './data.js'; +@Component({ + templateUrl: './transfer-titles.html' +}) +export class TransferTitlesComponent { + myOptions: Array = JSON.parse(JSON.stringify(myOptions)); + mySelecteds: Array = []; +} diff --git a/src/transfer/demo/src/app/transfer/data.js b/src/transfer/demo/src/app/transfer/data.js new file mode 100644 index 0000000..60f7a7d --- /dev/null +++ b/src/transfer/demo/src/app/transfer/data.js @@ -0,0 +1,81 @@ +export const myOptions = [ + { + label: '一帆风顺', + }, + { + label: '二龙戏珠', + }, + { + label: '三阳开泰', + }, + { + label: '四季发财', + }, + { + label: '五福临门', + }, + { + label: '六六大顺', + }, + { + label: '七星高照', + }, + { + label: '八面来风', + }, + { + label: '九九归一', + }, + { + label: '十全十美', + }, +]; + +export const valueOptions = [ + { + label: '一帆风顺', + name: '结庐在人境', + }, + { + label: '二龙戏珠', + name: '而无车马喧', + }, + { + label: '三阳开泰', + name: '问君何能尔', + }, + { + label: '四季发财', + name: '心远地自偏', + }, + { + label: '五福临门', + name: '采菊东篱下', + }, + { + label: '六六大顺', + name: '悠然见南山', + }, + { + label: '七星高照', + name: '山气日夕佳', + }, + { + label: '八面来风', + name: '飞鸟相与还', + }, + { + label: '九九归一', + name: '此中有真意', + }, + { + label: '十全十美', + name: '欲辨已忘言', + }, +]; + +export const mySelecteds = [ + { + label: '四季发财', + }, +]; diff --git a/src/transfer/demo/src/app/transfer/transfer-basic.html b/src/transfer/demo/src/app/transfer/transfer-basic.html new file mode 100644 index 0000000..dd8fa4b --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-basic.html @@ -0,0 +1,4 @@ + +
    +
    Current Select: {{ mySelecteds | json }}
    +
    diff --git a/src/transfer/demo/src/app/transfer/transfer-disabled.html b/src/transfer/demo/src/app/transfer/transfer-disabled.html new file mode 100644 index 0000000..df40058 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-disabled.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-event.html b/src/transfer/demo/src/app/transfer/transfer-event.html new file mode 100644 index 0000000..5d2acd2 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-event.html @@ -0,0 +1,9 @@ + + diff --git a/src/transfer/demo/src/app/transfer/transfer-id.html b/src/transfer/demo/src/app/transfer/transfer-id.html new file mode 100644 index 0000000..ac40e72 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-id.html @@ -0,0 +1,15 @@ +

    1.描述

    +

    transfer组件是否给所有可点击元素添加上了id

    +

    绑定id后,每一个可点击元素(或可以hover的交互元素),都自动有了id,哪些节点有了id,在本页面就可以查看,不必打开调试控制台。

    +

    导入

    +

    import {{ '{' }} TiTransferModule {{ '}' }} from '@opentiny/ng';

    +

    2.示例

    +
    + +
    +

    已经选中的项:{{ mySelecteds | json }}

    + +

    id是否存在:

    +

    {{id+':'+idExistMap.get(id)}}

    +

    所有id是否存在:{{allIdExist}}

    +
    diff --git a/src/transfer/demo/src/app/transfer/transfer-idkey.html b/src/transfer/demo/src/app/transfer/transfer-idkey.html new file mode 100644 index 0000000..9131afb --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-idkey.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-labelkey.html b/src/transfer/demo/src/app/transfer/transfer-labelkey.html new file mode 100644 index 0000000..80806cc --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-labelkey.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-lazy.html b/src/transfer/demo/src/app/transfer/transfer-lazy.html new file mode 100644 index 0000000..e7fc4d3 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-lazy.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-load.html b/src/transfer/demo/src/app/transfer/transfer-load.html new file mode 100644 index 0000000..520f600 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-load.html @@ -0,0 +1,14 @@ +

    1.描述

    +

    此处测试异常数据 1.changeWrongType 点击该按钮会报错 2.点击 changeUndefined changeNull changeWrongType 再点击 changeSelects 会报错

    + +
    +
    Current Select: {{ mySelecteds | json }}
    +
    + +
    + +
    + +
    + + diff --git a/src/transfer/demo/src/app/transfer/transfer-nodatatext.html b/src/transfer/demo/src/app/transfer/transfer-nodatatext.html new file mode 100644 index 0000000..6d66eba --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-nodatatext.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-pagination.html b/src/transfer/demo/src/app/transfer/transfer-pagination.html new file mode 100644 index 0000000..a42ebb8 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-pagination.html @@ -0,0 +1,11 @@ +

    1.分页场景

    + +

    2.分页+搜索场景

    + diff --git a/src/transfer/demo/src/app/transfer/transfer-placeholder.html b/src/transfer/demo/src/app/transfer/transfer-placeholder.html new file mode 100644 index 0000000..5385e70 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-placeholder.html @@ -0,0 +1,7 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-searchable.html b/src/transfer/demo/src/app/transfer/transfer-searchable.html new file mode 100644 index 0000000..a4ac1ff --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-searchable.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-searchkeys.html b/src/transfer/demo/src/app/transfer/transfer-searchkeys.html new file mode 100644 index 0000000..41f31ce --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-searchkeys.html @@ -0,0 +1,8 @@ + +
    diff --git a/src/transfer/demo/src/app/transfer/transfer-size.html b/src/transfer/demo/src/app/transfer/transfer-size.html new file mode 100644 index 0000000..fb57165 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-size.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer-table.html b/src/transfer/demo/src/app/transfer/transfer-table.html new file mode 100644 index 0000000..c37cb56 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-table.html @@ -0,0 +1,37 @@ +

    1.分页场景+配置每页显示条数为20

    + +

    2.搜索场景+自定义模板

    + + + {{row.firstName}} + {{row.lastName}} + {{row.age}} + + +

    3.分页+搜索场景

    + diff --git a/src/transfer/demo/src/app/transfer/transfer-titles.html b/src/transfer/demo/src/app/transfer/transfer-titles.html new file mode 100644 index 0000000..6721c22 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer-titles.html @@ -0,0 +1 @@ + diff --git a/src/transfer/demo/src/app/transfer/transfer.less b/src/transfer/demo/src/app/transfer/transfer.less new file mode 100644 index 0000000..39d5cb6 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/transfer.less @@ -0,0 +1,4 @@ +.transfer-button { + width: 200px; + margin: 10px; +} diff --git a/src/transfer/demo/src/app/transfer/webdoc/transfer-demos.js b/src/transfer/demo/src/app/transfer/webdoc/transfer-demos.js new file mode 100644 index 0000000..75c89a0 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/webdoc/transfer-demos.js @@ -0,0 +1,248 @@ +export default { + column: '1', + demos: [ + { + demoId: 'transfer-basic', + name: { 'zh-CN': '基本用法', 'en-US': 'transfer basic' }, + desc: { + 'zh-CN': '

    Transfer 的最简用法。

    ', + 'en-US': 'transfer basic' + }, + apis: [ 'TiTransferComponent.properties.options' ], + codeFiles: [ + 'transfer-basic.html', + 'TransferBasicComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'transfer disabled' + }, + desc: { + 'zh-CN': '

    通过属性options配置哪些数据被禁用

    ', + 'en-US': 'transfer disabled' + }, + codeFiles: [ + 'transfer-disabled.html', + 'TransferDisabledComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-lazy', + name: { + 'zh-CN': '懒加载', + 'en-US': 'transfer lazy' + }, + desc: { + 'zh-CN': '

    懒加载的场景。

    ', + 'en-US': 'transfer lazy' + }, + codeFiles: [ + 'transfer-lazy.html', + 'TransferLazyComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-size', + name: { + 'zh-CN': '自定义大小', + 'en-US': 'transfer size' + }, + desc: { + 'zh-CN': '

    通过属性widthheight配置宽高。

    ', + 'en-US': 'transfer size' + }, + apis: [ + 'TiTransferComponent.properties.height', + 'TiTransferComponent.properties.width' + ], + codeFiles: [ + 'transfer-size.html', + 'TransferSizeComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-labelkey', + name: { + 'zh-CN': '显示字段', + 'en-US': 'transfer labelkey' + }, + desc: { + 'zh-CN': '

    通过属性labelKey配置显示字段。

    ', + 'en-US': 'transfer labelkey' + }, + apis: [ + 'TiTransferComponent.properties.labelKey' + ], + codeFiles: [ + 'transfer-disabled.html', + 'TransferLabelkeyComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-nodatatext', + name: { + 'zh-CN': '无数据显示文本', + 'en-US': 'transfer nodatatext' + }, + desc: { + 'zh-CN': '

    通过属性noDataText配置无数据时显示的文本。

    ', + 'en-US': 'transfer nodatatext' + }, + apis: [ + 'TiTransferComponent.properties.noDataText' + ], + codeFiles: [ + 'transfer-nodatatext.html', + 'TransferNodatatextComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-titles', + name: { + 'zh-CN': '自定义面板头部标题', + 'en-US': 'transfer titles' + }, + desc: { + 'zh-CN': '

    通过属性leftTitlerightTitle配置左右面板的头部标题。

    ', + 'en-US': 'transfer titles' + }, + apis: [ + 'TiTransferComponent.properties.leftTitle', + 'TiTransferComponent.properties.rightTitle' + ], + codeFiles: [ + 'transfer-titles.html', + 'TransferTitlesComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-event', + name: { + 'zh-CN': '事件', + 'en-US': 'transfer event' + }, + desc: { + 'zh-CN': '

    当点击向右按钮的时侯触发transferToRight事件;当点击向左按钮的时侯触发transferToLeft事件;传递出去的参数为:此次穿梭的数据。

    ', + 'en-US': 'transfer event' + }, + apis: [ + 'TiTransferComponent.events.transferToLeft', + 'TiTransferComponent.events.transferToRight' + ], + codeFiles: [ + 'transfer-event.html', + 'TransferEventComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-searchable', + name: { + 'zh-CN': '搜索', + 'en-US': 'transfer searchable' + }, + desc: { + 'zh-CN': '

    通过属性searchable配置是否开启搜索。

    ', + 'en-US': 'transfer searchable' + }, + apis: [ 'TiTransferComponent.properties.searchable' ], + codeFiles: [ + 'transfer-searchable.html', + 'TransferSearchableComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-searchkeys', + name: { + 'zh-CN': '设置搜索字段', + 'en-US': 'transfer searchkeys' + }, + desc: { + 'zh-CN': '

    通过属性searchKeys配置搜索字段。

    ', + 'en-US': 'transfer searchkeys' + }, + apis: [ 'TiTransferComponent.properties.searchKeys' ], + codeFiles: [ + 'transfer-searchkeys.html', + 'TransferSearchkeysComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-placeholder', + name: { + 'zh-CN': '搜索框文字提示', + 'en-US': 'transfer placeholder' + }, + desc: { + 'zh-CN': '

    通过属性placeholder配置搜索框的文字提示。

    ', + 'en-US': 'transfer placeholder' + }, + apis: [ 'TiTransferComponent.properties.placeholder' ], + codeFiles: [ + 'transfer-placeholder.html', + 'TransferPlaceholderComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-idkey', + name: { + 'zh-CN': '唯一标识', + 'en-US': 'transfer idkey' + }, + desc: { + 'zh-CN': '

    通过属性idKey配置数据唯一标识的键值。

    ', + 'en-US': 'transfer idkey' + }, + apis: [ + 'TiTransferComponent.properties.idKey' + ], + codeFiles: [ + 'transfer-idkey.html', + 'TransferIdkeyComponent.ts', + 'data.js' + ] + }, + { + demoId: 'transfer-pagination', + name: { + 'zh-CN': '分页', + 'en-US': 'transfer pagination' + }, + desc: { + 'zh-CN': '

    通过属性pageable配置是否开启分页;通过属性pageSize配置每页展示的条数。

    ', + 'en-US': 'transfer pagination' + }, + apis: [ + 'TiTransferComponent.properties.pageable', + 'TiTransferComponent.properties.pageSize' + ] + }, + { + demoId: 'transfer-table', + name: { 'zh-CN': '表格', 'en-US': 'transfer table' }, + desc: { + 'zh-CN': '

    通过属性type配置穿梭框的类型,为table时使用表格类型;通过属性columns配置表格的表头列属性。

    ', + 'en-US': 'transfer table' + }, + apis: [ + 'TiTransferComponent.properties.type', + 'TiTransferComponent.properties.columns', + 'TiTransferComponent.slots.rowTemplate', + 'TiTransferColumn' + ] + } + ] +} diff --git a/src/transfer/demo/src/app/transfer/webdoc/transfer.cn.md b/src/transfer/demo/src/app/transfer/webdoc/transfer.cn.md new file mode 100644 index 0000000..aad514e --- /dev/null +++ b/src/transfer/demo/src/app/transfer/webdoc/transfer.cn.md @@ -0,0 +1,23 @@ +--- +title: Transfer 穿梭框 +--- +# Transfer 穿梭框 + +
    + +Transfer 穿梭框即双向选择器,选择一个或以上的选项后,点击对应的方向键,可以把选中的选项移动到另一栏。 + +```typescript +import { TiTransferModule } from '@opentiny/ng'; +``` + +
    + +
    + +Tip 是提供文本气泡提示的组件,可以通过指令方式、服务方式生成。 + +```typescript +import { TiTipModule } from '@opentiny/ng'; +``` +
    diff --git a/src/transfer/demo/src/app/transfer/webdoc/transfer.en.md b/src/transfer/demo/src/app/transfer/webdoc/transfer.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/transfer/demo/src/app/transfer/webdoc/transfer.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/transfer/demo/src/favicon.ico b/src/transfer/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/transfer/demo/src/index.html b/src/transfer/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/transfer/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/transfer/demo/src/main.ts b/src/transfer/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/transfer/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/transfer/demo/test.ts b/src/transfer/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/transfer/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/transfer/demo/tsconfig.app.json b/src/transfer/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/transfer/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/transfer/demo/tsconfig.spec.json b/src/transfer/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/transfer/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/transfer/lib/index.ts b/src/transfer/lib/index.ts new file mode 100644 index 0000000..e54e046 --- /dev/null +++ b/src/transfer/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTransferModule'; diff --git a/src/transfer/lib/ng-package.json b/src/transfer/lib/ng-package.json new file mode 100644 index 0000000..b1d24dd --- /dev/null +++ b/src/transfer/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/transfer", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/transfer/lib/package.json b/src/transfer/lib/package.json new file mode 100644 index 0000000..20ba4a3 --- /dev/null +++ b/src/transfer/lib/package.json @@ -0,0 +1,22 @@ +{ + "name": "@opentiny/ng-transfer", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-dropsearch": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-list": "~1.0.0-beta.0", + "@opentiny/ng-searchbox": "~1.0.0-beta.0", + "@opentiny/ng-table": "~1.0.0-beta.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-checkbox": "~1.0.0-beta.0", + "@opentiny/ng-pagination": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/transfer/lib/project.json b/src/transfer/lib/project.json new file mode 100644 index 0000000..264feaf --- /dev/null +++ b/src/transfer/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/transfer/lib", + "sourceRoot": "src/transfer/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/transfer"], + "options": { + "project": "src/transfer/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/transfer"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js transfer" + }, + { + "command": "ng default-build transfer" + }, + { + "command": "node build/clear-default-theme.js transfer" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/transfer && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build transfer && ng pack transfer && node build/publish.js transfer --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/transfer/lib/src/TiTransferColumn.ts b/src/transfer/lib/src/TiTransferColumn.ts new file mode 100644 index 0000000..4850f5c --- /dev/null +++ b/src/transfer/lib/src/TiTransferColumn.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiTableColumns } from '@opentiny/ng-table'; + +export interface TiTransferColumn extends TiTableColumns { + /** + * 表格每列按照源数据哪一项属性展示数据的标识值;使用自定义模板时无效 + */ + field?: string; +} diff --git a/src/transfer/lib/src/TiTransferComponent.ts b/src/transfer/lib/src/TiTransferComponent.ts new file mode 100644 index 0000000..c748422 --- /dev/null +++ b/src/transfer/lib/src/TiTransferComponent.ts @@ -0,0 +1,325 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, SimpleChanges, Output, EventEmitter, ChangeDetectionStrategy, ContentChild, TemplateRef } from '@angular/core'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiTransferUtil } from './transferUtil'; +import { TiTransferColumn } from './TiTransferColumn'; +import packageInfo from '../package.json'; + +@Component({ + selector: 'ti-transfer', + templateUrl: './transfer.html', + providers: [TiFormComponent.getValueAccessor(TiTransferComponent)], + styleUrls: ['./transfer.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiTransferComponent extends TiFormComponent { + /** + * 源数据 + */ + @Input() options: Array = []; + /** + * 列表数据要展示的键值 + */ + @Input() labelKey: string = 'label'; + /** + * 设置数据唯一标识的键值 + */ + @Input() idKey: string; + /** + * 左侧面板标题文本 + * + * 默认值 : 可选项(国际化) + */ + @Input() leftTitle: string = TiLocale.getLocaleWords().tiTransfer.available; + /** + * 右侧面板标题文本 + * + * 默认值 : 已选项(国际化) + */ + @Input() rightTitle: string = TiLocale.getLocaleWords().tiTransfer.selected; + /** + * 无数据时的显示文本 + * + * 默认值 : 暂无数据(国际化) + */ + @Input() noDataText: string = TiLocale.getLocaleWords().tiList.noDataText; + /** + * 穿梭框的高度 + * + * 1.type 为 list 默认值为 300px + * + * 2.type 为 table 默认值为 443px + * + */ + @Input() height: string; + /** + * 穿梭框的宽度 + * + * 1.type 为 list 默认值为 200px + * + * 2.type 为 table 默认值为 340px + * + */ + @Input() width: string; + /** + * 是否开启搜索 + */ + @Input() searchable: boolean = false; + /** + * 输入框的提示文本 + * + * 默认值 : 请输入关键字搜索(国际化) + */ + @Input() placeholder: string = TiLocale.getLocaleWords().tiTransfer.placeholder; + /** + * 搜索字段 + */ + @Input() searchKeys: Array; + /** + * 是否开启分页 + */ + @Input() pageable: boolean = false; + /** + * 每页显示数据条数 + */ + @Input() pageSize: number = 10; + /** + * 点击左向右的按钮点时触发的回调 + */ + @Output() readonly transferToRight: EventEmitter = new EventEmitter(); + /** + * 点击右向左的按钮点时触发的回调 + */ + @Output() readonly transferToLeft: EventEmitter = new EventEmitter(); + /** + * 穿梭框类型 + */ + @Input() type: 'list' | 'table' = 'list'; + /** + * 表格类型时表头显示配置 + */ + @Input() columns: Array = []; + /** + * @ignore + * 选中事件,向外通知option数据,因不了解业务使用场景,暂不对外开发该接口 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + /** + * 表格类型每行展示的模板 + */ + @ContentChild('row', { static: false }) rowTemplate: TemplateRef; + /** + * @ignore + * 左侧面板选中项 + */ + public leftSelectedOptions: Array = []; + /** + * @ignore + * 右侧面板选中项 + */ + public rightSelectedOptions: Array = []; + /** + * @ignore + * 设置向右button的disabled状态 + */ + public toRightButtonDisabled: boolean = true; + /** + * @ignore + * 设置向左button的disabled状态 + */ + public toLeftButtonDisabled: boolean = true; + /** + * @ignore + * 穿梭框的左侧面板的列表数据 + * + * 默认值 : [] + */ + public leftOptions: Array = []; // options的数据 + /** + * @ignore + * 穿梭框的右侧面板的列表数据 + * + * 默认值 : [] + */ + public rightOptions: Array = []; // options的数据 + protected versionInfo: string = super.getVersion(packageInfo); + + // 初始化右侧面板的数据 + writeValue(model: any): void { + super.writeValue(model); + this.rightOptions = this.getEqualOptions(model, this.options); + this.leftOptions = this.getLeftOptions(); + + // 根据左右面板最新数据来获取最新的左右面板选中项 + this.leftSelectedOptions = this.getEqualOptions(this.leftSelectedOptions, this.leftOptions); + this.rightSelectedOptions = this.getEqualOptions(this.rightSelectedOptions, this.rightOptions); + + this.toRightButtonDisabled = this.getButtondisabled(this.leftSelectedOptions); + this.toLeftButtonDisabled = this.getButtondisabled(this.rightSelectedOptions); + } + + private filterModel(option: any): boolean { + // 从右侧面板列表中查找当前的option的index + const index: number = this.rightOptions.findIndex((rightOption) => { + return TiTransferUtil.isEqualOption(this.idKey, this.labelKey, rightOption, option); + }); + // 结果大于-1,说明当前的option存在于右侧面板中 + return index > -1; + } + + // 获取left面板的数据 + public getLeftOptions(): Array { + const leftOptions: Array = this.options + ? this.options.filter((option) => { + // 过滤掉右侧面板数据 + return !this.filterModel(option); + }) + : []; + + return leftOptions; + } + + /** + * 点击向右button事件 + */ + public onClickToRight(): void { + if (this.toRightButtonDisabled) { + return; + } + // 1.将左侧的选中项同步到右侧 + this.rightOptions = [...this.leftSelectedOptions, ...this.rightOptions]; + // 2.将左侧的选中项从左侧的options中进行清除 + this.leftOptions = this.leftOptions.filter((option) => { + return this.leftSelectedOptions.indexOf(option) < 0; + }); + // 将右侧列表的值传给model,实现双向绑定 + this.model = this.rightOptions; + const selectParms: object = { + rightOptions: this.rightOptions, + leftOptions: this.leftOptions, + toRightOptions: this.leftSelectedOptions + }; + // 将相关数据传到外面 + this.transferToRight.emit(selectParms); + // 清除已选中项,清除选中项 + this.leftSelectedOptions = []; + // 设置向右箭头的disabled状态 + this.toRightButtonDisabled = true; + } + + /** + * 点击向左button事件 + */ + public onClickToLeft(): void { + if (this.toLeftButtonDisabled) { + return; + } + // 1.将右侧的选中项移动到左边,并进行恢复初始数据 + this.leftOptions = this.recoveryOptions([...this.leftOptions, ...this.rightSelectedOptions]); + // 2.将右侧的选中项从右侧的options中进行清除 + this.rightOptions = this.rightOptions.filter((option) => { + return this.rightSelectedOptions.indexOf(option) < 0; + }); + + // 将右侧列表的值传给model,实现双向绑定 + this.model = this.rightOptions; + const selectParms: object = { + rightOptions: this.rightOptions, + leftOptions: this.leftOptions, + toLeftOptions: this.rightSelectedOptions + }; + + // 将相关数据传到外面 + this.transferToLeft.emit(selectParms); + // 清除已选中项,清除选中状态; + this.rightSelectedOptions = []; + // 设置向左箭头的disabled状态 + this.toLeftButtonDisabled = true; + } + + // 为左右两侧面板数据赋值 + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + if (changes?.options) { + this.rightOptions = this.getEqualOptions(this.model, this.options); + this.leftOptions = this.getLeftOptions(); + + // 根据左右面板最新数据来获取最新的左右面板选中项 + this.leftSelectedOptions = this.getEqualOptions(this.leftSelectedOptions, this.leftOptions); + this.rightSelectedOptions = this.getEqualOptions(this.rightSelectedOptions, this.rightOptions); + + this.toRightButtonDisabled = this.getButtondisabled(this.leftSelectedOptions); + this.toLeftButtonDisabled = this.getButtondisabled(this.rightSelectedOptions); + } + } + + // 获取相同的option部分 + private getEqualOptions(modelOptions: Array = [], options: Array = []): Array { + const optionsTemp: Array = []; + modelOptions?.forEach((modelOption) => { + const equalOption: object = options?.find((option) => { + return TiTransferUtil.isEqualOption(this.idKey, this.labelKey, modelOption, option); + }); + + if (equalOption) { + optionsTemp.push(equalOption); + } + }); + + return optionsTemp; + } + public leftModelChange(options: Array = []): void { + // 判断左侧面板是否有选中项,从而控制向右button的禁用状态 + this.toRightButtonDisabled = this.getButtondisabled(options); + } + public rightModelChange(options: Array = []): void { + // 判断右侧面板是否有选中项,从而控制向左button的禁用状态 + this.toLeftButtonDisabled = this.getButtondisabled(options); + } + + // 获取左右箭头的disabled状态 + private getButtondisabled(options: Array = []): boolean { + return !options || options.length === 0; + } + + // 当右侧数据到左侧时,恢复左侧options的顺序 + private recoveryOptions(leftOptions: Array = []): Array { + const optionsTemp: Array = []; + this.options?.forEach((oldOption) => { + leftOptions?.forEach((leftOption) => { + if (TiTransferUtil.isEqualOption(this.idKey, this.labelKey, leftOption, oldOption)) { + optionsTemp.push(leftOption); + } + }); + }); + + return optionsTemp; + } + + /** + * @ignore + * 左侧面板选中/取消事件回调,暂不对外开放 + */ + public onLeftSelect(option: any): void { + this.select.emit(option); + } + + /** + * @ignore + * 右侧面板取消/选中事件回调,暂不对外开放 + */ + public onRightSelect(option: any): void { + this.select.emit(option); + } +} diff --git a/src/transfer/lib/src/TiTransferModule.ts b/src/transfer/lib/src/TiTransferModule.ts new file mode 100644 index 0000000..2e5df73 --- /dev/null +++ b/src/transfer/lib/src/TiTransferModule.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiTransferComponent } from './TiTransferComponent'; +import { TiTransferListComponent } from './transferlist/TiTransferListComponent'; +import { FormsModule } from '@angular/forms'; +import { TiDropsearchModule } from '@opentiny/ng-dropsearch'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiLocaleModule, TiLocale } from '@opentiny/ng-locale'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiListModule } from '@opentiny/ng-list'; +import { TiSearchboxModule } from '@opentiny/ng-searchbox'; +import { TiTableModule } from '@opentiny/ng-table'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiCheckboxModule } from '@opentiny/ng-checkbox'; +import { TiPaginationModule } from '@opentiny/ng-pagination'; +import { locales } from './i18n'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiLocaleModule, + TiIconModule, + TiDropsearchModule, + TiDropModule, + TiListModule, + TiSearchboxModule, + TiPaginationModule, + TiTableModule, + TiOverflowModule, + TiCheckboxModule + ], + exports: [TiTransferComponent, TiTransferListComponent], + declarations: [TiTransferComponent, TiTransferListComponent] +}) +export class TiTransferModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiTransferComponent } from './TiTransferComponent'; +export { TiTransferListComponent } from './transferlist/TiTransferListComponent'; +export { TiTransferColumn } from './TiTransferColumn'; diff --git a/src/transfer/lib/src/i18n/TiTransferWords.ts b/src/transfer/lib/src/i18n/TiTransferWords.ts new file mode 100644 index 0000000..cd0b91e --- /dev/null +++ b/src/transfer/lib/src/i18n/TiTransferWords.ts @@ -0,0 +1,7 @@ +export interface TiTransferWords { + tiTransfer: { + available: string; + selected: string; + placeholder: string; + }; +} diff --git a/src/transfer/lib/src/i18n/en_US.ts b/src/transfer/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..408bc8c --- /dev/null +++ b/src/transfer/lib/src/i18n/en_US.ts @@ -0,0 +1,9 @@ +import { TiTransferWords } from './TiTransferWords'; + +export const en_US: TiTransferWords = { + tiTransfer: { + available: 'Available', + selected: 'Selected', + placeholder: 'Enter a keyword.' + } +}; diff --git a/src/transfer/lib/src/i18n/es_US.ts b/src/transfer/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..0035e94 --- /dev/null +++ b/src/transfer/lib/src/i18n/es_US.ts @@ -0,0 +1,9 @@ +import { TiTransferWords } from './TiTransferWords'; + +export const es_US: TiTransferWords = { + tiTransfer: { + available: 'Disponible', + selected: 'Seleccionado/s', + placeholder: 'Ingrese una palabra clave.' + } +}; diff --git a/src/transfer/lib/src/i18n/fr_FR.ts b/src/transfer/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..e071788 --- /dev/null +++ b/src/transfer/lib/src/i18n/fr_FR.ts @@ -0,0 +1,9 @@ +import { TiTransferWords } from './TiTransferWords'; + +export const fr_FR: TiTransferWords = { + tiTransfer: { + available: 'Disponible', + selected: 'Sélectionné', + placeholder: 'Entrez un mot-clé.' + } +}; diff --git a/src/transfer/lib/src/i18n/index.ts b/src/transfer/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/transfer/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/transfer/lib/src/i18n/pt_BR.ts b/src/transfer/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..dc96629 --- /dev/null +++ b/src/transfer/lib/src/i18n/pt_BR.ts @@ -0,0 +1,9 @@ +import { TiTransferWords } from './TiTransferWords'; + +export const pt_BR: TiTransferWords = { + tiTransfer: { + available: 'Disponíveis', + selected: 'Selecionados', + placeholder: 'Insira uma palavra-chave.' + } +}; diff --git a/src/transfer/lib/src/i18n/zh_CN.ts b/src/transfer/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..8cab162 --- /dev/null +++ b/src/transfer/lib/src/i18n/zh_CN.ts @@ -0,0 +1,9 @@ +import { TiTransferWords } from './TiTransferWords'; + +export const zh_CN: TiTransferWords = { + tiTransfer: { + available: '可选项', + selected: '已选项', + placeholder: '请输入关键字搜索' + } +}; diff --git a/src/transfer/lib/src/transfer.html b/src/transfer/lib/src/transfer.html new file mode 100644 index 0000000..d268357 --- /dev/null +++ b/src/transfer/lib/src/transfer.html @@ -0,0 +1,73 @@ +
    + + + + + + + +
    +
    +
    +
    + + + + + + +
    + + + + {{row[key.field]}} + + diff --git a/src/transfer/lib/src/transfer.less b/src/transfer/lib/src/transfer.less new file mode 100644 index 0000000..39edb59 --- /dev/null +++ b/src/transfer/lib/src/transfer.less @@ -0,0 +1,75 @@ +:host { + // 按钮容器宽度 + --ti-transfer-buttons-container-width: 42px; + // 按钮尺寸 + --ti-transfer-button-size: var(--ti-common-size-5x); + // 按钮边框 + --ti-transfer-button-border-weight: var(--ti-common-border-weight-normal); + --ti-transfer-button-border-style: var(--ti-common-border-style-solid); + --ti-transfer-button-border-radius: var(--ti-common-border-radius-normal); + // 按钮边框色和背景色 + --ti-transfer-button-color: var(--ti-common-color-bg-emphasize); + // 按钮图标色 + --ti-transfer-button-icon-color: var(--ti-common-color-icon-white); + // 按钮边框禁用色 + --ti-transfer-button-border-color-disabled: var(--ti-common-color-line-disabled); + // 按钮背景禁用色 + --ti-transfer-button-bg-color-disabled: var(--ti-common-color-bg-disabled); + // 按钮下边距 + --ti-transfer-button-margin-bottom: var(--ti-common-space-base); + // 水平内边距 = (容器宽度 - 按钮尺寸 - 按钮边框 * 2) / 2 + --ti-transfer-buttons-padding-horizontal: calc( + (var(--ti-transfer-buttons-container-width) - var(--ti-transfer-button-size) - var(--ti-transfer-button-border-weight) * 2) / 2 + ); +} + +.ti3-transfer-container { + display: flex; +} + +.ti3-transfer-buttons-container { + display: flex; + flex-direction: column; + justify-content: center; + width: var(--ti-transfer-buttons-container-width); + padding: 0 var(--ti-transfer-buttons-padding-horizontal); + box-sizing: border-box; +} + +.ti3-transfer-button { + width: var(--ti-transfer-button-size); + height: var(--ti-transfer-button-size); + border: var(--ti-transfer-button-border-weight) var(--ti-transfer-button-border-style) var(--ti-transfer-button-color); + border-radius: var(--ti-transfer-button-border-radius); + line-height: var(--ti-transfer-button-size); + text-align: center; + background-color: var(--ti-transfer-button-color); + font-size: calc(var(--ti-transfer-button-size) * 3 / 5); + color: var(--ti-transfer-button-icon-color); + cursor: pointer; + box-sizing: content-box; + + &:hover, + &:focus, + &:active { + background-color: var(--ti-common-color-bg-hover); + } +} + +// 穿梭框左右button被禁用的样式 +.ti3-transfer-button-disabled { + border: var(--ti-transfer-button-border-weight) var(--ti-transfer-button-border-style) var(--ti-transfer-button-border-color-disabled); + color: var(--ti-common-color-icon-graybg-disabled); + background-color: var(--ti-transfer-button-bg-color-disabled); + cursor: not-allowed; + + &:hover, + &:focus, + &:active { + background-color: var(--ti-transfer-button-bg-color-disabled); + } +} + +.ti3-transfer-right-button { + margin-bottom: var(--ti-transfer-button-margin-bottom); +} diff --git a/src/transfer/lib/src/transferUtil.ts b/src/transfer/lib/src/transferUtil.ts new file mode 100644 index 0000000..e9c037a --- /dev/null +++ b/src/transfer/lib/src/transferUtil.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +export class TiTransferUtil { + /** + * @ignore + * 对比选中项与option数据的对应关系 + */ + public static isEqualOption(idKey: any, labelKey: any, modelOption: any, option: any): boolean { + if (idKey) { + return modelOption[idKey] === option[idKey]; + } + + return ( + modelOption === option || + (modelOption[labelKey] !== undefined && option[labelKey] !== undefined && modelOption[labelKey] === option[labelKey]) + ); + } +} diff --git a/src/transfer/lib/src/transferlist/TiTransferListComponent.ts b/src/transfer/lib/src/transferlist/TiTransferListComponent.ts new file mode 100644 index 0000000..e1f84ab --- /dev/null +++ b/src/transfer/lib/src/transferlist/TiTransferListComponent.ts @@ -0,0 +1,404 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + Component, + Input, + SimpleChanges, + Output, + EventEmitter, + ElementRef, + Renderer2, + ChangeDetectorRef, + TemplateRef, + ContentChild +} from '@angular/core'; +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiTableRowData, TiTableSrcData } from '@opentiny/ng-table'; +import { Util } from '@opentiny/ng-utils'; +import { TiTransferColumn } from '../TiTransferColumn'; +import { TiTransferUtil } from '../transferUtil'; +import packageInfo from '../../package.json'; + +@Component({ + selector: 'ti-transfer-list', + templateUrl: './transfer-list.html', + styleUrls: ['./transfer-list.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiTransferListComponent)] +}) +export class TiTransferListComponent extends TiFormComponent { + /** + * 穿梭框的源数据 + * + * 默认值 : [] + */ + @Input() options: Array = []; + /** + * 穿梭框列表要显示的字段 + * + * 默认值 : label + */ + @Input() labelKey: string = 'label'; + /** + * @ignore + * 列表数据中的唯一标识属性,该接口暂不对外开放,后续如果业务场景labelKey对应的值确实有重复时,再对外开放该接口。 + * + * 默认值 : label + */ + @Input() idKey: string; + /** + * 设置面板头部标题 + */ + @Input() title: string; + /** + * 无数据时的显示文本 + */ + @Input() noDataText: string; + /** + * 设置面板的高度 + * + * 缺省值 : 300px + */ + @Input() height: string; + /** + * 设置面板的宽度 + * + * 缺省值 : 200px + */ + @Input() width: string; + /** + * 设置是否添加搜索框 + * + * 缺省值 : false + */ + @Input() searchable: boolean = false; + /** + * 输入框的placeholder + */ + @Input() placeholder: string; + /** + * 要搜索的字段数组 + * + * 缺省值 : labelKey + */ + @Input() searchKeys: Array; + /** + * 设置是否开启分页 + * + * 缺省值 : false + */ + @Input() pageable: boolean = false; + /** + * 分页数据大小 + */ + @Input() pageSize: number = 10; + /** + * 表格类型 默认为list + */ + @Input() type: 'list' | 'table' = 'list'; + /** + * table类型表格表头 + */ + @Input() columns: Array = []; + /** + * @ignore + * 选中事件,向外通知option数据 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + @ContentChild(TemplateRef, { static: false }) itemTemplate: TemplateRef; + /** + * @ignore + * 面板左上角options总数显示 + */ + public selectedNumber: number = 0; + /** + * @ignore + * 面板左上角选中项个数显示 + */ + public totalNumber: number = 0; + /** + * @ignore + * 全选按钮的选中状态 + */ + public selectAllState: boolean; + /** + * @ignore + * 搜索框内单词 + */ + public searchWord: string; + /** + * @ignore + * 搜索结果数组 + */ + public searchResult: Array = new Array(); + /** + * @ignore + * list展示数据数组 + */ + public dispalyData: Array = new Array(); + /** + * @ignore + * 分页当前页数 + */ + public currentPage: number = 1; + /** + * @ignore + * 分页数据总数 + */ + public total: number; + /** + * @ignore + * table类型下数据总数 + */ + public tableTotalNumber: number; + /** + * @ignore + * table类型下选中项列表 + */ + public checkedList: Array = []; + /** + * @ignore + * table类型实际展示的数据 + */ + public displayed: Array = []; + /** + * @ignore + * table类型表个配置项 + */ + public srcData: TiTableSrcData; + + protected versionInfo: string = super.getVersion(packageInfo); + + constructor(hostRef: ElementRef, renderer: Renderer2, changeDetectorRef: ChangeDetectorRef) { + super(hostRef, renderer, changeDetectorRef); + } + /** + * 监听选中项的变化 + * @param model 选中项 + */ + protected ngOnModelChange(model: any): void { + // 更新选中项的总数 + this.selectedNumber = model ? model.length : 0; + // 点击option,设置全选按钮的选中状态 + this.setSelectedAllState(); + } + + // 监听options的变化,在options改变时,改变右上角数字总数的值 + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + /** + * 如果有搜索框,options改变时,在页面上显示的searchResult, + * 此处需要将改变的options重新赋给searchResult + */ + if (changes?.options) { + if (this.searchable) { + this.searchResult = this.options; + this.searchWord = ''; + } + // 更新options总数和选中项总数 + this.totalNumber = changes.options.currentValue ? changes.options.currentValue.length : 0; + this.selectedNumber = this.model ? this.model.length : 0; + if (this.type === 'list') { + // options改变时,设置全选按钮的选中状态 + this.setSelectedAllState(); + this.changePagination(this.currentPage); + } + if (this.type === 'table') { + this.srcData = { + data: this.options, + state: { + searched: false, + sorted: false, + paginated: false + } + }; + this.tableTotalNumber = this.options.length; + } + } + } + + // 触发分页修改总数的展示数据 + public changePagination(event: number): void { + if (this.pageable) { + this.total = this.searchable ? this.searchResult.length : this.options.length; + this.dispalyData = this.searchable + ? this.searchResult.slice(this.pageSize * (event - 1), this.pageSize * event) + : this.options.slice(this.pageSize * (event - 1), this.pageSize * event); + } else { + this.dispalyData = this.searchable ? this.searchResult : this.options; + } + } + + // list列表选中/取消选项事件 + public onSelect(option: any): void { + this.select.emit(option); + } + + public onClickSelectAll(): void { + this.changeModel(this.selectAllState === false || this.selectAllState === null); + } + + private changeModel(state: boolean): void { + this.model = state + ? this.options.filter((option) => { + return !option.disabled; + }) + : this.model.filter((option: any) => { + return option.disabled; + }); + this.selectAllState = state; + } + + // 设置全选按钮的选中状态 + public setSelectedAllState(): void { + if (this.isValidData()) { + this.selectAllState = false; + return; + } + + const selectedOptions: Array = this.getSelectedOptions(); + + if (selectedOptions.length === 0) { + this.selectAllState = false; + return; + } + + const selectableOptions: Array = this.getSelectableOptions(); + + this.selectAllState = selectedOptions.length === selectableOptions.length ? true : null; + } + + // 判断数据有效性 + private isValidData(): boolean { + return !this.model || this.model.length === 0 || !this.options || this.options.length === 0; + } + + private getSelectedOptions(): Array { + return this.options.filter((item: any) => { + return this.isSelectable(item) && this.isSelected(item); + }); + } + + private getSelectableOptions(): Array { + return this.options.filter((item: any) => { + return this.isSelectable(item); + }); + } + + // 是否为可选数据项 + protected isSelectable(item: any): boolean { + return !this.isDisabled(item); + } + + // 当前元素是否为禁用状态 + private isDisabled(item: any): boolean { + return item && item.disabled; + } + + // 是已选中数据项 + public isSelected(item: any): boolean { + if (!this.model) { + return false; + } + + return this.getIndex(item, this.model) !== -1; + } + + private getIndex(item: any, arr: Array): number { + if (!arr || !item) { + return -1; + } + for (let i: number = 0; i < arr.length; i++) { + if (TiTransferUtil.isEqualOption(this.idKey, this.labelKey, arr[i], item)) { + return i; + } + } + + return -1; + } + // 在searchword进行变化时,进行前台搜索相关处理 + public searchWordChange(searchWord: string): void { + if (this.type === 'table') { + return; + } + this.filterSearchReslut(searchWord); + } + + /** + * 前台搜索时使用,查找搜索结果 + */ + private filterSearchReslut(searchWord: string): void { + if (this.options?.length >= 0) { + // 搜索结果临时值。结果默认值,是原数据 + let searchResult: Array = this.options; + // 如果搜索词存在 + if (!Util.isEmptyString(searchWord)) { + // 在集合中搜索 + searchResult = searchResult.filter((option: any) => { + if (!this.searchKeys) { + // 没有定义searchKeys时,取labelKey + return this.isMatchbWithSWord(option, this.labelKey, searchWord); + } else { + // 已定义searchKeys,任一条目匹配即可 + return this.isMatchBySearchkeys(option, searchWord); + } + }); + } + this.searchResult = searchResult; + this.changePagination(this.currentPage); + } + } + + // 已定义searchKeys,任一条目匹配即可 + private isMatchBySearchkeys(option: any, searchWord: string): boolean { + for (const searchKey of this.searchKeys) { + if (this.isMatchbWithSWord(option, searchKey, searchWord)) { + return true; + } + } + + return false; + } + + // 下拉项中的searchKey字段是否和搜索字段相匹配 + private isMatchbWithSWord(option: any, searchKey: string, searchWord: string): boolean { + return Util.isString(option[searchKey]) && option[searchKey].toLowerCase().indexOf(searchWord.toLowerCase()) >= 0; + } + + /** + * 问题:搜索框无法聚焦 + * 原因:在list组件中为了避免domintor失焦,在mousedown中,阻止了默认行为。 + * 方案: + * 在transfer中不涉及domintor失焦场景,不需要阻止默认行为, + * 给list组件绑定mousedown事件 + * 使用event.stopImmediatePropagation() 阻止同一元素绑定相同事件,后续的执行的相同事件不会被触发 + * 此处mousedown的执行顺序:先执行transfer中的mousedown,再执行list中的mousedown。 + * 通过stopImmediatePropagation方法,从而阻止了list组件中ousedown事件的触发 + */ + public onListMousedown(event: MouseEvent): void { + event.stopImmediatePropagation(); + } + + // 将表格选中项通知给model + public onCheckedsChange(checkeds: Array): void { + this.model = [...checkeds]; + } + + // 将model更改后值的通知给表格选中项 + writeValue(model: any): void { + super.writeValue(model); + if (this.type === 'table' && model !== null) { + this.checkedList = model; + } + } +} diff --git a/src/transfer/lib/src/transferlist/transfer-list.html b/src/transfer/lib/src/transferlist/transfer-list.html new file mode 100644 index 0000000..6d23e3f --- /dev/null +++ b/src/transfer/lib/src/transferlist/transfer-list.html @@ -0,0 +1,153 @@ +
    +
    + + {{selectedNumber}} / {{totalNumber}} +
    +
    + + +
    + +
    + + + +
    +
    + +
    + +
    + + +
    + +
    + + + + + + + + + +
    + + {{column.title}}
    +
    +
    + + + + + + + + + + + + + + + + + + +
    + +
    {{noDataText}}
    +
    + +
    +
    +
    + + + + + {{ title }} + diff --git a/src/transfer/lib/src/transferlist/transfer-list.less b/src/transfer/lib/src/transferlist/transfer-list.less new file mode 100644 index 0000000..add9f54 --- /dev/null +++ b/src/transfer/lib/src/transferlist/transfer-list.less @@ -0,0 +1,156 @@ +:host { + // 容器边框 + --ti-transfer-list-container-border-weight: var(--ti-common-border-weight-normal); + --ti-transfer-list-container-border-style: var(--ti-common-border-style-solid); + --ti-transfer-list-container-border-radius: var(--ti-common-border-radius-normal); + --ti-transfer-list-container-border-color: var(--ti-common-color-line-normal); + --ti-transfer-list-header-height: var(--ti-common-size-10x); + --ti-transfer-list-header-bg-color: var(--ti-common-color-bg-white-emphasize); + --ti-transfer-list-header-padding-left: var(--ti-common-space-10); + --ti-transfer-list-selected-number-space-right: var(--ti-common-space-10); + --ti-transfer-list-checkbox-size: var(--ti-common-size-4x); + --ti-transfer-list-checkbox-margin-right: var(--ti-common-space-10); + --ti-transfer-list-checkbox-border-weight: var(--ti-common-border-weight-normal); + --ti-transfer-list-checkbox-border-style: var(--ti-common-border-style-solid); + --ti-transfer-list-checkbox-border-radius: var(--ti-common-border-radius-normal); + --ti-transfer-list-checkbox-bg-color: var(--ti-common-color-bg-white-normal); + --ti-transfer-list-checkbox-icon-color: var(--ti-common-color-icon-white); + --ti-transfer-list-checkbox-color-selected: var(--ti-common-color-bg-emphasize); + --ti-transfer-list-no-data-padding-top: var(--ti-common-size-25x); + --ti-transfer-list-no-data-image-size: var(--ti-common-size-20x); + --ti-transfer-list-no-data-text-color: var(--ti-common-color-text-secondary); +} + +.ti3-transfer-list-container { + border: var(--ti-transfer-list-container-border-weight) var(--ti-transfer-list-container-border-style) + var(--ti-transfer-list-container-border-color); + border-radius: var(--ti-transfer-list-container-border-radius); + box-sizing: border-box; +} + +.ti3-transfer-list-header { + position: relative; + height: var(--ti-transfer-list-header-height); + padding: 0 var(--ti-transfer-list-header-padding-left); + border-bottom: var(--ti-transfer-list-container-border-weight) var(--ti-transfer-list-container-border-style) + var(--ti-transfer-list-container-border-color); + line-height: var(--ti-transfer-list-header-height); + background: var(--ti-transfer-list-header-bg-color); + box-sizing: border-box; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); +} + +.ti3-transfer-list-selected-number { + position: absolute; + right: var(--ti-transfer-list-selected-number-space-right); +} + +.ti3-transfer-list-body { + height: calc(100% - var(--ti-transfer-list-header-height)); // 列表的高度是用户自定义高度减去面板头部高度 + overflow-y: auto; + box-sizing: border-box; +} + +.ti3-transfer-table-height { + height: calc(100% - var(--ti-transfer-list-header-height)); // 表格的高度是用户自定义高度减去面板头部高度 +} + +.ti3-transfer-table-body-height { + height: calc(100% - 29px); // 表格出现滚动条容器的最大高度是 ti-table 的高度减去面板头部高度 + border: none !important; +} + +.ti3-transfer-table-border { + border-bottom: 1px solid var(--ti-common-color-line-dividing) !important; + border-radius: 0px; +} + +.ti3-transfer-searchbox-drop { + height: calc(100% - var(--ti-transfer-list-header-height) - 48px); // 有搜索框无分页时,列表和表格的高度需要再减去搜索框占用的高度 +} + +.ti3-transfer-pagination-drop { + height: calc(100% - var(--ti-transfer-list-header-height) - 32px); // 有分页无搜索框时,列表和表格的高度需要再减去分页占用的高度 +} + +.ti3-transfer-searchbox-pagination-drop { + height: calc(100% - var(--ti-transfer-list-header-height) - 48px - 32px); // 表格容器的高度需要再减去搜索框和分页占用的高度 +} + +.ti3-transfer-searchbox-container { + padding: 10px; +} + +.ti3-transfer-table-pagination-hidden { + opacity: 0; + pointer-events: none; +} + +.ti3-transfer-table-shadow { + position: relative; + box-sizing: border-box; + height: 32px; + padding-top: 4px; + box-shadow: var(--ti-common-shadow-1-up); +} + +.ti3-transfer-pagination { + text-align: right; + .ti3-transfer-table-shadow(); +} + +.ti3-table > .ti3-page-container { + margin-top: 0px; + width: 100%; + .ti3-transfer-table-shadow(); +} + +::ng-deep .ti3-pag-mini-pages { + float: right; + padding-right: 9px; +} + +.ti3-transfer-list-checkbox { + display: inline-block; + width: var(--ti-transfer-list-checkbox-size); + height: var(--ti-transfer-list-checkbox-size); + margin-right: var(--ti-transfer-list-checkbox-margin-right); + border: var(--ti-transfer-list-checkbox-border-weight) var(--ti-transfer-list-checkbox-border-style) + var(--ti-transfer-list-container-border-color); + border-radius: var(--ti-transfer-list-checkbox-border-radius); + line-height: calc(var(--ti-transfer-list-checkbox-size) - var(--ti-transfer-list-checkbox-border-weight) * 2); + text-align: center; + background-color: var(--ti-transfer-list-checkbox-bg-color); + color: var(--ti-transfer-list-checkbox-icon-color); + box-sizing: border-box; + cursor: pointer; +} + +// 正常选中 +.ti3-transfer-list-checkbox-selected { + border-color: var(--ti-transfer-list-checkbox-color-selected); + background: var(--ti-transfer-list-checkbox-color-selected); +} + +// 半选 +.ti3-transfer-list-checkbox-indeterminate { + border-width: 5px; + border-color: var(--ti-transfer-list-checkbox-color-selected); + vertical-align: sub; + + &::before { + content: ''; + } +} + +.ti3-transfer-list-no-data { + text-align: center; + padding-top: var(--ti-transfer-list-no-data-padding-top); + box-sizing: border-box; + color: var(--ti-transfer-list-no-data-text-color); + background: no-repeat data-uri('../../../../themes/basic/img/table-nodata-bg.png'); + background-position-x: 50%; + background-position-y: 20px; + background-size: var(--ti-transfer-list-no-data-image-size) var(--ti-transfer-list-no-data-image-size); +} diff --git a/src/tree/demo/karma.conf.js b/src/tree/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/tree/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/tree/demo/project.json b/src/tree/demo/project.json new file mode 100644 index 0000000..316caef --- /dev/null +++ b/src/tree/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/tree/demo", + "sourceRoot": "src/tree/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/tree", + "index": "src/tree/demo/src/index.html", + "main": "src/tree/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tree/demo/tsconfig.app.json", + "assets": ["src/tree/demo/src/favicon.ico", "src/tree/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "tree-demo:build:production" + }, + "development": { + "browserTarget": "tree-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js tree" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/tree/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tree/demo/tsconfig.spec.json", + "karmaConfig": "src/tree/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/tree/demo/src/app/AppComponent.ts b/src/tree/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/tree/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/tree/demo/src/app/AppModule.ts b/src/tree/demo/src/app/AppModule.ts new file mode 100644 index 0000000..521f1ba --- /dev/null +++ b/src/tree/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TreeTestModule } from './tree/TreeTestModule'; + +@NgModule({ + imports: [ + TreeTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/tree/demo/src/app/IndexComponent.ts b/src/tree/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..e67d0e7 --- /dev/null +++ b/src/tree/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TreeTestModule } from './tree/TreeTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TreeTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/tree/demo/src/app/app.html b/src/tree/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/tree/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/tree/demo/src/app/tree/TreeBeforeExpandComponent.ts b/src/tree/demo/src/app/tree/TreeBeforeExpandComponent.ts new file mode 100644 index 0000000..b539c60 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeBeforeExpandComponent.ts @@ -0,0 +1,70 @@ +import { Component, ViewEncapsulation, ChangeDetectorRef } from '@angular/core'; +import { TiTreeComponent, TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-before-expand.html', + encapsulation: ViewEncapsulation.None +}) +export class TreeBeforeExpandComponent { + constructor(private changeDetectorRef: ChangeDetectorRef) {} + tree: Array = [ + { + label: '家用电器', + addable: true, + children: [] + }, + { + label: '视频', + addable: true, + children: [] + } + ]; + treeID: number = 5; + beforeExpand(TreeCom: TiTreeComponent): void { + const item: TiTreeNode = TreeCom.getBeforeExpandNode(); + item.loadStatus = 'loading'; + const getDataPromise: any = this.getNodeData(item); + getDataPromise + .then((data: Array) => { + item.children = data; + item.expanded = true; + item.loadStatus = 'success'; + }) + .catch(() => { + item.expanded = true; + item.loadStatus = 'error'; + }); + } + createData(item: TiTreeNode): Array { + const data: Array = []; + if (item.label === '家用电器') { + for (let i: number = 0; i < 3; i++) { + const dataList: TiTreeNode = { + type: 'FILE', + label: `item${this.treeID}` + }; + this.treeID++; + data.push(dataList); + } + + return data; + } + + return data; + } + getNodeData(item: TiTreeNode): any { + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + const data: Array = this.createData(item); + if (data && data.length > 0) { + resolve(data); + } else { + const errMsg: string = '获取数据失败'; + reject(errMsg); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef + this.changeDetectorRef.markForCheck(); + }, 2000); + }); + } +} diff --git a/src/tree/demo/src/app/tree/TreeBeforeMoreComponent.ts b/src/tree/demo/src/app/tree/TreeBeforeMoreComponent.ts new file mode 100644 index 0000000..8f35c2a --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeBeforeMoreComponent.ts @@ -0,0 +1,100 @@ +import { Component, ViewEncapsulation, ChangeDetectorRef } from '@angular/core'; +import { TiTreeComponent, TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-before-more.html', + encapsulation: ViewEncapsulation.None +}) +export class TreeBeforeMoreComponent { + constructor(private changeDetectorRef: ChangeDetectorRef) {} + innerData: Array = [ + { + label: '家用电器', + children: [] + }, + { + label: '视频', + children: [] + } + ]; + treeID: number = 0; + multiple: boolean = true; + offset: number; // 当前已展开的节点数量 + nodeNum: number = 2; // 每次点击展开节点的数量 + beforeExpand(TreeCom: TiTreeComponent): void { + const node: TiTreeNode = TreeCom.getBeforeExpandNode(); + node.loadStatus = 'loading'; + this.offset = 0; + const getDataPromise: any = this.getNodeData(node, this.offset, this.nodeNum, 'before'); + getDataPromise + .then((data: Array) => { + node.children = data; + node.showMore = true; + node.expanded = true; + node.loadStatus = 'success'; + }) + .catch(() => { + node.showMore = false; + node.expanded = true; + node.loadStatus = 'error'; + }); + } + beforeMore(node: TiTreeNode): void { + node.moreStatus = 'loading'; + this.offset = node.children?.length; + const getDataPromise: any = this.getNodeData(node, this.offset, this.nodeNum, 'more'); + getDataPromise + .then((data: Array) => { + node.children = node.children.concat(data); + node.moreStatus = 'success'; + }) + .catch(() => { + node.moreStatus = 'error'; + }); + } + getNodeData(item: TiTreeNode, offset: number, num: number, style: string): any { + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + const data: Array = this.createData(item, offset, num, style); + if (data && data.length > 0) { + resolve(data); + } else { + const errMsg: string = '获取数据失败'; + reject(errMsg); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef + this.changeDetectorRef.markForCheck(); + }, 2000); + }); + } + + createData(item: TiTreeNode, offset: number, num: number, style: string): Array { + const data: Array = []; + let result: Array; + this.treeID = 0; + if (style === 'before' && (item.label === '家用电器' || item.label === '视频')) { + for (let i: number = 0; i < 100; i++) { + const dataList: TiTreeNode = { + label: item.label + this.treeID, + children: [] + }; + this.treeID++; + data.push(dataList); + } + } + + if (style === 'more' && item.label === '家用电器') { + for (let i: number = 0; i < 100; i++) { + const dataList: TiTreeNode = { + label: item.label + this.treeID, + children: [] + }; + this.treeID++; + data.push(dataList); + } + } + result = data.slice(offset, offset + num); + + return result; + } +} diff --git a/src/tree/demo/src/app/tree/TreeChangedbycheckboxComponent.ts b/src/tree/demo/src/app/tree/TreeChangedbycheckboxComponent.ts new file mode 100644 index 0000000..fd0e44a --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeChangedbycheckboxComponent.ts @@ -0,0 +1,46 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-changedbycheckbox.html', + encapsulation: ViewEncapsulation.None +}) +export class TreeChangedbycheckboxComponent { + innerData: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeCheckRelationComponent.ts b/src/tree/demo/src/app/tree/TreeCheckRelationComponent.ts new file mode 100644 index 0000000..e652578 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeCheckRelationComponent.ts @@ -0,0 +1,55 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; +@Component({ + templateUrl: './tree-check-relation.html' +}) +export class TreeCheckRelationComponent { + multiple: boolean = true; + checkRelation: boolean = false; + innerData: Array = [ + { + label: 'LIC', + expanded: true, + children: [ + { + label: 'CBU安全', + expanded: true, + children: [ + { + label: 'SRE领域', + expanded: true, + children: [ + { + label: '操作系统' + }, + { + label: '网络' + } + ] + }, + { + label: '近6个小时' + }, + { + label: '近12个小时' + }, + { + label: '近1天' + } + ] + }, + { + label: '近3天', + children: [ + { + label: 'ALM' + }, + { + label: 'MIC' + } + ] + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeDisabledComponent.ts b/src/tree/demo/src/app/tree/TreeDisabledComponent.ts new file mode 100644 index 0000000..c969de2 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeDisabledComponent.ts @@ -0,0 +1,48 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-disabled.html' +}) +export class TreeDisabledComponent { + data: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: true, + children: [ + { + label: '海尔空调', + disabled: true + }, + { + label: '美的空调' + } + ] + } + ] + }, + { + label: '生活电器', + disabled: true, + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeDragBeforedropComponent.ts b/src/tree/demo/src/app/tree/TreeDragBeforedropComponent.ts new file mode 100644 index 0000000..f6dff6f --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeDragBeforedropComponent.ts @@ -0,0 +1,95 @@ +import { Component } from '@angular/core'; +import { TiTreeDragNode, TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-drag-beforedrop.html' +}) +export class TreeDragBeforedropComponent { + myLogs: Array = []; + innerData: Array = [ + { + label: '家用电器', + key: '0-1', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调', + disabled: true + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + expanded: true, + children: [ + { + label: '加湿器', + draggable: false + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + children: [ + { + label: '男装', + key: '0-2-1' + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + draggable: false, + children: [ + { + label: '面部护理', + draggable: false + }, + { + label: '口腔护理' + } + ] + } + ]; + + onbeforeDrop(event: TiTreeDragNode): boolean { + this.myLogs = [...this.myLogs, `beforeDrop nodeName = ${event.dragNode.label} =>${event.targetNode.label}`]; + const parentNode: TiTreeNode = TiTreeUtil.getParentNode(this.innerData, event.targetNode); + if (parentNode?.label === '大家电') { + return; + } else { + TiTreeUtil.dropApply(event, this.innerData); + } + } +} diff --git a/src/tree/demo/src/app/tree/TreeDragComponent.ts b/src/tree/demo/src/app/tree/TreeDragComponent.ts new file mode 100644 index 0000000..2958501 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeDragComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeDragNode, TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-drag.html' +}) +export class TreeDragComponent { + myLogs: Array = []; + innerData: Array = [ + { + label: '家用电器', + key: '0-1', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调', + disabled: true + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + children: [ + { + label: '男装', + key: '0-2-1' + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + onDrop(event: TiTreeDragNode): void { + this.myLogs = [...this.myLogs, `nodeDrop nodeName = ${event.dragNode.label} =>${event.targetNode.label}`]; + } +} diff --git a/src/tree/demo/src/app/tree/TreeEventComponent.ts b/src/tree/demo/src/app/tree/TreeEventComponent.ts new file mode 100644 index 0000000..8fd54ce --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeEventComponent.ts @@ -0,0 +1,74 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; +@Component({ + templateUrl: './tree-event.html' +}) +export class TreeEventComponent { + myLogs: Array = []; + innerData: Array = [ + { + label: 'LIC', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: 'CBU安全', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: 'SRE领域', + expanded: true, + checked: 'indeterminate', + custom: true, + children: [ + { + label: '操作系统', + checked: true + }, + { + label: '网络' + } + ] + }, + { + label: '近6个小时' + }, + { + label: '近12个小时' + }, + { + label: '近1天' + } + ] + }, + { + label: '近3天', + children: [ + { + label: 'ALM' + }, + { + label: 'MIC' + } + ] + } + ] + } + ]; + + selectFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `select nodeName= ${node.label}`]; + } + + changeFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `change nodeName= ${node.label}`]; + } + + expandFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `expand nodeName= ${node.label}`]; + } + collapseFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `collapse nodeName= ${node.label}`]; + } +} diff --git a/src/tree/demo/src/app/tree/TreeIconComponent.ts b/src/tree/demo/src/app/tree/TreeIconComponent.ts new file mode 100644 index 0000000..5d70a6e --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeIconComponent.ts @@ -0,0 +1,106 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-icon.html', // 指定组件模板 + encapsulation: ViewEncapsulation.None +}) +export class TreeIconComponent { + data: Array = [ + { + label: 'asdasdAAA', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '大家电', + expanded: true, + expandIcon: 'ti3-icon ti3-icon-document', + collapseIcon: 'ti3-icon ti3-icon-file', + checked: 'indeterminate', + children: [ + { + expandIcon: 'ti3-icon ti3-icon-document', + collapseIcon: 'ti3-icon ti3-icon-file', + label: '空调', + expanded: true, + checked: 'indeterminate', + children: [ + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '海尔空调', + checked: true + }, + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '美的空调' + } + ] + }, + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '冰箱' + }, + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '洗衣机' + }, + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '热水器' + } + ] + }, + { + label: '生活电器', + expandIcon: 'ti3-icon ti3-icon-document', + collapseIcon: 'ti3-icon ti3-icon-file', + children: [ + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '加湿器' + }, + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expandIcon: 'ti3-icon ti3-icon-document', + collapseIcon: 'ti3-icon ti3-icon-file', + children: [ + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '男装' + }, + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '女装' + } + ] + }, + { + label: '化妆', + expandIcon: 'ti3-icon ti3-icon-document', + collapseIcon: 'ti3-icon ti3-icon-file', + expanded: true, + checked: true, + children: [ + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '面部护理面部护理', + checked: true + }, + { + expandIcon: 'ti3-icon ti3-icon-calendar', + label: '口腔护理', + checked: true + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeLoadComponent.ts b/src/tree/demo/src/app/tree/TreeLoadComponent.ts new file mode 100644 index 0000000..2a1ce51 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeLoadComponent.ts @@ -0,0 +1,132 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-load.html' +}) +export class TreeLoadComponent { + /** + * 初始值采用空数据。经测试,初始值为undefined或null,会报错。 + */ + myData: Array = []; + + private dataA: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + private dataB: Array = [ + { + label: '家用电器', + children: [] + }, + { + label: '服饰', + children: [] + }, + { + label: '视频', + children: [] + }, + { + label: '食品', + children: [] + } + ]; + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + selectedData: Array = TiTreeUtil.getSelectedData(this.myData, false, false); + onChange(event: TiTreeNode): void { + console.log(event); + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData = TiTreeUtil.getSelectedData(this.myData, false, false); + } + // 每个组件都用下面六个函数,只改变函数内容 + changeUndefined(): void { + this.myData = undefined; + } + changeNull(): void { + this.myData = null; + } + changeWrongType(): void { + const temp: any = 5; + this.myData = temp; + } + changeNullData(): void { + this.myData = []; + this.selectedData = TiTreeUtil.getSelectedData(this.myData, false, false); + } + changeDataA(): void { + this.myData = this.dataA; + this.selectedData = TiTreeUtil.getSelectedData(this.myData, false, false); + } + changeDataB(): void { + this.myData = this.dataB; + this.selectedData = TiTreeUtil.getSelectedData(this.myData, false, false); + } +} diff --git a/src/tree/demo/src/app/tree/TreeManyComponent.ts b/src/tree/demo/src/app/tree/TreeManyComponent.ts new file mode 100644 index 0000000..a4ff946 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeManyComponent.ts @@ -0,0 +1,169 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-many.html' +}) +export class TreeManyComponent { + innerData1: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + selectedData1: Array = TiTreeUtil.getSelectedData(this.innerData1, false, false); + + innerData2: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + selectedData2: Array = TiTreeUtil.getSelectedData(this.innerData2, false, false); + + onSelect1(event: TiTreeNode): void { + console.log(event); + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData1 = TiTreeUtil.getSelectedData(this.innerData1, false, false); + } + + onSelect2(event: TiTreeNode): void { + console.log(event); + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData2 = TiTreeUtil.getSelectedData(this.innerData2, false, false); + } +} diff --git a/src/tree/demo/src/app/tree/TreeMultiselectComponent.ts b/src/tree/demo/src/app/tree/TreeMultiselectComponent.ts new file mode 100644 index 0000000..585fe63 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeMultiselectComponent.ts @@ -0,0 +1,57 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; +@Component({ + templateUrl: './tree-multiselect.html' +}) +export class TreeMultiselectComponent { + innerData: Array = [ + { + label: 'LIC', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: 'CBU安全', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: 'SRE领域', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '操作系统', + checked: true + }, + { + label: '网络' + } + ] + }, + { + label: '近6个小时' + }, + { + label: '近12个小时' + }, + { + label: '近1天' + } + ] + }, + { + label: '近3天', + children: [ + { + label: 'ALM' + }, + { + label: 'MIC' + } + ] + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeOperateComponent.ts b/src/tree/demo/src/app/tree/TreeOperateComponent.ts new file mode 100644 index 0000000..b99b771 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeOperateComponent.ts @@ -0,0 +1,110 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; +@Component({ + templateUrl: './tree-operate.html' +}) +export class TreeOperateComponent { + myLogs: Array = []; + innerData: Array = [ + { + label: 'LIC', + expanded: true, + checked: 'indeterminate', + addable: true, + editable: true, + deleteable: true, + children: [ + { + label: 'CBU安全', + expanded: true, + checked: 'indeterminate', + addable: true, + editable: true, + deleteable: true, + children: [ + { + label: 'SRE领域', + expanded: true, + checked: 'indeterminate', + custom: true, + addable: true, + editable: true, + deleteable: true, + children: [ + { + label: '操作系统', + checked: true, + addable: true + }, + { + label: '网络', + deleteable: true, + addable: true + } + ] + }, + { + label: '近6个小时', + disabled: true + }, + { + label: '近12个小时', + addable: true, + editable: true, + deleteable: true + }, + { + label: '近1天', + disabled: true, + addable: true, + editable: true, + deleteable: true + } + ] + }, + { + label: '近3天', + addable: true, + editable: true, + deleteable: true, + children: [ + { + label: 'ALM', + editable: true, + deleteable: true + }, + { + label: 'MIC', + editable: true, + deleteable: true + } + ] + } + ] + } + ]; + nodeAddFn(node: TiTreeNode): void { + node.addable = true; + node.editable = true; + node.deleteable = true; + this.myLogs = [...this.myLogs, `add nodeName = ${node.label}`]; + } + + nodeEditFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `edit nodeName = ${node.label}`]; + } + + nodeDeleteFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `delete nodeName = ${node.label}`]; + } + + // 编辑节点之后的回调 + afterNodeEditFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `afterNodeEdit nodeName = ${node.label}`]; + } + + // 添加节点之后的回调 + afterNodeAddFn(node: TiTreeNode): void { + this.myLogs = [...this.myLogs, `afterNodeAdd nodeName = ${node.label}`]; + } +} diff --git a/src/tree/demo/src/app/tree/TreeParentcheckableComponent.ts b/src/tree/demo/src/app/tree/TreeParentcheckableComponent.ts new file mode 100644 index 0000000..59f493d --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeParentcheckableComponent.ts @@ -0,0 +1,48 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-parentcheckable.html' +}) +export class TreeParentcheckableComponent { + multiple: boolean = true; + parentCheckable: boolean = false; + + innerData: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeRadioselectComponent.ts b/src/tree/demo/src/app/tree/TreeRadioselectComponent.ts new file mode 100644 index 0000000..a010486 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeRadioselectComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-radioselect.html' +}) +export class TreeRadioselectComponent { + innerData: Array = [ + { + label: '家用电器', + children: [ + { + label: '大家电', + children: [ + { + label: '空调', + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + checked: 'indeterminate', + expanded: true, + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + selectedData: Array = TiTreeUtil.getSelectedData(this.innerData, false, false); + + onSelect(event: TiTreeNode): void { + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData = TiTreeUtil.getSelectedData(this.innerData, false, false); + } +} diff --git a/src/tree/demo/src/app/tree/TreeSearchComponent.ts b/src/tree/demo/src/app/tree/TreeSearchComponent.ts new file mode 100644 index 0000000..f996c93 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeSearchComponent.ts @@ -0,0 +1,238 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-search.html', + encapsulation: ViewEncapsulation.None +}) +export class TreeSearchComponent { + data: Array = [ + { + label: '家用电器', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '大家电', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '空调', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '海尔空调', + checked: true + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + children: [ + { + label: '男装' + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + expanded: true, + checked: true, + children: [ + { + label: '面部护理面部护理', + checked: true + }, + { + label: '口腔护理', + checked: true + } + ] + } + ]; + datadynamic: Array = [ + { + label: '家用电器', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '大家电', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '空调', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '海尔空调', + checked: true + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + children: [ + { + label: '男装' + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + expanded: true, + checked: true, + children: [ + { + label: '面部护理面部护理', + checked: true + }, + { + label: '口腔护理', + checked: true + } + ] + } + ]; + + showData: Array = this.data; + showDatadynamic: Array = this.datadynamic; + + searchWord: string = ''; + searchWorddynamic: string = ''; + noData: boolean = false; + noDatadynamic: boolean = false; + highlightWords: string; + + onSearch(value: string): void { + // 树数据拷贝,服务也可自行实现 + const searchResult: Array = TiTreeUtil.copy(this.data); + // 根据用户传入的方法筛选 + TiTreeUtil.search(searchResult, (cnode: TiTreeNode): boolean => { + return cnode.label.indexOf(value) >= 0; + }); + // 展开整个树 + TiTreeUtil.traverse(searchResult, (node: TiTreeNode): void => { + node.expanded = true; + }); + // 将用户输入的值传入tree组件中并过滤显示高亮 + this.highlightWords = value; + this.showData = searchResult; + if (this.showData.length < 1) { + this.noData = true; + } + } + // 监听搜索字符串的改变,搭配search事件 + onChange(event: string): void { + if (event === '') { + // 如果搜索字符串清空,那么tree设置为原始数据。 + this.showData = this.data; + this.highlightWords = ''; // 取消高亮显示 + this.noData = false; + } + } + /** + * 监听输入搜索字符串的改变,返回搜索到的内容(高亮显示) + * @param event 搜索字符串 + */ + onChangedynamic(event: string): void { + const searchResult: Array = TiTreeUtil.copy(this.datadynamic); + if (event === '') { + // 如果搜索字符串清空,那么tree设置为原始数据。 + this.showDatadynamic = this.datadynamic; + this.noDatadynamic = false; + + return; + } + TiTreeUtil.search(searchResult, (cnode: TiTreeNode): boolean => { + return cnode.label.indexOf(event) >= 0; + }); + TiTreeUtil.traverse(searchResult, (node: TiTreeNode): void => { + node.expanded = true; + }); + + this.showDatadynamic = searchResult; + this.noDatadynamic = false; + if (this.showDatadynamic.length < 1) { + this.noDatadynamic = true; + } + } + // 在搜索结果中的每一次勾选,需要同步到原始数据也勾选 + onSelect(node: TiTreeNode): void { + // this.showData !== this.data表示正在显示搜索结果 + if (this.showData !== this.data && TiTreeUtil.isLeaf(node)) { + TiTreeUtil.checkedLeafNode(node) ? TiTreeUtil.selectNode(this.data, node, true) : TiTreeUtil.deSelectNode(this.data, node, true); + } + } + onSelectdynamic(node: TiTreeNode): void { + if (this.showDatadynamic !== this.datadynamic && TiTreeUtil.isLeaf(node)) { + TiTreeUtil.checkedLeafNode(node) + ? TiTreeUtil.selectNode(this.datadynamic, node, true) + : TiTreeUtil.deSelectNode(this.datadynamic, node, true); + } + } +} diff --git a/src/tree/demo/src/app/tree/TreeShortcutkeyComponent.ts b/src/tree/demo/src/app/tree/TreeShortcutkeyComponent.ts new file mode 100644 index 0000000..a866219 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeShortcutkeyComponent.ts @@ -0,0 +1,88 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-shortcutkey.html' +}) +export class TreeShortcutkeyComponent { + innerData: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + disabled: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机', + disabled: true + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + }, + { + label: '冰箱' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + }, + { + label: '儿童装', + disabled: true + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeSmallComponent.ts b/src/tree/demo/src/app/tree/TreeSmallComponent.ts new file mode 100644 index 0000000..2606bac --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeSmallComponent.ts @@ -0,0 +1,58 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-small.html' +}) +export class TreeSmallComponent { + innerData: Array = [ + { + label: 'LIC', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: 'CBU安全', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: 'SRE领域', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '操作系统', + checked: true + }, + { + label: '网络' + } + ] + }, + { + label: '近6个小时' + }, + { + label: '近12个小时' + }, + { + label: '近1天' + } + ] + }, + { + label: '近3天', + children: [ + { + label: 'ALM' + }, + { + label: 'MIC' + } + ] + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeTemplateComponent.ts b/src/tree/demo/src/app/tree/TreeTemplateComponent.ts new file mode 100644 index 0000000..7a1df36 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeTemplateComponent.ts @@ -0,0 +1,72 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-template.html' +}) +export class TreeTemplateComponent { + innerData: Array = [ + { + label: 'LIC', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: 'CBU安全', + expanded: true, + expandIcon: 'document', + collapseIcon: 'file', + checked: 'indeterminate', + children: [ + { + expandIcon: 'document', + collapseIcon: 'file', + label: 'SRE领域', + expanded: true, + checked: 'indeterminate', + custom: true, + children: [ + { + expandIcon: 'calendar', + label: '操作系统', + checked: true + }, + { + expandIcon: 'calendar', + label: '网络' + } + ] + }, + { + expandIcon: 'calendar', + label: '近6个小时' + }, + { + expandIcon: 'calendar', + label: '近12个小时' + }, + { + expandIcon: 'calendar', + label: '近1天' + } + ] + }, + { + label: '近3天', + expandIcon: 'document', + collapseIcon: 'file', + children: [ + { + expandIcon: 'calendar', + label: 'ALM' + }, + { + expandIcon: 'calendar', + label: 'MIC' + } + ] + } + ] + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeTestModule.ts b/src/tree/demo/src/app/tree/TreeTestModule.ts new file mode 100644 index 0000000..75027ad --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeTestModule.ts @@ -0,0 +1,160 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiIconModule, TiOverflowModule, TiSearchboxModule, TiTreeModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TreeRadioselectComponent } from './TreeRadioselectComponent'; +import { TreeMultiselectComponent } from './TreeMultiselectComponent'; +import { TreeUtilComponent } from './TreeUtilComponent'; +import { TreeBeforeExpandComponent } from './TreeBeforeExpandComponent'; +import { TreeParentcheckableComponent } from './TreeParentcheckableComponent'; +import { TreeSearchComponent } from './TreeSearchComponent'; +import { TreeLoadComponent } from './TreeLoadComponent'; +import { TreeChangedbycheckboxComponent } from './TreeChangedbycheckboxComponent'; +import { TreeManyComponent } from './TreeManyComponent'; +import { TreeDisabledComponent } from './TreeDisabledComponent'; +import { TreeSmallComponent } from './TreeSmallComponent'; +import { TreeTemplateComponent } from './TreeTemplateComponent'; +import { TreeIconComponent } from './TreeIconComponent'; +import { TreeShortcutkeyComponent } from './TreeShortcutkeyComponent'; +import { TreeDragComponent } from './TreeDragComponent'; +import { TreeOperateComponent } from './TreeOperateComponent'; +import { TreeBeforeMoreComponent } from './TreeBeforeMoreComponent'; +import { TreeDragBeforedropComponent } from './TreeDragBeforedropComponent'; +import { TreeEventComponent } from './TreeEventComponent'; +import { TreeCheckRelationComponent } from './TreeCheckRelationComponent'; +import { TreeVirtualscrollComponent } from './TreeVirtualscrollComponent'; +import { TreeVirtualscrollSmallComponent } from './TreeVirtualscrollSmallComponent'; +import { TreeVirtualscrollDragComponent } from './TreeVirtualscrollDragComponent'; +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiTreeModule, + TiOverflowModule, + TiSearchboxModule, + TiIconModule, + TiButtonModule, + DemoLogModule, + RouterModule.forChild(TreeTestModule.ROUTES) + ], + declarations: [ + TreeRadioselectComponent, + TreeMultiselectComponent, + TreeBeforeExpandComponent, + TreeParentcheckableComponent, + TreeUtilComponent, + TreeSearchComponent, + TreeLoadComponent, + TreeChangedbycheckboxComponent, + TreeManyComponent, + TreeDisabledComponent, + TreeSmallComponent, + TreeTemplateComponent, + TreeIconComponent, + TreeShortcutkeyComponent, + TreeDragComponent, + TreeDragBeforedropComponent, + TreeOperateComponent, + TreeEventComponent, + TreeBeforeMoreComponent, + TreeCheckRelationComponent, + TreeVirtualscrollComponent, + TreeVirtualscrollSmallComponent, + TreeVirtualscrollDragComponent + ] +}) +export class TreeTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiTreeComponent.html', label: 'Tree' }, + { href: 'classes/TiTreeUtil.html', label: 'TiTreeUtil' } + ]; + static readonly ROUTES: Routes = [ + { + path: 'tree/tree-radioselect', + component: TreeRadioselectComponent + }, + { + path: 'tree/tree-multiselect', + component: TreeMultiselectComponent + }, + { + path: 'tree/tree-before-expand', + component: TreeBeforeExpandComponent + }, + { + path: 'tree/tree-before-more', + component: TreeBeforeMoreComponent + }, + { + path: 'tree/tree-search', + component: TreeSearchComponent + }, + { + path: 'tree/tree-parentcheckable', + component: TreeParentcheckableComponent + }, + { + path: 'tree/tree-check-relation', + component: TreeCheckRelationComponent + }, + { + path: 'tree/tree-util', + component: TreeUtilComponent + }, + { + path: 'tree/tree-changedbycheckbox', + component: TreeChangedbycheckboxComponent + }, + { + path: 'tree/tree-disabled', + component: TreeDisabledComponent + }, + { + path: 'tree/tree-small', + component: TreeSmallComponent + }, + { + path: 'tree/tree-template', + component: TreeTemplateComponent + }, + { + path: 'tree/tree-shortcutkey', + component: TreeShortcutkeyComponent + }, + { + path: 'tree/tree-drag', + component: TreeDragComponent + }, + { + path: 'tree/tree-drag-beforedrop', + component: TreeDragBeforedropComponent + }, + { + path: 'tree/tree-operate', + component: TreeOperateComponent + }, + { + path: 'tree/tree-event', + component: TreeEventComponent + }, + { + path: 'tree/tree-virtualscroll', + component: TreeVirtualscrollComponent + }, + { path: 'tree/tree-load', component: TreeLoadComponent }, + { path: 'tree/tree-many', component: TreeManyComponent }, + { path: 'tree/tree-icon', component: TreeIconComponent }, + { + path: 'tree/tree-virtualscroll-small', + component: TreeVirtualscrollSmallComponent + }, + { + path: 'tree/tree-virtualscroll-drag', + component: TreeVirtualscrollDragComponent + } + ]; +} diff --git a/src/tree/demo/src/app/tree/TreeUtilComponent.ts b/src/tree/demo/src/app/tree/TreeUtilComponent.ts new file mode 100644 index 0000000..4317d63 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeUtilComponent.ts @@ -0,0 +1,278 @@ +import { AfterViewInit, Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; +@Component({ + templateUrl: './tree-util.html', // 指定组件模板 + styleUrls: ['./treeTest.less'], + encapsulation: ViewEncapsulation.None +}) +export class TreeUtilComponent implements OnInit, AfterViewInit { + newId: number; + arr: Array; + innerData: Array = [ + { + label: '家用电器', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '大家电', + expanded: true, + expandIcon: 'document', // 10.1.2版本之后可通过自定义模板设置图标及样式 + collapseIcon: 'file', + checked: 'indeterminate', + children: [ + { + expandIcon: 'document', + collapseIcon: 'file', + label: '空调', + expanded: true, + checked: 'indeterminate', + children: [ + { + expandIcon: 'calendar', + label: '海尔空调', + checked: true + }, + { + expandIcon: 'calendar', + label: '美的空调' + } + ] + }, + { + expandIcon: 'calendar', + label: '冰箱' + }, + { + expandIcon: 'calendar', + label: '洗衣机' + }, + { + expandIcon: 'calendar', + label: '热水器' + } + ] + }, + { + label: '生活电器', + expandIcon: 'document', + collapseIcon: 'file', + children: [ + { + expandIcon: 'calendar', + label: '加湿器' + }, + { + expandIcon: 'calendar', + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + expandIcon: 'document', + collapseIcon: 'file', + children: [ + { + expandIcon: 'calendar', + label: '男装' + }, + { + expandIcon: 'calendar', + label: '女装' + } + ] + }, + { + label: '化妆', + expandIcon: 'document', + collapseIcon: 'file', + expanded: false, + checked: true, + children: [ + { + expandIcon: 'calendar', + label: '面部护理', + checked: true + }, + { + expandIcon: 'calendar', + label: '口腔护理', + checked: true + } + ] + } + ]; + multiple: boolean = true; + selectedData: Array = []; + ngOnInit(): void { + [...this.arr] = this.innerData; + this.newId = 40; + this.selectedData = TiTreeUtil.getSelectedData(this.innerData, false, true); + } + + ngAfterViewInit(): void { + console.log(TiTreeUtil.getParentNode(this.innerData, this.innerData[0].children[0].children[0])); + } + selectFn(event: TiTreeNode): void { + this.selectedData = TiTreeUtil.getSelectedData(this.innerData, false, true); + } + + changeFn(event: TiTreeNode): void { + console.log(event, 'change'); + } + + /** + * @description 生成一个节点/多个节点: + * @param nodeNum 生成节点的个数:1,生成一个节点 + */ + createData(id: number, nodeNum: number): Array { + const data: Array = []; + if (nodeNum === 1) { + const dataList: TiTreeNode = { + label: `新增节点${id}` + }; + data.push(dataList); + } else { + for (let i: number = 0; i < nodeNum; i++) { + this.newId = id + i; + const dataLists: TiTreeNode = { + label: `新增节点${this.newId}` + }; + data.push(dataLists); + } + } + + return data; + } + + // 展开整个树 + expandNode(): void { + const data: Array = this.arr.concat(); + TiTreeUtil.traverse(data, traverseFn); + + function traverseFn(node: TiTreeNode): void { + node.expanded = true; + } + this.innerData = data; + } + + // 折叠整个树 + deExpandNode(): void { + const data: Array = this.arr.concat(); + TiTreeUtil.traverse(data, traverseFn); + + function traverseFn(node: TiTreeNode): void { + node.expanded = false; + } + this.innerData = data; + } + + // 根节点指定位置添加一个节点 + addNode1(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 1); + console.log(node); + TiTreeUtil.addNode(data, node, 1); + this.innerData = data; + } + + // 根节点指定位置添加多个节点 + addNode2(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 2); + TiTreeUtil.addNode(data, node, 1); + this.innerData = data; + } + + // 根节点追加一个节点 + addNode3(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 1); + TiTreeUtil.addNode(data, node, -1); + this.innerData = data; + } + + // 根节点追加多个节点 + addNode4(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 2); + TiTreeUtil.addNode(data, node, -1); + this.innerData = data; + } + + // 父节点指定位置添加一个节点 + addNode5(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 1); + TiTreeUtil.addNode(data, node, 0, data[1]); + // 更新数据的选中状态。 + // 多选树且叶子节点的父节点可选时,如果父节点是选中的,添加一个非选中的叶子节点,父节点的选中状态应该改变, + // 叶子节点的选中状态决定父节点的选中状态,要保证数据节点的选中状态一致,使用此方法更新数据选中项。 + TiTreeUtil.updateChecked(data); + this.innerData = data; + } + + // 父节点指定位置添加多个节点 + addNode6(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 2); + TiTreeUtil.addNode(data, node, 0, data[1]); + // 更新数据的选中状态。原因参考addNode5方法内注释 + TiTreeUtil.updateChecked(data); + + this.innerData = data; + } + + // 父节点追加一个节点 + addNode7(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 1); + TiTreeUtil.addNode(data, node, -1, data[1]); + // 更新数据的选中状态。原因参考addNode5方法内注释 + TiTreeUtil.updateChecked(data); + + this.innerData = data; + } + + // 父节点追加多个节点 + addNode8(): void { + const data: Array = this.arr.concat(); + const node: Array = this.createData(++this.newId, 2); + TiTreeUtil.addNode(data, node, -1, data[1]); + // 更新数据的选中状态。原因参考addNode5方法内注释 + TiTreeUtil.updateChecked(data); + this.innerData = data; + } + + // 删除一个节点:美的空调 + deleteNode(): void { + const data: Array = this.arr.concat(); + TiTreeUtil.removeNode(data, data[0].children[0].children[0].children[1]); + // 更新数据的选中状态。原因参考addNode5方法内注释 + TiTreeUtil.updateChecked(data); + this.innerData = data; + } + // 选中并展开面部护理 + selectNode(): void { + const data: Array = this.arr.concat(); + TiTreeUtil.expandNode(data, data[2].children[0]); + TiTreeUtil.selectNode(data, data[2].children[0], true); + this.innerData = data; + } + + // 取消选中并展开海尔空调 + deSelectNode(): void { + const data: Array = this.arr.concat(); + TiTreeUtil.deSelectNode(data, data[0].children[0].children[0].children[0], true); + TiTreeUtil.expandNode(data, data[0].children[0].children[0].children[0]); + this.innerData = data; + } + + remove(): void { + this.innerData.pop(); + } +} diff --git a/src/tree/demo/src/app/tree/TreeVirtualscrollComponent.ts b/src/tree/demo/src/app/tree/TreeVirtualscrollComponent.ts new file mode 100644 index 0000000..32ac1c3 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeVirtualscrollComponent.ts @@ -0,0 +1,48 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { TiTreeComponent, TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-virtualscroll.html', + styleUrls: ['./treeTest.less'] +}) +export class TreeVirtualscrollComponent implements OnInit { + @ViewChild('tree', { static: true }) treeCom: TiTreeComponent; + data: Array = []; + ngOnInit(): void { + this.data = this.createData('node-radio', 3, 60); + } + createData(parentLabel: string, level: number, num: number): Array { + const result: Array = []; + for (let i: number = 0; i < num; i++) { + const item: TiTreeNode = { + label: `${parentLabel}-${i}` + }; + if (level > 1) { + item.children = this.createData(item.label, level - 1, num); + } + result.push(item); + } + + return result; + } + + scrollStart(): void { + this.treeCom.virtualScrollViewport.scrollToIndex(0, 'smooth'); + } + + scrollMiddle(): void { + TiTreeUtil.traverse(this.data, (node: TiTreeNode) => { + return (node.expanded = false); + }); + + this.treeCom.virtualScrollViewport.scrollToIndex(this.data.length / 2, 'smooth'); + } + + scrollEnd(): void { + TiTreeUtil.traverse(this.data, (node: TiTreeNode) => { + return (node.expanded = false); + }); + + this.treeCom.virtualScrollViewport.scrollToIndex(this.data.length, 'smooth'); + } +} diff --git a/src/tree/demo/src/app/tree/TreeVirtualscrollDragComponent.ts b/src/tree/demo/src/app/tree/TreeVirtualscrollDragComponent.ts new file mode 100644 index 0000000..000c371 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeVirtualscrollDragComponent.ts @@ -0,0 +1,43 @@ +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-virtualscroll-drag.html', + encapsulation: ViewEncapsulation.None +}) +export class TreeVirtualscrollDragComponent implements OnInit { + data1: Array = []; + data2: Array = []; + selectedData1: Array; + selectedData2: Array; + + ngOnInit(): void { + this.data1 = this.createData('node-radio', 3, 60); + this.data2 = this.createData('node-multiple', 3, 50); + } + + onSelect1(event: TiTreeNode): void { + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData1 = TiTreeUtil.getSelectedData(this.data1, false, false); + } + + onSelect2(event: TiTreeNode): void { + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData2 = TiTreeUtil.getSelectedData(this.data2, false, true); + } + + createData(parentLabel: string, level: number, num: number): Array { + const result: Array = []; + for (let i: number = 0; i < num; i++) { + const item: TiTreeNode = { + label: `${parentLabel}-${i}` + }; + if (level > 1) { + item.children = this.createData(item.label, level - 1, num); + } + result.push(item); + } + + return result; + } +} diff --git a/src/tree/demo/src/app/tree/TreeVirtualscrollSmallComponent.ts b/src/tree/demo/src/app/tree/TreeVirtualscrollSmallComponent.ts new file mode 100644 index 0000000..aba1614 --- /dev/null +++ b/src/tree/demo/src/app/tree/TreeVirtualscrollSmallComponent.ts @@ -0,0 +1,42 @@ +import { Component, OnInit } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './tree-virtualscroll-small.html' +}) +export class TreeVirtualscrollSmallComponent implements OnInit { + data1: Array = []; + data2: Array = []; + selectedData1: Array; + selectedData2: Array; + + ngOnInit(): void { + this.data1 = this.createData('node-radio', 3, 60); + this.data2 = this.createData('node-multiple', 3, 50); + } + + onSelect1(event: TiTreeNode): void { + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData1 = TiTreeUtil.getSelectedData(this.data1, false, false); + } + + onSelect2(event: TiTreeNode): void { + // 获取当前选中项,参数:树节点数据,选中项是否只包含叶子节点,是否多选 + this.selectedData2 = TiTreeUtil.getSelectedData(this.data2, false, true); + } + + createData(parentLabel: string, level: number, num: number): Array { + const result: Array = []; + for (let i: number = 0; i < num; i++) { + const item: TiTreeNode = { + label: `${parentLabel}-${i}` + }; + if (level > 1) { + item.children = this.createData(item.label, level - 1, num); + } + result.push(item); + } + + return result; + } +} diff --git a/src/tree/demo/src/app/tree/tree-before-expand.html b/src/tree/demo/src/app/tree/tree-before-expand.html new file mode 100644 index 0000000..cfd7c33 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-before-expand.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-before-more.html b/src/tree/demo/src/app/tree/tree-before-more.html new file mode 100644 index 0000000..b05facf --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-before-more.html @@ -0,0 +1,7 @@ + diff --git a/src/tree/demo/src/app/tree/tree-changedbycheckbox.html b/src/tree/demo/src/app/tree/tree-changedbycheckbox.html new file mode 100644 index 0000000..532c18e --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-changedbycheckbox.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-check-relation.html b/src/tree/demo/src/app/tree/tree-check-relation.html new file mode 100644 index 0000000..aa060e1 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-check-relation.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-disabled.html b/src/tree/demo/src/app/tree/tree-disabled.html new file mode 100644 index 0000000..6be040b --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-disabled.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-drag-beforedrop.html b/src/tree/demo/src/app/tree/tree-drag-beforedrop.html new file mode 100644 index 0000000..9123704 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-drag-beforedrop.html @@ -0,0 +1,7 @@ + + + {{item.label}} + 不可拖拽 + + + diff --git a/src/tree/demo/src/app/tree/tree-drag.html b/src/tree/demo/src/app/tree/tree-drag.html new file mode 100644 index 0000000..4e67809 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-drag.html @@ -0,0 +1,2 @@ + + diff --git a/src/tree/demo/src/app/tree/tree-event.html b/src/tree/demo/src/app/tree/tree-event.html new file mode 100644 index 0000000..f0fac0e --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-event.html @@ -0,0 +1,10 @@ + + diff --git a/src/tree/demo/src/app/tree/tree-icon.html b/src/tree/demo/src/app/tree/tree-icon.html new file mode 100644 index 0000000..a67eae7 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-icon.html @@ -0,0 +1,6 @@ +

    1.描述

    +

    树组件通过接口配置class类名设置图标(供于内部测试、追溯,不对外暴露该实例)

    +

    2.示例

    +
    + +
    diff --git a/src/tree/demo/src/app/tree/tree-load.html b/src/tree/demo/src/app/tree/tree-load.html new file mode 100644 index 0000000..aae4b16 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-load.html @@ -0,0 +1,35 @@ +

    1.描述

    +

    单选树, 数据加载和数据改变

    +

    2.示例

    + + + + + + + + + +
    +
    + +
    +
    +
      +
    • +

      {{i+1}}.选中项:

      + {{item.label}}: +
      +

      {{i+1}}.父节点:

      + {{child.label}}/ +


      +
    • +
    +
    +

    每个组件改变数据,都用下面六个按钮。不变化

    +
    +
    +
    +
    +
    +
    diff --git a/src/tree/demo/src/app/tree/tree-many.html b/src/tree/demo/src/app/tree/tree-many.html new file mode 100644 index 0000000..0a138d7 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-many.html @@ -0,0 +1,60 @@ +

    1.描述

    +

    主要是为了测试同一个页面中存在两个及以上的树组件,在各自的select接口中调用TiTreeUtil.getSelectedData返回的数据是否相互影响。

    +

    2.示例

    +

    第一棵树;

    + + + + + + + + + +
    +
    + +
    +
    +
    +
      +
    • +

      {{i+1}}.选中项:

      + {{item.label}}: +
      +

      {{i+1}}.父节点:

      + {{child.label}}/ +


      +
    • +
    +
    +
    + +

    第二棵树;

    + + + + + + + + + +
    +
    + +
    +
    +
    +
      +
    • +

      {{i+1}}.选中项:

      + {{item.label}}: +
      +

      {{i+1}}.父节点:

      + {{child.label}}/ +


      +
    • +
    +
    +
    diff --git a/src/tree/demo/src/app/tree/tree-multiselect.html b/src/tree/demo/src/app/tree/tree-multiselect.html new file mode 100644 index 0000000..8173a42 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-multiselect.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-operate.html b/src/tree/demo/src/app/tree/tree-operate.html new file mode 100644 index 0000000..6a3f767 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-operate.html @@ -0,0 +1,11 @@ + + diff --git a/src/tree/demo/src/app/tree/tree-parentcheckable.html b/src/tree/demo/src/app/tree/tree-parentcheckable.html new file mode 100644 index 0000000..303e9fa --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-parentcheckable.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-radioselect.html b/src/tree/demo/src/app/tree/tree-radioselect.html new file mode 100644 index 0000000..b87931d --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-radioselect.html @@ -0,0 +1,5 @@ + +
    +
    选中项: {{ selectedData[0].label }}
    +
    父节点:{{child.label}}/
    +
    diff --git a/src/tree/demo/src/app/tree/tree-search.html b/src/tree/demo/src/app/tree/tree-search.html new file mode 100644 index 0000000..21f11d2 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-search.html @@ -0,0 +1,30 @@ +
    +
    +

    1. 点击搜索图标搜索

    + +
    暂无数据
    + +
    +
    +

    2. 动态输入实时搜索

    + + +
    +
    diff --git a/src/tree/demo/src/app/tree/tree-shortcutkey.html b/src/tree/demo/src/app/tree/tree-shortcutkey.html new file mode 100644 index 0000000..f3f6fd8 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-shortcutkey.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-small.html b/src/tree/demo/src/app/tree/tree-small.html new file mode 100644 index 0000000..3f03809 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-small.html @@ -0,0 +1 @@ + diff --git a/src/tree/demo/src/app/tree/tree-template.html b/src/tree/demo/src/app/tree/tree-template.html new file mode 100644 index 0000000..d46b13e --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-template.html @@ -0,0 +1,14 @@ + + + + + + {{item.label}} + + + diff --git a/src/tree/demo/src/app/tree/tree-util.html b/src/tree/demo/src/app/tree/tree-util.html new file mode 100644 index 0000000..c4609a6 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-util.html @@ -0,0 +1,18 @@ + +

    1. 展开、折叠整个树

    + + +

    2. 添加节点

    + +
    + +
    + +
    + + +

    3. 选中、展开、删除

    + +
    + + diff --git a/src/tree/demo/src/app/tree/tree-virtualscroll-drag.html b/src/tree/demo/src/app/tree/tree-virtualscroll-drag.html new file mode 100644 index 0000000..f68d3d3 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-virtualscroll-drag.html @@ -0,0 +1,41 @@ +

    1.描述

    +

    树虚拟滚动 + 支持拖拽

    +

    数据量大时可使用 virtual 接口开启虚拟滚动,默认值为 false。使用虚拟滚动时,需要给 ti-tree 设置高度。

    +

    配置 nodeDraggable 接口使节点支持拖放, 默认值为 false。

    + +

    导入

    +

    import {{ '{' }} TiTreeModule {{ '}' }} from '@opentiny/ng';

    + +

    2.示例

    +
    +
    +

    2.1 单选

    + + +

    + 选中项label:{{item.label}} +
    + +
    +

    2.2 多选

    + + +

    + 选中项label:{{item.label}}, +
    +
    diff --git a/src/tree/demo/src/app/tree/tree-virtualscroll-small.html b/src/tree/demo/src/app/tree/tree-virtualscroll-small.html new file mode 100644 index 0000000..69e3f88 --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-virtualscroll-small.html @@ -0,0 +1,35 @@ +

    1.描述

    +

    虚拟滚动 + small

    +

    数据量大时可使用 virtual 接口开启虚拟滚动,默认值为 false。

    +

    使用虚拟滚动时,需要给 ti-tree 设置高度。

    + +

    导入

    +

    import {{ '{' }} TiTreeModule {{ '}' }} from '@opentiny/ng';

    + +

    2.示例

    + +
    +
    +

    2.1 small型单选

    + + +

    + 选中项label:{{item.label}} +
    + +
    +

    2.2 small型多选

    + + +

    + 选中项label:{{item.label}}, +
    +
    diff --git a/src/tree/demo/src/app/tree/tree-virtualscroll.html b/src/tree/demo/src/app/tree/tree-virtualscroll.html new file mode 100644 index 0000000..560317a --- /dev/null +++ b/src/tree/demo/src/app/tree/tree-virtualscroll.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/tree/demo/src/app/tree/treeTest.less b/src/tree/demo/src/app/tree/treeTest.less new file mode 100644 index 0000000..04106cd --- /dev/null +++ b/src/tree/demo/src/app/tree/treeTest.less @@ -0,0 +1,4 @@ +.tree-button { + width: 200px; + margin: 10px; +} diff --git a/src/tree/demo/src/app/tree/webdoc/tree-demos.js b/src/tree/demo/src/app/tree/webdoc/tree-demos.js new file mode 100644 index 0000000..d31d91b --- /dev/null +++ b/src/tree/demo/src/app/tree/webdoc/tree-demos.js @@ -0,0 +1,249 @@ +export default { + column: '2', + demos: [ + { + demoId: 'tree-radioselect', + name: { + 'zh-CN': '单选树', + 'en-US': 'tree radioselect' + }, + desc: { + 'zh-CN': '

    单选树的基本用法。

    ', + 'en-US': 'tree radioselect' + }, + apis: ['TiTreeComponent.properties.data'] + }, + { + demoId: 'tree-multiselect', + name: { + 'zh-CN': '多选树', + 'en-US': 'tree multiselect' + }, + desc: { + 'zh-CN': '

    通过属性multiple配置是否为多选树。

    ', + 'en-US': 'tree multiselect' + }, + apis: ['TiTreeComponent.properties.multiple'] + }, + { + demoId: 'tree-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'tree disabled' + }, + desc: { + 'zh-CN': + '

    通过数据项的disabled属性配置树节点是否为禁用状态;10.1.3 版本到 10.1.14 版本父节点禁用,展开收起图标也禁用;10.1.15 版本及之后:父节点禁用,展开收起图标不禁用。

    ', + 'en-US': 'tree disabled' + } + }, + { + demoId: 'tree-parentcheckable', + name: { + 'zh-CN': '父节点是否支持选中', + 'en-US': 'tree parentcheckable' + }, + desc: { + 'zh-CN': '

    通过属性parentCheckable配置多选树时父节点是否支持选中。

    ', + 'en-US': 'tree parentcheckable' + }, + apis: ['TiTreeComponent.properties.parentCheckable'] + }, + { + demoId: 'tree-check-relation', + name: { + 'zh-CN': '不关联父子节点', + 'en-US': 'tree check-relation' + }, + desc: { + 'zh-CN': '

    通过属性checkRelation配置父子节点选中时是否具有关联关系。

    ', + 'en-US': 'tree check relation' + }, + apis: ['TiTreeComponent.properties.checkRelation'] + }, + { + demoId: 'tree-changedbycheckbox', + name: { + 'zh-CN': '只有点击checkbox能选中', + 'en-US': 'tree changedbycheckbox' + }, + desc: { + 'zh-CN': '

    通过属性changedByCheckbox配置是否只有点击 checkbox 能选中树节点,仅为多选树时生效。

    ', + 'en-US': 'tree changedbycheckbox' + }, + apis: ['TiTreeComponent.properties.changedByCheckbox'] + }, + { + demoId: 'tree-template', + name: { + 'zh-CN': '自定义模板', + 'en-US': 'tree template' + }, + desc: { + 'zh-CN': '

    通过模板item配置树节点的内容。

    ', + 'en-US': 'tree template' + }, + apis: ['TiTreeComponent.slots.itemTemplate'] + }, + { + demoId: 'tree-shortcutkey', + name: { + 'zh-CN': '支持快捷键操作', + 'en-US': 'tree shortcutkey' + }, + desc: { + 'zh-CN': + '

    支持快捷键操作;上下键:在同级的树节点移动,左右键:展开收起树节点及跨层级移动焦点,Enter 键或 Space 键:选中或取消当前焦点的树节点。

    ', + 'en-US': 'tree shortcutkey' + } + }, + { + demoId: 'tree-drag', + name: { + 'zh-CN': '支持拖拽', + 'en-US': 'tree drag' + }, + desc: { + 'zh-CN': '

    通过属性nodeDraggable配置是否支持拖拽;当拖拽入目标树节点的时候触发nodeDrop事件。

    ', + 'en-US': 'tree drag' + }, + apis: ['TiTreeComponent.properties.nodeDraggable', 'TiTreeComponent.events.nodeDrop'] + }, + { + demoId: 'tree-drag-beforedrop', + name: { + 'zh-CN': '部分拖拽', + 'en-US': 'tree drag beforedrop' + }, + desc: { + 'zh-CN': + '

    通过数据项的draggable属性配置当前树节点是否支持拖拽;当当前树节点将要拖拽入目标树节点的时候触发beforeDrop事件。

    ', + 'en-US': 'tree drag beforedrop' + }, + apis: ['TiTreeComponent.events.beforeDrop', 'TiTreeDragNode'] + }, + { + demoId: 'tree-operate', + name: { + 'zh-CN': '支持操作按钮', + 'en-US': 'tree operate' + }, + desc: { + 'zh-CN': + '

    组件提供增加、编辑、删除三种悬浮操作按钮,分别通过数据项的addableeditabledeleteable属性控制;当点击增加按钮的时候触发nodeAdded事件,传递出去的树节点 parent属性;当点击编辑按钮的时候触发nodeEdited事件;当点击删除按钮的时候触发nodeDeleted事件。

    ', + 'en-US': 'tree operate' + }, + apis: [ + 'TiTreeComponent.events.nodeAdded', + 'TiTreeComponent.events.nodeEdited', + 'TiTreeComponent.events.nodeDeleted', + 'TiTreeComponent.events.afterNodeEdit', + 'TiTreeComponent.events.afterNodeAdd' + ] + }, + { + demoId: 'tree-event', + name: { + 'zh-CN': '事件', + 'en-US': 'tree event' + }, + desc: { + 'zh-CN': + '

    当选中树节点的时候触发select事件;当选中项发生改变的时候触发change事件;expandcollapse事件,当父节点展开时候触发expand事件;当父节点收起触发collapse事件。

    ', + 'en-US': 'tree event' + }, + apis: [ + 'TiTreeComponent.events.select', + 'TiTreeComponent.events.change', + 'TiTreeComponent.events.expand', + 'TiTreeComponent.events.collapse', + 'TiTreeNode' + ] + }, + { + demoId: 'tree-before-expand', + name: { + 'zh-CN': '异步请求', + 'en-US': 'tree before expand' + }, + desc: { + 'zh-CN': + '

    当树节点将要展开的时候触发beforeExpand事件,一般用于异步数据获取,异步加载状态分为 loading(正在加载)、success(加载成功)、error(加载失败)三种状态。

    ', + 'en-US': 'tree before expand' + }, + apis: ['TiTreeComponent.events.beforeExpand', 'TiTreeComponent.methods.getBeforeExpandNode', 'TiTreeComponent'] + }, + { + demoId: 'tree-before-more', + name: { + 'zh-CN': '分段加载数据', + 'en-US': 'tree before-more' + }, + desc: { + 'zh-CN': '

    当点击更多按钮的时候触发beforeExpand事件,一般用于大数据需要分段加载的场景。

    ', + 'en-US': 'tree before more' + }, + apis: ['TiTreeComponent.events.beforeMore'] + }, + { + demoId: 'tree-search', + name: { + 'zh-CN': '树搜索', + 'en-US': 'tree search' + }, + desc: { + 'zh-CN': '

    使用 Searchbox 组件配合实现树搜索的场景。

    ', + 'en-US': 'tree search' + }, + apis: ['TiTreeComponent.properties.highlightWords'] + }, + { + demoId: 'tree-small', + name: { + 'zh-CN': '紧凑型', + 'en-US': 'tree small' + }, + desc: { + 'zh-CN': '

    通过属性small配置树组件是否为紧凑型。

    ', + 'en-US': 'tree small' + }, + apis: ['TiTreeComponent.properties.small'] + }, + { + demoId: 'tree-util', + name: { + 'zh-CN': '公共方法', + 'en-US': 'tree util' + }, + desc: { + 'zh-CN': '

    树组件实现对树节点的增删查等操作的公共方法。

    ', + 'en-US': 'tree util' + }, + apis: [ + 'TiTreeUtil.methods.copy', + 'TiTreeUtil.methods.addNode', + 'TiTreeUtil.methods.deSelectNode', + 'TiTreeUtil.methods.expandNode', + 'TiTreeUtil.methods.getParentNode', + 'TiTreeUtil.methods.getSelectedData', + 'TiTreeUtil.methods.removeNode', + 'TiTreeUtil.methods.search', + 'TiTreeUtil.methods.selectNode', + 'TiTreeUtil.methods.traverse', + 'TiTreeUtil.methods.updateChecked' + ] + }, + { + demoId: 'tree-virtualscroll', + name: { + 'zh-CN': '虚拟滚动', + 'en-US': 'tree virtualscroll' + }, + desc: { + 'zh-CN': '

    通过属性virtual配置是否开启虚拟滚动,虚拟滚动时需要设置高度。

    ', + 'en-US': 'tree virtualscroll' + }, + apis: ['TiTreeComponent.properties.virtual'] + } + ] +}; diff --git a/src/tree/demo/src/app/tree/webdoc/tree.cn.md b/src/tree/demo/src/app/tree/webdoc/tree.cn.md new file mode 100644 index 0000000..6b96d6a --- /dev/null +++ b/src/tree/demo/src/app/tree/webdoc/tree.cn.md @@ -0,0 +1,31 @@ +--- +title: Tree 树 +--- +# Tree 树 + +
    + +Tree 是可以完整展现数据的层级关系,并具有展开收起选择等交互功能的组件。   + ++ 支持单选、多选两种场景。 + ++ 提供 TiTreeUtil 公共方法,包括增、删、改、查、选中、取消选中、遍历、筛选、获取选中项等操作。 + +```typescript +import { TiTreeModule } from '@opentiny/ng'; +``` + +
    + +
    + +Tree 是可以完整展现数据的层级关系,并具有展开收起选择等交互功能的组件。   + ++ 支持单选、多选两种场景。 + ++ 提供 TiTreeUtil 公共方法,包括增、删、改、查、选中、取消选中、遍历、筛选、获取选中项等操作。 + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    diff --git a/src/tree/demo/src/app/tree/webdoc/tree.en.md b/src/tree/demo/src/app/tree/webdoc/tree.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/tree/demo/src/app/tree/webdoc/tree.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/tree/demo/src/favicon.ico b/src/tree/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/tree/demo/src/index.html b/src/tree/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/tree/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/tree/demo/src/main.ts b/src/tree/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/tree/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/tree/demo/test.ts b/src/tree/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/tree/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/tree/demo/tsconfig.app.json b/src/tree/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/tree/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/tree/demo/tsconfig.spec.json b/src/tree/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/tree/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/tree/lib/index.ts b/src/tree/lib/index.ts new file mode 100644 index 0000000..7d1d7cb --- /dev/null +++ b/src/tree/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTreeModule'; diff --git a/src/tree/lib/ng-package.json b/src/tree/lib/ng-package.json new file mode 100644 index 0000000..550b1e2 --- /dev/null +++ b/src/tree/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/tree", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/tree/lib/package.json b/src/tree/lib/package.json new file mode 100644 index 0000000..132c20a --- /dev/null +++ b/src/tree/lib/package.json @@ -0,0 +1,20 @@ +{ + "name": "@opentiny/ng-tree", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/cdk": ">=13.0.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@angular/forms": ">=13.0.0", + "@opentiny/ng-checkbox": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-loading": "~1.0.0-beta.0", + "@opentiny/ng-text": "~1.0.0-beta.0", + "@opentiny/ng-validation": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/tree/lib/project.json b/src/tree/lib/project.json new file mode 100644 index 0000000..0bfa362 --- /dev/null +++ b/src/tree/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/tree/lib", + "sourceRoot": "src/tree/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/tree"], + "options": { + "project": "src/tree/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/tree"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js tree" + }, + { + "command": "ng default-build tree" + }, + { + "command": "node build/clear-default-theme.js tree" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/tree && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build tree && ng pack tree && node build/publish.js tree --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/tree/lib/src/TiAutoSelectDirective.ts b/src/tree/lib/src/TiAutoSelectDirective.ts new file mode 100644 index 0000000..8e0c711 --- /dev/null +++ b/src/tree/lib/src/TiAutoSelectDirective.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef } from '@angular/core'; + +/** + * @ignore + * 指令功能: 悬浮操作按钮场景,提供input框内容选中功能 + */ +@Directive({ + selector: '[tiAutoSelect]' +}) +export class TiAutoSelectDirective { + constructor(private elementRef: ElementRef) {} + ngOnInit(): void { + setTimeout(() => { + this.elementRef.nativeElement.select(); + }, 0); + } +} diff --git a/src/tree/lib/src/TiHighlightPipe.ts b/src/tree/lib/src/TiHighlightPipe.ts new file mode 100644 index 0000000..626fa63 --- /dev/null +++ b/src/tree/lib/src/TiHighlightPipe.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Pipe, PipeTransform } from '@angular/core'; +/** + * @ignore + * TiHighlightPipe 替换搜索的值并高亮显示 + */ +@Pipe({ + name: 'tiHighlight', + pure: true +}) +export class TiHighlightPipe implements PipeTransform { + transform(value: string, keyword: string) { + if (!keyword) { + return value; + } + const regx: RegExp = /[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘',。、]/gi; + const keywords: string = keyword.replace(regx, (item) => { + return `\\${item}`; + }); + const reg: RegExp = new RegExp(keywords, 'ig'); + const result = value.replace(reg, (word) => { + return `${word}`; + }); + return result; + } +} diff --git a/src/tree/lib/src/TiTreeComponent.ts b/src/tree/lib/src/TiTreeComponent.ts new file mode 100644 index 0000000..d4747e5 --- /dev/null +++ b/src/tree/lib/src/TiTreeComponent.ts @@ -0,0 +1,1111 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + Input, + IterableChanges, + IterableDiffer, + IterableDiffers, + Output, + QueryList, + Renderer2, + SimpleChanges, + TemplateRef, + ViewChild, + ViewChildren +} from '@angular/core'; +import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { TiKeymap, Util } from '@opentiny/ng-utils'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiTreeUtil } from './TiTreeUtil'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import packageInfo from '../package.json'; + +// TODO: 采用TiCheckgroup重构后,不需要那么多TreeUtil方法,不需要选中节点/取消选中节点方法。 +/** + * 树组件中每个节点的配置 + */ +export interface TiTreeNode { + /** + * 节点文本 + */ + label?: string; + /** + * 节点是否展开 + */ + expanded?: boolean; + /** + * 多选场景下,节点选中状态 + */ + checked?: boolean | string; + /** + * 节点展开时的字体图标,10.1.2 支持自定义模板添加图标 + */ + expandIcon?: string; + /** + * 节点收起时的字体图标,10.1.2 支持自定义模板添加图标 + */ + collapseIcon?: string; + /** + * 是否禁用 + */ + disabled?: boolean; + /** + * 子节点数据集 + */ + children?: Array; + /** + * 异步加载状态 + */ + loadStatus?: 'loading' | 'error' | 'success'; + /** + * 是否支持悬浮编辑功能 + */ + editable?: boolean; + /** + * 是否支持悬浮添加功能 + */ + addable?: boolean; + /** + * 是否支持悬浮删除功能 + */ + deleteable?: boolean; + /** + * 是否显示'更多'按钮 + */ + showMore?: boolean; + /** + * '更多'加载状态 + */ + moreStatus?: 'loading' | 'error' | 'success'; + /** + * 是否支持拖拽 + * 1.禁用状态不可拖拽; + * 2.非禁用状态: + * true: 节点可拖拽; + * false: 节点不可拖拽; + * undefined: 如果 nodeDraggable 为 true,则可拖拽;为 false 或者 undefined,则不可拖拽 + */ + draggable?: boolean; + /** + * @ignore + * 父节点 + */ + parent?: TiTreeNode; + /** + * @ignore + * 开启虚拟滚动时会给节点添加该内部属性 + * 节点层级,根节点层级为0,往下依次类推 + */ + level?: number; + /** + * @ignore + * 开启虚拟滚动时会给节点添加该内部属性 + * 是否为同层级且隶属同一父节点的节点集合中的最后一个 + */ + isLast?: boolean; + /** + * @ignore + * 开启虚拟滚动时会给节点添加该内部属性 + * 是否为同层级且隶属同一父节点的节点集合中的第一个 + */ + isFirst?: boolean; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} +/** + * drag info + */ +export interface TiTreeDragNode { + /** + * 事件对象 + */ + event?: DragEvent; + /** + * 目标节点 + */ + targetNode?: TiTreeNode; + /** + * 被拖拽的节点 + */ + dragNode?: TiTreeNode; + /** + * 放置位置,-1 代表当前节点前,0 代表当前节点里面,1 代表当前节点后 + */ + dropPosition?: number; +} +/** + * Tree树组件 + * + * 分类:支持单选、多选两种类型 + * + * 公共方法:树组件提供 [TiTreeUtil]{@link ../classes/TiTreeUtil.html} 公共方法,包括增、删、改、查、选中、取消选中、遍历、筛选、获取选中项等操作 + * + */ +@Component({ + selector: 'ti-tree', + templateUrl: './tree.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./tree.less'], + host: { + '[class.ti3-tree-virtual-scroll]': 'virtual' + } +}) +export class TiTreeComponent extends TiBaseComponent { + private static readonly DEFAULT_ITEM_SIZE: number = 30; + private static readonly DEFAULT_ITEM_LEFT_SPACE: number = 24; // .ti3-tree-leaf-node 元素左边距 + private static readonly SMALL_ITEM_LEFT_SPACE: number = 16; // .ti3-tree-leaf-node 元素左边距 + /** + * 组件全量数据 + */ + @Input() data: Array; + /** + * 多选场景下,父节点是否可被点击选中 + */ + @Input() parentCheckable: boolean = true; + /** + * 是否多选 + */ + @Input() multiple: boolean = false; + /** + * 多选场景下,当 changedByCheckbox 为 true 时: + * + * 1.点击文本,复选框状态不会改变,只会触发 select 回调; + * + * 2.点击复选框只会触发 change 回调; + * + * 3.复选框 disabled 时,点击文本会触发 select 事件 + */ + @Input() changedByCheckbox: boolean = false; + /** + * 是否高亮匹配树节点中搜索到的文本 + */ + @Input() highlightWords: string; + /** + * @ignore + * 支持拖放 + * + * 设置为true:所有节点可拖拽 + * + * 设置为false/undefined:所有节点不可拖拽 + * + * 如果节点设置了draggable属性,节点是否可拖拽就需要看draggable的值 + */ + /** + * 是否支持拖放 + */ + @Input() nodeDraggable: boolean = false; + /** + * 多选场景下,是否关联父子节点选中状态 + */ + @Input() checkRelation: boolean = true; + /** + * 是否开启虚拟滚动 + */ + @Input() virtual: boolean = false; + /** + * 节点展开前触发的回调,一般用于异步数据获取 + */ + @Output() readonly beforeExpand: EventEmitter = new EventEmitter(); + /** + * 点击节点时触发的回调 + */ + @Output() readonly select: EventEmitter = new EventEmitter(); + /** + * 当前选中项改变时触发的回调,参数:改变的选中项 + */ + @Output() readonly change: EventEmitter = new EventEmitter(); + /** + * 拖拽节点放入目标节点前触发的回调,参数:当前拖拽节点 + */ + @Output() readonly beforeDrop: EventEmitter = new EventEmitter(); + /** + * 鼠标在拖放目标上释放时触发的回调,参数:当前拖拽节点 + */ + @Output() readonly nodeDrop: EventEmitter = new EventEmitter(); + /** + * 点击悬浮添加节点按钮时触发的回调,参数:当前新增的节点 + */ + @Output() readonly nodeAdded: EventEmitter = new EventEmitter(); + /** + * 点击悬浮编辑节点按钮时触发的回调,参数:当前编辑的节点 + */ + @Output() readonly nodeEdited: EventEmitter = new EventEmitter(); + /** + * 点击悬浮删除节点按钮时触发的回调,参数:当前删除的节点 + */ + @Output() readonly nodeDeleted: EventEmitter = new EventEmitter(); + /** + * 点击 '更多' 按钮时触发的回调,参数:当前点击的 '更多' 按钮节点 + */ + @Output() readonly beforeMore: EventEmitter = new EventEmitter(); + /** + * 节点展开时触发的回调,参数:当前点击的节点 + */ + @Output() readonly expand: EventEmitter = new EventEmitter(); + /** + * 节点折叠时触发的回调,参数:当前点击的节点 + */ + @Output() readonly collapse: EventEmitter = new EventEmitter(); + /** + * 新增节点后的回调,参数:当前新增的节点 + */ + @Output() readonly afterNodeAdd: EventEmitter = new EventEmitter(); + /** + * 编辑节点后的回调,参数:当前编辑的节点 + */ + @Output() readonly afterNodeEdit: EventEmitter = new EventEmitter(); + /** + * 用于异步场景:当前点击需要展开的父节点 + */ + private beforeExpandNode: TiTreeNode; + /** + * 监听data改变 + */ + private dataDiffer: IterableDiffer; + /** + * @ignore + * 获取到用户自定义的模板 + */ + @ContentChild(TemplateRef, { static: true }) itemTemplate: TemplateRef; + /** + * @ignore + * 获取文本区域dom集合 + */ + @ViewChildren('nodeList') elems: QueryList; + /** + * CdkVirtualScrollViewport 实例 + */ + @ViewChild(CdkVirtualScrollViewport, { static: false }) + virtualScrollViewport: CdkVirtualScrollViewport; + /** + * @ignore + * 模板中使用,高亮的选中项 + */ + public actived: TiTreeNode; + /** + * @ignore + * 词条 + */ + public treeLan = TiLocale.getLocaleWords().tiTree; + /** + * @ignore + * 模板中使用,虚拟滚动时每个节点占据的高度 + */ + public itemSize: number = TiTreeComponent.DEFAULT_ITEM_SIZE; + /** + * 被拖拽节点 + */ + private dragNode: TiTreeNode; + /** + * 拖拽position + */ + private dropPosition: number; + /** + * 拖放经过的节点 + */ + private overNode: TiTreeNode; + /** + * 当前操作的节点是否为新增的节点 + */ + private addingNode: boolean = false; + /** + * 是否为 small(紧凑型) 的树 + */ + private isSmall: boolean = false; + private oldData: Array; + protected versionInfo: string = super.getVersion(packageInfo); + constructor( + protected elementRef: ElementRef, + protected renderer2: Renderer2, + protected iterableDiffers: IterableDiffers, + private tiRenderer: TiRenderer, + private cdRef: ChangeDetectorRef + ) { + super(elementRef, renderer2); + } + ngOnInit(): void { + super.ngOnInit(); + this.dataDiffer = this.iterableDiffers.find(this.data).create((index: number, item: TiTreeNode) => { + return item.checked; + }); + // 内部使用的数据,用于记录用户的操作改变 + // TODO: 仅在初始化时挡非法数据,是不够的。建议去除。但因为要兼容已发出的版本,所以不去除。 + this.data = !Util.isArray(this.data) ? [] : this.data; + this.oldData = this.data; + this.isSmall = this.nativeElement.hasAttribute('small'); + } + + ngOnChanges(changes: SimpleChanges): void { + // 动态修改选中项时需要修改索引才能使选中项高亮 + if (changes['data'] && !changes['data'].firstChange) { + // 选中项背景高亮 + this.actived = this.initActived(this.data); + } + } + + ngDoCheck(): void { + super.ngDoCheck(); + if (this.data === this.oldData) { + const dataChanges: IterableChanges = this.dataDiffer.diff(this.data); + if (dataChanges) { + // 重新初始化选中项背景高亮 + this.actived = this.initActived(this.data); + } + } else { + this.oldData = this.data; + } + + // 异步挂载数据,改变节点属性时指引未发生变化,onpush模式下不会触发变更,故手动触发 + this.cdRef.markForCheck(); + } + /** + * @ignore + * @description 判断是否显示复选框 + * @param node 节点数据 + */ + public showCheckboxFn(node: TiTreeNode): boolean { + if (this.multiple !== true) { + return false; + } + + if (this.parentCheckable === true) { + return true; + } + + return TiTreeUtil.isLeaf(node); + } + + /** + * @ignore + * @description 点击父节点图标执行的逻辑 + * @param node 当前节点数据 + */ + public onClickPnodeIcon(node: TiTreeNode, event: MouseEvent): void { + // 阻止事件冒泡:点击父节点图标无高亮样式 + event.stopPropagation(); + this.beforeExpandNode = node; + // 1.当前节点是展开状态 + if (node.expanded) { + node.expanded = false; + this.collapse.emit(node); + } else if (this.beforeExpand.observers.length === 0) { + // 2.如果未定义beforeExpand事件(非异步),点击时让节点展开 + node.expanded = true; + this.expand.emit(node); + } else { + // 3.异步获取数据:将组件实例通知出去 + this.beforeExpand.emit(this); + } + } + + /** + * 获取当前需要展开的父节点,适用于异步场景 + */ + public getBeforeExpandNode(): TiTreeNode { + return this.beforeExpandNode; + } + /** + * @ignore + * @param node 节点 + */ + public onClickReload(event: MouseEvent, node): void { + node.expanded = false; + this.onClickPnodeIcon(node, event); + } + + /** + * @ignore + * @description 根据item的isExpanded属性获取item图标 + * @param node 当前节点数据 + */ + public getItemIcon(node: TiTreeNode): string { + if (TiTreeUtil.isLeaf(node)) { + return `${node.expandIcon} ti3-tree-node-icon`; + } + + return `${node.expanded ? node.expandIcon : node.collapseIcon} ti3-tree-node-icon`; + } + + /** + * @ignore + * @description 点击复选框触发select、change事件 + * 需要注意:click事件中拿到的是操作前的选中状态,而change事件中拿到的是操作后的选中状态 + * @param node 当前节点数据 + */ + public onInputChange(node: TiTreeNode, event: Event): void { + if (node.disabled === true) { + return; + } + // 点击当前项高亮 + this.actived = node; + if (this.checkRelation) { + this.setSeletedState(node, this.data, node.checked); + } + if (!this.changedByCheckbox) { + this.select.emit(node); + } + this.change.emit(node); + } + + /** + * @ignore + * 点击文本区域 + * @param node 当前节点数据 + * @param event 鼠标事件 + * @returns void + */ + public onItemWrapperClick(node: TiTreeNode): void { + if (node.disabled === true || node.editing) { + return; + } + // 悬浮功能: 点击生成节点时,显示操作按钮 + if (!node.editing) { + node.isHover = true; + } + // 点击当前项高亮 + this.actived = node; + // 1.处理多选情况 + if (this.multiple === true) { + if (this.changedByCheckbox) { + this.select.emit(node); + + return; + } + + // 1.1 处理父节点不支持多选 + if (!this.showCheckboxFn(node)) { + return; + } + + // 1.2 处理父节点支持多选 + node.checked = node.checked !== true; + // 处理当前节点选中状态变化后,对父子节点的影响 + if (this.checkRelation) { + this.setSeletedState(node, this.data, node.checked); + } + this.select.emit(node); + this.change.emit(node); + + return; + } + + // 2.处理单选场景 + // 2.1(单选且已选中)或者(单选且非叶子节点)的情况下,点击只会触发select事件,因为当前选中项不会发生变化 + if (node.checked === true || !TiTreeUtil.isLeaf(node)) { + // 触发select事件 + this.select.emit(node); + + return; + } + + // 2.2单选叶子节点未选中 + this.deSelectAllNode(this.data); + node.checked = true; + TiTreeUtil.selectParents(node, this.data, 'indeterminate'); // 设置祖先节点的选中状态 + + // 触发select和change事件 + this.select.emit(node); + + this.change.emit(node); + } + + // 处理当前节点选中状态变化后,对父子节点的影响 + private setSeletedState = (node: TiTreeNode, allData: Array, checked: boolean | string): void => { + if (checked === true) { + TiTreeUtil.selectAllChildren(node); + TiTreeUtil.selectParents(node, allData, true); + } else { + TiTreeUtil.deSelectAllChildren(node); + TiTreeUtil.deSelectParents(node, allData); + } + }; + + /** + * 单选时,取消所有节点的选中状态 + * @param allData 所有节点数据 + */ + private deSelectAllNode = (allData: Array): void => { + allData.forEach((node: TiTreeNode) => { + TiTreeUtil.deSelectAllChildren(node); + }); + }; + + /** + * @ignore + */ + public trackByFn(index: number, node: any): any { + return index; + } + /** + * @ignore + * 判断是否为叶子节点 + */ + public isLeaf(node: TiTreeNode): boolean { + return !Util.isArray(node.children); + } + + // 初始化选中项高亮 + private initActived = (data: Array): any => { + let result: TiTreeNode; + for (const node of data) { + if (node.checked === true) { + return node; + } + + if (!TiTreeUtil.isLeaf(node)) { + result = this.initActived(node.children); + } + + if (!Util.isUndefined(result)) { + return result; + } + } + + return result; + }; + /** + * @ignore + * @param node 节点数据 + */ + public onBlur(node: TiTreeNode): void { + if (node.focused) { + node.focused = false; // 失焦后节点删除获焦标志类 + } + } + /** + * @ignore + * @param node 节点数据 + */ + public onFocus(node: TiTreeNode): void { + if (!node.focused && !node.disabled) { + node.focused = true; // 设置获焦标志类 + } + } + /** + * @ignore + * @param event 键盘事件 + * @param node 当前节点数据 + */ + public onKeydown(event: KeyboardEvent, node: TiTreeNode): void { + /** + * 快捷键交互定义 + * 1,tab键切换焦点至树组件的文本区域,复选框无需提供焦点,文本样式为文本悬浮态 + * 2,树组件内部焦点切换支持上下左右键: + * -上下键:按上下键可以在并列的节点移动,如无并列节点无法移动。 + * -左右键:展开收起节点及跨层级移动焦点。当焦点再子节点时,按左键可收起改节点,焦点移动至父节点; + * 当焦点在父节点上时,按右键可将焦点移动到启子节点上,如节点未展开则同时展开该子节点 + * 3,Enter键或Space键:当焦点在节点名称上时,按下键则选中或取消该节点,选中后节点变为选中态 + */ + // 阻止默认和冒泡 + if ( + [ + TiKeymap.KEY_ENTER, + TiKeymap.KEY_NUMPAD_ENTER, + TiKeymap.KEY_SPACE, + TiKeymap.KEY_ARROW_UP, + TiKeymap.KEY_ARROW_DOWN, + TiKeymap.KEY_ARROW_LEFT, + TiKeymap.KEY_ARROW_RIGHT + ].indexOf(event.keyCode) > -1 + ) { + event.preventDefault(); + event.stopPropagation(); + } + switch (event.keyCode) { + case TiKeymap.KEY_ENTER: // ENTER键 + case TiKeymap.KEY_NUMPAD_ENTER: // ENTER键(苹果数字小键盘) + case TiKeymap.KEY_SPACE: + this.onItemWrapperClick(node); + break; + case TiKeymap.KEY_ARROW_UP: // 向上箭头,同级 + this.setKeyDownUp(node, 'up'); + break; + case TiKeymap.KEY_ARROW_DOWN: // 向下箭头,同级 + this.setKeyDownUp(node, 'down'); + break; + case TiKeymap.KEY_ARROW_LEFT: // 向左箭头,跨节点层级 + this.setKeyleft(node); + break; + case TiKeymap.KEY_ARROW_RIGHT: // 向右箭头,跨节点层级 + this.setKeyright(node); + break; + default: + break; + } + } + + // 上下键 + private setKeyDownUp(node: TiTreeNode, type: string): void { + let parArr: Array; + const pareNode: TiTreeNode = TiTreeUtil.getParentNode(this.data, node); // 获取父节点 + if (pareNode) { + parArr = pareNode.children; + } else { + parArr = this.data; + } + const curIndex: number = parArr.findIndex((item: TiTreeNode) => item === node); // 获取当前节点在其父节点集合的index + this.setFocusNode(node, parArr, curIndex, type); + } + // 左键 + private setKeyleft(node: TiTreeNode): void { + // 当正在编辑时,关闭其父节点时,节点恢复文本状态 + if (node.editing && node.label !== '') { + delete node.editing; + } + const pareNode: TiTreeNode = TiTreeUtil.getParentNode(this.data, node); + if (!pareNode) { + // 当前节点时一级节点时 return + return; + } + pareNode.expanded = false; + this.collapse.emit(node); + if (pareNode.disabled) { + this.setKeyleft(pareNode); + + return; + } + pareNode.focused = true; + this.setFocusElem(node); + } + // 右键 + private setKeyright(node: TiTreeNode): void { + if (TiTreeUtil.isLeaf(node)) { + return; + } + const childNodeArr: Array = node.children; + const childnoDisabledArr: Array = childNodeArr.filter((childNode: TiTreeNode) => !childNode.disabled); // 获取子节点未禁用的节点集合 + if (childnoDisabledArr.length < 1) { + return; + } + const childFirstNode: TiTreeNode = childnoDisabledArr[0]; // 未禁用的节点集合的first获取焦点 + childFirstNode.focused = true; + if (!node.expanded) { + node.expanded = true; + this.expand.emit(node); + } + this.setFocusElem(node); + } + // 上下键焦点处理 + private setFocusNode(node: TiTreeNode, parList: Array, curIndex: number, type: string): void { + let targetNode: TiTreeNode; + let targetIndex: number; + let _curIndex: number = curIndex; + if (type === 'up') { + // 同级获取上一个节点 + targetIndex = _curIndex - 1; + if (targetIndex < 0) { + targetIndex = parList.length - 1; + } + targetNode = parList[targetIndex]; + } else { + // 同级获取下一个节点 + targetIndex = _curIndex + 1; + if (targetIndex > parList.length - 1) { + targetIndex = 0; + } + targetNode = parList[targetIndex]; + } + // 上下移动时,first或last节点禁用时,return + if ((targetNode === parList[0] || targetNode === parList[parList.length - 1]) && targetNode.disabled) { + return; + } + // 当中间节点有禁用时,移动到禁用的节点上一个或下一个节点 + if (targetNode.disabled) { + _curIndex = targetIndex; + this.setFocusNode(node, parList, _curIndex, type); + + return; + } + targetNode.focused = true; + this.setFocusElem(node); + } + + // 目标元素获取焦点 + private setFocusElem(node: TiTreeNode): void { + if (node.focused) { + node.focused = false; + } + // 当按键操作会改变文本元素是否添加class`ti3-tree-text-focus`, 加延时是为了class变更后,获取正确的含有class的文本区域元素 + // 类ti3-tree-text-focus作为获焦元素的标志 + setTimeout(() => { + // 找到含有类ti3-tree-text-focus的文本元素 + const focusNode: ElementRef = this.elems.toArray().find((elem: ElementRef) => { + return this.tiRenderer.hasClass(elem.nativeElement, 'ti3-tree-text-focus'); + }); + if (focusNode) { + focusNode.nativeElement.focus(); + } + }, 0); + } + + /** + * @ignore + * @param node 当前节点数据 + * @description 节点是否可拖拽 + */ + public isDraggable(node: TiTreeNode): boolean { + return !node.disabled && (node.draggable || (this.nodeDraggable && node.draggable !== false)); + } + + /** + * @ignore + * @param startEvent 鼠标事件 + * @param node 当前节点数据 + * @description 拖拽开始时在被拖拽元素上触发此事件 + */ + public onDragstart(event: DragEvent, node: TiTreeNode): void { + // 当节点的draggable属性设置为false表示不可拖拽,但是选中文本时会触发dragstart事件,故阻止默认行为 + if (!this.isDraggable(node)) { + event.preventDefault(); + + return; + } + + event.stopPropagation(); + this.dragNode = node; // 保存被拖拽的节点 + } + + /** + * @ignore + * @param enterEvent 鼠标事件 + * @param node 当前节点数据 + * @description 拖拽鼠标进入元素时在该元素上触发 + */ + public onDragenter(event: DragEvent, node: TiTreeNode): void { + event.preventDefault(); + this.overNode = null; + + // 拖拽节点和目标节点是同一节点,retuen + if (node === this.dragNode) { + return; + } + + // 保存移动到的节点 + this.overNode = node; + + // 拖拽的节点拖拽到其子节点时,关闭其节点 + if (this.dragNode === TiTreeUtil.getParentNode(this.data, node)) { + this.dragNode.expanded = false; + this.collapse.emit(node); + } + } + + /** + * @ignore + * @param overEvent 鼠标事件 + * @param node 当前节点数据 + * @description 拖拽时鼠标在目标元素上移动时触发 + */ + public onDragover(event: DragEvent, node: TiTreeNode): void { + event.preventDefault(); + event.dataTransfer.effectAllowed = 'move'; + if (node === this.overNode) { + this.clearDragClass(event); // 清除节点之前的样式 + this.dropPosition = this.calcDropPosition(event); // 获取移动到节点的位置 + // 拖拽经过父节点,放置内部位置时,且该节点未展开时,展开其节点 + if (this.dropPosition === 0 && !node.expanded && node.children && node.children.length > 1) { + node.expanded = true; + this.expand.emit(node); + } + // 设置拖动过程中的状态样式 + this.renderer2.addClass(event.currentTarget, this.setDragOverClass(node)); + } + } + + /** + * @ignore + * @param leaveEvent 鼠标事件 + * @param node 当前节点数据 + * @description 拖拽时鼠标在离开目标元素时触发 + */ + public onDragleave(event: DragEvent, node: TiTreeNode): void { + event.preventDefault(); + this.clearDragClass(event); + } + + /** + * @ignore + * @param dropEvent 鼠标事件 + * @param node 当前节点数据 + * @description 鼠标在拖放目标上释放时,在拖放目标上触发 + */ + public onDrop(event: DragEvent, node: TiTreeNode): void { + event.preventDefault(); + this.dragNode.focused = false; + this.clearDragClass(event); + // 异步加载节点时禁止放置到节点内部 + if (this.dropPosition === 0 && node.loadStatus === 'loading') { + return; + } + // 拖拽节点和目标节点是同一节点,retuen + if (node === this.dragNode) { + return; + } + const params: TiTreeDragNode = { + event, + targetNode: node, + dragNode: this.dragNode, + dropPosition: this.dropPosition + }; + + if (this.beforeDrop.observers.length > 0) { + this.beforeDrop.emit(params); + + return; + } + + TiTreeUtil.dropApply(params, this.data); + + // 拖放节点是父节点且是收起状态时,展开该节点 + if (node.children && node.children.length > 0 && !node.expanded) { + node.expanded = true; + this.expand.emit(node); + } + this.nodeDrop.emit(params); + } + + /** + * @ignore + * @param endEvent 鼠标事件 + * @param node 当前节点数据 + * @description 鼠标在拖放目标上释放时,在拖拽元素上触发 + */ + public onDragend(event: DragEvent, node: TiTreeNode): void { + event.preventDefault(); + this.dragNode = null; + this.overNode = null; + } + + // 计算拖拽节点的放置方式0(作为目标节点的子节点),-1(放置在目标节点的前面),1(放置在目标节点的后面) + private calcDropPosition(event: DragEvent): number { + const clientY: number = event.clientY; + const { top, height } = (event.target as HTMLElement).getBoundingClientRect(); + const gapHeight: number = height / 3; + if (clientY > top + height - gapHeight) { + return 1; + } + if (clientY < top + gapHeight) { + return -1; + } + + return 0; + } + + // 清除拖拽样式 + private clearDragClass(event: DragEvent): void { + // 拖拽过程中的三种状态 + const dragClasses: Array = ['ti3-tree-drag-over-inner', 'ti3-tree-drag-over-top', 'ti3-tree-drag-over-bottom']; + dragClasses.forEach((item: string) => { + this.renderer2.removeClass(event.currentTarget, item); + }); + } + + // 根据拖动的位置获取相应的样式 + private setDragOverClass(node: TiTreeNode): string { + if (node !== this.overNode) { + return; + } + if (this.dropPosition === 0) { + return 'ti3-tree-drag-over-inner'; + } else if (this.dropPosition === -1) { + return 'ti3-tree-drag-over-top'; + } else if (this.dropPosition === 1) { + return 'ti3-tree-drag-over-bottom'; + } + + return ''; + } + + /** + * @ignore + * @param node 节点 + * @param type hover标志 + */ + public onMousenode(node: TiTreeNode, type: string): void { + // 鼠标进入节点且不是编辑状态时,显示操作按钮 + if (type === 'enter' && !node.editing) { + node.isHover = true; + } else { + delete node.isHover; + } + } + + /** + * @ignore + * @param node 节点 + */ + public onBlurEdit(node: TiTreeNode): void { + if (node.label === '') { + return; + } + + delete node.editing; + + if (this.addingNode) { + this.addingNode = false; + this.afterNodeAdd.emit(node); + + return; + } + + this.afterNodeEdit.emit(node); + } + + /** + * @ignore + * @param node 节点 + */ + public onKeydownEdit(event: KeyboardEvent): void { + // 当中文输入法成对输入符号(中括号 大括号),默认会触发左键将光标移动到前后符号中间,会触发父节点的左键事件,导致父节点收起 + event.stopPropagation(); + } + + /** + * @ignore + * @param deleteEvent 删除事件 + * @param node 节点 + */ + public deleteNode(event: Event, node: TiTreeNode): void { + // 阻止事件冒泡 防止触发选中 + event.stopPropagation(); + const parentNode: TiTreeNode = TiTreeUtil.getParentNode(this.data, node); + TiTreeUtil.removeNode(this.data, node); + // 节点的父节点无子节点时, 删除children属性,解决依旧会显示展开图标问题 + if (parentNode && parentNode.children.length < 1) { + delete parentNode.children; + } + // 更新多选状态 + if (this.multiple) { + TiTreeUtil.updateChecked(this.data); + } + this.nodeDeleted.emit(node); + } + + /** + * @ignore + * @param editEvent 编辑事件 + * @param node 节点 + */ + public editNode(event: Event, node: TiTreeNode): void { + // 阻止事件冒泡 防止触发选中 + event.stopPropagation(); + node.editing = true; + delete node.isHover; + this.nodeEdited.emit(node); + // autofocus视图变化时 会报错 视图变化时 强制变检一次,消除报错(Expression has changed after it was checked) + this.cdRef.detectChanges(); + } + + /** + * @ignore + * @param addEvent 增加事件 + * @param node 节点 + */ + public addNode(event: Event, node: TiTreeNode): void { + // 阻止事件冒泡 防止触发选中 + event.stopPropagation(); + this.addingNode = true; + const newNode: TiTreeNode = { + label: this.treeLan.newNode, + editing: true, + parent: node + }; + node.expanded = true; + this.expand.emit(node); + this.nodeAdded.emit(newNode); + TiTreeUtil.addNode(this.data, [newNode], 0, node); + // 更新多选状态 + if (this.multiple) { + TiTreeUtil.updateChecked(this.data); + } + // autofocus视图变化时 会报错 强制变检一次,消除报错(Expression has changed after it was checked) + this.cdRef.detectChanges(); + } + /** + * @ignore + * @param node 节点 + */ + public onClickMore(node: TiTreeNode): void { + this.beforeMore.emit(node); + } + + /** + * @ignore + * 开启虚拟滚动时需要对节点数据做扁平化处理 + * @param nodes 同层级且隶属同一父节点的节点集合 + * @param level 节点层级,根节点层级为0,往下依次类推 + */ + public getFlatData(nodes: Array, level?: number): Array { + let result: Array = []; + if (!nodes) { + return result; + } + nodes.forEach((item: TiTreeNode, index: number): void => { + item.level = level ? level : 0; + item.isLast = index === nodes.length - 1; + item.isFirst = index === 0; + result.push(item); + if (item.expanded && item.children && item.children.length > 0) { + result = result.concat(this.getFlatData(item.children, item.level + 1)); + } + }); + + return result; + } + + /** + * @ignore + * 虚拟滚动扁平化结构时,获取不同层级节点左侧的缩进 + * @param node 节点 + */ + public getNodeLeftSpace(node: TiTreeNode): string { + const space: number = this.isSmall ? TiTreeComponent.SMALL_ITEM_LEFT_SPACE : TiTreeComponent.DEFAULT_ITEM_LEFT_SPACE; + + return `${node.level * space}px`; + } + /** + * @ignore + * 是否为根节点 + * @param node 节点 + */ + public isRootNode(node: TiTreeNode): boolean { + return this.data.includes(node); + } + /** + * @ignore + * @param level 节点层级 + */ + public getVerticalGuideLines(level: number): Array { + return new Array(level); + } + + /** + * @ignore + * @param level 节点层级 + */ + public getVerticalGuideLineLeft(level: number): string { + const size: number = this.isSmall ? TiTreeComponent.SMALL_ITEM_LEFT_SPACE : TiTreeComponent.DEFAULT_ITEM_LEFT_SPACE; + // 其中 6 和 8 是 展开/收起图标宽度的一半 + const offset: number = this.isSmall ? 6 : 8; + + return `${(level * size + offset) * -1}px`; + } +} diff --git a/src/tree/lib/src/TiTreeModule.ts b/src/tree/lib/src/TiTreeModule.ts new file mode 100644 index 0000000..0beb93e --- /dev/null +++ b/src/tree/lib/src/TiTreeModule.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ScrollingModule } from '@angular/cdk/scrolling'; +import { TiTreeComponent } from './TiTreeComponent'; +import { TiCheckboxModule } from '@opentiny/ng-checkbox'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiLoadingModule } from '@opentiny/ng-loading'; +import { TiHighlightPipe } from './TiHighlightPipe'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiTextModule } from '@opentiny/ng-text'; +import { TiValidationModule } from '@opentiny/ng-validation'; +import { TiAutoSelectDirective } from './TiAutoSelectDirective'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ScrollingModule, + TiCheckboxModule, + TiIconModule, + FormsModule, + TiLoadingModule, + TiRendererModule, + TiTextModule, + TiValidationModule + ], + exports: [TiTreeComponent], + declarations: [TiTreeComponent, TiHighlightPipe, TiAutoSelectDirective] +}) +export class TiTreeModule { + constructor() { + TiLocale.setTiWords(locales); + } +} + +export * from './TiTreeComponent'; +export { TiTreeComponent, TiTreeNode, TiTreeDragNode } from './TiTreeComponent'; +export { TiTreeUtil } from './TiTreeUtil'; diff --git a/src/tree/lib/src/TiTreeUtil.ts b/src/tree/lib/src/TiTreeUtil.ts new file mode 100644 index 0000000..a6c5df6 --- /dev/null +++ b/src/tree/lib/src/TiTreeUtil.ts @@ -0,0 +1,620 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Util } from '@opentiny/ng-utils'; +import { TiTreeNode, TiTreeDragNode } from './TiTreeComponent'; +/** + * + * 用于封装给树组件 [TiTreeComponent]{@link ../components/TiTreeComponent.html} + * 提供公共方法,包括增、删、改、查、选中、取消选中、遍历、筛选、获取选中项等操作 + */ +export class TiTreeUtil { + private static selectedData: Array = []; + private static pnode: Array = []; + /** + * 查找指定节点的父节点 + * @param data 所有节点数据 + * @param node 节点 + */ + public static getParentNode(data: Array, node: TiTreeNode): TiTreeNode { + if (!Array.isArray(data)) { + return; + } + + for (const item of data) { + if (!TiTreeUtil.isLeaf(item)) { + if (item.children.indexOf(node) !== -1) { + return item; + } else { + const result: TiTreeNode = TiTreeUtil.getParentNode(item.children, node); + if (result) { + return result; + } + } + } + } + } + + /** + * 对整个树的每个节点执行一次给定的函数 + * @param data 所有节点数据 + * @param traverseFn 对各个节点执行的操作 + */ + public static traverse(data: Array, traverseFn: Function): void { + if (!Util.isFunction(traverseFn) || !Array.isArray(data)) { + return; + } + + for (const item of data) { + traverseFn(item); + if (!TiTreeUtil.isLeaf(item)) { + TiTreeUtil.traverse(item.children, traverseFn); + } + } + } + + /** + * 更新多选树各节点的选中状态;主要用于手动添加或删除节点时,当前节点的兄弟节点和父节点的选中状态不一致。 + * @param data 所有节点数据 + */ + public static updateChecked(data: Array): void { + TiTreeUtil.check(data); + } + + /** + * 检查当前数据的多选状态 + * @param data 当前节点集合 + * @param allData 全部数据集合 + */ + private static check(data: Array): void { + for (const item of data) { + // 非叶子节点深度优先递归,再回溯当前节点 + if (!TiTreeUtil.isLeaf(item)) { + TiTreeUtil.check(item.children); + // 根据子节点计算当前节点选中状态 + item.checked = TiTreeUtil.computeChecked(item); + } + } + } + + /** + * 当前节点是否是叶子节点的父节点, + * 因为更新节点的选中状态时,根据叶子节点的选中状态计算出父节点的选中状态,再更新父节点的选中状态。 + * @param node 当前节点 + */ + private static isLeafsParentNode(node: TiTreeNode): boolean { + if (!Array.isArray(node.children)) { + return false; + } else { + for (const child of node.children) { + if (Array.isArray(child.children)) { + return false; + } + } + + return true; + } + } + + /** + * 根据子节点的多选状态,计算出当前节点的多选状态 + * @param node 当前节点 + */ + private static computeChecked(node: TiTreeNode): boolean | string { + const childrens: Array = node.children; + if (!childrens?.length) { + return node?.checked; + } + let selectedNum: number = 0; + let unSelectedNum: number = 0; + for (const child of childrens) { + if (child.checked === true) { + selectedNum++; + } else if (!child.checked) { + // false和undefined 两种 + unSelectedNum++; + } + } + if (selectedNum === childrens.length) { + return true; + } else if (unSelectedNum === childrens.length) { + return false; + } else { + return 'indeterminate'; + } + } + + /** + * 添加节点 + * @param data 所有节点数据 + * @param node 添加的一个或多个节点 + * @param index 添加的位置: -1表示从尾部追加 + * @param pNode 指定要添加的节点的父节点,没有指定父节点时默认添加到根节点 + */ + public static addNode(data: Array, node: Array, index: any, pNode?: TiTreeNode): void { + const k: number = parseInt(index, 10); + if (isNaN(k) || !Array.isArray(data)) { + return; + } + + const nodes: Array = Array.isArray(node) ? node : [node]; + // 当没有指定父节点时,默认将节点添加到根节点 + if (!pNode) { + const resuleIndex: any = index === -1 ? data.length : index; + for (let i: number = 0; i < nodes.length; i++) { + data.splice(resuleIndex + i, 0, nodes[i]); + } + + return; + } + + // 如果父节点是叶子节点时,给叶子节点添加children数组 + if (TiTreeUtil.isLeaf(pNode)) { + pNode.children = []; + } + + // 根据Index值,将node插入指定位置 + if (index === -1) { + pNode.children = pNode.children.concat(nodes); // 返回值为新数组 + } else { + for (let j: number = 0; j < nodes.length; j++) { + pNode.children.splice(index + j, 0, nodes[j]); + } + } + } + + /** + * 删除指定节点 + * @param data 所有节点数据 + * @param node 节点 + */ + public static removeNode(data: Array, node: TiTreeNode): void { + if (!Array.isArray(data)) { + return; + } + + // 找到不匹配搜索内容的叶子节点并删除。为了避免要删除的元素在数组中的索引改变,从后向前循环 + for (let i: number = data.length - 1; i >= 0; i--) { + if (data[i] === node) { + TiTreeUtil.deleteArr(data, i); + + return; + } + + if (!TiTreeUtil.isLeaf(data[i])) { + TiTreeUtil.removeNode(data[i].children, node); + } + } + } + // 3.0.3 selectNode/unSelectNode第三个参数:是否单选, 改为是否多选。已确认无人使用。 + // TODO:这两个函数的重复代码,可以合并 + /** + * 选中指定节点 + * @param data 所有节点数据 + * @param node 节点 + * @param multiple 是否是多选树 + */ + public static selectNode(data: Array, node: TiTreeNode, multiple: boolean): void { + if (!Array.isArray(data)) { + return; + } + if (!multiple) { + // 单选模式 + TiTreeUtil.traverse(data, (travNode: TiTreeNode): void => { + travNode.checked = false; // 清空所有的选中项 + }); + node.checked = true; // 将当前项置为选中 + } else { + // 多选 + TiTreeUtil.selectAllChildren(node); // 先置当前节点选中,如果当前节点是非叶子节点 递归让其子节点全部选中 + } + + TiTreeUtil.selectParents(node, data, true); // 设置父节点选中情况 + } + // TODO:Tiny4 deselect改为unselect + /** + * 取消选中指定节点 + * @param data 所有节点数据 + * @param node 节点 + * @param multiple 是否是多选树 + */ + public static deSelectNode(data: Array, node: TiTreeNode, multiple: boolean): void { + if (!Array.isArray(data)) { + return; + } + if (!multiple) { + TiTreeUtil.traverse(data, (travNode: TiTreeNode): void => { + travNode.checked = false; // 清空所有的选中项 + }); + } else { + TiTreeUtil.deSelectAllChildren(node); + TiTreeUtil.deSelectParents(node, data); + } + } + + /** + * 展开指定节点的祖先节点 + * @param data 所有节点数据 + * @param node 节点 + */ + public static expandNode(data: Array, node: TiTreeNode): boolean { + if (!Array.isArray(data)) { + return; + } + + let num: number = 0; + let result: boolean = false; + for (const item of data) { + if (item === node) { + num++; + } else if (!TiTreeUtil.isLeaf(item)) { + result = TiTreeUtil.expandNode(item.children, node); + if (result) { + item.expanded = true; + num++; + } + } + } + + return num > 0; + } + + /** + * 筛选匹配的节点 + * @param data 所有节点数据 + * @param matchFn 用户传入匹配节点的回调 + * @returns 是否找到 + */ + public static search(data: Array, matchFn: (node: TiTreeNode) => boolean): boolean { + if (!Util.isFunction(matchFn) || !Array.isArray(data)) { + return false; + } + + let findNum: number = 0; + let isFind: boolean = false; + // 找到不匹配搜索内容的叶子节点并删除。为了避免要删除的元素在数组中的索引改变,从后向前循环 + for (let i: number = data.length - 1; i >= 0; i--) { + if (!matchFn(data[i]) && !TiTreeUtil.isLeaf(data[i])) { + // 未匹配到且为父节点 + isFind = TiTreeUtil.search(data[i].children, matchFn); + if (isFind) { + findNum++; + } else { + TiTreeUtil.deleteArr(data, i); + } + } else if (matchFn(data[i])) { + // 匹配到 + findNum++; + } else { + // 未匹配到且为叶子节点 + TiTreeUtil.deleteArr(data, i); + } + } + + return findNum > 0; + } + + /** + * 获取当前选中节点 + * @param data 所有节点数据 + * @param onlySelectLeaf 选中项中是否只包含叶子节点 + * @param multiple 是否是多选树 + * @param checkRelation 父子节点选中状态是否关联 + */ + public static getSelectedData( + data: Array, + onlySelectLeaf: boolean, + multiple: boolean, + checkRelation?: boolean + ): Array { + // 每次遍历之前需要清空当前选中项列表 + TiTreeUtil.selectedData = []; + TiTreeUtil.querySelectedNode(data, onlySelectLeaf, multiple, checkRelation); + + return TiTreeUtil.selectedData; + } + + /** + * @ignore 获取当前选中节点 + * @description 作为treeselect 组件单独使用内部方法,规避回显时和选中不一致问题。 + * @param data 所有节点数据 + * @param onlySelectLeaf 选中项中是否只包含叶子节点 + * @param multiple 是否是多选树 + * @param checkRelation 父子节点选中状态是否关联 + */ + public static getTreeSelectedData( + data: Array, + onlySelectLeaf: boolean, + multiple: boolean, + checkRelation?: boolean + ): Array { + // 每次遍历之前需要清空当前选中项列表 + TiTreeUtil.selectedData = []; + TiTreeUtil.querySelectedNode(data, onlySelectLeaf, multiple, checkRelation, true); + + return TiTreeUtil.selectedData; + } + + /** + * @ignore + * @description 根据整棵树的节点数据,查询所有选中项,并更新selectedData + * @param data 全部节点数据 + * @param onlySelectLeaf 选中项中是否只包含叶子节点 + * @param multiple 是否多选模式 + * @param checkRelation 父子节点选中状态是否关联 + * @param isTreeselectEcho 是否是下拉树回显 多选情况下 + */ + public static querySelectedNode( + data: Array, + onlySelectLeaf: boolean, + multiple: boolean, + checkRelation?: boolean, + isTreeselectEcho?: boolean + ): void { + let tempNode: any; + const relation: boolean = checkRelation !== false; + for (let i: number = 0; i < data.length; i++) { + tempNode = data[i]; + + //多选下拉 父节点为选中状态时 + if (multiple && !TiTreeUtil.isLeaf(tempNode) && isTreeselectEcho) { + if (tempNode.checked === true) { + TiTreeUtil.selectedData.push(tempNode); + continue; + } + } + + // 多选树,父节点且父子选中关系不关联 + if (multiple && !relation && !TiTreeUtil.isLeaf(tempNode)) { + if (tempNode.checked === true) { + TiTreeUtil.selectedData.push(tempNode); + } + + TiTreeUtil.querySelectedNode(tempNode.children, onlySelectLeaf, multiple, relation, isTreeselectEcho); + } else if (TiTreeUtil.checkedParentNode(tempNode)) { + TiTreeUtil.pnode.push(tempNode); + TiTreeUtil.querySelectedNode(tempNode.children, onlySelectLeaf, multiple, relation, isTreeselectEcho); + + // 单选情况下仅查找到第一个选中的叶子节点即可 + if (!multiple) { + break; + } + } + + // 叶子节点选中 + if (TiTreeUtil.checkedLeafNode(tempNode)) { + TiTreeUtil.selectedData.push(tempNode); + + if (!onlySelectLeaf && (!multiple || (multiple && relation))) { + tempNode.parent = TiTreeUtil.pnode.concat(); + // 单选情况下仅查找到第一个选中的叶子节点即可 + if (!multiple) { + TiTreeUtil.pnode.splice(0, TiTreeUtil.pnode.length); + break; + } + } + } + + // 遍历到最后一项时将父节点从pnode中移除 + if (i === data.length - 1) { + TiTreeUtil.pnode.pop(); + } + } + } + + /** + * @ignore + * @description 判断节点node是否是一个处于选中或半选状态的父节点 + * @param node 节点数据 + */ + public static checkedParentNode(node: TiTreeNode): boolean { + return (node.checked === true || node.checked === 'indeterminate') && !TiTreeUtil.isLeaf(node); + } + + /** + * @ignore + * @description 判断节点node是否是一个处于选中状态的叶子节点 + * @param node 节点数据 + */ + public static checkedLeafNode(node: TiTreeNode): boolean { + return node.checked === true && TiTreeUtil.isLeaf(node); + } + + /** + * @ignore + * 判断是否为叶子节点 + */ + public static isLeaf(item: TiTreeNode): boolean { + return !Util.isArray(item.children); + } + + /** + * @ignore + * 从数组arr中删除下标为index的节点 + */ + public static deleteArr(arr: Array, index: number): void { + arr.splice(index, 1); + } + // TODO: checked传入false时,与deSelectParents有什么区别?可以合并么? + /** + * @ignore + * 当子节点选中时,设置祖先元素的选中状态 + * @param item 子节点的数据 + * @param allData 全部节点数据 + * @param checked 取值为:true/false/'indeterminate' + */ + public static selectParents(item: TiTreeNode, allData: Array, checked: boolean | string): void { + const pNode: TiTreeNode = TiTreeUtil.getParentNode(allData, item); + if (Util.isUndefined(pNode)) { + return; + } + + // 当子元素为半选时,祖先元素一律设置为半选状态 + if (checked === 'indeterminate') { + pNode.checked = 'indeterminate'; + TiTreeUtil.selectParents(pNode, allData, 'indeterminate'); + + return; + } + + const childrens: Array = pNode.children; + let selectedNum: number = 0; + for (const child of childrens) { + if (child.checked === true) { + selectedNum++; + } + } + + if (selectedNum === childrens.length) { + pNode.checked = true; + TiTreeUtil.selectParents(pNode, allData, true); + } else { + pNode.checked = 'indeterminate'; + TiTreeUtil.selectParents(pNode, allData, 'indeterminate'); + } + } + + /** + * @ignore + * 根据父节点选择子节点 + */ + public static selectAllChildren(item: TiTreeNode): void { + // 如果子节点是禁用状态不做处理; + if (item.disabled !== true) { + item.checked = true; + } + + if (!TiTreeUtil.isLeaf(item)) { + item.children.forEach((child: TiTreeNode) => { + TiTreeUtil.selectAllChildren(child); + }); + } + } + + // TODO:Tiny4 deselect改为unselect + /** + * @ignore + * 父节点取消选中,置子节点都为取消选中状态 + * @param item 子节点的数据 + */ + public static deSelectAllChildren(item: TiTreeNode): void { + item.checked = false; + + if (Util.isArray(item.children) && item.children.length > 0) { + item.children.forEach((child: TiTreeNode) => { + TiTreeUtil.deSelectAllChildren(child); + }); + } + } + + // TODO:Tiny4 deselect改为unselect + /** + * @ignore + * 当前节点取消选中时,设置祖先元素的选中状态 + * @param item 当前节点的数据 + * @param allData 全部的节点数据 + */ + public static deSelectParents(item: TiTreeNode, allData: Array): void { + const pNode: TiTreeNode = TiTreeUtil.getParentNode(allData, item); + if (Util.isUndefined(pNode)) { + return; + } + + const childrens: Array = pNode.children; + let selectedNum: number = 0; + for (const child of childrens) { + if (child.checked === true || child.checked === 'indeterminate') { + selectedNum++; + } + } + + if (selectedNum === 0) { + pNode.checked = false; + TiTreeUtil.deSelectParents(pNode, allData); + } else { + pNode.checked = 'indeterminate'; + TiTreeUtil.selectParents(pNode, allData, 'indeterminate'); + } + } + /** + * @ignore + * @param event 拖拽数据 + * @param data 所用节点数据 + */ + public static dropApply(event: TiTreeDragNode, data: Array): void { + if (!Array.isArray(data)) { + return; + } + const dragNode: TiTreeNode = event.dragNode; // 拖拽节点 + const dragParentNode: TiTreeNode = TiTreeUtil.getParentNode(data, dragNode); // 拖拽节点的父节点 + const dropNode: TiTreeNode = event.targetNode; // 拖放节点 + const dropParentNode: TiTreeNode = TiTreeUtil.getParentNode(data, dropNode); // 拖放节点的父节点 + const dropPosition: number = event.dropPosition; // 拖拽位置 + TiTreeUtil.removeNode(data, dragNode); + // 拖拽父节点无子节点时,删除children属性,此处是为解决当children为空时,展开收起图标还会存在的问题 + if (dragParentNode?.children.length < 1) { + delete dragParentNode.children; + } + if (dropPosition === 0) { + TiTreeUtil.addNode(data, [dragNode], -1, dropNode); + } else { + let index: number; + if (!dropParentNode) { + // 拖放节点为顶级级节点时 + index = data.indexOf(dropNode); + if (dropPosition === -1) { + data.splice(index, 0, dragNode); + } else { + data.splice(index + 1, 0, dragNode); + } + } else { + index = dropParentNode.children.indexOf(dropNode); + if (dropPosition === -1) { + TiTreeUtil.addNode(data, [dragNode], index, dropParentNode); + } else { + TiTreeUtil.addNode(data, [dragNode], index + 1, dropParentNode); + } + } + } + // 更新选中状态 + TiTreeUtil.updateChecked(data); + } + + /** + * 主干深拷贝,叶子浅拷贝 + * 因为搜索结果里,叶子节点的勾选效果,需要保留在原始数据。 + * @param data 节点数据 + * @returns 复制后的数据 + */ + /** + * 拷贝节点数据,对非叶子节点深拷贝,叶子节点浅拷贝 + * @param data 节点数据 + * @returns 拷贝后的数据 + */ + public static copy(data: any): Array { + // 叶子节点 或 children 为 [] 也需要进行浅拷贝 + if ( + typeof data !== 'object' || + data === null || + (data.label !== undefined && (TiTreeUtil.isLeaf(data) || (data.children && data.children.length === 0))) + ) { + return data; + } + + const clone: any = Array.isArray(data) ? data.slice() : { ...data }; + const keys: Array = Object.keys(clone); + for (const key of keys) { + clone[key] = TiTreeUtil.copy(clone[key]); + } + + return clone; + } +} diff --git a/src/tree/lib/src/i18n/TiTreeWords.ts b/src/tree/lib/src/i18n/TiTreeWords.ts new file mode 100644 index 0000000..0788815 --- /dev/null +++ b/src/tree/lib/src/i18n/TiTreeWords.ts @@ -0,0 +1,9 @@ +export interface TiTreeWords { + tiTree: { + newNode: string; + create: string; + edit: string; + delete: string; + more: string; + }; +} diff --git a/src/tree/lib/src/i18n/en_US.ts b/src/tree/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..e7b9d5f --- /dev/null +++ b/src/tree/lib/src/i18n/en_US.ts @@ -0,0 +1,11 @@ +import { TiTreeWords } from './TiTreeWords'; + +export const en_US: TiTreeWords = { + tiTree: { + newNode: 'New node ', + create: 'Create', + edit: 'Edit', + delete: 'Delete', + more: 'More' + } +}; diff --git a/src/tree/lib/src/i18n/es_US.ts b/src/tree/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..2f487e7 --- /dev/null +++ b/src/tree/lib/src/i18n/es_US.ts @@ -0,0 +1,11 @@ +import { TiTreeWords } from './TiTreeWords'; + +export const es_US: TiTreeWords = { + tiTree: { + newNode: 'Nodo nuevo ', + create: 'Crear', + edit: 'Editar', + delete: 'Eliminar', + more: 'Más' + } +}; diff --git a/src/tree/lib/src/i18n/fr_FR.ts b/src/tree/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..11382bc --- /dev/null +++ b/src/tree/lib/src/i18n/fr_FR.ts @@ -0,0 +1,11 @@ +import { TiTreeWords } from './TiTreeWords'; + +export const fr_FR: TiTreeWords = { + tiTree: { + newNode: 'Nouveau nœud ', + create: 'Créer', + edit: 'Editer', + delete: 'Supprimer', + more: 'Plus' + } +}; diff --git a/src/tree/lib/src/i18n/index.ts b/src/tree/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/tree/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/tree/lib/src/i18n/pt_BR.ts b/src/tree/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..f1ef7d3 --- /dev/null +++ b/src/tree/lib/src/i18n/pt_BR.ts @@ -0,0 +1,11 @@ +import { TiTreeWords } from './TiTreeWords'; + +export const pt_BR: TiTreeWords = { + tiTree: { + newNode: 'Novo nó ', + create: 'Criar', + edit: 'Editar', + delete: 'Excluir', + more: 'Mais' + } +}; diff --git a/src/tree/lib/src/i18n/zh_CN.ts b/src/tree/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..aa763dd --- /dev/null +++ b/src/tree/lib/src/i18n/zh_CN.ts @@ -0,0 +1,11 @@ +import { TiTreeWords } from './TiTreeWords'; + +export const zh_CN: TiTreeWords = { + tiTree: { + newNode: '新节点', + create: '新增', + edit: '编辑', + delete: '删除', + more: '更多' + } +}; diff --git a/src/tree/lib/src/tree.html b/src/tree/lib/src/tree.html new file mode 100644 index 0000000..07c77f2 --- /dev/null +++ b/src/tree/lib/src/tree.html @@ -0,0 +1,293 @@ + + +
      + +
    • + +
    • +
      +
    +
    +
    + +
      +
    • + +
    • +
    +
    + + +
    + + + + + +
    + + + + + + + + + + + + + + + +
    +
    +
      +
    • + +
    • + + + + + +
    • +
      + + +
      +
    • +
      +
    +
    + +
  • + +
    + + +
    {{treeLan.more}}
    +
    + + + +
    +
  • +
    + + +
    + + + + + + +
    + + + + + + + + + + + + + + + +
    +
    +
    diff --git a/src/tree/lib/src/tree.less b/src/tree/lib/src/tree.less new file mode 100644 index 0000000..298d0b2 --- /dev/null +++ b/src/tree/lib/src/tree.less @@ -0,0 +1,360 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-tree-node-icon-size: var(--ti-common-size-4x); + --ti-tree-item-height: 30px; // 节点的高度 + --ti-tree-square-icon-size: var(--ti-common-size-4x); // 展开收起图标大小 + --ti-tree-icon-right-space: var(--ti-common-space-2x); // 展开收起图标、复选框右侧间距 + --ti-tree-node-icon-space: calc( + var(--ti-tree-square-icon-size) + var(--ti-tree-node-icon-size) + var(--ti-common-space-base) * 2 + var(--ti-tree-icon-right-space) * 2 + ); // 展开收起图标大小 + 复选图标大小 + 展开收起图标右侧间距 + 文本内容距离右侧间距 + 复选图标右侧间距 + --ti-tree-node-text-nomultiple-space: calc( + var(--ti-tree-square-icon-size) + var(--ti-tree-icon-right-space) * 2 + ); // 用于计算文本区域宽度:没有多选的场景 + --ti-tree-node-text-multiple-leaf-space: calc( + var(--ti-tree-node-icon-size) + var(--ti-common-space-base) + var(--ti-tree-icon-right-space) * 2 + ); // 用于计算文本区域宽度:多选且为叶子节点的场景,复选图标大小 + 复选图标左侧间距 + 复选图标右侧距离文本内容间距 * 文本内容距离右侧间距 + --ti-tree-node-text-padding-space: calc( + var(--ti-common-space-2x) + var(--ti-common-space-base) + ); // 用于计算文本区域宽度: 没有多选且为叶子节点场景,文本区域的左右padding值 + --ti-tree-item-guide-line-first-child-space: var(--ti-common-space-2x); + --ti-tree-item-guide-line-width: var(--ti-common-size-2x); // 导航线宽度 + --ti-tree-small-checkbox-size: var(--ti-common-size-3x); + --ti-tree-small-icon-font-size: var(--ti-common-size-3x); + --ti-tree-small-icon-space: calc(var(--ti-common-space-base) / 2); + --ti-tree-drag-border-color-default: var( + --ti-common-color-line-active + ); // 拖动放置时显示的边框颜色,单独提出避免与checkbox的边框颜色互相影响 + --ti-tree-square-bg-color-active: var( + --ti-common-color-bg-white-normal + ); // 展开收起图标背景激活色,单独提出避免与checkbox的背景色互相影响 +} +:host.ti3-tree-virtual-scroll { + display: block; + cdk-virtual-scroll-viewport { + height: 100%; + } +} +:host ::ng-deep .cdk-virtual-scroll-content-wrapper { + width: 100%; +} + +.ti3-tree-node-text { + max-width: 100%; + display: inline-block; + .ellipsis(); +} + +.ti3-tree-parent-node { + font-size: var(--ti-common-font-size-base); + position: relative; +} + +.ti3-tree-node-icon { + text-align: center; + font-size: var(--ti-tree-node-icon-size); + margin-right: var(--ti-tree-icon-right-space); +} + +.ti3-tree-leaf-node { + margin-left: var(--ti-common-space-6x); + position: relative; +} + +.ti3-tree-node-text-wrapper { + display: flex; + align-items: center; + width: ~'calc(100% - var(--ti-tree-node-icon-space))'; + white-space: nowrap; + padding: var(--ti-common-space-0) var(--ti-common-space-2x) var(--ti-common-space-0) var(--ti-common-space-base); + &:hover { + background: var(--ti-common-color-bg-white-emphasize); + &.ti3-tree-text-disabled-wrapper { + background: none; + } + } + &:focus { + outline: none; + } + &.ti3-tree-text-focus { + background: var(--ti-common-color-bg-white-emphasize); + outline: none; + } + .ti3-tree-operate { + font-size: var(--ti-common-font-size-2); + color: var(--ti-common-color-icon-normal); + margin-left: var(--ti-common-space-2x); + &:hover, + &:active { + color: var(--ti-common-color-icon-hover); + cursor: pointer; + } + &:first-of-type { + margin-left: var(--ti-common-space-3x); + } + } + .ti3-tree-edit-input { + height: var(--ti-tree-item-height); + } +} +.ti3-tree-text-nomultiple-wrapper { + width: ~'calc(100% - var(--ti-tree-node-text-nomultiple-space))'; +} + +.ti3-tree-text-multiple-leaf-wrapper { + width: ~'calc(100% - var(--ti-tree-node-text-multiple-leaf-space))'; +} + +.ti3-tree-text-leaf-wrapper { + width: ~'calc(100% - var(--ti-tree-node-text-padding-space))'; +} + +.ti3-tree-content-box { + display: flex; + align-items: center; + cursor: pointer; + height: var(--ti-tree-item-height); + line-height: var(--ti-tree-item-height); + padding-left: var(--ti-common-space-2x); +} + +.ti3-tree-content-box-disabled { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; +} + +.ti3-tree-item-active { + background: var(--ti-common-color-bg-light-normal); + &:hover, + &:focus { + background: var(--ti-common-color-bg-light-normal); + } +} + +.ti3-tree-item-leaf { + margin-left: calc(var(--ti-tree-square-icon-size) + var(--ti-common-space-base)); +} + +.ti3-tree-checkbox-wrapper { + display: inline-block; + font-size: 0; // 解决图标溢出问题 +} + +:host { + .ti3-tree-content-box .ti3-tree-checkbox-wrapper { + ::ng-deep input[type='checkbox'][tiCheckbox] { + & + .ti3-checkbox { + .ti3-checkbox-skin { + margin: 0 var(--ti-common-space-base) 0 var(--ti-common-space-base); // 规范调整,复选框距离左右侧间距4px + } + } + } + } +} + +.ti3-tree-item-loading { + padding: 0 2px 0 2px; + margin-right: var(--ti-common-space-base); +} + +.ti3-tree-container { + .ti3-icon-minus-square { + color: var(--ti-common-color-icon-active); + } + + .ti3-icon-plus-square { + color: var(--ti-common-color-icon-normal); + } + + .ti3-icon-minus-square, + .ti3-icon-plus-square { + margin-right: var(--ti-common-space-base); + font-size: var(--ti-tree-square-icon-size); + // i标签元素如果不设置具体宽度,在浏览器缩放小于100%的时候,宽度会变大,导致后面的文本区域换行。 + display: inline-block; + width: var(--ti-tree-square-icon-size); + // 由于line-height继承性,图标按下时,整个图标节点背景色会变化 + line-height: var(--ti-tree-square-icon-size); + + &:hover { + cursor: pointer; + color: var(--ti-common-color-icon-active); + } + + &:active { + color: var(--ti-common-color-icon-active); + background: var(--ti-tree-square-bg-color-active); + } + } +} + +.ti3-tree-content-drag-box { + height: var(--ti-tree-item-height); + border-bottom: 2px solid transparent; + border-top: 2px solid transparent; + box-sizing: border-box; +} + +.ti3-tree-drag-over-inner { + border: var(--ti-common-border-weight-1) var(--ti-common-border-style-solid) var(--ti-tree-drag-border-color-default); + padding: 0 var(--ti-common-space-6) 0 2px; +} + +.ti3-tree-drag-over-top { + border-top: var(--ti-common-border-weight-1) var(--ti-common-border-style-solid) var(--ti-tree-drag-border-color-default); +} + +.ti3-tree-drag-over-bottom { + border-bottom: var(--ti-common-border-weight-1) var(--ti-common-border-style-solid) var(--ti-tree-drag-border-color-default); +} +/* -----------------------------------------------左侧导航线样式--------------------------------------------*/ +.ti3-tree-leaf-node { + // 导航线基础样式 + & > .ti3-tree-content-box > .ti3-tree-item-guide-line { + position: absolute; + width: var(--ti-tree-item-guide-line-width); + height: var(--ti-tree-item-height); + .box-sizing(border-box); + border-left: 1px solid var(--ti-common-color-line-dividing); + border-bottom: 1px solid var(--ti-common-color-line-dividing); + top: calc(-1 * var(--ti-tree-item-height) / 2); + left: calc(-1 * var(--ti-tree-square-icon-size) / 2); + } + + // 第一个子节点的导航线样式:正常场景和虚拟滚动场景 + &:not(.ti3-tree-virtual-scroll-node):first-child, + &.ti3-tree-virtual-scroll-first-child-node { + & > .ti3-tree-content-box > .ti3-tree-item-guide-line { + height: calc( + var(--ti-tree-item-height) - var(--ti-tree-square-icon-size) / 2 - var(--ti-tree-item-guide-line-first-child-space) + ); // 第一个节点的导航线与图标的间距为8px + top: calc(var(--ti-tree-square-icon-size) / 2 + var(--ti-tree-item-guide-line-first-child-space) - var(--ti-tree-item-height) / 2); + } + } + + &:not(.ti3-tree-virtual-scroll-node):not(:last-child) > .ti3-tree-parent-node:after { + content: ''; + position: absolute; + height: 100%; + .box-sizing(border-box); + width: var(--ti-tree-item-guide-line-width); + border-left: 1px solid var(--ti-common-color-line-dividing); + left: calc(-1 * var(--ti-tree-square-icon-size) / 2); + top: calc(-1 * var(--ti-tree-item-height) / 2); + } + + &.ti3-tree-virtual-scroll-node { + > .ti3-tree-content-box > .ti3-tree-item-guide-line { + border-left: 0px; + } + > .ti3-tree-content-box > .ti3-tree-item-vertical-guide-line { + position: absolute; + height: 100%; + top: 0px; + width: 1px; + background-color: var(--ti-common-color-line-dividing); + &.ti3-tree-last-item-vertical-guide-line { + height: 50%; + } + } + } +} + +/* -----------------------------------------------紧凑型(运维场景)--------------------------------------------*/ +:host[small] { + .ti3-icon-minus-square, + .ti3-icon-plus-square { + margin-right: var(--ti-tree-small-icon-space); + font-size: var(--ti-tree-small-icon-font-size); + width: var(--ti-tree-small-icon-font-size); + line-height: var(--ti-tree-small-icon-font-size); + } + + .ti3-tree-node-icon { + font-size: var(--ti-tree-small-icon-font-size); + margin-right: var(--ti-common-space-base); + } + + .ti3-tree-content-box .ti3-tree-checkbox-wrapper { + ::ng-deep input[type='checkbox'][tiCheckbox] { + & + .ti3-checkbox { + .ti3-checkbox-skin { + margin: 0 var(--ti-tree-small-icon-space) 0 var(--ti-tree-small-icon-space); + width: var(--ti-tree-small-checkbox-size); + height: var(--ti-tree-small-checkbox-size); + line-height: var(--ti-tree-small-checkbox-size); + } + } + + &:indeterminate + .ti3-checkbox { + .ti3-checkbox-skin:before { + top: calc((var(--ti-tree-small-checkbox-size) - 8px) / 2); + left: calc((var(--ti-tree-small-checkbox-size) - 8px) / 2); + } + } + + &:checked + .ti3-checkbox { + .ti3-checkbox-skin:before { + width: calc(var(--ti-tree-small-checkbox-size) / 12 * 5); + height: calc(var(--ti-tree-small-checkbox-size) / 12 * 7); + } + } + } + } + + .ti3-tree-item-leaf { + margin-left: calc(var(--ti-tree-small-icon-font-size) + var(--ti-tree-small-icon-space)); + } + + .ti3-tree-content-box { + padding-left: var(--ti-common-space-base); + } + + .ti3-tree-leaf-node { + margin-left: calc(var(--ti-common-space-base) + var(--ti-tree-small-icon-font-size)); + + .ti3-tree-content-box { + padding-left: var(--ti-common-space-base); + } + + & > .ti3-tree-content-box > .ti3-tree-item-guide-line { + width: calc(var(--ti-tree-small-icon-font-size) / 2); + left: calc(-1 * var(--ti-tree-small-icon-font-size) / 2); + } + + &:not(.ti3-tree-virtual-scroll-node):not(:last-child) > .ti3-tree-parent-node:after { + left: calc(-1 * var(--ti-tree-small-icon-font-size) / 2); + } + } + + .ti3-tree-node-text-wrapper { + padding-left: var(--ti-tree-small-icon-space); + } +} + +// 搜索高亮色 +::ng-deep .ti3-font-highlight { + color: var(--ti-common-color-text-highlight); +} + +.ti3-tree-more-content { + display: inline-block; +} + +.ti3-tree-more-box { + margin-left: 0px; + padding: 0px; +} + +.ti3-tree-more-loading { + margin: 0 var(--ti-common-space-6) 0 var(--ti-common-space-10); +} + +.ti3-tree-node-more-text { + margin-left: var(--ti-common-space-base); + color: var(--ti-common-color-prompt-text); +} + +.ti3-tree-more-error { + padding-left: var(--ti-common-space-3x); +} diff --git a/src/treeselect/demo/karma.conf.js b/src/treeselect/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/treeselect/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/treeselect/demo/project.json b/src/treeselect/demo/project.json new file mode 100644 index 0000000..8989fb1 --- /dev/null +++ b/src/treeselect/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/treeselect/demo", + "sourceRoot": "src/treeselect/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/treeselect", + "index": "src/treeselect/demo/src/index.html", + "main": "src/treeselect/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/treeselect/demo/tsconfig.app.json", + "assets": ["src/treeselect/demo/src/favicon.ico", "src/treeselect/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "treeselect-demo:build:production" + }, + "development": { + "browserTarget": "treeselect-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js treeselect" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/treeselect/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/treeselect/demo/tsconfig.spec.json", + "karmaConfig": "src/treeselect/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/treeselect/demo/src/app/AppComponent.ts b/src/treeselect/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/treeselect/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/treeselect/demo/src/app/AppModule.ts b/src/treeselect/demo/src/app/AppModule.ts new file mode 100644 index 0000000..1a6fa8c --- /dev/null +++ b/src/treeselect/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { TreeselectTestModule } from './treeselect/TreeselectTestModule'; + +@NgModule({ + imports: [ + TreeselectTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/treeselect/demo/src/app/IndexComponent.ts b/src/treeselect/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..04d3e90 --- /dev/null +++ b/src/treeselect/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TreeselectTestModule } from './treeselect/TreeselectTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = TreeselectTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/treeselect/demo/src/app/app.html b/src/treeselect/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/treeselect/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectBasicComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectBasicComponent.ts new file mode 100644 index 0000000..6df660d --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectBasicComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-basic.html' +}) +export class TreeselectBasicComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 当前选中项 + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectBeforeExpandComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectBeforeExpandComponent.ts new file mode 100644 index 0000000..8fe8bd4 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectBeforeExpandComponent.ts @@ -0,0 +1,98 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-before-expand.html' +}) +export class TreeselectBeforeExpandComponent { + constructor(private changeDetectorRef: ChangeDetectorRef) {} + private treeID: number = 5; + // 单选 + options: Array = [ + { + label: '家用电器', + children: [] + }, + { + label: '服饰', + children: [] + }, + { + label: '视频', + children: [] + }, + { + label: '食品', + children: [] + } + ]; + + value: any = TiTreeUtil.getTreeSelectedData(this.options, true, false); + + // 异步请求数据后插入当前节点并展开 + onBeforeExpand(item: TiTreeNode, multile?: boolean): void { + if (item.children.length > 0) { + // 已经加载过数据就不再重复加载,业务可根据自身场景来决定是否需要进行该处理 + item.expanded = true; + + return; + } + item.loadStatus = 'loading'; // 标志正在加载 + const getDataPromise: any = this.getNodeData(item); + getDataPromise + .then((data: Array) => { + // 1.将请求到的数据挂到该节点上 + item.children = data; + // 2. 多选情况下,如果该节点是选中状态,则需要将所有子节点也选中 + if (multile && item.checked === true) { + item.children.forEach((child: TiTreeNode) => { + child.checked = true; + }); + } + // 3.将该节点展开 + item.expanded = true; + item.loadStatus = 'success'; // 标志加载成功 + }) + .catch(() => { + // 异步获取数据失败 + item.expanded = true; // 打开当前节点 + item.loadStatus = 'error'; // 标志加载失败 + }); + } + + private getNodeData(item: TiTreeNode): any { + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + const data: Array = this.createData(item); + if (data && data.length > 0) { + resolve(data); + } else { + const errMsg: string = '获取数据失败'; + reject(errMsg); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef + // 默认模式下不需要 + // 服务可根据自身接口增加该方法 + this.changeDetectorRef.markForCheck(); + }, 2000); + }); + } + + private createData(item: TiTreeNode): Array { + const data: Array = []; + if (item.label === '服饰' || item.label === '家用电器') { + for (let i: number = 0; i < 3; i++) { + const dataList: TiTreeNode = { + type: 'FILE', + label: `item${this.treeID}` + }; + this.treeID++; + data.push(dataList); + } + + return data; + } + + return data; + } +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectBeforeMoreComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectBeforeMoreComponent.ts new file mode 100644 index 0000000..5285f7e --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectBeforeMoreComponent.ts @@ -0,0 +1,143 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-before-more.html' +}) +export class TreeselectBeforeMoreComponent { + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + options: Array = [ + { + label: '家用电器', + children: [] + }, + { + label: '服饰', + children: [] + }, + { + label: '视频', + children: [] + }, + { + label: '食品', + children: [] + } + ]; + + value: any = TiTreeUtil.getTreeSelectedData(this.options, true, true); + treeID: number = 0; + offset: number; // 当前已展开的节点数量 + nodeNum: number = 3; // 每次点击展开节点的数量 + + // 异步请求数据后插入当前节点并展开 + beforeExpand(node: TiTreeNode, multile?: boolean): void { + if (node.children.length > 0) { + // 已经加载过数据就不再重复加载,业务可根据自身场景来决定是否需要进行该处理 + node.expanded = true; + + return; + } + + node.loadStatus = 'loading'; // 标志正在加载 + this.offset = 0; + const getDataPromise: Promise = this.getNodeData(node, this.offset, this.nodeNum, 'before'); + getDataPromise + .then((data: Array) => { + // 1.将请求到的数据挂到该节点上 + node.children = data; + // 2. 多选情况下,如果该节点是选中状态,则需要将所有子节点也选中 + if (multile && node.checked === true) { + node.children.forEach((child: TiTreeNode) => { + child.checked = true; + }); + } + node.showMore = true; // 当前节点下显示更多按钮 + // 3.将该节点展开 + node.expanded = true; + node.loadStatus = 'success'; // 标志加载成功 + }) + .catch((error: Error) => { + // 异步获取数据失败 + node.expanded = true; // 打开当前节点 + node.loadStatus = 'error'; // 标志加载失败 + }); + } + + // 点击更多异步请求其余节点 + beforeMore(node: TiTreeNode, multile?: boolean): void { + node.moreStatus = 'loading'; + this.offset = node.children?.length; + const getDataPromise: Promise = this.getNodeData(node, this.offset, this.nodeNum, 'more'); + getDataPromise + .then((data: Array) => { + // 多选情况下,如果该节点是选中状态,则需要将新添加的子节点也选中 + if (multile && node.checked === true) { + data.forEach((child: TiTreeNode) => { + child.checked = true; + }); + } + // 将请求的数据合并到该节点上 + node.children = node.children.concat(data); + node.moreStatus = 'success'; // 标志加载成功 + // 此处根据业务实际返回,数据加载完后隐藏更多按钮 + if (node.children.length > 6) { + node.showMore = false; + } + }) + .catch((error: Error) => { + node.moreStatus = 'error'; // 异步获取数据失败 + }); + } + + // 模拟异步请求 + private getNodeData(item: TiTreeNode, offset: number, num: number, style: string): any { + return new Promise((resolve: any, reject: any): any => { + setTimeout(() => { + const data: Array = this.createData(item, offset, num, style); + if (data && data.length > 0) { + resolve(data); + } else { + const errMsg: string = '获取数据失败'; + reject(errMsg); + } + // onPush模式下,setTimeout返回数据,需要使用changeDetectorRef + // 默认模式下不需要 + // 服务可根据自身接口增加该方法 + this.changeDetectorRef.markForCheck(); + }, 2000); + }); + } + + private createData(item: TiTreeNode, offset: number, num: number, style: string): Array { + const data: Array = []; + let result: Array; + this.treeID = 0; + if (style === 'before' && (item.label === '服饰' || item.label === '家用电器' || item.label === '视频' || item.label === '食品')) { + for (let i: number = 0; i < 100; i++) { + const dataList: TiTreeNode = { + label: item.label + this.treeID, + children: [] + }; + this.treeID++; + data.push(dataList); + } + } + + if (style === 'more' && (item.label === '服饰' || item.label === '家用电器')) { + for (let i: number = 0; i < 100; i++) { + const dataList: TiTreeNode = { + label: item.label + this.treeID, + children: [] + }; + this.treeID++; + data.push(dataList); + } + } + + result = data.slice(offset, offset + num); + + return result; + } +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectClearableComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectClearableComponent.ts new file mode 100644 index 0000000..e4fdd7c --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectClearableComponent.ts @@ -0,0 +1,90 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-clearable.html' +}) +export class TreeselectClearableComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 当前选中项 + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); + + myLogs: Array = []; + + onClear(): void { + this.myLogs = [...this.myLogs, `onClear()`]; + } +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectDisabledComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectDisabledComponent.ts new file mode 100644 index 0000000..f017ec5 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectDisabledComponent.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core'; +import { TiTreeNode } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-disabled.html' +}) +export class TreeselectDisabledComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调', + disabled: true + } + ] + } + ] + } + ] + } + ]; + + value: TiTreeNode = []; + myOptions: Array = JSON.parse(JSON.stringify(this.options)); + myValue: TiTreeNode = JSON.parse(JSON.stringify(this.value)); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectDropmaxheightComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectDropmaxheightComponent.ts new file mode 100644 index 0000000..623ea8c --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectDropmaxheightComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-dropmaxheight.html' +}) +export class TreeselectDropmaxheightComponent { + dropMaxHeight: number = 300; + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectEventComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectEventComponent.ts new file mode 100644 index 0000000..40644f4 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectEventComponent.ts @@ -0,0 +1,89 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-event.html' +}) +export class TreeselectEventComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + value: Array = TiTreeUtil.getTreeSelectedData(this.options, true, false); + + myLogs: Array = []; + + onSelect(event: any): void { + this.myLogs = [...this.myLogs, `onSelect() event=${JSON.stringify(event)}`]; + } +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectFocusComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectFocusComponent.ts new file mode 100644 index 0000000..67e3f1f --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectFocusComponent.ts @@ -0,0 +1,82 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-focus.html' +}) +export class TreeselectFocusComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + mySelected: any = TiTreeUtil.getTreeSelectedData(this.options, true, false); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectLabelkeyComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectLabelkeyComponent.ts new file mode 100644 index 0000000..51ca7eb --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectLabelkeyComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-labelkey.html' +}) +export class TreeselectLabelkeyComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 当前选中项 + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectLazyloadComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectLazyloadComponent.ts new file mode 100644 index 0000000..452231c --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectLazyloadComponent.ts @@ -0,0 +1,97 @@ +import { ChangeDetectorRef, Component } from '@angular/core'; +import { TiTreeNode, TiTreeselectComponent, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-lazyload.html' +}) +export class TreeselectLazyloadComponent { + options: Array = []; + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); + + constructor(private changeDetectorRef: ChangeDetectorRef) {} + + onBeforeOpen(treeselectComp: TiTreeselectComponent): void { + setTimeout(() => { + this.options = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 此处设置options的值,无法触发ngOnInit,需要设置下oldOptions + treeselectComp.oldOptions = this.options; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, false); + // OnPush模式下,异步刷新都需要手动触发。 + this.changeDetectorRef.markForCheck(); + treeselectComp.open(); + }, 1000); + } +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectLoadComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectLoadComponent.ts new file mode 100644 index 0000000..85a1ec8 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectLoadComponent.ts @@ -0,0 +1,130 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-load.html' +}) +export class TreeselectLoadComponent { + options: Array; + value: any; + + private dataA: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + private dataB: Array = [ + { + label: '家用电器', + children: [] + }, + { + label: '服饰', + children: [] + }, + { + label: '视频', + children: [] + }, + { + label: '食品', + children: [] + } + ]; + + // 每个组件都用下面六个函数,只改变函数内容 + changeUndefined(): void { + this.options = undefined; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, false); + } + changeNull(): void { + this.options = null; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, false); + } + changeWrongType(): void { + const temp: any = 5; + this.options = temp; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, false); + } + changeNullData(): void { + this.options = []; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, false); + } + changeDataA(): void { + this.options = this.dataA; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, false); + } + changeDataB(): void { + this.options = this.dataB; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, false); + } +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectMaxlineComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectMaxlineComponent.ts new file mode 100644 index 0000000..cf26518 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectMaxlineComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-maxline.html' +}) +export class TreeselectMaxlineComponent { + public options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 当前选中项 + public value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectModalComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectModalComponent.ts new file mode 100644 index 0000000..72664c3 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectModalComponent.ts @@ -0,0 +1,112 @@ +import { Component } from '@angular/core'; +import { TiModalService, TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-modal.html' +}) +export class TreeselectModalComponent { + constructor(private tiModal: TiModalService) {} + + openModal(): void { + this.tiModal.open(ModalTestComponent, {}); + } +} +@Component({ + template: ` + 在弹窗中使用树下拉选择组件 + + + + + + + + ` +}) +export class ModalTestComponent { + options: Array = []; + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, true); + data: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + ngOnInit(): void { + this.options = this.data; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, true); + } + + // 模板中实际调用的是Modal服务提供的close和dismiss方法,并非此处定义的方法; + // 在此处定义close和dismiss方法只是为了避免生产环境打包时报错 + close(): void {} + dismiss(): void {} +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectMultiComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectMultiComponent.ts new file mode 100644 index 0000000..19cae0a --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectMultiComponent.ts @@ -0,0 +1,83 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-multi.html' +}) +export class TreeselectMultiComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + // 当前选中项 + value: Array = TiTreeUtil.getTreeSelectedData(this.options, true, true); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectNodataComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectNodataComponent.ts new file mode 100644 index 0000000..afc65c4 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectNodataComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-nodata.html' +}) +export class TreeselectNodataComponent { + options: Array = []; + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); + noDataText: string = '空数据'; +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectOptionsChangeComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectOptionsChangeComponent.ts new file mode 100644 index 0000000..d6243ec --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectOptionsChangeComponent.ts @@ -0,0 +1,69 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-options-change.html' +}) +export class TreeselectOptionsChangeComponent implements OnInit { + options: Array = []; + value: TiTreeNode = []; + myOptions: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱' + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + } + ]; + + constructor(private cdRef: ChangeDetectorRef) {} + + ngOnInit(): void { + // 模拟请求后台数据 + setTimeout((): void => { + this.options = this.myOptions; + this.value = TiTreeUtil.getTreeSelectedData(this.options, true, true); + // OnPush模式下,异步刷新都需要手动触发。 + this.cdRef.markForCheck(); + }, 2000); + } +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectPanelwidthComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectPanelwidthComponent.ts new file mode 100644 index 0000000..8bcfe21 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectPanelwidthComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-panelwidth.html' +}) +export class TreeselectPanelwidthComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调 (1.5匹 静悦 新一级 变频省电 冷暖 卧室挂式空调挂机 大风口)' + }, + { + label: '美的空调 (新一级 极光先锋 大1.5匹 智能家电 变频冷暖 壁挂式空调挂机 一键智控温 K 1.5)' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 当前选中项 + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectSearchComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectSearchComponent.ts new file mode 100644 index 0000000..1ad7a5c --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectSearchComponent.ts @@ -0,0 +1,82 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-search.html' +}) +export class TreeselectSearchComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + value: Array = TiTreeUtil.getTreeSelectedData(this.options, true, true); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectSelectallComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectSelectallComponent.ts new file mode 100644 index 0000000..6c98d22 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectSelectallComponent.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-selectall.html' +}) +export class TreeselectSelectallComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + // 当前选中项 + value: Array = TiTreeUtil.getTreeSelectedData(this.options, true, true); +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectTestModule.ts b/src/treeselect/demo/src/app/treeselect/TreeselectTestModule.ts new file mode 100644 index 0000000..de128fc --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectTestModule.ts @@ -0,0 +1,153 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiModalModule, TiSelectModule, TiTreeselectModule, TiValidationModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { TreeselectBasicComponent } from './TreeselectBasicComponent'; +import { TreeselectMultiComponent } from './TreeselectMultiComponent'; +import { TreeselectEventComponent } from './TreeselectEventComponent'; +import { TreeselectSearchComponent } from './TreeselectSearchComponent'; +import { TreeselectDisabledComponent } from './TreeselectDisabledComponent'; +import { TreeselectSelectallComponent } from './TreeselectSelectallComponent'; +import { TreeselectNodataComponent } from './TreeselectNodataComponent'; +import { TreeselectValidationComponent } from './TreeselectValidationComponent'; +import { TreeselectLazyloadComponent } from './TreeselectLazyloadComponent'; +import { TreeselectLoadComponent } from './TreeselectLoadComponent'; +import { TreeselectFocusComponent } from './TreeselectFocusComponent'; +import { TreeselectDropmaxheightComponent } from './TreeselectDropmaxheightComponent'; +import { TreeselectOptionsChangeComponent } from './TreeselectOptionsChangeComponent'; +import { ModalTestComponent, TreeselectModalComponent } from './TreeselectModalComponent'; +import { TreeselectBeforeExpandComponent } from './TreeselectBeforeExpandComponent'; +import { TreeselectBeforeMoreComponent } from './TreeselectBeforeMoreComponent'; +import { TreeselectClearableComponent } from './TreeselectClearableComponent'; +import { TreeselectMaxlineComponent } from './TreeselectMaxlineComponent'; +import { TreeselectPanelwidthComponent } from './TreeselectPanelwidthComponent'; +import { TreeselectLabelkeyComponent } from './TreeselectLabelkeyComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiTreeselectModule, + TiValidationModule, + TiModalModule, + TiButtonModule, + TiSelectModule, + DemoLogModule, + RouterModule.forChild(TreeselectTestModule.ROUTES) + ], + declarations: [ + TreeselectBasicComponent, + TreeselectMultiComponent, + TreeselectEventComponent, + TreeselectSearchComponent, + TreeselectDisabledComponent, + TreeselectSelectallComponent, + TreeselectNodataComponent, + TreeselectValidationComponent, + TreeselectLazyloadComponent, + TreeselectLoadComponent, + TreeselectFocusComponent, + TreeselectDropmaxheightComponent, + TreeselectOptionsChangeComponent, + TreeselectModalComponent, + TreeselectBeforeExpandComponent, + TreeselectBeforeMoreComponent, + TreeselectClearableComponent, + TreeselectMaxlineComponent, + TreeselectPanelwidthComponent, + TreeselectLabelkeyComponent, + ModalTestComponent + ], + entryComponents: [ModalTestComponent] +}) +export class TreeselectTestModule { + static readonly LINKS: Array = [{ href: 'components/TiTreeselectComponent.html', label: 'Treeselect' }]; + static readonly ROUTES: Routes = [ + { + path: 'treeselect/treeselect-basic', + component: TreeselectBasicComponent + }, + { + path: 'treeselect/treeselect-multi', + component: TreeselectMultiComponent + }, + { + path: 'treeselect/treeselect-selectall', + component: TreeselectSelectallComponent + }, + { + path: 'treeselect/treeselect-clearable', + component: TreeselectClearableComponent + }, + { + path: 'treeselect/treeselect-event', + component: TreeselectEventComponent + }, + { + path: 'treeselect/treeselect-search', + component: TreeselectSearchComponent + }, + { + path: 'treeselect/treeselect-labelkey', + component: TreeselectLabelkeyComponent + }, + { + path: 'treeselect/treeselect-disabled', + component: TreeselectDisabledComponent + }, + { + path: 'treeselect/treeselect-nodata', + component: TreeselectNodataComponent + }, + { + path: 'treeselect/treeselect-validation', + component: TreeselectValidationComponent + }, + { + path: 'treeselect/treeselect-lazyload', + component: TreeselectLazyloadComponent + }, + + { + path: 'treeselect/treeselect-dropmaxheight', + component: TreeselectDropmaxheightComponent + }, + + { + path: 'treeselect/treeselect-before-expand', + component: TreeselectBeforeExpandComponent + }, + { + path: 'treeselect/treeselect-before-more', + component: TreeselectBeforeMoreComponent + }, + { + path: 'treeselect/treeselect-modal', + component: TreeselectModalComponent + }, + { + path: 'treeselect/treeselect-options-change', + component: TreeselectOptionsChangeComponent + }, + { + path: 'treeselect/treeselect-load', + component: TreeselectLoadComponent + }, + { + path: 'treeselect/treeselect-focus', + component: TreeselectFocusComponent + }, + { + path: 'treeselect/treeselect-maxline', + component: TreeselectMaxlineComponent + }, + { + path: 'treeselect/treeselect-panelwidth', + component: TreeselectPanelwidthComponent + } + ]; +} diff --git a/src/treeselect/demo/src/app/treeselect/TreeselectValidationComponent.ts b/src/treeselect/demo/src/app/treeselect/TreeselectValidationComponent.ts new file mode 100644 index 0000000..b25a3a0 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/TreeselectValidationComponent.ts @@ -0,0 +1,83 @@ +import { Component } from '@angular/core'; +import { TiTreeNode, TiTreeUtil } from '@opentiny/ng'; + +@Component({ + templateUrl: './treeselect-validation.html' +}) +export class TreeselectValidationComponent { + options: Array = [ + { + label: '家用电器', + expanded: true, + children: [ + { + label: '大家电', + expanded: true, + children: [ + { + label: '空调', + expanded: true, + disabled: false, + children: [ + { + label: '海尔空调' + }, + { + label: '美的空调' + } + ] + }, + { + label: '冰箱', + disabled: false + }, + { + label: '洗衣机' + }, + { + label: '热水器' + } + ] + }, + { + label: '生活电器', + children: [ + { + label: '加湿器' + }, + { + label: '电熨斗' + } + ] + } + ] + }, + { + label: '服饰', + expanded: true, + checked: 'indeterminate', + children: [ + { + label: '男装', + checked: true + }, + { + label: '女装' + } + ] + }, + { + label: '化妆', + children: [ + { + label: '面部护理' + }, + { + label: '口腔护理' + } + ] + } + ]; + + value: TiTreeNode = TiTreeUtil.getTreeSelectedData(this.options, true, false); +} diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-basic.html b/src/treeselect/demo/src/app/treeselect/treeselect-basic.html new file mode 100644 index 0000000..c878167 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-basic.html @@ -0,0 +1,5 @@ + + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-before-expand.html b/src/treeselect/demo/src/app/treeselect/treeselect-before-expand.html new file mode 100644 index 0000000..e4f2a2b --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-before-expand.html @@ -0,0 +1,8 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-before-more.html b/src/treeselect/demo/src/app/treeselect/treeselect-before-more.html new file mode 100644 index 0000000..3450a2b --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-before-more.html @@ -0,0 +1,10 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-clearable.html b/src/treeselect/demo/src/app/treeselect/treeselect-clearable.html new file mode 100644 index 0000000..4780be8 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-clearable.html @@ -0,0 +1,11 @@ + + + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-disabled.html b/src/treeselect/demo/src/app/treeselect/treeselect-disabled.html new file mode 100644 index 0000000..fcfebdc --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-disabled.html @@ -0,0 +1,6 @@ +

    1. 整体禁用

    + + + +

    2. 数据项禁用

    + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-dropmaxheight.html b/src/treeselect/demo/src/app/treeselect/treeselect-dropmaxheight.html new file mode 100644 index 0000000..083ad9a --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-dropmaxheight.html @@ -0,0 +1,8 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-event.html b/src/treeselect/demo/src/app/treeselect/treeselect-event.html new file mode 100644 index 0000000..320d983 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-event.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-focus.html b/src/treeselect/demo/src/app/treeselect/treeselect-focus.html new file mode 100644 index 0000000..b4a8aec --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-focus.html @@ -0,0 +1,33 @@ +

    1、autofocus

    +

    设置[(ngModel)]为空数组[]

    + tabindex为0 + +

    2、手动设置焦点

    +

    [(ngModel)]不为空数组

    + +tabindex为0

    +  +   +  + +

    + +

    3、tabindex

    +

    tabindex="1"

    + +

    +

    tabindex="4"

    + +

    +

    tabindex="2"

    + +

    +

    tabindex="3"

    + +

    diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-labelkey.html b/src/treeselect/demo/src/app/treeselect/treeselect-labelkey.html new file mode 100644 index 0000000..42ba731 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-labelkey.html @@ -0,0 +1 @@ + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-lazyload.html b/src/treeselect/demo/src/app/treeselect/treeselect-lazyload.html new file mode 100644 index 0000000..f4470c8 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-lazyload.html @@ -0,0 +1,8 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-load.html b/src/treeselect/demo/src/app/treeselect/treeselect-load.html new file mode 100644 index 0000000..638430b --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-load.html @@ -0,0 +1,19 @@ +

    1. 默认值声明options和ngModel绑定的变量,不进行赋值

    +

    设置[(ngModel)]为空数组[]

    + + +
    +
    Current Select: {{ value | json }}
    +
    + +

    每个组件改变数据,都用下面六个按钮。不变化

    + +使用TiTreeUtil.getSelectedData获取选中数据时会报错,需要手动将[(ngModel)]置为空数组
    + + +使用TiTreeUtil.getSelectedData获取选中数据时会报错,需要手动将[(ngModel)]置为空数组
    + +
    +
    +
    +
    diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-maxline.html b/src/treeselect/demo/src/app/treeselect/treeselect-maxline.html new file mode 100644 index 0000000..d444f45 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-maxline.html @@ -0,0 +1,2 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-modal.html b/src/treeselect/demo/src/app/treeselect/treeselect-modal.html new file mode 100644 index 0000000..d62de46 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-modal.html @@ -0,0 +1 @@ + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-multi.html b/src/treeselect/demo/src/app/treeselect/treeselect-multi.html new file mode 100644 index 0000000..2bb937a --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-multi.html @@ -0,0 +1,5 @@ + + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-nodata.html b/src/treeselect/demo/src/app/treeselect/treeselect-nodata.html new file mode 100644 index 0000000..7406063 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-nodata.html @@ -0,0 +1,2 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-options-change.html b/src/treeselect/demo/src/app/treeselect/treeselect-options-change.html new file mode 100644 index 0000000..eeecc14 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-options-change.html @@ -0,0 +1,5 @@ + + +
    +
    Current Select: {{ value | json }}
    +
    diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-panelwidth.html b/src/treeselect/demo/src/app/treeselect/treeselect-panelwidth.html new file mode 100644 index 0000000..a2d254a --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-panelwidth.html @@ -0,0 +1,2 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-search.html b/src/treeselect/demo/src/app/treeselect/treeselect-search.html new file mode 100644 index 0000000..7ee1a4e --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-search.html @@ -0,0 +1,10 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-selectall.html b/src/treeselect/demo/src/app/treeselect/treeselect-selectall.html new file mode 100644 index 0000000..c0e66c4 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-selectall.html @@ -0,0 +1,9 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/treeselect-validation.html b/src/treeselect/demo/src/app/treeselect/treeselect-validation.html new file mode 100644 index 0000000..6ce417c --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/treeselect-validation.html @@ -0,0 +1,10 @@ + + diff --git a/src/treeselect/demo/src/app/treeselect/webdoc/treeselect-demos.js b/src/treeselect/demo/src/app/treeselect/webdoc/treeselect-demos.js new file mode 100644 index 0000000..22cc97a --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/webdoc/treeselect-demos.js @@ -0,0 +1,202 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'treeselect-basic', + name: { + 'zh-CN': '基本使用', + 'en-US': 'treeselect basic', + }, + desc: { + 'zh-CN': 'TreeSelect 组件的最简用法。', + 'en-US': '

    treeselect basic

    ', + }, + apis: ['TiTreeselectComponent.properties.options'], + }, + { + demoId: 'treeselect-multi', + name: { + 'zh-CN': '多选', + 'en-US': 'multiple', + }, + desc: { + 'zh-CN': '

    通过属性multiple配置组件是否为多选。', + 'en-US': '

    multiple

    ', + }, + apis: ['TiTreeselectComponent.properties.multiple'], + }, + { + demoId: 'treeselect-selectall', + name: { + 'zh-CN': '全选', + 'en-US': 'selectAll', + }, + desc: { + 'zh-CN': + '

    通过属性selectAll配置组件在多选场景下是否显示全选框。', + 'en-US': '

    selectAll

    ', + }, + apis: ['TiTreeselectComponent.properties.selectAll'], + }, + { + demoId: 'treeselect-labelkey', + name: { + 'zh-CN': '自定义选中项', + 'en-US': 'labelkey', + }, + desc: { + 'zh-CN': '

    通过属性labelKey配置组件下拉显示的字段。', + 'en-US': '

    labelkey

    ', + }, + apis: ['TiTreeselectComponent.properties.labelKey'], + }, + { + demoId: 'treeselect-clearable', + name: { + 'zh-CN': '可清除', + 'en-US': 'clearable', + }, + desc: { + 'zh-CN': + '通过属性clearable配置组件是否开启清除已选项功能。', + 'en-US': '

    clearable

    ', + }, + apis: ['TiTreeselectComponent.properties.clearable'], + }, + { + demoId: 'treeselect-search', + name: { + 'zh-CN': '可搜索', + 'en-US': 'searchable', + }, + desc: { + 'zh-CN': '通过属性searchable配置组件是否显示搜索框。', + 'en-US': '

    searchable

    ', + }, + apis: ['TiTreeselectComponent.properties.searchable'], + }, + { + demoId: 'treeselect-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'disabled', + }, + desc: { + 'zh-CN': '通过属性disabled配置组件是否禁用。', + 'en-US': '

    disabled

    ', + }, + apis: ['TiTreeselectComponent.properties.disabled'], + }, + { + demoId: 'treeselect-dropmaxheight', + name: { + 'zh-CN': '下拉面板最大高度', + 'en-US': 'dropMaxHeight', + }, + desc: { + 'zh-CN': '通过属性dropMaxHeight配置组件下拉面板最大高度。', + 'en-US': '

    dropMaxHeight

    ', + }, + apis: ['TiTreeselectComponent.properties.dropMaxHeight'], + }, + { + demoId: 'treeselect-nodata', + name: { + 'zh-CN': '空数据', + 'en-US': 'nodata', + }, + desc: { + 'zh-CN': '通过属性noDataText配置组件空数据显示文本。', + 'en-US': '

    nodata

    ', + }, + apis: ['TiTreeselectComponent.properties.noDataText'], + }, + { + demoId: 'treeselect-maxline', + name: { + 'zh-CN': '显示行数', + 'en-US': 'maxline', + }, + desc: { + 'zh-CN': + '通过属性maxLine配置组件在多选场景下选择框显示的最大行数。', + 'en-US': '

    maxLine

    ', + }, + apis: ['TiTreeselectComponent.properties.maxLine'], + }, + { + demoId: 'treeselect-panelwidth', + name: { + 'zh-CN': '下拉宽度', + 'en-US': 'panelwidth', + }, + desc: { + 'zh-CN': + '

    通过属性panelWidth配置下拉宽度,包含justifiedautostring三种类型。', + 'en-US': '

    panelWidth

    ', + }, + apis: ['TiTreeselectComponent.properties.panelWidth'], + }, + { + demoId: 'treeselect-lazyload', + name: { + 'zh-CN': '懒加载', + 'en-US': 'lazyload', + }, + desc: { + 'zh-CN': '通过事件beforeOpen实现懒加载。', + 'en-US': '

    beforeOpen

    ', + }, + apis: ['TiTreeselectComponent.events.beforeOpen'], + }, + { + demoId: 'treeselect-before-expand', + name: { + 'zh-CN': '异步加载', + 'en-US': 'before-expand', + }, + desc: { + 'zh-CN': '通过事件beforeExpand实现异步加载。', + 'en-US': '

    disabled

    ', + }, + apis: ['TiTreeselectComponent.events.beforeExpand'], + }, + { + demoId: 'treeselect-before-more', + name: { + 'zh-CN': '分段加载', + 'en-US': 'before-more', + }, + desc: { + 'zh-CN': + '点击 “更多” 按钮,通过事件beforeMore实现分段异步加载。', + 'en-US': '

    before-more

    ', + }, + apis: ['TiTreeselectComponent.events.beforeMore'], + }, + { + demoId: 'treeselect-event', + name: { + 'zh-CN': '事件', + 'en-US': 'event', + }, + desc: { + 'zh-CN': '点击当前项时,触发select事件。', + 'en-US': '

    before-event

    ', + }, + apis: ['TiTreeselectComponent.events.select'], + }, + { + demoId: 'treeselect-validation', + name: { + 'zh-CN': '校验', + 'en-US': 'validation', + }, + desc: { + 'zh-CN': '

    通过指令tiValidation配置组件的校验规则。', + 'en-US': '

    validation

    ', + }, + }, + ], +}; diff --git a/src/treeselect/demo/src/app/treeselect/webdoc/treeselect.cn.md b/src/treeselect/demo/src/app/treeselect/webdoc/treeselect.cn.md new file mode 100644 index 0000000..f8e524e --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/webdoc/treeselect.cn.md @@ -0,0 +1,25 @@ +--- +title: Treeselect 树选择 +--- + +# Treeselect 树选择 + +
    + +Treeselect 组件是树选择组件。支持单选、多选等,默认为单选。多选选中后以标签形式呈现,标签可删除。 + +```typescript +import { TiTreeselectModule } from "@opentiny/ng"; +``` + +
    + +
    + +Treeselect 组件是树选择组件。支持单选、多选等,默认为单选。多选选中后以标签形式呈现,标签可删除。 + +```typescript +import { TiSelectModule } from "@opentiny/ng"; +``` + +
    diff --git a/src/treeselect/demo/src/app/treeselect/webdoc/treeselect.en.md b/src/treeselect/demo/src/app/treeselect/webdoc/treeselect.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/treeselect/demo/src/app/treeselect/webdoc/treeselect.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/treeselect/demo/src/favicon.ico b/src/treeselect/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/treeselect/demo/src/index.html b/src/treeselect/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/treeselect/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/treeselect/demo/src/main.ts b/src/treeselect/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/treeselect/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/treeselect/demo/test.ts b/src/treeselect/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/treeselect/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/treeselect/demo/tsconfig.app.json b/src/treeselect/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/treeselect/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/treeselect/demo/tsconfig.spec.json b/src/treeselect/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/treeselect/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/treeselect/lib/index.ts b/src/treeselect/lib/index.ts new file mode 100644 index 0000000..7efbc76 --- /dev/null +++ b/src/treeselect/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiTreeselectModule'; diff --git a/src/treeselect/lib/ng-package.json b/src/treeselect/lib/ng-package.json new file mode 100644 index 0000000..74219ad --- /dev/null +++ b/src/treeselect/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/treeselect", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/treeselect/lib/package.json b/src/treeselect/lib/package.json new file mode 100644 index 0000000..6d32b67 --- /dev/null +++ b/src/treeselect/lib/package.json @@ -0,0 +1,20 @@ +{ + "name": "@opentiny/ng-treeselect", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-tree": "~1.0.0-beta.0", + "@opentiny/ng-dominator": "~1.0.0-beta.0", + "@opentiny/ng-drop": "~1.0.0-beta.0", + "@opentiny/ng-searchbox": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-select": "~1.0.0-beta.0", + "@angular/forms": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-dropsearch": "~1.0.0-beta.0", + "@opentiny/ng-checkbox": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/treeselect/lib/project.json b/src/treeselect/lib/project.json new file mode 100644 index 0000000..9cfb9ca --- /dev/null +++ b/src/treeselect/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/treeselect/lib", + "sourceRoot": "src/treeselect/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/treeselect"], + "options": { + "project": "src/treeselect/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/treeselect"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js treeselect" + }, + { + "command": "ng default-build treeselect" + }, + { + "command": "node build/clear-default-theme.js treeselect" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/treeselect && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build treeselect && ng pack treeselect && node build/publish.js treeselect --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/treeselect/lib/src/TiTreeselectComponent.ts b/src/treeselect/lib/src/TiTreeselectComponent.ts new file mode 100644 index 0000000..72f1a1b --- /dev/null +++ b/src/treeselect/lib/src/TiTreeselectComponent.ts @@ -0,0 +1,625 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, SimpleChanges, ViewChild } from '@angular/core'; + +import { TiFormComponent } from '@opentiny/ng-base'; +import { TiTreeComponent, TiTreeNode, TiTreeUtil } from '@opentiny/ng-tree'; +import { TiDominatorComponent } from '@opentiny/ng-dominator'; +import { TiDropComponent } from '@opentiny/ng-drop'; +import { TiSearchboxNotsearchComponent } from '@opentiny/ng-searchbox'; +import { TiLocale } from '@opentiny/ng-locale'; +import { Util } from '@opentiny/ng-utils'; +import { TiSelectComponent } from '@opentiny/ng-select'; +import { TiDropsearchComponent } from '@opentiny/ng-dropsearch'; +import packageInfo from '../package.json'; +/** + * Treeselect树选择下拉组件 + * + * 支持单选/多选,全选,搜索,懒加载。 + * + * 单选主要功能为从Tree组件数据中选择某一条数据,单选与Tree功能相同,只是视觉呈现不同。 + * + * 多选主要功能是从Tree组件数据中任意选择多条数据,多选与Tree功能相同,只是视觉呈现不同。 + * + * 该组件继承自TiSelectComponent,其中 + * + * 输入属性:labelKey、searchKeys、tipPosition、panelMaxHeight和valueKey暂不支持; + * + */ +@Component({ + selector: 'ti-treeselect', + templateUrl: 'treeselect.html', + styleUrls: ['./treeselect.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [TiFormComponent.getValueAccessor(TiTreeselectComponent)], + host: { + '(blur)': 'onBlur()' + } +}) +export class TiTreeselectComponent extends TiSelectComponent { + // 搜索框的高度 + private static readonly SEARCHBOX_HEIGHT: number = 30; + /** + * 下拉面板最大高度 + */ + @Input() dropMaxHeight: number = (30 + 1) * 8; + /** + * 树节点展开前触发的回调,一般用于异步数据获取 + */ + @Output() readonly beforeExpand: EventEmitter = new EventEmitter(); + /** + * 点击 “更多” 按钮时触发的回调,一般用于分段加载场景 + */ + @Output() readonly beforeMore: EventEmitter = new EventEmitter(); + + /** + * @ignore 搜索结果 + */ + public oldOptions: Array; + + /** + * @ignore 内部标签 + */ + @ViewChild(TiDropComponent, { static: true }) dropCom: TiDropComponent; + /** + * @ignore 内部标签 + */ + @ViewChild(TiDominatorComponent, { static: true }) + dominatorCom: TiDominatorComponent; + /** + * @ignore 内部标签 + */ + @ViewChild('searchboxCom') searchboxCom: TiSearchboxNotsearchComponent; + /** + * @ignore 内部标签 + */ + @ViewChild('datatemplate') dataTemplate: ElementRef; + + /** + * @ignore 全选框的半选中状态 + */ + public indeterminate: boolean = false; + + /** + * @ignore 是否全选中 + */ + public isAllSelected: boolean = false; + + /** + * @ignore 搜索内容 + */ + public searchText: string = ''; + + /** + * @ignore 是否处于搜索状态 + */ + private isInSearch: boolean = false; + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + super.ngOnInit(); + // 强制装换,方便调用super.xxx + this.dropsearchCom = this.dropCom as unknown as TiDropsearchComponent; + + // 记录传入的数据,浅拷贝 + this.oldOptions = this.options; + } + + ngOnChanges(changes: SimpleChanges): void { + // 解决延迟或者动态设置options,树组件未渲染的问题 + if (changes['options'] && !changes['options'].firstChange) { + // 重新记录传入的数据 + this.oldOptions = changes['options'].currentValue; + } + } + + ngDoCheck(): void { + super.ngDoCheck(); + // 异步挂载数据,改变节点属性时指引未发生变化,onpush模式下不会触发变更,故手动触发 + this.changeDetectorRef.markForCheck(); + } + + ngAfterViewChecked(): void { + if (this.searchable !== this.oldSearchable) { + this.oldSearchable = this.searchable; + if (this.searchable) { + this.setFocusableElems( + this.dominatorCom.getFocusableElems().concat(this.searchboxCom.getFocusableElems()).concat(this.dropCom.nativeElement) + ); + } else { + this.setFocusableElems(this.dominatorCom.getFocusableElems().concat(this.dropCom.nativeElement)); + } + } + // 调用父类逻辑afterViewChecked才去设置autofocs + super.ngAfterViewChecked(); + } + + /** + * @ignore drop高度被压缩时,重新设置下拉面板的高度 + */ + private restyleDropMaxHeight(): void { + let dropCurMaxHeight: number = parseInt(this.dropCom.nativeElement.style.maxHeight, 10); + + let dropMaxHeightAdapted: number = this.dropMaxHeight; + if (!isNaN(dropCurMaxHeight)) { + // 减去搜索框高度和间距的和 + if (this.searchable) { + dropCurMaxHeight -= TiTreeselectComponent.SEARCHBOX_HEIGHT; + } + if (dropCurMaxHeight < this.dropMaxHeight) { + dropMaxHeightAdapted = dropCurMaxHeight; + } + } + + this.renderer.setStyle(this.dataTemplate.nativeElement, 'max-height', dropMaxHeightAdapted + 'px'); + } + + ngOnModelChange(): void { + if (this.multiple) { + // 多选时,重新设置下拉面板定位 + this.setPosition(); + if (this.selectAll) { + // 允许全选时,设置全选框状态 + this.setAllSelectCheckboxState(); + } + } + } + + /** + * @ignore 设置下拉面板定位 + */ + setPosition(optionsChange?: boolean): void { + setTimeout(() => { + if (optionsChange) { + this.dropCom.resetPosition(); + } else { + this.dropCom.setPosition(); + } + this.restyleDropMaxHeight(); + }, 0); + } + + /** + * @ignore 关闭下拉面板并转移焦点 + */ + public close(): void { + this.dropCom.hide(); + // 焦点转移至dominator + if (this.searchable) { + this.dominatorCom.focus(); + } + } + + /** + * @ignore 仅仅关闭下拉面板 + */ + private closeWithoutFocus(): void { + this.dropCom.hide(); + } + + /** + * @ignore 打开下拉面板 + */ + public open(): void { + // 初始化最大高度 + this.renderer.setStyle(this.dataTemplate.nativeElement, 'max-height', this.dropMaxHeight + 'px'); + this.dropCom.show(); + this.restyleDropMaxHeight(); + // 焦点转移至搜索框 + if (this.searchable) { + this.searchboxCom.focus(); + } + } + + /** + * 尝试打开下拉面板 + */ + public wantOpen(): void { + super.wantOpen(); + } + + /** + * @ignore 点击ti-dominator触发的事件 + * 点击下拉面板,展开或者关闭面板 + */ + public onClickDominator(): void { + if (this.disabled) { + return; + } + if (!this.dropCom.isShow) { + // 清空搜索内容 + this.searchText = ''; + this.searchTextChange(this.searchText); + this.wantOpen(); + } else { + this.close(); + } + } + + /** + * @ignore + * 单选点击清除按钮时触发 clear 事件, 如果下拉中有搜索,则需要聚焦于 searchbox。 + */ + public onClearDominator(): void { + this.clear.emit(); + + if (this.dropsearchCom.isShow && this.searchable) { + this.searchboxCom.focus(); + } + } + + /** + * @ignore 删除ti-dominator选中项时触发的事件 + * 需要取消树节点的选中 + */ + public onDeleteDominatorTag(item: any): void { + const deletedItem: TiTreeNode = item.item; + // 1.取消选中当前节点、设置子节点及祖先节点的选中情况。从model中删除时,item.item.checked还是为true,要设置为false + this.setSelectState(deletedItem, !deletedItem.checked); + + // 2.设置为非全选 + this.isAllSelected = false; + // 3.设置全选框状态 + if (this.multiple && this.selectAll) { + item.model.length === 0 ? (this.indeterminate = false) : (this.indeterminate = true); + } + } + + /** + * @ignore ti-searchbox-notsearch搜索框内容变化时触发的事件 + */ + public searchTextChange(searchText: string): void { + if (Util.isEmptyString(searchText)) { + this.options = this.oldOptions; + this.isInSearch = false; + } else { + // this.oldOptions 叶子浅拷贝 + const searchResult: Array = TiTreeUtil.copy(this.oldOptions); + TiTreeUtil.search(searchResult, (cnode: TiTreeNode): boolean => { + return cnode.label.toLowerCase().indexOf(this.searchText.toLowerCase()) >= 0; + }); + // 展开整个树 + TiTreeUtil.traverse(searchResult, (node: TiTreeNode): void => { + if (node.children && node.children.length > 0) { + node.expanded = true; + } + }); + this.options = searchResult; + this.isInSearch = true; + } + if (this.multiple && this.selectAll) { + this.setAllSelectCheckboxState(); + } + + // 重新设置面板定位 + this.setPosition(true); + } + + /** + * @ignore + */ + public onBeforeExpand(treeInst: TiTreeComponent): void { + const node: TiTreeNode = treeInst.getBeforeExpandNode(); + if (this.beforeExpand.observers.length === 0) { + node.expanded = true; + } else { + this.beforeExpand.emit(node); + } + } + + /** + * @ignore + */ + public onBeforeMore(node: TiTreeNode): void { + this.beforeMore.emit(node); + } + + /** + * @ignore 从model中获取option所在的索引 + */ + private getItemIndexFromModel(option: TiTreeNode): number { + let index: number = -1; + index = this.model.findIndex((item: any) => { + // 判断item和option的引用是否相同即可 + return item === option; + }); + + return index; + } + + /** + * @ignore 获取需要放到model中的节点,根据规范,如果子节点全部选中了,需要在model中放入父节点或者祖先节点。找到第一个checked为true的节点即可 + * node: 树组件select事件中的参数 + */ + private getRealSelectedNode(node: TiTreeNode): TiTreeNode { + let selectedNode: TiTreeNode = null; + const parentNode: TiTreeNode = TiTreeUtil.getParentNode(this.oldOptions, node); + parentNode && parentNode.checked === true ? (selectedNode = this.getRealSelectedNode(parentNode)) : (selectedNode = node); + + return selectedNode; + } + + /** + * @ignore 获取需要从model移除的节点,根据规范,如果某个子节点取消选中了,那么可能是它的父节点或祖先节点或所有子节点从model中被移除 + * node:树组件select事件的参数 + */ + private getRealUnselectedNode(node: TiTreeNode): TiTreeNode { + let unselectedNode: TiTreeNode = null; + let parentNode: TiTreeNode = TiTreeUtil.getParentNode(this.oldOptions, node); + + if (this.getItemIndexFromModel(node) !== -1) { + // 需要从model中移除的节点是node本身 + unselectedNode = node; + } else { + // 需要从model中移除的节点是node父节点或祖先节点 + // 当节点处于半选状态时,点击节点的复选框,节点状态为选中,否则为非选中,所以要先判断parentNode是否存在 + while (parentNode && this.getItemIndexFromModel(parentNode) === -1) { + parentNode = TiTreeUtil.getParentNode(this.oldOptions, parentNode); + } + // parentNode可能为undefined + unselectedNode = parentNode; + } + + return unselectedNode; + } + + /** + * @ignore 取消选中时,获取被选中的子节点,根据规范,父节点或祖先节点被选中之后,如果取消了某个子节点的选中,那么需要把它的所有选中的子节点加入model中 + * unselectedNode:需要从model中移除的节点 + */ + private getSelectedChildren(unselectedNode: TiTreeNode): Array { + let selectedChildren: Array = []; + if (unselectedNode.children) { + unselectedNode.children.forEach((child: TiTreeNode) => { + if (child.checked === true) { + selectedChildren.push(child); + } else { + selectedChildren = selectedChildren.concat(this.getSelectedChildren(child)); + } + }); + } + + return selectedChildren; + } + + /** + * @ignore 根据checked的值,设置option节点及其子节点和祖先节点的选中状态 + * option: 选中的节点;checked:option.checked属性,true/false + */ + private setSelectState(option: TiTreeNode, checked: boolean): void { + // 对oldOptions进行操作,将搜索时的结果映射回来 + TiTreeUtil.traverse(this.oldOptions, (node: TiTreeNode) => { + if (node === option) { + node.checked = checked; + if (checked === true) { + TiTreeUtil.selectAllChildren(node); + TiTreeUtil.selectParents(node, this.oldOptions, true); + } else { + TiTreeUtil.deSelectAllChildren(node); + TiTreeUtil.deSelectParents(node, this.oldOptions); + } + } + }); + } + + /** + * @ignore 选中某个节点后,更新this.model + * option: 选中的节点 + */ + private setModelWhenSelected(option: TiTreeNode): void { + // 1.获取option最后一个被选中的祖先节点 + const selectedNode: TiTreeNode = this.getRealSelectedNode(option); + // 2.从model中删除selectNode的所有子节点,一直到叶子节点 + TiTreeUtil.traverse([selectedNode], (node: TiTreeNode) => { + let deleteIndex: number = this.getItemIndexFromModel(node); + while (deleteIndex !== -1) { + this.model.splice(deleteIndex, 1); + deleteIndex = this.getItemIndexFromModel(node); + } + }); + // 3.将selectedNode加入到this.model中 + if (this.getItemIndexFromModel(selectedNode) === -1) { + this.model.push(selectedNode); + } + } + + /** + * @ignore 取消选中某个节点后,更新this.model + * option: 被取消选中的节点 + */ + private setModelWhenUnselected(option: TiTreeNode): void { + // 1.获取需要从model中删除的节点 + const unselectedNode: TiTreeNode = this.getRealUnselectedNode(option); + + // 2.更新model + if (unselectedNode) { + // 2.1unselectedNode为option或option的祖先节点 + const deleteIndex: number = this.getItemIndexFromModel(unselectedNode); + this.model.splice(deleteIndex, 1); + // 将option所有选中的子节点存放到this.model中 + if (unselectedNode !== option) { + const selectedChildren: Array = this.getSelectedChildren(unselectedNode); + this.model.push(...selectedChildren); + } + } else { + // 2.2unselectedNode为undefined,这种情况下,只需从model中删除option所有选中的子节点 + TiTreeUtil.traverse([option], (node: TiTreeNode) => { + const deleteIndex: number = this.getItemIndexFromModel(node); + if (deleteIndex !== -1) { + this.model.splice(deleteIndex, 1); + } + }); + } + } + + /** + * @ignore 处于搜索状态下,更新model + */ + private setModelWhenSearch(data: Array, checked: boolean): void { + // 或者所有处于checked状态的叶子节点 + let selectedLeafNodes: Array = []; + selectedLeafNodes = this.getLeafNodes(data, checked); + // 根据每个叶子节点更新model + for (const leafNode of selectedLeafNodes) { + // 设置oldOptions各节点状态 + this.setSelectState(leafNode, checked); + checked ? this.setModelWhenSelected(leafNode) : this.setModelWhenUnselected(leafNode); + } + } + + /** + * @ignore 获取data里面选中或者未选中的全部叶子节点 + * data:搜索范围,checked:true/false + */ + private getLeafNodes(data: Array, checked: boolean): Array { + const leafNodes: Array = []; + TiTreeUtil.traverse(data, (node: TiTreeNode) => { + if ((TiTreeUtil.isLeaf(node) || (node.children && node.children.length === 0)) && node.checked === checked) { + leafNodes.push(node); + } + }); + + return leafNodes; + } + + /** + * @ignore 设置全选checkbox的状态 + */ + private setAllSelectCheckboxState(): void { + // 1.假设节点都被选中或都没被选中 + let isAllSelected: boolean = true; + let isAllUnSelected: boolean = true; + + // 2.判断是否所有节点都被选中或都没被选中,disabled状态下的节点不用统计 + TiTreeUtil.traverse(this.options, (node: TiTreeNode) => { + // 2.1 有节点没被选中 + if (!node.disabled && node.checked !== true) { + isAllSelected = false; + } + // 2.2 有节点被选中或半选中 + if (!node.disabled && (node.checked === true || node.checked === 'indeterminate')) { + isAllUnSelected = false; + } + }); + + // 3.设置checkbox状态 + this.isAllSelected = isAllSelected; + this.indeterminate = !isAllSelected && !isAllUnSelected; + } + + /** + * @ignore ti-tree的select事件 + */ + public onTreeSelect(option: any): void { + // 节点被禁用,不做操作,直接返回 + if (option.disabled === true) { + return; + } + + // 1.触发select事件 + this.select.emit(option); + if (!this.multiple) { + // 2.1单选,直接替换model + TiTreeUtil.traverse(this.options, (node: TiTreeNode) => { + if (node === option) { + node.checked = true; + this.model = [option]; + } else { + node.checked = false; + } + }); + // 关闭下拉面板 + this.close(); + } else { + // 2.2多选 + if (!this.isInSearch) { + // 2.2.1非搜索状态 + // 设置oldOptions各节点状态 + this.setSelectState(option, option.checked); + if (option.checked === true) { + this.setModelWhenSelected(option); + } else { + this.setModelWhenUnselected(option); + } + } else { + // 2.2.2搜索状态 + this.setModelWhenSearch([option], option.checked); + } + + // 2.3手动触发ngModelChange事件,splice和push无法触发 + this.model = this.model.concat(); + // 2.4设置全选状态 + if (this.selectAll) { + this.setAllSelectCheckboxState(); + } + } + + // 3.重新设置ti-dominator高度 + this.setPosition(); + } + + /** + * @ignore 全选复选框的按钮事件 + */ + public onSelectAllChange(isAllSelected: any): void { + // 1.将当前tree节点全部选中 或 不选中 + TiTreeUtil.traverse(this.options, (node: TiTreeNode) => { + if (!node.disabled) { + node.checked = isAllSelected; + } + }); + + // 2.设置model + if (!this.isInSearch) { + // 2.1 未处于搜索状态下,使用concat进行单层深拷贝 + !this.isAllSelected ? (this.model = this.options.concat()) : (this.model = []); + } else { + // 2.2 处于搜索状态下 + this.setModelWhenSearch(this.options, isAllSelected); + // 手动触发ngModelChange事件 + this.model = this.model.concat(); + } + } + + /** + * @ignore 全选复选框的点击事件 + */ + public onClickSelectAll(event: any): void { + // 点击时,this.isAllSelected为false,所以要取反 + this.onSelectAllChange(!this.isAllSelected); + event.preventDefault(); + } + + /** + * @ignore + * 失焦情况下,仅关闭面板,不做聚焦等处理 + */ + public onBlur(): void { + this.closeWithoutFocus(); + } + + /** + * @ignore + * 鼠标点击到空白,原本会失焦,此处通过阻止默认事件的方式进行了处理 + */ + public onMouseDownDropOuter(event: any): void { + event.preventDefault(); + } + + /** + * @ignore + * #datatemplate内部滚动条会引起外部滚动条事件触发,引起弹框内的树下拉组件无法使用鼠标拖动滚动条,因此此处阻止事件冒泡 + */ + public onMouseWheel(event: any): void { + event.stopPropagation(); + } +} diff --git a/src/treeselect/lib/src/TiTreeselectModule.ts b/src/treeselect/lib/src/TiTreeselectModule.ts new file mode 100644 index 0000000..5908ddb --- /dev/null +++ b/src/treeselect/lib/src/TiTreeselectModule.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; + +import { TiTreeselectComponent } from './TiTreeselectComponent'; +import { TiDominatorModule } from '@opentiny/ng-dominator'; +import { TiDropsearchModule } from '@opentiny/ng-dropsearch'; +import { TiDropModule } from '@opentiny/ng-drop'; +import { TiTreeModule } from '@opentiny/ng-tree'; +import { TiSearchboxModule } from '@opentiny/ng-searchbox'; +import { TiCheckboxModule } from '@opentiny/ng-checkbox'; + +@NgModule({ + imports: [ + TiDominatorModule, + TiDropsearchModule, + TiDropModule, + TiTreeModule, + TiSearchboxModule, + TiCheckboxModule, + FormsModule, + CommonModule + ], + exports: [TiTreeselectComponent], + declarations: [TiTreeselectComponent] +}) +export class TiTreeselectModule {} + +export { TiTreeselectComponent } from './TiTreeselectComponent'; diff --git a/src/treeselect/lib/src/treeselect.html b/src/treeselect/lib/src/treeselect.html new file mode 100644 index 0000000..bd21ea3 --- /dev/null +++ b/src/treeselect/lib/src/treeselect.html @@ -0,0 +1,73 @@ + + + + + + + + +
    + + +
  • + +
  • + + +
    +
    +
    + + + +
    +
    diff --git a/src/treeselect/lib/src/treeselect.less b/src/treeselect/lib/src/treeselect.less new file mode 100644 index 0000000..f6d938d --- /dev/null +++ b/src/treeselect/lib/src/treeselect.less @@ -0,0 +1,49 @@ +@import '../../../themes/basic/base-all.less'; +@import '../../../themes/basic/compnent-container-border.less'; + +:host { + --ti-treeselect-container-width: 280px; +} + +:host :extend(.ti3-compnent-container-border all) { + width: var(--ti-treeselect-container-width); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); +} + +ti-drop { + outline: none; +} + +.ti3-treeselect-selectall-option { + list-style: none; + cursor: pointer; + color: inherit; + text-align: left; + line-height: var(--ti-common-line-height-number); + background-color: var(--ti-common-color-bg-white-normal); + padding: var(--ti-common-space-6) var(--ti-common-space-2x); + &:hover { + background-color: var(--ti-common-color-bg-white-emphasize); + color: var(--ti-common-color-text-highlight); + } +} + +.ti3-treeselect-searchbox { + width: 100% !important; +} + +.ti3-treeselect-datacontainer { + overflow-y: auto; + overflow-x: hidden; + padding: var(--ti-common-space-base) var(--ti-common-space-0); + max-height: inherit; + box-sizing: border-box; +} + +.ti3-treeselect-dropdown-no-data { + padding: var(--ti-common-space-6) var(--ti-common-space-10); + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + line-height: var(--ti-common-line-height-number); +} diff --git a/src/upload/demo/karma.conf.js b/src/upload/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/upload/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/upload/demo/project.json b/src/upload/demo/project.json new file mode 100644 index 0000000..60324c2 --- /dev/null +++ b/src/upload/demo/project.json @@ -0,0 +1,98 @@ +{ + "projectType": "application", + "root": "src/upload/demo", + "sourceRoot": "src/upload/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/upload", + "index": "src/upload/demo/src/index.html", + "main": "src/upload/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/upload/demo/tsconfig.app.json", + "assets": [ + "src/upload/demo/src/favicon.ico", + "src/upload/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/image", + "output": "/assets/image/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "upload-demo:build:production" + }, + "development": { + "browserTarget": "upload-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js upload" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/upload/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/upload/demo/tsconfig.spec.json", + "karmaConfig": "src/upload/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/upload/demo/src/app/AppComponent.ts b/src/upload/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/upload/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/upload/demo/src/app/AppModule.ts b/src/upload/demo/src/app/AppModule.ts new file mode 100644 index 0000000..f807c0d --- /dev/null +++ b/src/upload/demo/src/app/AppModule.ts @@ -0,0 +1,28 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { UploadTestModule } from './upload/UploadTestModule'; +import { UploadimageTestModule } from './uploadimage/UploadimageTestModule'; + +@NgModule({ + imports: [ + UploadTestModule, + UploadimageTestModule, + BrowserAnimationsModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/upload/demo/src/app/IndexComponent.ts b/src/upload/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..677850c --- /dev/null +++ b/src/upload/demo/src/app/IndexComponent.ts @@ -0,0 +1,52 @@ +import { Component } from '@angular/core'; +import { UploadTestModule } from './upload/UploadTestModule'; +import { UploadimageTestModule } from './uploadimage/UploadimageTestModule'; + +@Component({ + template: ` + + + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + moudles: Array = [UploadTestModule.ROUTES, UploadimageTestModule.ROUTES]; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/upload/demo/src/app/app.html b/src/upload/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/upload/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/upload/demo/src/app/upload/UploadAutoUploadComponent.ts b/src/upload/demo/src/app/upload/UploadAutoUploadComponent.ts new file mode 100644 index 0000000..ea5b62e --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadAutoUploadComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./upload-auto-upload.html` +}) +export class UploadAutoUploadComponent {} diff --git a/src/upload/demo/src/app/upload/UploadBasicComponent.ts b/src/upload/demo/src/app/upload/UploadBasicComponent.ts new file mode 100644 index 0000000..46aad7f --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadBasicComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./upload-basic.html` +}) +export class UploadBasicComponent {} diff --git a/src/upload/demo/src/app/upload/UploadBatchSendComponent.ts b/src/upload/demo/src/app/upload/UploadBatchSendComponent.ts new file mode 100644 index 0000000..069b419 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadBatchSendComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./upload-batch-send.html` +}) +export class UploadBatchSendComponent {} diff --git a/src/upload/demo/src/app/upload/UploadBeforeremoveComponent.ts b/src/upload/demo/src/app/upload/UploadBeforeremoveComponent.ts new file mode 100644 index 0000000..da2127e --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadBeforeremoveComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiFileItem } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-beforeRemove.html` +}) +export class UploadBeforeremoveComponent { + url: string = '/upload'; + + onBeforeRemoveItems(fileItemArry: Array): void { + setTimeout(() => { + fileItemArry[0].remove(); + }, 1000); + } +} diff --git a/src/upload/demo/src/app/upload/UploadButtonComponent.ts b/src/upload/demo/src/app/upload/UploadButtonComponent.ts new file mode 100644 index 0000000..46f6be1 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadButtonComponent.ts @@ -0,0 +1,6 @@ +import { Component, ViewChild } from '@angular/core'; + +@Component({ + templateUrl: `./upload-button.html` +}) +export class UploadButtonComponent {} diff --git a/src/upload/demo/src/app/upload/UploadButtonTestComponent.ts b/src/upload/demo/src/app/upload/UploadButtonTestComponent.ts new file mode 100644 index 0000000..9fb9a4e --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadButtonTestComponent.ts @@ -0,0 +1,77 @@ +import { Component } from '@angular/core'; +import { TiFileItem, TiFilter } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-button-test.html` +}) +export class UploadButtonTestComponent { + type: string = 'button'; + disabled: boolean = true; + url: string = '/upload'; + autoUpload: boolean = false; + batchSend: boolean = true; + method: string = 'get'; + alias: string = 'tiFileRename'; + formDataFirst: boolean = true; + headers: any = { + tiheadersConfig: 'aa' + }; + formData: any = {}; + filters: Array = [ + { + name: 'maxCount', + params: [5] + } + ]; + onAddItemSuccess(fileItem: TiFileItem): void { + console.log(`filename:${fileItem.file.name}`); + } + onAddItemFailed(fileItem: TiFileItem): void { + console.log(`filename:${fileItem.file.name}`); + } + onBeforeSendItems(fileItems: Array): void { + fileItems.forEach((item: TiFileItem) => { + item.formData = { + tinyFormdata: 'aa' + }; + }); + } + onProgressItems($event: any): void { + console.log('onProgressItems'); + console.log($event.fileItems); + console.log($event.progress); + } + onSuccessItems($event: any): void { + console.log('onSuccessItems'); + console.log($event.fileItems); + console.log($event.response); + console.log($event.status); + } + onErrorItems($event: any): void { + console.log('onErrorItems'); + console.log($event.fileItems); + console.log($event.response); + console.log($event.status); + } + onCancelItems($event: any): void { + console.log('onCancelItems'); + console.log($event.fileItems); + console.log($event.response); + console.log($event.status); + } + onRemoveItems($event: any): void { + console.log('onRemoveItems'); + console.log($event); + } + onCompleteItems($event: any): void { + // 根据状态码和返回消息设置详情信息 + console.log(`response${$event.response}`); + } + onCompleteAllItems($event: any): void { + console.log('onCompleteAllItems'); + console.log($event); + } + changeDisable(): void { + this.disabled = !this.disabled; + } +} diff --git a/src/upload/demo/src/app/upload/UploadCaseTestComponent.ts b/src/upload/demo/src/app/upload/UploadCaseTestComponent.ts new file mode 100644 index 0000000..7f678e7 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadCaseTestComponent.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { TiFileItem } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-case-test.html` +}) +export class UploadCaseTestComponent { + url: string = '/upload'; + autoUpload: boolean = false; + + onBeforeSendItems(files: Array): void { + console.log('onBeforeSendItems', files); + files[0].uploader.cancelAll(); // 在上传前全部取消 + } +} diff --git a/src/upload/demo/src/app/upload/UploadChangesComponent.ts b/src/upload/demo/src/app/upload/UploadChangesComponent.ts new file mode 100644 index 0000000..8175ea1 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadChangesComponent.ts @@ -0,0 +1,29 @@ +import { Component, ViewChild } from '@angular/core'; +import { TiFilter, TiUploadComponent } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-changes.html` +}) +export class UploadChangesComponent { + @ViewChild(TiUploadComponent) uploadCom: TiUploadComponent; + myLogs: Array = []; + buttonText: string = '按钮样式'; + inputFieldWidth: string = '500px'; + url: string = '/upload'; + headers: any = { + tiheadersConfig: 'aa' + }; + beforeSendItems($event: Event): void { + this.myLogs = [...this.myLogs, `url='${$event[0].url}';headers=${JSON.stringify($event[0].headers)}`]; + } + changeUrl(): void { + this.url = '/newUrl'; + this.myLogs = [...this.myLogs, `修改url='${this.url}'`]; + } + changeHeaders(): void { + this.headers = { + newTiheadersConfig: 'newConfig' + }; + this.myLogs = [...this.myLogs, `修改headers=${JSON.stringify(this.headers)}`]; + } +} diff --git a/src/upload/demo/src/app/upload/UploadChunksizeComponent.ts b/src/upload/demo/src/app/upload/UploadChunksizeComponent.ts new file mode 100644 index 0000000..efecaa5 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadChunksizeComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TiFileItem } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-chunksize.html` +}) +export class UploadChunksizeComponent { + myLogs: Array = []; + chunkSize: number = 1024 * 1024; + onAddItemSuccess(fileItem: TiFileItem): void { + this.myLogs = [...this.myLogs, `onAddItemSuccess() filename:${fileItem.file.name}`]; + } + onAddItemFailed(fileItem: TiFileItem): void { + this.myLogs = [...this.myLogs, `onAddItemFailed() filename:${fileItem.file.name}`]; + } + onBeforeSendItems(fileItems: Array): void { + this.myLogs = [...this.myLogs, `onBeforeSendItems()`]; + fileItems.forEach((item: TiFileItem) => { + item.formData = { + tinyFormdata: 'Custom data' + }; + }); + } + onProgressItems($event: any): void { + this.myLogs = [...this.myLogs, `onProgressItems() progress:${$event.progress}`]; + } + onSuccessItems($event: any): void { + this.myLogs = [...this.myLogs, `onSuccessItems() response:${$event.response} status:${$event.status}`]; + } + onErrorItems($event: any): void { + this.myLogs = [...this.myLogs, `onErrorItems() response:${$event.response} status:${$event.status}`]; + } + onCancelItems($event: any): void { + this.myLogs = [...this.myLogs, `onCancelItems() response:${$event.response} status:${$event.status}`]; + } + onRemoveItems($event: any): void { + this.myLogs = [...this.myLogs, `onRemoveItems()`]; + } + onBeforeRemoveItems(fileItemArry: Array): void { + this.myLogs = [...this.myLogs, `onBeforeRemoveItems()`]; + fileItemArry[0].remove(); + } + onCompleteItems($event: any): void { + this.myLogs = [...this.myLogs, `onCompleteItems() response:${$event.response}`]; + } + onCompleteAllItems($event: any): void { + this.myLogs = [...this.myLogs, `onCompleteAllItems()`]; + } +} diff --git a/src/upload/demo/src/app/upload/UploadCustomComponent.ts b/src/upload/demo/src/app/upload/UploadCustomComponent.ts new file mode 100644 index 0000000..527ea8d --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadCustomComponent.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./upload-custom.html` +}) +export class UploadCustomComponent { + myLogs: Array = []; + customHeaders: any = { + tiCustomHeader: 'TinyNG' + }; + customFormData: any = { + tiCustomFormData: 'hello tiny' + }; + beforeSendItems($event: Event): void { + this.myLogs = [ + ...this.myLogs, + `请求方式:${$event[0].method}; 文件名:'${$event[0].alias}'; 头信息:${JSON.stringify( + $event[0].headers + )}; 附加信息:${JSON.stringify($event[0].formData)}` + ]; + } +} diff --git a/src/upload/demo/src/app/upload/UploadEventComponent.ts b/src/upload/demo/src/app/upload/UploadEventComponent.ts new file mode 100644 index 0000000..60c9ae3 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadEventComponent.ts @@ -0,0 +1,48 @@ +import { Component } from '@angular/core'; +import { TiFileItem, TiFilter } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-event.html` +}) +export class UploadEventComponent { + myLogs: Array = []; + onAddItemSuccess(fileItem: TiFileItem): void { + this.myLogs = [...this.myLogs, `onAddItemSuccess() filename:${fileItem.file.name}`]; + } + onAddItemFailed(fileItem: TiFileItem): void { + this.myLogs = [...this.myLogs, `onAddItemFailed() filename:${fileItem.file.name}`]; + } + onBeforeSendItems(fileItems: Array): void { + this.myLogs = [...this.myLogs, `onBeforeSendItems()`]; + fileItems.forEach((item: TiFileItem) => { + item.formData = { + tinyFormdata: 'Custom data' + }; + }); + } + onProgressItems($event: any): void { + this.myLogs = [...this.myLogs, `onProgressItems() progress:${$event.progress}`]; + } + onSuccessItems($event: any): void { + this.myLogs = [...this.myLogs, `onSuccessItems() response:${$event.response} status:${$event.status}`]; + } + onErrorItems($event: any): void { + this.myLogs = [...this.myLogs, `onErrorItems() response:${$event.response} status:${$event.status}`]; + } + onCancelItems($event: any): void { + this.myLogs = [...this.myLogs, `onCancelItems() response:${$event.response} status:${$event.status}`]; + } + onRemoveItems($event: any): void { + this.myLogs = [...this.myLogs, `onRemoveItems()`]; + } + onBeforeRemoveItems(fileItemArry: Array): void { + this.myLogs = [...this.myLogs, `onBeforeRemoveItems()`]; + fileItemArry[0].remove(); + } + onCompleteItems($event: any): void { + this.myLogs = [...this.myLogs, `onCompleteItems() response:${$event.response}`]; + } + onCompleteAllItems($event: any): void { + this.myLogs = [...this.myLogs, `onCompleteItems()`]; + } +} diff --git a/src/upload/demo/src/app/upload/UploadFilterComponent.ts b/src/upload/demo/src/app/upload/UploadFilterComponent.ts new file mode 100644 index 0000000..c7de7c5 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadFilterComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiFilter } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-filter.html` +}) +export class UploadFilterComponent { + filters: Array = [ + { + name: 'type', + params: ['.png,.img'] + }, + { + name: 'maxSize', + params: [102400] + }, + { + name: 'maxCount', + params: [2] + } + ]; + myLogs: Array = []; + onAddItemFailed(event: any): void { + this.myLogs = [...this.myLogs, `errorType:${event.validResults.toString()}----filename:${event.file.name}`]; + } + onAddItemSuccess(event: any): void { + this.myLogs = [...this.myLogs, `success----filename:${event.file.name}`]; + } +} diff --git a/src/upload/demo/src/app/upload/UploadFormDataComponent.ts b/src/upload/demo/src/app/upload/UploadFormDataComponent.ts new file mode 100644 index 0000000..21ae5a5 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadFormDataComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { TiFileItem } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-form-data.html` +}) +export class UploadFormDataComponent { + inputValue: string = 'aa'; + url: string = '/upload'; + onBeforeSendItems(fileItems: Array): void { + // 上传前动态添加formData + fileItems.forEach((item: TiFileItem) => { + item.formData = { + inputValut: this.inputValue, + name: 'xxy' + }; + }); + } +} diff --git a/src/upload/demo/src/app/upload/UploadInitfilesTestComponent.ts b/src/upload/demo/src/app/upload/UploadInitfilesTestComponent.ts new file mode 100644 index 0000000..380c791 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadInitfilesTestComponent.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core'; +import { TiFileItem, TiFilter } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-initfiles-test.html` +}) +export class UploadInitfilesTestComponent { + initFiles: Array = [ + { + name: 'demo.txt' + }, + { + name: 'demo.sh' + } + ]; + filters: Array = [ + { + name: 'maxCount', + params: [4] + } + ]; + initFiles1: Array = [ + { + name: 'demo.txt' + } + ]; + filters1: Array = [ + { + name: 'maxCount', + params: [1] + } + ]; + onBeforeRemoveItems(fileItemArry: Array): void { + fileItemArry[0].remove(); + } +} diff --git a/src/upload/demo/src/app/upload/UploadInputFieldTestComponent.ts b/src/upload/demo/src/app/upload/UploadInputFieldTestComponent.ts new file mode 100644 index 0000000..239e30d --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadInputFieldTestComponent.ts @@ -0,0 +1,83 @@ +import { Component } from '@angular/core'; +import { TiFileInfo, TiFileItem, TiFilter } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-input-field-test.html` +}) +export class UploadInputFieldTestComponent { + disabled: boolean = true; + url: string = '/upload'; + autoUpload: boolean = false; + batchSend: boolean = true; + method: string = 'get'; + alias: string = 'tiFileRename'; + placeholder: string = 'please select file'; + formDataFirst: boolean = true; + headers: any = { + tiheadersConfig: 'aa' + }; + formData: any = {}; + filters: Array = [ + { + name: 'maxCount', + params: [5] + } + ]; + filters1: Array = [ + { + name: 'maxCount', + params: [1] + } + ]; + onAddItemSuccess(fileItem: TiFileItem): void { + console.log(`filename:${fileItem.file.name}`); + } + onAddItemFailed(fileInfo: TiFileInfo): void { + console.log(`filename:${fileInfo.name}`); + } + onBeforeSendItems(fileItems: Array): void { + fileItems.forEach((item: TiFileItem) => { + item.formData = { + tinyFormdata: 'aa' + }; + }); + } + onProgressItems($event: any): void { + console.log('onProgressItems'); + console.log($event.fileItems); + console.log($event.progress); + } + onSuccessItems($event: any): void { + console.log('onSuccessItems'); + console.log($event.fileItems); + console.log($event.response); + console.log($event.status); + } + onErrorItems($event: any): void { + console.log('onErrorItems'); + console.log($event.fileItems); + console.log($event.response); + console.log($event.status); + } + onCancelItems($event: any): void { + console.log('onCancelItems'); + console.log($event.fileItems); + console.log($event.response); + console.log($event.status); + } + onRemoveItems($event: any): void { + console.log('onRemoveItems'); + console.log($event); + } + onCompleteItems($event: any): void { + // 根据状态码和返回消息设置详情信息 + console.log(`response${$event.response}`); + } + onCompleteAllItems($event: any): void { + console.log('onCompleteAllItems'); + console.log($event); + } + changeDisable(): void { + this.disabled = false; + } +} diff --git a/src/upload/demo/src/app/upload/UploadPropsComponent.ts b/src/upload/demo/src/app/upload/UploadPropsComponent.ts new file mode 100644 index 0000000..a401497 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadPropsComponent.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./upload-props.html` +}) +export class UploadPropsComponent { + public isOpen: boolean = true; + public showUploadList: boolean = true; + public showErrorMessage: boolean = true; + initFiles: Array = [ + { + name: 'demo.txt', + allowDelete: false + }, + { + name: 'demo.sh' + } + ]; + changeDisabled(): void { + this.isOpen = !this.isOpen; + } + changeShowList(): void { + this.showUploadList = !this.showUploadList; + } + changeShowError(): void { + this.showErrorMessage = !this.showErrorMessage; + } +} diff --git a/src/upload/demo/src/app/upload/UploadServiceComponent.ts b/src/upload/demo/src/app/upload/UploadServiceComponent.ts new file mode 100644 index 0000000..d73a7c5 --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadServiceComponent.ts @@ -0,0 +1,72 @@ +import { Component } from '@angular/core'; +import { TiFileInfo, TiFileItem, TiUploadConfig, TiUploadRef, TiUploadService } from '@opentiny/ng'; + +@Component({ + templateUrl: './upload-service.html' +}) +export class UploadServiceComponent { + uploader: TiUploadRef; + constructor(uploaderService: TiUploadService) { + const config: TiUploadConfig = { + url: '/upload', + onAddItemFailed: (fileObject: TiFileInfo, validResults: Array): void => { + console.log('onAddItemFailed'); + console.log('onAddItemFailed.fileObject:', fileObject); + console.log('onAddItemFailed.validResults:', validResults); + }, + onAddItemSuccess: (fileItem: TiFileItem): void => { + // 需要给每个文件设置allowDelete和allowReload才能调用相应的事件 + // 单文件上传需要设置allowDelete = true才能替换原有的文件 + fileItem.allowDelete = true; + fileItem.allowReload = true; + }, + onBeforeSendItems: (fileItems: Array): void => { + for (let i: number = 0; i < fileItems.length; i++) { + fileItems[i].formData = { + data: `data${i}` + }; + } + }, + onProgressItems: (fileItems: Array, progress: number): void => { + console.log(`onProgressItems`); + console.log('onProgressItems.fileItems:', fileItems); + console.log('onProgressItems.progress:', progress); + }, + onCompleteItems: (fileItems: Array, response: string, status: number): void => { + console.log('onCompleteItems'); + console.log('onCompleteItems.fileItems:', fileItems); + console.log(`onCompleteItems.response:${response}`); + console.log(`onCompleteItems.status:${status}`); + }, + onSuccessItems: (fileItems: Array, response: string, status: number): void => { + // 根据状态码和返回消息设置详情信息 + console.log('onSuccessItems'); + console.log('onSuccessItems.fileItems:', fileItems); + console.log(`onSuccessItems.response:${response}`); + console.log(`onSuccessItems.status:${status}`); + }, + onErrorItems: (fileItems: Array, response: string, status: number): void => { + // 根据状态码和返回消息设置详情信息 + console.log('onErrorItems'); + console.log('onErrorItems.fileItems:', fileItems); + console.log(`onErrorItems.response:${response}`); + console.log(`onErrorItems.status:${status}`); + }, + onCancelItems: (fileItems: Array, response: string, status: number): void => { + console.log('onCancelItems'); + console.log('onCancelItems.fileItems:', fileItems); + console.log(`onCancelItems.response:${response}`); + console.log(`onCancelItems.status:${status}`); + }, + onRemoveItems: (fileItems: Array): void => { + console.log('onRemoveItems'); + console.log('onRemoveItems.fileItems:', fileItems); + }, + onCompleteAllItems: (fileItems: Array): void => { + console.log('onCompleteAllItems'); + console.log('onCompleteAllItems.fileItems', fileItems); + } + }; + this.uploader = uploaderService.create(config); + } +} diff --git a/src/upload/demo/src/app/upload/UploadServiceTestComponent.ts b/src/upload/demo/src/app/upload/UploadServiceTestComponent.ts new file mode 100644 index 0000000..583799c --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadServiceTestComponent.ts @@ -0,0 +1,89 @@ +import { Component } from '@angular/core'; +import { TiFileInfo, TiFileItem, TiUploadConfig, TiUploadRef, TiUploadService } from '@opentiny/ng'; + +@Component({ + templateUrl: './upload-service-test.html' +}) +export class UploadServiceTestComponent { + uploader: TiUploadRef; + uploader1: TiUploadRef; + constructor(uploaderService: TiUploadService) { + const config: TiUploadConfig = { + url: '/upload', + alias: 'myFileDdd', + headers: { + 'header-tt': 'aa' + }, + filters: [ + { + name: 'maxCount', + params: [5] + } + ], + onAddItemFailed: (fileObject: TiFileInfo, validResults: Array): void => { + console.log('onAddItemFailed'); + console.log(fileObject); + console.log(validResults); + }, + onAddItemSuccess: (fileItem: TiFileItem): void => { + console.log(`filename:${fileItem.file.name}`); + }, + onBeforeSendItems: (fileItems: Array): void => { + for (let i: number = 0; i < fileItems.length; i++) { + fileItems[i].formData = { + data: `data${i}` + }; + } + }, + onProgressItems: (fileItems: Array, progress: number): void => { + console.log(`onProgressItems`); + console.log(fileItems); + console.log(progress); + }, + onCompleteItems: (fileItems: Array, response: string, status: number): void => { + // 根据状态码和返回消息设置详情信息 + console.log('onCompleteItems'); + console.log(fileItems); + console.log(`response:${response}`); + console.log(`status:${status}`); + }, + onSuccessItems: (fileItems: Array, response: string, status: number): void => { + // 根据状态码和返回消息设置详情信息 + console.log('onSuccessItems'); + console.log(fileItems); + console.log(`response:${response}`); + console.log(`status:${status}`); + }, + onErrorItems: (fileItems: Array, response: string, status: number): void => { + // 根据状态码和返回消息设置详情信息 + console.log('onErrorItems'); + console.log(fileItems); + console.log(`response:${response}`); + console.log(`status:${status}`); + }, + onCancelItems: (fileItems: Array, response: string, status: number): void => { + console.log('onCancelItems'); + console.log(fileItems); + console.log(`response:${response}`); + console.log(`status:${status}`); + }, + onRemoveItems: (fileItems: Array): void => { + console.log('onRemoveItems'); + console.log(fileItems); + }, + onCompleteAllItems: (fileItems: Array): void => { + console.log('onCompleteAllItems'); + console.log(fileItems); + } + }; + this.uploader = uploaderService.create({ + ...config, + autoUpload: true + }); + this.uploader1 = uploaderService.create({ + ...config, + batchSend: true, + autoUpload: false + }); + } +} diff --git a/src/upload/demo/src/app/upload/UploadSingleComponent.ts b/src/upload/demo/src/app/upload/UploadSingleComponent.ts new file mode 100644 index 0000000..932210e --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadSingleComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { TiFilter } from '@opentiny/ng'; + +@Component({ + templateUrl: `./upload-single.html` +}) +export class UploadSingleComponent { + url: string = '/upload'; + placeholder: string = '单文件上传'; + // maxCount定义为1时,代表单文件上传 + filters: Array = [ + { + name: 'maxCount', + params: [1] + } + ]; +} diff --git a/src/upload/demo/src/app/upload/UploadTestModule.ts b/src/upload/demo/src/app/upload/UploadTestModule.ts new file mode 100644 index 0000000..abd6bad --- /dev/null +++ b/src/upload/demo/src/app/upload/UploadTestModule.ts @@ -0,0 +1,132 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiSwitchModule, TiTextModule, TiUploadModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { UploadBasicComponent } from './UploadBasicComponent'; +import { UploadButtonComponent } from './UploadButtonComponent'; +import { UploadPropsComponent } from './UploadPropsComponent'; +import { UploadAutoUploadComponent } from './UploadAutoUploadComponent'; +import { UploadBatchSendComponent } from './UploadBatchSendComponent'; +import { UploadEventComponent } from './UploadEventComponent'; +import { UploadFilterComponent } from './UploadFilterComponent'; +import { UploadFormDataComponent } from './UploadFormDataComponent'; +import { UploadServiceComponent } from './UploadServiceComponent'; +import { UploadSingleComponent } from './UploadSingleComponent'; +import { UploadServiceTestComponent } from './UploadServiceTestComponent'; +import { UploadInputFieldTestComponent } from './UploadInputFieldTestComponent'; +import { UploadButtonTestComponent } from './UploadButtonTestComponent'; +import { UploadCaseTestComponent } from './UploadCaseTestComponent'; +import { UploadBeforeremoveComponent } from './UploadBeforeremoveComponent'; +import { UploadChangesComponent } from './UploadChangesComponent'; +import { UploadCustomComponent } from './UploadCustomComponent'; +import { UploadInitfilesTestComponent } from './UploadInitfilesTestComponent'; +import { UploadChunksizeComponent } from './UploadChunksizeComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiTextModule, + TiButtonModule, + TiUploadModule, + TiSwitchModule, + DemoLogModule, + RouterModule.forChild(UploadTestModule.ROUTES) + ], + declarations: [ + UploadBasicComponent, + UploadButtonComponent, + UploadPropsComponent, + UploadServiceComponent, + UploadAutoUploadComponent, + UploadFormDataComponent, + UploadFilterComponent, + UploadSingleComponent, + UploadBatchSendComponent, + UploadEventComponent, + UploadServiceTestComponent, + UploadInputFieldTestComponent, + UploadButtonTestComponent, + UploadBeforeremoveComponent, + UploadChangesComponent, + UploadCaseTestComponent, + UploadCustomComponent, + UploadInitfilesTestComponent, + UploadChunksizeComponent + ] +}) +export class UploadTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiUploadComponent.html', label: 'Upload' }, + { href: 'directives/TiFileSelectDirective.html', label: 'FileSelect' } + ]; + static readonly ROUTES: Routes = [ + { + path: 'upload/upload-basic', + component: UploadBasicComponent + }, + { + path: 'upload/upload-button', + component: UploadButtonComponent + }, + { + path: 'upload/upload-props', + component: UploadPropsComponent + }, + { + path: 'upload/upload-service', + component: UploadServiceComponent + }, + { + path: 'upload/upload-auto-upload', + component: UploadAutoUploadComponent + }, + { + path: 'upload/upload-form-data', + component: UploadFormDataComponent + }, + { + path: 'upload/upload-filter', + component: UploadFilterComponent + }, + { + path: 'upload/upload-single', + component: UploadSingleComponent + }, + { + path: 'upload/upload-batch-send', + component: UploadBatchSendComponent + }, + { + path: 'upload/upload-beforeRemove', + component: UploadBeforeremoveComponent + }, + { + path: 'upload/upload-event', + component: UploadEventComponent + }, + { + path: 'upload/upload-changes', + component: UploadChangesComponent + }, + { path: 'upload/upload-chunksize', component: UploadChunksizeComponent }, + { + path: 'upload/uploadupload-service-test', + component: UploadServiceTestComponent + }, + { + path: 'upload/upload-input-field-test', + component: UploadInputFieldTestComponent + }, + { path: 'upload/upload-button-test', component: UploadButtonTestComponent }, + { path: 'upload/upload-case-test', component: UploadCaseTestComponent }, + { + path: 'upload/upload-initfiles-test', + component: UploadInitfilesTestComponent + } + ]; +} diff --git a/src/upload/demo/src/app/upload/upload-auto-upload.html b/src/upload/demo/src/app/upload/upload-auto-upload.html new file mode 100644 index 0000000..d0a86af --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-auto-upload.html @@ -0,0 +1,2 @@ + + diff --git a/src/upload/demo/src/app/upload/upload-basic.html b/src/upload/demo/src/app/upload/upload-basic.html new file mode 100644 index 0000000..2266669 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-basic.html @@ -0,0 +1 @@ + diff --git a/src/upload/demo/src/app/upload/upload-batch-send.html b/src/upload/demo/src/app/upload/upload-batch-send.html new file mode 100644 index 0000000..94f2c3e --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-batch-send.html @@ -0,0 +1 @@ + diff --git a/src/upload/demo/src/app/upload/upload-beforeremove.html b/src/upload/demo/src/app/upload/upload-beforeremove.html new file mode 100644 index 0000000..f129263 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-beforeremove.html @@ -0,0 +1,5 @@ +

    描述

    +

    通过beforeRemoveItems定义用户点击删除按钮,文件删除前事件

    +

    示例

    +

    点击删除按钮,延时1秒后文件删除

    + diff --git a/src/upload/demo/src/app/upload/upload-button-test.html b/src/upload/demo/src/app/upload/upload-button-test.html new file mode 100644 index 0000000..82c8aa1 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-button-test.html @@ -0,0 +1,78 @@ +

    基本功能+灰化设置

    + + + +

    合并上传测试

    + + + +

    非自动上传,不带filter测试

    + + + +

    第三个文件上传的详情显示

    +
    +

    {{item.file.name}}

    +

    {{item.progress}}

    +

    item isReady:{{item.isReady}}

    +

    item isUploading:{{item.isUploading}}

    +

    item isUploaded:{{item.isUploaded}}

    +

    item isCancel:{{item.isCancel}}

    +

    item isSuccess:{{item.isSuccess}}

    +

    item isError:{{item.isError}}

    + 重新上传 + 取消 + 删除 +
    diff --git a/src/upload/demo/src/app/upload/upload-button.html b/src/upload/demo/src/app/upload/upload-button.html new file mode 100644 index 0000000..de9c2d9 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-button.html @@ -0,0 +1,3 @@ + +
    + diff --git a/src/upload/demo/src/app/upload/upload-case-test.html b/src/upload/demo/src/app/upload/upload-case-test.html new file mode 100644 index 0000000..f34cb0f --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-case-test.html @@ -0,0 +1,5 @@ +

    描述

    +

    开发者在使用时组件有问题的业务场景,供测试使用。

    +

    场景1:

    +

    合并上传时,在beforeSendItems中取消全部上传:

    + diff --git a/src/upload/demo/src/app/upload/upload-changes.html b/src/upload/demo/src/app/upload/upload-changes.html new file mode 100644 index 0000000..1c3ec8a --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-changes.html @@ -0,0 +1,46 @@ +

    描述

    +

    文件上传组件 - 动态变更展示

    +

    动态变更支持url/headers

    +

    导入

    +

    import {{ '{' }} TiUploadModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    + +

    事件日志:

    +
    +
    +

    + {{i+' '+log}} +

    +
    +
    +
    + +

    1、默认按钮样式

    +
    +
    + + +
    +
    +

    3、inputField样式

    + + +
    +

    3、操作

    +
    +url = '{{url}}'' +
    +headers = '{{headers | json}}' diff --git a/src/upload/demo/src/app/upload/upload-chunksize.html b/src/upload/demo/src/app/upload/upload-chunksize.html new file mode 100644 index 0000000..e74cef5 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-chunksize.html @@ -0,0 +1,17 @@ + + diff --git a/src/upload/demo/src/app/upload/upload-custom.html b/src/upload/demo/src/app/upload/upload-custom.html new file mode 100644 index 0000000..028dc22 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-custom.html @@ -0,0 +1,11 @@ + + diff --git a/src/upload/demo/src/app/upload/upload-event.html b/src/upload/demo/src/app/upload/upload-event.html new file mode 100644 index 0000000..d2a58a9 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-event.html @@ -0,0 +1,16 @@ + + diff --git a/src/upload/demo/src/app/upload/upload-filter.html b/src/upload/demo/src/app/upload/upload-filter.html new file mode 100644 index 0000000..1760284 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-filter.html @@ -0,0 +1,8 @@ + + diff --git a/src/upload/demo/src/app/upload/upload-form-data.html b/src/upload/demo/src/app/upload/upload-form-data.html new file mode 100644 index 0000000..8b1a700 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-form-data.html @@ -0,0 +1,9 @@ +

    描述

    +

    + 文件上传附加信息定义使用示例,可通过两种方式添加附加信息:
    + 1.静态信息:可通过定义formData属性方式添加
    + 2.动态信息:可通过在beforeSendItems事件回调中动态定义item的formData属性方式添加(本示例中展示的是该种用法) +

    +

    示例

    + + diff --git a/src/upload/demo/src/app/upload/upload-initfiles-test.html b/src/upload/demo/src/app/upload/upload-initfiles-test.html new file mode 100644 index 0000000..cb806ad --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-initfiles-test.html @@ -0,0 +1,9 @@ +

    描述

    +

    同时设置 initFiles 和 maxCount 时,供测试使用。

    +

    场景1:

    +

    直接删除 initFiles 中文件:

    + +

    场景2:

    +

    调用 remove 方法删除 initFiles 中文件:

    + + diff --git a/src/upload/demo/src/app/upload/upload-input-field-test.html b/src/upload/demo/src/app/upload/upload-input-field-test.html new file mode 100644 index 0000000..1529ca4 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-input-field-test.html @@ -0,0 +1,93 @@ +

    基本功能+灰化设置

    + + + +

    合并上传测试

    + + +

    单文件上传测试

    + + +

    非自动上传,不带filter测试

    + + + +

    第三个文件上传的详情显示

    +
    +

    {{item.file.name}}

    +

    {{item.progress}}

    +

    item isReady:{{item.isReady}}

    +

    item isUploading:{{item.isUploading}}

    +

    item isUploaded:{{item.isUploaded}}

    +

    item isCancel:{{item.isCancel}}

    +

    item isSuccess:{{item.isSuccess}}

    +

    item isError:{{item.isError}}

    + 重新上传 + 取消 + 删除 +
    diff --git a/src/upload/demo/src/app/upload/upload-props.html b/src/upload/demo/src/app/upload/upload-props.html new file mode 100644 index 0000000..32fe72a --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-props.html @@ -0,0 +1,18 @@ + + +
    + + + diff --git a/src/upload/demo/src/app/upload/upload-service-test.html b/src/upload/demo/src/app/upload/upload-service-test.html new file mode 100644 index 0000000..fd3ddcb --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-service-test.html @@ -0,0 +1,39 @@ +

    描述

    +

    + 文件上传服务使用示例,适用于自定义上传样式场景:
    + 通过tiFileSelect指令定义上传实例对象;通过上传实例的queue属性读取上传文件信息及调用方法 +

    +

    示例

    +

    文件逐个上传,自动上传

    + +
    + {{item.file.name}} + {{item.progress}} + item isReady:{{item.isReady}} + item isUploading:{{item.isUploading}} + item isUploaded:{{item.isUploaded}} + item isCancel:{{item.isCancel}} + item isSuccess:{{item.isSuccess}} + item isError:{{item.isError}} + 重新上传 + 取消 + 删除 +
    + +

    文件合并上传,手动上传

    + + + +
    + {{item.file.name}} + {{item.progress}} + item isReady:{{item.isReady}} + item isUploading:{{item.isUploading}} + item isUploaded:{{item.isUploaded}} + item isCancel:{{item.isCancel}} + item isSuccess:{{item.isSuccess}} + item isError:{{item.isError}} + 重新上传 + 取消 + 删除 +
    diff --git a/src/upload/demo/src/app/upload/upload-service.html b/src/upload/demo/src/app/upload/upload-service.html new file mode 100644 index 0000000..1ee6b43 --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-service.html @@ -0,0 +1,37 @@ + +
    + {{item.file.name}} + -- + {{item.progress}} + -- + item isReady:{{item.isReady}} + -- + item isUploading:{{item.isUploading}} + -- + item isUploaded:{{item.isUploaded}} + -- + item isCancel:{{item.isCancel}} + -- + item isSuccess:{{item.isSuccess}} + -- + item isError:{{item.isError}} + -- + 重新上传 + -- + 取消 + -- + 删除 + -- +
    + diff --git a/src/upload/demo/src/app/upload/upload-single.html b/src/upload/demo/src/app/upload/upload-single.html new file mode 100644 index 0000000..0b16faa --- /dev/null +++ b/src/upload/demo/src/app/upload/upload-single.html @@ -0,0 +1,4 @@ +

    描述

    +

    单文件上传使用示例: 定义filters过滤条件maxCount为1时,代表单文件上传

    +

    示例

    + diff --git a/src/upload/demo/src/app/upload/webdoc/upload-demos.js b/src/upload/demo/src/app/upload/webdoc/upload-demos.js new file mode 100644 index 0000000..469b3b1 --- /dev/null +++ b/src/upload/demo/src/app/upload/webdoc/upload-demos.js @@ -0,0 +1,157 @@ +export default { + column: '2', + demos: [ + { + demoId: 'upload-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    Upload 组件的最简用法。通过属性url配置上传地址。

    ', + 'en-US': '

    ', + }, + apis: ['TiUploadComponent.properties.url'], + }, + { + demoId: 'upload-button', + name: { + 'zh-CN': '按钮类型', + 'en-US': 'Button Type', + }, + desc: { + 'zh-CN': + '

    通过属性type配置上传按钮的类型,包括buttontextButtoninputField,通过属性buttonText配置按钮文字。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadComponent.properties.type', + 'TiUploadComponent.properties.buttonText', + ], + }, + { + demoId: 'upload-props', + name: { + 'zh-CN': '常用属性', + 'en-US': 'Used props', + }, + desc: { + 'zh-CN': + '

    通过属性note配置 note 信息;通过属性errorMessage配置文件上传失败的提示信息;通过属性listMaxHeight配置文件列表区域最大高度;通过属性method配置请求方式,包括getpost;通过属性disabled配置禁用状态;通过属性showUploadList配置是否显示文件列表,注意:不会隐藏初始文件;通过属性showErrorMessage配置是否在文件上传失败时显示提示信息;通过属性initFiles配置初始文件列表;

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadComponent.properties.note', + 'TiUploadComponent.properties.errorMessage', + 'TiUploadComponent.properties.listMaxHeight', + 'TiUploadComponent.properties.disabled', + 'TiUploadComponent.properties.showUploadList', + 'TiUploadComponent.properties.showErrorMessage', + 'TiUploadComponent.properties.initFiles', + ], + }, + { + demoId: 'upload-custom', + name: { + 'zh-CN': '自定义上传信息', + 'en-US': 'Used props', + }, + desc: { + 'zh-CN': + '

    通过属性method配置请求方式,包括getpost;通过属性headers配置上传时的头信息;通过属性alias配置上传文件字段唯一标识的键值;通过属性formData配置文件上传的附带信息;通过属性formDataFirst配置附带信息是否先于上传文件对象。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadComponent.properties.method', + 'TiUploadComponent.properties.alias', + 'TiUploadComponent.properties.headers', + 'TiUploadComponent.properties.formData', + 'TiUploadComponent.properties.formDataFirst', + ], + }, + { + demoId: 'upload-filter', + name: { + 'zh-CN': '条件过滤', + 'en-US': 'Filter', + }, + desc: { + 'zh-CN': + '

    通过属性filter配置文件过滤条件,通过事件addItemFailed处理不符合条件的文件,通过事件addItemSuccess处理符合条件的文件。

    ', + 'en-US': '

    ', + }, + apis: ['TiUploadComponent.properties.filters'], + }, + { + demoId: 'upload-auto-upload', + name: { + 'zh-CN': '手动上传', + 'en-US': 'Manual Upload', + }, + desc: { + 'zh-CN': + '

    通过属性autoUpload定义是否自动上传;通过上传实例的upload方法触发上传。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadComponent.properties.autoUpload', + 'TiUploadComponent.methods.upload', + ], + }, + { + demoId: 'upload-batch-send', + name: { + 'zh-CN': '合并上传', + 'en-US': 'Batch', + }, + desc: { + 'zh-CN': + '

    通过属性batchSend配置是否将所有文件上传任务合并为一次请求,默认状态会单独依次处理每个文件上传任务。仅适用于手动上传场景。

    ', + 'en-US': '

    ', + }, + apis: ['TiUploadComponent.properties.batchSend'], + }, + { + demoId: 'upload-event', + name: { + 'zh-CN': '常用事件', + 'en-US': 'Used events', + }, + desc: { + 'zh-CN': + '

    通过事件addItemFailed配置添加文件失败事件回调;通过事件addItemSuccess配置添加文件成功事件回调;通过事件beforeSendItems配置发送文件前置事件回调,常用于上传前添加表单内容;通过事件progressItems配置发送文件过程事件回调;通过事件successItems配置发送文件成功事件回调;通过事件errorItems配置发送文件失败事件回调;通过事件cancelItems配置发送文件取消事件回调;通过事件removeItems配置删除文件事件回调;completeItems事件在文件上传后,不论成功或失败都必定被调用;completeAllItems事件在所有文件上传后,不论成功或失败都必定被调用。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadComponent.events.addItemFailed', + 'TiUploadComponent.events.addItemSuccess', + 'TiUploadComponent.events.beforeSendItems', + 'TiUploadComponent.events.progressItems', + 'TiUploadComponent.events.successItems', + 'TiUploadComponent.events.errorItems', + 'TiUploadComponent.events.cancelItems', + 'TiUploadComponent.events.beforeRemoveItems', + 'TiUploadComponent.events.removeItems', + 'TiUploadComponent.events.completeAllItems', + 'TiUploadComponent.events.completeItems', + ], + }, + { + demoId: 'upload-service', + name: { + 'zh-CN': '指令', + 'en-US': 'Directives', + }, + desc: { + 'zh-CN': + '

    通过指令tiFileSelect定义上传实例对象;通过上传实例的queue属性访问文件信息及方法;通过属性accept配置文件类型,使用逗号分隔。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiFileSelectDirective.properties.tiFileSelect', + 'TiUploadComponent.properties.accept', + ], + }, + ], +}; diff --git a/src/upload/demo/src/app/upload/webdoc/upload.cn.md b/src/upload/demo/src/app/upload/webdoc/upload.cn.md new file mode 100644 index 0000000..7be5d6a --- /dev/null +++ b/src/upload/demo/src/app/upload/webdoc/upload.cn.md @@ -0,0 +1,22 @@ +--- +title: Upload 文件上传 +--- +# Upload 文件上传 + +
    + +upload 是用于将本地文件上传至服务器的组件。 + +```typescript +import { TiUploadModule } from '@opentiny/ng'; +``` +
    + +
    + +upload 是用于将本地的图片或文件上传至服务器的组件。 + +```typescript +import { TiUploadModule } from '@opentiny/ng'; +``` +
    diff --git a/src/upload/demo/src/app/upload/webdoc/upload.en.md b/src/upload/demo/src/app/upload/webdoc/upload.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/upload/demo/src/app/upload/webdoc/upload.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/upload/demo/src/app/uploadimage/UploadimageAutoUploadComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageAutoUploadComponent.ts new file mode 100644 index 0000000..ce73d07 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageAutoUploadComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-auto-upload.html` +}) +export class UploadimageAutoUploadComponent {} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageBasicComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageBasicComponent.ts new file mode 100644 index 0000000..ebe4637 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageBasicComponent.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-basic.html` +}) +export class UploadimageBasicComponent {} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageChangesComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageChangesComponent.ts new file mode 100644 index 0000000..9962598 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageChangesComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-changes.html` +}) +export class UploadimageChangesComponent { + myLogs: Array = []; + customHeaders: any = { + tiheadersConfig: 'TinyNG' + }; + customFormData: any = { + tiCustomFormData: 'hello tiny' + }; + changeHeaders(): void { + this.customHeaders.tiCustomHeader = 'custom header'; + this.myLogs = [...this.myLogs, `添加头信息:${JSON.stringify(this.customHeaders)}`]; + } + beforeSendItems($event: Event): void { + this.myLogs = [ + ...this.myLogs, + `请求方式:${$event[0].method}; 文件名:'${$event[0].alias}'; 头信息:${JSON.stringify( + $event[0].headers + )}; 附加信息:${JSON.stringify($event[0].formData)}` + ]; + } +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageDeletableComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageDeletableComponent.ts new file mode 100644 index 0000000..be165e5 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageDeletableComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-deletable.html` +}) +export class UploadimageDeletableComponent { + deletable: boolean = false; +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageDisabledComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageDisabledComponent.ts new file mode 100644 index 0000000..73b86f2 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageDisabledComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-disabled.html` +}) +export class UploadimageDisabledComponent { + disabled: boolean = true; +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageDragComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageDragComponent.ts new file mode 100644 index 0000000..2996e12 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageDragComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-drag.html` +}) +export class UploadimageDragComponent { + url: string = '/upload'; +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageEventComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageEventComponent.ts new file mode 100644 index 0000000..d2c8663 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageEventComponent.ts @@ -0,0 +1,47 @@ +import { Component } from '@angular/core'; +import { TiFileItem } from '@opentiny/ng'; + +@Component({ + templateUrl: `./uploadimage-event.html` +}) +export class UploadimageEventComponent { + myLogs: Array = []; + onAddItemSuccess(fileItem: TiFileItem): void { + this.myLogs = [...this.myLogs, `onAddItemSuccess() filename:${fileItem.file.name}`]; + } + onAddItemFailed(fileItem: TiFileItem): void { + this.myLogs = [...this.myLogs, `onAddItemFailed() filename:${fileItem.file.name}`]; + } + onBeforeSendItems(fileItems: Array): void { + this.myLogs = [...this.myLogs, `onBeforeSendItems()`]; + fileItems.forEach((item: TiFileItem) => { + item.formData = { + tinyFormdata: 'Custom data' + }; + }); + } + onProgressItems($event: any): void { + this.myLogs = [...this.myLogs, `onProgressItems() progress:${$event.progress}`]; + } + onSuccessItems($event: any): void { + this.myLogs = [...this.myLogs, `onSuccessItems() response:${$event.response} status:${$event.status}`]; + } + onErrorItems($event: any): void { + this.myLogs = [...this.myLogs, `onErrorItems() response:${$event.response} status:${$event.status}`]; + } + onCancelItems($event: any): void { + this.myLogs = [...this.myLogs, `onCancelItems() response:${$event.response} status:${$event.status}`]; + } + onRemoveItems($event: any): void { + this.myLogs = [...this.myLogs, `onRemoveItems()`]; + } + onBeforeRemoveItems(fileItemArry: Array): void { + this.myLogs = [...this.myLogs, `onBeforeRemoveItems()`]; + } + onCompleteItems($event: any): void { + this.myLogs = [...this.myLogs, `onCompleteItems() response:${$event.response}`]; + } + onCompleteAllItems($event: any): void { + this.myLogs = [...this.myLogs, `onCompleteItems()`]; + } +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageFilterComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageFilterComponent.ts new file mode 100644 index 0000000..6643189 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageFilterComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiFilter } from '@opentiny/ng'; + +@Component({ + templateUrl: `./uploadimage-filter.html` +}) +export class UploadimageFilterComponent { + filters: Array = [ + { + name: 'type', + params: ['.png,.img'] + }, + { + name: 'maxSize', + params: [102400] + }, + { + name: 'maxCount', + params: [2] + } + ]; + myLogs: Array = []; + onAddItemFailed(event: any): void { + this.myLogs = [...this.myLogs, `errorType:${event.validResults.toString()}----filename:${event.file.name}`]; + } + onAddItemSuccess(event: any): void { + this.myLogs = [...this.myLogs, `success----filename:${event.file.name}`]; + } +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageInitfilesComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageInitfilesComponent.ts new file mode 100644 index 0000000..8d418e2 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageInitfilesComponent.ts @@ -0,0 +1,25 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { TiUploadimageInitFile } from '@opentiny/ng'; + +@Component({ + templateUrl: `./uploadimage-initfiles.html`, + styles: ['.preview-modal-class { width: 400px !important; }'], + encapsulation: ViewEncapsulation.None // 要想设置的样式生效,此处必须配置成 ViewEncapsulation.None +}) +export class UploadimageInitfilesComponent { + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL']; + initFiles: Array = [ + { + name: 'first.jpg', + previewUrl: `${this.baseUrl}assets/image/1.jpg` + }, + { + name: 'second.jpg', + previewUrl: `${this.baseUrl}assets/image/2.jpg` + }, + { + name: 'third.jpg', + previewUrl: `${this.baseUrl}assets/image/3.jpg` + } + ]; +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageMaxcountComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageMaxcountComponent.ts new file mode 100644 index 0000000..195ae95 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageMaxcountComponent.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-maxcount.html` +}) +export class UploadimageMaxcountComponent { + myLogs: Array = []; + uploadLimit(event: any): void { + this.myLogs = [...this.myLogs, `图片数量已达上限。`]; + } +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageTemplateComponent.ts b/src/upload/demo/src/app/uploadimage/UploadimageTemplateComponent.ts new file mode 100644 index 0000000..a6af919 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageTemplateComponent.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: `./uploadimage-template.html` +}) +export class UploadimageTemplateComponent { + url: string = '/upload'; +} diff --git a/src/upload/demo/src/app/uploadimage/UploadimageTestModule.ts b/src/upload/demo/src/app/uploadimage/UploadimageTestModule.ts new file mode 100644 index 0000000..1fede57 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/UploadimageTestModule.ts @@ -0,0 +1,91 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiAlertModule, TiButtonModule, TiIconModule, TiTipModule, TiUploadModule } from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { UploadimageBasicComponent } from './UploadimageBasicComponent'; +import { UploadimageAutoUploadComponent } from './UploadimageAutoUploadComponent'; +import { UploadimageMaxcountComponent } from './UploadimageMaxcountComponent'; +import { UploadimageDeletableComponent } from './UploadimageDeletableComponent'; +import { UploadimageEventComponent } from './UploadimageEventComponent'; +import { UploadimageDragComponent } from './UploadimageDragComponent'; +import { UploadimageFilterComponent } from './UploadimageFilterComponent'; +import { UploadimageDisabledComponent } from './UploadimageDisabledComponent'; +import { UploadimageTemplateComponent } from './UploadimageTemplateComponent'; +import { UploadimageInitfilesComponent } from './UploadimageInitfilesComponent'; +import { UploadimageChangesComponent } from './UploadimageChangesComponent'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TiTipModule, + TiIconModule, + TiUploadModule, + TiAlertModule, + TiButtonModule, + DemoLogModule, + RouterModule.forChild(UploadimageTestModule.ROUTES) + ], + declarations: [ + UploadimageBasicComponent, + UploadimageAutoUploadComponent, + UploadimageMaxcountComponent, + UploadimageDeletableComponent, + UploadimageEventComponent, + UploadimageDragComponent, + UploadimageFilterComponent, + UploadimageDisabledComponent, + UploadimageTemplateComponent, + UploadimageInitfilesComponent, + UploadimageChangesComponent + ] +}) +export class UploadimageTestModule { + static readonly LINKS: Array = [{ href: 'components/TiUploadimageComponent.html', label: 'Uploadimage' }]; + static readonly ROUTES: Routes = [ + { + path: 'uploadimage/uploadimage-basic', + component: UploadimageBasicComponent + }, + { + path: 'uploadimage/uploadimage-filter', + component: UploadimageFilterComponent + }, + { + path: 'uploadimage/uploadimage-maxcount', + component: UploadimageMaxcountComponent + }, + { + path: 'uploadimage/uploadimage-deletable', + component: UploadimageDeletableComponent + }, + { + path: 'uploadimage/uploadimage-event', + component: UploadimageEventComponent + }, + { + path: 'uploadimage/uploadimage-initfiles', + component: UploadimageInitfilesComponent + }, + { + path: 'uploadimage/uploadimage-changes', + component: UploadimageChangesComponent + }, + { + path: 'uploadimage/uploadimage-drag', + component: UploadimageDragComponent + }, + { + path: 'uploadimage/uploadimage-disabled', + component: UploadimageDisabledComponent + }, + { + path: 'uploadimage/uploadimage-template', + component: UploadimageTemplateComponent + } + ]; +} diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-auto-upload.html b/src/upload/demo/src/app/uploadimage/uploadimage-auto-upload.html new file mode 100644 index 0000000..0d35e60 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-auto-upload.html @@ -0,0 +1,3 @@ + +
    + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-basic.html b/src/upload/demo/src/app/uploadimage/uploadimage-basic.html new file mode 100644 index 0000000..eb571c2 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-basic.html @@ -0,0 +1 @@ + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-changes.html b/src/upload/demo/src/app/uploadimage/uploadimage-changes.html new file mode 100644 index 0000000..464748b --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-changes.html @@ -0,0 +1,15 @@ + + +
    + + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-deletable.html b/src/upload/demo/src/app/uploadimage/uploadimage-deletable.html new file mode 100644 index 0000000..b3d6706 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-deletable.html @@ -0,0 +1 @@ + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-disabled.html b/src/upload/demo/src/app/uploadimage/uploadimage-disabled.html new file mode 100644 index 0000000..939995d --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-disabled.html @@ -0,0 +1 @@ + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-drag.html b/src/upload/demo/src/app/uploadimage/uploadimage-drag.html new file mode 100644 index 0000000..5fd3578 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-drag.html @@ -0,0 +1,16 @@ +

    描述

    +

    + 10.1.13 + 版本开始对外隐藏这个示例,与谭莉沟通后发现drag类型和block类型没有什么太大区别,而且没有对应的实际应用场景,所以先隐藏,后续看情况在决定是否公开次此示例。
    +
    + 设置type接口为drag。与type为block的区别在于以下两点:
    + 1、drag类型下,没有提供上传结果展示列表,服务可以自定义;
    + 2、drag类型下,上传区域宽高可以自定义,不过这个没有对外展示。
    +
    + 默认为自动上传,如需手动上传,可设置autoUpload为false,并需要自定义上传按钮,调用组件的upload方法;
    + 手动上传时,可以通过addItemSuccess事件获取添加成功的文件,通过addItemFailed事件获取添加失败的文件。
    +

    +

    示例

    + + + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-event.html b/src/upload/demo/src/app/uploadimage/uploadimage-event.html new file mode 100644 index 0000000..8ae20e1 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-event.html @@ -0,0 +1,17 @@ + + + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-filter.html b/src/upload/demo/src/app/uploadimage/uploadimage-filter.html new file mode 100644 index 0000000..0530b14 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-filter.html @@ -0,0 +1,8 @@ + + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-initfiles.html b/src/upload/demo/src/app/uploadimage/uploadimage-initfiles.html new file mode 100644 index 0000000..99dd1db --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-initfiles.html @@ -0,0 +1 @@ + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-maxcount.html b/src/upload/demo/src/app/uploadimage/uploadimage-maxcount.html new file mode 100644 index 0000000..6110def --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-maxcount.html @@ -0,0 +1,2 @@ + + diff --git a/src/upload/demo/src/app/uploadimage/uploadimage-template.html b/src/upload/demo/src/app/uploadimage/uploadimage-template.html new file mode 100644 index 0000000..b40dc1f --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimage-template.html @@ -0,0 +1,8 @@ +

    描述

    +

    自定义样式:

    +

    示例

    + + +

    添加文件

    +
    +
    diff --git a/src/upload/demo/src/app/uploadimage/uploadimagetest.less b/src/upload/demo/src/app/uploadimage/uploadimagetest.less new file mode 100644 index 0000000..0145d45 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/uploadimagetest.less @@ -0,0 +1,5 @@ +.custom-alert-style { + position: absolute; + top: 0px; + right: 20px; +} diff --git a/src/upload/demo/src/app/uploadimage/webdoc/uploadimage-demos.js b/src/upload/demo/src/app/uploadimage/webdoc/uploadimage-demos.js new file mode 100644 index 0000000..8f3d7d3 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/webdoc/uploadimage-demos.js @@ -0,0 +1,143 @@ +export default { + column: '2', + demos: [ + { + demoId: 'uploadimage-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    Uploadimage 组件的最简用法。通过属性url配置上传地址。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadimageComponent.properties.url', + 'TiUploadimageComponent.properties.type', + ], + }, + { + demoId: 'uploadimage-disabled', + name: { + 'zh-CN': '禁用', + 'en-US': 'Disabled', + }, + desc: { + 'zh-CN': '

    通过属性disabled配置禁用状态。

    ', + 'en-US': '

    ', + }, + apis: ['TiUploadimageComponent.properties.disabled'], + }, + { + demoId: 'uploadimage-maxcount', + name: { + 'zh-CN': '数量限制', + 'en-US': 'Max length', + }, + desc: { + 'zh-CN': + '

    通过属性maxCount配置最大可上传的图片数;当元素失去焦点的时候触发uploadLimit事件。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadimageComponent.properties.maxCount', + 'TiUploadimageComponent.events.uploadLimit', + ], + }, + { + demoId: 'uploadimage-deletable', + name: { + 'zh-CN': '图片可删除', + 'en-US': 'Deletable', + }, + desc: { + 'zh-CN': + '

    通过属性deletable配置已上传的图片是否可以删除。注意:只对上传成功的图片生效。

    ', + 'en-US': '

    ', + }, + apis: ['TiUploadimageComponent.properties.deletable'], + }, + { + demoId: 'uploadimage-initfiles', + name: { + 'zh-CN': '初始图片', + 'en-US': 'Initfiles', + }, + desc: { + 'zh-CN': + '

    通过属性initfiles配置初始图片列表;通过属性modalClass配置图片预览时弹出框样式。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadimageComponent.properties.initFiles', + 'TiUploadimageComponent.properties.modalClass', + ], + }, + { + demoId: 'uploadimage-changes', + name: { + 'zh-CN': '常用属性', + 'en-US': 'Used props', + }, + desc: { + 'zh-CN': + '

    通过属性method配置请求方式,包括getpost;通过属性headers配置请求头信息;通过属性accept配置合法的本地文件类型;通过属性alias配置上传文件字段唯一标识的键值;通过属性formData配置文件上传的附带信息;通过属性formDataFirst配置附带信息是否先于上传文件对象。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadimageComponent.properties.method', + 'TiUploadimageComponent.properties.headers', + 'TiUploadimageComponent.properties.accept', + 'TiUploadimageComponent.properties.alias', + 'TiUploadimageComponent.properties.formData', + 'TiUploadimageComponent.properties.formDataFirst', + ], + }, + { + demoId: 'uploadimage-filter', + name: { + 'zh-CN': '条件过滤', + 'en-US': 'Filter', + }, + desc: { + 'zh-CN': + '

    通过属性filter配置文件过滤条件,通过事件addItemFailed处理不符合条件的文件,通过事件addItemSuccess处理符合条件的文件。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadimageComponent.properties.filters', + 'TiUploadimageComponent.events.addItemFailed', + 'TiUploadimageComponent.events.addItemSuccess', + ], + }, + { + demoId: 'uploadimage-event', + name: { + 'zh-CN': '常用事件', + 'en-US': 'Used events', + }, + desc: { + 'zh-CN': + '

    通过事件addItemFailed配置添加图片失败事件回调;通过事件addItemSuccess配置添加图片成功事件回调;通过事件beforeSendItems配置发送图片前置事件回调,常用于上传前添加表单内容;通过事件progressItems配置发送图片过程事件回调;通过事件successItems配置发送图片成功事件回调;通过事件errorItems配置发送图片失败事件回调;通过事件cancelItems配置发送图片取消事件回调;通过事件removeItems配置删除图片事件回调;completeItems事件在图片上传后,不论成功或失败都必定被调用;completeAllItems事件在所有图片上传后,不论成功或失败都必定被调用。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiUploadimageComponent.events.beforeSendItems', + 'TiUploadimageComponent.events.progressItems', + 'TiUploadimageComponent.events.successItems', + 'TiUploadimageComponent.events.errorItems', + 'TiUploadimageComponent.events.cancelItems', + 'TiUploadimageComponent.events.beforeRemoveItems', + 'TiUploadimageComponent.events.removeItems', + 'TiUploadimageComponent.events.completeAllItems', + 'TiUploadimageComponent.events.completeItems', + ], + }, + ], + ignoreApis: [ + 'TiUploadimageComponent.properties.autoUpload', + 'TiUploadimageComponent.properties.batchSend', + 'TiUploadimageComponent.methods.upload', + ], +}; diff --git a/src/upload/demo/src/app/uploadimage/webdoc/uploadimage.cn.md b/src/upload/demo/src/app/uploadimage/webdoc/uploadimage.cn.md new file mode 100644 index 0000000..5937560 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/webdoc/uploadimage.cn.md @@ -0,0 +1,22 @@ +--- +title: Uploadimage 图片上传 +--- +# Uploadimage 图片上传 + +
    + +Uploadimage 是用于将本地图片上传至服务器的组件。 + +```typescript +import { TiUploadModule } from '@opentiny/ng'; +``` +
    + +
    + +Uploadimage 是用于将本地图片上传至服务器的组件。 + +```typescript +import { TiUploadModule } from '@opentiny/ng'; +``` +
    diff --git a/src/upload/demo/src/app/uploadimage/webdoc/uploadimage.en.md b/src/upload/demo/src/app/uploadimage/webdoc/uploadimage.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/upload/demo/src/app/uploadimage/webdoc/uploadimage.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/upload/demo/src/favicon.ico b/src/upload/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/upload/demo/src/index.html b/src/upload/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/upload/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/upload/demo/src/main.ts b/src/upload/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/upload/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/upload/demo/test.ts b/src/upload/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/upload/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/upload/demo/tsconfig.app.json b/src/upload/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/upload/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/upload/demo/tsconfig.spec.json b/src/upload/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/upload/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/upload/lib/index.ts b/src/upload/lib/index.ts new file mode 100644 index 0000000..c1d2eb8 --- /dev/null +++ b/src/upload/lib/index.ts @@ -0,0 +1,4 @@ +export * from './src/TiUploadModule'; +export * from './src/TiUploadService'; +export * from './src/TiUploadServiceModule'; +export * from './src/TiFileInterface'; diff --git a/src/upload/lib/ng-package.json b/src/upload/lib/ng-package.json new file mode 100644 index 0000000..9c9092e --- /dev/null +++ b/src/upload/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/upload", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/upload/lib/package.json b/src/upload/lib/package.json new file mode 100644 index 0000000..b27c394 --- /dev/null +++ b/src/upload/lib/package.json @@ -0,0 +1,23 @@ +{ + "name": "@opentiny/ng-upload", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@opentiny/ng-overflow": "~1.0.0-beta.0", + "@opentiny/ng-modal": "~1.0.0-beta.0", + "@opentiny/ng-imagepreview": "~1.0.0-beta.0", + "@angular/platform-browser": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-progresspie": "~1.0.0-beta.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-validation": "~1.0.0-beta.0", + "@opentiny/ng-progressbar": "~1.0.0-beta.0", + "@opentiny/ng-button": "~1.0.0-beta.0", + "@opentiny/ng-outline": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/upload/lib/project.json b/src/upload/lib/project.json new file mode 100644 index 0000000..11ddb62 --- /dev/null +++ b/src/upload/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/upload/lib", + "sourceRoot": "src/upload/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/upload"], + "options": { + "project": "src/upload/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/upload"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js upload" + }, + { + "command": "ng default-build upload" + }, + { + "command": "node build/clear-default-theme.js upload" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/upload && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build upload && ng pack upload && node build/publish.js upload --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/upload/lib/src/TiDisabledDirective.ts b/src/upload/lib/src/TiDisabledDirective.ts new file mode 100644 index 0000000..b7cb499 --- /dev/null +++ b/src/upload/lib/src/TiDisabledDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; +/** + * @ignore + */ +@Directive({ + selector: '[tiDisabled]' +}) +export class TiDisabledDirective { + constructor(private hostEle: ElementRef, private renderer: Renderer2) {} + @Input() + set tiDisabled(value: boolean) { + if (value) { + this.renderer.setAttribute(this.hostEle.nativeElement, 'disabled', 'disabled'); + } else { + this.renderer.removeAttribute(this.hostEle.nativeElement, 'disabled'); + } + } +} diff --git a/src/upload/lib/src/TiFileInterface.ts b/src/upload/lib/src/TiFileInterface.ts new file mode 100644 index 0000000..0fe9cd8 --- /dev/null +++ b/src/upload/lib/src/TiFileInterface.ts @@ -0,0 +1,564 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * 单个文件基础信息接口,与浏览器中读取到的文件信息基本一致 + * + * 以下使用会用到该类型: + * + * 1.[TiUploadComponent.addItemFailed]{@link ../components/TiUploadComponent.html#addItemFailed}回调的参数类型 + * + * 2.[TiFileItem.file]{@link TiFileItem#file}的对象类型 + */ +export interface TiFileInfo { + /** + * 文件修改时间 + */ + lastModifiedDate: Date; + /** + * 文件真实大小值,单位为B + */ + size: number; + /** + * 文件大小,此处做了单位转换,方便界面详情显示,该数值根据文件大小单位区间显示,并保留两位小数 + */ + sizeWithUnit: string; // 做单位转换后的文件大小,方便界面详情显示 + /** + * 文件名称 + */ + name: string; + /** + * 文件类型,此处是取的文件后缀名 + */ + type: string; // 确保浏览器形式的一致性 + /** + * @ignore + */ + _file: File; // 文件对象,只在H5方式下有效 + /** + * @ignore + */ + _input: Element; // 文件input对象,只在IE9 form表单提交方式下有效 +} +/** + * 单个文件详细信息接口,包含业务配置的上传信息、上传状态及方法等 + * + * 作为文件上传回调中的参数类型使用,如[TiUploadComponent.completeAllItems]{@link ../components/TiUploadComponent.html#completeAllItems}回调中的参数类型 + */ +export interface TiFileItem { + /** + * 文件是否处于待上传状态(beforeSend前设置) + */ + isReady: boolean; + /** + * 文件是否正在上传 + */ + isUploading: boolean; + /** + * 文件是否已上传 + */ + isUploaded: boolean; + /** + * 文件是否取消上传 + */ + isCancel: boolean; + /** + * 文件是否上传成功 + */ + isSuccess: boolean; + /** + * 文件是否上传错误 + */ + isError: boolean; + /** + * 文件上传进度值 + */ + progress: number; + + /** + * @ignore + */ + isHover: boolean; + /** + * @ignore + * 文件名称是否溢出 + */ + isOverflow: boolean; + /** + * @ignore + */ + _xhr: any; + /** + * 文件在已选文件队列中的次序 + */ + index: number; + /** + * @ignore + * 文件上传地址,为业务配置信息,只可获取 + */ + url: string; // 后台地址 + /** + * 单个文件基础信息,只包含文件名及类型等信息,为业务配置信息,只可获取 + */ + file: TiFileInfo; // 文件详细信息情况 + /** + * @ignore + * 向后台发送请求时对应的文件对象name属性,该name属性是后台读取文件的入口值,为业务配置信息,只可获取 + */ + alias: string; // 文件name + /** + * @ignore + */ + _file: File; + /** + * @ignore + */ + _input: Element; + /** + * @ignore + */ + _form: any; + /** + * 上传文件附带信息,为对象形式,可在beforeSend事件回调中进行单个文件的formData动态设置 + */ + formData: object; // 业务可自定义为对象形式 + /** + * @ignore + */ + formDataFirst: boolean; + /** + * @ignore + */ + batchSend: boolean; + /** + * @ignore + */ + headers: object; // 业务可自定义为对象形式 + /** + * @ignore + */ + method: string; + /** + * 该文件对应的上传实例对象 + */ + uploader: TiUploadRef; // 上传实例对象 + /** + * 是否允许重新上传reload + */ + allowReload?: boolean; + /** + * 是否允许reload + */ + allowDelete?: boolean; + /** + * 上传该文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + upload(): void; + /** + * 取消上传该文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + cancel(): void; + /** + * 删除该文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + remove(): void; + /** + * 销毁该文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + destroy(): void; +} +/** + * 文件过滤条件自定义接口 + * + * 作为[TiUploadComponent.filters]{@link ../components/TiUploadComponent.html#filters}中的类型使用 + * + * 当前支持四种过滤类型: + * + * maxCount:最大选择文件个数,当参数为1时,代表单文件上传 + * + * type:文件选择类型,参数需使用文件扩展名分开,为了确保各浏览器的一致性,文件类型使用扩展名判断 + * + * minSize/maxSize:文件大小限制,参数为number类型,代表文件大小,单位为b,IE9不支持文件大小的获取,因此不支持该条规则 + */ +export interface TiFilter { + /** + * 规则名称 + */ + name: string; + /** + * 规则参数 + */ + params?: Array; + /** + * 规则函数 + * + * **参数:** + * + * fileItem: [TiFileInfo]{@link TiFileInfo} 文件信息 + * + * params: Array 传入的规则参数 + * + * fileQueue: Array<[TiFileItem]{@link TiFileItem}> 文件队列 + * + * **返回值:** + * + * 是否符合规则 boolean + */ + fn?(fileObj: TiFileInfo, params: Array, fileQueue: Array): boolean; +} +/** + * 创建文件实例方法的配置信息接口 + * + * 作为[TiUploadService.create]{@link ../injectables/TiUploadService.html#create}方法中的参数类型使用 + */ +export interface TiUploadConfig { + /** + * 文件上传地址配置 + */ + url: string; // 文件上传地址配置 + /** + * 上传方式,可选值为:get、post(其他方式IE9不支持) + * @default 'post' + */ + method?: string; // 上传方式,可选值为:get、post(其他方式IE9不支持) + /** + * 文件有效性判断条件数组 + */ + filters?: Array; // 文件有效性判断条件数组 + /** + * 向后台发送请求时对应的文件对象name属性,该name属性是后台读取文件的入口值 + * @default 'tiFile' + */ + alias?: string; // 向后台发送请求时对应的文件对象name属性,该name属性是后台读取文件的入口值,默认值为 'tiFile' + /** + * 上传文件附带信息对象 + */ + formData?: object; // 上传文件附带信息 + /** + * 是否自动上传 + * @default true + */ + autoUpload?: boolean; + /** + * 是否一次请求传输多个文件,默认情况下一次请求上传一个文件 + * @default false + */ + batchSend?: boolean; + /** + * 上传数据中,formData是否在file信息之前 + * @default false + */ + formDataFirst?: boolean; + /** + * 上传文件请求头配置,自定义为对象形式 + */ + headers?: object; // 上传文件请求头配置 + /** + * 分片上传每片大小 + */ + chunkSize?: number; // 分片大小,设置后开启分片上传单位是 b + /** + * 文件添加失败回调,可使用该回调定义上传错误时的错误提示 + * + * **参数:** + * + * file: [TiFileInfo]{@link TiFileInfo} 上传文件信息 + * + * validResults: Array<string> 校验不合法的规则name数组 + */ + onAddItemFailed?(fileObject: TiFileInfo, validResults: Array): void; // 文件添加失败回调,可使用该回调定义上传错误时的错误提示 + /** + * 文件添加成功回调 + * + * **参数:** + * + * fileItem: [TiFileItem]{@link TiFileItem} 上传文件对象 + */ + onAddItemSuccess?(fileItem: TiFileItem): void; + // 以下回调对单个或多个文件在同一url同时上传有效,可使用其进行文件上传过程中的业务处理 + /** + * 上传文件前回调,可在该回调中动态设置formData + * + * **参数:** + * fileItems: Array<[TiFileItem]{@link TiFileItem}> 上传的文件对象 + */ + onBeforeSendItems?(fileItems: Array): void; + /** + * 上传进度更新回调 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}> 上传的文件对象 + * + * progress: number 上传进度信息 + */ + onProgressItems?(fileItems: Array, progress: number): void; + /** + * 文件上传完成回调,成功/失败都会触发 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}>, // 上传文件对象数组 + * + * response: string // 文件上传响应信息 + * + * status: number 文件上传响应状态码 + */ + onCompleteItems?(fileItems: Array, response: string, status: number): void; + /** + * 文件上传成功回调 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}>, // 上传文件对象数组 + * + * response: string // 文件上传响应信息 + * + * status: number 文件上传响应状态码 + */ + onSuccessItems?(fileItems: Array, response: string, status: number): void; + /** + * 文件上传失败回调 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}>, // 上传文件对象数组 + * + * response: string // 文件上传响应信息 + * + * status: number 文件上传响应状态码 + */ + onErrorItems?(fileItems: Array, response: string, status: number): void; + /** + * 文件上传取消回调 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}>, // 上传文件对象数组 + * + * response: string // 文件上传响应信息 + * + * status: number 文件上传响应状态码 + */ + onCancelItems?(fileItems: Array, response: string, status: number): void; + /** + * 文件删除回调 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}>, // 上传文件对象数组 + */ + onRemoveItems?(fileItems: Array): void; + /** + * 文件删除前回调 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}>, // 上传文件对象数组 + */ + onBeforeRemoveItems?(fileItems: Array): void; + /** + * 所有文件上传完成回调 + * + * **参数:** + * + * fileItems: Array<[TiFileItem]{@link TiFileItem}>, // 上传文件对象数组 + */ + onCompleteAllItems?(fileItems: Array): void; // 一个序列中所有文件上传完成回调:适用于串行上传,当最后一个文件上传完成时,触发该回调 +} + +/** + * 文件上传实例对象接口 + * + * 以下使用会用到该类型: + * + * 1.[TiUploadService.create]{@link ../injectables/TiUploadService.html#create}方法的返回值类型 + * + * 2.[TiFileItem.uploader]{@link TiFileItem#uploader}对象类型 + * + * 3.[tiFileSelect]{@link ../directives/TiFileSelectDirective.html}属性的配置类型 + */ +export interface TiUploadRef { + /** + * 上传文件队列,只可读 + */ + queue: Array; + /** + * @ignore + * 文件是否有单文件选择限制,只可读 + */ + isSingleFile: boolean; + /** + * @ignore + */ + config: TiUploadConfig; + /** + * @ignore + */ + _uploadComponentInstance: /* TiUploadComponent | TiUploadimageComponent | */ any; + /** + * @ignore + */ + _addToQueue(files: FileList): Array; // 文件添加方法 + /** + * 获取文件队列中未上传完成文件方法 + * + * **参数:**无 + * + * **返回值:**文件对象数组 Array<[TiFileItem]{@link TiFileItem}> + */ + getNotUploadedItems(): Array; + /** + * 获取文件队列中已提交上传,但还未上传文件方法 + * + * **参数:**无 + * + * **返回值:**文件对象数组 Array<[TiFileItem]{@link TiFileItem}> + */ + getReadyItems(): Array; + /** + * 上传队列中所有还未执行过上传的文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + uploadAll(): void; + /** + * 获取上传队列中是否有未上传过的文件方法 + * + * **参数:**无 + * + * **返回值:**boolean + */ + isUploadedAll(): boolean; + /** + * 删除队列中所有文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + removeAll(): void; + /** + * 取消队列中所有上传文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + cancelAll(): void; + /** + * 重新上传队列中所有文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + reloadAll(): void; + /** + * 重新上传队列中所有上传错误文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + reloadAllError(): void; + /** + * 上传队列中某几项文件方法 + * + * **参数:**Array<[TiFileItem]{@link TiFileItem}> + * + * **返回值:**void + */ + uploadItems(items: Array): void; + /** + * 删除队列中某几项文件方法 + * + * **参数:**Array<[TiFileItem]{@link TiFileItem}> + * + * **返回值:**void + */ + removeItems(items: Array): void; + /** + * 取消队列中某几项文件方法 + * + * **参数:**Array<[TiFileItem]{@link TiFileItem}> + * + * **返回值:**void + */ + cancelItems(items: Array): void; +} +/** + * @ignore + * + * 分片上传的每片文件接口;暂时没有对外暴露 + */ +export interface TiChunked { + /** + * 文件上传地址配置 + */ + url: string; + /** + * 上传方式,可选值为:get、post + */ + method?: string; + /** + * 单个文件基础信息,只包含文件名及类型等信息,为业务配置信息,只可获取 + */ + file: TiFileInfo; + /** + * @ignore + * 向后台发送请求时对应的文件对象name属性,该name属性是后台读取文件的入口值,为业务配置信息,只可获取 + */ + alias: string; + /** + * 上传文件附带信息对象 + */ + formData?: object; + /** + * 单个文件基础信息,只包含文件名及类型等信息,为业务配置信息,只可获取 + */ + _file: any; + /** + * 上传数据中,formData是否在file信息之前 + * @default false + */ + formDataFirst?: boolean; + /** + * 上传文件请求头配置,自定义为对象形式 + */ + headers?: object; + /** + * @ignore + */ + _xhr: any; + /** + * 分片上传时第几项文件 + */ + index: number; +} diff --git a/src/upload/lib/src/TiFileSelectDirective.ts b/src/upload/lib/src/TiFileSelectDirective.ts new file mode 100644 index 0000000..bb82b56 --- /dev/null +++ b/src/upload/lib/src/TiFileSelectDirective.ts @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; +import { TiFileItem, TiUploadRef } from './TiFileInterface'; +import { TiUploadUtil } from './TiUploadUtil'; + +/** + * 该指令适用于自定义上传场景,指令定义在input type='file'元素上,传入的是文件上传实例 + * 文件上传实例的生成,请参考[TiUploadService.create]{@link ../injectables/TiUploadService.html#create} + * + * 除自定义使用方式外,Tiny还提供了两种已进行设计的上传组件供业务使用,具体见[TiUploadComponent]{@link ../components/TiUploadComponent.html} + * + */ +@Directive({ + selector: '[tiFileSelect]' +}) +export class TiFileSelectDirective { + /** + * 文件上传实例 + */ + @Input() tiFileSelect: TiUploadRef; + private nativeElement: any; + private fileChangeEvent: any; + constructor(private hostElementRef: ElementRef, private renderer: Renderer2) { + this.nativeElement = this.hostElementRef.nativeElement; + this.fileChangeEvent = this.renderer.listen(this.nativeElement, 'change', () => { + if (!this.tiFileSelect) { + // 配置的上传文件实例不存在情况下,不做后续处理 + return; + } + this.onFileChange(this.nativeElement); // 点选文件后的处理 + }); + } + /** + * @ignore + * 选择文件后,根据浏览器差异进行处理 + */ + onFileChange(fileSel: any): void { + const uploadInst: any = this.tiFileSelect; + const files: any = TiUploadUtil.isHTML5 ? fileSel.files : fileSel; // 获取文件信息 + const addedItems: Array = uploadInst._addToQueue(files); // 文件选择队列 + if (TiUploadUtil.isHTML5) { + // H5情况下,重置表单元素值,确保可重复选择文件 + this.nativeElement.value = ''; + } else { + // 非H5情况下,确保文件下次可继续选择,分两种情况:1.已选文件有效情况下,,重新替换表单元素,确保文件点选元素不会随表单上传消失; + // 2.文件未加入到队列(文件校验失败情况)情况下,重置点选表单,确保文件可重复选择(校验失败可能是文件队列长度不符,所以为确保用户体验,执行该操作) + const isRemoveInput: boolean = addedItems.length === 0; // 根据文件是否有效情况确定是否移除input + this.replaceFileInput(fileSel, isRemoveInput); + } + + // 自动上传情况下,进行文件上传 + if (uploadInst.config.autoUpload !== false && addedItems.length !== 0) { + uploadInst.uploadItems(addedItems); + } + // onpush模式下,点击添加文件后,上传列表详情不展示。 + if (this.tiFileSelect._uploadComponentInstance) { + this.tiFileSelect._uploadComponentInstance.changeDetectorRef.markForCheck(); + } + } + + /** + * @ignore + * 替换单个文件选择按钮,确保后续文件选择可继续点选 + * 当前文件选择框 + * 文件选择实例对象 + */ + replaceFileInput(fileSel: any, isRemoveInput: boolean): void { + // 清除当前文件输入框选择事件 + this.fileChangeEvent(); + + // 新增input,并处理当前input + const fileSelNew: any = fileSel.cloneNode(); // 文件元素克隆时,不会复用原有已选文件信息 + if (isRemoveInput) { + // 文件选择无效情况下移除input,确保下次可正常选择 + fileSel.parentNode.appendChild(fileSelNew); + fileSel.remove(); + } else { + fileSel.style.display = 'none'; // 隐藏已选文件,确保点选框页面呈现的唯一性 + fileSel.parentNode.insertBefore(fileSelNew, fileSel.nextSibling); // 点选文件已放入文件队列中,替换新的文件选择按钮,确保下次点选生效 + } + + // 增加当前input事件 + this.fileChangeEvent = this.renderer.listen(fileSelNew, 'change', () => { + this.onFileChange(fileSelNew); + }); + } +} diff --git a/src/upload/lib/src/TiUploadComponent.ts b/src/upload/lib/src/TiUploadComponent.ts new file mode 100644 index 0000000..60160b9 --- /dev/null +++ b/src/upload/lib/src/TiUploadComponent.ts @@ -0,0 +1,297 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ElementRef, Input, Renderer2, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; +import { TiFileItem } from './TiFileInterface'; +import { TiUploadService } from './TiUploadService'; +import { Util } from '@opentiny/ng-utils'; +import { TiOverflowService } from '@opentiny/ng-overflow'; +import { TiUploadbaseComponent, TiUploadInitFile } from './TiUploadbaseComponent'; + +/** + * 该组件用于实现已设计好的交互完整的文件上传组件,包含两种样式: + * + * 1.带输入框的样式(type为inputField) + * + * 2.按钮样式(type为button) + * + * 如果以上两种样式不满足业务场景,可使用自定义方式实现,具体实现请参考[tiFileSelect]{@link ../directives/TiFileSelectDirective.html} + * + */ +@Component({ + selector: 'ti-upload:not([type]), ti-upload[type="inputField"], ti-upload[type="button"], ti-upload[type="textButton"]', + templateUrl: './upload.html', + styleUrls: ['./upload.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiUploadComponent extends TiUploadbaseComponent { + constructor( + private uploaderService: TiUploadService, + hostEle: ElementRef, + renderer: Renderer2, + private tiOverflow: TiOverflowService, + public changeDetectorRef: ChangeDetectorRef + ) { + super(hostEle, renderer); + } + /** + * 上传样式类型,'textButton' 类型 + */ + @Input() type: 'inputField' | 'button' | 'textButton' = 'inputField'; + /** + * 按钮文字 + */ + @Input() buttonText: string; // 配置按钮文字,type 为 button 形式时,为文件选择按钮文字;type 为 inputField 时,为上传按钮文字 + + // inputField 形式文件上传的相关配置 + /** + * 设置上传文件选择框宽度,只适用于 inputField 类型 + * @ignore + */ + @Input() inputFieldWidth: string; + /** + * 是否显示提交按钮,只适用于 inputField 类型 + * @ignore + */ + @Input() showSubmitButton: boolean; + /** + * 占位文本,只适用于 inputField 类型 + * @ignore + */ + @Input() placeholder: string; + /** + * 是否自动聚焦,只适用于 inputField 类型 + * @ignore + */ + @Input() autofocus: boolean = false; + /** + * 设置已上传文件列表的提示信息,只适用于 button/textButton 类型 + */ + @Input() note: string; + /** + * 初始化显示的文件列表,只适用于 button/textButton 类型 + */ + @Input() initFiles: Array; + /** + * 是否显示上传列表 + */ + @Input() showUploadList: boolean = true; + /** + * 错误提示信息,只适用于 button/textButton 类型 + */ + @Input() errorMessage: string; + /** + * 是否显示错误提示,只适用于 button/textButton 类型 + */ + @Input() showErrorMessage: boolean = true; + /** + * 文件列表区域最大高度,只适用于 button/textButton 类型 + */ + @Input() listMaxHeight: string; + /** + * @ignore + */ + @ViewChild('fileInput') fileInput: any; + /** + * @ignore 多文件上传的状态提示信息 + */ + stateInfo: string; + /** + * 是否禁用重新上传 + */ + public reloadAllDisable: boolean = true; + /** + * @ignore 点击上传按钮时触发的回调 + */ + onSelectClick(): void { + this.fileInput.nativeElement.click(); + } + /** + * @ignore + */ + ngOnInit(): void { + super.ngOnInit(); + // 创建uploader实例 + const autoUpload: boolean = Util.isUndefined(this.autoUpload) ? true : this.autoUpload; + this.uploadConfig.autoUpload = autoUpload; + this.uploadInst = this.uploaderService.create(this.uploadConfig, this); + if (this.initFiles && this.filters) { + const maxCountIndex: number = this.getMaxCountIndex(); + if (maxCountIndex !== -1) { + this.uploadConfig.filters[maxCountIndex].params[0] -= this.initFiles.length; + } + } + this.placeholder = this.setPlaceholder(); + // 设置submit按钮的显示状态 + this.showSubmitButton = Util.isUndefined(this.showSubmitButton) ? !autoUpload : this.showSubmitButton; + // 是否定义beforeRemoveItems事件 + // 为初始显示文件增加各自的remove方法(仅在button/textButton类型时) + if ((this.type === 'button' || this.type === 'textButton') && this.initFiles) { + this.initFiles.forEach((item: TiUploadInitFile) => { + if (Util.isUndefined(item.allowDelete)) { + item.allowDelete = true; + } + item.remove = (): void => { + if (!item.allowDelete) { + return; + } + const index: number = this.initFiles.findIndex((_item: TiUploadInitFile) => { + return _item === item; + }); + this.removeItem(index); + this.changeDetectorRef.markForCheck(); + }; + }); + } + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + // 处理autofocus + if (this.autofocus === true) { + this.focus(); + } + } + + /** + * @ignore + * 移除显示的文件项目(button/textButton类型的showList项目接口) + */ + removeInitFiles(item: TiUploadInitFile): void { + if (!item.allowDelete) { + return; + } + const index: number = this.initFiles.findIndex((_item: TiUploadInitFile) => { + return _item === item; + }); + // 若有beforeRemoveItems事件,则触发该事件 + if (this.isRemove) { + this.beforeRemoveItems.emit([item]); + } else { + // 否则直接删除,并触发removeItems事件 + this.removeItem(index); + this.removeItems.emit([item]); + } + } + // 提供focus/blur方法,供外部调用 + /** + * @ignore + * 聚焦方法,只适用于inputField类型 + */ + focus(): void { + this.fileInput.nativeElement.nextElementSibling.focus(); + } + /** + * @ignore + * 失焦方法,只适用于inputField类型 + */ + blur(): void { + this.fileInput.nativeElement.nextElementSibling.blur(); + } + // 通过item元素的移入移出事件控制元素的hover状态,该hover状态会决定item中部分按钮的显示状态 + /** + * @ignore + */ + onItemMouseenter(item: TiFileItem, event: any): void { + if (this.disabled) { + return; + } + item.isHover = true; + // 文件名超长溢出时,显示名称 + const fileNameEle: Element = event.target.querySelector('.ti3-aui-file-name'); + item.isOverflow = this.tiOverflow.isOverflow(fileNameEle); + } + /** + * @ignore + */ + onItemMouseleave(item: TiFileItem): void { + if (this.disabled) { + return; + } + item.isHover = false; + } + // 获取上传文件状态,同时根据状态设置状态详细信息 + /** + * @ignore + */ + getUploadState(): string { + let uploadingLen: number = 0; + let uploadErrLen: number = 0; + let uploadSuccLen: number = 0; + this.uploadInst.queue.forEach((item: TiFileItem) => { + if (item.isUploading) { + uploadingLen++; + } else if (item.isError) { + uploadErrLen++; + } else if (item.isSuccess) { + uploadSuccLen++; + } + if (item.allowReload) { + this.reloadAllDisable = false; + } + }); + const fileQueueLen: number = this.uploadInst.queue.length; + if (uploadSuccLen === fileQueueLen) { + this.stateInfo = this.uploadLan.successInfo; + + return 'success'; + } + if (uploadErrLen !== 0) { + this.stateInfo = Util.formatEntry(this.uploadLan.errorMultiInfo, [uploadErrLen]); + + return 'error'; + } + if (uploadingLen !== 0) { + this.stateInfo = Util.formatEntry(this.uploadLan.uploadingMutiInfo, [`${uploadingLen}/${fileQueueLen}`]); + + return 'uploading'; + } + this.stateInfo = Util.formatEntry(this.uploadLan.addSuccessMutiInfo, [fileQueueLen]); + + return 'addSuccess'; + } + /** + * 初始设置输入框内提示文本 + * 四种场景:自动单文件上传,自动多文件上传,手动单文件上传,手动多文件上传 + */ + private setPlaceholder(): string { + if (this.placeholder) { + return this.placeholder; + } + + if (this.autoUpload) { + if (this.uploadInst.isSingleFile) { + return this.uploadLan.autoUploadFilePlaceholder; + } + + return this.uploadLan.autoUploadFilesPlaceholder; + } + + if (this.uploadInst.isSingleFile) { + return this.uploadLan.notAutoUploadFilePlaceholder; + } + + return this.uploadLan.notAutoUploadFilesPlaceholder; + } + + /** + * 移除初始文件 + */ + private removeItem(index: number): void { + this.initFiles.splice(index, 1); + if (this.filters) { + const maxCountIndex: number = this.getMaxCountIndex(); + if (maxCountIndex !== -1) { + this.uploadConfig.filters[maxCountIndex].params[0] += 1; + } + } + } +} diff --git a/src/upload/lib/src/TiUploadModule.ts b/src/upload/lib/src/TiUploadModule.ts new file mode 100644 index 0000000..00795a8 --- /dev/null +++ b/src/upload/lib/src/TiUploadModule.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiFileSelectDirective } from './TiFileSelectDirective'; +import { TiUploadComponent } from './TiUploadComponent'; +import { TiDisabledDirective } from './TiDisabledDirective'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { TiProgresspieModule } from '@opentiny/ng-progresspie'; +import { TiUploadServiceModule } from './TiUploadServiceModule'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiOverflowModule } from '@opentiny/ng-overflow'; +import { TiValidationModule } from '@opentiny/ng-validation'; +import { TiProgressbarModule } from '@opentiny/ng-progressbar'; +import { TiUploadbaseComponent } from './TiUploadbaseComponent'; +import { TiUploadimageComponent } from './TiUploadimageComponent'; +import { TiModalModule } from '@opentiny/ng-modal'; +import { TiImagepreviewModule } from '@opentiny/ng-imagepreview'; +import { TiButtonModule } from '@opentiny/ng-button'; +import { TiOutlineModule } from '@opentiny/ng-outline'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +@NgModule({ + imports: [ + CommonModule, + TiIconModule, + TiTipModule, + TiProgresspieModule, + TiUploadServiceModule, + TiOverflowModule, + TiValidationModule, + TiProgressbarModule, + TiModalModule, + TiImagepreviewModule, + TiButtonModule, + TiOutlineModule + ], + exports: [TiFileSelectDirective, TiUploadComponent, TiDisabledDirective, TiUploadbaseComponent, TiUploadimageComponent], + declarations: [TiFileSelectDirective, TiUploadComponent, TiDisabledDirective, TiUploadbaseComponent, TiUploadimageComponent] +}) +export class TiUploadModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiUploadComponent } from './TiUploadComponent'; +export { TiFileSelectDirective } from './TiFileSelectDirective'; +export { TiDisabledDirective } from './TiDisabledDirective'; +export { TiUploadbaseComponent, TiUploadInitFile, TiUploadimageInitFile } from './TiUploadbaseComponent'; +export { TiUploadimageComponent } from './TiUploadimageComponent'; diff --git a/src/upload/lib/src/TiUploadService.ts b/src/upload/lib/src/TiUploadService.ts new file mode 100644 index 0000000..7f18540 --- /dev/null +++ b/src/upload/lib/src/TiUploadService.ts @@ -0,0 +1,395 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Injectable } from '@angular/core'; +import { Util } from '@opentiny/ng-utils'; +import { TiFileItemUtil } from './TiUploadUtil'; +import { TiUploadUtil } from './TiUploadUtil'; +import { TiUploadServiceModule } from './TiUploadServiceModule'; +import { TiFileInfo, TiFileItem, TiFilter, TiUploadConfig, TiUploadRef } from './TiFileInterface'; +import { TiUploadComponent } from './TiUploadComponent'; +import { TiUploadimageComponent } from './TiUploadimageComponent'; + +// 文件上传服务封装,一个实例对应一个上传文件队列 +// @dynamic +/** + * 文件上传服务,通过该服务生成上传文件实例对象,一个实例对应一个上传文件队列 + * + * 该服务适用于自定义文件上传实例方式,使用该服务时需要引入模块TiUploadServiceModule,与[tiFileSelect]{@link ../directives/TiFileSelectDirective.html}配合使用 + * + * 除自定义使用方式外,Tiny还提供了两种已设计的上传样式供业务使用,具体见[TiUploadComponent]{@link ../components/TiUploadComponent.html} + * + */ +@Injectable({ + providedIn: TiUploadServiceModule +}) +export class TiUploadService { + // 文件校验规则定义 + private static readonly filterRules: any = { + maxSize: (fileObj: TiFileInfo, params: Array): boolean => { + const size: number = fileObj.size; + if (!Util.isNumber(size)) { + // 文件大小获取不到情况下,忽略该条校验规则 + return true; + } + + return !(size > params[0]); + }, + minSize: (fileObj: TiFileInfo, params: Array): boolean => { + const size: number = fileObj.size; + if (!Util.isNumber(size)) { + // 文件大小获取不到情况下,忽略该条校验规则 + return true; + } + + return !(size < params[0]); + }, + type: (fileObj: TiFileInfo, params: Array): boolean => { + // param参数需使用文件扩展名分开,为了确保各浏览器的一致性,文件类型使用扩展名判断,如果产品需要在选择时限制,请在input上设置accept属性 + let isValidType: boolean = false; + params[0].split(',').forEach((type: string) => { + if (fileObj.name.match(new RegExp(`.(${type.replace(/\./g, '\\.')})$`, 'i')) !== null) { + isValidType = true; + } + }); + + return isValidType; + }, + maxCount: (fileObj: TiFileInfo, params: Array, fileQueue: Array): boolean => { + return !(fileQueue.length >= params[0]); + } + }; + + /** + * 对单选进行过滤条件重置,单选去除maxCount条件,函数返回过滤后的生效规则 + */ + private static initFilter(rules: Array, isSingleFile: boolean): Array { + if (isSingleFile) { + // 单文件情况下去除maxCount设置 + return rules.filter((rule: TiFilter) => { + return rule.name !== 'maxCount'; + }); + } + + // 多文件情况下 filter不做处理 + return rules || []; + } + /** + * 根据用户配置的maxCount过滤条件,判断文件是否为单文件上传 + * 用户配置的过滤条件 + * 是否为单文件上传 + */ + private static isSingleFileFn(filters: Array): boolean { + if (!filters || !filters.length) { + // 不设置filter情况下,为多文件上传 + return false; + } + const maxCountIndex: number = filters.findIndex((item: TiFilter): boolean => { + return item.name === 'maxCount'; + }); + // 存在maxCount规则,并且其参数不为1的情况下,为多文件上传 + if (maxCountIndex === -1 || (filters[maxCountIndex].params && filters[maxCountIndex].params[0] !== 1)) { + return false; + } + + return true; + } + + /** + * 批量上传文件 + * {Array} 上传文件items数组 + * 返回 无 + */ + private static uploadItems(items: Array): void { + if (!items.length) { + return; + } + // 设置上传文件标志位,文件逐个上传情况下,会根据该标志位决定下个上传文件 + items.forEach((item: TiFileItem) => { + item.isReady = true; + }); + + // 开始上传文件 + if (items[0].batchSend) { + TiUploadUtil.uploadItems(items); + } else { + // 上传单个文件,在upload方法中会依据isReady的设置进行其他文件的串行上传 + TiUploadUtil.uploadItems([items[0]]); + } + } + + /** + * 批量取消上传文件 + * 返回 无 + */ + private static cancelItems(items: Array): void { + if (!items.length) { + return; + } + if (items[0].batchSend) { + // 一个链接上传情况下,一次取消多个items,仅触发一次事件 + TiUploadUtil.cancelItems(items); + } else { + // 逐个链接上传情况下,一次仅取消一个items,触发多次事件 + items.forEach((item: TiFileItem) => { + item.cancel(); + }); + } + } + + /** + * 批量删除文件,只涉及上传队列的文件删除,具体的后台删除还需要产品向后台发送文件删除请求实现 + * {Array} 上传文件items数组 + * 返回 无 + */ + private static removeItems(items: Array): void { + if (!items.length) { + return; + } + if (items[0].batchSend) { + // 一个链接上传情况下,一次删除多个items,仅触发一次事件 + TiUploadUtil.removeItems(items); + } else { + // 逐个链接上传情况下,一次仅删除一个items,触发多次事件 + items.forEach((item: TiFileItem) => { + item.remove(); + }); + } + } + + /** + * 单个文件的有效性校验 + * 返回 {Array} 由不符合的规则name组成的数组 + */ + private static getInvalidRules(tifileObject: TiFileInfo, filtersRules: Array, fileQueue: Array): Array { + // 无效判断 + const filterLen: number = filtersRules.length; + if (filterLen === 0) { + return []; + } + + // 逐条规则校验,并返回结果数组 + const invalidRetArr: Array = []; // 校验返回结果数组,该数组中返回的是不符合的校验规则name + for (let i: number = 0; i < filterLen; i++) { + const filterConfig: TiFilter = filtersRules[i]; // 单条校验规则配置 + + // 根据配置寻找规则函数(规则分为默认规则 和 自定义规则) + const filterName: string = filterConfig.name; // 配置规则名称 + let ruleFn: (fileObj: TiFileInfo, params: Array, fileQueue: Array) => boolean; // 规则函数 + if (typeof filterConfig.fn === 'function') { + ruleFn = filterConfig.fn; + } else if (TiUploadService.filterRules[filterName]) { + // 未定义fn的情况下,从默认规则中找 + ruleFn = TiUploadService.filterRules[filterName]; + } + + // 调用规则函数,判断文件有效性 + if (typeof ruleFn === 'function' && !ruleFn(tifileObject, filterConfig.params, fileQueue)) { + invalidRetArr.push(filterName); + } + } + + return invalidRetArr; + } + /** + * 获取未上传文件队列 + */ + private static getNotUploadedItems(fileQueue: Array): Array { + return fileQueue.filter((item: TiFileItem): boolean => { + return !item.isUploaded; + }); + } + /** + * 获取待上传文件,该方法用于文件批量上传时获取上传文件队列 + * 返回 {Array} 返回待上传文件,且文件队列返回值是根据文件点选次序排列 + */ + private static getReadyItems(fileQueue: Array): Array { + return fileQueue + .filter((item: TiFileItem): boolean => { + return item.isReady && !item.isUploading; + }) + .sort((item1: TiFileItem, item2: TiFileItem): number => { + return item1.index - item2.index; + }); + } + + /** + * 获取上传失败文件,该方法用于文件批量上传时获取上传失败文件队列 + * 返回 {Array} 返回上传失败文件,且文件队列返回值是根据文件点选次序排列 + */ + private static getErrorItems(fileQueue: Array): Array { + return fileQueue + .filter((item: TiFileItem): boolean => { + return item.isError; + }) + .sort((item1: TiFileItem, item2: TiFileItem): number => { + return item1.index - item2.index; + }); + } + + /** + * 重新上传列表中所有先前上传错误文件 + * 返回 无 + */ + private static reloadAllError(fileQueue: Array): void { + const allowReloadFileItemLength = fileQueue.filter((item: TiFileItem) => item.allowReload).length; + // 没有允许重新上传的实例对象 + if (allowReloadFileItemLength === 0) { + return; + } + TiUploadService.uploadItems(TiUploadService.getErrorItems(fileQueue)); + } + + /** + * 重新上传列表中所有文件 + * 返回 无 + */ + private static reloadAll(fileQueue: Array): void { + TiUploadService.uploadItems(fileQueue); + } + + /** + * 上传列表中所有未上传过的文件 + * 返回 无 + */ + private static uploadAll(fileQueue: Array): void { + const items: Array = this.getNotUploadedItems(fileQueue).filter((item: TiFileItem): boolean => { + return !item.isUploading; + }); + TiUploadService.uploadItems(items); + } + + /** + * 判断是否有未上传的 + * 返回 boolean + */ + private static isUploadedAll(fileQueue: Array): boolean { + const items: Array = this.getNotUploadedItems(fileQueue).filter((item: TiFileItem): boolean => { + return !item.isUploading; + }); + + return items.length === 0; + } + + /** + * 删除所有队列中的文件 + */ + private static removeAll(fileQueue: Array): void { + while (fileQueue.length !== 0) { + fileQueue[0].remove(); + } + } + + /** + * 取消所有队列中的文件(未上传和待上传文件) + */ + private static cancelAll(fileQueue: Array): void { + const items: Array = this.getNotUploadedItems(fileQueue); + TiUploadService.cancelItems(items); + } + + /** + * 创建文件上传实例 + */ + create(config: TiUploadConfig, uploadComInst?: TiUploadComponent | TiUploadimageComponent | any): TiUploadRef { + const isSingleFile: boolean = TiUploadService.isSingleFileFn(config.filters); // 文件是否为单文件设置 + const filters: Array = TiUploadService.initFilter(config.filters, isSingleFile); // 文件校验过滤规则 + + let uploader: TiUploadRef; + const fileQueue: Array = []; // 文件队列列表,一个实例对应一个文件列表 + let fileIndex: number = 0; // 文件次序定义,用作文件索引 + /** + * 将选择的文件进行有效验证后加入到队列中,外部调用该方法时,认为该队列中的文件和当前uploader的配置项一致 + * files {FileList|FileInput} 文件对象 + * 返回 {Array} 已添加文件数组 + */ + const addToQueue: (files: FileList | Element) => Array = (files: FileList | Element): Array => { + // 循环列表添加文件 + const addedItems: Array = []; // 用于记录本次选择文件列表 + if (files && !Util.isUndefined((files as FileList).length)) { + // H5方式 + for (let i: number = 0; i < (files as FileList).length; i++) { + addItem(files[i], addedItems); + } + } else { + addItem(files, addedItems); + } + + return addedItems; + }; + const addItem: (fileOrInput: any, addedItems: Array) => void = (fileOrInput: any, addedItems: Array): void => { + // 文件对象,该对象包含文件的基本信息,统一了浏览器的差异性,会在文件过滤失败回调中作为参数传递给外部 + const tifileObject: TiFileInfo = TiFileItemUtil.createFileObject(fileOrInput); + + // 校验单个文件的有效性,并根据校验结果进行文件操作 + const invalidArr: Array = TiUploadService.getInvalidRules(tifileObject, filters, fileQueue); // 校验结果数组,数组中定义校验错误规则的name + let isValid: boolean = !invalidArr.length; // 校验结果返回数组为空时有效 + + // 单文件情况下,需要覆盖原有有效文件 + if (isSingleFile) { + fileQueue[0]?.remove(); + uploadComInst?.initFiles && uploadComInst.initFiles[0]?.remove(); + if (invalidArr.length === 0) { + isValid = true; // 置位校验结果值 + } + } + if (isValid) { + // 校验成功情况下,将文件加入到上传队列中,并触发 + // 上传文件对象,该对象中包含文件及状态信息等,在回调中传递 + const fileItem: TiFileItem = TiFileItemUtil.createFileItem(tifileObject, fileOrInput, config, uploader); + addedItems.push(fileItem); + fileQueue.push(fileItem); + fileItem.index = ++fileIndex; // 文件序列数增加,确保整个文件队列中该值唯一 + if (typeof config.onAddItemSuccess === 'function') { + config.onAddItemSuccess(fileItem); + } + } else if (typeof config.onAddItemFailed === 'function') { + config.onAddItemFailed(tifileObject, invalidArr); + } + }; + uploader = { + queue: fileQueue, // 上传文件队列,可读属性 + isSingleFile, // 文件是否有单文件选择限制 + config, // 上传文件配置信息 + _uploadComponentInstance: uploadComInst, + _addToQueue: addToQueue, // 文件添加方法 + getNotUploadedItems(): Array { + return TiUploadService.getNotUploadedItems(fileQueue); + }, // 未上传完成文件获取 + getReadyItems(): Array { + return TiUploadService.getReadyItems(fileQueue); + }, // 已提交上传,但还未上传文件获取 + uploadAll(): void { + TiUploadService.uploadAll(fileQueue); + }, // 上传队列中所有还未执行过上传的文件 + isUploadedAll(): boolean { + return TiUploadService.isUploadedAll(fileQueue); + }, // 上传队列中是否有未上传过的 + removeAll(): void { + TiUploadService.removeAll(fileQueue); + }, // 删除队列中所有文件 + cancelAll(): void { + TiUploadService.cancelAll(fileQueue); + }, // 取消队列中所有上传文件 + reloadAll(): void { + TiUploadService.reloadAll(fileQueue); + }, // 重新上传队列中所有文件 + reloadAllError(): void { + TiUploadService.reloadAllError(fileQueue); + }, // 重新上传队列中所有上传错误文件 + uploadItems: TiUploadService.uploadItems, // 上传队列中某几项文件 + removeItems: TiUploadService.removeItems, // 删除队列中某几项文件 + cancelItems: TiUploadService.cancelItems // 取消队列中某几项文件 + }; + + return uploader; + } +} diff --git a/src/upload/lib/src/TiUploadServiceModule.ts b/src/upload/lib/src/TiUploadServiceModule.ts new file mode 100644 index 0000000..20d624a --- /dev/null +++ b/src/upload/lib/src/TiUploadServiceModule.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +/** + * @ignore + * 此TiUploadServiceModule不对用户暴露,用户仅需import TiUploadModule,则间接引入了TiUploadServiceModule + */ +@NgModule({ + imports: [CommonModule] +}) +export class TiUploadServiceModule {} diff --git a/src/upload/lib/src/TiUploadUtil.ts b/src/upload/lib/src/TiUploadUtil.ts new file mode 100644 index 0000000..771bfea --- /dev/null +++ b/src/upload/lib/src/TiUploadUtil.ts @@ -0,0 +1,749 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiChunked, TiFileInfo, TiFileItem, TiUploadConfig, TiUploadRef } from './TiFileInterface'; +import { Util } from '@opentiny/ng-utils'; +import { map, mergeAll } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; + +// 上传文件服务,提供上传过程中的通用方法 +/** + * @ignore + */ +export class TiUploadUtil { + // 已支持SSR + public static readonly isHTML5: boolean = typeof window !== 'undefined' && (window as any).File && (window as any).FormData; + /** + * 上传多个文件 + * 上传一个或多个文件item对象 + * 无 + */ + public static uploadItems(items: Array): void { + // 异常处理 1.入参不合法,2.正在上传的文件不允许重复上传 + for (let i: number = items.length - 1; i >= 0; i--) { + if (!TiUploadUtil.isValidFileItem(items[i]) || items[i].isUploading) { + items.splice(i, 1); + } + } + TiUploadUtil.onBeforeSend(items); // 文件上传前处理 + for (let j: number = items.length - 1; j >= 0; j--) { + if (items[j].isCancel) { + // 已取消上传的文件不再上传(此处主要是对beforeSend中取消上传的文件进行处理) + items.splice(j, 1); + } + } + if (items.length > 0) { + TiUploadUtil.isHTML5 ? TiUploadUtil.uploadXhr(items) : TiUploadUtil.uploadForm(items); + } + } + + /** + * 取消队列中多个正在上传中的文件(只在上传时可取消) + * 文件item对象数组 + * 无 + */ + public static cancelItems(items: Array): void { + for (let i: number = items.length - 1; i >= 0; i--) { + if (!TiUploadUtil.isValidFileItem(items[i])) { + // 异常处理:入参不合法情况下,不做处理 + items.splice(i, 1); + } else { + items[i].isCancel = true; + } + } + if (items[0].isUploading) { + // 正在上传过程中,取消上传,上传事件中,会处理响应赋值及事件触发 + TiUploadUtil.isHTML5 ? items[0]._xhr.abort() : items[0]._form.abort(); + } else { + // 未在上传过程中情况下,依然触发cancel和complete事件 + const response: string = undefined; + const status: number = 0; + TiUploadUtil.onCancel(items, response, status); + TiUploadUtil.onComplete(items, response, status); + } + } + + /** + * 删除队列中单个或多个上传文件 + * 文件item对象数组 + */ + public static removeItems(items: Array): void { + const itemsArr: Array = { ...items }; + for (let i: number = items.length - 1; i >= 0; i--) { + if (!TiUploadUtil.isValidFileItem(items[i])) { + items.splice(i, 1); + } + } + // 阻止文件上传进程 + if (items[0].isUploading) { + TiUploadUtil.cancelItems(items); + } + // 删除队列中的文件对象及文件对象相关引用 + for (let j: number = items.length - 1; j >= 0; j--) { + items[j].uploader.queue.splice(TiUploadUtil.getItemIndex(items[j]), 1); + itemsArr[j].destroy(); + } + // 触发外部定义的删除事件 + TiUploadUtil.onRemove(itemsArr); + } + + /** + * 触发onBeforeRemoveItems事件 + * 文件item对象数组 + */ + public static onBeforeRemove(items: Array): void { + TiUploadUtil.handleEvent('BeforeRemove', items); + } + + /** + * 获取文件对象 index值,获取该值用于后续判断item有效性 + * 上传文件item对象 + */ + private static getItemIndex(item: TiFileItem): number { + if (item && item.uploader) { + return item.uploader.queue.findIndex((itemInQueue: TiFileItem) => { + return itemInQueue.index === item.index; + }); + } + + return -1; + } + + /** + * 判断文件是否为有效文件 + * 上传文件item对象 + */ + private static isValidFileItem(item: TiFileItem): boolean { + return TiUploadUtil.getItemIndex(item) !== -1; + } + // 生成上传数据 + private static generateUploadData(items: Array): any { + // 上传数据组装 + const uploadDataObj: any = new FormData(); + if (items[0].formDataFirst) { + // formData先于文件信息情况下,先添加formData + addFormData(); + addFile(); + } else { + // formData后于文件信息情况下,后添加formData + addFile(); + addFormData(); + } + function addFile(): void { + items.forEach((item: TiFileItem | TiChunked) => { + uploadDataObj.append(item.alias, item._file, item.file.name); // 添加上传文件 + }); + } + function addFormData(): any { + // 添加单个文件的formData对象,加入到上传对象中 + for (const key in items[0].formData) { + if (Object.prototype.hasOwnProperty.call(items[0].formData, key)) { + uploadDataObj.append(key, items[0].formData[key]); + } + } + } + + return uploadDataObj; + } + private static setXhr(xhr: any, items: Array): void { + // 文件上传信息配置(使用XHR,支持跨域请求) + xhr.upload.onprogress = (event: any): void => { + // 文件进度获取事件 + const { lengthComputable, loaded, total } = event; + const progress: number = Math.round(lengthComputable ? (loaded * 100) / total : 0); // 读取当前进度信息 + TiUploadUtil.onProgress(items, progress); + }; + + xhr.onload = (): void => { + // 上传完成事件 + const { response, status } = xhr; + if (TiUploadUtil.isSuccessCode(status)) { + TiUploadUtil.onSuccess(items, response, status); + } else { + TiUploadUtil.onError(items, response, status); + } + TiUploadUtil.onComplete(items, response, status); + }; + + xhr.onerror = (): void => { + // 上传失败事件 + const { response, status } = xhr; + TiUploadUtil.onError(items, response, status); + TiUploadUtil.onComplete(items, response, status); + }; + + xhr.onabort = (): void => { + // 取消回调 + const { response, status } = xhr; + TiUploadUtil.onCancel(items, response, status); + TiUploadUtil.onComplete(items, response, status); + }; + + // 设置单个item的_xhr,该对象用于操作单个item的上传取消等处理 + items.forEach((item: TiFileItem) => { + item._xhr = xhr; + }); + } + private static setheaders(xhr: any, items: Array): void { + // 设置请求头 + for (const key in items[0].headers) { + if (Object.prototype.hasOwnProperty.call(items[0].headers, key)) { + xhr.setRequestHeader(key, items[0].headers[key]); + } + } + } + /** + * xhr方式上传文件 + * 上传一个或多个文件item对象,上传单个文件的情况下 + */ + private static uploadXhr(items: Array): void { + if (items.length === 1 && items[0].uploader.config.chunkSize && items[0].file.size > items[0].uploader.config.chunkSize) { + TiUploadUtil.uploadChunkedFile(items, items[0].uploader.config.chunkSize); + return; + } + const uploadDataObj: TiFileInfo = TiUploadUtil.generateUploadData(items); // 组装上传对象 + // 生成并设置xhr + const xhr: any = new XMLHttpRequest(); + TiUploadUtil.setXhr(xhr, items); + + // 开始上传 + xhr.open(items[0].method, items[0].url, true); + TiUploadUtil.setheaders(xhr, items); + xhr.send(uploadDataObj); + + // 设置对象的上传状态 + items.forEach((item: TiFileItem) => { + item.isUploading = true; + }); + } + private static uploadForm(items: Array): void { + const form: any = document.createElement('form'); + form.style.display = 'none'; + for (const item of items) { + // 清除原有form表单 + if (item._form) { + item._form.parentNode.replaceChild(items[0]._input, items[0]._form); // 清除先前对应的form表单对象,确保上传文件表单元素外层不被form包裹 + } + item._form = form; // 保存当前form + } + items[0]._input.parentNode.insertBefore(form, items[0]._input); // form插入inputSubmit之前,使其在页面显示 + + if (items[0].formDataFirst) { + // formData先于文件信息情况下,先添加formData + addFormData(); + addFile(); + } else { + // formData后于文件信息情况下,后添加formData + addFile(); + addFormData(); + } + + // 文件信息组装 + function addFile(): void { + for (const item of items) { + const inputSubmit: Element = item._input; + inputSubmit.setAttribute('name', item.alias); + form.appendChild(inputSubmit); // 在form中添加上传文件元素 + } + } + + // formData信息组装 + function addFormData(): void { + // 添加formData元素 + for (const key in items[0].formData) { + if (Object.prototype.hasOwnProperty.call(items[0].formData, key)) { + const formDataDomItem: any = document.createElement('input'); + formDataDomItem.setAttribute('type', 'hidden'); + formDataDomItem.setAttribute('name', key); + formDataDomItem.value = items[0].formData[key]; + form.appendChild(formDataDomItem); + } + } + } + + // 生成iframe元素,并将form表单和iframe元素结合 + const iframe: any = document.createElement('iframe'); + const name: string = Util.getUniqueId('tiFileIframe'); // 确保iframe唯一性,保证各文件上传最终能独立返回到相应的iframe + iframe.setAttribute('name', name); + form.setAttribute('action', items[0].url); + form.setAttribute('method', items[0].method); + form.setAttribute('target', name); + form.setAttribute('enctype', 'multipart/form-data'); + form.appendChild(iframe); + + // 表单提交 + form.submit(); + // 设置对象的上传状态 + items.forEach((item: TiFileItem) => { + item.isUploading = true; + }); + + // 设置假进度 + let newProgress: number = 0; + const progressInterval: any = setInterval(() => { + if (newProgress !== 98) { + newProgress += 2; + this.onProgress(items, newProgress); + } + }, 10); + + // 表单完成事件 + const loadEvent: () => void = (): void => { + let response: string = ''; + let status: number = 200; + + clearInterval(progressInterval); + this.onProgress(items, 100); + try { + response = iframe.contentDocument.body.innerHTML; // 后台正常返回情况获取返回结果 + this.onSuccess(items, response, status); + } catch (e) { + response = e; + status = 520; // 为方便使用者处理,失败情况下,统一返回520状态码 未知错误 + this.onError(items, response, status); + } + + this.onComplete(items, response, status); + }; + iframe.addEventListener('load', loadEvent); + + // 表单取消方法定义 + form.abort = (): void => { + clearInterval(progressInterval); + iframe.removeEventListener('load', loadEvent); // 去除load事件 + // 表单元素还原 + for (const node of form.childNodes) { + if ((node as any).tagName !== 'IFRAME') { + if (form.parentNode.lastChild === form) { + form.parentNode.appendChild(node); + } else { + form.parentNode.insertBefore(node, form.nextSibling); + } + } + } + form.remove(); + items.forEach((item: TiFileItem) => { + item._form = null; + }); + + const status: number = 0; + const response: string = undefined; + this.onCancel(items, response, status); + this.onComplete(items, response, status); + }; + } + + private static isSuccessCode(status: number): boolean { + // 和ajax请求一致,304 代表客户端已经执行了GET,但文件未变化 + return (status >= 200 && status < 300) || status === 304; + } + + /** + * 上传前处理 + * 上传一个或多个文件item对象 + */ + private static onBeforeSend(items: Array): void { + // 设置当前上传文件的状态信息 + for (const item of items) { + item.isReady = true; + item.isUploading = false; + item.isUploaded = false; + item.isSuccess = false; + item.isError = false; + item.isCancel = false; + item.progress = 0; + } + TiUploadUtil.handleEvent('BeforeSend', items); + } + + private static handleEvent(type: string, items: Array, params: Array = []): void { + const onEventTypeItems: any = items[0].uploader.config && items[0].uploader.config[`on${type}Items`]; + if (onEventTypeItems && typeof onEventTypeItems === 'function') { + onEventTypeItems.apply(null, [items, ...params]); + } + // onpush模式下文件上传实例状态未及时刷新 + if (items[0].uploader._uploadComponentInstance) { + items[0].uploader._uploadComponentInstance.changeDetectorRef.markForCheck(); + } + } + + private static onProgress(items: Array, progress: number): void { + TiUploadUtil.handleEvent('Progress', items, [progress]); + for (const item of items) { + item.progress = progress; // 设置单个文件的进度信息 + } + } + + private static onSuccess(items: Array, response: string, status: number): void { + // 设置当前上传文件的状态信息 + for (const item of items) { + item.isReady = false; + item.isUploading = false; + item.isUploaded = true; + item.isSuccess = true; + item.isError = false; + item.isCancel = false; + item.progress = 100; + } + TiUploadUtil.handleEvent('Success', items, [response, status]); + } + + private static onError(items: Array, response: string, status: number): void { + // 设置当前上传文件的状态信息 + for (const item of items) { + // 设置当前上传文件的状态信息 + item.isReady = false; + item.isUploading = false; + item.isUploaded = true; + item.isSuccess = false; + item.isError = true; + item.isCancel = false; + item.progress = 0; + } + TiUploadUtil.handleEvent('Error', items, [response, status]); + } + + private static onComplete(items: Array, response: string, status: number): void { + // 置位状态 + items.forEach((item: TiFileItem) => { + item.isUploading = false; + }); + // 上传下一个文件,对于同一序列的上传文件来说,其isReady状态均已被标识,因此此处可以通过该方法进行筛选 + const readyItemsArr: Array = items[0].uploader.getReadyItems(); + if (readyItemsArr && readyItemsArr.length !== 0) { + this.uploadItems([readyItemsArr[0]]); + } else { + // 本次上传序列中,所有文件上传完成后回调,因为文件上传是串行上传: + // 在一个文件上传完成后才执行下一个文件的上传操作,所以此处可以确保同一序列最终的上传完成 + TiUploadUtil.handleEvent('CompleteAll', items, [response, status]); + } + TiUploadUtil.handleEvent('Complete', items, [response, status]); + } + + private static onCancel(items: Array, response: string, status: number): void { + for (const item of items) { + item.isReady = false; + item.isUploading = false; + item.isUploaded = false; + item.isSuccess = false; + item.isError = false; + item.isCancel = true; + item.progress = 0; + } + TiUploadUtil.handleEvent('Cancel', items, [response, status]); + } + + private static onRemove(items: Array): void { + TiUploadUtil.handleEvent('Remove', items); + } + + private static chunkedFilePromise(items: Array, fileChunkItem: Array, totalNum: number): any { + const uploadDataObj: TiFileInfo = TiUploadUtil.generateUploadData(fileChunkItem); // 组装上传对象 + // 生成并设置xhr + const xhr: any = new XMLHttpRequest(); + + return new Promise((resolve: any): void => { + if (fileChunkItem[0].index === totalNum - 1) { + // 上传是并发上传的没法知道多个请求的进度,暂时以发起的倒数第二个请求的进度作为总体的进度吧 + xhr.upload.onprogress = (event: any): void => { + // 文件进度获取事件 + const { lengthComputable, loaded, total } = event; + const progress: number = Math.round(lengthComputable ? (loaded * 100) / total : 0); // 读取当前进度信息 + TiUploadUtil.onProgress(items, progress); + }; + } + + xhr.onload = (): void => { + // 上传完成事件 + const { response, status } = xhr; + resolve({ fileChunkItem, response, status }); + }; + + xhr.onerror = (): void => { + // 上传失败事件 + const { response, status } = xhr; + resolve({ fileChunkItem, response, status }); + }; + + xhr.onabort = (): void => { + // 取消回调,在请求发送时可能因为浏览器导致请求未发出,此时 reponse 也为''为了区分这里传出一个自定义的值 + const response: string = 'tiCancel'; + const status: number = xhr.status; + resolve({ fileChunkItem, response, status }); + }; + // 开始上传 + xhr.open(fileChunkItem[0].method, fileChunkItem[0].url, true); + TiUploadUtil.setheaders(xhr, fileChunkItem); + xhr.send(uploadDataObj); + fileChunkItem[0]._xhr = xhr; + }); + } + + private static uploadChunkedFile(items: Array, chunkSize: number): void { + const file: File = items[0]._file; // 拿到文件H5下存在_file;不考虑 ie 下_input + const { name, type, size } = file; + const fileTime: number = new Date().getTime(); + const fileChunkList: Array = []; + let fileSliceStart: number = 0; + let chunkedFileIndex: number = 0; + const chunks: number = Math.ceil(size / chunkSize); + while (fileSliceStart < file.size) { + chunkedFileIndex = chunkedFileIndex + 1; + const slicedFile: Blob = file.slice(fileSliceStart, fileSliceStart + chunkSize); + const newChunkFile: File = new File([slicedFile], `${fileTime}-${chunkedFileIndex}/${chunks}-${name}`, { type }); + const fileItem: TiChunked = { + url: items[0].url, + file: TiFileItemUtil.createFileObject(newChunkFile), + alias: items[0].alias, + _file: newChunkFile, + formData: items[0].formData || {}, + formDataFirst: items[0].formDataFirst, + headers: items[0].headers, + method: items[0].method, + _xhr: {}, + index: chunkedFileIndex + }; + fileSliceStart += chunkSize; + fileChunkList.push(fileItem); + } + TiUploadUtil.uploadChunkedXhr(items, fileChunkList); + } + + private static uploadChunkedXhr(items: Array, fileChunkList: Array): void { + items[0].isUploading = true; + let isSuccess: boolean = true; + let currentFileIndex: number = 0; + const source: Observable = of(...fileChunkList); + const chunkedUploadSub: Observable = source.pipe( + map((fileChunkItem: TiChunked) => TiUploadUtil.chunkedFilePromise(items, [fileChunkItem], fileChunkList.length)), + mergeAll() + ); + chunkedUploadSub.subscribe((val: any) => { + currentFileIndex++; + if (isSuccess && currentFileIndex === fileChunkList.length && TiUploadUtil.isSuccessCode(val.status)) { + // 如果之前都是成功的并且是最后一次且当次也成功 那么都上传成功 走 success 回调 + TiUploadUtil.onSuccess(items, val.response, val.status); + TiUploadUtil.onComplete(items, val.response, val.status); + } else if (isSuccess && val.response === 'tiCancel') { + // 如果之前都是成功的且触发了取消;那么是因为主动取消上传;走 cancel 的回调;这里需要修改标记值保证只有第一次取消才触发 + isSuccess = false; + TiUploadUtil.onCancel(items, '', val.status); + TiUploadUtil.onComplete(items, '', val.status); + } else if (isSuccess && !TiUploadUtil.isSuccessCode(val.status)) { + // 如果之前都是成功的并且上传状态码不是正确的那么上传失败;一旦失败修改标记值 走 error 回调并取消所有上传 + isSuccess = false; + TiUploadUtil.onError(items, val.response, val.status); + TiUploadUtil.onComplete(items, val.response, val.status); + // 有失败且不是最后一项取消上传 + currentFileIndex !== fileChunkList.length && TiUploadUtil.cancelFileChunkList(fileChunkList); + } + // 这里还应该有其他情况但不需要处理,暂时考虑以下两种: + // 1.因为 error 所以 cancel ,此时 isSuccess 为 false + // 2.因为 cancel 所以 cancel ,此时 isSuccess 为 false + }); + TiUploadUtil.replaceFileMethod(items, fileChunkList); + } + + // 这里替换掉原有 items[0] 的 cancel/remove/upload 方法;因为应该触发的不是原文件而是分片之后的文件队列 + private static replaceFileMethod(items: Array, fileChunkList: Array): void { + // 覆盖原有取消方法,原有方法不是箭头函数 this 指向items[0]这个对象本身;所以以下也用funcation去定义 + items[0].cancel = function (): void { + items[0].isUploading && TiUploadUtil.cancelFileChunkList(fileChunkList); + TiUploadUtil.onCancel(items, fileChunkList[0]._xhr.response, fileChunkList[0]._xhr.status); + TiUploadUtil.onComplete(items, fileChunkList[0]._xhr.response, fileChunkList[0]._xhr.status); + }; + // 覆盖原有移除方法 + items[0].remove = function (isRemove?: boolean): void { + if (!this.allowDelete) { + return; + } + if (isRemove) { + // 触发外部定义的删除前的事件 + TiUploadUtil.onBeforeRemove([this]); + + return; + } + // 阻止文件上传进程 + items[0].isUploading && TiUploadUtil.cancelFileChunkList(fileChunkList); + items[0].uploader.queue.splice(TiUploadUtil.getItemIndex(items[0]), 1); + items[0].destroy(); + TiUploadUtil.onRemove(items); + }; + // 覆盖原有上传方法 + items[0].upload = function (): void { + if (!this.allowReload) { + return; + } + TiUploadUtil.onBeforeSend(items); + TiUploadUtil.uploadChunkedFile(items, items[0].uploader.config.chunkSize); + }; + } + + private static cancelFileChunkList(fileChunkList: Array): void { + fileChunkList.forEach((i: TiChunked) => { + i._xhr.abort(); + }); + } +} + +// 单个文件对象服务封装,包括文件基本信息统一、文件状态信息、文件基本方法封装 +/** + * @ignore + */ +export class TiFileItemUtil { + /** + * 创建文件对象,该返回值用作后续文件的上传和文件操作,可作为文件回调参数传递 + * fileObject 文件信息对象 + * fileOrInpt 原始文件对象 H5下为file对象,非H5下为fileInput + * config 文件上传配置信息 + * uploader 上传文件对应的文件实例 + * return 文件对象 + */ + public static createFileItem( + tifileObject: TiFileInfo, + fileOrInput: Element | File, + config: TiUploadConfig, + uploader: TiUploadRef + ): TiFileItem { + function upload(): void { + if (!this.allowReload) { + return; + } + TiUploadUtil.uploadItems([this]); + } + function cancel(): void { + TiUploadUtil.cancelItems([this]); + } + + function remove(isRemove?: boolean): void { + if (!this.allowDelete) { + return; + } + if (isRemove) { + // 触发外部定义的删除前的事件 + TiUploadUtil.onBeforeRemove([this]); + + return; + } + + TiUploadUtil.removeItems([this]); + } + + function destroy(): void { + if (this._input) { + // 删除页面残留dom + this._input.remove(); + } + if (this._form) { + this._form.remove(); + } + this._input = null; // 清除引用 + this._form = null; + } + + // 上传文件对象赋值 + let _file: any = null; + let _input: any = null; + if (!(fileOrInput instanceof Element)) { + _file = fileOrInput; + } else { + _input = fileOrInput; + } + + return { + url: config.url || '/', // 后台地址 + file: tifileObject, + alias: config.alias || 'tiFile', // 文件name + _file, + _input, + formData: config.formData || {}, + formDataFirst: config.formDataFirst || false, + headers: config.headers || {}, + method: config.method || 'post', + batchSend: config.batchSend || false, + uploader, // 上传实例对象 + + upload, + cancel, + remove, + destroy, + + isReady: false, + isUploading: false, + isUploaded: false, + isCancel: false, + isSuccess: false, + isError: false, + progress: 0, + + isHover: false, + isOverflow: false, + _xhr: {}, + index: 0, + _form: undefined + }; + } + + /** + * 创建文件对象,统一H5和非H5情况下的文件信息 + * file {FileList|FileInput} 原始文件对象 H5为file对象形式,非H5为fileinput对象 + * return 类文件对象 + */ + public static createFileObject(fileThis: any): TiFileInfo { + if (TiUploadUtil.isHTML5) { + const fileName: string = fileThis.name; + + return { + lastModifiedDate: fileThis.lastModifiedDate, + size: fileThis.size, // 读取的文件真实大小值,单位为B + sizeWithUnit: TiFileItemUtil.formatSize(fileThis.size), // 做单位转换后的文件大小,方便界面详情显示显示 + name: fileThis.name, + type: fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase(), // 确保浏览器形式的一致性 + _file: fileThis, // 文件对象,只在H5方式下有效 + _input: null // 文件input对象,只在IE9 form表单提交方式下有效 + }; + } + const path: string = fileThis.value; + // 非H5情况下,浏览器不打开ActiveX,获取不到文件大小,该种情况下文件大小为null + const fileSize: number = Util.isUndefined(fileThis.size) ? null : fileThis.size; + + return { + lastModifiedDate: null, // 非H5情况下,获取不到该值 + size: fileSize, + sizeWithUnit: isNaN(fileSize) ? '' : this.formatSize(fileSize / 1024), // 做单位转换后的文件大小,方便界面详情显示显示 + name: path.slice(path.lastIndexOf('/') + path.lastIndexOf('\\') + 2), + type: path.slice(path.lastIndexOf('.') + 1).toLowerCase(), + _file: null, // 文件对象,只在H5方式下有效 + _input: fileThis // 文件input对象,只在IE9 form表单提交方式下有效 + }; + } + + /** + * 将文件大小显示标准化:根据文件大小做不同单位的显示,文件大小保留两位小数 + * 文件大小 + * 带单位文件大小 + */ + private static formatSize(size: number): string { + let sizeWithUnit: string; + const kbSize: number = size / 1024; + if (kbSize < 1) { + sizeWithUnit = size.toFixed(2) + 'B'; + } else if (kbSize < 1024) { + sizeWithUnit = kbSize.toFixed(2) + 'KB'; + } else if (kbSize < 1024 * 1024) { + sizeWithUnit = (kbSize / 1024).toFixed(2) + 'MB'; + } else { + sizeWithUnit = (kbSize / 1024 / 1024).toFixed(2) + 'GB'; + } + + return sizeWithUnit; + } +} diff --git a/src/upload/lib/src/TiUploadbaseComponent.ts b/src/upload/lib/src/TiUploadbaseComponent.ts new file mode 100644 index 0000000..c1678ef --- /dev/null +++ b/src/upload/lib/src/TiUploadbaseComponent.ts @@ -0,0 +1,401 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ElementRef, EventEmitter, Input, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core'; +import { TiFileInfo, TiFileItem, TiFilter, TiUploadConfig, TiUploadRef } from './TiFileInterface'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiBrowser, Util } from '@opentiny/ng-utils'; +import packageInfo from '../package.json'; +/** + * 初始化显示的文件列表项目(只适用于button/textButton类型) + */ +export interface TiUploadInitFile { + /** + * 文件名 + */ + name: string; + /** + * 操作按钮(删除按钮)是否禁用 + */ + allowDelete: boolean; + /** + * 删除该文件方法 + * + * **参数:**无 + * + * **返回值:**void + */ + remove?(): void; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} + +/** + * 初始化显示的已上传的图片文件信息 + * + * + */ +export interface TiUploadimageInitFile { + /** + * 图片文件名 + */ + name: string; + /** + * 图片预览地址 + */ + previewUrl?: string; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} + +/** + * + * upload基类,继承于TiBaseComponent组件 + */ +@Component({ + selector: 'ti-uploadbase', + template: '' +}) +export class TiUploadbaseComponent extends TiBaseComponent implements OnInit { + /** + * 必选,上传地址 + */ + @Input() url: string; + /** + * 上传方式 + */ + @Input() method: 'get' | 'post' = 'post'; + /** + * 自定义过滤器 + */ + @Input() filters: Array; + /** + * 上传的文件字段名 + */ + @Input() alias: string = 'tiFile'; + /** + * 上传文件的附带信息 + */ + @Input() formData: object; + /** + * 上传对象的附带信息是否先于file对象 + */ + @Input() formDataFirst: boolean = false; + /** + * 是否自动上传 + */ + @Input() autoUpload: boolean = true; + /** + * 是否一次上传多个文件。默认情况下,一次上传一个文件 + */ + @Input() batchSend: boolean = false; + /** + * + * 上传文件请求头配置,自定义为对象形式 + * + * 设置'X-Requested-With'可以判断是Ajax请求还是其他请求;'X-Requested-With':'XMLHttpRequest'返回数据是json,不设置返回普通html文本 + */ + /** + * 上传文件请求头 + */ + @Input() headers: object; + // 文件类型过滤配置,适用于IE10+浏览器,能做到文件弹窗中不出现非法类型的文件,主要适用于两种场景: + // 1.过滤规则中带类型过滤,但是想要去掉文件选择框中文件过滤的场景(设置accept为空字符串即可):此种场景应用于浏览器设置accept后导致文件窗打开很慢的情况 + // 2.不需要定义文件类型过滤条件,但是通过accept能精确的确保文件类型的场景 + /** + * 限制文件上传类型。设置此属性后,选择文件时的资源管理窗口将自动隐藏不符合规则的文件 + */ + @Input() accept: string; + + /** + * 是否禁用 + */ + @Input() disabled: boolean = false; + + /** + * 分片大小,设置后开启分片上传单位是 b + */ + @Input() chunkSize: number; + + /** + * @ignore + */ + title: string = TiBrowser.isChrome() ? ' ' : ''; // 禁用文件上传默认tip提示,Chrome下设置title为""时会显示默认提示,所以Chrome下需要设置为" ";而IE下设置为" "会显示文本为空的tip + + // 文件添加失败回调,可使用该回调定义上传错误提示 + // 参数为对象形式,结构如下: + // { + // file: [TiFileInfo]{@link ../interfaces/TiFileInfo.html}, // 上传文件信息 + // validResults: Array<string> // 校验不合法的规则name数组 + // } + /** + * 文件添加失败时触发的回调,可使用该回调定义上传错误提示,参数:文件对象 + */ + @Output() readonly addItemFailed: EventEmitter<{ + file: TiFileInfo; + validResults: Array; + }> = new EventEmitter<{ + file: TiFileInfo; + validResults: Array; + }>(); + /** + * 文件添加成功时触发的回调,参数:文件对象 + */ + @Output() readonly addItemSuccess: EventEmitter = new EventEmitter(); + // 以下回调对单个或多个文件在同一url同时上传有效,可使用其进行文件上传过程中的业务处理 + /** + * 上传文件前触发的回调,可在该回调中动态设置formData,参数:文件对象列表 + */ + @Output() readonly beforeSendItems: EventEmitter> = new EventEmitter>(); + // 上传文件进度改变回调 + // 该回调对单个或多个文件在同一url同时上传有效,可使用其进行文件上传过程中的业务处理 + // 参数为对象形式,结构如下: + // { + // fileItems: Array<[TiFileItem]{@link ../interfaces/TiFileItem.html}>, // 上传文件对象数组 + // progress: number // 进度信息 + // } + /** + * 上传进度改变时触发的回调,参数:文件对象 + */ + @Output() readonly progressItems: EventEmitter<{ + fileItems: Array; + progress: number; + }> = new EventEmitter<{ + fileItems: Array; + progress: number; + }>(); + // 文件上传完成回调,成功/失败都会触发 + // 该回调对单个或多个文件在同一url同时上传有效,可使用其进行文件上传过程中的业务处理 + // 参数为对象形式,结构如下: + // { + // fileItems: Array<[TiFileItem]{@link ../interfaces/TiFileItem.html}>, // 上传文件对象数组 + // response: string // 文件上传响应信息 + // status: number 文件上传响应状态码 + // } + /** + * 上传完成(成功或失败)时触发的回调,参数:文件对象 + */ + @Output() readonly completeItems: EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }> = new EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }>(); + // 文件上传成功回调 + // 该回调对单个或多个文件在同一url同时上传有效,可使用其进行文件上传过程中的业务处理 + // 参数为对象形式,结构如下: + // { + // fileItems: Array<[TiFileItem]{@link ../interfaces/TiFileItem.html}>, // 上传文件对象数组 + // response: string // 文件上传响应信息 + // status: number 文件上传响应状态码 + // } + /** + * 文件上传成功时触发的回调,参数:文件对象 + */ + @Output() readonly successItems: EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }> = new EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }>(); + // 文件上传取消回调 + // 该回调对单个或多个文件在同一url同时上传有效,可使用其进行文件上传过程中的业务处理 + // 参数为对象形式,结构如下: + // { + // fileItems: Array<[TiFileItem]{@link ../interfaces/TiFileItem.html}>, // 上传文件对象数组 + // response: string // 文件上传响应信息 + // status: number 文件上传响应状态码 + // } + /** + * 文件上传失败时触发的回调,参数:文件对象 + */ + @Output() readonly errorItems: EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }> = new EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }>(); + /** + * @ignore + * 文件上传取消回调 + * + * 该回调对单个或多个文件在同一url同时上传有效,可使用其进行文件上传过程中的业务处理 + * + * 参数为对象形式,结构如下: + * + * { + * + * fileItems: Array<[TiFileItem]{@link ../interfaces/TiFileItem.html}>, // 上传文件对象数组 + * + * response: string // 文件上传响应信息 + * + * status: number 文件上传响应状态码 + * + * } + */ + /** + * 文件上传取消时触发的回调,参数:文件对象 + */ + @Output() readonly cancelItems: EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }> = new EventEmitter<{ + fileItems: Array; + response: string; + status: number; + }>(); + /** + * 上传删除时触发的回调,参数:被删除的文件对象列表 + */ + @Output() readonly removeItems: EventEmitter> = new EventEmitter< + Array + >(); + /** + * 所有文件上传完成时触发的回调,参数:文件对象列表 + */ + @Output() readonly completeAllItems: EventEmitter> = new EventEmitter>(); + /** + * 上传文件删除前触发的回调,参数:将被删除的文件对象列表 + */ + @Output() readonly beforeRemoveItems: EventEmitter> = new EventEmitter< + Array + >(); + + /** + * 上传实例配置信息 + */ + public uploadConfig: TiUploadConfig; + + /** + * @ignore 上传文件实例 + */ + public uploadInst: TiUploadRef; + + /** + * @ignore 是否定义beforeRemoveItems事件 + */ + isRemove: boolean; + + /** + * @ignore 词条 + */ + protected versionInfo: string = super.getVersion(packageInfo); + public uploadLan = TiLocale.getLocaleWords().tiUpload; + + constructor(protected hostRef: ElementRef, protected renderer: Renderer2) { + super(hostRef, renderer); + } + ngOnChanges(changes: SimpleChanges): void { + // 如果uploadConfig存在,并监听到url和headers变化 + if (this.uploadConfig) { + // 由于uploadConfig的引用地址与实例对象的config相同,更新到实例对象的config中 + if (changes['url']) { + this.uploadConfig.url = this.url; + } + if (changes['headers']) { + this.uploadConfig.headers = this.headers; + } + } + } + ngOnInit(): void { + super.ngOnInit(); + this.isRemove = this.beforeRemoveItems.observers.length !== 0; + this.uploadConfig = { + url: this.url, // 文件上传地址配置 + method: this.method, // 上传方式,可选值为:get、post(其他方式IE9不支持) + filters: this.filters, // 文件有效性判断条件数组 + alias: this.alias, // 向后台发送请求时对应的文件对象name属性,该name属性是后台读取文件的入口值,默认值为 'tiFile' + formData: this.formData, // 上传文件附带信息 + autoUpload: this.autoUpload, // 是否自动上传,默认为true + batchSend: this.batchSend || false, // 是否一次性上传多个文件 + formDataFirst: this.formDataFirst || false, // formData是否先于file对象 + headers: this.headers, // 上传文件请求头配置 + chunkSize: this.chunkSize, // 分片大小,设置后开启分片上传单位是 b + onAddItemFailed: (file: TiFileInfo, validResults: Array): void => { + this.addItemFailed.emit({ file, validResults }); + }, + onAddItemSuccess: (fileItem: TiFileItem): void => { + // 默认删除、禁用图标为非禁用状态 + fileItem.allowDelete = true; + fileItem.allowReload = true; + this.addItemSuccess.emit(fileItem); + }, + onBeforeSendItems: (fileItems: Array): void => { + this.beforeSendItems.emit(fileItems); + }, + onProgressItems: (fileItems: Array, progress: number): void => { + this.progressItems.emit({ fileItems, progress }); + }, + onCompleteItems: (fileItems: Array, response: string, status: number): void => { + this.completeItems.emit({ fileItems, response, status }); + }, + onSuccessItems: (fileItems: Array, response: string, status: number): void => { + this.successItems.emit({ fileItems, response, status }); + }, + onErrorItems: (fileItems: Array, response: string, status: number): void => { + this.errorItems.emit({ fileItems, response, status }); + }, + onCancelItems: (fileItems: Array, response: string, status: number): void => { + this.cancelItems.emit({ fileItems, response, status }); + }, + onRemoveItems: (fileItems: Array): void => { + this.removeItems.emit(fileItems); + }, + onBeforeRemoveItems: (fileItems: Array): void => { + this.beforeRemoveItems.emit(fileItems); + }, + onCompleteAllItems: (fileItems: Array): void => { + this.completeAllItems.emit(fileItems); + } + }; + // 根据配置的文件类型过滤属性设置H5下的文件过滤属性,如果accept未定义,则按照type过滤条件进行设置; + // 如果accept已定义,则按照accept定义设置:此种场景应用于浏览器设置accept后导致文件窗打开很慢的情况 + if (Util.isUndefined(this.accept) && this.filters && this.filters.length) { + this.filters.forEach((filter: TiFilter) => { + if (filter.name === 'type') { + this.accept = filter.params[0]; + } + + return; + }); + } + } + /** + * 手动上传,可通过组件调用 + */ + public upload(): void { + this.uploadInst.uploadAll(); + } + + /** + * @ignore 获取maxCount条件下标 + */ + public getMaxCountIndex(): number { + return this.uploadConfig.filters?.findIndex((filter: TiFilter) => { + return filter.name === 'maxCount'; + }); + } +} diff --git a/src/upload/lib/src/TiUploadimageComponent.ts b/src/upload/lib/src/TiUploadimageComponent.ts new file mode 100644 index 0000000..7d27c83 --- /dev/null +++ b/src/upload/lib/src/TiUploadimageComponent.ts @@ -0,0 +1,383 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + Input, + NgZone, + Output, + Renderer2, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { TiFileInfo, TiFileItem, TiFilter } from './TiFileInterface'; +import { TiUploadService } from './TiUploadService'; +import { TiModalService } from '@opentiny/ng-modal'; +import { TiFilePreviewInfo, TiImagepreviewComponent } from '@opentiny/ng-imagepreview'; +import { TiUploadbaseComponent, TiUploadimageInitFile } from './TiUploadbaseComponent'; +import { DomSanitizer } from '@angular/platform-browser'; + +/** + * + * 该组件基于TiUploadService和已设计好的交互规范,实现上传图片功能(但不限于上传图片),包含两种样式: + * + * 1.带上传结果展示的样式(type为block) + * + * 2.不展示上传结果的样式(type为drag) + * + * 负责人:谭莉 + */ +@Component({ + selector: 'ti-upload[type="block"], ti-upload[type="drag"]', + templateUrl: './uploadimage.html', + styleUrls: ['./uploadimage.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiUploadimageComponent extends TiUploadbaseComponent { + /** + * 上传文件的最大数量 + */ + @Input() maxCount: number; + + /** + * 上传成功后是否可删除 + */ + @Input() deletable: boolean = true; + + /** + * 必选,上传按钮样式 + * @customType 'block' + */ + @Input() type: 'block' | 'drag'; + + /** + * 初始时显示已上传的图片列表 + */ + @Input() initFiles: Array; + + /** + * 图片预览时弹出框样式 + */ + @Input() modalClass: string = 'ti3-image-preview-modal'; + + /** + * @ignore + * 上传结果展示,目前只有一种,picture-card; + * 当type设置为drag时无效 + */ + listType: string = 'picture-card'; + + /** + * 文件上传数量达到上限时触发的回调,参数:文件预览信息列表 + */ + @Output() readonly uploadLimit: EventEmitter> = new EventEmitter>(); + + /** + * @ignore 内部变量 + */ + @ViewChild('uploadBtn', { static: false }) uploadBtn: ElementRef; + /** + * @ignore + * 上传组件对应的input元素 + */ + @ViewChild('uploadInput', { static: false }) uploadInputEle: ElementRef; + /** + * 自定义上传样式 + * @ignore + */ + @ContentChild('uploadContainer', { static: true }) + uploadContainerTemplate: ElementRef; + + /** + * 上传按钮的宽度,只适用于 type 为 drag 的上传按钮 + * @ignore + */ + @Input() uploadBtnWidth: string = '100px'; + + /** + * 上传按钮的高度,只适用于 type 为 drag 的上传按钮 + * @ignore + */ + @Input() uploadBtnHeight: string = '100px'; + + /** + * @ignore 需要展示的文件列表 + */ + fileList: Array = []; + constructor( + private uploaderService: TiUploadService, + hostEle: ElementRef, + renderer: Renderer2, + private tiModal: TiModalService, + public sanitizer: DomSanitizer, + private zone: NgZone, + public changeDetectorRef: ChangeDetectorRef + ) { + super(hostEle, renderer); + } + + /** + * @ignore + */ + ngOnInit(): void { + super.ngOnInit(); + // 设置上传实例配置 + this.setUploadConfig(); + // 创建uploader实例 + this.uploadInst = this.uploaderService.create(this.uploadConfig, this); + if (this.initFiles && this.maxCount) { + // 在创建服务实例时,如果存在特殊情况最大数量-初始化文件长度=1时,service会判断文件为单文件上传,但实际上场景应该为多文件上传 + // 因此在setUploadConfig时根据实际传入的maxcount设置一次,在实例判断是否是单文件上传之后再修改为还允许上传的文件数 + const maxCountIndex: number = this.getMaxCountIndex(); + if (maxCountIndex !== -1) { + this.uploadConfig.filters[maxCountIndex].params[0] = this.maxCount - this.initFiles.length; + } + } + if (this.type === 'drag') { + this.listType = ''; + } + } + + ngOnChanges(changes: SimpleChanges): void { + super.ngOnChanges(changes); + if (changes['initFiles']) { + if (changes['initFiles'].currentValue?.length > 0) { + this.setInitFiles(); + } + if (!changes['initFiles'].firstChange) { + const maxCountIndex: number = this.getMaxCountIndex(); + if (maxCountIndex !== -1) { + this.uploadConfig.filters[maxCountIndex].params[0] = this.maxCount - this.initFiles.length; + } + } + } + } + + ngAfterViewChecked(): void { + super.ngAfterViewChecked(); + // 拖拽添加/上传时,设置上传按钮的高度和宽度 + if (this.uploadBtn && this.type === 'drag') { + this.renderer.setStyle(this.uploadBtn.nativeElement, 'width', this.uploadBtnWidth); + this.renderer.setStyle(this.uploadBtn.nativeElement, 'height', this.uploadBtnHeight); + } + } + + /** + * 重新设置上传实例配置 + */ + private setUploadConfig(): void { + this.setFilters(); + this.uploadConfig.onAddItemFailed = (file: TiFileInfo, validResults: Array): void => { + // 文件上传达上限,触发回调 + if (validResults.includes('maxCount')) { + this.uploadLimit.emit(this.fileList); + } + this.addItemFailed.emit({ file, validResults }); + }; + this.uploadConfig.onAddItemSuccess = (fileItem: TiFileItem): void => { + // 为了兼容适配 TiUploadComponent 对相关 allowDelete 和 allowReload 的处理 + fileItem.allowDelete = true; + fileItem.allowReload = true; + this.addPreviewList(fileItem); + this.addItemSuccess.emit(fileItem); + }; + this.uploadConfig.onSuccessItems = (fileItems: Array, response: string, status: number): void => { + this.setPreviewInfo(fileItems[0]); + this.successItems.emit({ fileItems, response, status }); + }; + this.uploadConfig.onRemoveItems = (fileItems: Array): void => { + this.removePreviewList(fileItems[0]); + this.removeItems.emit(fileItems); + }; + } + + /** + * 将初始时要显示的已上传的图片文件添加到预览列表 + */ + private setInitFiles(): void { + this.initFiles.forEach((item: TiUploadimageInitFile) => { + const file: TiFilePreviewInfo = { + imgFileItem: { isSuccess: true, isUploaded: true }, + name: item.name, + previewUrl: item.previewUrl, + isImage: this.isImage(item.name, item.previewUrl), + fromInitFiles: true + }; + item.remove = (): void => { + const index: number = this.fileList.findIndex((fileItem: TiFilePreviewInfo): boolean => { + return fileItem === file; + }); + if (index !== -1) { + this.fileList.splice(index, 1); + this.changeDetectorRef.markForCheck(); + } + }; + file.imgFileItem.remove = (): void => { + // 获取需要删除的数据的索引 + const deleteIndex: number = this.fileList.findIndex((fileItem: TiFilePreviewInfo): boolean => { + return fileItem === file; + }); + const maxCountIndex: number = this.getMaxCountIndex(); + if (maxCountIndex !== -1) { + this.uploadConfig.filters[maxCountIndex].params[0] += 1; + } + if (this.isRemove) { + this.beforeRemoveItems.emit([item]); + } else { + this.fileList.splice(deleteIndex, 1); + this.removeItems.emit([item]); + } + }; + this.fileList.push(file); + }); + } + + /** + * 设置过滤条件 + */ + private setFilters(): void { + let uploadFilters: Array = this.filters || []; + if (typeof this.maxCount === 'number' && this.maxCount > 0) { + uploadFilters = uploadFilters.concat({ + name: 'maxCount', + params: [this.maxCount] + }); + } + this.uploadConfig.filters = uploadFilters; + } + + /** + * 添加文件成功后,增加一个预览对象 + */ + private addPreviewList(fileItem: TiFileItem): void { + const imgInfoItem: TiFilePreviewInfo = { + imgFileItem: fileItem, + name: fileItem._file.name, + previewUrl: '', + isImage: false + }; + this.fileList.push(imgInfoItem); + } + + /** + * 删除时,将文件信息从预览列表中移除 + */ + private removePreviewList(fileItem: TiFileItem): void { + // 获取需要删除的数据的索引 + const deleteIndex: number = this.fileList.findIndex((item: TiFilePreviewInfo): boolean => { + return fileItem._file === item.imgFileItem._file; + }); + this.fileList.splice(deleteIndex, 1); + } + + /** + * 上传成功后,更新预览地址 + */ + private setPreviewInfo(fileItem: TiFileItem): void { + // 生成预览url + const url: string = window.URL.createObjectURL(fileItem._file); + const isImagePromise: Promise = this.isImage(fileItem._file.name, url); + isImagePromise.then((result: boolean) => { + this.fileList.forEach((item: TiFilePreviewInfo): void => { + if (item.imgFileItem._file === fileItem._file) { + item.previewUrl = url; + item.isImage = result; + } + }); + this.changeDetectorRef.markForCheck(); + }); + } + + /** + * @ignore 在弹框中预览上传的图片 + * 参数i: 表示被点击图片的索引 + */ + public preview(i: number): void { + const previewList: Array = []; + let firstIndex: number = i; + this.fileList.forEach((item: TiFilePreviewInfo, index: number): void => { + if (item.imgFileItem.isSuccess && item.isImage) { + // 上传成功并且是图片 + previewList.push(item); + } else { + // 非图片或上传失败,并且在被点击的文件之前,第一个预览的索引需要-1 + if (i >= index) { + firstIndex -= 1; + } + } + }); + this.tiModal.open(TiImagepreviewComponent, { + id: 'imagePreviewModal', + modalClass: this.modalClass, + context: { + index: firstIndex, // 当前文件索引 + fileList: previewList // 预览列表 + } + }); + } + + /** + * 判断是否是图片类型 + * 参数fileName: 文件名称 + * 参数previewUrl: 预览地址 + */ + private isImage(fileName: string, previewUrl: string): Promise { + const tempArray: Array = fileName.split('.'); + const extension: string = tempArray[tempArray.length - 1].toLocaleLowerCase(); + const supportTypes: Array = ['jpg', 'png', 'jpeg', 'svg', 'gif', 'bmp']; + + return new Promise((resolve) => { + if (!supportTypes.includes(extension)) { + resolve(false); + + return; + } + this.zone.runOutsideAngular(() => { + // 测试图片能否加载成功,能加载成功,则认为可预览;否则,认为不可预览 + const img: any = document.createElement('img'); + img.src = previewUrl; + img.onerror = (event: any): void => { + resolve(false); + }; + img.onload = (event: any): void => { + resolve(true); + }; + }); + }); + } + + /** + * @ignore 禁用上传时,阻止默认事件、停止冒泡 + */ + selectFile(event: any): void { + if (this.disabled) { + event.stopPropagation(); + event.preventDefault(); + + return; + } + // 焦点转移 + this.uploadBtn.nativeElement.focus(); + } + + /** + * @ignore + */ + + public onKeydown(): void { + this.uploadInputEle.nativeElement.focus(); + } +} diff --git a/src/upload/lib/src/i18n/TiUploadWords.ts b/src/upload/lib/src/i18n/TiUploadWords.ts new file mode 100644 index 0000000..4f8f127 --- /dev/null +++ b/src/upload/lib/src/i18n/TiUploadWords.ts @@ -0,0 +1,21 @@ +export interface TiUploadWords { + tiUpload: { + addFile: string; + error: string; + successInfo: string; + uploadingSingleInfo: string; + errorSingleInfo: string; + addSuccessMutiInfo: string; + uploadingMutiInfo: string; + errorMultiInfo: string; + clearAll: string; + upload: string; + cancel: string; + reload: string; + delete: string; + autoUploadFilePlaceholder: string; + autoUploadFilesPlaceholder: string; + notAutoUploadFilePlaceholder: string; + notAutoUploadFilesPlaceholder: string; + }; +} diff --git a/src/upload/lib/src/i18n/en_US.ts b/src/upload/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..b692119 --- /dev/null +++ b/src/upload/lib/src/i18n/en_US.ts @@ -0,0 +1,23 @@ +import { TiUploadWords } from './TiUploadWords'; + +export const en_US: TiUploadWords = { + tiUpload: { + addFile: 'Select File', // upload_add_file_btn + error: 'Failed to upload the file.', // upload_error_info + successInfo: 'File uploaded successfully.', // upload_success_info + uploadingSingleInfo: 'Uploading', // upload_single_uploading_general_info + errorSingleInfo: 'Failed to upload the file.', // upload_single_error_general_info + addSuccessMutiInfo: 'You have added {0} files.', // upload_add_success_general_info + uploadingMutiInfo: 'Uploading: {0}', // upload_uploading_general_info + errorMultiInfo: 'Failed to upload {0} files.', // upload_error_general_info + clearAll: 'Clear All', // upload_remove_files + upload: 'Upload', // upload_file_btn + cancel: 'Cancel', // upload_cancel_files + reload: 'Upload Again', // upload_reload_files + delete: 'Delete', + autoUploadFilePlaceholder: 'Select a file to upload.', + autoUploadFilesPlaceholder: 'Select files to upload.', + notAutoUploadFilePlaceholder: 'Add a file and upload it.', + notAutoUploadFilesPlaceholder: 'Add files and upload them.' + } +}; diff --git a/src/upload/lib/src/i18n/es_US.ts b/src/upload/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..edcc5af --- /dev/null +++ b/src/upload/lib/src/i18n/es_US.ts @@ -0,0 +1,23 @@ +import { TiUploadWords } from './TiUploadWords'; + +export const es_US: TiUploadWords = { + tiUpload: { + addFile: 'Seleccionar archivo', + error: 'Error al cargar el archivo.', + successInfo: 'Se cargó el archivo.', + uploadingSingleInfo: 'Cargando', + errorSingleInfo: 'Error al cargar el archivo.', + addSuccessMutiInfo: 'Se agregaron {0} archivos.', + uploadingMutiInfo: 'Cargando: {0}', + errorMultiInfo: 'Error al cargar {0} archivos.', + clearAll: 'Eliminar todo', + upload: 'Cargar', + cancel: 'Cancelar', + reload: 'Volver a cargar', + delete: 'Delete', + autoUploadFilePlaceholder: 'Seleccione un archivo para cargar.', + autoUploadFilesPlaceholder: 'Seleccione archivos para cargar.', + notAutoUploadFilePlaceholder: 'Agregue un archivo y cárguelo.', + notAutoUploadFilesPlaceholder: 'Agregue archivos y cárguelos.' + } +}; diff --git a/src/upload/lib/src/i18n/fr_FR.ts b/src/upload/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..942afdf --- /dev/null +++ b/src/upload/lib/src/i18n/fr_FR.ts @@ -0,0 +1,23 @@ +import { TiUploadWords } from './TiUploadWords'; + +export const fr_FR: TiUploadWords = { + tiUpload: { + addFile: 'Ajouter le fichier', + error: 'Échec à télécharger le fichier.', + successInfo: 'Etape réalisée avec succès.', + uploadingSingleInfo: 'Téléchargement', + errorSingleInfo: 'Échec à télécharger le fichier.', + addSuccessMutiInfo: 'Vous avez ajouté {0} fichiers.', + uploadingMutiInfo: 'Téléchargement : {0}', + errorMultiInfo: 'Echec à télécharger {0} fichiers.', + clearAll: 'Tout effacer', + upload: 'Télécharger', + cancel: 'Annuler', + reload: 'Télécharger à nouveau', + delete: 'Delete', + autoUploadFilePlaceholder: 'Sélectionnez un fichier à télécharger.', + autoUploadFilesPlaceholder: 'Sélectionnez les fichiers à télécharger.', + notAutoUploadFilePlaceholder: 'Ajoutez un fichier et le télécharger.', + notAutoUploadFilesPlaceholder: 'Ajoutez des fichiers et les télécharger.' + } +}; diff --git a/src/upload/lib/src/i18n/index.ts b/src/upload/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/upload/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/upload/lib/src/i18n/pt_BR.ts b/src/upload/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..8d5117c --- /dev/null +++ b/src/upload/lib/src/i18n/pt_BR.ts @@ -0,0 +1,23 @@ +import { TiUploadWords } from './TiUploadWords'; + +export const pt_BR: TiUploadWords = { + tiUpload: { + addFile: 'Selecionar arquivo', + error: 'Não foi possível carregar o arquivo.', + successInfo: 'Arquivo carregado com sucesso.', + uploadingSingleInfo: 'Carregando', + errorSingleInfo: 'Não foi possível carregar o arquivo.', + addSuccessMutiInfo: 'Você adicionou {0} arquivos.', + uploadingMutiInfo: 'Carregando: {0}', + errorMultiInfo: 'Não foi possível carregar {0} arquivos.', + clearAll: 'Limpar tudo', + upload: 'Carregar', + cancel: 'Cancelar', + reload: 'Carregar novamente', + delete: 'Delete', + autoUploadFilePlaceholder: 'Selecione um arquivo para ser carregado.', + autoUploadFilesPlaceholder: 'Selecione arquivos para serem carregados.', + notAutoUploadFilePlaceholder: 'Adicione um arquivo e carregue-o.', + notAutoUploadFilesPlaceholder: 'Adicione arquivos e carregue-os.' + } +}; diff --git a/src/upload/lib/src/i18n/zh_CN.ts b/src/upload/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..5b6605f --- /dev/null +++ b/src/upload/lib/src/i18n/zh_CN.ts @@ -0,0 +1,23 @@ +import { TiUploadWords } from './TiUploadWords'; + +export const zh_CN: TiUploadWords = { + tiUpload: { + addFile: '添加文件', + error: '上传失败!', // upload_error_info + successInfo: '上传成功!', + uploadingSingleInfo: '正在上传', + errorSingleInfo: '上传失败!', + addSuccessMutiInfo: '已添加{0}个文件', + uploadingMutiInfo: '{0} 正在上传', + errorMultiInfo: '{0}个文件上传失败!', + clearAll: '清空选择', + upload: '上传文件', + cancel: '取消上传', + reload: '重新上传', + delete: '删除', + autoUploadFilePlaceholder: '点击右侧按钮上传文件', + autoUploadFilesPlaceholder: '点击右侧按钮上传文件', + notAutoUploadFilePlaceholder: '点击右侧按钮先添加再上传', + notAutoUploadFilesPlaceholder: '点击右侧按钮先添加再上传' + } +}; diff --git a/src/upload/lib/src/upload.html b/src/upload/lib/src/upload.html new file mode 100644 index 0000000..e79fc66 --- /dev/null +++ b/src/upload/lib/src/upload.html @@ -0,0 +1,293 @@ +
    + +
    + + {{buttonText || uploadLan.addFile}} +
    +
    + + {{buttonText || uploadLan.addFile}} +
    +
    {{note}}
    +
      + +
    • +
      + + {{item.name}} +
      +
      +
      + + +
      +
      +
    • + + +
    • +
      + + {{item.file.name}} +
      +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + + + +
      +
      + + +
    • +
      +
    +
    +
    + +
    + {{uploadLan.reload}} +
    +
    +
    +
    + +
    + {{uploadLan.reload}} +
    +
    +
    +
    +
    + +
    +
    +
    +
      + +
    • +
      +
      {{item.file.name}}
      +
      + ( + {{item.file.sizeWithUnit}} + ) +
      +
      +
      +
      + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      +
    • +
    • {{placeholder}}
    • +
    +
    + + +
    + + +
    + + + + + + +
    +
    +
    + {{stateInfo}} +
    +
    + {{stateInfo}} +
    {{uploadLan.cancel}}
    +
    +
    + + {{stateInfo}} +
    +
    + + {{stateInfo}} +
    + {{uploadLan.reload}} +
    +
    +
    +
    +
    + {{uploadLan.uploadingSingleInfo}} +
    +
    + + {{uploadLan.successInfo}} +
    +
    + + {{uploadLan.errorSingleInfo}} +
    + {{uploadLan.reload}} +
    +
    +
    +
    + + + + + {{item.file.name}} + diff --git a/src/upload/lib/src/upload.less b/src/upload/lib/src/upload.less new file mode 100644 index 0000000..f857e8c --- /dev/null +++ b/src/upload/lib/src/upload.less @@ -0,0 +1,356 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-upload-container-height: var(--ti-common-size-7x); + --ti-upload-icon-size: var(--ti-common-size-4x); + --ti-upload-file-item-height: 22px; + --ti-upload-item-container-padding-vertical: calc(var(--ti-upload-item-space) / 2); + --ti-upload-item-space: calc(var(--ti-common-space-1) * 2); + --ti-upload-item-height: (var(--ti-upload-container-height) - 2 * (var(--ti-upload-item-space)) - 2px); + --ti-upload-item-operate-container-width: (var(--ti-upload-icon-size) * 2 + var(--ti-common-space-2x) * 2); + --ti-upload-item-file-name-font-size: var(--ti-common-font-size-base); + --ti-upload-msg-font-size: var(--ti-common-font-size-base); + --ti-upload-textbutton-font-size: var(--ti-common-font-size-base); +} + +.ti3-file-btn { + position: relative; + overflow: hidden; +} +.ti3-file-input { + opacity: 0; + z-index: 1; + position: absolute; + font-size: 1000px; + margin: 0; + padding: 0; + cursor: pointer; + bottom: 0; + right: 0; + &[disabled] { + cursor: not-allowed; + } +} +.ti3-file-text { + line-height: calc(var(--ti-upload-container-height) - 2px); +} + +// Aui2文件上传样式 +.ti3-aui-file-disable { + .ti3-aui-file-field-container { + border-color: var(--ti-common-color-line-disabled); + background-color: var(--ti-common-color-bg-disabled); + cursor: not-allowed; + box-sizing: border-box; + } + .ti3-aui-file-select-input { + cursor: not-allowed; + } +} +.ti3-aui-file-container { + font-size: var(--ti-common-font-size-base); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-primary); +} +.ti3-aui-file-upload-container { + .clearfix(); +} +.ti3-aui-file-field-container { + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + background: var(--ti-common-color-bg-white-normal); + position: relative; + float: left; + width: 300px; // 默认宽度 +} +.ti3-aui-file-items-container { + overflow-y: auto; + overflow-x: hidden; + width: calc(100% - 2px); // 多出的2px用于防止缩放情况下右侧块掉下来 + min-height: calc(var(--ti-upload-container-height) - 2px); + max-height: calc((var(--ti-upload-container-height) - 2px) * 2 - var(--ti-upload-item-space)); + padding: var(--ti-upload-item-container-padding-vertical) 0; + .box-sizing(border-box); +} + +.ti3-aui-file-item { + float: left; + .box-sizing(border-box); + background-color: var(--ti-common-color-bg-normal); + max-width: 100%; + height: calc(var(--ti-upload-item-height)); + line-height: calc(var(--ti-upload-item-height)); + padding: 0 var(--ti-common-space-10); + margin: calc(var(--ti-upload-item-space) / 2) 0 calc(var(--ti-upload-item-space) / 2) var(--ti-upload-item-space); + cursor: pointer; + display: flex; + justify-content: space-between; + &.ti3-aui-single-file-item { + width: 100%; + } +} +.ti3-aui-file-name-size-container { + width: calc(100% - var(--ti-upload-item-operate-container-width)); + display: flex; + flex-shrink: 0; // 设置该属性为0,空间不足时,元素宽度不被压缩 +} +.ti3-aui-file-name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + height: 100%; + color: var(--ti-common-color-text-secondary); +} +.ti3-aui-file-size { + color: var(--ti-common-color-text-weaken); + padding-left: var(--ti-common-space-base); + .box-sizing(border-box); + display: flex; + flex-shrink: 0; +} +.ti3-aui-file-item-size { + max-width: 70px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.ti3-aui-file-operates { + width: calc(var(--ti-upload-item-operate-container-width)); +} +.ti3-aui-file-x, +.ti3-aui-file-error-reupload, +.ti3-aui-file-error-icon, +.ti3-aui-file-success { + color: var(--ti-common-color-icon-normal); + font-size: var(--ti-upload-icon-size); + float: right; +} +.ti3-aui-file-success { + color: var(--ti-common-color-success); +} +.ti3-aui-file-error-state .ti3-aui-file-x { + margin-right: var(--ti-common-space-2x); +} +.ti3-aui-file-error-icon { + color: var(--ti-common-color-error); + margin-right: var(--ti-common-space-2x); +} +.ti3-aui-file-progress-pie { + width: var(--ti-upload-icon-size); + height: var(--ti-upload-icon-size); + float: right; + position: relative; + top: calc((var(--ti-upload-item-height) - var(--ti-upload-icon-size)) / 2); +} +.ti3-aui-file-select-input { + opacity: 0; + width: 0; + height: 0; +} +.ti3-aui-file-tip-name { + display: inline-block; + margin-top: var(--ti-common-space-base); + font-size: var(--ti-common-font-size-base); +} +.ti3-aui-file-placeholder { + color: var(--ti-common-color-text-disabled); + line-height: calc(var(--ti-upload-container-height) - 2px - var(--ti-upload-item-container-padding-vertical) * 2); + padding-left: var(--ti-common-space-10); +} +.ti3-aui-upload-btn { + float: left; + margin-left: var(--ti-common-space-2x); + height: var(--ti-upload-container-height); +} +.ti3-aui-file-state-general { + margin-top: var(--ti-common-space-2x); +} +.ti3-aui-file-state-general-info { + display: flex; + align-items: center; +} +.ti3-aui-file-state-general-info-uploading, +.ti3-aui-file-state-general-info-addSuccess { + color: var(--ti-common-color-text-weaken); +} +.ti3-aui-file-state-general-info-error { + color: var(--ti-common-color-error-text); +} +.ti3-aui-file-state-general-info-success { + color: var(--ti-common-color-text-primary); +} +.ti3-aui-file-state-general-operate { + display: inline-block; + vertical-align: middle; + margin-left: var(--ti-common-space-base); + color: var(--ti-common-color-text-link); + cursor: pointer; + &:hover { + color: var(--ti-common-color-text-link-hover); + } +} +.ti3-aui-file-state-general-success, +.ti3-aui-file-state-general-error { + color: var(--ti-common-color-success); + margin-right: var(--ti-common-space-2x); + font-size: var(--ti-upload-icon-size); + line-height: 18px; +} +.ti3-aui-file-state-general-error { + color: var(--ti-common-color-error); +} + +.ti3-file-upload-btnCon { + float: left; + position: relative; +} + +.ti3-file-upload-btn-cover { + position: absolute; + top: 0; + right: 0; + left: var(--ti-common-space-2x); + bottom: 0; + cursor: not-allowed; +} + +// button/textButton类型 +.ti3-file-upload-container { + .ti3-file-text-btn { + display: inline-block; + overflow: hidden; + font-size: var(--ti-upload-textbutton-font-size); + font-weight: var(--ti-common-font-weight-4); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-link); + position: relative; + &:hover { + color: var(--ti-common-color-text-link-hover); + } + &.ti3-file-text-btn-disabled { + color: var(--ti-common-color-text-disabled); + } + } + .ti3-file-note { + margin-top: var(--ti-common-space-2x); + line-height: var(--ti-common-line-height-number); + color: var(--ti-common-color-text-weaken); + } + .ti3-file-items-container { + display: flex; + flex-direction: column; + margin-top: var(--ti-common-space-2x); + max-height: calc( + (var(--ti-upload-file-item-height) + var(--ti-common-space-10)) * 7 + + (var(--ti-upload-file-item-height) + var(--ti-common-space-base)) + ); // 列表默认最多显示8个文件,(22+10)*7 + (22+4)*1 + overflow-y: auto; + .ti3-file-item { + display: flex; + justify-content: space-between; + align-items: center; + padding-right: var(--ti-common-space-10); + margin-bottom: var(--ti-common-space-10); + height: var(--ti-upload-file-item-height); + cursor: pointer; + border-radius: var(--ti-common-border-radius-normal); + position: relative; + &:last-child { + margin-bottom: var(--ti-common-space-base); + } + &:hover { + background: var(--ti-common-color-bg-normal); + .ti3-file-operates { + div.ti3-file-operates-show, + div.ti3-file-operates-uploading, + div.ti3-file-operates-success { + display: flex; + } + } + } + .ti3-file-name-container { + display: flex; + align-items: center; + flex: 1; + height: var(--ti-upload-file-item-height); + overflow: hidden; + color: var(--ti-common-color-text-secondary); + &.ti3-file-state-general-info-error { + color: var(--ti-common-color-error-text); + } + .ti3-file-name { + margin-left: var(--ti-common-space-base); + font-size: var(--ti-upload-item-file-name-font-size); + font-weight: var(--ti-common-font-weight-4); + .ellipsis(); + } + } + .ti3-file-operates { + div { + display: flex; + align-items: center; + padding-left: var(--ti-common-space-4x); + .ti3-file-error-reupload { + margin-right: var(--ti-common-space-2x); + } + .ti3-icon:hover { + color: var(--ti-common-color-icon-hover); + } + &.ti3-file-operates-show, + &.ti3-file-operates-uploading, + &.ti3-file-operates-success { + display: none; + } + } + } + .ti3-file-progress-bar { + height: var(--ti-common-border-weight-1); + position: absolute; + left: 0; + top: var(--ti-common-space-6x); + } + .ti3-icon { + font-size: var(--ti-common-font-size-2); + } + } + } + .ti3-file-error-message { + .ti3-aui-file-state-general { + font-size: var(--ti-upload-msg-font-size); + font-weight: var(--ti-common-font-weight-4); + line-height: var(--ti-common-line-height-number); + ti-error-msg { + margin-top: 0; + } + } + } +} +:host ::ng-deep .ti3-file-progress-bar-color { + background-color: var(--ti-common-color-success) !important; +} + +.ti3-file-x-disabled { + color: var(--ti-common-color-icon-disabled); + cursor: not-allowed; + &:hover { + color: var(--ti-common-color-icon-disabled) !important; + } +} + +.ti3-aui-file-state-general-operate-disable { + color: var(--ti-common-color-text-disabled); + cursor: not-allowed; + &:hover { + color: var(--ti-common-color-text-disabled); + } +} + +/*************************************************动效**********************************************************************/ + +// IE和FF下,文件上传按钮加动画效果情况下,按钮点击两次才打开文件选择框问题 +.ti3-file-upload-container { + .ti3-btn-default:not(:focus):not([disabled]):hover, + [ti-button]:not(:focus):not([disabled]):hover { + animation: none; + } +} diff --git a/src/upload/lib/src/uploadimage.html b/src/upload/lib/src/uploadimage.html new file mode 100644 index 0000000..3e96ef0 --- /dev/null +++ b/src/upload/lib/src/uploadimage.html @@ -0,0 +1,96 @@ + + +
    +
    + +
    + +
    + + {{uploadLan.cancel}} +
    +
    +
    + + + + +
    + {{item.name}} +
    +
    + + +
    + + +
    + + + +
    + +
    +
    + + + + + diff --git a/src/upload/lib/src/uploadimage.less b/src/upload/lib/src/uploadimage.less new file mode 100644 index 0000000..5a700e3 --- /dev/null +++ b/src/upload/lib/src/uploadimage.less @@ -0,0 +1,166 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-upload-image-btn-width: var(--ti-common-size-25x); + --ti-upload-image-btn-height: var(--ti-common-size-25x); + --ti-picture-card-operation-width: var(--ti-common-size-4x); + --ti-picture-card-operation-height: var(--ti-common-size-4x); + --ti-picture-card-operation-top: calc((var(--ti-upload-image-btn-height) - var(--ti-picture-card-operation-height)) / 2); + --ti-picture-card-operation-left-center: calc((var(--ti-upload-image-btn-width) - var(--ti-picture-card-operation-width)) / 2); + --ti-picture-card-operation-preview-left: var(--ti-common-space-7x); + --ti-upload-image-progress-top: 57px; + --ti-upload-image-progress-width: var(--ti-common-size-20x); + --ti-upload-image-icon-margin: 37px; +} + +.ti3-picture-card-container { + display: inline-block; + position: relative; + width: var(--ti-upload-image-btn-width); + &:not(:last-child) { + margin-right: var(--ti-common-space-2x); + } + .ti3-picture-card-item { + position: relative; + height: var(--ti-upload-image-btn-height); + box-sizing: border-box; + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-solid) var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + .ti3-picture-card-item-img { + width: 100%; + height: 100%; + object-fit: cover; + border-style: none; + } + .ti3-picture-card-item-not-img { + background: data-uri('../../../themes/basic/img/upload-image-error.png') 50% no-repeat; + padding: calc(var(--ti-upload-image-btn-width) / 2); + } + .ti3-upload-image-progress { + position: absolute; + top: var(--ti-upload-image-progress-top); + width: var(--ti-upload-image-progress-width); + margin: 0 calc((var(--ti-upload-image-btn-width) - var(--ti-upload-image-progress-width)) / 2); + ti-progressbar { + position: absolute; + } + ::ng-deep .ti3-progress-bar { + background-color: var(--ti-common-color-success); + } + .ti3-upload-image-cancel-text { + width: 100%; + display: inline-block; + margin-top: calc(5px + var(--ti-common-space-2x)); // 5 + 8 + text-align: center; + cursor: pointer; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + color: var(--ti-common-color-prompt-text); + } + } + } + .ti3-picture-card-item-error { + border-color: var(--ti-common-color-error-border); + } + .ti3-picture-card-drop { + position: absolute; + width: var(--ti-upload-image-btn-width); + height: var(--ti-upload-image-btn-height); + top: 0; + background: rgba(0, 0, 0, 0.4); + opacity: 0; + border-radius: var(--ti-common-border-radius-normal); + &:hover { + opacity: 1; + } + .ti3-picture-card-operation { + display: inline-block; + height: var(--ti-picture-card-operation-height); + width: var(--ti-picture-card-operation-width); + position: absolute; + &:hover { + cursor: pointer; + } + } + .ti3-picture-card-operation-preview { + background-image: data-uri('../../../themes/basic/img/upload-image-preview.png'); + top: var(--ti-picture-card-operation-top); + left: var(--ti-picture-card-operation-preview-left); + } + .ti3-picture-card-operation-center { + left: var(--ti-picture-card-operation-left-center); + } + .ti3-picture-card-operation-success-center { + left: var(--ti-picture-card-operation-left-center) !important; + } + .ti3-picture-card-operation-delete { + background-image: data-uri('../../../themes/basic/img/upload-image-delete.png'); + top: var(--ti-picture-card-operation-top); + } + .ti3-picture-card-operation-delete-error { + left: var(--ti-picture-card-operation-left-center); + } + .ti3-picture-card-operation-delete-success { + left: calc(var(--ti-picture-card-operation-preview-left) * 2); + } + } + .ti3-picture-card-name { + display: inline-block; + width: 100%; + text-align: center; + margin-top: var(--ti-common-space-2x); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + } + .ti3-picture-card-name-error { + color: var(--ti-common-color-error-text); + } +} + +.ti3-upload-image-btn { + position: relative; + overflow: hidden; + width: var(--ti-upload-image-btn-width); + height: var(--ti-upload-image-btn-height); + border: var(--ti-common-border-weight-normal) var(--ti-common-border-style-dashed) var(--ti-common-color-line-normal); + border-radius: var(--ti-common-border-radius-normal); + padding: 0; + vertical-align: top; + &[disabled] { + cursor: not-allowed; + } + :hover { + cursor: pointer; + } + .ti3-upload-image-icon { + width: 100%; + display: inline-block; + margin: var(--ti-upload-image-icon-margin) auto; + position: absolute; + top: 0; + left: 0; + font-size: var(--ti-common-font-size-2); + } + + .ti3-upload-image-input { + position: absolute; + left: 0; + top: 0; + display: inline-block; + opacity: 0; + width: 100%; + height: 100%; + outline: none; + font-size: 0; // 解决IE下点击上传文件出现光标的问题 + &[disabled] { + cursor: not-allowed; + } + } +} + +::ng-deep .ti3-image-preview-modal { + width: 640px !important; +} diff --git a/src/utils/demo/project.json b/src/utils/demo/project.json new file mode 100644 index 0000000..5357107 --- /dev/null +++ b/src/utils/demo/project.json @@ -0,0 +1,78 @@ +{ + "projectType": "application", + "root": "src/utils/demo", + "sourceRoot": "src/utils/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/utils", + "index": "src/utils/demo/src/index.html", + "main": "src/utils/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/utils/demo/tsconfig.app.json", + "assets": ["src/utils/demo/src/favicon.ico", "src/utils/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "utils-demo:build:production" + }, + "development": { + "browserTarget": "utils-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js utils" + } + ] + } + } + } +} diff --git a/src/utils/demo/src/app/AppComponent.ts b/src/utils/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/utils/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/utils/demo/src/app/AppModule.ts b/src/utils/demo/src/app/AppModule.ts new file mode 100644 index 0000000..e11de06 --- /dev/null +++ b/src/utils/demo/src/app/AppModule.ts @@ -0,0 +1,30 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { BrowserTestModule } from './browser/BrowserTestModule'; +import { KeymapTestModule } from './keymap/KeymapTestModule'; +import { LogTestModule } from './log/LogTestModule'; +import { ThemeTestModule } from './theme/ThemeTestModule'; + +@NgModule({ + imports: [ + BrowserTestModule, + KeymapTestModule, + LogTestModule, + ThemeTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/utils/demo/src/app/IndexComponent.ts b/src/utils/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..99ee696 --- /dev/null +++ b/src/utils/demo/src/app/IndexComponent.ts @@ -0,0 +1,54 @@ +import { Component } from '@angular/core'; +import { BrowserTestModule } from './browser/BrowserTestModule'; +import { KeymapTestModule } from './keymap/KeymapTestModule'; +import { LogTestModule } from './log/LogTestModule'; +import { ThemeTestModule } from './theme/ThemeTestModule'; + +@Component({ + template: ` + + + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + moudles: Array = [BrowserTestModule.ROUTES, KeymapTestModule.ROUTES, LogTestModule.ROUTES, ThemeTestModule.ROUTES]; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/utils/demo/src/app/app.html b/src/utils/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/utils/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/utils/demo/src/app/browser/BrowserTestModule.ts b/src/utils/demo/src/app/browser/BrowserTestModule.ts new file mode 100644 index 0000000..3d993af --- /dev/null +++ b/src/utils/demo/src/app/browser/BrowserTestModule.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { BrowserUsageComponent } from './BrowserUsageComponent'; + +@NgModule({ + imports: [CommonModule, RouterModule.forChild(BrowserTestModule.ROUTES)], + declarations: [BrowserUsageComponent] +}) +export class BrowserTestModule { + static readonly LINKS: Array = [{ href: 'classes/TiBrowser.html', label: 'TiBrowser' }]; + static readonly ROUTES: Routes = [ + { + path: 'browser/browser-basic', + component: BrowserUsageComponent + } + ]; +} diff --git a/src/utils/demo/src/app/browser/BrowserUsageComponent.ts b/src/utils/demo/src/app/browser/BrowserUsageComponent.ts new file mode 100644 index 0000000..91dfa4b --- /dev/null +++ b/src/utils/demo/src/app/browser/BrowserUsageComponent.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { TiBrowser } from '@opentiny/ng'; + +@Component({ + templateUrl: './browser-usage.html' +}) +export class BrowserUsageComponent { + browser: string = TiBrowser.browser(); + version: number = TiBrowser.version(); + isEdge: boolean = TiBrowser.isEdge(); + isIE: boolean = TiBrowser.isIE(); + isFirefox: boolean = TiBrowser.isFirefox(); + isChrome: boolean = TiBrowser.isChrome(); + isOpera: boolean = TiBrowser.isOpera(); + isSafari: boolean = TiBrowser.isSafari(); + isOther: boolean = TiBrowser.isOther(); +} diff --git a/src/utils/demo/src/app/browser/browser-usage.html b/src/utils/demo/src/app/browser/browser-usage.html new file mode 100644 index 0000000..8f7930a --- /dev/null +++ b/src/utils/demo/src/app/browser/browser-usage.html @@ -0,0 +1,10 @@ +

    browser:{{browser}}

    +

    version:{{version}}

    +

    version>5:{{version>5}}

    +

    isEdge:{{isEdge}}

    +

    isIE:{{isIE}}

    +

    isFirefox:{{isFirefox}}

    +

    isChrome:{{isChrome}}

    +

    isOpera:{{isOpera}}

    +

    isSafari:{{isSafari}}

    +

    isOther:{{isOther}}

    diff --git a/src/utils/demo/src/app/browser/webdoc/browser-demos.js b/src/utils/demo/src/app/browser/webdoc/browser-demos.js new file mode 100644 index 0000000..7e24545 --- /dev/null +++ b/src/utils/demo/src/app/browser/webdoc/browser-demos.js @@ -0,0 +1,27 @@ +export default { + column: '2', + demos: [ + { + demoId: 'browser-usage', + name: { + 'zh-CN': '基本用法', + 'en-US': 'browser usage', + }, + desc: { + 'zh-CN': '

    检测当前浏览器的相关信息。

    ', + 'en-US': '

    browser usage

    ', + }, + apis: [ + 'TiBrowser.methods.browser', + 'TiBrowser.methods.isFirefox', + 'TiBrowser.methods.isOther', + 'TiBrowser.methods.isChrome', + 'TiBrowser.methods.isIE', + 'TiBrowser.methods.isEdge', + 'TiBrowser.methods.isOpera', + 'TiBrowser.methods.isSafari', + 'TiBrowser.methods.version', + ], + }, + ], +}; diff --git a/src/utils/demo/src/app/browser/webdoc/browser.cn.md b/src/utils/demo/src/app/browser/webdoc/browser.cn.md new file mode 100644 index 0000000..b035620 --- /dev/null +++ b/src/utils/demo/src/app/browser/webdoc/browser.cn.md @@ -0,0 +1,19 @@ +--- +title: Browser 浏览器信息 +--- +# Browser 浏览器信息 + +
    + +Browser 是浏览器工具类,提供浏览器类别和版本等信息。 + +
    + +
    + +Browser 是浏览器工具类,提供浏览器类别和版本等信息。 + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
    diff --git a/src/utils/demo/src/app/browser/webdoc/browser.en.md b/src/utils/demo/src/app/browser/webdoc/browser.en.md new file mode 100644 index 0000000..9fcb90f --- /dev/null +++ b/src/utils/demo/src/app/browser/webdoc/browser.en.md @@ -0,0 +1,28 @@ +--- +title: Accordion +--- +# Accordion + +
    + +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
    + +
    + +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/utils/demo/src/app/keymap/KeymapTestModule.ts b/src/utils/demo/src/app/keymap/KeymapTestModule.ts new file mode 100644 index 0000000..121e35e --- /dev/null +++ b/src/utils/demo/src/app/keymap/KeymapTestModule.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiTextModule } from '@opentiny/ng'; + +import { KeymapUsageComponent } from './KeymapUsageComponent'; + +@NgModule({ + imports: [CommonModule, TiTextModule, RouterModule.forChild(KeymapTestModule.ROUTES)], + declarations: [KeymapUsageComponent] +}) +export class KeymapTestModule { + static readonly LINKS: Array = [{ href: 'classes/TiKeymap.html', label: 'TiKeymap' }]; + static readonly ROUTES: Routes = [ + { + path: 'keymap/keymap-usage', + component: KeymapUsageComponent + } + ]; +} diff --git a/src/utils/demo/src/app/keymap/KeymapUsageComponent.ts b/src/utils/demo/src/app/keymap/KeymapUsageComponent.ts new file mode 100644 index 0000000..9a3955e --- /dev/null +++ b/src/utils/demo/src/app/keymap/KeymapUsageComponent.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { TiKeymap } from '@opentiny/ng'; +@Component({ + templateUrl: './keymap-usage.html' +}) +export class KeymapUsageComponent { + charCode: string; + charStr: string; + keyCode: string; + keypressStr: string; + + onKeypress(event: KeyboardEvent): void { + const charCode: number = event.charCode; + this.charCode = `字符编码为--${charCode}`; + this.charStr = `字符为--'${String.fromCharCode(charCode)}'`; + this.keyCode = `键值为--${event.keyCode}`; + // 此处示例,是为了展示keymap的用法 + if (charCode === TiKeymap.KEY_ENTER) { + this.keypressStr = '此时回车键用TiKeymap表示为: TiKeymap.KEY_ENTER'; + } else { + this.keypressStr = ''; + } + } +} diff --git a/src/utils/demo/src/app/keymap/keymap-usage.html b/src/utils/demo/src/app/keymap/keymap-usage.html new file mode 100644 index 0000000..0e76b0b --- /dev/null +++ b/src/utils/demo/src/app/keymap/keymap-usage.html @@ -0,0 +1,8 @@ + +
    +

    Current Keymap information:

    +

    {{ charCode }}

    +

    {{ charStr }}

    +

    {{ keyCode }}

    +

    {{ keypressStr }}

    +
    diff --git a/src/utils/demo/src/app/keymap/webdoc/keymap-demos.js b/src/utils/demo/src/app/keymap/webdoc/keymap-demos.js new file mode 100644 index 0000000..2728fb5 --- /dev/null +++ b/src/utils/demo/src/app/keymap/webdoc/keymap-demos.js @@ -0,0 +1,17 @@ +export default { + column: '2', + demos: [ + { + demoId: 'keymap-usage', + name: { + 'zh-CN': '基本用法', + 'en-US': 'keymap usage', + }, + desc: { + 'zh-CN': '

    键盘按键被按下时,获取字符编码和键值等相关信息。

    ', + 'en-US': '

    keymap usage

    ', + }, + apis: ['TiKeymap.properties.KEY_0', 'TiKeymap.properties.KEY_1'], + }, + ], +}; diff --git a/src/utils/demo/src/app/keymap/webdoc/keymap.cn.md b/src/utils/demo/src/app/keymap/webdoc/keymap.cn.md new file mode 100644 index 0000000..6c63b05 --- /dev/null +++ b/src/utils/demo/src/app/keymap/webdoc/keymap.cn.md @@ -0,0 +1,16 @@ +--- +title: Keymap 键值查询 +--- +# Keymap 键值查询 + +
    + +Keymap 是键值映射工具类,可以提高键值可读性,已经屏蔽浏览器差异。 + +
    + +
    + +Keymap 是键值映射工具类,可以提高键值可读性,已经屏蔽浏览器差异。 + +
    diff --git a/src/utils/demo/src/app/keymap/webdoc/keymap.en.md b/src/utils/demo/src/app/keymap/webdoc/keymap.en.md new file mode 100644 index 0000000..9fcb90f --- /dev/null +++ b/src/utils/demo/src/app/keymap/webdoc/keymap.en.md @@ -0,0 +1,28 @@ +--- +title: Accordion +--- +# Accordion + +
    + +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
    + +
    + +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/utils/demo/src/app/log/LogTestModule.ts b/src/utils/demo/src/app/log/LogTestModule.ts new file mode 100644 index 0000000..baf24ab --- /dev/null +++ b/src/utils/demo/src/app/log/LogTestModule.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { LogUsageComponent } from './LogUsageComponent'; + +@NgModule({ + imports: [CommonModule, RouterModule.forChild(LogTestModule.ROUTES)], + declarations: [LogUsageComponent] +}) +export class LogTestModule { + static readonly LINKS: Array = [{ href: 'classes/TiLog.html', label: 'TiLog' }]; + static readonly ROUTES: Routes = [ + { + path: 'log/log-usage', + component: LogUsageComponent + } + ]; +} diff --git a/src/utils/demo/src/app/log/LogUsageComponent.ts b/src/utils/demo/src/app/log/LogUsageComponent.ts new file mode 100644 index 0000000..8dbf1aa --- /dev/null +++ b/src/utils/demo/src/app/log/LogUsageComponent.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; +import { TiLog } from '@opentiny/ng'; + +@Component({ + templateUrl: './log-usage.html' +}) +export class LogUsageComponent { + constructor() { + // 当前日志的数值大于等于日志等级时,当前日志才会正常输出 + // 当前日志等级:LEVEL_LOG为1 + TiLog.setLevel(TiLog.LEVEL_LOG); + // log的数值是1,大于等于1,正常输出 + TiLog.log('LEVEL_LOG:log'); + // warn的数值是2,大于等于1,正常输出 + TiLog.warn('LEVEL_LOG:warn'); + // error的数值是3,大于等于1,正常输出 + TiLog.error('LEVEL_LOG:error'); + + // 当前日志等级:LEVEL_WARN为 2 + TiLog.setLevel(TiLog.LEVEL_WARN); + // log的数值是1,小于2,不会输出 + TiLog.log('LEVEL_WARN: log'); + // warn的数值是2,大于等于2,正常输出 + TiLog.warn('LEVEL_WARN: warn'); + // error的数值是3,大于等于2,正常输出 + TiLog.error('LEVEL_WARN: error'); + + // 以下同理 + TiLog.setLevel(TiLog.LEVEL_ERROR); + // 不输出 + TiLog.log('LEVEL_ERROR: log'); + // 不输出 + TiLog.warn('LEVEL_ERROR: warn'); + // 输出 + TiLog.error('LEVEL_ERROR: error'); + + TiLog.setLevel(TiLog.LEVEL_OFF); + // 不输出 + TiLog.log('LEVEL_OFF:log'); + // 不输出 + TiLog.warn('LEVEL_OFF:warn'); + // 不输出 + TiLog.error('LEVEL_OFF:error'); + } +} diff --git a/src/utils/demo/src/app/log/log-usage.html b/src/utils/demo/src/app/log/log-usage.html new file mode 100644 index 0000000..e020de6 --- /dev/null +++ b/src/utils/demo/src/app/log/log-usage.html @@ -0,0 +1 @@ +

    请打开控制台查看,日志是否可以正常输出,与当前Log等级有关

    diff --git a/src/utils/demo/src/app/log/webdoc/log-demos.js b/src/utils/demo/src/app/log/webdoc/log-demos.js new file mode 100644 index 0000000..4429411 --- /dev/null +++ b/src/utils/demo/src/app/log/webdoc/log-demos.js @@ -0,0 +1,23 @@ +export default { + column: '2', + demos: [ + { + demoId: 'log-usage', + name: { + 'zh-CN': '基本用法', + 'en-US': 'log usage', + }, + desc: { + 'zh-CN': + '

    当前日志的数值大于等于日志等级时,当前日志才会被打印输出

    ', + 'en-US': '

    log usage

    ', + }, + apis: [ + 'TiLog.methods.log', + 'TiLog.methods.error', + 'TiLog.methods.warn', + 'TiLog.methods.setLevel', + ], + }, + ], +}; diff --git a/src/utils/demo/src/app/log/webdoc/log.cn.md b/src/utils/demo/src/app/log/webdoc/log.cn.md new file mode 100644 index 0000000..4e5a48f --- /dev/null +++ b/src/utils/demo/src/app/log/webdoc/log.cn.md @@ -0,0 +1,16 @@ +--- +title: Log 日志 +--- +# Log 日志 + +
    + +Logger 工具类, 提供全局日志输出和级别控制。 + +
    + +
    + +Logger 工具类, 提供全局日志输出和级别控制。 + +
    diff --git a/src/utils/demo/src/app/log/webdoc/log.en.md b/src/utils/demo/src/app/log/webdoc/log.en.md new file mode 100644 index 0000000..e97a860 --- /dev/null +++ b/src/utils/demo/src/app/log/webdoc/log.en.md @@ -0,0 +1,26 @@ +--- +title: Accordion +--- +# Accordion + +
    + +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
    + +
    + +Use Accordion to store contents. + +- Place a group of content in multiple accordion panels, click the title of the panel to expand or contract its content. + +```typescript +import { TiAccordionModule } from '@opentiny/ng'; +``` +
    \ No newline at end of file diff --git a/src/utils/demo/src/app/theme/ThemeBasicComponent.ts b/src/utils/demo/src/app/theme/ThemeBasicComponent.ts new file mode 100644 index 0000000..4c396be --- /dev/null +++ b/src/utils/demo/src/app/theme/ThemeBasicComponent.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { TiTheme } from '@opentiny/ng'; + +@Component({ + templateUrl: './theme-basic.html' +}) +export class ThemeBasicComponent { + baseUrl: string = window['DEPLOY_URL'] + window['PUBLIC_URL'] ? window['DEPLOY_URL'] + window['PUBLIC_URL'] : ''; + switchState: boolean = true; + + blue(): void { + TiTheme.loadCss(`${this.baseUrl}assets/themes/theme-blue.css`, 'tiny3theme'); + } + green(): void { + TiTheme.loadCss(`${this.baseUrl}assets/themes/theme-green.css`, 'tiny3theme'); + } + purple(): void { + TiTheme.loadCss(`${this.baseUrl}assets/themes/theme-purple.css`, 'tiny3theme'); + } + red(): void { + TiTheme.loadCss(`${this.baseUrl}assets/themes/theme-red.css`, 'tiny3theme'); + } + default(): void { + TiTheme.loadCss(`${this.baseUrl}assets/themes/theme-default.css`, 'tiny3theme'); + } +} diff --git a/src/utils/demo/src/app/theme/ThemeTestModule.ts b/src/utils/demo/src/app/theme/ThemeTestModule.ts new file mode 100644 index 0000000..16e360c --- /dev/null +++ b/src/utils/demo/src/app/theme/ThemeTestModule.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; + +import { TiButtonModule, TiSwitchModule } from '@opentiny/ng'; + +import { ThemeBasicComponent } from './ThemeBasicComponent'; + +@NgModule({ + imports: [CommonModule, FormsModule, TiButtonModule, TiSwitchModule, RouterModule.forChild(ThemeTestModule.ROUTES)], + declarations: [ThemeBasicComponent] +}) +export class ThemeTestModule { + static readonly LINKS: Array = [{ href: 'classes/TiThemeUtil.html', label: 'Theme' }]; + static readonly ROUTES: Routes = [ + { + path: 'theme/theme-basic', + component: ThemeBasicComponent + } + ]; +} diff --git a/src/utils/demo/src/app/theme/theme-basic.html b/src/utils/demo/src/app/theme/theme-basic.html new file mode 100644 index 0000000..3e0837d --- /dev/null +++ b/src/utils/demo/src/app/theme/theme-basic.html @@ -0,0 +1,5 @@ +    +    + 
    + + diff --git a/src/utils/demo/src/app/theme/webdoc/theme-demos.js b/src/utils/demo/src/app/theme/webdoc/theme-demos.js new file mode 100644 index 0000000..61e1c88 --- /dev/null +++ b/src/utils/demo/src/app/theme/webdoc/theme-demos.js @@ -0,0 +1,17 @@ +export default { + column: '2', + demos: [ + { + demoId: 'theme-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'theme-basic', + }, + desc: { + 'zh-CN': '在线换肤能力展示。(仅支持生产环境,不支持调试环境)', + 'en-US': '

    theme basic

    ', + }, + apis: ['TiTheme.methods.loadCss'], + }, + ], +}; diff --git a/src/utils/demo/src/app/theme/webdoc/theme.cn.md b/src/utils/demo/src/app/theme/webdoc/theme.cn.md new file mode 100644 index 0000000..8000247 --- /dev/null +++ b/src/utils/demo/src/app/theme/webdoc/theme.cn.md @@ -0,0 +1,16 @@ +--- +title: Theme 主题配置 +--- +# Theme 主题配置 + +
    + +样式主题工具类 + +
    + +
    + +样式主题工具类 + +
    diff --git a/src/utils/demo/src/app/theme/webdoc/theme.en.md b/src/utils/demo/src/app/theme/webdoc/theme.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/utils/demo/src/app/theme/webdoc/theme.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/utils/demo/src/favicon.ico b/src/utils/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/utils/demo/src/index.html b/src/utils/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/utils/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/utils/demo/src/main.ts b/src/utils/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/utils/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/utils/demo/tsconfig.app.json b/src/utils/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/utils/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/utils/lib/index.ts b/src/utils/lib/index.ts new file mode 100644 index 0000000..876c199 --- /dev/null +++ b/src/utils/lib/index.ts @@ -0,0 +1,9 @@ +export * from './src/ObservableMap'; +export * from './src/ObservableSet'; +export * from './src/Position'; +export * from './src/TiBrowser'; +export * from './src/TiDateUtil'; +export * from './src/TiKeymap'; +export * from './src/TiLog'; +export * from './src/TiTheme'; +export * from './src/Util'; diff --git a/src/utils/lib/ng-package.json b/src/utils/lib/ng-package.json new file mode 100644 index 0000000..be81921 --- /dev/null +++ b/src/utils/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/utils", + "lib": { + "entryFile": "index.ts" + } +} diff --git a/src/utils/lib/package.json b/src/utils/lib/package.json new file mode 100644 index 0000000..7e6b4a4 --- /dev/null +++ b/src/utils/lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opentiny/ng-utils", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/platform-browser-dynamic": ">=13.0.0" + }, + "dependencies": { + "tslib": "^2.3.0" + } +} \ No newline at end of file diff --git a/src/utils/lib/project.json b/src/utils/lib/project.json new file mode 100644 index 0000000..c67be66 --- /dev/null +++ b/src/utils/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/utils/lib", + "sourceRoot": "src/utils/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/utils"], + "options": { + "project": "src/utils/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/utils"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js utils" + }, + { + "command": "ng default-build utils" + }, + { + "command": "node build/clear-default-theme.js utils" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/utils && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build utils && ng pack utils && node build/publish.js utils --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/utils/lib/src/ObservableMap.ts b/src/utils/lib/src/ObservableMap.ts new file mode 100644 index 0000000..6107270 --- /dev/null +++ b/src/utils/lib/src/ObservableMap.ts @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * @ignore + */ +export type HandlerFnType = (key: any, value: any, add: boolean, from?: any) => void; +// TODO:未来看typescript如何继承Map +/** + * @ignore + */ +export class ObservableMap { + // 代理模式 + private map: Map = new Map(); + get size(): number { + return this.map.size; + } + constructor() { + this.map = new Map(); + } + // 观察者模式 + private handlerSet: Set = new Set(); + public addObserver(handlerFn: HandlerFnType): void { + this.handlerSet.add(handlerFn); + } + public removeObserver(handlerFn: HandlerFnType): void { + this.handlerSet.delete(handlerFn); + } + // 通知函数,不会通知到发起源 + private notify(key: any, value: any, add: boolean, from: any): void { + this.handlerSet.forEach((handlerFn: HandlerFnType) => { + handlerFn(key, value, add, from); + }); + } + // 设置,可以添加发起源,那么消息不会通知到发起源。 + public set(key: any, value: any, from?: any): this { + if (!this.map.has(key) || (this.map.has(key) && this.map.get(key) !== value)) { + this.map.set(key, value); + this.notify(key, value, true, from); + } + + return this; + } + // 删除,可以添加发起源,那么消息不会通知到发起源。 + public delete(key: any, from?: any): boolean { + if (this.map.has(key)) { + this.map.delete(key); + this.notify(key, undefined, false, from); + + return true; + } + + return false; + } + public has(key: any): boolean { + return this.map.has(key); + } + public get(key: any): boolean { + return this.map.get(key); + } + public getMap(): Map { + // 方便遍历Map,所有有了这个方法。 + return this.map; + } +} diff --git a/src/utils/lib/src/ObservableSet.ts b/src/utils/lib/src/ObservableSet.ts new file mode 100644 index 0000000..b61ff55 --- /dev/null +++ b/src/utils/lib/src/ObservableSet.ts @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * @ignore + */ +export type SetHandlerFnType = (item: any, add: boolean, from?: any) => void; +// TODO:未来看typescript如何继承Set +/** + * @ignore + */ +export class ObservableSet { + // 代理模式 + private set: Set = new Set(); + get size(): number { + return this.set.size; + } + constructor(items?: any) { + this.set = new Set(items); + } + // 观察者模式 + private handlerSet: Set = new Set(); + public addObserver(handlerFn: SetHandlerFnType): void { + this.handlerSet.add(handlerFn); + } + public removeObserver(handlerFn: SetHandlerFnType): void { + this.handlerSet.delete(handlerFn); + } + private notify(item: any, isAdd: boolean, from: any): void { + this.handlerSet.forEach((handlerFn: SetHandlerFnType) => { + handlerFn(item, isAdd, from); + }); + } + public add(item: any, from?: any): this { + if (!this.set.has(item)) { + this.set.add(item); + this.notify(item, true, from); + } + + return this; + } + public delete(item: any, from?: any): boolean { + if (this.set.has(item)) { + this.set.delete(item); + this.notify(item, false, from); + + return true; + } + + return false; + } + public has(item: any): boolean { + return this.set.has(item); + } +} diff --git a/src/utils/lib/src/Position.ts b/src/utils/lib/src/Position.ts new file mode 100644 index 0000000..96d20d8 --- /dev/null +++ b/src/utils/lib/src/Position.ts @@ -0,0 +1,720 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiLog } from './TiLog'; +import { Util } from './Util'; +/** + * 位置显示类型定义,其中auto代表自适应位置显示 + */ +export type TiPositionType = + | 'top' + | 'top-left' + | 'top-right' + | 'bottom' + | 'bottom-left' + | 'bottom-right' + | 'left' + | 'left-top' + | 'left-bottom' + | 'right' + | 'right-top' + | 'right-bottom' + | 'center' + | 'auto' + | 'none'; +export type TiPositionEventType = 'tiScroll' | 'resize' | 'hashchange'; +interface TiPositionLayout { + hostLayout: TiHostLayout; + targetLayout: TiEleLayout; + avilableLayout: TiAvilableLayout; + hostOffsetLayout?: TiHostLayout; +} +export interface TiHostLayout { + top: number; + left: number; + width: number; + height: number; + // 最近的fixed定位的祖先元素 + fixedAncestor: any; +} +interface TiEleLayout { + top: number; + left: number; + width: number; + height: number; +} +interface TiAvilableLayout { + left: number; + maxLeft: number; + right: number; + maxRight: number; + top: number; + maxTop: number; + bottom: number; + maxBottom: number; + clientHeight: number; + clientWidth: number; +} +export interface TiPositionResult { + position: TiPositionType; + avilableHeight: number; + hostLayout?: TiHostLayout; +} +interface TiLayoutParams { + left: number; + top: number; + avilableHeight: number; +} +interface TiVerticalParams { + avilableHeight: number; + top: number; +} +/** + * @ignore + * 该类提供公共静态类方法,用于设置在body定位元素基于参照元素的位置信息,提供的核心方法如下: + * setPosition() + * 1. 入参为对象,参数信息如下: + * { + * targetEle:需要定位的元素 + * hostEle:参照元素 + * position:定位位置 + * hostSpace:待定位元素和参照元素间距 + * browserSpace :待定位元素和浏览器间距, 暂时只在上下位置生效 + * consoleHeaderHeight: console页面头部高度 + * hOffset: 自定义水平方向的偏移(在定位基础上的水平偏移,向左偏移为负值,向右偏移为正值) + * fixMaxHeight: 定位元素最大高度是否固定不变(显示不下时不用压缩高度) + * determinPositionFn:决定位置的函数 + * } + * 2. 返回值为对象,信息如下: + * { + * position:string 最终元素定位位置(当外部定义位置非13个可选位置时,会重新计算合适的位置进行定位,因此提供此位置信息供外部使用) + * avilableHeight:number 元素可用高度占位信息(外部可使用该信息进行高度的重新定义) + * } + */ +export class Position { + private static positionArr: Array = [ + 'top', + 'top-left', + 'top-right', + 'bottom', + 'bottom-left', + 'bottom-right', + 'left', + 'left-top', + 'left-bottom', + 'right', + 'right-top', + 'right-bottom', + 'center' + ]; + /* tip 针对较小元素做位置自适应处理,保证 tip 的箭头在被提示元素的中间,这是宽度或者高度的阈值 + * 取值为 15 的原因: 箭头定位偏移量9 + 箭头宽度的一半6 = 15px 相关变量变化时需要修改该常量 + */ + private static readonly ADAPTIVE_SIZE: number = 15; + public static setPosition(options: { + targetEle: any; + hostEle: any; + hostEleX?: any; // 决定定位元素水平方向的元素,用于宿主元素水平方向位置与host元素不一致的场景,暂不对外开放 + // 定位的参考元素,是离targetEle最近的absolute或relative元素,不传入时,是指targetEle以body为参考元素进行定位 + referElem?: any; + position?: TiPositionType; + hostSpace?: number; + browserSpace?: number; // 与浏览器距离像素 + consoleHeaderHeight?: number; + hOffset?: number; // 自定义水平方向的偏移(在定位基础上的水平偏移,向左偏移为负值,向右偏移为正值) + fixMaxHeight?: boolean; // 定位元素最大高度是否固定不变(显示不下时不用压缩高度) + hasOffsetFix?: boolean; // 是否存在定位偏差量(针对 tip 组件:当被提示元素尺寸较小时,保证 tip 的箭头在被提示元素的中间) + determinPositionFn?(layout: any): string; + }): TiPositionResult { + // 入参非有效元素情况下,不做处理 + if (!Util.isElement(options.hostEle) || !Util.isElement(options.targetEle)) { + TiLog.warn('position: hostEle or targetEle type is not element'); + + // 防止外部使用报错,此处做返回值处理 + return { + position: 'top', + avilableHeight: 9999 + }; + } + // 分别获取宿主和待定位元素当前位置 + const curLayout: TiPositionLayout = Position.getLayout( + options.hostEle, + options.targetEle, + options.hostEleX, + options.referElem, + options.consoleHeaderHeight || 0 + ); + const hostSpace: number = options.hostSpace || 0; + // 判断元素位置 + const position: any = options.determinPositionFn + ? options.determinPositionFn(curLayout) + : Position.determinPosition(options.position, curLayout, hostSpace, options.browserSpace || 0); + const elePos: TiLayoutParams = Position.getLayoutParam( + curLayout, + position, + hostSpace, + options.browserSpace || 0, + options.hasOffsetFix || false + ); + const hOffset: number = options.hOffset || 0; + Position.setLayout(options.targetEle, elePos, curLayout, hOffset); + Position.setMaxHeight(options.targetEle, elePos, curLayout, options.fixMaxHeight); + Position.setDominatorDropDetachState(options.targetEle, elePos, curLayout, options.fixMaxHeight); + + return { + position, // 最终元素定位位置 + avilableHeight: elePos.avilableHeight, // 元素可用高度占位 + hostLayout: curLayout.hostLayout + }; + } + + /** + * 添加影响host position的事件并返回其事件句柄 + * 由于Angular listen不支持多事件定义,因此在此处封装函数进行单独处理;此外,事件也不支持定义命名空间 + * 监听全局事件,用于处理页面位置出现变化导致宿主元素位置偏移,而tip的特殊情况下tip消失, + * 并定义组件事件监听句柄,事件取消时会用到 + * 特殊场景包括: + * 1. 拖动弹框位置导致的宿主元素位置变化 + * 2. 页面局部出滚动条,滚动条位置变化导致的宿主元素位置变化 + * 鼠标导致的滚轮事件可通过mousewheel/DOMMouseScroll监听(有冒泡的特性) + * 拖拽导致的滚轮变化,需要业务通过trigger Tiny自定义事件tiScroll进行处理(无事件冒泡的特性) + * 3.页面缩放 + * 4.路由切换页面 + */ + public static addPosChangeEvts(eventCallback: any, render: any, eventTypes?: Array): Array<() => void> { + const eventHandles: Array<() => void> = []; + // 修复SSR报错:ERROR ReferenceError: document is not defined + if (typeof document === 'undefined') { + return []; + } + // 需要添加的事件数据抽象 + let eventArr: Array = [ + { + // 鼠标拖动页面内滚动条场景,该事件需要业务通过trigger tiScroll事件进行处理 + ele: typeof document !== 'undefined' ? document : null, + eventType: 'tiScroll', + callback: eventCallback + }, + { + // 页面缩放 + ele: typeof window !== 'undefined' ? window : null, + eventType: 'resize', + callback: eventCallback + }, + { + // 路由切换页面 + ele: typeof window !== 'undefined' ? window : null, + eventType: 'hashchange', + callback: eventCallback + } + ]; + if (eventTypes) { + eventArr = eventArr.filter((item: any) => eventTypes.includes(item.eventType)); + } + eventArr.forEach((item: any) => { + // 修复SSR问题: ERROR TypeError: Cannot read property 'addEventListener' of null + if (item.ele) { + return eventHandles.push(render.listen(item.ele, item.eventType, item.callback)); + } + }); + + return eventHandles; + } + /** + * 清除绑定事件,与addPosChangeEvts对称 + */ + public static removePosChangeEvts(eventHandles: any): void { + eventHandles.forEach((item: any) => { + if (item) { + item(); + } + }); + } + // 获取host和target元素的布局参数 + private static getLayout( + hostEle: any, + targetEle: any, + horizonHostEle: any, + referElem?: any, + consoleHeaderHeight?: number + ): TiPositionLayout { + const hostLayout: TiHostLayout = Position.getHostEleLayout(hostEle, horizonHostEle); + const layout = { + hostLayout, + targetLayout: Position.getEleLayout(targetEle), + avilableLayout: Position.getAvilableLayout(hostLayout, consoleHeaderHeight) + }; + if (Util.isElement(referElem)) { + const hostOffsetLayout: any = + referElem === hostEle + ? { + left: 0, + top: 0 + } + : { + left: hostEle.offsetLeft, + top: hostEle.offsetTop + }; + layout['hostOffsetLayout'] = hostOffsetLayout; + } + + return layout; + } + /** + * 根据高亮浮层位置及提示弹窗位置情况计算具体显示位置 + * @param position 元素位置定义信息 + * @param layout 宿主和待定位元素位置信息 + * @param hostSpace 宿主和待定位元素间距 + * @param browserSpace 浏览器和待定位元素间距 + * @returns 提示弹窗呈现位置 + */ + private static determinPosition(position: TiPositionType, layout: TiPositionLayout, hostSpace: number, browserSpace: number): string { + // position定义有效情况下,不做处理 + if (Position.isValidPosition(position, layout, hostSpace, browserSpace)) { + return position; + } + const targetLayout: TiEleLayout = layout.targetLayout; + const avilableLayout: TiAvilableLayout = layout.avilableLayout; + // 元素位置距各部分位置 + const avilableLeft: number = avilableLayout.left; + const avilableRight: number = avilableLayout.right; + const avilableTop: number = avilableLayout.top; + const avilableBottom: number = avilableLayout.bottom; + const targetMaxWidth: number = targetLayout.width + hostSpace + browserSpace; + const targetMaxHeight: number = targetLayout.height + hostSpace + browserSpace; + const hostLayout: TiHostLayout = layout.hostLayout; + if (avilableTop >= targetMaxHeight) { + // 上方可显示完全 + const positionTop: string = Position.determinHorizon(layout, hostLayout); + if (positionTop !== undefined) { + return 'top' + positionTop; + } + } + if (avilableRight >= targetMaxWidth) { + // 右侧可显示完全,具体思路同上方可显示完全情况 + const positionRight: string = Position.determinVertical(layout, hostLayout); + if (positionRight !== undefined) { + return 'right' + positionRight; + } + } + if (avilableBottom >= targetMaxHeight) { + // 下方可显示完全,具体思路同上方可显示完全情况 + const positionBottom: string = Position.determinHorizon(layout, hostLayout); + if (positionBottom !== undefined) { + return 'bottom' + positionBottom; + } + } + if (avilableLeft >= targetMaxWidth) { + // 左侧足够显示情况下,左侧居中显示 + const positionLeft: string = Position.determinVertical(layout, hostLayout); + if (positionLeft !== undefined) { + return 'left' + positionLeft; + } + } + + return 'center'; // 上下左右均无法足够显示情况下,居中显示内容 + } + /** + * 垂直位置确定后,确定水平位置 + */ + private static determinHorizon(layout: TiPositionLayout, hostLayout: TiHostLayout): string { + const targetLayout: TiEleLayout = layout.targetLayout; + const avilableLayout: TiAvilableLayout = layout.avilableLayout; + const avilableLeft: number = avilableLayout.left; + const avilableMaxLeft: number = avilableLayout.maxLeft; + const avilableRight: number = avilableLayout.right; + const avilableMaxRight: number = avilableLayout.maxRight; + const targetWidth: number = targetLayout.width; + const targetCenterWidth: number = (targetLayout.width - hostLayout.width) / 2; // 水平居中情况下,左右侧占用的宽度分别比宿主元素宽多少 + if (avilableRight >= targetCenterWidth && avilableLeft >= targetCenterWidth) { + // 提示元素超出被提示元素部分足够显示的情况下,优先居中显示 + return ''; + } + if (avilableMaxRight >= targetWidth) { + // 提示元素可在被提示元素左边界靠右完全显示情况下,向右显示 + return '-left'; + } + if (avilableMaxLeft >= targetWidth) { + // 提示元素可在被提示元素左边界靠左完全显示情况下,向左显示 + return '-right'; + } + } + /** + * 水平位置确定后,确定垂直位置 + */ + private static determinVertical(layout: TiPositionLayout, hostLayout: TiHostLayout): string { + const targetLayout: TiEleLayout = layout.targetLayout; + const avilableLayout: TiAvilableLayout = layout.avilableLayout; + const avilableTop: number = avilableLayout.top; + const avilableMaxTop: number = avilableLayout.maxTop; + const avilableBottom: number = avilableLayout.bottom; + const avilableMaxBottom: number = avilableLayout.maxBottom; + const targetHeight: number = targetLayout.height; + const targetMiddleHeight: number = (targetLayout.height - hostLayout.height) / 2; // 垂直居中情况下,上下两侧占用的高度分别比宿主元素高多少 + if (avilableTop >= targetMiddleHeight && avilableBottom >= targetMiddleHeight) { + return ''; + } + if (avilableMaxBottom >= targetHeight) { + return '-top'; + } + if (avilableMaxTop >= targetHeight) { + return '-bottom'; + } + } + + // 判断定义的position是否为有效位置 + private static isValidPosition(position: TiPositionType, layout: TiPositionLayout, hostSpace: number, browserSpace: number): boolean { + if (Position.positionArr.indexOf(position) === -1) { + return false; + } + // 判断在指定的position是否能显示下 + const positionFragments: Array = position.split('-'); + const targetLayout: TiEleLayout = layout.targetLayout; + const avilableLayout: TiAvilableLayout = layout.avilableLayout; + // 元素位置距各部分位置 + const avilableLeft: number = avilableLayout.left; + const avilableRight: number = avilableLayout.right; + const avilableTop: number = avilableLayout.top; + const avilableBottom: number = avilableLayout.bottom; + const targetMaxWidth: number = targetLayout.width + hostSpace + browserSpace; + const targetMaxHeight: number = targetLayout.height + hostSpace + browserSpace; + if (positionFragments[0] === 'top') { + if (avilableTop < targetMaxHeight) { + return false; + } + + return Position.isValidHorizonPosition(positionFragments[1], layout); + } + if (positionFragments[0] === 'bottom') { + if (avilableBottom < targetMaxHeight) { + return false; + } + + return Position.isValidHorizonPosition(positionFragments[1], layout); + } + if (positionFragments[0] === 'left') { + if (avilableLeft < targetMaxWidth) { + return false; + } + + return Position.isValidVerticalPosition(positionFragments[1], layout); + } + if (positionFragments[0] === 'right') { + if (avilableRight < targetMaxWidth) { + return false; + } + + return Position.isValidVerticalPosition(positionFragments[1], layout); + } + + return true; + } + + // 确定垂直位置是有效的后,判断水平位置是否有效 + private static isValidHorizonPosition(horizonPosition: string, layout: TiPositionLayout): boolean { + const targetLayout: TiEleLayout = layout.targetLayout; + const avilableLayout: TiAvilableLayout = layout.avilableLayout; + const hostLayout: TiHostLayout = layout.hostLayout; + const avilableLeft: number = avilableLayout.left; + const avilableRight: number = avilableLayout.right; + const avilableMaxLeft: number = avilableLayout.maxLeft; + const avilableMaxRight: number = avilableLayout.maxRight; + const targetWidth: number = targetLayout.width; + const targetCenterWidth: number = (targetLayout.width - hostLayout.width) / 2; // 水平居中情况下,左右侧占用的宽度分别比宿主元素宽多少 + if (!horizonPosition) { + return avilableRight >= targetCenterWidth && avilableLeft >= targetCenterWidth; + } + if (horizonPosition === 'left') { + return avilableMaxRight >= targetWidth; + } + if (horizonPosition === 'right') { + return avilableMaxLeft >= targetWidth; + } + + return false; + } + + // 确定水平位置是有效的后,判断垂直位置是否有效 + private static isValidVerticalPosition(verticalPosition: string, layout: TiPositionLayout): boolean { + const targetLayout: TiEleLayout = layout.targetLayout; + const avilableLayout: TiAvilableLayout = layout.avilableLayout; + const hostLayout: TiHostLayout = layout.hostLayout; + const avilableTop: number = avilableLayout.top; + const avilableBottom: number = avilableLayout.bottom; + const avilableMaxTop: number = avilableLayout.maxTop; + const avilableMaxBottom: number = avilableLayout.maxBottom; + const targetHeight: number = targetLayout.height; + const targetMiddleHeight: number = (targetLayout.height - hostLayout.height) / 2; // 垂直居中情况下,上下两侧占用的高度分别比宿主元素高多少 + if (!verticalPosition) { + return avilableTop >= targetMiddleHeight && avilableBottom >= targetMiddleHeight; + } + if (verticalPosition === 'top') { + return avilableMaxBottom >= targetHeight; + } + if (verticalPosition === 'bottom') { + return avilableMaxTop >= targetHeight; + } + + return false; + } + + // 设置提示弹窗位置 + private static setLayout(ele: any, position: TiLayoutParams, layoutParam: TiPositionLayout, hOffset: number): void { + if (layoutParam.hostLayout.fixedAncestor && !layoutParam.hostOffsetLayout) { + // fixed定位情况下,滚动条对其不受影响,因此此处需要减掉滚动条的位置影响 + // 如果是跟随宿主元素,也不能改为fixed定位,会脱离文档流,定位偏离 + ele.style.position = 'fixed'; + ele.style.left = `${position.left - window.pageXOffset + hOffset}px`; + ele.style.top = `${position.top - window.pageYOffset}px`; + } else { + ele.style.left = `${position.left + hOffset}px`; + ele.style.top = `${position.top}px`; + } + } + // 设置提示弹窗的MaxHeight + private static setMaxHeight(ele: any, position: any, layoutParam: any, fixMaxHeight: boolean): void { + if (fixMaxHeight || position.avilableHeight >= layoutParam.targetLayout.height) { + return; + } + ele.style.maxHeight = `${position.avilableHeight}px`; + } + /** + * 当drop固定高度(设置fixMaxHeight=ture),且空间不足时,dominator和drop不相邻,此场景需要隐藏drop中的边框覆盖线, + * 添加样式类'ti3-detach-dominator-drop'标记drop的这种场景状态,通过css样式控制drop中边框覆盖线的隐藏和显示 + * 目前该场景出现在日期类组件中 + */ + public static setDominatorDropDetachState(ele: HTMLElement, position: any, layoutParam: any, fixMaxHeight: boolean): void { + if (!fixMaxHeight) { + return; + } + if (position.avilableHeight < layoutParam.targetLayout.height) { + ele.classList.add('ti3-detach-dominator-drop'); + } else { + ele.classList.remove('ti3-detach-dominator-drop'); + } + } + + private static getEleLayout(ele: any): TiEleLayout { + // 修复SSR报错:ERROR TypeError: ele.getBoundingClientRect is not a function + return { + top: ele.offsetTop, + left: ele.offsetLeft, + width: typeof ele.getBoundingClientRect === 'function' ? ele.getBoundingClientRect().width : 0, + height: typeof ele.getBoundingClientRect === 'function' ? ele.getBoundingClientRect().height : 0 + }; + } + public static getHostEleLayout(hostEle: any, horizonHostEle?: any): TiHostLayout { + let horizonEle = hostEle; + if (Util.isElement(horizonHostEle)) { + horizonEle = horizonHostEle; + } + // 修复SSR报错:ERROR TypeError: hostEle.getBoundingClientRect is not a function + // 元素相对于windows的位置 + let top: number = typeof hostEle.getBoundingClientRect === 'function' ? hostEle.getBoundingClientRect().top : 0; + let left: number = typeof hostEle.getBoundingClientRect === 'function' ? horizonEle.getBoundingClientRect().left : 0; + // 修复SSR报错:ERROR ReferenceError: getComputedStyle is not defined + // 下面利用body相对于windows的位置,计算元素相对于body的位置。 + const bodyPositon: string = typeof getComputedStyle === 'function' ? getComputedStyle(document.body, null).position : 'static'; + // body是static|relative|absolute|fixed都已测试,Chrome高低版本/Firefox/IE11已测。 + if (bodyPositon === 'static') { + // static时,body对window没有偏移,但是需要考虑页面滚动条 + if (typeof window !== 'undefined') { + top += window.pageYOffset; + left += window.pageXOffset; + } + } else { + // relative|fix|absolute时,需要考虑body相对于windows的偏移。 + top -= document.body.getBoundingClientRect().top; + left -= document.body.getBoundingClientRect().left; + } + + return { + top, + left, + width: horizonEle.offsetWidth, + height: hostEle.offsetHeight, + fixedAncestor: this.getFixedAncestor(hostEle) + }; + } + + private static getAvilableLayout(hostLayout: TiHostLayout, consoleHeaderHeight?: number): TiAvilableLayout { + // 修复报错:ERROR ReferenceError: document is not defined + // 当前浏览器可视区域的宽高 + const curBrowseWidth: number = typeof document !== 'undefined' ? document.documentElement.clientWidth : 0; + const curBrowseheight: number = typeof document !== 'undefined' ? document.documentElement.clientHeight : 0; + // 元素位置距各部分位置: + // 1.以元素四个边为边界,上下左右预留的可用宽高,用avilableLeft等标识 + // 2.去掉元素的宽高占位,计算的最大可用宽高,用avilableMax...标识 + // 修复SSR错误:ERROR ReferenceError: pageXOffset is not defined + const avilableLeft: number = hostLayout.left - (typeof window !== 'undefined' ? window.pageXOffset : 0); + const avilableMaxLeft: number = avilableLeft + hostLayout.width; + const avilableRight: number = curBrowseWidth - avilableMaxLeft; + const avilableMaxRight: number = curBrowseWidth - avilableLeft; + // document.body.scrollTop document.documentElement.scrollTop存在浏览器差异,chrome高低版本表现不同。 + const curTop: number = hostLayout.top - (typeof window !== 'undefined' ? window.pageYOffset : 0); // pageYOffset支持IE9以上。 + const curMaxTop: number = curTop + hostLayout.height; + const avilableTop: number = curTop - consoleHeaderHeight; + const avilableMaxTop: number = avilableTop + hostLayout.height; + const avilableBottom: number = curBrowseheight - curMaxTop; + const avilableMaxBottom: number = curBrowseheight - curTop; + + return { + left: avilableLeft, + maxLeft: avilableMaxLeft, + right: avilableRight, + maxRight: avilableMaxRight, + top: avilableTop, + maxTop: avilableMaxTop, + bottom: avilableBottom, + maxBottom: avilableMaxBottom, + clientHeight: curBrowseheight, + clientWidth: curBrowseWidth + }; + } + // 确定元素是否为fixed定位 + private static getFixedAncestor(ele: any): any { + if (!ele || ele.nodeName === 'HTML') { + return null; + } + // 修复SSR报错:ERROR ReferenceError: getComputedStyle is not defined + if (typeof getComputedStyle === 'undefined') { + return null; + } + if (getComputedStyle(ele).position === 'fixed') { + return ele; + } + + return this.getFixedAncestor(ele.parentNode); + } + private static getLayoutParam( + layout: TiPositionLayout, + position: string, + hostSpace: number, + browserSpace: number, + hasOffsetFix: boolean + ): TiLayoutParams { + const positionArr: Array = position.split('-'); // 跟上面静态变量重名,最好改一下 + const left: number = Position.getHorizionParam(layout, positionArr, hostSpace, hasOffsetFix); + const verticalParam: TiVerticalParams = Position.getVerticalParam(layout, positionArr, hostSpace, browserSpace, hasOffsetFix); + + return { + left, + top: verticalParam.top, + avilableHeight: verticalParam.avilableHeight + }; + } + private static getHorizionParam(curLayout: TiPositionLayout, posArr: Array, space: number, hasOffsetFix: boolean): number { + const { + hostOffsetLayout, + hostLayout, + targetLayout + }: { + hostOffsetLayout?: any; + hostLayout: TiHostLayout; + targetLayout: TiEleLayout; + } = curLayout; + + // 定位偏差调整量(该偏差量针对较小元素,保证 tip 的箭头在被提示元素的中间) + const offset = hasOffsetFix ? this.getOffsetFixVal(curLayout.hostLayout.width) : 0; + const hostLeft: number = hostOffsetLayout ? hostOffsetLayout.left : hostLayout.left; + + // 'left', 'left-top', 'left-bottom' + if (posArr[0] === 'left') { + return hostLeft - targetLayout.width - space; + } + // 'right', 'right-top', 'right-bottom' + if (posArr[0] === 'right') { + return hostLeft + hostLayout.width + space; + } + // 'top-left', 'bottom-left' + if (posArr[1] === 'left') { + return hostLeft - offset; + } + // 'top-right', 'bottom-right' + if (posArr[1] === 'right') { + return hostLeft + hostLayout.width - targetLayout.width + offset; + } + + // 'top', 'bottom', 'center' + /** + * 问题修复:当left为负值时,tip会超出页面,显示不全,此处进行优化。 + */ + const leftTemp: number = hostLeft - (targetLayout.width - hostLayout.width) / 2; + + return leftTemp >= 0 || hostOffsetLayout ? leftTemp : 0; + } + private static getVerticalParam( + curLayout: TiPositionLayout, + posArr: Array, + hostSpace: number, + browserSpace: number, + hasOffsetFix: boolean + ): TiVerticalParams { + const avilableLayout: TiAvilableLayout = curLayout.avilableLayout; // 宿主元素对应的可用位置参数 + const hostHeight: number = curLayout.hostLayout.height; // 宿主元素本身高度 + const targetLayout: TiEleLayout = curLayout.targetLayout; + const hostLayout: TiHostLayout = curLayout.hostLayout; + const hostOffsetLayout: TiHostLayout = curLayout.hostOffsetLayout; + + // 定位偏差调整量(该偏差量针对较小元素,保证 tip 的箭头在被提示元素的中间) + const offset = hasOffsetFix ? this.getOffsetFixVal(curLayout.hostLayout.height) : 0; + const hostTop: number = hostOffsetLayout ? hostOffsetLayout.top : hostLayout.top; + // 纵向可用高度是根据位置中top、bottom、居中情况进行的处理,因此,此处先将position进行分割,再根据其情况进行可用高度计算 + // 弹出元素下边线与宿主元素上边线相接后,弹出元素向上延伸情况,对应位置包含'top', 'top-left', 'top-right' + if (posArr[0] === 'top') { + const avilableHeight: number = avilableLayout.top - hostSpace - browserSpace; + const top: number = + avilableHeight >= targetLayout.height ? hostTop - targetLayout.height - hostSpace : hostTop - avilableLayout.top + browserSpace; + + return { + avilableHeight, + top + }; + } + // 弹出元素上边线与宿主元素下边线相接后,弹出元素向下延伸情况,对应位置包含'bottom', 'bottom-left', 'bottom-right' + if (posArr[0] === 'bottom') { + return { + avilableHeight: avilableLayout.bottom - hostSpace - browserSpace, + top: hostTop + hostLayout.height + hostSpace + }; + } + // 弹出元素上边线与宿主元素上边线对齐情况,对应位置包含'left-top', 'right-top' + if (posArr[1] === 'top') { + return { + avilableHeight: avilableLayout.bottom + hostHeight, + top: hostTop - offset + }; + } + // 弹出元素上边线与宿主元素下边线对齐情况,对应位置包含'right-bottom', 'left-bottom' + if (posArr[1] === 'bottom') { + return { + avilableHeight: avilableLayout.top + hostHeight, + top: hostTop + hostLayout.height - targetLayout.height + offset + }; + } + + // 弹出元素中线与宿主元素中线对齐情况,对应位置包含'left', 'right', 'center' + const avilableHeight: number = avilableLayout.clientHeight - browserSpace; + const topTemp: number = + avilableHeight >= targetLayout.height + ? hostTop - (targetLayout.height - hostLayout.height) / 2 + : hostTop - avilableLayout.top + browserSpace; + + return { + avilableHeight, + top: topTemp >= 0 || hostOffsetLayout ? topTemp : 0 + }; + } + // 获取定位偏差调整量 + private static getOffsetFixVal(hostLayoutSize: number): number { + return hostLayoutSize / 2 < Position.ADAPTIVE_SIZE ? Position.ADAPTIVE_SIZE - hostLayoutSize / 2 : 0; + } +} diff --git a/src/utils/lib/src/TiBrowser.ts b/src/utils/lib/src/TiBrowser.ts new file mode 100644 index 0000000..ca99498 --- /dev/null +++ b/src/utils/lib/src/TiBrowser.ts @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/* eslint-disable no-cond-assign */ +// 另,angular也有brower-util.ts,但是没有对浏览器版本号的判断。 + +/* + * 浏览器类型的枚举定义。字符串枚举,提供了运行时可读性。 + */ + +export class TiBrowser { + /** + * EDGE 浏览器字符串 + */ + public static readonly EDGE: string = 'EDGE'; + /** + * IE 浏览器字符串 + */ + public static readonly IE: string = 'IE'; + /** + * FIREFOX 浏览器字符串 + */ + public static readonly FIREFOX: string = 'FIREFOX'; + /** + * CHROME 浏览器字符串 + */ + public static readonly CHROME: string = 'CHROME'; + /** + * OPERA 浏览器字符串 + */ + public static readonly OPERA: string = 'OPERA'; + /** + * SAFARI 浏览器字符串 + */ + public static readonly SAFARI: string = 'SAFARI'; + /** + * OTHER 浏览器字符串 + */ + public static readonly OTHER: string = 'OTHER'; + + private static _browser: string = TiBrowser.OTHER; + private static _version: number = 0; // 浏览器的大版本号,整数。取第一个小数点前 + private static isInit: boolean = false; + + /** + * 浏览器类型 + * @returns 返回枚举型浏览器类型字符串 + */ + public static browser(): string { + TiBrowser.init(); + + return TiBrowser._browser; + } + /** + * 浏览器版本 + */ + public static version(): number { + TiBrowser.init(); + + return TiBrowser._version; + } + /** + * 是否 Edge + */ + public static isEdge(): boolean { + TiBrowser.init(); + + return TiBrowser.browser() === TiBrowser.EDGE; + } + /** + * 是否 IE + */ + public static isIE(): boolean { + TiBrowser.init(); + + return TiBrowser.browser() === TiBrowser.IE; + } + /** + * 是否 Firefox + */ + public static isFirefox(): boolean { + TiBrowser.init(); + + return TiBrowser.browser() === TiBrowser.FIREFOX; + } + /** + * 是否 Chrome + */ + public static isChrome(): boolean { + TiBrowser.init(); + + return TiBrowser.browser() === TiBrowser.CHROME; + } + /** + * 是否 Opera + */ + public static isOpera(): boolean { + TiBrowser.init(); + + return TiBrowser.browser() === TiBrowser.OPERA; + } + /** + * 是否 Safari + */ + public static isSafari(): boolean { + TiBrowser.init(); + + return TiBrowser.browser() === TiBrowser.SAFARI; + } + /** + * 是否 Other + */ + public static isOther(): boolean { + TiBrowser.init(); + + return TiBrowser.browser() === TiBrowser.OTHER; + } + + private static init(): void { + if (TiBrowser.isInit) { + // 如果已经初始化,那么返回 + return; + } + // 浏览器判断,已支持SSR + const userAgent: string = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase() : ''; + let aAgentInfo: Array | null; + + if (userAgent.match(/edge\/([\d.]+)/)) { + aAgentInfo = userAgent.match(/edge\/([\d.]+)/); + TiBrowser._browser = TiBrowser.EDGE; + } else if (userAgent.match(/rv:([\d.]+)\) like gecko/)) { + aAgentInfo = userAgent.match(/rv:([\d.]+)\) like gecko/); + TiBrowser._browser = TiBrowser.IE; + } else if (userAgent.match(/msie ([\d.]+)/)) { + aAgentInfo = userAgent.match(/msie ([\d.]+)/); + TiBrowser._browser = TiBrowser.IE; + } else if (userAgent.match(/firefox\/([\d.]+)/)) { + aAgentInfo = userAgent.match(/firefox\/([\d.]+)/); + TiBrowser._browser = TiBrowser.FIREFOX; + } else if (userAgent.match(/chrome\/([\d.]+)/)) { + aAgentInfo = userAgent.match(/chrome\/([\d.]+)/); + TiBrowser._browser = TiBrowser.CHROME; + } else if (userAgent.match(/version\/([\d.]+).*safari/)) { + aAgentInfo = userAgent.match(/version\/([\d.]+).*safari/); + TiBrowser._browser = TiBrowser.SAFARI; + } else { + TiBrowser._browser = TiBrowser.OTHER; + } + // 版本号取整 + TiBrowser._version = aAgentInfo !== null ? parseInt(aAgentInfo[1].split('.')[0], 10) : 0; + // 改变初始化值 + TiBrowser.isInit = true; + } +} diff --git a/src/utils/lib/src/TiDateUtil.ts b/src/utils/lib/src/TiDateUtil.ts new file mode 100644 index 0000000..52d15dc --- /dev/null +++ b/src/utils/lib/src/TiDateUtil.ts @@ -0,0 +1,470 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * @ignore + */ +export class TiDateUtil { + /* + * 区分中英文冒号 + */ + public static COLON_REGEXP: RegExp = /[:\uff1a]/; + /* + * 默认时间格式 + */ + public static DEFAULT_TIME_FORMAT: string = 'HH:mm:ss'; + /** + * 日期类组件的默认最小值 + */ + public static minDate(): Date { + return new Date(1970, 0, 1, 0, 0, 0); + } + /** + * 日期类组件的默认最大值 + */ + public static maxDate(): Date { + return new Date(2099, 11, 31, 23, 59, 59); + } + /** + * 判断是不是合法的时间对象 + */ + public static isDate(date: object | string): boolean { + if (Object.prototype.toString.call(date) === '[object Date]' && String(date) !== 'Invalid Date') { + return true; + } + + return false; + } + + /** + * @description 根据当前格式,格式化最大值 + * @param maxValue 最大值 + * @param picker 面板格式 + * @param isDateTime 是否是日期时间组件 + */ + public static changeMaxTime(maxValue: Date, picker?: string, isDateTime?: boolean): Date { + let value: Date = maxValue; + + if (!TiDateUtil.isDate(value)) { + value = TiDateUtil.maxDate(); + } + + if (isDateTime) { + return TiDateUtil.transformDatetimeToExactDatetime(value, picker, true); + } + + if (picker === 'quarter') { + const transfromDateToString: string = TiDateUtil.transformDateToQuarter(value); + + return TiDateUtil.getValidQuarterDate(transfromDateToString, true); + } + + return TiDateUtil.transformDateToExactDate(value, picker, true); + } + + /** + * @description 根据当前格式,格式化最小值 + * @param minValue 最小值 + * @param picker 面板格式 + * @param isDateTime 是否是日期时间组件 + */ + public static changeMinTime(minValue: Date, picker?: string, isDateTime?: boolean): Date { + let value: Date = minValue; + + if (!TiDateUtil.isDate(value)) { + value = TiDateUtil.minDate(); + } + + if (isDateTime) { + return TiDateUtil.transformDatetimeToExactDatetime(value, picker); + } + + if (picker === 'quarter') { + const transfromDateToString: string = TiDateUtil.transformDateToQuarter(value); + + return TiDateUtil.getValidQuarterDate(transfromDateToString); + } + + return TiDateUtil.transformDateToExactDate(value, picker); + } + + /** + * @description 判断两个日期是不是相等:只包括年月日 + * @param newValue 新值 + * @param oldValue 旧值 + */ + public static isDateEqual(newValue: Date, oldValue: Date): boolean { + if (newValue === oldValue) { + return true; + } + + // 当两个value都是Date类型时,要判断两个值的年月日是否相等,直接用‘===’判断不准确 + if (TiDateUtil.isDate(newValue) && TiDateUtil.isDate(oldValue)) { + // 转换成年月日,然后重新生成一个Date,再将其转换成毫秒数进行判断 + const newDate: Date = new Date(newValue.getFullYear(), newValue.getMonth(), newValue.getDate()); + const oldDate: Date = new Date(oldValue.getFullYear(), oldValue.getMonth(), oldValue.getDate()); + if (newDate.getTime() === oldDate.getTime()) { + return true; + } + } + + return false; + } + + /** + * @description 判断两个时间日期是不是相等; + * @param newValue 新值 + * @param oldValue 旧值 + */ + public static isDatetimeEqual(newValue: Date, oldValue: Date): boolean { + if (newValue === oldValue) { + return true; + } + + if (TiDateUtil.isDate(newValue) && TiDateUtil.isDate(oldValue)) { + if (newValue.getTime() === oldValue.getTime()) { + return true; + } + } + + return false; + } + + /** + * @description 判断是不是合法的NowDateTime值 + * 存在nowDateTime值,并且是时间对象,且大于最小值小于最大值 + */ + public static isValidNowDateTime(nowDateTime: Date, min: Date, max: Date): boolean { + return ( + nowDateTime && TiDateUtil.isDate(nowDateTime) && nowDateTime.getTime() >= min.getTime() && nowDateTime.getTime() <= max.getTime() + ); + } + + /** + * @description 判断value1是否大于value2 + */ + public static isBigger(value1: Date, value2: Date): boolean { + return value1.getTime() > value2.getTime(); + } + + /** + * @description 判断value1是否大于value2 + */ + public static isBiggerOrEqual(value1: Date, value2: Date): boolean { + return value1.getTime() >= value2.getTime(); + } + + /** + * @description 判断value1是否小于value2 + */ + public static isSmaller(value1: Date, value2: Date): boolean { + return value1.getTime() < value2.getTime(); + } + + /** + * @description 将Date类型的日期转换成年月日字符串 + * @param date 要进行转换的日期值 + */ + public static getDateStr(date: Date): string { + return date === null ? '' : `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; + } + + /** + * @description 将Date类型的日期转换成时分秒字符串 + * @param date 要进行转换的日期值 + */ + public static getTimeStr(date: Date): string { + const hour = date.getHours() < 10 ? '0' + date.getHours() : date.getHours(); + const minute = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes(); + const second = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds(); + + return `${hour}:${minute}:${second}`; + } + + /** + * @description 判断是不是一个合法的最大值 + * @param min 最小值 + * @param max 最大值 + */ + public static isValidMaxValue(min: Date, max: Date): boolean { + return TiDateUtil.isDate(max) && max.getTime() >= min.getTime(); + } + + /** + * @description 判断是不是一个合法的最小值 + * @param min 最小值 + * @param max 最大值 + */ + public static isValidMinValue(min: Date, max: Date): boolean { + return TiDateUtil.isDate(min) && max.getTime() >= min.getTime(); + } + + /** + * @description 是否在最大值最小值区间 + * @param min 最小值 + * @param max 最大值 + */ + public static isBetweenMaxAndmin(value: Date, min: Date, max: Date): boolean { + return value.getTime() >= min.getTime() && value.getTime() <= max.getTime(); + } + + /** + * @description 12小时制添加AMPM + */ + public static addAmPm(timeFormat: string): string { + if (timeFormat.match(/h/) && !timeFormat.match(/a/)) { + // 已设置ampm格式情况下,不做处理 + return timeFormat + ' a'; // Angular 的datePipe 设置时间时,"a"代表AMPM + } + + return timeFormat; + } + + /** + * @description: 若时间字符串没有一个冒号时处理成合法的事件字符串,根据时间字符串冒号的个数和是否是结束时间添加为完整的时间(包含时分秒) + * 例如 value: 12, isEndTime: true, 则返回12:59:59 + * @param value 时间字符串 + * @param isEndTime 是否是结束时间 + */ + public static addColon(value: string, isEndTime?: boolean): string { + const colonNumber: number = value.split(':').length - 1; + if (colonNumber) { + if (isEndTime && colonNumber === 1) { + return `${value}:59`; + } + + return value; + } + + const ampm: any = value.match(/am|AM|pm|PM/) || ''; + + if (isEndTime) { + return `${parseInt(value, 10)}:59:59 ${ampm}`; + } + + return `${parseInt(value, 10)}: ${ampm}`; + } + + /** + * @ignore + * date类型转换为季度 + */ + public static transformDateToQuarter(val: Date): string { + switch (val?.getMonth()) { + case 0: + case 1: + case 2: + return `${val.getFullYear()}/Q1`; + case 3: + case 4: + case 5: + return `${val.getFullYear()}/Q2`; + case 6: + case 7: + case 8: + return `${val.getFullYear()}/Q3`; + case 9: + case 10: + case 11: + return `${val.getFullYear()}/Q4`; + default: + return undefined; + } + } + /** + * 季度值转换为date类型 + */ + public static transFormQuarterToDate(year: number, val: string, isEnd?: boolean): any { + if (isEnd) { + switch (val) { + case 'Q1': + return new Date(year, 2, 31, 23, 59, 59, 999); + case 'Q2': + return new Date(year, 5, 30, 23, 59, 59, 999); + case 'Q3': + return new Date(year, 8, 30, 23, 59, 59, 999); + case 'Q4': + return new Date(year, 11, 31, 23, 59, 59, 999); + default: + return undefined; + } + } else { + switch (val) { + case 'Q1': + return new Date(year, 0, 1, 0, 0, 0); + case 'Q2': + return new Date(year, 3, 1, 0, 0, 0); + case 'Q3': + return new Date(year, 6, 1, 0, 0, 0); + case 'Q4': + return new Date(year, 9, 1, 0, 0, 0); + default: + return undefined; + } + } + } + /** + * @description 将当前日期转化为对应具体日期时间 + * + * @param value 要进行转换的日期值 + * @param picker 面板格式 + * @param isEnd 是否是结束日期 + */ + public static transformDateToExactDate(value: Date, picker: string, isEnd?: boolean): Date { + if (!this.isDate(value)) { + return; + } + const year: number = value.getFullYear(); + const month: number = value.getMonth() || value.getMonth() === 0 ? value.getMonth() : 11; + + if (isEnd) { + switch (picker) { + case 'onlyYear': + case 'year': + // 纯年份格式下,结束具体日期时间为12月31号23时59分59秒 + return new Date(year, 11, 31, 23, 59, 59, 999); + case 'onlyYearMonth': + case 'month': + // 年月格式下,结束具体日期时间当月最后一天的23时59分59秒 + // new Date(val.getFullYear(), month + 1, 0).getDate()获取当月的总天数, + // 此处month需要加1,否则获取的是上月总天数。 + return new Date(year, month, new Date(value.getFullYear(), month + 1, 0).getDate(), 23, 59, 59, 999); + default: + // 正常情况下,结束时间值取23时59分59秒,开始时间不处理,默认为零点。 + return new Date(year, month, value.getDate(), 23, 59, 59, 999); + } + } else { + switch (picker) { + case 'onlyYear': + case 'year': + // 纯年份格式下,开始具体日期时间为1月1号零点 + return new Date(year, 0, 1); + case 'onlyYearMonth': + case 'month': + // 年月格式下,开始具体日期时间当月第一天零点 + return new Date(year, month); + default: + // 正常情况下,开始时间值零点 + return new Date(year, month, value.getDate()); + } + } + } + + /** + * @description 将当前日期时间转化为对应具体日期时间 + * + * @param value 要进行转换的日期时间值 + * @param dateTimePicker 时间格式 + */ + public static transformDatetimeToExactDatetime(value: Date, dateTimePicker: string, isEnd?: boolean): Date { + if (!TiDateUtil.isDate(value)) { + return; + } + + const year: number = value.getFullYear(); + const month: number = value.getMonth(); + const date: number = value.getDate(); + const hours: number = value.getHours(); + const minutes: number = value.getMinutes(); + const seconds: number = value.getSeconds(); + + if (isEnd) { + switch (dateTimePicker) { + case 'onlyHours': + // 纯小时格式下,结束具体日期时间到59分59秒 + return new Date(year, month, date, hours, 59, 59, 999); + case 'onlyHoursMinutes': + // 纯时分格式下,结束具体日期时间到59秒 + return new Date(year, month, date, hours, minutes, 59, 999); + default: + // 正常情况下,结束时间值具体到秒 + return new Date(year, month, date, hours, minutes, seconds, 999); + } + } else { + switch (dateTimePicker) { + case 'onlyHours': + // 纯小时格式下,结束具体日期时间到时 + return new Date(year, month, date, hours); + case 'onlyHoursMinutes': + // 纯时分格式下,结束具体日期时间到分 + return new Date(year, month, date, hours, minutes); + default: + // 正常情况下,结束时间值具体到秒 + return new Date(year, month, date, hours, minutes, seconds); + } + } + } + + /** + * @description 获取当前合法的季度值并转化为对应的日期 + * + * @param value 季度值 + * @param isEnd 是否是结束日期 + */ + public static getValidQuarterDate(value: string, isEnd?: boolean): Date { + const quarters: Array = ['Q1', 'Q2', 'Q3', 'Q4']; + const reg: RegExp = /[年月日\-/._]/; + const dateArr: Array = value.split(reg); + const index: number = quarters.findIndex((item: string) => item === dateArr[1].toUpperCase()); + if (dateArr.length < 2 || index === -1) { + return; + } + + return TiDateUtil.transFormQuarterToDate(Number(dateArr[0]), dateArr[1].toUpperCase(), isEnd); + } + + /** + * @description 判断是否为禁用日期 + * + * @param disabledDays 禁用的日期 + * @param value 日期值 + */ + public static isDisabledDays(disabledDays: Array, value: Date): boolean { + if (!Array.isArray(disabledDays)) { + return false; + } + + return disabledDays.some((item: Date) => item.getTime() === value.getTime()); + } + + /** + * @description 根据时区获取当前选中本地时间 + * + * @param dateString 当前选择的时间 + * @param timeZoneable 时区选择开关 + * @param selectedOption 时区选择项 + */ + public static getLocalDate(dateString: string, timeZoneable: boolean, selectedOption: string): Date { + if (dateString === 'null') { + return null; + } + + // 将UTC时间转化为本地时间 + return timeZoneable && selectedOption === 'UTC/GMT' ? new Date(`${dateString} UTC`) : new Date(`${dateString}`); + } + + /** + * @description 根据时区获取当前选中UTC时间(此处的UTC时间指的并不是实际意义上的UTC时间,而是根据UTC时间转化为对应的本地时间呈现给用户) + * + * @param date 当前选择的时间 + * @param timeZoneable 时区选择开关 + * @param selectedOption 时区选择项 + */ + public static getUtcDate(date: Date, timeZoneable: boolean, selectedOption: string): Date { + if (date === null) { + return null; + } + + // 将UTC时间转化为new Date()形式时间格式 + // getTimezoneOffset() 返回 UTC 时间与本地时间之间的时差,以分钟为单位。 + return timeZoneable && selectedOption === 'UTC/GMT' ? new Date(date.getTime() + date.getTimezoneOffset() * 60 * 1000) : date; + } +} diff --git a/src/utils/lib/src/TiKeymap.ts b/src/utils/lib/src/TiKeymap.ts new file mode 100644 index 0000000..f92df46 --- /dev/null +++ b/src/utils/lib/src/TiKeymap.ts @@ -0,0 +1,381 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiBrowser } from './TiBrowser'; + +// 另,Angular官方键值调用方法: + +/* 使用键值请参考: +https://blog.csdn.net/q1054261752/article/details/50359617 +http://www.runoob.com/try/try.php?filename=tryjsref_event_key_keycode2 +https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent +事件顺序:onkeydown > onkeypress >onkeyup +早期标准: +keydown和keyup捕获具体物理按键,event.keyCode输出键值,可以区分主副键盘的具体按键; +keypress捕获按键经过处理后的字符,event.charCode输出ASCII字符编码,可以区分大小写字母,主副键盘相同符号取值相同。 +event.which根据情况输出charCode(keypress时)或keyCode(keydown时)。 +IE8只有keyCode属性,IE9起已支持which和charCode。 +Chrome和IE等,在keypress时,keyCode也存放了charCode值。(是为了兼容只有keyCode属性的IE8) +FireFox,keypress只输出event.charCode,keydown只输出event.keycode;另一个值为0。 +现今情况: +keyCode/charCode/which都已废弃,但keyCode依然是目前最好选择!(2018年4月,须兼容IE9) +未来情况: +keypress和keydown时,都可用event.key("z")和event.code("KeyZ"),是推荐标准,都是字符串可读性值。 +IE9不支持event.code。 +IE9的event.key是旧标准。小键盘符号支持很差:和大键盘取值不同,且keypress和keydown时取值不同。有时符号,有时符号名称。 + +综上:建议只使用keydow和keyup事件和event.keyCode键值,不使用keypress/which/charCode +很少有需求使用keypress,仅配合 event.charCode取出字符编码即可。 +再配合'A'.charCodeAt(0)===event.charCode或者'A'===String.fromCharCode(event.charCode); +*/ +/* HTML W3C标准键值查找类,已经屏蔽浏览器差异 + * + * 参考文档: + * https://www.w3.org/TR/uievents/ + * + * https://www.w3.org/TR/uievents-code/ + * + * https://www.w3.org/TR/uievents-key + * + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code + * + * https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button + * + * https://github.com/kabirbaidhya/keycode-js 这份代码采用了Mac命名方式,比如回车键叫Return + * + * https://github.com/wesbos/keycodes/blob/gh-pages/scripts.js + * + * http://blog.csdn.net/joeblackzqq/article/details/46442121 + * + * + * W3C规定了一些固定键值,其他键值可能随硬件(PC/Mac)、操作系统、浏览器而不同。 + * + * 命名规范统一按照W3C名称,已涵盖标准101键盘,其他键盘暂未涵盖。 + * + * js中左右Ctrl、大小键盘Enter是同一键值,所以比windows编程环境的键值要少一些。 + * + * 多余Mac键盘值已删除。 + * + * 标点符号,因为Firefox与其他浏览器有三个不同,已做兼容性处理 + */ + +export class TiKeymap { + /* 此部分为W3C规定的固定键值 start↓ */ + /** + * Mac称作Delete 【注意: Backspace 8/Delete 46(windows用后向删除)是两个不同键值。】 + */ + public static readonly KEY_BACKSPACE: number = 8; + /** + * Table键 + */ + public static readonly KEY_TAB: number = 9; + /** + * 回车键,数字小键盘回车也是13。 Mac称作Return。【注意:108是Mac数字小键盘Enter】 + */ + public static readonly KEY_ENTER: number = 13; + /** + * 左右Shift + */ + public static readonly KEY_SHIFT: number = 16; + /** + * 左右Ctrl + */ + public static readonly KEY_CONTROL: number = 17; + /** + * 左右Alt Mac称作Option + */ + public static readonly KEY_ALT: number = 18; + /** + * W3C未规定固定或者不固定,是PC(非Mac)独有 + */ + public static readonly KEY_PAUSE_BREAK: number = 19; + /** + * 大小写锁定 + */ + public static readonly KEY_CAPS_LOCK: number = 20; + /** + * 左上角Esc + */ + public static readonly KEY_ESCAPE: number = 27; + public static readonly KEY_SPACE: number = 32; + public static readonly KEY_PAGE_UP: number = 33; + public static readonly KEY_PAGE_DOWN: number = 34; + public static readonly KEY_END: number = 35; + public static readonly KEY_HOME: number = 36; + /** + * 箭头方向键 + */ + public static readonly KEY_ARROW_LEFT: number = 37; + public static readonly KEY_ARROW_UP: number = 38; + public static readonly KEY_ARROW_RIGHT: number = 39; + public static readonly KEY_ARROW_DOWN: number = 40; + /** + * W3C未规定固定或非固定,是PC(非Mac)独有 + */ + public static readonly KEY_PRINT_SCREEN: number = 44; // W3C未规定固定或非固定,是PC(非Mac)独有 + /** + * W3C未规定固定或非固定,是PC(非Mac)独有 + */ + public static readonly KEY_INSERT: number = 45; // W3C未规定固定或非固定,是PC(非Mac)独有 + /** + * 后向删除 + */ + public static readonly KEY_DELETE: number = 46; + public static readonly KEY_0: number = 48; + public static readonly KEY_1: number = 49; + public static readonly KEY_2: number = 50; + public static readonly KEY_3: number = 51; + public static readonly KEY_4: number = 52; + public static readonly KEY_5: number = 53; + public static readonly KEY_6: number = 54; + public static readonly KEY_7: number = 55; + public static readonly KEY_8: number = 56; + public static readonly KEY_9: number = 57; + public static readonly KEY_A: number = 65; + public static readonly KEY_B: number = 66; + public static readonly KEY_C: number = 67; + public static readonly KEY_D: number = 68; + public static readonly KEY_E: number = 69; + public static readonly KEY_F: number = 70; + public static readonly KEY_G: number = 71; + public static readonly KEY_H: number = 72; + public static readonly KEY_I: number = 73; + public static readonly KEY_J: number = 74; + public static readonly KEY_K: number = 75; + public static readonly KEY_L: number = 76; + public static readonly KEY_M: number = 77; + public static readonly KEY_N: number = 78; + public static readonly KEY_O: number = 79; + public static readonly KEY_P: number = 80; + public static readonly KEY_Q: number = 81; + public static readonly KEY_R: number = 82; + public static readonly KEY_S: number = 83; + public static readonly KEY_T: number = 84; + public static readonly KEY_U: number = 85; + public static readonly KEY_V: number = 86; + public static readonly KEY_W: number = 87; + public static readonly KEY_X: number = 88; + public static readonly KEY_Y: number = 89; + public static readonly KEY_Z: number = 90; + /* 此部分为W3C规定的固定键值 end↑ */ + /** + * "Windows Key Left/ Left Command⌘ (Mac)/ Chromebook Search key"; + */ + public static readonly KEY_META_LEFT: number = 91; + /** + * "Windows Key Right + */ + public static readonly KEY_META_RIGHT: number = 92; + /** + * "Windows Menu / Right Command⌘(Mac)"; + */ + public static readonly KEY_CONTEXT_MENU: number = 93; + public static readonly KEY_NUMPAD_0: number = 96; + public static readonly KEY_NUMPAD_1: number = 97; + public static readonly KEY_NUMPAD_2: number = 98; + public static readonly KEY_NUMPAD_3: number = 99; + public static readonly KEY_NUMPAD_4: number = 100; + public static readonly KEY_NUMPAD_5: number = 101; + public static readonly KEY_NUMPAD_6: number = 102; + public static readonly KEY_NUMPAD_7: number = 103; + public static readonly KEY_NUMPAD_8: number = 104; + public static readonly KEY_NUMPAD_9: number = 105; + /** + * 小键盘* + */ + public static readonly KEY_NUMPAD_MULTIPLY: number = 106; + /** + * 小键盘+ + */ + public static readonly KEY_NUMPAD_ADD: number = 107; + /** + * Mac小键盘Enter。可能某些键盘除法符号。且这个键值可能在Firefox与其他浏览器不同。 + */ + public static readonly KEY_NUMPAD_ENTER: number = 108; + /** + * 小键盘- + */ + public static readonly KEY_NUMPAD_SUBTRACT: number = 109; + /** + * 小键盘. (小数点) + */ + public static readonly KEY_NUMPAD_DECIMAL: number = 110; + /** + * 小键盘/ + */ + public static readonly KEY_NUMPAD_DIVIDE: number = 111; + public static readonly KEY_F1: number = 112; + public static readonly KEY_F2: number = 113; + public static readonly KEY_F3: number = 114; + public static readonly KEY_F4: number = 115; + public static readonly KEY_F5: number = 116; + public static readonly KEY_F6: number = 117; + public static readonly KEY_F7: number = 118; + public static readonly KEY_F8: number = 119; + public static readonly KEY_F9: number = 120; + public static readonly KEY_F10: number = 121; + public static readonly KEY_F11: number = 122; + public static readonly KEY_F12: number = 123; + public static readonly KEY_F13: number = 124; + public static readonly KEY_F14: number = 125; + public static readonly KEY_F15: number = 126; + public static readonly KEY_F16: number = 127; + public static readonly KEY_F17: number = 128; + public static readonly KEY_F18: number = 129; + public static readonly KEY_F19: number = 130; + public static readonly KEY_F20: number = 131; + public static readonly KEY_F21: number = 132; + public static readonly KEY_F22: number = 133; + public static readonly KEY_F23: number = 134; + public static readonly KEY_F24: number = 135; + /** + * W3C未规定固定或不固定,是PC(非Mac)独有。 在Mac上这个键值给numpad_clear用了。 + */ + public static readonly KEY_NUM_LOCK: number = 144; + /** + * W3C未规定固定或不固定,是PC(非Mac)独有 + */ + public static readonly KEY_SCROLL_LOCK: number = 145; + /* 下面是W3C符号美标键值,不固定。Firefox符号键值有3个与其他浏览器不同 */ + /** + * ";" Firefox键值是59,使用时已经屏蔽差异 + */ + public static KEY_SEMICOLON: number = 186; + /** + * ":" Firefox键值是59,使用时已经屏蔽差异 + */ + public static KEY_COLON: number = TiKeymap.KEY_SEMICOLON; + /** + * "=" Firefox键值是61,使用时已经屏蔽差异 + */ + public static KEY_EQUALS_SIGN: number = 187; + /** + * "+" Firefox键值是61,使用时已经屏蔽差异 + */ + public static KEY_PLUS: number = TiKeymap.KEY_EQUALS_SIGN; + /** + * "-" Firefox键值是173,使用时已经屏蔽差异 + */ + public static KEY_MINUS: number = 189; + /** + * "_" Firefox键值是173,使用时已经屏蔽差异 + */ + public static KEY_UNDERSCORE: number = TiKeymap.KEY_MINUS; + /** + * "," + */ + public static readonly KEY_COMMA: number = 188; + /** + * "<" + */ + public static readonly KEY_LESS_THAN_SIGN: number = 188; + /** + * "." + */ + public static readonly KEY_PERIOD: number = 190; + /** + * ">" + */ + public static readonly KEY_GREATER_THAN_SIGN: number = 190; + /** + * "/" + */ + public static readonly KEY_FORWARD_SLASH: number = 191; + /** + * "?" + */ + public static readonly KEY_QUESTION_MARK: number = 191; + /** + * "`" + */ + public static readonly KEY_BACKTICK: number = 192; + /** + * "~" + */ + public static readonly KEY_TILDE: number = 192; + /** + * "[" + */ + public static readonly KEY_OPENING_SQUACE_BRACKET: number = 219; + /** + * "{" + */ + public static readonly KEY_OPENING_CURLY_BRACE: number = 219; + /** + * "\" + */ + public static readonly KEY_BACKSLASH: number = 220; + /** + * "|" + */ + public static readonly KEY_PIPE: number = 220; + /** + * "]" + */ + public static readonly KEY_CLOSING_SQUARE_BRACKET: number = 221; + /** + * "}" + */ + public static readonly KEY_CLOSING_CURLY_BRACE: number = 221; + /** + * "'" + */ + public static readonly KEY_SINGLE_QUOTE: number = 222; + /** + * """ + */ + public static readonly KEY_DOUBLE_QUOTE: number = 222; + + /* 鼠标键值,前三个为W3C固定,命名暂时按照程序员习惯。 */ + /** + * 鼠标键值:primary button + */ + public static readonly MOUSE_LEFT_BUTTON: number = 0; // primary button + /** + * 鼠标键值:auxiliary button wheel button + */ + public static readonly MOUSE_MIDDLE_BUTTON: number = 1; // auxiliary button wheel button + /** + * 鼠标键值:secondary button + */ + public static readonly MOUSE_RIGHT_BUTTON: number = 2; // secondary button + /** + * 鼠标键值:浏览器后退 + */ + public static readonly MOUSE_BACK_BUTTON: number = 3; // 浏览器后退 + /** + * 鼠标键值:浏览器前进 + */ + public static readonly MOUSE_FORWARD_BUTTON: number = 4; // 浏览器前进 + /** + * Typescript没有静态代码段,所以这样代替静态代码段 + */ + protected static staticCode: void = TiKeymap.initFirefox(); + /* Firefox符号键值有3个与其他浏览器不同 + Semicolon ";" 186 //Firefox 59 + Colon ":" 186 //Firefox 59 + Equals sign "=" 187 //Firefox 61 + Plus "+" 187 //Firefox 61 + Minus "-" 189 //Firefox 173 + Underscore "_" 189 //Firefox 173 + */ + private static initFirefox(): void { + if (TiBrowser.isFirefox()) { + TiKeymap.KEY_SEMICOLON = 59; // ";" + TiKeymap.KEY_COLON = TiKeymap.KEY_SEMICOLON; // ":" + TiKeymap.KEY_EQUALS_SIGN = 61; // "=" + TiKeymap.KEY_PLUS = TiKeymap.KEY_EQUALS_SIGN; // "+" + TiKeymap.KEY_MINUS = 173; // "-" + TiKeymap.KEY_UNDERSCORE = TiKeymap.KEY_MINUS; // "_" + } + } +} +// 因为Typescript没有类的静态代码段。所以对类的static变量初始化,写在这里。 diff --git a/src/utils/lib/src/TiLog.ts b/src/utils/lib/src/TiLog.ts new file mode 100644 index 0000000..0ee8af0 --- /dev/null +++ b/src/utils/lib/src/TiLog.ts @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/* + * Logger工具类提供全局日志输出级别控制。 + * + * 提供三个静态日志方法,log()/warn()/error() + * + * log()是调试期日志,产品时一定关闭。组件对打包对外提供时也关闭,不干扰用户开发。 + * + * info()是产品期也可以输出的重要日志。暂不提供。 + * + * warn()/error()产品期,可酌情打开。后期可以考虑,error日志回传给服务器。 + * + */ + +export class TiLog { + /** + * 输出 Log 以上日志,包含 Log/Warn/Error,log 表示一般的调试和运行信息。 + */ + public static readonly LEVEL_LOG: number = 1; + /** + * 输出 Warn 以上日志,包含 Warn/Error 日志,Warn 表示会出现潜在错误的提示。 + */ + public static readonly LEVEL_WARN: number = 2; + /** + * 输出 Error 日志,Error 表示发生错误事件,已经影响系统的正常运行。 + */ + public static readonly LEVEL_ERROR: number = 3; + /** + * 关闭输出所有日志 + */ + public static readonly LEVEL_OFF: number = 10; + + private static level: number = TiLog.LEVEL_OFF; + /** + * 设置输出日志级别:LOG/WARN/ERROR/OFF. 默认是 OFF + */ + public static setLevel(level: number): void { + TiLog.level = level; + } + /** + * 打印一般的调试和运行信息 + * @param [message] 信息 + * @param optionalParams 参数 + */ + public static log(message?: any, ...optionalParams: Array): void { + if (TiLog.LEVEL_LOG >= TiLog.level && console.log) { + console.log(message, ...optionalParams); + } + } + + /** + * 打印会出现潜在错误的提示 + * @param [message] 信息 + * @param optionalParams 参数 + */ + public static warn(message?: any, ...optionalParams: Array): void { + if (TiLog.LEVEL_WARN >= TiLog.level && console.warn) { + console.warn(message, ...optionalParams); + } + } + /** + * 打印在发生错误,已经影响系统的正常运行的信息 + * @param [message] 信息 + * @param optionalParams 参数 + */ + public static error(message?: any, ...optionalParams: Array): void { + if (TiLog.LEVEL_ERROR >= TiLog.level && console.error) { + console.error(message, ...optionalParams); + } + } +} diff --git a/src/utils/lib/src/TiTheme.ts b/src/utils/lib/src/TiTheme.ts new file mode 100644 index 0000000..8ce6648 --- /dev/null +++ b/src/utils/lib/src/TiTheme.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +export class TiTheme { + /** + * 加载CSS文件,挂在head link + * @param cssPath css路径 + * @param id link id + */ + public static loadCss(cssPath: string, id: string): HTMLLinkElement { + let link: HTMLLinkElement = document.getElementById(id) as HTMLLinkElement; + + // 原本没有link,那么创建link + if (!link) { + link = document.createElement('link'); + document.head.appendChild(link); + } + // 如果link href没变,那么就不处理了。 + if (link.href === cssPath) { + return; + } + link.href = cssPath; + link.id = id; + link.rel = 'stylesheet'; + link.type = 'text/css'; + + // 被CSSVar补丁处理后,补丁会给link打上disabled。去除disabled,才能再次补丁生效。 + if (link.hasAttribute('disabled')) { + link.removeAttribute('disabled'); + } + + return link; + } + /** + * 检测浏览器是否原生支持CSSVar + * @returns 是否支持CSSVar + */ + public static isNativeSupportCssVar(): boolean { + const isBrowser: boolean = typeof window !== 'undefined'; + const isNativeSupport: boolean = isBrowser && window.CSS && window.CSS.supports && window.CSS.supports('(--a: 0)'); + + return isNativeSupport; + } + /** + * 启动AppModule + * @param app 传入的AppModule + */ + public static bootstrapModule(app: any): void { + platformBrowserDynamic() + .bootstrapModule(app) + .catch((err: any) => console.error(err)); + } +} diff --git a/src/utils/lib/src/Util.ts b/src/utils/lib/src/Util.ts new file mode 100644 index 0000000..80fe772 --- /dev/null +++ b/src/utils/lib/src/Util.ts @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * @ignore + * 工具类 + */ +export class Util { + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + private static idCounter: number = 0; + /** + * 获得一个唯一标示 + * @param prefix 前缀 + * @returns 唯一标示字符串 + */ + public static getUniqueId(prefix: string): string { + Util.idCounter++; + const id: string = `${prefix}_${Util.idCounter}`; + + return id; + } + // TODO: 是否可以去除? + /** + * 是否未定义 + * @param value 值 + * @returns 是否 + */ + public static isUndefined(value: any): boolean { + return value === undefined; + } + // TODO: 是否可以去除? + /** + * 是否空 + * @param value 值 + * @returns 是否 + */ + public static isNull(value: any): boolean { + return value === null; + } + // TODO: 是否可以去除? + /** + * 是否String + * @param value 值 + * @returns 是否 + */ + public static isString(value: any): boolean { + return typeof value === 'string'; + } + // TODO: 是否可以去除? + /** + * 是否Number + * @param value 值 + * @returns 是否 + */ + public static isNumber(value: any): boolean { + return typeof value === 'number' && !isNaN(value); + } + + /** + * 是否空字符串 + * @param value 值 + * @returns 是否 + */ + public static isEmptyString(value: string): boolean { + return value === undefined || value === null || value === ''; + } + // TODO: 是否可以去除 + /** + * 是否Array + * @param value 值 + * @returns 是否 + */ + public static isArray(value: any): boolean { + return Array.isArray(value); + } + // TODO: 是否可以去除 + /** + * 是否函数 + * @param value 值 + * @returns 是否 + */ + public static isFunction(value: any): boolean { + return typeof value === 'function'; + } + + /** + * 使用固定参数值格式化填充字串 + * @param source 源字串,其中使用{N}代表需要匹配的参数次序,N从0开始 + * @param params Array 参数数组 + */ + public static formatEntry(source: string, params: Array): string { + let formatSource: string = source; + if (!this.isArray(params) || formatSource === '') { + return formatSource; + } + params.forEach((param: any, i: number) => { + formatSource = formatSource.replace(new RegExp(`\\{${i}\\}`, 'g'), () => { + return param; + }); + }); + + return formatSource; + } + + /** + * 发出一个HTML事件 + * @param: ele 触发事件的DOM + * @param: eventName 事件名称 + * @param: [canBubble] 是否冒泡,可选参数默认不冒泡 false + */ + public static trigger(ele: any, eventName: string, canBubble?: boolean): void { + // 修复SSR错误:ERROR ReferenceError: document is not defined + if (typeof document === 'undefined') { + return; + } + const event: Event = document.createEvent('HTMLEvents'); + event.initEvent(eventName, canBubble || false, true); + // event.initEvent(eventType,canBubble,cancelable) + ele?.dispatchEvent(event); + } + /** + * 判断是否为dom元素 + */ + public static isElement(ele: any): boolean { + if (ele && ele.nodeType) { + return ele.nodeType === 1; + } else { + return false; + } + } + /** + * 判断是否支持CSS Var + */ + public static supportsCssVars(): boolean { + return typeof window !== 'undefined' && (window as any).CSS && (window as any).CSS.supports && (window as any).CSS.supports('(--a: 0)'); + } + /** + * 通过tab键在弹窗内切换的时候,焦点需要一直在弹窗内部循环。 + */ + public static focusInDialogOnTabchange(event: KeyboardEvent, focusableElements: NodeList): void { + if (!focusableElements || focusableElements.length === 0) { + return; + } + const firstFocusableEle: any = focusableElements[0]; + const lastFocusableEle: any = focusableElements[focusableElements.length - 1]; + const target: EventTarget = event.target; // 获得触发事件的元素 + + // 按下tab+shift键时,如果当前已获取焦点元素是弹出框中的第一个可获取焦点元素,则聚焦最后一个元素 + if (event.shiftKey) { + if (target === firstFocusableEle) { + lastFocusableEle.focus(); + event.preventDefault(); // 阻止默认事件,确保此处手动focus生效 + } + } else if (target === lastFocusableEle) { + // 按下tab键时,如当前已获取焦点元素是最后一个可获取焦点元素,则聚焦第一个元素 + firstFocusableEle.focus(); + event.preventDefault(); // 阻止默认事件,确保此处手动focus生效 + } + } +} diff --git a/src/validation/demo/karma.conf.js b/src/validation/demo/karma.conf.js new file mode 100644 index 0000000..d08ec60 --- /dev/null +++ b/src/validation/demo/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/src/validation/demo/project.json b/src/validation/demo/project.json new file mode 100644 index 0000000..a7f1e24 --- /dev/null +++ b/src/validation/demo/project.json @@ -0,0 +1,90 @@ +{ + "projectType": "application", + "root": "src/validation/demo", + "sourceRoot": "src/validation/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/validation", + "index": "src/validation/demo/src/index.html", + "main": "src/validation/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/validation/demo/tsconfig.app.json", + "assets": ["src/validation/demo/src/favicon.ico", "src/validation/demo/src/assets"], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "validation-demo:build:production" + }, + "development": { + "browserTarget": "validation-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js validation" + } + ] + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/validation/demo/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/validation/demo/tsconfig.spec.json", + "karmaConfig": "src/validation/demo/karma.conf.js", + "inlineStyleLanguage": "less", + "styles": ["src/styles.less"], + "scripts": [] + } + } + } +} diff --git a/src/validation/demo/src/app/AppComponent.ts b/src/validation/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/validation/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/validation/demo/src/app/AppModule.ts b/src/validation/demo/src/app/AppModule.ts new file mode 100644 index 0000000..34d499d --- /dev/null +++ b/src/validation/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ValidationTestModule } from './validation/ValidationTestModule'; + +@NgModule({ + imports: [ + ValidationTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/validation/demo/src/app/IndexComponent.ts b/src/validation/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..289595c --- /dev/null +++ b/src/validation/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ValidationTestModule } from './validation/ValidationTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = ValidationTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/validation/demo/src/app/app.html b/src/validation/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/validation/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/validation/demo/src/app/validation/ValidationAsyncCheckComponent.ts b/src/validation/demo/src/app/validation/ValidationAsyncCheckComponent.ts new file mode 100644 index 0000000..6b76777 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationAsyncCheckComponent.ts @@ -0,0 +1,53 @@ +import { Component } from '@angular/core'; +import { AsyncValidatorFn, AbstractControl, FormControl, ValidationErrors } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { debounceTime, delay, distinctUntilChanged, first, map, catchError, switchMap } from 'rxjs/operators'; +import { TiValidators, TiValidationDirective } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-async-check.html' +}) +export class ValidationAsyncCheckComponent { + asyncControl: FormControl = new FormControl('', null, CustomAsyncValidators.isRightUserName()); + + mixControl: FormControl = new FormControl( + '', + [TiValidators.required, TiValidators.minLength(5)], + CustomAsyncValidators.isRightUserName() + ); +} + +export class CustomAsyncValidators { + static isRightUserName(): AsyncValidatorFn { + return (control: AbstractControl): Promise | Observable => { + if (control.valueChanges) { + return control.valueChanges.pipe( + debounceTime(TiValidationDirective.ASYNC_DEBOUNCE_TIME), + distinctUntilChanged(), + switchMap((value: string) => CustomAsyncValidators.isRight(value)), + map((isRight: boolean) => { + return isRight + ? { + rightUserName: { + actualValue: control.value, + tiAsyncErrorMessage: '用户名 {0} 不正确' + } + } + : null; + }), + first(), + catchError(() => null) + ); + } else { + return of(null); + } + }; + } + + static isRight(value: string): Observable { + return of(value !== 'hello tiny').pipe( + delay(2000), + catchError(() => of(false)) + ); + } +} diff --git a/src/validation/demo/src/app/validation/ValidationAsyncCheckTestComponent.ts b/src/validation/demo/src/app/validation/ValidationAsyncCheckTestComponent.ts new file mode 100644 index 0000000..6150d09 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationAsyncCheckTestComponent.ts @@ -0,0 +1,58 @@ +import { Component } from '@angular/core'; +import { AsyncValidatorFn, AbstractControl, FormControl, ValidationErrors } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { debounceTime, delay, distinctUntilChanged, first, map, catchError, switchMap } from 'rxjs/operators'; +import { TiValidators, TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-async-check-test.html' +}) +export class ValidationAsyncCheckTestComponent { + label: string = '用户名:'; + ccControl: FormControl = new FormControl('', [TiValidators.required, TiValidators.minLength(5)], CustomAsyncValidators.isRightUserName()); + ccControl1: FormControl = new FormControl( + '', + [TiValidators.required, TiValidators.minLength(5)], + CustomAsyncValidators.isRightUserName() + ); +} + +export class CustomAsyncValidators { + // 自定义异步校验规则 + static isRightUserName(): AsyncValidatorFn { + return (control: AbstractControl): Promise | Observable => { + if (control.valueChanges) { + // 初始时control中可能没有valueChanges属性 + return control.valueChanges.pipe( + debounceTime(500), // 防抖处理(输入停顿后再进行校验) + distinctUntilChanged(), // 防止对前后相同的值进行校验 + switchMap((value: string) => CustomAsyncValidators.isRight(value)), // 进行后台请求校验 + map((isRight: boolean) => { + // 拿到后台返回值 + // 异步校验需要在校验错误信息中通过 tiAsyncErrorMessage 属性来设置校验错误提示信息 + return isRight + ? { + rightUserName: { + actualValue: control.value, + tiAsyncErrorMessage: '用户名 {0} 不正确' + } + } + : null; + }), + first(), // complete control.valueChanges + catchError(() => null) + ); + } else { + return of(null); + } + }; + } + + // 模拟后台异步请求 + static isRight(value: string): Observable { + return of(value !== 'happy').pipe( + delay(2000), + catchError(() => of(false)) + ); + } +} diff --git a/src/validation/demo/src/app/validation/ValidationBasicControlComponent.ts b/src/validation/demo/src/app/validation/ValidationBasicControlComponent.ts new file mode 100644 index 0000000..de5452b --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationBasicControlComponent.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-basic-control.html' +}) +export class ValidationBasicControlComponent { + customFormControl: FormControl = new FormControl('hello tiny', [TiValidators.required, TiValidators.equal('hello tiny')]); +} diff --git a/src/validation/demo/src/app/validation/ValidationBasicDirectiveComponent.ts b/src/validation/demo/src/app/validation/ValidationBasicDirectiveComponent.ts new file mode 100644 index 0000000..a5e40dd --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationBasicDirectiveComponent.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './validation-basic-directive.html' +}) +export class ValidationBasicDirectiveComponent { + templateRequiredValue: string = ''; + templateEqualValue: string = 'hello tiny'; +} diff --git a/src/validation/demo/src/app/validation/ValidationBlurCheckComponent.ts b/src/validation/demo/src/app/validation/ValidationBlurCheckComponent.ts new file mode 100644 index 0000000..2615742 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationBlurCheckComponent.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-blur-check.html' +}) +export class ValidationBlurCheckComponent { + inputValue: string; + typeConfig: TiValidationConfig = { + type: 'blur' + }; +} diff --git a/src/validation/demo/src/app/validation/ValidationErrorMsgComponent.ts b/src/validation/demo/src/app/validation/ValidationErrorMsgComponent.ts new file mode 100644 index 0000000..5002917 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationErrorMsgComponent.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-error-msg.html' +}) +export class ValidationErrorMsgComponent { + errorMsgBaseValue: string = 'hello'; + errorMsgMultirowValue: string = 'hello'; + errorMsgClearValue: string = 'hello'; + + errorMsgBaseConfig: TiValidationConfig = { + type: 'blur', + errorMessage: { + equal: 'not equal to {0}' + } + }; + errorMsgMultirowConfig: TiValidationConfig = { + errorMessage: { + equal: '输入值
    应该为{0}。' + } + }; + errorMsgClearConfig: TiValidationConfig = { + type: 'blur', + errorMessage: { + equal: '' + } + }; +} diff --git a/src/validation/demo/src/app/validation/ValidationFormGroupComponent.ts b/src/validation/demo/src/app/validation/ValidationFormGroupComponent.ts new file mode 100644 index 0000000..3e914e0 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationFormGroupComponent.ts @@ -0,0 +1,150 @@ +import { Component, ElementRef, Input } from '@angular/core'; +import { AbstractControl, AsyncValidatorFn, FormBuilder, FormControl, FormGroup, ValidationErrors } from '@angular/forms'; +import { Observable, of, Subscription, zip } from 'rxjs'; +import { catchError, debounceTime, delay, distinctUntilChanged, first, map, switchMap } from 'rxjs/operators'; +import { TiValidationDirective, TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-form-group.html' +}) +export class ValidationFormGroupComponent { + countries: Array = [ + { + label: '中国', + englishname: 'China' + }, + { + label: '美国', + englishname: 'America' + }, + { + label: '加拿大', + englishname: 'Canada' + } + ]; + groupFormControl: FormGroup; + showRequiredController: boolean = true; + templateEqualValue: string = 'hello tiny'; + templateRequiredValue: string = ''; + defaultEmail: string = 'hello@example.com'; + asyncFormGroup: FormGroup; + constructor(builder: FormBuilder, private elementRef: ElementRef) { + this.groupFormControl = builder.group({ + emailController: new FormControl('hello tiny', [TiValidators.email, TiValidators.required]), + requiredController: new FormControl('', [TiValidators.required]), + selectControllor: new FormControl('', [TiValidators.required]) + }); + this.asyncFormGroup = builder.group({ + syncInput: new FormControl('', [TiValidators.required]), + asyncInput: new FormControl('', null, CustomAsyncValidators.isRightUserName(2000)) + }); + } + checkGroupFormControl(): void { + const errors: ValidationErrors | null = TiValidators.check(this.groupFormControl); + if (errors) { + const firstError: any = Object.keys(errors)[0]; + this.elementRef.nativeElement.querySelector(`[formControlName=${firstError}]`).focus(); + } + } + hideAndCheck(): void { + this.showRequiredController = false; + this.groupFormControl.removeControl('requiredController'); + } + resetGroup(): void { + this.groupFormControl.reset({ + emailController: 'hello tiny', + requiredController: '', + selectControllor: '' + }); + } + selectDisabled(): void { + if (this.groupFormControl.controls.selectControllor.disabled) { + this.groupFormControl.controls.selectControllor.enable(); + } else { + this.groupFormControl.controls.selectControllor.disable(); + } + } + + checkTemplateForm(form: FormGroup): void { + const errors: ValidationErrors | null = TiValidators.check(form); + console.log('errors', errors); + + // 整体校验后如果需要聚焦到第一个校验不通过元素,请参考以下代码 + if (errors) { + // 注意:要保证fb.group时各个FormControl的顺序与对应表单元素dom放置顺序一致 + const firstError: any = Object.keys(errors)[0]; + this.elementRef.nativeElement.querySelector(`[name=${firstError}]`).focus(); + } + } + + checkAsyncForm(): void { + let errors: ValidationErrors | null = TiValidators.check(this.asyncFormGroup); + const pendingStatusChangesArray: Array = []; + const pendingControlsArray: Array = []; + for (const key in this.asyncFormGroup.controls) { + if (Object.prototype.hasOwnProperty.call(this.asyncFormGroup.controls, key)) { + const control: AbstractControl = this.asyncFormGroup.controls[key]; + if (control.pending) { + pendingControlsArray.push({ name: key, control: control }); + pendingStatusChangesArray.push(control.statusChanges); + } + } + } + if (pendingStatusChangesArray.length > 0) { + const pendingStatusChanges: Subscription = zip(...pendingStatusChangesArray).subscribe((resultArray: Array) => { + resultArray.forEach((result: string, index: number) => { + if (result === 'INVALID') { + if (errors === null) { + errors = {}; + } + errors[pendingControlsArray[index].name] = pendingControlsArray[index].control.errors; + } + }); + pendingStatusChanges.unsubscribe(); + if (errors) { + const firstError: any = Object.keys(errors)[0]; + this.elementRef.nativeElement.querySelector(`[formControlName=${firstError}]`).focus(); + } + }); + } + } + + setDefaultEmail(): void { + this.groupFormControl.controls.emailController.setValue(this.defaultEmail); + } +} + +export class CustomAsyncValidators { + static isRightUserName(delay: number): AsyncValidatorFn { + return (control: AbstractControl): Promise | Observable => { + if (control.valueChanges) { + return control.valueChanges.pipe( + debounceTime(TiValidationDirective.ASYNC_DEBOUNCE_TIME), + distinctUntilChanged(), + switchMap((value: string) => CustomAsyncValidators.isRight(value, delay)), + map((isRight: boolean) => { + return isRight + ? { + rightUserName: { + actualValue: control.value, + tiAsyncErrorMessage: '用户名 {0} 不正确' + } + } + : null; + }), + first(), + catchError(() => null) + ); + } else { + return of(null); + } + }; + } + + static isRight(value: string, time: number): Observable { + return of(value !== 'tiny').pipe( + delay(time), + catchError(() => of(false)) + ); + } +} diff --git a/src/validation/demo/src/app/validation/ValidationFormGroupConfigComponent.ts b/src/validation/demo/src/app/validation/ValidationFormGroupConfigComponent.ts new file mode 100644 index 0000000..a5a9271 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationFormGroupConfigComponent.ts @@ -0,0 +1,60 @@ +import { Component, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, ValidationErrors } from '@angular/forms'; +import { TiValidationConfig, TiValidationCheckConfig, TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-form-group-config.html' +}) +export class ValidationFormGroupConfigComponent { + myLogs: Array = []; + groupFormControl: FormGroup; + pwdConfig: TiValidationConfig = { + type: 'password' + }; + constructor(fb: FormBuilder, private elementRef: ElementRef) { + this.groupFormControl = fb.group({ + pwdInput: new FormControl('', [ + TiValidators.password({ + notEqualPosRev: () => { + return this.groupFormControl.get('emailInput'); + } + }) + ]), + emailInput: new FormControl('hello', [TiValidators.email]), + nicknameInput: new FormControl('', [TiValidators.required]), + childGroupFormControl: new FormGroup({ + usernameInput: new FormControl('', [TiValidators.required]) + }) + }); + } + checkGroup(): void { + const errors: ValidationErrors | null = TiValidators.check(this.groupFormControl); + this.myLogs = [...this.myLogs, `整体校验结果:${JSON.stringify(errors)}`]; + if (errors) { + const firstError: any = Object.keys(errors)[0]; + this.elementRef.nativeElement.querySelector(`[formControlName=${firstError}]`).focus(); + } + } + checkGroupWithConfig(): void { + const config: TiValidationCheckConfig = { + emitEvent: false, + ignoreNames: ['pwdInput'], + onlySelf: true, + errorsFlatted: false + }; + const errors: ValidationErrors | null = TiValidators.check(this.groupFormControl, config); + if (errors) { + this.myLogs = [...this.myLogs, `屏蔽密码输入框的校验结果:${JSON.stringify(errors)}`]; + const firstError: any = Object.keys(errors)[0]; + this.elementRef.nativeElement.querySelector(`[formControlName=${firstError}]`).focus(); + } + } + checkGroupWithErrorsFlatted(): void { + const errors: ValidationErrors | null = TiValidators.check(this.groupFormControl, { errorsFlatted: true }); + this.myLogs = [...this.myLogs, `校验结果扁平化结构:${JSON.stringify(errors)}`]; + if (errors) { + const firstError: any = Object.keys(errors)[0]; + this.elementRef.nativeElement.querySelector(`[formControlName=${firstError}]`).focus(); + } + } +} diff --git a/src/validation/demo/src/app/validation/ValidationFormGroupTestComponent.ts b/src/validation/demo/src/app/validation/ValidationFormGroupTestComponent.ts new file mode 100644 index 0000000..8a0ae22 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationFormGroupTestComponent.ts @@ -0,0 +1,69 @@ +import { Component } from '@angular/core'; +import { FormArray, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-form-group-test.html' +}) +export class ValidationFormGroupTestComponent { + form: FormGroup; + nest: FormArray; + labelData: Array = [ + ['Grade', 'Class'], + ['Hobby1', 'Hobby2'] + ]; + constructor(fb: FormBuilder) { + this.form = fb.group({ + myInput: new FormControl('N', [TiValidators.required]), + myInput1: new FormControl('', [TiValidators.digits, TiValidators.rangeValue(0, 100)]), + nest: fb.array([ + fb.group({ + myInput01: new FormControl('One', [TiValidators.required]), + myInput02: new FormControl('', [TiValidators.required]) + }), + fb.group( + { + myInput11: new FormControl('study', [TiValidators.required]), + myInput12: new FormControl('', [TiValidators.required]) + }, + { validators: this.myValidator } + ) + ]) + }); + } + + ngOnInit(): void { + this.nest = this.form.get('nest') as FormArray; + + // 测试 checkGroup 和 checkGroup1WithConfig 的区别。checkGroup 会触发此事件,checkGroupWithConfig不会触发此事件 + this.form.controls.myInput.valueChanges.subscribe(() => { + console.log('form myInput valueChanges'); + }); + } + + // 表单整体校验,通过调用check方法实现整体校验的相关呈现 + checkGroup(): void { + const errors: ValidationErrors | null = TiValidators.check(this.form); + console.log(errors); + } + + checkGroupWithConfig(): void { + // 从10.0.1版本开始TiValidators.check方法支持传入第二个参数,第二个参数可选,具体参数意义可参考https://angular.io/api/forms/AbstractControl#updatevalueandvalidity + const errors: ValidationErrors | null = TiValidators.check(this.form, { + emitEvent: false + }); + console.log(errors); + } + + myValidator: ValidatorFn = (control: FormGroup): ValidationErrors | null => { + const value1 = control.get('myInput11'); + const value2 = control.get('myInput12'); + + return value1 && value2 && value2.value === value1.value + ? { + myValidator: true, + errorMessage: `${this.labelData[1][0]} and ${this.labelData[1][0]} cannot be the same. ` + } + : null; + }; +} diff --git a/src/validation/demo/src/app/validation/ValidationParamChangeComponent.ts b/src/validation/demo/src/app/validation/ValidationParamChangeComponent.ts new file mode 100644 index 0000000..4c8bdeb --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationParamChangeComponent.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './validation-param-change.html' +}) +export class ValidationParamChangeComponent { + myValue: string = 'a'; + myValue1: number = 11; + equal: string = 'aa'; + number: boolean = true; + + onClick(): void { + this.equal = 'bb'; + } + + onClick1(): void { + this.number = !this.number; + } +} diff --git a/src/validation/demo/src/app/validation/ValidationPwdCheckComponent.ts b/src/validation/demo/src/app/validation/ValidationPwdCheckComponent.ts new file mode 100644 index 0000000..8392ff6 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationPwdCheckComponent.ts @@ -0,0 +1,166 @@ +import { Component } from '@angular/core'; +import { AbstractControl, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'; +import { TiPasswordValidatorConfig, TiValidationConfig, TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-pwd-check.html' +}) +export class ValidationPwdCheckComponent { + passwordConfig1: TiValidationConfig = { + type: 'password' + }; + + passwordConfig2: TiValidationConfig = { + type: 'password', + passwordConfig: { + validator: { + params: { + rangeSize: [8, 10], + minCharType: [3] + }, + message: { + rangeSize: '����Ϊ{0}��{1}��', + minCharType: '���ٰ�����д��ĸ��Сд��ĸ�����ּ��������`!=�е�{0}��' + } + } + } + }; + passwordConfig3: TiValidationConfig = { + type: 'password', + errorMessage: { + password: 'error msg define' + } + }; + passwordConfig4: TiValidationConfig = { + type: 'password', + passwordConfig: { + validator: { + params: { + custom1: ['bb'] + }, + message: { + custom1: 'should not equal to {0}' + } + } + } + }; + passwordConfig5: TiValidationConfig = { + errorMessage: { + custom1: 'error msg define', + custom2: 'error msg define' + }, + passwordConfig: { + validator: { + rule: 'custiomRule2', + params: { + custom1: ['cc'], + custom2: [8] + }, + message: { + custom1: 'should not equal to {0}', + custom2: 'length should be {0}' + } + }, + levelFn(value: string, validator: TiPasswordValidatorConfig): number { + if (value.length === 2) { + return 0; + } + if (value.length > 2 && value.length < validator.params.custom2[0]) { + return 1; + } + if (value.length === validator.params.custom2[0]) { + return 2; + } + } + } + }; + passwordConfig6: TiValidationConfig = { + type: 'password', + errorMessage: { + password: '' + } + }; + form: FormGroup; + constructor(fb: FormBuilder) { + const custiomRule1: (params: any) => ValidatorFn = (params: any): ValidatorFn => { + return Validators.compose([ + TiValidators.password({ + notEqualPosRev: () => { + return this.form.get('usernameInput'); + } + }), + this.x(params.t) + ]); + }; + + const custiomRule2: (params: any) => ValidatorFn = (params: any): ValidatorFn => { + return Validators.compose([this.x(params.m), this.y(params.n)]); + }; + + this.form = fb.group({ + usernameInput: new FormControl('TinyNG'), + passwordInput1: new FormControl('', [ + TiValidators.password({ + notEqualPosRev: () => { + return this.form.get('usernameInput'); + } + }) + ]), + passwordInput2: new FormControl('', [ + TiValidators.password({ + notEqualPosRev: () => { + return this.form.get('usernameInput'); + }, + rangeSize: [8, 10], + minCharType: [ + 3, + { + digitsCharReg: /[0-9]+/, + lowerCharReg: /[a-z]+/, + upperCharReg: /[A-Z]+/, + specialCharReg: /[`!=]/ + } + ] + }) + ]), + passwordInput3: new FormControl('', [ + TiValidators.password({ + notEqualPosRev: () => { + return this.form.get('usernameInput'); + } + }) + ]), + passwordInput4: new FormControl('', custiomRule1({ t: 'bb' })), + passwordInput5: new FormControl('', custiomRule2({ m: 'cc', n: 8 })), + passwordInput6: new FormControl('', [ + TiValidators.password({ + notEqualPosRev: () => { + return this.form.get('usernameInput'); + } + }) + ]) + }); + } + checkGroup(): void { + console.log(TiValidators.check(this.form)); + } + + x(param: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return control.value !== param ? null : { custom1: { notEqualValue: param, actualValue: control.value } }; + }; + } + + y(length: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return control.value.length === length + ? null + : { + custom2: { + requiredLength: length, + actualLength: control.value.length + } + }; + }; + } +} diff --git a/src/validation/demo/src/app/validation/ValidationRulesCustomComponent.ts b/src/validation/demo/src/app/validation/ValidationRulesCustomComponent.ts new file mode 100644 index 0000000..c356135 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationRulesCustomComponent.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { AbstractControl, FormControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +@Component({ + templateUrl: './validation-rules-custom.html' +}) +export class ValidationRulesCustomComponent { + rulesCustomControl: FormControl = new FormControl('hello', CustomValidators.isEqualTo('hello tiny')); +} +export class CustomValidators { + static isEqualTo(pvalue: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return pvalue !== control.value + ? { + isEqualTo: { + requiredValue: pvalue, + actualValue: control.value, + tiErrorMessage: 'error, should input {0}' + } + } + : null; + }; + } +} diff --git a/src/validation/demo/src/app/validation/ValidationRulesCustomDirectiveComponent.ts b/src/validation/demo/src/app/validation/ValidationRulesCustomDirectiveComponent.ts new file mode 100644 index 0000000..894f2b1 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationRulesCustomDirectiveComponent.ts @@ -0,0 +1,40 @@ +import { Component, Directive, forwardRef, Input } from '@angular/core'; +import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn } from '@angular/forms'; + +@Component({ + templateUrl: './validation-rules-custom-directive.html' +}) +export class ValidationRulesCustomDirectiveComponent { + text: string = ''; +} + +@Directive({ + selector: '[myIsEqualTo][formControlName],[myIsEqualTo][formControl],[myIsEqualTo][ngModel]', + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CustomValidatorsDirective), + multi: true + } + ] +}) +export class CustomValidatorsDirective implements Validator { + @Input() myIsEqualTo: string; + + validate(control: AbstractControl): ValidationErrors | null { + return this.isEqualTo(this.myIsEqualTo)(control); + } + isEqualTo(pvalue: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return pvalue !== control.value + ? { + isEqualTo: { + requiredValue: pvalue, + actualValue: control.value, + tiErrorMessage: 'custom rule is error, should input {0}' + } + } + : null; + }; + } +} diff --git a/src/validation/demo/src/app/validation/ValidationRulesTestComponent.ts b/src/validation/demo/src/app/validation/ValidationRulesTestComponent.ts new file mode 100644 index 0000000..dee39af --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationRulesTestComponent.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; +import { TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-rules-test.html' +}) +export class ValidationRulesTestComponent { + myValue: string; + myValue1: string; + myValue2: string; + myValue3: string; + myValue4: string; + myValue5: string; + myValue6: string; + myValue7: string; + myValue8: string; + myValue9: string; + myValue10: string; + myValue11: string; + myValue12: string; + myValue13: string; + myValue14: string; + myValue15: string; + myValue16: string; + myValue17: string; + myValue18: string; + myValue19: string; + myValue20: string; + myValue21: string; + myValue22: string; + myValue23: string; + myValue24: string; + disableStatus: boolean = false; + validationObj1: TiValidationConfig = {}; + validationObj: TiValidationConfig = { + type: 'change' + }; + changeDisable(): void { + this.disableStatus = !this.disableStatus; + } +} diff --git a/src/validation/demo/src/app/validation/ValidationTemplateFormNestedComponent.ts b/src/validation/demo/src/app/validation/ValidationTemplateFormNestedComponent.ts new file mode 100644 index 0000000..27f6951 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationTemplateFormNestedComponent.ts @@ -0,0 +1,63 @@ +import { Component, ElementRef } from '@angular/core'; +import { ControlContainer, FormGroup, NgForm, ValidationErrors } from '@angular/forms'; +import { TiValidators } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-template-form-nested.html' +}) +export class ValidationTemplateFormNestedComponent { + value1: string = 'a'; + value2: string = ''; + value3: string = 'b'; + value4: string = ''; + + constructor(private elementRef: ElementRef) {} + + // 表单整体校验,通过调用check方法实现整体校验的相关呈现 + checkGroup(form: FormGroup): void { + console.log(form.controls); + const errors: ValidationErrors | null = TiValidators.check(form); + console.log(errors); + + // 整体校验后如果需要聚焦到第一个校验不通过元素,请参考以下代码 + if (errors) { + const firstError: any = Object.keys(errors)[0]; + this.elementRef.nativeElement.querySelector(`[name=${firstError}]`).focus(); + } + } +} + +@Component({ + selector: 'custom-child1-test', + template: ` +
    +

    子组件区域:

    +

    + +
    + `, + viewProviders: [{ provide: ControlContainer, useExisting: NgForm }] // 有该行的配置时,才会将该组件中的表单控件添加到父级组件的 FormGroup(NgForm) 中去 +}) +export class CustomChild1TestComponent { + value1: string = 'c'; + value2: string = ''; +} + +@Component({ + selector: 'custom-child2-test', + template: ` +
    +

    子组件区域:

    + +
    + + +
    +
    + `, + viewProviders: [{ provide: ControlContainer, useExisting: NgForm }] // 有该行的配置时,才会将该组件中的表单控件添加到父级组件的 FormGroup(NgForm) 中去 +}) +export class CustomChild2TestComponent { + value1: string = 'c'; + value2: string = ''; +} diff --git a/src/validation/demo/src/app/validation/ValidationTestModule.ts b/src/validation/demo/src/app/validation/ValidationTestModule.ts new file mode 100644 index 0000000..558f2fc --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationTestModule.ts @@ -0,0 +1,153 @@ +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { + TiButtonModule, + TiFormfieldModule, + TiScrollModule, + TiSelectModule, + TiTextareaModule, + TiTextModule, + TiValidationModule +} from '@opentiny/ng'; + +import { DemoLogModule } from '../../../../../ng/demolog/DemoLogModule'; +import { ValidationBasicDirectiveComponent } from './ValidationBasicDirectiveComponent'; +import { ValidationBasicControlComponent } from './ValidationBasicControlComponent'; +import { ValidationFormGroupComponent } from './ValidationFormGroupComponent'; +import { ValidationFormGroupConfigComponent } from './ValidationFormGroupConfigComponent'; +import { ValidationBlurCheckComponent } from './ValidationBlurCheckComponent'; +import { ValidationRulesCustomComponent } from './ValidationRulesCustomComponent'; +import { ValidationPwdCheckComponent } from './ValidationPwdCheckComponent'; +import { ValidationTipComponent } from './ValidationTipComponent'; +import { ValidationErrorMsgComponent } from './ValidationErrorMsgComponent'; +import { ValidationRulesTestComponent } from './ValidationRulesTestComponent'; +import { ValidationFormGroupTestComponent } from './ValidationFormGroupTestComponent'; +import { ValidationTiscrollComponent } from './ValidationTiscrollComponent'; +import { ValidationParamChangeComponent } from './ValidationParamChangeComponent'; +import { ValidationAsyncCheckComponent } from './ValidationAsyncCheckComponent'; +import { ValidationAsyncCheckTestComponent } from './ValidationAsyncCheckTestComponent'; +import { CustomValidatorsDirective, ValidationRulesCustomDirectiveComponent } from './ValidationRulesCustomDirectiveComponent'; +import { + CustomChild1TestComponent, + CustomChild2TestComponent, + ValidationTemplateFormNestedComponent +} from './ValidationTemplateFormNestedComponent'; + +@NgModule({ + imports: [ + CommonModule, + TiFormfieldModule, + TiValidationModule, + TiTextareaModule, + TiTextModule, + TiScrollModule, + TiSelectModule, + FormsModule, + ReactiveFormsModule, + TiButtonModule, + DemoLogModule, + RouterModule.forChild(ValidationTestModule.ROUTES) + ], + declarations: [ + ValidationBasicDirectiveComponent, + ValidationBasicControlComponent, + ValidationBlurCheckComponent, + ValidationFormGroupComponent, + ValidationFormGroupConfigComponent, + ValidationRulesCustomComponent, + ValidationPwdCheckComponent, + ValidationTipComponent, + ValidationErrorMsgComponent, + ValidationRulesTestComponent, + ValidationFormGroupTestComponent, + ValidationTiscrollComponent, + ValidationParamChangeComponent, + ValidationAsyncCheckComponent, + ValidationAsyncCheckTestComponent, + ValidationTemplateFormNestedComponent, + CustomChild1TestComponent, + CustomChild2TestComponent, + ValidationRulesCustomDirectiveComponent, + CustomValidatorsDirective + ] +}) +export class ValidationTestModule { + static readonly LINKS: Array = [ + { href: 'components/TiErrorMsgComponent.html', label: 'ErrorMsg' }, + { href: 'components/TiPwdMsgComponent.html', label: 'PwdMsg' }, + { href: 'directives/TiValidationDirective.html', label: 'Validation' } + ]; + static readonly ROUTES: Routes = [ + { + path: 'validation/validation-basic-directive', + component: ValidationBasicDirectiveComponent + }, + { + path: 'validation/validation-basic-control', + component: ValidationBasicControlComponent + }, + { + path: 'validation/validation-form-group', + component: ValidationFormGroupComponent + }, + { + path: 'validation/validation-form-group-config', + component: ValidationFormGroupConfigComponent + }, + { + path: 'validation/validation-blur-check', + component: ValidationBlurCheckComponent + }, + { + path: 'validation/validation-pwd-check', + component: ValidationPwdCheckComponent + }, + { + path: 'validation/validation-tip', + component: ValidationTipComponent + }, + { + path: 'validation/validation-error-msg', + component: ValidationErrorMsgComponent + }, + { + path: 'validation/validation-rules-custom', + component: ValidationRulesCustomComponent + }, + { + path: 'validation/validation-rules-custom-directive', + component: ValidationRulesCustomDirectiveComponent + }, + { + path: 'validation/validation-async-check', + component: ValidationAsyncCheckComponent + }, + { + path: 'validation/validation-tiscroll', + component: ValidationTiscrollComponent + }, + { + path: 'validation/validation-rules-test', + component: ValidationRulesTestComponent + }, + { + path: 'validation/validation-form-group-test', + component: ValidationFormGroupTestComponent + }, + { + path: 'validation/validation-param-change', + component: ValidationParamChangeComponent + }, + { + path: 'validation/validation-async-check-test', + component: ValidationAsyncCheckTestComponent + }, + { + path: 'validation/validation-template-form-nested', + component: ValidationTemplateFormNestedComponent + } + ]; +} diff --git a/src/validation/demo/src/app/validation/ValidationTipComponent.ts b/src/validation/demo/src/app/validation/ValidationTipComponent.ts new file mode 100644 index 0000000..2968d14 --- /dev/null +++ b/src/validation/demo/src/app/validation/ValidationTipComponent.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { TiValidationConfig } from '@opentiny/ng'; + +@Component({ + templateUrl: './validation-tip.html' +}) +export class ValidationTipComponent { + inputValue: string = ''; + tipConfig: TiValidationConfig = { + tip: '自定义文案:请输入 hello tiny', + tipPosition: 'bottom', + tipMaxWidth: '100px' + }; +} diff --git a/src/validation/demo/src/app/validation/validation-async-check-test.html b/src/validation/demo/src/app/validation/validation-async-check-test.html new file mode 100644 index 0000000..931a9e2 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-async-check-test.html @@ -0,0 +1,24 @@ +

    描述

    +

    异步校验需要在校验错误信息中通过 tiAsyncErrorMessage 属性来设置校验错误提示信息。

    +

    异步校验不支持type:blur和type:password的校验类型;异步校验只支持 input[tiText] 输入框组件。

    + +

    示例

    + +

    1. 在formfield中pending状态的loading图标是否正常:

    +

    同步校验规则:1.输入不能为空; 2.字符最小长度为5。

    +

    异步校验规则:1.后台进行校验(2秒)后返回结果,输入 'happy' 则成功,否则失败

    + + + + + + +

    2. input后面有元素的时候,loading图标的出现会不会影响后面元素的位置:

    +

    同步校验规则:1.输入不能为空; 2.字符最小长度为5。

    +

    异步校验规则:1.后台进行校验(2秒)后返回结果,输入 'happy' 则成功,否则失败

    +测试元素 +
    + +

    场景待续...... diff --git a/src/validation/demo/src/app/validation/validation-async-check.html b/src/validation/demo/src/app/validation/validation-async-check.html new file mode 100644 index 0000000..235cf13 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-async-check.html @@ -0,0 +1,5 @@ +

    1. 仅异步校验(2秒后校验,输入 'hello tiny' 视为通过)

    + +

    +

    2. 同步校验(非空且最小长度为5),同时加入异步校验(2秒后校验,输入 'hello tiny' 视为通过)

    + diff --git a/src/validation/demo/src/app/validation/validation-basic-control.html b/src/validation/demo/src/app/validation/validation-basic-control.html new file mode 100644 index 0000000..915dd3f --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-basic-control.html @@ -0,0 +1 @@ + diff --git a/src/validation/demo/src/app/validation/validation-basic-directive.html b/src/validation/demo/src/app/validation/validation-basic-directive.html new file mode 100644 index 0000000..a187d93 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-basic-directive.html @@ -0,0 +1,4 @@ +非空校验: +
    +
    +内容校验: diff --git a/src/validation/demo/src/app/validation/validation-blur-check.html b/src/validation/demo/src/app/validation/validation-blur-check.html new file mode 100644 index 0000000..0c0204b --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-blur-check.html @@ -0,0 +1 @@ + diff --git a/src/validation/demo/src/app/validation/validation-error-msg.html b/src/validation/demo/src/app/validation/validation-error-msg.html new file mode 100644 index 0000000..3bc0002 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-error-msg.html @@ -0,0 +1,6 @@ +

    1. 自定义错误信息

    + +
    自定义检验信息容器:
    + +

    2. 清空错误信息

    + diff --git a/src/validation/demo/src/app/validation/validation-form-group-config.html b/src/validation/demo/src/app/validation/validation-form-group-config.html new file mode 100644 index 0000000..b11210b --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-form-group-config.html @@ -0,0 +1,10 @@ +
    + 密码:

    + 邮箱:

    + 昵称:

    + 子表单:
    + + + + + diff --git a/src/validation/demo/src/app/validation/validation-form-group-test.html b/src/validation/demo/src/app/validation/validation-form-group-test.html new file mode 100644 index 0000000..06bdbb2 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-form-group-test.html @@ -0,0 +1,39 @@ +

    描述

    +

    表单整体校验调用check方法实现

    +

    示例

    +

    多层FormGroup嵌套,并且在formGroup上添加自定义校验规则

    + +
    + name:

    + age:

    +
    +
    +
    + {{labelData[i][0]}}: +

    + {{labelData[i][1]}}: +
    + + {{item.errors.errorMessage}} + +

    +
    +
    +

    + + + + +
    + diff --git a/src/validation/demo/src/app/validation/validation-form-group.html b/src/validation/demo/src/app/validation/validation-form-group.html new file mode 100644 index 0000000..848d9f6 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-form-group.html @@ -0,0 +1,36 @@ +

    1. 响应式表单整体校验

    +
    +

    邮箱校验:

    +
    +

    邮箱取值:{{ this.groupFormControl.controls.emailController.value }}

    +
    +

    非空校验:

    +
    + +
    + + + +
    + + +
    +

    +

    2. 模板驱动式表单整体校验

    +
    +

    表单项一:

    +
    +

    表单项二:

    + +
    +

    +

    3. 异步整体校验

    +
    +

    同步校验:(非空)

    + +

    +

    异步校验:(2秒后校验,输入 'tiny' 视为通过)

    + +
    + +
    diff --git a/src/validation/demo/src/app/validation/validation-param-change.html b/src/validation/demo/src/app/validation/validation-param-change.html new file mode 100644 index 0000000..7efd03a --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-param-change.html @@ -0,0 +1,11 @@ +

    描述

    +

    带参数的校验指令:支持参数变更

    +

    注意:使用 tiText 组件,请导入 TiTextModule。

    + +

    示例

    +

    1.tiEqual(带参数):输入值等于{{equal}}

    +

    + +

    1.tiNumber(不带参数)

    +

    + diff --git a/src/validation/demo/src/app/validation/validation-pwd-check.html b/src/validation/demo/src/app/validation/validation-pwd-check.html new file mode 100644 index 0000000..e7b36f2 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-pwd-check.html @@ -0,0 +1,30 @@ +
    + 用户名: + +

    +

    1.基础用法

    + 密码: + +

    +

    2.配置校验规则及提示信息

    + 密码: + +

    +

    3.配置错误提示信息

    + 密码: + +

    +

    4.增量添加自定义规则

    + 密码: + +

    +

    5.完全自定义规则

    + 密码: + +

    +

    6.关闭错误信息提示

    + 密码: + +

    + +
    diff --git a/src/validation/demo/src/app/validation/validation-rules-custom-directive.html b/src/validation/demo/src/app/validation/validation-rules-custom-directive.html new file mode 100644 index 0000000..857059b --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-rules-custom-directive.html @@ -0,0 +1 @@ + diff --git a/src/validation/demo/src/app/validation/validation-rules-custom.html b/src/validation/demo/src/app/validation/validation-rules-custom.html new file mode 100644 index 0000000..bce656c --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-rules-custom.html @@ -0,0 +1 @@ + diff --git a/src/validation/demo/src/app/validation/validation-rules-test.html b/src/validation/demo/src/app/validation/validation-rules-test.html new file mode 100644 index 0000000..dad6db7 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-rules-test.html @@ -0,0 +1,75 @@ +
    + 1.Equal:aa + +

    + 2.MaxLength:5 + +

    + 3.Email: + +

    + 4.Digits: + +

    + 5.Script: + +

    + 6.tiContains:aa + +

    + 7.tiNotContains:aa + +

    + 8.Date + +

    + 9.Integer + +

    + 10.IPv4 + +

    + 11.IPv6 + +

    + 12.MaxValue:6 + +

    + 13.MinLength:3 + +

    + 14.MinValue:5 + +

    + 15.NotEqual:aa + +

    + 16.Number + +

    + 17.tiPassword + +

    + 18.port + +

    + 19.MinLength:3 required + +

    + 20.RangeSize:[3,5] + +

    + 21.RangeValue:[5,10] + +

    + 22.regExp: + +

    + 23.Required + +

    + 24.URL + +

    + +
    diff --git a/src/validation/demo/src/app/validation/validation-template-form-nested.html b/src/validation/demo/src/app/validation/validation-template-form-nested.html new file mode 100644 index 0000000..3b90c36 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-template-form-nested.html @@ -0,0 +1,33 @@ +

    描述

    +

    + 模板驱动表单,form 标签中嵌套子组件,且子组件中也有表单控件, 调用 check 方法进行表单 form + 整体校验时可连带子组件中的表单控件一起进行校验。 +

    +

    示例

    + +

    1.模板驱动式表单 + 嵌套含有表单控件的子组件 + 整体校验: 在 FormGroup 里父组件中的表单控件与子组件中的表单控件平级

    +
    +
    +

    +

    + +

    + + +
    +

    + +

    + 2.模板驱动式表单 + 嵌套含有表单控件的子组件 + 整体校验: 在 FormGroup + 里父组件中的表单控件与子组件中的表单控件平级不是平级的,子组件中的表单控件是 FormGroup 里的子级 +

    +
    +
    +

    +

    + +

    + + +
    +

    diff --git a/src/validation/demo/src/app/validation/validation-tip.html b/src/validation/demo/src/app/validation/validation-tip.html new file mode 100644 index 0000000..a0ee35b --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-tip.html @@ -0,0 +1 @@ + diff --git a/src/validation/demo/src/app/validation/validation-tiscroll.html b/src/validation/demo/src/app/validation/validation-tiscroll.html new file mode 100644 index 0000000..32819e0 --- /dev/null +++ b/src/validation/demo/src/app/validation/validation-tiscroll.html @@ -0,0 +1,24 @@ +

    描述

    +

    校验规则使用提供了指令和TS中声明两种方式,凡是Tiny封装的校验规则均以ti开头

    +

    + 当在页面局部有滚动条的容器中使用校验指令,鼠标拖动容器滚动条时,为了防止校验信息错位, + 开发者需要在对应的局部容器中添加tiScroll属性,使校验信息在拖动局部滚动条时消失。 +

    +

    注意:使用 tiText 组件,请导入 TiTextModule。使用tiScroll指令,请导入 TiScrollModule。

    +

    导入

    +

    import {{ '{' }} TiScrollModule, TiTextModule, TiValidationModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    +

    1.基本:

    +
    + +

    +











    +
    +

    2.注册tiScroll事件

    +

    除上述方式,还可以通过手动方式给滚动容器注册事件,参考 tiSelect 组件 tiScroll(下拉列表分离) 示例

    diff --git a/src/validation/demo/src/app/validation/validationTiscrollComponent.ts b/src/validation/demo/src/app/validation/validationTiscrollComponent.ts new file mode 100644 index 0000000..a3ffb70 --- /dev/null +++ b/src/validation/demo/src/app/validation/validationTiscrollComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; +@Component({ + templateUrl: './validation-tiscroll.html' +}) +export class ValidationTiscrollComponent { + myValue: string = 'a'; +} diff --git a/src/validation/demo/src/app/validation/webdoc/validation-demos.js b/src/validation/demo/src/app/validation/webdoc/validation-demos.js new file mode 100644 index 0000000..597602e --- /dev/null +++ b/src/validation/demo/src/app/validation/webdoc/validation-demos.js @@ -0,0 +1,165 @@ +export default { + column: '2', + + demos: [ + { + demoId: 'validation-basic-directive', + name: { + 'zh-CN': '模板表单基本用法', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    tiValidation指令的基本用法之一。使用模板驱动表单,TinyNG 实现的校验规则及方法见 TiValidators,你也可以使用 Angular 表单校验规则或自定义校验规则。

    ', + 'en-US': '

    ', + }, + apis: ['TiValidationDirective.properties.tiValidation'], + }, + { + demoId: 'validation-basic-control', + name: { + 'zh-CN': '响应式表单基本用法', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    tiValidation指令的另一种基本用法。使用响应式表单,TinyNG 实现的校验规则及方法见 TiValidators,你也可以使用 Angular 表单校验规则或自定义校验规则。

    ', + 'en-US': '

    ', + }, + }, + { + demoId: 'validation-blur-check', + name: { + 'zh-CN': '失焦校验', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    通过将TiValidationConfig实例的属性type配置为blur开启失焦校验。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiValidationDirective.properties.errorMessageWrapper', + 'TiValidationConfig.properties.type', + ], + }, + { + demoId: 'validation-tip', + name: { + 'zh-CN': 'Tip', + 'en-US': 'Tip', + }, + desc: { + 'zh-CN': + '

    通过TiValidationConfig实例的属性tip配置 Tip 内容,通过属性tipPosition配置 Tip 弹出位置。注意:Tip 指提示语,是在用户输入内容前对用户输入预期的描述。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiValidationConfig.properties.tip', + 'TiValidationConfig.properties.tipPosition', + 'TiValidationConfig.properties.tipMaxWidth', + ], + }, + { + demoId: 'validation-error-msg', + name: { + 'zh-CN': '自定义错误信息', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    通过TiValidationConfig实例的属性errorMessage自定义错误提示信息;当typeblur/password时,可以通过属性errorMessageWrapper配置校验信息容器。注意:错误信息指用户输入内容后,对输入内容不符合校验规则部分的提示。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiValidationConfig.properties.errorMessage', + 'TiValidationConfig.properties.errorMessageWrapper', + ], + }, + { + demoId: 'validation-pwd-check', + name: { + 'zh-CN': '密码校验', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    通过将TiValidationConfig实例的属性type配置为password开启密码校验。

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiValidationConfig.properties.passwordConfig', + 'TiPasswordValidatorConfig.properties.rule', + 'TiPasswordValidatorConfig.properties.message', + 'TiPasswordValidatorConfig.properties.params', + ], + }, + { + demoId: 'validation-async-check', + name: { + 'zh-CN': '异步校验', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    支持在 tiText 组件中通过自定义规则进行异步校验,支持在校验错误信息中通过tiAsyncErrorMessage属性配置校验错误提示信息。注意:异步校验不支持blurpassword类型;异步校验只支持 tiText 组件。

    ', + 'en-US': '

    ', + }, + }, + { + demoId: 'validation-rules-custom-directive', + name: { + 'zh-CN': '模板表单中自定义规则', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    支持在模板表单中自定义校验规则,规则定义请参考 Angular 官方说明

    ', + 'en-US': '

    ', + }, + }, + { + demoId: 'validation-rules-custom', + name: { + 'zh-CN': '方法调用形式自定义规则', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    支持在响应式表单中自定义校验规则,通过FormControl构造函数中的参数配置规则,规则定义请参考 Angular 官方说明

    ', + 'en-US': '

    ', + }, + }, + { + demoId: 'validation-form-group', + name: { + 'zh-CN': '表单整体校验', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    使用TiValidators.check方法调用表单整体校验。

    ', + 'en-US': '

    ', + }, + apis: ['TiValidationDirective.properties.tiValidation'], + }, + { + demoId: 'validation-form-group-config', + name: { + 'zh-CN': '表单整体校验自定义配置', + 'en-US': 'Basic usage', + }, + desc: { + 'zh-CN': + '

    使用check方法调用表单整体校验;通过TiValidationCheckConfig实例配置校验规则。注意:关于onlySelfemitEvent属性请参考 Angular 官方文档

    ', + 'en-US': '

    ', + }, + apis: [ + 'TiValidationCheckConfig.properties.emitEvent', + 'TiValidationCheckConfig.properties.ignoreNames', + 'TiValidationCheckConfig.properties.onlySelf', + 'TiValidationCheckConfig.properties.errorsFlatted', + ], + }, + ], +}; diff --git a/src/validation/demo/src/app/validation/webdoc/validation.cn.md b/src/validation/demo/src/app/validation/webdoc/validation.cn.md new file mode 100644 index 0000000..8e55e84 --- /dev/null +++ b/src/validation/demo/src/app/validation/webdoc/validation.cn.md @@ -0,0 +1,52 @@ +--- +title: Validation 表单校验 +--- +# Validation 表单校验 + +
    + +TinyNG 表单校验是基于 Angular 提供的表单校验进行的封装,在使用 TinyNG 组件前,请先了解 Angular 表单校验。 + +Angular 提供了两种表单处理方式:响应式表单(Reactive forms)和 模板驱动表单(Template-driven forms)。 关于二者的区别请**务必仔细阅读** Angular 表单简介。 + +- 支持响应式表单和模板表单两种方式驱动表单。 +- TinyNG 实现的校验规则及方法见 TiValidators,你也可以使用 Angular 表单校验规则或自定义校验规则。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +如需使用响应式表单,请导入。 +```typescript +import { FormControl } from '@opentiny/ng'; +``` + +如需使用 TinyNG 封装的校验方法,请导入。 +```typescript +import { TiValidators } from '@opentiny/ng'; +``` +
    + +
    + +TinyNG 表单校验是基于 Angular 提供的表单校验进行的封装,在使用 TinyNG 组件前,请先了解 Angular表单校验。 + +Angular 提供了两种表单处理方式:响应式表单(Reactive forms)和 模板驱动表单(Template-driven forms)。 关于二者的区别请**务必仔细阅读** Angular 表单简介。 + +- 支持响应式表单和模板表单两种方式驱动表单。 +- TinyNG 实现的校验规则及方法见 TiValidators,你也可以使用 Angular 表单校验规则或自定义校验规则。 + +```typescript +import { TiValidationModule } from '@opentiny/ng'; +``` + +如需使用响应式表单,请导入。 +```typescript +import { FormControl } from '@opentiny/ng'; +``` + +如需使用 TinyNG 封装的校验方法,请导入。 +```typescript +import { TiValidators } from '@opentiny/ng'; +``` +
    diff --git a/src/validation/demo/src/app/validation/webdoc/validation.en.md b/src/validation/demo/src/app/validation/webdoc/validation.en.md new file mode 100644 index 0000000..b1af2f6 --- /dev/null +++ b/src/validation/demo/src/app/validation/webdoc/validation.en.md @@ -0,0 +1,29 @@ +--- +title: Select +--- +# Select + +
    + +The user can select one or more options from multiple options.   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` + +
    + +
    + +The user can select one or more options from multiple options.。   + ++ it supports single select, multiple select, group select, search, and lazy loading. + +```typescript +import { TiSelectModule } from '@opentiny/ng'; +``` +
    + + diff --git a/src/validation/demo/src/favicon.ico b/src/validation/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 zcmeI5dvqMteaDAKLg|a7Crydd#5|HTEiDbDr%f7Co0hhDU@X~|W!cioIIa40j4jD} zt-|*7AO2~3)N|S%pEi$*7E&3Ai3mPHHkKZ4>yDdFw6(n>jP2t#u>w(0;JNMFuQ-)y_<3sU3p?vqEwtCEi|uu9qMVv< zuN%bQI~LpPZ+UL}XRe6C{%{*vfm$js*Wdi9Y=7%iwzv6rs2zeS;>vE!SuKwgZ#(P? z%yw^n_e@{Qo@_dG5vB^JavgQ6?JO00&zyz*NOSwDt$>P02+a1krohoDjV0pA2UAYG zF=y32ioE`?D3D8U{V@0$$z#dJ)cmeHZgt|s?=mOOrZNrh42w404pzX60_3~1=~Ne3 zI_Jm}an+x?iPl*yrkt2$&b%B&{%lwjnCWj>1zw(UWl6*-!Bj{6A36DdP|KLJuy_u) z!>&Ls^Z8%O%RvZs+{%zJ<-+cHET_Ewp7~vCQ!ngXbA#H@aG8Cd`8_L84FxPYh?h&o zCM#cpsau*pqnM>I_SOKpJ<3oLa^gf&h&2m7;Sf9$VIb=1E$-?45K@BeY8v*Bt-(RpDk6|m$W zUJ*RVC{3o?>;EMDeLs0Dfg^BbxB5@S(s_8N3KTfmo4OJFOvqr!zK)aq&A&|>TE^oWOzMMi z^4&1*3;8wpIT{2*_H9u9gEU#J8B-x(sS`Yr1;7;aKWiL=S5c18czL$3WzAxy={;fe z&8W4>$U)o#Z7&!)>-Py-uE|z=xnSyr_PWmm8>d3VlE&0}Fx61?Tsn@YV9M!OFXDLy zL%2U@$82Bf7Mdp$HJ`3!d+FQ+eP^r9OsVw;&r}Fm>I6?@wP(uC*<0E(LomLkaRH3y zF*-mym)W)ge!jPquLo5xn9A*1``PNosSvXiUZ(7f(fik7*1Xzj|Q%-)e2)`N@V>5b=Pu!uR`_pIAEtA-9Cp~JqEZ4vF zw@82F%04GIPDO~N=rd*ap$lf8(HNW2vmE2V*^3=5KZkQiO>+>XIF`?pKbE}DeQ>z` zB=UG7_aK-lRfcaoWoO0D`#0eWCq$o78=K)-T2bPRauDe2&6V=$TGyLQ?W(`sUz`d7 zOLU#LQl5yncr)e1;~Ds$na_W&=gwaaBV)O~)ceuS2W!nrx!d>8lq{Aio%>+KDL*WU z_!SIOKP~sz)aY~C*bMQYtJe8^OAg|V8foEl9xAzPuD9hk>D+|-eAjr7Q~r38GWS6S zOUzU;KUvP_)7Y%P_2wX+OF-KJM<=4hl4xsXOvQ{PW~vxdPIVMF%zXZAX4`eL`i#?E zZCBBl59_blJP5%?rKQQ#t~>519j9W-Qs)MY!(hSm{eDvEu?qS3=8@liXr?dKT`6D2 zJcwM#_tWVdleerqree-guuO%0JeB7k75Mi>uS`*#%cfiQSFkO=ZLP}pO{O~P*A~X9 zgs{X+$@HzVnWEnhH0D9XDt6O#3OcXgx{!sbgt0_S?GCK($8ie&Mma^qrKjT(o2>gG zn8NeQ>k6KcSNhpruFoT?_#P&ic|4JO&`M*>Bw~`aB%YWlS+VNvVtqf%Pkfh=gLvEO zhjIGbm8s&-$;)G@rgIB`}o>E>6XrsSEZvHJ5I^9^SiACE;&LC}(@N{05y@@cZ)Xs1H}l;=&S*p9D*yvEZjGfO&C4eCPIhCi_PTV%h-@r--l?=9nA4E;CQlgw0S-Fl}Ol?0Y@ zyBiR*m?=r;@3*j?F-hwRMb5$NzL;^!##EA6Vy2|@`Go(|eOlvQ{V-miZ~1lV-^{nK z+pwGy55avGX3jz6^1S69oW@I{k0%(imjU(tB6Z(F1-{NqrI(q~W0S*uv*kY1wJO(Z zWwAs#3B8Z!Rx(pk;uSIV?Fv{FaV`d>!Bl5`LnP-u&~+ulMErjT%x|7x7bw3kjRsl#}YFog(VSRh^ealu2XbkpAn}z)^GF_r>eqJZCGNa zq%uXnEqx?9{x5akwe!kM&(@D>If`ks`h5vS&NuWNCRQ}JhsnUd1y6TY5!rgG`6AEq2d zAyaW>iRSj{amii=G@gok-;b_oKbhn>Wir*dA*IKu5V1tpaV6y+#F)v>k@Y<@B_&=l zQzA}jOu6u6xj$Xpr|zD|3wG{yd;w^<9or)c0H8>Uht( z15aU^`{2xwe(#2r8MnWQC1JBY8$U_?;A+0$YjJ0a_K%r@6BRI(AeNXZDSbYXqadcf zm*_Z^Y5owMm*+bhwrX)Ifh+~g)R~z38|@!6C73FkCvVRqk}{?c)LiHKZG}9AjV1T%wbs0zGdp(1mRaIBC5lDH&T5+(Qjq_A}wJTX&uabl%tsM+VGcVoRZ2?b>}`5XQptjgy`o9 zj;tm3{iJa9GR9Ls3~LMtrd>9sr1DhW{k5g%61lPAb$NbVvhu`nN(xWZS389{v_A^Q zccryiav#%Rs(a&K6~CWC+&f!N87-CsTLHx>n!6+U7=~(n%#`T+Nnt9QEHP73nLL&4 zOFb9!^G4Bg;!I}iH8h@@>uUJx%HveDSz@N7GW8NO<%g+wu*6JBW-6U}p5qka)b7T= zDL+odizQ}CCR431f+uE*`hI0G6+)Jb>$QT5%FE3`&7+8Ujsi^ewM=uIqVd$mzjcpO zVPk1|?nC7aiM;w|T<_J5^L{eo6*G0RZ}Un$N8!X&X_jg|_n~s8mRSmzDVn!kG;b8~ z`WMjtlM&wsU&luvdVT&x^R?w{&8O`hBpjrd#OzQq329 zjhTw~IYljGDuFB!Pt26BbF(EbEh3jbV|g!Yy^&du$y8V4cIw9!v$(`1>g#C?*~>nT zQxaHM#5%GP))N_q?E6{{Q?X~M7mSfHQ!@NG@k~u;QXi!8LvHt`FW6&LVpw9PWUwUG zv4HdTto_`br1wrtC5|O#O7I|~WTr%%>T28(O_nGRSR|KcA2SJloJ#Kd$zW=JHl3PF zQe3j{tB6yxC7E)|aaXe3Z$4anUlL2bTQHu21;Z5GTQ0+Q!@7>dGxdDmogbj_)ND`V zUCwc;5@y`KBUtcTN@j_flF8>h&KbFwpEru?Rh%g|HoPwP%aHhfl~`h?iu#C0K8Pn~ z3ULba9d<6qDX+}9eO|EOx2!Tt%#>`VC|1c_XQb98;*`#m8yjAi`(;RcKOmNvDQP}y zv}yZq116D;^G3t4iV`{0znGnW~;2f<9Kl5-Tk*)o`-e7f&-MRgtXnJvFc zISPMF`DI9azeX$x)|e^Z92iW|TngXoit0K{rn;Njy~ioRg5R>nEHP6u{h#JgFjHEN z!V6P=84}-@#S+ce^VrXG`g|~Pik{Od(}%;hE@F!Ap$c2O0&9;Mr+PMZl#Ww^1sP=_ zPmXiNX&!_*K63h70!tcS&A8^93gZ5$xHCm_(wQl3JXMM*8643%Lda4tcp_t_WSq;v zl=hq#8S9GVI&yuf_fw7{*WK9Z9H)}N67j@LN$RgLQ?5)Ui6v%A5>KjekMB#;))&EX znHi^gns)1PDv2yHQ_>jF@0(|)sP9Ki8Nb8iuMG2eNwDKqa*jd*TlCDNQxet_$*~me z_grS%3K~z%c5m*I%~Bw9ADnXHB9CY09^4qRFK4DCu)s|DW~wkQMUN%HRv=8p{TxnA z#r=9g(HLsRsh*~8Z*eMYEYWo;upvy#;|W&EEtx6baY}!#Gc!efzp|N<#ge4C4;A_ToPQh;QCYxu17b2ipG}Je8Feq-uI*5^oaYjc8fBV z?QMP^`Os{4Q;&P!FWxNScPMaOg_-glqx5S@%v4dPlED%)C3U_lF%|RQ11h1H!4z_J zy~}YbSu8PAC49uoTn7EOe3>cq{dA_1$r3XqovBwP^&^XqTfDFyTc?z>q^4n zlzR}&lqv(tQN()Q&P+P>Bk+`HrtrJ-S5uB+&cc)Wza%hq zCdqNiWU6O#I{GY8Zd}jd*?B4YK8^3dm?{5#KqF2uQ;1VpJ5zzgB^h%cmaj1t<;ICM zgC*jLnexw*7MuE0KScQ&$0;z?y}93vQ-SaV7HZYcv$3Qzh55wX_w%0gsmzp=SoJLQd|hao#8NPY`F>w?ewK>FXQ+fMF4>PaGbN3wH|*Lb>AgEs z$ze$x?_ijU{qs*KPBByH`yoc%t;eb4vBXSCiBI}itUvV(^!$m?G>N4dr!=Ny@D#q> zL%GM5Y{C4OW7jz6a4@#ObWy-#;(!nJH;Zo%N!7+&*hE)wkvE%kTTiW6ASe zB8hRyek_?O2~733PC-{@ipEoAF%?gi1Y67$GbMb%V|jL_uk|tf_Ey(iU(4^RigF3% zC~`X+uAECZUZ3k}`F+g&`z+=uwScp`=K8k$1LD`eBZu(-?u9sn=ZZgtzZX!R)1NU8 z&dPp9k^bBVPyM_q#wELMC;M9ekhG}g3)_tQZ(cl&eO$6@8?*NpusuYFLD+B>9A^8$VY(%- z-`r!SB=r5#DT-6)W5Ci2+5Xlc#EUlZJMuF8KP>CeIoy^X5BpnF)PJ4nCk|8KFa-`% z;7}VQ6NeXJp5|FIQ{MD)(f9jU9z~|;IiKiLrMke;&0+EVFtvmFxb%$Oynd<9;STBt zL%#wJ@!3ySdmpgfxR0lZsW)Ms&Sy3FJYAO_5e%uB%%#;n_PqY;D1hS_Yo>6%dJ6j( zMyB%e3eRodenoYTFt0Ue1roy4TiDOq$06 z{)^4?#f>;sE2b7jUP1i!doV|pw-QzbC`W4<|23emPh-Cf<#55Do`cbUH=|ZQTTblx z9@8`e`Vkt836f5AO09^y5IR+>DQoo0>-|aK$);q}%Tgt!QE*Jv4 z;x{{8Hph!ufy63Mf~l88UIFvYu1%~+yyL8Z2L)_nDSF<^@_Ys3J_rx1umY?AD-eAJ zzN~&g`LcSu@+Gw$b{A}`@}FuQ%3D$1r2L0^t@3YbsGeyNebIRjVxd4rypF-@_A!k_O|+fWXm97lN;rqD2Yl+mUU{5=dCgbgVF1t#wWllOwjK`=@C zK<9z`!Q{WFe6Mpdy*c>3CV#sR{&z3@@xRp}*c9v{OpT!}VV@RZ(=gi4ao8|y5Ju;Y zWhP0J`@rOV29sY=r9Ia$_=C(_tcn7A)OTq9@im;o?}z`~r;ea}8K$z8t-%tsIt^Qd zep4vNVZ*RN*Z?e}d<}7uG`2JOKRl+aieCq{QaXM^IL8iY$9GWqU-bm+986`axdux( z28*yM*f@^SFc_p`GyuyO$1H`v*J#I(;^c$sb>MY{qN>_+w1aY^L~dtDy-d^l0qDOM z$73&!&tCLv_QG%2;;O+Cj_o3B3e1h899A9xlY7A=`2d|q_aaV`PdupJrg8az(SOaT z{0utliQ-}P7nHB7H!BZ8uZN)DL(ua<^(QD*HrBv{4=mxci?Au!IBXbxHHdNm{*=Mr zJ78^ipHlXL%ZJe4AtoQza<7+JT{2RCQ~USf9P=>tZy)w|ANGHr`Zi2uV-0HT154Qc zB8+@;96maXl6-UkEM`!48GgIW?XA93`Xh&_+0dr*$UmS8FyYmfjR zIE{Tg0UOr*{}IHqM=*cj5sYVOTvO`x82WCHqA$81v2Z`G`RqrW+>dKM`{6rmtU)|| z;I!fABPjR4|GQ8&DBl7zkE*{|3Yon6gX&7?9zPqal{$&=_tR5 z%Q0ND6>J_d*nAvZ9s-w#w0+Hh+X2`h_InuRIBW_w4O@i04O7`z1B(xw!{6i|Bd|S& zA2p!71q@xs*OSXQX%L&^IKPY|W|D4)keeSx+&m064};Cah?$4Mywy0=u9>;1L$8rSa9vB_pHto2NY0U8T-ZHQWn)8U~>$|ZVYUWfy*&)IR-Arklz^tn`2;eOnnQcvia8FO=vg`8-bD5_rT~F+ymm@!<8b0;B+ONjaeJV5~`O zoczPVn90xPU z!OU?ma~yGownN+H7c0ODI4eN+Ts#FXpH#0=p1}2)C)5uqQ+#i~vsk>86<`He0akz& zUiRl|$3wZj zxXxLb>gsZ&V58>tF0Ni>uQcl}tiHsx?#ycCDqE%bz0uW*a(8~ES(icGWzKaM71Sv! z^D4#nE@*Z6jwR~O=yg|EzbNWP^*UvhrBc*oj5_O18g&;Hqt3dMM%@Lo&a#t6-I?XO zWp#|Y(IRyj_c}C1w8hdGeVu5EYaN>6S~r?s_l7QxzRpa3-I&QK*PHT#7oeGvyoz|3sx~sH0ttka{%H8xm ztto|dmrwLDHHuor7*3n-m;d;<;<83XS>;*-dCw`?<&{|f zTBWx$4;!O?rPz0i`&+8C{liD_h*j%rrM5`dQ~NVx*6LNu-=pfRUTak4*Xi(xDtjFo aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/validation/demo/src/index.html b/src/validation/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/validation/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/validation/demo/src/main.ts b/src/validation/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/validation/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/validation/demo/test.ts b/src/validation/demo/test.ts new file mode 100644 index 0000000..1822c8b --- /dev/null +++ b/src/validation/demo/test.ts @@ -0,0 +1,24 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); + +// Then we find all the tests. +const context = require.context('./src/app/', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/src/validation/demo/tsconfig.app.json b/src/validation/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/validation/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/validation/demo/tsconfig.spec.json b/src/validation/demo/tsconfig.spec.json new file mode 100644 index 0000000..6da1843 --- /dev/null +++ b/src/validation/demo/tsconfig.spec.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "./test.ts", + "../../polyfills.ts" + ], + "include": [ + "./src/**/*.spec.ts", + "./src/**/*.d.ts", + "../../../node_modules/@types/jasmine/index.d.ts", + "../../../node_modules/karma-viewport/dist/index.d.ts" + ] +} diff --git a/src/validation/lib/index.ts b/src/validation/lib/index.ts new file mode 100644 index 0000000..35d9fd4 --- /dev/null +++ b/src/validation/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiValidationModule'; diff --git a/src/validation/lib/ng-package.json b/src/validation/lib/ng-package.json new file mode 100644 index 0000000..15d4be9 --- /dev/null +++ b/src/validation/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/validation", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/validation/lib/package.json b/src/validation/lib/package.json new file mode 100644 index 0000000..3f17f5e --- /dev/null +++ b/src/validation/lib/package.json @@ -0,0 +1,18 @@ +{ + "name": "@opentiny/ng-validation", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@opentiny/ng-utils": "~1.0.0-beta.0", + "@angular/forms": ">=13.0.0", + "@angular/core": ">=13.0.0", + "@opentiny/ng-locale": "~1.0.0-beta.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0", + "@opentiny/ng-popup": "~1.0.0-beta.0", + "@opentiny/ng-tip": "~1.0.0-beta.0", + "@opentiny/ng-base": "~1.0.0-beta.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-icon": "~1.0.0-beta.0", + "@opentiny/ng-loading": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/validation/lib/project.json b/src/validation/lib/project.json new file mode 100644 index 0000000..348a21d --- /dev/null +++ b/src/validation/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/validation/lib", + "sourceRoot": "src/validation/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/validation"], + "options": { + "project": "src/validation/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/validation"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js validation" + }, + { + "command": "ng default-build validation" + }, + { + "command": "node build/clear-default-theme.js validation" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/validation && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build validation && ng pack validation && node build/publish.js validation --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/validation/lib/src/TiErrorMsgComponent.ts b/src/validation/lib/src/TiErrorMsgComponent.ts new file mode 100644 index 0000000..bdd8801 --- /dev/null +++ b/src/validation/lib/src/TiErrorMsgComponent.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, ViewEncapsulation, ChangeDetectionStrategy } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * @ignore + */ +@Component({ + selector: 'ti-error-msg', + template: ` + + + + + `, + styleUrls: ['./errorMsg.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, // 由于样式文件中存在全局样式,因此使用该方式 + host: { + '[class.ti3-unifyvalid-error-container]': '!appendToTip', + '[class.ti3-unifyvalid-tip-error-container]': 'true' + } +}) +export class TiErrorMsgComponent extends TiBaseComponent { + @Input() errorMessage: string; + @Input() appendToTip: boolean; + protected versionInfo: string = super.getVersion(packageInfo); +} diff --git a/src/validation/lib/src/TiPendingStateComponent.ts b/src/validation/lib/src/TiPendingStateComponent.ts new file mode 100644 index 0000000..6a637c3 --- /dev/null +++ b/src/validation/lib/src/TiPendingStateComponent.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TiBaseComponent } from '@opentiny/ng-base'; +import packageInfo from '../package.json'; +/** + * @ignore + */ +@Component({ + selector: 'ti-pending-state', + templateUrl: './pending-state.html', + styleUrls: ['./pending-state.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.ti3-validation-pending-container]': 'true' + } +}) +export class TiPendingStateComponent extends TiBaseComponent { + // 校验元素 + @Input() validElement: any; + protected versionInfo: string = super.getVersion(packageInfo); + + ngOnInit(): void { + this.renderer.addClass(this.validElement, 'ti3-text-input-show-pending'); + } + + ngOnDestroy(): void { + if (this.validElement) { + this.renderer.removeClass(this.validElement, 'ti3-text-input-show-pending'); + } + } +} diff --git a/src/validation/lib/src/TiPwdMsgComponent.html b/src/validation/lib/src/TiPwdMsgComponent.html new file mode 100644 index 0000000..c5f520b --- /dev/null +++ b/src/validation/lib/src/TiPwdMsgComponent.html @@ -0,0 +1,24 @@ +
    +
    + + + + + + + + {{item.msg}} +
    +
    + {{msgModel.securityText}} +
    + + + +
    + {{msgModel.securityStatus}} +
    +
    diff --git a/src/validation/lib/src/TiPwdMsgComponent.ts b/src/validation/lib/src/TiPwdMsgComponent.ts new file mode 100644 index 0000000..285650a --- /dev/null +++ b/src/validation/lib/src/TiPwdMsgComponent.ts @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { FormControl, ValidationErrors } from '@angular/forms'; +import { Util } from '@opentiny/ng-utils'; +import { TiLocale } from '@opentiny/ng-locale'; +/** + * @ignore + */ +export interface MsgItem { + msg: string; + validStatus: any; +} +/** + * @ignore + */ +export interface MsgModel { + level: number; + msgItems: Array; + securityText: string; + securityStatus: string; +} +/** + * @ignore + */ +@Component({ + selector: 'ti-pwd-msg', + templateUrl: './TiPwdMsgComponent.html', + styleUrls: ['./pwdMsg.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiPwdMsgComponent implements OnInit { + @Input() control: FormControl; + @Input() msgItems: Array; + @Input() validator: any; + @Input() levelFn: Function; + public msgModel: MsgModel = { + level: -1, + msgItems: [], + securityText: '', + securityStatus: '' + }; + + private static _getPwdStrengthLan(key: string): string { + return TiLocale.getLocaleWords().tiValid.passwordStrength[key]; + } + + // 获取各项规则提示 + private static _getMsgItems(msgObj: any, control: FormControl): Array { + let validStatus: number | boolean; + if (control.untouched && control.pristine) { + // 初次聚焦,未输入情况下,不校验 + validStatus = -1; + } + + const msgInfoArr: Array = []; + let msgItem: string = ''; + for (const key in msgObj) { + if (Object.prototype.hasOwnProperty.call(msgObj, key)) { + msgItem = msgObj[key]; + msgInfoArr.push({ + msg: msgItem, + validStatus: TiPwdMsgComponent._getValidStatus(validStatus, key, control) + }); + } + } + + return msgInfoArr; + } + + private static _getValidStatus(validStatus: number | boolean, key: string, control: FormControl): number | boolean { + const errors: ValidationErrors | null = control.errors; + // validStatus已定义情况下,直接返回 + if (!Util.isUndefined(validStatus)) { + return validStatus; + } + // errors中有该条消息,说明校验未通过 + if (errors && !Util.isUndefined(errors[key])) { + return false; + } + // 值为空的情况下,除notEqualPosRev外,其他规则均为错误 + if (control.value === '' && key !== 'notEqualPosRev') { + return false; + } + + return true; + } + + /** + * 获取密码强度等级 + * @param value 输入值 + * @param validator 校验规则配置 + * @param levelFn 密码等级计算函数 + * @returns 密码强度等级 + */ + private static _getLevel(value: string, validator: any, levelFn: Function): number { + return levelFn(value, validator); + } + ngOnInit(): void { + const value: string = this.control.value; + // level计算 + let level: number = -1; + if (!(value === null || value.length === 0)) { + // value值为空情况下,level无效处理 + level = TiPwdMsgComponent._getLevel(value, this.validator, this.levelFn); + } + + // 设置呈现时需要使用的字段 + this.msgModel = { + level, + msgItems: TiPwdMsgComponent._getMsgItems(this.msgItems, this.control), + securityText: TiPwdMsgComponent._getPwdStrengthLan('securityText'), + securityStatus: level === -1 ? '' : TiPwdMsgComponent._getPwdStrengthLan('levelDecArr')[level] + }; + } +} diff --git a/src/validation/lib/src/TiValidationConfig.ts b/src/validation/lib/src/TiValidationConfig.ts new file mode 100644 index 0000000..2fa9653 --- /dev/null +++ b/src/validation/lib/src/TiValidationConfig.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { TiPwdConfig } from './checkHandle/TiPwdConfig'; +import { TiPositionType } from '@opentiny/ng-utils'; +// 定义组件默认配置参数 +/** + * @ignore + */ +export class TiValidationDefaultConfig extends TiPwdConfig { + // 默认校验方式 + public static type: 'change' | 'blur' | 'password' = 'change'; + public static tipPosition: TiPositionType = 'right'; +} diff --git a/src/validation/lib/src/TiValidationDirective.ts b/src/validation/lib/src/TiValidationDirective.ts new file mode 100644 index 0000000..a7c2656 --- /dev/null +++ b/src/validation/lib/src/TiValidationDirective.ts @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, Renderer2, RendererFactory2, SimpleChanges } from '@angular/core'; +import { NgControl } from '@angular/forms'; +import { CheckStyleService } from './checkHandle/CheckStyleService'; +import { CommonService } from './checkHandle/CommonService'; +import { TiValidationConfig, TiValidationType } from './TiValidationInterface'; +import { Subscription, timer } from 'rxjs'; +import { debounce } from 'rxjs/operators'; + +/** + * Tiny校验是基于Angular提供的表单校验进行的封装,在使用Tiny组件前,请先了解[Angular表单校验]{@link https://angular.io/guide/form-validation} + * + * 组件支持的校验规则及方法见 [TiValidators]{@link ../classes/TiValidators.html} + * + */ +@Directive({ + selector: '[tiValidation]', + // 声明该组件定义时需要用到的服务 + providers: [CheckStyleService, CommonService], + host: { + '(focus)': 'onFocus()', + '(blur)': 'onBlur()' + } +}) +export class TiValidationDirective implements OnInit, OnChanges, OnDestroy { + public static readonly ASYNC_DEBOUNCE_TIME: number = 500; // 这个数值跟tiny2的异步校验一致 + /** + * 校验配置信息 + */ + @Input() tiValidation: TiValidationConfig | ''; + /** + * 错误提示信息显示容器,适用于blur/password类型校验形式下,错误提示信息位置自定义场景 + */ + @Input() errorMessageWrapper: Element; + private renderer: Renderer2; + // 当前formControl的statusChanges订阅,在指令销毁时取消 + private formStatusSubscription: Subscription; + private asyncFormStatusSubscription: Subscription; + constructor( + private formControl: NgControl, + private checkStyleFactory: CheckStyleService, + private element: ElementRef, + rendererFactory: RendererFactory2 + ) { + this.renderer = rendererFactory.createRenderer(null, null); + } + private _validationHandleFn: any; + private eleNative: Element = this.element.nativeElement; + ngOnInit(): void { + // 初始化handleFn,用于处理tiValidation指令声明但未定义值场景 + this._setHandleFn(); + // 订阅onStatusChange事件,传递校验时机 + this.formStatusSubscription = this.formControl.statusChanges.subscribe(() => { + this._validationHandleFn.onStatusChange(this.element, this.formControl); + }); + // 订阅onStatusChange事件来特别处理异步校验pending状态的loading图标 + if (this.formControl.control.asyncValidator) { + this.asyncFormStatusSubscription = this.formControl.statusChanges + .pipe( + // 由于异步校验有防抖处理(输入停顿后再进行异步校验),所以需要在异步校验开始时才能出现loading图标 + debounce(() => timer(Number(this.formControl.pending) * TiValidationDirective.ASYNC_DEBOUNCE_TIME)) + ) + .subscribe(() => { + if (this._validationHandleFn.onAsyncStatusChange) { + this._validationHandleFn.onAsyncStatusChange(this.element, this.formControl); + } + }); + } + } + ngOnChanges(changes: SimpleChanges): void { + if (changes['tiValidation'] && !changes['tiValidation'].firstChange) { + // validation动态修改支持,此处根据validation重新创建校验实体方法函数 + this._setHandleFn(); + } + } + ngOnDestroy(): void { + // 宿主元素销毁时,销毁其附属tip + this._validationHandleFn.destroy(this.element); + // 取消formControl中statusChanges的订阅 + this.formStatusSubscription.unsubscribe(); + if (this.asyncFormStatusSubscription) { + this.asyncFormStatusSubscription.unsubscribe(); + } + } + /** + * @ignore + */ + onFocus(): void { + this._markAsFocus(); + this._validationHandleFn.onFocus(this.element, this.formControl); + } + /** + * @ignore + */ + onBlur(): void { + this._markAsBlur(); + this._validationHandleFn.onBlur(this.element, this.formControl); + } + private _setHandleFn(): void { + // 将errorMessageWrapper和tiValidation属性合并,方便后续处理 + const validationConfig: TiValidationConfig = { + ...this.tiValidation, + errorMessageWrapper: this.errorMessageWrapper + }; + let type: TiValidationType; + if (['TI-CHECKBOX-GROUP', 'TI-RADIO-GROUP'].includes(this.eleNative.tagName) || this.eleNative.matches('input[ticheckbox]')) { + type = 'radiobase'; + this.renderer.setAttribute(this.eleNative, 'tiRadiobaseCheck', ''); + } + this._validationHandleFn = this.checkStyleFactory.createHandler( + type || (this.tiValidation && this.tiValidation.type), + validationConfig + ); + + // 失焦校验时给宿主元素添加tiBlurCheck属性标识 + if (this.tiValidation && this.tiValidation.type === 'blur') { + this.renderer.setAttribute(this.eleNative, 'tiBlurCheck', ''); + } + } + + // 设置focus状态标志,标志包括两部分(如下markAsBlur逻辑类似): + // 1.样式类,用于根据focus/blur状态设置CSS中表单边框颜色; + // 2.标志位,用于根据focus/blur判断是否显示提示信息 + private _markAsFocus(): void { + this.renderer.setAttribute(this.eleNative, 'tiFocused', 'tiFocused'); + } + private _markAsBlur(): void { + this.renderer.removeAttribute(this.eleNative, 'tiFocused'); + } +} diff --git a/src/validation/lib/src/TiValidationInterface.ts b/src/validation/lib/src/TiValidationInterface.ts new file mode 100644 index 0000000..0ba6be2 --- /dev/null +++ b/src/validation/lib/src/TiValidationInterface.ts @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/** + * @ignore + */ +export type TiValidationType = 'change' | 'blur' | 'password' | 'radiobase'; +/** + * tiValidation指令接口类型 + */ +export interface TiValidationConfig { + /** + * 校验方式,包括'change' 、 'blur' 、 'password' + * + * @default 'change' + */ + type?: TiValidationType; + /** + * + * 自定义已封装的校验规则的错误提示信息 + */ + errorMessage?: any; + /** + * tip 提示内容,在表单元素聚焦且没有错误提示信息显示 + */ + tip?: string; + /** + * tip 提示方向,默认为 'right' + * + * @default 'right' + */ + tipPosition?: + | 'top' + | 'top-left' + | 'top-right' + | 'bottom' + | 'bottom-left' + | 'bottom-right' + | 'left' + | 'left-top' + | 'left-bottom' + | 'right' + | 'right-top' + | 'right-bottom' + | 'center'; + /** + * tip 最大宽度,默认为 '276px' + * + * @default '276px' + */ + tipMaxWidth?: string; + /** + * 失焦校验或密码校验时,自定义错误信息展示区域 + */ + errorMessageWrapper?: Element; + /** + * 密码校验配置项,支持两个可选参数:
    + * validator?: TiPasswordValidator。关于 TiPasswordValidator,请查看下方对 TiPasswordValidator 接口的说明。
    + * levelFn?: (value:string, validator: TiPasswordValidator) => number。value 为用户输入的内容;关于 TiPasswordValidator,请查看下方对 TiPasswordValidator 接口的说明;返回值为当前输入内容对应的强度等级,0:弱、1:中、2:强 + */ + passwordConfig?: { + validator?: TiPasswordValidatorConfig; + levelFn?(value: string, validator: TiPasswordValidatorConfig): number; + }; +} +/** + * 密码校验规则定义接口 + */ +export interface TiPasswordValidatorConfig { + /** + * 校验规则名称 + */ + rule?: string; + /** + * 自定义校验规则的参数。key 值支持:rangeSize、minCharType 以及自定义规则;value 为需要向校验规则中传递的参数。
    + * rangeSize:定义长度范围; + * minCharType:至少需要的类型数量,类型包括数字、小写字母、大写字母、特殊字符。 + */ + params?: { + [propName: string]: Array; + }; + /** + * 自定义规则对应的提示信息。key 值支持:rangeSize、minCharType 以及自定义规则;value 为对应的提示信息内容。
    + * 支持在提示信息中通过{index: number}获取校验规则中的参数,如{0}代表第1个参数,{1}代表第2个参数 + */ + message?: { + [propName: string]: string; + }; +} + +/** + * + * check方法内部使用的校验方法是AbstractControl的updateValueAndValidity方法 + * 此处将方法参数开放给开发者,onlySelf和emitEvent属性意义具体可参考https://angular.io/api/forms/AbstractControl#updatevalueandvalidity + */ +export interface TiValidationCheckConfig { + /** + * 应用更新和有效性检查后如何传递状态。值为 false 标记所有直系祖先,值为 true 只标记当前控件。默认为 false + */ + onlySelf?: boolean; + /** + * 应用更新和有效性检查后是否回调相关函数。当值为 false 时不会回调 statusChanges、valueChanges 函数。默认为 true + */ + emitEvent?: boolean; + /** + * 指定要忽略校验的表单项键值 + */ + ignoreNames?: Array; + /** + * 是否对错误信息数据结构做扁平化处理。默认为 false。 + */ + errorsFlatted?: boolean; + /** + * 允许有多余的属性字段 + */ + [propName: string]: any; +} diff --git a/src/validation/lib/src/TiValidationModule.ts b/src/validation/lib/src/TiValidationModule.ts new file mode 100644 index 0000000..cda34a7 --- /dev/null +++ b/src/validation/lib/src/TiValidationModule.ts @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiIconModule } from '@opentiny/ng-icon'; +import { TiLoadingModule } from '@opentiny/ng-loading'; +import { TiValidationDirective } from './TiValidationDirective'; +import { TiPwdMsgComponent } from './TiPwdMsgComponent'; +import { TiErrorMsgComponent } from './TiErrorMsgComponent'; +import { TiPendingStateComponent } from './TiPendingStateComponent'; +import { MaxLengthValidatorDirective } from './validators/directives/MaxLengthValidatorDirective'; +import { RangeSizeValidatorDirective } from './validators/directives/RangeSizeValidatorDirective'; +import { RangeValueValidatorDirective } from './validators/directives/RangeValueValidatorDirective'; +import { MinLengthValidatorDirective } from './validators/directives/MinLengthValidatorDirective'; +import { EmailValidatorDirective } from './validators/directives/EmailValidatorDirective'; +import { NotScriptValidatorDirective } from './validators/directives/NotScriptValidatorDirective'; +import { ContainsValidatorDirective } from './validators/directives/ContainsValidatorDirective'; +import { NotContainsValidatorDirective } from './validators/directives/NotContainsValidatorDirective'; +import { DateValidatorDirective } from './validators/directives/DateValidatorDirective'; +import { DigitsValidatorDirective } from './validators/directives/DigitsValidatorDirective'; +import { EqualValidatorDirective } from './validators/directives/EqualValidatorDirective'; +import { Ipv4ValidatorDirective } from './validators/directives/Ipv4ValidatorDirective'; +import { Ipv6ValidatorDirective } from './validators/directives/Ipv6ValidatorDirective'; +import { MaxValueValidatorDirective } from './validators/directives/MaxValueValidatorDirective'; +import { IntegerValidatorDirective } from './validators/directives/IntegerValidatorDirective'; +import { MinValueValidatorDirective } from './validators/directives/MinValueValidatorDirective'; +import { NotEqualValidatorDirective } from './validators/directives/NotEqualValidatorDirective'; +import { NumberValidatorDirective } from './validators/directives/NumberValidatorDirective'; +import { PasswordValidatorDirective } from './validators/directives/PasswordValidatorDirective'; +import { PortValidatorDirective } from './validators/directives/PortValidatorDirective'; +import { RegExpValidatorDirective } from './validators/directives/RegExpValidatorDirective'; +import { UrlValidatorDirective } from './validators/directives/UrlValidatorDirective'; +import { RequiredValidatorDirective } from './validators/directives/RequiredValidatorDirective'; +import { TiRendererModule } from '@opentiny/ng-renderer'; +import { TiTipModule } from '@opentiny/ng-tip'; +import { BaseValidator } from './validators/directives/BaseValidator'; +import { MaxValueByStringValidatorDirective } from './validators/directives/MaxValueByStringValidatorDirective'; +import { MinValueByStringValidatorDirective } from './validators/directives/MinValueByStringValidatorDirective'; +import { RangeValueByStringValidatorDirective } from './validators/directives/RangeValueByStringValidatorDirective'; +import { BigIntegerValidatorDirective } from './validators/directives/BigIntegerValidatorDirective'; +import { BigNumberValidatorDirective } from './validators/directives/BigNumberValidatorDirective'; +import { BigDigitsValidatorDirective } from './validators/directives/BigDigitsValidatorDirective'; +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; + +@NgModule({ + imports: [ + CommonModule, + TiIconModule, + TiRendererModule, + TiTipModule, // 引用TiTipModule,可以使用Tip指令和Tip服务 + TiLoadingModule + ], + // 下面必须写两遍,因为生产环境不允许编译之前执行函数。 + exports: [ + BaseValidator, + TiErrorMsgComponent, + TiPendingStateComponent, + TiValidationDirective, // 额外加的 + MaxLengthValidatorDirective, + RangeSizeValidatorDirective, + RangeValueValidatorDirective, + MinLengthValidatorDirective, + EmailValidatorDirective, + NotScriptValidatorDirective, + ContainsValidatorDirective, + NotContainsValidatorDirective, + DateValidatorDirective, + DigitsValidatorDirective, + EqualValidatorDirective, + Ipv4ValidatorDirective, + Ipv6ValidatorDirective, + MaxValueValidatorDirective, + IntegerValidatorDirective, + MinValueValidatorDirective, + NotEqualValidatorDirective, + NumberValidatorDirective, + PasswordValidatorDirective, + PortValidatorDirective, + RegExpValidatorDirective, + UrlValidatorDirective, + RequiredValidatorDirective, + MaxValueByStringValidatorDirective, + MinValueByStringValidatorDirective, + RangeValueByStringValidatorDirective, + BigIntegerValidatorDirective, + BigNumberValidatorDirective, + BigDigitsValidatorDirective + ], + declarations: [ + BaseValidator, + TiValidationDirective, + TiPwdMsgComponent, + TiPendingStateComponent, + TiErrorMsgComponent, // 额外加的 + MaxLengthValidatorDirective, + RangeSizeValidatorDirective, + RangeValueValidatorDirective, + MinLengthValidatorDirective, + EmailValidatorDirective, + NotScriptValidatorDirective, + ContainsValidatorDirective, + NotContainsValidatorDirective, + DateValidatorDirective, + DigitsValidatorDirective, + EqualValidatorDirective, + Ipv4ValidatorDirective, + Ipv6ValidatorDirective, + MaxValueValidatorDirective, + IntegerValidatorDirective, + MinValueValidatorDirective, + NotEqualValidatorDirective, + NumberValidatorDirective, + PasswordValidatorDirective, + PortValidatorDirective, + RegExpValidatorDirective, + UrlValidatorDirective, + RequiredValidatorDirective, + MaxValueByStringValidatorDirective, + MinValueByStringValidatorDirective, + RangeValueByStringValidatorDirective, + BigIntegerValidatorDirective, + BigNumberValidatorDirective, + BigDigitsValidatorDirective + ], + entryComponents: [TiPwdMsgComponent, TiErrorMsgComponent, TiPendingStateComponent] +}) +export class TiValidationModule { + constructor() { + TiLocale.setTiWords(locales); + } +} +export { TiValidationDirective } from './TiValidationDirective'; +export { TiPasswordValidatorConfig, TiValidationConfig, TiValidationType, TiValidationCheckConfig } from './TiValidationInterface'; +export { TiValidators } from './validators/TiValidators'; +export { BaseValidator } from './validators/directives/BaseValidator'; +export { TiErrorMsgComponent } from './TiErrorMsgComponent'; +export { TiPendingStateComponent } from './TiPendingStateComponent'; +export { MaxLengthValidatorDirective } from './validators/directives/MaxLengthValidatorDirective'; +export { RangeSizeValidatorDirective } from './validators/directives/RangeSizeValidatorDirective'; +export { RangeValueValidatorDirective } from './validators/directives/RangeValueValidatorDirective'; +export { MinLengthValidatorDirective } from './validators/directives/MinLengthValidatorDirective'; +export { EmailValidatorDirective } from './validators/directives/EmailValidatorDirective'; +export { NotScriptValidatorDirective } from './validators/directives/NotScriptValidatorDirective'; +export { ContainsValidatorDirective } from './validators/directives/ContainsValidatorDirective'; +export { NotContainsValidatorDirective } from './validators/directives/NotContainsValidatorDirective'; +export { DateValidatorDirective } from './validators/directives/DateValidatorDirective'; +export { DigitsValidatorDirective } from './validators/directives/DigitsValidatorDirective'; +export { EqualValidatorDirective } from './validators/directives/EqualValidatorDirective'; +export { Ipv4ValidatorDirective } from './validators/directives/Ipv4ValidatorDirective'; +export { Ipv6ValidatorDirective } from './validators/directives/Ipv6ValidatorDirective'; +export { MaxValueValidatorDirective } from './validators/directives/MaxValueValidatorDirective'; +export { IntegerValidatorDirective } from './validators/directives/IntegerValidatorDirective'; +export { MinValueValidatorDirective } from './validators/directives/MinValueValidatorDirective'; +export { NotEqualValidatorDirective } from './validators/directives/NotEqualValidatorDirective'; +export { NumberValidatorDirective } from './validators/directives/NumberValidatorDirective'; +export { PasswordValidatorDirective } from './validators/directives/PasswordValidatorDirective'; +export { PortValidatorDirective } from './validators/directives/PortValidatorDirective'; +export { RegExpValidatorDirective } from './validators/directives/RegExpValidatorDirective'; +export { UrlValidatorDirective } from './validators/directives/UrlValidatorDirective'; +export { RequiredValidatorDirective } from './validators/directives/RequiredValidatorDirective'; +export { MaxValueByStringValidatorDirective } from './validators/directives/MaxValueByStringValidatorDirective'; +export { MinValueByStringValidatorDirective } from './validators/directives/MinValueByStringValidatorDirective'; +export { RangeValueByStringValidatorDirective } from './validators/directives/RangeValueByStringValidatorDirective'; +export { BigIntegerValidatorDirective } from './validators/directives/BigIntegerValidatorDirective'; +export { BigNumberValidatorDirective } from './validators/directives/BigNumberValidatorDirective'; +export { BigDigitsValidatorDirective } from './validators/directives/BigDigitsValidatorDirective'; diff --git a/src/validation/lib/src/checkHandle/AsyncCheck.ts b/src/validation/lib/src/checkHandle/AsyncCheck.ts new file mode 100644 index 0000000..b9ae069 --- /dev/null +++ b/src/validation/lib/src/checkHandle/AsyncCheck.ts @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Util } from '@opentiny/ng-utils'; +import { CheckHandle } from './CheckHandle'; +import { NgControl, ValidationErrors } from '@angular/forms'; +import { ComponentRef, ElementRef } from '@angular/core'; +import { TiPendingStateComponent } from '../TiPendingStateComponent'; + +/** + * @ignore + * + */ +export class AsyncCheck extends CheckHandle { + // 处理异步校验错误提示信息 + public onStatusChange(ele: ElementRef, formControl: NgControl): void { + // 清除先前校验信息 + this.commonHandle.clearValidMsg(ele); + + const errors: ValidationErrors = formControl.control.errors; + if (!errors) { + return; + } + + const ruleKey: string = Object.keys(errors)[0]; + const ruleErrors: any = errors[ruleKey]; + // 添加异步校验错误提示信息 + if (typeof ruleErrors === 'object' && Util.isString(ruleErrors.tiAsyncErrorMessage)) { + this.commonHandle.addValidMsg(ele, this.validationConf, formControl, true); + } + } + + // 处理异步校验pending状态时的loading图标 + public onAsyncStatusChange(ele: ElementRef, formControl: NgControl): void { + if (formControl.pending) { + this.addPendingIcon(ele, formControl); + } else { + this.clearPendingIcon(ele); + } + } + + public addPendingIcon(ele: ElementRef, formControl: NgControl): void { + if (ele.nativeElement.tiPendingStateRef) { + return; + } + const pendingStateRef: ComponentRef = this.getPendingStateRef(ele); + const pendingDom: Element = pendingStateRef.location.nativeElement; + if (pendingDom === null || pendingDom.childNodes.length === 0) { + return; + } + this.commonHandle._tiRenderer.insertAfter(pendingDom, ele.nativeElement); + + ele.nativeElement.tiPendingStateRef = pendingStateRef; + } + + public clearPendingIcon(ele: ElementRef): void { + const pendingStateRef: ComponentRef = ele.nativeElement.tiPendingStateRef; + if (pendingStateRef) { + pendingStateRef.destroy(); + ele.nativeElement.tiPendingStateRef = undefined; + } + } + + private getPendingStateRef(ele: ElementRef): ComponentRef { + const pendignComponentRef: ComponentRef = this.commonHandle._tiPopupService.createCompoentRef({ + componentType: TiPendingStateComponent, + context: { + validElement: ele.nativeElement + } + }); + + return pendignComponentRef; + } +} diff --git a/src/validation/lib/src/checkHandle/BlurCheck.ts b/src/validation/lib/src/checkHandle/BlurCheck.ts new file mode 100644 index 0000000..3e569c1 --- /dev/null +++ b/src/validation/lib/src/checkHandle/BlurCheck.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Util } from '@opentiny/ng-utils'; +import { CheckHandle } from './CheckHandle'; +import { NgControl } from '@angular/forms'; +import { ElementRef } from '@angular/core'; +import { TiValidationConfig } from '../TiValidationInterface'; +/** + * @ignore + */ +export class BlurCheck extends CheckHandle { + public onStatusChange(ele: ElementRef, formControl: NgControl): void { + if (this.commonHandle.isFocused(ele)) { + // 获取焦点/正在输入情况下,显示/修改tip提示 + this.addTip(ele, this.validationConf, formControl); + + return; + } + // 未输入过且未聚焦的情况下,先清除先前校验信息(存在reset即改变untounced状态情况下清除校验信息) + if (formControl.untouched) { + this.commonHandle.clearValidMsg(ele); // 清除先前校验信息 + + return; + } + // 失焦情况下,根据校验结果,显示/清除错误提示 + this.commonHandle.clearValidMsg(ele); // 清除先前校验信息 + this.commonHandle.addValidMsg(ele, this.validationConf, formControl); + } + public onFocus(ele: ElementRef, formControl: NgControl): void { + this.commonHandle.clearValidMsg(ele); // 清除先前错误提示信息 + this.addTip(ele, this.validationConf, formControl); + } + public onBlur(ele: ElementRef, formControl: NgControl): void { + this.commonHandle.destroyTip(ele); // 清除先前校验信息 + this.commonHandle.addValidMsg(ele, this.validationConf, formControl); + } + + public addTip(ele: ElementRef, validationConf: TiValidationConfig, formControl: NgControl): void { + // 该方法子类会复写 + this.commonHandle.destroyTip(ele); // 清除先前tip提示信息 + const tipContent: string = validationConf && validationConf.tip; + // tip内容无效情况下不做处理 + if (tipContent === '' || Util.isUndefined(tipContent)) { + return; + } + // 添加Tip提示并缓存 + this.commonHandle.generateTip(ele, tipContent, validationConf); + } +} diff --git a/src/validation/lib/src/checkHandle/ChangeCheck.ts b/src/validation/lib/src/checkHandle/ChangeCheck.ts new file mode 100644 index 0000000..feb5590 --- /dev/null +++ b/src/validation/lib/src/checkHandle/ChangeCheck.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Util } from '@opentiny/ng-utils'; +import { AsyncCheck } from './AsyncCheck'; +import { NgControl, ValidationErrors } from '@angular/forms'; +import { ElementRef } from '@angular/core'; +import { TiValidationConfig } from '../TiValidationInterface'; + +/** + * @ignore + */ +export class ChangeCheck extends AsyncCheck { + public onStatusChange(ele: ElementRef, formControl: NgControl): void { + super.onStatusChange(ele, formControl); + this._checkHandle(ele, this.validationConf, formControl); + } + public onFocus(ele: ElementRef, formControl: NgControl): void { + this._checkHandle(ele, this.validationConf, formControl); + } + public onBlur(ele: ElementRef, formControl: NgControl): void { + // 清除先前校验信息,已失焦情况不做校验提示或tip提示,只边框标红,边框标红在样式类中通过ng-invalid处理 + this.commonHandle.destroyTip(ele); + } + + // 校验处理函数,该函数覆盖了focus和statusChange状态下的校验处理 + private _checkHandle(ele: ElementRef, validationConf: TiValidationConfig, formControl: NgControl): void { + this.commonHandle.destroyTip(ele); // 清除先前校验信息 + // 已失焦情况不做校验提示或tip提示,只边框标红,边框标红只在样式类中通过ng-invalid处理,JS中无需处理 + if (!this.commonHandle.isFocused(ele)) { + return; + } + + // 三种情况只做Tip提示: + // 1.初次focus且初次focus情况下输入值未发生改变时不进行校验 + // (初次focus值一旦发生变化且出错情况下,就要改变为错误提示) + // 2.校验正确 + // 3.是弹窗中autofocus的元素 + // (弹窗中autofocus的元素,在弹窗打开动画结束后会先blur再focus,会使其由untouched变成touched, + // 但需要使其表现与初次focus一致) + const errors: ValidationErrors = formControl.control.errors; + if ((formControl.pristine && formControl.untouched) || errors === null || ele.nativeElement.querySelector('[tiautofocusinmodal]')) { + this._addTip(ele, validationConf, validationConf && validationConf.tip); + + return; + } + // 其它情况下做错误提示 + this._addValidTip(ele, errors, validationConf); + } + + // 显示错误提示信息 + private _addValidTip(ele: ElementRef, errors: ValidationErrors, validationConf: TiValidationConfig): void { + const tipContentDom: any = this.commonHandle.getErrorMsg(errors, validationConf, true); + if (tipContentDom === null) { + // 无错误信息情况下,不做处理 + return; + } + this._addTip(ele, validationConf, tipContentDom); + } + + // 显示tip提示 + private _addTip(ele: ElementRef, validationConf: TiValidationConfig, tipContent: string): /* ViewContainerData */ void { + // tip内容无效情况下不做处理 + if (tipContent === '' || Util.isUndefined(tipContent)) { + return; + } + // 添加Tip提示 + this.commonHandle.generateTip(ele, tipContent, validationConf); + } +} diff --git a/src/validation/lib/src/checkHandle/CheckHandle.ts b/src/validation/lib/src/checkHandle/CheckHandle.ts new file mode 100644 index 0000000..0d0099f --- /dev/null +++ b/src/validation/lib/src/checkHandle/CheckHandle.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgControl } from '@angular/forms'; +import { ElementRef } from '@angular/core'; +import { TiValidationConfig } from '../TiValidationInterface'; + +/** + * @ignore + */ +export class CheckHandle { + constructor(protected validationConf: TiValidationConfig, protected commonHandle: any) {} + onStatusChange(ele: ElementRef, formControl: NgControl): void {} + onFocus(ele: ElementRef, formControl: NgControl): void {} + onBlur(ele: ElementRef, formControl: NgControl): void {} + destroy(ele: ElementRef): void { + this.commonHandle.destroyTip(ele); + } +} diff --git a/src/validation/lib/src/checkHandle/CheckStyleModule.ts b/src/validation/lib/src/checkHandle/CheckStyleModule.ts new file mode 100644 index 0000000..aa41b5c --- /dev/null +++ b/src/validation/lib/src/checkHandle/CheckStyleModule.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonServiceModule } from './CommonServiceModule'; +// 命名含有Service的Module,都不是对外暴露的Service,是内部使用。外部使用此Service,导入TiDragModule即可 +/** + * @ignore + */ +@NgModule({ + imports: [CommonServiceModule] +}) +export class CheckStyleModule {} diff --git a/src/validation/lib/src/checkHandle/CheckStyleService.ts b/src/validation/lib/src/checkHandle/CheckStyleService.ts new file mode 100644 index 0000000..c95d2fe --- /dev/null +++ b/src/validation/lib/src/checkHandle/CheckStyleService.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +// 该功能类用于根据校验方式定义校验实体执行函数 +import { Injectable } from '@angular/core'; +import { ChangeCheck } from './ChangeCheck'; +import { BlurCheck } from './BlurCheck'; +import { PwdCheck } from './PwdCheck'; +import { RadiobaseCheck } from './RadiobaseCheck'; +import { CommonService } from './CommonService'; +import { TiValidationDefaultConfig } from '../TiValidationConfig'; +import { Util } from '@opentiny/ng-utils'; +import { TiValidationConfig, TiValidationType } from '../TiValidationInterface'; +import { CheckStyleModule } from './CheckStyleModule'; + +/** + * @ignore + */ +@Injectable({ + providedIn: CheckStyleModule +}) +export class CheckStyleService { + constructor(private commonService: CommonService) {} + public createHandler(type: TiValidationType | '', config: TiValidationConfig): any { + let resultType: any = type; + // 设置checkStyle默认值 + if (Util.isUndefined(type)) { + resultType = TiValidationDefaultConfig.type; + // 设置passwordConfig情况下,type默认为'password' + if (!Util.isUndefined(config.passwordConfig)) { + resultType = 'password'; + } + } + // 根据不同的配置,设置校验方式执行函数 + let handle: any; + switch (resultType) { + case 'radiobase': + handle = new RadiobaseCheck(config, this.commonService); + break; + case 'blur': + handle = new BlurCheck(config, this.commonService); + break; + case 'password': + handle = new PwdCheck(config, this.commonService); + break; + default: // 定义为'change'及其他无效方式,均采用change校验方式 + handle = new ChangeCheck(config, this.commonService); + } + + return handle; + } +} diff --git a/src/validation/lib/src/checkHandle/CommonService.ts b/src/validation/lib/src/checkHandle/CommonService.ts new file mode 100644 index 0000000..a203350 --- /dev/null +++ b/src/validation/lib/src/checkHandle/CommonService.ts @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ComponentRef, ElementRef, Injectable, Renderer2 } from '@angular/core'; +import { TiLocale } from '@opentiny/ng-locale'; +import { TiRenderer } from '@opentiny/ng-renderer'; +import { TiPopupService } from '@opentiny/ng-popup'; +import { Util } from '@opentiny/ng-utils'; +import { TiTipRef, TiTipService } from '@opentiny/ng-tip'; +import { TiValidationDefaultConfig } from '../TiValidationConfig'; +import { TiValidationConfig } from '../TiValidationInterface'; +import { TiErrorMsgComponent } from '../TiErrorMsgComponent'; +import { NgControl, ValidationErrors } from '@angular/forms'; +import { CommonServiceModule } from './CommonServiceModule'; + +/** + * @ignore + */ +@Injectable({ + providedIn: CommonServiceModule +}) +export class CommonService { + constructor( + private _renderer: Renderer2, + private _tiRenderer: TiRenderer, + private _tipService: TiTipService, + private _tiPopupService: TiPopupService + ) {} + + // 获取错误信息源字串 + private static _getSourceStr(ruleKey: string, ruleErrors: any, validationConf: TiValidationConfig, isAsync: boolean): string { + // 优先使用tiValidation中的规则项配置errorMsg + const errMsgConf: any = validationConf && validationConf.errorMessage; + if (!Util.isUndefined(errMsgConf) && Util.isString(errMsgConf[ruleKey])) { + return errMsgConf[ruleKey]; + } + + if (typeof ruleErrors === 'object') { + // 获取自定义校验规则中的 tiErrorMsg + if (!isAsync && Util.isString(ruleErrors.tiErrorMessage)) { + return ruleErrors.tiErrorMessage; + } + + // 获取自定义异步校验规则中的 tiAsyncErrorMessage + if (isAsync && Util.isString(ruleErrors.tiAsyncErrorMessage)) { + return ruleErrors.tiAsyncErrorMessage; + } + } + + // 获取到的tiValidation errorMsg无效情况下,使用默认规则提示 + return TiLocale.getLocaleWords().tiValid.errorMsg[ruleKey] || ''; + } + + // 获取错误提示字符串,思路是: + // 1.先获取错误信息源字串(可能带params标识{0}/{1}等); + // 2.将获取的源字串中的参数替换为真实数据 + private static _getErrorStr(errors: ValidationErrors, validationConf: TiValidationConfig, isAsync: boolean): string { + const ruleKey: string = Object.keys(errors)[0]; + const ruleErrors: any = errors[ruleKey]; + const msgStr: string = CommonService._getSourceStr(ruleKey, ruleErrors, validationConf, isAsync); + // 获取错误信息参数,无参数情况下,不需做格式匹配直接返回 + // errors格式示例:{required:true,{'maxlength': {'requiredLength': maxLength, 'actualLength': length}}} + if (typeof ruleErrors !== 'object') { + return msgStr; + } + const params: Array = Object.values(ruleErrors); // 此处对错误信息定义有要求:要求错误返回对象与词条中的参数次序与一致 + + return Util.formatEntry(msgStr, params); + } + public isFocused(ele: any): boolean { + return ele.nativeElement.attributes.tiFocused !== undefined; + } + // 获取密码校验中校验规则信息 + public getMsg(ruleKey: string, params: any): string { + const messageStr: string = TiLocale.getLocaleWords().tiValid.message[ruleKey]; + + return Util.formatEntry(messageStr, params); + } + // 获取错误提示信息DOM文本,此处TiErrorMsgComponent组件处理的不仅是提示信息,还包括错误时的组件边框变红样式加载 + public getErrorMsg( + errors: ValidationErrors, + validationConf: TiValidationConfig, + appendToTip: boolean = false, + isAsync?: boolean + ): Element { + const errorMsg: string = CommonService._getErrorStr(errors, validationConf, isAsync); + if (!errorMsg) { + return null; + } + const errorMsgComponentRef: ComponentRef = this._tiPopupService.createCompoentRef({ + componentType: TiErrorMsgComponent, + context: { + errorMessage: errorMsg, + appendToTip + } + }); + + return errorMsgComponentRef.location.nativeElement; + } + + // 调用ti-Tip组件生成Tip提示,该公共方法中同时传递了tip的位置信息等 + public generateTip(ele: ElementRef, tipContent: string, validationConf: TiValidationConfig, context?: any): TiTipRef { + const tipInstance: TiTipRef = this._tipService.create(ele.nativeElement, { + position: validationConf.tipPosition || TiValidationDefaultConfig.tipPosition, + maxWidth: validationConf.tipMaxWidth + }); + tipInstance.show(tipContent, context); + ele.nativeElement.tiValidTip = tipInstance; + + return tipInstance; + } + // 销毁Tip提示 + public destroyTip(ele: ElementRef): void { + const tipEle: TiTipRef = ele.nativeElement.tiValidTip; + if (!Util.isUndefined(tipEle)) { + tipEle.hide(); + } + } + // 根据errors结果生成可占位的错误提示信息(blurCheck和pwdCheck中使用) + public addValidMsg(ele: ElementRef, validationConf: TiValidationConfig, formControl: NgControl, isAsync?: boolean): void { + const errors: ValidationErrors = formControl.control.errors; + // 校验正确情况下不做处理 + if (errors === null) { + return; + } + const errorDom: Element = this.getErrorMsg(errors, validationConf, false, isAsync); + if (errorDom === null || errorDom.childNodes.length === 0) { + return; + } + // 添加错误信息 + if (validationConf && validationConf.errorMessageWrapper) { + this._renderer.appendChild(validationConf.errorMessageWrapper, errorDom); + } else if (ele.nativeElement.hasAttribute('tiTextarea')) { + this._tiRenderer.insertAfter(errorDom, this._renderer.parentNode(ele.nativeElement)); + } else { + this._tiRenderer.insertAfter(errorDom, ele.nativeElement); + } + ele.nativeElement.tiErrorMessage = errorDom; + } + // 销毁可占位的错误提示信息(blurCheck和pwdCheck中使用) + public clearValidMsg(ele: ElementRef): void { + const errMsgDom: any = ele.nativeElement.tiErrorMessage; + if (!Util.isUndefined(errMsgDom)) { + this._renderer.removeChild(errMsgDom.parentNode, errMsgDom); + } + } +} diff --git a/src/validation/lib/src/checkHandle/CommonServiceModule.ts b/src/validation/lib/src/checkHandle/CommonServiceModule.ts new file mode 100644 index 0000000..cb79bb5 --- /dev/null +++ b/src/validation/lib/src/checkHandle/CommonServiceModule.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +// 命名含有Service的Module,都不是对外暴露的Service,是内部使用。外部使用此Service,导入TiDragModule即可 +/** + * @ignore + */ +@NgModule({}) +export class CommonServiceModule {} diff --git a/src/validation/lib/src/checkHandle/PwdCheck.ts b/src/validation/lib/src/checkHandle/PwdCheck.ts new file mode 100644 index 0000000..ca82587 --- /dev/null +++ b/src/validation/lib/src/checkHandle/PwdCheck.ts @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Util } from '@opentiny/ng-utils'; +import { TiPwdMsgComponent } from '../TiPwdMsgComponent'; +import { BlurCheck } from './BlurCheck'; +import { CheckHandle } from './CheckHandle'; +import { TiValidationDefaultConfig } from '../TiValidationConfig'; +import { TiPasswordValidatorConfig, TiValidationConfig } from '../TiValidationInterface'; +import { NgControl } from '@angular/forms'; +import { ElementRef } from '@angular/core'; + +/** + * @ignore + */ +export class PwdCheck extends BlurCheck implements CheckHandle { + private msgItems: any; + private validator: TiPasswordValidatorConfig; + private levelFn: (value: string, validator: TiPasswordValidatorConfig) => void; + constructor(private config: TiValidationConfig, private common: any) { + super(config, common); + const pwdConfig: any = this.config && this.config.passwordConfig; + // 设置参数,用于规则提示组件数据组装 + this._setValidator(pwdConfig); + this._setMsgItems(); + this._setLevelFn(pwdConfig); + } + + onFocus(ele: ElementRef, formControl: NgControl): void { + // 由于密码组件需要依赖于其他输入框,因此在focus时需要更新其状态 + formControl.control.updateValueAndValidity(); + super.onFocus(ele, formControl); + } + + // 设置validator params和msg + private _setValidator(pwdConfig: any): void { + let pwdValidator: any = TiValidationDefaultConfig.pwdValidator; + if (pwdConfig && pwdConfig.validator) { + // validator定义情况下,处理validator + const pwdValidatorConfig: TiPasswordValidatorConfig = pwdConfig.validator; + if (Util.isUndefined(pwdValidatorConfig.rule) || pwdValidatorConfig.rule === pwdValidator.rule) { + // 依然使用默认规则情况下,只做参数合并 + pwdValidator = { + params: { ...pwdValidator.params, ...pwdValidatorConfig.params }, + message: { ...pwdValidator.message, ...pwdValidatorConfig.message } + }; + } else { + pwdValidator = { + params: pwdValidatorConfig.params, + message: pwdValidatorConfig.message + }; + } + } + this.validator = pwdValidator; + } + + // 设置msgItems,msgItems数据格式为:{ruleKey: msgStr,...} + // 其中msgStr为格式化后的页面可显示字串 + private _setMsgItems(): void { + const validatorConfig: TiPasswordValidatorConfig = this.validator; + const msgConfig: any = validatorConfig.message; + const paramsConfig: any = validatorConfig.params; + let msgStr: string; + const msgItems: any = {}; + // msgConfig中定义的规则提示才会在页面中显示 + for (const key in msgConfig) { + if (Object.prototype.hasOwnProperty.call(msgConfig, key)) { + // 规则提示优先从config中读取,如果读取到的是无效字串,则从国际化默认配置中读取 + msgStr = msgConfig[key] || this.common.getMsg(key); + // 如果对应的规则有参数的情况下,则根据参数做字串格式化 + if (paramsConfig && Object.prototype.hasOwnProperty.call(paramsConfig, key)) { + msgStr = Util.formatEntry(msgStr, paramsConfig[key]); + } + msgItems[key] = msgStr; + } + } + this.msgItems = msgItems; + } + + // 设置levelFn:levelFn支持业务自定义 + private _setLevelFn(pwdConfig: any): void { + let levelFn: (value: string, validator: TiPasswordValidatorConfig) => void = TiValidationDefaultConfig.pwdLevelFn; + if (pwdConfig && Util.isFunction(pwdConfig.levelFn)) { + levelFn = pwdConfig.levelFn; + } + this.levelFn = levelFn; + } + public addTip(ele: ElementRef, validationConf: TiValidationConfig, formControl: NgControl): void { + this.commonHandle.destroyTip(ele); // 清除先前提示信息 + const inputsObj: any = { + msgItems: this.msgItems, + validator: this.validator, + control: formControl, + levelFn: this.levelFn + }; + // tip默认值设置 + const configAssigned: TiValidationDefaultConfig = { + tipPosition: TiValidationDefaultConfig.pwdTipPostion, + tipMaxWidth: TiValidationDefaultConfig.pwdTipMaxWidth, + ...validationConf + }; + this.common.generateTip(ele, TiPwdMsgComponent, configAssigned, inputsObj); + } +} diff --git a/src/validation/lib/src/checkHandle/RadiobaseCheck.ts b/src/validation/lib/src/checkHandle/RadiobaseCheck.ts new file mode 100644 index 0000000..b0f434c --- /dev/null +++ b/src/validation/lib/src/checkHandle/RadiobaseCheck.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { ElementRef } from '@angular/core'; +import { NgControl, NgModel } from '@angular/forms'; +import { TiValidationConfig } from '../TiValidationInterface'; +import { CheckHandle } from './CheckHandle'; + +export class RadiobaseCheck extends CheckHandle { + constructor(protected validationConf: TiValidationConfig, protected commonHandle: any) { + super(validationConf, commonHandle); + } + public onStatusChange(ele: ElementRef, formControl: NgControl): void { + if (formControl instanceof NgModel && !ele.nativeElement.isInit) { + ele.nativeElement.isInit = true; + + return; + } + this.commonHandle.clearValidMsg(ele, this.validationConf, formControl); + if (formControl.invalid) { + this.commonHandle.addValidMsg(ele, this.validationConf, formControl); + } + } + + public destroy(ele: ElementRef): void { + this.commonHandle.clearValidMsg(ele); + } +} diff --git a/src/validation/lib/src/checkHandle/TiPwdConfig.ts b/src/validation/lib/src/checkHandle/TiPwdConfig.ts new file mode 100644 index 0000000..d4cdac5 --- /dev/null +++ b/src/validation/lib/src/checkHandle/TiPwdConfig.ts @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Util } from '@opentiny/ng-utils'; +import { TiPasswordValidatorConfig } from '../TiValidationInterface'; +/** + * @ignore + */ +export interface TiPwdParams { + minCharLen: number; + minCharTypeNum: number; + charTypeRegObj: any; +} +/** + * @ignore + */ +export class TiPwdConfig { + public static pwdTipPostion: string = 'right'; + public static pwdTipMaxWidth: string = '440px'; + // pwd校验规则 + public static pwdCharTypeRegObj: any = { + // 默认字串类型集合,特殊规则可在此处进行配置 + // specialCharReg: /[~`!\?,.:;\-_'"\(\)\{\}\[\]\/<>@#\$%\^&\*\+\|\\=\s]/, + // https://www.bejson.com/zhuanyi/ Json转义工具有错,单引号未转义 + // eslint-disable-next-line prefer-regex-literals + specialCharReg: new RegExp('[~`!\\?,.:;\\-_\'"\\(\\)\\{\\}\\[\\]\\/<>@#\\$%\\^&\\*\\+\\|\\\\=\\s]'), + // eslint-disable-next-line prefer-regex-literals + lowerCharReg: new RegExp('[a-z]+'), + // eslint-disable-next-line prefer-regex-literals + upperCharReg: new RegExp('[A-Z]+'), + // eslint-disable-next-line prefer-regex-literals + digitsCharReg: new RegExp('[0-9]+') + }; + public static pwdValidator: any = { + rule: 'password', + params: { + rangeSize: [6, 32], + minCharType: [2] // 该校验方法支持两个参数传递,第二个参数为charTypeRegObj,不传递第二个参数情况下,代表使用该配置中的默认值 + }, + message: { + rangeSize: '', // ''代表使用组件国际化语言中的默认提示文本 + minCharType: '', + notEqualPosRev: '' + } + }; + // /** + // * 计算密码熵值,具体的计算密码熵值的算法如下: + // * 1、第一个字符为4bits + // * 2、2-8个字符,每一个字符为2bits + // * 3、9-20个字符之间,每一个字符为1.5bits + // * 4、21个字符之后,每一个字符为1bit + // * 5、如果密码中同时存在大写字母和特殊字符,则熵值+6 + // * @param {String} str 密码字串 + // * @param {Number} minLen 密码最小长度 + // * @param {Number} minType 密码最小组合种类 + // * @returns {Number} level 密码等级 + // */ + public static pwdLevelFn(value: string, validator: TiPasswordValidatorConfig): number { + const validatorParam: TiPwdParams = getValidatorParam(); + const minCharLen: number = validatorParam.minCharLen; + const minCharTypeNum: number = validatorParam.minCharTypeNum; + const charTypeRegObj: any = validatorParam.charTypeRegObj as any; + // 根据字符个数初步计算熵值 + const len: number = value.length; + let pwdScore: number = 0; + if (len >= 21) { + pwdScore = len - 21 + 1.5 * 12 + 2 * 7 + 4; + } else if (len >= 9 && len < 21) { + pwdScore = (len - 9 + 1) * 1.5 + 2 * 7 + 4; + } else if (len >= 2 && len < 9) { + pwdScore = (len - 2 + 1) * 2 + 4; + } else if (len >= 1 && len < 2) { + pwdScore = 4; + } + // 同时存在大写字母和特殊字符,熵值+6 + if (charTypeRegObj.upperCharReg && charTypeRegObj.upperCharReg.test(value) && charTypeRegObj.specialCharReg.test(value)) { + pwdScore += 6; + + // 重置正则表达式的lastIndex属性:只有正则表达式使用了表示全局检索的 "g" 标志时,该属性才会起作用, + // 当前正则匹配到字串后,下次使用同样的正则匹配该字串时,正则检查位置会发生变化 + charTypeRegObj.upperCharReg.lastIndex = 0; + charTypeRegObj.specialCharReg.lastIndex = 0; + } + + // 根据熵值计算等级 + return countLevel(pwdScore); + + function getValidatorParam(): TiPwdParams { + const params: any = validator.params; + // 默认值设定 + let minCharLength: number = 0; + let minCharTypeNumber: number = 0; + let charTypeRegObject: any = { + // 默认字串类型集合,特殊规则可在此处进行配置 + specialCharReg: /[~`!\?,.:;\-_'"\(\)\{\}\[\]\/<>@#\$%\^&\*\+\|\\=\s]/, + lowerCharReg: /[a-z]+/, + upperCharReg: /[A-Z]+/, + digitsCharReg: /[0-9]+/ + }; + + // 通过rangeSize规则获取最小长度 + const rangeSizeParams: number = params.rangeSize; + if (!Util.isUndefined(rangeSizeParams)) { + minCharLength = parseInt(rangeSizeParams[0], 10) || 0; + } + // 通过minCharType规则获取最小字符类型及其正则 + const minCharTypeParams: number = params.minCharType; + if (!Util.isUndefined(minCharTypeParams)) { + minCharTypeNumber = parseInt(minCharTypeParams[0], 10) || 0; + charTypeRegObject = minCharTypeParams[1] || charTypeRegObject; + } + + // 返回对象值 + return { + minCharLen: minCharLength, + minCharTypeNum: minCharTypeNumber, + charTypeRegObj: charTypeRegObject + }; + } + + /** + * 计算密码等级 + * 低: 0~25.5( 小于等于25 .5) + * 中: 25.5~30(大于25 .5 小于等于30) + * 高: 30~(大于30) + * @param score 密码分数 + * @returns 密码等级 + */ + function countLevel(score: number): number { + let level: number = 0; + if (score > 30) { + level = 2; + } else if (score > 25.5 && score <= 30) { + level = 1; + } else if (score >= 0 && score <= 25.5) { + level = 0; + } + level = levelHandle(minCharLen, minCharTypeNum, level); + + return level; + } + + // 密码降级处理 + function levelHandle(psdLen: number, psdType: number, level: number): number { + const type: number = getCharType(value); + let pwdLevel: number = level; + if (level > 0) { + if ((psdLen && len < psdLen) || (psdType && type < psdType)) { + pwdLevel = level - 1; + } + } + + return pwdLevel; + } + + // 获取当前密码字符种类 + function getCharType(str: string): number { + let typeNum: number = 0; + for (const key in charTypeRegObj) { + if (Object.prototype.hasOwnProperty.call(charTypeRegObj, key)) { + const regExp: RegExp = charTypeRegObj[key]; + if (regExp.test(str)) { + typeNum++; + regExp.lastIndex = 0; + } + } + } + + return typeNum; + } + } +} diff --git a/src/validation/lib/src/errorMsg.less b/src/validation/lib/src/errorMsg.less new file mode 100644 index 0000000..41dbd00 --- /dev/null +++ b/src/validation/lib/src/errorMsg.less @@ -0,0 +1,40 @@ +@import '../../../themes/basic/base-all.less'; + +ti-error-msg { + --ti-error-icon-size: var(--ti-common-size-4x); +} + +ti-error-msg { + color: var(--ti-common-color-text-gray); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + line-height: var(--ti-common-line-height-number); +} + +.ti3-error-icon-bg { + width: var(--ti-error-icon-size); + height: var(--ti-error-icon-size); + border-radius: 50%; + text-align: center; + background-color: var(--ti-common-color-error); + margin-right: var(--ti-common-space-2x); + flex-shrink: 0; // 校验文本过多时不允许挤压图标部分 + margin-top: calc( + (var(--ti-common-font-size-base) * var(--ti-common-line-height-number) - var(--ti-error-icon-size)) / 2 + ); // 左侧图标16px 与右侧文字18px 保持垂直居中 +} +.ti3-error-icon { + color: var(--ti-common-color-text-white); + font-size: calc(var(--ti-error-icon-size) * 0.75); + line-height: var(--ti-error-icon-size); +} + +.ti3-unifyvalid-error-container { + color: var(--ti-common-color-error-text); + display: block; // IE9不支持flex,故使用block代替 + margin-top: var(--ti-common-space-2x); +} +.ti3-unifyvalid-tip-error-container { + display: flex; + align-content: center; +} diff --git a/src/validation/lib/src/i18n/TiValidationWords.ts b/src/validation/lib/src/i18n/TiValidationWords.ts new file mode 100644 index 0000000..b8d7e48 --- /dev/null +++ b/src/validation/lib/src/i18n/TiValidationWords.ts @@ -0,0 +1,39 @@ +export interface TiValidationWords { + tiValid: { + errorMsg: { + required: string; + maxLength: string; + minLength: string; + rangeSize: string; + maxValue: string; + minValue: string; + rangeValue: string; + regExp: string; + contains: string; + notContains: string; + notScript: string; + equal: string; + notEqual: string; + port: string; + path: string; + email: string; + date: string; + url: string; + integer: string; + number: string; + digits: string; + ipv4: string; + ipv6: string; + password: string; + }; + message: { + rangeSize: string; + minCharType: string; + notEqualPosRev: string; + }; + passwordStrength: { + securityText: string; + levelDecArr: Array; + }; + }; +} diff --git a/src/validation/lib/src/i18n/en_US.ts b/src/validation/lib/src/i18n/en_US.ts new file mode 100644 index 0000000..043de72 --- /dev/null +++ b/src/validation/lib/src/i18n/en_US.ts @@ -0,0 +1,43 @@ +import { TiValidationWords } from './TiValidationWords'; + +export const en_US: TiValidationWords = { + tiValid: { + errorMsg: { + required: 'This field cannot be left blank.', + maxLength: 'Enter a maximum of {0} characters.', + minLength: 'Enter at least {0} characters.', + rangeSize: 'Enter {0} to {1} characters.', + maxValue: 'Enter a value less than or equal to {0}.', + minValue: 'Enter a value greater than or equal to {0}.', + rangeValue: 'Enter a value from {0} to {1}.', + regExp: 'Invalid value.', + contains: 'The value must contain the following characters: {0}.', + notContains: 'The value cannot contain the following invalid characters: {0}.', + notScript: 'The value cannot contain script tags.', + equal: 'The value must be {0}.', + notEqual: 'The value cannot be {0}.', + port: 'Enter an integer from {0} to {1}.', + path: 'Enter a valid file path.', + email: 'Enter a valid email address.', + date: 'Enter a valid date.', + url: 'Enter a valid URL.', + integer: 'Enter a valid integer.', + number: 'Enter a valid number.', + digits: 'Enter a valid number.', + ipv4: 'Enter a valid IPv4 address.', + ipv6: 'Enter a valid IPv6 address.', + password: 'Invalid password.' + }, + message: { + rangeSize: 'Must be {0} to {1} characters long.', + minCharType: + 'Must contain at least {0} of the following character types: ' + + 'letters, digits, and special characters ( `~!@#$%^&*()-_=+\\|[{}];:\'",<.>/? and spaces). ', + notEqualPosRev: 'Cannot be the username or the username spelled backwards.' + }, + passwordStrength: { + securityText: 'Password Strength:', + levelDecArr: ['Weak', 'Medium', 'Strong'] + } + } +}; diff --git a/src/validation/lib/src/i18n/es_US.ts b/src/validation/lib/src/i18n/es_US.ts new file mode 100644 index 0000000..8513ed5 --- /dev/null +++ b/src/validation/lib/src/i18n/es_US.ts @@ -0,0 +1,43 @@ +import { TiValidationWords } from './TiValidationWords'; + +export const es_US: TiValidationWords = { + tiValid: { + errorMsg: { + required: 'Este campo es obligatorio.', + maxLength: 'Ingrese {0} caracteres como máximo.', + minLength: 'Ingrese {0} caracteres como mínimo.', + rangeSize: 'Ingrese entre {0} y {1} caracteres.', + maxValue: 'Ingrese un valor inferior o igual a {0}.', + minValue: 'Ingrese un valor superior o igual a {0}.', + rangeValue: 'Ingrese un valor entre {0} y {1}.', + regExp: 'Valor no válido.', + contains: 'El valor debe contener los siguientes caracteres: {0}.', + notContains: 'El valor no puede contener los siguientes caracteres no válidos: {0}.', + notScript: 'El valor no puede contener etiquetas de scripts.', + equal: 'El valor debe ser {0}', + notEqual: 'El valor no puede ser {0}.', + port: 'Ingrese un número entero entre {0} y {1}.', + path: 'Ingrese una ruta de archivo válida.', + email: 'Ingrese una dirección de correo electrónico válida.', + date: 'Ingrese una fecha válida.', + url: 'Ingrese un URL válido.', + integer: 'Ingrese un número entero válido.', + number: 'Ingrese un número válido.', + digits: 'Ingrese un número válido.', + ipv4: 'Ingrese una dirección IPv4 válida.', + ipv6: 'Ingrese una dirección IPv6 válida.', + password: 'Contraseña no válida.' + }, + message: { + rangeSize: 'Debe contener entre {0} y {1} caracteres.', + minCharType: + 'Debe contener al menos {0} de los siguientes tipos de caracteres: ' + + 'letras, dígitos y caracteres especiales ( `~!@#$%^&*()-_=+\\|[{}];:\'",<.>/? o espacios). ', + notEqualPosRev: 'No puede ser el nombre de usuario ni el nombre de usuario escrito al revés.' + }, + passwordStrength: { + securityText: 'Nivel de seguridad:', + levelDecArr: ['Bajo', 'Medio', 'Alto'] + } + } +}; diff --git a/src/validation/lib/src/i18n/fr_FR.ts b/src/validation/lib/src/i18n/fr_FR.ts new file mode 100644 index 0000000..ff28ca7 --- /dev/null +++ b/src/validation/lib/src/i18n/fr_FR.ts @@ -0,0 +1,43 @@ +import { TiValidationWords } from './TiValidationWords'; + +export const fr_FR: TiValidationWords = { + tiValid: { + errorMsg: { + required: 'Ce champs ne peut pas être vide.', + maxLength: 'Saisissez un maximum de {0} caractères.', + minLength: 'Saisissez au moins {0} caractères.', + rangeSize: 'Saisissez les caractères {0} à {1}.', + maxValue: 'Saisissez une valeur inférieure ou égale à {0}.', + minValue: 'Saisissez une valeur supérieure ou égale à {0}.', + rangeValue: 'Saisissez une valeur comprise entre {0} et {1}.', + regExp: 'Valeur non valable.', + contains: 'La valeur doit contenir les caractères suivants : {0}.', + notContains: 'La valeur ne peut pas contenir les caractères non valables suivants : {0}.', + notScript: 'La valeur ne peut pas contenir de script tags.', + equal: "La valeur d'entrée doit être égale à {0}.", + notEqual: 'La valeur ne peut pas être {0}', + port: 'Le numéro de port est un nombre entier allant de {0} à {1}.', + path: 'Entrez un chemin de fichier valide.', + email: 'Entrez une adresse électronique valide.', + date: 'Saisissez une date valide.', + url: 'Saisissez une URL valide.', + integer: 'Saisissez un nombre entier valide.', + number: 'Saisissez un nombre valide.', + digits: 'Saisissez un nombre valide.', + ipv4: 'Saisissez une adresse IPv4 valide.', + ipv6: 'Saisissez une adresse IPv6 valide.', + password: 'Mot de passe non valide.' + }, + message: { + rangeSize: 'Doit comporter de {0} à {1} caractères.', + minCharType: + 'Doit contenir au moins {0} des types de caractères suivants: ' + + 'lettres, chiffres et caractères spéciaux ( `~!@#$%^&*()-_=+|[{}];:\'",<.>/ ? et espaces). ', + notEqualPosRev: "Il ne peut s'agir du nom d'utilisateur ou du nom d'utilisateur orthographié à l'envers." + }, + passwordStrength: { + securityText: 'Force du mot de passe :', + levelDecArr: ['Faible', 'Moyen', 'Fort'] + } + } +}; diff --git a/src/validation/lib/src/i18n/index.ts b/src/validation/lib/src/i18n/index.ts new file mode 100644 index 0000000..cf521dc --- /dev/null +++ b/src/validation/lib/src/i18n/index.ts @@ -0,0 +1,7 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; diff --git a/src/validation/lib/src/i18n/pt_BR.ts b/src/validation/lib/src/i18n/pt_BR.ts new file mode 100644 index 0000000..777e1d3 --- /dev/null +++ b/src/validation/lib/src/i18n/pt_BR.ts @@ -0,0 +1,43 @@ +import { TiValidationWords } from './TiValidationWords'; + +export const pt_BR: TiValidationWords = { + tiValid: { + errorMsg: { + required: 'Este campo não pode ser deixado em branco.', + maxLength: 'Insira um máximo de {0} caracteres.', + minLength: 'Insira pelo menos {0} caracteres.', + rangeSize: 'Insira de {0} a {1} caracteres.', + maxValue: 'Insira um valor inferior ou igual a {0}.', + minValue: 'Insira um valor superior ou igual a {0}.', + rangeValue: 'Insira um valor de {0} a {1}.', + regExp: 'Valor inválido.', + contains: 'O valor deve conter os caracteres a seguir: {0}.', + notContains: 'O valor não pode conter os caracteres a seguir: {0}.', + notScript: 'O valor não pode conter tags de scripts.', + equal: 'O valor deve ser {0}.', + notEqual: 'O valor não pode ser {0}.', + port: 'Insira um número inteiro de {0} a {1}.', + path: 'Insira um caminho de arquivo válido.', + email: 'Insira um endereço de e-mail válido.', + date: 'Insira uma data válida.', + url: 'Insira uma URL válida.', + integer: 'Insira um número inteiro válido.', + number: 'Insira um número válido.', + digits: 'Insira um número válido.', + ipv4: 'Insira um endereço IPv4 válido.', + ipv6: 'Insira um endereço IPv6 válido.', + password: 'Senha inválida.' + }, + message: { + rangeSize: 'Deve ter de {0} a {1} caracteres.', + minCharType: + 'Deve conter pelo menos {0} dos seguintes tipos de caracteres: ' + + 'letras, dígitos e caracteres especiais ( `~!@#$%^&*()-_=+\\|[{}];:\'",<.>/? e espaços). ', + notEqualPosRev: 'Não pode ser o nome de usuário escrito normalmente ou de trás para frente.' + }, + passwordStrength: { + securityText: 'Força da senha:', + levelDecArr: ['Fraca', 'Média', 'Forte'] + } + } +}; diff --git a/src/validation/lib/src/i18n/zh_CN.ts b/src/validation/lib/src/i18n/zh_CN.ts new file mode 100644 index 0000000..7f62e88 --- /dev/null +++ b/src/validation/lib/src/i18n/zh_CN.ts @@ -0,0 +1,41 @@ +import { TiValidationWords } from './TiValidationWords'; + +export const zh_CN: TiValidationWords = { + tiValid: { + errorMsg: { + required: '输入不能为空。', + maxLength: '输入长度不能超过{0}个字符。', + minLength: '输入长度不能小于{0}个字符。', + rangeSize: '输入长度范围为{0}到{1}个字符。', + maxValue: '输入值不能超过{0}。', + minValue: '输入值不能小于{0}。', + rangeValue: '输入值必须在{0}到{1}之间。', + regExp: '输入值无效。', + contains: '输入值必须包含有字符{0}。', + notContains: '输入值不能含有非法字符{0}。', + notScript: '输入值不能含有script标签。', + equal: '输入值必须等于{0}。', + notEqual: '输入值不能等于{0}。', + port: '端口号为{0}到{1}的整数。', + path: '输入值未满足路径格式要求。', + email: '输入email地址无效。', + date: '输入日期无效。', + url: '输入URL无效。', + integer: '输入值不是有效整数。', + number: '输入值不是有效数字。', + digits: '输入值不是有效数字字符。', + ipv4: '输入值不是有效IPv4地址。', + ipv6: '输入值不是有效IPv6地址。', + password: '密码输入不符合要求,请重新输入。' + }, + message: { + rangeSize: '{0}~{1}个字符。', + minCharType: '至少包含以下字符中的{0}种:大写字母、小写字母、数字、特殊字符`~!@#$%^&*()-_=+\\|[{}];:\'",<.>/? 和空格。', + notEqualPosRev: '不能与用户名或倒序的用户名相同。' + }, + passwordStrength: { + securityText: '安全程度:', + levelDecArr: ['弱', '中', '强'] + } + } +}; diff --git a/src/validation/lib/src/pending-state.html b/src/validation/lib/src/pending-state.html new file mode 100644 index 0000000..94040cf --- /dev/null +++ b/src/validation/lib/src/pending-state.html @@ -0,0 +1 @@ + diff --git a/src/validation/lib/src/pending-state.less b/src/validation/lib/src/pending-state.less new file mode 100644 index 0000000..40f04c2 --- /dev/null +++ b/src/validation/lib/src/pending-state.less @@ -0,0 +1,37 @@ +:host { + --ti-pending-state-container-height: var(--ti-common-size-7x); + --ti-pending-state-loading-icon-top: calc( + (var(--ti-pending-state-container-height) - var(--ti-common-border-weight-normal) * 2 - var(--ti-common-size-3x)) / 2 + ); // 7px (container高度 - 上下边框 - loading默认高度) / 2 + --ti-pending-state-loading-icon-left: calc((var(--ti-common-space-10) + var(--ti-common-size-3x)) * -1); // -22px 右间距 + loading默认宽度 +} + +:host.ti3-validation-pending-container { + position: relative; // 用于给ti-loading定位,但是宽度要为0,不能占位置,免得影响input后面的元素位置 + vertical-align: top; + display: inline-block; + height: var(--ti-pending-state-container-height); + width: 0; +} + +.ti3-validation-pending-loading { + position: absolute; // 定位,脱离文档流 + top: var(--ti-pending-state-loading-icon-top); + left: var(--ti-pending-state-loading-icon-left); +} + +::ng-deep [tiText].ti3-text-input-show-clear + .ti3-validation-pending-container .ti3-validation-pending-loading { + left: calc( + (var(--ti-common-space-10) + var(--ti-common-size-3x) + var(--ti-common-size-4x)) * -1 + ); // -38px 右间距 + loading默认宽度 + clear默认宽度 +} + +::ng-deep [tiText].ti3-text-input-show-pending { + padding-right: calc(var(--ti-common-space-10) * 2 + var(--ti-common-size-3x)) !important; // 32px 左右间距 + loading默认宽度 +} + +::ng-deep [tiText].ti3-text-input-show-pending.ti3-text-input-show-clear { + padding-right: calc( + var(--ti-common-space-10) * 2 + var(--ti-common-size-3x) + var(--ti-common-size-4x) + ) !important; // 48px 左右间距 + loading默认宽度 + clear默认宽度 +} diff --git a/src/validation/lib/src/pwdMsg.less b/src/validation/lib/src/pwdMsg.less new file mode 100644 index 0000000..19a56b4 --- /dev/null +++ b/src/validation/lib/src/pwdMsg.less @@ -0,0 +1,161 @@ +@import '../../../themes/basic/base-all.less'; + +:host { + --ti-valid-pwd-tip-line-height: calc(var(--ti-common-font-size-base) * var(--ti-common-line-height-number)); + --ti-valid-pwd-status-icon-size: var(--ti-common-size-4x); + --ti-valid-pwd-status-null-border-weight: var(--ti-common-border-weight-normal); + --ti-valid-pwd-status-null-border-color: rgba(255, 255, 255, 0.1); + --ti-valid-pwd-status-null-size: calc(var(--ti-valid-pwd-status-icon-size) - var(--ti-valid-pwd-status-null-border-weight) * 2); + --ti-valid-pwd-space: var(--ti-common-space-2x); + --ti-valid-pwd-level-bar-height: 2px; + --ti-valid-pwd-level-bar-item-width: var(--ti-common-size-15x); + --ti-valid-pwd-circle-size: 6px; +} + +.ti3-valid-pwd-tip-container { + line-height: var(--ti-valid-pwd-tip-line-height); + font-size: var(--ti-common-font-size-base); + font-weight: var(--ti-common-font-weight-4); + color: var(--ti-common-color-text-gray); +} + +.ti3-valid-pwd-tip-item { + margin-bottom: var(--ti-common-space-3x); + .clearfix(); +} +.ti3-valid-pwd-tip-status-null { + width: var(--ti-valid-pwd-status-null-size); + height: var(--ti-valid-pwd-status-null-size); + float: left; + margin-right: var(--ti-valid-pwd-space); + border: var(--ti-valid-pwd-status-null-border-weight) var(--ti-common-border-style-solid) var(--ti-valid-pwd-status-null-border-color); + border-radius: var(--ti-valid-pwd-status-null-size); + .box-sizing(content-box); + position: relative; + top: calc((var(--ti-valid-pwd-tip-line-height) - var(--ti-valid-pwd-status-icon-size)) / 2); + // 内嵌圆圈 + &:after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: var(--ti-valid-pwd-circle-size); + height: var(--ti-valid-pwd-circle-size); + background-color: var(--ti-common-color-icon-darkbg-normal); + border-radius: var(--ti-common-border-radius-3); + .box-sizing(border-box); + } +} + +.ti3-valid-pwd-tip-success { + .ti3-valid-pwd-tip(var(--ti-common-color-success)); +} +.ti3-valid-pwd-tip-error { + .ti3-valid-pwd-tip(var(--ti-common-color-error)); +} + +.ti3-valid-pwd-tip-msg { + float: left; + width: calc(100% - var(--ti-valid-pwd-status-icon-size) - var(--ti-valid-pwd-space)); +} +.ti3-valid-pwd-tip-security-text { + float: left; + margin-right: var(--ti-common-space-3x); +} +.ti3-valid-pwd-level-bars { + margin-right: var(--ti-common-space-3x); + float: left; + margin-top: calc(var(--ti-valid-pwd-tip-line-height) / 2); + &.ti3-valid-pwd-level-strong { + .active { + .ti3-valid-pwd-level-active(var(--ti-common-color-success)); + } + } + &.ti3-valid-pwd-level-medium { + .active { + .ti3-valid-pwd-level-active(var(--ti-common-color-warn)); + } + } + &.ti3-valid-pwd-level-weak { + .active { + .ti3-valid-pwd-level-active(var(--ti-common-color-error)); + } + } +} + +.ti3-valid-pwd-level-bar { + float: left; + width: calc(var(--ti-valid-pwd-level-bar-item-width) + var(--ti-valid-pwd-circle-size)); + height: var(--ti-valid-pwd-level-bar-height); + background: var(--ti-common-color-text-weaken); + position: relative; + &:first-child { + width: calc(var(--ti-valid-pwd-level-bar-item-width) + var(--ti-valid-pwd-circle-size) * 2); + &:before { + .ti3-valid-pwd-level-circle(); + left: 0; + } + } + + &:after { + .ti3-valid-pwd-level-circle(); + right: 0; + } +} + +.ti3-valid-pwd-level { + margin-left: calc(var(--ti-valid-pwd-status-icon-size) + var(--ti-valid-pwd-space)); + margin-top: var(--ti-common-space-4x); + color: var(--ti-common-color-text-gray); + .clearfix(); +} +.ti3-valid-pwd-tip-security-status { + float: left; + height: var(--ti-common-size-5x); //设置该字段高度为20px,确保其换行状态下显示状态切换时不会发生跳变 +} + +// 提示图标样式 +.ti3-valid-pwd-tip (@bgColor) { + width: var(--ti-valid-pwd-status-icon-size); + height: var(--ti-valid-pwd-status-icon-size); + font-size: var(--ti-valid-pwd-status-icon-size); + color: var(--ti-common-color-text-gray); + float: left; + margin-right: var(--ti-valid-pwd-space); + border-radius: var(--ti-common-border-radius-3); + margin-top: calc((var(--ti-valid-pwd-tip-line-height) - var(--ti-valid-pwd-status-icon-size)) / 2); + background-color: @bgColor; + text-align: center; + ti-icon[local] { + font-size: calc(var(--ti-valid-pwd-status-icon-size) * 0.75); + vertical-align: top; + line-height: var(--ti-valid-pwd-status-icon-size); + } +} + +// 密码等级激活样式 +.ti3-valid-pwd-level-active (@bgColor) { + background: @bgColor; + &:first-child { + &:before { + background-color: @bgColor; + } + } + + &:after { + background-color: @bgColor; + } +} + +// 密码等级圆圈 +.ti3-valid-pwd-level-circle { + content: ''; + position: absolute; + top: calc((var(--ti-valid-pwd-level-bar-height) - var(--ti-valid-pwd-circle-size)) / 2); + width: var(--ti-valid-pwd-circle-size); + height: var(--ti-valid-pwd-circle-size); + background-color: var(--ti-common-color-text-weaken); + border-radius: var(--ti-common-border-radius-3); + .box-sizing(border-box); +} diff --git a/src/validation/lib/src/validators/TiValidators.ts b/src/validation/lib/src/validators/TiValidators.ts new file mode 100644 index 0000000..fc464a3 --- /dev/null +++ b/src/validation/lib/src/validators/TiValidators.ts @@ -0,0 +1,792 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +/* eslint-disable no-eq-null */ +/* eslint-disable eqeqeq */ +import { AbstractControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'; +import { Util } from '@opentiny/ng-utils'; +import { TiPwdConfig } from '../checkHandle/TiPwdConfig'; +import { TiValidationCheckConfig } from '../TiValidationInterface'; + +// 1:比较值高的; 0:比较值相等 ;-1:比较值低的 +type compareResult = 1 | 0 | -1; +// 下面注释,可以避免编译lib时正则报错。原理未知,副作用未知。 +// @dynamic +/** + * Tiny封装的常用校验规则及校验方法 + * + * ### 规则使用 + * **提供了两种使用方式:** + * + * 1.静态方法:通过TiValidators.XXX(规则名)在JS中定义 + * + * 2.指令方式:对应规则的指令名为tiXXX(规则名首字母大写),如required规则的指令名为 tiRequired + * + * **支持的校验规则如下:** + * + * | 规则名称 | 配置参数意义/类型 | 规则描述 | + * | -------- | :----- | :---- | + * | required | 无 | 为空校验 | + * | maxLength | 最大字符长度/number | 字符长度最大值校验 | + * | minLength | 最小字符长度/number | 字符长度最小值校验 | + * | rangeSize | 最小长度限制/number
    最大长度限制/number | 字符长度大小区间校验 | + * | integer | 无 | 整数校验 | + * | number | 无 | 数字校验 | + * | digits | 无 | 自然数校验 | + * | maxValue | 最大数值/number | 数字最大数值校验 | + * | minValue | 最小数值/number | 数字最小数值校验 | + * | rangeValue | 最小数值限制/number
    最大数值限制/number | 数字大小区间校验 | + * | bigInteger | 无 | 校验字符串是否为整数,不支持科学计数法 | + * | bigNumber | 无 | 校验字符串是否为数字,不支持科学计数法 | + * | bigDigits | 无 | 校验字符串是否为自然数,不支持科学计数法 | + * | maxValueByString | 最大数值/string | 最大数值校验(支持任意大小和精度的数字),不支持科学计数法 | + * | minValueByString | 最小数值/string | 最小数值校验(支持任意大小和精度的数字),不支持科学计数法 | + * | rangeValueByString | 最小数值/string | 数值范围校验(支持任意大小和精度的数字),不支持科学计数法 | + * | regExp | 正则表达式参数/RegExp|string(不包括正则表达式头尾标识符'^(?:'、')$') | 正则校验 | + * | email | 无 | 邮箱校验 | + * | contains | 包含的内容/(string/number) | 包含校验 | + * | notContains | 不包含的内容/(string/number) | 不包含校验 | + * | equal | 相等的内容/(string/number/boolean ) | 相等校验 | + * | notEqual | 不相等的内容/(string/number) | 不相等校验 | + * | notScript | 无 | 包含script标签校验 | + * | port | 无 | 端口号校验,范围为0~65535 | + * | date | 无 | 日期类型校验 | + * | url | 无 | url校验 | + * | ipv4 | 无 | ipv4校验 | + * | ipv6 | 无 | ipv6校验 | + * | minCharType | 1. 符合要求的字符种类/number;
    2. 字符集对象类型(可选)/{string:RegExp}。
    默认的字符种类分别为:大写字母、小写字母、数字、特殊字符`~!@#$%^&*()-_=+\\\|[{}];:\'\",<.>/? 和空格 | 符合最小字符种类校验,默认情况为至少包含2种字符类型`。 | + * | notEqualPosRev | 需要比对的表单formControl对象获取函数/ () => AbstractControl | 不能和表单对象的正序或倒序相同 | + * | password | 密码校验各项规则参数/对象形式 | 密码校验 | + * + * + * **password规则的参数类型** + * + * { + * rangeSize?: [number, number], + * minCharType?: [bumber, { + * digitsCharReg: RegExp, + * specialCharReg: RegExp, + * lowerCharReg: RegExp, + * upperCharReg: RegExp}], + * notEqualPosRev: () => AbstractControl + * } + */ +export class TiValidators { + private static readonly EMAIL_REGEXP: RegExp = + /^((([A-Za-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([A-Za-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([A-Za-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([A-Za-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([A-Za-z]|\d|-|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([A-Za-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([A-Za-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([A-Za-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([A-Za-z]|\d|-|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([A-Za-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/; + private static readonly SCRIPT_REGEXP: RegExp = /<+\/?[Ss][Cc][Rr][Ii][Pp][Tt] *.*>*/; + private static readonly DIGITS_REGEXP: RegExp = /^\d+$/; + private static readonly PORT_RANGE: Array = [0, 65535]; + private static readonly INTEGER_REGEXP: RegExp = /^-?\d+$/; + private static readonly NUMBER_REGEXP: RegExp = /^(?:-?\d+|[+-]?[\d]+([\.][\d]+)?([Ee][+-]?[\d]+)?|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/; // 支持科学计数法 + private static readonly BIG_NUMBER_REGEXP: RegExp = /^[-]?([1-9]\d*\.\d+$|0\.\d+$|[1-9]\d*$|0$)/; + // 采用自研的ipv4正则表达式 + private static readonly IPV4_REGEXP: RegExp = + /^(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))$/i; + // 采用自研的ipv6正则表达式 + private static readonly IPV6_REGEXP: RegExp = + /^(((([\da-f]{1,4}):){7}([\da-f]{1,4}))|(((([\da-f]{1,4}):){1,7}:)|((([\da-f]{1,4}):){6}:([\da-f]{1,4}))|((([\da-f]{1,4}):){5}:(([\da-f]{1,4}):)?([\da-f]{1,4}))|((([\da-f]{1,4}):){4}:(([\da-f]{1,4}):){0,2}([\da-f]{1,4}))|((([\da-f]{1,4}):){3}:(([\da-f]{1,4}):){0,3}([\da-f]{1,4}))|((([\da-f]{1,4}):){2}:(([\da-f]{1,4}):){0,4}([\da-f]{1,4}))|((([\da-f]{1,4}):){1}:(([\da-f]{1,4}):){0,5}([\da-f]{1,4}))|(::(([\da-f]{1,4}):){0,6}([\da-f]{1,4}))|(::([\da-f]{1,4})?))|(((([\da-f]{1,4}):){6}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){5}:(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){4}:(([\da-f]{1,4}):)?(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){3}:(([\da-f]{1,4}):){0,2}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|((([\da-f]{1,4}):){2}:(([\da-f]{1,4}):){0,3}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|(([\da-f]{1,4})::(([\da-f]{1,4}):){0,4}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))|(::(([\da-f]{1,4}):){0,5}(((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5]))\.){3}((1?[1-9]?\d)|(10\d)|(2[0-4]\d)|(25[0-5])))))$/i; + private static isEmptyInputValue(value: any): boolean { + // we don't check for string here so it also works with arrays + return value == null || value.length === 0; + } + + // 字符串大小比较 + private static comparePlaces(aPlaces: string, bPlaces: string, type: 'integer' | 'decimal'): compareResult { + const len: number = Math.max(aPlaces.length, bPlaces.length); + const aVal: string = type === 'integer' ? aPlaces.padStart(len, '0') : aPlaces.padEnd(len, '0'); + const bVal: string = type === 'integer' ? bPlaces.padStart(len, '0') : bPlaces.padEnd(len, '0'); + if (aVal === bVal) { + return 0; + } + return [aVal, bVal].sort()[0] === aVal ? -1 : 1; + } + + private static realNumberStringCondition(aInfo, bInfo): compareResult { + function zeroDeal(aInfo, bInfo) { + if (aInfo.isZero && bInfo.isZero) { + return 0; + } else if (aInfo.isZero) { + return bInfo.isNegative ? 1 : -1; + } else { + return aInfo.isNegative ? -1 : 1; + } + } + + function zeroCompare(aInfo, bInfo) { + if (aInfo.isNegative === bInfo.isNegative) { + let result: compareResult = TiValidators.comparePlaces(aInfo.integerPlaces, bInfo.integerPlaces, 'integer'); + if (result === 0) { + result = TiValidators.comparePlaces(aInfo.decimalPlaces, bInfo.decimalPlaces, 'decimal'); + } + // To avoid ambiguity, do not return -0 + if (result === 0) { + return 0; + } else { + return (result * (aInfo.isNegative ? -1 : 1)) as 1 | -1; + } + } else { + return aInfo.isNegative ? -1 : 1; + } + } + + if (aInfo.isZero || bInfo.isZero) { + return zeroDeal(aInfo, bInfo); + } else { + return zeroCompare(aInfo, bInfo); + } + } + // 返回字符串的 整型,十进制 ,字符串第一个值,是否存在0 + private static getInfo(value: string) { + const num: string = ['+', '-'].includes(value[0]) ? value.slice(1) : value; + const index: number = num.indexOf('.'); + const integerPlaces: string = index > -1 ? num.slice(0, index) : num; + const decimalPlaces: string = index > -1 ? num.slice(index + 1) : '0'; + return { + integerPlaces, + decimalPlaces, + isNegative: value[0] === '-', + isZero: Array.from([...integerPlaces, ...decimalPlaces]).every((e) => e === '0') + }; + } + + private static realNumberStringCompareTo = function (a: string, b: string): compareResult { + // a字符串的拆分 + // tslint:disable-next-line: typedef + const aInfo = TiValidators.getInfo(a); + // b字符串的拆分 + // tslint:disable-next-line: typedef + const bInfo = TiValidators.getInfo(b); + + // tslint:disable-next-line: typedef + return TiValidators.realNumberStringCondition(aInfo, bInfo); + }; + + /** + * @ignore + */ + public static required(control: AbstractControl): ValidationErrors | null { + return Validators.required(control); + } + /** + * @ignore + */ + public static maxLength(maxLength: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const length: number = control.value ? control.value.length : 0; + + return length > maxLength ? { maxLength: { requiredLength: maxLength, actualLength: length } } : null; + }; + } + /** + * @ignore + */ + public static minLength(minLength: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + const length: number = control.value ? control.value.length : 0; + + return length < minLength ? { minLength: { requiredLength: minLength, actualLength: length } } : null; + }; + } + /** + * @ignore + */ + public static rangeSize(minLength: number, maxLength: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + const length: number = control.value ? control.value.length : 0; + + return length > maxLength || length < minLength + ? { + rangeSize: { + requiredMinLength: minLength, + requiredMaxLength: maxLength, + actualLength: length + } + } + : null; + }; + } + /** + * @ignore + */ + public static maxValue(max: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(max)) { + return null; // don't validate empty values to allow optional controls + } + const value: number = parseFloat(control.value); + + // Controls with NaN values after parsing should be treated as not having a + // maximum, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-max + return !isNaN(value) && value > max ? { maxValue: { requiredMaxValue: max, actual: control.value } } : null; + }; + } + /** + * @ignore + */ + public static minValue(min: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(min)) { + return null; // don't validate empty values to allow optional controls + } + const value: number = parseFloat(control.value); + + // Controls with NaN values after parsing should be treated as not having a + // maximum, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-max + return !isNaN(value) && value < min ? { minValue: { requiredMinValue: min, actual: control.value } } : null; + }; + } + /** + * @ignore + */ + public static maxValueByString(max: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(max)) { + return null; // don't validate empty values to allow optional controls + } + const value: any = control.value; + const err: ValidationErrors = { + maxValue: { requiredMaxValue: max, actual: value } + }; + if (typeof max !== 'string' || !TiValidators.BIG_NUMBER_REGEXP.test(max)) { + return err; + } + if (typeof value !== 'string' || !TiValidators.BIG_NUMBER_REGEXP.test(value)) { + return null; + } + + return TiValidators.realNumberStringCompareTo(value, max) > 0 ? err : null; + }; + } + /** + * @ignore + */ + public static minValueByString(min: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(min)) { + return null; // don't validate empty values to allow optional controls + } + const value: any = control.value; + const err: ValidationErrors = { + minValue: { requiredMinValue: min, actual: control.value } + }; + if (typeof min !== 'string' || !TiValidators.BIG_NUMBER_REGEXP.test(min)) { + return err; + } + if (typeof value !== 'string' || !TiValidators.BIG_NUMBER_REGEXP.test(value)) { + return null; + } + + return TiValidators.realNumberStringCompareTo(min, value) > 0 ? err : null; + }; + } + /** + * @ignore + */ + public static rangeValue(min: number, max: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(min) || TiValidators.isEmptyInputValue(max)) { + return null; // don't validate empty values to allow optional controls + } + const value: number = parseFloat(control.value); + + // Controls with NaN values after parsing should be treated as not having a + // maximum, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-max + return !isNaN(value) && (value < min || value > max) + ? { + rangeValue: { + requiredMinValue: min, + requiredMaxValue: max, + actual: control.value + } + } + : null; + }; + } + /** + * @ignore + */ + public static rangeValueByString(min: string, max: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(min) || TiValidators.isEmptyInputValue(max)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.minValueByString(min)(control) || TiValidators.maxValueByString(max)(control); + }; + } + /** + * @ignore + */ + public static regExp(pattern: string | RegExp): ValidatorFn { + if (!pattern) { + return Validators.nullValidator; + } + let regex: RegExp; + let regexStr: string; + if (typeof pattern === 'string') { + regexStr = ''; + + if (pattern.charAt(0) !== '^') { + regexStr += '^'; + } + + regexStr += pattern; + + if (pattern.charAt(pattern.length - 1) !== '$') { + regexStr += '$'; + } + + regex = new RegExp(regexStr); + } else { + regexStr = pattern.toString(); + regex = pattern; + } + + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + const value: string = control.value; + + return regex.test(value) ? null : { regExp: { requiredPattern: regexStr, actualValue: value } }; + }; + } + /** + * @ignore + */ + public static email(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.EMAIL_REGEXP.test(control.value) ? null : { email: true }; + } + /** + * @ignore + */ + public static contains(contain: string | number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(contain)) { + return null; // don't validate empty values to allow optional controls + } + + return control.value.indexOf(contain) === -1 + ? { + contains: { requiredContains: contain, actualValue: control.value } + } + : null; + }; + } + /** + * @ignore + */ + public static notContains(contain: string | number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(contain)) { + return null; // don't validate empty values to allow optional controls + } + + return control.value.indexOf(contain) !== -1 + ? { + notContains: { + requiredNotContains: contain, + actualValue: control.value + } + } + : null; + }; + } + /** + * @ignore + */ + public static equal(equal: string | number | boolean): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(equal)) { + return null; // don't validate empty values to allow optional controls + } + + return control.value !== equal ? { equal: { requiredEqual: equal, actualValue: control.value } } : null; + }; + } + /** + * @ignore + */ + public static notEqual(equal: string | number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(equal)) { + return null; // don't validate empty values to allow optional controls + } + + return control.value === equal ? { notEqual: { requiredNotEqual: equal, actualValue: control.value } } : null; + }; + } + /** + * @ignore + */ + public static notScript(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.SCRIPT_REGEXP.test(control.value) ? { notScript: true } : null; + } + /** + * @ignore + */ + public static port(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.DIGITS_REGEXP.test(control.value) && + control.value >= TiValidators.PORT_RANGE[0] && + control.value <= TiValidators.PORT_RANGE[1] + ? null + : { + port: { + min: TiValidators.PORT_RANGE[0], + max: TiValidators.PORT_RANGE[1] + } + }; + } + /** + * @ignore + */ + public static date(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return !/Invalid|NaN/.test(new Date(control.value).toString()) ? null : { date: true }; + } + /** + * @ignore + */ + public static url(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + try { + new URL(control.value); + return null; + } catch (e) { + return { url: true }; + } + } + /** + * @ignore + */ + public static integer(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.INTEGER_REGEXP.test(control.value) ? null : { integer: true }; + } + /** + * @ignore + */ + public static number(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.NUMBER_REGEXP.test(control.value) ? null : { number: true }; + } + /** + * @ignore + */ + public static digits(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.DIGITS_REGEXP.test(control.value) ? null : { digits: true }; + } + /** + * @ignore + */ + public static bigInteger(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.INTEGER_REGEXP.test(control.value) && + TiValidators.BIG_NUMBER_REGEXP.test(control.value) && + typeof control.value === 'string' + ? null + : { integer: true }; + } + /** + * @ignore + */ + public static bigNumber(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.NUMBER_REGEXP.test(control.value) && + TiValidators.BIG_NUMBER_REGEXP.test(control.value) && + typeof control.value === 'string' + ? null + : { number: true }; + } + /** + * @ignore + */ + public static bigDigits(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.DIGITS_REGEXP.test(control.value) && + TiValidators.BIG_NUMBER_REGEXP.test(control.value) && + typeof control.value === 'string' + ? null + : { digits: true }; + } + /** + * @ignore + */ + public static ipv4(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.IPV4_REGEXP.test(control.value) ? null : { ipv4: true }; + } + /** + * @ignore + */ + public static ipv6(control: AbstractControl): ValidationErrors | null { + if (TiValidators.isEmptyInputValue(control.value)) { + return null; // don't validate empty values to allow optional controls + } + + return TiValidators.IPV6_REGEXP.test(control.value) ? null : { ipv6: true }; + } + + /** + * 正则表达式转为字符串并首尾去除“/”符号,为添加g修饰符做准备 + */ + private static regExpToString(regExp: RegExp): string { + if (typeof regExp !== 'object') { + return ''; + } + const regExpStr: string = String(regExp); + + return regExpStr.substring(1, regExpStr.length - 1); + } + + /** + * @ignore + * 密码字符类型最小种类校验 + * 用于密码校验,其规则如下: + * 口令必须包含且只能包含如下至少两种字符的组合: + * -至少一个小写字母; + * -至少一个大写字母; + * -至少一个数字; + * -至少一个特殊字符:`~!@#$%^&*()-_=+\|[{}];:'",<.>/?和空格 + * @param num 字符类型最小个数 + * @param charTypeRegObj 字符类型校验正则表达式,涉及到强度计算,参考defaults中定义 + */ + + public static minCharType(num: number, charTypeRegObj?: any): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || TiValidators.isEmptyInputValue(num) || num === 0) { + return null; // don't validate empty values to allow optional controls + } + // 各类字符校验正则表达式集合,默认使用pwdConfig中的默认配置 + const charRegObj: any = typeof charTypeRegObj === 'object' ? charTypeRegObj : TiPwdConfig.pwdCharTypeRegObj; + + // 校验输入是否符合单条校验字符规则,并记录符合的单条规则个数 + let checkValidNum: number = 0; // 校验通过结果个数 + let replacedValue: string = control.value; // 正则替换后的value值 + + for (const key in charRegObj) { + if (Object.prototype.hasOwnProperty.call(charRegObj, key)) { + const regExp: RegExp = charRegObj[key]; + const checkResult = regExp.test(control.value); + if (checkResult) { + checkValidNum++; + // 使用全局替换方式,用于替换字串中所有符合正则规则的字符 + replacedValue = regExp.global + ? replacedValue.replace(regExp, '') + : replacedValue.replace(new RegExp(TiValidators.regExpToString(regExp), 'g'), ''); + } + } + } + // 替换后的值非空情况:代表值中存在非法字符 + return replacedValue.length > 0 || checkValidNum < num ? { minCharType: true } : null; + }; + } + + private static notEqualPosRevValue(value: string, refreValue: string): any { + return value.toLowerCase() !== refreValue.split('').reverse().join('').toLowerCase() && value.toLowerCase() !== refreValue.toLowerCase() + ? null + : { notEqualPosRev: true }; + } + + /** + * @ignore + * 用于密码校验:密码不能和用户名或其倒序相同 + * @param getControlFn 获取对比的表单元素formControl函数 + */ + public static notEqualPosRev(getControlFn: () => AbstractControl): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (TiValidators.isEmptyInputValue(control.value) || Util.isUndefined(getControlFn && getControlFn())) { + return null; // don't validate empty values to allow optional controls + } + const value: string = control.value; + const refreValue: string = getControlFn().value; + + // 使用如下方式会出现卡顿的情况: + // 对比formControl value修改时,需要同步修改该表单的状态,否则会存在下次focus该元素或表单整体校验时 + // 该元素校验结果依然是上次的结果 + + return TiValidators.notEqualPosRevValue(value, refreValue); + }; + } + + private static _executeValidators(control: AbstractControl, validators: Array): Array { + return validators.map((v: any) => v(control)); + } + private static _mergeErrorsPwd(arrayOfErrors: Array): ValidationErrors | null { + const res: { [key: string]: any } = arrayOfErrors.reduce((resource: ValidationErrors | null, errors: ValidationErrors | null) => { + return errors != null ? { ...resource, ...errors } : resource; + }, {}); + + return Object.keys(res).length === 0 ? null : { password: true, ...res }; + } + + /** + * @ignore + * 密码校验规则 + * @param pValue 规则参数:{ruleKey: param} + */ + public static password(pValue: { + rangeSize?: [number, number]; + minCharType?: [number, any]; + notEqualPosRev?(): AbstractControl; + }): ValidatorFn { + const rangeSizeParamArr: Array = pValue.rangeSize || TiPwdConfig.pwdValidator.params.rangeSize; + const minCharTypeParamArr: Array = pValue.minCharType || TiPwdConfig.pwdValidator.params.minCharType; + if (Util.isUndefined(minCharTypeParamArr[1])) { + minCharTypeParamArr[1] = TiPwdConfig.pwdCharTypeRegObj; + } + const validatorComposeArr: Array = [ + TiValidators.required, + TiValidators.rangeSize(rangeSizeParamArr[0], rangeSizeParamArr[1]), + TiValidators.minCharType(minCharTypeParamArr[0], minCharTypeParamArr[1]), + TiValidators.notEqualPosRev(pValue.notEqualPosRev) + ]; + + // 由于错误提示为password专有提示,需要自定义password error,而compose方法不能自定义,因此此处未使用compose + // 此外,各子规则的错误依然需要保留,用于规则提示信息 + return (control: AbstractControl): any => TiValidators._mergeErrorsPwd(TiValidators._executeValidators(control, validatorComposeArr)); + } + + private static checkErrors( + form: FormGroup, + config: TiValidationCheckConfig, + errorsFlatted: boolean, + ignoreForms: string | string[], + updateValueAndValidityConfig: TiValidationCheckConfig + ) { + let errors = form.errors ? { ...form.errors } : {}; + + for (const key in form.controls) { + if (Object.prototype.hasOwnProperty.call(form.controls, key)) { + const control: AbstractControl | any = form.controls[key]; + if (control.controls) { + // 嵌套的FormArray,FormGroup + const checkedErrors: any = TiValidators.check(control, config); + errors = TiValidators.getErrors(errors, checkedErrors, errorsFlatted, key); + } else if (control.tiGroup && !control.disabled) { + // 类似ipsection组件场景 FormControl嵌套FormGroup 表单禁用时不验证 + const checkedErrors: any = TiValidators.check(control.tiGroup, config); + errors = TiValidators.getErrors(errors, checkedErrors, errorsFlatted, key); + } else { + // 配置忽略校验的表单元素不做处理 + if (ignoreForms && ignoreForms.indexOf(key) !== -1) { + continue; + } + + control.markAsTouched(); + // 由于表单间可能有关联关系(如密码校验的notEqualPosRev,该关联关系需要对单个表单元素再次校验 + control.updateValueAndValidity(updateValueAndValidityConfig); + // 读取errors信息 + if (control.errors !== null) { + errors[key] = control.errors; + } + } + } + } + return errors; + } + + // 该方法用于整体校验:由于表单未foucs时,即使错误也不会显示红框, + // 而整体校验时需要将错误的表单边框标红显示,因此需要显示调用整体校验方法 + /** + * 该方法用于整体校验 + * + * 参数form:需要校验的表单族 + * + * 参数config:可选 表单族的配置 + * + * 返回:表单错误信息 + */ + public static check(form: FormGroup, config?: TiValidationCheckConfig): ValidationErrors | null { + let ignoreForms: Array; // 忽略校验的表单元素 + let updateValueAndValidityConfig: TiValidationCheckConfig; // updateValueAndValidity方法的参数 + let errorsFlatted: boolean; // 错误信息是否扁平化 + + if (config) { + ignoreForms = config.ignoreNames; + updateValueAndValidityConfig = { + onlySelf: config.onlySelf, + emitEvent: config.emitEvent + }; + errorsFlatted = config.errorsFlatted; + } + + let errors = TiValidators.checkErrors(form, config, errorsFlatted, ignoreForms, updateValueAndValidityConfig); + // 无errors信息时,设置为null,方便业务处理 + if (JSON.stringify(errors) === '{}') { + errors = null; + } + + return errors; + } + + private static getErrors(errors: any, checkedErrors: any, flatten: boolean, key: string): any { + if (checkedErrors === null) { + return errors; + } + + let result: any = { ...errors }; + + if (flatten) { + result = { ...result, ...checkedErrors }; + } else { + result[key] = checkedErrors; + } + + return result; + } +} diff --git a/src/validation/lib/src/validators/directives/BaseValidator.ts b/src/validation/lib/src/validators/directives/BaseValidator.ts new file mode 100644 index 0000000..aed3be3 --- /dev/null +++ b/src/validation/lib/src/validators/directives/BaseValidator.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, forwardRef, OnChanges, SimpleChanges, Type } from '@angular/core'; +import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn, Validators } from '@angular/forms'; +import { TiValidators } from '../TiValidators'; +import { Util } from '@opentiny/ng-utils'; +// 下面注释,是为了避免lib编译错误。具体原理未知,副作用未知。 +// @dynamic +/** + * @ignore + */ +@Directive({ selector: '[BaseValidator]' }) +export class BaseValidator implements Validator, OnChanges { + public validatorStr: string; + public baseValue: string; // 需要传参的校验规则的参数 + public enabled: boolean = true; // 无需传参的校验规则是否开启校验 + private _validator: ValidatorFn = Validators.nullValidator; + private _onChange?: () => void; + + /** + * 获取各directive中的参数配置 + * @param validatorClass 校验实现类 + * @param name 校验指令名:由于该方法在类外部调用,无法使用BaseValidator中的参数,因此需要传入 + */ + public static getDirectiveConfig(validatorClass: Type, name: string): Directive { + return { + selector: `[${name}][formControlName],[${name}][formControl],[${name}][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => validatorClass), + multi: true + } + ] + }; + } + public static getValidatorStr(name: string): any { + return name.replace(/ti/, '').replace(/^\S/, (s: string) => { + return s.toLowerCase(); + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['baseValue'] || changes['enabled']) { + this._createValidator(); + if (this._onChange) { + this._onChange(); + } + } + } + + validate(c: AbstractControl): ValidationErrors | null { + return this.enabled === false || (this.enabled as any) === 'false' ? null : this._validator(c); + } + + registerOnValidatorChange(fn: () => void): void { + this._onChange = fn; + } + + private _createValidator(): void { + const validatorStr: string = this.validatorStr; + const validatorParam: string = this.baseValue; + if (Util.isArray(validatorParam)) { + this._validator = TiValidators[validatorStr].apply(null, validatorParam); + } else if (!Util.isUndefined(validatorParam)) { + this._validator = TiValidators[validatorStr](validatorParam); + } else { + this._validator = TiValidators[validatorStr]; + } + } +} diff --git a/src/validation/lib/src/validators/directives/BigDigitsValidatorDirective.ts b/src/validation/lib/src/validators/directives/BigDigitsValidatorDirective.ts new file mode 100644 index 0000000..c9828e4 --- /dev/null +++ b/src/validation/lib/src/validators/directives/BigDigitsValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; + +@Directive({ + selector: `[tiBigDigits][formControlName],[tiBigDigits][formControl],[tiBigDigits][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => BigDigitsValidatorDirective), + multi: true + } + ] +}) +export class BigDigitsValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiBigDigits'; + @Input(BigDigitsValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(BigDigitsValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/BigIntegerValidatorDirective.ts b/src/validation/lib/src/validators/directives/BigIntegerValidatorDirective.ts new file mode 100644 index 0000000..2a13b3a --- /dev/null +++ b/src/validation/lib/src/validators/directives/BigIntegerValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; + +@Directive({ + selector: `[tiBigInteger][formControlName],[tiBigInteger][formControl],[tiBigInteger][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => BigIntegerValidatorDirective), + multi: true + } + ] +}) +export class BigIntegerValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiBigInteger'; + @Input(BigIntegerValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(BigIntegerValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/BigNumberValidatorDirective.ts b/src/validation/lib/src/validators/directives/BigNumberValidatorDirective.ts new file mode 100644 index 0000000..5c22423 --- /dev/null +++ b/src/validation/lib/src/validators/directives/BigNumberValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; + +@Directive({ + selector: `[tiBigNumber][formControlName][tiValidation],[tiBigNumber][formControl][tiValidation],[tiBigNumber][ngModel][tiValidation]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => BigNumberValidatorDirective), + multi: true + } + ] +}) +export class BigNumberValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiBigNumber'; + @Input(BigNumberValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(BigNumberValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/ContainsValidatorDirective.ts b/src/validation/lib/src/validators/directives/ContainsValidatorDirective.ts new file mode 100644 index 0000000..a3c7c56 --- /dev/null +++ b/src/validation/lib/src/validators/directives/ContainsValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; + +@Directive({ + selector: `[tiContains][formControlName],[tiContains][formControl],[tiContains][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ContainsValidatorDirective), + multi: true + } + ] +}) +export class ContainsValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiContains'; + @Input(ContainsValidatorDirective.NAME) baseValue: string; + validatorStr: string = BaseValidator.getValidatorStr(ContainsValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/DateValidatorDirective.ts b/src/validation/lib/src/validators/directives/DateValidatorDirective.ts new file mode 100644 index 0000000..0aebb28 --- /dev/null +++ b/src/validation/lib/src/validators/directives/DateValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiDate][formControlName],[tiDate][formControl],[tiDate][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DateValidatorDirective), + multi: true + } + ] +}) +export class DateValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiDate'; + @Input(DateValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(DateValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/DigitsValidatorDirective.ts b/src/validation/lib/src/validators/directives/DigitsValidatorDirective.ts new file mode 100644 index 0000000..0b11d60 --- /dev/null +++ b/src/validation/lib/src/validators/directives/DigitsValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiDigits][formControlName],[tiDigits][formControl],[tiDigits][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DigitsValidatorDirective), + multi: true + } + ] +}) +export class DigitsValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiDigits'; + @Input(DigitsValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(DigitsValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/EmailValidatorDirective.ts b/src/validation/lib/src/validators/directives/EmailValidatorDirective.ts new file mode 100644 index 0000000..54144f8 --- /dev/null +++ b/src/validation/lib/src/validators/directives/EmailValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; + +@Directive({ + selector: `[tiEmail][formControlName],[tiEmail][formControl],[tiEmail][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EmailValidatorDirective), + multi: true + } + ] +}) +export class EmailValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiEmail'; + @Input(EmailValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(EmailValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/EqualValidatorDirective.ts b/src/validation/lib/src/validators/directives/EqualValidatorDirective.ts new file mode 100644 index 0000000..67678cb --- /dev/null +++ b/src/validation/lib/src/validators/directives/EqualValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; + +@Directive({ + selector: `[tiEqual][formControlName],[tiEqual][formControl],[tiEqual][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EqualValidatorDirective), + multi: true + } + ] +}) +export class EqualValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiEqual'; + @Input(EqualValidatorDirective.NAME) baseValue: string; + validatorStr: string = BaseValidator.getValidatorStr(EqualValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/IntegerValidatorDirective.ts b/src/validation/lib/src/validators/directives/IntegerValidatorDirective.ts new file mode 100644 index 0000000..aaff757 --- /dev/null +++ b/src/validation/lib/src/validators/directives/IntegerValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; + +@Directive({ + selector: `[tiInteger][formControlName],[tiInteger][formControl],[tiInteger][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => IntegerValidatorDirective), + multi: true + } + ] +}) +export class IntegerValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiInteger'; + @Input(IntegerValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(IntegerValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/Ipv4ValidatorDirective.ts b/src/validation/lib/src/validators/directives/Ipv4ValidatorDirective.ts new file mode 100644 index 0000000..a6c907b --- /dev/null +++ b/src/validation/lib/src/validators/directives/Ipv4ValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiIpv4][formControlName],[tiIpv4][formControl],[tiIpv4][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Ipv4ValidatorDirective), + multi: true + } + ] +}) +export class Ipv4ValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiIpv4'; + @Input(Ipv4ValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(Ipv4ValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/Ipv6ValidatorDirective.ts b/src/validation/lib/src/validators/directives/Ipv6ValidatorDirective.ts new file mode 100644 index 0000000..7e5e448 --- /dev/null +++ b/src/validation/lib/src/validators/directives/Ipv6ValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiIpv6][formControlName],[tiIpv6][formControl],[tiIpv6][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Ipv6ValidatorDirective), + multi: true + } + ] +}) +export class Ipv6ValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiIpv6'; + @Input(Ipv6ValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(Ipv6ValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/MaxLengthValidatorDirective.ts b/src/validation/lib/src/validators/directives/MaxLengthValidatorDirective.ts new file mode 100644 index 0000000..83638a7 --- /dev/null +++ b/src/validation/lib/src/validators/directives/MaxLengthValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiMaxLength][formControlName],[tiMaxLength][formControl],[tiMaxLength][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MaxLengthValidatorDirective), + multi: true + } + ] +}) +export class MaxLengthValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiMaxLength'; + @Input(MaxLengthValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(MaxLengthValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/MaxValueByStringValidatorDirective.ts b/src/validation/lib/src/validators/directives/MaxValueByStringValidatorDirective.ts new file mode 100644 index 0000000..fc2a17a --- /dev/null +++ b/src/validation/lib/src/validators/directives/MaxValueByStringValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiMaxValueByString][formControlName],[tiMaxValueByString][formControl],[tiMaxValueByString][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MaxValueByStringValidatorDirective), + multi: true + } + ] +}) +export class MaxValueByStringValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiMaxValueByString'; + @Input(MaxValueByStringValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(MaxValueByStringValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/MaxValueValidatorDirective.ts b/src/validation/lib/src/validators/directives/MaxValueValidatorDirective.ts new file mode 100644 index 0000000..68df4ee --- /dev/null +++ b/src/validation/lib/src/validators/directives/MaxValueValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiMaxValue][formControlName],[tiMaxValue][formControl],[tiMaxValue][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MaxValueValidatorDirective), + multi: true + } + ] +}) +export class MaxValueValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiMaxValue'; + @Input(MaxValueValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(MaxValueValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/MinLengthValidatorDirective.ts b/src/validation/lib/src/validators/directives/MinLengthValidatorDirective.ts new file mode 100644 index 0000000..23d02e6 --- /dev/null +++ b/src/validation/lib/src/validators/directives/MinLengthValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiMinLength][formControlName],[tiMinLength][formControl],[tiMinLength][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MinLengthValidatorDirective), + multi: true + } + ] +}) +export class MinLengthValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiMinLength'; + @Input(MinLengthValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(MinLengthValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/MinValueByStringValidatorDirective.ts b/src/validation/lib/src/validators/directives/MinValueByStringValidatorDirective.ts new file mode 100644 index 0000000..8d342e2 --- /dev/null +++ b/src/validation/lib/src/validators/directives/MinValueByStringValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiMinValueByString][formControlName],[tiMinValueByString][formControl],[tiMinValueByString][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MinValueByStringValidatorDirective), + multi: true + } + ] +}) +export class MinValueByStringValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiMinValueByString'; + @Input(MinValueByStringValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(MinValueByStringValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/MinValueValidatorDirective.ts b/src/validation/lib/src/validators/directives/MinValueValidatorDirective.ts new file mode 100644 index 0000000..5e443d2 --- /dev/null +++ b/src/validation/lib/src/validators/directives/MinValueValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiMinValue][formControlName],[tiMinValue][formControl],[tiMinValue][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MinValueValidatorDirective), + multi: true + } + ] +}) +export class MinValueValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiMinValue'; + @Input(MinValueValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(MinValueValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/NotContainsValidatorDirective.ts b/src/validation/lib/src/validators/directives/NotContainsValidatorDirective.ts new file mode 100644 index 0000000..991f532 --- /dev/null +++ b/src/validation/lib/src/validators/directives/NotContainsValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiNotContains][formControlName],[tiNotContains][formControl],[tiNotContains][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => NotContainsValidatorDirective), + multi: true + } + ] +}) +export class NotContainsValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiNotContains'; + @Input(NotContainsValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(NotContainsValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/NotEqualValidatorDirective.ts b/src/validation/lib/src/validators/directives/NotEqualValidatorDirective.ts new file mode 100644 index 0000000..0d2f189 --- /dev/null +++ b/src/validation/lib/src/validators/directives/NotEqualValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiNotEqual][formControlName],[tiNotEqual][formControl],[tiNotEqual][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => NotEqualValidatorDirective), + multi: true + } + ] +}) +export class NotEqualValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiNotEqual'; + @Input(NotEqualValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(NotEqualValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/NotScriptValidatorDirective.ts b/src/validation/lib/src/validators/directives/NotScriptValidatorDirective.ts new file mode 100644 index 0000000..1aaa836 --- /dev/null +++ b/src/validation/lib/src/validators/directives/NotScriptValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiNotScript][formControlName],[tiNotScript][formControl],[tiNotScript][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => NotScriptValidatorDirective), + multi: true + } + ] +}) +export class NotScriptValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiNotScript'; + @Input(NotScriptValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(NotScriptValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/NumberValidatorDirective.ts b/src/validation/lib/src/validators/directives/NumberValidatorDirective.ts new file mode 100644 index 0000000..33725b2 --- /dev/null +++ b/src/validation/lib/src/validators/directives/NumberValidatorDirective.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms'; +@Directive({ + // 数字校验与inputNumber组件重名,故此处选择器加tiValidation,其余校验规则不变 + selector: `[tiNumber][formControlName][tiValidation],[tiNumber][formControl][tiValidation],[tiNumber][ngModel][tiValidation]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => NumberValidatorDirective), + multi: true + } + ] +}) +export class NumberValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiNumber'; + @Input(NumberValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(NumberValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/PasswordValidatorDirective.ts b/src/validation/lib/src/validators/directives/PasswordValidatorDirective.ts new file mode 100644 index 0000000..678191f --- /dev/null +++ b/src/validation/lib/src/validators/directives/PasswordValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiPassword][formControlName],[tiPassword][formControl],[tiPassword][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => PasswordValidatorDirective), + multi: true + } + ] +}) +export class PasswordValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiPassword'; + @Input(PasswordValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(PasswordValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/PortValidatorDirective.ts b/src/validation/lib/src/validators/directives/PortValidatorDirective.ts new file mode 100644 index 0000000..6fe5ebc --- /dev/null +++ b/src/validation/lib/src/validators/directives/PortValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiPort][formControlName],[tiPort][formControl],[tiPort][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => PortValidatorDirective), + multi: true + } + ] +}) +export class PortValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiPort'; + @Input(PortValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(PortValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/RangeSizeValidatorDirective.ts b/src/validation/lib/src/validators/directives/RangeSizeValidatorDirective.ts new file mode 100644 index 0000000..c467d81 --- /dev/null +++ b/src/validation/lib/src/validators/directives/RangeSizeValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiRangeSize][formControlName],[tiRangeSize][formControl],[tiRangeSize][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RangeSizeValidatorDirective), + multi: true + } + ] +}) +export class RangeSizeValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiRangeSize'; + @Input(RangeSizeValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(RangeSizeValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/RangeValueByStringValidatorDirective.ts b/src/validation/lib/src/validators/directives/RangeValueByStringValidatorDirective.ts new file mode 100644 index 0000000..d505d23 --- /dev/null +++ b/src/validation/lib/src/validators/directives/RangeValueByStringValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiRangeValueByString][formControlName],[tiRangeValueByString][formControl],[tiRangeValueByString][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RangeValueByStringValidatorDirective), + multi: true + } + ] +}) +export class RangeValueByStringValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiRangeValueByString'; + @Input(RangeValueByStringValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(RangeValueByStringValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/RangeValueValidatorDirective.ts b/src/validation/lib/src/validators/directives/RangeValueValidatorDirective.ts new file mode 100644 index 0000000..96a9236 --- /dev/null +++ b/src/validation/lib/src/validators/directives/RangeValueValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiRangeValue][formControlName],[tiRangeValue][formControl],[tiRangeValue][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RangeValueValidatorDirective), + multi: true + } + ] +}) +export class RangeValueValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiRangeValue'; + @Input(RangeValueValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(RangeValueValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/RegExpValidatorDirective.ts b/src/validation/lib/src/validators/directives/RegExpValidatorDirective.ts new file mode 100644 index 0000000..3ad4f38 --- /dev/null +++ b/src/validation/lib/src/validators/directives/RegExpValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiRegExp][formControlName],[tiRegExp][formControl],[tiRegExp][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RegExpValidatorDirective), + multi: true + } + ] +}) +export class RegExpValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiRegExp'; + @Input(RegExpValidatorDirective.NAME) baseValue: any; + validatorStr: string = BaseValidator.getValidatorStr(RegExpValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/RequiredValidatorDirective.ts b/src/validation/lib/src/validators/directives/RequiredValidatorDirective.ts new file mode 100644 index 0000000..a63073b --- /dev/null +++ b/src/validation/lib/src/validators/directives/RequiredValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiRequired][formControlName],[tiRequired][formControl],[tiRequired][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RequiredValidatorDirective), + multi: true + } + ] +}) +export class RequiredValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiRequired'; + @Input(RequiredValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(RequiredValidatorDirective.NAME); +} diff --git a/src/validation/lib/src/validators/directives/UrlValidatorDirective.ts b/src/validation/lib/src/validators/directives/UrlValidatorDirective.ts new file mode 100644 index 0000000..08a3d5c --- /dev/null +++ b/src/validation/lib/src/validators/directives/UrlValidatorDirective.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Directive, Input, forwardRef } from '@angular/core'; +import { BaseValidator } from './BaseValidator'; +import { NG_VALIDATORS } from '@angular/forms'; +@Directive({ + selector: `[tiUrl][formControlName],[tiUrl][formControl],[tiUrl][ngModel]`, + providers: [ + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => UrlValidatorDirective), + multi: true + } + ] +}) +export class UrlValidatorDirective extends BaseValidator { + static readonly NAME: string = 'tiUrl'; + @Input(UrlValidatorDirective.NAME) enabled: boolean; + validatorStr: string = BaseValidator.getValidatorStr(UrlValidatorDirective.NAME); +} diff --git a/src/zoom/demo/project.json b/src/zoom/demo/project.json new file mode 100644 index 0000000..b4eb46b --- /dev/null +++ b/src/zoom/demo/project.json @@ -0,0 +1,86 @@ +{ + "projectType": "application", + "root": "src/zoom/demo", + "sourceRoot": "src/zoom/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/zoom", + "index": "src/zoom/demo/src/index.html", + "main": "src/zoom/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/zoom/demo/tsconfig.app.json", + "assets": [ + "src/zoom/demo/src/favicon.ico", + "src/zoom/demo/src/assets", + { + "glob": "**/*", + "input": "src/ng/demo/src/assets/image", + "output": "/assets/image/" + } + ], + "styles": ["src/themes/basic/build.less", "src/themes/theme-default/build.less", "src/styles.less"], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "zoom-demo:build:production" + }, + "development": { + "browserTarget": "zoom-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js zoom" + } + ] + } + } + } +} diff --git a/src/zoom/demo/src/app/AppComponent.ts b/src/zoom/demo/src/app/AppComponent.ts new file mode 100644 index 0000000..76c5465 --- /dev/null +++ b/src/zoom/demo/src/app/AppComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html' +}) +export class AppComponent {} diff --git a/src/zoom/demo/src/app/AppModule.ts b/src/zoom/demo/src/app/AppModule.ts new file mode 100644 index 0000000..9845c80 --- /dev/null +++ b/src/zoom/demo/src/app/AppModule.ts @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { ZoomTestModule } from './zoom/ZoomTestModule'; + +@NgModule({ + imports: [ + ZoomTestModule, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent } // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/zoom/demo/src/app/IndexComponent.ts b/src/zoom/demo/src/app/IndexComponent.ts new file mode 100644 index 0000000..89f1f05 --- /dev/null +++ b/src/zoom/demo/src/app/IndexComponent.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { ZoomTestModule } from './zoom/ZoomTestModule'; + +@Component({ + template: ` + + + + + + + +
    {{ routes[0]?.path.split('/')[0] }} demo
    + + {{ path2name(route.path) }} + +
    ` +}) +export class IndexComponent { + routes: any = ZoomTestModule.ROUTES; + + path2name(path: string): string { + return path.split('/').pop(); + } +} diff --git a/src/zoom/demo/src/app/app.html b/src/zoom/demo/src/app/app.html new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/src/zoom/demo/src/app/app.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/zoom/demo/src/app/zoom/ZoomBasicComponent.ts b/src/zoom/demo/src/app/zoom/ZoomBasicComponent.ts new file mode 100644 index 0000000..96984e4 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/ZoomBasicComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './zoom-basic.html', + styleUrls: ['./zoom.less'] +}) +export class ZoomBasicComponent {} diff --git a/src/zoom/demo/src/app/zoom/ZoomChangeSrcComponent.ts b/src/zoom/demo/src/app/zoom/ZoomChangeSrcComponent.ts new file mode 100644 index 0000000..7f55e30 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/ZoomChangeSrcComponent.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './zoom-change-src.html' +}) +export class ZoomChangeSrcComponent { + srcArray: Array = ['assets/image/1.jpg', 'assets/image/2.jpg', 'assets/image/3.jpg']; + index: number = 0; + count: number = 0; + src: string = this.srcArray[this.index]; + // 循环切换 + changeSrc(event: any): void { + if (!this.srcArray.length) { + return; + } + this.count += 1; + this.index = this.count % this.srcArray.length; + this.src = this.srcArray[this.index]; + } +} diff --git a/src/zoom/demo/src/app/zoom/ZoomRatioComponent.ts b/src/zoom/demo/src/app/zoom/ZoomRatioComponent.ts new file mode 100644 index 0000000..1263380 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/ZoomRatioComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './zoom-ratio.html', + styleUrls: ['./zoom.less'] +}) +export class ZoomRatioComponent {} diff --git a/src/zoom/demo/src/app/zoom/ZoomTestComponent.ts b/src/zoom/demo/src/app/zoom/ZoomTestComponent.ts new file mode 100644 index 0000000..0bcbcf7 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/ZoomTestComponent.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './zoom-test.html', + styleUrls: ['./zoom.less'] +}) +export class ZoomTestComponent {} diff --git a/src/zoom/demo/src/app/zoom/ZoomTestModule.ts b/src/zoom/demo/src/app/zoom/ZoomTestModule.ts new file mode 100644 index 0000000..33d0ae2 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/ZoomTestModule.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { TiZoomModule } from '@opentiny/ng'; + +import { ZoomBasicComponent } from './ZoomBasicComponent'; +import { ZoomChangeSrcComponent } from './ZoomChangeSrcComponent'; +import { ZoomTestComponent } from './ZoomTestComponent'; +import { ZoomRatioComponent } from './ZoomRatioComponent'; + +@NgModule({ + imports: [CommonModule, TiZoomModule, RouterModule.forChild(ZoomTestModule.ROUTES)], + declarations: [ZoomBasicComponent, ZoomChangeSrcComponent, ZoomTestComponent, ZoomRatioComponent] +}) +export class ZoomTestModule { + static readonly LINKS: Array = [{ href: 'directives/TiZoomDirective.html', label: 'Zoom' }]; + static readonly ROUTES: Routes = [ + { + path: 'zoom/zoom-basic', + component: ZoomBasicComponent + }, + { + path: 'zoom/zoom-ratio', + component: ZoomRatioComponent + }, + { path: 'zoom/zoom-change-src', component: ZoomChangeSrcComponent }, // 自测用例,改变src属性 + { path: 'zoom/zoom-test', component: ZoomTestComponent } // 自测用例 + ]; +} diff --git a/src/zoom/demo/src/app/zoom/zoom-basic.html b/src/zoom/demo/src/app/zoom/zoom-basic.html new file mode 100644 index 0000000..79b4c19 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/zoom-basic.html @@ -0,0 +1,11 @@ +

    描述

    +

    + 基本用法,通过在img元素上添加tiZoom属性可以实现图片放大效果。
    + 在其他元素上添加tiZoom无效。 +

    +

    导入

    +

    import {{ '{' }} TiZoomModule {{ '}' }} from '@opentiny/ng';

    +

    示例

    +
    + +
    diff --git a/src/zoom/demo/src/app/zoom/zoom-change-src.html b/src/zoom/demo/src/app/zoom/zoom-change-src.html new file mode 100644 index 0000000..834cedc --- /dev/null +++ b/src/zoom/demo/src/app/zoom/zoom-change-src.html @@ -0,0 +1,5 @@ +

    描述

    +

    改变img元素src属性。

    +

    示例

    + + diff --git a/src/zoom/demo/src/app/zoom/zoom-ratio.html b/src/zoom/demo/src/app/zoom/zoom-ratio.html new file mode 100644 index 0000000..9f7a783 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/zoom-ratio.html @@ -0,0 +1,9 @@ +

    描述

    +

    + 通过设置zoomSelectorLength、zoomViewerLength可以自定义放大比例。分别默认150和300。
    + 放大比例为(zoomViewerLength / zoomSelectorLength) * (zoomViewerLength / zoomSelectorLength) +

    +

    示例

    +
    + +
    diff --git a/src/zoom/demo/src/app/zoom/zoom-test.html b/src/zoom/demo/src/app/zoom/zoom-test.html new file mode 100644 index 0000000..57ac7c3 --- /dev/null +++ b/src/zoom/demo/src/app/zoom/zoom-test.html @@ -0,0 +1,15 @@ +

    描述

    +

    自测用例

    +

    示例

    +

    1、非img元素,无效

    +
    +

    2、居中

    +
    + +
    +

    3、不居中

    +
    + +
    +

    4、无父元素

    + diff --git a/src/zoom/demo/src/app/zoom/zoom.less b/src/zoom/demo/src/app/zoom/zoom.less new file mode 100644 index 0000000..f767b3e --- /dev/null +++ b/src/zoom/demo/src/app/zoom/zoom.less @@ -0,0 +1,11 @@ +.zoom-container { + width: 400px; + height: 400px; + background-color: rgb(245, 245, 245); + .zoom-img { + box-sizing: border-box; + border: 1px solid #dfe1e6; + width: 100%; + height: 100%; + } +} diff --git a/src/zoom/demo/src/favicon.ico b/src/zoom/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/src/zoom/demo/src/index.html b/src/zoom/demo/src/index.html new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/src/zoom/demo/src/index.html @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/src/zoom/demo/src/main.ts b/src/zoom/demo/src/main.ts new file mode 100644 index 0000000..67a86e9 --- /dev/null +++ b/src/zoom/demo/src/main.ts @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); diff --git a/src/zoom/demo/tsconfig.app.json b/src/zoom/demo/tsconfig.app.json new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/src/zoom/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/src/zoom/lib/index.ts b/src/zoom/lib/index.ts new file mode 100644 index 0000000..db7e256 --- /dev/null +++ b/src/zoom/lib/index.ts @@ -0,0 +1 @@ +export * from './src/TiZoomModule'; diff --git a/src/zoom/lib/ng-package.json b/src/zoom/lib/ng-package.json new file mode 100644 index 0000000..6f5e1d0 --- /dev/null +++ b/src/zoom/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/zoom", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/src/zoom/lib/package.json b/src/zoom/lib/package.json new file mode 100644 index 0000000..9aebea7 --- /dev/null +++ b/src/zoom/lib/package.json @@ -0,0 +1,10 @@ +{ + "name": "@opentiny/ng-zoom", + "version": "1.0.0-beta.0", + "license": "MIT", + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@angular/common": ">=13.0.0", + "@opentiny/ng-renderer": "~1.0.0-beta.0" + } +} \ No newline at end of file diff --git a/src/zoom/lib/project.json b/src/zoom/lib/project.json new file mode 100644 index 0000000..99c8fab --- /dev/null +++ b/src/zoom/lib/project.json @@ -0,0 +1,62 @@ +{ + "projectType": "library", + "root": "src/zoom/lib", + "sourceRoot": "src/zoom/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": ["dist/libs/zoom"], + "options": { + "project": "src/zoom/lib/ng-package.json", + "updateBuildableProjectDepsInPackageJson": false + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/zoom"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js zoom" + }, + { + "command": "ng default-build zoom" + }, + { + "command": "node build/clear-default-theme.js zoom" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/zoom && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build zoom && ng pack zoom && node build/publish.js zoom --tag={args.tag}" + } + ] + } + } + } +} diff --git a/src/zoom/lib/src/TiZoomComponent.ts b/src/zoom/lib/src/TiZoomComponent.ts new file mode 100644 index 0000000..bc3f335 --- /dev/null +++ b/src/zoom/lib/src/TiZoomComponent.ts @@ -0,0 +1,257 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { Component, ElementRef, HostListener, Input, OnDestroy, OnInit, Renderer2, ChangeDetectionStrategy } from '@angular/core'; +import { TiRenderer } from '@opentiny/ng-renderer'; + +/** + * @ignore + * + * 10.0.3版本增加,暂时不对外开放 + * + * 该指令可以实现图片放大镜功能,添加tiZoom后会创建放大区域选择器元素(div)和放大结果呈现元素(div) + * + * 通过设置放大结果的background-image、background-size和background-position来达到放大效果 + * + * 10.1.0版本从指令调整为组件形式 + * + */ +@Component({ + // 非img元素,tiZoom指令无效 + selector: 'img[tiZoom]', + template: '', + styleUrls: ['./zoom.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TiZoomComponent implements OnInit, OnDestroy { + // 放大结果的边框宽度 + private static readonly ZOOM_VIEWER_BORDER_WIDTH: number = 1; + + // 选择遮罩的宽高,默认150px + @Input() zoomSelectorLength: number = 150; + + // 放大展示的宽高,默认300px + @Input() zoomViewerLength: number = 300; + + // 比例 + private ratio: number; + + // 宿主元素 + private hostEle: any; + + // 宿主元素的父元素 + private parentEle: any; + + // 宿主元素的图片路径 + private imgSrc: string = ''; + + // 放大区域选择器 + private zoomSelectorEle: any; + + // 放大结果展示区域 + private zoomViewerEle: any; + + // 选择遮罩监听事件 + private selectorMouseMoveHandler: () => void; + private selectorMouseLeaveHandler: () => void; + + constructor(hostRef: ElementRef, private renderer: Renderer2, private tiRenderer: TiRenderer) { + this.hostEle = hostRef.nativeElement; + } + + ngOnInit(): void { + // 设置父元素 + this.setParentEle(); + // 设置放大比例 + this.ratio = this.zoomViewerLength / this.zoomSelectorLength; + } + + /** + * @ignore + * 图片加载成功事件处理 + */ + @HostListener('load', ['$event']) public onHostLoad(event: any): void { + this.imgSrc = event.target.src; + } + + /** + * @ignore + * 鼠标进入宿主元素,创建放大效果相关元素 + */ + @HostListener('mouseenter', ['$event']) public onHostMouseEnter(event: any): void { + this.createZoomSelector(); + this.createZoomViewer(); + this.reStyleResults(event); + } + + /** + * @ignore + * 鼠标离开宿主元素,移除放大效果相关元素 + */ + @HostListener('mouseleave', ['$event']) public onHostMouseLeave(event: any): void { + // 当鼠标离开宿主元素并且不是进入选择遮罩时,移除选择遮罩和放大结果 + if (event.relatedTarget !== this.zoomSelectorEle) { + this.removeZoomEle(); + } + } + + /** + * 设置父元素 + */ + private setParentEle(): void { + const parentEle: any = this.hostEle.parentElement; + if (parentEle !== null && parentEle.clientWidth !== 0 && parentEle.clientHeight !== 0) { + if (this.hostEle.offsetParent !== parentEle) { + // 父元素必须能够定位 + this.renderer.setStyle(parentEle, 'position', 'relative'); + } + this.parentEle = parentEle; + } else { + this.parentEle = document.body; + } + } + + /** + * 创建选择遮罩,初始化样式,增加事件监听 + */ + private createZoomSelector(): void { + this.zoomSelectorEle = this.renderer.createElement('div'); + + // 添加样式,tinyplus3的productpreview需要根据分辨率修改元素尺寸 + this.renderer.addClass(this.zoomSelectorEle, 'ti3-img-zoom-selector'); + this.tiRenderer.setStyles(this.zoomSelectorEle, { + width: this.zoomSelectorLength + 'px', + height: this.zoomSelectorLength + 'px' + }); + + // 鼠标在选择遮罩上移动时的事件 + this.selectorMouseMoveHandler = this.renderer.listen(this.zoomSelectorEle, 'mousemove', (event: any): void => { + this.reStyleResults(event); + }); + // 鼠标离开选择遮罩的事件,销毁遮罩元素和放大结果呈现元素 + this.selectorMouseLeaveHandler = this.renderer.listen(this.zoomSelectorEle, 'mouseleave', (event: MouseEvent) => { + this.renderer.removeChild(this.parentEle, this.zoomSelectorEle); + this.renderer.removeChild(document.body, this.zoomViewerEle); + this.zoomSelectorEle = undefined; + this.zoomViewerEle = undefined; + }); + + this.renderer.appendChild(this.parentEle, this.zoomSelectorEle); + } + + /** + * 创建放大结果呈现元素,初始化样式 + */ + private createZoomViewer(): void { + this.zoomViewerEle = this.renderer.createElement('div'); + const hostEleRect: DOMRect = this.hostEle.getBoundingClientRect(); + const parentEleRect: DOMRect = this.parentEle.getBoundingClientRect(); + + // 放大结果元素相对于父元素进行定位 + // left = 父元素宽度 - 边框宽度 + 滚动距离 + let left: number = this.parentEle.offsetWidth - TiZoomComponent.ZOOM_VIEWER_BORDER_WIDTH + parentEleRect.left + window.pageXOffset; + // top = 父元素到视口距离 + 滚动距离 + let top: number = parentEleRect.top + window.pageYOffset; + // 当父元素为body时,调整为相对于宿主元素定位 + if (this.parentEle === document.body) { + left = this.hostEle.offsetWidth - TiZoomComponent.ZOOM_VIEWER_BORDER_WIDTH + hostEleRect.left + window.pageXOffset; + top = hostEleRect.top + window.pageYOffset; + } + + // 添加样式,tinyplus3的productpreview需要根据分辨率修改元素尺寸 + this.renderer.addClass(this.zoomViewerEle, 'ti3-img-zoom-viewer'); + this.tiRenderer.setStyles(this.zoomViewerEle, { + left: left + 'px', + top: top + 'px', + width: `${this.zoomViewerLength}px`, + height: `${this.zoomViewerLength}px`, + background: `url('${this.imgSrc}')`, + 'background-size': `${this.hostEle.width * this.ratio}px ${this.hostEle.height * this.ratio}px` + }); + + this.renderer.appendChild(document.body, this.zoomViewerEle); + } + + // 计算鼠标相对宿主元素的位置 + private getCursorPos(mouseEvent: MouseEvent): any { + const hostEleRect: DOMRect = this.hostEle.getBoundingClientRect(); + // 考虑滚动情况 + const left: number = mouseEvent.pageX - hostEleRect.left - window.pageXOffset; + const top: number = mouseEvent.pageY - hostEleRect.top - window.pageYOffset; + + return { x: left, y: top }; + } + + /** + * 计算选择遮罩偏移和放大区域 + */ + private reStyleResults(mouseEvent: MouseEvent): void { + // 1. 获取计算鼠标相对于宿主元素左上角的位置 + const cursorPos: any = this.getCursorPos(mouseEvent); + // 获取遮罩宽度 + const zoomSelectorWidth: number = this.zoomSelectorEle.offsetWidth; + + // 2. 计算放大结果的位置 + let viewerPosX: number = cursorPos.x - zoomSelectorWidth / 2; + let viewerPosY: number = cursorPos.y - zoomSelectorWidth / 2; + + // 3. 调整 + // 3.1 选择遮罩移出宿主元素的右侧 + if (viewerPosX > this.hostEle.offsetWidth - zoomSelectorWidth) { + viewerPosX = this.hostEle.offsetWidth - zoomSelectorWidth; + } + // 3.2 选择遮罩移出宿主元素的左侧 + if (viewerPosX < 0) { + viewerPosX = 0; + } + // 3.3 选择遮罩移出宿主元素的下方 + if (viewerPosY > this.hostEle.offsetHeight - zoomSelectorWidth) { + viewerPosY = this.hostEle.offsetHeight - zoomSelectorWidth; + } + // 3.4 选择遮罩移出宿主元素的上方 + if (viewerPosY < 0) { + viewerPosY = 0; + } + + // 4. 设置选择区域偏移,选择区域偏移=放大结果位置 + 宿主元素的偏移 + this.tiRenderer.setStyles(this.zoomSelectorEle, { + left: `${viewerPosX + this.hostEle.offsetLeft}px`, + top: `${viewerPosY + this.hostEle.offsetTop}px` + }); + + // 5. 设置放大结果 + this.renderer.setStyle(this.zoomViewerEle, 'background-position', `-${viewerPosX * this.ratio}px -${viewerPosY * this.ratio}px`); + } + + ngOnDestroy(): void { + this.removeZoomEle(); + } + + /** + * 移除选择遮罩和放大结果元素、解绑相关事件 + */ + private removeZoomEle(): void { + if (this.zoomSelectorEle !== undefined) { + this.renderer.removeChild(this.parentEle, this.zoomSelectorEle); + this.zoomSelectorEle = undefined; + } + if (this.zoomViewerEle !== undefined) { + this.renderer.removeChild(document.body, this.zoomViewerEle); + this.zoomViewerEle = undefined; + } + if (this.selectorMouseMoveHandler) { + this.selectorMouseMoveHandler(); + } + if (this.selectorMouseLeaveHandler) { + this.selectorMouseLeaveHandler(); + } + } +} diff --git a/src/zoom/lib/src/TiZoomModule.ts b/src/zoom/lib/src/TiZoomModule.ts new file mode 100644 index 0000000..2b754f8 --- /dev/null +++ b/src/zoom/lib/src/TiZoomModule.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022 - present TinyUI Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TiZoomComponent } from './TiZoomComponent'; +import { TiRendererModule } from '@opentiny/ng-renderer'; + +@NgModule({ + imports: [CommonModule, TiRendererModule], + exports: [TiZoomComponent], + declarations: [TiZoomComponent] +}) +export class TiZoomModule {} +export { TiZoomComponent } from './TiZoomComponent'; diff --git a/src/zoom/lib/src/zoom.less b/src/zoom/lib/src/zoom.less new file mode 100644 index 0000000..4bf6e41 --- /dev/null +++ b/src/zoom/lib/src/zoom.less @@ -0,0 +1,19 @@ +.ti3-img-zoom-selector { + position: absolute; + top: 0; + left: 0; + background-color: var(--ti-common-color-icon-hover); + opacity: 0.2; + display: block; + cursor: move; +} + +.ti3-img-zoom-viewer { + position: absolute; + top: 0; + border: 1px solid var(--ti-common-color-line-dividing); + box-sizing: border-box; + display: block; + // 图片宽度极小或长度极小时,允许放大区域留白 + background-repeat: no-repeat !important; +} diff --git a/tools/generators/ti-lib-generator/files/demo/project.json__tmpl__ b/tools/generators/ti-lib-generator/files/demo/project.json__tmpl__ new file mode 100644 index 0000000..fe4751f --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/project.json__tmpl__ @@ -0,0 +1,85 @@ +{ + "projectType": "application", + "root": "src/<%= name %>/demo", + "sourceRoot": "src/<%= name %>/demo/src", + "generators": { + "@schematics/angular:component": { + "style": "less" + } + }, + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "dependsOn": [], + "options": { + "outputPath": "dist/apps/<%= name %>", + "index": "src/<%= name %>/demo/src/index.html", + "main": "src/<%= name %>/demo/src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/<%= name %>/demo/tsconfig.app.json", + "assets": [ + "src/<%= name %>/demo/src/favicon.ico", + "src/<%= name %>/demo/src/assets" + ], + "styles": [ + "src/themes/basic/build.less", + "src/themes/theme-default/build.less", + "src/styles.less" + ], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "<%= name %>-demo:build:production" + }, + "development": { + "browserTarget": "<%= name %>-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "preview": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "node build/preview-demo.js <%= name %>" + } + ] + } + } + } +} diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/AppComponent.ts__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/AppComponent.ts__tmpl__ new file mode 100644 index 0000000..ed76202 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/AppComponent.ts__tmpl__ @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.html', +}) +export class AppComponent {} diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/AppModule.ts__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/AppModule.ts__tmpl__ new file mode 100644 index 0000000..f5645b4 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/AppModule.ts__tmpl__ @@ -0,0 +1,24 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './AppComponent'; +import { IndexComponent } from './IndexComponent'; +import { <%= demoModuleName %> } from './<%= name %>/<%= demoModuleName %>'; + +@NgModule({ + imports: [ + <%= demoModuleName %>, + BrowserModule.withServerTransition({ appId: 'serverApp' }), + RouterModule.forRoot( + [ + { path: '', component: IndexComponent }, // 路由地址为空 + { path: '**', component: IndexComponent }, // 路由地址找不到。注意:放在数组最后 + ], + { useHash: true } // 使用#路由 + ) + ], + declarations: [AppComponent, IndexComponent], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/IndexComponent.ts__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/IndexComponent.ts__tmpl__ new file mode 100644 index 0000000..12a58f7 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/IndexComponent.ts__tmpl__ @@ -0,0 +1,54 @@ +import { Component } from '@angular/core'; +import { <%= demoModuleName %> } from './<%= name %>/<%= demoModuleName %>'; + +@Component({ + template: ` + + + + + + + +
    {{routes[0]?.path.split('/')[0]}} demo
    + + {{ path2name(route.path) }} + +
    `, +}) +export class IndexComponent { + routes: any = <%= demoModuleName %>.ROUTES; + + path2name(path: string): string | undefined { + return path.split('/').pop(); + } +} \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoComponentName__.ts__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoComponentName__.ts__tmpl__ new file mode 100644 index 0000000..a9563a6 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoComponentName__.ts__tmpl__ @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './<%= name %>-basic.html' +}) +export class <%= demoComponentName %> { +} diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoModuleName__.ts__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoModuleName__.ts__tmpl__ new file mode 100644 index 0000000..2b975d8 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__demoModuleName__.ts__tmpl__ @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { <%= moduleName %> } from '@opentiny/ng'; +import { <%= demoComponentName %> } from './<%= demoComponentName %>'; + +@NgModule({ + imports: [ + CommonModule, + <%= moduleName %>, + RouterModule.forChild(<%= demoModuleName %>.ROUTES) + ], + declarations: [<%= demoComponentName %>] +}) +export class <%= demoModuleName %> { + static readonly ROUTES: Routes = [ + { + path: '<%= name %>/<%= name %>-basic', + component: <%= demoComponentName %> + } + ]; +} diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__name__-basic.html__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__name__-basic.html__tmpl__ new file mode 100644 index 0000000..5ac3822 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/__name__-basic.html__tmpl__ @@ -0,0 +1 @@ +

    <%= name %> demo

    \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__-demos.js__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__-demos.js__tmpl__ new file mode 100644 index 0000000..97b0f9c --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__-demos.js__tmpl__ @@ -0,0 +1,17 @@ +export default { + column: '2', + demos: [ + { + demoId: '<%= name %>-basic', + name: { + 'zh-CN': '基本用法', + 'en-US': 'Basic usage' + }, + desc: { + 'zh-CN':'', + 'en-US':'' + }, + apis: [] + } + ] +}; diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.cn.md__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.cn.md__tmpl__ new file mode 100644 index 0000000..0a92345 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.cn.md__tmpl__ @@ -0,0 +1,15 @@ +--- +title: <%= camelName %> 组件中文名 +--- +# <%= camelName %> 组件中文名 + +
    + +组件总述。 + ++ 可列举的特性。 + +```typescript +import { <%= moduleName %> } from '@opentiny/ng'; +``` +
    diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.en.md__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.en.md__tmpl__ new file mode 100644 index 0000000..0eaa10a --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/__name__/webdoc/__name__.en.md__tmpl__ @@ -0,0 +1,13 @@ +--- +title: <%= camelName %> +--- +# <%= camelName %> + +
    + +```typescript +import { <%= moduleName %> } from '@opentiny/ng'; +``` +
    + + diff --git a/tools/generators/ti-lib-generator/files/demo/src/app/app.html__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/app/app.html__tmpl__ new file mode 100644 index 0000000..fb12cdb --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/app/app.html__tmpl__ @@ -0,0 +1,3 @@ +
    + +
    diff --git a/tools/generators/ti-lib-generator/files/demo/src/favicon.ico__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/favicon.ico__tmpl__ new file mode 100644 index 0000000000000000000000000000000000000000..719c584f71c05d90cfa7593676ec15a8f50937ad GIT binary patch literal 187502 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 aZdMi6smm`*Tb36Ze{o%bB-(=L=<@&Hra^Q7 literal 0 HcmV?d00001 diff --git a/tools/generators/ti-lib-generator/files/demo/src/index.html__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/index.html__tmpl__ new file mode 100644 index 0000000..d0e32a8 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/index.html__tmpl__ @@ -0,0 +1,16 @@ + + + + + + + Tiny + + + + + + + + + diff --git a/tools/generators/ti-lib-generator/files/demo/src/main.ts__tmpl__ b/tools/generators/ti-lib-generator/files/demo/src/main.ts__tmpl__ new file mode 100644 index 0000000..e1cdf67 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/src/main.ts__tmpl__ @@ -0,0 +1,14 @@ +import { enableProdMode } from '@angular/core'; + +import { AppModule } from './app/AppModule'; +import { environment } from '../../../environments/environment'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err: any) => console.error(err)); + diff --git a/tools/generators/ti-lib-generator/files/demo/tsconfig.app.json__tmpl__ b/tools/generators/ti-lib-generator/files/demo/tsconfig.app.json__tmpl__ new file mode 100644 index 0000000..16ac296 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/demo/tsconfig.app.json__tmpl__ @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "moduleResolution": "node", + "types": [] + }, + "files": [ + "src/main.ts", + "../../polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/lib/index.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/index.ts__tmpl__ new file mode 100644 index 0000000..3335bb0 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/index.ts__tmpl__ @@ -0,0 +1,2 @@ +export * from './src/<%= moduleName %>'; +export * from './src/<%= componentName %>'; \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/lib/ng-package.json__tmpl__ b/tools/generators/ti-lib-generator/files/lib/ng-package.json__tmpl__ new file mode 100644 index 0000000..0c7626e --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/ng-package.json__tmpl__ @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/<%= name %>", + "lib": { + "entryFile": "./index.ts" + } +} \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/lib/package.json__tmpl__ b/tools/generators/ti-lib-generator/files/lib/package.json__tmpl__ new file mode 100644 index 0000000..54ac1b7 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/package.json__tmpl__ @@ -0,0 +1,8 @@ +{ + "name": "<%= importPath %>", + "version": "<%= version %>", + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@angular/core": ">=13.0.0" + } +} \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/lib/project.json__tmpl__ b/tools/generators/ti-lib-generator/files/lib/project.json__tmpl__ new file mode 100644 index 0000000..8cf2e91 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/project.json__tmpl__ @@ -0,0 +1,63 @@ +{ + "projectType": "library", + "root": "src/<%= name %>/lib", + "sourceRoot": "src/<%= name %>/lib/src", + "targets": { + "default-build": { + "executor": "@nrwl/angular:package", + "outputs": [ + "dist/libs/<%= name %>" + ], + "options": { + "project": "src/<%= name %>/lib/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "src/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "build": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["dist/libs/<%= name %>"], + "options": { + "commands": [ + { + "command": "node build/add-default-theme.js <%= name %>" + }, + { + "command": "ng default-build <%= name %>" + }, + { + "command": "node build/clear-default-theme.js <%= name %>" + } + ], + "parallel": false + } + }, + "pack": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "cd dist/libs/<%= name %> && npm pack" + } + ] + } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "ng build <%= name %> && ng pack <%= name %> && node build/publish.js <%= name %> --tag={args.tag}" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/lib/src/__componentName__.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/__componentName__.ts__tmpl__ new file mode 100644 index 0000000..b60e24f --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/__componentName__.ts__tmpl__ @@ -0,0 +1,14 @@ +import { Component, Input } from '@angular/core'; +<% if(i18n === 'y') { %> +import { TiLocale } from '@opentiny/ng-locale'; +<% } %> +/** + * 组件描述 + */ +@Component({ + selector: 'ti-<%= name %>', + templateUrl: '<%= name %>.html', + styleUrls: ['<%= name %>.less'] +}) +export class <%= componentName %> { +} diff --git a/tools/generators/ti-lib-generator/files/lib/src/__moduleName__.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/__moduleName__.ts__tmpl__ new file mode 100644 index 0000000..1bf0fe7 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/__moduleName__.ts__tmpl__ @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { <%= componentName %> } from './<%= componentName %>'; +<% if(i18n === 'y') { %> +import { TiLocale } from '@opentiny/ng-locale'; +import { locales } from './i18n'; +<% } %> + +@NgModule({ + imports: [CommonModule], + exports: [<%= componentName %>], + declarations: [<%= componentName %>] +}) +export class <%= moduleName %> { +<% if(i18n === 'y') { %> + constructor() { + TiLocale.setTiWords(locales); + } +<% } %> +} \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/lib/src/__name__.html__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/__name__.html__tmpl__ new file mode 100644 index 0000000..e69de29 diff --git a/tools/generators/ti-lib-generator/files/lib/src/__name__.less__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/__name__.less__tmpl__ new file mode 100644 index 0000000..e69de29 diff --git a/tools/generators/ti-lib-generator/files/lib/src/i18n/__i18nWords__.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/i18n/__i18nWords__.ts__tmpl__ new file mode 100644 index 0000000..289181f --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/i18n/__i18nWords__.ts__tmpl__ @@ -0,0 +1,5 @@ +export interface <%= i18nWords %> { + <%= i18nWordsName %>: { + label: string + } +} \ No newline at end of file diff --git a/tools/generators/ti-lib-generator/files/lib/src/i18n/en_US.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/i18n/en_US.ts__tmpl__ new file mode 100644 index 0000000..5b4d14c --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/i18n/en_US.ts__tmpl__ @@ -0,0 +1,7 @@ +import { <%= i18nWords %> } from './<%= i18nWords %>'; + +export const en_US: <%= i18nWords %> = { + <%= i18nWordsName %>: { + label: 'Confirm' + } +}; diff --git a/tools/generators/ti-lib-generator/files/lib/src/i18n/es_US.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/i18n/es_US.ts__tmpl__ new file mode 100644 index 0000000..87276be --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/i18n/es_US.ts__tmpl__ @@ -0,0 +1,7 @@ +import { <%= i18nWords %> } from './<%= i18nWords %>'; + +export const es_US: <%= i18nWords %> = { + <%= i18nWordsName %>: { + label: 'Confirmar' + } +}; diff --git a/tools/generators/ti-lib-generator/files/lib/src/i18n/fr_FR.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/i18n/fr_FR.ts__tmpl__ new file mode 100644 index 0000000..71d1549 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/i18n/fr_FR.ts__tmpl__ @@ -0,0 +1,7 @@ +import { <%= i18nWords %> } from './<%= i18nWords %>'; + +export const fr_FR: <%= i18nWords %> = { + <%= i18nWordsName %>: { + label: 'Confirmer' + } +}; diff --git a/tools/generators/ti-lib-generator/files/lib/src/i18n/index.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/i18n/index.ts__tmpl__ new file mode 100644 index 0000000..4d3a7eb --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/i18n/index.ts__tmpl__ @@ -0,0 +1,8 @@ +import { en_US } from './en_US'; +import { es_US } from './es_US'; +import { fr_FR } from './fr_FR'; +import { pt_BR } from './pt_BR'; +import { zh_CN } from './zh_CN'; + +export const locales = { en_US, es_US, fr_FR, pt_BR, zh_CN }; + diff --git a/tools/generators/ti-lib-generator/files/lib/src/i18n/pt_BR.ts__tmpl__ b/tools/generators/ti-lib-generator/files/lib/src/i18n/pt_BR.ts__tmpl__ new file mode 100644 index 0000000..333b049 --- /dev/null +++ b/tools/generators/ti-lib-generator/files/lib/src/i18n/pt_BR.ts__tmpl__ @@ -0,0 +1,7 @@ +import { <%= i18nWords %> } from './<%= i18nWords %>'; + +export const pt_BR: <%= i18nWords %> = { + <%= i18nWordsName %>: { + label: 'Confirmar' + } +}; diff --git a/tools/generators/ti-lib-generator/index.ts b/tools/generators/ti-lib-generator/index.ts new file mode 100644 index 0000000..7770e70 --- /dev/null +++ b/tools/generators/ti-lib-generator/index.ts @@ -0,0 +1,76 @@ +import { Tree, formatFiles, generateFiles, joinPathFragments, updateJson } from '@nrwl/devkit'; +import { TiLibGeneratorSchema } from './schema'; + +export default async function (tree: Tree, schema: TiLibGeneratorSchema) { + const importPath = `@opentiny/ng-${schema.name}`; + const libraryRoot = `src/${schema.name}`; + const camelName = transform2CamelCase(schema.name); + const opentinyNgDir = 'src/ng'; + schema.i18n = schema.i18n?.toLocaleLowerCase(); + + updateJson(tree, 'tsconfig.base.json', (json) => { + const c = json.compilerOptions; + if (c.paths[importPath]) { + throw new Error(`You already have a library using the name "${schema.name}". Make sure to specify a unique one.`); + } + + c.paths[importPath] = [joinPathFragments(libraryRoot, '/lib/index.ts')]; + + return json; + }); + + updateJson(tree, 'angular.json', (json) => { + json.projects[schema.name] = joinPathFragments(libraryRoot, '/lib'); + const demoProjectName = `${schema.name}-demo`; + json.projects[demoProjectName] = joinPathFragments(libraryRoot, '/demo'); + + return json; + }); + + updateJson(tree, joinPathFragments(opentinyNgDir, 'lib/package.json'), (json) => { + json.peerDependencies[importPath] = schema.version; + + return json; + }); + + const file = tree.read(joinPathFragments(opentinyNgDir, 'lib/index.ts'), 'utf8'); + tree.write( + joinPathFragments(opentinyNgDir, 'lib/index.ts'), + `${file} +export * from '${importPath}'; + ` + ); + + generateFiles( + tree, // the virtual file system + joinPathFragments(__dirname, './files'), // path to the file templates + libraryRoot, // destination path of the files + { + ...schema, + tmpl: '', + camelName, + importPath, + componentName: `Ti${camelName}Component`, + moduleName: `Ti${camelName}Module`, + demoComponentName: `${camelName}BasicComponent`, + demoModuleName: `${camelName}TestModule`, + i18nWords: `Ti${camelName}Words`, + i18nWordsName: `ti${camelName}` + } // config object to replace variable in file templates + ); + + if (schema.i18n === 'n') { + tree.delete(joinPathFragments(libraryRoot, '/lib/src/i18n')); + } + + await formatFiles(tree); + return () => {}; +} + +function transform2CamelCase(str) { + const arr = str.split('-'); + const result = arr.map((item) => { + return `${item.slice(0, 1).toUpperCase()}${item.slice(1)}`; + }); + return result.join(''); +} diff --git a/tools/generators/ti-lib-generator/schema.d.ts b/tools/generators/ti-lib-generator/schema.d.ts new file mode 100644 index 0000000..3bc3251 --- /dev/null +++ b/tools/generators/ti-lib-generator/schema.d.ts @@ -0,0 +1,5 @@ +export interface TiLibGeneratorSchema { + name: string; + version?: string; + i18n?: string; +} diff --git a/tools/generators/ti-lib-generator/schema.json b/tools/generators/ti-lib-generator/schema.json new file mode 100644 index 0000000..3ef06d8 --- /dev/null +++ b/tools/generators/ti-lib-generator/schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "$id": "TiLibGeneratorSchematics", + "title": "", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "lib库名", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "你的 library 库名称是什么?(比如, alert 或 base-shared)" + }, + "version": { + "type": "string", + "description": "lib库版本号", + "$default": { + "$source": "argv", + "index": 1 + }, + "default": "1.0.0", + "x-prompt": "你的 library 库版本号是什么?(例如 1.0.3,默认是 1.0.0)" + }, + "i18n": { + "type": "string", + "description": "lib库国际化词条", + "$default": { + "$source": "argv", + "index": 2 + }, + "default": "Y", + "x-prompt": "你的 library 库是否需要国际化词条(Y/n)" + } + }, + "required": ["name"] +} diff --git a/tools/tsconfig.tools.json b/tools/tsconfig.tools.json new file mode 100644 index 0000000..addb016 --- /dev/null +++ b/tools/tsconfig.tools.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "outDir": "../dist/out-tsc/tools", + "rootDir": ".", + "noUnusedLocals": true, + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "types": ["node"], + "typeRoots": ["node_modules/@types"], + "lib": ["es2017", "dom"], + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "baseUrl": "." + }, + "exclude": ["node_modules", "tmp"], + "include": ["**/*.ts"] +} -- Gitee